wip: render hashtags correctly

This commit is contained in:
Alejandro Gómez
2025-12-10 13:59:26 +01:00
parent 0e59c288ff
commit c6e3325720
7 changed files with 57 additions and 46 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, Activity } from "react";
import { useState, useEffect } from "react";
import { useGrimoire } from "@/core/state";
import { useAccountSync } from "@/hooks/useAccountSync";
import Feed from "./nostr/Feed";
@@ -141,21 +141,16 @@ export default function Home() {
<UserMenu />
</header>
<section className="flex-1 relative overflow-hidden">
{Object.values(state.workspaces).map((workspace) => (
<Activity
key={workspace.id}
mode={
workspace.id === state.activeWorkspaceId ? "visible" : "hidden"
}
>
{workspace.layout === null ? (
{state.workspaces[state.activeWorkspaceId] && (
<>
{state.workspaces[state.activeWorkspaceId].layout === null ? (
<GrimoireWelcome
onLaunchCommand={() => setCommandLauncherOpen(true)}
/>
) : (
<Mosaic
renderTile={renderTile}
value={workspace.layout}
value={state.workspaces[state.activeWorkspaceId].layout}
onChange={updateLayout}
onRelease={(node) => {
// When Mosaic removes a node from the layout, clean up the window
@@ -166,8 +161,8 @@ export default function Home() {
className="mosaic-blueprint-theme"
/>
)}
</Activity>
))}
</>
)}
</section>
<TabBar />
</main>

View File

@@ -5,13 +5,5 @@ interface HashtagNodeProps {
}
export function Hashtag({ node }: HashtagNodeProps) {
return (
<a
href={`/t/${node.hashtag}`}
className="text-muted-foreground hover:text-primary cursor-crosshair"
onClick={(e) => e.preventDefault()}
>
#{node.hashtag}
</a>
);
return <span className="text-muted-foreground">#{node.hashtag}</span>;
}

View File

@@ -12,23 +12,24 @@ function hasRTLCharacters(text: string): boolean {
export function Text({ node }: TextNodeProps) {
const text = node.value;
// If no newlines, render as inline span
if (!text.includes("\n")) {
const isRTL = hasRTLCharacters(text);
return <span dir={isRTL ? "rtl" : "auto"}>{text || "\u00A0"}</span>;
}
// If has newlines, use divs for proper RTL per line
// If has newlines, use spans with <br> tags
const lines = text.split("\n");
return (
<>
{lines.map((line, idx) => {
const isRTL = hasRTLCharacters(line);
return (
<div key={idx} dir={isRTL ? "rtl" : "auto"}>
{line || "\u00A0"}
</div>
<span key={idx}>
<span dir={isRTL ? "rtl" : "auto"}>{line || "\u00A0"}</span>
{idx < lines.length - 1 && <br />}
</span>
);
})}
</>

View File

@@ -30,7 +30,7 @@ export function EventAuthor({ pubkey }: { pubkey: string }) {
<div className="flex flex-col gap-0">
<UserName
pubkey={pubkey}
className="text-md cursor-crosshair font-semibold hover:underline hover:decoration-dotted"
className="text-md text-accent cursor-crosshair font-semibold hover:underline hover:decoration-dotted"
/>
</div>
);

View File

@@ -1,5 +1,5 @@
import { useEffect } from "react";
import { useObservableMemo, useEventStore } from "applesauce-react/hooks";
import { useEventStore, useObservableMemo } from "applesauce-react/hooks";
import accounts from "@/services/accounts";
import { useGrimoire } from "@/core/state";
import { getInboxes, getOutboxes } from "applesauce-core/helpers";

View File

@@ -1,32 +1,54 @@
import { useEffect } from "react";
import { kinds } from "nostr-tools";
import { useState, useEffect } from "react";
import { profileLoader } from "@/services/loaders";
import { useEventStore, useObservableMemo } from "applesauce-react/hooks";
import { ProfileContent } from "applesauce-core/helpers";
import { ProfileModel } from "applesauce-core/models/profile";
import { kinds } from "nostr-tools";
import db from "@/services/db";
export function useProfile(pubkey: string): ProfileContent | undefined {
const eventStore = useEventStore();
const [profile, setProfile] = useState<ProfileContent | undefined>();
const profile = useObservableMemo(
() => eventStore.model(ProfileModel, pubkey),
[eventStore, pubkey],
);
// Fetch profile if not in store (only runs once per pubkey)
useEffect(() => {
if (profile) return; // Already have the event
let mounted = true;
// Load from IndexedDB first
db.profiles.get(pubkey).then((cachedProfile) => {
if (mounted && cachedProfile) {
setProfile(cachedProfile);
}
});
// Fetch from network
const sub = profileLoader({ kind: kinds.Metadata, pubkey }).subscribe({
next: (fetchedEvent) => {
if (fetchedEvent) {
eventStore.add(fetchedEvent);
next: async (fetchedEvent) => {
if (!fetchedEvent || !fetchedEvent.content) return;
try {
const profileData = JSON.parse(fetchedEvent.content) as ProfileContent;
// Save to IndexedDB
await db.profiles.put({
...profileData,
pubkey,
created_at: fetchedEvent.created_at,
});
if (mounted) {
setProfile(profileData);
}
} catch (e) {
console.error("[useProfile] Failed to parse profile:", e);
}
},
error: (err) => {
console.error("[useProfile] Error fetching profile:", err);
},
});
return () => sub.unsubscribe();
}, [pubkey, eventStore]); // Removed event and loading from deps
return () => {
mounted = false;
sub.unsubscribe();
};
}, [pubkey]);
return profile;
}

View File

@@ -2,6 +2,7 @@ import { ProfileContent } from "applesauce-core/helpers";
import { Dexie, Table } from "dexie";
export interface Profile extends ProfileContent {
pubkey: string;
created_at: number;
}