Browse Source

bug-fixes

imwald-v0.58.5
silberengel 4 months ago
parent
commit
b160c32a4b
  1. 30
      app/web/src/App.svelte
  2. 98
      app/web/src/ComposeView.svelte
  3. 147
      app/web/src/EventsView.svelte
  4. 3
      app/web/src/constants.js
  5. 42
      app/web/src/helpers.tsx

30
app/web/src/App.svelte

@ -893,8 +893,18 @@
authMethod = storedAuthMethod; authMethod = storedAuthMethod;
// Restore signer for extension method // Restore signer for extension method
if (storedAuthMethod === "extension" && window.nostr) { if (storedAuthMethod === "extension") {
userSigner = window.nostr; if (window.nostr) {
userSigner = window.nostr;
} else {
// Extension might not be loaded yet, try again after a short delay
setTimeout(() => {
if (window.nostr && !userSigner) {
userSigner = window.nostr;
console.log("Extension signer restored after delay");
}
}, 500);
}
} }
} }
@ -958,7 +968,7 @@
searchTabs = []; searchTabs = [];
// Reload all relay-dependent data // Reload all relay-dependent data
loadRelayData(); await loadRelayData();
// If the events tab is currently active, reload events // If the events tab is currently active, reload events
if (selectedTab === "events" && isLoggedIn) { if (selectedTab === "events" && isLoggedIn) {
@ -2795,6 +2805,12 @@
return; return;
} }
// If userSigner is null but auth method is extension, try to restore it
if (!userSigner && authMethod === "extension" && window.nostr) {
userSigner = window.nostr;
console.log("Restored extension signer");
}
if (!userSigner) { if (!userSigner) {
alert( alert(
"No signer available. Please log in with a valid authentication method.", "No signer available. Please log in with a valid authentication method.",
@ -2841,6 +2857,12 @@
return; return;
} }
// If userSigner is null but auth method is extension, try to restore it
if (!userSigner && authMethod === "extension" && window.nostr) {
userSigner = window.nostr;
console.log("Restored extension signer for publishing");
}
if (!userSigner) { if (!userSigner) {
composePublishError = "No signer available. Please log in with a valid authentication method."; composePublishError = "No signer available. Please log in with a valid authentication method.";
return; return;
@ -3076,8 +3098,6 @@
bind:composeEventJson bind:composeEventJson
bind:localOnly={composeLocalOnly} bind:localOnly={composeLocalOnly}
{userPubkey} {userPubkey}
{userRole}
{policyEnabled}
publishError={composePublishError} publishError={composePublishError}
on:reformatJson={reformatJson} on:reformatJson={reformatJson}
on:signEvent={signEvent} on:signEvent={signEvent}

98
app/web/src/ComposeView.svelte

@ -1,8 +1,6 @@
<script> <script>
export let composeEventJson = ""; export let composeEventJson = "";
export let userPubkey = ""; export let userPubkey = "";
export let userRole = "";
export let policyEnabled = false;
export let publishError = ""; export let publishError = "";
export let localOnly = true; export let localOnly = true;
@ -13,6 +11,17 @@
let isTemplateSelectorOpen = false; let isTemplateSelectorOpen = false;
// Check if event is signed
$: isSigned = (() => {
if (!composeEventJson.trim()) return false;
try {
const event = JSON.parse(composeEventJson);
return !!(event.id && event.sig && event.pubkey);
} catch {
return false;
}
})();
function reformatJson() { function reformatJson() {
dispatch("reformatJson"); dispatch("reformatJson");
} }
@ -53,16 +62,41 @@
<button class="compose-btn reformat-btn" on:click={reformatJson} <button class="compose-btn reformat-btn" on:click={reformatJson}
>Reformat</button >Reformat</button
> >
<button class="compose-btn sign-btn" on:click={signEvent}>Sign</button> <button
class="compose-btn sign-btn"
class:signed={isSigned}
on:click={signEvent}
title={isSigned ? "Event is signed ✓" : "Sign the event before publishing"}
>
{isSigned ? "✓ Signed" : "Sign"}
</button>
<label class="local-only-label"> <label class="local-only-label">
<input type="checkbox" bind:checked={localOnly} /> <input type="checkbox" bind:checked={localOnly} />
This relay only This relay only
</label> </label>
<button class="compose-btn publish-btn" on:click={publishEvent} <button
>Publish</button class="compose-btn publish-btn"
class:disabled={!isSigned}
on:click={publishEvent}
disabled={!isSigned}
title={!isSigned ? "Please sign the event first" : "Publish to relay"}
> >
Publish
</button>
</div> </div>
{#if !isSigned && composeEventJson.trim()}
<div class="info-banner">
<div class="info-content">
<span class="info-icon"></span>
<span class="info-message">
<strong>Step 1:</strong> Click "Sign" to sign your event with your private key.
<strong>Step 2:</strong> Click "Publish" to send it to the relay.
</span>
</div>
</div>
{/if}
{#if publishError} {#if publishError}
<div class="error-banner"> <div class="error-banner">
<div class="error-content"> <div class="error-content">
@ -77,7 +111,12 @@
<textarea <textarea
bind:value={composeEventJson} bind:value={composeEventJson}
class="compose-textarea" class="compose-textarea"
placeholder="Enter your Nostr event JSON here, or click 'Generate Template' to start with a template..." placeholder="Enter your Nostr event JSON here, or click 'Generate Template' to start with a template...
Workflow:
1. Generate Template (or write JSON manually)
2. Click 'Sign' to sign the event with your private key
3. Click 'Publish' to send it to the relay"
spellcheck="false" spellcheck="false"
></textarea> ></textarea>
</div> </div>
@ -236,6 +275,53 @@
padding: 0; padding: 0;
} }
.sign-btn.signed {
background: var(--success);
color: var(--text-color);
}
.publish-btn.disabled {
opacity: 0.5;
cursor: not-allowed;
background: var(--secondary);
}
.publish-btn.disabled:hover {
background: var(--secondary);
filter: none;
}
.info-banner {
display: flex;
align-items: center;
padding: 0.75em 1em;
margin: 0 0.5em;
background: var(--info);
border: 1px solid var(--border-color);
border-radius: 0.25rem;
color: var(--text-color);
font-size: 0.9rem;
}
.info-content {
display: flex;
align-items: center;
gap: 0.5em;
}
.info-icon {
font-size: 1.2em;
flex-shrink: 0;
}
.info-message {
line-height: 1.4;
}
.info-message strong {
font-weight: 600;
}
.compose-textarea { .compose-textarea {
flex: 1; flex: 1;
width: 100%; width: 100%;

147
app/web/src/EventsView.svelte

@ -8,10 +8,15 @@
export let showOnlyMyEvents = false; export let showOnlyMyEvents = false;
export let showFilterBuilder = false; export let showFilterBuilder = false;
import { createEventDispatcher } from "svelte"; import { createEventDispatcher, onMount } from "svelte";
import FilterBuilder from "./FilterBuilder.svelte"; import FilterBuilder from "./FilterBuilder.svelte";
import { fetchUserProfile } from "./nostr.js";
import { getKindName, truncatePubkey } from "./helpers.tsx";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
// Profile cache to avoid fetching the same profile multiple times
let profileCache = new Map();
// Local state for JSON editor toggle // Local state for JSON editor toggle
let showJsonEditor = false; let showJsonEditor = false;
@ -55,59 +60,43 @@
dispatch("filterClear"); dispatch("filterClear");
} }
function truncatePubkey(pubkey) { // Fetch profile for a pubkey (with caching)
if (!pubkey) return ""; async function getProfile(pubkey) {
return pubkey.slice(0, 8) + "..." + pubkey.slice(-8); if (!pubkey) return null;
}
// Check cache first
function getKindName(kind) { if (profileCache.has(pubkey)) {
const kindNames = { return profileCache.get(pubkey);
0: "Profile", }
1: "Text Note",
2: "Recommend Relay", // Fetch profile
3: "Contacts", try {
4: "Encrypted DM", const profile = await fetchUserProfile(pubkey);
5: "Delete", profileCache.set(pubkey, profile);
6: "Repost", return profile;
7: "Reaction", } catch (error) {
8: "Badge Award", console.warn("Failed to fetch profile for", pubkey, error);
16: "Generic Repost", // Cache null to avoid repeated failed fetches
40: "Channel Creation", profileCache.set(pubkey, null);
41: "Channel Metadata", return null;
42: "Channel Message", }
43: "Channel Hide Message", }
44: "Channel Mute User",
1984: "Reporting", // Load profiles for all unique pubkeys in filteredEvents
9734: "Zap Request", $: if (filteredEvents.length > 0) {
9735: "Zap", const uniquePubkeys = new Set(
10000: "Mute List", filteredEvents
10001: "Pin List", .map(e => e.pubkey)
10002: "Relay List", .filter(p => p && !profileCache.has(p))
22242: "Client Auth", );
24133: "Nostr Connect",
27235: "HTTP Auth", // Fetch profiles for new pubkeys (fire-and-forget, errors are handled in getProfile)
30000: "Categorized People", uniquePubkeys.forEach(pubkey => {
30001: "Categorized Bookmarks", getProfile(pubkey).catch(err => {
30008: "Profile Badges", // Error already logged in getProfile, just prevent unhandled rejection
30009: "Badge Definition", console.debug("Profile fetch failed (already handled):", pubkey);
30017: "Create or update a stall", });
30018: "Create or update a product", });
30023: "Long-form Content",
30024: "Draft Long-form Content",
30078: "Application-specific Data",
30311: "Live Event",
30315: "User Statuses",
30402: "Classified Listing",
30403: "Draft Classified Listing",
31922: "Date-Based Calendar Event",
31923: "Time-Based Calendar Event",
31924: "Calendar",
31925: "Calendar Event RSVP",
31989: "Handler recommendation",
31990: "Handler information",
34550: "Community Definition",
};
return kindNames[kind] || `Kind ${kind}`;
} }
function formatTimestamp(timestamp) { function formatTimestamp(timestamp) {
@ -125,6 +114,7 @@
<div class="events-view-content" on:scroll={handleScroll}> <div class="events-view-content" on:scroll={handleScroll}>
{#if filteredEvents.length > 0} {#if filteredEvents.length > 0}
{#each filteredEvents as event} {#each filteredEvents as event}
{@const profile = profileCache.get(event.pubkey)}
<div <div
class="events-view-item" class="events-view-item"
class:expanded={expandedEvents.has(event.id)} class:expanded={expandedEvents.has(event.id)}
@ -139,11 +129,27 @@
tabindex="0" tabindex="0"
> >
<div class="events-view-avatar"> <div class="events-view-avatar">
<div class="avatar-placeholder">👤</div> {#if profile?.picture}
<img
src={profile.picture}
alt={profile.name || truncatePubkey(event.pubkey)}
class="avatar-image"
/>
{:else}
<div class="avatar-placeholder">👤</div>
{/if}
</div> </div>
<div class="events-view-info"> <div class="events-view-info">
<div class="events-view-author"> <div class="events-view-author">
{truncatePubkey(event.pubkey)} {#if profile}
<span class="author-name">{profile.name || truncatePubkey(event.pubkey)}</span>
{#if profile.nip05}
<span class="author-nip05" title={profile.nip05}>@{profile.nip05}</span>
{/if}
<span class="author-pubkey" title={event.pubkey}>{truncatePubkey(event.pubkey)}</span>
{:else}
<span class="author-pubkey">{truncatePubkey(event.pubkey)}</span>
{/if}
</div> </div>
<div class="events-view-kind"> <div class="events-view-kind">
<span <span
@ -387,6 +393,14 @@
border: 0; border: 0;
} }
.avatar-image {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
border: 1px solid var(--border-color);
}
.events-view-info { .events-view-info {
flex-shrink: 0; flex-shrink: 0;
min-width: 120px; min-width: 120px;
@ -396,6 +410,27 @@
font-weight: 600; font-weight: 600;
color: var(--text-color); color: var(--text-color);
font-size: 0.9em; font-size: 0.9em;
display: flex;
flex-direction: column;
gap: 0.2em;
}
.author-name {
font-weight: 600;
color: var(--text-color);
}
.author-nip05 {
font-size: 0.85em;
color: var(--text-color);
opacity: 0.8;
font-style: italic;
}
.author-pubkey {
font-size: 0.75em;
color: var(--text-color);
opacity: 0.6;
font-family: monospace; font-family: monospace;
} }

3
app/web/src/constants.js

@ -92,7 +92,8 @@ export const replaceableKinds = [
{ value: 30403, label: "Draft Classified Listing (30403)" }, { value: 30403, label: "Draft Classified Listing (30403)" },
{ value: 30617, label: "Repository announcements (30617)" }, { value: 30617, label: "Repository announcements (30617)" },
{ value: 30618, label: "Repository state announcements (30618)" }, { value: 30618, label: "Repository state announcements (30618)" },
{ value: 30818, label: "Wiki article (30818)" }, { value: 30817, label: "Wiki Article Markdown (30817)" },
{ value: 30818, label: "Wiki Article Asciidoc (30818)" },
{ value: 30819, label: "Redirects (30819)" }, { value: 30819, label: "Redirects (30819)" },
{ value: 31234, label: "Draft Event (31234)" }, { value: 31234, label: "Draft Event (31234)" },
{ value: 31388, label: "Link Set (31388)" }, { value: 31388, label: "Link Set (31388)" },

42
app/web/src/helpers.tsx

@ -56,6 +56,8 @@ export const KIND_NAMES = {
30023: "Long-form Content", 30023: "Long-form Content",
30024: "Draft Long-form Content", 30024: "Draft Long-form Content",
30030: "Emoji Sets", 30030: "Emoji Sets",
30040: "Curated Publication Index",
30041: "Curated Publication Content",
30063: "Release Artifact Sets", 30063: "Release Artifact Sets",
30078: "Application-specific Data", 30078: "Application-specific Data",
30311: "Live Event", 30311: "Live Event",
@ -65,7 +67,8 @@ export const KIND_NAMES = {
30403: "Draft Classified Listing", 30403: "Draft Classified Listing",
30617: "Repository Announcement", 30617: "Repository Announcement",
30618: "Repository State Announcement", 30618: "Repository State Announcement",
30818: "Wiki Article", 30817: "Wiki Article Markdown",
30818: "Wiki Article Asciidoc",
30819: "Redirects", 30819: "Redirects",
31922: "Date-Based Calendar Event", 31922: "Date-Based Calendar Event",
31923: "Time-Based Calendar Event", 31923: "Time-Based Calendar Event",
@ -83,7 +86,7 @@ export function getKindName(kind) {
} }
// Validate hex string (for pubkeys and event IDs) // Validate hex string (for pubkeys and event IDs)
export function isValidHex(str, length = null) { export function isValidHex(str: string | null | undefined, length: number | null = null): boolean {
if (!str || typeof str !== "string") return false; if (!str || typeof str !== "string") return false;
const hexRegex = /^[0-9a-fA-F]+$/; const hexRegex = /^[0-9a-fA-F]+$/;
if (!hexRegex.test(str)) return false; if (!hexRegex.test(str)) return false;
@ -92,12 +95,12 @@ export function isValidHex(str, length = null) {
} }
// Validate pubkey (64 character hex) // Validate pubkey (64 character hex)
export function isValidPubkey(pubkey) { export function isValidPubkey(pubkey: string | null | undefined): boolean {
return isValidHex(pubkey, 64); return isValidHex(pubkey, 64);
} }
// Validate event ID (64 character hex) // Validate event ID (64 character hex)
export function isValidEventId(eventId) { export function isValidEventId(eventId: string | null | undefined): boolean {
return isValidHex(eventId, 64); return isValidHex(eventId, 64);
} }
@ -140,6 +143,33 @@ export function truncateContent(content, maxLength = 100) {
} }
// Build Nostr filter from form data // Build Nostr filter from form data
interface FilterTag {
name: string;
value: string;
}
interface FilterOptions {
searchText?: string | null;
kinds?: number[];
authors?: string[];
ids?: string[];
tags?: FilterTag[];
since?: number | null;
until?: number | null;
limit?: number | null;
}
interface NostrFilter {
search?: string;
kinds?: number[];
authors?: string[];
ids?: string[];
[key: string]: any; // For dynamic tag keys like #e, #p, #a
since?: number;
until?: number;
limit?: number;
}
export function buildFilter({ export function buildFilter({
searchText = null, searchText = null,
kinds = [], kinds = [],
@ -149,8 +179,8 @@ export function buildFilter({
since = null, since = null,
until = null, until = null,
limit = null, limit = null,
}) { }: FilterOptions = {}): NostrFilter {
const filter = {}; const filter: NostrFilter = {};
if (searchText && searchText.trim()) { if (searchText && searchText.trim()) {
filter.search = searchText.trim(); filter.search = searchText.trim();

Loading…
Cancel
Save