gitalk-react 1.0.0 → 1.1.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 (63) hide show
  1. package/README-zh-CN.md +152 -53
  2. package/README.md +132 -34
  3. package/dist/gitalk-dark.css +1 -1
  4. package/dist/gitalk-light.css +1 -1
  5. package/dist/gitalk.browser.full.js +102 -0
  6. package/dist/gitalk.browser.js +94 -0
  7. package/dist/gitalk.cjs +94 -0
  8. package/dist/gitalk.d.cts +14 -0
  9. package/dist/gitalk.d.mts +174 -0
  10. package/dist/gitalk.js +2737 -10382
  11. package/package.json +61 -41
  12. package/proxy-server/.env.example +6 -0
  13. package/proxy-server/README.md +38 -0
  14. package/proxy-server/server.mjs +110 -0
  15. package/dist/gitalk.d.ts +0 -623
  16. package/dist/gitalk.umd.cjs +0 -126
  17. package/lib/assets/arrow-down.svg +0 -1
  18. package/lib/assets/edit.svg +0 -3
  19. package/lib/assets/github.svg +0 -3
  20. package/lib/assets/heart-filled.svg +0 -3
  21. package/lib/assets/heart.svg +0 -3
  22. package/lib/assets/reply.svg +0 -3
  23. package/lib/assets/tip.svg +0 -8
  24. package/lib/components/action.tsx +0 -21
  25. package/lib/components/avatar.tsx +0 -42
  26. package/lib/components/button.tsx +0 -35
  27. package/lib/components/comment-textarea.tsx +0 -166
  28. package/lib/components/comment.tsx +0 -204
  29. package/lib/components/comments-list.tsx +0 -112
  30. package/lib/components/meta.tsx +0 -133
  31. package/lib/components/svg.tsx +0 -29
  32. package/lib/constants/index.ts +0 -71
  33. package/lib/contexts/I18nContext.ts +0 -20
  34. package/lib/gitalk.tsx +0 -891
  35. package/lib/i18n/ar.json +0 -20
  36. package/lib/i18n/bn.json +0 -20
  37. package/lib/i18n/de.json +0 -20
  38. package/lib/i18n/en.json +0 -20
  39. package/lib/i18n/es.json +0 -20
  40. package/lib/i18n/fa.json +0 -20
  41. package/lib/i18n/fr.json +0 -20
  42. package/lib/i18n/hi.json +0 -20
  43. package/lib/i18n/index.ts +0 -53
  44. package/lib/i18n/ja.json +0 -20
  45. package/lib/i18n/ko.json +0 -20
  46. package/lib/i18n/pl.json +0 -21
  47. package/lib/i18n/pt.json +0 -20
  48. package/lib/i18n/ru.json +0 -20
  49. package/lib/i18n/ur.json +0 -20
  50. package/lib/i18n/zh-CN.json +0 -20
  51. package/lib/i18n/zh-TW.json +0 -20
  52. package/lib/interfaces/index.ts +0 -198
  53. package/lib/services/graphql/comment.ts +0 -84
  54. package/lib/services/request.ts +0 -24
  55. package/lib/services/user.ts +0 -40
  56. package/lib/themes/base.scss +0 -630
  57. package/lib/themes/gitalk-dark.scss +0 -28
  58. package/lib/themes/gitalk-light.scss +0 -28
  59. package/lib/utils/compatibility.ts +0 -35
  60. package/lib/utils/dom.ts +0 -15
  61. package/lib/utils/logger.ts +0 -66
  62. package/lib/utils/query.ts +0 -19
  63. package/lib/vite-env.d.ts +0 -1
