mirror of
https://github.com/block/buzz.git
synced 2026-11-02 07:22:55 +00:00
fix(market): align bidder profile identity
Signed-off-by: Wintermute <c0fc581234c3585602139eec347ced7b82af65b6f6c10728348515c0c06c51c3@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -203,7 +203,7 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
);
|
||||
const [isMainDeferredEditPending, setMainDeferredEditPending] = React.useState(false);
|
||||
const isMarketObserver = useMarketObserver(agentPubkeys, currentPubkey);
|
||||
const { isMarketChannel, messageFooters: marketMessageFooters, messages: marketTimelineMessages } = useMarketChannelTimeline(messages);
|
||||
const { isMarketChannel, messageFooters: marketMessageFooters, messages: marketTimelineMessages } = useMarketChannelTimeline(messages, profiles);
|
||||
const isNonMemberView =
|
||||
activeChannel !== null &&
|
||||
!activeChannel.isMember &&
|
||||
@@ -369,7 +369,7 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
onOpenMembers,
|
||||
onWelcomeAddAgent: onAddAgent ? handleWelcomeAddAgent : undefined,
|
||||
});
|
||||
const channelIntro = isHuddleTranscript ? null : standardChannelIntro;
|
||||
const channelIntro = isHuddleTranscript || isMarketChannel ? null : standardChannelIntro;
|
||||
const { mainTimelineEntries, recentMentions, visibleMessages } =
|
||||
useChannelPaneMessages({
|
||||
activeChannel,
|
||||
|
||||
32
desktop/src/features/market/lib/marketBidIdentity.test.mjs
Normal file
32
desktop/src/features/market/lib/marketBidIdentity.test.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { resolveMarketBidIdentity } from "./marketBidIdentity.ts";
|
||||
|
||||
const BIDDER = "a".repeat(64);
|
||||
const bid = { actorName: "Fizz envelope", bidderPubkey: BIDDER };
|
||||
|
||||
test("market bids use the signed bidder's profile identity", () => {
|
||||
assert.deepEqual(
|
||||
resolveMarketBidIdentity(bid, {
|
||||
[BIDDER]: {
|
||||
avatarUrl: "https://relay.example/fizz.png",
|
||||
displayName: "Fizz",
|
||||
nip05Handle: null,
|
||||
ownerPubkey: null,
|
||||
isAgent: true,
|
||||
},
|
||||
}),
|
||||
{
|
||||
avatarUrl: "https://relay.example/fizz.png",
|
||||
displayName: "Fizz",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("market bids fall back to the signed envelope name without a profile", () => {
|
||||
assert.deepEqual(resolveMarketBidIdentity(bid), {
|
||||
avatarUrl: null,
|
||||
displayName: "Fizz envelope",
|
||||
});
|
||||
});
|
||||
14
desktop/src/features/market/lib/marketBidIdentity.ts
Normal file
14
desktop/src/features/market/lib/marketBidIdentity.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { MarketBid } from "@/features/market/lib/marketProtocol";
|
||||
import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
|
||||
export function resolveMarketBidIdentity(
|
||||
bid: Pick<MarketBid, "actorName" | "bidderPubkey">,
|
||||
profiles?: UserProfileLookup,
|
||||
) {
|
||||
const profile = profiles?.[normalizePubkey(bid.bidderPubkey)];
|
||||
return {
|
||||
avatarUrl: profile?.avatarUrl ?? null,
|
||||
displayName: profile?.displayName?.trim() || bid.actorName,
|
||||
};
|
||||
}
|
||||
@@ -8,8 +8,12 @@ import {
|
||||
} from "@/features/market/lib/marketTimeline";
|
||||
import { MarketChannelIntro } from "@/features/market/ui/MarketChannelIntro";
|
||||
import type { TimelineMessage } from "@/features/messages/types";
|
||||
import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
|
||||
export function useMarketChannelTimeline(messages: TimelineMessage[]) {
|
||||
export function useMarketChannelTimeline(
|
||||
messages: TimelineMessage[],
|
||||
profiles?: UserProfileLookup,
|
||||
) {
|
||||
const projection = useMarketChannel();
|
||||
const timelineMessages = useMarketTimelineMessages(messages);
|
||||
const anchor = React.useMemo(
|
||||
@@ -24,11 +28,12 @@ export function useMarketChannelTimeline(messages: TimelineMessage[]) {
|
||||
<MarketChannelIntro
|
||||
anchorMessage={anchor}
|
||||
bids={projection?.bids ?? []}
|
||||
profiles={profiles}
|
||||
/>
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
[anchor, projection?.bids],
|
||||
[anchor, profiles, projection?.bids],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAppNavigation } from "@/app/navigation/useAppNavigation";
|
||||
import { MessageCircle } from "lucide-react";
|
||||
|
||||
import { useMarketChannel } from "@/features/market/lib/MarketChannelContext";
|
||||
import { resolveMarketBidIdentity } from "@/features/market/lib/marketBidIdentity";
|
||||
import type { MarketBid } from "@/features/market/lib/marketProtocol";
|
||||
import {
|
||||
isMarketProtocolMessage,
|
||||
@@ -11,15 +12,18 @@ import {
|
||||
} from "@/features/market/lib/marketTimeline";
|
||||
import { MarketContractCard } from "@/features/market/ui/MarketContractCard";
|
||||
import type { TimelineMessage } from "@/features/messages/types";
|
||||
import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { UserAvatar } from "@/shared/ui/UserAvatar";
|
||||
|
||||
function BidList({
|
||||
bids,
|
||||
channelId,
|
||||
profiles,
|
||||
}: {
|
||||
bids: MarketBid[];
|
||||
channelId: string;
|
||||
profiles?: UserProfileLookup;
|
||||
}) {
|
||||
const { goChannel } = useAppNavigation();
|
||||
return (
|
||||
@@ -34,46 +38,52 @@ function BidList({
|
||||
<p className="text-sm text-muted-foreground">No bids yet.</p>
|
||||
) : (
|
||||
<div className="divide-y divide-border/60 overflow-hidden rounded-xl border border-border/70">
|
||||
{bids.map((bid) => (
|
||||
<div
|
||||
className="flex items-center gap-3 px-3 py-2.5"
|
||||
key={bid.eventId}
|
||||
>
|
||||
<UserAvatar
|
||||
avatarUrl={null}
|
||||
className="h-8 w-8 shrink-0 text-xs"
|
||||
displayName={bid.actorName}
|
||||
shape="squircle"
|
||||
size="sm"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-baseline gap-x-2">
|
||||
<span className="text-sm font-medium">{bid.actorName}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{bid.amountSats != null
|
||||
? `${bid.amountSats} sats per unit`
|
||||
: "Terms in thread"}
|
||||
{` · ${bid.quantity} ${bid.quantity === 1 ? "unit" : "units"}`}
|
||||
</span>
|
||||
</div>
|
||||
<p className="truncate text-sm text-muted-foreground">
|
||||
{bid.message}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
aria-label={`Open negotiation with ${bid.actorName}`}
|
||||
onClick={() => {
|
||||
void goChannel(channelId, { thread: bid.eventId });
|
||||
}}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
{bids.map((bid) => {
|
||||
const { avatarUrl, displayName } = resolveMarketBidIdentity(
|
||||
bid,
|
||||
profiles,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-3 px-3 py-2.5"
|
||||
key={bid.eventId}
|
||||
>
|
||||
<MessageCircle className="mr-1.5 h-4 w-4" />
|
||||
Thread
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<UserAvatar
|
||||
avatarUrl={avatarUrl}
|
||||
className="h-8 w-8 shrink-0 text-xs"
|
||||
displayName={displayName}
|
||||
shape="squircle"
|
||||
size="sm"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-baseline gap-x-2">
|
||||
<span className="text-sm font-medium">{displayName}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{bid.amountSats != null
|
||||
? `${bid.amountSats} sats per unit`
|
||||
: "Terms in thread"}
|
||||
{` · ${bid.quantity} ${bid.quantity === 1 ? "unit" : "units"}`}
|
||||
</span>
|
||||
</div>
|
||||
<p className="truncate text-sm text-muted-foreground">
|
||||
{bid.message}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
aria-label={`Open negotiation with ${displayName}`}
|
||||
onClick={() => {
|
||||
void goChannel(channelId, { thread: bid.eventId });
|
||||
}}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<MessageCircle className="mr-1.5 h-4 w-4" />
|
||||
Thread
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
@@ -83,9 +93,11 @@ function BidList({
|
||||
export function MarketChannelIntro({
|
||||
anchorMessage,
|
||||
bids,
|
||||
profiles,
|
||||
}: {
|
||||
anchorMessage: Pick<TimelineMessage, "body" | "createdAt" | "id">;
|
||||
bids: MarketBid[];
|
||||
profiles?: UserProfileLookup;
|
||||
}): React.ReactNode {
|
||||
const projection = useMarketChannel();
|
||||
if (!projection || isMarketProtocolMessage(anchorMessage)) return undefined;
|
||||
@@ -93,7 +105,11 @@ export function MarketChannelIntro({
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<MarketContractCard scenario={projection.scenario} />
|
||||
<BidList bids={visibleBids} channelId={projection.channelId} />
|
||||
<BidList
|
||||
bids={visibleBids}
|
||||
channelId={projection.channelId}
|
||||
profiles={profiles}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user