@reviewflow/ui 0.0.0-stage → 6.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +95 -2
  3. package/package.json +101 -3
  4. package/src/board/bucket-section.tsx +94 -0
  5. package/src/board/dataSource.ts +44 -0
  6. package/src/board/facts.ts +183 -0
  7. package/src/board/index.ts +12 -0
  8. package/src/board/pull-request-board.tsx +239 -0
  9. package/src/board/pull-request-row.tsx +181 -0
  10. package/src/components/branch-diff/diff-themes.ts +6 -0
  11. package/src/components/branch-diff/diff-worker-pool.tsx +12 -0
  12. package/src/components/branch-diff/diff-worker-pool.web.tsx +56 -0
  13. package/src/components/branch-diff/diff-worker.ts +7 -0
  14. package/src/components/branch-diff/use-collapsed-files.ts +49 -0
  15. package/src/components/branch-diff/use-diff-settings.tsx +97 -0
  16. package/src/components/branch-diff/whitespace-button.tsx +32 -0
  17. package/src/components/copyable-code-block.tsx +101 -0
  18. package/src/components/label-pill.tsx +84 -0
  19. package/src/components/link-in-row.ts +6 -0
  20. package/src/components/markdown-suggestion.tsx +60 -0
  21. package/src/components/markdown-text.tsx +1067 -0
  22. package/src/components/pull-request-actions-merge.tsx +341 -0
  23. package/src/components/pull-request-actions-review.tsx +218 -0
  24. package/src/components/pull-request-checks.tsx +86 -0
  25. package/src/components/pull-request-commit-files.ts +143 -0
  26. package/src/components/pull-request-commits.tsx +221 -0
  27. package/src/components/pull-request-conversation-card.tsx +220 -0
  28. package/src/components/pull-request-conversation-edit.tsx +147 -0
  29. package/src/components/pull-request-conversation-event.tsx +267 -0
  30. package/src/components/pull-request-conversation-person.tsx +28 -0
  31. package/src/components/pull-request-conversation-thread.tsx +158 -0
  32. package/src/components/pull-request-diff-annotations.tsx +443 -0
  33. package/src/components/pull-request-diff-collapse.ts +43 -0
  34. package/src/components/pull-request-diff-data.ts +174 -0
  35. package/src/components/pull-request-diff-impacts-summary.tsx +97 -0
  36. package/src/components/pull-request-diff-impacts.ts +45 -0
  37. package/src/components/pull-request-diff-jump.ts +87 -0
  38. package/src/components/pull-request-diff-lines.ts +103 -0
  39. package/src/components/pull-request-diff-overview.tsx +416 -0
  40. package/src/components/pull-request-diff-view.tsx +438 -0
  41. package/src/components/pull-request-diff-viewer.tsx +1374 -0
  42. package/src/components/pull-request-files.tsx +533 -0
  43. package/src/components/pull-request-markdown-input.tsx +91 -0
  44. package/src/components/pull-request-panel.tsx +1270 -0
  45. package/src/components/pull-request-part-notice.tsx +82 -0
  46. package/src/components/pull-request-reactions.tsx +178 -0
  47. package/src/components/pull-request-review.tsx +164 -0
  48. package/src/components/pull-request-revisions.tsx +273 -0
  49. package/src/components/pull-request-screen.tsx +299 -0
  50. package/src/components/pull-request-suggestion-actions.tsx +201 -0
  51. package/src/components/pull-request-timeline.tsx +270 -0
  52. package/src/components/repository-attachment-banner.tsx +42 -0
  53. package/src/components/scroll-parent.ts +50 -0
  54. package/src/css.d.ts +1 -0
  55. package/src/dataSource.ts +242 -0
  56. package/src/editor/alerts.ts +105 -0
  57. package/src/editor/code-editor.native.tsx +25 -0
  58. package/src/editor/code-editor.tsx +112 -0
  59. package/src/editor/codeEditorExtensions.ts +65 -0
  60. package/src/editor/codeHighlight.ts +71 -0
  61. package/src/editor/commands.ts +175 -0
  62. package/src/editor/decorations.ts +425 -0
  63. package/src/editor/editable-file.tsx +224 -0
  64. package/src/editor/emojiCompletion.ts +110 -0
  65. package/src/editor/extensions.ts +149 -0
  66. package/src/editor/frontmatter.ts +76 -0
  67. package/src/editor/inlineFormat.ts +114 -0
  68. package/src/editor/lineTypes.ts +55 -0
  69. package/src/editor/markdown-editor.css +511 -0
  70. package/src/editor/markdown-editor.native.tsx +29 -0
  71. package/src/editor/markdown-editor.tsx +295 -0
  72. package/src/editor/nodeClasses.ts +69 -0
  73. package/src/index.ts +75 -0
  74. package/src/markdown-github/flatten.ts +49 -0
  75. package/src/markdown-github/html-tokens.ts +93 -0
  76. package/src/markdown-github/pair-html.ts +354 -0
  77. package/src/markdown-github/safe-url.ts +15 -0
  78. package/src/markdown-github/table-columns.ts +23 -0
  79. package/src/markdown-github/toggle-task.ts +164 -0
  80. package/src/nativewind-env.d.ts +1 -0
  81. package/src/prStatus.ts +90 -0
  82. package/src/services/PullRequestDraftsProvider.tsx +179 -0
  83. package/src/services/PullRequestReviewDraftProvider.tsx +178 -0
  84. package/src/services/PullRequestSuggestionBatchProvider.tsx +112 -0
  85. package/src/services/emojify.ts +13 -0
  86. package/src/services/errorToMessage.ts +6 -0
  87. package/src/services/expiringDraftStore.ts +102 -0
  88. package/src/services/formatDataAge.ts +12 -0
  89. package/src/services/formatRelativeDate.ts +56 -0
  90. package/src/services/githubImages.tsx +33 -0
  91. package/src/services/isViewerAuthor.ts +7 -0
  92. package/src/slots.tsx +34 -0
