artalk 2.1.9 → 2.2.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/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "artalk",
3
- "version": "2.1.9",
4
- "license": "GPL-3.0",
3
+ "version": "2.2.0",
4
+ "backendMinVersion": "2.1.1",
5
+ "license": "LGPL-3.0",
5
6
  "description": "A Selfhosted Comment System",
6
7
  "keywords": [
7
8
  "comment-system",
package/src/api/index.ts CHANGED
@@ -117,6 +117,12 @@ export default class Api {
117
117
  }
118
118
  }
119
119
 
120
+ /** 用户 · 登录状态 */
121
+ public async loginStatus() {
122
+ const data = await GET<any>(this.ctx, `${this.baseURL}/login-status`)
123
+ return (data || { is_login: false }) as { is_login: boolean }
124
+ }
125
+
120
126
  // ============================
121
127
  // 页面 Page
122
128
  // ============================
@@ -302,4 +308,10 @@ export default class Api {
302
308
  const data = await GET<any>(this.ctx, `${this.baseURL}/captcha/check`, { value })
303
309
  return (data.img_data || '') as string
304
310
  }
311
+
312
+ /** 验证码 · 状态 */
313
+ public async captchaStatus() {
314
+ const data = await GET<any>(this.ctx, `${this.baseURL}/captcha/status`)
315
+ return (data || { is_pass: false }) as { is_pass: boolean }
316
+ }
305
317
  }
