@waline/client 2.4.2 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/component.esm.js +1 -1
  2. package/dist/component.esm.js.map +1 -1
  3. package/dist/component.js +1 -1
  4. package/dist/component.js.map +1 -1
  5. package/dist/legacy.d.ts +42 -0
  6. package/dist/legacy.js +1 -1
  7. package/dist/legacy.js.map +1 -1
  8. package/dist/pageview.cjs.js +1 -1
  9. package/dist/pageview.cjs.js.map +1 -1
  10. package/dist/pageview.esm.js +1 -1
  11. package/dist/pageview.esm.js.map +1 -1
  12. package/dist/pageview.js +1 -1
  13. package/dist/pageview.js.map +1 -1
  14. package/dist/shim.d.ts +43 -1
  15. package/dist/shim.esm.d.ts +43 -1
  16. package/dist/shim.esm.js +1 -1
  17. package/dist/shim.esm.js.map +1 -1
  18. package/dist/shim.js +1 -1
  19. package/dist/shim.js.map +1 -1
  20. package/dist/waline.cjs.d.ts +43 -1
  21. package/dist/waline.cjs.js +1 -1
  22. package/dist/waline.cjs.js.map +1 -1
  23. package/dist/waline.css +1 -1
  24. package/dist/waline.css.map +1 -1
  25. package/dist/waline.d.ts +43 -1
  26. package/dist/waline.esm.d.ts +43 -1
  27. package/dist/waline.esm.js +1 -1
  28. package/dist/waline.esm.js.map +1 -1
  29. package/dist/waline.js +1 -1
  30. package/dist/waline.js.map +1 -1
  31. package/package.json +21 -22
  32. package/src/comment.ts +7 -2
  33. package/src/components/CommentBox.vue +121 -4
  34. package/src/components/CommentCard.vue +1 -1
  35. package/src/components/Icons.ts +20 -0
  36. package/src/components/ImageWall.vue +166 -0
  37. package/src/composables/like.ts +1 -1
  38. package/src/config/default.ts +91 -1
  39. package/src/config/i18n/en.ts +2 -0
  40. package/src/config/i18n/generate.ts +2 -0
  41. package/src/config/i18n/jp.ts +8 -0
  42. package/src/config/i18n/pt-BR.ts +8 -0
  43. package/src/config/i18n/ru.ts +8 -0
  44. package/src/config/i18n/vi-VN.ts +8 -0
  45. package/src/config/i18n/zh-CN.ts +2 -0
  46. package/src/config/i18n/zh-TW.ts +2 -0
  47. package/src/init.ts +0 -3
  48. package/src/pageview.ts +2 -1
  49. package/src/styles/card.scss +1 -0
  50. package/src/styles/emoji.scss +112 -94
  51. package/src/styles/gif.scss +70 -0
  52. package/src/styles/index.scss +3 -0
  53. package/src/styles/panel.scss +0 -4
  54. package/src/typings/base.ts +40 -0
  55. package/src/typings/locale.ts +2 -0
  56. package/src/typings/waline.ts +8 -0
  57. package/src/utils/config.ts +5 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waline/client",
3
- "version": "2.4.2",
3
+ "version": "2.6.0",
4
4
  "description": "client for waline comment system",
5
5
  "keywords": [
6
6
  "valine",
@@ -71,30 +71,29 @@
71
71
  ]
72
72
  },
73
73
  "dependencies": {
74
- "@vueuse/core": "^8.4.2",
74
+ "@vueuse/core": "^8.6.0",
75
75
  "autosize": "^5.0.1",
76
- "marked": "^4.0.15",
77
- "vue": "^3.2.33"
76
+ "marked": "^4.0.16",
77
+ "vue": "^3.2.37"
78
78
  },
