Skip to content
Source
Sign inSign up

pullRequestReactions.ts

97 lines3.4 KB

1import type { PullRequestReaction, PullRequestReactionContent } from "@t3tools/contracts";
2​
3export const PULL_REQUEST_REACTION_ORDER: ReadonlyArray<PullRequestReactionContent> = [
4 "thumbs-up",
5 "thumbs-down",
6 "laugh",
7 "hooray",
8 "confused",
9 "heart",
10 "rocket",
11 "eyes",
12];
13​
14const REACTION_EMOJI: Record<PullRequestReactionContent, string> = {
15 "thumbs-up": "πŸ‘",
16 "thumbs-down": "πŸ‘Ž",
17 laugh: "πŸ˜„",
18 hooray: "πŸŽ‰",
19 confused: "πŸ˜•",
20 heart: "❀️",
21 rocket: "πŸš€",
22 eyes: "πŸ‘€",
23};
24​
25const REACTION_NAME: Record<PullRequestReactionContent, string> = {
26 "thumbs-up": "thumbs up",
27 "thumbs-down": "thumbs down",
28 laugh: "laugh",
29 hooray: "hooray",
30 confused: "confused",
31 heart: "heart",
32 rocket: "rocket",
33 eyes: "eyes",
34};
35​
36export function pullRequestReactionEmoji(content: PullRequestReactionContent): string {
37 return REACTION_EMOJI[content];
39​
40export function pullRequestReactionName(content: PullRequestReactionContent): string {
41 return REACTION_NAME[content];
43​
44const NAMED_ACTOR_LIMIT = 3;
45​
46function joinNames(parts: ReadonlyArray<string>): string {
47 if (parts.length <= 1) return parts[0] ?? "";
48 if (parts.length === 2) return `${parts[0]} and ${parts[1]}`;
49 return `${parts.slice(0, -1).join(", ")}, and ${parts.at(-1)}`;
51​
52function countRemainder(count: number, named: boolean): string {
53 if (named) return `${count} ${count === 1 ? "other" : "others"}`;
54 return `${count} ${count === 1 ? "person" : "people"}`;
56​
57/** Hosts omit the viewer from actors; add You only if count has room, since viewer login is unavailable. */
58export function pullRequestReactionTooltip(reaction: PullRequestReaction): string {
59 const viewerHasRoom = reaction.actors.length < reaction.count;
60 const names =
61 reaction.viewerHasReacted && viewerHasRoom ? ["You", ...reaction.actors] : [...reaction.actors];
62 const shown = names.slice(0, Math.min(NAMED_ACTOR_LIMIT, reaction.count));
63 const others = Math.max(0, reaction.count - shown.length);
64 const parts = [...shown, ...(others > 0 ? [countRemainder(others, shown.length > 0)] : [])];
65 return `${joinNames(parts)} reacted with ${pullRequestReactionName(reaction.content)} emoji`;
67​
68/** Changes whenever the host reports different reactions, so pending overlays can reset. */
69export function pullRequestReactionsSignature(
70 reactions: ReadonlyArray<PullRequestReaction>,
71): string {
72 return reactions
73 .map((reaction) => `${reaction.content}:${reaction.count}:${reaction.viewerHasReacted ? 1 : 0}`)
74 .join(" ");
76​
77export function applyPendingPullRequestReactions(
78 reactions: ReadonlyArray<PullRequestReaction>,
79 pending: ReadonlyMap<PullRequestReactionContent, boolean>,
80): ReadonlyArray<PullRequestReaction> {
81 if (pending.size === 0) return reactions;
82 const byContent = new Map(reactions.map((reaction) => [reaction.content, reaction] as const));
83 for (const [content, reacted] of pending) {
84 const current = byContent.get(content);
85 if (current === undefined) {
86 if (reacted) {
87 byContent.set(content, { content, count: 1, actors: [], viewerHasReacted: true });
88 }
89 continue;
90 }
91 if (current.viewerHasReacted === reacted) continue;
92 const count = current.count + (reacted ? 1 : -1);
93 if (count <= 0) byContent.delete(content);
94 else byContent.set(content, { ...current, count, viewerHasReacted: reacted });
95 }
96 return PULL_REQUEST_REACTION_ORDER.flatMap((content) => byContent.get(content) ?? []);