mirror of
https://github.com/hoornet/vega.git
synced 2026-05-06 20:29:12 -07:00
Bump to v0.4.0 — Phase 3: image lightbox, bookmarks, discover, language filter, UI polish
This commit is contained in:
@@ -4,15 +4,17 @@ import { useUserStore } from "../../stores/user";
|
||||
import { useMuteStore } from "../../stores/mute";
|
||||
import { useUIStore } from "../../stores/ui";
|
||||
import { fetchFollowFeed, getNDK } from "../../lib/nostr";
|
||||
import { detectScript, getEventLanguageTag, FILTER_SCRIPTS } from "../../lib/language";
|
||||
import { NoteCard } from "./NoteCard";
|
||||
import { ComposeBox } from "./ComposeBox";
|
||||
import { SkeletonNoteList } from "../shared/Skeleton";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
|
||||
export function Feed() {
|
||||
const { notes, loading, connected, error, connect, loadCachedFeed, loadFeed, focusedNoteIndex } = useFeedStore();
|
||||
const { loggedIn, follows } = useUserStore();
|
||||
const { mutedPubkeys } = useMuteStore();
|
||||
const { feedTab: tab, setFeedTab: setTab } = useUIStore();
|
||||
const { feedTab: tab, setFeedTab: setTab, feedLanguageFilter, setFeedLanguageFilter } = useUIStore();
|
||||
const [followNotes, setFollowNotes] = useState<NDKEvent[]>([]);
|
||||
const [followLoading, setFollowLoading] = useState(false);
|
||||
|
||||
@@ -49,6 +51,29 @@ export function Feed() {
|
||||
// Filter out notes that look like base64 blobs or relay protocol messages
|
||||
if (c.length > 500 && /^[A-Za-z0-9+/=]{50,}$/.test(c.slice(0, 100))) return false;
|
||||
if (c.startsWith("nlogpost:") || c.startsWith("T1772")) return false;
|
||||
// Language/script filter
|
||||
if (feedLanguageFilter) {
|
||||
const langTag = getEventLanguageTag(event.tags);
|
||||
if (langTag) {
|
||||
// Map ISO-639-1 codes to script names for comparison
|
||||
const langToScript: Record<string, string> = {
|
||||
en: "Latin", es: "Latin", fr: "Latin", de: "Latin", pt: "Latin", it: "Latin", nl: "Latin", pl: "Latin", sv: "Latin", da: "Latin", no: "Latin", fi: "Latin", ro: "Latin", tr: "Latin", cs: "Latin", hr: "Latin", hu: "Latin",
|
||||
zh: "CJK", ja: "CJK",
|
||||
ko: "Korean",
|
||||
ru: "Cyrillic", uk: "Cyrillic", bg: "Cyrillic", sr: "Cyrillic",
|
||||
ar: "Arabic", fa: "Arabic", ur: "Arabic",
|
||||
hi: "Devanagari", mr: "Devanagari", ne: "Devanagari",
|
||||
th: "Thai",
|
||||
he: "Hebrew",
|
||||
el: "Greek",
|
||||
};
|
||||
const script = langToScript[langTag];
|
||||
if (script && script !== feedLanguageFilter) return false;
|
||||
} else {
|
||||
const script = detectScript(c);
|
||||
if (script !== feedLanguageFilter) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -81,6 +106,16 @@ export function Feed() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<select
|
||||
value={feedLanguageFilter ?? ""}
|
||||
onChange={(e) => setFeedLanguageFilter(e.target.value || null)}
|
||||
className="bg-transparent text-text-dim text-[11px] border border-border px-1.5 py-0.5 focus:outline-none hover:border-text-dim transition-colors cursor-pointer"
|
||||
>
|
||||
<option value="">all scripts</option>
|
||||
{FILTER_SCRIPTS.map((s) => (
|
||||
<option key={s} value={s}>{s.toLowerCase()}</option>
|
||||
))}
|
||||
</select>
|
||||
{connected && (
|
||||
<span className="text-success text-[11px] flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-success inline-block" />
|
||||
@@ -109,16 +144,25 @@ export function Feed() {
|
||||
)}
|
||||
|
||||
{isLoading && filteredNotes.length === 0 && (
|
||||
<div className="px-4 py-8 text-text-dim text-[12px] text-center">
|
||||
{isFollowing ? "Loading notes from people you follow…" : "Connecting to relays…"}
|
||||
</div>
|
||||
<SkeletonNoteList count={6} />
|
||||
)}
|
||||
|
||||
{!isLoading && filteredNotes.length === 0 && (
|
||||
<div className="px-4 py-8 text-text-dim text-[12px] text-center">
|
||||
{isFollowing && follows.length === 0
|
||||
? "You're not following anyone yet."
|
||||
: "No notes yet."}
|
||||
<div className="px-4 py-12 text-center space-y-2">
|
||||
<p className="text-text-dim text-[13px]">
|
||||
{isFollowing && follows.length === 0
|
||||
? "You're not following anyone yet."
|
||||
: feedLanguageFilter
|
||||
? `No ${feedLanguageFilter} notes found.`
|
||||
: "No notes to show."}
|
||||
</p>
|
||||
<p className="text-text-dim text-[11px] opacity-60">
|
||||
{isFollowing && follows.length === 0
|
||||
? "Use search to find people to follow."
|
||||
: feedLanguageFilter
|
||||
? "Try clearing the script filter or refreshing."
|
||||
: "Try refreshing or switching tabs."}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useReactionCount } from "../../hooks/useReactionCount";
|
||||
import { useZapCount } from "../../hooks/useZapCount";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useMuteStore } from "../../stores/mute";
|
||||
import { useBookmarkStore } from "../../stores/bookmark";
|
||||
import { useUIStore } from "../../stores/ui";
|
||||
import { timeAgo, shortenPubkey } from "../../lib/utils";
|
||||
import { publishReaction, publishReply, publishRepost, getNDK, fetchNoteById } from "../../lib/nostr";
|
||||
@@ -41,6 +42,8 @@ export function NoteCard({ event, focused }: NoteCardProps) {
|
||||
const { loggedIn, pubkey: ownPubkey } = useUserStore();
|
||||
const { mutedPubkeys, mute, unmute } = useMuteStore();
|
||||
const isMuted = mutedPubkeys.includes(event.pubkey);
|
||||
const { bookmarkedIds, addBookmark, removeBookmark } = useBookmarkStore();
|
||||
const isBookmarked = bookmarkedIds.includes(event.id!);
|
||||
const { openProfile, openThread, currentView } = useUIStore();
|
||||
|
||||
const parentEventId = getParentEventId(event);
|
||||
@@ -261,6 +264,14 @@ export function NoteCard({ event, focused }: NoteCardProps) {
|
||||
: "⚡ zap"}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => isBookmarked ? removeBookmark(event.id!) : addBookmark(event.id!)}
|
||||
className={`text-[11px] transition-colors ${
|
||||
isBookmarked ? "text-accent" : "text-text-dim hover:text-accent"
|
||||
}`}
|
||||
>
|
||||
{isBookmarked ? "▪ saved" : "▫ save"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useUIStore } from "../../stores/ui";
|
||||
import { fetchNoteById } from "../../lib/nostr";
|
||||
import { useProfile } from "../../hooks/useProfile";
|
||||
import { shortenPubkey } from "../../lib/utils";
|
||||
import { ImageLightbox } from "../shared/ImageLightbox";
|
||||
|
||||
// Regex patterns
|
||||
const URL_REGEX = /https?:\/\/[^\s<>"')\]]+/g;
|
||||
@@ -209,6 +210,7 @@ export function NoteContent({ content }: { content: string }) {
|
||||
const images: string[] = segments.filter((s) => s.type === "image").map((s) => s.value);
|
||||
const videos: string[] = segments.filter((s) => s.type === "video").map((s) => s.value);
|
||||
const quoteIds: string[] = segments.filter((s) => s.type === "quote").map((s) => s.value);
|
||||
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
|
||||
|
||||
const inlineElements: ReactNode[] = [];
|
||||
|
||||
@@ -284,7 +286,8 @@ export function NoteContent({ content }: { content: string }) {
|
||||
src={src}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
className="max-w-full max-h-80 rounded-sm object-cover bg-bg-raised border border-border"
|
||||
className="max-w-full max-h-80 rounded-sm object-cover bg-bg-raised border border-border cursor-zoom-in"
|
||||
onClick={(e) => { e.stopPropagation(); setLightboxIndex(i); }}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
@@ -293,6 +296,15 @@ export function NoteContent({ content }: { content: string }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lightboxIndex !== null && (
|
||||
<ImageLightbox
|
||||
images={images}
|
||||
index={lightboxIndex}
|
||||
onClose={() => setLightboxIndex(null)}
|
||||
onNavigate={setLightboxIndex}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Quoted notes */}
|
||||
{quoteIds.map((id) => (
|
||||
<QuotePreview key={id} eventId={id} />
|
||||
|
||||
Reference in New Issue
Block a user