Commit Graph
345 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Opus 4.8 d552e9017c feat(sync): NWC wallet sync (opt-in, spend-gated) + shared opt-in flags
Adds encrypted cross-login sync for the NWC wallet connection (todo #15),
reusing the encrypted-sync core under d-tag "bitvid:nwc".

- js/services/settingsSyncFlags.js: shared per-pubkey opt-in flag store
  (storage/wallet); storageSyncService refactored onto it (identical behavior,
  tests still green).
- js/services/walletSyncService.js: factory (NwcSettingsService is per-app, not a
  singleton). push() syncs the current nwcUri (+defaultZap); pull() applies it via
  updateActiveNwcSettings; disable() wipes the note.
- Wallet pane (components/profile-modal.html + ProfileWalletController): opt-in
  toggle + Restore button + a strong SPENDING warning, shown only when a signer
  can encrypt. Enabling requires an explicit window.confirm (the URI can spend).
  Re-pushes on save when enabled; and on wallet DISCONNECT while synced it clears
  the note so a removed spending URI doesn't linger (encrypted) on relays.

tests/wallet-sync-service.test.mjs: enable-pushes-uri, pull-restores-via-
updateActiveNwcSettings, disable-clears, no-wallet-doesn't-publish. Mutation-
verified (skip-apply + skip-clear both fail).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 17:57:39 -04:00
thePR0M3TH3ANandClaude Opus 4.8 7376ff91e0 feat(sync): Storage pane UI for encrypted settings sync (opt-in toggle + restore)
Wires the storage-credential sync (6f7f4196) into the profile Storage pane.

UI (components/profile-modal.html): a "Cross-device sync" card shown only when
storage is unlocked — an opt-in toggle ("Sync these settings to my Nostr account
(encrypted)"), a "Restore from my Nostr account" button, the public-relay /
key-compromise warning copy, and a status line.

Controller (ProfileStorageController):
- toggle on -> storageSyncService.enable() (encrypt + publish), with rollback +
  message if nothing is stored yet; toggle off -> disable() (wipe the note)
- restore -> pull() + re-render the pane so the restored connection shows
- after a successful Save, re-push the synced copy when sync is enabled

Also routes the core's relay READ through the subscription manager (injected
listEvents) instead of pool.list, satisfying lint:pool-access for the new file;
the facade supplies it via nostrClient.getSubscriptionManager().list.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 17:17:35 -04:00
thePR0M3TH3ANandClaude Opus 4.8 9d3a0df02a feat(my-videos): wire bucket reconciliation into the tab (Phase 2, step 3b)
After the note-side list renders, the My Videos tab now reconciles against the
bucket: lists the user's objects (unlock-gated), then marks any active note whose
hosted file is gone as a 🟠 "Missing file", and lists bucket objects no current
video references in an "Orphaned storage objects" section — including files left
behind by videos deleted on Nostr (the headline cleanup case). Each orphan has a
"Delete file" button (with a confirm) that calls deleteStorageKeys and refreshes.

Reconciliation runs after the list is shown so the UI stays responsive, and is a
no-op (orphan section hidden) when storage is locked/unconfigured. Rows carry a
data-video-id + a data-role pill so missing status can be applied in place.

Build + full lint green (DOM via createElement/textContent, no innerHTML).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 15:00:43 -04:00
thePR0M3TH3ANandClaude Opus 4.8 475880198b feat(my-videos): add the My Videos profile tab + listing (Phase 1, step 3)
Adds a "My Videos" tab to the profile modal that lists every video the logged-in
user has published — collapsed to one row per video in its current state
(including tombstoned ones) — each with a note-side health pill: red "No playable
source", grey "Deleted on Nostr", green "OK". Shows a summary line
(N videos · M need attention · K deleted) and refreshes from relays on open.

- New MyVideosController (lazy-populated via selectPane, like the other panes):
  fetchVideosByAuthors -> collapseUserVideos -> classifyVideoHealth -> render.
  Rows built with createElement/textContent (no innerHTML; user titles/URLs are
  not interpolated into markup). Status pills reuse the relay-pill token styles.
- Sidebar nav button + pane markup in components/profile-modal.html.
- Wired into profileModalController (cache refs, listeners, selectPane hook);
  the existing nav-button loop auto-binds the click.

This is display-only; later steps add the hosted-URL HEAD probe (dead-url), row
actions (edit/delete/revert/storage cleanup), and Phase 2 bucket reconciliation.

Build + full lint green (no new file-size/innerHTML/token violations).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 14:32:20 -04:00
thePR0M3TH3ANandClaude Opus 4.8 c8a63c57c6 feat(edit): thumbnail + video file upload in Edit modal (steps 2-3/3)
Brings the Edit modal to upload parity with the Upload modal: re-upload a
thumbnail or video file instead of only pasting URLs.

- Wire storage services (r2/s3/storageService) + pubkey/npub accessors into
  EditModal via ModalManager/initEditModal; EditModal constructs a shared
  MediaUploader.
- New editModalUpload.js holds all the upload glue (file-picker wiring, progress,
  storage-unlock checks, field write-back) to keep EditModal under its file-size
  budget. EditModal calls initEditModalUpload(this, context) in one line.
- edit-video-modal.html: "Replace video file…" (under the URL field) and
  "Replace thumbnail file…" (under the thumbnail field), each with a hidden file
  input + status line.
- On thumbnail upload: writes the new URL into the thumbnail field. On video
  upload: writes url + magnet (+ ws/xs hints). All written via setFieldUnlocked,
  which marks the field edited so EditModal.collect actually persists it (gated
  behind storage being configured + unlocked).

setFieldUnlocked is unit-tested (mutation-verified): without the unlock, the
re-upload would silently not save. Build + full lint + unit suite green.

NOTE: the upload/storage flow itself can't be unit-tested (browser + R2/S3);
pending manual verification of editing a video's thumbnail/file.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 09:09:07 -04:00
thePR0M3TH3ANandClaude Opus 4.8 95071fd16b feat(relays): clearer relay health UI + fix broken relay operations
Relay tab improvements so users can see which relays to remove:
- Per-relay status pill: Online (with ping) / Offline / Checking… / Blocked.
- Insecure remote ws:// relays (CSP-blocked) flagged immediately with a hint.
- Offline relays show a "consider removing" hint; their Remove button is
  emphasized.
- Summary banner when relays are down (incl. the all-dead case explaining why
  videos won't load), pointing at Restore defaults / healthy relays.

Also fixes a pre-existing decomposition bug (commit 7300977a): handleRelayOperation
called this.runRelayOperation, but that method lives on the main
ProfileModalController — so ALL relay ops (add/remove/mode/restore) threw
"this.runRelayOperation is not a function". Route via this.mainController.

Tests: relay-insecure-url (isInsecureRelayUrl), relay-operation-routing (routes to
mainController.runRelayOperation, guards no-pubkey). Both mutation-verified.
Build + lint + full unit suite green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 08:27:41 -04:00
thePR0M3TH3ANandClaude Opus 4.8 c62114557c feat(webseed): manage multiple web seeds in the edit modal (slice 3/3)
Completes end-to-end multi-webseed support: the editor can now see, add, and
remove every web seed a video carries.

- magnetShared: promote normalizeWebSeedList to a shared export (videoNotePayload
  now imports it) and add setMagnetWebSeeds(), which replaces a magnet's COMPLETE
  ws= set with exactly the requested list. ensureWebSeeds is append-only, so
  without set-semantics a webseed a user deleted would linger in the magnet;
  setMagnetWebSeeds strips the existing ws= params first, making removal stick.
- edit-video-modal.html: the single "Web seed (ws=)" input becomes a "Web seeds
  (one per line)" textarea with backup-origin help text.
- EditModal: open() populates the field from extractAllWebSeeds() (every seed,
  not just the first). On save, when the field was manually edited the magnet's
  ws set is rebuilt from the field via setMagnetWebSeeds (true add/remove);
  otherwise existing seeds are preserved untouched. The content `ws` field still
  stores the primary (first) seed while the magnet carries the full set.

Mutation-verified (no-strip and no-add both fail the removal/add tests); build,
relevant lints (inline-styles, tokens, innerHTML), and full unit suite green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-19 08:52:11 -04:00
thePR0M3TH3ANandClaude Opus 4.8 ce7ef9ef2d feat(webseed): upload form accepts multiple web seeds (slice 2/3)
Let uploaders declare a backup origin at creation time so the swarm can keep
streaming when the primary CDN is overloaded.

- upload-modal.html: the single "Web Seed (ws)" input becomes a "Web seeds
  (one per line)" textarea with help text explaining the backup-origin use
  case and the byte-identical-file requirement.
- UploadModal: in upload mode the R2/CDN url stays the primary webseed and any
  textarea entries follow it (passed as [primaryUrl, textarea]); external mode
  already flowed the field through. All parsing stays in the payload layer.
- videoNotePayload.normalizeWebSeedList now splits every entry (array OR
  string) on newlines/commas, so the upload-modal's [url, "a\nb"] shape and a
  plain multi-line string both expand + dedupe correctly.

Mutation-verified (array entries no longer split fails the upload-shape test);
build + full unit suite green. Lints relevant to the change (inline-styles,
tokens, innerHTML) pass.

Note: npm run lint also reports pre-existing file-size violations in unrelated
files (SignerManager, watchHistory, playbackService, webtorrent, etc.) that
grew in earlier commits — none are touched here. Flagging separately.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-19 08:36:13 -04:00
thePR0M3TH3ANandClaude Opus 4.8 96302a59f3 chore: remove dev Agent Dashboard sidebar link + view constant
Follow-up to c79120ba (which only captured the file deletion): removes the
sidebar nav link and the unused DEV_DASHBOARD view constant. No dangling refs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 20:36:27 -04:00
thePR0M3TH3AN 0cf1dc0f55 improved playright integration 2026-02-21 20:45:47 -05:00
google-labs-jules[bot]andPR0M3TH3AN 106f7640c4 fix(exploreData): trigger refresh immediately on visibility change
- `handleVisibility` only scheduled intervals but didn't trigger an immediate refresh when returning to the tab.
- This caused `tests/unit/services/exploreDataService.test.mjs` to fail (assertion `0 !== 1`).
- Added explicit calls to `refreshWatchHistoryTagCounts` and `refreshTagIdf` when `document.hidden` becomes false.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-18 15:10:29 +00:00
google-labs-jules[bot]andPR0M3TH3AN f16a1ff5e4 Content Audit: Fix HTML accepted types and moderation config
Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-17 22:15:53 +00:00
google-labs-jules[bot]andPR0M3TH3AN 30bd4a75f6 feat: integrate TORCH agent dashboard into UI sidebar
- Add "Dev Tools" button to the sidebar footer menu.
- Configure `js/constants.js` with `DEV_DASHBOARD` view type.
- Refactor `views/dev/agent-dashboard.html` to be compatible with `loadView` (moved styles to body, updated links).
- Add cleanup logic to dashboard script to prevent memory leaks when navigating away.
- Fix duplicate import in `js/nostr/reactionEvents.js`.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-13 23:50:42 +00:00
Claude d0a6c9622b feat: add Playwright agent testability infrastructure
Add test harness (window.__bitvidTest__), data-testid attributes, relay
override support, and Playwright fixtures so agents can programmatically
login, seed relay data, and inspect app state during E2E testing.

- js/testHarness.js: exposes loginWithNsec, getAppState, relay overrides
- data-testid attrs on login/upload/video modals, header buttons, feed
- simple-relay.mjs: HTTP seeding API (POST /seed, GET /events, /health)
- bitvidTestFixture.ts: reusable Playwright fixture with mock relay
- agent-testability.spec.ts: E2E tests validating the infrastructure

https://claude.ai/code/session_01XnrszNmdqrAGd8g8qhAn4G
2026-02-10 05:09:46 +00:00
google-labs-jules[bot]andPR0M3TH3AN 0ac1def5c2 Optimize avatar image loading with lazy and async attributes
Added `loading="lazy"` and `decoding="async"` to author avatar images in `VideoCard.js` and the creator avatar in `video-modal.html`. This aligns these components with `SimilarContentCard.js` and `VideoCard` thumbnails, improving initial page load performance and reducing main-thread blocking by deferring off-screen image loading and decoding.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-04 16:50:23 +00:00
google-labs-jules[bot]andPR0M3TH3AN d45b115f07 Fix annoying DM relay warning and clarify settings UI
- Throttle the "Privacy warning: direct messages are using your default relays..." message so it only appears once per session/identity change.
- Rename "DM Relay Hints" to "NIP-17 Relay List" in the DM Settings modal to match the warning message and reduce user confusion.
- Add regression test for the warning throttling logic.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-04 16:16:15 +00:00
google-labs-jules[bot]andPR0M3TH3AN 1fb5ae3039 fix: resolve CI lint failure in dm-settings-modal
- Replaces arbitrary `max-h-[90vh]` with tokenized `max-h-modal-shell` in `components/dm-settings-modal.html` to satisfy `lint:tailwind-brackets` check.
- This accompanies the documentation updates for content uploads.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-03 02:43:45 +00:00
google-labs-jules[bot]andPR0M3TH3AN 9485003ffd Refactor DM settings into a dedicated modal
- Created `DMSettingsModalController` to manage the new settings modal UI.
- Added `components/dm-settings-modal.html` with sections for Privacy and DM Relay Hints.
- Updated `AppShell.js` to delegate the settings action to `onOpenSettings` callback, bypassing the legacy popover.
- Updated `ProfileModalController.js` to wire up the new modal controller, handling state persistence for relays and privacy settings.
- This improves the UX for managing NIP-17 DM relay hints and privacy options.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-02 19:43:06 +00:00
google-labs-jules[bot]andPR0M3TH3AN 5df489e504 Combine relay list with relay health check
- Remove separate relay health panel from profile modal.
- Merge health indicators (latency, errors) directly into the relay list items.
- Consolidate refresh logic to update both list configuration and health status.
- Move telemetry toggle to the bottom of the relay section.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-02 14:03:09 +00:00
google-labs-jules[bot]andPR0M3TH3AN 00ca2298e1 Fix CI lint error: Replace arbitrary tailwind value
- Replaced `h-[80vh]` with standard `h-4/5` in `components/subscription-history-modal.html` to satisfy `lint:tailwind-brackets` token rules.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-02 03:11:19 +00:00
google-labs-jules[bot]andPR0M3TH3AN a50e5c861a Add subscription history and backup/restore functionality
- Added `SUBSCRIPTION_BACKUP` note type schema
- Implemented `createBackup`, `fetchHistory`, `restoreBackup`, `rebroadcastBackup` in `SubscriptionsManager`
- Added Subscription History UI modal with list/details view
- Integrated History/Backup buttons into Profile Modal Subscriptions pane
- Fixed XSS vulnerability in history details view
- Added profile fetching for history details

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-02 03:05:13 +00:00
thePR0M3TH3AN acf73bc5a3 Add permission CTA for subscriptions and hashtags 2026-01-30 15:39:52 -05:00
thePR0M3TH3AN 09c0ac8127 Add relay health summary banner 2026-01-30 11:08:02 -05:00
thePR0M3TH3AN 379287c87c Defer subscription and hashtag decrypt prompts 2026-01-29 13:08:35 -05:00
thePR0M3TH3AN c4a00653f4 Move DM relay settings into DM settings popover 2026-01-29 11:52:41 -05:00
thePR0M3TH3AN 9a5a1fe542 Add refresh icon buttons to profile modal 2026-01-29 11:30:09 -05:00
thePR0M3TH3AN c86d64b211 Add DM relay hints panel to profile messages 2026-01-29 11:23:34 -05:00
google-labs-jules[bot]andPR0M3TH3AN cfd7c49a9d Refactor docs navigation and clean up legacy views
Co-authored-by: PR0M3TH3AN <[email protected]>
2026-01-27 16:32:01 +00:00
thePR0M3TH3AN 7087c4aab8 Add docs link to sidebar footer 2026-01-26 23:23:14 -05:00
google-labs-jules[bot] 6e3562b64b feat(ui): enhance custom S3 storage configuration with helper text and force path style option
- Add helper text to the S3 storage configuration form to guide users on Endpoint URL format.
- Add 'Force Path Style' checkbox to S3 storage configuration to support MinIO and other S3-compatible providers that require it.
- Update `ProfileModalController` to toggle visibility of these new fields based on the selected provider.
- Update `ProfileModalController` to save and load the `forcePathStyle` setting.
- Update `tests/ui/storageService.test.mjs` to verify persistence of `forcePathStyle`.
2026-01-26 16:50:48 +00:00
google-labs-jules[bot] ad1d7de092 Clarify Public URL field in storage settings and add validation
- Renamed "Public URL / Prefix" to "Public Access URL" in Profile Modal to differentiate from S3 API Endpoint.
- Added helper text clarifying that Account ID is used for connection and Public URL is for CDN.
- Implemented validation in `ProfileModalController` to detect and warn if an S3/R2 API endpoint is entered into the Public URL field.
- Added blocking validation on save to prevent misconfiguration.
- Verified changes with unit tests and Playwright frontend verification.
2026-01-26 01:24:04 +00:00
google-labs-jules[bot] e1324c1bfc Truncate Storage Status fields in Upload Modal
Truncate Provider, Bucket, and URL Style fields to prevent layout overflow.
Add `title` attribute for native tooltips on hover.
Add `tabindex="0"` for keyboard accessibility.
2026-01-25 21:19:42 +00:00
google-labs-jules[bot] 070c054043 Refactor upload flow for immediate uploads and fix logging crash
- Refactored `UploadModal.js` to trigger uploads immediately on file selection.
- Added progress bars and result fields (Video URL, Magnet, Torrent) to the UI.
- Fixed a crash in `r2Service.js` caused by `userLogger.debug` usage.
- Exposed `prepareUpload` and `uploadFile` methods in `r2Service` and `s3UploadService` for granular control.
- Updated `upload-modal.html` with new UI elements.
2026-01-25 20:53:16 +00:00
google-labs-jules[bot] 2062ba0889 Refactor Upload Modal to use read-only storage status
Simplified the Upload Modal by removing inline storage configuration forms.
The modal now displays the active storage provider (read-only) or prompts the user
to configure storage via the Profile Modal if missing.

Changes:
- Refactored `js/ui/components/UploadModal.js` to consume stored credentials directly.
- Updated `components/upload-modal.html` to remove complex forms.
- Updated `tests/ui/uploadModal-integration.test.mjs` to match new UI structure.
- Removed race-prone logic for saving credentials within the upload flow.
2026-01-25 19:20:08 +00:00
google-labs-jules[bot] 50acab55fc feat: Enable Custom S3 storage provider option
Replaces the disabled 'S3 Compatible' placeholder with an active 'Custom S3' option in the profile modal storage settings. This maps to the 'generic_s3' provider which is already supported by the backend logic.

- Updated `components/profile-modal.html` to enable the option.
- Verified via script and visual inspection (headless screenshot).
- Verified existing tests pass.
2026-01-25 18:05:41 +00:00
google-labs-jules[bot] a33092fc47 feat(upload): support presigned multipart manifest for S3 uploads
- Updated `js/s3-quick.js` to support new manifest structure: `{ bucket, key, uploadId, parts: [{partNumber, url}] }`.
- Added logic to sort parts, upload chunks, collect ETags, and call `completeUrl` if present.
- Updated `js/ui/components/UploadModal.js` to handle manifest flow, validate new structure, and alert user if completion is manual (missing `completeUrl`).
- Updated `components/upload-modal.html` with new placeholder example and documentation note.
- Verified logic with reproduction script and UI with Playwright.
2026-01-25 02:44:49 +00:00
google-labs-jules[bot] adad6a07a4 feat: Add S3 quick upload with presigned manifest support
- Add `js/s3-quick.js` with `uploadWithManifest` and `uploadWithKeys`.
- Update `components/upload-modal.html` to add "Use presigned manifest" toggle and warning.
- Update `js/ui/components/UploadModal.js` to wire up the manifest upload flow.
- Add local persistence for quick S3 settings (though mainly handled by existing storage settings).
- Ensure keys are only used for client-side uploads or replaced by manifest flow.
2026-01-25 01:38:41 +00:00
thePR0M3TH3AN 999235d71e Add S3 storage option to upload modal 2026-01-24 20:00:00 -05:00
google-labs-jules[bot] 6e21247505 fix(ui): ensure modals render above video player
- Adds --z-modal-always-top: 200 token in css/tokens.css
- Adds .modal-always-on-top utility class in css/tailwind.source.css
- Applies class to EventDetailsModal, EmbedVideoModal, and ShareNostrModal
2026-01-24 20:12:04 +00:00
thePR0M3TH3AN 7666629cba Update embed modal pointer encoding 2026-01-23 17:58:45 -05:00
thePR0M3TH3AN b3de79f46c Add embed video modal 2026-01-23 17:14:07 -05:00
thePR0M3TH3AN 1709972f14 Add embed action to video modal 2026-01-23 15:24:09 -05:00
google-labs-jules[bot] f53bfbeccf Replace search filter popover with responsive modal
Replaces the existing search filter popover with a new responsive modal component to improve UI/UX on both desktop and mobile devices.

- Adds `components/search-filter-modal.html` implementing a responsive grid layout.
- Adds `js/ui/components/SearchFilterModal.js` to manage filter state and modal interactions.
- Updates `js/index.js` to bootstrap the new modal and bind trigger events.
- Removes obsolete `js/ui/components/SearchFiltersPopover.js`.
- Keeps advanced filters hidden by default behind a toggle.

Signed-off-by: Jules <[email protected]>
2026-01-23 19:41:10 +00:00
google-labs-jules[bot] 6541421928 Fix NIP-46 QR code styling to use tokenized colors and utility classes
Updated `js/ui/loginModalController.js` to resolve QR code colors from CSS variables (`--color-white`, `--color-black`) instead of hardcoding hex values, ensuring compatibility with the design system.
Updated `components/login-modal.html` to use Tailwind utility classes (`bg-white`, `text-black`, `p-6`) instead of inline styles, improving maintainability and readability.
This ensures the QR code remains scannable (high contrast) while respecting the codebase's tokenized styling approach.
2026-01-23 14:49:28 +00:00
google-labs-jules[bot] 57c065125c Fix NIP-46 QR code styling to ensure white background
Hardcoded the QR code colors to black-on-white and added explicit inline styles to the container to prevent theme-based inversions or transparency issues. Increased container padding to improve readability.
2026-01-23 14:29:59 +00:00
google-labs-jules[bot] 559aed3a5d fix: Update "For Kids" sidebar icon to inline SVG 2026-01-23 07:56:52 +00:00
google-labs-jules[bot] 986e4ef497 ui: move Kids link below Recently Added and add tags to Kids page 2026-01-23 07:40:44 +00:00
thePR0M3TH3AN 33cc5d23c7 Add kids feed view 2026-01-23 02:09:08 -05:00
google-labs-jules[bot] 38881c8244 Redesign DM UI with independent scrolling and split layout
- Implemented split-pane layout for Conversations and Message History.
- Added independent vertical scrolling for desktop views.
- Enforced full-height containers to prevent parent modal scrolling on desktop.
- Preserved mobile scrolling behavior by wrapping height constraints in media queries.
- Added avatars, message bubbles, and alignment styling.
- Removed space-y utility from profile modal container to prevent layout issues.
- Updated tailwind source CSS with new scrollbar utilities and overrides.
2026-01-23 06:24:15 +00:00
thePR0M3TH3AN 012643ce12 Add share on Nostr modal 2026-01-22 22:36:56 -05:00