Fix desktop compose to open modal, cache right sidebar data, render vine videos on detail page

This commit is contained in:
Chad Curtis
2026-02-19 16:54:16 -06:00
parent f125abeef8
commit 95c3a5accc
3 changed files with 77 additions and 11 deletions
+11 -8
View File
@@ -10,6 +10,7 @@ import { MewLogo } from '@/components/MewLogo';
import { ProfileSearchDropdown } from '@/components/ProfileSearchDropdown';
import LoginDialog from '@/components/auth/LoginDialog';
import SignupDialog from '@/components/auth/SignupDialog';
import { ReplyComposeModal } from '@/components/ReplyComposeModal';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useLoggedInAccounts, type Account } from '@/hooks/useLoggedInAccounts';
import { useLoginActions } from '@/hooks/useLoginActions';
@@ -62,6 +63,7 @@ export function LeftSidebar() {
const [loginDialogOpen, setLoginDialogOpen] = useState(false);
const [signupDialogOpen, setSignupDialogOpen] = useState(false);
const [accountPopoverOpen, setAccountPopoverOpen] = useState(false);
const [composeOpen, setComposeOpen] = useState(false);
/** Map route name → lucide icon (size-6 for sidebar). */
const ROUTE_ICONS: Record<string, React.ReactNode> = {
@@ -158,14 +160,15 @@ export function LeftSidebar() {
{/* Compose/Join button */}
{user ? (
<Button
className="w-full mt-4 rounded-full h-12 text-base font-bold bg-primary hover:bg-primary/90 text-primary-foreground"
onClick={() => {
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
>
<span>Compose</span>
</Button>
<>
<Button
className="w-full mt-4 rounded-full h-12 text-base font-bold bg-primary hover:bg-primary/90 text-primary-foreground"
onClick={() => setComposeOpen(true)}
>
<span>Compose</span>
</Button>
<ReplyComposeModal open={composeOpen} onOpenChange={setComposeOpen} />
</>
) : (
<Button
className="w-full mt-4 rounded-full h-12 text-base font-bold bg-primary hover:bg-primary/90 text-primary-foreground"
+12 -3
View File
@@ -65,14 +65,23 @@ export function TrendSparkline({ data }: { data: number[] }) {
);
}
/** Track whether the sidebar has already loaded data at least once. */
let hasLoadedOnce = false;
export function RightSidebar() {
const isXl = useIsXl();
// Delay sidebar data loading to prioritize feed performance
const [sidebarEnabled, setSidebarEnabled] = useState(false);
// Delay sidebar data loading only on the very first mount to prioritize
// initial feed performance. On subsequent mounts (page navigations) skip
// the delay since TanStack Query will serve cached data instantly.
const [sidebarEnabled, setSidebarEnabled] = useState(hasLoadedOnce);
useEffect(() => {
const timer = setTimeout(() => setSidebarEnabled(true), 3000);
if (hasLoadedOnce) return;
const timer = setTimeout(() => {
hasLoadedOnce = true;
setSidebarEnabled(true);
}, 3000);
return () => clearTimeout(timer);
}, []);
+54
View File
@@ -103,6 +103,28 @@ function parseImetaMap(tags: string[][]): Map<string, ImetaEntry> {
return map;
}
/** Get the first value for a tag name. */
function getTag(tags: string[][], name: string): string | undefined {
return tags.find(([n]) => n === name)?.[1];
}
/** Parse single imeta tag into structured object (for kind 34236 vines). */
function parseImeta(tags: string[][]): { url?: string; thumbnail?: string } {
const imetaTag = tags.find(([name]) => name === 'imeta');
if (!imetaTag) return {};
const result: Record<string, string> = {};
for (let i = 1; i < imetaTag.length; i++) {
const part = imetaTag[i];
const spaceIdx = part.indexOf(' ');
if (spaceIdx === -1) continue;
const key = part.slice(0, spaceIdx);
const value = part.slice(spaceIdx + 1);
if (key === 'url') result.url = value;
else if (key === 'image') result.thumbnail = value;
}
return result;
}
/** Formats a timestamp into a full date string like "Feb 16, 2026, 2:53 PM". */
function formatFullDate(timestamp: number): string {
const date = new Date(timestamp * 1000);
@@ -543,6 +565,37 @@ function EventNotFound({
);
}
/** Video + title + hashtags for a kind 34236 vine on the detail page. */
function VineDetailContent({ event }: { event: NostrEvent }) {
const imeta = useMemo(() => parseImeta(event.tags), [event.tags]);
const vineTitle = getTag(event.tags, 'title');
const hashtags = event.tags.filter(([n]) => n === 't').map(([, v]) => v);
return (
<div className="mt-3">
{vineTitle && (
<p className="text-[15px] leading-relaxed break-words mb-2">{vineTitle}</p>
)}
{imeta.url && (
<VideoPlayer src={imeta.url} poster={imeta.thumbnail} />
)}
{hashtags.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-2">
{hashtags.slice(0, 8).map((tag) => (
<Link
key={tag}
to={`/t/${encodeURIComponent(tag)}`}
className="text-sm text-primary hover:underline"
>
#{tag}
</Link>
))}
</div>
)}
</div>
);
}
function PostDetailContent({ event }: { event: NostrEvent }) {
const { user } = useCurrentUser();
const author = useAuthor(event.pubkey);
@@ -640,6 +693,7 @@ function PostDetailContent({ event }: { event: NostrEvent }) {
{/* Post content — kind-based dispatch (same as NoteCard) */}
{isVine || isPoll || isGeocache || isFoundLog || isColor || isFollowPack ? (
<>
{isVine && <VineDetailContent event={event} />}
{isPoll && <PollContent event={event} />}
{isGeocache && <GeocacheContent event={event} />}
{isFoundLog && <FoundLogContent event={event} />}