From c65ddb1c26a035a1bde04f12d6cc1aa97aa1e74f Mon Sep 17 00:00:00 2001 From: Jure <44338+hoornet@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:32:57 +0100 Subject: [PATCH] Fix test bugs: mute filtering, notification toggles, external links, emoji picker - Fix mute button having no effect in Media Feed (missing filter) - Fix notification toggle switches overlapping labels (sizing + shrink-0) - Fix external links not opening in system browser (use Tauri opener plugin) - Fix trending refresh showing no visual feedback (clear list on force refresh) - Fix emoji reactions inaccessible behind WebKitGTK context menu (visible + button) - Add emoji picker to compose box, inline reply, and thread reply - New shared EmojiPicker component with categorized emoji groups --- src/App.tsx | 21 ++++++++++- src/components/feed/ComposeBox.tsx | 31 +++++++++++---- src/components/feed/NoteCard.tsx | 39 ++++++++++++++++++- src/components/media/MediaFeed.tsx | 7 +++- src/components/shared/EmojiPicker.tsx | 52 ++++++++++++++++++++++++++ src/components/shared/SettingsView.tsx | 8 ++-- src/components/thread/ThreadView.tsx | 27 +++++++++++++ src/stores/feed.ts | 2 +- 8 files changed, 170 insertions(+), 17 deletions(-) create mode 100644 src/components/shared/EmojiPicker.tsx diff --git a/src/App.tsx b/src/App.tsx index 4e60b2a..011835b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { openUrl } from "@tauri-apps/plugin-opener"; import { Sidebar } from "./components/sidebar/Sidebar"; import { Feed } from "./components/feed/Feed"; import { SearchView } from "./components/search/SearchView"; @@ -55,6 +56,24 @@ function App() { useKeyboardShortcuts(); + // Intercept external link clicks and open in system browser via Tauri opener + useEffect(() => { + const handler = (e: MouseEvent) => { + const anchor = (e.target as HTMLElement).closest("a[href]") as HTMLAnchorElement | null; + if (!anchor) return; + const href = anchor.getAttribute("href"); + if (!href) return; + // Only intercept external http(s) links + if (href.startsWith("http://") || href.startsWith("https://")) { + e.preventDefault(); + e.stopPropagation(); + openUrl(href).catch(() => {}); + } + }; + document.addEventListener("click", handler, true); + return () => document.removeEventListener("click", handler, true); + }, []); + if (!onboardingDone) { return setOnboardingDone(true)} />; } diff --git a/src/components/feed/ComposeBox.tsx b/src/components/feed/ComposeBox.tsx index 8f04b77..14bc9a2 100644 --- a/src/components/feed/ComposeBox.tsx +++ b/src/components/feed/ComposeBox.tsx @@ -6,6 +6,7 @@ import { useFeedStore } from "../../stores/feed"; import { shortenPubkey } from "../../lib/utils"; import { open } from "@tauri-apps/plugin-dialog"; import { readFile } from "@tauri-apps/plugin-fs"; +import { EmojiPicker } from "../shared/EmojiPicker"; const COMPOSE_DRAFT_KEY = "wrystr_compose_draft"; @@ -17,6 +18,7 @@ export function ComposeBox({ onPublished, onNoteInjected }: { onPublished?: () = const [publishing, setPublishing] = useState(false); const [uploading, setUploading] = useState(false); const [error, setError] = useState(null); + const [showEmoji, setShowEmoji] = useState(false); const textareaRef = useRef(null); const { profile, npub } = useUserStore(); @@ -40,20 +42,20 @@ export function ComposeBox({ onPublished, onNoteInjected }: { onPublished?: () = const overLimit = charCount > 4000; const canPost = text.trim().length > 0 && !publishing && !uploading; - // Insert a URL at the current cursor position in the textarea - const insertUrl = (url: string) => { + // Insert text at the current cursor position in the textarea + const insertAtCursor = (str: string) => { const ta = textareaRef.current; if (ta) { const start = ta.selectionStart ?? text.length; const end = ta.selectionEnd ?? text.length; - const next = text.slice(0, start) + url + text.slice(end); + const next = text.slice(0, start) + str + text.slice(end); setText(next); setTimeout(() => { - ta.selectionStart = ta.selectionEnd = start + url.length; + ta.selectionStart = ta.selectionEnd = start + str.length; ta.focus(); }, 0); } else { - setText((t) => t + url); + setText((t) => t + str); } }; @@ -63,7 +65,7 @@ export function ComposeBox({ onPublished, onNoteInjected }: { onPublished?: () = setError(null); try { const url = await uploadImage(file); - insertUrl(url); + insertAtCursor(url); } catch (err) { setError(`Image upload failed: ${err}`); } finally { @@ -86,7 +88,7 @@ export function ComposeBox({ onPublished, onNoteInjected }: { onPublished?: () = }; const mimeType = mimeMap[ext] || "application/octet-stream"; const url = await uploadBytes(new Uint8Array(bytes), fileName, mimeType); - insertUrl(url); + insertAtCursor(url); } catch (err) { setError(`Upload failed: ${err}`); } finally { @@ -238,6 +240,21 @@ export function ComposeBox({ onPublished, onNoteInjected }: { onPublished?: () = )}
+
+ + {showEmoji && ( + insertAtCursor(emoji)} + onClose={() => setShowEmoji(false)} + /> + )} +
-
+
+ {!liked && !liking && ( + + )} {showEmojiPicker && ( <>
setShowEmojiPicker(false)} /> @@ -383,6 +393,31 @@ export function NoteCard({ event, focused }: NoteCardProps) { /> {replyError &&

{replyError}

}
+
+ + {showReplyEmoji && ( + { + const ta = replyRef.current; + if (ta) { + const start = ta.selectionStart ?? replyText.length; + const end = ta.selectionEnd ?? replyText.length; + setReplyText(replyText.slice(0, start) + emoji + replyText.slice(end)); + setTimeout(() => { ta.selectionStart = ta.selectionEnd = start + emoji.length; ta.focus(); }, 0); + } else { + setReplyText((t) => t + emoji); + } + }} + onClose={() => setShowReplyEmoji(false)} + /> + )} +
Ctrl+Enter + ))} +
+ {/* Emoji grid */} +
+ {EMOJI_GROUPS[group].emojis.map((emoji) => ( + + ))} +
+
+ + ); +} diff --git a/src/components/shared/SettingsView.tsx b/src/components/shared/SettingsView.tsx index 5a20fc9..ac44273 100644 --- a/src/components/shared/SettingsView.tsx +++ b/src/components/shared/SettingsView.tsx @@ -360,16 +360,16 @@ function NotificationSection() {

{items.map(({ key, label }) => ( -