mirror of
https://github.com/hoornet/vega.git
synced 2026-05-08 05:09:12 -07:00
Bump to v0.7.0 — writer tools, NIP-98 uploads, multi-draft, article bookmarks
- NIP-98 HTTP Auth for image uploads with fallback services (void.cat, nostrimg.com) - Markdown toolbar (bold, italic, heading, link, image, quote, code, list) + Ctrl+B/I/K - Multi-draft management with draft list, resume, delete, auto-migrate - Cover image file picker in article meta panel - Article bookmarks via NIP-51 'a' tags; Notes/Articles tabs in BookmarkView - Removed Rust upload_file command; dropped reqwest/mime_guess deps - Upload spinner, draft count badge, empty states
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { create } from "zustand";
|
||||
import { fetchBookmarkList, publishBookmarkList } from "../lib/nostr";
|
||||
import { fetchBookmarkList, fetchBookmarkListFull, publishBookmarkListFull } from "../lib/nostr";
|
||||
|
||||
const STORAGE_KEY = "wrystr_bookmarks";
|
||||
const ARTICLE_STORAGE_KEY = "wrystr_bookmarks_articles";
|
||||
|
||||
function loadLocal(): string[] {
|
||||
try {
|
||||
@@ -15,47 +16,96 @@ function saveLocal(ids: string[]) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(ids));
|
||||
}
|
||||
|
||||
function loadArticleAddrs(): string[] {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(ARTICLE_STORAGE_KEY) ?? "[]");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveArticleAddrs(addrs: string[]) {
|
||||
localStorage.setItem(ARTICLE_STORAGE_KEY, JSON.stringify(addrs));
|
||||
}
|
||||
|
||||
interface BookmarkState {
|
||||
bookmarkedIds: string[];
|
||||
bookmarkedArticleAddrs: string[]; // "30023:<pubkey>:<d-tag>" format
|
||||
fetchBookmarks: (pubkey: string) => Promise<void>;
|
||||
addBookmark: (eventId: string) => Promise<void>;
|
||||
removeBookmark: (eventId: string) => Promise<void>;
|
||||
isBookmarked: (eventId: string) => boolean;
|
||||
addArticleBookmark: (addr: string) => Promise<void>;
|
||||
removeArticleBookmark: (addr: string) => Promise<void>;
|
||||
isArticleBookmarked: (addr: string) => boolean;
|
||||
}
|
||||
|
||||
export const useBookmarkStore = create<BookmarkState>((set, get) => ({
|
||||
bookmarkedIds: loadLocal(),
|
||||
bookmarkedArticleAddrs: loadArticleAddrs(),
|
||||
|
||||
fetchBookmarks: async (pubkey: string) => {
|
||||
try {
|
||||
const ids = await fetchBookmarkList(pubkey);
|
||||
if (ids.length === 0) return;
|
||||
const local = get().bookmarkedIds;
|
||||
const merged = Array.from(new Set([...ids, ...local]));
|
||||
set({ bookmarkedIds: merged });
|
||||
saveLocal(merged);
|
||||
const { eventIds, articleAddrs } = await fetchBookmarkListFull(pubkey);
|
||||
const localIds = get().bookmarkedIds;
|
||||
const localAddrs = get().bookmarkedArticleAddrs;
|
||||
const mergedIds = Array.from(new Set([...eventIds, ...localIds]));
|
||||
const mergedAddrs = Array.from(new Set([...articleAddrs, ...localAddrs]));
|
||||
set({ bookmarkedIds: mergedIds, bookmarkedArticleAddrs: mergedAddrs });
|
||||
saveLocal(mergedIds);
|
||||
saveArticleAddrs(mergedAddrs);
|
||||
} catch {
|
||||
// Non-critical — local bookmarks still work
|
||||
// Fallback to old format
|
||||
try {
|
||||
const ids = await fetchBookmarkList(pubkey);
|
||||
if (ids.length === 0) return;
|
||||
const local = get().bookmarkedIds;
|
||||
const merged = Array.from(new Set([...ids, ...local]));
|
||||
set({ bookmarkedIds: merged });
|
||||
saveLocal(merged);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
},
|
||||
|
||||
addBookmark: async (eventId: string) => {
|
||||
const { bookmarkedIds } = get();
|
||||
const { bookmarkedIds, bookmarkedArticleAddrs } = get();
|
||||
if (bookmarkedIds.includes(eventId)) return;
|
||||
const updated = [...bookmarkedIds, eventId];
|
||||
set({ bookmarkedIds: updated });
|
||||
saveLocal(updated);
|
||||
publishBookmarkList(updated).catch(() => {});
|
||||
publishBookmarkListFull(updated, bookmarkedArticleAddrs).catch(() => {});
|
||||
},
|
||||
|
||||
removeBookmark: async (eventId: string) => {
|
||||
const { bookmarkedArticleAddrs } = get();
|
||||
const updated = get().bookmarkedIds.filter((id) => id !== eventId);
|
||||
set({ bookmarkedIds: updated });
|
||||
saveLocal(updated);
|
||||
publishBookmarkList(updated).catch(() => {});
|
||||
publishBookmarkListFull(updated, bookmarkedArticleAddrs).catch(() => {});
|
||||
},
|
||||
|
||||
isBookmarked: (eventId: string) => {
|
||||
return get().bookmarkedIds.includes(eventId);
|
||||
},
|
||||
|
||||
addArticleBookmark: async (addr: string) => {
|
||||
const { bookmarkedIds, bookmarkedArticleAddrs } = get();
|
||||
if (bookmarkedArticleAddrs.includes(addr)) return;
|
||||
const updated = [...bookmarkedArticleAddrs, addr];
|
||||
set({ bookmarkedArticleAddrs: updated });
|
||||
saveArticleAddrs(updated);
|
||||
publishBookmarkListFull(bookmarkedIds, updated).catch(() => {});
|
||||
},
|
||||
|
||||
removeArticleBookmark: async (addr: string) => {
|
||||
const { bookmarkedIds } = get();
|
||||
const updated = get().bookmarkedArticleAddrs.filter((a) => a !== addr);
|
||||
set({ bookmarkedArticleAddrs: updated });
|
||||
saveArticleAddrs(updated);
|
||||
publishBookmarkListFull(bookmarkedIds, updated).catch(() => {});
|
||||
},
|
||||
|
||||
isArticleBookmarked: (addr: string) => {
|
||||
return get().bookmarkedArticleAddrs.includes(addr);
|
||||
},
|
||||
}));
|
||||
|
||||
122
src/stores/drafts.ts
Normal file
122
src/stores/drafts.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
const STORAGE_KEY = "wrystr_article_drafts";
|
||||
const ACTIVE_KEY = "wrystr_active_draft";
|
||||
const OLD_DRAFT_KEY = "wrystr_article_draft";
|
||||
|
||||
export interface ArticleDraft {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
summary: string;
|
||||
image: string;
|
||||
tags: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
function generateId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
||||
}
|
||||
|
||||
function loadDrafts(): ArticleDraft[] {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) return JSON.parse(stored);
|
||||
} catch { /* ignore */ }
|
||||
|
||||
// Auto-migrate old single-draft format
|
||||
try {
|
||||
const old = localStorage.getItem(OLD_DRAFT_KEY);
|
||||
if (old) {
|
||||
const data = JSON.parse(old);
|
||||
if (data && (data.title || data.content)) {
|
||||
const migrated: ArticleDraft = {
|
||||
id: generateId(),
|
||||
title: data.title || "",
|
||||
content: data.content || "",
|
||||
summary: data.summary || "",
|
||||
image: data.image || "",
|
||||
tags: data.tags || "",
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify([migrated]));
|
||||
localStorage.removeItem(OLD_DRAFT_KEY);
|
||||
return [migrated];
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function saveDrafts(drafts: ArticleDraft[]) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts));
|
||||
}
|
||||
|
||||
function loadActiveDraftId(): string | null {
|
||||
return localStorage.getItem(ACTIVE_KEY) || null;
|
||||
}
|
||||
|
||||
function saveActiveDraftId(id: string | null) {
|
||||
if (id) {
|
||||
localStorage.setItem(ACTIVE_KEY, id);
|
||||
} else {
|
||||
localStorage.removeItem(ACTIVE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
interface DraftState {
|
||||
drafts: ArticleDraft[];
|
||||
activeDraftId: string | null;
|
||||
createDraft: () => string;
|
||||
updateDraft: (id: string, fields: Partial<Pick<ArticleDraft, "title" | "content" | "summary" | "image" | "tags">>) => void;
|
||||
deleteDraft: (id: string) => void;
|
||||
setActiveDraft: (id: string | null) => void;
|
||||
}
|
||||
|
||||
export const useDraftStore = create<DraftState>((set, get) => ({
|
||||
drafts: loadDrafts(),
|
||||
activeDraftId: loadActiveDraftId(),
|
||||
|
||||
createDraft: () => {
|
||||
const id = generateId();
|
||||
const draft: ArticleDraft = {
|
||||
id,
|
||||
title: "",
|
||||
content: "",
|
||||
summary: "",
|
||||
image: "",
|
||||
tags: "",
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
const updated = [draft, ...get().drafts];
|
||||
set({ drafts: updated, activeDraftId: id });
|
||||
saveDrafts(updated);
|
||||
saveActiveDraftId(id);
|
||||
return id;
|
||||
},
|
||||
|
||||
updateDraft: (id, fields) => {
|
||||
const updated = get().drafts.map((d) =>
|
||||
d.id === id ? { ...d, ...fields, updatedAt: Date.now() } : d
|
||||
);
|
||||
set({ drafts: updated });
|
||||
saveDrafts(updated);
|
||||
},
|
||||
|
||||
deleteDraft: (id) => {
|
||||
const updated = get().drafts.filter((d) => d.id !== id);
|
||||
const activeId = get().activeDraftId === id ? null : get().activeDraftId;
|
||||
set({ drafts: updated, activeDraftId: activeId });
|
||||
saveDrafts(updated);
|
||||
saveActiveDraftId(activeId);
|
||||
},
|
||||
|
||||
setActiveDraft: (id) => {
|
||||
set({ activeDraftId: id });
|
||||
saveActiveDraftId(id);
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user