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