gitalk-react 1.0.0-beta.5 → 1.0.0-beta.7

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.
package/lib/gitalk.tsx CHANGED
@@ -2,35 +2,31 @@ import "./i18n";
2
2
 
3
3
  import { useRequest } from "ahooks";
4
4
  import React, {
5
- forwardRef,
6
5
  useCallback,
7
6
  useEffect,
8
7
  useMemo,
9
8
  useRef,
10
9
  useState,
11
10
  } from "react";
12
- import FlipMove from "react-flip-move";
13
11
 
14
- import ArrowDown from "./assets/arrow-down.svg?raw";
15
- import Github from "./assets/github.svg?raw";
16
- import Tip from "./assets/tip.svg?raw";
17
- import Action from "./components/action";
18
- import Avatar from "./components/avatar";
19
12
  import Button from "./components/button";
20
- import Comment, { type CommentProps } from "./components/comment";
21
- import Svg from "./components/svg";
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";
22
17
  import {
23
18
  ACCESS_TOKEN_KEY,
24
19
  DATE_FNS_LOCALE_MAP,
20
+ DEFAULT_FLIP_MOVE_OPTIONS,
25
21
  DEFAULT_LABELS,
22
+ DEFAULT_PROXY,
26
23
  DEFAULT_USER,
27
- HOMEPAGE,
28
- VERSION,
29
24
  } from "./constants";
30
- import I18nContext from "./contexts/I18nContext";
25
+ import I18nContext, { type I18nContextValue } from "./contexts/I18nContext";
31
26
  import i18n, { type Lang } from "./i18n";
32
27
  import type {
33
28
  Comment as CommentType,
29
+ GitalkProps,
34
30
  Issue as IssueType,
35
31
  User as UserType,
36
32
  } from "./interfaces";
