From f593d95358e47e38f5d4cb88487928dc12e25afc Mon Sep 17 00:00:00 2001 From: Ilya Y <75578537+ilya-bkv@users.noreply.github.com> Date: Mon, 9 Oct 2023 08:14:58 +0300 Subject: [PATCH 1/2] Feature/paste image from buffer (#251) Editor: Paste images from clipboard --- public/locales/en/translation.json | 3 +- public/locales/ru/translation.json | 3 +- src/components/Editor/Editor.tsx | 80 ++++++++++++++++++++++++++++++ src/utils/renderUploadedImage.ts | 2 +- 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 910abebc..19eabb6d 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -212,7 +212,6 @@ "Nothing here yet": "There's nothing here yet", "Nothing is here": "There is nothing here", "Notifications": "Notifications", - "Registered since {{date}}": "Registered since {{date}}", "Or continue with social network": "Or continue with social network", "Or paste a link to an image": "Or paste a link to an image", "Ordered list": "Ordered list", @@ -250,6 +249,7 @@ "Quotes": "Quotes", "Reason uknown": "Reason unknown", "Recent": "Fresh", + "Registered since {{date}}": "Registered since {{date}}", "Remove link": "Remove link", "Reply": "Reply", "Report": "Complain", @@ -327,6 +327,7 @@ "Upload": "Upload", "Upload error": "Upload error", "Upload video": "Upload video", + "Uploading image": "Uploading image", "Username": "Username", "Userpic": "Userpic", "Users": "Users", diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 5ecc804f..11f3a432 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -222,7 +222,6 @@ "Nothing here yet": "Здесь пока ничего нет", "Nothing is here": "Здесь ничего нет", "Notifications": "Уведомления", - "Registered since {{date}}": "На сайте c {{date}}", "Or continue with social network": "Или войдите через соцсеть", "Or paste a link to an image": "Или вставьте ссылку на изображение", "Ordered list": "Нумерованный список", @@ -264,6 +263,7 @@ "Quotes": "Цитаты", "Reason uknown": "Причина неизвестна", "Recent": "Свежее", + "Registered since {{date}}": "На сайте c {{date}}", "Release date...": "Дата выхода...", "Remove link": "Убрать ссылку", "Reply": "Ответить", @@ -345,6 +345,7 @@ "Upload": "Загрузить", "Upload error": "Ошибка загрузки", "Upload video": "Загрузить видео", + "Uploading image": "Загружаем изображение", "Username": "Имя пользователя", "Userpic": "Аватар", "Users": "Пользователи", diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index f31f4e1f..8b6ecfb5 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -44,6 +44,9 @@ import { EditorFloatingMenu } from './EditorFloatingMenu' import './Prosemirror.scss' import { Image } from '@tiptap/extension-image' import { Footnote } from './extensions/Footnote' +import { handleFileUpload } from '../../utils/handleFileUpload' +import { imageProxy } from '../../utils/imageProxy' +import { useSnackbar } from '../../context/snackbar' type Props = { shoutId: number @@ -51,6 +54,17 @@ type Props = { onChange: (text: string) => void } +const allowedImageTypes = new Set([ + 'image/bmp', + 'image/gif', + 'image/jpeg', + 'image/jpg', + 'image/png', + 'image/tiff', + 'image/webp', + 'image/x-icon' +]) + const yDocs: Record = {} const providers: Record = {} @@ -61,6 +75,10 @@ export const Editor = (props: Props) => { const [isCommonMarkup, setIsCommonMarkup] = createSignal(false) const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false) + const { + actions: { showSnackbar } + } = useSnackbar() + const docName = `shout-${props.shoutId}` if (!yDocs[docName]) { @@ -114,12 +132,73 @@ export const Editor = (props: Props) => { content: 'figcaption image' }) + const handleClipboardPaste = async () => { + try { + const clipboardItems = await navigator.clipboard.read() + + if (clipboardItems.length === 0) return + const [clipboardItem] = clipboardItems + const { types } = clipboardItem + const imageType = types.find((type) => allowedImageTypes.has(type)) + + if (!imageType) return + const blob = await clipboardItem.getType(imageType) + const extension = imageType.split('/')[1] + const file = new File([blob], `clipboardImage.${extension}`) + + const uplFile = { + source: blob.toString(), + name: file.name, + size: file.size, + file + } + + showSnackbar({ body: t('Uploading image') }) + const result = await handleFileUpload(uplFile) + + editor() + .chain() + .focus() + .insertContent({ + type: 'capturedImage', + content: [ + { + type: 'figcaption', + content: [ + { + type: 'text', + text: result.originalFilename + } + ] + }, + { + type: 'image', + attrs: { + src: imageProxy(result.url) + } + } + ] + }) + .run() + } catch (error) { + console.log('!!! Paste image Error:', error) + } + } + const { initialContent } = props + const editor = createTiptapEditor(() => ({ element: editorElRef.current, editorProps: { attributes: { class: 'articleEditor' + }, + transformPastedHTML(html) { + return html.replaceAll(//g, '') + }, + handlePaste: () => { + handleClipboardPaste() + return false } }, extensions: [ @@ -246,6 +325,7 @@ export const Editor = (props: Props) => { TrailingNode, Article ], + enablePasteRules: [Link], content: initialContent ?? null })) diff --git a/src/utils/renderUploadedImage.ts b/src/utils/renderUploadedImage.ts index 30aa4749..40b95f14 100644 --- a/src/utils/renderUploadedImage.ts +++ b/src/utils/renderUploadedImage.ts @@ -15,7 +15,7 @@ export const renderUploadedImage = (editor: Editor, image: UploadedFile) => { content: [ { type: 'text', - text: image.originalFilename + text: image.originalFilename ?? '' } ] }, From e2c110140a8c6e105883e0d9ca27f31b431f3e3a Mon Sep 17 00:00:00 2001 From: Ilya Y <75578537+ilya-bkv@users.noreply.github.com> Date: Mon, 9 Oct 2023 12:14:41 +0300 Subject: [PATCH 2/2] Feature/icu plurals (#252) ICU with plurals --- package-lock.json | 132 +++++++++++++++++- package.json | 2 + public/locales/en/translation.json | 5 +- public/locales/ru/translation.json | 5 +- .../Author/AuthorBadge/AuthorBadge.tsx | 18 ++- .../Author/AuthorCard/AuthorCard.tsx | 5 +- src/components/Editor/Editor.tsx | 2 +- .../Topic/TopicBadge/TopicBadge.tsx | 2 +- src/components/Views/Author/Author.tsx | 2 - src/pages/profile/profileSettings.page.tsx | 2 +- src/renderer/_default.page.client.tsx | 5 +- 11 files changed, 157 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 207b6dd9..f7b9e8ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "dependencies": { "form-data": "4.0.0", "i18next": "22.4.15", + "i18next-icu": "2.3.0", + "intl-messageformat": "10.5.3", "mailgun.js": "8.2.1", "node-fetch": "3.3.1" }, @@ -1991,6 +1993,50 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.2.tgz", + "integrity": "sha512-k2mTh0m+IV1HRdU0xXM617tSQTi53tVR2muvYOsBeYcUgEAyxV1FOC7Qj279th3fBVQ+Dj6muvNJZcHSPNdbKg==", + "dependencies": { + "@formatjs/intl-localematcher": "0.4.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz", + "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.6.2.tgz", + "integrity": "sha512-nF/Iww7sc5h+1MBCDRm68qpHTCG4xvGzYs/x9HFcDETSGScaJ1Fcadk5U/NXjXeCtzD+DhN4BAwKFVclHfKMdA==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.17.2", + "@formatjs/icu-skeleton-parser": "1.6.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.6.2.tgz", + "integrity": "sha512-VtB9Slo4ZL6QgtDFJ8Injvscf0xiDd4bIV93SOJTBjUF4xe2nAWOoSjLEtqIG+hlIs1sNrVKAaFo3nuTI4r5ZA==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.17.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz", + "integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@graphql-codegen/cli": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-3.2.2.tgz", @@ -10149,6 +10195,14 @@ } } }, + "node_modules/i18next-icu": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/i18next-icu/-/i18next-icu-2.3.0.tgz", + "integrity": "sha512-x+j7kd5nDJCfbU53uwsMfXD7ALPu5uv0bqjAMQ5nVvXRoj1L7gkmswKtM3XDWYo4YUHf1jznlhSdPyy0xEwU+Q==", + "peerDependencies": { + "intl-messageformat": "^10.3.3" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -10441,6 +10495,17 @@ "node": ">= 0.4" } }, + "node_modules/intl-messageformat": { + "version": "10.5.3", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.3.tgz", + "integrity": "sha512-TzKn1uhJBMyuKTO4zUX47SU+d66fu1W9tVzIiZrQ6hBqQQeYscBMIzKL/qEXnFbJrH9uU5VV3+T5fWib4SIcKA==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.17.2", + "@formatjs/fast-memoize": "2.2.0", + "@formatjs/icu-messageformat-parser": "2.6.2", + "tslib": "^2.4.0" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -17482,8 +17547,7 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -19561,6 +19625,50 @@ "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", "dev": true }, + "@formatjs/ecma402-abstract": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.2.tgz", + "integrity": "sha512-k2mTh0m+IV1HRdU0xXM617tSQTi53tVR2muvYOsBeYcUgEAyxV1FOC7Qj279th3fBVQ+Dj6muvNJZcHSPNdbKg==", + "requires": { + "@formatjs/intl-localematcher": "0.4.2", + "tslib": "^2.4.0" + } + }, + "@formatjs/fast-memoize": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz", + "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==", + "requires": { + "tslib": "^2.4.0" + } + }, + "@formatjs/icu-messageformat-parser": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.6.2.tgz", + "integrity": "sha512-nF/Iww7sc5h+1MBCDRm68qpHTCG4xvGzYs/x9HFcDETSGScaJ1Fcadk5U/NXjXeCtzD+DhN4BAwKFVclHfKMdA==", + "requires": { + "@formatjs/ecma402-abstract": "1.17.2", + "@formatjs/icu-skeleton-parser": "1.6.2", + "tslib": "^2.4.0" + } + }, + "@formatjs/icu-skeleton-parser": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.6.2.tgz", + "integrity": "sha512-VtB9Slo4ZL6QgtDFJ8Injvscf0xiDd4bIV93SOJTBjUF4xe2nAWOoSjLEtqIG+hlIs1sNrVKAaFo3nuTI4r5ZA==", + "requires": { + "@formatjs/ecma402-abstract": "1.17.2", + "tslib": "^2.4.0" + } + }, + "@formatjs/intl-localematcher": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz", + "integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==", + "requires": { + "tslib": "^2.4.0" + } + }, "@graphql-codegen/cli": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-3.2.2.tgz", @@ -25570,6 +25678,12 @@ } } }, + "i18next-icu": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/i18next-icu/-/i18next-icu-2.3.0.tgz", + "integrity": "sha512-x+j7kd5nDJCfbU53uwsMfXD7ALPu5uv0bqjAMQ5nVvXRoj1L7gkmswKtM3XDWYo4YUHf1jznlhSdPyy0xEwU+Q==", + "requires": {} + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -25777,6 +25891,17 @@ "side-channel": "^1.0.4" } }, + "intl-messageformat": { + "version": "10.5.3", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.3.tgz", + "integrity": "sha512-TzKn1uhJBMyuKTO4zUX47SU+d66fu1W9tVzIiZrQ6hBqQQeYscBMIzKL/qEXnFbJrH9uU5VV3+T5fWib4SIcKA==", + "requires": { + "@formatjs/ecma402-abstract": "1.17.2", + "@formatjs/fast-memoize": "2.2.0", + "@formatjs/icu-messageformat-parser": "2.6.2", + "tslib": "^2.4.0" + } + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -30923,8 +31048,7 @@ "tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "tsutils": { "version": "3.21.0", diff --git a/package.json b/package.json index 082da1e1..561a99ee 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "dependencies": { "form-data": "4.0.0", "i18next": "22.4.15", + "i18next-icu": "2.3.0", + "intl-messageformat": "10.5.3", "mailgun.js": "8.2.1", "node-fetch": "3.3.1" }, diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 19eabb6d..fcdb8ea9 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -249,7 +249,7 @@ "Quotes": "Quotes", "Reason uknown": "Reason unknown", "Recent": "Fresh", - "Registered since {{date}}": "Registered since {{date}}", + "Registered since {date}": "Registered since {date}", "Remove link": "Remove link", "Reply": "Reply", "Report": "Complain", @@ -409,5 +409,6 @@ "user already exist": "user already exists", "video": "video", "view": "view", - "zine": "zine" + "zine": "zine", + "PublicationsWithCount": "{count, plural, =0 {no publications} one {{count} publication} other {{count} publications}}" } diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 11f3a432..8f0adb92 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -263,7 +263,7 @@ "Quotes": "Цитаты", "Reason uknown": "Причина неизвестна", "Recent": "Свежее", - "Registered since {{date}}": "На сайте c {{date}}", + "Registered since {date}": "На сайте c {date}", "Release date...": "Дата выхода...", "Remove link": "Убрать ссылку", "Reply": "Ответить", @@ -435,5 +435,6 @@ "user already exist": "пользователь уже существует", "video": "видео", "view": "просмотр", - "zine": "журнал" + "zine": "журнал", + "PublicationsWithCount": "{count, plural, =0 {нет публикаций} one {{count} публикация} few {{count} публикации} other {{count} публикаций}}" } diff --git a/src/components/Author/AuthorBadge/AuthorBadge.tsx b/src/components/Author/AuthorBadge/AuthorBadge.tsx index 39285612..a5408d93 100644 --- a/src/components/Author/AuthorBadge/AuthorBadge.tsx +++ b/src/components/Author/AuthorBadge/AuthorBadge.tsx @@ -2,7 +2,7 @@ import { clsx } from 'clsx' import styles from './AuthorBadge.module.scss' import { Userpic } from '../Userpic' import { Author, FollowingEntity } from '../../../graphql/types.gen' -import { createMemo, createSignal, Show } from 'solid-js' +import { createMemo, createSignal, Match, Show, Switch } from 'solid-js' import { formatDate } from '../../../utils' import { useLocalize } from '../../../context/localize' import { Button } from '../../_shared/Button' @@ -47,16 +47,22 @@ export const AuthorBadge = (props: Props) => {
{props.author.name}
- - {t('Registered since {{date}}', { date: formatDate(new Date(props.author.createdAt)) })} + {t('Registered since {date}', { date: formatDate(new Date(props.author.createdAt)) })} } > -
- + +
+ + 0}> +
+ {t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })} +
+
+
diff --git a/src/components/Author/AuthorCard/AuthorCard.tsx b/src/components/Author/AuthorCard/AuthorCard.tsx index 713605de..395498a3 100644 --- a/src/components/Author/AuthorCard/AuthorCard.tsx +++ b/src/components/Author/AuthorCard/AuthorCard.tsx @@ -193,12 +193,13 @@ export const AuthorCard = (props: Props) => { {name()}
- {/*TODO: implement plurals by i18n*/} {props.author.stat?.shouts} публикаций
+
+ {t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })} +
) : ( '' ) diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index 8b6ecfb5..817a570b 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -181,7 +181,7 @@ export const Editor = (props: Props) => { }) .run() } catch (error) { - console.log('!!! Paste image Error:', error) + console.error('[Paste Image Error]:', error) } } diff --git a/src/components/Topic/TopicBadge/TopicBadge.tsx b/src/components/Topic/TopicBadge/TopicBadge.tsx index ee43e986..3d958645 100644 --- a/src/components/Topic/TopicBadge/TopicBadge.tsx +++ b/src/components/Topic/TopicBadge/TopicBadge.tsx @@ -56,7 +56,7 @@ export const TopicBadge = (props: Props) => { when={props.topic.body} fallback={
- {props.topic.stat.shouts ?? 0} {t('Publications')} + {t('PublicationsWithCount', { count: props.topic.stat.shouts ?? 0 })}
} > diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index cd479d8f..73b15285 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -35,8 +35,6 @@ export const AuthorView = (props: Props) => { const { page } = useRouter() const author = createMemo(() => authorEntities()[props.authorSlug]) - - console.log('!!! author:', author()) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isBioExpanded, setIsBioExpanded] = createSignal(false) const [followers, setFollowers] = createSignal([]) diff --git a/src/pages/profile/profileSettings.page.tsx b/src/pages/profile/profileSettings.page.tsx index 02326938..eddb61d0 100644 --- a/src/pages/profile/profileSettings.page.tsx +++ b/src/pages/profile/profileSettings.page.tsx @@ -123,7 +123,7 @@ export const ProfileSettingsPage = () => {

{t('Here you can customize your profile the way you want.')}

{t('Userpic')}

-
+