Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions desktop/src/features/messages/ui/ComposerAudienceChips.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { X } from "lucide-react";

export type ComposerAudienceChip = {
displayName: string;
pubkey: string;
};

export function ComposerAudienceChips({
audience,
onRemove,
}: {
audience: readonly ComposerAudienceChip[];
onRemove: (pubkey: string) => void;
}) {
if (audience.length === 0) return null;

return (
<div
className="mb-2 flex flex-wrap gap-1.5"
data-testid="composer-audience-chips"
>
{audience.map(({ displayName, pubkey }) => (
<span
className="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-1 text-2xs text-foreground"
data-testid={`composer-audience-chip-${pubkey}`}
key={pubkey}
>
{displayName}
<button
aria-label={`Remove ${displayName}`}
className="rounded-full text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"
data-testid={`composer-audience-chip-remove-${pubkey}`}
onClick={() => onRemove(pubkey)}
type="button"
>
<X aria-hidden="true" className="size-3" />
</button>
</span>
))}
</div>
);
}
12 changes: 6 additions & 6 deletions desktop/src/features/messages/ui/MessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
type MentionSuggestion,
} from "./MentionAutocomplete";
import { ComposerDockToolbar } from "./ComposerDockToolbar";
import { ComposerAudienceChips } from "./ComposerAudienceChips";
import { NonMemberMentionDialog } from "./NonMemberMentionDialog";
import { useMentionSendFlow } from "./useMentionSendFlow";
import { usePersistentAgentMentionHydration } from "./usePersistentAgentMentionHydration";
Expand Down Expand Up @@ -292,7 +293,6 @@ function MessageComposerImpl({
persistentMentionHydration,
);
persistentMentionHydrationRef.current = persistentMentionHydration;

const mentionSendFlow = useMentionSendFlow({
channelId,
channelLinks,
Expand Down Expand Up @@ -609,8 +609,8 @@ function MessageComposerImpl({
),
spoileredAttachmentUrls,
trimmed,
audienceGeneration: persistentAudience.generation,
audienceRevision: audienceScope ? persistentAudience.revision : null,
audienceGeneration: persistentMentionHydration.audience.generation,
audienceRevision: persistentMentionHydration.getAudienceRevision(),
});
} finally {
persistentMentionHydration.endSubmit();
Expand All @@ -634,10 +634,7 @@ function MessageComposerImpl({
syncComposerContentFromEditor,
onCaptureSendContext,
onPreparingMentionSendChange,
audienceScope,
persistentMentionHydration,
persistentAudience.generation,
persistentAudience.revision,
]);
submitMessageRef.current = submitMessage;

Expand Down Expand Up @@ -955,6 +952,9 @@ function MessageComposerImpl({
</button>
</div>
) : null}
<ComposerAudienceChips
{...persistentMentionHydration.audienceChipsProps}
/>

{(media.pendingImeta.length > 0 || media.isUploading) && (
<div className="mb-2 flex items-center gap-2">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import assert from "node:assert/strict";
import test from "node:test";

import { truncatePubkey } from "@/shared/lib/pubkey";

import {
getPersistentMentionTokenRemovalRange,
resolvePersistentMentionTargets,
} from "./usePersistentAgentMentionHydration.ts";

const agentA = "a".repeat(64);
const agentB = "b".repeat(64);

test("persistent hydration gives duplicate agent names identity-safe mention text", () => {
const targets = resolvePersistentMentionTargets(
[agentA, agentB],
() => "Morgarita",
);

assert.deepEqual(targets, [
{ pubkey: agentA, displayName: `Morgarita (${truncatePubkey(agentA)})` },
{ pubkey: agentB, displayName: `Morgarita (${truncatePubkey(agentB)})` },
]);
});

test("persistent mention removal targets the duplicate-name pubkey's exact label", () => {
const targets = resolvePersistentMentionTargets(
[agentA, agentB],
() => "Morgarita",
);
const hydratedLabels = new Map(
targets.map((target) => [target.pubkey, target.displayName]),
);
const text = `${targets.map((target) => `@${target.displayName}`).join(" ")} `;

const range = getPersistentMentionTokenRemovalRange(
text,
agentB,
hydratedLabels,
() => "Morgarita",
);

assert.deepEqual(range, {
from: `@${targets[0].displayName} `.length,
to: text.length,
});
assert.equal(
text.slice(0, range.from) + text.slice(range.to),
`@${targets[0].displayName} `,
);
});
Loading