From eb5ee5a969bdfcb25bea5b3ea24084562b7b435a Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 1 Apr 2026 15:12:01 -0700 Subject: [PATCH] more scoping stuff --- components/Mention.tsx | 171 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 158 insertions(+), 13 deletions(-) diff --git a/components/Mention.tsx b/components/Mention.tsx index 4b8e6bf1..55568996 100644 --- a/components/Mention.tsx +++ b/components/Mention.tsx @@ -33,7 +33,7 @@ export function MentionAutocomplete(props: { const inputRef = useRef(null); const contentRef = useRef(null); - const { suggestionIndex, setSuggestionIndex, suggestions, scope, setScope, searchComplete } = + const { suggestionIndex, setSuggestionIndex, suggestions, scope, setScope, searchComplete, hasDidScopableServices } = useMentionSuggestions(searchQuery, props.open); const noResults = searchComplete && searchQuery !== "" && suggestions.length === 0; @@ -119,14 +119,66 @@ export function MentionAutocomplete(props: { uri: selectedSuggestion.uri, name: selectedSuggestion.name, }); + } else if ( + selectedSuggestion?.type === "did" && + hasDidScopableServices + ) { + // Tab on a DID result → show services that can be scoped to this DID + e.preventDefault(); + handleScopeChange({ + type: "did_services", + did: selectedSuggestion.did, + name: selectedSuggestion.displayName || selectedSuggestion.handle, + }); + } else if ( + selectedSuggestion?.type === "service" && + scope.type === "did_services" + ) { + // Tab on a service in the DID services scope → search that service scoped to the DID + e.preventDefault(); + handleScopeChange({ + type: "service", + serviceUri: selectedSuggestion.serviceUri, + name: selectedSuggestion.name, + scope: scope.did, + serviceName: selectedSuggestion.name, + scopedDid: scope.did, + scopedDidName: scope.name, + }); } else if (selectedSuggestion?.type === "service") { e.preventDefault(); handleScopeChange(serviceScopeFromMention(selectedSuggestion)); + } else if ( + selectedSuggestion?.type === "service_result" && + selectedSuggestion.subscope && + scope.type === "service" + ) { + e.preventDefault(); + handleScopeChange({ + type: "service", + serviceUri: scope.serviceUri, + name: selectedSuggestion.name, + scope: selectedSuggestion.subscope.scope, + serviceName: scope.serviceName ?? scope.name, + }); } } else if (e.key === "Enter") { e.preventDefault(); const selectedSuggestion = sortedSuggestions[suggestionIndex]; - if (selectedSuggestion?.type === "service") { + if ( + selectedSuggestion?.type === "service" && + scope.type === "did_services" + ) { + handleScopeChange({ + type: "service", + serviceUri: selectedSuggestion.serviceUri, + name: selectedSuggestion.name, + scope: scope.did, + serviceName: selectedSuggestion.name, + scopedDid: scope.did, + scopedDidName: scope.name, + }); + } else if (selectedSuggestion?.type === "service") { handleScopeChange(serviceScopeFromMention(selectedSuggestion)); } else if ( (e.ctrlKey || e.metaKey) && @@ -166,13 +218,30 @@ export function MentionAutocomplete(props: { scope?.type === "identities" || scope?.type === "publications" || scope?.type === "publication" || - scope?.type === "service" + scope?.type === "service" || + scope?.type === "did_services" ) { return ( { - handleScopeChange({ type: "default" }); + // When in a service entered from did_services, go back to did_services + if (scope.type === "service" && scope.scopedDid) { + handleScopeChange({ + type: "did_services", + did: scope.scopedDid, + name: scope.scopedDidName ?? scope.scopedDid, + }); + // When in a subscoped service, go back to the parent service scope + } else if (scope.type === "service" && scope.scope) { + handleScopeChange({ + type: "service", + serviceUri: scope.serviceUri, + name: scope.serviceName ?? scope.name, + }); + } else { + handleScopeChange({ type: "default" }); + } }} /> ); @@ -283,6 +352,17 @@ export function MentionAutocomplete(props: { handle={result.handle} avatar={result.avatar} selected={index === suggestionIndex} + onServicesClick={ + hasDidScopableServices + ? () => { + handleScopeChange({ + type: "did_services", + did: result.did, + name: result.displayName || result.handle, + }); + } + : undefined + } /> ) : result.type === "publication" ? ( { + handleScopeChange({ + type: "service", + serviceUri: scope.serviceUri, + name: result.name, + scope: result.subscope!.scope, + serviceName: scope.serviceName ?? scope.name, + }); + } + : undefined + } selected={index === suggestionIndex} /> ) : ( @@ -420,7 +514,9 @@ const DidResult = (props: { onClick: () => void; onMouseDown: (e: React.MouseEvent) => void; selected?: boolean; + onServicesClick?: () => void; }) => { + const nameContent = props.displayName ? props.displayName : props.handle; return ( ) } - result={props.displayName ? props.displayName : props.handle} + result={ + props.onServicesClick ? ( + <> +
{nameContent}
+ Search + + ) : ( + nameContent + ) + } subtext={props.displayName && `@${props.handle}`} onClick={props.onClick} onMouseDown={props.onMouseDown} @@ -517,10 +622,15 @@ const ServiceSearchResult = (props: { icon?: string; hasEmbed?: boolean; onEmbedClick?: () => void; + subscope?: SearchService.SubscopeInfo; + onSubscopeClick?: () => void; onClick: () => void; onMouseDown: (e: React.MouseEvent) => void; selected?: boolean; }) => { + const hasActions = + (props.hasEmbed && props.onEmbedClick) || + (props.subscope && props.onSubscopeClick); return (
{props.name}
- Embed + {props.subscope && props.onSubscopeClick && ( + + {props.subscope.label} + + )} + {props.hasEmbed && props.onEmbedClick && ( + Embed + )} ) : (
{props.name}
@@ -562,7 +679,9 @@ const ScopeHeader = (props: { ? "Publications" : props.scope.type === "publication" ? `Posts from ${props.scope.name}` - : `Results from ${props.scope.name}`; + : props.scope.type === "did_services" + ? `Services for ${props.scope.name}` + : `Results from ${props.scope.name}`; return (