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.
- package/README-zh-CN.md +159 -57
- package/README.md +142 -40
- package/dist/gitalk-dark.css +1 -1
- package/dist/gitalk-light.css +1 -1
- package/dist/gitalk.browser.full.js +102 -0
- package/dist/gitalk.browser.js +94 -0
- package/dist/gitalk.cjs +94 -0
- package/dist/gitalk.d.cts +14 -0
- package/dist/gitalk.d.mts +174 -0
- package/dist/gitalk.js +7427 -12628
- package/package.json +67 -44
- package/proxy-server/.env.example +6 -0
- package/proxy-server/README.md +38 -0
- package/proxy-server/server.mjs +110 -0
- package/dist/gitalk.d.ts +0 -455
- package/dist/gitalk.umd.cjs +0 -123
- package/lib/assets/arrow-down.svg +0 -1
- package/lib/assets/edit.svg +0 -3
- package/lib/assets/github.svg +0 -3
- package/lib/assets/heart-filled.svg +0 -3
- package/lib/assets/heart.svg +0 -3
- package/lib/assets/reply.svg +0 -3
- package/lib/assets/tip.svg +0 -8
- package/lib/components/action.tsx +0 -21
- package/lib/components/avatar.tsx +0 -42
- package/lib/components/button.tsx +0 -35
- package/lib/components/comment-textarea.tsx +0 -164
- package/lib/components/comment.tsx +0 -185
- package/lib/components/comments-list.tsx +0 -106
- package/lib/components/meta.tsx +0 -129
- package/lib/components/svg.tsx +0 -29
- package/lib/constants/index.ts +0 -57
- package/lib/contexts/I18nContext.ts +0 -20
- package/lib/gitalk.tsx +0 -882
- package/lib/i18n/de.json +0 -20
- package/lib/i18n/en.json +0 -20
- package/lib/i18n/es-ES.json +0 -20
- package/lib/i18n/fa.json +0 -20
- package/lib/i18n/fr.json +0 -20
- package/lib/i18n/index.ts +0 -40
- package/lib/i18n/ja.json +0 -20
- package/lib/i18n/ko.json +0 -20
- package/lib/i18n/pl.json +0 -21
- package/lib/i18n/ru.json +0 -20
- package/lib/i18n/zh-CN.json +0 -20
- package/lib/i18n/zh-TW.json +0 -20
- package/lib/interfaces/index.ts +0 -196
- package/lib/services/graphql/comment.ts +0 -84
- package/lib/services/request.ts +0 -24
- package/lib/services/user.ts +0 -40
- package/lib/themes/base.scss +0 -628
- package/lib/themes/gitalk-dark.scss +0 -28
- package/lib/themes/gitalk-light.scss +0 -28
- package/lib/utils/compatibility.ts +0 -35
- package/lib/utils/dom.ts +0 -15
- package/lib/utils/logger.ts +0 -66
- package/lib/utils/query.ts +0 -19
- 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;
|