bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
15 lines • 9.32 kB
JSON
{
"name": "social-components-messagedisplay",
"type": "registry:component",
"dependencies": [],
"devDependencies": [],
"registryDependencies": [],
"files": [
{
"path": "components/social/components/MessageDisplay.tsx",
"type": "registry:component",
"content": "\"use client\";\nimport type {\n\tBapIdentity,\n\tMessageMeta,\n\tMessageTransaction,\n} from \"bmap-api-types\";\nimport { CONTAINER_WIDTHS } from \"../../../lib/layout-constants.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport { BitcoinAvatar } from \"../../ui-components/index.js\";\nimport { Badge } from \"../../ui/badge.js\";\nimport { Card, CardContent } from \"../../ui/card.js\";\n\nexport interface MessageDisplayProps {\n\tmessage: MessageTransaction;\n\tmeta?: MessageMeta;\n\tsigner?: BapIdentity;\n\tcurrentIdKey?: string;\n\tshowAvatar?: boolean;\n\tshowTimestamp?: boolean;\n\tshowDeliveryStatus?: boolean;\n\tcompact?: boolean;\n\tonUserClick?: (identity: BapIdentity) => void;\n\tonMessageClick?: (message: MessageTransaction) => void;\n\tclassName?: string;\n}\n\n/**\n * MessageDisplay shows individual messages in chat/DM contexts\n *\n * Features:\n * - Message bubbles with proper alignment\n * - Read/delivered status indicators\n * - Rich content support\n * - Timestamp display\n * - User avatars and names\n * - Click handlers for interaction\n */\nexport function MessageDisplay({\n\tmessage,\n\tmeta,\n\tsigner,\n\tcurrentIdKey,\n\tshowAvatar = true,\n\tshowTimestamp = true,\n\tshowDeliveryStatus = true,\n\tcompact = false,\n\tonUserClick,\n\tonMessageClick,\n\tclassName = \"\",\n}: MessageDisplayProps) {\n\tconst isOwnMessage =\n\t\tsigner?.idKey === currentIdKey || signer?.currentAddress === currentIdKey;\n\tconst content = message.B[0]?.content || \"\";\n\tconst contentType = message.B[0]?.[\"content-type\"] || \"text/plain\";\n\tconst isMarkdown = contentType === \"text/markdown\";\n\tconst txId = message.tx.h;\n\tconst timestamp = message.timestamp;\n\tconst app = message.MAP[0]?.app || \"unknown\";\n\n\tconst signerName =\n\t\tsigner?.identity?.alternateName ||\n\t\tsigner?.identity?.givenName ||\n\t\tsigner?.displayName;\n\tconst signerImage = signer?.identity?.image || signer?.icon;\n\tconst signerAddress = signer?.currentAddress;\n\tconst fallbackText =\n\t\tsignerName?.charAt(0) || signerAddress?.slice(0, 2) || \"?\";\n\n\tconst formatTimestamp = (ts: number) => {\n\t\tconst date = new Date(ts);\n\t\tconst now = new Date();\n\t\tconst isToday = date.toDateString() === now.toDateString();\n\n\t\tif (isToday) {\n\t\t\treturn date.toLocaleTimeString([], {\n\t\t\t\thour: \"2-digit\",\n\t\t\t\tminute: \"2-digit\",\n\t\t\t});\n\t\t}\n\n\t\tconst yesterday = new Date(now);\n\t\tyesterday.setDate(yesterday.getDate() - 1);\n\t\tconst isYesterday = date.toDateString() === yesterday.toDateString();\n\n\t\tif (isYesterday) {\n\t\t\treturn `Yesterday ${date.toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" })}`;\n\t\t}\n\n\t\treturn date.toLocaleDateString([], {\n\t\t\tmonth: \"short\",\n\t\t\tday: \"numeric\",\n\t\t\thour: \"2-digit\",\n\t\t\tminute: \"2-digit\",\n\t\t});\n\t};\n\n\tconst handleUserClick = () => {\n\t\tif (signer && onUserClick) {\n\t\t\tonUserClick(signer);\n\t\t}\n\t};\n\n\tconst handleMessageClick = () => {\n\t\tif (onMessageClick) {\n\t\t\tonMessageClick(message);\n\t\t}\n\t};\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t\"flex flex-col gap-1 w-full\",\n\t\t\t\tisOwnMessage ? \"items-end self-end\" : \"items-start self-start\",\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t\tstyle={{\n\t\t\t\tmaxWidth: compact\n\t\t\t\t\t? CONTAINER_WIDTHS.CARD_SMALL\n\t\t\t\t\t: CONTAINER_WIDTHS.POPOVER_MEDIUM,\n\t\t\t}}\n\t\t>\n\t\t\t{/* Message Header (for incoming messages) */}\n\t\t\t{!isOwnMessage && showAvatar && signer && !compact && (\n\t\t\t\t<div className=\"flex items-center gap-2 mb-1\">\n\t\t\t\t\t<BitcoinAvatar\n\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\tsrc={signerImage}\n\t\t\t\t\t\talt={signerName || \"User\"}\n\t\t\t\t\t\tfallback={fallbackText}\n\t\t\t\t\t\tclassName={cn(onUserClick && \"cursor-pointer\")}\n\t\t\t\t\t\tonClick={onUserClick ? handleUserClick : undefined}\n\t\t\t\t\t/>\n\t\t\t\t\t<span\n\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\"text-xs text-muted-foreground font-medium\",\n\t\t\t\t\t\t\tonUserClick && \"cursor-pointer\",\n\t\t\t\t\t\t)}\n\t\t\t\t\t\tonClick={handleUserClick}\n\t\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\t\tif ((e.key === \"Enter\" || e.key === \" \") && onUserClick) {\n\t\t\t\t\t\t\t\thandleUserClick();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}}\n\t\t\t\t\t\ttabIndex={onUserClick ? 0 : undefined}\n\t\t\t\t\t\trole={onUserClick ? \"button\" : undefined}\n\t\t\t\t\t>\n\t\t\t\t\t\t{signerName ||\n\t\t\t\t\t\t\t(signerAddress ? `${signerAddress.slice(0, 8)}...` : \"Anonymous\")}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t{/* Message Bubble */}\n\t\t\t<Card\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"max-w-full\",\n\t\t\t\t\tisOwnMessage\n\t\t\t\t\t\t? \"bg-primary text-primary-foreground border-0\"\n\t\t\t\t\t\t: \"bg-muted\",\n\t\t\t\t\tonMessageClick && \"cursor-pointer\",\n\t\t\t\t)}\n\t\t\t\tstyle={{\n\t\t\t\t\tborderRadius: isOwnMessage\n\t\t\t\t\t\t? \"16px 16px 4px 16px\"\n\t\t\t\t\t\t: \"16px 16px 16px 4px\",\n\t\t\t\t}}\n\t\t\t\tonClick={handleMessageClick}\n\t\t\t>\n\t\t\t\t<CardContent\n\t\t\t\t\tclassName={cn(\"flex flex-col gap-1\", compact ? \"p-2\" : \"p-3\")}\n\t\t\t\t>\n\t\t\t\t\t{/* Message Content */}\n\t\t\t\t\t{content && (\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t{isMarkdown ? (\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\tcompact ? \"text-[13px]\" : \"text-sm\",\n\t\t\t\t\t\t\t\t\t\t\"leading-relaxed whitespace-pre-wrap\",\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{content}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<p\n\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\tcompact ? \"text-xs\" : \"text-sm\",\n\t\t\t\t\t\t\t\t\t\t\"whitespace-pre-wrap leading-relaxed\",\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{content}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\n\t\t\t\t\t{/* Rich Media - would be extracted from B protocol data if present */}\n\n\t\t\t\t\t{/* Message Footer */}\n\t\t\t\t\t<div className=\"flex items-center justify-between gap-2 mt-1\">\n\t\t\t\t\t\t{showTimestamp && (\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"text-xs opacity-80\",\n\t\t\t\t\t\t\t\t\tisOwnMessage\n\t\t\t\t\t\t\t\t\t\t? \"text-primary-foreground/80\"\n\t\t\t\t\t\t\t\t\t\t: \"text-muted-foreground\",\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{formatTimestamp(timestamp)}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t{/* Delivery Status (for own messages) */}\n\t\t\t\t\t\t{isOwnMessage && showDeliveryStatus && meta && (\n\t\t\t\t\t\t\t<div className=\"flex items-center gap-1\">\n\t\t\t\t\t\t\t\t{meta.delivered ? (\n\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\t\t\t\twidth=\"12\"\n\t\t\t\t\t\t\t\t\t\t\theight=\"12\"\n\t\t\t\t\t\t\t\t\t\t\tfill=\"currentColor\"\n\t\t\t\t\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\t\t\"opacity-70\",\n\t\t\t\t\t\t\t\t\t\t\t\tisOwnMessage\n\t\t\t\t\t\t\t\t\t\t\t\t\t? \"text-primary-foreground/70\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t: \"text-muted-foreground\",\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\taria-label=\"Delivered\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<title>Delivered</title>\n\t\t\t\t\t\t\t\t\t\t\t<path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z\" />\n\t\t\t\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t\t\t\t{meta.readBy && meta.readBy.length > 0 && (\n\t\t\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"text-xs opacity-70\",\n\t\t\t\t\t\t\t\t\t\t\t\t\tisOwnMessage\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t? \"text-primary-foreground/70\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"text-muted-foreground\",\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\tRead\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\t\"text-xs opacity-70\",\n\t\t\t\t\t\t\t\t\t\t\tisOwnMessage\n\t\t\t\t\t\t\t\t\t\t\t\t? \"text-primary-foreground/70\"\n\t\t\t\t\t\t\t\t\t\t\t\t: \"text-muted-foreground\",\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tSending...\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\n\t\t\t{/* Message Metadata (for debugging/development) */}\n\t\t\t{!compact && (\n\t\t\t\t<div className=\"flex items-center gap-1 opacity-60\">\n\t\t\t\t\t<span className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t{txId.slice(0, 6)}...{txId.slice(-4)}\n\t\t\t\t\t</span>\n\t\t\t\t\t<Badge variant=\"outline\" className=\"text-xs h-5\">\n\t\t\t\t\t\t{app}\n\t\t\t\t\t</Badge>\n\t\t\t\t\t{meta?.edited && (\n\t\t\t\t\t\t<span className=\"text-xs text-muted-foreground\">edited</span>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n",
"target": "<%- config.aliases.components %>/messagedisplay.tsx"
}
]
}