artalk 2.3.3 → 2.3.4

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "artalk",
3
- "version": "2.3.3",
4
- "backendMinVersion": "2.1.7",
3
+ "version": "2.3.4",
4
+ "backendMinVersion": "2.1.10",
5
5
  "license": "LGPL-3.0",
6
6
  "description": "A Selfhosted Comment System",
7
7
  "keywords": [
@@ -29,7 +29,7 @@
29
29
  "dependencies": {
30
30
  "hanabi": "^0.4.0",
31
31
  "insane": "^2.6.2",
32
- "marked": "^4.0.16",
32
+ "marked": "^4.0.17",
33
33
  "abortcontroller-polyfill": "^1.7.3"
34
34
  },
35
35
  "scripts": {
package/src/api/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { CommentData, ListData, UserData, PageData, SiteData, NotifyData } from '~/types/artalk-data'
2
2
  import ArtalkConfig from '~/types/artalk-config'
3
3
  import Context from '~/types/context'
4
- import { Fetch, ToFormData, POST, GET } from './request'
4
+ import { Fetch, ToFormData, POST } from './request'
5
5
  import * as Utils from '../lib/utils'
6
6
  import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'
7
7
 
@@ -58,7 +58,7 @@ export default class Api {
58
58
  }
59
59
 
60
60
  /** 评论 · 修改 */
61
- public async commentEdit(data: CommentData) {
61
+ public async commentEdit(data: Partial<CommentData>) {
62
62
  const params: any = {
63
63
  ...data,
64
64
  }
@@ -126,6 +126,11 @@ export default class Api {
126
126
  return (data || { is_login: false, is_admin: false }) as { is_login: boolean, is_admin: boolean }
127
127
  }
128
128
 
129
+ /** 用户 · 注销 */
130
+ public async logout() {
131
+ return POST(this.ctx, `${this.baseURL}/logout`)
132
+ }
133
+
129
134
  // ============================
130
135
  // 页面 Page
131
136
  // ============================
@@ -9,6 +9,9 @@ export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit,
9
9
  init.headers = headers
10
10
  }
11
11
 
12
+ // 跨域请求携带 cookie (无需)
13
+ // init.credentials = 'include'
14
+
12
15
  // 请求操作
13
16
  const resp = await timeoutFetch(ctx, input, timeout || ctx.conf.reqTimeout || 15000, init)
14
17
 
@@ -60,19 +63,20 @@ export async function POST<T>(ctx: Context, url: string, data?: {[key: string]:
60
63
  const init: RequestInit = {
61
64
  method: 'POST',
62
65
  }
63
- if (data) init.body = ToFormData(data)
66
+ data = { site_name: ctx.conf.site || '', ...(data || {}) } // 总是携带 site_name
67
+ init.body = ToFormData(data)
64
68
 
65
69
  const json = await Fetch(ctx, url, init)
66
70
  return ((json.data || {}) as T)
67
71
  }
68
72
 
69
- /** 公共 GET 请求 */
70
- export async function GET<T>(ctx: Context, url: string, data?: {[key: string]: any}) {
71
- const json = await Fetch(ctx, url + (data ? (`?${new URLSearchParams(data)}`) : ''), {
72
- method: 'GET',
73
- })
74
- return ((json.data || {}) as T)
75
- }
73
+ /** 公共 GET 请求 (无 GET 方式 API) */
74
+ // export async function GET<T>(ctx: Context, url: string, data?: {[key: string]: any}) {
75
+ // const json = await Fetch(ctx, url + (data ? (`?${new URLSearchParams(data)}`) : ''), {
76
+ // method: 'GET',
77
+ // })
78
+ // return ((json.data || {}) as T)
79
+ // }
76
80
 
77
81
  /** 对象转 FormData */
78
82
  export function ToFormData(object: {[key: string]: any}): FormData {
package/src/artalk.ts CHANGED
@@ -13,7 +13,7 @@ import Editor from './editor'
13
13
  import List from './list'
14
14
  import SidebarLayer from './layer/sidebar-layer'
15
15
 
16
- import Layer, { GetLayerWrap } from './layer'
16
+ import Layer from './layer'
17
17
  import * as Utils from './lib/utils'
18
18
  import * as Ui from './lib/ui'
19
19
  import * as Stat from './lib/stat'
@@ -35,9 +35,11 @@ export default class Artalk {
35
35
  public list!: List
36
36
  public sidebarLayer!: SidebarLayer
37
37
 
38
- protected static Plugins: ArtalkPlug[] = [
39
- Stat.PvCountWidget
40
- ]
38
+ /** Plugins (in global scope) */
39
+ protected static Plugins: ArtalkPlug[] = [ Stat.PvCountWidget ]
40
+
41
+ /** Plugins (in a instance scope) */
42
+ protected instancePlugins: ArtalkPlug[] = []
41
43
 
42
44
  constructor(customConf: Partial<ArtalkConfig>) {
43
45
  /* 初始化基本配置 */
@@ -86,7 +88,7 @@ export default class Artalk {
86
88
  // 事件绑定初始化
87
89
  this.initEventBind()
88
90
 
89
- // 插件初始化
91
+ // 插件初始化 (global scope)
90
92
  Artalk.Plugins.forEach(plugin => {
91
93
  if (typeof plugin === 'function')
92
94
  plugin(this.ctx)
@@ -198,30 +200,6 @@ export default class Artalk {
198
200
  }
199
201
  }
200
202
 
201
- /** 设置暗黑模式 */
202
- public setDarkMode(darkMode: boolean) {
203
- const darkModeClassName = 'atk-dark-mode'
204
-
205
- this.ctx.conf.darkMode = darkMode
206
- this.ctx.trigger('conf-updated')
207
-
208
- if (this.conf.darkMode) {
209
- this.$root.classList.add(darkModeClassName)
210
- } else {
211
- this.$root.classList.remove(darkModeClassName)
212
- }
213
-
214
- // for Layer
215
- const { $wrap: $layerWrap } = GetLayerWrap(this.ctx)
216
- if ($layerWrap) {
217
- if (this.conf.darkMode) {
218
- $layerWrap.classList.add(darkModeClassName)
219
- } else {
220
- $layerWrap.classList.remove(darkModeClassName)
221
- }
222
- }
223
- }
224
-
225
203
  /** 监听事件 */
226
204
  public on<K extends keyof EventPayloadMap>(name: K, handler: Handler<EventPayloadMap[K]>) {
227
205
  this.ctx.on(name, handler, 'external')
@@ -242,11 +220,28 @@ export default class Artalk {
242
220
  this.ctx.listReload()
243
221
  }
244
222
 
245
- /** Use Plugin */
246
- public static Use(plugin: ArtalkPlug) {
223
+ /** 设置暗黑模式 */
224
+ public setDarkMode(darkMode: boolean) {
225
+ this.ctx.setDarkMode(darkMode)
226
+ }
227
+
228
+ /** Use Plugin (specific instance) */
229
+ public use(plugin: ArtalkPlug) {
230
+ this.instancePlugins.push(plugin)
231
+ if (typeof plugin === 'function') plugin(this.ctx)
232
+ }
233
+
234
+ /** Use Plugin (static method for global scope) */
235
+ public static use(plugin: ArtalkPlug) {
247
236
  this.Plugins.push(plugin)
248
237
  }
249
238
 
239
+ /** @deprecated Please replace it with lowercase function name `use(...)`. */
240
+ public static Use(plugin: ArtalkPlug) {
241
+ this.use(plugin)
242
+ console.warn('`Use(...)` is deprecated, replace it with lowercase `use(...)`.')
243
+ }
244
+
250
245
  /** 装载数量统计元素 */
251
246
  public static LoadCountWidget(customConf: Partial<ArtalkConfig>) {
252
247
  const conf = this.HandelBaseConf(customConf)
@@ -214,7 +214,7 @@ export default class CommentRender {
214
214
  }
215
215
  }
216
216
 
217
- // 绑定回复按钮事件
217
+ // 回复按钮
218
218
  if (this.data.is_allow_reply) {
219
219
  const replyBtn = Utils.createElement(`<span>${this.ctx.$t('reply')}</span>`)
220
220
  this.$actions.append(replyBtn)
@@ -228,7 +228,7 @@ export default class CommentRender {
228
228
  })
229
229
  }
230
230
 
231
- // 绑定折叠按钮事件
231
+ // 折叠按钮
232
232
  const collapseBtn = new ActionBtn(this.ctx, {
233
233
  text: () => (this.data.is_collapsed ? this.ctx.$t('expand') : this.ctx.$t('collapse')),
234
234
  adminOnly: true
@@ -238,7 +238,7 @@ export default class CommentRender {
238
238
  this.comment.getActions().adminEdit('collapsed', collapseBtn)
239
239
  })
240
240
 
241
- // 绑定待审核按钮事件
241
+ // 审核按钮
242
242
  const pendingBtn = new ActionBtn(this.ctx, {
243
243
  text: () => (this.data.is_pending ? this.ctx.$t('pending') : this.ctx.$t('approved')),
244
244
  adminOnly: true
@@ -248,7 +248,27 @@ export default class CommentRender {
248
248
  this.comment.getActions().adminEdit('pending', pendingBtn)
249
249
  })
250
250
 
251
- // 绑定删除按钮事件
251
+ // 置顶按钮
252
+ const pinnedBtn = new ActionBtn(this.ctx, {
253
+ text: () => (this.data.is_pinned ? this.ctx.$t('unpin') : this.ctx.$t('pin')),
254
+ adminOnly: true
255
+ })
256
+ pinnedBtn.appendTo(this.$actions)
257
+ pinnedBtn.setClick(() => {
258
+ this.comment.getActions().adminEdit('pinned', pendingBtn)
259
+ })
260
+
261
+ // 编辑按钮
262
+ const editBtn = new ActionBtn(this.ctx, {
263
+ text: this.ctx.$t('edit'),
264
+ adminOnly: true
265
+ })
266
+ editBtn.appendTo(this.$actions)
267
+ editBtn.setClick(() => {
268
+ this.ctx.editComment(this.data, this.$el)
269
+ })
270
+
271
+ // 删除按钮
252
272
  const delBtn = new ActionBtn(this.ctx, {
253
273
  text: this.ctx.$t('delete'),
254
274
  confirm: true,
@@ -259,16 +279,6 @@ export default class CommentRender {
259
279
  delBtn.setClick(() => {
260
280
  this.comment.getActions().adminDelete(delBtn)
261
281
  })
262
-
263
- // 绑定置顶按钮事件
264
- const pinnedBtn = new ActionBtn(this.ctx, {
265
- text: () => (this.data.is_pinned ? this.ctx.$t('unpin') : this.ctx.$t('pin')),
266
- adminOnly: true
267
- })
268
- pinnedBtn.appendTo(this.$actions)
269
- pinnedBtn.setClick(() => {
270
- this.comment.getActions().adminEdit('pinned', pendingBtn)
271
- })
272
282
  }
273
283
 
274
284
  /** 内容限高检测 */
package/src/context.ts CHANGED
@@ -115,6 +115,10 @@ export default class Context implements ContextApi {
115
115
  this.list.insertComment(commentData)
116
116
  }
117
117
 
118
+ public updateComment(commentData: CommentData): void {
119
+ this.list.updateComment(commentData)
120
+ }
121
+
118
122
  public replyComment(commentData: CommentData, $comment: HTMLElement, scroll?: boolean): void {
119
123
  this.editor.setReply(commentData, $comment, scroll)
120
124
  }
@@ -123,6 +127,14 @@ export default class Context implements ContextApi {
123
127
  this.editor.cancelReply()
124
128
  }
125
129
 
130
+ public editComment(commentData: CommentData, $comment: HTMLElement): void {
131
+ this.editor.setEditComment(commentData, $comment)
132
+ }
133
+
134
+ public cancelEditComment(): void {
135
+ this.editor.cancelEditComment()
136
+ }
137
+
126
138
  public updateNotifies(notifies: NotifyData[]): void {
127
139
  this.list.updateUnread(notifies)
128
140
  }
@@ -132,6 +144,10 @@ export default class Context implements ContextApi {
132
144
  this.list.reload()
133
145
  }
134
146
 
147
+ public reload(): void {
148
+ this.listReload()
149
+ }
150
+
135
151
  public listRefreshUI(): void {
136
152
  this.list.refreshUI()
137
153
  }
@@ -234,4 +250,27 @@ export default class Context implements ContextApi {
234
250
 
235
251
  return str
236
252
  }
253
+
254
+ public setDarkMode(darkMode: boolean): void {
255
+ const darkModeClassName = 'atk-dark-mode'
256
+
257
+ this.conf.darkMode = darkMode
258
+ this.trigger('conf-updated')
259
+
260
+ if (this.conf.darkMode) {
261
+ this.$root.classList.add(darkModeClassName)
262
+ } else {
263
+ this.$root.classList.remove(darkModeClassName)
264
+ }
265
+
266
+ // for Layer
267
+ const { $wrap: $layerWrap } = GetLayerWrap(this)
268
+ if ($layerWrap) {
269
+ if (this.conf.darkMode) {
270
+ $layerWrap.classList.add(darkModeClassName)
271
+ } else {
272
+ $layerWrap.classList.remove(darkModeClassName)
273
+ }
274
+ }
275
+ }
237
276
  }
@@ -31,10 +31,18 @@ export default class Editor extends Component {
31
31
  public $submitBtn: HTMLButtonElement
32
32
  public $notifyWrap: HTMLElement
33
33
 
34
+ // Editor 的不同状态
35
+ private isTraveling = false
36
+
37
+ /** 回复评论 */
34
38
  private replyComment: CommentData|null = null
35
39
  private $sendReply: HTMLElement|null = null
40
+ public get isReplyMode() { return this.replyComment !== null }
36
41
 
37
- private isTraveling = false
42
+ /** 编辑评论 */
43
+ private editComment: CommentData|null = null
44
+ private $editCancelBtn: HTMLElement|null = null
45
+ public get isEditMode() { return this.editComment !== null }
38
46
 
39
47
  /** 启用的插件 */
40
48
  private readonly ENABLED_PLUGS = [ EmoticonsPlug, UploadPlug, PreviewPlug, HeaderInputPlug ]
@@ -98,6 +106,8 @@ export default class Editor extends Component {
98
106
  }
99
107
 
100
108
  private onHeaderInput(key: string, $input: HTMLInputElement) {
109
+ if (this.isEditMode) return // 评论编辑模式,不修改个人信息
110
+
101
111
  this.user.update({
102
112
  [key]: $input.value.trim()
103
113
  })
@@ -128,8 +138,13 @@ export default class Editor extends Component {
128
138
  })
129
139
  }
130
140
 
141
+ private refreshSendBtnText() {
142
+ if (this.isEditMode) this.$submitBtn.innerText = this.$t('save')
143
+ else this.$submitBtn.innerText = this.ctx.conf.sendBtn || this.$t('send')
144
+ }
145
+
131
146
  private initSubmitBtn() {
132
- this.$submitBtn.innerText = this.ctx.conf.sendBtn || this.$t('send')
147
+ this.refreshSendBtnText()
133
148
  this.$submitBtn.addEventListener('click', () => (this.submit()))
134
149
  }
135
150
 
@@ -186,21 +201,16 @@ export default class Editor extends Component {
186
201
  }
187
202
  }
188
203
 
189
- public clearEditor() {
190
- this.setContent('')
191
- this.cancelReply()
192
- }
193
-
194
204
  private adjustTextareaHeight() {
195
205
  const diff = this.$textarea.offsetHeight - this.$textarea.clientHeight
196
206
  this.$textarea.style.height = '0px' // it's a magic. 若不加此行,内容减少,高度回不去
197
207
  this.$textarea.style.height = `${this.$textarea.scrollHeight + diff}px`
198
208
  }
199
209
 
210
+ /** 设置回复评论 */
200
211
  public setReply(commentData: CommentData, $comment: HTMLElement, scroll = true) {
201
- if (this.replyComment !== null) {
202
- this.cancelReply()
203
- }
212
+ if (this.isEditMode) this.cancelEditComment()
213
+ if (this.isReplyMode) this.cancelReply()
204
214
 
205
215
  if (this.$sendReply === null) {
206
216
  this.$sendReply = Utils.createElement(`<div class="atk-send-reply">${this.$t('reply')} <span class="atk-text"></span><span class="atk-cancel">×</span></div>`);
@@ -221,6 +231,7 @@ export default class Editor extends Component {
221
231
  this.$textarea.focus()
222
232
  }
223
233
 
234
+ /** 取消回复评论 */
224
235
  public cancelReply() {
225
236
  if (this.$sendReply !== null) {
226
237
  this.$sendReply.remove()
@@ -233,6 +244,53 @@ export default class Editor extends Component {
233
244
  }
234
245
  }
235
246
 
247
+ /** 设置编辑评论 */
248
+ public setEditComment(commentData: CommentData, $comment: HTMLElement) {
249
+ if (this.isEditMode) this.cancelEditComment()
250
+ if (this.isReplyMode) this.cancelReply()
251
+
252
+ if (this.$editCancelBtn === null) {
253
+ this.$editCancelBtn = Utils.createElement(`<div class="atk-send-reply">${this.$t('editCancel')}<span class="atk-cancel">×</span></div>`);
254
+ this.$editCancelBtn.onclick = () => {
255
+ this.cancelEditComment()
256
+ }
257
+ this.$textareaWrap.append(this.$editCancelBtn)
258
+ }
259
+ this.editComment = commentData
260
+
261
+ this.$header.style.display = 'none' // TODO 暂时隐藏
262
+
263
+ this.travel($comment)
264
+
265
+ this.$nick.value = commentData.nick
266
+ this.$email.value = commentData.email || ''
267
+ this.$link.value = commentData.link
268
+
269
+ this.setContent(commentData.content)
270
+ this.$textarea.focus()
271
+
272
+ this.refreshSendBtnText()
273
+ }
274
+
275
+ /** 取消编辑评论 */
276
+ public cancelEditComment() {
277
+ if (this.$editCancelBtn !== null) {
278
+ this.$editCancelBtn.remove()
279
+ this.$editCancelBtn = null
280
+ }
281
+
282
+ this.editComment = null
283
+ this.travelBack()
284
+
285
+ this.$nick.value = this.user.data.nick
286
+ this.$email.value = this.user.data.email
287
+ this.$link.value = this.user.data.link
288
+
289
+ this.setContent('')
290
+ this.refreshSendBtnText()
291
+ this.$header.style.display = '' // TODO
292
+ }
293
+
236
294
  public showNotify(msg: string, type: "i"|"s"|"w"|"e") {
237
295
  Ui.showNotify(this.$notifyWrap, msg, type)
238
296
  }
@@ -245,7 +303,7 @@ export default class Editor extends Component {
245
303
  Ui.hideLoading(this.$el)
246
304
  }
247
305
 
248
- /** 提交评论 */
306
+ /** 点击评论提交按钮事件 */
249
307
  public async submit() {
250
308
  if (this.getFinalContent().trim() === '') {
251
309
  this.$textarea.focus()
@@ -256,6 +314,17 @@ export default class Editor extends Component {
256
314
 
257
315
  this.showLoading()
258
316
 
317
+ if (!this.isEditMode) {
318
+ await this.submitAdd()
319
+ } else {
320
+ await this.submitEdit()
321
+ }
322
+
323
+ this.hideLoading()
324
+ }
325
+
326
+ /** 创建评论 */
327
+ public async submitAdd() {
259
328
  try {
260
329
  const nComment = await this.ctx.getApi().add({
261
330
  content: this.getFinalContent(),
@@ -274,14 +343,40 @@ export default class Editor extends Component {
274
343
  }
275
344
 
276
345
  this.ctx.insertComment(nComment)
277
- this.clearEditor() // 清空编辑器
346
+
347
+ // 复原编辑器
348
+ this.setContent('')
349
+ this.cancelReply()
350
+
351
+ this.ctx.trigger('editor-submitted')
352
+ } catch (err: any) {
353
+ console.error(err)
354
+ this.showNotify(`${this.$t('commentFail')},${err.msg || String(err)}`, 'e')
355
+ }
356
+ }
357
+
358
+ /** 修改评论 */
359
+ public async submitEdit() {
360
+ try {
361
+ const saveData = {
362
+ content: this.getFinalContent(),
363
+ nick: this.$nick.value,
364
+ email: this.$email.value,
365
+ link: this.$link.value,
366
+ }
367
+ const nComment = await this.ctx.getApi().commentEdit({
368
+ ...this.editComment, ...saveData
369
+ })
370
+ this.ctx.updateComment(nComment)
371
+
372
+ // 复原编辑器
373
+ this.setContent('')
374
+ this.cancelEditComment()
375
+
278
376
  this.ctx.trigger('editor-submitted')
279
377
  } catch (err: any) {
280
378
  console.error(err)
281
379
  this.showNotify(`${this.$t('commentFail')},${err.msg || String(err)}`, 'e')
282
- return
283
- } finally {
284
- this.hideLoading()
285
380
  }
286
381
  }
287
382
 
package/src/i18n/en.ts CHANGED
@@ -3,6 +3,7 @@ export default {
3
3
  placeholder: 'Leave a comment',
4
4
  noComment: 'No Comment',
5
5
  send: 'Send',
6
+ save: 'Save',
6
7
  nick: 'Nickname',
7
8
  email: 'Email',
8
9
  link: 'Website',
@@ -42,6 +43,8 @@ export default {
42
43
  approved: 'Approved',
43
44
  pending: 'Pending',
44
45
  pendingMsg: 'Pending, visible only to commenter.',
46
+ edit: 'Edit',
47
+ editCancel: 'Cancel Edit',
45
48
  delete: 'Delete',
46
49
  deleteConfirm: 'Confirm',
47
50
  pin: 'Pin',
package/src/i18n/zh-CN.ts CHANGED
@@ -5,6 +5,7 @@ const zhCN: I18n = {
5
5
  placeholder: '键入内容...',
6
6
  noComment: '「此时无声胜有声」',
7
7
  send: '发送评论',
8
+ save: '保存评论',
8
9
  nick: '昵称',
9
10
  email: '邮箱',
10
11
  link: '网址',
@@ -40,10 +41,12 @@ const zhCN: I18n = {
40
41
  collapse: '折叠',
41
42
  collapsed: '已折叠',
42
43
  collapsedMsg: '该评论已被系统或管理员折叠',
43
- expand: '取消折叠',
44
+ expand: '展开',
44
45
  approved: '已审',
45
46
  pending: '待审',
46
47
  pendingMsg: '审核中,仅本人可见。',
48
+ edit: '编辑',
49
+ editCancel: '取消编辑',
47
50
  delete: '删除',
48
51
  deleteConfirm: '确认删除',
49
52
  pin: '置顶',
@@ -429,6 +429,14 @@ export default class ListLite extends Component {
429
429
  this.ctx.trigger('list-inserted', commentData)
430
430
  }
431
431
 
432
+ /** 更新评论 */
433
+ public updateComment(commentData: CommentData) {
434
+ const comment = this.ctx.findComment(commentData.id)
435
+ if (comment) {
436
+ comment.setData(commentData)
437
+ }
438
+ }
439
+
432
440
  /** 更新未读数据 */
433
441
  public updateUnread(notifies: NotifyData[]) {
434
442
  this.unread = notifies
@@ -258,9 +258,8 @@
258
258
  color: var(--at-color-red);
259
259
  }
260
260
 
261
- &:not(:first-child):not(.atk-hide) {
262
- margin-left: 8px;
263
- padding-left: 8px;
261
+ &:not(:last-child):not(.atk-hide) {
262
+ margin-right: 16px;
264
263
  }
265
264
 
266
265
  &:hover {
package/test/CSRF.html ADDED
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>CSRF Test</title>
8
+
9
+ <!-- 解除注释来测试当 referer 为空时的行为 -->
10
+ <!-- <meta name="referrer" content="never"> -->
11
+ </head>
12
+ <body>
13
+
14
+ <form action="http://localhost:23366/api/add" method="post">
15
+ <input type="text" name="name" value="admin">
16
+ <input type="text" name="email" value="test@example.com">
17
+ <input type="text" name="link" value="">
18
+ <input type="text" name="content" value="CSRF Success">
19
+ <input type="text" name="rid" value="0">
20
+ <input type="text" name="page_key" value="https://artalk.js.org/">
21
+ <input type="text" name="site_name" value="ArtalkDocs">
22
+ <button type="submit">Fire!</button>
23
+ </form>
24
+
25
+ </body>
26
+ </html>
@@ -8,6 +8,9 @@ export interface CommentData {
8
8
  /** 用户昵称 */
9
9
  nick: string
10
10
 
11
+ /** 用户邮箱 (该字段仅管理员可见) */
12
+ email?: string
13
+
11
14
  /** 用户邮箱(已加密) */
12
15
  email_encrypted: string
13
16
 
@@ -63,18 +63,30 @@ export default interface ContextApi {
63
63
  /** 插入评论 */
64
64
  insertComment(commentData: CommentData): void
65
65
 
66
+ /** 更新评论 */
67
+ updateComment(commentData: CommentData): void
68
+
66
69
  /** 评论回复 */
67
70
  replyComment(commentData: CommentData, $comment: HTMLElement, scroll?: boolean): void
68
71
 
69
72
  /** 取消回复评论 */
70
73
  cancelReplyComment(): void
71
74
 
75
+ /** 编辑评论 */
76
+ editComment(commentData: CommentData, $comment: HTMLElement): void
77
+
78
+ /** 取消编辑评论 */
79
+ cancelEditComment(): void
80
+
72
81
  /** 更新通知数据 */
73
82
  updateNotifies(notifies: NotifyData[]): void
74
83
 
75
84
  /** 列表 - 重新加载数据 */
76
85
  listReload(): void
77
86
 
87
+ /** 列表 - 重新加载数据 (别名) */
88
+ reload(): void
89
+
78
90
  /** 列表 - UI 更新 */
79
91
  listRefreshUI(): void
80
92
 
@@ -125,4 +137,7 @@ export default interface ContextApi {
125
137
 
126
138
  /** i18n 翻译 */
127
139
  $t(key: keyof I18n, args?: {[key: string]: string}): string
140
+
141
+ /** 设置暗黑模式 */
142
+ setDarkMode(darkMode: boolean): void
128
143
  }