import { Button, Popover } from "../../../../components/ui"; import { Box, Text, useUiCapabilities } from "../../../../ui"; import { TextAttributes } from "../../../../ui"; import { colors } from "../../../../theme/colors"; import type { ChatUserSummary, PublicPortfolioAnalytics } from "../../../../api-client"; import { formatNumber } from "../../../../utils/format"; import { truncateWithEllipsis } from "../../../../utils/text-wrap"; export const PROFILE_POPOVER_CLOSE_DELAY_MS = 40; function hasPortfolioAnalytics(analytics: PublicPortfolioAnalytics | null | undefined): boolean { return Boolean( analytics && ( analytics.oneYearReturn != null || analytics.spyBeta != null ), ); } export function hasPublicChatProfileInfo(user: ChatUserSummary): boolean { if (user.profilePublic === false) return false; return Boolean(user.bio?.trim() || user.title?.trim() || user.company?.trim() || hasPortfolioAnalytics(user.portfolioAnalytics)); } export function hasChatProfileDetails(user: ChatUserSummary): boolean { return Boolean(user.bio?.trim() || user.title?.trim() || user.company?.trim()); } export function shouldOfferChatProfileSetup(user: ChatUserSummary, isOwnProfile: boolean): boolean { return isOwnProfile && !hasChatProfileDetails(user); } function formatSignedPercent(value: number): string { const percent = value * 100; return `${percent >= 0 ? "+" : ""}${formatNumber(percent, 2)}%`; } type AnalyticsMetric = { id: "one-year" | "beta"; label: string; value: string; rawValue: number; }; function analyticsValueColor(id: string, value: number): string { if (id === "one-year") { if (value > 0) return colors.positive; if (value < 0) return colors.negative; } return colors.warning; } function analyticsMetrics(analytics: PublicPortfolioAnalytics): AnalyticsMetric[] { return [ analytics.oneYearReturn != null ? { id: "one-year", label: "1Y", value: formatSignedPercent(analytics.oneYearReturn), rawValue: analytics.oneYearReturn, } : null, analytics.spyBeta != null ? { id: "beta", label: "Beta", value: formatNumber(analytics.spyBeta, 2), rawValue: analytics.spyBeta, } : null, ].filter((metric): metric is AnalyticsMetric => !!metric); } function headerMetricLabel(metric: AnalyticsMetric): string { return metric.id === "beta" ? "Beta" : metric.label; } function headerMetricsNaturalWidth(metrics: AnalyticsMetric[]): number { const metricWidth = metrics.reduce((sum, metric) => ( sum + headerMetricLabel(metric).length + 1 + metric.value.length ), 0); return metricWidth + Math.max(0, metrics.length - 1); } function HeaderAnalyticsStats({ metrics, width, }: { metrics: AnalyticsMetric[]; width: number; }) { if (metrics.length === 0 || width < 8) return null; const gapWidth = Math.max(0, metrics.length - 1); const naturalWidths = metrics.map((metric) => headerMetricLabel(metric).length + 1 + metric.value.length); const availableMetricWidth = Math.max(metrics.length * 4, width - gapWidth); let overflow = Math.max(0, naturalWidths.reduce((sum, value) => sum + value, 0) - availableMetricWidth); const metricWidths = naturalWidths.map((naturalWidth, index) => { const shrinkable = Math.max(0, naturalWidth - 4); const shrink = Math.min(shrinkable, Math.ceil(overflow / (naturalWidths.length - index))); overflow -= shrink; return naturalWidth - shrink; }); return ( {metrics.map((metric, index) => { const label = headerMetricLabel(metric); const metricWidth = metricWidths[index] ?? 4; const labelWidth = Math.max(1, Math.min(label.length, metricWidth - 2)); const valueWidth = Math.max(1, metricWidth - labelWidth - 1); return ( {truncateWithEllipsis(label, labelWidth)} {truncateWithEllipsis(metric.value, valueWidth)} ); })} ); } export function UserProfilePopover({ user, width, onClose, onKeepOpen, isOwnProfile = false, onSetUpProfile, }: { user: ChatUserSummary; width: number; onClose: () => void; onKeepOpen: () => void; isOwnProfile?: boolean; onSetUpProfile?: () => void; }) { const popoverWidth = Math.max(24, Math.min(38, width - 4)); const meta = [user.title, user.company].filter(Boolean).join(" ยท "); const bio = user.bio?.trim(); const analytics = user.portfolioAnalytics; const metrics = analytics ? analyticsMetrics(analytics) : []; const headerWidth = Math.max(1, popoverWidth - 2); const maxStatsWidth = Math.max(0, headerWidth - 8); const statsWidth = metrics.length > 0 && maxStatsWidth >= 8 ? Math.min(headerMetricsNaturalWidth(metrics), maxStatsWidth) : 0; const usernameWidth = Math.max(1, headerWidth - (statsWidth > 0 ? statsWidth + 1 : 0)); const showSetupAction = shouldOfferChatProfileSetup(user, isOwnProfile) && !!onSetUpProfile; const { nativePaneChrome } = useUiCapabilities(); const content = ( <> {truncateWithEllipsis(user.username ? `@${user.username}` : user.displayName, usernameWidth)} {statsWidth > 0 ? ( <> ) : null} {meta ? {truncateWithEllipsis(meta, popoverWidth - 2)} : null} {bio ? ( {bio} ) : null} {showSetupAction ? (