79
79
  "devDependencies": {
80
- "@babel/core": "^7.17.10",
81
- "@babel/preset-env": "^7.17.10",
82
- "@rollup/plugin-babel": "^5.3.1",
83
- "@rollup/plugin-commonjs": "^22.0.0",
84
- "@rollup/plugin-node-resolve": "^13.3.0",
85
- "@rollup/plugin-replace": "^4.0.0",
86
- "@types/autosize": "^4.0.1",
87
- "@types/marked": "^4.0.3",
88
- "@types/node": "^17.0.31",
89
- "@vitejs/plugin-vue": "^2.3.2",
90
- "@vue/compiler-sfc": "^3.2.33",
91
- "rimraf": "^3.0.2",
92
- "rollup": "^2.72.1",
93
- "rollup-plugin-dts": "^4.2.1",
94
- "rollup-plugin-terser": "^7.0.2",
95
- "rollup-plugin-ts": "^2.0.7",
96
- "typescript": "^4.6.4",
97
- "vite": "^2.9.8"
80
+ "@babel/core": "7.18.2",
81
+ "@babel/preset-env": "7.18.2",
82
+ "@rollup/plugin-babel": "5.3.1",
83
+ "@rollup/plugin-commonjs": "22.0.0",
84
+ "@rollup/plugin-node-resolve": "13.3.0",
85
+ "@rollup/plugin-replace": "4.0.0",
86
+ "@types/autosize": "4.0.1",
87
+ "@types/marked": "4.0.3",
88
+ "@types/node": "17.0.41",
89
+ "@vitejs/plugin-vue": "2.3.3",
90
+ "rimraf": "3.0.2",
91
+ "rollup": "2.75.6",
92
+ "rollup-plugin-dts": "4.2.2",
93
+ "rollup-plugin-terser": "7.0.2",
94
+ "rollup-plugin-ts": "3.0.2",
95
+ "typescript": "4.7.3",
96
+ "vite": "2.9.10"
98
97
  },
