1import { Platform, Text as NativeText, View } from "react-native";
2
3import { cn } from "../../lib/cn";
4import { MOBILE_CODE_SURFACE } from "../../lib/typography";
5
6import type { ReviewRenderableLineRow } from "./reviewModel";
7import type { ReviewHighlightedToken } from "./reviewHighlightedToken.types";
8
9export const REVIEW_MONO_FONT_FAMILY = Platform.select({
10 ios: "ui-monospace",
11 android: "monospace",
12 default: "monospace",
13});
14
15export const REVIEW_DIFF_LINE_HEIGHT = MOBILE_CODE_SURFACE.rowHeight;
16
17export function renderVisibleWhitespace(value: string): string {
18 const expandedTabs = value.replace(/\t/g, " ");
19 return expandedTabs.replace(/^( +)/, (leading) => leading.replaceAll(" ", "\u00A0"));
20}
21
22export function changeTone(change: ReviewRenderableLineRow["change"]): string {
23 if (change === "add") return "bg-emerald-500/10";
24 if (change === "delete") return "bg-rose-500/10";
25 return "bg-card";
26}
27
28export function changeBarTone(change: ReviewRenderableLineRow["change"]): string {
29 if (change === "add") return "bg-emerald-400";
30 if (change === "delete") return "bg-rose-400";
31 return "bg-border/50";
32}
33
34function diffHighlightColor(change: ReviewRenderableLineRow["change"]): string | undefined {
35 if (change === "add") return "rgba(16, 185, 129, 0.24)";
36 if (change === "delete") return "rgba(244, 63, 94, 0.24)";
37 return undefined;
38}
39
40export function ReviewChangeBar(props: {
41 readonly change: ReviewRenderableLineRow["change"];
42 readonly height?: number;
43}) {
44 const height = props.height ?? REVIEW_DIFF_LINE_HEIGHT;
45 if (props.change === "delete") {
46 return (
47 <View className="w-[5px] overflow-hidden" style={{ height }}>
48 <View>
49 {Array.from({ length: Math.ceil(height / 2) }, (_, index) => (
50 <View key={index}>
51 <View className="h-px w-[5px] bg-rose-400" />
52 <View className="h-px" />
53 </View>
54 ))}
55 </View>
56 </View>
57 );
58 }
59
60 return (
61 <View className="w-[5px] overflow-hidden" style={{ height }}>
62 <View className={cn("h-full w-[5px] flex-1", changeBarTone(props.change))} />
63 </View>
64 );
65}
66
67export function DiffTokenText(props: {
68 readonly tokens: ReadonlyArray<ReviewHighlightedToken> | null;
69 readonly fallback: string;
70 readonly change?: ReviewRenderableLineRow["change"];
71 readonly className?: string;
72 readonly fontSize?: number;
73 readonly lineHeight?: number;
74}) {
75 const fontSize = props.fontSize ?? MOBILE_CODE_SURFACE.fontSize;
76 const lineHeight = props.lineHeight ?? MOBILE_CODE_SURFACE.rowHeight;
77 if (!props.tokens || props.tokens.length === 0) {
78 return (
79 <NativeText
80 numberOfLines={1}
81 selectable
82 className={cn("font-normal text-foreground", props.className)}
83 style={{
84 fontFamily: REVIEW_MONO_FONT_FAMILY,
85 fontSize,
86 lineHeight,
87 }}
88 >
89 {renderVisibleWhitespace(props.fallback || " ")}
90 </NativeText>
91 );
92 }
93
94 return (
95 <NativeText
96 numberOfLines={1}
97 selectable
98 className={cn("font-normal text-foreground", props.className)}
99 style={{
100 fontFamily: REVIEW_MONO_FONT_FAMILY,
101 fontSize,
102 lineHeight,
103 }}
104 >
105 {(() => {
106 let offset = 0;
107
108 return props.tokens.map((token) => {
109 const start = offset;
110 offset += token.content.length;
111
112 const fontWeight =
113 token.fontStyle !== null && (token.fontStyle & 2) === 2
114 ? ("700" as const)
115 : ("500" as const);
116 const fontStyle =
117 token.fontStyle !== null && (token.fontStyle & 1) === 1
118 ? ("italic" as const)
119 : ("normal" as const);
120
121 return (
122 <NativeText
123 key={`${start}:${token.content.length}:${token.color ?? ""}:${token.fontStyle ?? ""}`}
124 selectable
125 style={{
126 color: token.color ?? undefined,
127 fontFamily: REVIEW_MONO_FONT_FAMILY,
128 fontWeight,
129 fontStyle,
130 backgroundColor:
131 token.diffHighlight && props.change
132 ? diffHighlightColor(props.change)
133 : undefined,
134 borderRadius: token.diffHighlight ? 4 : undefined,
135 }}
136 >
137 {token.content.length > 0 ? renderVisibleWhitespace(token.content) : " "}
138 </NativeText>
139 );
140 });
141 })()}
142 </NativeText>
143 );
144}