@@ -0,0 +1,147 @@
1
+ import type { PullRequestDetail, PullRequestEdit } from "@reviewflow/sdk";
2
+ import { ActionButton, Button, Text, View } from "alouette";
3
+ import type { ReactNode } from "react";
4
+ import { usePullRequestDataSource } from "../dataSource.ts";
5
+ import { useDraftText } from "../services/PullRequestDraftsProvider.tsx";
6
+ import { errorToMessage } from "../services/errorToMessage.ts";
7
+ import { PullRequestMarkdownInput } from "./pull-request-markdown-input.tsx";
8
+
9
+ /** What every gesture of the conversation needs to write and to re-read. */
10
+ export interface ConversationEditing {
11
+ pullRequest: PullRequestDetail;
12
+ /** reads again the parts of the pull request the change made stale */
13
+ onEdited: (edit: PullRequestEdit) => Promise<void>;
14
+ }
15
+
16
+ /** Writes the edit through the data source, then reads again what it made stale. */
17
+ export const useSendEdit = ({
18
+ pullRequest,
19
+ onEdited,
20
+ }: ConversationEditing): ((edit: PullRequestEdit) => Promise<void>) => {
21
+ const { edit: writeEdit } = usePullRequestDataSource();
22
+ return async (edit) => {
23
+ await writeEdit(pullRequest, edit);
24
+ await onEdited(edit);
25
+ };
26
+ };
27
+
28
+ interface BodyEditorProps {
29
+ editing: ConversationEditing;
30
+ /** where the edit in progress is kept, see `useDraftText` */
31
+ draftKey: string;
32
+ initial: string;
33
+ ariaLabel: string;
34
+ toEdit: (body: string) => PullRequestEdit;
35
+ onDone: () => void;
36
+ }
37
+
38
+ /** The inline editor of a description or a comment: the source, with its preview. */
39
+ export function BodyEditor({
40
+ editing,
41
+ draftKey,
42
+ initial,
43
+ ariaLabel,
44
+ toEdit,
45
+ onDone,
46
+ }: BodyEditorProps): ReactNode {
47
+ const sendEdit = useSendEdit(editing);
48
+ const draft = useDraftText(draftKey, initial);
49
+ return (
50
+ <View className="gap-xs">
51
+ <PullRequestMarkdownInput
52
+ aria-label={ariaLabel}
53
+ value={draft.text}
54
+ onChange={draft.setText}
55
+ />
56
+ <View className="flex-row flex-wrap items-center justify-end gap-xs">
57
+ <Button
58
+ text="Cancel"
59
+ accent="neutral"
60
+ variant="soft"
61
+ size="sm"
62
+ onPress={() => {
63
+ draft.discard();
64
+ onDone();
65
+ }}
66
+ />
67
+ <ActionButton
68
+ text="Save"
69
+ accent="brand"
70
+ variant="tonal"
71
+ size="sm"
72
+ errorToMessage={errorToMessage}
73
+ onPress={async () => {
74
+ const sent = draft.text;
75
+ await sendEdit(toEdit(sent));
76
+ draft.discard(sent);
77
+ onDone();
78
+ }}
79
+ />
80
+ </View>
81
+ </View>
82
+ );
83
+ }
84
+
85
+ export interface ComposerDraft {
86
+ body: string;
87
+ clear: () => void;
88
+ }
89
+
90
+ interface ComposerProps {
91
+ editing: ConversationEditing;
92
+ /** where the text being written is kept, see `useDraftText` */
93
+ draftKey: string;
94
+ title?: string;
95
+ ariaLabel: string;
96
+ buttonText: string;
97
+ toEdit: (body: string) => PullRequestEdit;
98
+ /** before the submit button, e.g. Resolve under a reply */
99
+ extraActions?: (draft: ComposerDraft) => ReactNode;
100
+ }
101
+
102
+ /** A new comment or a reply: cleared once GitHub took it, kept when it refused. */
103
+ export function Composer({
104
+ editing,
105
+ draftKey,
106
+ title,
107
+ ariaLabel,
108
+ buttonText,
109
+ toEdit,
110
+ extraActions,
111
+ }: ComposerProps): ReactNode {
112
+ const sendEdit = useSendEdit(editing);
113
+ const draft = useDraftText(draftKey);
114
+ return (
115
+ <View className="gap-xs">
116
+ {title === undefined ? null : (
117
+ <Text className="font-body-bold text-sm">{title}</Text>
118
+ )}
119
+ <PullRequestMarkdownInput
120
+ aria-label={ariaLabel}
121
+ value={draft.text}
122
+ onChange={draft.setText}
123
+ />
124
+ <View className="flex-row flex-wrap items-center justify-end gap-xs">
125
+ {extraActions?.({
126
+ body: draft.text,
127
+ clear: () => {
128
+ draft.discard();
129
+ },
130
+ })}
131
+ <ActionButton
132
+ text={buttonText}
133
+ accent="brand"
134
+ variant="tonal"
135
+ size="sm"
136
+ disabled={draft.text.trim() === ""}
137
+ errorToMessage={errorToMessage}
138
+ onPress={async () => {
139
+ const sent = draft.text;
140
+ await sendEdit(toEdit(sent));
141
+ draft.discard(sent);
142
+ }}
143
+ />
144
+ </View>
145
+ </View>
146
+ );
147
+ }
@@ -0,0 +1,267 @@
1
+ import type { PullRequestEvent } from "@reviewflow/sdk";
2
+ import type { IconProps } from "alouette";
3
+ import { Code, Icon, Text, View } from "alouette";
4
+ import { ArrowsClockwiseRegularIcon } from "alouette-icons/phosphor-icons/ArrowsClockwise";
5
+ import { CheckCircleRegularIcon } from "alouette-icons/phosphor-icons/CheckCircle";
6
+ import { CircleDashedRegularIcon } from "alouette-icons/phosphor-icons/CircleDashed";
7
+ import { EyeRegularIcon } from "alouette-icons/phosphor-icons/Eye";
8
+ import { FileDashedRegularIcon } from "alouette-icons/phosphor-icons/FileDashed";
9
+ import { GitBranchRegularIcon } from "alouette-icons/phosphor-icons/GitBranch";
10
+ import { GitCommitRegularIcon } from "alouette-icons/phosphor-icons/GitCommit";
11
+ import { GitMergeRegularIcon } from "alouette-icons/phosphor-icons/GitMerge";
12
+ import { LightningRegularIcon } from "alouette-icons/phosphor-icons/Lightning";
13
+ import { PencilSimpleRegularIcon } from "alouette-icons/phosphor-icons/PencilSimple";
14
+ import { ProhibitRegularIcon } from "alouette-icons/phosphor-icons/Prohibit";
15
+ import { TagRegularIcon } from "alouette-icons/phosphor-icons/Tag";
16
+ import { UserMinusRegularIcon } from "alouette-icons/phosphor-icons/UserMinus";
17
+ import { UserPlusRegularIcon } from "alouette-icons/phosphor-icons/UserPlus";
18
+ import { XCircleRegularIcon } from "alouette-icons/phosphor-icons/XCircle";
19
+ import type { ReactNode } from "react";
20
+ import { formatDataAge } from "../services/formatDataAge.ts";
21
+ import { LabelPill } from "./label-pill.tsx";
22
+
23
+ interface EventRowProps {
24
+ icon: IconProps["icon"];
25
+ event: PullRequestEvent;
26
+ /** the words before the subject */
27
+ text: string;
28
+ /** the thing the action names; a string is set bold */
29
+ subject?: ReactNode;
30
+ /** the words after it */
31
+ after?: string;
32
+ /** a second thing after `after`, e.g. the new base branch */
33
+ children?: ReactNode;
34
+ }
35
+
36
+ function EventRow({
37
+ icon,
38
+ event,
39
+ text,
40
+ subject,
41
+ after,
42
+ children,
43
+ }: EventRowProps): ReactNode {
44
+ return (
45
+ <View className="min-w-0 flex-row flex-wrap items-center gap-xs pl-xs">
46
+ <Icon icon={icon} size={16} className="text-muted" />
47
+ <Text className="text-sm font-body-bold">
48
+ {event.actor?.login ?? "ghost"}
49
+ </Text>
50
+ <Text className="text-sm text-muted">{text}</Text>
51
+ {typeof subject === "string" ? (
52
+ <Text className="text-sm font-body-bold">{subject}</Text>
53
+ ) : (
54
+ subject
55
+ )}
56
+ {after === undefined ? null : (
57
+ <Text className="text-sm text-muted">{after}</Text>
58
+ )}
59
+ {children}
60
+ <Text className="text-sm text-muted">
61
+ {formatDataAge(event.createdAt)}
62
+ </Text>
63
+ </View>
64
+ );
65
+ }
66
+
67
+ /** Events about the branches and the pull request's own state. */
68
+ function BranchEventLine({ event }: { event: PullRequestEvent }): ReactNode {
69
+ const { action, subject, detail } = event;
70
+ if (action === "baseRefChanged") {
71
+ return (
72
+ <EventRow
73
+ icon={<GitBranchRegularIcon />}
74
+ event={event}
75
+ text="changed the base branch from"
76
+ subject={<Code>{detail ?? ""}</Code>}
77
+ after="to"
78
+ >
79
+ <Code>{subject ?? ""}</Code>
80
+ </EventRow>
81
+ );
82
+ }
83
+ if (action === "headRefForcePushed") {
84
+ return (
85
+ <EventRow
86
+ icon={<ArrowsClockwiseRegularIcon />}
87
+ event={event}
88
+ text="force-pushed the head branch"
89
+ subject={detail === null ? undefined : <Code>{detail}</Code>}
90
+ />
91
+ );
92
+ }
93
+ if (action === "merged") {
94
+ return (
95
+ <EventRow
96
+ icon={<GitMergeRegularIcon />}
97
+ event={event}
98
+ text="merged this pull request"
99
+ subject={subject ?? undefined}
100
+ />
101
+ );
102
+ }
103
+ if (action === "closed") {
104
+ return (
105
+ <EventRow
106
+ icon={<XCircleRegularIcon />}
107
+ event={event}
108
+ text="closed this pull request"
109
+ />
110
+ );
111
+ }
112
+ if (action === "reopened") {
113
+ return (
114
+ <EventRow
115
+ icon={<CircleDashedRegularIcon />}
116
+ event={event}
117
+ text="reopened this pull request"
118
+ />
119
+ );
120
+ }
121
+ if (action === "reviewDismissed") {
122
+ return (
123
+ <EventRow
124
+ icon={<ProhibitRegularIcon />}
125
+ event={event}
126
+ text="dismissed a review"
127
+ />
128
+ );
129
+ }
130
+ if (action === "autoMergeEnabled" || action === "autoMergeDisabled") {
131
+ return (
132
+ <EventRow
133
+ icon={<LightningRegularIcon />}
134
+ event={event}
135
+ text={
136
+ action === "autoMergeEnabled"
137
+ ? "enabled auto-merge"
138
+ : "disabled auto-merge"
139
+ }
140
+ />
141
+ );
142
+ }
143
+ return (
144
+ <EventRow
145
+ icon={<GitCommitRegularIcon />}
146
+ event={event}
147
+ text="added a commit"
148
+ subject={subject ?? undefined}
149
+ />
150
+ );
151
+ }
152
+
153
+ /** One event of the pull request, in GitHub's wording. */
154
+ export function EventLine({ event }: { event: PullRequestEvent }): ReactNode {
155
+ const { action, subject, detail, label } = event;
156
+ if (action === "reviewRequested") {
157
+ return (
158
+ <EventRow
159
+ icon={<EyeRegularIcon />}
160
+ event={event}
161
+ text="requested a review from"
162
+ subject={subject ?? undefined}
163
+ />
164
+ );
165
+ }
166
+ if (action === "reviewRequestRemoved") {
167
+ return (
168
+ <EventRow
169
+ icon={<EyeRegularIcon />}
170
+ event={event}
171
+ text="removed the review request for"
172
+ subject={subject ?? undefined}
173
+ />
174
+ );
175
+ }
176
+ if (action === "readyForReview") {
177
+ return (
178
+ <EventRow
179
+ icon={<CheckCircleRegularIcon />}
180
+ event={event}
181
+ text="marked this pull request as ready for review"
182
+ />
183
+ );
184
+ }
185
+ if (action === "convertedToDraft") {
186
+ return (
187
+ <EventRow
188
+ icon={<FileDashedRegularIcon />}
189
+ event={event}
190
+ text="marked this pull request as draft"
191
+ />
192
+ );
193
+ }
194
+ if (action === "labeled" || action === "unlabeled") {
195
+ return (
196
+ <EventRow
197
+ icon={<TagRegularIcon />}
198
+ event={event}
199
+ text={action === "labeled" ? "added the" : "removed the"}
200
+ subject={
201
+ label === null ? (subject ?? undefined) : <LabelPill label={label} />
202
+ }
203
+ after="label"
204
+ />
205
+ );
206
+ }
207
+ if (action === "assigned" || action === "unassigned") {
208
+ return (
209
+ <EventRow
210
+ icon={
211
+ action === "assigned" ? (
212
+ <UserPlusRegularIcon />
213
+ ) : (
214
+ <UserMinusRegularIcon />
215
+ )
216
+ }
217
+ event={event}
218
+ text={action}
219
+ subject={subject ?? undefined}
220
+ />
221
+ );
222
+ }
223
+ if (action === "renamed") {
224
+ return (
225
+ <EventRow
226
+ icon={<PencilSimpleRegularIcon />}
227
+ event={event}
228
+ text="changed the title"
229
+ subject={`${detail ?? ""} → ${subject ?? ""}`}
230
+ />
231
+ );
232
+ }
233
+ return <BranchEventLine event={event} />;
234
+ }
235
+
236
+ /** Consecutive commits of one actor: a line for the push, then each headline. */
237
+ export function CommitsLine({
238
+ events,
239
+ }: {
240
+ events: PullRequestEvent[];
241
+ }): ReactNode {
242
+ const [first] = events;
243
+ if (first === undefined) return null;
244
+ const count = events.length;
245
+ return (
246
+ <View className="min-w-0 gap-xxs">
247
+ <EventRow
248
+ icon={<GitCommitRegularIcon />}
249
+ event={first}
250
+ text="added"
251
+ subject={String(count)}
252
+ after={count === 1 ? "commit" : "commits"}
253
+ />
254
+ {events.map((event, index) => (
255
+ <View
256
+ key={`${event.detail ?? ""}:${String(index)}`}
257
+ className="min-w-0 flex-row items-center gap-xs pl-xl"
258
+ >
259
+ <Text className="min-w-0 shrink text-sm" numberOfLines={1}>
260
+ {event.subject ?? ""}
261
+ </Text>
262
+ {event.detail === null ? null : <Code>{event.detail}</Code>}
263
+ </View>
264
+ ))}
265
+ </View>
266
+ );
267
+ }
@@ -0,0 +1,28 @@
1
+ import type { PullRequestPerson } from "@reviewflow/sdk";
2
+ import { Avatar } from "alouette";
3
+ import type { ReactNode } from "react";
4
+ import { Image, View } from "react-native";
5
+
6
+ /**
7
+ * alouette's Avatar draws initials only, so GitHub's picture is laid over it:
8
+ * while it loads, or when it fails, the initials are what shows.
9
+ */
10
+ export function PersonAvatar({
11
+ person,
12
+ }: {
13
+ person: PullRequestPerson | null;
14
+ }): ReactNode {
15
+ const login = person?.login ?? "ghost";
16
+ return (
17
+ <View className="size-[28px] shrink-0">
18
+ <Avatar size="sm" name={login} />
19
+ {person?.avatarUrl ? (
20
+ <Image
21
+ source={{ uri: person.avatarUrl }}
22
+ accessibilityLabel={login}
23
+ className="absolute inset-0 size-[28px] rounded-full"
24
+ />
25
+ ) : null}
26
+ </View>
27
+ );
28
+ }
@@ -0,0 +1,158 @@
1
+ import type { PullRequestThread } from "@reviewflow/sdk";
2
+ import { suggestionOriginalLines } from "@reviewflow/sdk";
3
+ import {
4
+ ActionButton,
5
+ Badge,
6
+ Box,
7
+ Button,
8
+ CodeBlock,
9
+ ExternalLinkText,
10
+ Text,
11
+ View,
12
+ } from "alouette";
13
+ import type { ReactNode } from "react";
14
+ import { useMemo, useState } from "react";
15
+ import { errorToMessage } from "../services/errorToMessage.ts";
16
+ import { SuggestionOriginalContext } from "./markdown-suggestion.tsx";
17
+ import { CommentCard } from "./pull-request-conversation-card.tsx";
18
+ import type { ConversationEditing } from "./pull-request-conversation-edit.tsx";
19
+ import { Composer, useSendEdit } from "./pull-request-conversation-edit.tsx";
20
+ import { SuggestionActions } from "./pull-request-suggestion-actions.tsx";
21
+
22
+ /** The last lines of the hunk: the ones the comment was written under. */
23
+ const hunkTail = (diffHunk: string): string =>
24
+ diffHunk
25
+ .split("\n")
26
+ .filter((line) => !line.startsWith("@@"))
27
+ .slice(-4)
28
+ .join("\n");
29
+
30
+ const lineRange = (thread: PullRequestThread): string => {
31
+ const line = thread.line ?? thread.originalLine;
32
+ if (line === null) return "";
33
+ if (thread.startLine !== null && thread.startLine !== line) {
34
+ return `:${String(thread.startLine)}-${String(line)}`;
35
+ }
36
+ return `:${String(line)}`;
37
+ };
38
+
39
+ interface ThreadProps {
40
+ editing: ConversationEditing;
41
+ thread: PullRequestThread;
42
+ }
43
+
44
+ /** Resolved threads start folded, as on GitHub: they are history, not a request. */
45
+ export function Thread({ editing, thread }: ThreadProps): ReactNode {
46
+ const sendEdit = useSendEdit(editing);
47
+ const [open, setOpen] = useState(!thread.resolved);
48
+ const unlisted = thread.commentsTotal - thread.comments.length;
49
+ const first = thread.comments[0];
50
+ const last = thread.comments.at(-1);
51
+ const original = useMemo(() => suggestionOriginalLines(thread), [thread]);
52
+
53
+ const canToggle = thread.resolved
54
+ ? thread.viewerCanUnresolve
55
+ : thread.viewerCanResolve;
56
+ const toggleResolved = canToggle ? (
57
+ <ActionButton
58
+ text={thread.resolved ? "Unresolve conversation" : "Resolve conversation"}
59
+ accent="neutral"
60
+ variant="tonal"
61
+ size="sm"
62
+ errorToMessage={errorToMessage}
63
+ onPress={async () => {
64
+ await sendEdit({
65
+ kind: "resolveThread",
66
+ threadId: thread.id,
67
+ resolved: !thread.resolved,
68
+ });
69
+ }}
70
+ />
71
+ ) : null;
72
+
73
+ return (
74
+ <Box className="surface min-w-0 gap-sm p-m">
75
+ <View className="flex-row flex-wrap items-center gap-xs">
76
+ <Text
77
+ className="min-w-0 shrink text-sm font-body-bold"
78
+ numberOfLines={1}
79
+ >
80
+ {thread.path + lineRange(thread)}
81
+ </Text>
82
+ {thread.outdated ? (
83
+ <Badge accent="warning" size="sm">
84
+ outdated
85
+ </Badge>
86
+ ) : null}
87
+ {thread.resolved ? (
88
+ <Badge accent="success" size="sm">
89
+ resolved
90
+ </Badge>
91
+ ) : null}
92
+ {thread.resolved ? (
93
+ <Button
94
+ text={open ? "Hide" : "Show"}
95
+ accent="neutral"
96
+ variant="soft"
97
+ size="sm"
98
+ onPress={() => {
99
+ setOpen(!open);
100
+ }}
101
+ />
102
+ ) : null}
103
+ </View>
104
+ {open ? (
105
+ <View className="gap-m">
106
+ <CodeBlock size="sm">{hunkTail(thread.diffHunk)}</CodeBlock>
107
+ <SuggestionOriginalContext value={original}>
108
+ {thread.comments.map((comment) => (
109
+ <CommentCard
110
+ key={comment.id}
111
+ bare
112
+ editing={editing}
113
+ author={comment.author}
114
+ action="commented"
115
+ createdAt={comment.createdAt}
116
+ lastEditedAt={comment.lastEditedAt}
117
+ url={comment.url}
118
+ body={comment.body}
119
+ canEdit={comment.viewerCanUpdate}
120
+ reactable={comment}
121
+ toEdit={(body) => ({
122
+ kind: "editComment",
123
+ target: "reviewComment",
124
+ databaseId: comment.databaseId,
125
+ body,
126
+ })}
127
+ footer={<SuggestionActions thread={thread} comment={comment} />}
128
+ />
129
+ ))}
130
+ </SuggestionOriginalContext>
131
+ {unlisted > 0 && last !== undefined ? (
132
+ <ExternalLinkText
133
+ size="sm"
134
+ href={last.url}
135
+ text={`${String(unlisted)} more on GitHub`}
136
+ />
137
+ ) : null}
138
+ {thread.viewerCanReply && first !== undefined ? (
139
+ <Composer
140
+ editing={editing}
141
+ draftKey={`reply:${thread.id}`}
142
+ ariaLabel="Reply"
143
+ buttonText="Reply"
144
+ toEdit={(body) => ({
145
+ kind: "reply",
146
+ inReplyTo: first.databaseId,
147
+ body,
148
+ })}
149
+ extraActions={() => toggleResolved}
150
+ />
151
+ ) : (
152
+ toggleResolved
153
+ )}
154
+ </View>
155
+ ) : null}
156
+ </Box>
157
+ );
158
+ }