diff --git a/app/[leaflet_id]/publish/BskyPostEditorProsemirror.tsx b/app/[leaflet_id]/publish/BskyPostEditorProsemirror.tsx
index 204e2dea..26a74252 100644
--- a/app/[leaflet_id]/publish/BskyPostEditorProsemirror.tsx
+++ b/app/[leaflet_id]/publish/BskyPostEditorProsemirror.tsx
@@ -435,9 +435,10 @@ export const addMentionToEditor = (
const atMentionNode = schema.nodes.atMention.create({
atURI: mention.uri,
text,
- ...(mention.type === "service_result" && mention.href
- ? { href: mention.href }
- : {}),
+ ...(mention.type === "service_result" && {
+ href: mention.href,
+ icon: mention.icon,
+ }),
});
tr.insert(from, atMentionNode);
}
diff --git a/app/api/rpc/[command]/proxy_mention_search.ts b/app/api/rpc/[command]/proxy_mention_search.ts
index 94deba53..a0d7538b 100644
--- a/app/api/rpc/[command]/proxy_mention_search.ts
+++ b/app/api/rpc/[command]/proxy_mention_search.ts
@@ -67,6 +67,7 @@ export const proxy_mention_search = makeRoute({
uri: String(r.uri || ""),
name: String(r.name || ""),
href: r.href ? String(r.href) : undefined,
+ icon: r.icon ? String(r.icon) : undefined,
})),
},
};
diff --git a/components/AtMentionLink.tsx b/components/AtMentionLink.tsx
index cf3028fe..fb23f93c 100644
--- a/components/AtMentionLink.tsx
+++ b/components/AtMentionLink.tsx
@@ -8,28 +8,34 @@ import { atUriToUrl, classifyAtUri } from "src/utils/mentionUtils";
export function AtMentionLink({
atURI,
href,
+ icon: iconUrl,
children,
className = "",
}: {
atURI: string;
href?: string;
+ icon?: string;
children: React.ReactNode;
className?: string;
}) {
const { isPublication, isDocument } = classifyAtUri(atURI);
- // Show publication icon if available
- const icon =
- isPublication || isDocument ? (
-
- ) : null;
+ // Show publication icon, or service-provided icon
+ const iconSrc =
+ isPublication || isDocument
+ ? `/api/pub_icon?at_uri=${encodeURIComponent(atURI)}`
+ : iconUrl ?? null;
+
+ const icon = iconSrc ? (
+
+ ) : null;
const linkHref = href || atUriToUrl(atURI);
diff --git a/components/Blocks/TextBlock/RenderYJSFragment.tsx b/components/Blocks/TextBlock/RenderYJSFragment.tsx
index 57578255..d277d17e 100644
--- a/components/Blocks/TextBlock/RenderYJSFragment.tsx
+++ b/components/Blocks/TextBlock/RenderYJSFragment.tsx
@@ -114,8 +114,9 @@ export function RenderYJSFragment({
const atURI = node.getAttribute("atURI") || "";
const text = node.getAttribute("text") || "";
const href = node.getAttribute("href") || undefined;
+ const icon = node.getAttribute("icon") || undefined;
return (
-
+
{text}
);
diff --git a/components/Blocks/TextBlock/schema.ts b/components/Blocks/TextBlock/schema.ts
index a9ade1b8..853b0518 100644
--- a/components/Blocks/TextBlock/schema.ts
+++ b/components/Blocks/TextBlock/schema.ts
@@ -128,6 +128,7 @@ let baseSchema = {
atURI: {},
text: { default: "" },
href: { default: undefined },
+ icon: { default: undefined },
},
group: "inline",
inline: true,
@@ -142,6 +143,7 @@ let baseSchema = {
atURI: dom.getAttribute("data-at-uri"),
text: dom.textContent || "",
href: dom.getAttribute("data-href") || undefined,
+ icon: dom.getAttribute("data-icon") || undefined,
};
},
},
@@ -161,16 +163,24 @@ let baseSchema = {
if (node.attrs.href) {
attrs["data-href"] = node.attrs.href;
}
+ if (node.attrs.icon) {
+ attrs["data-icon"] = node.attrs.icon;
+ }
+
+ // Show icon for publications/documents or service results
+ const iconSrc =
+ isPublication || isDocument
+ ? `/api/pub_icon?at_uri=${encodeURIComponent(node.attrs.atURI)}`
+ : node.attrs.icon ?? null;
- // For publications and documents, show icon
- if (isPublication || isDocument) {
+ if (iconSrc) {
return [
"span",
attrs,
[
"img",
{
- src: `/api/pub_icon?at_uri=${encodeURIComponent(node.attrs.atURI)}`,
+ src: iconSrc,
class:
"inline-block w-4 h-4 rounded-full mt-[3px] mr-1 align-text-top",
alt: "",
diff --git a/components/Mention.tsx b/components/Mention.tsx
index c22e29e3..5d97e3ff 100644
--- a/components/Mention.tsx
+++ b/components/Mention.tsx
@@ -323,6 +323,7 @@ export function MentionAutocomplete(props: {
}}
onMouseDown={(e) => e.preventDefault()}
name={result.name}
+ icon={result.icon}
selected={index === suggestionIndex}
/>
) : (
@@ -507,12 +508,22 @@ const ServiceEntry = (props: {
const ServiceSearchResult = (props: {
name: string;
+ icon?: string;
onClick: () => void;
onMouseDown: (e: React.MouseEvent) => void;
selected?: boolean;
}) => {
return (
+ ) : undefined
+ }
result={{props.name}
}
onClick={props.onClick}
onMouseDown={props.onMouseDown}
@@ -569,6 +580,7 @@ export type Mention =
uri: string;
name: string;
href?: string;
+ icon?: string;
};
export type MentionScope =
@@ -690,19 +702,22 @@ function useMentionSuggestions(query: string | null, open: boolean) {
useEffect(() => {
let stale = false;
- // Only skip debounce for purely local operations (showing service list)
- const delay =
- !query && hasServices && scope.type === "default" ? 0 : 300;
+ // Default scope with services: show local filter instantly, debounce network fallback
+ if (hasServices && scope.type === "default") {
+ const filtered = allServices.filter((s) =>
+ s.type === "service"
+ ? s.name.toLowerCase().includes((query || "").toLowerCase())
+ : true,
+ );
+ setSuggestions(filtered);
+
+ // If local filter found matches, no need for network search
+ if (!query || filtered.length > 0) return;
+ }
const handler = setTimeout(async () => {
if (stale || !open) return;
- if (!query && scope.type === "default") {
- // No query: show services if available, otherwise clear (sync, no race)
- setSuggestions(hasServices ? allServices : []);
- return;
- }
-
let results: Array;
if (scope.type === "identities") {
@@ -716,7 +731,7 @@ function useMentionSuggestions(query: string | null, open: boolean) {
query: query || "",
limit: 10,
});
- results = documents.result.documents.map((d) => ({
+ results = (documents?.result?.documents ?? []).map((d) => ({
type: "post" as const,
uri: d.uri,
title: d.title,
@@ -728,26 +743,19 @@ function useMentionSuggestions(query: string | null, open: boolean) {
service_uri: scope.serviceUri,
search: query || "",
});
- results = res.result.results.map(
- (r: { uri: string; name: string; href?: string }) => ({
+ const items = res?.result?.results ?? [];
+ results = items.map(
+ (r: { uri: string; name: string; href?: string; icon?: string }) => ({
type: "service_result" as const,
uri: r.uri,
name: r.name,
href: r.href,
+ icon: r.icon,
}),
);
} else if (hasServices) {
- // Default scope with services: filter locally, fall back to identity search
- const filtered = allServices.filter((s) =>
- s.type === "service"
- ? s.name.toLowerCase().includes((query || "").toLowerCase())
- : true,
- );
- if (filtered.length > 0) {
- results = filtered;
- } else {
- results = await searchIdentities(query || "", 10);
- }
+ // Default scope with services: local filter showed no matches, fall back to identity search
+ results = await searchIdentities(query || "", 10);
} else {
// Default scope, no services: search people & publications together
const [identities, publications] = await Promise.all([
@@ -760,7 +768,7 @@ function useMentionSuggestions(query: string | null, open: boolean) {
if (!stale) {
setSuggestions(results);
}
- }, delay);
+ }, 300);
return () => {
stale = true;
diff --git a/lexicons/parts/page/mention/searchService.json b/lexicons/parts/page/mention/searchService.json
index f62e562f..2779701b 100644
--- a/lexicons/parts/page/mention/searchService.json
+++ b/lexicons/parts/page/mention/searchService.json
@@ -69,6 +69,11 @@
"type": "string",
"format": "uri",
"description": "Optional web URL for the mentioned entity"
+ },
+ "icon": {
+ "type": "string",
+ "format": "uri",
+ "description": "Optional icon URL for the mentioned entity, displayed next to the mention"
}
}
}
diff --git a/lexicons/src/mentionService.ts b/lexicons/src/mentionService.ts
index 46466598..6069280d 100644
--- a/lexicons/src/mentionService.ts
+++ b/lexicons/src/mentionService.ts
@@ -90,6 +90,12 @@ export const PagePartsMentionSearchService: LexiconDoc = {
format: "uri",
description: "Optional web URL for the mentioned entity",
},
+ icon: {
+ type: "string",
+ format: "uri",
+ description:
+ "Optional icon URL for the mentioned entity, displayed next to the mention",
+ },
},
},
},
diff --git a/mentions/services/wikipedia.ts b/mentions/services/wikipedia.ts
index 0f64e3e2..92f1e177 100644
--- a/mentions/services/wikipedia.ts
+++ b/mentions/services/wikipedia.ts
@@ -1,4 +1,4 @@
-type Result = { uri: string; name: string; href?: string };
+type Result = { uri: string; name: string; href?: string; icon?: string };
export async function wikipedia(search: string, limit: number): Promise {
if (!search.trim()) return [];
@@ -21,5 +21,6 @@ export async function wikipedia(search: string, limit: number): Promise