@@ -43,6 +43,7 @@ export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit,
43
43
  json = await (new Promise<any>((resolve, reject) => {
44
44
  ctx.trigger('checker-captcha', {
45
45
  imgData: json.data.img_data,
46
+ iframe: json.data.iframe,
46
47
  onSuccess: () => {
47
48
  recall(resolve, reject)
48
49
  },
@@ -65,12 +65,7 @@ export default class Layer extends Component {
65
65
  }
66
66
 
67
67
  // body style 禁止滚动 + 防抖
68
- this.bodyStyleOrgOverflow = document.body.style.overflow
69
- this.bodyStyleOrgPaddingRight = document.body.style.paddingRight
70
- document.body.style.overflow = 'hidden'
71
-
72
- const bpr = parseInt(window.getComputedStyle(document.body, null).getPropertyValue('padding-right'), 10)
73
- document.body.style.paddingRight = `${Ui.getScrollBarWidth() + bpr || 0}px`
68
+ this.pageBodyScrollBarHide()
74
69
  }
75
70
 
76
71
  hide () {
@@ -81,8 +76,7 @@ export default class Layer extends Component {
81
76
  this.newActionTimer(() => {
82
77
  this.$wrap.style.display = 'none'
83
78
  // body style 禁止滚动解除
84
- document.body.style.overflow = this.bodyStyleOrgOverflow
85
- document.body.style.paddingRight = this.bodyStyleOrgPaddingRight
79
+ this.pageBodyScrollBarShow()
86
80
  this.checkCleanLayer()
87
81
  }, 450)
88
82
  this.newActionTimer(() => {
@@ -95,6 +89,22 @@ export default class Layer extends Component {
95
89
  this.maskClickHideEnable = enable
96
90
  }
97
91
 
92
+ // 页面滚动条隐藏
93
+ pageBodyScrollBarHide() {
94
+ this.bodyStyleOrgOverflow = document.body.style.overflow
95
+ this.bodyStyleOrgPaddingRight = document.body.style.paddingRight
96
+ document.body.style.overflow = 'hidden'
97
+
98
+ const bpr = parseInt(window.getComputedStyle(document.body, null).getPropertyValue('padding-right'), 10)
99
+ document.body.style.paddingRight = `${Ui.getScrollBarWidth() + bpr || 0}px`
100
+ }
101
+
102
+ // 页面滚动条显示
103
+ pageBodyScrollBarShow() {
104
+ document.body.style.overflow = this.bodyStyleOrgOverflow
105
+ document.body.style.paddingRight = this.bodyStyleOrgPaddingRight
106
+ }
107
+
98
108
  // Timers
99
109
  private static actionTimers: {act: Function, tid: number}[] = []
100
110
 
@@ -120,6 +130,7 @@ export default class Layer extends Component {
120
130
  disposeNow () {
121
131
  document.body.style.overflow = ''
122
132
  this.$el.remove()
133
+ this.pageBodyScrollBarShow()
123
134
  // this.$el dispose
124
135
  this.checkCleanLayer()
125
136
  }
@@ -7,6 +7,7 @@ import Comment from './comment'
7
7
  import { ListData, CommentData, NotifyData, ApiVersionData } from '~/types/artalk-data'
8
8
  import Pagination from './pagination'
9
9
  import ReadMoreBtn from './read-more-btn'
10
+ import { backendMinVersion } from '../../package.json'
10
11
 
11
12
  export default class ListLite extends Component {
12
13
  protected $commentsWrap: HTMLElement
@@ -117,7 +118,9 @@ export default class ListLite extends Component {
117
118
  this.data = data
118
119
 
119
120
  // 版本检测
120
- if (this.ctx.conf.versionCheck && this.versionCheck(data.api_version)) return
121
+ const feMinVersion = data.api_version?.fe_min_version || '0.0.0'
122
+ if (this.ctx.conf.versionCheck && this.versionCheck('前端', feMinVersion, ARTALK_VERSION)) return
123
+ if (this.ctx.conf.versionCheck && this.versionCheck('后端', backendMinVersion, data.api_version?.version)) return
121
124
 
122
125
  // 图片上传功能
123
126
  if (data.conf && typeof data.conf.img_upload === "boolean") {
@@ -472,26 +475,25 @@ export default class ListLite extends Component {
472
475
  }
473
476
  }
474
477
 
475
- /** 前端版本检测 */
476
- public versionCheck(versionData: ApiVersionData): boolean {
477
- const needVersion = versionData?.fe_min_version || '0.0.0'
478
- const needUpdate = Utils.versionCompare(needVersion, ARTALK_VERSION) === 1
478
+ /** 版本检测 */
479
+ public versionCheck(name: '前端'|'后端', needVersion: string, curtVersion: string): boolean {
480
+ const needUpdate = Utils.versionCompare(needVersion, curtVersion) === 1
479
481
  if (needUpdate) {
480
482
  // 需要更新
481
- const errEl = Utils.createElement(`<div>前端 Artalk 版本已过时,请更新以获得完整体验<br/>`
482
- + `若您是站点管理员,请前往 “<a href="https://artalk.js.org/" target="_blank">官方文档</a>” 获取帮助`
483
+ const errEl = Utils.createElement(`<div>Artalk ${name}版本已过时,请更新以获得完整体验<br/>`
484
+ + `如果你是管理员,请前往 “<a href="https://artalk.js.org/" target="_blank">官方文档</a>” 获得帮助`
483
485
  + `<br/><br/>`
484
- + `<span style="color: var(--at-color-meta);">前端版本 ${ARTALK_VERSION},需求版本 >= ${needVersion}</span><br/><br/>`
486
+ + `<span style="color: var(--at-color-meta);">当前${name}版本 ${curtVersion},需求版本 >= ${needVersion}</span><br/><br/>`
485
487
  + `</div>`)
486
488
  const ignoreBtn = Utils.createElement('<span style="cursor:pointer;">忽略</span>')
487
489
  ignoreBtn.onclick = () => {
488
- Ui.setError(this.ctx, null)
490
+ Ui.setError(this.$el.parentElement!, null)
489
491
  this.ctx.conf.versionCheck = false
490
492
  this.ctx.trigger('conf-updated')
491
493
  this.fetchComments(0)
492
494
  }
493
495
  errEl.append(ignoreBtn)
494
- Ui.setError(this.ctx, errEl, '<span class="atk-warn-title">Artalk Warn</span>')
496
+ Ui.setError(this.$el.parentElement!, errEl, '<span class="atk-warn-title">Artalk Warn</span>')
495
497
  }
496
498
 
497
499
  return needUpdate
@@ -6,6 +6,7 @@ import * as Utils from '@/lib/utils'
6
6
  import SidebarHTML from './html/sidebar-layer.html?raw'
7
7
  import * as Ui from '@/lib/ui'
8
8
  import Layer from './layer'
9
+ import Api from '../api'
9
10
 
10
11
  export default class SidebarLayer extends Component {
11
12
  public layer?: Layer
@@ -60,6 +61,26 @@ export default class SidebarLayer extends Component {
60
61
 
61
62
  // 第一次加载
62
63
  if (this.firstShow) {
64
+ // 管理员身份验证
65
+ if (this.ctx.user.data.isAdmin) {
66
+ const resp = await new Api(this.ctx).loginStatus()
67
+ if (!resp.is_login) {
68
+ await (new Promise<any>((resolve, reject) => {
69
+ this.ctx.trigger('checker-admin', {
70
+ onSuccess: () => {
71
+ resolve(null)
72
+ setTimeout(() => {
73
+ this.show()
74
+ }, 500)
75
+ },
76
+ onCancel: () => {
77
+ this.layer?.hide()
78
+ }
79
+ })
80
+ }))
81
+ }
82
+ }
83
+
63
84
  this.$iframeWrap.innerHTML = ''
64
85
  this.$iframe = Utils.createElement<HTMLIFrameElement>('<iframe></iframe>')
65
86
 
@@ -5,7 +5,7 @@ import { Checker } from '.'
5
5
  const AdminChecker: Checker = {
6
6
  inputType: 'password',
7
7
 
8
- request(that, inputVal) {
8
+ request(that, ctx, inputVal) {
9
9
  const data = {
10
10
  name: that.ctx.user.data.nick,
11
11
  email: that.ctx.user.data.email,
@@ -15,11 +15,11 @@ const AdminChecker: Checker = {
15
15
  return new Api(that.ctx).login(data.name, data.email, data.password)
16
16
  },
17
17
 
18
- body() {
18
+ body(that, ctx) {
19
19
  return Utils.createElement('<span>敲入密码来验证管理员身份:</span>')
20
20
  },
21
21
 
22
- onSuccess(that, userToken, inputVal, formEl) {
22
+ onSuccess(that, ctx, userToken, inputVal, formEl) {
23
23
  that.ctx.user.data.isAdmin = true
24
24
  that.ctx.user.data.token = userToken
25
25
  that.ctx.user.save()
@@ -27,7 +27,7 @@ const AdminChecker: Checker = {
27
27
  that.ctx.trigger('list-reload')
28
28
  },
29
29
 
30
- onError(that, err, inputVal, formEl) {}
30
+ onError(that, ctx, err, inputVal, formEl) {}
31
31
  }
32
32
 
33
33
  export default AdminChecker
@@ -1,15 +1,61 @@
1
1
  import Api from '@/api'
2
2
  import * as Utils from '../utils'
3
+ import * as Ui from '../ui'
3
4
  import { Checker } from '.'
4
5
 
5
6
  const CaptchaChecker: Checker = {
6
- request(that, inputVal) {
7
+ request(that, ctx, inputVal) {
7
8
  return new Api(that.ctx).captchaCheck(inputVal)
8
9
  },
9
10
 
10
- body(that) {
11
+ body(that, ctx) {
12
+ // Iframe 验证方式
13
+ if (that.captchaConf.iframe) {
14
+ const $iframeWrap = Utils.createElement(`<div class="atk-checker-iframe-wrap"></div>`)
15
+ const $iframe = Utils.createElement<HTMLIFrameElement>(`<iframe class="atk-fade-in"></iframe>`)
16
+ $iframe.style.display = 'none'
17
+ Ui.showLoading($iframeWrap, { transparentBg: true })
18
+ $iframe.src = `${that.ctx.conf.server}/captcha/get`
19
+ $iframe.onload = () => {
20
+ $iframe.style.display = ''
21
+ Ui.hideLoading($iframeWrap)
22
+ }
23
+ $iframeWrap.append($iframe)
24
+
25
+ const $closeBtn = Utils.createElement(`<div class="atk-close-btn"><i class="atk-icon atk-icon-close"></i></div>`)
26
+ $iframeWrap.append($closeBtn)
27
+
28
+ ctx.hideInteractInput()
29
+
30
+ // 轮询状态
31
+ let stop = false // 打断
32
+ const sleep = (ms: number) => new Promise((resolve) => { window.setTimeout(() => { resolve(null) }, ms) })
33
+ ;(async function queryStatus() {
34
+ await sleep(1000)
35
+ if (stop) return
36
+ let isPass = false
37
+ try {
38
+ const resp = await new Api(that.ctx).captchaStatus()
39
+ isPass = resp.is_pass
40
+ } catch { isPass = false }
41
+ if (isPass) {
42
+ ctx.triggerSuccess()
43
+ } else {
44
+ queryStatus()
45
+ }
46
+ })()
47
+
48
+ $closeBtn.onclick = () => {
49
+ stop = true
50
+ ctx.cancel()
51
+ }
52
+
53
+ return $iframeWrap
54
+ }
55
+
56
+ // 图片验证方式
11
57
  const elem = Utils.createElement(
12
- `<span><img class="atk-captcha-img" src="${that.submitCaptchaImgData || ''}" alt="验证码">敲入验证码继续:</span>`
58
+ `<span><img class="atk-captcha-img" src="${that.captchaConf.imgData || ''}" alt="验证码">敲入验证码继续:</span>`
13
59
  );
14
60
 
15
61
  // 刷新验证码
@@ -26,11 +72,11 @@ const CaptchaChecker: Checker = {
26
72
  return elem
27
73
  },
28
74
 
29
- onSuccess(that, data, inputVal, formEl) {
30
- that.submitCaptchaVal = inputVal
75
+ onSuccess(that, ctx, data, inputVal, formEl) {
76
+ that.captchaConf.val = inputVal
31
77
  },
32
78
 
33
- onError(that, err, inputVal, formEl) {
79
+ onError(that, ctx, err, inputVal, formEl) {
34
80
  formEl.querySelector<HTMLElement>('.atk-captcha-img')!.click() // 刷新验证码
35
81
  }
36
82
  }
@@ -14,8 +14,7 @@ export default class CheckerLauncher {
14
14
  public ctx: Context
15
15
 
16
16
  public launched: Checker[] = []
17
- public submitCaptchaVal?: string
18
- public submitCaptchaImgData?: string
17
+ public captchaConf: { val?: string, imgData?: string, iframe?: string } = {}
19
18
 
20
19
  constructor(ctx: Context) {
21
20
  this.ctx = ctx
@@ -26,9 +25,9 @@ export default class CheckerLauncher {
26
25
  /** 初始化事件绑定 */
27
26
  public initEventBind() {
28
27
  this.ctx.on('checker-captcha', (conf) => {
29
- if (conf.imgData) {
30
- this.submitCaptchaImgData = conf.imgData
31
- }
28
+ if (conf.imgData) this.captchaConf.imgData = conf.imgData
29
+ if (conf.iframe) this.captchaConf.iframe = conf.iframe
30
+
32
31
  this.fire(CaptchaChecker, conf)
33
32
  })
34
33
 
@@ -41,20 +40,42 @@ export default class CheckerLauncher {
41
40
  if (this.launched.includes(checker)) return // 阻止同时 fire 相同的 checker
42
41
  this.launched.push(checker)
43
42
 
43
+ // 创建层
44
44
  const layer = new Layer(this.ctx, `checker-${new Date().getTime()}`)
45
45
  layer.setMaskClickHide(false)
46
46
  layer.show()
47
47
 
48
+ // Checker 的上下文
49
+ let hideInteractInput = false
50
+ const checkerCtx: CheckerCtx = {
51
+ getLayer: () => layer,
52
+ hideInteractInput: () => {
53
+ hideInteractInput = true
54
+ },
55
+ triggerSuccess: () => {
56
+ this.close(checker, layer)
57
+ if (checker.onSuccess) checker.onSuccess(this, checkerCtx, "", "", formEl)
58
+ if (payload.onSuccess) payload.onSuccess("", dialog.$el)
59
+ },
60
+ cancel: () => {
61
+ this.close(checker, layer)
62
+ if (payload.onCancel) payload.onCancel()
63
+ }
64
+ }
65
+
66
+ // 创建表单
48
67
  const formEl = Utils.createElement()
49
- formEl.appendChild(checker.body(this))
68
+ formEl.appendChild(checker.body(this, checkerCtx))
50
69
 
51
- const input = Utils.createElement<HTMLInputElement>(
70
+ // 输入框
71
+ const $input = Utils.createElement<HTMLInputElement>(
52
72
  `<input id="check" type="${checker.inputType || 'text'}" autocomplete="off" required placeholder="">`
53
73
  )
54
- formEl.appendChild(input)
55
- setTimeout(() => input.focus(), 80) // 延迟 Focus
74
+ formEl.appendChild($input)
75
+ setTimeout(() => $input.focus(), 80) // 延迟 Focus
56
76
 
57
- input.onkeyup = (evt) => {
77
+ // 绑定键盘事件
78
+ $input.onkeyup = (evt) => {
58
79
  if (evt.key === 'Enter' || evt.keyCode === 13) {
59
80
  // 按下回车键
60
81
  evt.preventDefault()
@@ -70,7 +91,9 @@ export default class CheckerLauncher {
70
91
 
71
92
  // 确认按钮
72
93
  dialog.setYes((btnEl) => {
73
- const inputVal = input.value.trim()
94
+ const inputVal = $input.value.trim()
95
+
96
+ // 按钮操作
74
97
  if (!btnTextOrg) btnTextOrg = btnEl.innerText
75
98
  const btnTextSet = (btnText: string) => {
76
99
  btnEl.innerText = btnText
@@ -83,21 +106,25 @@ export default class CheckerLauncher {
83
106
 
84
107
  btnEl.innerText = '加载中...'
85
108
 
109
+ // 发送请求
86
110
  checker
87
- .request(this, inputVal)
111
+ .request(this, checkerCtx, inputVal)
88
112
  .then((data) => {
89
113
  // 请求成功
90
- this.done(checker, layer)
91
- if (checker.onSuccess) checker.onSuccess(this, data, inputVal, formEl)
114
+ this.close(checker, layer)
115
+
116
+ if (checker.onSuccess) checker.onSuccess(this, checkerCtx, data, inputVal, formEl)
92
117
  if (payload.onSuccess) payload.onSuccess(inputVal, dialog.$el)
93
118
  })
94
119
  .catch((err) => {
95
120
  // 请求失败
96
121
  btnTextSet(String(err.msg || String(err)))
97
- if (checker.onError) checker.onError(this, err, inputVal, formEl)
98
122
 
123
+ if (checker.onError) checker.onError(this, checkerCtx, err, inputVal, formEl)
124
+
125
+ // 错误显示 3s 后恢复按钮
99
126
  const tf = setTimeout(() => btnTextRestore(), 3000)
100
- input.onfocus = () => {
127
+ $input.onfocus = () => {
101
128
  btnTextRestore()
102
129
  clearTimeout(tf)
103
130
  }
@@ -108,17 +135,25 @@ export default class CheckerLauncher {
108
135
 
109
136
  // 取消按钮
110
137
  dialog.setNo(() => {
111
- this.done(checker, layer)
138
+ this.close(checker, layer)
112
139
  if (payload.onCancel) payload.onCancel()
113
140
  return false
114
141
  })
115
142
 
143
+ if (hideInteractInput) {
144
+ $input.style.display = 'none'
145
+ dialog.$el.querySelector<HTMLElement>('.atk-layer-dialog-actions')!.style.display = 'none'
146
+ }
147
+
148
+ // 层装载 dialog 元素
116
149
  layer.getEl().append(dialog.$el)
117
150
 
118
- if (payload.onMount) payload.onMount(dialog.$el) // onMount
151
+ // onMount 回调
152
+ if (payload.onMount) payload.onMount(dialog.$el)
119
153
  }
120
154
 
121
- private done(checker: Checker, layer: Layer) {
155
+ // 关闭 checker 对话框
156
+ private close(checker: Checker, layer: Layer) {
122
157
  layer.disposeNow()
123
158
  this.launched = this.launched.filter(c => c !== checker)
124
159
  }
@@ -127,8 +162,15 @@ export default class CheckerLauncher {
127
162
  export interface Checker {
128
163
  el?: HTMLElement
129
164
  inputType?: 'password' | 'text'
130
- body: (launcher: CheckerLauncher) => HTMLElement
131
- request: (launcher: CheckerLauncher, inputVal: string) => Promise<string>
132
- onSuccess?: (launcher: CheckerLauncher, data: string, inputVal: string, formEl: HTMLElement) => void
133
- onError?: (launcher: CheckerLauncher, err: any, inputVal: string, formEl: HTMLElement) => void
165
+ body: (launcher: CheckerLauncher, ctx: CheckerCtx) => HTMLElement
166
+ request: (launcher: CheckerLauncher, ctx: CheckerCtx, inputVal: string) => Promise<string>
167
+ onSuccess?: (launcher: CheckerLauncher, ctx: CheckerCtx, data: string, inputVal: string, formEl: HTMLElement) => void
168
+ onError?: (launcher: CheckerLauncher, ctx: CheckerCtx, err: any, inputVal: string, formEl: HTMLElement) => void
169
+ }
170
+
171
+ export interface CheckerCtx {
172
+ getLayer(): Layer
173
+ hideInteractInput(): void
174
+ triggerSuccess(): void
175
+ cancel(): void
134
176
  }
package/src/lib/ui.ts CHANGED
@@ -2,7 +2,7 @@ import * as Utils from './utils'
2
2
  import Context from '../context'
3
3
 
4
4
  /** 显示加载 */
5
- export function showLoading(parentElem: HTMLElement|Context) {
5
+ export function showLoading(parentElem: HTMLElement|Context, conf?: { transparentBg?: boolean }) {
6
6
  if (parentElem instanceof Context) parentElem = parentElem.$root
7
7
 
8
8
  let $loading = parentElem.querySelector<HTMLElement>('.atk-loading')
@@ -13,6 +13,7 @@ export function showLoading(parentElem: HTMLElement|Context) {
13
13
  <svg viewBox="25 25 50 50"><circle cx="50" cy="50" r="20" fill="none" stroke-width="2" stroke-miterlimit="10"></circle></svg>
14
14
  </div>
15
15
  </div>`)
16
+ if (conf?.transparentBg) $loading.style.background = 'transparent'
16
17
  parentElem.appendChild($loading)
17
18
  }
18
19
  $loading.style.display = ''
@@ -303,6 +303,41 @@ i.atk-icon {
303
303
  }
304
304
  }
305
305
  }
306
+
307
+ .atk-checker-iframe-wrap {
308
+ position: fixed;
309
+ z-index: 999998;
310
+ left: 0;
311
+ top: 0;
312
+ height: 100vh;
313
+ width: 100vw;
314
+
315
+ & > iframe {
316
+ width: 100%;
317
+ height: 100%;
318
+ border: 0;
319
+ }
320
+
321
+ .atk-close-btn {
322
+ z-index: 999999;
323
+ position: fixed;
324
+ top: 20px;
325
+ right: 20px;
326
+ display: flex;
327
+ flex-direction: column;
328
+ width: 50px;
329
+ height: 50px;
330
+ align-items: center;
331
+ place-content: center;
332
+ cursor: pointer;
333
+ user-select: none;
334
+ margin-left: 10px;
335
+
336
+ &:hover .atk-icon-close {
337
+ background-color: rgba(232, 17, 35, 0.9);
338
+ }
339
+ }
340
+ }
306
341
  }
307
342
  }
308
343
 
package/tsconfig.json CHANGED
@@ -1,28 +1,12 @@
1
1
  {
2
+ "extends": "../../tsconfig.base.json",
2
3
  "compilerOptions": {
3
4
  "baseUrl": ".",
4
5
  "outDir": "dist",
5
- "target": "esnext",
6
- "useDefineForClassFields": true,
7
- "module": "esnext",
8
- "lib": ["esnext", "dom"],
9
- "moduleResolution": "node",
10
- "strict": true,
11
- "sourceMap": true,
12
- "experimentalDecorators": true,
13
- "resolveJsonModule": true,
14
- "esModuleInterop": true,
15
- "noEmit": true,
16
- "noImplicitAny": false,
17
- "allowSyntheticDefaultImports": true,
18
- "noUnusedLocals": true,
19
- "noUnusedParameters": true,
20
- "noImplicitReturns": true,
21
6
  "paths": {
22
7
  "~/*": ["./*"],
23
8
  "@/*": ["./src/*"]
24
- },
25
- "types": ["vite/client"]
9
+ }
26
10
  },
27
11
  "include": ["./src", "./types"]
28
12
  }
package/types/event.d.ts CHANGED
@@ -55,6 +55,7 @@ export interface NotifyConf {
55
55
 
56
56
  export interface CheckerCaptchaPayload extends CheckerPayload {
57
57
  imgData?: string
58
+ iframe?: string
58
59
  }
59
60
 
60
61
  export interface UnreadUpdatePayload {