99
98
  "engines": {
100
99
  "node": ">=14"
package/src/comment.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { useUserInfo } from './composables';
2
- import { decodePath, errorHandler, fetchCommentCount } from './utils';
2
+ import {
3
+ decodePath,
4
+ errorHandler,
5
+ fetchCommentCount,
6
+ getServerURL,
7
+ } from './utils';
3
8
  import type { WalineAbort } from './typings';
4
9
 
5
10
  export interface WalineCommentCountOptions {
@@ -54,7 +59,7 @@ WalineCommentCountOptions): WalineAbort => {
54
59
 
55
60
  if (elements.length)
56
61
  void fetchCommentCount({
57
- serverURL,
62
+ serverURL: getServerURL(serverURL),
58
63
  paths: Array.from(elements).map((element) =>
59
64
  decodePath(element.dataset.path || element.getAttribute('id') || path)
60
65
  ),
@@ -35,7 +35,7 @@
35
35
  <input
36
36
  :ref="
37
37
  (element) => {
38
- if (element) inputRefs[kind] = element;
38
+ if (element) inputRefs[kind] = element as HTMLInputElement;
39
39
  }
40
40
  "
41
41
  :id="`wl-${kind}`"
@@ -88,6 +88,17 @@
88
88
  <EmojiIcon />
89
89
  </button>
90
90
 
91
+ <button
92
+ v-if="config.search"
93
+ ref="gifButtonRef"
94
+ class="wl-action"
95
+ :class="{ actived: showGif }"
96
+ :title="locale.gif"
97
+ @click="showGif = !showGif"
98
+ >
99
+ <GifIcon />
100
+ </button>
101
+
91
102
  <input
92
103
  ref="imageUploadRef"
93
104
  class="upload"
@@ -152,6 +163,31 @@
152
163
  </button>
153
164
  </div>
154
165
 
166
+ <div
167
+ ref="gifPopupRef"
168
+ class="wl-gif-popup"
169
+ :class="{ display: showGif }"
170
+ >
171
+ <input
172
+ type="text"
173
+ :placeholder="locale.gifSearchPlaceholder"
174
+ ref="gifSearchInputRef"
175
+ @input="onGifSearch"
176
+ />
177
+
178
+ <ImageWall
179
+ :items="gifData.list"
180
+ :column-width="200"
181
+ :gap="6"
182
+ @insert="insert($event)"
183
+ @scroll="onImageWallScroll"
184
+ >
185
+ </ImageWall>
186
+
187
+ <div v-if="gifData.loading" class="wl-loading">
188
+ <LoadingIcon :size="30" />
189
+ </div>
190
+ </div>
155
191
  <div
156
192
  ref="emojiPopupRef"
157
193
  class="wl-emoji-popup"
@@ -210,6 +246,7 @@
210
246
  </template>
211
247
 
212
248
  <script lang="ts">
249
+ import { useDebounceFn } from '@vueuse/core';
213
250
  import autosize from 'autosize';
214
251
  import {
215
252
  computed,
@@ -217,6 +254,7 @@ import {
217
254
  inject,
218
255
  onMounted,
219
256
  onUnmounted,
257
+ reactive,
220
258
  ref,
221
259
  watch,
222
260
  } from 'vue';
@@ -228,19 +266,26 @@ import {
228
266
  MarkdownIcon,
229
267
  PreviewIcon,
230
268
  LoadingIcon,
269
+ GifIcon,
231
270
  } from './Icons';
271
+ import ImageWall from './ImageWall.vue';
232
272
  import { useEditor, useUserMeta, useUserInfo } from '../composables';
233
273
  import {
274
+ getEmojis,
234
275
  getImagefromDataTransfer,
235
- parseMarkdown,
236
276
  getWordNumber,
237
277
  parseEmoji,
278
+ parseMarkdown,
238
279
  postComment,
239
- getEmojis,
240
280
  } from '../utils';
241
281
 
242
282
  import type { ComputedRef, DeepReadonly } from 'vue';
243
- import type { WalineCommentData, WalineImageUploader } from '../typings';
283
+ import type {
284
+ WalineCommentData,
285
+ WalineImageUploader,
286
+ WalineSearchOptions,
287
+ WalineSearchResult,
288
+ } from '../typings';
244
289
  import type { WalineConfig, WalineEmojiConfig } from '../utils';
245
290
 
246
291
  export default defineComponent({
@@ -250,9 +295,11 @@ export default defineComponent({
250
295
  CloseIcon,
251
296
  EmojiIcon,
252
297
  ImageIcon,
298
+ ImageWall,
253
299
  MarkdownIcon,
254
300
  PreviewIcon,
255
301
  LoadingIcon,
302
+ GifIcon,
256
303
  },
257
304
 
258
305
  props: {
@@ -286,14 +333,23 @@ export default defineComponent({
286
333
  const imageUploadRef = ref<HTMLInputElement | null>(null);
287
334
  const emojiButtonRef = ref<HTMLDivElement | null>(null);
288
335
  const emojiPopupRef = ref<HTMLDivElement | null>(null);
336
+ const gifButtonRef = ref<HTMLDivElement | null>(null);
337
+ const gifPopupRef = ref<HTMLDivElement | null>(null);
338
+ const gifSearchInputRef = ref<HTMLInputElement | null>(null);
289
339
 
290
340
  const emoji = ref<DeepReadonly<WalineEmojiConfig>>({ tabs: [], map: {} });
291
341
  const emojiTabIndex = ref(0);
292
342
  const showEmoji = ref(false);
343
+ const showGif = ref(false);
293
344
  const showPreview = ref(false);
294
345
  const previewText = ref('');
295
346
  const wordNumber = ref(0);
296
347
 
348
+ const searchResults = reactive({
349
+ loading: true,
350
+ list: [] as WalineSearchResult[],
351
+ });
352
+
297
353
  const wordLimit = ref(0);
298
354
  const isWordNumberLegal = ref(false);
299
355
 
@@ -548,8 +604,43 @@ export default defineComponent({
548
604
  !(emojiPopupRef.value as HTMLElement).contains(event.target as Node)
549
605
  )
550
606
  showEmoji.value = false;
607
+
608
+ if (
609
+ !(gifButtonRef.value as HTMLElement).contains(event.target as Node) &&
610
+ !(gifPopupRef.value as HTMLElement).contains(event.target as Node)
611
+ )
612
+ showGif.value = false;
613
+ };
614
+
615
+ const onImageWallScroll = async (event: Event): Promise<void> => {
616
+ const { scrollTop, clientHeight, scrollHeight } =
617
+ event.target as HTMLDivElement;
618
+ const percent = (clientHeight + scrollTop) / scrollHeight;
619
+ const searchOptions = config.value.search as WalineSearchOptions;
620
+ const keyword = gifSearchInputRef.value?.value || '';
621
+
622
+ if (percent < 0.9 || searchResults.loading) return;
623
+
624
+ searchResults.loading = true;
625
+
626
+ searchResults.list.push(
627
+ ...(searchOptions.more
628
+ ? await searchOptions.more(keyword, searchResults.list.length)
629
+ : await searchOptions.search(keyword))
630
+ );
631
+
632
+ searchResults.loading = false;
633
+
634
+ setTimeout(() => {
635
+ (event.target as HTMLDivElement).scrollTop = scrollTop;
636
+ }, 50);
551
637
  };
552
638
 
639
+ const onGifSearch = useDebounceFn((event: Event) => {
640
+ searchResults.list = [];
641
+ onImageWallScroll(event);
642
+ }, 300);
643
+
553
644
  // update wordNumber
554
645
  watch(
555
646
  [config, wordNumber],
@@ -575,6 +666,23 @@ export default defineComponent({
575
666
  { immediate: true }
576
667
  );
577
668
 
669
+ watch(showGif, async (showGif) => {
670
+ if (!showGif) return;
671
+
672
+ const searchOptions = config.value.search as WalineSearchOptions;
673
+
674
+ // clear input
675
+ if (gifSearchInputRef.value) gifSearchInputRef.value.value = '';
676
+
677
+ searchResults.loading = true;
678
+
679
+ searchResults.list = searchOptions.default
680
+ ? await searchOptions.default()
681
+ : await searchOptions.search('');
682
+
683
+ searchResults.loading = false;
684
+ });
685
+
578
686
  onMounted(() => {
579
687
  document.body.addEventListener('click', popupHandler);
580
688
 
@@ -632,6 +740,8 @@ export default defineComponent({
632
740
  onLogout,
633
741
  onProfile,
634
742
  submitComment,
743
+ onImageWallScroll,
744
+ onGifSearch,
635
745
 
636
746
  isLogin,
637
747
  userInfo,
@@ -651,6 +761,10 @@ export default defineComponent({
651
761
  emojiTabIndex,
652
762
  showEmoji,
653
763
 
764
+ // gif
765
+ gifData: searchResults,
766
+ showGif,
767
+
654
768
  // image
655
769
  canUploadImage,
656
770
 
@@ -663,7 +777,10 @@ export default defineComponent({
663
777
  editorRef,
664
778
  emojiButtonRef,
665
779
  emojiPopupRef,
780
+ gifButtonRef,
781
+ gifPopupRef,
666
782
  imageUploadRef,
783
+ gifSearchInputRef,
667
784
  };
668
785
  },
669
786
  });
@@ -25,7 +25,7 @@
25
25
  <span v-if="comment.label" class="wl-badge" v-text="comment.label" />
26
26
  <span v-if="comment.sticky" class="wl-badge" v-text="locale.sticky" />
27
27
  <span
28
- v-if="comment.level && comment.level >= 0"
28
+ v-if="comment.level !== undefined && comment.level >= 0"
29
29
  :class="`wl-badge level${comment.level}`"
30
30
  v-text="locale[`level${comment.level}`] || `Level ${comment.level}`"
31
31
  />
@@ -148,3 +148,23 @@ export const LoadingIcon: FunctionalComponent<{ size: number }> = ({ size }) =>
148
148
  })
149
149
  )
150
150
  );
151
+
152
+ export const GifIcon: FunctionalComponent = () =>
153
+ h(
154
+ 'svg',
155
+ {
156
+ width: 24,
157
+ height: 24,
158
+ fill: 'currentcolor',
159
+ viewBox: '0 0 24 24',
160
+ },
161
+ [
162
+ h('path', {
163
+ style: 'transform: translateY(0.5px)',
164
+ d: 'M18.968 10.5H15.968V11.484H17.984V12.984H15.968V15H14.468V9H18.968V10.5V10.5ZM8.984 9C9.26533 9 9.49967 9.09367 9.687 9.281C9.87433 9.46833 9.968 9.70267 9.968 9.984V10.5H6.499V13.5H8.468V12H9.968V14.016C9.968 14.2973 9.87433 14.5317 9.687 14.719C9.49967 14.9063 9.26533 15 8.984 15H5.984C5.70267 15 5.46833 14.9063 5.281 14.719C5.09367 14.5317 5 14.2973 5 14.016V9.985C5 9.70367 5.09367 9.46933 5.281 9.282C5.46833 9.09467 5.70267 9.001 5.984 9.001H8.984V9ZM11.468 9H12.968V15H11.468V9V9Z',
165
+ }),
166
+ h('path', {
167
+ d: 'M18.5 3H5.75C3.6875 3 2 4.6875 2 6.75V18C2 20.0625 3.6875 21.75 5.75 21.75H18.5C20.5625 21.75 22.25 20.0625 22.25 18V6.75C22.25 4.6875 20.5625 3 18.5 3ZM20.75 18C20.75 19.2375 19.7375 20.25 18.5 20.25H5.75C4.5125 20.25 3.5 19.2375 3.5 18V6.75C3.5 5.5125 4.5125 4.5 5.75 4.5H18.5C19.7375 4.5 20.75 5.5125 20.75 6.75V18Z',
168
+ }),
169
+ ]
170
+ );
@@ -0,0 +1,166 @@
1
+ <!-- forked from https://github.com/DerYeger/vue-masonry-wall/blob/master/src/masonry-wall.vue -->
2
+
3
+ <!-- MIT License
4
+
5
+ Copyright (c) 2021 Fuxing Loh, Jan Müller
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE. -->
24
+
25
+ <template>
26
+ <div ref="wall" class="wl-gallery" :style="{ gap: `${gap}px` }">
27
+ <div
28
+ v-for="(column, columnIndex) in columns"
29
+ :key="columnIndex"
30
+ class="wl-gallery-column"
31
+ :data-index="columnIndex"
32
+ :style="{ gap: `${gap}px` }"
33
+ >
34
+ <template v-for="itemIndex in column" :key="itemIndex">
35
+ <LoadingIcon
36
+ v-if="!state[items[itemIndex].src]"
37
+ :size="36"
38
+ style="margin: 20px auto"
39
+ />
40
+ <img
41
+ class="wl-gallery-item"
42
+ :src="items[itemIndex].src"
43
+ :title="items[itemIndex].title"
44
+ loading="lazy"
45
+ @load="state[items[itemIndex].src] = true"
46
+ @click="$emit('insert', `![](${items[itemIndex].src})`)"
47
+ />
48
+ </template>
49
+ </div>
50
+ </div>
51
+ </template>
52
+
53
+ <script lang="ts">
54
+ import {
55
+ defineComponent,
56
+ nextTick,
57
+ onBeforeUnmount,
58
+ onMounted,
59
+ ref,
60
+ watch,
61
+ } from 'vue';
62
+
63
+ import { LoadingIcon } from './Icons';
64
+
65
+ import type { PropType } from 'vue';
66
+ import type { WalineSearchResult } from '../typings';
67
+
68
+ type Column = number[];
69
+
70
+ export default defineComponent({
71
+ name: 'ImageWall',
72
+
73
+ components: {
74
+ LoadingIcon,
75
+ },
76
+
77
+ props: {
78
+ items: { type: Array as PropType<WalineSearchResult[]>, default: () => [] },
79
+ columnWidth: { type: Number, default: 300 },
80
+ gap: { type: Number, default: 0 },
81
+ },
82
+
83
+ emit: ['insert'],
84
+
85
+ setup(props) {
86
+ let resizeObserver: ResizeObserver | null = null;
87
+ const wall = ref<HTMLDivElement | null>(null);
88
+ const state = ref<Record<string, boolean>>({});
89
+ const columns = ref<Column[]>([]);
90
+
91
+ const getColumnCount = (): number => {
92
+ const count = Math.floor(
93
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
94
+ (wall.value!.getBoundingClientRect().width + props.gap) /
95
+ (props.columnWidth + props.gap)
96
+ );
97
+
98
+ return count > 0 ? count : 1;
99
+ };
100
+
101
+ const createColumns = (count: number): Column[] =>
102
+ new Array(count).fill(null).map(() => []);
103
+
104
+ const fillColumns = async (itemIndex: number): Promise<void> => {
105
+ if (itemIndex >= props.items.length) return;
106
+
107
+ await nextTick();
108
+
109
+ const columnDivs = Array.from(
110
+ wall.value?.children || []
111
+ ) as HTMLDivElement[];
112
+
113
+ const target = columnDivs.reduce((prev, curr) =>
114
+ curr.getBoundingClientRect().height <
115
+ prev.getBoundingClientRect().height
116
+ ? curr
117
+ : prev
118
+ );
119
+
120
+ columns.value[Number(target.dataset.index)].push(itemIndex);
121
+
122
+ await fillColumns(itemIndex + 1);
123
+ };
124
+
125
+ const redraw = async (force = false): Promise<void> => {
126
+ if (columns.value.length === getColumnCount() && !force) return;
127
+
128
+ columns.value = createColumns(getColumnCount());
129
+
130
+ const scrollY = window.scrollY;
131
+
132
+ await fillColumns(0);
133
+
134
+ window.scrollTo({ top: scrollY });
135
+ };
136
+
137
+ watch(
138
+ () => [props.items],
139
+ () => {
140
+ state.value = {};
141
+ redraw(true);
142
+ }
143
+ );
144
+ watch(
145
+ () => [props.columnWidth, props.gap],
146
+ () => redraw()
147
+ );
148
+
149
+ onMounted(() => {
150
+ redraw(true);
151
+ resizeObserver = new ResizeObserver(() => redraw());
152
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
153
+ resizeObserver.observe(wall.value!);
154
+ });
155
+
156
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
157
+ onBeforeUnmount(() => resizeObserver!.unobserve(wall.value!));
158
+
159
+ return {
160
+ columns,
161
+ state,
162
+ wall,
163
+ };
164
+ },
165
+ });
166
+ </script>
@@ -2,7 +2,7 @@ import { useStorage } from '@vueuse/core';
2
2
 
3
3
  import type { Ref } from 'vue';
4
4
 
5
- const LIKE_KEY = 'WALIKE_LIKE';
5
+ const LIKE_KEY = 'WALINE_LIKE';
6
6
 
7
7
  export type LikeID = number | string;
8
8
 
@@ -1,4 +1,4 @@
1
- import type { WalineMeta } from '../typings';
1
+ import type { WalineMeta, WalineSearchOptions } from '../typings';
2
2
 
3
3
  const availableMeta: WalineMeta[] = ['nick', 'mail', 'link'];
4
4
 
@@ -19,3 +19,93 @@ export const defaultTexRenderer = (blockMode: boolean): string =>
19
19
  blockMode === true
20
20
  ? '<p class="wl-tex">Tex is not available in preview</p>'
21
21
  : '<span class="wl-tex">Tex is not available in preview</span>';
22
+
23
+ export const getDefaultSearchOptions = (): WalineSearchOptions => {
24
+ interface FetchGifRequest {
25
+ keyword: string;
26
+ pos?: string;
27
+ }
28
+
29
+ type GifFormat =
30
+ | 'gif'
31
+ | 'mediumgif'
32
+ | 'tinygif'
33
+ | 'nanogif'
34
+ | 'mp4'
35
+ | 'loopedmp4'
36
+ | 'tinymp4'
37
+ | 'nanomp4'
38
+ | 'webm'
39
+ | 'tinywebm'
40
+ | 'nanowebm';
41
+
42
+ interface MediaObject {
43
+ preview: string;
44
+ url: string;
45
+ dims: number[];
46
+ size: number;
47
+ }
48
+
49
+ interface GifObject {
50
+ created: number;
51
+ hasaudio: boolean;
52
+ id: string;
53
+ media: Record<GifFormat, MediaObject>[];
54
+ tags: string[];
55
+ title: string;
56
+ itemurl: string;
57
+ hascaption: boolean;
58
+ url: string;
59
+ }
60
+
61
+ interface FetchGifResponse {
62
+ next: string;
63
+ results: GifObject[];
64
+ }
65
+
66
+ const state = {
67
+ next: '',
68
+ };
69
+
70
+ const fetchGif = ({
71
+ keyword,
72
+ pos,
73
+ }: FetchGifRequest): Promise<FetchGifResponse> => {
74
+ const baseUrl = `https://g.tenor.com/v1/search`;
75
+ const query = new URLSearchParams('media_filter=minimal');
76
+ query.set('key', 'PAY5JLFIH6V6');
77
+ query.set('limit', '20');
78
+ query.set('pos', pos || '');
79
+ query.set('q', keyword);
80
+
81
+ return fetch(`${baseUrl}?${query.toString()}`, {
82
+ headers: {
83
+ // eslint-disable-next-line @typescript-eslint/naming-convention
84
+ 'Content-Type': 'application/json',
85
+ },
86
+ })
87
+ .then((resp) => resp.json() as Promise<FetchGifResponse>)
88
+ .catch(() => ({ next: pos || '', results: [] }));
89
+ };
90
+
91
+ return {
92
+ search: (word = '') =>
93
+ fetchGif({ keyword: word }).then((resp) => {
94
+ state.next = resp.next;
95
+
96
+ return resp.results.map((item) => ({
97
+ title: item.title,
98
+ src: item.media[0].tinygif.url,
99
+ }));
100
+ }),
101
+ more: (word) =>
102
+ fetchGif({ keyword: word, pos: state.next }).then((resp) => {
103
+ state.next = resp.next;
104
+
105
+ return resp.results.map((item) => ({
106
+ title: item.title,
107
+ src: item.media[0].tinygif.url,
108
+ }));
109
+ }),
110
+ };
111
+ };
@@ -39,4 +39,6 @@ export default generateLocale([
39
39
  'Wizards',
40
40
  'Elves',
41
41
  'Maiar',
42
+ 'GIF',
43
+ 'Search GIF',
42
44
  ]);
@@ -39,6 +39,8 @@ const localeKeys = [
39
39
  'level3',
40
40
  'level4',
41
41
  'level5',
42
+ 'gif',
43
+ 'gifSearchPlaceholder',
42
44
  ];
43
45
 
44
46
  export const generateLocale = (locale: string[]): WalineLocale =>
@@ -33,4 +33,12 @@ export default generateLocale([
33
33
  'ワード',
34
34
  'コメントは $0 から $1 ワードの間でなければなりません!\n 現在の単語番号: $2',
35
35
  '匿名',
36
+ 'うえにん',
37
+ 'なかにん',
38
+ 'しもおし',
39
+ '特にしもおし',
40
+ 'かげ',
41
+ 'なぬし',
42
+ 'GIF',
43
+ '探す GIF',
36
44
  ]);
@@ -33,4 +33,12 @@ export default generateLocale([
33
33
  'Palavras',
34
34
  'Favor enviar comentário com $0 a $1 palavras!\n Número de palavras atuais: $2',
35
35
  'Anônimo',
36
+ 'Dwarves',
37
+ 'Hobbits',
38
+ 'Ents',
39
+ 'Wizards',
40
+ 'Elves',
41
+ 'Maiar',
42
+ 'GIF',
43
+ 'Pesquisar GIF',
36
44
  ]);
@@ -33,4 +33,12 @@ export default generateLocale([
33
33
  'Слова',
34
34
  'Пожалуйста, введите комментарии от $0 до $1 слов!\nНомер текущего слова: $2',
35
35
  'Анонимный',
36
+ 'Dwarves',
37
+ 'Hobbits',
38
+ 'Ents',
39
+ 'Wizards',
40
+ 'Elves',
41
+ 'Maiar',
42
+ 'GIF',
43
+ 'Поиск GIF',
36
44
  ]);