package/lib/gitalk.tsx DELETED
@@ -1,891 +0,0 @@
1
- import "./i18n";
2
-
3
- import { useRequest } from "ahooks";
4
- import cn from "classnames";
5
- import React, {
6
- useCallback,
7
- useEffect,
8
- useMemo,
9
- useRef,
10
- useState,
11
- } from "react";
12
-
13
- import Button from "./components/button";
14
- import { type CommentProps } from "./components/comment";
15
- import CommentTextarea from "./components/comment-textarea";
16
- import CommentsList from "./components/comments-list";
17
- import Meta from "./components/meta";
18
- import {
19
- ACCESS_TOKEN_KEY,
20
- DATE_FNS_LOCALE_MAP,
21
- DEFAULT_FLIP_MOVE_OPTIONS,
22
- DEFAULT_LABELS,
23
- DEFAULT_PROXY,
24
- DEFAULT_USER,
25
- } from "./constants";
26
- import I18nContext, { type I18nContextValue } from "./contexts/I18nContext";
27
- import i18n, { type Lang } from "./i18n";
28
- import type {
29
- Comment as CommentType,
30
- GitalkProps,
31
- Issue as IssueType,
32
- User as UserType,
33
- } from "./interfaces";
34
- import {
35
- getIssueCommentsQL,
36
- type IssueCommentsQLResponse,
37
- } from "./services/graphql/comment";
38
- import getOctokitInstance from "./services/request";
39
- import { getAccessToken, getAuthorizeUrl } from "./services/user";
40
- import { supportsCSSVariables, supportsES2020 } from "./utils/compatibility";
41
- import logger from "./utils/logger";
42
- import { parseSearchQuery, stringifySearchQuery } from "./utils/query";
43
-
44
- const isModernBrowser = supportsCSSVariables() && supportsES2020();
45
-
46
- const Gitalk: React.FC<GitalkProps> = (props) => {
47
- const {
48
- clientID,
49
- clientSecret,
50
- owner,
51
- repo,
52
- admin,
53
- id: propsIssueId = location.host + location.pathname,
54
- number: propsIssueNumber,
55
- labels: propsIssueLabels = DEFAULT_LABELS,
56
- title: issueTitle = document.title,
57
- body: issueBody = location.href +
58
- "\n\n" +
59
- (document
60
- ?.querySelector('meta[name="description"]')
61
- ?.getAttribute("content") ?? ""),
62
- language = navigator.language as Lang,
63
- perPage: propsPerPage = 10,
64
- pagerDirection = "last",
65
- createIssueManually = false,
66
- enableHotKey = true,
67
- distractionFreeMode = false,
68
- flipMoveOptions = DEFAULT_FLIP_MOVE_OPTIONS,
69
- proxy = DEFAULT_PROXY,
70
- defaultUser: propsDefaultUser,
71
- defaultAuthor: propsDefaultAuthor,
72
- collapsedHeight: propsCollapsedHeight,
73
- highlightAdminComment = true,
74
- updateCountCallback,
75
- onCreateIssue,
76
- onCreateComment,
77
- className = "",
78
- ...restProps
79
- } = props;
80
-
81
- const [issue, setIssue] = useState<IssueType>();
82
- const issueNumber = useMemo(
83
- () =>
84
- propsIssueNumber && propsIssueNumber > 0 ? propsIssueNumber : undefined,
85
- [propsIssueNumber],
86
- );
87
- const issueId = useMemo(() => propsIssueId.slice(0, 50), [propsIssueId]);
88
- const issueLabels = useMemo(
89
- () => propsIssueLabels.concat([issueId]),
90
- [propsIssueLabels, issueId],
91
- );
92
-
93
- const textareaRef = useRef<HTMLTextAreaElement>(null);
94
- const [inputComment, setInputComment] = useState<string>("");
95
- const [isInputFocused, setIsInputFocused] = useState<boolean>(false);
96
-
97
- const [commentsCount, setCommentsCount] = useState<number>(0);
98
- const [commentsCursor, setCommentsCursor] = useState("");
99
- const [commentsPage, setCommentsPage] = useState<number>(1);
100
- const commentsPerPage = useMemo(
101
- () => (propsPerPage > 100 ? 100 : propsPerPage < 0 ? 10 : propsPerPage),
102
- [propsPerPage],
103
- );
104
- /** Current sort order, have effect when user is logged */
105
- const [commentsPagerDirection, setCommentsPagerDirection] =
106
- useState(pagerDirection);
107
- const defaultUser = useMemo(
108
- () =>
109
- propsDefaultUser
110
- ? propsDefaultUser
111
- : propsDefaultAuthor
112
- ? {
113
- avatar_url: propsDefaultAuthor.avatarUrl,
114
- login: propsDefaultAuthor.login,
115
- html_url: propsDefaultAuthor.url,
116
- }
117
- : DEFAULT_USER,
118
- [propsDefaultAuthor, propsDefaultUser],
119
- );
120
- const collapsedHeight =
121
- propsCollapsedHeight && propsCollapsedHeight > 0
122
- ? propsCollapsedHeight
123
- : undefined;
124
-
125
- const [alert, setAlert] = useState<string>("");
126
-
127
- const polyglot = useMemo(() => i18n(language), [language]);
128
- const i18nContextValue: I18nContextValue = useMemo(
129
- () => ({
130
- language,
131
- polyglot,
132
- dateFnsLocaleMap: DATE_FNS_LOCALE_MAP,
133
- }),
134
- [language, polyglot],
135
- );
136
-
137
- const {
138
- data: accessToken = localStorage.getItem(ACCESS_TOKEN_KEY) ?? undefined,
139
- mutate: setAccessToken,
140
- loading: getAccessTokenLoading,
141
- run: runGetAccessToken,
142
- } = useRequest(
143
- async (code: string) =>
144
- await getAccessToken({ url: proxy, code, clientID, clientSecret }),
145
- {
146
- manual: true,
147
- ready: !!proxy && !!clientID && !!clientSecret,
148
- onSuccess: (data) => {
149
- localStorage.setItem(ACCESS_TOKEN_KEY, data);
150
- logger.s(`Get access token successfully:`, data);
151
- },
152
- onError: (error) => {
153
- setAlert(`An error occurred while getting access token: ${error}`);
154
- logger.e(`An error occurred while getting access token:`, error);
155
- },
156
- },
157
- );
158
-
159
- useEffect(() => {
160
- const query = parseSearchQuery();
161
- const code = query["code"];
162
-
163
- if (code && !accessToken) {
164
- delete query["code"];
165
- const replacedUrl = `${window.location.origin}${window.location.pathname}?${stringifySearchQuery(query)}${window.location.hash}`;
166
- history.replaceState(null, "", replacedUrl);
167
-
168
- runGetAccessToken(code);
169
- }
170
- }, [accessToken, runGetAccessToken]);
171
-
172
- const octokit = useMemo(() => getOctokitInstance(accessToken), [accessToken]);
173
-
174
- const {
175
- data: user,
176
- mutate: setUser,
177
- loading: getUserLoading,
178
- } = useRequest(
179
- async () => {
180
- const getUserRes = await octokit.request("GET /user");
181
- if (getUserRes.status === 200) {
182
- const _user = getUserRes.data;
183
- logger.s(`Login successfully:`, _user);
184
- return _user;
185
- } else {
186
- localStorage.removeItem(ACCESS_TOKEN_KEY);
187
- setAccessToken(undefined);
188
- logger.e(`Get user details with access token failed:`, getUserRes);
189
- return undefined;
190
- }
191
- },
192
- {
193
- ready: !!accessToken,
194
- refreshDeps: [accessToken],
195
- },
196
- );
197
-
198
- const isAdmin = useMemo(() => {
199
- return (
200
- user &&
201
- !!admin.find(
202
- (username) => username.toLowerCase() === user.login.toLocaleLowerCase(),
203
- )
204
- );
205
- }, [admin, user]);
206
-
207
- const { loading: createIssueLoading, run: runCreateIssue } = useRequest(
208
- async () => {
209
- logger.i(`Creating issue...`);
210
-
211
- const createIssueRes = await octokit.request(
212
- "POST /repos/{owner}/{repo}/issues",
213
- {
214
- owner,
215
- repo,
216
- title: issueTitle,
217
- labels: issueLabels,
218
- body: issueBody,
219
- },
220
- );
221
-
222
- if (createIssueRes.status === 201) {
223
- const _issue = createIssueRes.data;
224
- logger.s(`Create issue successfully:`, _issue);
225
-
226
- onCreateIssue?.(_issue);
227
-
228
- setIssue(_issue);
229
- } else {
230
- setAlert(`Create issue failed: ${createIssueRes}`);
231
- logger.e(`Create issue failed:`, createIssueRes);
232
- }
233
- },
234
- {
235
- manual: true,
236
- ready:
237
- !!isAdmin && !!owner && !!repo && !!issueTitle && !!issueLabels.length,
238
- },
239
- );
240
-
241
- const { loading: getIssueLoading } = useRequest(
242
- async () => {
243
- if (issueNumber) {
244
- const getIssueRes = await octokit.request(
245
- "GET /repos/{owner}/{repo}/issues/{issue_number}",
246
- {
247
- owner,
248
- repo,
249
- issue_number: issueNumber,
250
- },
251
- );
252
-
253
- if (getIssueRes.status === 200) {
254
- const _issue = getIssueRes.data;
255
- logger.s(
256
- `Locate issue ${issueNumber} in repository ${owner}/${repo} successfully:`,
257
- _issue,
258
- );
259
- setIssue(_issue);
260
- } else if (getIssueRes.status === 404) {
261
- logger.w(
262
- `Issue ${issueNumber} in repository ${owner}/${repo} was not found.`,
263
- );
264
-
265
- if (isAdmin && !createIssueManually) {
266
- runCreateIssue();
267
- }
268
- } else {
269
- setAlert(
270
- `Get issue ${issueNumber} in repository ${owner}/${repo} failed: ${getIssueRes}`,
271
- );
272
- logger.e(
273
- `Get issue ${issueNumber} in repository ${owner}/${repo} failed:`,
274
- getIssueRes,
275
- );
276
- }
277
- } else if (issueId) {
278
- const getIssuesRes = await octokit.request(
279
- "GET /repos/{owner}/{repo}/issues",
280
- {
281
- owner,
282
- repo,
283
- labels: issueLabels.join(","),
284
- per_page: 1,
285
- },
286
- );
287
- const { status: getIssuesStatus, data: issues = [] } = getIssuesRes;
288
-
289
- if (getIssuesStatus === 200) {
290
- if (issues.length) {
291
- const _issue = issues[0];
292
- logger.s(
293
- `Locate issue with labels ${issueLabels} in repository ${owner}/${repo} successfully:`,
294
- _issue,
295
- );
296
- setIssue(_issue);
297
- } else {
298
- logger.w(
299
- `Issue with labels ${issueLabels} in repository ${owner}/${repo} was not found.`,
300
- );
301
-
302
- if (isAdmin && !createIssueManually) {
303
- runCreateIssue();
304
- }
305
- }
306
- } else if (getIssuesStatus === 404) {
307
- logger.w(
308
- `Issue with labels ${issueLabels} in repository ${owner}/${repo} was not found.`,
309
- );
310
-
311
- if (isAdmin && !createIssueManually) {
312
- runCreateIssue();
313
- }
314
- } else {
315
- setAlert(
316
- `Get issue with labels ${issueLabels} in repository ${owner}/${repo} failed: ${getIssuesRes}`,
317
- );
318
- logger.e(
319
- `Get issue with labels ${issueLabels} in repository ${owner}/${repo} failed:`,
320
- getIssuesRes,
321
- );
322
- }
323
- }
324
- },
325
- {
326
- ready: !!owner && !!repo && (!!issueNumber || !!issueId),
327
- refreshDeps: [owner, repo, issueNumber, issueId],
328
- onBefore: () => {
329
- setIssue(undefined);
330
- },
331
- },
332
- );
333
-
334
- const {
335
- data: comments = [],
336
- mutate: setComments,
337
- run: runGetComments,
338
- loading: getCommentsLoading,
339
- } = useRequest(
340
- async (): Promise<CommentType[]> => {
341
- const { number: currentIssueNumber } = issue as IssueType;
342
- const from = (commentsPage - 1) * commentsPerPage + 1;
343
- const to = commentsPage * commentsPerPage;
344
-
345
- if (user) {
346
- // Get comments via GraphQL, witch requires being logged and able to sort
347
- const query = getIssueCommentsQL({
348
- pagerDirection: commentsPagerDirection,
349
- });
350
-
351
- const getIssueCommentsRes: IssueCommentsQLResponse =
352
- await octokit.graphql(query, {
353
- owner,
354
- repo,
355
- id: currentIssueNumber,
356
- pageSize: commentsPerPage,
357
- ...(commentsCursor ? { cursor: commentsCursor } : {}),
358
- });
359
-
360
- if (getIssueCommentsRes.repository) {
361
- const _comments =
362
- getIssueCommentsRes.repository.issue.comments.nodes.map(
363
- (comment) => {
364
- return {
365
- ...comment,
366
- id: comment.databaseId,
367
- user: {
368
- ...defaultUser,
369
- avatar_url: comment.author.avatarUrl,
370
- login: comment.author.login,
371
- html_url: comment.author.url,
372
- },
373
- body_html: comment.bodyHTML,
374
- created_at: comment.createdAt,
375
- html_url: comment.resourcePath,
376
- reactions: {
377
- heart: comment.reactions?.totalCount ?? 0,
378
- },
379
- reactionsHeart: comment.reactions,
380
- } as CommentType;
381
- },
382
- );
383
- logger.s(
384
- `Get comments from ${from} to ${to} successfully:`,
385
- _comments,
386
- );
387
-
388
- const commentsPageInfo =
389
- getIssueCommentsRes.repository.issue.comments.pageInfo;
390
- const commentsPageCursor =
391
- commentsPageInfo.startCursor || commentsPageInfo.endCursor || "";
392
- setCommentsCursor(commentsPageCursor);
393
-
394
- if (commentsPagerDirection === "last")
395
- return [..._comments, ...comments];
396
- else return [...comments, ..._comments];
397
- } else {
398
- setAlert(
399
- `Get comments from ${from} to ${to} failed: ${getIssueCommentsRes}`,
400
- );
401
- logger.e(
402
- `Get comments from ${from} to ${to} failed:`,
403
- getIssueCommentsRes,
404
- );
405
- }
406
- } else {
407
- // Get comments via RESTful API, which not need be logged but unable to sort
408
- const getIssueCommentsRes = await octokit.request(
409
- "GET /repos/{owner}/{repo}/issues/{issue_number}/comments",
410
- {
411
- owner,
412
- repo,
413
- issue_number: currentIssueNumber,
414
- page: commentsPage,
415
- per_page: commentsPerPage,
416
- headers: {
417
- accept: "application/vnd.github.v3.full+json",
418
- },
419
- },
420
- );
421
-
422
- if (getIssueCommentsRes.status === 200) {
423
- const _comments = getIssueCommentsRes.data.map((comment) => {
424
- const reactionsHeartTotalCount = comment.reactions?.heart ?? 0;
425
- return {
426
- ...defaultUser,
427
- ...comment,
428
- reactionsHeart: {
429
- totalCount: reactionsHeartTotalCount,
430
- viewerHasReacted: false,
431
- nodes: [],
432
- },
433
- } as CommentType;
434
- });
435
- logger.s(
436
- `Get comments from ${from} to ${to} successfully:`,
437
- _comments,
438
- );
439
-
440
- setCommentsPage((prev) => prev + 1);
441
-
442
- return [...comments, ..._comments];
443
- } else {
444
- setAlert(
445
- `Get comments from ${from} to ${to} failed: ${getIssueCommentsRes}`,
446
- );
447
- logger.e(
448
- `Get comments from ${from} to ${to} failed:`,
449
- getIssueCommentsRes,
450
- );
451
- }
452
- }
453
-
454
- return comments;
455
- },
456
- {
457
- manual: true,
458
- ready: !!owner && !!repo && !!issue && !getUserLoading,
459
- },
460
- );
461
-
462
- const {
463
- data: localComments = [],
464
- mutate: setLocalComments,
465
- loading: createIssueCommentLoading,
466
- runAsync: runCreateIssueComment,
467
- } = useRequest(
468
- async (): Promise<CommentType[]> => {
469
- const { number: currentIssueNumber } = issue as IssueType;
470
-
471
- const createIssueCommentRes = await octokit.request(
472
- "POST /repos/{owner}/{repo}/issues/{issue_number}/comments",
473
- {
474
- owner,
475
- repo,
476
- issue_number: currentIssueNumber,
477
- body: inputComment,
478
- headers: {
479
- accept: "application/vnd.github.v3.full+json",
480
- },
481
- },
482
- );
483
-
484
- if (createIssueCommentRes.status === 201) {
485
- const createdIssueComment = {
486
- ...defaultUser,
487
- ...createIssueCommentRes.data,
488
- reactions: { heart: 0 },
489
- reactionsHeart: {
490
- totalCount: 0,
491
- viewerHasReacted: false,
492
- nodes: [],
493
- },
494
- } as CommentType;
495
- logger.s(`Create issue comment successfully.`);
496
-
497
- setInputComment("");
498
- setCommentsCount((prev) => prev + 1);
499
-
500
- onCreateComment?.(createdIssueComment);
501
-
502
- return localComments.concat([createdIssueComment]);
503
- } else {
504
- setAlert(`Create issue comment failed: ${createIssueCommentRes}`);
505
- logger.e(`Create issue comment failed:`, createIssueCommentRes);
506
- return localComments;
507
- }
508
- },
509
- {
510
- manual: true,
511
- ready: !!owner && !!repo && !!issue && !!inputComment,
512
- },
513
- );
514
-
515
- useEffect(() => {
516
- setComments([]);
517
- setCommentsCount(issue?.comments ?? 0);
518
- setCommentsCursor("");
519
- setCommentsPage(1);
520
- setLocalComments([]);
521
-
522
- setTimeout(() => {
523
- runGetComments();
524
- });
525
- }, [issue, runGetComments, setComments, setLocalComments]);
526
-
527
- useEffect(() => {
528
- setComments([]);
529
- setCommentsCursor("");
530
- setCommentsPage(1);
531
-
532
- setTimeout(() => {
533
- runGetComments();
534
- });
535
- }, [user, commentsPagerDirection, setComments, runGetComments]);
536
-
537
- /** sorted all comments */
538
- const loadedComments = useMemo(() => {
539
- const _loadedComments: CommentType[] = [];
540
-
541
- // filter duplicate comments if exist
542
- const commentIdsSet = new Set();
543
- for (const comment of comments.concat(localComments)) {
544
- if (!commentIdsSet.has(comment.id)) {
545
- commentIdsSet.add(comment.id);
546
- _loadedComments.push(comment);
547
- }
548
- }
549
-
550
- if (!!user && commentsPagerDirection === "last") {
551
- // sort comments by date DESC
552
- _loadedComments.reverse();
553
- }
554
-
555
- return _loadedComments;
556
- }, [comments, commentsPagerDirection, localComments, user]);
557
-
558
- useEffect(() => {
559
- updateCountCallback?.(commentsCount);
560
- }, [commentsCount, updateCountCallback]);
561
-
562
- const { loading: likeOrDislikeCommentLoading, run: runLikeOrDislikeComment } =
563
- useRequest(
564
- async (like: boolean, commentId: number, reactionId?: number) => {
565
- const deletedReactionId = reactionId;
566
- let createdReactionId: number = -1;
567
- if (like) {
568
- const likeCommentRes = await octokit.request(
569
- "POST /repos/{owner}/{repo}/issues/comments/{comment_id}/reactions",
570
- {
571
- owner,
572
- repo,
573
- comment_id: commentId,
574
- content: "heart",
575
- },
576
- );
577
-
578
- if (likeCommentRes.status === 201) {
579
- logger.s(`You like the comment!`);
580
- createdReactionId = likeCommentRes.data.id;
581
- } else if (likeCommentRes.status === 200) {
582
- logger.i(`You already liked the comment!`);
583
- } else {
584
- logger.e(`Failed to like the comment.`);
585
- return;
586
- }
587
- } else {
588
- if (reactionId) {
589
- const dislikeCommentRes = await octokit.request(
590
- "DELETE /repos/{owner}/{repo}/issues/comments/{comment_id}/reactions/{reaction_id}",
591
- {
592
- owner,
593
- repo,
594
- comment_id: commentId,
595
- reaction_id: reactionId,
596
- },
597
- );
598
-
599
- if (dislikeCommentRes.status === 204) {
600
- logger.s(`You unlike the comment.`);
601
- } else {
602
- logger.e(`Failed to unlike the comment.`);
603
- return;
604
- }
605
- } else {
606
- logger.e("Reaction ID is not provided.");
607
- return;
608
- }
609
- }
610
-
611
- let isLocalComment = false;
612
- let targetComment = comments.find(
613
- (comment) => comment.id === commentId,
614
- );
615
- if (!targetComment) {
616
- targetComment = localComments.find(
617
- (comment) => comment.id === commentId,
618
- );
619
- isLocalComment = true;
620
- }
621
- if (targetComment) {
622
- const username = (user as UserType).login;
623
-
624
- const prevHeartCount = targetComment.reactions?.heart ?? 0;
625
- const newHeartCount = like ? prevHeartCount + 1 : prevHeartCount - 1;
626
-
627
- const prevHeartNodes = targetComment.reactionsHeart?.nodes ?? [];
628
- const newHeartNodes = like
629
- ? prevHeartNodes.concat([
630
- {
631
- databaseId: createdReactionId,
632
- user: {
633
- login: username,
634
- },
635
- },
636
- ])
637
- : prevHeartNodes.filter(
638
- (node) => node.databaseId !== deletedReactionId,
639
- );
640
-
641
- targetComment.reactions = {
642
- heart: newHeartCount,
643
- };
644
- targetComment.reactionsHeart = {
645
- totalCount: newHeartCount,
646
- viewerHasReacted: like,
647
- nodes: newHeartNodes,
648
- };
649
- }
650
- if (isLocalComment) {
651
- setLocalComments((prev) => [...(prev ?? [])]);
652
- } else {
653
- setComments((prev) => [...(prev ?? [])]);
654
- }
655
- },
656
- {
657
- manual: true,
658
- ready: !!owner && !!repo && !!user,
659
- },
660
- );
661
-
662
- const initialized = useMemo(
663
- () =>
664
- !getAccessTokenLoading &&
665
- !getUserLoading &&
666
- !createIssueLoading &&
667
- !getIssueLoading,
668
- [
669
- createIssueLoading,
670
- getAccessTokenLoading,
671
- getIssueLoading,
672
- getUserLoading,
673
- ],
674
- );
675
-
676
- const issueCreated = useMemo(
677
- () => initialized && !!issue,
678
- [initialized, issue],
679
- );
680
-
681
- const onLogin = () => {
682
- const url = getAuthorizeUrl(clientID);
683
- window.location.href = url;
684
- };
685
-
686
- const onLogout = () => {
687
- setAccessToken(undefined);
688
- localStorage.removeItem(ACCESS_TOKEN_KEY);
689
- setUser(undefined);
690
- };
691
-
692
- const onCommentInputFocus: React.FocusEventHandler<HTMLTextAreaElement> = (
693
- e,
694
- ) => {
695
- if (!distractionFreeMode) return e.preventDefault();
696
- setIsInputFocused(true);
697
- };
698
-
699
- const onCommentInputBlur: React.FocusEventHandler<HTMLTextAreaElement> = (
700
- e,
701
- ) => {
702
- if (!distractionFreeMode) return e.preventDefault();
703
- setIsInputFocused(false);
704
- };
705
-
706
- useEffect(() => {
707
- const textarea = textareaRef.current;
708
- if (textarea) {
709
- textarea.style.height = "auto";
710
- textarea.style.height = `${textarea.scrollHeight + 2}px`;
711
- }
712
- }, [inputComment]);
713
-
714
- const onCommentInputKeyDown: React.KeyboardEventHandler<
715
- HTMLTextAreaElement
716
- > = (e) => {
717
- if (enableHotKey && (e.metaKey || e.ctrlKey) && e.keyCode === 13) {
718
- runCreateIssueComment();
719
- }
720
- };
721
-
722
- const onReplyComment: CommentProps["onReply"] = useCallback(
723
- (repliedComment) => {
724
- const { body: repliedCommentBody = "", user: repliedCommentUser } =
725
- repliedComment;
726
- let repliedCommentBodyArray = repliedCommentBody.split("\n");
727
- const repliedCommentUsername = repliedCommentUser?.login;
728
-
729
- if (repliedCommentUsername) {
730
- repliedCommentBodyArray.unshift(`@${repliedCommentUsername}`);
731
- }
732
- repliedCommentBodyArray = repliedCommentBodyArray.map(
733
- (text) => `> ${text}`,
734
- );
735
-
736
- setInputComment((prevComment) => {
737
- if (prevComment) {
738
- repliedCommentBodyArray.unshift("", "");
739
- }
740
- repliedCommentBodyArray.push("");
741
- const newComment = `${prevComment}${repliedCommentBodyArray.join("\n")}`;
742
- return newComment;
743
- });
744
-
745
- setTimeout(() => {
746
- textareaRef.current?.focus();
747
- });
748
- },
749
- [],
750
- );
751
-
752
- if (!isModernBrowser) {
753
- logger.e(
754
- `Gitalk React can only be rendered well in modern browser that supports CSS variables and ES2020.`,
755
- `If you have compatibility requirements, please consider using the original project which is compatible with older browsers: https://github.com/gitalk/gitalk`,
756
- );
757
- return null;
758
- }
759
-
760
- if (!(clientID && clientSecret)) {
761
- logger.e(
762
- `You must specify the \`clientId\` and \`clientSecret\` of Github APP`,
763
- );
764
- return null;
765
- }
766
-
767
- if (!(repo && owner)) {
768
- logger.e(`You must specify the \`owner\` and \`repo\` of Github`);
769
- return null;
770
- }
771
-
772
- if (!(Array.isArray(admin) && admin.length > 0)) {
773
- logger.e(`You must specify the \`admin\` for the Github repository`);
774
- return null;
775
- }
776
-
777
- return (
778
- <I18nContext.Provider value={i18nContextValue}>
779
- <div
780
- className={cn(
781
- "gt-container",
782
- { "gt-input-focused": isInputFocused },
783
- className,
784
- )}
785
- {...restProps}
786
- >
787
- {/* Alert */}
788
- {alert && <div className="gt-error">{alert}</div>}
789
-
790
- {initialized ? (
791
- issueCreated ? (
792
- <>
793
- {/* Meta */}
794
- <Meta
795
- issue={issue}
796
- user={user}
797
- commentsCount={commentsCount}
798
- pagerDirection={commentsPagerDirection}
799
- onPagerDirectionChange={setCommentsPagerDirection}
800
- onLogin={onLogin}
801
- onLogout={onLogout}
802
- />
803
-
804
- {/* Comment textarea */}
805
- <CommentTextarea
806
- ref={textareaRef}
807
- value={inputComment}
808
- onChange={(e) => setInputComment(e.target.value)}
809
- onFocus={onCommentInputFocus}
810
- onBlur={onCommentInputBlur}
811
- onKeyDown={onCommentInputKeyDown}
812
- placeholder={polyglot.t("leave-a-comment")}
813
- octokit={octokit}
814
- user={user}
815
- onLogin={onLogin}
816
- onCreateComment={async () => {
817
- await runCreateIssueComment();
818
- }}
819
- createCommentLoading={createIssueCommentLoading}
820
- onPreviewError={(e) => {
821
- setAlert(`Preview rendered comment failed: ${e}`);
822
- logger.e(`Preview rendered comment failed:`, e);
823
- }}
824
- />
825
-
826
- {/* Comments */}
827
- <CommentsList
828
- comments={loadedComments}
829
- commentsCount={commentsCount}
830
- onGetComments={runGetComments}
831
- getCommentsLoading={getCommentsLoading}
832
- flipMoveOptions={flipMoveOptions}
833
- user={user}
834
- admin={admin}
835
- onReply={onReplyComment}
836
- onLike={runLikeOrDislikeComment}
837
- likeLoading={likeOrDislikeCommentLoading}
838
- collapsedHeight={collapsedHeight}
839
- highlightAdminComment={highlightAdminComment}
840
- />
841
- </>
842
- ) : (
843
- // Issue not created placeholder
844
- <div className="gt-no-init" key="no-init">
845
- <p
846
- dangerouslySetInnerHTML={{
847
- __html: polyglot.t("no-found-related", {
848
- link: `<a href="https://github.com/${owner}/${repo}/issues" target="_blank" rel="noopener noreferrer">Issues</a>`,
849
- }),
850
- }}
851
- />
852
- <p>
853
- {polyglot.t("please-contact", {
854
- user: admin.map((u) => `@${u}`).join(" "),
855
- })}
856
- </p>
857
- {isAdmin ? (
858
- <p>
859
- <Button
860
- onClick={runCreateIssue}
861
- isLoading={createIssueLoading}
862
- text={polyglot.t("init-issue")}
863
- />
864
- </p>
865
- ) : null}
866
- {!user && (
867
- <Button
868
- className="gt-btn-login"
869
- onClick={onLogin}
870
- text={polyglot.t("login-with-github")}
871
- />
872
- )}
873
- </div>
874
- )
875
- ) : (
876
- // Loading issue placeholder
877
- <div className="gt-initing">
878
- <i className="gt-loader" />
879
- <p className="gt-initing-text">{polyglot.t("init")}</p>
880
- </div>
881
- )}
882
- </div>
883
- </I18nContext.Provider>
884
- );
885
- };
886
-
887
- export type { GitalkProps };
888
-
889
- export { DEFAULT_FLIP_MOVE_OPTIONS };
890
-
891
- export default Gitalk;