Compare commits

...
30 Commits
Author SHA1 Message Date
thePR0M3TH3AN 1d1655d2a7 Merge pull request #166 from PR0M3TH3AN/unstable
Unstable
2025-09-30 09:10:39 -04:00
thePR0M3TH3AN 4ebc6f6f4a Merge pull request #165 from PR0M3TH3AN/codex/ensure-notification-box-visibility-on-modals
Ensure notifications appear over modal overlays
2025-09-30 09:06:45 -04:00
thePR0M3TH3AN db408f4b06 Raise notification portal above modals 2025-09-30 09:05:03 -04:00
thePR0M3TH3AN f832695a6a Ensure notifications appear above modals 2025-09-30 08:59:18 -04:00
thePR0M3TH3AN 213f92f496 Merge pull request #164 from PR0M3TH3AN/codex/update-moderation-list-loading-indication
Replace moderation relay warning with loading status
2025-09-30 00:01:10 -04:00
thePR0M3TH3AN 96e8adc553 Ensure moderation loading banner clears after sync 2025-09-29 23:59:52 -04:00
thePR0M3TH3AN 233c50d238 Show moderation filter loading status 2025-09-29 23:51:20 -04:00
thePR0M3TH3AN 7937410e87 Merge pull request #163 from PR0M3TH3AN/codex/update-rendersavedprofiles-for-metadata-fetching
Auto-fetch metadata for saved profiles lacking hints
2025-09-29 23:46:53 -04:00
thePR0M3TH3AN b50996b907 Auto-fetch saved profile metadata 2025-09-29 23:38:22 -04:00
thePR0M3TH3AN 3f4b56ae90 Merge pull request #162 from PR0M3TH3AN/codex/extend-profile-switching-functionality
Add profile switching flow to saved profile cards
2025-09-29 23:19:20 -04:00
thePR0M3TH3AN edbc3786eb Enhance profile switching interactions 2025-09-29 23:18:37 -04:00
thePR0M3TH3AN c7419a7f74 Merge pull request #161 from PR0M3TH3AN/codex/enhance-nostrclient.login-with-options-object
Add NIP-07 profile selection flow to profile switcher
2025-09-29 23:15:18 -04:00
thePR0M3TH3AN ee09b723af Add profile switching support for NIP-07 accounts 2025-09-29 23:14:52 -04:00
thePR0M3TH3AN c617fcbfda Merge pull request #160 from PR0M3TH3AN/codex/implement-profile-modal-rendering-logic
Refine saved profile rendering
2025-09-29 23:08:25 -04:00
thePR0M3TH3AN 622aa756cc Refine saved profile rendering 2025-09-29 23:07:45 -04:00
thePR0M3TH3AN c52199b1d0 Merge pull request #159 from PR0M3TH3AN/codex/refactor-login-function-and-update-logout-logic
Refine login flow and profile storage
2025-09-29 22:57:33 -04:00
thePR0M3TH3AN ec83b1222f Refine login flow and profile storage 2025-09-29 22:55:40 -04:00
thePR0M3TH3AN 033d3b0288 Merge pull request #158 from PR0M3TH3AN/codex/add-saved-profile-management-in-app.js
Persist saved Nostr profiles in localStorage
2025-09-29 22:40:53 -04:00
thePR0M3TH3AN 5cfdd79a18 Add saved Nostr profile storage helpers 2025-09-29 17:24:25 -04:00
thePR0M3TH3AN ce1f2a8f37 Merge pull request #157 from PR0M3TH3AN/codex/add-ui-for-new-nostr-profiles
Add UI affordance for managing multiple Nostr profiles
2025-09-29 16:01:10 -04:00
thePR0M3TH3AN 2b1d5488d6 Restore hidden profile panes 2025-09-29 15:58:12 -04:00
thePR0M3TH3AN e50780e4b3 Consolidate profile modal account styling 2025-09-29 15:10:53 -04:00
thePR0M3TH3AN c2846f9348 Polish profile switcher card styling 2025-09-29 14:51:05 -04:00
thePR0M3TH3AN 4266d0bd18 Refine account cards in profile modal 2025-09-29 14:39:23 -04:00
thePR0M3TH3AN 711df9bf07 Ensure full npub wraps in profile modal 2025-09-29 14:27:45 -04:00
thePR0M3TH3AN 24328c4c52 Refine active profile layout in modal 2025-09-29 14:23:18 -04:00
thePR0M3TH3AN d15c574b13 Restyle profile switcher active row 2025-09-29 14:15:10 -04:00
thePR0M3TH3AN 198fd97a80 Adjust profile switcher layout 2025-09-29 14:09:48 -04:00
thePR0M3TH3AN c285bd7e9c Stack profile switcher list vertically 2025-09-29 13:59:07 -04:00
thePR0M3TH3AN 55774f588e Add UI affordance for managing multiple Nostr profiles 2025-09-29 13:52:34 -04:00
7 changed files with 1507 additions and 73 deletions