@@ -41,168 +37,9 @@ import {
41
37
  import getOctokitInstance from "./services/request";
42
38
  import { getAccessToken, getAuthorizeUrl } from "./services/user";
43
39
  import { supportsCSSVariables, supportsES2020 } from "./utils/compatibility";
44
- import { hasClassInParent } from "./utils/dom";
45
40
  import logger from "./utils/logger";
46
41
  import { parseSearchQuery, stringifySearchQuery } from "./utils/query";
47
42
 
48
- export interface GitalkProps
49
- extends Omit<
50
- React.DetailedHTMLProps<
51
- React.HTMLAttributes<HTMLDivElement>,
52
- HTMLDivElement
53
- >,
54
- "id" | "title"
55
- > {
56
- /**
57
- * GitHub Application Client ID.
58
- */
59
- clientID: string;
60
- /**
61
- * GitHub Application Client Secret.
62
- */
63
- clientSecret: string;
64
- /**
65
- * GitHub repository owner.
66
- * Can be personal user or organization.
67
- */
68
- owner: string;
69
- /**
70
- * Name of Github repository.
71
- */
72
- repo: string;
73
- /**
74
- * GitHub repository owner and collaborators.
75
- * (Users who having write access to this repository)
76
- */
77
- admin: string[];
78
- /**
79
- * The unique id of the page.
80
- * Length must less than 50.
81
- *
82
- * @default location.host + location.pathname
83
- */
84
- id?: string;
85
- /**
86
- * The issue ID of the page.
87
- * If the number attribute is not defined, issue will be located using id.
88
- */
89
- number?: number;
90
- /**
91
- * GitHub issue labels.
92
- *
93
- * @default ['Gitalk']
94
- */
95
- labels?: string[];
96
- /**
97
- * GitHub issue title.
98
- *
99
- * @default document.title
100
- */
101
- title?: string;
102
- /**
103
- * GitHub issue body.
104
- *
105
- * @default location.href + header.meta[description]
106
- */
107
- body?: string;
108
- /**
109
- * Localization language key.
110
- *
111
- * @default navigator.language
112
- */
113
- language?: Lang;
114
- /**
115
- * Pagination size, with maximum 100.
116
- *
117
- * @default 10
118
- */
119
- perPage?: number;
120
- /**
121
- * Comment sorting direction.
122
- * Available values are `last` and `first`.
123
- *
124
- * @default "last"
125
- */
126
- pagerDirection?: "last" | "first";
127
- /**
128
- * By default, Gitalk will create a corresponding github issue for your every single page automatically when the logined user is belong to the admin users.
129
- * You can create it manually by setting this option to true.
130
- *
131
- * @default false
132
- */
133
- createIssueManually?: boolean;
134
- /**
135
- * Enable hot key (cmd|ctrl + enter) submit comment.
136
- *
137
- * @default true
138
- */
139
- enableHotKey?: boolean;
140
- /**
141
- * Facebook-like distraction free mode.
142
- *
143
- * @default false
144
- */
145
- distractionFreeMode?: boolean;
146
- /**
147
- * Comment list animation.
148
- *
149
- * @default
150
- * ```ts
151
- * {
152
- * staggerDelayBy: 150,
153
- * appearAnimation: 'accordionVertical',
154
- * enterAnimation: 'accordionVertical',
155
- * leaveAnimation: 'accordionVertical',
156
- * }
157
- * ```
158
- * @link https://github.com/joshwcomeau/react-flip-move/blob/master/documentation/enter_leave_animations.md
159
- */
160
- flipMoveOptions?: FlipMove.FlipMoveProps;
161
- /**
162
- * GitHub oauth request reverse proxy for CORS.
163
- * [Why need this?](https://github.com/isaacs/github/issues/330)
164
- *
165
- * @default "https://cors-anywhere.azm.workers.dev/https://github.com/login/oauth/access_token"
166
- */
167
- proxy?: string;
168
- /**
169
- * Default user field if comments' author is not provided
170
- *
171
- * @default
172
- * ```ts
173
- * {
174
- * avatar_url: "//avatars1.githubusercontent.com/u/29697133?s=50",
175
- * login: "null",
176
- * html_url: ""
177
- * }
178
- */
179
- defaultUser?: CommentType["user"];
180
- /**
181
- * Default user field if comments' author is not provided
182
- *
183
- * @deprecated use `defaultUser`
184
- */
185
- defaultAuthor?: IssueCommentsQLResponse["repository"]["issue"]["comments"]["nodes"][number]["author"];
186
- /**
187
- * Callback method invoked when updating the number of comments.
188
- *
189
- * @param count comments number
190
- */
191
- updateCountCallback?: (count: number) => void;
192
- /**
193
- * Callback method invoked when a new issue is successfully created.
194
- *
195
- * @param issue created issue
196
- */
197
- onCreateIssue?: (issue: IssueType) => void;
198
- /**
199
- * Callback method invoked when a new comment is successfully created.
200
- *
201
- * @param comment created comment
202
- */
203
- onCreateComment?: (comment: CommentType) => void;
204
- }
205
-
206
43
  const isModernBrowser = supportsCSSVariables() && supportsES2020();
207
44
 
208
45
  const Gitalk: React.FC<GitalkProps> = (props) => {
@@ -227,15 +64,11 @@ const Gitalk: React.FC<GitalkProps> = (props) => {
227
64
  createIssueManually = false,
228
65
  enableHotKey = true,
229
66
  distractionFreeMode = false,
230
- flipMoveOptions = {
231
- staggerDelayBy: 150,
232
- appearAnimation: "accordionVertical",
233
- enterAnimation: "accordionVertical",
234
- leaveAnimation: "accordionVertical",
235
- },
236
- proxy = "https://cors-anywhere.azm.workers.dev/https://github.com/login/oauth/access_token",
67
+ flipMoveOptions = DEFAULT_FLIP_MOVE_OPTIONS,
68
+ proxy = DEFAULT_PROXY,
237
69
  defaultUser: propsDefaultUser,
238
70
  defaultAuthor: propsDefaultAuthor,
71
+ collapsedHeight: propsCollapsedHeight,
239
72
  updateCountCallback,
240
73
  onCreateIssue,
241
74
  onCreateComment,
@@ -258,7 +91,6 @@ const Gitalk: React.FC<GitalkProps> = (props) => {
258
91
  const textareaRef = useRef<HTMLTextAreaElement>(null);
259
92
  const [inputComment, setInputComment] = useState<string>("");
260
93
  const [isInputFocused, setIsInputFocused] = useState<boolean>(false);
261
- const [isPreviewComment, setIsPreviewComment] = useState<boolean>(false);
262
94
 
263
95
  const [commentsCount, setCommentsCount] = useState<number>(0);
264
96
  const [commentsCursor, setCommentsCursor] = useState("");
@@ -267,7 +99,7 @@ const Gitalk: React.FC<GitalkProps> = (props) => {
267
99
  () => (propsPerPage > 100 ? 100 : propsPerPage < 0 ? 10 : propsPerPage),
268
100
  [propsPerPage],
269
101
  );
270
- const [commentsLoaded, setCommentsLoaded] = useState<boolean>(false);
102
+ /** Current sort order, have effect when user is logged */
271
103
  const [commentsPagerDirection, setCommentsPagerDirection] =
272
104
  useState(pagerDirection);
273
105
  const defaultUser = useMemo(
@@ -283,12 +115,22 @@ const Gitalk: React.FC<GitalkProps> = (props) => {
283
115
  : DEFAULT_USER,
284
116
  [propsDefaultAuthor, propsDefaultUser],
285
117
  );
286
-
287
- const [showPopup, setShowPopup] = useState<boolean>(false);
118
+ const collapsedHeight =
119
+ propsCollapsedHeight && propsCollapsedHeight > 0
120
+ ? propsCollapsedHeight
121
+ : undefined;
288
122
 
289
123
  const [alert, setAlert] = useState<string>("");
290
124
 
291
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
+ );
292
134
 
293
135
  const {
294
136
  data: accessToken = localStorage.getItem(ACCESS_TOKEN_KEY) ?? undefined,
@@ -490,6 +332,7 @@ const Gitalk: React.FC<GitalkProps> = (props) => {
490
332
  const {
491
333
  data: comments = [],
492
334
  mutate: setComments,
335
+ run: runGetComments,
493
336
  loading: getCommentsLoading,
494
337
  } = useRequest(
495
338
  async (): Promise<CommentType[]> => {
@@ -500,7 +343,7 @@ const Gitalk: React.FC<GitalkProps> = (props) => {
500
343
  if (user) {
501
344
  // Get comments via GraphQL, witch requires being logged and able to sort
502
345
  const query = getIssueCommentsQL({
503
- pagerDirection,
346
+ pagerDirection: commentsPagerDirection,
504
347
  });
505
348
 
506
349
  const getIssueCommentsRes: IssueCommentsQLResponse =
@@ -540,11 +383,14 @@ const Gitalk: React.FC<GitalkProps> = (props) => {
540
383
  _comments,
541
384
  );
542
385
 
543
- if (_comments.length < commentsPerPage) {
544
- setCommentsLoaded(true);
545
- }
386
+ const commentsPageInfo =
387
+ getIssueCommentsRes.repository.issue.comments.pageInfo;
388
+ const commentsPageCursor =
389
+ commentsPageInfo.startCursor || commentsPageInfo.endCursor || "";
390
+ setCommentsCursor(commentsPageCursor);
546
391
 
547
- if (pagerDirection === "last") return [..._comments, ...comments];
392
+ if (commentsPagerDirection === "last")
393
+ return [..._comments, ...comments];
548
394
  else return [...comments, ..._comments];
549
395
  } else {
550
396
  setAlert(
@@ -589,9 +435,7 @@ const Gitalk: React.FC<GitalkProps> = (props) => {
589
435
  _comments,
590
436
  );
591
437
 
592
- if (_comments.length < commentsPerPage) {
593
- setCommentsLoaded(true);
594
- }
438
+ setCommentsPage((prev) => prev + 1);
595
439
 
596
440
  return [...comments, ..._comments];
597
441
  } else {
@@ -608,8 +452,8 @@ const Gitalk: React.FC<GitalkProps> = (props) => {
608
452
  return comments;
609
453
  },
610
454
  {
611
- ready: !!owner && !!repo && !!issue && !getUserLoading && !commentsLoaded,
612
- refreshDeps: [commentsPage, issue, user, pagerDirection],
455
+ manual: true,
456
+ ready: !!owner && !!repo && !!issue && !getUserLoading,
613
457
  },
614
458
  );
615
459
 
@@ -617,7 +461,7 @@ const Gitalk: React.FC<GitalkProps> = (props) => {
617
461
  data: localComments = [],
618
462
  mutate: setLocalComments,
619
463
  loading: createIssueCommentLoading,
620
- run: runCreateIssueComment,
464
+ runAsync: runCreateIssueComment,
621
465
  } = useRequest(
622
466
  async (): Promise<CommentType[]> => {
623
467
  const { number: currentIssueNumber } = issue as IssueType;
@@ -649,6 +493,8 @@ const Gitalk: React.FC<GitalkProps> = (props) => {
649
493
  logger.s(`Create issue comment successfully.`);
650
494
 
651
495
  setInputComment("");
496
+ setCommentsCount((prev) => prev + 1);
497
+
652
498
  onCreateComment?.(createdIssueComment);
653
499
 
654
500
  return localComments.concat([createdIssueComment]);
@@ -666,63 +512,50 @@ const Gitalk: React.FC<GitalkProps> = (props) => {
666
512
 
667
513
  useEffect(() => {
668
514
  setComments([]);
669
- setCommentsCount(0);
515
+ setCommentsCount(issue?.comments ?? 0);
670
516
  setCommentsCursor("");
671
517
  setCommentsPage(1);
672
- setCommentsLoaded(false);
673
518
  setLocalComments([]);
674
519
 
675
- if (issue) {
676
- setCommentsCount(issue.comments);
677
- }
678
- }, [issue, user, pagerDirection, setComments, setLocalComments]);
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]);
679
534
 
680
535
  /** sorted all comments */
681
- const allComments = useMemo(() => {
682
- const _allComments = comments.concat(localComments);
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
+ }
683
547
 
684
- if (commentsPagerDirection === "last" && !!user) {
548
+ if (!!user && commentsPagerDirection === "last") {
685
549
  // sort comments by date DESC
686
- _allComments.reverse();
550
+ _loadedComments.reverse();
687
551
  }
688
552
 
689
- return _allComments;
553
+ return _loadedComments;
690
554
  }, [comments, commentsPagerDirection, localComments, user]);
691
555
 
692
- const allCommentsCount = commentsCount + (localComments ?? []).length;
693
-
694
556
  useEffect(() => {
695
- updateCountCallback?.(allCommentsCount);
696
- }, [allCommentsCount, updateCountCallback]);
697
-
698
- const {
699
- data: commentHtml = "",
700
- mutate: setCommentHtml,
701
- loading: getCommentHtmlLoading,
702
- run: runGetCommentHtml,
703
- cancel: cancelGetCommentHtml,
704
- } = useRequest(
705
- async () => {
706
- const getPreviewedHtmlRes = await octokit.request("POST /markdown", {
707
- text: inputComment,
708
- });
709
-
710
- if (getPreviewedHtmlRes.status === 200) {
711
- const _commentHtml = getPreviewedHtmlRes.data;
712
- return _commentHtml;
713
- } else {
714
- setAlert(`Preview rendered comment failed: ${getPreviewedHtmlRes}`);
715
- logger.e(`Preview rendered comment failed:`, getPreviewedHtmlRes);
716
- return "";
717
- }
718
- },
719
- {
720
- manual: true,
721
- onBefore: () => {
722
- setCommentHtml("");
723
- },
724
- },
725
- );
557
+ updateCountCallback?.(commentsCount);
558
+ }, [commentsCount, updateCountCallback]);
726
559
 
727
560
  const { loading: likeOrDislikeCommentLoading, run: runLikeOrDislikeComment } =
728
561
  useRequest(
@@ -843,29 +676,6 @@ const Gitalk: React.FC<GitalkProps> = (props) => {
843
676
  [initialized, issue],
844
677
  );
845
678
 
846
- const hidePopup = useCallback((e: MouseEvent) => {
847
- const target = e.target as HTMLElement;
848
- if (target && hasClassInParent(target, "gt-user", "gt-popup")) {
849
- return;
850
- }
851
- document.removeEventListener("click", hidePopup);
852
- setShowPopup(false);
853
- }, []);
854
-
855
- const onShowOrHidePopup: React.MouseEventHandler<HTMLDivElement> = (e) => {
856
- e.preventDefault();
857
- e.stopPropagation();
858
-
859
- setShowPopup((visible) => {
860
- if (visible) {
861
- document.removeEventListener("click", hidePopup);
862
- } else {
863
- document.addEventListener("click", hidePopup);
864
- }
865
- return !visible;
866
- });
867
- };
868
-
869
679
  const onLogin = () => {
870
680
  const url = getAuthorizeUrl(clientID);
871
681
  window.location.href = url;
@@ -891,6 +701,14 @@ const Gitalk: React.FC<GitalkProps> = (props) => {
891
701
  setIsInputFocused(false);
892
702
  };
893
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
+
894
712
  const onCommentInputKeyDown: React.KeyboardEventHandler<
895
713
  HTMLTextAreaElement
896
714
  > = (e) => {
@@ -899,41 +717,35 @@ const Gitalk: React.FC<GitalkProps> = (props) => {
899
717
  }
900
718
  };
901
719
 
902
- const onCommentInputPreview: React.MouseEventHandler<
903
- HTMLButtonElement
904
- > = () => {
905
- if (isPreviewComment) {
906
- setIsPreviewComment(false);
907
- cancelGetCommentHtml();
908
- } else {
909
- setIsPreviewComment(true);
910
- runGetCommentHtml();
911
- }
912
- };
913
-
914
- const onReplyComment: CommentProps["onReply"] = (repliedComment) => {
915
- const { body: repliedCommentBody = "", user: repliedCommentUser } =
916
- repliedComment;
917
- let repliedCommentBodyArray = repliedCommentBody.split("\n");
918
- const repliedCommentUsername = repliedCommentUser?.login;
919
-
920
- if (repliedCommentUsername) {
921
- repliedCommentBodyArray.unshift(`@${repliedCommentUsername}`);
922
- }
923
- repliedCommentBodyArray = repliedCommentBodyArray.map(
924
- (text) => `> ${text}`,
925
- );
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;
926
726
 
927
- if (inputComment) {
928
- repliedCommentBodyArray.unshift("", "");
929
- }
727
+ if (repliedCommentUsername) {
728
+ repliedCommentBodyArray.unshift(`@${repliedCommentUsername}`);
729
+ }
730
+ repliedCommentBodyArray = repliedCommentBodyArray.map(
731
+ (text) => `> ${text}`,
732
+ );
930
733
 
931
- repliedCommentBodyArray.push("", "");
734
+ setInputComment((prevComment) => {
735
+ if (prevComment) {
736
+ repliedCommentBodyArray.unshift("", "");
737
+ }
738
+ repliedCommentBodyArray.push("", "");
739
+ const newComment = `${prevComment}${repliedCommentBodyArray.join("\n")}`;
740
+ return newComment;
741
+ });
932
742
 
933
- const newComment = `${inputComment}${repliedCommentBodyArray.join("\n")}`;
934
- setInputComment(newComment);
935
- textareaRef.current?.focus();
936
- };
743
+ setTimeout(() => {
744
+ textareaRef.current?.focus();
745
+ });
746
+ },
747
+ [],
748
+ );
937
749
 
938
750
  if (!isModernBrowser) {
939
751
  logger.e(
@@ -960,284 +772,110 @@ const Gitalk: React.FC<GitalkProps> = (props) => {
960
772
  return null;
961
773
  }
962
774
 
963
- const renderInitializing = () => {
964
- return (
965
- <div className="gt-initing">
966
- <i className="gt-loader" />
967
- <p className="gt-initing-text">{polyglot.t("init")}</p>
968
- </div>
969
- );
970
- };
971
-
972
- const renderIssueNotInitialized = () => {
973
- return (
974
- <div className="gt-no-init" key="no-init">
975
- <p
976
- dangerouslySetInnerHTML={{
977
- __html: polyglot.t("no-found-related", {
978
- link: `<a href="https://github.com/${owner}/${repo}/issues" target="_blank" rel="noopener noreferrer">Issues</a>`,
979
- }),
980
- }}
981
- />
982
- <p>
983
- {polyglot.t("please-contact", {
984
- user: admin.map((u) => `@${u}`).join(" "),
985
- })}
986
- </p>
987
- {isAdmin ? (
988
- <p>
989
- <Button
990
- onClick={runCreateIssue}
991
- isLoading={createIssueLoading}
992
- text={polyglot.t("init-issue")}
993
- />
994
- </p>
995
- ) : null}
996
- {!user && (
997
- <Button
998
- className="gt-btn-login"
999
- onClick={onLogin}
1000
- text={polyglot.t("login-with-github")}
1001
- />
1002
- )}
1003
- </div>
1004
- );
1005
- };
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>}
1006
783
 
1007
- const renderHeader = () => {
1008
- return (
1009
- <div className="gt-header" key="header">
1010
- {user ? (
1011
- <Avatar
1012
- className="gt-header-avatar"
1013
- src={user.avatar_url}
1014
- alt={user.login}
1015
- href={user.html_url}
1016
- />
1017
- ) : (
1018
- <a className="gt-avatar-github" onClick={onLogin}>
1019
- <Svg className="gt-ico-github" icon={Github} />
1020
- </a>
1021
- )}
1022
- <div className="gt-header-comment">
1023
- <textarea
1024
- ref={textareaRef}
1025
- className="gt-header-textarea"
1026
- style={{ display: isPreviewComment ? "none" : undefined }}
1027
- value={inputComment}
1028
- onChange={(e) => setInputComment(e.target.value)}
1029
- onFocus={onCommentInputFocus}
1030
- onBlur={onCommentInputBlur}
1031
- onKeyDown={onCommentInputKeyDown}
1032
- placeholder={polyglot.t("leave-a-comment")}
1033
- />
1034
- <div
1035
- className="gt-header-preview markdown-body"
1036
- style={{ display: isPreviewComment ? undefined : "none" }}
1037
- dangerouslySetInnerHTML={{
1038
- __html: commentHtml,
1039
- }}
1040
- />
1041
- <div className="gt-header-controls">
1042
- <a
1043
- className="gt-header-controls-tip"
1044
- href="https://guides.github.com/features/mastering-markdown/"
1045
- target="_blank"
1046
- rel="noopener noreferrer"
1047
- >
1048
- <Svg
1049
- className="gt-ico-tip"
1050
- icon={Tip}
1051
- text={polyglot.t("support-markdown")}
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}
1052
796
  />
1053
- </a>
1054
-
1055
- <Button
1056
- className="gt-btn-preview gt-btn--secondary"
1057
- onClick={onCommentInputPreview}
1058
- text={
1059
- isPreviewComment ? polyglot.t("edit") : polyglot.t("preview")
1060
- }
1061
- isLoading={getCommentHtmlLoading}
1062
- disabled={false}
1063
- />
1064
-
1065
- {user ? (
1066
- <Button
1067
- className="gt-btn-public"
1068
- onClick={runCreateIssueComment}
1069
- text={polyglot.t("comment")}
1070
- isLoading={createIssueCommentLoading}
1071
- disabled={createIssueCommentLoading || !inputComment}
1072
- />
1073
- ) : (
1074
- <Button
1075
- className="gt-btn-login"
1076
- onClick={onLogin}
1077
- text={polyglot.t("login-with-github")}
1078
- />
1079
- )}
1080
- </div>
1081
- </div>
1082
- </div>
1083
- );
1084
- };
1085
-
1086
- // Why forwardRef? https://www.npmjs.com/package/react-flip-move#usage-with-functional-components
1087
- const CommentWithForwardedRef = forwardRef<
1088
- HTMLDivElement,
1089
- { comment: CommentType }
1090
- >(({ comment }, ref) => {
1091
- const {
1092
- id: commentId,
1093
- user: commentAuthor,
1094
- reactionsHeart: commentReactionsHeart,
1095
- } = comment;
1096
-
1097
- const commentAuthorName = commentAuthor?.login;
1098
- const isAuthor =
1099
- !!user && !!commentAuthorName && user.login === commentAuthorName;
1100
- const isAdmin =
1101
- !!commentAuthorName &&
1102
- !!admin.find(
1103
- (username) =>
1104
- username.toLowerCase() === commentAuthorName.toLowerCase(),
1105
- );
1106
- const heartReactionId = commentReactionsHeart?.nodes.find(
1107
- (node) => node.user.login === user?.login,
1108
- )?.databaseId;
1109
-
1110
- return (
1111
- <div ref={ref}>
1112
- <Comment
1113
- comment={comment}
1114
- isAuthor={isAuthor}
1115
- isAdmin={isAdmin}
1116
- onReply={onReplyComment}
1117
- onLike={(like) => {
1118
- runLikeOrDislikeComment(like, commentId, heartReactionId);
1119
- }}
1120
- likeLoading={likeOrDislikeCommentLoading}
1121
- />
1122
- </div>
1123
- );
1124
- });
1125
797
 
1126
- const renderCommentList = () => {
1127
- return (
1128
- <div className="gt-comments" key="comments">
1129
- <FlipMove {...flipMoveOptions}>
1130
- {allComments.map((comment) => (
1131
- <CommentWithForwardedRef key={comment.id} comment={comment} />
1132
- ))}
1133
- </FlipMove>
1134
- {!allCommentsCount && (
1135
- <p className="gt-comments-null">
1136
- {polyglot.t("first-comment-person")}
1137
- </p>
1138
- )}
1139
- {!commentsLoaded && allCommentsCount ? (
1140
- <div className="gt-comments-controls">
1141
- <Button
1142
- className="gt-btn-loadmore"
1143
- onClick={() => setCommentsPage((prev) => prev + 1)}
1144
- isLoading={getCommentsLoading}
1145
- text={polyglot.t("load-more")}
1146
- />
1147
- </div>
1148
- ) : null}
1149
- </div>
1150
- );
1151
- };
1152
-
1153
- const renderMeta = () => {
1154
- const isDesc = commentsPagerDirection === "last";
798
+ {/* Comment textarea */}
799
+ <CommentTextarea
800
+ value={inputComment}
801
+ onChange={(e) => setInputComment(e.target.value)}
802
+ onFocus={onCommentInputFocus}
803
+ onBlur={onCommentInputBlur}
804
+ onKeyDown={onCommentInputKeyDown}
805
+ placeholder={polyglot.t("leave-a-comment")}
806
+ octokit={octokit}
807
+ user={user}
808
+ onLogin={onLogin}
809
+ onCreateComment={async () => {
810
+ await runCreateIssueComment();
811
+ }}
812
+ createCommentLoading={createIssueCommentLoading}
813
+ onPreviewError={(e) => {
814
+ setAlert(`Preview rendered comment failed: ${e}`);
815
+ logger.e(`Preview rendered comment failed:`, e);
816
+ }}
817
+ />
1155
818
 
1156
- return (
1157
- <div className="gt-meta" key="meta">
1158
- <span
1159
- className="gt-counts"
1160
- dangerouslySetInnerHTML={{
1161
- __html: polyglot.t("counts", {
1162
- counts: `<a class="gt-link gt-link-counts" href="${issue?.html_url}" target="_blank" rel="noopener noreferrer">${allCommentsCount}</a>`,
1163
- smart_count: allCommentsCount,
1164
- }),
1165
- }}
1166
- />
1167
- {showPopup && (
1168
- <div className="gt-popup">
1169
- {user
1170
- ? [
1171
- <Action
1172
- key={"sort-asc"}
1173
- className={`gt-action-sortasc${!isDesc ? " is--active" : ""}`}
1174
- onClick={() => setCommentsPagerDirection("first")}
1175
- text={polyglot.t("sort-asc")}
1176
- />,
1177
- <Action
1178
- key={"sort-desc"}
1179
- className={`gt-action-sortdesc${isDesc ? " is--active" : ""}`}
1180
- onClick={() => setCommentsPagerDirection("last")}
1181
- text={polyglot.t("sort-desc")}
1182
- />,
1183
- ]
1184
- : null}
1185
- {user ? (
1186
- <Action
1187
- className="gt-action-logout"
1188
- onClick={onLogout}
1189
- text={polyglot.t("logout")}
819
+ {/* Comments */}
820
+ <CommentsList
821
+ comments={loadedComments}
822
+ commentsCount={commentsCount}
823
+ onGetComments={runGetComments}
824
+ getCommentsLoading={getCommentsLoading}
825
+ flipMoveOptions={flipMoveOptions}
826
+ user={user}
827
+ admin={admin}
828
+ onReply={onReplyComment}
829
+ onLike={runLikeOrDislikeComment}
830
+ likeLoading={likeOrDislikeCommentLoading}
831
+ collapsedHeight={collapsedHeight}
1190
832
  />
1191
- ) : (
1192
- <a className="gt-action gt-action-login" onClick={onLogin}>
1193
- {polyglot.t("login-with-github")}
1194
- </a>
1195
- )}
1196
- <div className="gt-copyright">
1197
- <a
1198
- className="gt-link gt-link-project"
1199
- href={HOMEPAGE}
1200
- target="_blank"
1201
- rel="noopener noreferrer"
1202
- >
1203
- GitalkR
1204
- </a>
1205
- <span className="gt-version">{VERSION}</span>
833
+ </>
834
+ ) : (
835
+ // Issue not created placeholder
836
+ <div className="gt-no-init" key="no-init">
837
+ <p
838
+ dangerouslySetInnerHTML={{
839
+ __html: polyglot.t("no-found-related", {
840
+ link: `<a href="https://github.com/${owner}/${repo}/issues" target="_blank" rel="noopener noreferrer">Issues</a>`,
841
+ }),
842
+ }}
843
+ />
844
+ <p>
845
+ {polyglot.t("please-contact", {
846
+ user: admin.map((u) => `@${u}`).join(" "),
847
+ })}
848
+ </p>
849
+ {isAdmin ? (
850
+ <p>
851
+ <Button
852
+ onClick={runCreateIssue}
853
+ isLoading={createIssueLoading}
854
+ text={polyglot.t("init-issue")}
855
+ />
856
+ </p>
857
+ ) : null}
858
+ {!user && (
859
+ <Button
860
+ className="gt-btn-login"
861
+ onClick={onLogin}
862
+ text={polyglot.t("login-with-github")}
863
+ />
864
+ )}
1206
865
  </div>
866
+ )
867
+ ) : (
868
+ // Loading issue placeholder
869
+ <div className="gt-initing">
870
+ <i className="gt-loader" />
871
+ <p className="gt-initing-text">{polyglot.t("init")}</p>
1207
872
  </div>
1208
873
  )}
1209
- <div className="gt-user">
1210
- <div
1211
- className={`gt-user-inner${showPopup ? " is--poping" : ""}`}
1212
- onClick={onShowOrHidePopup}
1213
- >
1214
- <span className="gt-user-name">
1215
- {user?.login ?? polyglot.t("anonymous")}
1216
- </span>
1217
- <Svg className="gt-ico-arrdown" icon={ArrowDown} />
1218
- </div>
1219
- </div>
1220
- </div>
1221
- );
1222
- };
1223
-
1224
- return (
1225
- <I18nContext.Provider
1226
- value={{ language, polyglot, dateFnsLocaleMap: DATE_FNS_LOCALE_MAP }}
1227
- >
1228
- <div
1229
- className={`gt-container${isInputFocused ? " gt-input-focused" : ""} ${className}`}
1230
- {...restProps}
1231
- >
1232
- {alert && <div className="gt-error">{alert}</div>}
1233
- {initialized
1234
- ? issueCreated
1235
- ? [renderMeta(), renderHeader(), renderCommentList()]
1236
- : renderIssueNotInitialized()
1237
- : renderInitializing()}
1238
874
  </div>
1239
875
  </I18nContext.Provider>
1240
876
  );
1241
877
  };
1242
878
 
879
+ export type { GitalkProps };
880
+
1243
881
  export default Gitalk;