No files matched your search

+82 -19
View File
@@ -107,27 +107,90 @@
role="tabpanel"
aria-labelledby="profileNavAccount"
>
<div class="flex items-center space-x-4">
<img
id="profileModalAvatar"
src="assets/svg/default-profile.svg"
alt="Profile"
class="w-16 h-16 object-cover rounded-full"
/>
<p id="profileModalName" class="text-lg font-semibold truncate">
Loading...
<!--
The account pane now relies on bespoke CSS (see css/style.css under
"Profile modal account pane") so we are not dependent on utility
classes that may be purged or overridden by other builds.
-->
<div class="profile-switcher" id="profileSwitcherTray">
<div class="profile-switcher__header">
<div class="profile-switcher__intro">
<h3 class="profile-switcher__title">Switch profiles</h3>
<p class="profile-switcher__subtitle">
Save multiple Nostr profiles so you can swap identities without leaving BitVid.
</p>
</div>
<span class="profile-switcher__badge">Preview</span>
</div>
<div
class="profile-card profile-card--active"
aria-label="Current Nostr profile"
aria-pressed="true"
role="button"
tabindex="0"
>
<span class="profile-card__avatar">
<img
id="profileModalAvatar"
src="assets/svg/default-profile.svg"
alt="Active profile avatar"
/>
</span>
<span class="profile-card__meta">
<span class="profile-card__topline">
<span class="profile-card__label">Active profile</span>
<a
id="profileChannelLink"
href="#"
class="profile-card__action hidden"
>
View channel
</a>
</span>
<span id="profileModalName" class="profile-card__name">Loading...</span>
<span id="profileModalNpub" class="profile-card__npub">npub1...</span>
</span>
</div>
<div
id="profileSwitcherList"
class="profile-switcher__list"
aria-live="polite"
aria-label="Additional Nostr profiles"
></div>
<button
type="button"
id="profileAddAccountBtn"
class="profile-switcher__add"
aria-label="Add a new Nostr profile"
>
<span class="profile-switcher__addAvatar">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
</span>
<span class="profile-switcher__addCopy">
<span class="profile-switcher__addTitle">Add profile</span>
<span class="profile-switcher__addHint">Link another npub</span>
</span>
</button>
<p class="profile-switcher__helper">
Profiles you add here will stay on this device so you can quickly switch before posting or moderating.
</p>
</div>
<a
id="profileChannelLink"
href="#"
class="inline-flex items-center text-sm text-blue-400 hover:text-blue-300 transition-colors hidden"
>
View your channel
</a>
<p class="text-sm text-gray-400">
Manage your account details and upcoming preferences here.
</p>
</section>
<section
+331
View File
@@ -269,6 +269,290 @@ header img {
-webkit-backdrop-filter: blur(0.625rem); /* For Safari */
}
/* --------------------------------------------------------------------------
Profile modal account pane
--------------------------------------------------------------------------
The modal previously mixed Tailwind-style utility classes with our global
stylesheet. Those utilities are not always present in the production build,
so important layout rules (borders, gaps, flex behaviour) were getting
dropped. This block consolidates the account tab styling in one predictable
place so future tweaks only require CSS updates here instead of scattered
inline class lists.
--------------------------------------------------------------------------- */
#profileModal .profile-pane {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/*
Ensure the global `.hidden` helper keeps working for the tab panes. Our
profile-pane declaration above sets a `display` value with higher
specificity than `.hidden`, so we explicitly restore the hidden state when
both classes are present.
*/
#profileModal .profile-pane.hidden {
display: none;
}
#profileModal .profile-switcher {
display: flex;
flex-direction: column;
gap: 1.5rem;
padding: clamp(1.25rem, 2vw, 1.5rem);
border-radius: 1.25rem;
border: 0.0625rem solid rgba(148, 163, 184, 0.28);
background: linear-gradient(145deg, rgba(15, 23, 42, 0.94), rgba(15, 23, 42, 0.72));
box-shadow: 0 1.75rem 3.5rem rgba(8, 11, 30, 0.45);
}
#profileModal .profile-switcher__header {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
#profileModal .profile-switcher__intro {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
#profileModal .profile-switcher__title {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--color-text);
}
#profileModal .profile-switcher__subtitle {
margin: 0;
font-size: 0.875rem;
color: var(--color-muted);
}
#profileModal .profile-switcher__badge {
align-self: flex-start;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.35rem 0.85rem;
font-size: 0.625rem;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
border-radius: 9999px;
background: rgba(59, 130, 246, 0.18);
color: rgba(191, 219, 254, 0.95);
border: 0.0625rem solid rgba(96, 165, 250, 0.25);
}
#profileModal .profile-card {
position: relative;
display: flex;
flex-direction: column;
gap: 0.85rem;
padding: 1rem;
border-radius: 1rem;
border: 0.0625rem solid rgba(148, 163, 184, 0.24);
background: rgba(15, 23, 42, 0.72);
box-shadow: inset 0 1px 0 rgba(148, 163, 184, 0.08);
color: var(--color-text);
cursor: pointer;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
outline: none;
}
#profileModal .profile-card:hover {
border-color: rgba(148, 163, 184, 0.45);
}
#profileModal .profile-card:focus-visible {
border-color: rgba(96, 165, 250, 0.6);
box-shadow: 0 0 0 0.1875rem rgba(37, 99, 235, 0.25);
}
#profileModal .profile-card--active {
border-color: rgba(59, 130, 246, 0.55);
background: linear-gradient(160deg, rgba(37, 99, 235, 0.22), rgba(15, 23, 42, 0.78));
box-shadow: 0 1.35rem 2.8rem rgba(37, 99, 235, 0.22);
}
#profileModal .profile-card__avatar {
flex: 0 0 auto;
width: 3rem;
height: 3rem;
display: grid;
place-items: center;
border-radius: 9999px;
overflow: hidden;
border: 0.1875rem solid rgba(148, 163, 184, 0.3);
background: rgba(15, 23, 42, 0.95);
}
#profileModal .profile-card__avatar img {
width: 100%;
height: 100%;
border-radius: inherit;
object-fit: cover;
}
#profileModal .profile-card--active .profile-card__avatar {
border-color: rgba(96, 165, 250, 0.7);
box-shadow: 0 0 0 0.1875rem rgba(59, 130, 246, 0.18);
}
#profileModal .profile-card__meta {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
#profileModal .profile-card__topline {
display: flex;
flex-direction: column;
gap: 0.35rem;
align-items: flex-start;
}
#profileModal .profile-card__label {
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.2em;
text-transform: uppercase;
color: rgba(191, 219, 254, 0.9);
}
#profileModal .profile-card__action {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.3rem 0.8rem;
border-radius: 9999px;
border: 0.0625rem solid rgba(59, 130, 246, 0.4);
background: rgba(59, 130, 246, 0.14);
color: rgba(191, 219, 254, 0.95);
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
text-decoration: none;
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
#profileModal .profile-card__action:hover,
#profileModal .profile-card__action:focus-visible {
background: rgba(59, 130, 246, 0.24);
border-color: rgba(59, 130, 246, 0.6);
color: #ffffff;
}
#profileModal .profile-card__name {
font-size: 0.95rem;
font-weight: 600;
color: var(--color-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#profileModal .profile-card__npub {
font-size: 0.75rem;
font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
color: rgba(148, 163, 184, 0.85);
word-break: break-all;
}
#profileModal .profile-switcher__list {
display: flex;
flex-direction: column;
gap: 1rem;
}
#profileModal .profile-switcher__add {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border-radius: 1rem;
border: 0.125rem dashed rgba(148, 163, 184, 0.35);
background: rgba(15, 23, 42, 0.55);
color: rgba(226, 232, 240, 0.85);
text-align: left;
cursor: pointer;
transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
}
#profileModal .profile-switcher__add:hover,
#profileModal .profile-switcher__add:focus-visible {
border-color: rgba(96, 165, 250, 0.65);
background: rgba(59, 130, 246, 0.18);
color: #ffffff;
outline: none;
}
#profileModal .profile-switcher__addAvatar {
flex: 0 0 auto;
width: 3rem;
height: 3rem;
display: grid;
place-items: center;
border-radius: 9999px;
border: 0.125rem dashed currentColor;
color: inherit;
background: rgba(15, 23, 42, 0.75);
}
#profileModal .profile-switcher__addAvatar svg {
width: 1.25rem;
height: 1.25rem;
}
#profileModal .profile-switcher__addCopy {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
#profileModal .profile-switcher__addTitle {
font-size: 0.9rem;
font-weight: 600;
color: inherit;
}
#profileModal .profile-switcher__addHint {
font-size: 0.75rem;
color: rgba(148, 163, 184, 0.85);
}
#profileModal .profile-switcher__helper {
margin: 0;
font-size: 0.75rem;
color: rgba(148, 163, 184, 0.7);
}
@media (min-width: 640px) {
#profileModal .profile-switcher__header {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
#profileModal .profile-card {
flex-direction: row;
align-items: center;
}
#profileModal .profile-card__topline {
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 0.75rem;
}
}
/* If you ever want to show it, add ".flex" class dynamically */
#playerModal.flex {
display: flex;
@@ -535,6 +819,27 @@ button:not(.icon-button):focus {
}
/* Notifications */
#notificationPortal {
position: fixed;
top: calc(env(safe-area-inset-top, 0px) + 1.5rem);
left: 50%;
transform: translateX(-50%);
z-index: 400; /* Keep notifications above the highest modal overlays */
width: min(90vw, 32rem);
display: flex;
flex-direction: column;
gap: 0.75rem;
pointer-events: none;
}
#notificationPortal > * {
width: 100%;
margin-bottom: 0;
pointer-events: auto;
border-radius: 0.75rem;
box-shadow: 0 1.25rem 3rem rgba(15, 23, 42, 0.3);
}
/* Base styling without a forced display */
#errorContainer,
#successContainer {
@@ -550,6 +855,32 @@ button:not(.icon-button):focus {
display: flex;
}
.status-banner {
align-items: center;
background-color: rgba(31, 41, 55, 0.9);
border-radius: 0.5rem;
color: #e5e7eb;
display: flex;
gap: 0.75rem;
margin-bottom: 1rem;
padding: 1rem;
}
.status-banner .status-spinner {
animation: spin 1s linear infinite;
border: 0.125rem solid rgba(229, 231, 235, 0.35);
border-radius: 9999px;
border-top-color: #f9fafb;
height: 1.25rem;
width: 1.25rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Responsive Design */
@media (max-width: 640px) {
#videoList {
+73
View File
@@ -0,0 +1,73 @@
# Nostr authentication storage
Bitvid keeps lightweight state in `localStorage` so the UI can remember which
accounts signed in recently. This document explains the storage schema so future
updates can evolve it without breaking existing users.
## Storage keys
- `bitvid:profileCache:v1` – short-lived metadata cache for profile names and
avatars. See `js/app.js` for TTL and eviction rules.
- `bitvid:savedProfiles:v1` – persistent list of accounts the user authenticated
with in this browser.
Both keys live in the main origin scope. Clearing one should not affect the
other.
## Saved profile schema
`bitvid:savedProfiles:v1` is a JSON object with the shape:
```json
{
"version": 1,
"entries": [
{
"pubkey": "<hex-encoded pubkey>",
"npub": "<bech32 npub, optional>",
"name": "<cached display name>",
"picture": "<cached avatar URL>",
"authType": "<auth strategy>"
}
],
"activePubkey": "<hex pubkey or null>"
}
```
Notes:
- `pubkey` always stores a lowercase 64-character hex string and is the primary
dedupe key.
- `npub` is optional. When omitted, the UI regenerates it on load using
`NostrTools.nip19.npubEncode`.
- `name` and `picture` are cached hints that keep the profile switcher snappy.
They may be empty strings and should be treated as hints, not the source of
truth.
- `activePubkey` tracks the last account that successfully authenticated in this
browser. It may be `null` when the user logs out but keeps saved entries.
### `authType` enum
`authType` describes how the profile authenticated:
- `"nip07"` – Browser extension flow (current default).
- `"nsec"` – Reserved for future direct key import or signer integrations. New
auth strategies should pick a distinct string and document how migration works
alongside any recovery tooling.
When the app reads stored entries it normalises unknown values back to
`"nip07"` but keeps recognised alternatives intact.
## Migration notes
Earlier builds only persisted a single `userPubKey` string. During startup the
app now:
1. Attempts to read `bitvid:savedProfiles:v1` and validate the payload.
2. If the key is missing (or malformed) but a legacy `userPubKey` entry exists,
it seeds `savedProfiles` with that value and writes the new structure.
3. Once the JSON payload is written successfully, the legacy `userPubKey` entry
is removed.
Future migrations should follow the same pattern: validate, normalise, write the
new format, then clean up legacy keys to avoid data loss.
+3
View File
@@ -17,5 +17,8 @@ Run this checklist before publishing releases or merging changes that touch uplo
5. **Cross-Browser Verification**
- Spot-check Chromium and Firefox (desktop) for console warnings about CORS, Range requests, or tracker connectivity.
- Optionally validate on mobile Safari/Chrome if the change targets mobile UX.
6. **Saved Profile Metadata Refresh**
- In the browser devtools console/storage panel, delete `bitvid:profileCache:v1` but keep `bitvid:savedProfiles:v1` populated with Nostr pubkeys that lack stored names/avatars.
- Reload the page and open the profile switcher; confirm avatars and display names populate automatically without manual refresh.
Document findings (pass/fail notes plus relevant screenshots or logs) so they can be attached to release or PR notes.
+28 -9
View File
@@ -149,17 +149,36 @@
</div>
</header>
<!-- Error Container -->
<!-- Notification Portal -->
<div
id="errorContainer"
class="hidden bg-red-100 text-red-900 p-4 rounded-md mb-4"
></div>
id="notificationPortal"
role="region"
aria-live="polite"
aria-label="System notifications"
>
<!-- Error Container -->
<div
id="errorContainer"
class="hidden bg-red-100 text-red-900 p-4 rounded-md mb-4"
></div>
<!-- Success Container -->
<div
id="successContainer"
class="hidden bg-green-100 text-green-900 p-4 rounded-md mb-4"
></div>
<!-- Status Container -->
<div
id="statusContainer"
class="hidden status-banner"
role="status"
aria-live="polite"
>
<span class="status-spinner" aria-hidden="true"></span>
<span class="status-message" data-status-message></span>
</div>
<!-- Success Container -->
<div
id="successContainer"
class="hidden bg-green-100 text-green-900 p-4 rounded-md mb-4"
></div>
</div>
<!-- Dynamic views (like most-recent-videos.html, etc.) -->
<main id="viewContainer" class="flex-grow mb-8"></main>
+949 -44
View File
File diff suppressed because it is too large. Load diff
+41 -1
View File
@@ -593,7 +593,7 @@ class NostrClient {
/**
* Attempt login with a Nostr extension
*/
async login() {
async login(options = {}) {
try {
const extension = window.nostr;
if (!extension) {
@@ -603,6 +603,13 @@ class NostrClient {
);
}
const { allowAccountSelection = false, expectPubkey } =
typeof options === "object" && options !== null ? options : {};
const normalizedExpectedPubkey =
typeof expectPubkey === "string" && expectPubkey.trim()
? expectPubkey.trim().toLowerCase()
: null;
if (typeof extension.getPublicKey !== "function") {
throw new Error(
"This NIP-07 extension is missing getPublicKey support. Please update the extension."
@@ -624,6 +631,30 @@ class NostrClient {
}
}
if (allowAccountSelection && typeof extension.selectAccounts === "function") {
try {
const selection = await extension.selectAccounts(
expectPubkey ? [expectPubkey] : undefined
);
const didCancelSelection =
selection === undefined ||
selection === null ||
selection === false ||
(Array.isArray(selection) && selection.length === 0);
if (didCancelSelection) {
throw new Error("Account selection was cancelled.");
}
} catch (selectionErr) {
const message =
selectionErr && typeof selectionErr.message === "string"
? selectionErr.message
: "Account selection was cancelled.";
throw new Error(message);
}
}
let timeoutId;
const pubkey = await Promise.race([
extension.getPublicKey(),
@@ -646,6 +677,15 @@ class NostrClient {
"The NIP-07 extension did not return a public key. Please try again."
);
}
if (
normalizedExpectedPubkey &&
pubkey.toLowerCase() !== normalizedExpectedPubkey
) {
throw new Error(
"The selected account doesn't match the expected profile. Please try again."
);
}
const npub = window.NostrTools.nip19.npubEncode(pubkey);
if (isDevMode) {