diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index b04f4260c..6cb1b3499 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -16,12 +16,22 @@ jobs: steps: - uses: actions/checkout@v3.3.0 + if: github.event_name != 'pull_request_target' with: fetch-depth: 0 submodules: true - - name: Checkout HEAD + - uses: actions/checkout@v3.3.0 if: github.event_name == 'pull_request_target' - run: git checkout ${{ github.head_ref }} + with: + fetch-depth: 0 + submodules: true + ref: "refs/pull/${{ github.event.number }}/merge" + - name: Checkout actual HEAD + if: github.event_name == 'pull_request_target' + id: rev + run: | + echo "base=$(git rev-list --parents -n1 HEAD | cut -d" " -f2)" >> $GITHUB_OUTPUT + git checkout $(git rev-list --parents -n1 HEAD | cut -d" " -f3) - name: Install pnpm uses: pnpm/action-setup@v2 with: @@ -68,7 +78,7 @@ jobs: if: github.event_name == 'pull_request_target' id: chromatic_pull_request run: | - DIFF="${{ github.base_ref }} HEAD" + DIFF="${{ steps.rev.outputs.base }} HEAD" if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then DIFF="HEAD" fi @@ -76,7 +86,11 @@ jobs: if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then echo "skip=true" >> $GITHUB_OUTPUT fi - pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static $(echo "$CHROMATIC_PARAMETER") + BRANCH="${{ github.event.pull_request.head.user.login }}:${{ github.event.pull_request.head.ref }}" + if [ "$BRANCH" = "misskey-dev:${{ github.event.pull_request.head.ref }}" ]; then + BRANCH="${{ github.event.pull_request.head.ref }}" + fi + pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name $BRANCH $(echo "$CHROMATIC_PARAMETER") env: CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - name: Notify that Chromatic detects changes @@ -91,18 +105,6 @@ jobs: commit_sha: context.sha, body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).' }) - - name: Notify that Chromatic will skip testing - uses: actions/github-script@v6.4.0 - if: github.event_name == 'pull_request_target' && steps.chromatic_pull_request.outputs.skip == 'true' - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'Chromatic will skip testing but you may still have to [review the changes on Chromatic](https://www.chromatic.com/pullrequests?appId=6428f7d7b962f0b79f97d6e4).' - }) - name: Upload Artifacts uses: actions/upload-artifact@v3 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8594c42d1..a06300656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,44 @@ --> +## 13.13.0 + +### General +- カスタム絵文字ごとにそれをリアクションとして使えるロールを設定できるように +- カスタム絵文字ごとに連合するかどうか設定できるように +- カスタム絵文字ごとにセンシティブフラグを設定できるように +- センシティブなカスタム絵文字のリアクションを受け入れない設定が可能に +- タイムラインにフォロイーの行った他人へのリプライを含めるかどうかの設定をアカウントに保存するのをやめるように + - 今後はAPI呼び出し時およびストリーミング接続時に設定するようになります +- リストを公開できるようになりました + +### Client +- リアクションの取り消し/変更時に確認ダイアログを出すように +- 開発者モードを追加 +- AiScriptを0.13.3に更新 +- Deck UIを使用している場合、`/`以外にアクセスした際にZen UIで表示するように + - メインカラムを設置していない場合の問題を解決 +- ハッシュタグのノート一覧ページから、そのハッシュタグで投稿するボタンを追加 +- アカウント初期設定ウィザードに戻るボタンを追加 +- アカウントの初期設定ウィザードにあとでボタンを追加 +- サーバーにカスタム絵文字の種類が多い場合のパフォーマンスの改善 +- Fix: URLプレビューで情報が取得できなかった際の挙動を修正 +- Fix: Safari、Firefoxでの新規登録時、パスワードマネージャーにメールアドレスが登録されていた挙動を修正 +- Fix: ロールタイムラインが無効でも投稿が流れてしまう問題の修正 +- Fix: ロールタイムラインにて全ての投稿が流れてしまう問題の修正 +- Fix: 「アクセストークンの管理」画面でアプリの情報が表示されない問題の修正 +- Fix: Firefoxにおける絵文字ピッカーのTabキーフォーカス問題の修正 +- Fix: フォローボタンがテーマのカラースキームによって視認性が悪くなる問題を修正 + - 新しいプロパティ `fgOnWhite` が追加されました + +### Server +- bullをbull-mqにアップグレードし、ジョブキューのパフォーマンスを改善 +- ストリーミングのパフォーマンスを改善 +- Fix: 無効化されたアンテナにアクセスがあった際に再度有効化するように +- Fix: お知らせの画像URLを空にできない問題を修正 +- Fix: i/notificationsのsinceIdが機能しない問題を修正 +- Fix: pageのピン留めを解除することができない問題を修正 + ## 13.12.2 ## NOTE @@ -87,6 +125,7 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー * 画像が全て隠れた状態で表示されるようになります - 閲覧注意設定された画像は表示した状態でもそれが閲覧注意だと分かる表示をするように - モデレーターはノートに添付された画像上から直接NSFW設定できるように +- 1枚だけのメディアリストの画像のアスペクト比を画像に応じて縦長にするように - プロフィール設定「追加情報」の項目の削除と並び替えができるように - 新しい実績を追加 - AiScriptを0.13.2に更新 @@ -334,6 +373,7 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー - アンテナでCWも検索対象にするように - ノートの操作部をホバー時のみ表示するオプションを追加 - サウンドを追加 +- enhance(client): MFMのx2, scale, positionが含まれていたらノートをたたむように - サーバーのパフォーマンスを改善 ### Bugfixes diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 9185be344..827a326d1 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -21,6 +21,8 @@ import './commands' Cypress.on('uncaught:exception', (err, runnable) => { if ([ + 'The source image cannot be decoded', + // Chrome 'ResizeObserver loop limit exceeded', diff --git a/docs/DONATORS.md b/docs/DONATORS.md deleted file mode 100644 index 9da5c1a94..000000000 --- a/docs/DONATORS.md +++ /dev/null @@ -1,25 +0,0 @@ -DONATORS -======== -The list of people who have sent donation for Misskey. - -(In random order, honorific titles are omitted.) - -* らふぁ -* 俺様 -* なぎうり -* スルメ https://surume.tk/ -* 藍 -* 音船 https://otofune.me/ -* aqz https://misskey.xyz/aqz -* kotodu "虚無創作中" -* Maya Minatsuki -* Knzk https://knzk.me/@Knzk -* ねじりわさび https://knzk.me/@y -* NCLS https://knzk.me/@imncls] -* こじま @skoji@sandbox.skoji.jp - -:heart: Thanks for donating, guys! - ---- - -If your name is missing, please contact us! diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index bfca086f5..5d0fd201b 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -2,6 +2,7 @@ _lang_: "العربية" headlineMisskey: "شبكة مرتبطة بالملاحظات" introMisskey: "اهلا بك! ميسكي هو منصة تدوين مصغر لا مركزية ومفتوحة المصدر.\nيمكنك مشاركة \"ملاحظات\" عن ما يجري حولك، وإخبار الجميع عن نفسك 📡\nتسمح لك \"الانفعالات\" بتعبير عن شعورك حول ملاحظات الآخرين 👍\nاكتشف عالمًا جديدًا 🚀" +poweredByMisskeyDescription: "{name} هو إحدى الخِدمات التي تستخدم المنصة مفتوحة المصدر <b>ميسكي</b> (يشار إليه كمثيل ميسكي)" monthAndDay: "{day}/{month}" search: "البحث" notifications: "الإشعارات" @@ -49,6 +50,7 @@ deleteAndEdit: "إزالة وإعادة الصياغة" deleteAndEditConfirm: "أمتأكد من حذف الملاحظة؟ ستفقد كل مشاركاتها، والتفاعلات، والردود عليها." addToList: "أضفه إلى قائمة" sendMessage: "أرسل رسالة" +copyRSS: "انسخ رابط RSS" copyUsername: "انسخ اسم المستخدم" searchUser: "ابحث عن مستخدمين" reply: "رد" @@ -102,6 +104,8 @@ renoted: "أُعيد نشره" cantRenote: "لا يمكن إعادة نشر الملاحظة" cantReRenote: "لا يمكنك إعادة نشر ملاحظة معاد نشرها" quote: "اقتبس" +inChannelRenote: "إعادة نشر في قناة" +inChannelQuote: "اقتباس في قناة" pinnedNote: "ملاحظة مدبسة" pinned: "دبّسها على الصفحة الشخصية" you: "أنت" @@ -119,6 +123,8 @@ unmarkAsSensitive: "ألغ تعيينه كمحتوى حساس" enterFileName: "ادخل اسم الملف" mute: "اكتم" unmute: "إلغاء الكتم" +renoteMute: "اكتم إعادة النشر" +renoteUnmute: "ارفع الكتم عن إعادة النشر" block: "احجب" unblock: "إلغاء الحجب" suspend: "علِق" @@ -141,6 +147,7 @@ emojiUrl: "رابط الإيموجي" addEmoji: "إضافة إيموجي" settingGuide: "الإعدادات المستحسنة" cacheRemoteFiles: "خزن مؤقتا الملفات البعيدة" +cacheRemoteFilesDescription: "إذا عُطل هذا الإعداد، ستُحمل الملفات من المثيل البعيد، هذا سيقلل من المساحة المستغلة على القرص لكن سيزيد حجم تدفق البيانات وهذا لأن الصور المصغرة لن تولّد." flagAsBot: "علّمه كحساب آلي" flagAsBotDescription: "فعّل هذا الخيار إذا كان هذا الحساب يُدار عبر برمجية. إذا فُعل فسيكون بمثابة علامة للمطورين الآخرين لتجنب سلاسل لا متناهية من التفاعل بين حسابات الآلية وضبط أنظمة ميسكي للتعامل مع هذا الحساب كآلي." flagAsCat: "علّم هذا الحساب كحساب قط" @@ -253,14 +260,15 @@ startMessaging: "ابدأ محادثة" nUsersRead: "قرأه {n}" agreeTo: "اوافق على {0}" agree: "أقبل" +agreeBelow: "أقبل ما يلي" basicNotesBeforeCreateAccount: "ملاحظات مهمة" termsOfService: "شروط الخدمة" start: "البداية" home: "الرئيسي" remoteUserCaution: "هذه المعلومات قد لا تكون مكتملة بما أن المستخدم من مثيل بعيد." activity: "النشاط" -images: "الصور" -image: "الصور" +images: "صور" +image: "صور" birthday: "تاريخ الميلاد" yearsOld: "{age} سنة" registeredDate: "انضم في" @@ -362,6 +370,7 @@ antennaExcludeKeywords: "الكلمات المفتاحية المستثناة" antennaKeywordsDescription: "افصل بينهم بمسافة لاستخدام معامل \"و\" أو بسطر لاستخدام معامل \"أو\"" notifyAntenna: "نبهني بصول ملاحظات جديدة" withFileAntenna: "ملاحظات تحوي ملفات فقط" +enableServiceworker: "فعّل إرسال الإشعارات للمتصفح" antennaUsersDescription: "اكتب اسم مستخدم لكل سطر" caseSensitive: "حساسية حالة الأحرف" withReplies: "بالردود" @@ -391,6 +400,7 @@ moderation: "الإشراف" nUsersMentioned: "{n} مستخدمين أُشير إليهم" securityKey: "مفتاح الأمان" lastUsed: "آخر استخدام" +lastUsedAt: "آخر استخدام: {t}" unregister: "إلغاء التسجيل" passwordLessLogin: "لِج مِن دون كلمة سرية" resetPassword: "أعد تعيين كلمتك السرية" @@ -441,6 +451,7 @@ or: "أو" language: "اللغة" uiLanguage: "لغة واجهة المستخدم" aboutX: "عن {x}" +emojiStyle: "نمط الوجوه التعبيرية" noHistory: "السجل فارغ" signinHistory: "تاريخ تسجيل الدخول" doing: "انتظر لحظة" @@ -451,6 +462,7 @@ createAccount: "أنشئ حسابًا" existingAccount: "الحسابات الموجودة" regenerate: "أعِد التوليد" fontSize: "حجم الخط" +limitTo: "سقفهُ لـ{x}" noFollowRequests: "ليس لديك طلبات متابعة معلقة" openImageInNewTab: "إفتح الصورة بصفحة جديدة" dashboard: "لوحة التحكم" @@ -479,6 +491,7 @@ objectStorageUseProxyDesc: "عطل هذا الخيار إذا لم ترد است serverLogs: "سجلات الخادم" deleteAll: "حذف الكل" showFixedPostForm: "أظهر نموذج الكتابة في أعلى الصفحة" +showFixedPostFormInChannel: "أظهر نموذج الكتابة في أعلى الخط الزمني (قنوات)" newNoteRecived: "هناك ملاحظات جديدة" sounds: "الرنات" sound: "الرنات" @@ -514,6 +527,7 @@ userSilenced: "كُتم هذا المستخدم." yourAccountSuspendedTitle: "هذا الحساب معلق" yourAccountSuspendedDescription: "عُلق الحساب بسبب انتهاك شروط خدمة المثيل و ما شابه. إذا أردت معرفة التفصيل تواصل مع مدير المثيل. رجاءً لا تنشئ حساب جديد." accountDeleted: "حُذف الحساب" +accountDeletedDescription: "حُذف هذا الحساب." menu: "القائمة" divider: "فاصل" addItem: "إضافة عنصر" @@ -539,6 +553,7 @@ author: "الكاتب" leaveConfirm: "لديك تغييرات غير محفوظة. أتريد المتابعة دون حفظها؟" manage: "إدارة " plugins: "الإضافات" +preferencesBackups: "النُسخ الاحتياطية للإعدادات" useFullReactionPicker: "استخدم الحجم الكامل لمنتقي التفاعلات" width: "العرض" height: "الإرتفاع" @@ -648,6 +663,7 @@ contact: "التواصل" useSystemFont: "استخدم الخط الافتراضية للنظام" clips: "مشابك" experimentalFeatures: "ميّزات اختبارية" +experimental: "اختباري" developer: "المطور" makeExplorable: "أظهر الحساب في صفحة \"استكشاف\"" makeExplorableDescription: "بتعطيل هذا الخيار لن يظهر حسابك في صفحة \"استكشاف\"" @@ -669,6 +685,7 @@ accentColor: "طابع لوني" textColor: "لون النص" saveAs: "احفظ كـ..." advanced: "متقدم" +advancedSettings: "إعدادات متقدمة" value: "القيمة" createdAt: "أُنشئ في" updatedAt: "حُدّث في" @@ -733,6 +750,7 @@ popularPosts: "المشاركات المتداولة" shareWithNote: "شاركه في ملاحظة" ads: "الإعلانات" expiration: "ينتهي استطلاع الرأي في" +startingperiod: "ابدأ" memo: "تذكير" priority: "الأولوية" high: "عالية" @@ -763,6 +781,7 @@ lastCommunication: "آخر تواصل" resolved: "عولج" unresolved: "لم يعالج" breakFollow: "إلغاء الاشتراك" +breakFollowConfirm: "أمتأكد من إزالة المتابِع ؟" itsOn: "مفعّل" itsOff: "معطّل" emailRequiredForSignup: "عنوان البريد الإلكتروني إلزامي للتسجيل" @@ -777,6 +796,7 @@ muteThread: "اكتم النقاش" unmuteThread: "ارفع الكتم عن النقاش" ffVisibility: "مرئية المتابِعين/المتابَعين" ffVisibilityDescription: "يسمح لك بتحديد من يمكنهم رؤية متابِعيك ومتابَعيك." +continueThread: "اعرض بقية النقاش" deleteAccountConfirm: "سيحذف حسابك نهائيًا، أتريد المتابعة؟" incorrectPassword: "كلمة السر خاطئة." voteConfirm: "متيقِّن من تصويتك لـ {choice}؟" @@ -798,25 +818,127 @@ tenMinutes: "10 دقائق" oneHour: "ساعة" oneDay: "يوم" oneWeek: "أسبوع" +oneMonth: "شهر" failedToFetchAccountInformation: "تعذر جلب معلومات الحساب" +cropNo: "استخدمها كما هي" file: "الملفات" +recentNHours: "آخر {n} ساعة" +recentNDays: "آخر {n} أيام" +noEmailServerWarning: "خادم البريد غير مضبوط." +thereIsUnresolvedAbuseReportWarning: "توجد بلاغات غير معالجة." +recommended: "مقترح" +driveCapOverrideLabel: "غيّر حجم قرص التخزين لهذا المستخدم" +driveCapOverrideCaption: "أعد الحجم إلى القيمة الافتراضية بإدخال 0 أو أقل." +requireAdminForView: "لاستعراض هذه الصفحة وجب عليك الولوج كمدير." +typeToConfirm: "أدخل {x} للتأكيد" +deleteAccount: "احذف الحساب" +document: "التوثيق" +numberOfPageCache: "عدد الصفحات المخزنة مؤقتًا" +logoutConfirm: "أتريد الخروج؟" +lastActiveDate: "آخر استخدام" +statusbar: "شريط الحالة" +pleaseSelect: "حدد خيارًا" reverse: "اقلب" colored: "ملوّن" label: "التسمية" +type: "نوع" +speed: "سرعة" +slow: "بطيء" +fast: "سريع" +sensitiveMediaDetection: "التعرف على المحتوى الحساس" localOnly: "المحلي فقط" +failedToUpload: "فشل الرفع" +cannotUploadBecauseInappropriate: "تعذر رفع الملف لوجود محتوى حساس فيه." +cannotUploadBecauseNoFreeSpace: "تعذر رفع الملف لنقص مساحة التخزين." +cannotUploadBecauseExceedsFileSizeLimit: "تعذر رفع الملف بسبب تجاوز حجمه للحد المسموح" +beta: "بيتا" +navbar: "شريط التنقل" +shuffle: "خلط" account: "الحسابات" +move: "أنقل" +pushNotification: "إرسال الإشعارات" +subscribePushNotification: "فعّل إرسال الإشعارات" +unsubscribePushNotification: "عطل إرسال الإشعارات" +pushNotificationAlreadySubscribed: "إرسال الإشعارات مفعل سلفًا" +pushNotificationNotSupported: "متصفحك لا يدعم إرسال الإشعارات أو المثيل لا يدعمها." +sendPushNotificationReadMessage: "احذف الإشعارات فور قراءتها" +sendPushNotificationReadMessageCaption: "هذا قد يزيد من معدل استهلاك الطاقة لجهازك." +caption: "التعليق التوضيحي" +tools: "أدوات" cannotLoad: "تعذر التحميل" like: "أعجبني" +unlike: "ألغِ الإعجاب" show: "المظهر" +neverShow: "لا تظهره مجددًا" +didYouLikeMisskey: "هل أعجبك ميسكي؟" +roles: "الأدوار" +role: "الدور" +noRole: "لم يُعثر على دور" +normalUser: "مستخدم عادي" +undefined: "غير معرّف" color: "اللون" +manageCustomEmojis: "إدارة الإيموجي المخصصة" +cannotPerformTemporary: "غير متاح مؤقتاً" +permissionDeniedError: "رُفضة العملية" +preset: "إعدادات مسبقة" +selectFromPresets: "اختر من الإعدادات المسبقة" +achievements: "الإنجازات" +gotInvalidResponseError: "استجابة غير متوقعة من الخادم" +gotInvalidResponseErrorDescription: "يتعذر الوصول إلى الخادم أوأنه يُصان، رجاءً حاول لاحقًا." +thisPostMayBeAnnoying: "هذا قد يزعج الآخرين." +thisPostMayBeAnnoyingHome: "أنشر في الخط الزمني الرئيس" +thisPostMayBeAnnoyingCancel: "ألغِ" +internalServerError: "خطأ داخلي في الخادم" +internalServerErrorDescription: "واجه الخادم خطأ غي متوقع." +copyErrorInfo: "انسخ تفاصيل الخطأ" +joinThisServer: "سجل في هذا المثيل" +exploreOtherServers: "اعثر على مثيل آخر" +disableFederationOk: "عطّل" +invitationRequiredToRegister: "هذا المثيل للمدعوين فقط. لتسجيل فيه تحتاج رمزًا صالحًا." +postToTheChannel: "انشر في قناة" +cannotBeChangedLater: "لا يمكن تغييره لاحقًا." +reactionAcceptance: "قبول التفاعلات" +rolesAssignedToMe: "الأدوار المسندة إلي" +resetPasswordConfirm: "هل تريد إعادة تعيين كلمة السر؟" +noteIdOrUrl: "معرف الملاحظة أو رابطها" +video: "فيديو" +videos: "فيديوهات" +accountMigration: "ترحيل الحساب" +accountMoved: "نقل هذا المستخدم حسابه:" +accountMovedShort: "رُحل هذا الحساب." +operationForbidden: "عملية ممنوعة" +forceShowAds: "أظهر الإعلانات التجارية دائما" +vertical: "عمودي" horizontal: "جانبي" +position: "الموضع" +serverRules: "قوانين الخادم" +pleaseConfirmBelowBeforeSignup: "رجاءً وافق على ما يلي قبل التسجيل." +pleaseAgreeAllToContinue: "للمتابعة وافق على الحقول أعلاه." +continue: "متابعة" +preservedUsernames: "أسماء المستخدمين المحجوزة" +preservedUsernamesDescription: "قائمة بأسماء المستخدمين المحجوزة كلٌ في سطر. لن يُقبل التسجيل بهذه الأسماء وستبقى محصورة على التسجيل اليدوي بواسطة المديرين. لن يتأثر المستخدمون الذين يملكون هذه الأسماء سلفًا." +archive: "الأرشيف" youFollowing: "متابَع" +options: "خيارات" _role: + new: "دور جديد" + edit: "حرر الأدوار" + name: "اسم الدور" + description: "وصف الدور" + permission: "أذونات الدور" + assignTarget: "نوع الإسناد" + options: "خيارات" + policies: "السياسة العامة" priority: "الأولوية" _priority: low: "منخفضة" middle: "متوسط" high: "عالية" + _options: + canManageCustomEmojis: "إدارة الإيموجي المخصصة" + _condition: + isLocal: "مستخدم محلي" + isRemote: "مستخدم بعيد" _emailUnavailable: used: "هذا البريد الإلكتروني مستخدم" format: "صيغة البريد الإلكتروني غير صالحة" @@ -1064,6 +1186,7 @@ _widgets: onlineUsers: "المتّصلون" jobQueue: "قائمة الانتظار" serverMetric: "إحصائيات الخادم" + userList: "قائمة المستخدمين" _userList: chooseList: "اختر قائمة" _cw: @@ -1127,6 +1250,7 @@ _profile: changeBanner: "غيّر اللافتة" _exportOrImport: allNotes: "كل الملاحظات" + favoritedNotes: " الملاحظات المفضلة" followingList: "المتابَعون" muteList: "المستخدمون المكتومون" blockingList: "المستخدمون المحجوبون" @@ -1145,6 +1269,8 @@ _charts: notesTotal: "إجمالي الملاحظات" filesIncDec: "تباين عدد الملفات" filesTotal: "العدد الإجمالي للملفات" + storageUsageIncDec: "التباين في استغلال مساحة التخزين" + storageUsageTotal: "اجمالي مساحة التخزين المستغلة" _instanceCharts: requests: "الطلبات" users: "تباين عدد المستخدمين" @@ -1205,7 +1331,7 @@ _pages: text: "نص" textarea: "حقل نصي" section: "قسم" - image: "الصور" + image: "صور" button: "زرّ" note: "ملاحظة مضمّنة" _note: @@ -1264,3 +1390,5 @@ _deck: _webhookSettings: name: "الإسم" active: "مفعّل" + _events: + reaction: "عند تلقي تفاعل" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 843470cf4..c4c12cb1a 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -52,6 +52,8 @@ addToList: "Zu Liste hinzufügen" sendMessage: "Nachricht senden" copyRSS: "RSS kopieren" copyUsername: "Benutzernamen kopieren" +copyUserId: "Benutzer-ID kopieren" +copyNoteId: "Notiz-ID kopieren" searchUser: "Nach einem Benutzer suchen" reply: "Antworten" loadMore: "Mehr laden" @@ -790,6 +792,7 @@ noMaintainerInformationWarning: "Betreiberinformationen sind nicht konfiguriert. noBotProtectionWarning: "Schutz vor Bots ist nicht konfiguriert." configure: "Konfigurieren" postToGallery: "Neuen Galeriebeitrag erstellen" +postToHashtag: "Mit diesem Hashtag senden" gallery: "Galerie" recentPosts: "Neue Beiträge" popularPosts: "Beliebte Beiträge" @@ -823,6 +826,7 @@ translatedFrom: "Aus {x} übersetzt" accountDeletionInProgress: "Die Löschung deines Benutzerkontos ist momentan in Bearbeitung." usernameInfo: "Ein Name, durch den dein Benutzerkonto auf diesem Server identifiziert werden kann. Du kannst das Alphabet (a~z, A~Z), Ziffern (0~9) oder Unterstriche (_) verwenden. Benutzernamen können später nicht geändert werden." aiChanMode: "Ai-Modus" +devMode: "Entwicklermodus" keepCw: "Inhaltswarnungen beibehalten" pubSub: "Pub/Sub Benutzerkonten" lastCommunication: "Letzte Kommunikation" @@ -832,6 +836,8 @@ breakFollow: "Follower entfernen" breakFollowConfirm: "Diesen Follower wirklich entfernen?" itsOn: "Eingeschaltet" itsOff: "Ausgeschaltet" +on: "An" +off: "Aus" emailRequiredForSignup: "Angabe einer Email-Adresse als benötigt markieren" unread: "Ungelesen" filter: "Filter" @@ -986,6 +992,8 @@ cannotBeChangedLater: "Kann später nicht mehr geändert werden." reactionAcceptance: "Reaktionsannahme" likeOnly: "Nur \"Gefällt mir\"" likeOnlyForRemote: "Nur \"Gefällt mir\" für fremde Instanzen" +nonSensitiveOnly: "Keine Sensitiven" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "Keine Sensitiven (Nur \"Gefällt mir\" von fremden Instanzen)" rolesAssignedToMe: "Mir zugewiesene Rollen" resetPasswordConfirm: "Wirklich Passwort zurücksetzen?" sensitiveWords: "Sensible Wörter" @@ -1043,6 +1051,17 @@ preventAiLearning: "Verwendung in machinellem Lernen (Generative bzw. Prediktive preventAiLearningDescription: "Fordert Crawler auf, gepostetes Text- oder Bildmaterial usw. nicht in Datensätzen für maschinelles Lernen (Generative bzw. Prediktive AI/KI) zu verwenden. Dies wird durch das Hinzufügen einer \"noai\"-Flag in der HTML-Antwort des jeweiligen Inhalts erreicht. Da diese Flag jedoch ignoriert werden kann, ist eine vollständige Verhinderung hierdurch nicht möglich." options: "Optionen" specifyUser: "Spezifischer Benutzer" +failedToPreviewUrl: "Vorschau nicht anzeigbar" +update: "Aktualisieren" +rolesThatCanBeUsedThisEmojiAsReaction: "Rollen, die dieses Emoji als Reaktion verwenden können" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Sind keine Rollen angegeben, kann jeder dieses Emoji als Reaktion verwenden." +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Diese Rollen müssen öffentlich sein." +cancelReactionConfirm: "Möchtest du deine Reaktion wirklich löschen?" +changeReactionConfirm: "Möchtest du deine Reaktion wirklich ändern?" +later: "Später" +goToMisskey: "Zu Misskey" +additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher" +installed: "Installiert" _initialAccountSetting: accountCreated: "Dein Konto wurde erfolgreich erstellt!" letsStartAccountSetup: "Lass uns nun dein Konto einrichten." @@ -1057,6 +1076,7 @@ _initialAccountSetting: haveFun: "Viel Spaß mit {name}!" ifYouNeedLearnMore: "Besuche {link}, falls du mehr über {name} (Misskey) lernen möchtest." skipAreYouSure: "Die Kontoeinrichtung wirklich überspringen?" + laterAreYouSure: "Die Kontoeinrichtung wirklich später erledigen?" _serverRules: description: "Eine Reihe von Regeln, die vor der Registrierung angezeigt werden. Eine Zusammenfassung der Nutzungsbedingungen anzuzeigen ist empfohlen." _accountMigration: diff --git a/locales/en-US.yml b/locales/en-US.yml index 3ea2313b2..0f1c7c89f 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -52,6 +52,8 @@ addToList: "Add to list" sendMessage: "Send a message" copyRSS: "Copy RSS" copyUsername: "Copy username" +copyUserId: "Copy user ID" +copyNoteId: "Copy note ID" searchUser: "Search for a user" reply: "Reply" loadMore: "Load more" @@ -790,6 +792,7 @@ noMaintainerInformationWarning: "Maintainer information is not configured." noBotProtectionWarning: "Bot protection is not configured." configure: "Configure" postToGallery: "Create new gallery post" +postToHashtag: "Post to this hashtag" gallery: "Gallery" recentPosts: "Recent posts" popularPosts: "Popular posts" @@ -823,6 +826,7 @@ translatedFrom: "Translated from {x}" accountDeletionInProgress: "Account deletion is currently in progress" usernameInfo: "A name that identifies your account from others on this server. You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames cannot be changed later." aiChanMode: "Ai Mode" +devMode: "Developer mode" keepCw: "Keep content warnings" pubSub: "Pub/Sub Accounts" lastCommunication: "Last communication" @@ -832,6 +836,8 @@ breakFollow: "Remove follower" breakFollowConfirm: "Really remove this follower?" itsOn: "Enabled" itsOff: "Disabled" +on: "On" +off: "Off" emailRequiredForSignup: "Require email address for sign-up" unread: "Unread" filter: "Filter" @@ -986,6 +992,8 @@ cannotBeChangedLater: "This cannot be changed later." reactionAcceptance: "Reaction Acceptance" likeOnly: "Only likes" likeOnlyForRemote: "Only likes for remote instances" +nonSensitiveOnly: "Non-sensitive only" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non-sensitive only (Only likes from remote)" rolesAssignedToMe: "Roles assigned to me" resetPasswordConfirm: "Really reset your password?" sensitiveWords: "Sensitive words" @@ -1043,6 +1051,17 @@ preventAiLearning: "Reject usage in Machine Learning (Generative AI)" preventAiLearningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (Predictive / Generative AI) data sets. This is achieved by adding a \"noai\" HTML-Response flag to the respective content. A complete prevention can however not be achieved through this flag, as it may simply be ignored." options: "Options" specifyUser: "Specific user" +failedToPreviewUrl: "Could not preview" +update: "Update" +rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "If no roles are specified, anyone can use this emoji as reaction." +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "These roles must be public." +cancelReactionConfirm: "Really delete your reaction?" +changeReactionConfirm: "Really change your reaction?" +later: "Later" +goToMisskey: "To Misskey" +additionalEmojiDictionary: "Additional emoji dictionaries" +installed: "Installed" _initialAccountSetting: accountCreated: "Your account was successfully created!" letsStartAccountSetup: "For starters, let's set up your profile." @@ -1057,6 +1076,7 @@ _initialAccountSetting: haveFun: "Enjoy {name}!" ifYouNeedLearnMore: "If you'd like to learn more about how to use {name} (Misskey), please visit {link}." skipAreYouSure: "Really skip profile setup?" + laterAreYouSure: "Really do profile setup later?" _serverRules: description: "A set of rules to be displayed before registration. Setting a summary of the Terms of Service is recommended." _accountMigration: diff --git a/locales/es-ES.yml b/locales/es-ES.yml index b043ecf3c..a5dd18e3f 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -20,6 +20,7 @@ noNotes: "No hay notas" noNotifications: "No hay notificaciones" instance: "Instancia" settings: "Configuración" +notificationSettings: "Configurar las notificaciones" basicSettings: "Configuración Básica" otherSettings: "Configuración avanzada" openInWindow: "Abrir en una ventana" @@ -262,8 +263,10 @@ noMoreHistory: "El historial se ha acabado" startMessaging: "Iniciar chat" nUsersRead: "Leído por {n} personas" agreeTo: "De acuerdo con {0}" +agree: "De acuerdo." agreeBelow: "Estoy de acuerdo con lo siguiente" basicNotesBeforeCreateAccount: "Notas básicas" +termsOfService: "Términos y condiciones" start: "Comenzar" home: "Inicio" remoteUserCaution: "Para el usuario remoto, la información está incompleta" @@ -473,6 +476,8 @@ createAccount: "Crear cuenta" existingAccount: "Cuenta existente" regenerate: "Regenerar" fontSize: "Tamaño de la letra" +mediaListWithOneImageAppearance: "Altura de la lista de medios con una sola imagen." +limitTo: "{x} hasta un máximo de" noFollowRequests: "No hay solicitudes de seguimiento" openImageInNewTab: "Abrir imagen en nueva pestaña" dashboard: "Panel de control" @@ -555,6 +560,7 @@ accountDeletedDescription: "Esta cuenta ha sido borrada." menu: "Menú" divider: "Divisor" addItem: "Agregar elemento" +rearrange: "Ordenar" relays: "Relés" addRelay: "Agregar relé" inboxUrl: "Inbox URL" @@ -698,6 +704,8 @@ contact: "Contacto" useSystemFont: "Utilizar la tipografía por defecto del sistema" clips: "Clip" experimentalFeatures: "Características experimentales" +experimental: "Función experimental" +thisIsExperimentalFeature: "Se trata de una función experimental. Las especificaciones pueden cambiar o puede que no funcione correctamente." developer: "Desarrolladores" makeExplorable: "Hacer visible la cuenta en \"Explorar\"" makeExplorableDescription: "Si desactiva esta opción, su cuenta no aparecerá en la sección \"Explorar\"." @@ -991,9 +999,12 @@ largeNoteReactions: "Agrandar las reacciones de las notas" noteIdOrUrl: "ID o URL de la nota" accountMigration: "Migración de cuenta" accountMoved: "Este usuario se ha mudado a una nueva cuenta:" +accountMovedShort: "Esta cuenta ha sido migrada." horizontal: "Horizontal" youFollowing: "Siguiendo" options: "Opción" +_initialAccountSetting: + accountCreated: "¡La cuenta ha sido creada!" _accountMigration: moveFrom: "Trasladar de otra cuenta a ésta" moveFromLabel: "Cuenta desde la que se realiza el traslado:" @@ -1174,6 +1185,8 @@ _achievements: _client30min: title: "Un descansito" description: "30 minutos dedicados a Misskey" + _client60min: + title: "Viendo mucho Misskey." _noteDeletedWithin1min: title: "Ah... Mejor no..." description: "Borrar una nota antes que de pase 1 minuto" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index d8ac41c92..173380805 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -908,12 +908,14 @@ neverShow: "Ne plus afficher" remindMeLater: "Peut-être plus tard" roles: "Rôles" role: "Rôles" +noRole: "Aucun rôle" normalUser: "Simple utilisateur·rice" assign: "Attribuer" color: "Couleur" manageCustomEmojis: "Gestion des émojis personnalisés" preset: "Préréglage" selectFromPresets: "Sélectionner à partir des préréglages" +thisPostMayBeAnnoying: "Cette note peut gêner d'autres personnes." thisPostMayBeAnnoyingCancel: "Annuler" license: "Licence" video: "Vidéo" @@ -921,6 +923,7 @@ videos: "Vidéos" dataSaver: "Économiseur de données" accountMigration: "Migration de compte" accountMoved: "Cet·te utilisateur·rice a migré son compte vers :" +addMemo: "Ajouter un mémo" notificationDisplay: "Style des notifications" leftTop: "En haut à gauche" rightTop: "En haut à droite" @@ -935,6 +938,8 @@ _achievements: _notes1: description: "Publiez votre première note" flavor: "Passez un bon moment avec Misskey !" + _notes100: + title: "Beaucoup de notes" _notes100000: title: "ALL YOUR NOTE ARE BELONG TO US" _login3: @@ -985,6 +990,8 @@ _achievements: title: "Joyeux Anniversaire !" _loggedInOnNewYearsDay: title: "Bonne année !" + _cookieClicked: + flavor: "Attendez une minute, vous êtes sur le mauvais site web ?" _role: assignTarget: "Attribuer" priority: "Priorité" diff --git a/locales/generateDTS.js b/locales/generateDTS.js new file mode 100644 index 000000000..5949aee7c --- /dev/null +++ b/locales/generateDTS.js @@ -0,0 +1,72 @@ +const fs = require('fs'); +const yaml = require('js-yaml'); +const ts = require('typescript'); + +function createMembers(record) { + return Object.entries(record) + .map(([k, v]) => ts.factory.createPropertySignature( + undefined, + ts.factory.createStringLiteral(k), + undefined, + typeof v === 'string' + ? ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword) + : ts.factory.createTypeLiteralNode(createMembers(v)), + )); +} + +module.exports = function generateDTS() { + const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8')); + const members = createMembers(locale); + const elements = [ + ts.factory.createInterfaceDeclaration( + [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier('Locale'), + undefined, + undefined, + members, + ), + ts.factory.createVariableStatement( + [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)], + ts.factory.createVariableDeclarationList( + [ts.factory.createVariableDeclaration( + ts.factory.createIdentifier('locales'), + undefined, + ts.factory.createTypeLiteralNode([ts.factory.createIndexSignature( + undefined, + [ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier('lang'), + undefined, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + undefined, + )], + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier('Locale'), + undefined, + ), + )]), + undefined, + )], + ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags, + ), + ), + ts.factory.createExportAssignment( + undefined, + true, + ts.factory.createIdentifier('locales'), + ), + ]; + const printed = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed, + }).printList( + ts.ListFormat.MultiLine, + ts.factory.createNodeArray(elements), + ts.createSourceFile('index.d.ts', '', ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS), + ); + + fs.writeFileSync(`${__dirname}/index.d.ts`, `/* eslint-disable */ +// This file is generated by locales/generateDTS.js +// Do not edit this file directly. +${printed}`, 'utf-8'); +} diff --git a/locales/id-ID.yml b/locales/id-ID.yml index df42697cc..70217caa2 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -20,6 +20,7 @@ noNotes: "Tidak ada catatan" noNotifications: "Tidak ada pemberitahuan" instance: "Instansi" settings: "Pengaturan" +notificationSettings: "Atur Notifikasi" basicSettings: "Pengaturan umum" otherSettings: "Pengaturan lainnya" openInWindow: "Buka di jendela" @@ -261,8 +262,10 @@ noMoreHistory: "Tidak ada sejarah lagi" startMessaging: "Mulai mengirim pesan" nUsersRead: "Dibaca oleh {n}" agreeTo: "Saya setuju kepada {0}" +agree: "Setuju" agreeBelow: "Saya setuju dengan di bawah ini" basicNotesBeforeCreateAccount: "Catatan penting" +termsOfService: "Syarat dan ketentuan" start: "Mulai" home: "Beranda" remoteUserCaution: "Informasi ini mungkin tidak mutakhir, karena pengguna ini berasal dari instansi luar." diff --git a/locales/index.d.ts b/locales/index.d.ts index fe3edb445..7047f42ef 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1,3 +1,2150 @@ -declare const locales: { [lang: string]: any }; - +/* eslint-disable */ +// This file is generated by locales/generateDTS.js +// Do not edit this file directly. +export interface Locale { + "_lang_": string; + "headlineMisskey": string; + "introMisskey": string; + "poweredByMisskeyDescription": string; + "monthAndDay": string; + "search": string; + "notifications": string; + "username": string; + "password": string; + "forgotPassword": string; + "fetchingAsApObject": string; + "ok": string; + "gotIt": string; + "cancel": string; + "noThankYou": string; + "enterUsername": string; + "renotedBy": string; + "noNotes": string; + "noNotifications": string; + "instance": string; + "settings": string; + "notificationSettings": string; + "basicSettings": string; + "otherSettings": string; + "openInWindow": string; + "profile": string; + "timeline": string; + "noAccountDescription": string; + "login": string; + "loggingIn": string; + "logout": string; + "signup": string; + "uploading": string; + "save": string; + "users": string; + "addUser": string; + "favorite": string; + "favorites": string; + "unfavorite": string; + "favorited": string; + "alreadyFavorited": string; + "cantFavorite": string; + "pin": string; + "unpin": string; + "copyContent": string; + "copyLink": string; + "delete": string; + "deleteAndEdit": string; + "deleteAndEditConfirm": string; + "addToList": string; + "sendMessage": string; + "copyRSS": string; + "copyUsername": string; + "copyUserId": string; + "copyNoteId": string; + "searchUser": string; + "reply": string; + "loadMore": string; + "showMore": string; + "showLess": string; + "youGotNewFollower": string; + "receiveFollowRequest": string; + "followRequestAccepted": string; + "mention": string; + "mentions": string; + "directNotes": string; + "importAndExport": string; + "import": string; + "export": string; + "files": string; + "download": string; + "driveFileDeleteConfirm": string; + "unfollowConfirm": string; + "exportRequested": string; + "importRequested": string; + "lists": string; + "noLists": string; + "note": string; + "notes": string; + "following": string; + "followers": string; + "followsYou": string; + "createList": string; + "manageLists": string; + "error": string; + "somethingHappened": string; + "retry": string; + "pageLoadError": string; + "pageLoadErrorDescription": string; + "serverIsDead": string; + "youShouldUpgradeClient": string; + "enterListName": string; + "privacy": string; + "makeFollowManuallyApprove": string; + "defaultNoteVisibility": string; + "follow": string; + "followRequest": string; + "followRequests": string; + "unfollow": string; + "followRequestPending": string; + "enterEmoji": string; + "renote": string; + "unrenote": string; + "renoted": string; + "cantRenote": string; + "cantReRenote": string; + "quote": string; + "inChannelRenote": string; + "inChannelQuote": string; + "pinnedNote": string; + "pinned": string; + "you": string; + "clickToShow": string; + "sensitive": string; + "add": string; + "reaction": string; + "reactions": string; + "reactionSetting": string; + "reactionSettingDescription2": string; + "rememberNoteVisibility": string; + "attachCancel": string; + "markAsSensitive": string; + "unmarkAsSensitive": string; + "enterFileName": string; + "mute": string; + "unmute": string; + "renoteMute": string; + "renoteUnmute": string; + "block": string; + "unblock": string; + "suspend": string; + "unsuspend": string; + "blockConfirm": string; + "unblockConfirm": string; + "suspendConfirm": string; + "unsuspendConfirm": string; + "selectList": string; + "selectChannel": string; + "selectAntenna": string; + "selectWidget": string; + "editWidgets": string; + "editWidgetsExit": string; + "customEmojis": string; + "emoji": string; + "emojis": string; + "emojiName": string; + "emojiUrl": string; + "addEmoji": string; + "settingGuide": string; + "cacheRemoteFiles": string; + "cacheRemoteFilesDescription": string; + "flagAsBot": string; + "flagAsBotDescription": string; + "flagAsCat": string; + "flagAsCatDescription": string; + "flagShowTimelineReplies": string; + "flagShowTimelineRepliesDescription": string; + "autoAcceptFollowed": string; + "addAccount": string; + "reloadAccountsList": string; + "loginFailed": string; + "showOnRemote": string; + "general": string; + "wallpaper": string; + "setWallpaper": string; + "removeWallpaper": string; + "searchWith": string; + "youHaveNoLists": string; + "followConfirm": string; + "proxyAccount": string; + "proxyAccountDescription": string; + "host": string; + "selectUser": string; + "recipient": string; + "annotation": string; + "federation": string; + "instances": string; + "registeredAt": string; + "latestRequestReceivedAt": string; + "latestStatus": string; + "storageUsage": string; + "charts": string; + "perHour": string; + "perDay": string; + "stopActivityDelivery": string; + "blockThisInstance": string; + "operations": string; + "software": string; + "version": string; + "metadata": string; + "withNFiles": string; + "monitor": string; + "jobQueue": string; + "cpuAndMemory": string; + "network": string; + "disk": string; + "instanceInfo": string; + "statistics": string; + "clearQueue": string; + "clearQueueConfirmTitle": string; + "clearQueueConfirmText": string; + "clearCachedFiles": string; + "clearCachedFilesConfirm": string; + "blockedInstances": string; + "blockedInstancesDescription": string; + "muteAndBlock": string; + "mutedUsers": string; + "blockedUsers": string; + "noUsers": string; + "editProfile": string; + "noteDeleteConfirm": string; + "pinLimitExceeded": string; + "intro": string; + "done": string; + "processing": string; + "preview": string; + "default": string; + "defaultValueIs": string; + "noCustomEmojis": string; + "noJobs": string; + "federating": string; + "blocked": string; + "suspended": string; + "all": string; + "subscribing": string; + "publishing": string; + "notResponding": string; + "instanceFollowing": string; + "instanceFollowers": string; + "instanceUsers": string; + "changePassword": string; + "security": string; + "retypedNotMatch": string; + "currentPassword": string; + "newPassword": string; + "newPasswordRetype": string; + "attachFile": string; + "more": string; + "featured": string; + "usernameOrUserId": string; + "noSuchUser": string; + "lookup": string; + "announcements": string; + "imageUrl": string; + "remove": string; + "removed": string; + "removeAreYouSure": string; + "deleteAreYouSure": string; + "resetAreYouSure": string; + "saved": string; + "messaging": string; + "upload": string; + "keepOriginalUploading": string; + "keepOriginalUploadingDescription": string; + "fromDrive": string; + "fromUrl": string; + "uploadFromUrl": string; + "uploadFromUrlDescription": string; + "uploadFromUrlRequested": string; + "uploadFromUrlMayTakeTime": string; + "explore": string; + "messageRead": string; + "noMoreHistory": string; + "startMessaging": string; + "nUsersRead": string; + "agreeTo": string; + "agree": string; + "agreeBelow": string; + "basicNotesBeforeCreateAccount": string; + "termsOfService": string; + "start": string; + "home": string; + "remoteUserCaution": string; + "activity": string; + "images": string; + "image": string; + "birthday": string; + "yearsOld": string; + "registeredDate": string; + "location": string; + "theme": string; + "themeForLightMode": string; + "themeForDarkMode": string; + "light": string; + "dark": string; + "lightThemes": string; + "darkThemes": string; + "syncDeviceDarkMode": string; + "drive": string; + "fileName": string; + "selectFile": string; + "selectFiles": string; + "selectFolder": string; + "selectFolders": string; + "renameFile": string; + "folderName": string; + "createFolder": string; + "renameFolder": string; + "deleteFolder": string; + "addFile": string; + "emptyDrive": string; + "emptyFolder": string; + "unableToDelete": string; + "inputNewFileName": string; + "inputNewDescription": string; + "inputNewFolderName": string; + "circularReferenceFolder": string; + "hasChildFilesOrFolders": string; + "copyUrl": string; + "rename": string; + "avatar": string; + "banner": string; + "nsfw": string; + "whenServerDisconnected": string; + "disconnectedFromServer": string; + "reload": string; + "doNothing": string; + "reloadConfirm": string; + "watch": string; + "unwatch": string; + "accept": string; + "reject": string; + "normal": string; + "instanceName": string; + "instanceDescription": string; + "maintainerName": string; + "maintainerEmail": string; + "tosUrl": string; + "thisYear": string; + "thisMonth": string; + "today": string; + "dayX": string; + "monthX": string; + "yearX": string; + "pages": string; + "integration": string; + "connectService": string; + "disconnectService": string; + "enableLocalTimeline": string; + "enableGlobalTimeline": string; + "disablingTimelinesInfo": string; + "registration": string; + "enableRegistration": string; + "invite": string; + "driveCapacityPerLocalAccount": string; + "driveCapacityPerRemoteAccount": string; + "inMb": string; + "iconUrl": string; + "bannerUrl": string; + "backgroundImageUrl": string; + "basicInfo": string; + "pinnedUsers": string; + "pinnedUsersDescription": string; + "pinnedPages": string; + "pinnedPagesDescription": string; + "pinnedClipId": string; + "pinnedNotes": string; + "hcaptcha": string; + "enableHcaptcha": string; + "hcaptchaSiteKey": string; + "hcaptchaSecretKey": string; + "recaptcha": string; + "enableRecaptcha": string; + "recaptchaSiteKey": string; + "recaptchaSecretKey": string; + "turnstile": string; + "enableTurnstile": string; + "turnstileSiteKey": string; + "turnstileSecretKey": string; + "avoidMultiCaptchaConfirm": string; + "antennas": string; + "manageAntennas": string; + "name": string; + "antennaSource": string; + "antennaKeywords": string; + "antennaExcludeKeywords": string; + "antennaKeywordsDescription": string; + "notifyAntenna": string; + "withFileAntenna": string; + "enableServiceworker": string; + "antennaUsersDescription": string; + "caseSensitive": string; + "withReplies": string; + "connectedTo": string; + "notesAndReplies": string; + "withFiles": string; + "silence": string; + "silenceConfirm": string; + "unsilence": string; + "unsilenceConfirm": string; + "popularUsers": string; + "recentlyUpdatedUsers": string; + "recentlyRegisteredUsers": string; + "recentlyDiscoveredUsers": string; + "exploreUsersCount": string; + "exploreFediverse": string; + "popularTags": string; + "userList": string; + "about": string; + "aboutMisskey": string; + "administrator": string; + "token": string; + "2fa": string; + "totp": string; + "totpDescription": string; + "moderator": string; + "moderation": string; + "nUsersMentioned": string; + "securityKeyAndPasskey": string; + "securityKey": string; + "lastUsed": string; + "lastUsedAt": string; + "unregister": string; + "passwordLessLogin": string; + "passwordLessLoginDescription": string; + "resetPassword": string; + "newPasswordIs": string; + "reduceUiAnimation": string; + "share": string; + "notFound": string; + "notFoundDescription": string; + "uploadFolder": string; + "cacheClear": string; + "markAsReadAllNotifications": string; + "markAsReadAllUnreadNotes": string; + "markAsReadAllTalkMessages": string; + "help": string; + "inputMessageHere": string; + "close": string; + "invites": string; + "members": string; + "transfer": string; + "title": string; + "text": string; + "enable": string; + "next": string; + "retype": string; + "noteOf": string; + "quoteAttached": string; + "quoteQuestion": string; + "noMessagesYet": string; + "newMessageExists": string; + "onlyOneFileCanBeAttached": string; + "signinRequired": string; + "invitations": string; + "invitationCode": string; + "checking": string; + "available": string; + "unavailable": string; + "usernameInvalidFormat": string; + "tooShort": string; + "tooLong": string; + "weakPassword": string; + "normalPassword": string; + "strongPassword": string; + "passwordMatched": string; + "passwordNotMatched": string; + "signinWith": string; + "signinFailed": string; + "or": string; + "language": string; + "uiLanguage": string; + "aboutX": string; + "emojiStyle": string; + "native": string; + "disableDrawer": string; + "showNoteActionsOnlyHover": string; + "noHistory": string; + "signinHistory": string; + "enableAdvancedMfm": string; + "enableAnimatedMfm": string; + "doing": string; + "category": string; + "tags": string; + "docSource": string; + "createAccount": string; + "existingAccount": string; + "regenerate": string; + "fontSize": string; + "mediaListWithOneImageAppearance": string; + "limitTo": string; + "noFollowRequests": string; + "openImageInNewTab": string; + "dashboard": string; + "local": string; + "remote": string; + "total": string; + "weekOverWeekChanges": string; + "dayOverDayChanges": string; + "appearance": string; + "clientSettings": string; + "accountSettings": string; + "promotion": string; + "promote": string; + "numberOfDays": string; + "hideThisNote": string; + "showFeaturedNotesInTimeline": string; + "objectStorage": string; + "useObjectStorage": string; + "objectStorageBaseUrl": string; + "objectStorageBaseUrlDesc": string; + "objectStorageBucket": string; + "objectStorageBucketDesc": string; + "objectStoragePrefix": string; + "objectStoragePrefixDesc": string; + "objectStorageEndpoint": string; + "objectStorageEndpointDesc": string; + "objectStorageRegion": string; + "objectStorageRegionDesc": string; + "objectStorageUseSSL": string; + "objectStorageUseSSLDesc": string; + "objectStorageUseProxy": string; + "objectStorageUseProxyDesc": string; + "objectStorageSetPublicRead": string; + "s3ForcePathStyleDesc": string; + "serverLogs": string; + "deleteAll": string; + "showFixedPostForm": string; + "showFixedPostFormInChannel": string; + "newNoteRecived": string; + "sounds": string; + "sound": string; + "listen": string; + "none": string; + "showInPage": string; + "popout": string; + "volume": string; + "masterVolume": string; + "details": string; + "chooseEmoji": string; + "unableToProcess": string; + "recentUsed": string; + "install": string; + "uninstall": string; + "installedApps": string; + "nothing": string; + "installedDate": string; + "lastUsedDate": string; + "state": string; + "sort": string; + "ascendingOrder": string; + "descendingOrder": string; + "scratchpad": string; + "scratchpadDescription": string; + "output": string; + "script": string; + "disablePagesScript": string; + "updateRemoteUser": string; + "deleteAllFiles": string; + "deleteAllFilesConfirm": string; + "removeAllFollowing": string; + "removeAllFollowingDescription": string; + "userSuspended": string; + "userSilenced": string; + "yourAccountSuspendedTitle": string; + "yourAccountSuspendedDescription": string; + "tokenRevoked": string; + "tokenRevokedDescription": string; + "accountDeleted": string; + "accountDeletedDescription": string; + "menu": string; + "divider": string; + "addItem": string; + "rearrange": string; + "relays": string; + "addRelay": string; + "inboxUrl": string; + "addedRelays": string; + "serviceworkerInfo": string; + "deletedNote": string; + "invisibleNote": string; + "enableInfiniteScroll": string; + "visibility": string; + "poll": string; + "useCw": string; + "enablePlayer": string; + "disablePlayer": string; + "expandTweet": string; + "themeEditor": string; + "description": string; + "describeFile": string; + "enterFileDescription": string; + "author": string; + "leaveConfirm": string; + "manage": string; + "plugins": string; + "preferencesBackups": string; + "deck": string; + "undeck": string; + "useBlurEffectForModal": string; + "useFullReactionPicker": string; + "width": string; + "height": string; + "large": string; + "medium": string; + "small": string; + "generateAccessToken": string; + "permission": string; + "enableAll": string; + "disableAll": string; + "tokenRequested": string; + "pluginTokenRequestedDescription": string; + "notificationType": string; + "edit": string; + "emailServer": string; + "enableEmail": string; + "emailConfigInfo": string; + "email": string; + "emailAddress": string; + "smtpConfig": string; + "smtpHost": string; + "smtpPort": string; + "smtpUser": string; + "smtpPass": string; + "emptyToDisableSmtpAuth": string; + "smtpSecure": string; + "smtpSecureInfo": string; + "testEmail": string; + "wordMute": string; + "regexpError": string; + "regexpErrorDescription": string; + "instanceMute": string; + "userSaysSomething": string; + "makeActive": string; + "display": string; + "copy": string; + "metrics": string; + "overview": string; + "logs": string; + "delayed": string; + "database": string; + "channel": string; + "create": string; + "notificationSetting": string; + "notificationSettingDesc": string; + "useGlobalSetting": string; + "useGlobalSettingDesc": string; + "other": string; + "regenerateLoginToken": string; + "regenerateLoginTokenDescription": string; + "setMultipleBySeparatingWithSpace": string; + "fileIdOrUrl": string; + "behavior": string; + "sample": string; + "abuseReports": string; + "reportAbuse": string; + "reportAbuseOf": string; + "fillAbuseReportDescription": string; + "abuseReported": string; + "reporter": string; + "reporteeOrigin": string; + "reporterOrigin": string; + "forwardReport": string; + "forwardReportIsAnonymous": string; + "send": string; + "abuseMarkAsResolved": string; + "openInNewTab": string; + "openInSideView": string; + "defaultNavigationBehaviour": string; + "editTheseSettingsMayBreakAccount": string; + "instanceTicker": string; + "waitingFor": string; + "random": string; + "system": string; + "switchUi": string; + "desktop": string; + "clip": string; + "createNew": string; + "optional": string; + "createNewClip": string; + "unclip": string; + "confirmToUnclipAlreadyClippedNote": string; + "public": string; + "i18nInfo": string; + "manageAccessTokens": string; + "accountInfo": string; + "notesCount": string; + "repliesCount": string; + "renotesCount": string; + "repliedCount": string; + "renotedCount": string; + "followingCount": string; + "followersCount": string; + "sentReactionsCount": string; + "receivedReactionsCount": string; + "pollVotesCount": string; + "pollVotedCount": string; + "yes": string; + "no": string; + "driveFilesCount": string; + "driveUsage": string; + "noCrawle": string; + "noCrawleDescription": string; + "lockedAccountInfo": string; + "alwaysMarkSensitive": string; + "loadRawImages": string; + "disableShowingAnimatedImages": string; + "verificationEmailSent": string; + "notSet": string; + "emailVerified": string; + "noteFavoritesCount": string; + "pageLikesCount": string; + "pageLikedCount": string; + "contact": string; + "useSystemFont": string; + "clips": string; + "experimentalFeatures": string; + "experimental": string; + "thisIsExperimentalFeature": string; + "developer": string; + "makeExplorable": string; + "makeExplorableDescription": string; + "showGapBetweenNotesInTimeline": string; + "duplicate": string; + "left": string; + "center": string; + "wide": string; + "narrow": string; + "reloadToApplySetting": string; + "needReloadToApply": string; + "showTitlebar": string; + "clearCache": string; + "onlineUsersCount": string; + "nUsers": string; + "nNotes": string; + "sendErrorReports": string; + "sendErrorReportsDescription": string; + "myTheme": string; + "backgroundColor": string; + "accentColor": string; + "textColor": string; + "saveAs": string; + "advanced": string; + "advancedSettings": string; + "value": string; + "createdAt": string; + "updatedAt": string; + "saveConfirm": string; + "deleteConfirm": string; + "invalidValue": string; + "registry": string; + "closeAccount": string; + "currentVersion": string; + "latestVersion": string; + "youAreRunningUpToDateClient": string; + "newVersionOfClientAvailable": string; + "usageAmount": string; + "capacity": string; + "inUse": string; + "editCode": string; + "apply": string; + "receiveAnnouncementFromInstance": string; + "emailNotification": string; + "publish": string; + "inChannelSearch": string; + "useReactionPickerForContextMenu": string; + "typingUsers": string; + "jumpToSpecifiedDate": string; + "showingPastTimeline": string; + "clear": string; + "markAllAsRead": string; + "goBack": string; + "unlikeConfirm": string; + "fullView": string; + "quitFullView": string; + "addDescription": string; + "userPagePinTip": string; + "notSpecifiedMentionWarning": string; + "info": string; + "userInfo": string; + "unknown": string; + "onlineStatus": string; + "hideOnlineStatus": string; + "hideOnlineStatusDescription": string; + "online": string; + "active": string; + "offline": string; + "notRecommended": string; + "botProtection": string; + "instanceBlocking": string; + "selectAccount": string; + "switchAccount": string; + "enabled": string; + "disabled": string; + "quickAction": string; + "user": string; + "administration": string; + "accounts": string; + "switch": string; + "noMaintainerInformationWarning": string; + "noBotProtectionWarning": string; + "configure": string; + "postToGallery": string; + "postToHashtag": string; + "gallery": string; + "recentPosts": string; + "popularPosts": string; + "shareWithNote": string; + "ads": string; + "expiration": string; + "startingperiod": string; + "memo": string; + "priority": string; + "high": string; + "middle": string; + "low": string; + "emailNotConfiguredWarning": string; + "ratio": string; + "previewNoteText": string; + "customCss": string; + "customCssWarn": string; + "global": string; + "squareAvatars": string; + "sent": string; + "received": string; + "searchResult": string; + "hashtags": string; + "troubleshooting": string; + "useBlurEffect": string; + "learnMore": string; + "misskeyUpdated": string; + "whatIsNew": string; + "translate": string; + "translatedFrom": string; + "accountDeletionInProgress": string; + "usernameInfo": string; + "aiChanMode": string; + "devMode": string; + "keepCw": string; + "pubSub": string; + "lastCommunication": string; + "resolved": string; + "unresolved": string; + "breakFollow": string; + "breakFollowConfirm": string; + "itsOn": string; + "itsOff": string; + "on": string; + "off": string; + "emailRequiredForSignup": string; + "unread": string; + "filter": string; + "controlPanel": string; + "manageAccounts": string; + "makeReactionsPublic": string; + "makeReactionsPublicDescription": string; + "classic": string; + "muteThread": string; + "unmuteThread": string; + "ffVisibility": string; + "ffVisibilityDescription": string; + "continueThread": string; + "deleteAccountConfirm": string; + "incorrectPassword": string; + "voteConfirm": string; + "hide": string; + "useDrawerReactionPickerForMobile": string; + "welcomeBackWithName": string; + "clickToFinishEmailVerification": string; + "overridedDeviceKind": string; + "smartphone": string; + "tablet": string; + "auto": string; + "themeColor": string; + "size": string; + "numberOfColumn": string; + "searchByGoogle": string; + "instanceDefaultLightTheme": string; + "instanceDefaultDarkTheme": string; + "instanceDefaultThemeDescription": string; + "mutePeriod": string; + "period": string; + "indefinitely": string; + "tenMinutes": string; + "oneHour": string; + "oneDay": string; + "oneWeek": string; + "oneMonth": string; + "reflectMayTakeTime": string; + "failedToFetchAccountInformation": string; + "rateLimitExceeded": string; + "cropImage": string; + "cropImageAsk": string; + "cropYes": string; + "cropNo": string; + "file": string; + "recentNHours": string; + "recentNDays": string; + "noEmailServerWarning": string; + "thereIsUnresolvedAbuseReportWarning": string; + "recommended": string; + "check": string; + "driveCapOverrideLabel": string; + "driveCapOverrideCaption": string; + "requireAdminForView": string; + "isSystemAccount": string; + "typeToConfirm": string; + "deleteAccount": string; + "document": string; + "numberOfPageCache": string; + "numberOfPageCacheDescription": string; + "logoutConfirm": string; + "lastActiveDate": string; + "statusbar": string; + "pleaseSelect": string; + "reverse": string; + "colored": string; + "refreshInterval": string; + "label": string; + "type": string; + "speed": string; + "slow": string; + "fast": string; + "sensitiveMediaDetection": string; + "localOnly": string; + "remoteOnly": string; + "failedToUpload": string; + "cannotUploadBecauseInappropriate": string; + "cannotUploadBecauseNoFreeSpace": string; + "cannotUploadBecauseExceedsFileSizeLimit": string; + "beta": string; + "enableAutoSensitive": string; + "enableAutoSensitiveDescription": string; + "activeEmailValidationDescription": string; + "navbar": string; + "shuffle": string; + "account": string; + "move": string; + "pushNotification": string; + "subscribePushNotification": string; + "unsubscribePushNotification": string; + "pushNotificationAlreadySubscribed": string; + "pushNotificationNotSupported": string; + "sendPushNotificationReadMessage": string; + "sendPushNotificationReadMessageCaption": string; + "windowMaximize": string; + "windowMinimize": string; + "windowRestore": string; + "caption": string; + "loggedInAsBot": string; + "tools": string; + "cannotLoad": string; + "numberOfProfileView": string; + "like": string; + "unlike": string; + "numberOfLikes": string; + "show": string; + "neverShow": string; + "remindMeLater": string; + "didYouLikeMisskey": string; + "pleaseDonate": string; + "roles": string; + "role": string; + "noRole": string; + "normalUser": string; + "undefined": string; + "assign": string; + "unassign": string; + "color": string; + "manageCustomEmojis": string; + "youCannotCreateAnymore": string; + "cannotPerformTemporary": string; + "cannotPerformTemporaryDescription": string; + "invalidParamError": string; + "invalidParamErrorDescription": string; + "permissionDeniedError": string; + "permissionDeniedErrorDescription": string; + "preset": string; + "selectFromPresets": string; + "achievements": string; + "gotInvalidResponseError": string; + "gotInvalidResponseErrorDescription": string; + "thisPostMayBeAnnoying": string; + "thisPostMayBeAnnoyingHome": string; + "thisPostMayBeAnnoyingCancel": string; + "thisPostMayBeAnnoyingIgnore": string; + "collapseRenotes": string; + "internalServerError": string; + "internalServerErrorDescription": string; + "copyErrorInfo": string; + "joinThisServer": string; + "exploreOtherServers": string; + "letsLookAtTimeline": string; + "disableFederationConfirm": string; + "disableFederationConfirmWarn": string; + "disableFederationOk": string; + "invitationRequiredToRegister": string; + "emailNotSupported": string; + "postToTheChannel": string; + "cannotBeChangedLater": string; + "reactionAcceptance": string; + "likeOnly": string; + "likeOnlyForRemote": string; + "nonSensitiveOnly": string; + "nonSensitiveOnlyForLocalLikeOnlyForRemote": string; + "rolesAssignedToMe": string; + "resetPasswordConfirm": string; + "sensitiveWords": string; + "sensitiveWordsDescription": string; + "sensitiveWordsDescription2": string; + "notesSearchNotAvailable": string; + "license": string; + "unfavoriteConfirm": string; + "myClips": string; + "drivecleaner": string; + "retryAllQueuesNow": string; + "retryAllQueuesConfirmTitle": string; + "retryAllQueuesConfirmText": string; + "enableChartsForRemoteUser": string; + "enableChartsForFederatedInstances": string; + "showClipButtonInNoteFooter": string; + "largeNoteReactions": string; + "noteIdOrUrl": string; + "video": string; + "videos": string; + "dataSaver": string; + "accountMigration": string; + "accountMoved": string; + "accountMovedShort": string; + "operationForbidden": string; + "forceShowAds": string; + "addMemo": string; + "editMemo": string; + "reactionsList": string; + "renotesList": string; + "notificationDisplay": string; + "leftTop": string; + "rightTop": string; + "leftBottom": string; + "rightBottom": string; + "stackAxis": string; + "vertical": string; + "horizontal": string; + "position": string; + "serverRules": string; + "pleaseConfirmBelowBeforeSignup": string; + "pleaseAgreeAllToContinue": string; + "continue": string; + "preservedUsernames": string; + "preservedUsernamesDescription": string; + "createNoteFromTheFile": string; + "archive": string; + "channelArchiveConfirmTitle": string; + "channelArchiveConfirmDescription": string; + "thisChannelArchived": string; + "displayOfNote": string; + "initialAccountSetting": string; + "youFollowing": string; + "preventAiLearning": string; + "preventAiLearningDescription": string; + "options": string; + "specifyUser": string; + "failedToPreviewUrl": string; + "update": string; + "rolesThatCanBeUsedThisEmojiAsReaction": string; + "rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription": string; + "rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn": string; + "cancelReactionConfirm": string; + "changeReactionConfirm": string; + "later": string; + "goToMisskey": string; + "additionalEmojiDictionary": string; + "installed": string; + "_initialAccountSetting": { + "accountCreated": string; + "letsStartAccountSetup": string; + "letsFillYourProfile": string; + "profileSetting": string; + "privacySetting": string; + "theseSettingsCanEditLater": string; + "youCanEditMoreSettingsInSettingsPageLater": string; + "followUsers": string; + "pushNotificationDescription": string; + "initialAccountSettingCompleted": string; + "haveFun": string; + "ifYouNeedLearnMore": string; + "skipAreYouSure": string; + "laterAreYouSure": string; + }; + "_serverRules": { + "description": string; + }; + "_accountMigration": { + "moveFrom": string; + "moveFromSub": string; + "moveFromLabel": string; + "moveFromDescription": string; + "moveTo": string; + "moveToLabel": string; + "moveCannotBeUndone": string; + "moveAccountDescription": string; + "moveAccountHowTo": string; + "startMigration": string; + "migrationConfirm": string; + "movedAndCannotBeUndone": string; + "postMigrationNote": string; + "movedTo": string; + }; + "_achievements": { + "earnedAt": string; + "_types": { + "_notes1": { + "title": string; + "description": string; + "flavor": string; + }; + "_notes10": { + "title": string; + "description": string; + }; + "_notes100": { + "title": string; + "description": string; + }; + "_notes500": { + "title": string; + "description": string; + }; + "_notes1000": { + "title": string; + "description": string; + }; + "_notes5000": { + "title": string; + "description": string; + }; + "_notes10000": { + "title": string; + "description": string; + }; + "_notes20000": { + "title": string; + "description": string; + }; + "_notes30000": { + "title": string; + "description": string; + }; + "_notes40000": { + "title": string; + "description": string; + }; + "_notes50000": { + "title": string; + "description": string; + }; + "_notes60000": { + "title": string; + "description": string; + }; + "_notes70000": { + "title": string; + "description": string; + }; + "_notes80000": { + "title": string; + "description": string; + }; + "_notes90000": { + "title": string; + "description": string; + }; + "_notes100000": { + "title": string; + "description": string; + "flavor": string; + }; + "_login3": { + "title": string; + "description": string; + "flavor": string; + }; + "_login7": { + "title": string; + "description": string; + "flavor": string; + }; + "_login15": { + "title": string; + "description": string; + }; + "_login30": { + "title": string; + "description": string; + }; + "_login60": { + "title": string; + "description": string; + }; + "_login100": { + "title": string; + "description": string; + "flavor": string; + }; + "_login200": { + "title": string; + "description": string; + }; + "_login300": { + "title": string; + "description": string; + }; + "_login400": { + "title": string; + "description": string; + }; + "_login500": { + "title": string; + "description": string; + "flavor": string; + }; + "_login600": { + "title": string; + "description": string; + }; + "_login700": { + "title": string; + "description": string; + }; + "_login800": { + "title": string; + "description": string; + }; + "_login900": { + "title": string; + "description": string; + }; + "_login1000": { + "title": string; + "description": string; + "flavor": string; + }; + "_noteClipped1": { + "title": string; + "description": string; + }; + "_noteFavorited1": { + "title": string; + "description": string; + }; + "_myNoteFavorited1": { + "title": string; + "description": string; + }; + "_profileFilled": { + "title": string; + "description": string; + }; + "_markedAsCat": { + "title": string; + "description": string; + "flavor": string; + }; + "_following1": { + "title": string; + "description": string; + }; + "_following10": { + "title": string; + "description": string; + }; + "_following50": { + "title": string; + "description": string; + }; + "_following100": { + "title": string; + "description": string; + }; + "_following300": { + "title": string; + "description": string; + }; + "_followers1": { + "title": string; + "description": string; + }; + "_followers10": { + "title": string; + "description": string; + }; + "_followers50": { + "title": string; + "description": string; + }; + "_followers100": { + "title": string; + "description": string; + }; + "_followers300": { + "title": string; + "description": string; + }; + "_followers500": { + "title": string; + "description": string; + }; + "_followers1000": { + "title": string; + "description": string; + }; + "_collectAchievements30": { + "title": string; + "description": string; + }; + "_viewAchievements3min": { + "title": string; + "description": string; + }; + "_iLoveMisskey": { + "title": string; + "description": string; + "flavor": string; + }; + "_foundTreasure": { + "title": string; + "description": string; + }; + "_client30min": { + "title": string; + "description": string; + }; + "_client60min": { + "title": string; + "description": string; + }; + "_noteDeletedWithin1min": { + "title": string; + "description": string; + }; + "_postedAtLateNight": { + "title": string; + "description": string; + "flavor": string; + }; + "_postedAt0min0sec": { + "title": string; + "description": string; + "flavor": string; + }; + "_selfQuote": { + "title": string; + "description": string; + }; + "_htl20npm": { + "title": string; + "description": string; + }; + "_viewInstanceChart": { + "title": string; + "description": string; + }; + "_outputHelloWorldOnScratchpad": { + "title": string; + "description": string; + }; + "_open3windows": { + "title": string; + "description": string; + }; + "_driveFolderCircularReference": { + "title": string; + "description": string; + }; + "_reactWithoutRead": { + "title": string; + "description": string; + }; + "_clickedClickHere": { + "title": string; + "description": string; + }; + "_justPlainLucky": { + "title": string; + "description": string; + }; + "_setNameToSyuilo": { + "title": string; + "description": string; + }; + "_passedSinceAccountCreated1": { + "title": string; + "description": string; + }; + "_passedSinceAccountCreated2": { + "title": string; + "description": string; + }; + "_passedSinceAccountCreated3": { + "title": string; + "description": string; + }; + "_loggedInOnBirthday": { + "title": string; + "description": string; + }; + "_loggedInOnNewYearsDay": { + "title": string; + "description": string; + "flavor": string; + }; + "_cookieClicked": { + "title": string; + "description": string; + "flavor": string; + }; + "_brainDiver": { + "title": string; + "description": string; + "flavor": string; + }; + }; + }; + "_role": { + "new": string; + "edit": string; + "name": string; + "description": string; + "permission": string; + "descriptionOfPermission": string; + "assignTarget": string; + "descriptionOfAssignTarget": string; + "manual": string; + "conditional": string; + "condition": string; + "isConditionalRole": string; + "isPublic": string; + "descriptionOfIsPublic": string; + "options": string; + "policies": string; + "baseRole": string; + "useBaseValue": string; + "chooseRoleToAssign": string; + "iconUrl": string; + "asBadge": string; + "descriptionOfAsBadge": string; + "isExplorable": string; + "descriptionOfIsExplorable": string; + "displayOrder": string; + "descriptionOfDisplayOrder": string; + "canEditMembersByModerator": string; + "descriptionOfCanEditMembersByModerator": string; + "priority": string; + "_priority": { + "low": string; + "middle": string; + "high": string; + }; + "_options": { + "gtlAvailable": string; + "ltlAvailable": string; + "canPublicNote": string; + "canInvite": string; + "canManageCustomEmojis": string; + "driveCapacity": string; + "alwaysMarkNsfw": string; + "pinMax": string; + "antennaMax": string; + "wordMuteMax": string; + "webhookMax": string; + "clipMax": string; + "noteEachClipsMax": string; + "userListMax": string; + "userEachUserListsMax": string; + "rateLimitFactor": string; + "descriptionOfRateLimitFactor": string; + "canHideAds": string; + "canSearchNotes": string; + }; + "_condition": { + "isLocal": string; + "isRemote": string; + "createdLessThan": string; + "createdMoreThan": string; + "followersLessThanOrEq": string; + "followersMoreThanOrEq": string; + "followingLessThanOrEq": string; + "followingMoreThanOrEq": string; + "notesLessThanOrEq": string; + "notesMoreThanOrEq": string; + "and": string; + "or": string; + "not": string; + }; + }; + "_sensitiveMediaDetection": { + "description": string; + "sensitivity": string; + "sensitivityDescription": string; + "setSensitiveFlagAutomatically": string; + "setSensitiveFlagAutomaticallyDescription": string; + "analyzeVideos": string; + "analyzeVideosDescription": string; + }; + "_emailUnavailable": { + "used": string; + "format": string; + "disposable": string; + "mx": string; + "smtp": string; + }; + "_ffVisibility": { + "public": string; + "followers": string; + "private": string; + }; + "_signup": { + "almostThere": string; + "emailAddressInfo": string; + "emailSent": string; + }; + "_accountDelete": { + "accountDelete": string; + "mayTakeTime": string; + "sendEmail": string; + "requestAccountDelete": string; + "started": string; + "inProgress": string; + }; + "_ad": { + "back": string; + "reduceFrequencyOfThisAd": string; + "hide": string; + }; + "_forgotPassword": { + "enterEmail": string; + "ifNoEmail": string; + "contactAdmin": string; + }; + "_gallery": { + "my": string; + "liked": string; + "like": string; + "unlike": string; + }; + "_email": { + "_follow": { + "title": string; + }; + "_receiveFollowRequest": { + "title": string; + }; + }; + "_plugin": { + "install": string; + "installWarn": string; + "manage": string; + }; + "_preferencesBackups": { + "list": string; + "saveNew": string; + "loadFile": string; + "apply": string; + "save": string; + "inputName": string; + "cannotSave": string; + "nameAlreadyExists": string; + "applyConfirm": string; + "saveConfirm": string; + "deleteConfirm": string; + "renameConfirm": string; + "noBackups": string; + "createdAt": string; + "updatedAt": string; + "cannotLoad": string; + "invalidFile": string; + }; + "_registry": { + "scope": string; + "key": string; + "keys": string; + "domain": string; + "createKey": string; + }; + "_aboutMisskey": { + "about": string; + "contributors": string; + "allContributors": string; + "source": string; + "translation": string; + "donate": string; + "morePatrons": string; + "patrons": string; + }; + "_nsfw": { + "respect": string; + "ignore": string; + "force": string; + }; + "_instanceTicker": { + "none": string; + "remote": string; + "always": string; + }; + "_serverDisconnectedBehavior": { + "reload": string; + "dialog": string; + "quiet": string; + }; + "_channel": { + "create": string; + "edit": string; + "setBanner": string; + "removeBanner": string; + "featured": string; + "owned": string; + "following": string; + "usersCount": string; + "notesCount": string; + "nameAndDescription": string; + "nameOnly": string; + }; + "_menuDisplay": { + "sideFull": string; + "sideIcon": string; + "top": string; + "hide": string; + }; + "_wordMute": { + "muteWords": string; + "muteWordsDescription": string; + "muteWordsDescription2": string; + "softDescription": string; + "hardDescription": string; + "soft": string; + "hard": string; + "mutedNotes": string; + }; + "_instanceMute": { + "instanceMuteDescription": string; + "instanceMuteDescription2": string; + "title": string; + "heading": string; + }; + "_theme": { + "explore": string; + "install": string; + "manage": string; + "code": string; + "description": string; + "installed": string; + "installedThemes": string; + "builtinThemes": string; + "alreadyInstalled": string; + "invalid": string; + "make": string; + "base": string; + "addConstant": string; + "constant": string; + "defaultValue": string; + "color": string; + "refProp": string; + "refConst": string; + "key": string; + "func": string; + "funcKind": string; + "argument": string; + "basedProp": string; + "alpha": string; + "darken": string; + "lighten": string; + "inputConstantName": string; + "importInfo": string; + "deleteConstantConfirm": string; + "keys": { + "accent": string; + "bg": string; + "fg": string; + "focus": string; + "indicator": string; + "panel": string; + "shadow": string; + "header": string; + "navBg": string; + "navFg": string; + "navHoverFg": string; + "navActive": string; + "navIndicator": string; + "link": string; + "hashtag": string; + "mention": string; + "mentionMe": string; + "renote": string; + "modalBg": string; + "divider": string; + "scrollbarHandle": string; + "scrollbarHandleHover": string; + "dateLabelFg": string; + "infoBg": string; + "infoFg": string; + "infoWarnBg": string; + "infoWarnFg": string; + "cwBg": string; + "cwFg": string; + "cwHoverBg": string; + "toastBg": string; + "toastFg": string; + "buttonBg": string; + "buttonHoverBg": string; + "inputBorder": string; + "listItemHoverBg": string; + "driveFolderBg": string; + "wallpaperOverlay": string; + "badge": string; + "messageBg": string; + "accentDarken": string; + "accentLighten": string; + "fgHighlighted": string; + }; + }; + "_sfx": { + "note": string; + "noteMy": string; + "notification": string; + "chat": string; + "chatBg": string; + "antenna": string; + "channel": string; + }; + "_ago": { + "future": string; + "justNow": string; + "secondsAgo": string; + "minutesAgo": string; + "hoursAgo": string; + "daysAgo": string; + "weeksAgo": string; + "monthsAgo": string; + "yearsAgo": string; + "invalid": string; + }; + "_time": { + "second": string; + "minute": string; + "hour": string; + "day": string; + }; + "_timelineTutorial": { + "title": string; + "step1_1": string; + "step1_2": string; + "step2_1": string; + "step2_2": string; + "step3_1": string; + "step3_2": string; + "step4_1": string; + "step4_2": string; + }; + "_2fa": { + "alreadyRegistered": string; + "registerTOTP": string; + "passwordToTOTP": string; + "step1": string; + "step2": string; + "step2Click": string; + "step2Url": string; + "step3Title": string; + "step3": string; + "step4": string; + "securityKeyNotSupported": string; + "registerTOTPBeforeKey": string; + "securityKeyInfo": string; + "chromePasskeyNotSupported": string; + "registerSecurityKey": string; + "securityKeyName": string; + "tapSecurityKey": string; + "removeKey": string; + "removeKeyConfirm": string; + "whyTOTPOnlyRenew": string; + "renewTOTP": string; + "renewTOTPConfirm": string; + "renewTOTPOk": string; + "renewTOTPCancel": string; + }; + "_permissions": { + "read:account": string; + "write:account": string; + "read:blocks": string; + "write:blocks": string; + "read:drive": string; + "write:drive": string; + "read:favorites": string; + "write:favorites": string; + "read:following": string; + "write:following": string; + "read:messaging": string; + "write:messaging": string; + "read:mutes": string; + "write:mutes": string; + "write:notes": string; + "read:notifications": string; + "write:notifications": string; + "read:reactions": string; + "write:reactions": string; + "write:votes": string; + "read:pages": string; + "write:pages": string; + "read:page-likes": string; + "write:page-likes": string; + "read:user-groups": string; + "write:user-groups": string; + "read:channels": string; + "write:channels": string; + "read:gallery": string; + "write:gallery": string; + "read:gallery-likes": string; + "write:gallery-likes": string; + }; + "_auth": { + "shareAccessTitle": string; + "shareAccess": string; + "shareAccessAsk": string; + "permission": string; + "permissionAsk": string; + "pleaseGoBack": string; + "callback": string; + "denied": string; + "pleaseLogin": string; + }; + "_antennaSources": { + "all": string; + "homeTimeline": string; + "users": string; + "userList": string; + }; + "_weekday": { + "sunday": string; + "monday": string; + "tuesday": string; + "wednesday": string; + "thursday": string; + "friday": string; + "saturday": string; + }; + "_widgets": { + "profile": string; + "instanceInfo": string; + "memo": string; + "notifications": string; + "timeline": string; + "calendar": string; + "trends": string; + "clock": string; + "rss": string; + "rssTicker": string; + "activity": string; + "photos": string; + "digitalClock": string; + "unixClock": string; + "federation": string; + "instanceCloud": string; + "postForm": string; + "slideshow": string; + "button": string; + "onlineUsers": string; + "jobQueue": string; + "serverMetric": string; + "aiscript": string; + "aiscriptApp": string; + "aichan": string; + "userList": string; + "_userList": { + "chooseList": string; + }; + "clicker": string; + }; + "_cw": { + "hide": string; + "show": string; + "chars": string; + "files": string; + }; + "_poll": { + "noOnlyOneChoice": string; + "choiceN": string; + "noMore": string; + "canMultipleVote": string; + "expiration": string; + "infinite": string; + "at": string; + "after": string; + "deadlineDate": string; + "deadlineTime": string; + "duration": string; + "votesCount": string; + "totalVotes": string; + "vote": string; + "showResult": string; + "voted": string; + "closed": string; + "remainingDays": string; + "remainingHours": string; + "remainingMinutes": string; + "remainingSeconds": string; + }; + "_visibility": { + "public": string; + "publicDescription": string; + "home": string; + "homeDescription": string; + "followers": string; + "followersDescription": string; + "specified": string; + "specifiedDescription": string; + "disableFederation": string; + "disableFederationDescription": string; + }; + "_postForm": { + "replyPlaceholder": string; + "quotePlaceholder": string; + "channelPlaceholder": string; + "_placeholders": { + "a": string; + "b": string; + "c": string; + "d": string; + "e": string; + "f": string; + }; + }; + "_profile": { + "name": string; + "username": string; + "description": string; + "youCanIncludeHashtags": string; + "metadata": string; + "metadataEdit": string; + "metadataDescription": string; + "metadataLabel": string; + "metadataContent": string; + "changeAvatar": string; + "changeBanner": string; + }; + "_exportOrImport": { + "allNotes": string; + "favoritedNotes": string; + "followingList": string; + "muteList": string; + "blockingList": string; + "userLists": string; + "excludeMutingUsers": string; + "excludeInactiveUsers": string; + }; + "_charts": { + "federation": string; + "apRequest": string; + "usersIncDec": string; + "usersTotal": string; + "activeUsers": string; + "notesIncDec": string; + "localNotesIncDec": string; + "remoteNotesIncDec": string; + "notesTotal": string; + "filesIncDec": string; + "filesTotal": string; + "storageUsageIncDec": string; + "storageUsageTotal": string; + }; + "_instanceCharts": { + "requests": string; + "users": string; + "usersTotal": string; + "notes": string; + "notesTotal": string; + "ff": string; + "ffTotal": string; + "cacheSize": string; + "cacheSizeTotal": string; + "files": string; + "filesTotal": string; + }; + "_timelines": { + "home": string; + "local": string; + "social": string; + "global": string; + }; + "_play": { + "new": string; + "edit": string; + "created": string; + "updated": string; + "deleted": string; + "pageSetting": string; + "editThisPage": string; + "viewSource": string; + "my": string; + "liked": string; + "featured": string; + "title": string; + "script": string; + "summary": string; + }; + "_pages": { + "newPage": string; + "editPage": string; + "readPage": string; + "created": string; + "updated": string; + "deleted": string; + "pageSetting": string; + "nameAlreadyExists": string; + "invalidNameTitle": string; + "invalidNameText": string; + "editThisPage": string; + "viewSource": string; + "viewPage": string; + "like": string; + "unlike": string; + "my": string; + "liked": string; + "featured": string; + "inspector": string; + "contents": string; + "content": string; + "variables": string; + "title": string; + "url": string; + "summary": string; + "alignCenter": string; + "hideTitleWhenPinned": string; + "font": string; + "fontSerif": string; + "fontSansSerif": string; + "eyeCatchingImageSet": string; + "eyeCatchingImageRemove": string; + "chooseBlock": string; + "selectType": string; + "contentBlocks": string; + "inputBlocks": string; + "specialBlocks": string; + "blocks": { + "text": string; + "textarea": string; + "section": string; + "image": string; + "button": string; + "note": string; + "_note": { + "id": string; + "idDescription": string; + "detailed": string; + }; + }; + }; + "_relayStatus": { + "requesting": string; + "accepted": string; + "rejected": string; + }; + "_notification": { + "fileUploaded": string; + "youGotMention": string; + "youGotReply": string; + "youGotQuote": string; + "youRenoted": string; + "youWereFollowed": string; + "youReceivedFollowRequest": string; + "yourFollowRequestAccepted": string; + "pollEnded": string; + "unreadAntennaNote": string; + "emptyPushNotificationMessage": string; + "achievementEarned": string; + "_types": { + "all": string; + "follow": string; + "mention": string; + "reply": string; + "renote": string; + "quote": string; + "reaction": string; + "pollEnded": string; + "receiveFollowRequest": string; + "followRequestAccepted": string; + "achievementEarned": string; + "app": string; + }; + "_actions": { + "followBack": string; + "reply": string; + "renote": string; + }; + }; + "_deck": { + "alwaysShowMainColumn": string; + "columnAlign": string; + "addColumn": string; + "configureColumn": string; + "swapLeft": string; + "swapRight": string; + "swapUp": string; + "swapDown": string; + "stackLeft": string; + "popRight": string; + "profile": string; + "newProfile": string; + "deleteProfile": string; + "introduction": string; + "introduction2": string; + "widgetsIntroduction": string; + "_columns": { + "main": string; + "widgets": string; + "notifications": string; + "tl": string; + "antenna": string; + "list": string; + "channel": string; + "mentions": string; + "direct": string; + "roleTimeline": string; + }; + }; + "_dialog": { + "charactersExceeded": string; + "charactersBelow": string; + }; + "_disabledTimeline": { + "title": string; + "description": string; + }; + "_drivecleaner": { + "orderBySizeDesc": string; + "orderByCreatedAtAsc": string; + }; + "_webhookSettings": { + "createWebhook": string; + "name": string; + "secret": string; + "events": string; + "active": string; + "_events": { + "follow": string; + "followed": string; + "note": string; + "reply": string; + "renote": string; + "reaction": string; + "mention": string; + }; + }; +} +declare const locales: { + [lang: string]: Locale; +}; export = locales; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0b7108fe6..fcba3fb82 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -52,6 +52,8 @@ addToList: "リストに追加" sendMessage: "メッセージを送信" copyRSS: "RSSをコピー" copyUsername: "ユーザー名をコピー" +copyUserId: "ユーザーIDをコピー" +copyNoteId: "ノートIDをコピー" searchUser: "ユーザーを検索" reply: "返信" loadMore: "もっと見る" @@ -790,6 +792,7 @@ noMaintainerInformationWarning: "管理者情報が設定されていません noBotProtectionWarning: "Botプロテクションが設定されていません。" configure: "設定する" postToGallery: "ギャラリーへ投稿" +postToHashtag: "このハッシュタグで投稿" gallery: "ギャラリー" recentPosts: "最近の投稿" popularPosts: "人気の投稿" @@ -823,6 +826,7 @@ translatedFrom: "{x}から翻訳" accountDeletionInProgress: "アカウントの削除が進行中です" usernameInfo: "サーバー上であなたのアカウントを一意に識別するための名前。アルファベット(a~z, A~Z)、数字(0~9)、およびアンダーバー(_)が使用できます。ユーザー名は後から変更することは出来ません。" aiChanMode: "藍モード" +devMode: "開発者モード" keepCw: "CWを維持する" pubSub: "Pub/Subのアカウント" lastCommunication: "直近の通信" @@ -987,7 +991,9 @@ postToTheChannel: "チャンネルに投稿" cannotBeChangedLater: "後から変更できません。" reactionAcceptance: "リアクションの受け入れ" likeOnly: "いいねのみ" -likeOnlyForRemote: "リモートからはいいねのみ" +likeOnlyForRemote: "全て (リモートはいいねのみ)" +nonSensitiveOnly: "非センシティブのみ" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "非センシティブのみ (リモートはいいねのみ)" rolesAssignedToMe: "自分に割り当てられたロール" resetPasswordConfirm: "パスワードリセットしますか?" sensitiveWords: "センシティブワード" @@ -1045,6 +1051,17 @@ preventAiLearning: "生成AIによる学習を拒否" preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したノートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。" options: "オプション" specifyUser: "ユーザー指定" +failedToPreviewUrl: "プレビューできません" +update: "更新" +rolesThatCanBeUsedThisEmojiAsReaction: "リアクションとして使えるロール" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールの指定が一つもない場合、誰でもリアクションとして使えます。" +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "ロールは公開ロールである必要があります。" +cancelReactionConfirm: "リアクションを取り消しますか?" +changeReactionConfirm: "リアクションを変更しますか?" +later: "あとで" +goToMisskey: "Misskeyへ" +additionalEmojiDictionary: "絵文字の追加辞書" +installed: "インストール済み" _initialAccountSetting: accountCreated: "アカウントの作成が完了しました!" @@ -1060,6 +1077,7 @@ _initialAccountSetting: haveFun: "{name}をお楽しみください!" ifYouNeedLearnMore: "{name}(Misskey)の使い方などを詳しく知るには{link}をご覧ください。" skipAreYouSure: "初期設定をスキップしますか?" + laterAreYouSure: "初期設定をあとでやり直しますか?" _serverRules: description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index d09f75155..652814ca9 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -8,7 +8,7 @@ search: "探す" notifications: "通知" username: "ユーザー名" password: "パスワード" -forgotPassword: "パスワード忘れてもうた" +forgotPassword: "パスワード忘れたん?" fetchingAsApObject: "今ちと連合に照会しとるで" ok: "ええで" gotIt: "ほい" @@ -47,11 +47,13 @@ copyContent: "内容をコピー" copyLink: "リンクをコピー" delete: "ほかす" deleteAndEdit: "ほかして直す" -deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのリアクション、Renote、返信も全部消えるんやけどそれでもええん?" +deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのツッコミ、Renote、返信も全部消えるんやけどそれでもええん?" addToList: "リストに入れたる" sendMessage: "メッセージを送る" copyRSS: "RSSをコピー" copyUsername: "ユーザー名をコピー" +copyUserId: "ユーザーIDをコピー" +copyNoteId: "ノートIDをコピー" searchUser: "ユーザーを検索" reply: "返事" loadMore: "まだまだあるで!" @@ -1043,6 +1045,10 @@ preventAiLearning: "生成AIの学習に使わんといて" preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したノートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。" options: "オプション" specifyUser: "ユーザー指定" +rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールが一個も指定されてへんかったら、誰でもツッコミとして使えるで。" +cancelReactionConfirm: "ツッコむんをやっぱやめるか?" +changeReactionConfirm: "ツッコミを別のに変えるか?" _initialAccountSetting: accountCreated: "アカウント作り終わったで。" letsStartAccountSetup: "アカウントの初期設定をしよか。" @@ -1614,7 +1620,7 @@ _timelineTutorial: step2_2: "最初のノートは、自己紹介とか「{name}始めてみたんや」とかがええと思うで。" step3_1: "投稿できた?" step3_2: "あんたのノートがタイムラインに出てきたら成功や。" - step4_1: "ノートには、「リアクション」を付けれるで。" + step4_1: "ノートには、「ツッコミ」を付けれるで。" step4_2: "ツッコむんやったら、ノートの「+」マークを押して、好きな絵文字を選ぶで。" _2fa: alreadyRegistered: "もう設定終わっとるわ。" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 39574d332..fd46eef1f 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -52,6 +52,8 @@ addToList: "리스트에 추가" sendMessage: "메시지 보내기" copyRSS: "RSS 복사" copyUsername: "유저명 복사" +copyUserId: "유저 ID 복사" +copyNoteId: "노트 ID 복사" searchUser: "사용자 검색" reply: "답글" loadMore: "더 보기" @@ -505,7 +507,7 @@ objectStoragePrefixDesc: "이 Prefix 의 디렉토리 아래에 파일이 저장 objectStorageEndpoint: "Endpoint" objectStorageEndpointDesc: "AWS S3의 경우 공란, 다른 서비스의 경우 각 서비스의 가이드에 맞게 endpoint를 설정해주세요. '<host>' 혹은 '<host>:<port>' 와 같이 지정합니다." objectStorageRegion: "Region" -objectStorageRegionDesc: "'xx-east-1'와 같이 region을 지정해주세요. 사용하는 서비스에 region 개념이 없는 경우, 비워 두거나 'us-east-1'으로 설정해 주세요." +objectStorageRegionDesc: "'xx-east-1'와 같이 region을 지정해 주세요. 사용하는 서비스에 region 개념이 없는 경우 'us-east-1'으로 설정해 주세요. AWS 설정 파일 또는 환경 변수를 참조할 경우에는 비워주세요." objectStorageUseSSL: "SSL 사용" objectStorageUseSSLDesc: "API 호출시 HTTPS 를 사용하지 않는 경우 OFF 로 설정해 주세요" objectStorageUseProxy: "연결에 프록시를 사용" @@ -790,6 +792,7 @@ noMaintainerInformationWarning: "관리자 정보가 설정되어 있지 않습 noBotProtectionWarning: "Bot 방어가 설정되어 있지 않습니다." configure: "설정하기" postToGallery: "갤러리에 업로드" +postToHashtag: "이 해시태그에 게시" gallery: "갤러리" recentPosts: "최근 포스트" popularPosts: "인기 포스트" @@ -823,6 +826,7 @@ translatedFrom: "{x}에서 번역" accountDeletionInProgress: "계정 삭제 작업을 진행하고 있습니다" usernameInfo: "서버상에서 계정을 식별하기 위한 이름. 알파벳(a~z, A~Z), 숫자(0~9) 및 언더바(_)를 사용할 수 있습니다. 사용자명은 나중에 변경할 수 없습니다." aiChanMode: "아이 모드" +devMode: "개발자 모드" keepCw: "CW 유지하기" pubSub: "Pub/Sub 계정" lastCommunication: "마지막 통신" @@ -830,8 +834,10 @@ resolved: "해결됨" unresolved: "해결되지 않음" breakFollow: "팔로워 해제" breakFollowConfirm: "팔로우를 해제하시겠습니까?" -itsOn: "켜짐" -itsOff: "꺼짐" +itsOn: "켜져 있습니다" +itsOff: "꺼져 있습니다" +on: "켜짐" +off: "꺼짐" emailRequiredForSignup: "가입할 때 이메일 주소 입력을 필수로 하기" unread: "읽지 않음" filter: "필터" @@ -864,7 +870,7 @@ instanceDefaultLightTheme: "서버 기본 라이트 테마" instanceDefaultDarkTheme: "서버 기본 다크 테마" instanceDefaultThemeDescription: "객체 형식의 테마 코드를 입력해 주세요." mutePeriod: "뮤트할 기간" -period: "투표 기한" +period: "기간" indefinitely: "무기한" tenMinutes: "10분" oneHour: "1시간" @@ -986,10 +992,13 @@ cannotBeChangedLater: "나중에 변경할 수 없습니다." reactionAcceptance: "리액션 수신" likeOnly: "좋아요만 받기" likeOnlyForRemote: "리모트에서는 좋아요만 받기" +nonSensitiveOnly: "열람 주의로 설정되지 않았을 때만 받기" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "열람 주의로 설정되지 않았을 때만 받기 (리모트에서는 좋아요만 받기)" rolesAssignedToMe: "나에게 할당된 역할" resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?" sensitiveWords: "민감한 단어" sensitiveWordsDescription: "설정한 단어가 포함된 노트의 공개 범위를 '홈'으로 강제합니다. 개행으로 구분하여 여러 개를 지정할 수 있습니다." +sensitiveWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다." notesSearchNotAvailable: "노트 검색을 이용하실 수 없습니다." license: "라이선스" unfavoriteConfirm: "즐겨찾기를 해제하시겠습니까?" @@ -1038,31 +1047,47 @@ thisChannelArchived: "이 채널은 아카이브되었습니다." displayOfNote: "노트 표시" initialAccountSetting: "초기 설정" youFollowing: "팔로잉" +preventAiLearning: "기계학습(생성형 AI)으로의 사용을 거부" +preventAiLearningDescription: "외부의 문장 생성 AI나 이미지 생성 AI에 대해 제출한 노트나 이미지 등의 콘텐츠를 학습의 대상으로 사용하지 않도록 요구합니다. 다만, 이 요구사항을 지킬 의무는 없기 때문에 학습을 완전히 방지하는 것은 아닙니다." options: "옵션" +specifyUser: "사용자 지정" +failedToPreviewUrl: "미리 볼 수 없음" +update: "업데이트" +rolesThatCanBeUsedThisEmojiAsReaction: "이 이모지를 리액션으로 사용할 수 있는 역할" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "역할을 지정하지 않으면, 누구나 이 이모지를 리액션으로 사용할 수 있습니다." +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "역할은 공개로 설정되어 있어야 합니다." +cancelReactionConfirm: "리액션을 취소하시겠습니까?" +changeReactionConfirm: "리액션을 변경하시겠습니까?" +later: "나중에" +goToMisskey: "Misskey로" +additionalEmojiDictionary: "이모지 추가 사전" +installed: "설치됨" _initialAccountSetting: accountCreated: "계정 생성이 완료되었습니다!" letsStartAccountSetup: "계정의 초기 설정을 진행합니다." letsFillYourProfile: "우선 나의 프로필을 설정해 보아요." profileSetting: "프로필 설정" + privacySetting: "프라이버시 설정" theseSettingsCanEditLater: "이 설정들은 나중에도 변경할 수 있습니다." - youCanEditMoreSettingsInSettingsPageLater: "이 외에도 '설정' 페이지에서 다양한 설정을 나의 입맛에 맛게 조절할 수 있습니다. 꼭 확인해 보세요!" + youCanEditMoreSettingsInSettingsPageLater: "이 외에도 '설정' 페이지에서 다양한 설정을 나의 입맛에 맞게 조절할 수 있습니다. 꼭 확인해 보세요!" followUsers: "관심사가 맞는 유저를 팔로우하여 타임라인을 가꾸어 봅시다." pushNotificationDescription: "푸시 알림을 활성화하면 {name}의 알림을 나의 기기에서 받아볼 수 있게 됩니다." initialAccountSettingCompleted: "초기 설정을 모두 마쳤습니다!" haveFun: "{name}와 함께 즐거운 시간 보내세요!" ifYouNeedLearnMore: "{name}(Misskey)의 사용 방법에 대해 자세히 알아보려면 {link}를 참고해 주세요." - skipAreYouSure: "초기 설정을 넘기시겠습니까?" + skipAreYouSure: "초기 설정을 중단하시겠습니까?" + laterAreYouSure: "초기 설정을 나중에 진행하시겠습니까?" _serverRules: description: "회원 가입 이전에 간단하게 표시할 서버 규칙입니다. 이용 약관의 요약으로 구성하는 것을 추천합니다." _accountMigration: moveFrom: "다른 계정에서 이 계정으로 이사" moveFromSub: "다른 계정에 대한 별칭을 생성" - moveFromLabel: "기존 계정:" + moveFromLabel: "기존 계정 #{n}" moveFromDescription: "다른 계정에서 이 계정으로 팔로워를 가져오려면, 우선 여기에서 별칭을 지정해야 합니다. 반드시 이사하기 전에 지정해야 합니다! 기존 계정을 다음과 같은 형식으로 입력해 주십시오: @person@instance.com" moveTo: "이 계정에서 다른 계정으로 이사" moveToLabel: "이사할 계정:" moveCannotBeUndone: "한 번 이사하면, 두 번 다시 되돌릴 수 없습니다." - moveAccountDescription: "이 작업은 취소할 수 없습니다. 먼저 이사할 계정에서 이 계정에 대한 별칭을 지정하였는지 다시 한 번 확인해 주십시오. 별칭을 지정한 다음, 이사할 계정을 다음과 같은 형식으로 입력해 주십시오: @person@instance.com" + moveAccountDescription: "새 계정으로 이전합니다.\n ・팔로워가 새 계정을 자동으로 팔로우 합니다\n ・이 계정에서 팔로우는 모두 해제됩니다\n ・이 계정으로는 노트 작성 등을 할 수 없게 됩니다\n\n팔로워는 자동으로 이전되지만, 팔로우는 수동으로 진행해야 합니다. 이전하기 전에 이 계정에서 팔로우를 내보내고, 이전 후에는 즉시 이전한 계정에서 가져오기를 진행하십시오.\n리스트・뮤트・차단에 대해서도 마찬가지이므로 수동으로 이전해야 합니다.\n\n(이 설명은 이 서버(Misskey v13.12.0 이후)의 사양입니다. Mastodon 등의 다른 ActivityPub 소프트웨어에서는 작동이 다를 수 있습니다.)" moveAccountHowTo: "계정을 이사하려면 우선 이사갈 계정에서 이 계정에 대한 별칭을 지정해야 합니다.\n별칭을 작성한 다음, 이사갈 계정을 다음과 같이 입력하십시오:\n@username@server.example.com" startMigration: "이사하기" migrationConfirm: "정말로 이 계정을 {account} 으로 이전하시겠습니까? 한 번 이전한 다음에는 취소할 수 없으며, 두 번 다시 원래 상태로 복구할 수 없습니다.\n이사할 계정에서 계정 별칭을 지정하였는지 다시 한 번 확인하십시오." @@ -1491,7 +1516,7 @@ _menuDisplay: hide: "숨기기" _wordMute: muteWords: "뮤트할 단어" - muteWordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다。" + muteWordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다." muteWordsDescription2: "정규 표현식을 사용하려면 키워드를 빗금표(/)로 감싸 주세요." softDescription: "지정한 조건의 노트를 타임라인에서 숨깁니다." hardDescription: "지정한 조건의 노트를 타임라인에 추가하지 않습니다. 타임라인에 추가되지 않은 노트는 조건을 변경해도 표시되지 않습니다." diff --git a/locales/no-NO.yml b/locales/no-NO.yml index 36c29295f..ec2900527 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -1,21 +1,31 @@ --- _lang_: "Norsk Bokmål" +headlineMisskey: "Et nettverk forbundet med Notes" +introMisskey: "Velkommen! Misskey er en desentralisert mikrobloggtjeneste med åpen kildekode.\nOpprett \"Notes\" for å dele tankene dine med alle rundt deg. 📡\nMed \"reaksjoner\" kan du også raskt gi uttrykk for hva du synes om alles Notes. 👍\nLa oss utforske en ny verden! 🚀" +monthAndDay: "{day}-{month}" search: "Søk" notifications: "Varsler" username: "Brukernavn" password: "Passord" forgotPassword: "Glemt passord" +fetchingAsApObject: "Henter fra Fediverse..." ok: "OK" gotIt: "Skjønner" cancel: "Avbryt" -noThankYou: "Avbryt" +noThankYou: "Ikke nå" +enterUsername: "Skriv inn brukernavn" +renotedBy: "Renotes av {user}" +noNotes: "Ingen Notes" noNotifications: "Ingen varsler" instance: "Server" settings: "Innstillinger" notificationSettings: "Varslingsinnstillinger" +basicSettings: "Grunnleggende innstillinger" otherSettings: "Andre innstillinger" +openInWindow: "Åpne i vindu" profile: "Profil" timeline: "Tidslinje" +noAccountDescription: "Denne brukeren har ikke skrevet sin biografi ennå." login: "Logg inn" loggingIn: "Logget inn" logout: "Logg ut" @@ -24,17 +34,21 @@ uploading: "Laster opp" save: "Lagre" users: "Brukere" addUser: "Legg til bruker" -favorite: "Favoritt" +favorite: "Legg til i favoritter" favorites: "Favoritter" -unfavorite: "Fjern favoritt" -pin: "Fest" -unpin: "Opphev festing" +unfavorite: "Fjern fra favoritter" +favorited: "Lagt til i favoritter." +alreadyFavorited: "Allerede lagt til i favoritter." +cantFavorite: "Kunne ikke legge til i favoritter." +pin: "Fest til profil" +unpin: "Fjern fra profil" copyContent: "Kopier innhold" copyLink: "Kopier lenke" delete: "Slett" deleteAndEdit: "Slett og rediger" +deleteAndEditConfirm: "Er du sikker på at du vil slette denne Noten og redigere den? Du vil miste alle reaksjoner, Renotes og svar på den." addToList: "Legg til i liste" -sendMessage: "Send melding" +sendMessage: "Send en melding" copyRSS: "Kopier RSS" copyUsername: "Kopier brukernavn" searchUser: "Søk brukere" @@ -42,140 +56,290 @@ reply: "Svar" loadMore: "Vis mer" showMore: "Vis mer" showLess: "Lukk" +youGotNewFollower: "fulgte deg" +followRequestAccepted: "Følgeforespørsel akseptert" +importAndExport: "Importer og eksporter" +import: "Importer" +export: "Eksporter" files: "Filer" download: "Nedlastinger" +driveFileDeleteConfirm: "Er du sikker på at du vil slette \"{name}\"? Det vil også forsvinne fra alt innhold som bruker det." +unfollowConfirm: "Er du sikker på at du vil slutte å følge {name}?" +importRequested: "Du har bedt om import. Dette kan ta en stund." lists: "Lister" noLists: "Ingen lister" -following: "Følg" +note: "Note" +notes: "Notes" +following: "Følger" followers: "Følgere" followsYou: "Følger deg" createList: "Opprett liste" error: "Feil" +somethingHappened: "En feil har oppstått" retry: "Prøv igjen" pageLoadError: "Kunne ikke hente side." +serverIsDead: "Denne serveren svarer ikke. Vennligst vent en stund og prøv igjen." +enterListName: "Skriv inn et navn på listen" privacy: "Personvern" +defaultNoteVisibility: "Standard synlighet" follow: "Følg" followRequest: "Følgeforespørsel" followRequests: "Følgeforespørsel" unfollow: "Avfølg" followRequestPending: "Venter på godkjenning" +enterEmoji: "Skriv inn en emoji" +renote: "Renote" +renoted: "Renotet." +cantRenote: "Dette innlegget kan ikke renotes." +cantReRenote: "En Renote kan ikke renotes." quote: "Sitat" -pinned: "Fest" +inChannelRenote: "Renote kun for kanal" +inChannelQuote: "Sitat kun for kanal" +pinnedNote: "Festet Note" +pinned: "Fest til profil" you: "Du" clickToShow: "Klikk for å vise" add: "Legg til" reaction: "Reaksjon" reactions: "Reaksjoner" +reactionSetting: "Reaksjoner som vises i reaksjonsvelgeren" +reactionSettingDescription2: "Dra for å endre rekkefølgen, klikk for å slette, trykk \"+\" for å legge til." +rememberNoteVisibility: "Husk innstillingene for synlighet av Notes" +attachCancel: "Fjern vedlegg" +enterFileName: "Skriv inn filnavn" mute: "Skjul" unmute: "Vis" +renoteMute: "Skjul Renotes" +renoteUnmute: "Vis Renotes" block: "Blokker" unblock: "Opphev blokkering" -blockConfirm: "Blokker?" -selectList: "Velg liste" -selectChannel: "Velg kanal" +suspend: "Suspender" +blockConfirm: "Er du sikker på at du vil blokke denne kontoen?" +unblockConfirm: "Er du sikker på at du vil oppheve blokkeringen av denne kontoen?" +suspendConfirm: "Er du sikker på at du vil suspendere denne kontoen?" +selectList: "Velg en liste" +selectChannel: "Velg en kanal" +selectAntenna: "Velg en antenne" +selectWidget: "Velg en widget" +editWidgets: "Rediger widgeter" +editWidgetsExit: "Ferdig" emoji: "Emoji" emojis: "Emojier" addEmoji: "Legg til emoji" +settingGuide: "Anbefalte innstillinger" +flagAsBot: "Merk denne kontoen som en bot" +flagAsBotDescription: "Aktiver dette alternativet hvis denne kontoen styres av et program. Hvis det er aktivert, vil det fungere som et flagg for andre utviklere for å forhindre endeløse interaksjonskjeder med andre roboter og justere Misskeys interne systemer til å behandle denne kontoen som en bot." +flagAsCat: "Merk denne kontoen som en katt" +flagAsCatDescription: "Aktiver dette alternativet for å merke denne kontoen som en katt." +flagShowTimelineReplies: "Vis svar i tidslinje" addAccount: "Legg til konto" -selectUser: "Velg bruker" -instances: "Server" +reloadAccountsList: "Last inn kontoliste på nytt" +loginFailed: "Kunne ikke logge inn" +general: "Generelt" +searchWith: "Søk: {q}" +youHaveNoLists: "Du har ingen lister" +followConfirm: "Er du sikker på at du vil følge {name}?" +host: "Vert" +selectUser: "Velg en bruker" +recipient: "Mottaker" +annotation: "Kommentarer" +federation: "Føderasjon" +instances: "Servere" registeredAt: "Registrerte seg" +latestRequestReceivedAt: "Siste forespørsel mottatt" +latestStatus: "Siste status" +charts: "Diagrammer" perHour: "Per time" perDay: "Per dag" +stopActivityDelivery: "Slutt å sende aktiviteter" blockThisInstance: "Blokker denne serveren" +operations: "Operasjoner" +software: "Programvare" version: "Versjon" +metadata: "Metadata" +withNFiles: "{n} fil(er)" +network: "Nettverk" +instanceInfo: "Serverinformasjon" statistics: "Statistikk" clearQueue: "Tøm kø" -clearQueueConfirmTitle: "Vil du tømme kø?" +clearQueueConfirmTitle: "Er du sikker på at du vil tømme køen?" blockedInstances: "Blokkerte severe" +blockedInstancesDescription: "Skriv opp vertsnavnene til serverne du vil blokkere, atskilt med linjeskift. Serverne i listen vil ikke lenger kunne kommunisere med denne serveren." muteAndBlock: "Skjul og blokker" mutedUsers: "Skjulte brukere" blockedUsers: "Blokkerte brukere" +noUsers: "Det er ingen brukere" editProfile: "Rediger profil" +noteDeleteConfirm: "Er du sikker på at du vil slette denne Noten?" pinLimitExceeded: "Du kan ikke feste flere." -noCustomEmojis: "Ingen emoji" +intro: "Installasjonen av Misskey er ferdig! Vennligst opprett en administratorkonto." +done: "Ferdig" +default: "Standard" +defaultValueIs: "Standard: {value}" +noCustomEmojis: "Det er ingen emoji" +noJobs: "Det er ingen jobber" blocked: "Blokkert" +suspended: "Suspendert" all: "Alle" +notResponding: "Svarer ikke" changePassword: "Endre passord" security: "Sikkerhet" +retypedNotMatch: "Inngangene stemmer ikke overens." +currentPassword: "Nåværende passord" newPassword: "Nytt passord" newPasswordRetype: "Nytt passord (gjenta)" +attachFile: "Legg ved filer" more: "Mer!" +noSuchUser: "Bruker ikke funnet" +announcements: "Kunngjøringer" remove: "Slett" -removed: "Slettet" +removed: "Vellykket slettet" +removeAreYouSure: "Er du sikker på at du vil fjerne \"{x}\"?" +deleteAreYouSure: "Er du sikker på at du vil slette \"{x}\"?" saved: "Lagret" upload: "Laste opp" +keepOriginalUploading: "Behold originalbildet" +fromUrl: "Fra URL" +uploadFromUrl: "Last opp fra en URL" +uploadFromUrlDescription: "URL til filen du vil laste opp" explore: "Utforsk" messageRead: "Lest" -agree: "Jeg godtar" +nUsersRead: "lest av {n}" +agreeTo: "Jeg godtar {0}" +agree: "Godta" +agreeBelow: "Jeg godtar følgende" +basicNotesBeforeCreateAccount: "Viktige merknader" +termsOfService: "Vilkår for bruk" home: "Hjem" +activity: "Aktivitet" images: "Bilder" -image: "Bilder" +image: "Bilde" birthday: "Bursdag" yearsOld: "{age} år gammel" +theme: "Temaer" light: "Lys" dark: "Mørk" +lightThemes: "Lyse temaer" +darkThemes: "Mørke temaer" +syncDeviceDarkMode: "Synkroniser mørkmodus med enhetens innstillinger" fileName: "Filnavn" -selectFile: "Velg fil" -selectFiles: "Velg fil" -selectFolder: "Velg mappe" -selectFolders: "Velg mappe" +selectFile: "Velg en fil" +selectFiles: "Velg filer" +selectFolder: "Velg en mappe" +selectFolders: "Velg mapper" renameFile: "Endre filnavn" folderName: "Mappenavn" -createFolder: "Opprett mappe" +createFolder: "Opprett en mappe" renameFolder: "Endre mappenavn" -deleteFolder: "Slett mappe" -addFile: "Legg til fil" +deleteFolder: "Slett denne mappen" +addFile: "Legg til en fil" emptyFolder: "Denne mappen er tom" +unableToDelete: "Kan ikke slette" +inputNewFileName: "Skriv inn et nytt filnavn" +inputNewDescription: "Skriv inn ny bildetekst" +inputNewFolderName: "Skriv inn et nytt mappenavn" +circularReferenceFolder: "Målmappen er en undermappe til mappen du ønsker å flytte." +hasChildFilesOrFolders: "Siden denne mappen ikke er tom, kan den ikke slettes." copyUrl: "Kopier URL" rename: "Endre navn" -doNothing: "Gjør ingenting" +avatar: "Avatar" +banner: "Banner" +doNothing: "Ignorer" accept: "Tillatt" reject: "Avslå" instanceName: "Servernavn" -thisYear: "I år" +instanceDescription: "Serverbeskrivelse" +thisYear: "År" +thisMonth: "Måned" today: "I dag" +dayX: "{day}" +monthX: "{month}" +yearX: "{year}" pages: "Sider" -pinnedUsers: "Festete brukrere" -pinnedPages: "Festete sider" +integration: "Integrasjon" +enableLocalTimeline: "Aktiver lokal tidslinje" +enableGlobalTimeline: "Aktiver global tidslinje" +disablingTimelinesInfo: "Administratorer og Moderatorer vil alltid ha tilgang til alle tidslinjer, selv om de ikke er aktivert." +registration: "Registrer" +enableRegistration: "Aktiver registrering av nye brukere" +invite: "Inviter" +basicInfo: "Grunnleggende informasjon" +pinnedUsers: "Festede brukrere" +pinnedUsersDescription: "Liste over brukernavn atskilt med linjeskift som skal festes i \"Utforsk\" fanen." +pinnedPages: "Festede sider" +pinnedNotes: "Festet Note" hcaptcha: "hCaptcha" +enableHcaptcha: "Aktiver hCaptcha" recaptcha: "reCAPTCHA" +enableRecaptcha: "Aktiver reCAPTCHA" +turnstile: "Turnstile" +enableTurnstile: "Aktiver Turnstile" +antennas: "Antenner" name: "Navn" +antennaSource: "Antennekilde" +notifyAntenna: "Varsle om nye Notes" +withFileAntenna: "Bare Notes med filer" +notesAndReplies: "Notes og svar" popularUsers: "Populære brukere" exploreUsersCount: "Det finnes {count} brukere" +exploreFediverse: "Utforsk Fediverse" userList: "Lister" -about: "Infomasjon" +about: "Informasjon" aboutMisskey: "Om Misskey" +newPasswordIs: "Det nye passordet er \"{password}\"." share: "Del" +notFound: "Ikke funnet" +markAsReadAllNotifications: "Merk alle varsler som lest" +markAsReadAllUnreadNotes: "Merk alle Notes som lest" help: "Hjelp" +inputMessageHere: "Skriv inn melding her" close: "Lukk" +invites: "Inviter" members: "Medlemmer" +title: "Tittel" text: "Tekst" next: "Neste" retype: "Gjenta" +quoteAttached: "Sitat" +noMessagesYet: "Ingen meldinger ennå" +newMessageExists: "Det er nye meldinger" +onlyOneFileCanBeAttached: "Du kan bare legge ved én fil i en melding" +invitations: "Inviter" available: "Tilgjengelig" unavailable: "Utilgjengelig" tooShort: "For kort" tooLong: "For langt" +weakPassword: "Svakt passord" +normalPassword: "Gjennomsnittlig passord" +strongPassword: "Sterkt passord" +signinWith: "Logg inn med {x}" +signinFailed: "Kunne ikke logge inn. Det oppgitte brukernavnet eller passordet er feil." or: "eller" language: "Språk" aboutX: "Om {x}" -category: "Kategorier" +category: "Kategori" createAccount: "Opprett konto" +openImageInNewTab: "Åpne bilder i ny fane" +clientSettings: "Klientinnstillinger" +accountSettings: "Kontoinnstillinger" objectStorageRegion: "Region" objectStorageUseSSL: "Bruk SSL" objectStorageUseProxy: "Bruk Proxy" deleteAll: "Slett alt" +newNoteRecived: "Det er nye Notes" listen: "Lytt" none: "Ingen" +volume: "Volum" chooseEmoji: "Velg emoji" recentUsed: "Sist brukte" install: "Installer" +uninstall: "Avinstaller" nothing: "Ingenting" deleteAllFiles: "Slett alle filer" -deleteAllFilesConfirm: "Vil du slette alle filer?" +deleteAllFilesConfirm: "Er du sikker på at du vil slette alle filer?" +userSuspended: "Denne brukeren har blitt suspendert." accountDeleted: "Kontoen blir slettet" -accountDeletedDescription: "Denne kontoen blir slettet" +accountDeletedDescription: "Denne kontoen har blitt slettet." menu: "Meny" poll: "Avstemning" description: "Beskrivelse" @@ -186,6 +350,7 @@ small: "Liten" notificationType: "Varseltype" edit: "Rediger" email: "E-post" +smtpHost: "Vert" smtpUser: "Brukernavn" smtpPass: "Passord" userSaysSomething: "{name} sa noe" @@ -201,16 +366,25 @@ reportAbuse: "Rappoter" send: "Send" openInNewTab: "Åpne i ny fane" waitingFor: "Venter på {x}" +random: "Tilfeldig" system: "System" +desktop: "Skrivebord" +i18nInfo: "Misskey oversettes til flere språk av frivillige. Du kan hjelpe til på {link}." followingCount: "Følger" followersCount: "Følgere" yes: "Ja" no: "Nei" +contact: "Kontakt" +developer: "Utvikler" +makeExplorable: "Gjør konto synlig i \"Utforsk\"" +makeExplorableDescription: "Hvis du slår av dette, vises ikke kontoen din i \"Utforsk\" delen." left: "Venstre" +nNotes: "{n} Notes" saveAs: "Lagre som" value: "Verdi" deleteConfirm: "Vil du slette?" invalidValue: "Verdien er ugyldig." +closeAccount: "Avslutt konto" emailNotification: "E-postvarsler" inChannelSearch: "Søk i kanal" clear: "Tøm" @@ -224,16 +398,23 @@ accounts: "Kontoer" switch: "Bytt" gallery: "Galleri" ads: "Annonser" +memo: "Notat" high: "Høy" low: "Lav" -sent: "Send" +sent: "Sendt" +received: "Mottatt" learnMore: "Les mer" +misskeyUpdated: "Misskey har blitt oppdatert!" translate: "Oversett" +translatedFrom: "Oversatt fra {x}" unread: "Ulest" manageAccounts: "Administrer konto" classic: "Klassisk" muteThread: "Skjul denne tråden" unmuteThread: "Vis denne tråden" +continueThread: "Vis fortsettelse av tråden" +hide: "Skjul" +smartphone: "Smarttelefon" tablet: "Nettbrett" auto: "Automatisk" size: "Størrelse" @@ -249,10 +430,10 @@ check: "Sjekk" deleteAccount: "Slett konto" document: "Dokumenter" logoutConfirm: "Vil du logge ut?" -pleaseSelect: "Vennligst velg" +pleaseSelect: "Velg et alternativ" type: "Type" beta: "Beta" -account: "Kontoer" +account: "Konto" move: "Flytt" pushNotification: "Push-varsler" tools: "Verktøy" @@ -268,6 +449,7 @@ role: "Rolle" color: "Farge" youCannotCreateAnymore: "Du kan ikke opprette flere." cannotPerformTemporary: "Midlertidig utilgjengelig" +achievements: "Prestasjoner" thisPostMayBeAnnoyingCancel: "Avbryt" exploreOtherServers: "Utforsk andre severe" letsLookAtTimeline: "La oss se på tidslinje" @@ -283,6 +465,26 @@ _initialAccountSetting: theseSettingsCanEditLater: "Du kan endre disse innstillingene senere." _achievements: _types: + _notes10: + title: "Noen Notes" + _notes100: + title: "Mange Notes" + _notes500: + title: "Dekket i Notes" + _notes1000: + title: "Et fjell av Notes" + _notes5000: + title: "Overfylte Notes" + _notes10000: + title: "Super Notes" + _notes20000: + title: "Trenger... mer... Notes..." + _notes30000: + title: "Notes Notes Notes!" + _notes40000: + title: "Note fabrikk" + _notes50000: + title: "Planet av Notes" _notes100000: flavor: "Du har jammen mye å si." _noteFavorited1: @@ -311,11 +513,25 @@ _achievements: _justPlainLucky: title: "Rett og slett heldig" _setNameToSyuilo: - description: "Du har satt navnet ditt til \"syuilo\"" + description: "Du satte navnet ditt til \"syuilo\"" + _passedSinceAccountCreated1: + title: "Ett års jubileum" + description: "Det har gått ett år siden kontoen din ble opprettet" + _passedSinceAccountCreated2: + title: "To års jubileum" + description: "Det har gått to år siden kontoen din ble opprettet" + _passedSinceAccountCreated3: + title: "Tre års jubileum" + description: "Det har gått tre år siden kontoen din ble opprettet" _loggedInOnBirthday: title: "Gratulerer med dagen" + description: "Du logget inn på bursdagen din" _loggedInOnNewYearsDay: title: "Godt nytt år" + description: "Du logget inn på årets første dag" + _cookieClicked: + description: "Du klikket på kjeksen" + flavor: "Er du på riktig nettsted?" _brainDiver: title: "Brain Diver" flavor: "Misskey-Misskey La-Tu-Ma" @@ -333,6 +549,9 @@ _ad: _gallery: like: "Liker!" unlike: "Liker ikke" +_email: + _follow: + title: "fulgte deg" _preferencesBackups: saveNew: "Lagre som ny" cannotSave: "Kunne ikke lagre" @@ -351,6 +570,8 @@ _channel: featured: "Populært" following: "Følger" nameAndDescription: "Navn og beskrivelse" +_menuDisplay: + hide: "Skjul" _wordMute: soft: "Myk" hard: "Hard" @@ -360,15 +581,17 @@ _theme: key: "Nøkkel" keys: link: "Lenke" + renote: "Renote" _sfx: + note: "Notes" notification: "Varsler" _ago: future: "Fremitid" justNow: "Akkurat nå" - secondsAgo: "{n} sekunder siden" - minutesAgo: "{n} minutter siden" - hoursAgo: "{n} timer siden" - daysAgo: "{n} dager siden" + secondsAgo: "{n}s siden" + minutesAgo: "{n}m siden" + hoursAgo: "{n}t siden" + daysAgo: "{n}d siden" weeksAgo: "{n} uker siden" monthsAgo: "{n} måneder siden" yearsAgo: "{n} år siden" @@ -380,6 +603,7 @@ _time: day: "Dager" _timelineTutorial: title: "Hvordan bruke Misskey" + step2_2: "Hva med å skrive en selvpresentasjon, eller bare \"Hei {name}!\" hvis du ikke har lyst?" _2fa: renewTOTPCancel: "Avbryt" _weekday: @@ -392,18 +616,22 @@ _weekday: saturday: "Lørdag" _widgets: profile: "Profil" + instanceInfo: "Serverinformasjon" notifications: "Varsler" timeline: "Tidslinje" calendar: "Kalender" trends: "Populært" clock: "Klokke" + activity: "Aktivitet" photos: "Bilder" + federation: "Føderasjon" button: "Knapp" aiscriptApp: "AiScript App" userList: "Brukerliste" _userList: chooseList: "Velg liste" _cw: + hide: "Skjul" show: "Vis mer" _poll: noOnlyOneChoice: "Trenger minst to valger." @@ -424,6 +652,7 @@ _postForm: _profile: name: "Navn" username: "Brukernavn" + description: "Biografi" metadataContent: "Innhold" _exportOrImport: followingList: "Følg" @@ -431,6 +660,7 @@ _exportOrImport: blockingList: "Blokker" userLists: "Lister" _charts: + federation: "Føderasjon" filesIncDec: "Forskjell på antall filer" _instanceCharts: users: "Forskjell på antall brukere" @@ -442,14 +672,18 @@ _play: new: "Opprett Play" edit: "Rediger Play" featured: "Populært" + title: "Tittel" summary: "Beskrivelse" _pages: + invalidNameText: "Pass på at sidetittelen ikke er tom" like: "Liker" unlike: "Liker ikke" my: "Mine sider" featured: "Populært" contents: "Innhold" + title: "Tittel" url: "Side URL" + hideTitleWhenPinned: "Skjul sidetittel når festet til profil" fontSerif: "Serif" fontSansSerif: "Sans Serif" selectType: "Velg type" @@ -459,13 +693,18 @@ _pages: image: "Bilde" button: "Knapp" _notification: + youWereFollowed: "fulgte deg" + unreadAntennaNote: "Antenne {name}" + achievementEarned: "Prestasjon låst opp" _types: - follow: "Følg" + follow: "Nye følgere" reply: "Svar" - quote: "Sitat" - reaction: "Reaksjon" + renote: "Renotes" + quote: "Sitater" + reaction: "Reaksjoner" _actions: reply: "Svar" + renote: "Renote" _deck: swapLeft: "Flytt til venstre" swapRight: "Flytt til høyre" @@ -477,6 +716,7 @@ _deck: _columns: notifications: "Varsler" tl: "Tidslinje" + antenna: "Antenner" list: "Lister" channel: "Kanaler" direct: "Direkte" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index a123ad726..e92449fdb 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -2,7 +2,7 @@ _lang_: "Русский" headlineMisskey: "Сеть, сплетённая из заметок" introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀" -poweredByMisskeyDescription: "{name} – один из инстансов (также называемый экземпляром Misskey), использующий платформу с открытым исходным кодом <b>Misskey</b>." +poweredByMisskeyDescription: "{name} – сервис на платформе с открытым исходным кодом <b>Misskey</b>, называемый инстансом Misskey." monthAndDay: "{day}.{month}" search: "Поиск" notifications: "Уведомления" @@ -560,6 +560,7 @@ accountDeletedDescription: "Эта учетная запись удалена" menu: "Меню" divider: "Линия-разделитель" addItem: "Добавить элемент" +rearrange: "Сортировать по" relays: "Ретрансляторы" addRelay: "Добавить ретранслятор" inboxUrl: "URL ящика входящих сообщений" @@ -648,8 +649,8 @@ abuseReported: "Жалоба отправлена. Большое спасибо reporter: "Сообщивший" reporteeOrigin: "О ком сообщено" reporterOrigin: "Кто сообщил" -forwardReport: "Перенаправление отчета на инстант." -forwardReportIsAnonymous: "Удаленный инстант не сможет увидеть вашу информацию и будет отображаться как анонимная системная учетная запись." +forwardReport: "Отправить жалобу на инстанс автора." +forwardReportIsAnonymous: "Жалоба на удалённый инстанс будет отправлена анонимно. Вместо ваших данных у получателя будет отображена системная учётная запись." send: "Отправить" abuseMarkAsResolved: "Отметить жалобу как решённую" openInNewTab: "Открыть в новой вкладке" @@ -822,6 +823,7 @@ translatedFrom: "Перевод. Язык оригинала — {x}" accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи" usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже." aiChanMode: "Режим Ай" +devMode: "Режим разработчика" keepCw: "Сохраняйте Предупреждения о содержимом" pubSub: "Учётные записи Pub/Sub" lastCommunication: "Последнее сообщение" @@ -913,8 +915,8 @@ cannotUploadBecauseInappropriate: "Файл не может быть загру cannotUploadBecauseNoFreeSpace: "Файл не может быть загружен, так как не осталось места на диске" cannotUploadBecauseExceedsFileSizeLimit: "Файл не может быть загружен, так как он превышает лимит размера файла." beta: "Бета" -enableAutoSensitive: "Автоматическое определение NSFW" -enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена автоматически в зависимости от инстанта." +enableAutoSensitive: "Автоматическое определение содержимого не для всех" +enableAutoSensitiveDescription: "Позволяет определять наличие содержимого не для всех при помощи искусственного интеллекта там, где это возможно. Даже если эту опцию отключить, она всё равно может быть включена на весь инстанс." activeEmailValidationDescription: "Если включено, будет проводиться более строгая проверка адреса электронной почты, в том числе на то, что он действительный и не временный. Если же отключено, то проверяется только корректность написания адреса." navbar: "Панель навигации" shuffle: "Перемешать" @@ -989,6 +991,7 @@ rolesAssignedToMe: "Мои роли" resetPasswordConfirm: "Сбросить пароль?" sensitiveWords: "Чувствительные слова" sensitiveWordsDescription: "Установите общедоступный диапазон заметки, содержащей заданное слово, на домашний. Можно сделать несколько настроек, разделив их переносами строк." +sensitiveWordsDescription2: "Разделение пробелом создаёт спецификацию AND, а разделение косой чертой создаёт регулярное выражение." notesSearchNotAvailable: "Поиск заметок недоступен" license: "Лицензия" unfavoriteConfirm: "Удалить избранное?" @@ -1004,6 +1007,7 @@ noteIdOrUrl: "ID или ссылка на заметку" video: "Видео" videos: "Видео" dataSaver: "Экономия трафика" +renotesList: "Репосты" horizontal: "Сбоку" youFollowing: "Подписки" options: "Настройки ролей" @@ -1178,6 +1182,9 @@ _achievements: _client30min: title: "Перерыв на обед" description: "Прошло 30 минут с момента запуска клиента" + _client60min: + title: "Не наглядеться на Misskey" + description: "Misskey был открыт 60 минут подряд" _noteDeletedWithin1min: title: "Ой, нет!" description: "Заметка удалена через минуту после публикации" @@ -1280,6 +1287,7 @@ _role: canInvite: "Может создавать пригласительные коды" canManageCustomEmojis: "Управлять пользовательскими эмодзи" driveCapacity: "Доступное пространство на «диске»" + alwaysMarkNsfw: "Всегда отмечать файлы как «не для всех»" pinMax: "Доступное количество закреплённых заметок" antennaMax: "Доступное количество антенн" wordMuteMax: "Доступное количество знаков в списке скрытия слов" @@ -1307,7 +1315,7 @@ _sensitiveMediaDetection: description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно." sensitivity: "Чувствительность обнаружения" sensitivityDescription: "Более низкая чувствительность уменьшает количество ложных срабатываний (false positives). Повышение чувствительности уменьшает утечку при обнаружении (ложноотрицательные результаты)." - setSensitiveFlagAutomatically: "Установить флаг NSFW" + setSensitiveFlagAutomatically: "Обозначить как не для всех" setSensitiveFlagAutomaticallyDescription: "Даже если этот параметр отключен, результат оценки сохраняется внутри системы." analyzeVideos: "Анализировать видео?" analyzeVideosDescription: "Анализируйте видео в дополнение к неподвижным изображениям. Нагрузка на сервер немного увеличивается." @@ -1526,6 +1534,16 @@ _time: minute: "мин" hour: "ч" day: "сут" +_timelineTutorial: + title: "Как пользоваться Misskey" + step1_1: "Это лицо Misskey, так называемая лента. Ваш инстанс, {name}, покажет тут все опубликованные на нём заметки в хронологическом порядке." + step1_2: "Здесь есть несколько лент. К примеру «персональная» лента отображает заметки тех, на кого вы подписаны. А «местная» — заметки тех, кого приютил {name}." + step2_1: "Что ж, теперь самое время опубликовать заметку. Если нажать вверху страницы на изображение карандаша, появится форма для текста." + step2_2: "Почему бы не написать немного о себе? Ну, или хотя бы «Привет, {name}»?" + step3_1: "Справились с первой заметкой?" + step3_2: "Отлично, теперь она должна появиться в вашей ленте." + step4_1: "А ещё здесь можно делиться своими реакциями на заметки." + step4_2: "Отмечайте реакции, нажимая на символ «+» под заметкой и выбирая значок по душе." _2fa: alreadyRegistered: "Двухфакторная аутентификация уже настроена." registerTOTP: "Начните настраивать приложение-аутентификатор" @@ -1866,6 +1884,9 @@ _deck: _dialog: charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}" charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}" +_disabledTimeline: + title: "Лента отключена" + description: "Ваша текущая роль не позволяет пользоваться этой лентой." _webhookSettings: name: "Название" active: "Вкл." diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 22f110eba..d8e68202d 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -560,6 +560,7 @@ accountDeletedDescription: "บัญชีนี้ถูกลบไปแล menu: "เมนู" divider: "ตัวแบ่ง" addItem: "เพิ่มรายการ" +rearrange: "จัดใหม่" relays: "รีเลย์" addRelay: "เพิ่มรีเลย์" inboxUrl: "อินบ็อกซ์ URL" @@ -1030,6 +1031,7 @@ continue: "ดำเนินการต่อ" preservedUsernames: "ชื่อผู้ใช้ที่สงวนไว้" preservedUsernamesDescription: "ลิสต์ชื่อผู้ใช้ที่จะสำรองโดยคั่นด้วยการแบ่งบรรทัดนั้น เพราะสิ่งเหล่านี้จะไม่สามารถทำได้ในระหว่างการสร้างบัญชีตามปกติ บัญชีที่มีอยู่แล้วนั้นโดยใช้ชื่อผู้ใช้เหล่านี้จะไม่ได้รับผลกระทบอะไร" createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้" +archive: "เก็บถาวร" youFollowing: "ติดตามแล้ว" options: "ตัวเลือกบทบาท" _serverRules: @@ -1329,6 +1331,7 @@ _role: canInvite: "สร้างรหัสเชิญอินสแตนซ์" canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" driveCapacity: "ความจุของไดรฟ์" + alwaysMarkNsfw: "ทำเครื่องหมายไฟล์ว่าเป็น NSFW เสมอ" pinMax: "จํานวนสูงสุดของโน้ตที่ปักหมุดไว้" antennaMax: "จำนวนสูงสุดของเสาอากาศ" wordMuteMax: "จำนวนอักขระสูงสุดที่อนุญาตในการปิดเสียงคำ" @@ -1580,6 +1583,12 @@ _time: minute: "นาที" hour: "ชั่วโมง" day: "วัน" +_timelineTutorial: + title: "วิธีใช้งาน Misskey" + step3_1: "เสร็จสิ้นการโพสต์โน้ตย่อแรกของคุณแล้วอย่างงั้นหรอ?" + step3_2: "ไชโย! ตอนนี้โน้ตย่อแรกของคุณได้ปรากฏบนไทม์ไลน์ของคุณแล้วนะ" + step4_1: "คุณยังสามารถแนบ \"ปฏิกิริยา\" ไปกับโน้ตได้อีกด้วยนะค่ะ" + step4_2: "หากต้องการแนบการแสดงความรู้สึก ให้กดเครื่องหมาย \"+\" บนโน้ตแล้วเลือกอิโมจิที่คุณต้องการแสดงความรู้สึกที่ตนเองชอบได้เลย" _2fa: alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว" registerTOTP: "ลงทะเบียนแอพตัวตรวจสอบสิทธิ์" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 638cc1cf8..9c278ea75 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -52,6 +52,8 @@ addToList: "添加至列表" sendMessage: "发送" copyRSS: "复制RSS" copyUsername: "复制用户名" +copyUserId: "复制用户ID" +copyNoteId: "复制帖子ID" searchUser: "搜索用户" reply: "回复" loadMore: "查看更多" @@ -560,6 +562,7 @@ accountDeletedDescription: "此帐户已经被删除。" menu: "菜单" divider: "分割线" addItem: "添加项目" +rearrange: "排序方式" relays: "中继" addRelay: "添加中继" inboxUrl: "Inbox URL" @@ -789,6 +792,7 @@ noMaintainerInformationWarning: "管理人员信息未设置。" noBotProtectionWarning: "Bot保护未设置。" configure: "设置" postToGallery: "发送到图库" +postToHashtag: "投稿到这个标签" gallery: "图库" recentPosts: "最新发布" popularPosts: "热门投稿" @@ -822,6 +826,7 @@ translatedFrom: "从 {x} 翻译" accountDeletionInProgress: "正在删除账户" usernameInfo: "在服务器上唯一标识您的帐户的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。" aiChanMode: "小蓝模式" +devMode: "开发者模式" keepCw: "回复时维持隐藏内容" pubSub: "Pub/Sub账户" lastCommunication: "最近通信" @@ -831,6 +836,8 @@ breakFollow: "移除关注者" breakFollowConfirm: "你想取消关注吗?" itsOn: "已开启" itsOff: "已关闭" +on: "开启" +off: "关闭" emailRequiredForSignup: "注册账户需要电子邮件地址" unread: "未读" filter: "筛选" @@ -985,10 +992,13 @@ cannotBeChangedLater: "之后不能再更改。" reactionAcceptance: "接受表情回应" likeOnly: "仅点赞" likeOnlyForRemote: "远程仅点赞" +nonSensitiveOnly: "仅限非敏感内容" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "仅限非敏感内容(远程仅点赞)" rolesAssignedToMe: "指派给自己的角色" resetPasswordConfirm: "确定重置密码?" sensitiveWords: "敏感词" sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。" +sensitiveWordsDescription2: "用空格分割关键词作为AND格式,用斜线包裹关键字来构成正则表达式。" notesSearchNotAvailable: "帖子检索不可用" license: "许可信息" unfavoriteConfirm: "确定要取消收藏吗?" @@ -1028,28 +1038,60 @@ pleaseConfirmBelowBeforeSignup: "在这个服务器上注册账号前,请确 pleaseAgreeAllToContinue: "必须全部勾选「同意」才能够继续。" continue: "继续" preservedUsernames: "保留的用户名" +preservedUsernamesDescription: "列出需要保留的用户名,使用换行来作为分割。被指定的用户名在建立账户时无法使用,但由管理员所创建的账户不受该限制。此外,现有的账户也不会受到影响。" createNoteFromTheFile: "从文件创建帖子" +archive: "归档" +channelArchiveConfirmTitle: "要将{name}归档吗?" +channelArchiveConfirmDescription: "归档后,在频道列表与搜索结果中不会显示,也无法发布新的贴文。" +thisChannelArchived: "该频道已被归档。" +displayOfNote: "显示帖子" +initialAccountSetting: "初始设置" youFollowing: "正在关注" +preventAiLearning: "拒绝接受生成式AI的学习" +preventAiLearningDescription: "要求文章生成AI或图像生成AI不能够以发布的帖子和图像等内容作为学习对象。这是通过在HTML响应中包含noai标志来实现的,这不能完全阻止AI学习你的发布内容,并不是所有AI都会遵守这类请求。" options: "选项" +specifyUser: "用户指定" +failedToPreviewUrl: "无法预览" +update: "更新" +rolesThatCanBeUsedThisEmojiAsReaction: "可以使用表情作为回应的角色" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "在没有指定角色的情况下,任何人都可以使用表情作为回应。" +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "角色必须是公开的。" +cancelReactionConfirm: "要取消回应吗?" +changeReactionConfirm: "要更改回应吗?" +later: "一会再说" +goToMisskey: "去往Misskey" +installed: "已安装" _initialAccountSetting: accountCreated: "账户创建完成了!" letsStartAccountSetup: "来进行帐户的初始设置吧。" letsFillYourProfile: "首先,来设定你的个人档案吧!" profileSetting: "个人资料设置" + privacySetting: "隐私设置" theseSettingsCanEditLater: "也可以在稍后修改这里的设置。" + youCanEditMoreSettingsInSettingsPageLater: "还可以在「设置」页面进行其它各种设置,稍后就来确认一下看看吧。" + followUsers: "为了建立属于你自己的时间线,试着去关注你感兴趣的用户吧。" + pushNotificationDescription: "启用推送通知的话,就可以在设备上受到来自{name}的通知了。" + initialAccountSettingCompleted: "初始设定已经完成了!" + haveFun: "希望{name}在这里玩得开心!" + ifYouNeedLearnMore: "关于{name}(Misskey)的使用方法,详见{link}。" + skipAreYouSure: "要跳过初始设置吗?" + laterAreYouSure: "要稍后再进行初始设定吗?" _serverRules: description: "在新用户注册前显示服务器的简单规则。推荐显示服务条款的主要内容。" _accountMigration: moveFrom: "从别的账号迁移到此账户" + moveFromSub: "为另一个账户建立别名" moveFromLabel: "迁移前的账户" moveFromDescription: "如果迁移时需要继承其他账户的关注者,请在此创造别名。此操作需要在实行迁移之前完成!请如已下输入需要迁移的账户:@person@instance.com" moveTo: "把这个账户迁移到新的账户" moveToLabel: "迁移后的账户" moveCannotBeUndone: "一旦迁移账户,就无法撤销。" moveAccountDescription: "此操作无法取消。请先确认您已在迁移后的账户上,为此账户创造了别名。创造别名后,请如以下输入您的迁移后的账户:@person@instance.com" + moveAccountHowTo: "要进行账户迁移,请现在目标账户中为此账户建立一个别名。\n建立别名后,请像这样输入目标账户:@username@server.example.com" startMigration: "迁移" migrationConfirm: "确定要把此账户迁移到{account}吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时,请确认迁移后的账户,已创造别名。" movedAndCannotBeUndone: "该账户已被迁移。\n迁移操作无法撤销。" + postMigrationNote: "这个账户的关注会在迁移操作后的24小时后解除。该账户的「关注中」和「关注者」皆会变为0。由于不会解除关注关系,你的关注者仍然可以继续查看该账户发补给关注者的帖子。" movedTo: "迁移后的账户" _achievements: earnedAt: "达成时间" @@ -1331,6 +1373,7 @@ _role: canInvite: "发放服务器邀请码" canManageCustomEmojis: "管理自定义表情符号" driveCapacity: "网盘容量" + alwaysMarkNsfw: "总是将文件标记为NSFW" pinMax: "帖子置顶数量限制" antennaMax: "可创建的最大天线数量" wordMuteMax: "屏蔽词的字数限制" @@ -1583,8 +1626,15 @@ _time: hour: "小时" day: "日" _timelineTutorial: + title: "Misskey的使用方法" + step1_1: "这个画面是「时间线」。{name}的投稿会按照帖子的发布时间顺序来显示。" + step1_2: "时间线有许多种类,比如在「首页时间线」中展现的是你关注的人的贴文;而在「本地时间线」中展现的是{name}里全部用户的贴文。" + step2_1: "那么接下来,试着写一些什么东西来发布吧!你可以通过点击屏幕上的铅笔图标来打开投稿页面。" + step2_2: "第一次发布的帖子内容,建议包含自我介绍,以及「开始使用{name}了」。" step3_1: "将想说的话发出去了吗?" step3_2: "太棒了!现在你可以在你的时间线中看到刚刚发布的帖子了。" + step4_1: "试着对帖子使用「回应」吧!" + step4_2: "在他人的帖子上按下「+」图标,即可选择想要的表情来进行「回应」。" _2fa: alreadyRegistered: "此设备已被注册" registerTOTP: "开始设置认证应用" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index d2b42313a..ef0baeef5 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -52,6 +52,8 @@ addToList: "加入至清單" sendMessage: "發送訊息" copyRSS: "複製RSS" copyUsername: "複製使用者名稱" +copyUserId: "複製使用者ID" +copyNoteId: "複製貼文ID" searchUser: "搜尋使用者" reply: "回覆" loadMore: "載入更多" @@ -790,6 +792,7 @@ noMaintainerInformationWarning: "尚未設定管理員信息。" noBotProtectionWarning: "尚未設定Bot防護。" configure: "設定" postToGallery: "發佈到相簿" +postToHashtag: "以此主題標籤發布" gallery: "相簿" recentPosts: "最新貼文" popularPosts: "熱門的貼文" @@ -823,6 +826,7 @@ translatedFrom: "從 {x} 翻譯" accountDeletionInProgress: "正在刪除帳戶" usernameInfo: "在伺服器上您的帳戶是唯一的識別名稱。您可以使用字母 (a ~ z, A ~ Z)、數字 (0 ~ 9) 和下底線 (_)。之後帳戶名是不能更改的。" aiChanMode: "小藍模式" +devMode: "開發者模式" keepCw: "保持CW" pubSub: "Pub/Sub 帳戶" lastCommunication: "最近的通信" @@ -832,6 +836,8 @@ breakFollow: "解除追隨者" breakFollowConfirm: "確定要取消被追隨嗎?" itsOn: "已開啟" itsOff: "已關閉" +on: "開啟" +off: "關閉" emailRequiredForSignup: "註冊帳戶需要電子郵件地址" unread: "未讀" filter: "篩選" @@ -986,6 +992,8 @@ cannotBeChangedLater: "之後不能變更。" reactionAcceptance: "接受表情反應" likeOnly: "僅限讚" likeOnlyForRemote: "遠端僅限讚" +nonSensitiveOnly: "僅限非敏感" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "僅限非敏感(遠端僅限按讚)" rolesAssignedToMe: "指派給自己的角色" resetPasswordConfirm: "重設密碼?" sensitiveWords: "敏感詞" @@ -1039,14 +1047,27 @@ thisChannelArchived: "這個頻道已被封存。" displayOfNote: "顯示貼文" initialAccountSetting: "初始設定" youFollowing: "追隨中" -preventAiLearning: "拒絕接受產生式AI的學習" -preventAiLearningDescription: "要求外部的文章產生AI或圖像產生AI不以發布的貼文和圖像等內容為學習對象。這是透過在HTML響應中包含noai旗標來實現的,但不能完全防止AI的學習,因為這要看該AI是否遵守這個要求。" +preventAiLearning: "拒絕接受生成式AI的訓練" +preventAiLearningDescription: "要求外部的文章生成式AI或圖像生成式AI不以發布的貼文和圖像等內容為學習對象。這是透過在HTML響應中包含noai旗標來實現的,但不能完全防止AI的學習,因為這要看該AI是否遵守這個要求。" options: "選項" +specifyUser: "指定使用者" +failedToPreviewUrl: "無法預覽" +update: "更新" +rolesThatCanBeUsedThisEmojiAsReaction: "可以當成反應使用的角色" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "如果是未指定角色的情況,則任何人都可以被當成反應來使用。" +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "角色必須是公開的角色。" +cancelReactionConfirm: "要取消做出的反應嗎?" +changeReactionConfirm: "要變更做出的反應嗎?" +later: "稍後再說" +goToMisskey: "往Misskey" +additionalEmojiDictionary: "表情符號的附加辭典" +installed: "已安裝" _initialAccountSetting: accountCreated: "帳戶已建立完成!" letsStartAccountSetup: "來進行帳戶的初始設定吧。" letsFillYourProfile: "首先,來設定您的個人檔案吧。" profileSetting: "個人檔案設定" + privacySetting: "隱私設定" theseSettingsCanEditLater: "這裡的設定可以在之後變更。" youCanEditMoreSettingsInSettingsPageLater: "除此之外,還可以在「設定」頁面進行各種設定。之後請確認看看。" followUsers: "為了構築時間軸,試著追蹤您感興趣的使用者吧。" @@ -1055,6 +1076,7 @@ _initialAccountSetting: haveFun: "盡情享受{name}吧!" ifYouNeedLearnMore: "關於如何使用{name}(Misskey)的詳細資訊,請見{link}。" skipAreYouSure: "要略過初始設定嗎?" + laterAreYouSure: "稍後再重新進行初始設定嗎?" _serverRules: description: "設定伺服器的簡要規則,在新的註冊之前顯示。建議的內容是使用條款的摘要。" _accountMigration: diff --git a/package.json b/package.json index a21d7ab9e..8cf7d37f6 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "misskey", - "version": "13.12.2", + "version": "13.13.0", "codename": "nasubi", "repository": { "type": "git", "url": "https://github.com/misskey-dev/misskey.git" }, - "packageManager": "pnpm@8.3.1", + "packageManager": "pnpm@8.6.0", "workspaces": [ "packages/frontend", "packages/backend", @@ -51,16 +51,16 @@ "gulp-replace": "1.1.4", "gulp-terser": "2.1.0", "js-yaml": "4.1.0", - "typescript": "5.0.4" + "typescript": "5.1.3" }, "devDependencies": { "@types/gulp": "4.0.10", "@types/gulp-rename": "2.0.1", - "@typescript-eslint/eslint-plugin": "5.59.5", - "@typescript-eslint/parser": "5.59.5", + "@typescript-eslint/eslint-plugin": "5.59.8", + "@typescript-eslint/parser": "5.59.8", "cross-env": "7.0.3", - "cypress": "12.12.0", - "eslint": "8.40.0", + "cypress": "12.13.0", + "eslint": "8.41.0", "start-server-and-test": "2.0.0" }, "optionalDependencies": { diff --git a/packages/backend/migration/1683847157541-UserList.js b/packages/backend/migration/1683847157541-UserList.js new file mode 100644 index 000000000..b50a50eed --- /dev/null +++ b/packages/backend/migration/1683847157541-UserList.js @@ -0,0 +1,13 @@ +export class UserList1683847157541 { + name = 'UserList1683847157541' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_list" ADD "isPublic" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`CREATE INDEX "IDX_48a00f08598662b9ca540521eb" ON "user_list" ("isPublic") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_48a00f08598662b9ca540521eb"`); + await queryRunner.query(`ALTER TABLE "user_list" DROP COLUMN "isPublic"`); + } +} diff --git a/packages/backend/migration/1683869758873-UserListFavorites.js b/packages/backend/migration/1683869758873-UserListFavorites.js new file mode 100644 index 000000000..ac9c4c42b --- /dev/null +++ b/packages/backend/migration/1683869758873-UserListFavorites.js @@ -0,0 +1,19 @@ +export class UserListFavorites1683869758873 { + name = 'UserListFavorites1683869758873' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "user_list_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userListId" character varying(32) NOT NULL, CONSTRAINT "PK_c0974b21e18502a4c8178e09fe6" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_016f613dc4feb807e03e3e7da9" ON "user_list_favorite" ("userId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d6765a8c2a4c17c33f9d7f948b" ON "user_list_favorite" ("userId", "userListId") `); + await queryRunner.query(`ALTER TABLE "user_list_favorite" ADD CONSTRAINT "FK_016f613dc4feb807e03e3e7da92" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_list_favorite" ADD CONSTRAINT "FK_4d52b20bfe32c8552e7a61e80d2" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_list_favorite" DROP CONSTRAINT "FK_4d52b20bfe32c8552e7a61e80d2"`); + await queryRunner.query(`ALTER TABLE "user_list_favorite" DROP CONSTRAINT "FK_016f613dc4feb807e03e3e7da92"`); + await queryRunner.query(`DROP INDEX "public"."IDX_d6765a8c2a4c17c33f9d7f948b"`); + await queryRunner.query(`DROP INDEX "public"."IDX_016f613dc4feb807e03e3e7da9"`); + await queryRunner.query(`DROP TABLE "user_list_favorite"`); + } +} diff --git a/packages/backend/migration/1684206886988-remove-showTimelineReplies.js b/packages/backend/migration/1684206886988-remove-showTimelineReplies.js new file mode 100644 index 000000000..690653bd7 --- /dev/null +++ b/packages/backend/migration/1684206886988-remove-showTimelineReplies.js @@ -0,0 +1,11 @@ +export class RemoveShowTimelineReplies1684206886988 { + name = 'RemoveShowTimelineReplies1684206886988' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "showTimelineReplies"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" ADD "showTimelineReplies" boolean NOT NULL DEFAULT false`); + } +} diff --git a/packages/backend/migration/1684386446061-emoji-improve.js b/packages/backend/migration/1684386446061-emoji-improve.js new file mode 100644 index 000000000..40b0a2bc5 --- /dev/null +++ b/packages/backend/migration/1684386446061-emoji-improve.js @@ -0,0 +1,15 @@ +export class EmojiImprove1684386446061 { + name = 'EmojiImprove1684386446061' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" ADD "localOnly" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "emoji" ADD "isSensitive" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "emoji" ADD "roleIdsThatCanBeUsedThisEmojiAsReaction" character varying(128) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "roleIdsThatCanBeUsedThisEmojiAsReaction"`); + await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "isSensitive"`); + await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "localOnly"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 4bab4a734..56ecbc2ea 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -35,49 +35,52 @@ "@swc/core-win32-x64-msvc": "1.3.56", "@tensorflow/tfjs": "4.4.0", "@tensorflow/tfjs-node": "4.4.0", - "slacc-android-arm-eabi": "0.0.7", - "slacc-android-arm64": "0.0.7", - "slacc-darwin-arm64": "0.0.7", - "slacc-darwin-universal": "0.0.7", - "slacc-darwin-x64": "0.0.7", - "slacc-linux-arm-gnueabihf": "0.0.7", - "slacc-linux-arm64-gnu": "0.0.7", - "slacc-linux-arm64-musl": "0.0.7", - "slacc-linux-x64-gnu": "0.0.7", - "slacc-win32-arm64-msvc": "0.0.7", - "slacc-win32-x64-msvc": "0.0.7" + "bufferutil": "^4.0.7", + "slacc-android-arm-eabi": "0.0.9", + "slacc-android-arm64": "0.0.9", + "slacc-darwin-arm64": "0.0.9", + "slacc-darwin-universal": "0.0.9", + "slacc-darwin-x64": "0.0.9", + "slacc-freebsd-x64": "0.0.9", + "slacc-linux-arm-gnueabihf": "0.0.9", + "slacc-linux-arm64-gnu": "0.0.9", + "slacc-linux-arm64-musl": "0.0.9", + "slacc-linux-x64-gnu": "0.0.9", + "slacc-win32-arm64-msvc": "0.0.9", + "slacc-win32-x64-msvc": "0.0.9", + "utf-8-validate": "^6.0.3" }, "dependencies": { "@aws-sdk/client-s3": "3.321.1", "@aws-sdk/lib-storage": "3.321.1", "@aws-sdk/node-http-handler": "3.321.1", - "@bull-board/api": "5.1.2", - "@bull-board/fastify": "5.1.2", - "@bull-board/ui": "5.1.2", + "@bull-board/api": "5.2.0", + "@bull-board/fastify": "5.2.0", + "@bull-board/ui": "5.2.0", "@discordapp/twemoji": "14.1.2", "@fastify/accepts": "4.1.0", "@fastify/cookie": "8.3.0", - "@fastify/cors": "8.2.1", + "@fastify/cors": "8.3.0", "@fastify/http-proxy": "9.1.0", "@fastify/multipart": "7.6.0", - "@fastify/static": "6.10.1", + "@fastify/static": "6.10.2", "@fastify/view": "7.4.1", - "@nestjs/common": "9.4.0", - "@nestjs/core": "9.4.0", - "@nestjs/testing": "9.4.0", + "@nestjs/common": "9.4.2", + "@nestjs/core": "9.4.2", + "@nestjs/testing": "9.4.2", "@peertube/http-signature": "1.7.0", - "@sinonjs/fake-timers": "10.0.2", + "@sinonjs/fake-timers": "10.2.0", "@swc/cli": "0.1.62", - "@swc/core": "1.3.56", + "@swc/core": "1.3.61", "accepts": "1.3.8", "ajv": "8.12.0", "archiver": "5.3.1", "autwh": "0.1.0", "bcryptjs": "2.4.3", "blurhash": "2.0.5", - "bull": "4.10.4", + "bullmq": "3.15.0", "cacheable-lookup": "6.1.0", - "cbor": "8.1.0", + "cbor": "9.0.0", "chalk": "5.2.0", "chalk-template": "0.4.0", "chokidar": "3.5.3", @@ -93,30 +96,30 @@ "fluent-ffmpeg": "2.1.2", "form-data": "4.0.0", "got": "12.6.0", - "happy-dom": "9.16.0", + "happy-dom": "9.20.3", "hpagent": "1.2.0", "ioredis": "5.3.2", "ip-cidr": "3.1.0", "is-svg": "4.3.2", "js-yaml": "4.1.0", - "jsdom": "21.1.1", + "jsdom": "22.1.0", "json5": "2.2.3", - "jsonld": "8.1.1", - "meilisearch": "0.32.3", + "jsonld": "8.2.0", "jsrsasign": "10.8.6", + "meilisearch": "0.32.5", "mfm-js": "0.23.3", "mime-types": "2.1.35", "misskey-js": "workspace:*", "ms": "3.0.0-canary.1", "nested-property": "4.0.0", "node-fetch": "3.3.1", - "nodemailer": "6.9.2", + "nodemailer": "6.9.3", "nsfwjs": "2.4.2", "oauth": "0.10.0", "os-utils": "0.0.14", "otpauth": "9.1.2", "parse5": "7.1.2", - "pg": "8.10.0", + "pg": "8.11.0", "private-ip": "3.0.0", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", @@ -126,7 +129,7 @@ "qrcode": "1.5.3", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.18.0", + "re2": "1.19.0", "redis-lock": "0.1.4", "reflect-metadata": "0.1.13", "rename": "1.0.4", @@ -136,27 +139,26 @@ "s-age": "1.1.2", "sanitize-html": "2.10.0", "seedrandom": "3.0.5", - "semver": "7.5.0", + "semver": "7.5.1", "sharp": "0.32.1", "sharp-read-bmp": "github:misskey-dev/sharp-read-bmp", - "slacc": "0.0.7", + "slacc": "0.0.9", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "summaly": "github:misskey-dev/summaly", - "systeminformation": "5.17.12", + "systeminformation": "5.17.16", "tinycolor2": "1.6.0", "tmp": "0.2.1", "tsc-alias": "1.8.6", "tsconfig-paths": "4.2.0", "twemoji-parser": "14.0.0", "typeorm": "0.3.16", - "typescript": "5.0.4", + "typescript": "5.1.3", "ulid": "2.3.0", - "unzipper": "0.10.11", + "unzipper": "0.10.14", "uuid": "9.0.0", "vary": "1.1.2", "web-push": "3.6.1", - "websocket": "1.0.34", "ws": "8.13.0", "xev": "3.0.2" }, @@ -166,23 +168,22 @@ "@types/accepts": "1.3.5", "@types/archiver": "5.3.2", "@types/bcryptjs": "2.4.2", - "@types/bull": "4.10.0", "@types/cbor": "6.0.0", "@types/color-convert": "2.0.0", "@types/content-disposition": "0.5.5", "@types/escape-regexp": "0.0.1", "@types/fluent-ffmpeg": "2.1.21", - "@types/jest": "29.5.1", + "@types/jest": "29.5.2", "@types/js-yaml": "4.0.5", "@types/jsdom": "21.1.1", "@types/jsonld": "1.5.8", "@types/jsrsasign": "10.5.8", "@types/mime-types": "2.1.1", - "@types/node": "20.1.3", + "@types/node": "20.2.5", "@types/node-fetch": "3.0.3", - "@types/nodemailer": "6.4.7", + "@types/nodemailer": "6.4.8", "@types/oauth": "0.9.1", - "@types/pg": "8.6.6", + "@types/pg": "8.10.1", "@types/pug": "2.0.6", "@types/punycode": "2.1.0", "@types/qrcode": "1.5.0", @@ -196,17 +197,17 @@ "@types/sinonjs__fake-timers": "8.1.2", "@types/tinycolor2": "1.4.3", "@types/tmp": "0.2.3", - "@types/unzipper": "0.10.5", + "@types/unzipper": "0.10.6", "@types/uuid": "9.0.1", "@types/vary": "1.1.0", "@types/web-push": "3.3.2", "@types/websocket": "1.0.5", "@types/ws": "8.5.4", - "@typescript-eslint/eslint-plugin": "5.59.5", - "@typescript-eslint/parser": "5.59.5", + "@typescript-eslint/eslint-plugin": "5.59.8", + "@typescript-eslint/parser": "5.59.8", "aws-sdk-client-mock": "2.1.1", "cross-env": "7.0.3", - "eslint": "8.40.0", + "eslint": "8.41.0", "eslint-plugin-import": "2.27.5", "execa": "6.1.0", "jest": "29.5.0", diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 5fb4e8ef3..406e3192b 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -4,7 +4,7 @@ import * as Redis from 'ioredis'; import { DataSource } from 'typeorm'; import { MeiliSearch } from 'meilisearch'; import { DI } from './di-symbols.js'; -import { loadConfig } from './config.js'; +import { Config, loadConfig } from './config.js'; import { createPostgresDataSource } from './postgres.js'; import { RepositoryModule } from './models/RepositoryModule.js'; import type { Provider, OnApplicationShutdown } from '@nestjs/common'; @@ -25,7 +25,7 @@ const $db: Provider = { const $meilisearch: Provider = { provide: DI.meilisearch, - useFactory: (config) => { + useFactory: (config: Config) => { if (config.meilisearch) { return new MeiliSearch({ host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`, @@ -40,7 +40,7 @@ const $meilisearch: Provider = { const $redis: Provider = { provide: DI.redis, - useFactory: (config) => { + useFactory: (config: Config) => { return new Redis.Redis({ port: config.redis.port, host: config.redis.host, @@ -55,7 +55,7 @@ const $redis: Provider = { const $redisForPub: Provider = { provide: DI.redisForPub, - useFactory: (config) => { + useFactory: (config: Config) => { const redis = new Redis.Redis({ port: config.redisForPubsub.port, host: config.redisForPubsub.host, @@ -71,7 +71,7 @@ const $redisForPub: Provider = { const $redisForSub: Provider = { provide: DI.redisForSub, - useFactory: (config) => { + useFactory: (config: Config) => { const redis = new Redis.Redis({ port: config.redisForPubsub.port, host: config.redisForPubsub.host, @@ -100,7 +100,7 @@ export class GlobalModule implements OnApplicationShutdown { @Inject(DI.redisForSub) private redisForSub: Redis.Redis, ) {} - async onApplicationShutdown(signal: string): Promise<void> { + public async dispose(): Promise<void> { if (process.env.NODE_ENV === 'test') { // XXX: // Shutting down the existing connections causes errors on Jest as @@ -116,4 +116,8 @@ export class GlobalModule implements OnApplicationShutdown { this.redisForSub.disconnect(), ]); } + + async onApplicationShutdown(signal: string): Promise<void> { + await this.dispose(); + } } diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index c6e107538..9d1945e4d 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -144,7 +144,7 @@ export function loadConfig() { const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json'); const clientManifest = clientManifestExists ? JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8')) - : { 'src/init.ts': { file: 'src/init.ts' } }; + : { 'src/_boot_.ts': { file: 'src/_boot_.ts' } }; const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; const mixin = {} as Mixin; @@ -165,7 +165,7 @@ export function loadConfig() { mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`; mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`; mixin.userAgent = `Misskey/${meta.version} (${config.url})`; - mixin.clientEntry = clientManifest['src/init.ts']; + mixin.clientEntry = clientManifest['src/_boot_.ts']; mixin.clientManifestExists = clientManifestExists; const externalMediaProxy = config.mediaProxy ? @@ -190,6 +190,6 @@ function tryCreateUrl(url: string) { try { return new URL(url); } catch (e) { - throw `url="${url}" is not a valid URL.`; + throw new Error(`url="${url}" is not a valid URL.`); } } diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index 2d4226a32..d8df37191 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -55,11 +55,6 @@ export class AntennaService implements OnApplicationShutdown { this.redisForSub.on('message', this.onRedisMessage); } - @bindThis - public onApplicationShutdown(signal?: string | undefined) { - this.redisForSub.off('message', this.onRedisMessage); - } - @bindThis private async onRedisMessage(_: string, data: string): Promise<void> { const obj = JSON.parse(data); @@ -196,4 +191,14 @@ export class AntennaService implements OnApplicationShutdown { return this.antennas; } + + @bindThis + public dispose(): void { + this.redisForSub.off('message', this.onRedisMessage); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index cf1e81ffc..de33e4c24 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -166,7 +166,12 @@ export class CacheService implements OnApplicationShutdown { } @bindThis - public onApplicationShutdown(signal?: string | undefined) { + public dispose(): void { this.redisForSub.off('message', this.onMessage); } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index 7aaa1b833..1a52a229c 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -30,7 +30,7 @@ export class CaptchaService { }, { throwErrorWhenResponseNotOk: false }); if (!res.ok) { - throw `${res.status}`; + throw new Error(`${res.status}`); } return await res.json() as CaptchaResponse; @@ -39,48 +39,48 @@ export class CaptchaService { @bindThis public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> { if (response == null) { - throw 'recaptcha-failed: no response provided'; + throw new Error('recaptcha-failed: no response provided'); } const result = await this.getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(err => { - throw `recaptcha-request-failed: ${err}`; + throw new Error(`recaptcha-request-failed: ${err}`); }); if (result.success !== true) { const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; - throw `recaptcha-failed: ${errorCodes}`; + throw new Error(`recaptcha-failed: ${errorCodes}`); } } @bindThis public async verifyHcaptcha(secret: string, response: string | null | undefined): Promise<void> { if (response == null) { - throw 'hcaptcha-failed: no response provided'; + throw new Error('hcaptcha-failed: no response provided'); } const result = await this.getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(err => { - throw `hcaptcha-request-failed: ${err}`; + throw new Error(`hcaptcha-request-failed: ${err}`); }); if (result.success !== true) { const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; - throw `hcaptcha-failed: ${errorCodes}`; + throw new Error(`hcaptcha-failed: ${errorCodes}`); } } @bindThis public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> { if (response == null) { - throw 'turnstile-failed: no response provided'; + throw new Error('turnstile-failed: no response provided'); } const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => { - throw `turnstile-request-failed: ${err}`; + throw new Error(`turnstile-request-failed: ${err}`); }); if (result.success !== true) { const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; - throw `turnstile-failed: ${errorCodes}`; + throw new Error(`turnstile-failed: ${errorCodes}`); } } } diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 93557ce61..3499df38b 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -7,7 +7,7 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Emoji } from '@/models/entities/Emoji.js'; -import type { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository, Role } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -15,6 +15,8 @@ import type { Config } from '@/config.js'; import { query } from '@/misc/prelude/url.js'; import type { Serialized } from '@/server/api/stream/types.js'; +const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; + @Injectable() export class CustomEmojiService { private cache: MemoryKVCache<Emoji | null>; @@ -63,6 +65,9 @@ export class CustomEmojiService { aliases: string[]; host: string | null; license: string | null; + isSensitive: boolean; + localOnly: boolean; + roleIdsThatCanBeUsedThisEmojiAsReaction: Role['id'][]; }): Promise<Emoji> { const emoji = await this.emojisRepository.insert({ id: this.idService.genId(), @@ -75,6 +80,9 @@ export class CustomEmojiService { publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url, type: data.driveFile.webpublicType ?? data.driveFile.type, license: data.license, + isSensitive: data.isSensitive, + localOnly: data.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction, }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); if (data.host == null) { @@ -90,10 +98,14 @@ export class CustomEmojiService { @bindThis public async update(id: Emoji['id'], data: { + driveFile?: DriveFile; name?: string; category?: string | null; aliases?: string[]; license?: string | null; + isSensitive?: boolean; + localOnly?: boolean; + roleIdsThatCanBeUsedThisEmojiAsReaction?: Role['id'][]; }): Promise<void> { const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() }); @@ -105,6 +117,12 @@ export class CustomEmojiService { category: data.category, aliases: data.aliases, license: data.license, + isSensitive: data.isSensitive, + localOnly: data.localOnly, + originalUrl: data.driveFile != null ? data.driveFile.url : undefined, + publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined, + type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined, + roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined, }); this.localEmojisCache.refresh(); @@ -259,7 +277,7 @@ export class CustomEmojiService { @bindThis public parseEmojiStr(emojiName: string, noteUserHost: string | null) { - const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/); + const match = emojiName.match(parseEmojiStrRegexp); if (!match) return { name: null, host: null }; const name = match[1]; diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index 8103d5afe..9de633350 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -116,14 +116,14 @@ export class FetchInstanceMetadataService { const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo') .catch(err => { if (err.statusCode === 404) { - throw 'No nodeinfo provided'; + throw new Error('No nodeinfo provided'); } else { throw err.statusCode ?? err.message; } }) as Record<string, unknown>; if (wellknown.links == null || !Array.isArray(wellknown.links)) { - throw 'No wellknown links'; + throw new Error('No wellknown links'); } const links = wellknown.links as any[]; @@ -134,7 +134,7 @@ export class FetchInstanceMetadataService { const link = lnik2_1 ?? lnik2_0 ?? lnik1_0; if (link == null) { - throw 'No nodeinfo link provided'; + throw new Error('No nodeinfo link provided'); } const info = await this.httpRequestService.getJson(link.href) diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts index 0b861be8d..5acc9ad9a 100644 --- a/packages/backend/src/core/MetaService.ts +++ b/packages/backend/src/core/MetaService.ts @@ -120,8 +120,13 @@ export class MetaService implements OnApplicationShutdown { } @bindThis - public onApplicationShutdown(signal?: string | undefined) { + public dispose(): void { clearInterval(this.intervalId); this.redisForSub.off('message', this.onMessage); } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 9b2d5dc0f..dffee16e0 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -83,7 +83,7 @@ export class MfmService { if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) { text += txt; // メンション - } else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) { + } else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) { const part = txt.split('@'); if (part.length === 2 && href) { diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 977c9052c..1c8491bf5 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -510,7 +510,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.poll && data.poll.expiresAt) { const delay = data.poll.expiresAt.getTime() - Date.now(); - this.queueService.endedPollNotificationQueue.add({ + this.queueService.endedPollNotificationQueue.add(note.id, { noteId: note.id, }, { delay, @@ -790,7 +790,13 @@ export class NoteCreateService implements OnApplicationShutdown { return mentionedUsers; } - onApplicationShutdown(signal?: string | undefined) { + @bindThis + public dispose(): void { this.#shutdownController.abort(); } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts index 1129bd159..e57e57d31 100644 --- a/packages/backend/src/core/NoteReadService.ts +++ b/packages/backend/src/core/NoteReadService.ts @@ -122,7 +122,13 @@ export class NoteReadService implements OnApplicationShutdown { } } - onApplicationShutdown(signal?: string | undefined): void { + @bindThis + public dispose(): void { this.#shutdownController.abort(); } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index a245908c9..ed47165f7 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -152,7 +152,13 @@ export class NotificationService implements OnApplicationShutdown { */ } - onApplicationShutdown(signal?: string | undefined): void { + @bindThis + public dispose(): void { this.#shutdownController.abort(); } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index 0cee2076b..bf50a1cde 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -208,7 +208,7 @@ export class QueryService { } @bindThis - public generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User, 'id' | 'showTimelineReplies'> | null): void { + public generateRepliesQuery(q: SelectQueryBuilder<any>, withReplies: boolean, me?: Pick<User, 'id'> | null): void { if (me == null) { q.andWhere(new Brackets(qb => { qb .where('note.replyId IS NULL') // 返信ではない @@ -217,7 +217,7 @@ export class QueryService { .andWhere('note.replyUserId = note.userId'); })); })); - } else if (!me.showTimelineReplies) { + } else if (!withReplies) { q.andWhere(new Brackets(qb => { qb .where('note.replyId IS NULL') // 返信ではない .orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信 diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index 1d7394777..3384ca457 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -1,42 +1,11 @@ import { setTimeout } from 'node:timers/promises'; import { Inject, Module, OnApplicationShutdown } from '@nestjs/common'; -import Bull from 'bull'; +import * as Bull from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; +import { QUEUE, baseQueueOptions } from '@/queue/const.js'; import type { Provider } from '@nestjs/common'; -import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData, DbJobMap } from '../queue/types.js'; - -function q<T>(config: Config, name: string, limitPerSec = -1) { - return new Bull<T>(name, { - redis: { - port: config.redisForJobQueue.port, - host: config.redisForJobQueue.host, - family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family, - password: config.redisForJobQueue.pass, - db: config.redisForJobQueue.db ?? 0, - }, - prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue` : 'queue', - limiter: limitPerSec > 0 ? { - max: limitPerSec, - duration: 1000, - } : undefined, - settings: { - backoffStrategies: { - apBackoff, - }, - }, - }); -} - -// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019 -function apBackoff(attemptsMade: number, err: Error) { - const baseDelay = 60 * 1000; // 1min - const maxBackoff = 8 * 60 * 60 * 1000; // 8hours - let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay; - backoff = Math.min(backoff, maxBackoff); - backoff += Math.round(backoff * Math.random() * 0.2); - return backoff; -} +import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js'; export type SystemQueue = Bull.Queue<Record<string, unknown>>; export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>; @@ -49,49 +18,49 @@ export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>; const $system: Provider = { provide: 'queue:system', - useFactory: (config: Config) => q(config, 'system'), + useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM, baseQueueOptions(config, QUEUE.SYSTEM)), inject: [DI.config], }; const $endedPollNotification: Provider = { provide: 'queue:endedPollNotification', - useFactory: (config: Config) => q(config, 'endedPollNotification'), + useFactory: (config: Config) => new Bull.Queue(QUEUE.ENDED_POLL_NOTIFICATION, baseQueueOptions(config, QUEUE.ENDED_POLL_NOTIFICATION)), inject: [DI.config], }; const $deliver: Provider = { provide: 'queue:deliver', - useFactory: (config: Config) => q(config, 'deliver', config.deliverJobPerSec ?? 128), + useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)), inject: [DI.config], }; const $inbox: Provider = { provide: 'queue:inbox', - useFactory: (config: Config) => q(config, 'inbox', config.inboxJobPerSec ?? 16), + useFactory: (config: Config) => new Bull.Queue(QUEUE.INBOX, baseQueueOptions(config, QUEUE.INBOX)), inject: [DI.config], }; const $db: Provider = { provide: 'queue:db', - useFactory: (config: Config) => q(config, 'db'), + useFactory: (config: Config) => new Bull.Queue(QUEUE.DB, baseQueueOptions(config, QUEUE.DB)), inject: [DI.config], }; const $relationship: Provider = { provide: 'queue:relationship', - useFactory: (config: Config) => q(config, 'relationship', config.relashionshipJobPerSec ?? 64), + useFactory: (config: Config) => new Bull.Queue(QUEUE.RELATIONSHIP, baseQueueOptions(config, QUEUE.RELATIONSHIP)), inject: [DI.config], }; const $objectStorage: Provider = { provide: 'queue:objectStorage', - useFactory: (config: Config) => q(config, 'objectStorage'), + useFactory: (config: Config) => new Bull.Queue(QUEUE.OBJECT_STORAGE, baseQueueOptions(config, QUEUE.OBJECT_STORAGE)), inject: [DI.config], }; const $webhookDeliver: Provider = { provide: 'queue:webhookDeliver', - useFactory: (config: Config) => q(config, 'webhookDeliver', 64), + useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)), inject: [DI.config], }; @@ -131,7 +100,7 @@ export class QueueModule implements OnApplicationShutdown { @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, ) {} - async onApplicationShutdown(signal: string): Promise<void> { + public async dispose(): Promise<void> { if (process.env.NODE_ENV === 'test') { // XXX: // Shutting down the existing connections causes errors on Jest as @@ -151,4 +120,8 @@ export class QueueModule implements OnApplicationShutdown { this.webhookDeliverQueue.close(), ]); } + + async onApplicationShutdown(signal: string): Promise<void> { + await this.dispose(); + } } diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index b4ffffecc..2ae8a2b75 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -1,6 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; import { v4 as uuid } from 'uuid'; -import Bull from 'bull'; import type { IActivity } from '@/core/activitypub/type.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js'; @@ -11,6 +10,7 @@ import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; import type httpSignature from '@peertube/http-signature'; +import type * as Bull from 'bullmq'; @Injectable() export class QueueService { @@ -26,7 +26,43 @@ export class QueueService { @Inject('queue:relationship') public relationshipQueue: RelationshipQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, - ) {} + ) { + this.systemQueue.add('tickCharts', { + }, { + repeat: { pattern: '55 * * * *' }, + removeOnComplete: true, + }); + + this.systemQueue.add('resyncCharts', { + }, { + repeat: { pattern: '0 0 * * *' }, + removeOnComplete: true, + }); + + this.systemQueue.add('cleanCharts', { + }, { + repeat: { pattern: '0 0 * * *' }, + removeOnComplete: true, + }); + + this.systemQueue.add('aggregateRetention', { + }, { + repeat: { pattern: '0 0 * * *' }, + removeOnComplete: true, + }); + + this.systemQueue.add('clean', { + }, { + repeat: { pattern: '0 0 * * *' }, + removeOnComplete: true, + }); + + this.systemQueue.add('checkExpiredMutings', { + }, { + repeat: { pattern: '*/5 * * * *' }, + removeOnComplete: true, + }); + } @bindThis public deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean) { @@ -42,11 +78,10 @@ export class QueueService { isSharedInbox, }; - return this.deliverQueue.add(data, { + return this.deliverQueue.add(to, data, { attempts: this.config.deliverJobMaxAttempts ?? 12, - timeout: 1 * 60 * 1000, // 1min backoff: { - type: 'apBackoff', + type: 'custom', }, removeOnComplete: true, removeOnFail: true, @@ -60,11 +95,10 @@ export class QueueService { signature, }; - return this.inboxQueue.add(data, { + return this.inboxQueue.add('', data, { attempts: this.config.inboxJobMaxAttempts ?? 8, - timeout: 5 * 60 * 1000, // 5min backoff: { - type: 'apBackoff', + type: 'custom', }, removeOnComplete: true, removeOnFail: true, @@ -212,7 +246,7 @@ export class QueueService { private generateToDbJobData<T extends 'importFollowingToDb' | 'importBlockingToDb', D extends DbJobData<T>>(name: T, data: D): { name: string, data: D, - opts: Bull.JobOptions, + opts: Bull.JobsOptions, } { return { name, @@ -299,10 +333,10 @@ export class QueueService { } @bindThis - private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData, opts: Bull.JobOptions = {}): { + private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData, opts: Bull.JobsOptions = {}): { name: string, data: RelationshipJobData, - opts: Bull.JobOptions, + opts: Bull.JobsOptions, } { return { name, @@ -351,11 +385,10 @@ export class QueueService { eventId: uuid(), }; - return this.webhookDeliverQueue.add(data, { + return this.webhookDeliverQueue.add(webhook.id, data, { attempts: 4, - timeout: 1 * 60 * 1000, // 1min backoff: { - type: 'apBackoff', + type: 'custom', }, removeOnComplete: true, removeOnFail: true, @@ -367,11 +400,11 @@ export class QueueService { this.deliverQueue.once('cleaned', (jobs, status) => { //deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); }); - this.deliverQueue.clean(0, 'delayed'); + this.deliverQueue.clean(0, Infinity, 'delayed'); this.inboxQueue.once('cleaned', (jobs, status) => { //inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); }); - this.inboxQueue.clean(0, 'delayed'); + this.inboxQueue.clean(0, Infinity, 'delayed'); } } diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index a274b19e4..4b01b6af7 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -20,6 +20,7 @@ import { bindThis } from '@/decorators.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { RoleService } from '@/core/RoleService.js'; const FALLBACK = '❤'; @@ -54,6 +55,9 @@ type DecodedReaction = { host?: string | null; }; +const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/; +const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/; + @Injectable() export class ReactionService { constructor( @@ -72,6 +76,7 @@ export class ReactionService { private utilityService: UtilityService, private metaService: MetaService, private customEmojiService: CustomEmojiService, + private roleService: RoleService, private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private userBlockingService: UserBlockingService, @@ -85,7 +90,7 @@ export class ReactionService { } @bindThis - public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string | null) { + public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, _reaction?: string | null) { // Check blocking if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); @@ -99,10 +104,41 @@ export class ReactionService { throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.'); } - if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) { + let reaction = _reaction ?? FALLBACK; + + if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) { reaction = '❤️'; - } else { - reaction = await this.toDbReaction(reaction, user.host); + } else if (_reaction) { + const custom = reaction.match(isCustomEmojiRegexp); + if (custom) { + const reacterHost = this.utilityService.toPunyNullable(user.host); + + const name = custom[1]; + const emoji = reacterHost == null + ? (await this.customEmojiService.localEmojisCache.fetch()).get(name) + : await this.emojisRepository.findOneBy({ + host: reacterHost, + name, + }); + + if (emoji) { + if (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || (await this.roleService.getUserRoles(user.id)).some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))) { + reaction = reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; + + // センシティブ + if ((note.reactionAcceptance === 'nonSensitiveOnly') && emoji.isSensitive) { + reaction = FALLBACK; + } + } else { + // リアクションとして使う権限がない + reaction = FALLBACK; + } + } else { + reaction = FALLBACK; + } + } else { + reaction = this.normalize(reaction ?? null); + } } const record: NoteReaction = { @@ -288,11 +324,9 @@ export class ReactionService { } @bindThis - public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> { + public normalize(reaction: string | null): string { if (reaction == null) return FALLBACK; - reacterHost = this.utilityService.toPunyNullable(reacterHost); - // 文字列タイプのリアクションを絵文字に変換 if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; @@ -306,25 +340,12 @@ export class ReactionService { return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, ''); } - const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/); - if (custom) { - const name = custom[1]; - const emoji = reacterHost == null - ? (await this.customEmojiService.localEmojisCache.fetch()).get(name) - : await this.emojisRepository.findOneBy({ - host: reacterHost, - name, - }); - - if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; - } - return FALLBACK; } @bindThis public decodeReaction(str: string): DecodedReaction { - const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/); + const custom = str.match(decodeCustomEmojiRegexp); if (custom) { const name = custom[1]; diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 68087ccc3..40ae10666 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -306,6 +306,14 @@ export class RoleService implements OnApplicationShutdown { return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator); } + @bindThis + public async isExplorable(role: { id: Role['id']} | null): Promise<boolean> { + if (role == null) return false; + const check = await this.rolesRepository.findOneBy({ id: role.id }); + if (check == null) return false; + return check.isExplorable; + } + @bindThis public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> { const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); @@ -425,7 +433,12 @@ export class RoleService implements OnApplicationShutdown { } @bindThis - public onApplicationShutdown(signal?: string | undefined) { + public dispose(): void { this.redisForSub.off('message', this.onMessage); } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/WebfingerService.ts b/packages/backend/src/core/WebfingerService.ts index 3ee799064..f58a6a10f 100644 --- a/packages/backend/src/core/WebfingerService.ts +++ b/packages/backend/src/core/WebfingerService.ts @@ -16,6 +16,9 @@ type IWebFinger = { subject: string; }; +const urlRegex = /^https?:\/\//; +const mRegex = /^([^@]+)@(.*)/; + @Injectable() export class WebfingerService { constructor( @@ -35,12 +38,12 @@ export class WebfingerService { @bindThis private genUrl(query: string): string { - if (query.match(/^https?:\/\//)) { + if (query.match(urlRegex)) { const u = new URL(query); return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query }); } - const m = query.match(/^([^@]+)@(.*)/); + const m = query.match(mRegex); if (m) { const hostname = m[2]; const useHttp = process.env.MISSKEY_WEBFINGER_USE_HTTP && process.env.MISSKEY_WEBFINGER_USE_HTTP.toLowerCase() === 'true'; diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts index 57baade77..467755a07 100644 --- a/packages/backend/src/core/WebhookService.ts +++ b/packages/backend/src/core/WebhookService.ts @@ -81,7 +81,12 @@ export class WebhookService implements OnApplicationShutdown { } @bindThis - public onApplicationShutdown(signal?: string | undefined) { + public dispose(): void { this.redisForSub.off('message', this.onMessage); } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 60e19bfca..d8b95ca4d 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -277,7 +277,7 @@ export class ApRendererService { const name = reaction.replaceAll(':', ''); const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name); - if (emoji) object.tag = [this.renderEmoji(emoji)]; + if (emoji && !emoji.localOnly) object.tag = [this.renderEmoji(emoji)]; } return object; @@ -400,7 +400,7 @@ export class ApRendererService { })); const emojis = await this.getEmojis(note.emojis); - const apemojis = emojis.map(emoji => this.renderEmoji(emoji)); + const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); const tag = [ ...hashtagTags, @@ -479,7 +479,7 @@ export class ApRendererService { } const emojis = await this.getEmojis(user.emojis); - const apemojis = emojis.map(emoji => this.renderEmoji(emoji)); + const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); const hashtagTags = (user.tags ?? []).map(tag => this.renderHashtag(tag)); diff --git a/packages/backend/src/core/activitypub/LdSignatureService.ts b/packages/backend/src/core/activitypub/LdSignatureService.ts index 2dc1a410a..20fe2a0a7 100644 --- a/packages/backend/src/core/activitypub/LdSignatureService.ts +++ b/packages/backend/src/core/activitypub/LdSignatureService.ts @@ -94,7 +94,7 @@ class LdSignature { @bindThis private getLoader() { return async (url: string): Promise<any> => { - if (!url.match('^https?\:\/\/')) throw `Invalid URL ${url}`; + if (!url.match('^https?\:\/\/')) throw new Error(`Invalid URL ${url}`); if (this.preLoad) { if (url in CONTEXTS) { @@ -126,7 +126,7 @@ class LdSignature { timeout: this.loderTimeout, }, { throwErrorWhenResponseNotOk: false }).then(res => { if (!res.ok) { - throw `${res.status} ${res.statusText}`; + throw new Error(`${res.status} ${res.statusText}`); } else { return res.json(); } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 87a9db405..76757f530 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -18,6 +18,7 @@ import { PollService } from '@/core/PollService.js'; import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; +import { checkHttps } from '@/misc/check-https.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { ApLoggerService } from '../ApLoggerService.js'; @@ -32,7 +33,6 @@ import { ApQuestionService } from './ApQuestionService.js'; import { ApImageService } from './ApImageService.js'; import type { Resolver } from '../ApResolverService.js'; import type { IObject, IPost } from '../type.js'; -import { checkHttps } from '@/misc/check-https.js'; @Injectable() export class ApNoteService { @@ -230,7 +230,7 @@ export class ApNoteService { quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw 'quote resolve failed'; + throw new Error('quote resolve failed'); } } } @@ -311,7 +311,7 @@ export class ApNoteService { // ブロックしてたら中断 const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw { statusCode: 451 }; + if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451); const unlock = await this.appLockService.getApLock(uri); diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index eea1d1b84..f52ebed10 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -32,6 +32,8 @@ import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import type { AccountMoveService } from '@/core/AccountMoveService.js'; +import { checkHttps } from '@/misc/check-https.js'; import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { extractApHashtags } from './tag.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -42,8 +44,6 @@ import type { ApLoggerService } from '../ApLoggerService.js'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports import type { ApImageService } from './ApImageService.js'; import type { IActor, IObject } from '../type.js'; -import type { AccountMoveService } from '@/core/AccountMoveService.js'; -import { checkHttps } from '@/misc/check-https.js'; const nameLength = 128; const summaryLength = 2048; @@ -306,7 +306,6 @@ export class ApPersonService implements OnModuleInit { tags, isBot, isCat: (person as any).isCat === true, - showTimelineReplies: false, })) as RemoteUser; await transactionalEntityManager.save(new UserProfile({ @@ -696,7 +695,7 @@ export class ApPersonService implements OnModuleInit { if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) { return 'skip: dst.alsoKnownAs is empty'; } - if (!dst.alsoKnownAs?.includes(src.uri)) { + if (!dst.alsoKnownAs.includes(src.uri)) { return 'skip: alsoKnownAs does not include from.uri'; } diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts index 03e361265..b0e9e534d 100644 --- a/packages/backend/src/core/chart/ChartManagementService.ts +++ b/packages/backend/src/core/chart/ChartManagementService.ts @@ -60,7 +60,8 @@ export class ChartManagementService implements OnApplicationShutdown { }, 1000 * 60 * 20); } - async onApplicationShutdown(signal: string): Promise<void> { + @bindThis + public async dispose(): Promise<void> { clearInterval(this.saveIntervalId); if (process.env.NODE_ENV !== 'test') { await Promise.all( @@ -68,4 +69,9 @@ export class ChartManagementService implements OnApplicationShutdown { ); } } + + @bindThis + async onApplicationShutdown(signal: string): Promise<void> { + await this.dispose(); + } } diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 3bad048bc..4a18cd1b3 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -26,6 +26,8 @@ export class EmojiEntityService { category: emoji.category, // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) url: emoji.publicUrl || emoji.originalUrl, + isSensitive: emoji.isSensitive ? true : undefined, + roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined, }; } @@ -51,6 +53,9 @@ export class EmojiEntityService { // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) url: emoji.publicUrl || emoji.originalUrl, license: emoji.license, + isSensitive: emoji.isSensitive, + localOnly: emoji.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, }; } diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 7f61e1d6f..bfd506ea8 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -466,7 +466,6 @@ export class UserEntityService implements OnModuleInit { mutedInstances: profile!.mutedInstances, mutingNotificationTypes: profile!.mutingNotificationTypes, emailNotificationTypes: profile!.emailNotificationTypes, - showTimelineReplies: user.showTimelineReplies ?? falsy, achievements: profile!.achievements, loggedInDays: profile!.loggedInDates.length, policies: this.roleService.getUserPolicies(user.id), diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts index 2461cb2c1..862881927 100644 --- a/packages/backend/src/core/entities/UserListEntityService.ts +++ b/packages/backend/src/core/entities/UserListEntityService.ts @@ -35,6 +35,7 @@ export class UserListEntityService { createdAt: userList.createdAt.toISOString(), name: userList.name, userIds: users.map(x => x.userId), + isPublic: userList.isPublic, }; } } diff --git a/packages/backend/src/daemons/JanitorService.ts b/packages/backend/src/daemons/JanitorService.ts index 8cdfb703f..f826d5062 100644 --- a/packages/backend/src/daemons/JanitorService.ts +++ b/packages/backend/src/daemons/JanitorService.ts @@ -34,7 +34,12 @@ export class JanitorService implements OnApplicationShutdown { } @bindThis - public onApplicationShutdown(signal?: string | undefined) { + public dispose(): void { clearInterval(this.intervalId); } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/daemons/QueueStatsService.ts b/packages/backend/src/daemons/QueueStatsService.ts index b717434e0..53a0d14cd 100644 --- a/packages/backend/src/daemons/QueueStatsService.ts +++ b/packages/backend/src/daemons/QueueStatsService.ts @@ -1,7 +1,11 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import Xev from 'xev'; +import * as Bull from 'bullmq'; import { QueueService } from '@/core/QueueService.js'; import { bindThis } from '@/decorators.js'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; +import { QUEUE, baseQueueOptions } from '@/queue/const.js'; import type { OnApplicationShutdown } from '@nestjs/common'; const ev = new Xev(); @@ -13,6 +17,9 @@ export class QueueStatsService implements OnApplicationShutdown { private intervalId: NodeJS.Timer; constructor( + @Inject(DI.config) + private config: Config, + private queueService: QueueService, ) { } @@ -31,11 +38,14 @@ export class QueueStatsService implements OnApplicationShutdown { let activeDeliverJobs = 0; let activeInboxJobs = 0; - this.queueService.deliverQueue.on('global:active', () => { + const deliverQueueEvents = new Bull.QueueEvents(QUEUE.DELIVER, baseQueueOptions(this.config, QUEUE.DELIVER)); + const inboxQueueEvents = new Bull.QueueEvents(QUEUE.INBOX, baseQueueOptions(this.config, QUEUE.INBOX)); + + deliverQueueEvents.on('active', () => { activeDeliverJobs++; }); - this.queueService.inboxQueue.on('global:active', () => { + inboxQueueEvents.on('active', () => { activeInboxJobs++; }); @@ -71,9 +81,14 @@ export class QueueStatsService implements OnApplicationShutdown { this.intervalId = setInterval(tick, interval); } - + @bindThis - public onApplicationShutdown(signal?: string | undefined) { + public dispose(): void { clearInterval(this.intervalId); } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts index bb190cf60..6cd71c0e2 100644 --- a/packages/backend/src/daemons/ServerStatsService.ts +++ b/packages/backend/src/daemons/ServerStatsService.ts @@ -63,9 +63,14 @@ export class ServerStatsService implements OnApplicationShutdown { } @bindThis - public onApplicationShutdown(signal?: string | undefined) { + public dispose(): void { clearInterval(this.intervalId); } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } // CPU STAT diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index c06c7a715..4a073f102 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -25,6 +25,7 @@ export const DI = { userSecurityKeysRepository: Symbol('userSecurityKeysRepository'), userPublickeysRepository: Symbol('userPublickeysRepository'), userListsRepository: Symbol('userListsRepository'), + userListFavoritesRepository: Symbol('userListFavoritesRepository'), userListJoiningsRepository: Symbol('userListJoiningsRepository'), userNotePiningsRepository: Symbol('userNotePiningsRepository'), userIpsRepository: Symbol('userIpsRepository'), diff --git a/packages/backend/src/misc/id/aid.ts b/packages/backend/src/misc/id/aid.ts index 9e206ee98..f0cbc9900 100644 --- a/packages/backend/src/misc/id/aid.ts +++ b/packages/backend/src/misc/id/aid.ts @@ -21,7 +21,7 @@ function getNoise(): string { export function genAid(date: Date): string { const t = date.getTime(); - if (isNaN(t)) throw 'Failed to create AID: Invalid Date'; + if (isNaN(t)) throw new Error('Failed to create AID: Invalid Date'); counter++; return getTime(t) + getNoise(); } diff --git a/packages/backend/src/misc/prelude/time.ts b/packages/backend/src/misc/prelude/time.ts index 34e8b6b17..b21978b18 100644 --- a/packages/backend/src/misc/prelude/time.ts +++ b/packages/backend/src/misc/prelude/time.ts @@ -5,15 +5,16 @@ const dateTimeIntervals = { }; export function dateUTC(time: number[]): Date { - const d = time.length === 2 ? Date.UTC(time[0], time[1]) - : time.length === 3 ? Date.UTC(time[0], time[1], time[2]) - : time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3]) - : time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4]) - : time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5]) - : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6]) - : null; + const d = + time.length === 2 ? Date.UTC(time[0], time[1]) + : time.length === 3 ? Date.UTC(time[0], time[1], time[2]) + : time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3]) + : time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4]) + : time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5]) + : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6]) + : null; - if (!d) throw 'wrong number of arguments'; + if (!d) throw new Error('wrong number of arguments'); return new Date(d); } diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 588c98b58..4231acc04 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite, UserMemo } from './index.js'; +import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite, UserMemo, UserListFavorite } from './index.js'; import type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; @@ -112,6 +112,12 @@ const $userListsRepository: Provider = { inject: [DI.db], }; +const $userListFavoritesRepository: Provider = { + provide: DI.userListFavoritesRepository, + useFactory: (db: DataSource) => db.getRepository(UserListFavorite), + inject: [DI.db], +}; + const $userListJoiningsRepository: Provider = { provide: DI.userListJoiningsRepository, useFactory: (db: DataSource) => db.getRepository(UserListJoining), @@ -416,6 +422,7 @@ const $userMemosRepository: Provider = { $userSecurityKeysRepository, $userPublickeysRepository, $userListsRepository, + $userListFavoritesRepository, $userListJoiningsRepository, $userNotePiningsRepository, $userIpsRepository, @@ -483,6 +490,7 @@ const $userMemosRepository: Provider = { $userSecurityKeysRepository, $userPublickeysRepository, $userListsRepository, + $userListFavoritesRepository, $userListJoiningsRepository, $userNotePiningsRepository, $userIpsRepository, diff --git a/packages/backend/src/models/entities/Emoji.ts b/packages/backend/src/models/entities/Emoji.ts index dbb437d43..8fd3e65f5 100644 --- a/packages/backend/src/models/entities/Emoji.ts +++ b/packages/backend/src/models/entities/Emoji.ts @@ -60,4 +60,20 @@ export class Emoji { length: 1024, nullable: true, }) public license: string | null; + + @Column('boolean', { + default: false, + }) + public localOnly: boolean; + + @Column('boolean', { + default: false, + }) + public isSensitive: boolean; + + // TODO: 定期ジョブで存在しなくなったロールIDを除去するようにする + @Column('varchar', { + array: true, length: 128, default: '{}', + }) + public roleIdsThatCanBeUsedThisEmojiAsReaction: string[]; } diff --git a/packages/backend/src/models/entities/Note.ts b/packages/backend/src/models/entities/Note.ts index df508b4dc..4f49a0595 100644 --- a/packages/backend/src/models/entities/Note.ts +++ b/packages/backend/src/models/entities/Note.ts @@ -90,7 +90,7 @@ export class Note { @Column('varchar', { length: 64, nullable: true, }) - public reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | null; + public reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null; @Column('smallint', { default: 0, diff --git a/packages/backend/src/models/entities/User.ts b/packages/backend/src/models/entities/User.ts index 8e10f999b..6669890cf 100644 --- a/packages/backend/src/models/entities/User.ts +++ b/packages/backend/src/models/entities/User.ts @@ -232,12 +232,6 @@ export class User { }) public followersUri: string | null; - @Column('boolean', { - default: false, - comment: 'Whether to show users replying to other users in the timeline.', - }) - public showTimelineReplies: boolean; - @Index({ unique: true }) @Column('char', { length: 16, nullable: true, unique: true, diff --git a/packages/backend/src/models/entities/UserList.ts b/packages/backend/src/models/entities/UserList.ts index b8a4b54d4..94f3dc3cb 100644 --- a/packages/backend/src/models/entities/UserList.ts +++ b/packages/backend/src/models/entities/UserList.ts @@ -19,6 +19,12 @@ export class UserList { }) public userId: User['id']; + @Index() + @Column('boolean', { + default: false, + }) + public isPublic: boolean; + @ManyToOne(type => User, { onDelete: 'CASCADE', }) diff --git a/packages/backend/src/models/entities/UserListFavorite.ts b/packages/backend/src/models/entities/UserListFavorite.ts new file mode 100644 index 000000000..e57abb460 --- /dev/null +++ b/packages/backend/src/models/entities/UserListFavorite.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { UserList } from './UserList.js'; + +@Entity() +@Index(['userId', 'userListId'], { unique: true }) +export class UserListFavorite { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public userListId: UserList['id']; + + @ManyToOne(type => UserList, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public userList: UserList | null; +} diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index b8ba28db9..4b230ab74 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -49,6 +49,7 @@ import { User } from '@/models/entities/User.js'; import { UserIp } from '@/models/entities/UserIp.js'; import { UserKeypair } from '@/models/entities/UserKeypair.js'; import { UserList } from '@/models/entities/UserList.js'; +import { UserListFavorite } from './entities/UserListFavorite.js'; import { UserListJoining } from '@/models/entities/UserListJoining.js'; import { UserNotePining } from '@/models/entities/UserNotePining.js'; import { UserPending } from '@/models/entities/UserPending.js'; @@ -117,6 +118,7 @@ export { UserIp, UserKeypair, UserList, + UserListFavorite, UserListJoining, UserNotePining, UserPending, @@ -184,6 +186,7 @@ export type UsersRepository = Repository<User>; export type UserIpsRepository = Repository<UserIp>; export type UserKeypairsRepository = Repository<UserKeypair>; export type UserListsRepository = Repository<UserList>; +export type UserListFavoritesRepository = Repository<UserListFavorite>; export type UserListJoiningsRepository = Repository<UserListJoining>; export type UserNotePiningsRepository = Repository<UserNotePining>; export type UserPendingsRepository = Repository<UserPending>; diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index db4fd62cf..63f56e77c 100644 --- a/packages/backend/src/models/json-schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -22,6 +22,19 @@ export const packedEmojiSimpleSchema = { type: 'string', optional: false, nullable: false, }, + isSensitive: { + type: 'boolean', + optional: true, nullable: false, + }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, }, } as const; @@ -63,5 +76,22 @@ export const packedEmojiDetailedSchema = { type: 'string', optional: false, nullable: true, }, + isSensitive: { + type: 'boolean', + optional: false, nullable: false, + }, + localOnly: { + type: 'boolean', + optional: false, nullable: false, + }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/user-list.ts b/packages/backend/src/models/json-schema/user-list.ts index 3ba5dc4a8..1e620516e 100644 --- a/packages/backend/src/models/json-schema/user-list.ts +++ b/packages/backend/src/models/json-schema/user-list.ts @@ -25,5 +25,10 @@ export const packedUserListSchema = { format: 'id', }, }, + isPublic: { + type: 'boolean', + nullable: false, + optional: false, + }, }, } as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index f3d404e6c..488979c40 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -57,6 +57,7 @@ import { User } from '@/models/entities/User.js'; import { UserIp } from '@/models/entities/UserIp.js'; import { UserKeypair } from '@/models/entities/UserKeypair.js'; import { UserList } from '@/models/entities/UserList.js'; +import { UserListFavorite } from '@/models/entities/UserListFavorite.js'; import { UserListJoining } from '@/models/entities/UserListJoining.js'; import { UserNotePining } from '@/models/entities/UserNotePining.js'; import { UserPending } from '@/models/entities/UserPending.js'; @@ -132,6 +133,7 @@ export const entities = [ UserKeypair, UserPublickey, UserList, + UserListFavorite, UserListJoining, UserNotePining, UserSecurityKey, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index dc025f988..42f9c1af7 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -1,10 +1,9 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import * as Bull from 'bullmq'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; -import { QueueService } from '@/core/QueueService.js'; import { bindThis } from '@/decorators.js'; -import { getJobInfo } from './get-job-info.js'; import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; @@ -35,17 +34,51 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; +import { QUEUE, baseQueueOptions } from './const.js'; + +// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019 +function httpRelatedBackoff(attemptsMade: number) { + const baseDelay = 60 * 1000; // 1min + const maxBackoff = 8 * 60 * 60 * 1000; // 8hours + let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay; + backoff = Math.min(backoff, maxBackoff); + backoff += Math.round(backoff * Math.random() * 0.2); + return backoff; +} + +function getJobInfo(job: Bull.Job | undefined, increment = false): string { + if (job == null) return '-'; + + const age = Date.now() - job.timestamp; + + const formated = age > 60000 ? `${Math.floor(age / 1000 / 60)}m` + : age > 10000 ? `${Math.floor(age / 1000)}s` + : `${age}ms`; + + // onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする + const currentAttempts = job.attemptsMade + (increment ? 1 : 0); + const maxAttempts = job.opts ? job.opts.attempts : 0; + + return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`; +} @Injectable() -export class QueueProcessorService { +export class QueueProcessorService implements OnApplicationShutdown { private logger: Logger; + private systemQueueWorker: Bull.Worker; + private dbQueueWorker: Bull.Worker; + private deliverQueueWorker: Bull.Worker; + private inboxQueueWorker: Bull.Worker; + private webhookDeliverQueueWorker: Bull.Worker; + private relationshipQueueWorker: Bull.Worker; + private objectStorageQueueWorker: Bull.Worker; + private endedPollNotificationQueueWorker: Bull.Worker; constructor( @Inject(DI.config) private config: Config, private queueLoggerService: QueueLoggerService, - private queueService: QueueService, private webhookDeliverProcessorService: WebhookDeliverProcessorService, private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, private deliverProcessorService: DeliverProcessorService, @@ -77,10 +110,7 @@ export class QueueProcessorService { private cleanProcessorService: CleanProcessorService, ) { this.logger = this.queueLoggerService.logger; - } - @bindThis - public start() { function renderError(e: Error): any { if (e) { // 何故かeがundefinedで来ることがある return { @@ -97,146 +127,232 @@ export class QueueProcessorService { } } - const systemLogger = this.logger.createSubLogger('system'); - const deliverLogger = this.logger.createSubLogger('deliver'); - const webhookLogger = this.logger.createSubLogger('webhook'); - const inboxLogger = this.logger.createSubLogger('inbox'); - const dbLogger = this.logger.createSubLogger('db'); - const relationshipLogger = this.logger.createSubLogger('relationship'); - const objectStorageLogger = this.logger.createSubLogger('objectStorage'); + //#region system + this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => { + switch (job.name) { + case 'tickCharts': return this.tickChartsProcessorService.process(); + case 'resyncCharts': return this.resyncChartsProcessorService.process(); + case 'cleanCharts': return this.cleanChartsProcessorService.process(); + case 'aggregateRetention': return this.aggregateRetentionProcessorService.process(); + case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process(); + case 'clean': return this.cleanProcessorService.process(); + default: throw new Error(`unrecognized job type ${job.name} for system`); + } + }, { + ...baseQueueOptions(this.config, QUEUE.SYSTEM), + autorun: false, + }); - this.queueService.systemQueue - .on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`)) + const systemLogger = this.logger.createSubLogger('system'); + + this.systemQueueWorker .on('active', (job) => systemLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`)); + .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => systemLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`)); + //#endregion - this.queueService.deliverQueue - .on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`)) - .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`)) - .on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); + //#region db + this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => { + switch (job.name) { + case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job); + case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job); + case 'exportNotes': return this.exportNotesProcessorService.process(job); + case 'exportFavorites': return this.exportFavoritesProcessorService.process(job); + case 'exportFollowing': return this.exportFollowingProcessorService.process(job); + case 'exportMuting': return this.exportMutingProcessorService.process(job); + case 'exportBlocking': return this.exportBlockingProcessorService.process(job); + case 'exportUserLists': return this.exportUserListsProcessorService.process(job); + case 'exportAntennas': return this.exportAntennasProcessorService.process(job); + case 'importFollowing': return this.importFollowingProcessorService.process(job); + case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job); + case 'importMuting': return this.importMutingProcessorService.process(job); + case 'importBlocking': return this.importBlockingProcessorService.process(job); + case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job); + case 'importUserLists': return this.importUserListsProcessorService.process(job); + case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job); + case 'importAntennas': return this.importAntennasProcessorService.process(job); + case 'deleteAccount': return this.deleteAccountProcessorService.process(job); + default: throw new Error(`unrecognized job type ${job.name} for db`); + } + }, { + ...baseQueueOptions(this.config, QUEUE.DB), + autorun: false, + }); - this.queueService.inboxQueue - .on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`)) - .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) - .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) - .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => inboxLogger.warn(`stalled ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`)); + const dbLogger = this.logger.createSubLogger('db'); - this.queueService.dbQueue - .on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`)) + this.dbQueueWorker .on('active', (job) => dbLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => dbLogger.warn(`stalled id=${job.id}`)); + .on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => dbLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`)); + //#endregion - this.queueService.relationshipQueue - .on('waiting', (jobId) => relationshipLogger.debug(`waiting id=${jobId}`)) - .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => relationshipLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => relationshipLogger.warn(`stalled id=${job.id}`)); + //#region deliver + this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => this.deliverProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.DELIVER), + autorun: false, + concurrency: this.config.deliverJobConcurrency ?? 128, + limiter: { + max: this.config.deliverJobPerSec ?? 128, + duration: 1000, + }, + settings: { + backoffStrategy: httpRelatedBackoff, + }, + }); - this.queueService.objectStorageQueue - .on('waiting', (jobId) => objectStorageLogger.debug(`waiting id=${jobId}`)) - .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`)); + const deliverLogger = this.logger.createSubLogger('deliver'); - this.queueService.webhookDeliverQueue - .on('waiting', (jobId) => webhookLogger.debug(`waiting id=${jobId}`)) + this.deliverQueueWorker + .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) + .on('error', (err: Error) => deliverLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`)); + //#endregion + + //#region inbox + this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.INBOX), + autorun: false, + concurrency: this.config.inboxJobConcurrency ?? 16, + limiter: { + max: this.config.inboxJobPerSec ?? 16, + duration: 1000, + }, + settings: { + backoffStrategy: httpRelatedBackoff, + }, + }); + + const inboxLogger = this.logger.createSubLogger('inbox'); + + this.inboxQueueWorker + .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) + .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) + .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => inboxLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`)); + //#endregion + + //#region webhook deliver + this.webhookDeliverQueueWorker = new Bull.Worker(QUEUE.WEBHOOK_DELIVER, (job) => this.webhookDeliverProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.WEBHOOK_DELIVER), + autorun: false, + concurrency: 64, + limiter: { + max: 64, + duration: 1000, + }, + settings: { + backoffStrategy: httpRelatedBackoff, + }, + }); + + const webhookLogger = this.logger.createSubLogger('webhook'); + + this.webhookDeliverQueueWorker .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`)) - .on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); + .on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) + .on('error', (err: Error) => webhookLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`)); + //#endregion - this.queueService.systemQueue.add('tickCharts', { + //#region relationship + this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => { + switch (job.name) { + case 'follow': return this.relationshipProcessorService.processFollow(job); + case 'unfollow': return this.relationshipProcessorService.processUnfollow(job); + case 'block': return this.relationshipProcessorService.processBlock(job); + case 'unblock': return this.relationshipProcessorService.processUnblock(job); + default: throw new Error(`unrecognized job type ${job.name} for relationship`); + } }, { - repeat: { cron: '55 * * * *' }, - removeOnComplete: true, + ...baseQueueOptions(this.config, QUEUE.RELATIONSHIP), + autorun: false, + concurrency: this.config.relashionshipJobConcurrency ?? 16, + limiter: { + max: this.config.relashionshipJobPerSec ?? 64, + duration: 1000, + }, }); - this.queueService.systemQueue.add('resyncCharts', { - }, { - repeat: { cron: '0 0 * * *' }, - removeOnComplete: true, - }); - - this.queueService.systemQueue.add('cleanCharts', { - }, { - repeat: { cron: '0 0 * * *' }, - removeOnComplete: true, - }); - - this.queueService.systemQueue.add('aggregateRetention', { - }, { - repeat: { cron: '0 0 * * *' }, - removeOnComplete: true, - }); - - this.queueService.systemQueue.add('clean', { - }, { - repeat: { cron: '0 0 * * *' }, - removeOnComplete: true, - }); - - this.queueService.systemQueue.add('checkExpiredMutings', { - }, { - repeat: { cron: '*/5 * * * *' }, - removeOnComplete: true, - }); - - this.queueService.deliverQueue.process(this.config.deliverJobConcurrency ?? 128, (job) => this.deliverProcessorService.process(job)); - this.queueService.inboxQueue.process(this.config.inboxJobConcurrency ?? 16, (job) => this.inboxProcessorService.process(job)); - this.queueService.endedPollNotificationQueue.process((job, done) => this.endedPollNotificationProcessorService.process(job, done)); - this.queueService.webhookDeliverQueue.process(64, (job) => this.webhookDeliverProcessorService.process(job)); - - this.queueService.dbQueue.process('deleteDriveFiles', (job, done) => this.deleteDriveFilesProcessorService.process(job, done)); - this.queueService.dbQueue.process('exportCustomEmojis', (job, done) => this.exportCustomEmojisProcessorService.process(job, done)); - this.queueService.dbQueue.process('exportNotes', (job, done) => this.exportNotesProcessorService.process(job, done)); - this.queueService.dbQueue.process('exportFavorites', (job, done) => this.exportFavoritesProcessorService.process(job, done)); - this.queueService.dbQueue.process('exportFollowing', (job, done) => this.exportFollowingProcessorService.process(job, done)); - this.queueService.dbQueue.process('exportMuting', (job, done) => this.exportMutingProcessorService.process(job, done)); - this.queueService.dbQueue.process('exportBlocking', (job, done) => this.exportBlockingProcessorService.process(job, done)); - this.queueService.dbQueue.process('exportUserLists', (job, done) => this.exportUserListsProcessorService.process(job, done)); - this.queueService.dbQueue.process('exportAntennas', (job, done) => this.exportAntennasProcessorService.process(job, done)); - this.queueService.dbQueue.process('importFollowing', (job, done) => this.importFollowingProcessorService.process(job, done)); - this.queueService.dbQueue.process('importFollowingToDb', (job) => this.importFollowingProcessorService.processDb(job)); - this.queueService.dbQueue.process('importMuting', (job, done) => this.importMutingProcessorService.process(job, done)); - this.queueService.dbQueue.process('importBlocking', (job, done) => this.importBlockingProcessorService.process(job, done)); - this.queueService.dbQueue.process('importBlockingToDb', (job) => this.importBlockingProcessorService.processDb(job)); - this.queueService.dbQueue.process('importUserLists', (job, done) => this.importUserListsProcessorService.process(job, done)); - this.queueService.dbQueue.process('importCustomEmojis', (job, done) => this.importCustomEmojisProcessorService.process(job, done)); - this.queueService.dbQueue.process('importAntennas', (job, done) => this.importAntennasProcessorService.process(job, done)); - this.queueService.dbQueue.process('deleteAccount', (job) => this.deleteAccountProcessorService.process(job)); - - this.queueService.objectStorageQueue.process('deleteFile', 16, (job) => this.deleteFileProcessorService.process(job)); - this.queueService.objectStorageQueue.process('cleanRemoteFiles', 16, (job, done) => this.cleanRemoteFilesProcessorService.process(job, done)); + const relationshipLogger = this.logger.createSubLogger('relationship'); - { - const maxJobs = this.config.relashionshipJobConcurrency ?? 16; - this.queueService.relationshipQueue.process('follow', maxJobs, (job) => this.relationshipProcessorService.processFollow(job)); - this.queueService.relationshipQueue.process('unfollow', maxJobs, (job) => this.relationshipProcessorService.processUnfollow(job)); - this.queueService.relationshipQueue.process('block', maxJobs, (job) => this.relationshipProcessorService.processBlock(job)); - this.queueService.relationshipQueue.process('unblock', maxJobs, (job) => this.relationshipProcessorService.processUnblock(job)); - } + this.relationshipQueueWorker + .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => relationshipLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`)); + //#endregion - this.queueService.systemQueue.process('tickCharts', (job, done) => this.tickChartsProcessorService.process(job, done)); - this.queueService.systemQueue.process('resyncCharts', (job, done) => this.resyncChartsProcessorService.process(job, done)); - this.queueService.systemQueue.process('cleanCharts', (job, done) => this.cleanChartsProcessorService.process(job, done)); - this.queueService.systemQueue.process('aggregateRetention', (job, done) => this.aggregateRetentionProcessorService.process(job, done)); - this.queueService.systemQueue.process('checkExpiredMutings', (job, done) => this.checkExpiredMutingsProcessorService.process(job, done)); - this.queueService.systemQueue.process('clean', (job, done) => this.cleanProcessorService.process(job, done)); + //#region object storage + this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => { + switch (job.name) { + case 'deleteFile': return this.deleteFileProcessorService.process(job); + case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job); + default: throw new Error(`unrecognized job type ${job.name} for objectStorage`); + } + }, { + ...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE), + autorun: false, + concurrency: 16, + }); + + const objectStorageLogger = this.logger.createSubLogger('objectStorage'); + + this.objectStorageQueueWorker + .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => objectStorageLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`)); + //#endregion + + //#region ended poll notification + this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => this.endedPollNotificationProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION), + autorun: false, + }); + //#endregion + } + + @bindThis + public async start(): Promise<void> { + await Promise.all([ + this.systemQueueWorker.run(), + this.dbQueueWorker.run(), + this.deliverQueueWorker.run(), + this.inboxQueueWorker.run(), + this.webhookDeliverQueueWorker.run(), + this.relationshipQueueWorker.run(), + this.objectStorageQueueWorker.run(), + this.endedPollNotificationQueueWorker.run(), + ]); + } + + @bindThis + public async stop(): Promise<void> { + await Promise.all([ + this.systemQueueWorker.close(), + this.dbQueueWorker.close(), + this.deliverQueueWorker.close(), + this.inboxQueueWorker.close(), + this.webhookDeliverQueueWorker.close(), + this.relationshipQueueWorker.close(), + this.objectStorageQueueWorker.close(), + this.endedPollNotificationQueueWorker.close(), + ]); + } + + @bindThis + public async onApplicationShutdown(signal?: string | undefined): Promise<void> { + await this.stop(); } } diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts new file mode 100644 index 000000000..d240fe70e --- /dev/null +++ b/packages/backend/src/queue/const.ts @@ -0,0 +1,26 @@ +import { Config } from '@/config.js'; +import type * as Bull from 'bullmq'; + +export const QUEUE = { + DELIVER: 'deliver', + INBOX: 'inbox', + SYSTEM: 'system', + ENDED_POLL_NOTIFICATION: 'endedPollNotification', + DB: 'db', + RELATIONSHIP: 'relationship', + OBJECT_STORAGE: 'objectStorage', + WEBHOOK_DELIVER: 'webhookDeliver', +}; + +export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions { + return { + connection: { + port: config.redisForJobQueue.port, + host: config.redisForJobQueue.host, + family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family, + password: config.redisForJobQueue.pass, + db: config.redisForJobQueue.db ?? 0, + }, + prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue:${queueName}` : `queue:${queueName}`, + }; +} diff --git a/packages/backend/src/queue/get-job-info.ts b/packages/backend/src/queue/get-job-info.ts deleted file mode 100644 index d33e349c3..000000000 --- a/packages/backend/src/queue/get-job-info.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Bull from 'bull'; - -export function getJobInfo(job: Bull.Job, increment = false) { - const age = Date.now() - job.timestamp; - - const formated = age > 60000 ? `${Math.floor(age / 1000 / 60)}m` - : age > 10000 ? `${Math.floor(age / 1000)}s` - : `${age}ms`; - - // onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする - const currentAttempts = job.attemptsMade + (increment ? 1 : 0); - const maxAttempts = job.opts ? job.opts.attempts : 0; - - return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`; -} diff --git a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts index e2720b4fe..600ce0828 100644 --- a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts +++ b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts @@ -9,7 +9,7 @@ import { deepClone } from '@/misc/clone.js'; import { IdService } from '@/core/IdService.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; @Injectable() export class AggregateRetentionProcessorService { @@ -32,7 +32,7 @@ export class AggregateRetentionProcessorService { } @bindThis - public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> { + public async process(): Promise<void> { this.logger.info('Aggregating retention...'); const now = new Date(); @@ -62,7 +62,6 @@ export class AggregateRetentionProcessorService { } catch (err) { if (isDuplicateKeyValueError(err)) { this.logger.succ('Skip because it has already been processed by another worker.'); - done(); return; } throw err; @@ -88,6 +87,5 @@ export class AggregateRetentionProcessorService { } this.logger.succ('Retention aggregated.'); - done(); } } diff --git a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts index 2476d71a5..c4ee212ba 100644 --- a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts @@ -7,7 +7,7 @@ import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { UserMutingService } from '@/core/UserMutingService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; @Injectable() export class CheckExpiredMutingsProcessorService { @@ -27,7 +27,7 @@ export class CheckExpiredMutingsProcessorService { } @bindThis - public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> { + public async process(): Promise<void> { this.logger.info('Checking expired mutings...'); const expired = await this.mutingsRepository.createQueryBuilder('muting') @@ -41,6 +41,5 @@ export class CheckExpiredMutingsProcessorService { } this.logger.succ('All expired mutings checked.'); - done(); } } diff --git a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts index b45816704..22d7c1b4f 100644 --- a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts @@ -16,7 +16,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; @Injectable() export class CleanChartsProcessorService { @@ -45,7 +45,7 @@ export class CleanChartsProcessorService { } @bindThis - public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> { + public async process(): Promise<void> { this.logger.info('Clean charts...'); await Promise.all([ @@ -64,6 +64,5 @@ export class CleanChartsProcessorService { ]); this.logger.succ('All charts successfully cleaned.'); - done(); } } diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index 1936e8df2..cefa6da5e 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -7,7 +7,7 @@ import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; @Injectable() export class CleanProcessorService { @@ -36,7 +36,7 @@ export class CleanProcessorService { } @bindThis - public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> { + public async process(): Promise<void> { this.logger.info('Cleaning...'); this.userIpsRepository.delete({ @@ -72,6 +72,5 @@ export class CleanProcessorService { } this.logger.succ('Cleaned.'); - done(); } } diff --git a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts index 5a33c2718..c54bf59ae 100644 --- a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts @@ -5,9 +5,9 @@ import type { DriveFilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; import { bindThis } from '@/decorators.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; @Injectable() export class CleanRemoteFilesProcessorService { @@ -27,7 +27,7 @@ export class CleanRemoteFilesProcessorService { } @bindThis - public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> { + public async process(job: Bull.Job<Record<string, unknown>>): Promise<void> { this.logger.info('Deleting cached remote files...'); let deletedCount = 0; @@ -47,7 +47,7 @@ export class CleanRemoteFilesProcessorService { }); if (files.length === 0) { - job.progress(100); + job.updateProgress(100); break; } @@ -62,10 +62,9 @@ export class CleanRemoteFilesProcessorService { isLink: false, }); - job.progress(deletedCount / total); + job.updateProgress(deletedCount / total); } this.logger.succ('All cached remote files has been deleted.'); - done(); } } diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts index e36a78de6..39dd801af 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -8,10 +8,10 @@ import { DriveService } from '@/core/DriveService.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Note } from '@/models/entities/Note.js'; import { EmailService } from '@/core/EmailService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; -import type { DbUserDeleteJobData } from '../types.js'; import { bindThis } from '@/decorators.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { DbUserDeleteJobData } from '../types.js'; @Injectable() export class DeleteAccountProcessorService { diff --git a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts index 604497cf5..6772c5dc7 100644 --- a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts @@ -5,10 +5,10 @@ import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; -import type { DbJobDataWithUser } from '../types.js'; import { bindThis } from '@/decorators.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { DbJobDataWithUser } from '../types.js'; @Injectable() export class DeleteDriveFilesProcessorService { @@ -31,12 +31,11 @@ export class DeleteDriveFilesProcessorService { } @bindThis - public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> { + public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> { this.logger.info(`Deleting drive files of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -56,7 +55,7 @@ export class DeleteDriveFilesProcessorService { }); if (files.length === 0) { - job.progress(100); + job.updateProgress(100); break; } @@ -71,10 +70,9 @@ export class DeleteDriveFilesProcessorService { userId: user.id, }); - job.progress(deletedCount / total); + job.updateProgress(deletedCount / total); } this.logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`); - done(); } } diff --git a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts index 2fb2f56f8..edf87bd92 100644 --- a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts @@ -3,10 +3,10 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; -import type { ObjectStorageFileJobData } from '../types.js'; import { bindThis } from '@/decorators.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { ObjectStorageFileJobData } from '../types.js'; @Injectable() export class DeleteFileProcessorService { diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index f293bd4d7..406e9df85 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import * as Bull from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, InstancesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; @@ -16,7 +17,6 @@ import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; import type { DeliverJobData } from '../types.js'; @Injectable() @@ -121,15 +121,13 @@ export class DeliverProcessorService { isSuspended: true, }); }); - return `${host} is gone`; + throw new Bull.UnrecoverableError(`${host} is gone`); } - // HTTPステータスコード4xxはクライアントエラーであり、それはつまり - // 何回再送しても成功することはないということなのでエラーにはしないでおく - return `${res.statusCode} ${res.statusMessage}`; + throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); } // 5xx etc. - throw `${res.statusCode} ${res.statusMessage}`; + throw new Error(`${res.statusCode} ${res.statusMessage}`); } else { // DNS error, socket error, timeout ... throw res; diff --git a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts index 501ed4090..21501592f 100644 --- a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts +++ b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts @@ -6,7 +6,7 @@ import type Logger from '@/logger.js'; import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { EndedPollNotificationJobData } from '../types.js'; @Injectable() @@ -30,10 +30,9 @@ export class EndedPollNotificationProcessorService { } @bindThis - public async process(job: Bull.Job<EndedPollNotificationJobData>, done: () => void): Promise<void> { + public async process(job: Bull.Job<EndedPollNotificationJobData>): Promise<void> { const note = await this.notesRepository.findOneBy({ id: job.data.noteId }); if (note == null || !note.hasPoll) { - done(); return; } @@ -51,7 +50,5 @@ export class EndedPollNotificationProcessorService { noteId: note.id, }); } - - done(); } } diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts index 894903e79..ac52325c8 100644 --- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts @@ -12,7 +12,7 @@ import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type { DBExportAntennasData } from '../types.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; @Injectable() export class ExportAntennasProcessorService { @@ -39,10 +39,9 @@ export class ExportAntennasProcessorService { } @bindThis - public async process(job: Bull.Job<DBExportAntennasData>, done: () => void): Promise<void> { + public async process(job: Bull.Job<DBExportAntennasData>): Promise<void> { const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } const [path, cleanup] = await createTemp(); @@ -96,7 +95,6 @@ export class ExportAntennasProcessorService { this.logger.succ('Exported to: ' + driveFile.id); } finally { cleanup(); - done(); } } } diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts index c7b54070d..eb758e162 100644 --- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts @@ -9,10 +9,10 @@ import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; -import type { DbJobDataWithUser } from '../types.js'; import { bindThis } from '@/decorators.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { DbJobDataWithUser } from '../types.js'; @Injectable() export class ExportBlockingProcessorService { @@ -36,12 +36,11 @@ export class ExportBlockingProcessorService { } @bindThis - public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> { + public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> { this.logger.info(`Exporting blocking of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -69,7 +68,7 @@ export class ExportBlockingProcessorService { }); if (blockings.length === 0) { - job.progress(100); + job.updateProgress(100); break; } @@ -99,7 +98,7 @@ export class ExportBlockingProcessorService { blockerId: user.id, }); - job.progress(exportedCount / total); + job.updateProgress(exportedCount / total); } stream.end(); @@ -112,7 +111,5 @@ export class ExportBlockingProcessorService { } finally { cleanup(); } - - done(); } } diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts index b50f373ef..3203d9f3e 100644 --- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts @@ -13,7 +13,7 @@ import { createTemp, createTempDir } from '@/misc/create-temp.js'; import { DownloadService } from '@/core/DownloadService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; @Injectable() export class ExportCustomEmojisProcessorService { @@ -37,12 +37,11 @@ export class ExportCustomEmojisProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise<void> { + public async process(job: Bull.Job): Promise<void> { this.logger.info('Exporting custom emojis ...'); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -117,24 +116,26 @@ export class ExportCustomEmojisProcessorService { metaStream.end(); // Create archive - const [archivePath, archiveCleanup] = await createTemp(); - const archiveStream = fs.createWriteStream(archivePath); - const archive = archiver('zip', { - zlib: { level: 0 }, - }); - archiveStream.on('close', async () => { - this.logger.succ(`Exported to: ${archivePath}`); + await new Promise<void>(async (resolve) => { + const [archivePath, archiveCleanup] = await createTemp(); + const archiveStream = fs.createWriteStream(archivePath); + const archive = archiver('zip', { + zlib: { level: 0 }, + }); + archiveStream.on('close', async () => { + this.logger.succ(`Exported to: ${archivePath}`); - const fileName = 'custom-emojis-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.zip'; - const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true }); + const fileName = 'custom-emojis-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.zip'; + const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true }); - this.logger.succ(`Exported to: ${driveFile.id}`); - cleanup(); - archiveCleanup(); - done(); + this.logger.succ(`Exported to: ${driveFile.id}`); + cleanup(); + archiveCleanup(); + resolve(); + }); + archive.pipe(archiveStream); + archive.directory(path, false); + archive.finalize(); }); - archive.pipe(archiveStream); - archive.directory(path, false); - archive.finalize(); } } diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts index f2f2383a8..76c38a6b8 100644 --- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts @@ -12,7 +12,7 @@ import type { Poll } from '@/models/entities/Poll.js'; import type { Note } from '@/models/entities/Note.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; @Injectable() @@ -42,12 +42,11 @@ export class ExportFavoritesProcessorService { } @bindThis - public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> { + public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> { this.logger.info(`Exporting favorites of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -91,7 +90,7 @@ export class ExportFavoritesProcessorService { }) as (NoteFavorite & { note: Note & { user: User } })[]; if (favorites.length === 0) { - job.progress(100); + job.updateProgress(100); break; } @@ -112,7 +111,7 @@ export class ExportFavoritesProcessorService { userId: user.id, }); - job.progress(exportedFavoritesCount / total); + job.updateProgress(exportedFavoritesCount / total); } await write(']'); @@ -127,8 +126,6 @@ export class ExportFavoritesProcessorService { } finally { cleanup(); } - - done(); } } diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts index fa9c1ac1e..8726cb140 100644 --- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts @@ -10,10 +10,10 @@ import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import type { Following } from '@/models/entities/Following.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; -import type { DbExportFollowingData } from '../types.js'; import { bindThis } from '@/decorators.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { DbExportFollowingData } from '../types.js'; @Injectable() export class ExportFollowingProcessorService { @@ -40,12 +40,11 @@ export class ExportFollowingProcessorService { } @bindThis - public async process(job: Bull.Job<DbExportFollowingData>, done: () => void): Promise<void> { + public async process(job: Bull.Job<DbExportFollowingData>): Promise<void> { this.logger.info(`Exporting following of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -116,7 +115,5 @@ export class ExportFollowingProcessorService { } finally { cleanup(); } - - done(); } } diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts index b14bf5f5b..0f11a9e84 100644 --- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts @@ -9,10 +9,10 @@ import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; -import type { DbJobDataWithUser } from '../types.js'; import { bindThis } from '@/decorators.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { DbJobDataWithUser } from '../types.js'; @Injectable() export class ExportMutingProcessorService { @@ -39,12 +39,11 @@ export class ExportMutingProcessorService { } @bindThis - public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> { + public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> { this.logger.info(`Exporting muting of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -73,7 +72,7 @@ export class ExportMutingProcessorService { }); if (mutes.length === 0) { - job.progress(100); + job.updateProgress(100); break; } @@ -103,7 +102,7 @@ export class ExportMutingProcessorService { muterId: user.id, }); - job.progress(exportedCount / total); + job.updateProgress(exportedCount / total); } stream.end(); @@ -116,7 +115,5 @@ export class ExportMutingProcessorService { } finally { cleanup(); } - - done(); } } diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts index e4f12ad10..24fb33188 100644 --- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -12,7 +12,7 @@ import type { Poll } from '@/models/entities/Poll.js'; import type { Note } from '@/models/entities/Note.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; @Injectable() @@ -39,12 +39,11 @@ export class ExportNotesProcessorService { } @bindThis - public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> { + public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> { this.logger.info(`Exporting notes of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -87,7 +86,7 @@ export class ExportNotesProcessorService { }) as Note[]; if (notes.length === 0) { - job.progress(100); + job.updateProgress(100); break; } @@ -108,7 +107,7 @@ export class ExportNotesProcessorService { userId: user.id, }); - job.progress(exportedNotesCount / total); + job.updateProgress(exportedNotesCount / total); } await write(']'); @@ -123,8 +122,6 @@ export class ExportNotesProcessorService { } finally { cleanup(); } - - done(); } } diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts index 54bde4404..ec6335805 100644 --- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts @@ -9,10 +9,10 @@ import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; -import type { DbJobDataWithUser } from '../types.js'; import { bindThis } from '@/decorators.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { DbJobDataWithUser } from '../types.js'; @Injectable() export class ExportUserListsProcessorService { @@ -39,12 +39,11 @@ export class ExportUserListsProcessorService { } @bindThis - public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> { + public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> { this.logger.info(`Exporting user lists of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -92,7 +91,5 @@ export class ExportUserListsProcessorService { } finally { cleanup(); } - - done(); } } diff --git a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts index d06131b8c..575cad69d 100644 --- a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts @@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import { DBAntennaImportJobData } from '../types.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; const validate = new Ajv().compile({ type: 'object', @@ -59,7 +59,7 @@ export class ImportAntennasProcessorService { } @bindThis - public async process(job: Bull.Job<DBAntennaImportJobData>, done: () => void): Promise<void> { + public async process(job: Bull.Job<DBAntennaImportJobData>): Promise<void> { const now = new Date(); try { for (const antenna of job.data.antenna) { @@ -89,8 +89,6 @@ export class ImportAntennasProcessorService { } } catch (err: any) { this.logger.error(err); - } finally { - done(); } } } diff --git a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts index 3f075b02d..2f1a9e5b0 100644 --- a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts @@ -7,11 +7,11 @@ import * as Acct from '@/misc/acct.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { DownloadService } from '@/core/DownloadService.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; -import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js'; import { bindThis } from '@/decorators.js'; import { QueueService } from '@/core/QueueService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js'; @Injectable() export class ImportBlockingProcessorService { @@ -34,12 +34,11 @@ export class ImportBlockingProcessorService { } @bindThis - public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> { + public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> { this.logger.info(`Importing blocking of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -47,7 +46,6 @@ export class ImportBlockingProcessorService { id: job.data.fileId, }); if (file == null) { - done(); return; } @@ -56,7 +54,6 @@ export class ImportBlockingProcessorService { this.queueService.createImportBlockingToDbJob({ id: user.id }, targets); this.logger.succ('Import jobs created'); - done(); } @bindThis @@ -85,7 +82,7 @@ export class ImportBlockingProcessorService { } if (target == null) { - throw `Unable to resolve user: @${username}@${host}`; + throw new Error(`Unable to resolve user: @${username}@${host}`); } // skip myself diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index cf78d8330..d86256787 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -12,7 +12,7 @@ import { DriveService } from '@/core/DriveService.js'; import { DownloadService } from '@/core/DownloadService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { DbUserImportJobData } from '../types.js'; // TODO: 名前衝突時の動作を選べるようにする @@ -45,14 +45,13 @@ export class ImportCustomEmojisProcessorService { } @bindThis - public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> { + public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> { this.logger.info('Importing custom emojis ...'); const file = await this.driveFilesRepository.findOneBy({ id: job.data.fileId, }); if (file == null) { - done(); return; } @@ -107,13 +106,15 @@ export class ImportCustomEmojisProcessorService { aliases: emojiInfo.aliases, driveFile, license: emojiInfo.license, + isSensitive: emojiInfo.isSensitive, + localOnly: emojiInfo.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: [], }); } cleanup(); this.logger.succ('Imported'); - done(); }); unzipStream.pipe(extractor); this.logger.succ(`Unzipping to ${outputPath}`); diff --git a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts index aa5cf12c5..15bee9672 100644 --- a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts @@ -7,11 +7,11 @@ import * as Acct from '@/misc/acct.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { DownloadService } from '@/core/DownloadService.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; -import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js'; import { bindThis } from '@/decorators.js'; import { QueueService } from '@/core/QueueService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js'; @Injectable() export class ImportFollowingProcessorService { @@ -34,12 +34,11 @@ export class ImportFollowingProcessorService { } @bindThis - public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> { + public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> { this.logger.info(`Importing following of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -47,7 +46,6 @@ export class ImportFollowingProcessorService { id: job.data.fileId, }); if (file == null) { - done(); return; } @@ -56,7 +54,6 @@ export class ImportFollowingProcessorService { this.queueService.createImportFollowingToDbJob({ id: user.id }, targets); this.logger.succ('Import jobs created'); - done(); } @bindThis @@ -85,7 +82,7 @@ export class ImportFollowingProcessorService { } if (target == null) { - throw `Unable to resolve user: @${username}@${host}`; + throw new Error(`Unable to resolve user: @${username}@${host}`); } // skip myself diff --git a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts index 379994ee7..723935cd3 100644 --- a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts @@ -9,10 +9,10 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { DownloadService } from '@/core/DownloadService.js'; import { UserMutingService } from '@/core/UserMutingService.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; -import type { DbUserImportJobData } from '../types.js'; import { bindThis } from '@/decorators.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { DbUserImportJobData } from '../types.js'; @Injectable() export class ImportMutingProcessorService { @@ -38,12 +38,11 @@ export class ImportMutingProcessorService { } @bindThis - public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> { + public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> { this.logger.info(`Importing muting of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -51,7 +50,6 @@ export class ImportMutingProcessorService { id: job.data.fileId, }); if (file == null) { - done(); return; } @@ -83,7 +81,7 @@ export class ImportMutingProcessorService { } if (target == null) { - throw `cannot resolve user: @${username}@${host}`; + throw new Error(`cannot resolve user: @${username}@${host}`); } // skip myself @@ -98,6 +96,5 @@ export class ImportMutingProcessorService { } this.logger.succ('Imported'); - done(); } } diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts index c42386341..824ee8157 100644 --- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts @@ -12,7 +12,7 @@ import { IdService } from '@/core/IdService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { DbUserImportJobData } from '../types.js'; @Injectable() @@ -46,12 +46,11 @@ export class ImportUserListsProcessorService { } @bindThis - public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> { + public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> { this.logger.info(`Importing user lists of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -59,7 +58,6 @@ export class ImportUserListsProcessorService { id: job.data.fileId, }); if (file == null) { - done(); return; } @@ -109,6 +107,5 @@ export class ImportUserListsProcessorService { } this.logger.succ('Imported'); - done(); } } diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index ab8b1e9e2..ce1d7aaa1 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -1,8 +1,8 @@ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import httpSignature from '@peertube/http-signature'; +import * as Bull from 'bullmq'; import { DI } from '@/di-symbols.js'; -import type { InstancesRepository, DriveFilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { MetaService } from '@/core/MetaService.js'; @@ -23,10 +23,8 @@ import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js'; import { ApInboxService } from '@/core/activitypub/ApInboxService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; import type { InboxJobData } from '../types.js'; -// ユーザーのinboxにアクティビティが届いた時の処理 @Injectable() export class InboxProcessorService { private logger: Logger; @@ -35,12 +33,6 @@ export class InboxProcessorService { @Inject(DI.config) private config: Config, - @Inject(DI.instancesRepository) - private instancesRepository: InstancesRepository, - - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - private utilityService: UtilityService, private metaService: MetaService, private apInboxService: ApInboxService, @@ -93,24 +85,24 @@ export class InboxProcessorService { try { authUser = await this.apDbResolverService.getAuthUserFromApId(getApId(activity.actor)); } catch (err) { - // 対象が4xxならスキップ + // 対象が4xxならスキップ if (err instanceof StatusError) { if (err.isClientError) { - return `skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`; + throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`); } - throw `Error in actor ${activity.actor} - ${err.statusCode ?? err}`; + throw new Error(`Error in actor ${activity.actor} - ${err.statusCode ?? err}`); } } } // それでもわからなければ終了 if (authUser == null) { - return 'skip: failed to resolve user'; + throw new Bull.UnrecoverableError('skip: failed to resolve user'); } // publicKey がなくても終了 if (authUser.key == null) { - return 'skip: failed to resolve user publicKey'; + throw new Bull.UnrecoverableError('skip: failed to resolve user publicKey'); } // HTTP-Signatureの検証 @@ -118,10 +110,10 @@ export class InboxProcessorService { // また、signatureのsignerは、activity.actorと一致する必要がある if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { - // 一致しなくても、でもLD-Signatureがありそうならそっちも見る + // 一致しなくても、でもLD-Signatureがありそうならそっちも見る if (activity.signature) { if (activity.signature.type !== 'RsaSignature2017') { - return `skip: unsupported LD-signature type ${activity.signature.type}`; + throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`); } // activity.signature.creator: https://example.oom/users/user#main-key @@ -134,32 +126,32 @@ export class InboxProcessorService { // keyIdからLD-Signatureのユーザーを取得 authUser = await this.apDbResolverService.getAuthUserFromKeyId(activity.signature.creator); if (authUser == null) { - return 'skip: LD-Signatureのユーザーが取得できませんでした'; + throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした'); } if (authUser.key == null) { - return 'skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした'; + throw new Bull.UnrecoverableError('skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした'); } // LD-Signature検証 const ldSignature = this.ldSignatureService.use(); const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false); if (!verified) { - return 'skip: LD-Signatureの検証に失敗しました'; + throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました'); } // もう一度actorチェック if (authUser.user.uri !== activity.actor) { - return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`; + throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`); } // ブロックしてたら中断 const ldHost = this.utilityService.extractDbHost(authUser.user.uri); if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) { - return `Blocked request: ${ldHost}`; + throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`); } } else { - return `skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`; + throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`); } } @@ -168,7 +160,7 @@ export class InboxProcessorService { const signerHost = this.utilityService.extractDbHost(authUser.user.uri!); const activityIdHost = this.utilityService.extractDbHost(activity.id); if (signerHost !== activityIdHost) { - return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`; + throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`); } } diff --git a/packages/backend/src/queue/processors/RelationshipProcessorService.ts b/packages/backend/src/queue/processors/RelationshipProcessorService.ts index ff454df45..722260d94 100644 --- a/packages/backend/src/queue/processors/RelationshipProcessorService.ts +++ b/packages/backend/src/queue/processors/RelationshipProcessorService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; diff --git a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts index e5840f3da..eab8e1e68 100644 --- a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts @@ -15,7 +15,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; @Injectable() export class ResyncChartsProcessorService { @@ -43,7 +43,7 @@ export class ResyncChartsProcessorService { } @bindThis - public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> { + public async process(): Promise<void> { this.logger.info('Resync charts...'); // TODO: ユーザーごとのチャートも更新する @@ -55,6 +55,5 @@ export class ResyncChartsProcessorService { ]); this.logger.succ('All charts successfully resynced.'); - done(); } } diff --git a/packages/backend/src/queue/processors/TickChartsProcessorService.ts b/packages/backend/src/queue/processors/TickChartsProcessorService.ts index 7ff84c15a..f1696bf56 100644 --- a/packages/backend/src/queue/processors/TickChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/TickChartsProcessorService.ts @@ -16,7 +16,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; @Injectable() export class TickChartsProcessorService { @@ -45,7 +45,7 @@ export class TickChartsProcessorService { } @bindThis - public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> { + public async process(): Promise<void> { this.logger.info('Tick charts...'); await Promise.all([ @@ -64,6 +64,5 @@ export class TickChartsProcessorService { ]); this.logger.succ('All charts successfully ticked.'); - done(); } } diff --git a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts index 84a5c21c4..8b40c1674 100644 --- a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import * as Bull from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { WebhooksRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; @@ -7,7 +8,6 @@ import { HttpRequestService } from '@/core/HttpRequestService.js'; import { StatusError } from '@/misc/status-error.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; import type { WebhookDeliverJobData } from '../types.js'; @Injectable() @@ -66,11 +66,11 @@ export class WebhookDeliverProcessorService { if (res instanceof StatusError) { // 4xx if (res.isClientError) { - return `${res.statusCode} ${res.statusMessage}`; + throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); } // 5xx etc. - throw `${res.statusCode} ${res.statusMessage}`; + throw new Error(`${res.statusCode} ${res.statusMessage}`); } else { // DNS error, socket error, timeout ... throw res; diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index e675d9cf1..455acd1e4 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -585,7 +585,7 @@ export class ActivityPubServerService { name: request.params.emoji, }); - if (emoji == null) { + if (emoji == null || emoji.localOnly) { reply.code(404); return; } diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 9257fee13..c3d45e4ad 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -194,7 +194,7 @@ export class ServerService implements OnApplicationShutdown { fastify.register(this.clientServerService.createServer); - this.streamingApiServerService.attachStreamingApi(fastify.server); + this.streamingApiServerService.attach(fastify.server); fastify.server.on('error', err => { switch ((err as any).code) { @@ -222,7 +222,14 @@ export class ServerService implements OnApplicationShutdown { await fastify.ready(); } - async onApplicationShutdown(signal: string): Promise<void> { + @bindThis + public async dispose(): Promise<void> { + await this.streamingApiServerService.detach(); await this.#fastify.close(); } + + @bindThis + async onApplicationShutdown(signal: string): Promise<void> { + await this.dispose(); + } } diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index e3483c82c..dad1a4132 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -359,7 +359,12 @@ export class ApiCallService implements OnApplicationShutdown { } @bindThis - public onApplicationShutdown(signal?: string | undefined) { + public dispose(): void { clearInterval(this.userIpHistoriesClearIntervalId); } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index 6548c475b..e23591d87 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -36,7 +36,7 @@ export class AuthenticateService { } @bindThis - public async authenticate(token: string | null | undefined): Promise<[LocalUser | null | undefined, AccessToken | null | undefined]> { + public async authenticate(token: string | null | undefined): Promise<[LocalUser | null, AccessToken | null]> { if (token == null) { return [null, null]; } diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index ee1aae5b6..1e32e9988 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -321,6 +321,9 @@ import * as ep___users_lists_pull from './endpoints/users/lists/pull.js'; import * as ep___users_lists_push from './endpoints/users/lists/push.js'; import * as ep___users_lists_show from './endpoints/users/lists/show.js'; import * as ep___users_lists_update from './endpoints/users/lists/update.js'; +import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js'; +import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js'; +import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js'; import * as ep___users_notes from './endpoints/users/notes.js'; import * as ep___users_pages from './endpoints/users/pages.js'; import * as ep___users_reactions from './endpoints/users/reactions.js'; @@ -659,6 +662,9 @@ const $users_lists_pull: Provider = { provide: 'ep:users/lists/pull', useClass: const $users_lists_push: Provider = { provide: 'ep:users/lists/push', useClass: ep___users_lists_push.default }; const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass: ep___users_lists_show.default }; const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default }; +const $users_lists_favorite: Provider = { provide: 'ep:users/lists/favorite', useClass: ep___users_lists_favorite.default }; +const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite', useClass: ep___users_lists_unfavorite.default }; +const $users_lists_create_from_public: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_create_from_public.default }; const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default }; const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default }; const $users_reactions: Provider = { provide: 'ep:users/reactions', useClass: ep___users_reactions.default }; @@ -1001,6 +1007,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_lists_push, $users_lists_show, $users_lists_update, + $users_lists_favorite, + $users_lists_unfavorite, + $users_lists_create_from_public, $users_notes, $users_pages, $users_reactions, @@ -1335,6 +1344,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_lists_push, $users_lists_show, $users_lists_update, + $users_lists_favorite, + $users_lists_unfavorite, + $users_lists_create_from_public, $users_notes, $users_pages, $users_reactions, diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index 258e8de03..893dfe956 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -1,23 +1,27 @@ import { EventEmitter } from 'events'; import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import * as websocket from 'websocket'; +import * as WebSocket from 'ws'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, RenoteMutingsRepository } from '@/models/index.js'; +import type { UsersRepository, AccessToken } from '@/models/index.js'; import type { Config } from '@/config.js'; import { NoteReadService } from '@/core/NoteReadService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; -import { AuthenticateService } from './AuthenticateService.js'; +import { LocalUser } from '@/models/entities/User'; +import { AuthenticateService, AuthenticationError } from './AuthenticateService.js'; import MainStreamConnection from './stream/index.js'; import { ChannelsService } from './stream/ChannelsService.js'; -import type { ParsedUrlQuery } from 'querystring'; import type * as http from 'node:http'; @Injectable() export class StreamingApiServerService { + #wss: WebSocket.WebSocketServer; + #connections = new Map<WebSocket.WebSocket, number>(); + #cleanConnectionsIntervalId: NodeJS.Timeout | null = null; + constructor( @Inject(DI.config) private config: Config, @@ -28,24 +32,6 @@ export class StreamingApiServerService { @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - - @Inject(DI.mutingsRepository) - private mutingsRepository: MutingsRepository, - - @Inject(DI.renoteMutingsRepository) - private renoteMutingsRepository: RenoteMutingsRepository, - - @Inject(DI.blockingsRepository) - private blockingsRepository: BlockingsRepository, - - @Inject(DI.channelFollowingsRepository) - private channelFollowingsRepository: ChannelFollowingsRepository, - - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - private cacheService: CacheService, private noteReadService: NoteReadService, private authenticateService: AuthenticateService, @@ -55,25 +41,65 @@ export class StreamingApiServerService { } @bindThis - public attachStreamingApi(server: http.Server) { - // Init websocket server - const ws = new websocket.server({ - httpServer: server, + public attach(server: http.Server): void { + this.#wss = new WebSocket.WebSocketServer({ + noServer: true, }); - ws.on('request', async (request) => { - const q = request.resourceURL.query as ParsedUrlQuery; - - // TODO: トークンが間違ってるなどしてauthenticateに失敗したら - // コネクション切断するなりエラーメッセージ返すなりする - // (現状はエラーがキャッチされておらずサーバーのログに流れて邪魔なので) - const [user, miapp] = await this.authenticateService.authenticate(q.i as string); - - if (user?.isSuspended) { - request.reject(400); + server.on('upgrade', async (request, socket, head) => { + if (request.url == null) { + socket.write('HTTP/1.1 400 Bad Request\r\n\r\n'); + socket.destroy(); return; } + const q = new URL(request.url, `http://${request.headers.host}`).searchParams; + + let user: LocalUser | null = null; + let app: AccessToken | null = null; + + try { + [user, app] = await this.authenticateService.authenticate(q.get('i')); + } catch (e) { + if (e instanceof AuthenticationError) { + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + } else { + socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n'); + } + socket.destroy(); + return; + } + + if (user?.isSuspended) { + socket.write('HTTP/1.1 403 Forbidden\r\n\r\n'); + socket.destroy(); + return; + } + + const stream = new MainStreamConnection( + this.channelsService, + this.noteReadService, + this.notificationService, + this.cacheService, + user, app, + ); + + await stream.init(); + + this.#wss.handleUpgrade(request, socket, head, (ws) => { + this.#wss.emit('connection', ws, request, { + stream, user, app, + }); + }); + }); + + this.#wss.on('connection', async (connection: WebSocket.WebSocket, request: http.IncomingMessage, ctx: { + stream: MainStreamConnection, + user: LocalUser | null; + app: AccessToken | null + }) => { + const { stream, user, app } = ctx; + const ev = new EventEmitter(); async function onRedisMessage(_: string, data: string): Promise<void> { @@ -83,21 +109,11 @@ export class StreamingApiServerService { this.redisForSub.on('message', onRedisMessage); - const main = new MainStreamConnection( - this.channelsService, - this.noteReadService, - this.notificationService, - this.cacheService, - ev, user, miapp, - ); + await stream.listen(ev, connection); - await main.init(); + this.#connections.set(connection, Date.now()); - const connection = request.accept(); - - main.init2(connection); - - const intervalId = user ? setInterval(() => { + const userUpdateIntervalId = user ? setInterval(() => { this.usersRepository.update(user.id, { lastActiveDate: new Date(), }); @@ -110,16 +126,38 @@ export class StreamingApiServerService { connection.once('close', () => { ev.removeAllListeners(); - main.dispose(); + stream.dispose(); this.redisForSub.off('message', onRedisMessage); - if (intervalId) clearInterval(intervalId); + if (userUpdateIntervalId) clearInterval(userUpdateIntervalId); }); connection.on('message', async (data) => { - if (data.type === 'utf8' && data.utf8Data === 'ping') { + this.#connections.set(connection, Date.now()); + if (data.toString() === 'ping') { connection.send('pong'); } }); }); + + this.#cleanConnectionsIntervalId = setInterval(() => { + const now = Date.now(); + for (const [connection, lastActive] of this.#connections.entries()) { + if (now - lastActive > 1000 * 60 * 5) { + connection.terminate(); + this.#connections.delete(connection); + } + } + }, 1000 * 60 * 5); + } + + @bindThis + public detach(): Promise<void> { + if (this.#cleanConnectionsIntervalId) { + clearInterval(this.#cleanConnectionsIntervalId); + this.#cleanConnectionsIntervalId = null; + } + return new Promise((resolve) => { + this.#wss.close(() => resolve()); + }); } } diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 09bd7cbff..7e678a640 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -320,6 +320,9 @@ import * as ep___users_lists_list from './endpoints/users/lists/list.js'; import * as ep___users_lists_pull from './endpoints/users/lists/pull.js'; import * as ep___users_lists_push from './endpoints/users/lists/push.js'; import * as ep___users_lists_show from './endpoints/users/lists/show.js'; +import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js'; +import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js'; +import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js'; import * as ep___users_lists_update from './endpoints/users/lists/update.js'; import * as ep___users_notes from './endpoints/users/notes.js'; import * as ep___users_pages from './endpoints/users/pages.js'; @@ -656,7 +659,10 @@ const eps = [ ['users/lists/pull', ep___users_lists_pull], ['users/lists/push', ep___users_lists_push], ['users/lists/show', ep___users_lists_show], + ['users/lists/favorite', ep___users_lists_favorite], + ['users/lists/unfavorite', ep___users_lists_unfavorite], ['users/lists/update', ep___users_lists_update], + ['users/lists/create-from-public', ep___users_lists_create_from_public], ['users/notes', ep___users_notes], ['users/pages', ep___users_pages], ['users/reactions', ep___users_reactions], diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts index 2393c2441..12db1f78f 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts @@ -25,7 +25,7 @@ export const paramDef = { id: { type: 'string', format: 'misskey:id' }, title: { type: 'string', minLength: 1 }, text: { type: 'string', minLength: 1 }, - imageUrl: { type: 'string', nullable: true, minLength: 1 }, + imageUrl: { type: 'string', nullable: true, minLength: 0 }, }, required: ['id', 'title', 'text', 'imageUrl'], } as const; @@ -46,7 +46,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { updatedAt: new Date(), title: ps.title, text: ps.text, - imageUrl: ps.imageUrl, + /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */ + imageUrl: ps.imageUrl || null, }); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 2fb3e489e..509224e9c 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -25,9 +25,24 @@ export const meta = { export const paramDef = { type: 'object', properties: { + name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, fileId: { type: 'string', format: 'misskey:id' }, + category: { + type: 'string', + nullable: true, + description: 'Use `null` to reset the category.', + }, + aliases: { type: 'array', items: { + type: 'string', + } }, + license: { type: 'string', nullable: true }, + isSensitive: { type: 'boolean' }, + localOnly: { type: 'boolean' }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { + type: 'string', + } }, }, - required: ['fileId'], + required: ['name', 'fileId'], } as const; // TODO: ロジックをサービスに切り出す @@ -45,18 +60,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { ) { super(meta, paramDef, async (ps, me) => { const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); - if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); - const name = driveFile.name.split('.')[0].match(/^[a-z0-9_]+$/) ? driveFile.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`; - const emoji = await this.customEmojiService.add({ driveFile, - name, - category: null, - aliases: [], + name: ps.name, + category: ps.category ?? null, + aliases: ps.aliases ?? [], host: null, - license: null, + license: ps.license ?? null, + isSensitive: ps.isSensitive ?? false, + localOnly: ps.localOnly ?? false, + roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [], }); this.moderationLogService.insertModerationLog(me, 'addEmoji', { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index f63348b60..fb22bdc47 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -1,6 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import type { DriveFilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -15,6 +17,11 @@ export const meta = { code: 'NO_SUCH_EMOJI', id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8', }, + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: '14fb9fd9-0731-4e2f-aeb9-f09e4740333d', + }, sameNameEmojiExists: { message: 'Emoji that have same name already exists.', code: 'SAME_NAME_EMOJI_EXISTS', @@ -28,6 +35,7 @@ export const paramDef = { properties: { id: { type: 'string', format: 'misskey:id' }, name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, + fileId: { type: 'string', format: 'misskey:id' }, category: { type: 'string', nullable: true, @@ -37,6 +45,11 @@ export const paramDef = { type: 'string', } }, license: { type: 'string', nullable: true }, + isSensitive: { type: 'boolean' }, + localOnly: { type: 'boolean' }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { + type: 'string', + } }, }, required: ['id', 'name', 'aliases'], } as const; @@ -45,14 +58,28 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { + let driveFile; + + if (ps.fileId) { + driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); + if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); + } + await this.customEmojiService.update(ps.id, { + driveFile, name: ps.name, category: ps.category ?? null, aliases: ps.aliases, license: ps.license ?? null, + isSensitive: ps.isSensitive, + localOnly: ps.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, }); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts index f12738bd3..f2d4aa899 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts @@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { ) { super(meta, paramDef, async (ps, me) => { try { - if (new URL(ps.inbox).protocol !== 'https:') throw 'https only'; + if (new URL(ps.inbox).protocol !== 'https:') throw new Error('https only'); } catch { throw new ApiError(meta.errors.invalidUrl); } diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index dca0f443b..e756a9b51 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -113,6 +113,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } this.antennasRepository.update(antenna.id, { + isActive: true, lastUsedAt: new Date(), }); diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts index cb2e661bf..05842460c 100644 --- a/packages/backend/src/server/api/endpoints/auth/accept.ts +++ b/packages/backend/src/server/api/endpoints/auth/accept.ts @@ -55,7 +55,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { throw new ApiError(meta.errors.noSuchSession); } - // Generate access token const accessToken = secureRndstr(32, true); // Fetch exist access token @@ -65,7 +64,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { }); if (exist == null) { - // Lookup app const app = await this.appsRepository.findOneByOrFail({ id: session.appId }); // Generate Hash @@ -75,7 +73,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { const now = new Date(); - // Insert access token doc await this.accessTokensRepository.insert({ id: this.idService.genId(), createdAt: now, diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index ad33398da..e8985a9cd 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -1,6 +1,6 @@ import { promisify } from 'node:util'; import bcrypt from 'bcryptjs'; -import * as cbor from 'cbor'; +import cbor from 'cbor'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index 3361e5a4d..48fb03a8a 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -26,7 +26,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { ) { super(meta, paramDef, async (ps, me) => { const query = this.accessTokensRepository.createQueryBuilder('token') - .where('token.userId = :userId', { userId: me.id }); + .where('token.userId = :userId', { userId: me.id }) + .leftJoinAndSelect('token.app', 'app'); switch (ps.sort) { case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break; @@ -40,7 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { return await Promise.all(tokens.map(token => ({ id: token.id, - name: token.name, + name: token.name ?? token.app?.name, createdAt: token.createdAt, lastUsedAt: token.lastUsedAt, permission: token.permission, diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index e141be764..f5662f4a0 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -91,18 +91,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; - const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 const notificationsRes = await this.redisClient.xrevrange( `notificationTimeline:${me.id}`, ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+', - '-', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-', 'COUNT', limit); if (notificationsRes.length === 0) { return []; } - let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId) as Notification[]; + let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as Notification[]; if (includeTypes && includeTypes.length > 0) { notifications = notifications.filter(notification => includeTypes.includes(notification.type)); diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 74be00a8b..8f5e6177c 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -141,13 +141,12 @@ export const paramDef = { preventAiLearning: { type: 'boolean' }, isBot: { type: 'boolean' }, isCat: { type: 'boolean' }, - showTimelineReplies: { type: 'boolean' }, injectFeaturedNote: { type: 'boolean' }, receiveAnnouncementEmail: { type: 'boolean' }, alwaysMarkNsfw: { type: 'boolean' }, autoSensitive: { type: 'boolean' }, ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, - pinnedPageId: { type: 'string', format: 'misskey:id' }, + pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true }, mutedWords: { type: 'array' }, mutedInstances: { type: 'array', items: { type: 'string', @@ -239,7 +238,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus; if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions; if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot; - if (typeof ps.showTimelineReplies === 'boolean') updates.showTimelineReplies = ps.showTimelineReplies; if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 584ea07c3..53d724a9d 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -1,5 +1,6 @@ import { IsNull, LessThanOrEqual, MoreThan } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; +import * as JSON5 from 'json5'; import type { AdsRepository, UsersRepository } from '@/models/index.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -292,8 +293,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, - defaultLightTheme: instance.defaultLightTheme, - defaultDarkTheme: instance.defaultDarkTheme, + // クライアントの手間を減らすためあらかじめJSONに変換しておく + defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null, + defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null, ads: ads.map(ad => ({ id: ad.id, url: ad.url, diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 3f7f2cdec..96be5ed84 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -99,7 +99,7 @@ export const paramDef = { } }, cw: { type: 'string', nullable: true, maxLength: 100 }, localOnly: { type: 'boolean', default: false }, - reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote'], default: null }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, noExtractMentions: { type: 'boolean', default: false }, noExtractHashtags: { type: 'boolean', default: false }, noExtractEmojis: { type: 'boolean', default: false }, diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index c11c1eac4..88c1ca7f5 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -34,11 +34,8 @@ export const meta = { export const paramDef = { type: 'object', properties: { - withFiles: { - type: 'boolean', - default: false, - description: 'Only show notes that have attached files.', - }, + withFiles: { type: 'boolean', default: false }, + withReplies: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -78,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - this.queryService.generateRepliesQuery(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); if (me) { this.queryService.generateMutedUserQuery(query, me); this.queryService.generateMutedNoteQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 89abd91c7..7a3581e6e 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -46,11 +46,8 @@ export const paramDef = { includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, - withFiles: { - type: 'boolean', - default: false, - description: 'Only show notes that have attached files.', - }, + withFiles: { type: 'boolean', default: false }, + withReplies: { type: 'boolean', default: false }, }, required: [], } as const; @@ -98,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { .setParameters(followingQuery.getParameters()); this.queryService.generateChannelQuery(query, me); - this.queryService.generateRepliesQuery(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); this.queryService.generateVisibilityQuery(query, me); this.queryService.generateMutedUserQuery(query, me); this.queryService.generateMutedNoteQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index afdafc7c5..2ee549232 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -36,11 +36,8 @@ export const meta = { export const paramDef = { type: 'object', properties: { - withFiles: { - type: 'boolean', - default: false, - description: 'Only show notes that have attached files.', - }, + withFiles: { type: 'boolean', default: false }, + withReplies: { type: 'boolean', default: false }, fileType: { type: 'array', items: { type: 'string', } }, @@ -86,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateChannelQuery(query, me); - this.queryService.generateRepliesQuery(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); this.queryService.generateVisibilityQuery(query, me); if (me) this.queryService.generateMutedUserQuery(query, me); if (me) this.queryService.generateMutedNoteQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index 2956bf1cb..742df0ca9 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -82,14 +82,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { try { if (ps.tag) { - if (!safeForSql(normalizeForSearch(ps.tag))) throw 'Injection'; + if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection'); query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`); } else { query.andWhere(new Brackets(qb => { for (const tags of ps.query!) { qb.orWhere(new Brackets(qb => { for (const tag of tags) { - if (!safeForSql(normalizeForSearch(tag))) throw 'Injection'; + if (!safeForSql(normalizeForSearch(tag))) throw new Error('Injection'); qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`); } })); diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index c6ee1e5c2..e1f286439 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -35,11 +35,8 @@ export const paramDef = { includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, - withFiles: { - type: 'boolean', - default: false, - description: 'Only show notes that have attached files.', - }, + withFiles: { type: 'boolean', default: false }, + withReplies: { type: 'boolean', default: false }, }, required: [], } as const; @@ -84,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } this.queryService.generateChannelQuery(query, me); - this.queryService.generateRepliesQuery(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); this.queryService.generateVisibilityQuery(query, me); this.queryService.generateMutedUserQuery(query, me); this.queryService.generateMutedNoteQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index 4ced6d3ff..1d4825f81 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -34,7 +34,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { private redisClient: Redis.Redis, ) { super(meta, paramDef, async (ps, me) => { - if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test'; + if (process.env.NODE_ENV !== 'test') throw new Error('NODE_ENV is not a test'); await redisClient.flushdb(); await resetDb(this.db); diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index 6202c740f..42e36cb04 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -93,6 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { const query = this.notesRepository.createQueryBuilder('note') .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .andWhere('(note.visibility = \'public\')') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts new file mode 100644 index 000000000..8591e4ab9 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts @@ -0,0 +1,148 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import type { UserList } from '@/models/entities/UserList.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { RoleService } from '@/core/RoleService.js'; +import { UserListService } from '@/core/UserListService.js'; + +export const meta = { + requireCredential: true, + prohibitMoved: true, + res: { + type: 'object', + optional: false, nullable: false, + ref: 'UserList', + }, + + errors: { + tooManyUserLists: { + message: 'You cannot create user list any more.', + code: 'TOO_MANY_USERLISTS', + id: 'e9c105b2-c595-47de-97fb-7f7c2c33e92f', + }, + noSuchList: { + message: 'No such list.', + code: 'NO_SUCH_LIST', + id: '9292f798-6175-4f7d-93f4-b6742279667d', + }, + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '13c457db-a8cb-4d88-b70a-211ceeeabb5f', + }, + + alreadyAdded: { + message: 'That user has already been added to that list.', + code: 'ALREADY_ADDED', + id: 'c3ad6fdb-692b-47ee-a455-7bd12c7af615', + }, + + youHaveBeenBlocked: { + message: 'You cannot push this user because you have been blocked by this user.', + code: 'YOU_HAVE_BEEN_BLOCKED', + id: 'a2497f2a-2389-439c-8626-5298540530f4', + }, + + tooManyUsers: { + message: 'You can not push users any more.', + code: 'TOO_MANY_USERS', + id: '1845ea77-38d1-426e-8e4e-8b83b24f5bd7', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { type: 'string', minLength: 1, maxLength: 100 }, + listId: { type: 'string', format: 'misskey:id' }, + }, + required: ['name', 'listId'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + private userListService: UserListService, + private userListEntityService: UserListEntityService, + private idService: IdService, + private getterService: GetterService, + private roleService: RoleService, + ) { + super(meta, paramDef, async (ps, me) => { + const list = await this.userListsRepository.findOneBy({ + id: ps.listId, + isPublic: true, + }); + if (list === null) throw new ApiError(meta.errors.noSuchList); + const currentCount = await this.userListsRepository.countBy({ + userId: me.id, + }); + if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) { + throw new ApiError(meta.errors.tooManyUserLists); + } + + const userList = await this.userListsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + name: ps.name, + } as UserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); + + const users = (await this.userListJoiningsRepository.findBy({ + userListId: ps.listId, + })).map(x => x.userId); + + for (const user of users) { + const currentUser = await this.getterService.getUser(user).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + if (currentUser.id !== me.id) { + const block = await this.blockingsRepository.findOneBy({ + blockerId: currentUser.id, + blockeeId: me.id, + }); + if (block) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } + + const exist = await this.userListJoiningsRepository.findOneBy({ + userListId: userList.id, + userId: currentUser.id, + }); + + if (exist) { + throw new ApiError(meta.errors.alreadyAdded); + } + + try { + await this.userListService.push(currentUser, userList, me); + } catch (err) { + if (err instanceof UserListService.TooManyUsersError) { + throw new ApiError(meta.errors.tooManyUsers); + } + throw err; + } + } + return await this.userListEntityService.pack(userList); + }); + } +} + diff --git a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts new file mode 100644 index 000000000..263852fde --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts @@ -0,0 +1,70 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UserListFavoritesRepository, UserListsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { ApiError } from '@/server/api/error.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + requireCredential: true, + errors: { + noSuchList: { + message: 'No such user list.', + code: 'NO_SUCH_USER_LIST', + id: '7dbaf3cf-7b42-4b8f-b431-b3919e580dbe', + }, + + alreadyFavorited: { + message: 'The list has already been favorited.', + code: 'ALREADY_FAVORITED', + id: '6425bba0-985b-461e-af1b-518070e72081', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + listId: { type: 'string', format: 'misskey:id' }, + }, + required: ['listId'], +} as const; + +@Injectable() // eslint-disable-next-line import/no-default-export +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor ( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListFavoritesRepository) + private userListFavoritesRepository: UserListFavoritesRepository, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const userList = await this.userListsRepository.findOneBy({ + id: ps.listId, + isPublic: true, + }); + + if (userList === null) { + throw new ApiError(meta.errors.noSuchList); + } + + const exist = await this.userListFavoritesRepository.findOneBy({ + userId: me.id, + userListId: ps.listId, + }); + + if (exist !== null) { + throw new ApiError(meta.errors.alreadyFavorited); + } + + await this.userListFavoritesRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + userListId: ps.listId, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts index 2104c4377..eab29944b 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/list.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts @@ -1,13 +1,14 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository } from '@/models/index.js'; +import type { UserListsRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; +import { ApiError } from '@/server/api/error.js'; import { DI } from '@/di-symbols.js'; export const meta = { tags: ['lists', 'account'], - requireCredential: true, + requireCredential: false, kind: 'read:account', @@ -22,26 +23,58 @@ export const meta = { ref: 'UserList', }, }, + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: 'a8af4a82-0980-4cc4-a6af-8b0ffd54465e', + }, + remoteUser: { + message: 'Not allowed to load the remote user\'s list', + code: 'REMOTE_USER_NOT_ALLOWED', + id: '53858f1b-3315-4a01-81b7-db9b48d4b79a', + }, + invalidParam: { + message: 'Invalid param.', + code: 'INVALID_PARAM', + id: 'ab36de0e-29e9-48cb-9732-d82f1281620d', + }, + }, } as const; export const paramDef = { type: 'object', - properties: {}, + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, required: [], } as const; -// eslint-disable-next-line import/no-default-export -@Injectable() +@Injectable() // eslint-disable-next-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, private userListEntityService: UserListEntityService, ) { super(meta, paramDef, async (ps, me) => { - const userLists = await this.userListsRepository.findBy({ + if (typeof ps.userId !== 'undefined') { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + if (user === null) throw new ApiError(meta.errors.noSuchUser); + if (user.host !== null) throw new ApiError(meta.errors.remoteUser); + } else if (me === null) { + throw new ApiError(meta.errors.invalidParam); + } + + const userLists = await this.userListsRepository.findBy(typeof ps.userId === 'undefined' && me !== null ? { userId: me.id, + } : { + userId: ps.userId, + isPublic: true, }); return await Promise.all(userLists.map(x => this.userListEntityService.pack(x))); diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index 77f9cba80..8077841c8 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository } from '@/models/index.js'; +import type { UserListsRepository, UserListFavoritesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -8,7 +8,7 @@ import { ApiError } from '../../../error.js'; export const meta = { tags: ['lists', 'account'], - requireCredential: true, + requireCredential: false, kind: 'read:account', @@ -33,31 +33,54 @@ export const paramDef = { type: 'object', properties: { listId: { type: 'string', format: 'misskey:id' }, + forPublic: { type: 'boolean', default: false }, }, required: ['listId'], } as const; -// eslint-disable-next-line import/no-default-export -@Injectable() +@Injectable() // eslint-disable-next-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { constructor( @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, + @Inject(DI.userListFavoritesRepository) + private userListFavoritesRepository: UserListFavoritesRepository, + private userListEntityService: UserListEntityService, ) { super(meta, paramDef, async (ps, me) => { + const additionalProperties: Partial<{ likedCount: number, isLiked: boolean }> = {}; // Fetch the list - const userList = await this.userListsRepository.findOneBy({ + const userList = await this.userListsRepository.findOneBy(!ps.forPublic && me !== null ? { id: ps.listId, userId: me.id, + } : { + id: ps.listId, + isPublic: true, }); if (userList == null) { throw new ApiError(meta.errors.noSuchList); } - return await this.userListEntityService.pack(userList); + if (ps.forPublic && userList.isPublic) { + additionalProperties.likedCount = await this.userListFavoritesRepository.countBy({ + userListId: ps.listId, + }); + if (me !== null) { + additionalProperties.isLiked = (await this.userListFavoritesRepository.findOneBy({ + userId: me.id, + userListId: ps.listId, + }) !== null); + } else { + additionalProperties.isLiked = false; + } + } + return { + ...await this.userListEntityService.pack(userList), + ...additionalProperties, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts new file mode 100644 index 000000000..be8e31781 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts @@ -0,0 +1,63 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UserListFavoritesRepository, UserListsRepository } from '@/models/index.js'; +import { ApiError } from '@/server/api/error.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + requireCredential: true, + errors: { + noSuchList: { + message: 'No such user list.', + code: 'NO_SUCH_USER_LIST', + id: 'baedb33e-76b8-4b0c-86a8-9375c0a7b94b', + }, + + notFavorited: { + message: 'You have not favorited the list.', + code: 'ALREADY_FAVORITED', + id: '835c4b27-463d-4cfa-969b-a9058678d465', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + listId: { type: 'string', format: 'misskey:id' }, + }, + required: ['listId'], +} as const; + +@Injectable() // eslint-disable-next-line import/no-default-export +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor ( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListFavoritesRepository) + private userListFavoritesRepository: UserListFavoritesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const userList = await this.userListsRepository.findOneBy({ + id: ps.listId, + isPublic: true, + }); + + if (userList === null) { + throw new ApiError(meta.errors.noSuchList); + } + + const exist = await this.userListFavoritesRepository.findOneBy({ + userListId: ps.listId, + userId: me.id, + }); + + if (exist === null) { + throw new ApiError(meta.errors.notFavorited); + } + + await this.userListFavoritesRepository.delete({ id: exist.id }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts index 6453d7d98..b0a95a2f2 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts @@ -34,8 +34,9 @@ export const paramDef = { properties: { listId: { type: 'string', format: 'misskey:id' }, name: { type: 'string', minLength: 1, maxLength: 100 }, + isPublic: { type: 'boolean' }, }, - required: ['listId', 'name'], + required: ['listId'], } as const; // eslint-disable-next-line import/no-default-export @@ -48,7 +49,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { private userListEntityService: UserListEntityService, ) { super(meta, paramDef, async (ps, me) => { - // Fetch the list const userList = await this.userListsRepository.findOneBy({ id: ps.listId, userId: me.id, @@ -60,6 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { await this.userListsRepository.update(userList.id, { name: ps.name, + isPublic: ps.isPublic, }); return await this.userListEntityService.pack(userList.id); diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 5454836fe..d3339072c 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -13,6 +13,7 @@ class GlobalTimelineChannel extends Channel { public readonly chName = 'globalTimeline'; public static shouldShare = true; public static requireCredential = false; + private withReplies: boolean; constructor( private metaService: MetaService, @@ -31,6 +32,8 @@ class GlobalTimelineChannel extends Channel { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.gtlAvailable) return; + this.withReplies = params.withReplies as boolean; + // Subscribe events this.subscriber.on('notesStream', this.onNote); } @@ -54,7 +57,7 @@ class GlobalTimelineChannel extends Channel { } // 関係ない返信は除外 - if (note.reply && !this.user!.showTimelineReplies) { + if (note.reply && !this.withReplies) { const reply = note.reply; // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index ee874ad81..1755aa94c 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -11,6 +11,7 @@ class HomeTimelineChannel extends Channel { public readonly chName = 'homeTimeline'; public static shouldShare = true; public static requireCredential = true; + private withReplies: boolean; constructor( private noteEntityService: NoteEntityService, @@ -24,6 +25,8 @@ class HomeTimelineChannel extends Channel { @bindThis public async init(params: any) { + this.withReplies = params.withReplies as boolean; + this.subscriber.on('notesStream', this.onNote); } @@ -63,7 +66,7 @@ class HomeTimelineChannel extends Channel { } // 関係ない返信は除外 - if (note.reply && !this.user!.showTimelineReplies) { + if (note.reply && !this.withReplies) { const reply = note.reply; // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 4f7b4e78b..5a33e13cf 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -13,6 +13,7 @@ class HybridTimelineChannel extends Channel { public readonly chName = 'hybridTimeline'; public static shouldShare = true; public static requireCredential = true; + private withReplies: boolean; constructor( private metaService: MetaService, @@ -31,6 +32,8 @@ class HybridTimelineChannel extends Channel { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.ltlAvailable) return; + this.withReplies = params.withReplies as boolean; + // Subscribe events this.subscriber.on('notesStream', this.onNote); } @@ -75,7 +78,7 @@ class HybridTimelineChannel extends Channel { if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return; // 関係ない返信は除外 - if (note.reply && !this.user!.showTimelineReplies) { + if (note.reply && !this.withReplies) { const reply = note.reply; // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 09b0005ac..9ca4db8ce 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -12,6 +12,7 @@ class LocalTimelineChannel extends Channel { public readonly chName = 'localTimeline'; public static shouldShare = true; public static requireCredential = false; + private withReplies: boolean; constructor( private metaService: MetaService, @@ -30,6 +31,8 @@ class LocalTimelineChannel extends Channel { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.ltlAvailable) return; + this.withReplies = params.withReplies as boolean; + // Subscribe events this.subscriber.on('notesStream', this.onNote); } @@ -54,7 +57,7 @@ class LocalTimelineChannel extends Channel { } // 関係ない返信は除外 - if (note.reply && this.user && !this.user.showTimelineReplies) { + if (note.reply && this.user && !this.withReplies) { const reply = note.reply; // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return; diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts index 9d106c8b2..ab9c1aa0b 100644 --- a/packages/backend/src/server/api/stream/channels/role-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts @@ -5,15 +5,17 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; import { StreamMessages } from '../types.js'; +import { RoleService } from '@/core/RoleService.js'; class RoleTimelineChannel extends Channel { public readonly chName = 'roleTimeline'; public static shouldShare = false; public static requireCredential = false; private roleId: string; - + constructor( private noteEntityService: NoteEntityService, + private roleservice: RoleService, id: string, connection: Channel['connection'], @@ -34,6 +36,11 @@ class RoleTimelineChannel extends Channel { if (data.type === 'note') { const note = data.body; + if (!(await this.roleservice.isExplorable({ id: this.roleId }))) { + return; + } + if (note.visibility !== 'public') return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する @@ -61,6 +68,7 @@ export class RoleTimelineChannelService { constructor( private noteEntityService: NoteEntityService, + private roleservice: RoleService, ) { } @@ -68,6 +76,7 @@ export class RoleTimelineChannelService { public create(id: string, connection: Channel['connection']): RoleTimelineChannel { return new RoleTimelineChannel( this.noteEntityService, + this.roleservice, id, connection, ); diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index a6f914595..8b1c2c09c 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -1,3 +1,4 @@ +import * as WebSocket from 'ws'; import type { User } from '@/models/entities/User.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; import type { Packed } from '@/misc/json-schema.js'; @@ -7,7 +8,6 @@ import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; import { UserProfile } from '@/models/index.js'; import type { ChannelsService } from './ChannelsService.js'; -import type * as websocket from 'websocket'; import type { EventEmitter } from 'events'; import type Channel from './channel.js'; import type { StreamEventEmitter, StreamMessages } from './types.js'; @@ -18,7 +18,7 @@ import type { StreamEventEmitter, StreamMessages } from './types.js'; export default class Connection { public user?: User; public token?: AccessToken; - private wsConnection: websocket.connection; + private wsConnection: WebSocket.WebSocket; public subscriber: StreamEventEmitter; private channels: Channel[] = []; private subscribingNotes: any = {}; @@ -37,11 +37,9 @@ export default class Connection { private notificationService: NotificationService, private cacheService: CacheService, - subscriber: EventEmitter, user: User | null | undefined, token: AccessToken | null | undefined, ) { - this.subscriber = subscriber; if (user) this.user = user; if (token) this.token = token; } @@ -70,12 +68,16 @@ export default class Connection { if (this.user != null) { await this.fetch(); - this.fetchIntervalId = setInterval(this.fetch, 1000 * 10); + if (!this.fetchIntervalId) { + this.fetchIntervalId = setInterval(this.fetch, 1000 * 10); + } } } @bindThis - public async init2(wsConnection: websocket.connection) { + public async listen(subscriber: EventEmitter, wsConnection: WebSocket.WebSocket) { + this.subscriber = subscriber; + this.wsConnection = wsConnection; this.wsConnection.on('message', this.onWsConnectionMessage); @@ -88,14 +90,11 @@ export default class Connection { * クライアントからメッセージ受信時 */ @bindThis - private async onWsConnectionMessage(data: websocket.Message) { - if (data.type !== 'utf8') return; - if (data.utf8Data == null) return; - + private async onWsConnectionMessage(data: WebSocket.RawData) { let obj: Record<string, any>; try { - obj = JSON.parse(data.utf8Data); + obj = JSON.parse(data.toString()); } catch (e) { return; } @@ -246,7 +245,7 @@ export default class Connection { const ch: Channel = channelService.create(id, this); this.channels.push(ch); - ch.init(params); + ch.init(params ?? {}); if (pong) { this.sendMessageToWs('connected', { diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index fd7f54da5..38ae8ad2e 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -116,9 +116,9 @@ } } } - const colorSchema = localStorage.getItem('colorSchema'); - if (colorSchema) { - document.documentElement.style.setProperty('color-schema', colorSchema); + const colorScheme = localStorage.getItem('colorScheme'); + if (colorScheme) { + document.documentElement.style.setProperty('color-scheme', colorScheme); } //#endregion @@ -160,37 +160,41 @@ <path d="M12 9v2m0 4v.01"></path> <path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path> </svg> - <h1>An error has occurred!</h1> - <button class="button-big" onclick="location.reload();"> - <span class="button-label-big">Refresh</span> + <h1>Failed to load<br>読み込みに失敗しました</h1> + <button class="button-big" onclick="location.reload(true);"> + <span class="button-label-big">Reload / リロード</span> </button> - <p class="dont-worry">Don't worry, it's (probably) not your fault.</p> - <p>If the problem persists after refreshing, please contact your instance's administrator.<br>You may also try the following options:</p> - <p>Update your os and browser.</p> - <p>Disable an adblocker.</p> - <a href="/flush"> - <button class="button-small"> - <span class="button-label-small">Clear preferences and cache</span> - </button> - </a> - <br> - <a href="/cli"> - <button class="button-small"> - <span class="button-label-small">Start the simple client</span> - </button> - </a> - <br> - <a href="/bios"> - <button class="button-small"> - <span class="button-label-small">Start the repair tool</span> - </button> - </a> + <p><b>The following actions may solve the problem. / 以下を行うと解決する可能性があります。</b></p> + <p>Clear the browser cache / ブラウザのキャッシュをクリアする</p> + <p>Update your os and browser / ブラウザおよびOSを最新バージョンに更新する</p> + <p>Disable an adblocker / アドブロッカーを無効にする</p> + <details style="color: #86b300;"> + <summary>Other options / その他のオプション</summary> + <a href="/flush"> + <button class="button-small"> + <span class="button-label-small">Clear preferences and cache</span> + </button> + </a> + <br> + <a href="/cli"> + <button class="button-small"> + <span class="button-label-small">Start the simple client</span> + </button> + </a> + <br> + <a href="/bios"> + <button class="button-small"> + <span class="button-label-small">Start the repair tool</span> + </button> + </a> + </details> <br> <div id="errors"></div> `; errorsElement = document.getElementById('errors'); } const detailsElement = document.createElement('details'); + detailsElement.id = 'errorInfo'; detailsElement.innerHTML = ` <br> <summary> @@ -247,7 +251,7 @@ .button-label-big { color: #222; font-weight: bold; - font-size: 20px; + font-size: 1.2em; padding: 12px; } @@ -267,11 +271,6 @@ font-size: 16px; } - .dont-worry, - #msg { - font-size: 18px; - } - .icon-warning { color: #dec340; height: 4rem; @@ -279,14 +278,15 @@ } h1 { - font-size: 32px; + font-size: 1.5em; + margin: 1em; } code { font-family: Fira, FiraCode, monospace; } - details { + #errorInfo { background: #333; margin-bottom: 2rem; padding: 0.5rem 1rem; @@ -296,16 +296,16 @@ margin: auto; } - summary { + #errorInfo summary { cursor: pointer; } - summary > * { + #errorInfo summary > * { display: inline; } @media screen and (max-width: 500px) { - details { + #errorInfo { width: 50%; } `) diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index cb5d05a40..69b3f68e0 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -25,7 +25,6 @@ html meta(name='referrer' content='origin') meta(name='theme-color' content= themeColor || '#86b300') meta(name='theme-color-orig' content= themeColor || '#86b300') - meta(property='twitter:card' content='summary') meta(property='og:site_name' content= instanceName || 'Misskey') meta(name='viewport' content='width=device-width, initial-scale=1') link(rel='icon' href= icon || '/favicon.ico') @@ -36,7 +35,7 @@ html link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg') //- https://github.com/misskey-dev/misskey/issues/9842 - link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.17.0') + link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.21.0') link(rel='modulepreload' href=`/vite/${clientEntry.file}`) if !config.clientManifestExists @@ -59,6 +58,7 @@ html meta(property='og:title' content= title || 'Misskey') meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨') meta(property='og:image' content= img) + meta(property='twitter:card' content='summary') style include ../style.css diff --git a/packages/backend/src/server/web/views/channel.pug b/packages/backend/src/server/web/views/channel.pug index 486f0ecc4..c514025e0 100644 --- a/packages/backend/src/server/web/views/channel.pug +++ b/packages/backend/src/server/web/views/channel.pug @@ -16,3 +16,4 @@ block og meta(property='og:description' content= channel.description) meta(property='og:url' content= url) meta(property='og:image' content= channel.bannerUrl) + meta(property='twitter:card' content='summary') diff --git a/packages/backend/src/server/web/views/clip.pug b/packages/backend/src/server/web/views/clip.pug index 74dc62f1e..5a0018803 100644 --- a/packages/backend/src/server/web/views/clip.pug +++ b/packages/backend/src/server/web/views/clip.pug @@ -17,6 +17,7 @@ block og meta(property='og:description' content= clip.description) meta(property='og:url' content= url) meta(property='og:image' content= avatarUrl) + meta(property='twitter:card' content='summary') block meta if profile.noCrawle diff --git a/packages/backend/src/server/web/views/flash.pug b/packages/backend/src/server/web/views/flash.pug index 5594fcdfb..1549aa790 100644 --- a/packages/backend/src/server/web/views/flash.pug +++ b/packages/backend/src/server/web/views/flash.pug @@ -17,6 +17,7 @@ block og meta(property='og:description' content= flash.summary) meta(property='og:url' content= url) meta(property='og:image' content= avatarUrl) + meta(property='twitter:card' content='summary') block meta if profile.noCrawle diff --git a/packages/backend/src/server/web/views/gallery-post.pug b/packages/backend/src/server/web/views/gallery-post.pug index 10f2d269b..a458d7f8c 100644 --- a/packages/backend/src/server/web/views/gallery-post.pug +++ b/packages/backend/src/server/web/views/gallery-post.pug @@ -17,6 +17,7 @@ block og meta(property='og:description' content= post.description) meta(property='og:url' content= url) meta(property='og:image' content= post.files[0].thumbnailUrl) + meta(property='twitter:card' content='summary_large_image') block meta if user.host || profile.noCrawle diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug index badfcccd6..874c48c60 100644 --- a/packages/backend/src/server/web/views/note.pug +++ b/packages/backend/src/server/web/views/note.pug @@ -5,6 +5,8 @@ block vars - const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`; - const url = `${config.url}/notes/${note.id}`; - const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null; + - const image = (note.files || []).find(file => file.type.startsWith('image/') && !file.type.isSensitive) + - const video = (note.files || []).find(file => file.type.startsWith('video/') && !file.type.isSensitive) block title = `${title} | ${instanceName}` @@ -17,7 +19,19 @@ block og meta(property='og:title' content= title) meta(property='og:description' content= summary) meta(property='og:url' content= url) - meta(property='og:image' content= avatarUrl) + if video + meta(property='og:video:url' content= video.url) + meta(property='og:video:secure_url' content= video.url) + meta(property='og:video:type' content= video.type) + // FIXME: add width and height + // FIXME: add embed player for Twitter + if image + meta(property='twitter:card' content='summary_large_image') + meta(property='og:image' content= image.url) + else + meta(property='twitter:card' content='summary') + meta(property='og:image' content= avatarUrl) + block meta if user.host || isRenote || profile.noCrawle diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug index ddffc361c..08bb08ffe 100644 --- a/packages/backend/src/server/web/views/page.pug +++ b/packages/backend/src/server/web/views/page.pug @@ -17,6 +17,7 @@ block og meta(property='og:description' content= page.summary) meta(property='og:url' content= url) meta(property='og:image' content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl) + meta(property='twitter:card' content= page.eyeCatchingImage ? 'summary_large_image' : 'summary') block meta if profile.noCrawle diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug index f4c83aa89..83d57349a 100644 --- a/packages/backend/src/server/web/views/user.pug +++ b/packages/backend/src/server/web/views/user.pug @@ -16,6 +16,7 @@ block og meta(property='og:description' content= profile.description) meta(property='og:url' content= url) meta(property='og:image' content= avatarUrl) + meta(property='twitter:card' content='summary') block meta if user.host || profile.noCrawle diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts index 0addb430c..5da997f28 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -2,7 +2,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as crypto from 'node:crypto'; -import * as cbor from 'cbor'; +import cbor from 'cbor'; import * as OTPAuth from 'otpauth'; import { loadConfig } from '../../src/config.js'; import { signup, api, post, react, startServer, waitFire } from '../utils.js'; diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts new file mode 100644 index 000000000..dd3b09f85 --- /dev/null +++ b/packages/backend/test/e2e/antennas.ts @@ -0,0 +1,653 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { inspect } from 'node:util'; +import { DEFAULT_POLICIES } from '@/core/RoleService.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { + signup, + post, + userList, + page, + role, + startServer, + api, + successfulApiCall, + failedApiCall, + uploadFile, + testPaginationConsistency, +} from '../utils.js'; +import type * as misskey from 'misskey-js'; +import type { INestApplicationContext } from '@nestjs/common'; + +const compareBy = <T extends { id: string }>(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => { + return selector(a).localeCompare(selector(b)); +}; + +describe('アンテナ', () => { + // エンティティとしてのアンテナを主眼においたテストを記述する + // (Antennaを返すエンドポイント、Antennaエンティティを書き換えるエンドポイント、Antennaからノートを取得するエンドポイントをテストする) + + // BUG misskey-jsとjson-schemaが一致していない。 + // - srcのenumにgroupが残っている + // - userGroupIdが残っている, isActiveがない + type Antenna = misskey.entities.Antenna | Packed<'Antenna'>; + type User = misskey.entities.MeDetailed & { token: string }; + type Note = misskey.entities.Note; + + // アンテナを作成できる最小のパラメタ + const defaultParam = { + caseSensitive: false, + excludeKeywords: [['']], + keywords: [['keyword']], + name: 'test', + notify: false, + src: 'all' as const, + userListId: null, + users: [''], + withFile: false, + withReplies: false, + }; + + let app: INestApplicationContext; + + let root: User; + let alice: User; + let bob: User; + let carol: User; + + let alicePost: Note; + let aliceList: misskey.entities.UserList; + let bobFile: misskey.entities.DriveFile; + let bobList: misskey.entities.UserList; + + let userNotExplorable: User; + let userLocking: User; + let userSilenced: User; + let userSuspended: User; + let userDeletedBySelf: User; + let userDeletedByAdmin: User; + let userFollowingAlice: User; + let userFollowedByAlice: User; + let userBlockingAlice: User; + let userBlockedByAlice: User; + let userMutingAlice: User; + let userMutedByAlice: User; + + beforeAll(async () => { + app = await startServer(); + }, 1000 * 60 * 2); + + beforeAll(async () => { + root = await signup({ username: 'root' }); + alice = await signup({ username: 'alice' }); + alicePost = await post(alice, { text: 'test' }); + aliceList = await userList(alice, {}); + bob = await signup({ username: 'bob' }); + aliceList = await userList(alice, {}); + bobFile = (await uploadFile(bob)).body; + bobList = await userList(bob); + carol = await signup({ username: 'carol' }); + await api('users/lists/push', { listId: aliceList.id, userId: bob.id }, alice); + await api('users/lists/push', { listId: aliceList.id, userId: carol.id }, alice); + + userNotExplorable = await signup({ username: 'userNotExplorable' }); + await post(userNotExplorable, { text: 'test' }); + await api('i/update', { isExplorable: false }, userNotExplorable); + userLocking = await signup({ username: 'userLocking' }); + await post(userLocking, { text: 'test' }); + await api('i/update', { isLocked: true }, userLocking); + userSilenced = await signup({ username: 'userSilenced' }); + await post(userSilenced, { text: 'test' }); + const roleSilenced = await role(root, {}, { canPublicNote: { priority: 0, useDefault: false, value: false } }); + await api('admin/roles/assign', { userId: userSilenced.id, roleId: roleSilenced.id }, root); + userSuspended = await signup({ username: 'userSuspended' }); + await post(userSuspended, { text: 'test' }); + await successfulApiCall({ endpoint: 'i/update', parameters: { description: '#user_testuserSuspended' }, user: userSuspended }); + await api('admin/suspend-user', { userId: userSuspended.id }, root); + userDeletedBySelf = await signup({ username: 'userDeletedBySelf', password: 'userDeletedBySelf' }); + await post(userDeletedBySelf, { text: 'test' }); + await api('i/delete-account', { password: 'userDeletedBySelf' }, userDeletedBySelf); + userDeletedByAdmin = await signup({ username: 'userDeletedByAdmin' }); + await post(userDeletedByAdmin, { text: 'test' }); + await api('admin/delete-account', { userId: userDeletedByAdmin.id }, root); + userFollowedByAlice = await signup({ username: 'userFollowedByAlice' }); + await post(userFollowedByAlice, { text: 'test' }); + await api('following/create', { userId: userFollowedByAlice.id }, alice); + userFollowingAlice = await signup({ username: 'userFollowingAlice' }); + await post(userFollowingAlice, { text: 'test' }); + await api('following/create', { userId: alice.id }, userFollowingAlice); + userBlockingAlice = await signup({ username: 'userBlockingAlice' }); + await post(userBlockingAlice, { text: 'test' }); + await api('blocking/create', { userId: alice.id }, userBlockingAlice); + userBlockedByAlice = await signup({ username: 'userBlockedByAlice' }); + await post(userBlockedByAlice, { text: 'test' }); + await api('blocking/create', { userId: userBlockedByAlice.id }, alice); + userMutingAlice = await signup({ username: 'userMutingAlice' }); + await post(userMutingAlice, { text: 'test' }); + await api('mute/create', { userId: alice.id }, userMutingAlice); + userMutedByAlice = await signup({ username: 'userMutedByAlice' }); + await post(userMutedByAlice, { text: 'test' }); + await api('mute/create', { userId: userMutedByAlice.id }, alice); + }, 1000 * 60 * 10); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(async () => { + // テスト間で影響し合わないように毎回全部消す。 + for (const user of [alice, bob]) { + const list = await api('/antennas/list', {}, user); + for (const antenna of list.body) { + await api('/antennas/delete', { antennaId: antenna.id }, user); + } + } + }); + + //#region 作成(antennas/create) + + test('が作成できること、キーが過不足なく入っていること。', async () => { + const response = await successfulApiCall({ + endpoint: 'antennas/create', + parameters: { ...defaultParam }, + user: alice, + }); + assert.match(response.id, /[0-9a-z]{10}/); + const expected = { + id: response.id, + caseSensitive: false, + createdAt: new Date(response.createdAt).toISOString(), + excludeKeywords: [['']], + hasUnreadNote: false, + isActive: true, + keywords: [['keyword']], + name: 'test', + notify: false, + src: 'all', + userListId: null, + users: [''], + withFile: false, + withReplies: false, + } as Antenna; + assert.deepStrictEqual(response, expected); + }); + + test('が上限いっぱいまで作成できること', async () => { + // antennaLimit + 1まで作れるのがキモ + const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit + 1)].map(() => successfulApiCall({ + endpoint: 'antennas/create', + parameters: { ...defaultParam }, + user: alice, + }))); + + const expected = await successfulApiCall({ endpoint: 'antennas/list', parameters: {}, user: alice }); + assert.deepStrictEqual( + response.sort(compareBy(s => s.id)), + expected.sort(compareBy(s => s.id))); + + failedApiCall({ + endpoint: 'antennas/create', + parameters: { ...defaultParam }, + user: alice, + }, { + status: 400, + code: 'TOO_MANY_ANTENNAS', + id: 'faf47050-e8b5-438c-913c-db2b1576fde4', + }); + }); + + test('を作成するとき他人のリストを指定したらエラーになる', async () => { + failedApiCall({ + endpoint: 'antennas/create', + parameters: { ...defaultParam, src: 'list', userListId: bobList.id }, + user: alice, + }, { + status: 400, + code: 'NO_SUCH_USER_LIST', + id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f', + }); + }); + + const antennaParamPattern = [ + { parameters: (): object => ({ name: 'x'.repeat(100) }) }, + { parameters: (): object => ({ name: 'x' }) }, + { parameters: (): object => ({ src: 'home' }) }, + { parameters: (): object => ({ src: 'all' }) }, + { parameters: (): object => ({ src: 'users' }) }, + { parameters: (): object => ({ src: 'list' }) }, + { parameters: (): object => ({ userListId: null }) }, + { parameters: (): object => ({ src: 'list', userListId: aliceList.id }) }, + { parameters: (): object => ({ keywords: [['x']] }) }, + { parameters: (): object => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) }, + { parameters: (): object => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) }, + { parameters: (): object => ({ users: [alice.username] }) }, + { parameters: (): object => ({ users: [alice.username, bob.username, carol.username] }) }, + { parameters: (): object => ({ caseSensitive: false }) }, + { parameters: (): object => ({ caseSensitive: true }) }, + { parameters: (): object => ({ withReplies: false }) }, + { parameters: (): object => ({ withReplies: true }) }, + { parameters: (): object => ({ withFile: false }) }, + { parameters: (): object => ({ withFile: true }) }, + { parameters: (): object => ({ notify: false }) }, + { parameters: (): object => ({ notify: true }) }, + ]; + test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => { + const response = await successfulApiCall({ + endpoint: 'antennas/create', + parameters: { ...defaultParam, ...parameters() }, + user: alice, + }); + const expected = { ...response, ...parameters() }; + assert.deepStrictEqual(response, expected); + }); + + //#endregion + //#region 更新(antennas/update) + + test.each(antennaParamPattern)('を変更できること($#)', async ({ parameters }) => { + const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice }); + const response = await successfulApiCall({ + endpoint: 'antennas/update', + parameters: { antennaId: antenna.id, ...defaultParam, ...parameters() }, + user: alice, + }); + const expected = { ...response, ...parameters() }; + assert.deepStrictEqual(response, expected); + }); + test.todo('は他人のものは変更できない'); + + test('を変更するとき他人のリストを指定したらエラーになる', async () => { + const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice }); + failedApiCall({ + endpoint: 'antennas/update', + parameters: { antennaId: antenna.id, ...defaultParam, src: 'list', userListId: bobList.id }, + user: alice, + }, { + status: 400, + code: 'NO_SUCH_USER_LIST', + id: '1c6b35c9-943e-48c2-81e4-2844989407f7', + }); + }); + + //#endregion + //#region 表示(antennas/show) + + test('をID指定で表示できること。', async () => { + const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice }); + const response = await successfulApiCall({ + endpoint: 'antennas/show', + parameters: { antennaId: antenna.id }, + user: alice, + }); + const expected = { ...antenna }; + assert.deepStrictEqual(response, expected); + }); + test.todo('は他人のものをID指定で表示できない'); + + //#endregion + //#region 一覧(antennas/list) + + test('をリスト形式で取得できること。', async () => { + const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice }); + await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: bob }); + const response = await successfulApiCall({ + endpoint: 'antennas/list', + parameters: {}, + user: alice, + }); + const expected = [{ ...antenna }]; + assert.deepStrictEqual(response, expected); + }); + + //#endregion + //#region 削除(antennas/delete) + + test('を削除できること。', async () => { + const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice }); + const response = await successfulApiCall({ + endpoint: 'antennas/delete', + parameters: { antennaId: antenna.id }, + user: alice, + }); + assert.deepStrictEqual(response, null); + const list = await successfulApiCall({ endpoint: 'antennas/list', parameters: {}, user: alice }); + assert.deepStrictEqual(list, []); + }); + test.todo('は他人のものを削除できない'); + + //#endregion + + describe('のノート', () => { + //#region アンテナのノート取得(antennas/notes) + + test('を取得できること。', async () => { + const keyword = 'キーワード'; + await post(bob, { text: `test ${keyword} beforehand` }); + const antenna = await successfulApiCall({ + endpoint: 'antennas/create', + parameters: { ...defaultParam, keywords: [[keyword]] }, + user: alice, + }); + const note = await post(bob, { text: `test ${keyword}` }); + const response = await successfulApiCall({ + endpoint: 'antennas/notes', + parameters: { antennaId: antenna.id }, + user: alice, + }); + const expected = [note]; + assert.deepStrictEqual(response, expected); + }); + + const keyword = 'キーワード'; + test.each([ + { + label: '全体から', + parameters: (): object => ({ src: 'all' }), + posts: [ + { note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true }, + { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, + { note: (): Promise<Note> => post(carol, { text: `test ${keyword}` }), included: true }, + ], + }, + { + // BUG e4144a1 以降home指定は壊れている(allと同じ) + label: 'ホーム指定はallと同じ', + parameters: (): object => ({ src: 'home' }), + posts: [ + { note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true }, + { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, + { note: (): Promise<Note> => post(carol, { text: `test ${keyword}` }), included: true }, + ], + }, + { + // https://github.com/misskey-dev/misskey/issues/9025 + label: 'ただし、フォロワー限定投稿とDM投稿を含まない。フォロワーであっても。', + parameters: (): object => ({}), + posts: [ + { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'public' }), included: true }, + { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'home' }), included: true }, + { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'followers' }) }, + { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'specified', visibleUserIds: [alice.id] }) }, + ], + }, + { + label: 'ブロックしているユーザーのノートは含む', + parameters: (): object => ({}), + posts: [ + { note: (): Promise<Note> => post(userBlockedByAlice, { text: `${keyword}` }), included: true }, + ], + }, + { + label: 'ブロックされているユーザーのノートは含まない', + parameters: (): object => ({}), + posts: [ + { note: (): Promise<Note> => post(userBlockingAlice, { text: `${keyword}` }) }, + ], + }, + { + label: 'ミュートしているユーザーのノートは含まない', + parameters: (): object => ({}), + posts: [ + { note: (): Promise<Note> => post(userMutedByAlice, { text: `${keyword}` }) }, + ], + }, + { + label: 'ミュートされているユーザーのノートは含む', + parameters: (): object => ({}), + posts: [ + { note: (): Promise<Note> => post(userMutingAlice, { text: `${keyword}` }), included: true }, + ], + }, + { + label: '「見つけやすくする」がOFFのユーザーのノートも含まれる', + parameters: (): object => ({}), + posts: [ + { note: (): Promise<Note> => post(userNotExplorable, { text: `${keyword}` }), included: true }, + ], + }, + { + label: '鍵付きユーザーのノートも含まれる', + parameters: (): object => ({}), + posts: [ + { note: (): Promise<Note> => post(userLocking, { text: `${keyword}` }), included: true }, + ], + }, + { + label: 'サイレンスのノートも含まれる', + parameters: (): object => ({}), + posts: [ + { note: (): Promise<Note> => post(userSilenced, { text: `${keyword}` }), included: true }, + ], + }, + { + label: '削除ユーザーのノートも含まれる', + parameters: (): object => ({}), + posts: [ + { note: (): Promise<Note> => post(userDeletedBySelf, { text: `${keyword}` }), included: true }, + { note: (): Promise<Note> => post(userDeletedByAdmin, { text: `${keyword}` }), included: true }, + ], + }, + { + label: 'ユーザー指定で', + parameters: (): object => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }), + posts: [ + { note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, + { note: (): Promise<Note> => post(carol, { text: `test ${keyword}` }), included: true }, + ], + }, + { + label: 'リスト指定で', + parameters: (): object => ({ src: 'list', userListId: aliceList.id }), + posts: [ + { note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, + { note: (): Promise<Note> => post(carol, { text: `test ${keyword}` }), included: true }, + ], + }, + { + label: 'CWにもマッチする', + parameters: (): object => ({ keywords: [[keyword]] }), + posts: [ + { note: (): Promise<Note> => post(bob, { text: 'test', cw: `cw ${keyword}` }), included: true }, + ], + }, + { + label: 'キーワード1つ', + parameters: (): object => ({ keywords: [[keyword]] }), + posts: [ + { note: (): Promise<Note> => post(alice, { text: 'test' }) }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, + { note: (): Promise<Note> => post(carol, { text: 'test' }) }, + ], + }, + { + label: 'キーワード3つ(AND)', + parameters: (): object => ({ keywords: [['A', 'B', 'C']] }), + posts: [ + { note: (): Promise<Note> => post(bob, { text: 'test A' }) }, + { note: (): Promise<Note> => post(bob, { text: 'test A B' }) }, + { note: (): Promise<Note> => post(bob, { text: 'test B C' }) }, + { note: (): Promise<Note> => post(bob, { text: 'test A B C' }), included: true }, + { note: (): Promise<Note> => post(bob, { text: 'test C B A A B C' }), included: true }, + ], + }, + { + label: 'キーワード3つ(OR)', + parameters: (): object => ({ keywords: [['A'], ['B'], ['C']] }), + posts: [ + { note: (): Promise<Note> => post(bob, { text: 'test' }) }, + { note: (): Promise<Note> => post(bob, { text: 'test A' }), included: true }, + { note: (): Promise<Note> => post(bob, { text: 'test A B' }), included: true }, + { note: (): Promise<Note> => post(bob, { text: 'test B C' }), included: true }, + { note: (): Promise<Note> => post(bob, { text: 'test B C A' }), included: true }, + { note: (): Promise<Note> => post(bob, { text: 'test C B' }), included: true }, + { note: (): Promise<Note> => post(bob, { text: 'test C' }), included: true }, + ], + }, + { + label: '除外ワード3つ(AND)', + parameters: (): object => ({ excludeKeywords: [['A', 'B', 'C']] }), + posts: [ + { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }), included: true }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword} A B` }), included: true }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword} B C` }), included: true }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword} B C A` }) }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword} C B` }), included: true }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword} C` }), included: true }, + ], + }, + { + label: '除外ワード3つ(OR)', + parameters: (): object => ({ excludeKeywords: [['A'], ['B'], ['C']] }), + posts: [ + { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }) }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword} A B` }) }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword} B C` }) }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword} B C A` }) }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword} C B` }) }, + { note: (): Promise<Note> => post(bob, { text: `test ${keyword} C` }) }, + ], + }, + { + label: 'キーワード1つ(大文字小文字区別する)', + parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: true }), + posts: [ + { note: (): Promise<Note> => post(bob, { text: 'keyword' }) }, + { note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }) }, + { note: (): Promise<Note> => post(bob, { text: 'KEYWORD' }), included: true }, + ], + }, + { + label: 'キーワード1つ(大文字小文字区別しない)', + parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: false }), + posts: [ + { note: (): Promise<Note> => post(bob, { text: 'keyword' }), included: true }, + { note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }), included: true }, + { note: (): Promise<Note> => post(bob, { text: 'KEYWORD' }), included: true }, + ], + }, + { + label: '除外ワード1つ(大文字小文字区別する)', + parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }), + posts: [ + { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true }, + { note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }), included: true }, + { note: (): Promise<Note> => post(bob, { text: `${keyword} kEyWoRd` }), included: true }, + { note: (): Promise<Note> => post(bob, { text: `${keyword} KEYWORD` }) }, + ], + }, + { + label: '除外ワード1つ(大文字小文字区別しない)', + parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }), + posts: [ + { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true }, + { note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }) }, + { note: (): Promise<Note> => post(bob, { text: `${keyword} kEyWoRd` }) }, + { note: (): Promise<Note> => post(bob, { text: `${keyword} KEYWORD` }) }, + ], + }, + { + label: '添付ファイルを問わない', + parameters: (): object => ({ withFile: false }), + posts: [ + { note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true }, + { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true }, + ], + }, + { + label: '添付ファイル付きのみ', + parameters: (): object => ({ withFile: true }), + posts: [ + { note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true }, + { note: (): Promise<Note> => post(bob, { text: `${keyword}` }) }, + ], + }, + { + label: 'リプライ以外', + parameters: (): object => ({ withReplies: false }), + posts: [ + { note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }) }, + { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true }, + ], + }, + { + label: 'リプライも含む', + parameters: (): object => ({ withReplies: true }), + posts: [ + { note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }), included: true }, + { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true }, + ], + }, + ])('が取得できること($label)', async ({ parameters, posts }) => { + const antenna = await successfulApiCall({ + endpoint: 'antennas/create', + parameters: { ...defaultParam, keywords: [[keyword]], ...parameters() }, + user: alice, + }); + + const notes = await posts.reduce(async (prev, current) => { + // includedに関わらずnote()は評価して投稿する。 + const p = await prev; + const n = await current.note(); + if (current.included) return p.concat(n); + return p; + }, Promise.resolve([] as Note[])); + + // alice視点でNoteを取り直す + const expected = await Promise.all(notes.reverse().map(s => successfulApiCall({ + endpoint: 'notes/show', + parameters: { noteId: s.id }, + user: alice, + }))); + + const response = await successfulApiCall({ + endpoint: 'antennas/notes', + parameters: { antennaId: antenna.id }, + user: alice, + }); + assert.deepStrictEqual( + response.map(({ userId, id, text }) => ({ userId, id, text })), + expected.map(({ userId, id, text }) => ({ userId, id, text }))); + assert.deepStrictEqual(response, expected); + }); + + test.skip('が取得でき、日付指定のPaginationに一貫性があること', async () => { }); + test.each([ + { label: 'ID指定', offsetBy: 'id' }, + + // BUG sinceDate, untilDateはsinceIdや他のエンドポイントとは異なり、その時刻に一致するレコードを含んでしまう。 + // { label: '日付指定', offsetBy: 'createdAt' }, + ] as const)('が取得でき、$labelのPaginationに一貫性があること', async ({ offsetBy }) => { + const antenna = await successfulApiCall({ + endpoint: 'antennas/create', + parameters: { ...defaultParam, keywords: [[keyword]] }, + user: alice, + }); + const notes = await [...Array(30)].reduce(async (prev, current, index) => { + const p = await prev; + const n = await post(alice, { text: `${keyword} (${index})` }); + return [n].concat(p); + }, Promise.resolve([] as Note[])); + + // antennas/notesは降順のみで、昇順をサポートしない。 + await testPaginationConsistency(notes, async (paginationParam) => { + return successfulApiCall({ + endpoint: 'antennas/notes', + parameters: { antennaId: antenna.id, ...paginationParam }, + user: alice, + }) as any as Note[]; + }, offsetBy, 'desc'); + }); + + // BUG 7日過ぎると作り直すしかない。 https://github.com/misskey-dev/misskey/issues/10476 + test.todo('を取得したときActiveに戻る'); + + //#endregion + }); +}); diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index a7f8210c8..02684c93b 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -43,7 +43,6 @@ describe('ユーザー', () => { type MeDetailed = UserDetailedNotMe & misskey.entities.MeDetailed & { - showTimelineReplies: boolean, achievements: object[], loggedInDays: number, policies: object, @@ -160,7 +159,6 @@ describe('ユーザー', () => { mutedInstances: user.mutedInstances, mutingNotificationTypes: user.mutingNotificationTypes, emailNotificationTypes: user.emailNotificationTypes, - showTimelineReplies: user.showTimelineReplies, achievements: user.achievements, loggedInDays: user.loggedInDays, policies: user.policies, @@ -406,7 +404,6 @@ describe('ユーザー', () => { assert.deepStrictEqual(response.mutedInstances, []); assert.deepStrictEqual(response.mutingNotificationTypes, []); assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']); - assert.strictEqual(response.showTimelineReplies, false); assert.deepStrictEqual(response.achievements, []); assert.deepStrictEqual(response.loggedInDays, 0); assert.deepStrictEqual(response.policies, DEFAULT_POLICIES); @@ -470,8 +467,6 @@ describe('ユーザー', () => { { parameters: (): object => ({ isBot: false }) }, { parameters: (): object => ({ isCat: true }) }, { parameters: (): object => ({ isCat: false }) }, - { parameters: (): object => ({ showTimelineReplies: true }) }, - { parameters: (): object => ({ showTimelineReplies: false }) }, { parameters: (): object => ({ injectFeaturedNote: true }) }, { parameters: (): object => ({ injectFeaturedNote: false }) }, { parameters: (): object => ({ receiveAnnouncementEmail: true }) }, diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index 6b31e6861..a7bcd859a 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -52,11 +52,7 @@ export class MockResolver extends Resolver { const r = this._rs.get(value); if (!r) { - throw { - name: 'StatusError', - statusCode: 404, - message: 'Not registed for mock', - }; + throw new Error('Not registed for mock'); } const object = JSON.parse(r.content); diff --git a/packages/backend/test/unit/ReactionService.ts b/packages/backend/test/unit/ReactionService.ts index 38db081ac..aa68f4117 100644 --- a/packages/backend/test/unit/ReactionService.ts +++ b/packages/backend/test/unit/ReactionService.ts @@ -15,78 +15,74 @@ describe('ReactionService', () => { reactionService = app.get<ReactionService>(ReactionService); }); - describe('toDbReaction', () => { + describe('normalize', () => { test('絵文字リアクションはそのまま', async () => { - assert.strictEqual(await reactionService.toDbReaction('👍'), '👍'); - assert.strictEqual(await reactionService.toDbReaction('🍅'), '🍅'); + assert.strictEqual(await reactionService.normalize('👍'), '👍'); + assert.strictEqual(await reactionService.normalize('🍅'), '🍅'); }); test('既存のリアクションは絵文字化する pudding', async () => { - assert.strictEqual(await reactionService.toDbReaction('pudding'), '🍮'); + assert.strictEqual(await reactionService.normalize('pudding'), '🍮'); }); test('既存のリアクションは絵文字化する like', async () => { - assert.strictEqual(await reactionService.toDbReaction('like'), '👍'); + assert.strictEqual(await reactionService.normalize('like'), '👍'); }); test('既存のリアクションは絵文字化する love', async () => { - assert.strictEqual(await reactionService.toDbReaction('love'), '❤'); + assert.strictEqual(await reactionService.normalize('love'), '❤'); }); test('既存のリアクションは絵文字化する laugh', async () => { - assert.strictEqual(await reactionService.toDbReaction('laugh'), '😆'); + assert.strictEqual(await reactionService.normalize('laugh'), '😆'); }); test('既存のリアクションは絵文字化する hmm', async () => { - assert.strictEqual(await reactionService.toDbReaction('hmm'), '🤔'); + assert.strictEqual(await reactionService.normalize('hmm'), '🤔'); }); test('既存のリアクションは絵文字化する surprise', async () => { - assert.strictEqual(await reactionService.toDbReaction('surprise'), '😮'); + assert.strictEqual(await reactionService.normalize('surprise'), '😮'); }); test('既存のリアクションは絵文字化する congrats', async () => { - assert.strictEqual(await reactionService.toDbReaction('congrats'), '🎉'); + assert.strictEqual(await reactionService.normalize('congrats'), '🎉'); }); test('既存のリアクションは絵文字化する angry', async () => { - assert.strictEqual(await reactionService.toDbReaction('angry'), '💢'); + assert.strictEqual(await reactionService.normalize('angry'), '💢'); }); test('既存のリアクションは絵文字化する confused', async () => { - assert.strictEqual(await reactionService.toDbReaction('confused'), '😥'); + assert.strictEqual(await reactionService.normalize('confused'), '😥'); }); test('既存のリアクションは絵文字化する rip', async () => { - assert.strictEqual(await reactionService.toDbReaction('rip'), '😇'); + assert.strictEqual(await reactionService.normalize('rip'), '😇'); }); test('既存のリアクションは絵文字化する star', async () => { - assert.strictEqual(await reactionService.toDbReaction('star'), '⭐'); + assert.strictEqual(await reactionService.normalize('star'), '⭐'); }); test('異体字セレクタ除去', async () => { - assert.strictEqual(await reactionService.toDbReaction('㊗️'), '㊗'); + assert.strictEqual(await reactionService.normalize('㊗️'), '㊗'); }); test('異体字セレクタ除去 必要なし', async () => { - assert.strictEqual(await reactionService.toDbReaction('㊗'), '㊗'); - }); - - test('fallback - undefined', async () => { - assert.strictEqual(await reactionService.toDbReaction(undefined), '❤'); + assert.strictEqual(await reactionService.normalize('㊗'), '㊗'); }); test('fallback - null', async () => { - assert.strictEqual(await reactionService.toDbReaction(null), '❤'); + assert.strictEqual(await reactionService.normalize(null), '❤'); }); test('fallback - empty', async () => { - assert.strictEqual(await reactionService.toDbReaction(''), '❤'); + assert.strictEqual(await reactionService.normalize(''), '❤'); }); test('fallback - unknown', async () => { - assert.strictEqual(await reactionService.toDbReaction('unknown'), '❤'); + assert.strictEqual(await reactionService.normalize('unknown'), '❤'); }); }); }); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 809ed2c66..22f7d81e4 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -124,6 +124,13 @@ export const react = async (user: any, note: any, reaction: string): Promise<any }, user); }; +export const userList = async (user: any, userList: any = {}): Promise<any> => { + const res = await api('users/lists/create', { + name: 'test', + }, user); + return res.body; +}; + export const page = async (user: any, page: any = {}): Promise<any> => { const res = await api('pages/create', { alignCenter: false, @@ -380,8 +387,98 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde }; }; +/** + * あるAPIエンドポイントのPaginationが複数の条件で一貫した挙動であることをテストします。 + * (sinceId, untilId, sinceDate, untilDate, offset, limit) + * @param expected 期待値となるEntityの並び(例:Note[])昇順降順が一致している必要がある + * @param fetchEntities Entity[]を返却するテスト対象のAPIを呼び出す関数 + * @param offsetBy 何をキーとしてPaginationするか。 + * @param ordering 昇順・降順 + */ +export async function testPaginationConsistency<Entity extends { id: string, createdAt?: string }>( + expected: Entity[], + fetchEntities: (paginationParam: { + limit?: number, + offset?: number, + sinceId?: string, + untilId?: string, + sinceDate?: number, + untilDate?: number, + }) => Promise<Entity[]>, + offsetBy: 'offset' | 'id' | 'createdAt' = 'id', + ordering: 'desc' | 'asc' = 'desc'): Promise<void> { + const rangeToParam = (p: { limit?: number, until?: Entity, since?: Entity }): object => { + if (offsetBy === 'id') { + return { limit: p.limit, sinceId: p.since?.id, untilId: p.until?.id }; + } else { + const sinceDate = p.since?.createdAt !== undefined ? new Date(p.since.createdAt).getTime() : undefined; + const untilDate = p.until?.createdAt !== undefined ? new Date(p.until.createdAt).getTime() : undefined; + return { limit: p.limit, sinceDate, untilDate }; + } + }; + + for (const limit of [1, 5, 10, 100, undefined]) { + // 1. sinceId/DateとuntilId/Dateで両端を指定して取得した結果が期待通りになっていること + if (ordering === 'desc') { + const end = expected[expected.length - 1]; + let last = await fetchEntities(rangeToParam({ limit, since: end })); + const actual: Entity[] = []; + while (last.length !== 0) { + actual.push(...last); + last = await fetchEntities(rangeToParam({ limit, until: last[last.length - 1], since: end })); + } + actual.push(end); + assert.deepStrictEqual( + actual.map(({ id, createdAt }) => id + ':' + createdAt), + expected.map(({ id, createdAt }) => id + ':' + createdAt)); + } + + // 2. sinceId/Date指定+limitで取得してつなぎ合わせた結果が期待通りになっていること + if (ordering === 'asc') { + // 昇順にしたときの先頭(一番古いもの)をもってくる(expected[1]を基準に降順にして0番目) + let last = await fetchEntities({ limit: 1, untilId: expected[1].id }); + const actual: Entity[] = []; + while (last.length !== 0) { + actual.push(...last); + last = await fetchEntities(rangeToParam({ limit, since: last[last.length - 1] })); + } + assert.deepStrictEqual( + actual.map(({ id, createdAt }) => id + ':' + createdAt), + expected.map(({ id, createdAt }) => id + ':' + createdAt)); + } + + // 3. untilId指定+limitで取得してつなぎ合わせた結果が期待通りになっていること + if (ordering === 'desc') { + let last = await fetchEntities({ limit }); + const actual: Entity[] = []; + while (last.length !== 0) { + actual.push(...last); + last = await fetchEntities(rangeToParam({ limit, until: last[last.length - 1] })); + } + assert.deepStrictEqual( + actual.map(({ id, createdAt }) => id + ':' + createdAt), + expected.map(({ id, createdAt }) => id + ':' + createdAt)); + } + + // 4. offset指定+limitで取得してつなぎ合わせた結果が期待通りになっていること + if (offsetBy === 'offset') { + let last = await fetchEntities({ limit, offset: 0 }); + let offset = limit ?? 10; + const actual: Entity[] = []; + while (last.length !== 0) { + actual.push(...last); + last = await fetchEntities({ limit, offset }); + offset += limit ?? 10; + } + assert.deepStrictEqual( + actual.map(({ id, createdAt }) => id + ':' + createdAt), + expected.map(({ id, createdAt }) => id + ':' + createdAt)); + } + } +} + export async function initTestDb(justBorrow = false, initEntities?: any[]) { - if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test'; + if (process.env.NODE_ENV !== 'test') throw new Error('NODE_ENV is not a test'); const db = new DataSource({ type: 'postgres', diff --git a/packages/frontend/.eslintrc.js b/packages/frontend/.eslintrc.js index e8e0e57d2..24c3ad4b8 100644 --- a/packages/frontend/.eslintrc.js +++ b/packages/frontend/.eslintrc.js @@ -56,14 +56,15 @@ module.exports = { 'vue/require-v-for-key': 'warn', 'vue/no-unused-components': 'warn', 'vue/no-unused-vars': 'warn', + 'vue/no-dupe-keys': 'warn', 'vue/valid-v-for': 'warn', 'vue/return-in-computed-property': 'warn', 'vue/no-setup-props-destructure': 'warn', 'vue/max-attributes-per-line': 'off', 'vue/html-self-closing': 'off', 'vue/singleline-html-element-content-newline': 'off', - // (vue/vue3-recommended disabled the autofix for Vue 2 compatibility) - 'vue/v-on-event-hyphenation': ['warn', 'always', { autofix: true }], + 'vue/v-on-event-hyphenation': ['error', 'never', { autofix: true }], + 'vue/attribute-hyphenation': ['error', 'never'], }, globals: { // Node.js diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index 7c51d4c00..f44242210 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -397,6 +397,7 @@ function toStories(component: string): string { Promise.all([ glob('src/components/global/*.vue'), glob('src/components/Mk{A,B}*.vue'), + glob('src/components/MkDigitalClock.vue'), glob('src/components/MkGalleryPostPreview.vue'), glob('src/components/MkSignupServerRules.vue'), glob('src/components/MkUserSetupDialog.vue'), diff --git a/packages/frontend/.storybook/preview-head.html b/packages/frontend/.storybook/preview-head.html index ab694f64f..f6a9a4875 100644 --- a/packages/frontend/.storybook/preview-head.html +++ b/packages/frontend/.storybook/preview-head.html @@ -1,6 +1,6 @@ <link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous"> <link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous"> -<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.12.0/tabler-icons.min.css"> +<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.21.0/tabler-icons.min.css"> <link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css"> <style> html { diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts new file mode 100644 index 000000000..3929bf060 --- /dev/null +++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts @@ -0,0 +1,597 @@ +import { parse } from 'acorn'; +import { generate } from 'astring'; +import { describe, expect, it } from 'vitest'; +import { normalizeClass, unwindCssModuleClassName } from './rollup-plugin-unwind-css-module-class-name'; +import type * as estree from 'estree'; + +function parseExpression(code: string): estree.Expression { + const program = parse(code, { ecmaVersion: 'latest', sourceType: 'module' }) as unknown as estree.Program; + const statement = program.body[0] as estree.ExpressionStatement; + return statement.expression; +} + +describe(normalizeClass.name, () => { + it('should normalize string', () => { + expect(normalizeClass(parseExpression('"a b c"'))).toBe('a b c'); + }); + it('should trim redundant spaces', () => { + expect(normalizeClass(parseExpression('" a b c "'))).toBe('a b c'); + }); + it('should ignore undefined', () => { + expect(normalizeClass(parseExpression('undefined'))).toBe(''); + }); + it('should ignore non string literals', () => { + expect(normalizeClass(parseExpression('0'))).toBe(''); + expect(normalizeClass(parseExpression('true'))).toBe(''); + expect(normalizeClass(parseExpression('null'))).toBe(''); + expect(normalizeClass(parseExpression('/I.D/'))).toBe(''); + }); + it('should not normalize identifiers', () => { + expect(normalizeClass(parseExpression('EScape'))).toBeNull(); + }); + it('should normalize recursively array', () => { + expect(normalizeClass(parseExpression('["from", ...["Utopia"]]'))).toBe('from Utopia'); + expect(normalizeClass(parseExpression('["from", ...[Utopia]]'))).toBeNull(); + }); + it('should normalize recursively template literal', () => { + expect(normalizeClass(parseExpression('`name ${"shiho"} code ${33}`'))).toBe('name shiho code'); + expect(normalizeClass(parseExpression('`name ${shiho.name} code ${33}`'))).toBeNull(); + }); + it('should normalize recursively binary expression', () => { + expect(normalizeClass(parseExpression('"mirage" + "mirror"'))).toBe('miragemirror'); + expect(normalizeClass(parseExpression('"mirage" + mirror'))).toBeNull(); + }); + it('should normalize recursively object expression', () => { + expect(normalizeClass(parseExpression('({ a: true, b: "c" })'))).toBe('a b'); + expect(normalizeClass(parseExpression('({ a: false, b: "c" })'))).toBe('b'); + expect(normalizeClass(parseExpression('({ a: true, b: c })'))).toBeNull(); + expect(normalizeClass(parseExpression('({ a: true, b: "c", ...({ d: true }) })'))).toBe('a b d'); + expect(normalizeClass(parseExpression('({ a: true, [b]: "c" })'))).toBeNull(); + expect(normalizeClass(parseExpression('({ a: true, b: false, c: !false, d: !!0 })'))).toBe('a c'); + }); +}); + +it('Composition API (standard)', () => { + const ast = parse(` +import { c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js'; +import { M as MkContainer } from './MkContainer-!~{03M}~.js'; +import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js'; +import './photoswipe-!~{003}~.js'; + +const _hoisted_1 = /* @__PURE__ */ createBaseVNode("i", { class: "ti ti-photo" }, null, -1); +const _sfc_main = /* @__PURE__ */ defineComponent({ + __name: "index.photos", + props: { + user: {} + }, + setup(__props) { + const props = __props; + let fetching = ref(true); + let images = ref([]); + function thumbnail(image) { + return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl; + } + onMounted(() => { + const image = [ + "image/jpeg", + "image/webp", + "image/avif", + "image/png", + "image/gif", + "image/apng", + "image/vnd.mozilla.apng" + ]; + api("users/notes", { + userId: props.user.id, + fileType: image, + excludeNsfw: defaultStore.state.nsfw !== "ignore", + limit: 10 + }).then((notes) => { + for (const note of notes) { + for (const file of note.files) { + images.value.push({ + note, + file + }); + } + } + fetching.value = false; + }); + }); + return (_ctx, _cache) => { + const _component_MkLoading = resolveComponent("MkLoading"); + const _component_MkA = resolveComponent("MkA"); + return openBlock(), createBlock(MkContainer, { + "max-height": 300, + foldable: true + }, { + icon: withCtx(() => [ + _hoisted_1 + ]), + header: withCtx(() => [ + createTextVNode(toDisplayString(unref(i18n).ts.images), 1) + ]), + default: withCtx(() => [ + createBaseVNode("div", { + class: normalizeClass(_ctx.$style.root) + }, [ + unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, { key: 0 })) : createCommentVNode("", true), + !unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", { + key: 1, + class: normalizeClass(_ctx.$style.stream) + }, [ + (openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), (image) => { + return openBlock(), createBlock(_component_MkA, { + key: image.note.id + image.file.id, + class: normalizeClass(_ctx.$style.img), + to: unref(notePage)(image.note) + }, { + default: withCtx(() => [ + createVNode(ImgWithBlurhash, { + hash: image.file.blurhash, + src: thumbnail(image.file), + title: image.file.name + }, null, 8, ["hash", "src", "title"]) + ]), + _: 2 + }, 1032, ["class", "to"]); + }), 128)) + ], 2)) : createCommentVNode("", true), + !unref(fetching) && unref(images).length == 0 ? (openBlock(), createElementBlock("p", { + key: 2, + class: normalizeClass(_ctx.$style.empty) + }, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true) + ], 2) + ]), + _: 1 + }); + }; + } +}); + +const root = "xenMW"; +const stream = "xaZzf"; +const img = "xtA8t"; +const empty = "xhYKj"; +const style0 = { + root: root, + stream: stream, + img: img, + empty: empty +}; + +const cssModules = { + "$style": style0 +}; +const index_photos = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]); + +export { index_photos as default }; +`.slice(1), { ecmaVersion: 'latest', sourceType: 'module' }); + unwindCssModuleClassName(ast); + expect(generate(ast)).toBe(` +import {c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js'; +import {M as MkContainer} from './MkContainer-!~{03M}~.js'; +import {b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode} from './vue-!~{002}~.js'; +import './photoswipe-!~{003}~.js'; +const _hoisted_1 = createBaseVNode("i", { + class: "ti ti-photo" +}, null, -1); +const _sfc_main = defineComponent({ + __name: "index.photos", + props: { + user: {} + }, + setup(__props) { + const props = __props; + let fetching = ref(true); + let images = ref([]); + function thumbnail(image) { + return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl; + } + onMounted(() => { + const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"]; + api("users/notes", { + userId: props.user.id, + fileType: image, + excludeNsfw: defaultStore.state.nsfw !== "ignore", + limit: 10 + }).then(notes => { + for (const note of notes) { + for (const file of note.files) { + images.value.push({ + note, + file + }); + } + } + fetching.value = false; + }); + }); + return (_ctx, _cache) => { + const _component_MkLoading = resolveComponent("MkLoading"); + const _component_MkA = resolveComponent("MkA"); + return (openBlock(), createBlock(MkContainer, { + "max-height": 300, + foldable: true + }, { + icon: withCtx(() => [_hoisted_1]), + header: withCtx(() => [createTextVNode(toDisplayString(unref(i18n).ts.images), 1)]), + default: withCtx(() => [createBaseVNode("div", { + class: "xenMW" + }, [unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, { + key: 0 + })) : createCommentVNode("", true), !unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", { + key: 1, + class: "xaZzf" + }, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), image => { + return (openBlock(), createBlock(_component_MkA, { + key: image.note.id + image.file.id, + class: "xtA8t", + to: unref(notePage)(image.note) + }, { + default: withCtx(() => [createVNode(ImgWithBlurhash, { + hash: image.file.blurhash, + src: thumbnail(image.file), + title: image.file.name + }, null, 8, ["hash", "src", "title"])]), + _: 2 + }, 1032, ["class", "to"])); + }), 128))], 2)) : createCommentVNode("", true), !unref(fetching) && unref(images).length == 0 ? (openBlock(), createElementBlock("p", { + key: 2, + class: "xhYKj" + }, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true)], 2)]), + _: 1 + })); + }; + } +}); +const root = "xenMW"; +const stream = "xaZzf"; +const img = "xtA8t"; +const empty = "xhYKj"; +const style0 = { + root: root, + stream: stream, + img: img, + empty: empty +}; +const cssModules = { + "$style": style0 +}; +const index_photos = _sfc_main; +export {index_photos as default}; +`.slice(1)); +}); + +it('Composition API (with `useCssModule()`)', () => { + const ast = parse(` +import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js'; +import { d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js'; + +function isDebuggerEnabled(id) { + try { + return localStorage.getItem(\`DEBUG_\${id}\`) !== null; + } catch { + return false; + } +} +function stackTraceInstances() { + let instance = getCurrentInstance(); + const stack = []; + while (instance) { + stack.push(instance); + instance = instance.parent; + } + return stack; +} + +const _sfc_main = defineComponent({ + props: { + items: { + type: Array, + required: true + }, + direction: { + type: String, + required: false, + default: "down" + }, + reversed: { + type: Boolean, + required: false, + default: false + }, + noGap: { + type: Boolean, + required: false, + default: false + }, + ad: { + type: Boolean, + required: false, + default: false + } + }, + setup(props, { slots, expose }) { + const $style = useCssModule(); + function getDateText(time) { + const date = new Date(time).getDate(); + const month = new Date(time).getMonth() + 1; + return i18n.t("monthAndDay", { + month: month.toString(), + day: date.toString() + }); + } + if (props.items.length === 0) + return; + const renderChildrenImpl = () => props.items.map((item, i) => { + if (!slots || !slots.default) + return; + const el = slots.default({ + item + })[0]; + if (el.key == null && item.id) + el.key = item.id; + if (i !== props.items.length - 1 && new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()) { + const separator = h("div", { + class: $style["separator"], + key: item.id + ":separator" + }, h("p", { + class: $style["date"] + }, [ + h("span", { + class: $style["date-1"] + }, [ + h("i", { + class: \`ti ti-chevron-up \${$style["date-1-icon"]}\` + }), + getDateText(item.createdAt) + ]), + h("span", { + class: $style["date-2"] + }, [ + getDateText(props.items[i + 1].createdAt), + h("i", { + class: \`ti ti-chevron-down \${$style["date-2-icon"]}\` + }) + ]) + ])); + return [el, separator]; + } else { + if (props.ad && item._shouldInsertAd_) { + return [h(MkAd, { + key: item.id + ":ad", + prefer: ["horizontal", "horizontal-big"] + }), el]; + } else { + return el; + } + } + }); + const renderChildren = () => { + const children = renderChildrenImpl(); + if (isDebuggerEnabled(6864)) { + const nodes = children.flatMap((node) => node ?? []); + const keys = new Set(nodes.map((node) => node.key)); + if (keys.size !== nodes.length) { + const id = crypto.randomUUID(); + const instances = stackTraceInstances(); + toast(instances.reduce((a, c) => \`\${a} at \${c.type.name}\`, \`[DEBUG_6864 (\${id})]: \${nodes.length - keys.size} duplicated keys found\`)); + console.warn({ id, debugId: 6864, stack: instances }); + } + } + return children; + }; + function onBeforeLeave(el) { + el.style.top = \`\${el.offsetTop}px\`; + el.style.left = \`\${el.offsetLeft}px\`; + } + function onLeaveCanceled(el) { + el.style.top = ""; + el.style.left = ""; + } + return () => h( + defaultStore.state.animation ? TransitionGroup : "div", + { + class: { + [$style["date-separated-list"]]: true, + [$style["date-separated-list-nogap"]]: props.noGap, + [$style["reversed"]]: props.reversed, + [$style["direction-down"]]: props.direction === "down", + [$style["direction-up"]]: props.direction === "up" + }, + ...defaultStore.state.animation ? { + name: "list", + tag: "div", + onBeforeLeave, + onLeaveCanceled + } : {} + }, + { default: renderChildren } + ); + } +}); + +const reversed = "xxiZh"; +const separator = "xxeDx"; +const date = "xxawD"; +const style0 = { + "date-separated-list": "xfKPa", + "date-separated-list-nogap": "xf9zr", + "direction-up": "x7AeO", + "direction-down": "xBIqc", + reversed: reversed, + separator: separator, + date: date, + "date-1": "xwtmh", + "date-1-icon": "xsNPa", + "date-2": "x1xvw", + "date-2-icon": "x9ZiG" +}; + +const cssModules = { + "$style": style0 +}; +const MkDateSeparatedList = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]); + +export { MkDateSeparatedList as M }; +`.slice(1), { ecmaVersion: 'latest', sourceType: 'module' }); + unwindCssModuleClassName(ast); + expect(generate(ast)).toBe(` +import {a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup} from './!~{002}~.js'; +import {d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js'; +function isDebuggerEnabled(id) { + try { + return localStorage.getItem(\`DEBUG_\${id}\`) !== null; + } catch { + return false; + } +} +function stackTraceInstances() { + let instance = getCurrentInstance(); + const stack = []; + while (instance) { + stack.push(instance); + instance = instance.parent; + } + return stack; +} +const _sfc_main = defineComponent({ + props: { + items: { + type: Array, + required: true + }, + direction: { + type: String, + required: false, + default: "down" + }, + reversed: { + type: Boolean, + required: false, + default: false + }, + noGap: { + type: Boolean, + required: false, + default: false + }, + ad: { + type: Boolean, + required: false, + default: false + } + }, + setup(props, {slots, expose}) { + const $style = useCssModule(); + function getDateText(time) { + const date = new Date(time).getDate(); + const month = new Date(time).getMonth() + 1; + return i18n.t("monthAndDay", { + month: month.toString(), + day: date.toString() + }); + } + if (props.items.length === 0) return; + const renderChildrenImpl = () => props.items.map((item, i) => { + if (!slots || !slots.default) return; + const el = slots.default({ + item + })[0]; + if (el.key == null && item.id) el.key = item.id; + if (i !== props.items.length - 1 && new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()) { + const separator = h("div", { + class: $style["separator"], + key: item.id + ":separator" + }, h("p", { + class: $style["date"] + }, [h("span", { + class: $style["date-1"] + }, [h("i", { + class: \`ti ti-chevron-up \${$style["date-1-icon"]}\` + }), getDateText(item.createdAt)]), h("span", { + class: $style["date-2"] + }, [getDateText(props.items[i + 1].createdAt), h("i", { + class: \`ti ti-chevron-down \${$style["date-2-icon"]}\` + })])])); + return [el, separator]; + } else { + if (props.ad && item._shouldInsertAd_) { + return [h(MkAd, { + key: item.id + ":ad", + prefer: ["horizontal", "horizontal-big"] + }), el]; + } else { + return el; + } + } + }); + const renderChildren = () => { + const children = renderChildrenImpl(); + if (isDebuggerEnabled(6864)) { + const nodes = children.flatMap(node => node ?? []); + const keys = new Set(nodes.map(node => node.key)); + if (keys.size !== nodes.length) { + const id = crypto.randomUUID(); + const instances = stackTraceInstances(); + toast(instances.reduce((a, c) => \`\${a} at \${c.type.name}\`, \`[DEBUG_6864 (\${id})]: \${nodes.length - keys.size} duplicated keys found\`)); + console.warn({ + id, + debugId: 6864, + stack: instances + }); + } + } + return children; + }; + function onBeforeLeave(el) { + el.style.top = \`\${el.offsetTop}px\`; + el.style.left = \`\${el.offsetLeft}px\`; + } + function onLeaveCanceled(el) { + el.style.top = ""; + el.style.left = ""; + } + return () => h(defaultStore.state.animation ? TransitionGroup : "div", { + class: { + [$style["date-separated-list"]]: true, + [$style["date-separated-list-nogap"]]: props.noGap, + [$style["reversed"]]: props.reversed, + [$style["direction-down"]]: props.direction === "down", + [$style["direction-up"]]: props.direction === "up" + }, + ...defaultStore.state.animation ? { + name: "list", + tag: "div", + onBeforeLeave, + onLeaveCanceled + } : {} + }, { + default: renderChildren + }); + } +}); +const reversed = "xxiZh"; +const separator = "xxeDx"; +const date = "xxawD"; +const style0 = { + "date-separated-list": "xfKPa", + "date-separated-list-nogap": "xf9zr", + "direction-up": "x7AeO", + "direction-down": "xBIqc", + reversed: reversed, + separator: separator, + date: date, + "date-1": "xwtmh", + "date-1-icon": "xsNPa", + "date-2": "x1xvw", + "date-2-icon": "x9ZiG" +}; +const cssModules = { + "$style": style0 +}; +const MkDateSeparatedList = _export_sfc(_sfc_main, [["__cssModules", cssModules]]); +export {MkDateSeparatedList as M}; +`.slice(1)); +}); diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts new file mode 100644 index 000000000..a18f0d904 --- /dev/null +++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts @@ -0,0 +1,275 @@ +import { generate } from 'astring'; +import * as estree from 'estree'; +import { walk } from '../node_modules/estree-walker/src/index.js'; +import type * as estreeWalker from 'estree-walker'; +import type { Plugin } from 'vite'; + +function isFalsyIdentifier(identifier: estree.Identifier): boolean { + return identifier.name === 'undefined' || identifier.name === 'NaN'; +} + +function normalizeClassWalker(tree: estree.Node): string | null { + if (tree.type === 'Identifier') return isFalsyIdentifier(tree) ? '' : null; + if (tree.type === 'Literal') return typeof tree.value === 'string' ? tree.value : ''; + if (tree.type === 'BinaryExpression') { + if (tree.operator !== '+') return null; + const left = normalizeClassWalker(tree.left); + const right = normalizeClassWalker(tree.right); + if (left === null || right === null) return null; + return `${left}${right}`; + } + if (tree.type === 'TemplateLiteral') { + if (tree.expressions.some((x) => x.type !== 'Literal' && (x.type !== 'Identifier' || !isFalsyIdentifier(x)))) return null; + return tree.quasis.reduce((a, c, i) => { + const v = i === tree.quasis.length - 1 ? '' : (tree.expressions[i] as Partial<estree.Literal>).value; + return a + c.value.raw + (typeof v === 'string' ? v : ''); + }, ''); + } + if (tree.type === 'ArrayExpression') { + const values = tree.elements.map((treeNode) => { + if (treeNode === null) return ''; + if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument); + return normalizeClassWalker(treeNode); + }); + if (values.some((x) => x === null)) return null; + return values.join(' '); + } + if (tree.type === 'ObjectExpression') { + const values = tree.properties.map((treeNode) => { + if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument); + let x = treeNode.value; + let inveted = false; + while (x.type === 'UnaryExpression' && x.operator === '!') { + x = x.argument; + inveted = !inveted; + } + if (x.type === 'Literal') { + if (inveted === !x.value) { + return treeNode.key.type === 'Identifier' ? treeNode.computed ? null : treeNode.key.name : treeNode.key.type === 'Literal' ? treeNode.key.value : ''; + } else { + return ''; + } + } + if (x.type === 'Identifier') { + if (inveted !== isFalsyIdentifier(x)) { + return ''; + } else { + return null; + } + } + return null; + }); + if (values.some((x) => x === null)) return null; + return values.join(' '); + } + console.error(`Unexpected node type: ${tree.type}`); + return null; +} + +export function normalizeClass(tree: estree.Node): string | null { + const walked = normalizeClassWalker(tree); + return walked && walked.replace(/^\s+|\s+(?=\s)|\s+$/g, ''); +} + +export function unwindCssModuleClassName(ast: estree.Node): void { + (walk as typeof estreeWalker.walk)(ast, { + enter(node, parent): void { + if (parent?.type !== 'Program') return; + if (node.type !== 'VariableDeclaration') return; + if (node.declarations.length !== 1) return; + if (node.declarations[0].id.type !== 'Identifier') return; + const name = node.declarations[0].id.name; + if (node.declarations[0].init?.type !== 'CallExpression') return; + if (node.declarations[0].init.callee.type !== 'Identifier') return; + if (node.declarations[0].init.callee.name !== '_export_sfc') return; + if (node.declarations[0].init.arguments.length !== 2) return; + if (node.declarations[0].init.arguments[0].type !== 'Identifier') return; + const ident = node.declarations[0].init.arguments[0].name; + if (!ident.startsWith('_sfc_main')) return; + if (node.declarations[0].init.arguments[1].type !== 'ArrayExpression') return; + if (node.declarations[0].init.arguments[1].elements.length === 0) return; + const __cssModulesIndex = node.declarations[0].init.arguments[1].elements.findIndex((x) => { + if (x?.type !== 'ArrayExpression') return false; + if (x.elements.length !== 2) return false; + if (x.elements[0]?.type !== 'Literal') return false; + if (x.elements[0].value !== '__cssModules') return false; + if (x.elements[1]?.type !== 'Identifier') return false; + return true; + }); + if (!~__cssModulesIndex) return; + const cssModuleForestName = ((node.declarations[0].init.arguments[1].elements[__cssModulesIndex] as estree.ArrayExpression).elements[1] as estree.Identifier).name; + const cssModuleForestNode = parent.body.find((x) => { + if (x.type !== 'VariableDeclaration') return false; + if (x.declarations.length !== 1) return false; + if (x.declarations[0].id.type !== 'Identifier') return false; + if (x.declarations[0].id.name !== cssModuleForestName) return false; + if (x.declarations[0].init?.type !== 'ObjectExpression') return false; + return true; + }) as unknown as estree.VariableDeclaration; + const moduleForest = new Map((cssModuleForestNode.declarations[0].init as estree.ObjectExpression).properties.flatMap((property) => { + if (property.type !== 'Property') return []; + if (property.key.type !== 'Literal') return []; + if (property.value.type !== 'Identifier') return []; + return [[property.key.value as string, property.value.name as string]]; + })); + const sfcMain = parent.body.find((x) => { + if (x.type !== 'VariableDeclaration') return false; + if (x.declarations.length !== 1) return false; + if (x.declarations[0].id.type !== 'Identifier') return false; + if (x.declarations[0].id.name !== ident) return false; + return true; + }) as unknown as estree.VariableDeclaration; + if (sfcMain.declarations[0].init?.type !== 'CallExpression') return; + if (sfcMain.declarations[0].init.callee.type !== 'Identifier') return; + if (sfcMain.declarations[0].init.callee.name !== 'defineComponent') return; + if (sfcMain.declarations[0].init.arguments.length !== 1) return; + if (sfcMain.declarations[0].init.arguments[0].type !== 'ObjectExpression') return; + const setup = sfcMain.declarations[0].init.arguments[0].properties.find((x) => { + if (x.type !== 'Property') return false; + if (x.key.type !== 'Identifier') return false; + if (x.key.name !== 'setup') return false; + return true; + }) as unknown as estree.Property; + if (setup.value.type !== 'FunctionExpression') return; + const render = setup.value.body.body.find((x) => { + if (x.type !== 'ReturnStatement') return false; + return true; + }) as unknown as estree.ReturnStatement; + if (render.argument?.type !== 'ArrowFunctionExpression') return; + if (render.argument.params.length !== 2) return; + const ctx = render.argument.params[0]; + if (ctx.type !== 'Identifier') return; + if (ctx.name !== '_ctx') return; + if (render.argument.body.type !== 'BlockStatement') return; + for (const [key, value] of moduleForest) { + const cssModuleTreeNode = parent.body.find((x) => { + if (x.type !== 'VariableDeclaration') return false; + if (x.declarations.length !== 1) return false; + if (x.declarations[0].id.type !== 'Identifier') return false; + if (x.declarations[0].id.name !== value) return false; + return true; + }) as unknown as estree.VariableDeclaration; + if (cssModuleTreeNode.declarations[0].init?.type !== 'ObjectExpression') return; + const moduleTree = new Map(cssModuleTreeNode.declarations[0].init.properties.flatMap((property) => { + if (property.type !== 'Property') return []; + const actualKey = property.key.type === 'Identifier' ? property.key.name : property.key.type === 'Literal' ? property.key.value : null; + if (typeof actualKey !== 'string') return []; + if (property.value.type === 'Literal') return [[actualKey, property.value.value as string]]; + if (property.value.type !== 'Identifier') return []; + const labelledValue = property.value.name; + const actualValue = parent.body.find((x) => { + if (x.type !== 'VariableDeclaration') return false; + if (x.declarations.length !== 1) return false; + if (x.declarations[0].id.type !== 'Identifier') return false; + if (x.declarations[0].id.name !== labelledValue) return false; + return true; + }) as unknown as estree.VariableDeclaration; + if (actualValue.declarations[0].init?.type !== 'Literal') return []; + return [[actualKey, actualValue.declarations[0].init.value as string]]; + })); + (walk as typeof estreeWalker.walk)(render.argument.body, { + enter(childNode) { + if (childNode.type !== 'MemberExpression') return; + if (childNode.object.type !== 'MemberExpression') return; + if (childNode.object.object.type !== 'Identifier') return; + if (childNode.object.object.name !== ctx.name) return; + if (childNode.object.property.type !== 'Identifier') return; + if (childNode.object.property.name !== key) return; + if (childNode.property.type !== 'Identifier') return; + const actualValue = moduleTree.get(childNode.property.name); + if (actualValue === undefined) return; + this.replace({ + type: 'Literal', + value: actualValue, + }); + }, + }); + (walk as typeof estreeWalker.walk)(render.argument.body, { + enter(childNode) { + if (childNode.type !== 'MemberExpression') return; + if (childNode.object.type !== 'MemberExpression') return; + if (childNode.object.object.type !== 'Identifier') return; + if (childNode.object.object.name !== ctx.name) return; + if (childNode.object.property.type !== 'Identifier') return; + if (childNode.object.property.name !== key) return; + if (childNode.property.type !== 'Identifier') return; + console.error(`Undefined style detected: ${key}.${childNode.property.name} (in ${name})`); + this.replace({ + type: 'Identifier', + name: 'undefined', + }); + }, + }); + (walk as typeof estreeWalker.walk)(render.argument.body, { + enter(childNode) { + if (childNode.type !== 'CallExpression') return; + if (childNode.callee.type !== 'Identifier') return; + if (childNode.callee.name !== 'normalizeClass') return; + if (childNode.arguments.length !== 1) return; + const normalized = normalizeClass(childNode.arguments[0]); + if (normalized === null) return; + this.replace({ + type: 'Literal', + value: normalized, + }); + }, + }); + } + if (node.declarations[0].init.arguments[1].elements.length === 1) { + this.replace({ + type: 'VariableDeclaration', + declarations: [{ + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: node.declarations[0].id.name, + }, + init: { + type: 'Identifier', + name: ident, + }, + }], + kind: 'const', + }); + } else { + this.replace({ + type: 'VariableDeclaration', + declarations: [{ + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: node.declarations[0].id.name, + }, + init: { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: '_export_sfc', + }, + arguments: [{ + type: 'Identifier', + name: ident, + }, { + type: 'ArrayExpression', + elements: node.declarations[0].init.arguments[1].elements.slice(0, __cssModulesIndex).concat(node.declarations[0].init.arguments[1].elements.slice(__cssModulesIndex + 1)), + }], + }, + }], + kind: 'const', + }); + } + }, + }); +} + +// eslint-disable-next-line import/no-default-export +export default function pluginUnwindCssModuleClassName(): Plugin { + return { + name: 'UnwindCssModuleClassName', + renderChunk(code): { code: string } { + const ast = this.parse(code) as unknown as estree.Node; + unwindCssModuleClassName(ast); + return { code: generate(ast) }; + }, + }; +} diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 5b4004d8e..506d18790 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -19,26 +19,28 @@ "@rollup/plugin-json": "6.0.0", "@rollup/plugin-replace": "5.0.2", "@rollup/pluginutils": "5.0.2", - "@syuilo/aiscript": "0.13.2", - "@tabler/icons-webfont": "2.17.0", - "@vitejs/plugin-vue": "4.2.2", - "@vue-macros/reactivity-transform": "0.3.6", - "@vue/compiler-sfc": "3.3.1", - "autosize": "5.0.2", - "blurhash": "2.0.5", - "broadcast-channel": "4.20.2", + "@syuilo/aiscript": "0.13.3", + "@tabler/icons-webfont": "2.21.0", + "@vitejs/plugin-vue": "4.2.3", + "@vue-macros/reactivity-transform": "0.3.9", + "@vue/compiler-sfc": "3.3.4", + "astring": "1.8.6", + "autosize": "6.0.1", + "broadcast-channel": "5.1.0", "browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3", + "buraha": "github:misskey-dev/buraha", "canvas-confetti": "1.6.0", "chart.js": "4.3.0", "chartjs-adapter-date-fns": "3.0.0", "chartjs-chart-matrix": "2.0.1", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.0.1", - "chromatic": "6.17.4", + "chromatic": "6.18.0", "compare-versions": "5.0.3", "cropperjs": "2.0.0-beta.2", "date-fns": "2.30.0", "escape-regexp": "0.0.1", + "estree-walker": "^3.0.3", "eventemitter3": "5.0.1", "gsap": "3.11.5", "idb-keyval": "6.2.1", @@ -53,7 +55,7 @@ "punycode": "2.3.0", "querystring": "0.2.1", "rndstr": "1.0.0", - "rollup": "3.21.6", + "rollup": "3.23.0", "s-age": "1.1.2", "sanitize-html": "2.10.0", "sass": "1.62.1", @@ -61,71 +63,70 @@ "strict-event-emitter-types": "2.0.0", "syuilo-password-strength": "0.0.1", "textarea-caret": "3.1.0", - "three": "0.151.3", + "three": "0.153.0", "throttle-debounce": "5.0.0", "tinycolor2": "1.6.0", "tsc-alias": "1.8.6", "tsconfig-paths": "4.2.0", "twemoji-parser": "14.0.0", - "typescript": "5.0.4", + "typescript": "5.1.3", "uuid": "9.0.0", "vanilla-tilt": "1.8.0", - "vite": "4.3.5", - "vue": "3.3.1", - "vue-plyr": "7.0.0", + "vite": "4.3.9", + "vue": "3.3.4", "vue-prism-editor": "2.0.0-alpha.2", "vuedraggable": "next" }, "devDependencies": { - "@storybook/addon-actions": "7.0.10", - "@storybook/addon-essentials": "7.0.10", - "@storybook/addon-interactions": "7.0.10", - "@storybook/addon-links": "7.0.10", - "@storybook/addon-storysource": "7.0.10", - "@storybook/addons": "7.0.10", - "@storybook/blocks": "7.0.10", - "@storybook/core-events": "7.0.10", + "@storybook/addon-actions": "7.0.18", + "@storybook/addon-essentials": "7.0.18", + "@storybook/addon-interactions": "7.0.18", + "@storybook/addon-links": "7.0.18", + "@storybook/addon-storysource": "7.0.18", + "@storybook/addons": "7.0.18", + "@storybook/blocks": "7.0.18", + "@storybook/core-events": "7.0.18", "@storybook/jest": "0.1.0", - "@storybook/manager-api": "7.0.10", - "@storybook/preview-api": "7.0.10", - "@storybook/react": "7.0.10", - "@storybook/react-vite": "7.0.10", + "@storybook/manager-api": "7.0.18", + "@storybook/preview-api": "7.0.18", + "@storybook/react": "7.0.18", + "@storybook/react-vite": "7.0.18", "@storybook/testing-library": "0.1.0", - "@storybook/theming": "7.0.10", - "@storybook/types": "7.0.10", - "@storybook/vue3": "7.0.10", - "@storybook/vue3-vite": "7.0.10", + "@storybook/theming": "7.0.18", + "@storybook/types": "7.0.18", + "@storybook/vue3": "7.0.18", + "@storybook/vue3-vite": "7.0.18", "@testing-library/jest-dom": "5.16.5", "@testing-library/vue": "7.0.0", "@types/escape-regexp": "0.0.1", "@types/estree": "1.0.1", "@types/gulp": "4.0.10", "@types/gulp-rename": "2.0.2", - "@types/matter-js": "0.18.3", + "@types/matter-js": "0.18.5", "@types/micromatch": "4.0.2", - "@types/node": "20.1.3", + "@types/node": "20.2.5", "@types/punycode": "2.1.0", "@types/sanitize-html": "2.9.0", "@types/seedrandom": "3.0.5", - "@types/testing-library__jest-dom": "^5.14.5", + "@types/testing-library__jest-dom": "^5.14.6", "@types/throttle-debounce": "5.0.0", "@types/tinycolor2": "1.4.3", "@types/uuid": "9.0.1", "@types/websocket": "1.0.5", "@types/ws": "8.5.4", - "@typescript-eslint/eslint-plugin": "5.59.5", - "@typescript-eslint/parser": "5.59.5", - "@vitest/coverage-c8": "0.31.0", - "@vue/runtime-core": "3.3.1", - "astring": "1.8.4", + "@typescript-eslint/eslint-plugin": "5.59.8", + "@typescript-eslint/parser": "5.59.8", + "@vitest/coverage-c8": "0.31.4", + "@vue/runtime-core": "3.3.4", + "acorn": "^8.8.2", "chokidar-cli": "3.0.0", "cross-env": "7.0.3", - "cypress": "12.12.0", - "eslint": "8.40.0", + "cypress": "12.13.0", + "eslint": "8.41.0", "eslint-plugin-import": "2.27.5", - "eslint-plugin-vue": "9.12.0", + "eslint-plugin-vue": "9.14.1", "fast-glob": "3.2.12", - "happy-dom": "9.16.0", + "happy-dom": "9.20.3", "micromatch": "3.1.10", "msw": "1.2.1", "msw-storybook-addon": "1.8.0", @@ -133,13 +134,13 @@ "react": "18.2.0", "react-dom": "18.2.0", "start-server-and-test": "2.0.0", - "storybook": "7.0.10", + "storybook": "7.0.18", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "summaly": "github:misskey-dev/summaly", "vite-plugin-turbosnap": "1.0.2", - "vitest": "0.31.0", + "vitest": "0.31.4", "vitest-fetch-mock": "0.2.2", - "vue-eslint-parser": "9.2.1", - "vue-tsc": "1.6.4" + "vue-eslint-parser": "9.3.0", + "vue-tsc": "1.6.5" } } diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts new file mode 100644 index 000000000..921c16176 --- /dev/null +++ b/packages/frontend/src/_boot_.ts @@ -0,0 +1,14 @@ +// https://vitejs.dev/config/build-options.html#build-modulepreload +import 'vite/modulepreload-polyfill'; + +import '@/style.scss'; +import { mainBoot } from './boot/main-boot'; +import { subBoot } from './boot/sub-boot'; + +const subBootPaths = ['/share', '/auth', '/miauth', '/signup-complete']; + +if (subBootPaths.some(i => location.pathname === i || location.pathname.startsWith(i + '/'))) { + subBoot(); +} else { + mainBoot(); +} diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index 9b104391d..4770f616a 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -3,11 +3,11 @@ import * as misskey from 'misskey-js'; import { showSuspendedDialog } from './scripts/show-suspended-dialog'; import { i18n } from './i18n'; import { miLocalStorage } from './local-storage'; +import { MenuButton } from './types/menu'; import { del, get, set } from '@/scripts/idb-proxy'; import { apiUrl } from '@/config'; import { waiting, api, popup, popupMenu, success, alert } from '@/os'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload'; -import { MenuButton } from './types/menu'; // TODO: 他のタブと永続化されたstateを同期 @@ -101,57 +101,57 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr 'Content-Type': 'application/json', }, }) - .then(res => new Promise<Account | { error: Record<string, any> }>((done2, fail2) => { - if (res.status >= 500 && res.status < 600) { + .then(res => new Promise<Account | { error: Record<string, any> }>((done2, fail2) => { + if (res.status >= 500 && res.status < 600) { // サーバーエラー(5xx)の場合をrejectとする // (認証エラーなど4xxはresolve) - return fail2(res); - } - res.json().then(done2, fail2); - })) - .then(async res => { - if (res.error) { - if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') { + return fail2(res); + } + res.json().then(done2, fail2); + })) + .then(async res => { + if (res.error) { + if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') { // SUSPENDED - if (forceShowDialog || $i && (token === $i.token || id === $i.id)) { - await showSuspendedDialog(); - } - } else if (res.error.id === 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a') { + if (forceShowDialog || $i && (token === $i.token || id === $i.id)) { + await showSuspendedDialog(); + } + } else if (res.error.id === 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a') { // USER_IS_DELETED // アカウントが削除されている - if (forceShowDialog || $i && (token === $i.token || id === $i.id)) { - await alert({ - type: 'error', - title: i18n.ts.accountDeleted, - text: i18n.ts.accountDeletedDescription, - }); - } - } else if (res.error.id === 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14') { + if (forceShowDialog || $i && (token === $i.token || id === $i.id)) { + await alert({ + type: 'error', + title: i18n.ts.accountDeleted, + text: i18n.ts.accountDeletedDescription, + }); + } + } else if (res.error.id === 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14') { // AUTHENTICATION_FAILED // トークンが無効化されていたりアカウントが削除されたりしている - if (forceShowDialog || $i && (token === $i.token || id === $i.id)) { + if (forceShowDialog || $i && (token === $i.token || id === $i.id)) { + await alert({ + type: 'error', + title: i18n.ts.tokenRevoked, + text: i18n.ts.tokenRevokedDescription, + }); + } + } else { await alert({ type: 'error', - title: i18n.ts.tokenRevoked, - text: i18n.ts.tokenRevokedDescription, + title: i18n.ts.failedToFetchAccountInformation, + text: JSON.stringify(res.error), }); } - } else { - await alert({ - type: 'error', - title: i18n.ts.failedToFetchAccountInformation, - text: JSON.stringify(res.error), - }); - } - // rejectかつ理由がtrueの場合、削除対象であることを示す - fail(true); - } else { - (res as Account).token = token; - done(res as Account); - } - }) - .catch(fail); + // rejectかつ理由がtrueの場合、削除対象であることを示す + fail(true); + } else { + (res as Account).token = token; + done(res as Account); + } + }) + .catch(fail); }); } @@ -305,3 +305,7 @@ export async function openAccountMenu(opts: { }); } } + +if (_DEV_) { + (window as any).$i = $i; +} diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts new file mode 100644 index 000000000..e1b12fe7d --- /dev/null +++ b/packages/frontend/src/boot/common.ts @@ -0,0 +1,262 @@ +import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent, App } from 'vue'; +import { compareVersions } from 'compare-versions'; +import widgets from '@/widgets'; +import directives from '@/directives'; +import components from '@/components'; +import { version, ui, lang, updateLocale } from '@/config'; +import { applyTheme } from '@/scripts/theme'; +import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; +import { i18n, updateI18n } from '@/i18n'; +import { confirm, alert, post, popup, toast } from '@/os'; +import { $i, refreshAccount, login, updateAccount, signout } from '@/account'; +import { defaultStore, ColdDeviceStorage } from '@/store'; +import { fetchInstance, instance } from '@/instance'; +import { deviceKind } from '@/scripts/device-kind'; +import { reloadChannel } from '@/scripts/unison-reload'; +import { reactionPicker } from '@/scripts/reaction-picker'; +import { getUrlWithoutLoginId } from '@/scripts/login-id'; +import { getAccountFromId } from '@/scripts/get-account-from-id'; +import { deckStore } from '@/ui/deck/deck-store'; +import { miLocalStorage } from '@/local-storage'; +import { fetchCustomEmojis } from '@/custom-emojis'; +import { mainRouter } from '@/router'; + +export async function common(createVue: () => App<Element>) { + console.info(`Misskey v${version}`); + + if (_DEV_) { + console.warn('Development mode!!!'); + + console.info(`vue ${vueVersion}`); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as any).$i = $i; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as any).$store = defaultStore; + + window.addEventListener('error', event => { + console.error(event); + /* + alert({ + type: 'error', + title: 'DEV: Unhandled error', + text: event.message + }); + */ + }); + + window.addEventListener('unhandledrejection', event => { + console.error(event); + /* + alert({ + type: 'error', + title: 'DEV: Unhandled promise rejection', + text: event.reason + }); + */ + }); + } + + const splash = document.getElementById('splash'); + // 念のためnullチェック(HTMLが古い場合があるため(そのうち消す)) + if (splash) splash.addEventListener('transitionend', () => { + splash.remove(); + }); + + let isClientUpdated = false; + + //#region クライアントが更新されたかチェック + const lastVersion = miLocalStorage.getItem('lastVersion'); + if (lastVersion !== version) { + miLocalStorage.setItem('lastVersion', version); + + // テーマリビルドするため + miLocalStorage.removeItem('theme'); + + try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため + if (lastVersion != null && compareVersions(version, lastVersion) === 1) { + isClientUpdated = true; + } + } catch (err) { /* empty */ } + } + //#endregion + + //#region Detect language & fetch translations + const localeVersion = miLocalStorage.getItem('localeVersion'); + const localeOutdated = (localeVersion == null || localeVersion !== version); + if (localeOutdated) { + const res = await window.fetch(`/assets/locales/${lang}.${version}.json`); + if (res.status === 200) { + const newLocale = await res.text(); + const parsedNewLocale = JSON.parse(newLocale); + miLocalStorage.setItem('locale', newLocale); + miLocalStorage.setItem('localeVersion', version); + updateLocale(parsedNewLocale); + updateI18n(parsedNewLocale); + } + } + //#endregion + + // タッチデバイスでCSSの:hoverを機能させる + document.addEventListener('touchend', () => {}, { passive: true }); + + // 一斉リロード + reloadChannel.addEventListener('message', path => { + if (path !== null) location.href = path; + else location.reload(); + }); + + // If mobile, insert the viewport meta tag + if (['smartphone', 'tablet'].includes(deviceKind)) { + const viewport = document.getElementsByName('viewport').item(0); + viewport.setAttribute('content', + `${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`); + } + + //#region Set lang attr + const html = document.documentElement; + html.setAttribute('lang', lang); + //#endregion + + await defaultStore.ready; + await deckStore.ready; + + const fetchInstanceMetaPromise = fetchInstance(); + + fetchInstanceMetaPromise.then(() => { + miLocalStorage.setItem('v', instance.version); + }); + + //#region loginId + const params = new URLSearchParams(location.search); + const loginId = params.get('loginId'); + + if (loginId) { + const target = getUrlWithoutLoginId(location.href); + + if (!$i || $i.id !== loginId) { + const account = await getAccountFromId(loginId); + if (account) { + await login(account.token, target); + } + } + + history.replaceState({ misskey: 'loginId' }, '', target); + } + //#endregion + + // NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため) + watch(defaultStore.reactiveState.darkMode, (darkMode) => { + applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme')); + }, { immediate: miLocalStorage.getItem('theme') == null }); + + const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme')); + const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme')); + + watch(darkTheme, (theme) => { + if (defaultStore.state.darkMode) { + applyTheme(theme); + } + }); + + watch(lightTheme, (theme) => { + if (!defaultStore.state.darkMode) { + applyTheme(theme); + } + }); + + //#region Sync dark mode + if (ColdDeviceStorage.get('syncDeviceDarkMode')) { + defaultStore.set('darkMode', isDeviceDarkmode()); + } + + window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => { + if (ColdDeviceStorage.get('syncDeviceDarkMode')) { + defaultStore.set('darkMode', mql.matches); + } + }); + //#endregion + + fetchInstanceMetaPromise.then(() => { + if (defaultStore.state.themeInitial) { + if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme)); + if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme)); + defaultStore.set('themeInitial', false); + } + }); + + watch(defaultStore.reactiveState.useBlurEffectForModal, v => { + document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none'); + }, { immediate: true }); + + watch(defaultStore.reactiveState.useBlurEffect, v => { + if (v) { + document.documentElement.style.removeProperty('--blur'); + } else { + document.documentElement.style.setProperty('--blur', 'none'); + } + }, { immediate: true }); + + //#region Fetch user + if ($i && $i.token) { + if (_DEV_) { + console.log('account cache found. refreshing...'); + } + + refreshAccount(); + } + //#endregion + + try { + await fetchCustomEmojis(); + } catch (err) { /* empty */ } + + const app = createVue(); + + if (_DEV_) { + app.config.performance = true; + } + + widgets(app); + directives(app); + components(app); + + // https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 + // なぜか2回実行されることがあるため、mountするdivを1つに制限する + const rootEl = ((): HTMLElement => { + const MISSKEY_MOUNT_DIV_ID = 'misskey_app'; + + const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID); + + if (currentRoot) { + console.warn('multiple import detected'); + return currentRoot; + } + + const root = document.createElement('div'); + root.id = MISSKEY_MOUNT_DIV_ID; + document.body.appendChild(root); + return root; + })(); + + app.mount(rootEl); + + // boot.jsのやつを解除 + window.onerror = null; + window.onunhandledrejection = null; + + removeSplash(); + + return { + isClientUpdated, + app, + }; +} + +function removeSplash() { + const splash = document.getElementById('splash'); + if (splash) { + splash.style.opacity = '0'; + splash.style.pointerEvents = 'none'; + } +} diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts new file mode 100644 index 000000000..76e8c5072 --- /dev/null +++ b/packages/frontend/src/boot/main-boot.ts @@ -0,0 +1,254 @@ +import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; +import { common } from './common'; +import { version, ui, lang, updateLocale } from '@/config'; +import { i18n, updateI18n } from '@/i18n'; +import { confirm, alert, post, popup, toast } from '@/os'; +import { useStream } from '@/stream'; +import * as sound from '@/scripts/sound'; +import { $i, refreshAccount, login, updateAccount, signout } from '@/account'; +import { defaultStore, ColdDeviceStorage } from '@/store'; +import { makeHotkey } from '@/scripts/hotkey'; +import { reactionPicker } from '@/scripts/reaction-picker'; +import { miLocalStorage } from '@/local-storage'; +import { claimAchievement, claimedAchievements } from '@/scripts/achievements'; +import { mainRouter } from '@/router'; +import { initializeSw } from '@/scripts/initialize-sw'; + +export async function mainBoot() { + const { isClientUpdated } = await common(() => createApp( + new URLSearchParams(window.location.search).has('zen') || (ui === 'deck' && location.pathname !== '/') ? defineAsyncComponent(() => import('@/ui/zen.vue')) : + !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : + ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : + ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : + defineAsyncComponent(() => import('@/ui/universal.vue')), + )); + + reactionPicker.init(); + + if (isClientUpdated && $i) { + popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed'); + } + + const stream = useStream(); + + let reloadDialogShowing = false; + stream.on('_disconnected_', async () => { + if (defaultStore.state.serverDisconnectedBehavior === 'reload') { + location.reload(); + } else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') { + if (reloadDialogShowing) return; + reloadDialogShowing = true; + const { canceled } = await confirm({ + type: 'warning', + title: i18n.ts.disconnectedFromServer, + text: i18n.ts.reloadConfirm, + }); + reloadDialogShowing = false; + if (!canceled) { + location.reload(); + } + } + }); + + for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { + import('../plugin').then(async ({ install }) => { + // Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740 + await new Promise(r => setTimeout(r, 0)); + install(plugin); + }); + } + + const hotkeys = { + 'd': (): void => { + defaultStore.set('darkMode', !defaultStore.state.darkMode); + }, + 's': (): void => { + mainRouter.push('/search'); + }, + }; + + if ($i) { + // only add post shortcuts if logged in + hotkeys['p|n'] = post; + + defaultStore.loaded.then(() => { + if (defaultStore.state.accountSetupWizard !== -1) { + popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed'); + } + }); + + if ($i.isDeleted) { + alert({ + type: 'warning', + text: i18n.ts.accountDeletionInProgress, + }); + } + + const now = new Date(); + const m = now.getMonth() + 1; + const d = now.getDate(); + + if ($i.birthday) { + const bm = parseInt($i.birthday.split('-')[1]); + const bd = parseInt($i.birthday.split('-')[2]); + if (m === bm && d === bd) { + claimAchievement('loggedInOnBirthday'); + } + } + + if (m === 1 && d === 1) { + claimAchievement('loggedInOnNewYearsDay'); + } + + if ($i.loggedInDays >= 3) claimAchievement('login3'); + if ($i.loggedInDays >= 7) claimAchievement('login7'); + if ($i.loggedInDays >= 15) claimAchievement('login15'); + if ($i.loggedInDays >= 30) claimAchievement('login30'); + if ($i.loggedInDays >= 60) claimAchievement('login60'); + if ($i.loggedInDays >= 100) claimAchievement('login100'); + if ($i.loggedInDays >= 200) claimAchievement('login200'); + if ($i.loggedInDays >= 300) claimAchievement('login300'); + if ($i.loggedInDays >= 400) claimAchievement('login400'); + if ($i.loggedInDays >= 500) claimAchievement('login500'); + if ($i.loggedInDays >= 600) claimAchievement('login600'); + if ($i.loggedInDays >= 700) claimAchievement('login700'); + if ($i.loggedInDays >= 800) claimAchievement('login800'); + if ($i.loggedInDays >= 900) claimAchievement('login900'); + if ($i.loggedInDays >= 1000) claimAchievement('login1000'); + + if ($i.notesCount > 0) claimAchievement('notes1'); + if ($i.notesCount >= 10) claimAchievement('notes10'); + if ($i.notesCount >= 100) claimAchievement('notes100'); + if ($i.notesCount >= 500) claimAchievement('notes500'); + if ($i.notesCount >= 1000) claimAchievement('notes1000'); + if ($i.notesCount >= 5000) claimAchievement('notes5000'); + if ($i.notesCount >= 10000) claimAchievement('notes10000'); + if ($i.notesCount >= 20000) claimAchievement('notes20000'); + if ($i.notesCount >= 30000) claimAchievement('notes30000'); + if ($i.notesCount >= 40000) claimAchievement('notes40000'); + if ($i.notesCount >= 50000) claimAchievement('notes50000'); + if ($i.notesCount >= 60000) claimAchievement('notes60000'); + if ($i.notesCount >= 70000) claimAchievement('notes70000'); + if ($i.notesCount >= 80000) claimAchievement('notes80000'); + if ($i.notesCount >= 90000) claimAchievement('notes90000'); + if ($i.notesCount >= 100000) claimAchievement('notes100000'); + + if ($i.followersCount > 0) claimAchievement('followers1'); + if ($i.followersCount >= 10) claimAchievement('followers10'); + if ($i.followersCount >= 50) claimAchievement('followers50'); + if ($i.followersCount >= 100) claimAchievement('followers100'); + if ($i.followersCount >= 300) claimAchievement('followers300'); + if ($i.followersCount >= 500) claimAchievement('followers500'); + if ($i.followersCount >= 1000) claimAchievement('followers1000'); + + if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) { + claimAchievement('passedSinceAccountCreated1'); + } + if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) { + claimAchievement('passedSinceAccountCreated2'); + } + if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) { + claimAchievement('passedSinceAccountCreated3'); + } + + if (claimedAchievements.length >= 30) { + claimAchievement('collectAchievements30'); + } + + window.setInterval(() => { + if (Math.floor(Math.random() * 20000) === 0) { + claimAchievement('justPlainLucky'); + } + }, 1000 * 10); + + window.setTimeout(() => { + claimAchievement('client30min'); + }, 1000 * 60 * 30); + + window.setTimeout(() => { + claimAchievement('client60min'); + }, 1000 * 60 * 60); + + const lastUsed = miLocalStorage.getItem('lastUsed'); + if (lastUsed) { + const lastUsedDate = parseInt(lastUsed, 10); + // 二時間以上前なら + if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) { + toast(i18n.t('welcomeBackWithName', { + name: $i.name || $i.username, + })); + } + } + miLocalStorage.setItem('lastUsed', Date.now().toString()); + + const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt'); + const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo'); + if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) { + if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) { + popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed'); + } + } + + if ('Notification' in window) { + // 許可を得ていなかったらリクエスト + if (Notification.permission === 'default') { + Notification.requestPermission(); + } + } + + const main = markRaw(stream.useChannel('main', null, 'System')); + + // 自分の情報が更新されたとき + main.on('meUpdated', i => { + updateAccount(i); + }); + + main.on('readAllNotifications', () => { + updateAccount({ hasUnreadNotification: false }); + }); + + main.on('unreadNotification', () => { + updateAccount({ hasUnreadNotification: true }); + }); + + main.on('unreadMention', () => { + updateAccount({ hasUnreadMentions: true }); + }); + + main.on('readAllUnreadMentions', () => { + updateAccount({ hasUnreadMentions: false }); + }); + + main.on('unreadSpecifiedNote', () => { + updateAccount({ hasUnreadSpecifiedNotes: true }); + }); + + main.on('readAllUnreadSpecifiedNotes', () => { + updateAccount({ hasUnreadSpecifiedNotes: false }); + }); + + main.on('readAllAntennas', () => { + updateAccount({ hasUnreadAntenna: false }); + }); + + main.on('unreadAntenna', () => { + updateAccount({ hasUnreadAntenna: true }); + sound.play('antenna'); + }); + + main.on('readAllAnnouncements', () => { + updateAccount({ hasUnreadAnnouncement: false }); + }); + + // トークンが再生成されたとき + // このままではMisskeyが利用できないので強制的にサインアウトさせる + main.on('myTokenRegenerated', () => { + signout(); + }); + } + + // shortcut + document.addEventListener('keydown', makeHotkey(hotkeys)); + + initializeSw(); +} diff --git a/packages/frontend/src/boot/sub-boot.ts b/packages/frontend/src/boot/sub-boot.ts new file mode 100644 index 000000000..c2664f6c1 --- /dev/null +++ b/packages/frontend/src/boot/sub-boot.ts @@ -0,0 +1,8 @@ +import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; +import { common } from './common'; + +export async function subBoot() { + const { isClientUpdated } = await common(() => createApp( + defineAsyncComponent(() => import('@/ui/minimum.vue')), + )); +} diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue index 9f2bf9933..48236782d 100644 --- a/packages/frontend/src/components/MkAbuseReportWindow.vue +++ b/packages/frontend/src/components/MkAbuseReportWindow.vue @@ -1,5 +1,5 @@ <template> -<MkWindow ref="uiWindow" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')"> +<MkWindow ref="uiWindow" :initialWidth="400" :initialHeight="500" :canResize="true" @closed="emit('closed')"> <template #header> <i class="ti ti-exclamation-circle" style="margin-right: 0.5em;"></i> <I18n :src="i18n.ts.reportAbuseOf" tag="span"> @@ -8,8 +8,8 @@ </template> </I18n> </template> - <MkSpacer :margin-min="20" :margin-max="28"> - <div class="dpvffvvy _gaps_m"> + <MkSpacer :marginMin="20" :marginMax="28"> + <div class="_gaps_m" :class="$style.root"> <div class=""> <MkTextarea v-model="comment"> <template #label>{{ i18n.ts.details }}</template> @@ -60,8 +60,8 @@ function send() { } </script> -<style lang="scss" scoped> -.dpvffvvy { +<style lang="scss" module> +.root { --root-margin: 16px; } </style> diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue index b02bfdc2b..bc07b9ba5 100644 --- a/packages/frontend/src/components/MkAccountMoved.vue +++ b/packages/frontend/src/components/MkAccountMoved.vue @@ -7,11 +7,11 @@ </template> <script lang="ts" setup> +import { ref } from 'vue'; +import { UserLite } from 'misskey-js/built/entities'; import MkMention from './MkMention.vue'; import { i18n } from '@/i18n'; import { host as localHost } from '@/config'; -import { ref } from 'vue'; -import { UserLite } from 'misskey-js/built/entities'; import { api } from '@/os'; const user = ref<UserLite>(); diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue index d30037dcf..3fdb261da 100644 --- a/packages/frontend/src/components/MkAchievements.vue +++ b/packages/frontend/src/components/MkAchievements.vue @@ -3,7 +3,14 @@ <div v-if="achievements" :class="$style.root"> <div v-for="achievement in achievements" :key="achievement" :class="$style.achievement" class="_panel"> <div :class="$style.icon"> - <div :class="[$style.iconFrame, $style['iconFrame_' + ACHIEVEMENT_BADGES[achievement.name].frame]]"> + <div + :class="[$style.iconFrame, { + [$style.iconFrame_bronze]: ACHIEVEMENT_BADGES[achievement.name].frame === 'bronze', + [$style.iconFrame_silver]: ACHIEVEMENT_BADGES[achievement.name].frame === 'silver', + [$style.iconFrame_gold]: ACHIEVEMENT_BADGES[achievement.name].frame === 'gold', + [$style.iconFrame_platinum]: ACHIEVEMENT_BADGES[achievement.name].frame === 'platinum', + }]" + > <div :class="[$style.iconInner]" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg }"> <img :class="$style.iconImg" :src="ACHIEVEMENT_BADGES[achievement.name].img"> </div> diff --git a/packages/frontend/src/components/MkAnalogClock.stories.impl.ts b/packages/frontend/src/components/MkAnalogClock.stories.impl.ts index e7fbb4728..0aebdccf4 100644 --- a/packages/frontend/src/components/MkAnalogClock.stories.impl.ts +++ b/packages/frontend/src/components/MkAnalogClock.stories.impl.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { StoryObj } from '@storybook/vue3'; +import isChromatic from 'chromatic/isChromatic'; import MkAnalogClock from './MkAnalogClock.vue'; -import isChromatic from 'chromatic'; export const Default = { render(args) { return { diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue index f12020f81..05caffe7d 100644 --- a/packages/frontend/src/components/MkAnalogClock.vue +++ b/packages/frontend/src/components/MkAnalogClock.vue @@ -39,6 +39,7 @@ --> <line + ref="sLine" :class="[$style.s, { [$style.animate]: !disableSAnimate && sAnimation !== 'none', [$style.elastic]: sAnimation === 'elastic', [$style.easeOut]: sAnimation === 'easeOut' }]" :x1="5 - (0 * (sHandLengthRatio * handsTailLength))" :y1="5 + (1 * (sHandLengthRatio * handsTailLength))" @@ -73,9 +74,10 @@ </template> <script lang="ts" setup> -import { computed, onMounted, onBeforeUnmount } from 'vue'; +import { computed, onMounted, onBeforeUnmount, ref } from 'vue'; import tinycolor from 'tinycolor2'; import { globalEvents } from '@/events.js'; +import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js'; // https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles const angleDiff = (a: number, b: number) => { @@ -145,6 +147,7 @@ let mAngle = $ref<number>(0); let sAngle = $ref<number>(0); let disableSAnimate = $ref(false); let sOneRound = false; +const sLine = ref<SVGPathElement>(); function tick() { const now = props.now(); @@ -160,17 +163,21 @@ function tick() { } hAngle = Math.PI * (h % (props.twentyfour ? 24 : 12) + (m + s / 60) / 60) / (props.twentyfour ? 12 : 6); mAngle = Math.PI * (m + s / 60) / 30; - if (sOneRound) { // 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない) + if (sOneRound && sLine.value) { // 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない) sAngle = Math.PI * 60 / 30; - window.setTimeout(() => { + defaultIdlingRenderScheduler.delete(tick); + sLine.value.addEventListener('transitionend', () => { disableSAnimate = true; - window.setTimeout(() => { + requestAnimationFrame(() => { sAngle = 0; - window.setTimeout(() => { + requestAnimationFrame(() => { disableSAnimate = false; - }, 100); - }, 100); - }, 700); + if (enabled) { + defaultIdlingRenderScheduler.add(tick); + } + }); + }); + }, { once: true }); } else { sAngle = Math.PI * s / 30; } @@ -194,20 +201,13 @@ function calcColors() { calcColors(); onMounted(() => { - const update = () => { - if (enabled) { - tick(); - window.setTimeout(update, 1000); - } - }; - update(); - + defaultIdlingRenderScheduler.add(tick); globalEvents.on('themeChanged', calcColors); }); onBeforeUnmount(() => { enabled = false; - + defaultIdlingRenderScheduler.delete(tick); globalEvents.off('themeChanged', calcColors); }); </script> diff --git a/packages/frontend/src/components/MkAnimBg.vue b/packages/frontend/src/components/MkAnimBg.vue new file mode 100644 index 000000000..575ea7c5e --- /dev/null +++ b/packages/frontend/src/components/MkAnimBg.vue @@ -0,0 +1,243 @@ +<template> +<canvas ref="canvasEl" style="width: 100%; height: 100%; pointer-events: none;"></canvas> +</template> + +<script lang="ts" setup> +import { onMounted, onUnmounted, shallowRef } from 'vue'; +import isChromatic from 'chromatic/isChromatic'; + +const canvasEl = shallowRef<HTMLCanvasElement>(); + +const props = withDefaults(defineProps<{ + scale?: number; + focus?: number; +}>(), { + scale: 1.0, + focus: 1.0, +}); + +function loadShader(gl, type, source) { + const shader = gl.createShader(type); + + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + alert( + `falied to compile shader: ${gl.getShaderInfoLog(shader)}`, + ); + gl.deleteShader(shader); + return null; + } + + return shader; +} + +function initShaderProgram(gl, vsSource, fsSource) { + const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource); + const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource); + + const shaderProgram = gl.createProgram(); + gl.attachShader(shaderProgram, vertexShader); + gl.attachShader(shaderProgram, fragmentShader); + gl.linkProgram(shaderProgram); + + if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { + alert( + `failed to init shader: ${gl.getProgramInfoLog( + shaderProgram, + )}`, + ); + return null; + } + + return shaderProgram; +} + +let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null; + +onMounted(() => { + const canvas = canvasEl.value!; + canvas.width = canvas.offsetWidth; + canvas.height = canvas.offsetHeight; + + const gl = canvas.getContext('webgl', { premultipliedAlpha: true }); + if (gl == null) return; + + gl.clearColor(0.0, 0.0, 0.0, 0.0); + gl.clear(gl.COLOR_BUFFER_BIT); + + const positionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + + const shaderProgram = initShaderProgram(gl, ` + attribute vec2 vertex; + + uniform vec2 u_scale; + + varying vec2 v_pos; + + void main() { + gl_Position = vec4(vertex, 0.0, 1.0); + v_pos = vertex / u_scale; + } + `, ` + precision mediump float; + + vec3 mod289(vec3 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; + } + + vec2 mod289(vec2 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; + } + + vec3 permute(vec3 x) { + return mod289(((x*34.0)+1.0)*x); + } + + float snoise(vec2 v) { + const vec4 C = vec4(0.211324865405187, + 0.366025403784439, + -0.577350269189626, + 0.024390243902439); + + vec2 i = floor(v + dot(v, C.yy) ); + vec2 x0 = v - i + dot(i, C.xx); + + vec2 i1; + i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); + vec4 x12 = x0.xyxy + C.xxzz; + x12.xy -= i1; + + i = mod289(i); + vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) + + i.x + vec3(0.0, i1.x, 1.0 )); + + vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0); + m = m*m ; + m = m*m ; + + vec3 x = 2.0 * fract(p * C.www) - 1.0; + vec3 h = abs(x) - 0.5; + vec3 ox = floor(x + 0.5); + vec3 a0 = x - ox; + + m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ); + + vec3 g; + g.x = a0.x * x0.x + h.x * x0.y; + g.yz = a0.yz * x12.xz + h.yz * x12.yw; + return 130.0 * dot(m, g); + } + + uniform float u_time; + uniform vec2 u_resolution; + uniform float u_spread; + uniform float u_speed; + uniform float u_warp; + uniform float u_focus; + uniform float u_itensity; + + varying vec2 v_pos; + + float circle( in vec2 _pos, in vec2 _origin, in float _radius ) { + float SPREAD = 0.7 * u_spread; + float SPEED = 0.00055 * u_speed; + float WARP = 1.5 * u_warp; + float FOCUS = 1.15 * u_focus; + + vec2 dist = _pos - _origin; + + float distortion = snoise( vec2( + _pos.x * 1.587 * WARP + u_time * SPEED * 0.5, + _pos.y * 1.192 * WARP + u_time * SPEED * 0.3 + ) ) * 0.5 + 0.5; + + float feather = 0.01 + SPREAD * pow( distortion, FOCUS ); + + return 1.0 - smoothstep( + _radius - ( _radius * feather ), + _radius + ( _radius * feather ), + dot( dist, dist ) * 4.0 + ); + } + + void main() { + vec3 green = vec3( 1.0 ) - vec3( 153.0 / 255.0, 211.0 / 255.0, 221.0 / 255.0 ); + vec3 purple = vec3( 1.0 ) - vec3( 195.0 / 255.0, 165.0 / 255.0, 242.0 / 255.0 ); + vec3 orange = vec3( 1.0 ) - vec3( 255.0 / 255.0, 156.0 / 255.0, 136.0 / 255.0 ); + + float ratio = u_resolution.x / u_resolution.y; + + vec2 uv = vec2( v_pos.x, v_pos.y / ratio ) * 0.5 + 0.5; + + vec3 color = vec3( 0.0 ); + + float greenMix = snoise( v_pos * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5; + float purpleMix = snoise( v_pos * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5; + float orangeMix = snoise( v_pos * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5; + + float alphaOne = 0.35 + 0.65 * pow( snoise( vec2( u_time * 0.00012, uv.x ) ) * 0.5 + 0.5, 1.2 ); + float alphaTwo = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 1561.0 ) * 0.00014, uv.x ) ) * 0.5 + 0.5, 1.2 ); + float alphaThree = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 3917.0 ) * 0.00013, uv.x ) ) * 0.5 + 0.5, 1.2 ); + + color += vec3( circle( uv, vec2( 0.22 + sin( u_time * 0.000201 ) * 0.06, 0.80 + cos( u_time * 0.000151 ) * 0.06 ), 0.15 ) ) * alphaOne * ( purple * purpleMix + orange * orangeMix ); + color += vec3( circle( uv, vec2( 0.90 + cos( u_time * 0.000166 ) * 0.06, 0.42 + sin( u_time * 0.000138 ) * 0.06 ), 0.18 ) ) * alphaTwo * ( green * greenMix + purple * purpleMix ); + color += vec3( circle( uv, vec2( 0.19 + sin( u_time * 0.000112 ) * 0.06, 0.25 + sin( u_time * 0.000192 ) * 0.06 ), 0.09 ) ) * alphaThree * ( orange * orangeMix ); + + color *= u_itensity + 1.0 * pow( snoise( vec2( v_pos.y + u_time * 0.00013, v_pos.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 ); + + vec3 inverted = vec3( 1.0 ) - color; + gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) ); + } + `); + + gl.useProgram(shaderProgram); + const u_resolution = gl.getUniformLocation(shaderProgram, 'u_resolution'); + const u_time = gl.getUniformLocation(shaderProgram, 'u_time'); + const u_spread = gl.getUniformLocation(shaderProgram, 'u_spread'); + const u_speed = gl.getUniformLocation(shaderProgram, 'u_speed'); + const u_warp = gl.getUniformLocation(shaderProgram, 'u_warp'); + const u_focus = gl.getUniformLocation(shaderProgram, 'u_focus'); + const u_itensity = gl.getUniformLocation(shaderProgram, 'u_itensity'); + const u_scale = gl.getUniformLocation(shaderProgram, 'u_scale'); + gl.uniform2fv(u_resolution, [canvas.width, canvas.height]); + gl.uniform1f(u_spread, 1.0); + gl.uniform1f(u_speed, 1.0); + gl.uniform1f(u_warp, 1.0); + gl.uniform1f(u_focus, props.focus); + gl.uniform1f(u_itensity, 0.5); + gl.uniform2fv(u_scale, [props.scale, props.scale]); + + const vertex = gl.getAttribLocation(shaderProgram, 'vertex'); + gl.enableVertexAttribArray(vertex); + gl.vertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0); + + const vertices = [1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0]; + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.DYNAMIC_DRAW); + + if (isChromatic()) { + gl!.uniform1f(u_time, 0); + gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4); + } else { + function render(timeStamp) { + gl!.uniform1f(u_time, timeStamp); + gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4); + + handle = window.requestAnimationFrame(render); + } + + handle = window.requestAnimationFrame(render); + } +}); + +onUnmounted(() => { + if (handle) { + window.cancelAnimationFrame(handle); + } +}); +</script> + +<style lang="scss" module> +</style> diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue index 6ade5316c..8bfcfa6aa 100644 --- a/packages/frontend/src/components/MkAsUi.vue +++ b/packages/frontend/src/components/MkAsUi.vue @@ -11,29 +11,29 @@ <div v-else-if="c.type === 'buttons'" class="_buttons" :style="{ justifyContent: align }"> <MkButton v-for="button in c.buttons" :primary="button.primary" :rounded="button.rounded" :disabled="button.disabled" inline :small="size === 'small'" @click="button.onClick">{{ button.text }}</MkButton> </div> - <MkSwitch v-else-if="c.type === 'switch'" :model-value="valueForSwitch" @update:model-value="onSwitchUpdate"> + <MkSwitch v-else-if="c.type === 'switch'" :modelValue="valueForSwitch" @update:modelValue="onSwitchUpdate"> <template v-if="c.label" #label>{{ c.label }}</template> <template v-if="c.caption" #caption>{{ c.caption }}</template> </MkSwitch> - <MkTextarea v-else-if="c.type === 'textarea'" :model-value="c.default" @update:model-value="c.onInput"> + <MkTextarea v-else-if="c.type === 'textarea'" :modelValue="c.default" @update:modelValue="c.onInput"> <template v-if="c.label" #label>{{ c.label }}</template> <template v-if="c.caption" #caption>{{ c.caption }}</template> </MkTextarea> - <MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :model-value="c.default" @update:model-value="c.onInput"> + <MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :modelValue="c.default" @update:modelValue="c.onInput"> <template v-if="c.label" #label>{{ c.label }}</template> <template v-if="c.caption" #caption>{{ c.caption }}</template> </MkInput> - <MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :model-value="c.default" type="number" @update:model-value="c.onInput"> + <MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :modelValue="c.default" type="number" @update:modelValue="c.onInput"> <template v-if="c.label" #label>{{ c.label }}</template> <template v-if="c.caption" #caption>{{ c.caption }}</template> </MkInput> - <MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :model-value="c.default" @update:model-value="c.onChange"> + <MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="c.default" @update:modelValue="c.onChange"> <template v-if="c.label" #label>{{ c.label }}</template> <template v-if="c.caption" #caption>{{ c.caption }}</template> <option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option> </MkSelect> <MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" inline @click="openPostForm">{{ c.text }}</MkButton> - <MkFolder v-else-if="c.type === 'folder'" :default-open="c.opened"> + <MkFolder v-else-if="c.type === 'folder'" :defaultOpen="c.opened"> <template #label>{{ c.title }}</template> <template v-for="child in c.children" :key="child"> <MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/> diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 663c57623..fd892d817 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -10,7 +10,7 @@ </li> <li tabindex="-1" :class="$style.item" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li> </ol> - <ol v-else-if="hashtags.length > 0" ref="suggests" :class="[$style.list, $style.hashtags]"> + <ol v-else-if="hashtags.length > 0" ref="suggests" :class="$style.list"> <li v-for="hashtag in hashtags" tabindex="-1" :class="$style.item" @click="complete(type, hashtag)" @keydown="onKeydown"> <span class="name">{{ hashtag }}</span> </li> @@ -42,7 +42,7 @@ import { acct } from '@/filters/user'; import * as os from '@/os'; import { MFM_TAGS } from '@/scripts/mfm-tags'; import { defaultStore } from '@/store'; -import { emojilist } from '@/scripts/emojilist'; +import { emojilist, getEmojiName } from '@/scripts/emojilist'; import { i18n } from '@/i18n'; import { miLocalStorage } from '@/local-storage'; import { customEmojis } from '@/custom-emojis'; @@ -71,14 +71,14 @@ const emojiDb = computed(() => { url: char2path(x.char), })); - for (const x of lib) { - if (x.keywords) { - for (const k of x.keywords) { + for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) { + for (const [emoji, keywords] of Object.entries(index)) { + for (const k of keywords) { unicodeEmojiDB.push({ - emoji: x.char, + emoji: emoji, name: k, - aliasOf: x.name, - url: char2path(x.char), + aliasOf: getEmojiName(emoji)!, + url: char2path(emoji), }); } } diff --git a/packages/frontend/src/components/MkAvatars.vue b/packages/frontend/src/components/MkAvatars.vue index 995a72e51..630620fc0 100644 --- a/packages/frontend/src/components/MkAvatars.vue +++ b/packages/frontend/src/components/MkAvatars.vue @@ -1,7 +1,7 @@ <template> <div> <div v-for="user in users" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;"> - <MkAvatar :user="user" style="width:32px;height:32px;" indicator link preview/> + <MkAvatar :user="user" style="width:32px; height:32px;" indicator link preview/> </div> </div> </template> diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index 0ddee34f0..16e44ec61 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -2,23 +2,23 @@ <button v-if="!link" ref="el" class="_button" - :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.asLike]: asLike }]" + :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]" :type="type" @click="emit('click', $event)" @mousedown="onMousedown" > - <div ref="ripples" :class="$style.ripples"></div> + <div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div> <div :class="$style.content"> <slot></slot> </div> </button> <MkA v-else class="_button" - :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.asLike]: asLike }]" + :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]" :to="to" @mousedown="onMousedown" > - <div ref="ripples" :class="$style.ripples"></div> + <div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div> <div :class="$style.content"> <slot></slot> </div> @@ -26,9 +26,7 @@ </template> <script lang="ts" setup> -import { nextTick, onMounted, useCssModule } from 'vue'; - -const $style = useCssModule(); +import { nextTick, onMounted } from 'vue'; const props = defineProps<{ type?: 'button' | 'submit' | 'reset'; @@ -44,6 +42,7 @@ const props = defineProps<{ full?: boolean; small?: boolean; large?: boolean; + transparent?: boolean; asLike?: boolean; }>(); @@ -80,7 +79,7 @@ function onMousedown(evt: MouseEvent): void { const rect = target.getBoundingClientRect(); const ripple = document.createElement('div'); - ripple.classList.add($style.ripple); + ripple.classList.add(ripples!.dataset.childrenClass!); ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px'; ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px'; @@ -194,6 +193,10 @@ function onMousedown(evt: MouseEvent): void { } } + &.transparent { + background: transparent; + } + &.gradate { font-weight: bold; color: var(--fgOnAccent) !important; diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue index 9e275d617..7b7bef478 100644 --- a/packages/frontend/src/components/MkChannelFollowButton.vue +++ b/packages/frontend/src/components/MkChannelFollowButton.vue @@ -1,20 +1,20 @@ <template> <button - class="hdcaacmi _button" - :class="{ wait, active: isFollowing, full }" + class="_button" + :class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing, [$style.full]: full }]" :disabled="wait" @click="onClick" > <template v-if="!wait"> <template v-if="isFollowing"> - <span v-if="full">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i> + <span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i> </template> <template v-else> - <span v-if="full">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i> + <span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i> </template> </template> <template v-else> - <span v-if="full">{{ i18n.ts.processing }}</span><MkLoading :em="true"/> + <span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true"/> </template> </button> </template> @@ -57,8 +57,8 @@ async function onClick() { } </script> -<style lang="scss" scoped> -.hdcaacmi { +<style lang="scss" module> +.root { position: relative; display: inline-block; font-weight: bold; @@ -103,7 +103,7 @@ async function onClick() { } &.active { - color: #fff; + color: var(--fgOnAccent); background: var(--accent); &:hover { @@ -121,9 +121,9 @@ async function onClick() { cursor: wait !important; opacity: 0.7; } +} - > span { - margin-right: 6px; - } +.text { + margin-right: 6px; } </style> diff --git a/packages/frontend/src/components/MkChannelList.vue b/packages/frontend/src/components/MkChannelList.vue index 408eab739..4050520eb 100644 --- a/packages/frontend/src/components/MkChannelList.vue +++ b/packages/frontend/src/components/MkChannelList.vue @@ -26,6 +26,3 @@ const props = withDefaults(defineProps<{ extractor: (item) => item, }); </script> - -<style lang="scss" scoped> -</style> diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index 06d5b9949..00ff98774 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -1,8 +1,8 @@ <template> -<div class="cbbedffa"> +<div :class="$style.root"> <canvas ref="chartEl"></canvas> <MkChartLegend ref="legendEl" style="margin-top: 8px;"/> - <div v-if="fetching" class="fetching"> + <div v-if="fetching" :class="$style.fetching"> <MkLoading/> </div> </div> @@ -817,22 +817,22 @@ onMounted(() => { /* eslint-enable id-denylist */ </script> -<style lang="scss" scoped> -.cbbedffa { +<style lang="scss" module> +.root { position: relative; +} - > .fetching { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - -webkit-backdrop-filter: var(--blur, blur(12px)); - backdrop-filter: var(--blur, blur(12px)); - display: flex; - justify-content: center; - align-items: center; - cursor: wait; - } +.fetching { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + -webkit-backdrop-filter: var(--blur, blur(12px)); + backdrop-filter: var(--blur, blur(12px)); + display: flex; + justify-content: center; + align-items: center; + cursor: wait; } </style> diff --git a/packages/frontend/src/components/MkChartTooltip.vue b/packages/frontend/src/components/MkChartTooltip.vue index 7cfe535ed..fe5b78754 100644 --- a/packages/frontend/src/components/MkChartTooltip.vue +++ b/packages/frontend/src/components/MkChartTooltip.vue @@ -1,5 +1,5 @@ <template> -<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" :direction="'top'" :inner-margin="16" @closed="emit('closed')"> +<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :maxWidth="340" :direction="'top'" :innerMargin="16" @closed="emit('closed')"> <div v-if="title || series"> <div v-if="title" :class="$style.title">{{ title }}</div> <template v-if="series"> diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue index da6439fd2..a6ab5aded 100644 --- a/packages/frontend/src/components/MkClickerGame.vue +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -3,7 +3,7 @@ <div v-if="game.ready" :class="$style.game"> <div :class="$style.cps" class="">{{ number(cps) }}cps</div> <div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div> - <button v-click-anime class="_button" :class="$style.button" @click="onClick"> + <button v-click-anime class="_button" @click="onClick"> <img src="/client-assets/cookie.png" :class="$style.img"> </button> </div> @@ -84,10 +84,6 @@ onUnmounted(() => { margin-bottom: 6px; } -.button { - -} - .img { max-width: 90px; } diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index d03331a6e..af1c57b34 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -1,12 +1,12 @@ <template> -<div ref="rootEl" class="_panel" :class="[$style.root, { [$style.naked]: naked, [$style.thin]: thin, [$style.hideHeader]: !showHeader, [$style.scrollable]: scrollable, [$style.closed]: !showBody }]"> +<div ref="rootEl" class="_panel" :class="[$style.root, { [$style.naked]: naked, [$style.thin]: thin, [$style.scrollable]: scrollable }]"> <header v-if="showHeader" ref="headerEl" :class="$style.header"> <div :class="$style.title"> <span :class="$style.titleIcon"><slot name="icon"></slot></span> <slot name="header"></slot> </div> <div :class="$style.headerSub"> - <slot name="func" :button-style-class="$style.headerButton"></slot> + <slot name="func" :buttonStyleClass="$style.headerButton"></slot> <button v-if="foldable" :class="$style.headerButton" class="_button" @click="() => showBody = !showBody"> <template v-if="showBody"><i class="ti ti-chevron-up"></i></template> <template v-else><i class="ti ti-chevron-down"></i></template> @@ -14,14 +14,14 @@ </div> </header> <Transition - :enter-active-class="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''" - :leave-active-class="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''" - :enter-from-class="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''" - :leave-to-class="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''" + :enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''" @enter="enter" - @after-enter="afterEnter" + @afterEnter="afterEnter" @leave="leave" - @after-leave="afterLeave" + @afterLeave="afterLeave" > <div v-show="showBody" ref="contentEl" :class="[$style.content, { [$style.omitted]: omitted }]"> <slot></slot> @@ -34,7 +34,7 @@ </template> <script lang="ts" setup> -import { onMounted, ref, shallowRef, watch } from 'vue'; +import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'; import { defaultStore } from '@/store'; import { i18n } from '@/i18n'; @@ -83,13 +83,19 @@ function afterLeave(el) { const calcOmit = () => { if (omitted.value || ignoreOmit.value || props.maxHeight == null) return; + if (!contentEl.value) return; const height = contentEl.value.offsetHeight; omitted.value = height > props.maxHeight; }; +const omitObserver = new ResizeObserver((entries, observer) => { + calcOmit(); +}); + onMounted(() => { watch(showBody, v => { - const headerHeight = props.showHeader ? headerEl.value.offsetHeight : 0; + if (!rootEl.value) return; + const headerHeight = props.showHeader ? headerEl.value?.offsetHeight ?? 0 : 0; rootEl.value.style.minHeight = `${headerHeight}px`; if (v) { rootEl.value.style.flexBasis = 'auto'; @@ -100,13 +106,15 @@ onMounted(() => { immediate: true, }); - rootEl.value.style.setProperty('--maxHeight', props.maxHeight + 'px'); + if (rootEl.value) rootEl.value.style.setProperty('--maxHeight', props.maxHeight + 'px'); calcOmit(); - new ResizeObserver((entries, observer) => { - calcOmit(); - }).observe(contentEl.value); + if (contentEl.value) omitObserver.observe(contentEl.value); +}); + +onUnmounted(() => { + omitObserver.disconnect(); }); </script> diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue index b81c806b0..fb11834f4 100644 --- a/packages/frontend/src/components/MkContextMenu.vue +++ b/packages/frontend/src/components/MkContextMenu.vue @@ -1,10 +1,10 @@ <template> <Transition appear - :enter-active-class="defaultStore.state.animation ? $style.transition_fade_enterActive : ''" - :leave-active-class="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''" - :enter-from-class="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''" - :leave-to-class="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''" + :enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''" > <div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> <MkMenu :items="items" :align="'left'" @close="$emit('closed')"/> diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 043a614e4..82363499b 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -4,7 +4,7 @@ :width="800" :height="500" :scroll="false" - :with-ok-button="true" + :withOkButton="true" @close="cancel()" @ok="ok()" @closed="$emit('closed')" diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index d6303f967..6942a0e6c 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -36,7 +36,7 @@ export default defineComponent({ }, setup(props, { slots, expose }) { - const $style = useCssModule(); + const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫 function getDateText(time: string) { const date = new Date(time).getDate(); const month = new Date(time).getMonth() + 1; diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 9f5404ce1..4d5df0bba 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -1,10 +1,18 @@ <template> -<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="emit('closed')"> +<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')"> <div :class="$style.root"> <div v-if="icon" :class="$style.icon"> <i :class="icon"></i> </div> - <div v-else-if="!input && !select" :class="[$style.icon, $style['type_' + type]]"> + <div + v-else-if="!input && !select" + :class="[$style.icon, { + [$style.type_success]: type === 'success', + [$style.type_error]: type === 'error', + [$style.type_warning]: type === 'warning', + [$style.type_info]: type === 'info', + }]" + > <i v-if="type === 'success'" :class="$style.iconInner" class="ti ti-check"></i> <i v-else-if="type === 'error'" :class="$style.iconInner" class="ti ti-circle-x"></i> <i v-else-if="type === 'warning'" :class="$style.iconInner" class="ti ti-alert-triangle"></i> diff --git a/packages/frontend/src/components/MkDigitalClock.stories.impl.ts b/packages/frontend/src/components/MkDigitalClock.stories.impl.ts new file mode 100644 index 000000000..344f6de47 --- /dev/null +++ b/packages/frontend/src/components/MkDigitalClock.stories.impl.ts @@ -0,0 +1,32 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { StoryObj } from '@storybook/vue3'; +import isChromatic from 'chromatic/isChromatic'; +import MkDigitalClock from './MkDigitalClock.vue'; +export const Default = { + render(args) { + return { + components: { + MkDigitalClock, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkDigitalClock v-bind="props" />', + }; + }, + args: { + now: isChromatic() ? () => new Date('2023-01-01T10:10:30') : undefined, + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkDigitalClock>; diff --git a/packages/frontend/src/components/MkDigitalClock.vue b/packages/frontend/src/components/MkDigitalClock.vue index 278dc8a5e..aea20f248 100644 --- a/packages/frontend/src/components/MkDigitalClock.vue +++ b/packages/frontend/src/components/MkDigitalClock.vue @@ -11,19 +11,21 @@ </template> <script lang="ts" setup> -import { onUnmounted, ref, watch } from 'vue'; +import { onMounted, onUnmounted, ref, watch } from 'vue'; +import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js'; const props = withDefaults(defineProps<{ showS?: boolean; showMs?: boolean; offset?: number; + now?: () => Date; }>(), { showS: true, showMs: false, offset: 0 - new Date().getTimezoneOffset(), + now: () => new Date(), }); -let intervalId; const hh = ref(''); const mm = ref(''); const ss = ref(''); @@ -39,9 +41,9 @@ watch(showColon, (v) => { } }); -const tick = () => { - const now = new Date(); - now.setMinutes(now.getMinutes() + (new Date().getTimezoneOffset() + props.offset)); +const tick = (): void => { + const now = props.now(); + now.setMinutes(now.getMinutes() + now.getTimezoneOffset() + props.offset); hh.value = now.getHours().toString().padStart(2, '0'); mm.value = now.getMinutes().toString().padStart(2, '0'); ss.value = now.getSeconds().toString().padStart(2, '0'); @@ -52,13 +54,12 @@ const tick = () => { tick(); -watch(() => props.showMs, () => { - if (intervalId) window.clearInterval(intervalId); - intervalId = window.setInterval(tick, props.showMs ? 10 : 1000); -}, { immediate: true }); +onMounted(() => { + defaultIdlingRenderScheduler.add(tick); +}); onUnmounted(() => { - window.clearInterval(intervalId); + defaultIdlingRenderScheduler.delete(tick); }); </script> diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index ab408b500..f0641161b 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -1,7 +1,6 @@ <template> <div - class="ncvczrfv" - :class="{ isSelected }" + :class="[$style.root, { [$style.isSelected]: isSelected }]" draggable="true" :title="title" @click="onClick" @@ -9,25 +8,27 @@ @dragstart="onDragstart" @dragend="onDragend" > - <div v-if="$i?.avatarId == file.id" class="label"> - <img src="/client-assets/label.svg"/> - <p>{{ i18n.ts.avatar }}</p> - </div> - <div v-if="$i?.bannerId == file.id" class="label"> - <img src="/client-assets/label.svg"/> - <p>{{ i18n.ts.banner }}</p> - </div> - <div v-if="file.isSensitive" class="label red"> - <img src="/client-assets/label-red.svg"/> - <p>{{ i18n.ts.nsfw }}</p> - </div> + <div style="pointer-events: none;"> + <div v-if="$i?.avatarId == file.id" :class="[$style.label]"> + <img :class="$style.labelImg" src="/client-assets/label.svg"/> + <p :class="$style.labelText">{{ i18n.ts.avatar }}</p> + </div> + <div v-if="$i?.bannerId == file.id" :class="[$style.label]"> + <img :class="$style.labelImg" src="/client-assets/label.svg"/> + <p :class="$style.labelText">{{ i18n.ts.banner }}</p> + </div> + <div v-if="file.isSensitive" :class="[$style.label, $style.red]"> + <img :class="$style.labelImg" src="/client-assets/label-red.svg"/> + <p :class="$style.labelText">{{ i18n.ts.nsfw }}</p> + </div> - <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> + <MkDriveFileThumbnail :class="$style.thumbnail" :file="file" fit="contain"/> - <p class="name"> - <span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> - <span v-if="file.name.lastIndexOf('.') != -1" class="ext">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span> - </p> + <p :class="$style.name"> + <span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> + <span v-if="file.name.lastIndexOf('.') != -1" style="opacity: 0.5;">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span> + </p> + </div> </div> </template> @@ -88,20 +89,13 @@ function onDragend() { } </script> -<style lang="scss" scoped> -.ncvczrfv { +<style lang="scss" module> +.root { position: relative; padding: 8px 0 0 0; min-height: 180px; border-radius: 8px; - - &, * { - cursor: pointer; - } - - > * { - pointer-events: none; - } + cursor: pointer; &:hover { background: rgba(#000, 0.05); @@ -165,82 +159,78 @@ function onDragend() { color: #fff; } } +} - > .label { +.label { + position: absolute; + top: 0; + left: 0; + pointer-events: none; + + &:before, + &:after { + content: ""; + display: block; position: absolute; - top: 0; - left: 0; - pointer-events: none; + z-index: 1; + background: #0c7ac9; + } + &:before { + top: 0; + left: 57px; + width: 28px; + height: 8px; + } + + &:after { + top: 57px; + left: 0; + width: 8px; + height: 28px; + } + + &.red { &:before, &:after { - content: ""; - display: block; - position: absolute; - z-index: 1; - background: #0c7ac9; - } - - &:before { - top: 0; - left: 57px; - width: 28px; - height: 8px; - } - - &:after { - top: 57px; - left: 0; - width: 8px; - height: 28px; - } - - &.red { - &:before, - &:after { - background: #c12113; - } - } - - > img { - position: absolute; - z-index: 2; - top: 0; - left: 0; - } - - > p { - position: absolute; - z-index: 3; - top: 19px; - left: -28px; - width: 120px; - margin: 0; - text-align: center; - line-height: 28px; - color: #fff; - transform: rotate(-45deg); - } - } - - > .thumbnail { - width: 110px; - height: 110px; - margin: auto; - } - - > .name { - display: block; - margin: 4px 0 0 0; - font-size: 0.8em; - text-align: center; - word-break: break-all; - color: var(--fg); - overflow: hidden; - - > .ext { - opacity: 0.5; + background: #c12113; } } } + +.labelImg { + position: absolute; + z-index: 2; + top: 0; + left: 0; +} + +.labelText { + position: absolute; + z-index: 3; + top: 19px; + left: -28px; + width: 120px; + margin: 0; + text-align: center; + line-height: 28px; + color: #fff; + transform: rotate(-45deg); +} + +.thumbnail { + width: 110px; + height: 110px; + margin: auto; +} + +.name { + display: block; + margin: 4px 0 0 0; + font-size: 0.8em; + text-align: center; + word-break: break-all; + color: var(--fg); + overflow: hidden; +} </style> diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 156013b9a..196934240 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -1,7 +1,6 @@ <template> <div - class="rghtznwe" - :class="{ draghover }" + :class="[$style.root, { [$style.draghover]: draghover }]" draggable="true" :title="title" @click="onClick" @@ -15,15 +14,15 @@ @dragstart="onDragstart" @dragend="onDragend" > - <p class="name"> - <template v-if="hover"><i class="ti ti-folder ti-fw"></i></template> - <template v-if="!hover"><i class="ti ti-folder ti-fw"></i></template> + <p :class="$style.name"> + <template v-if="hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template> + <template v-if="!hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template> {{ folder.name }} </p> - <p v-if="defaultStore.state.uploadFolder == folder.id" class="upload"> + <p v-if="defaultStore.state.uploadFolder == folder.id" :class="$style.upload"> {{ i18n.ts.uploadFolder }} </p> - <button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button> + <button v-if="selectMode" class="_button" :class="[$style.checkbox, { [$style.checked]: isSelected }]" @click.prevent.stop="checkboxClicked"></button> </div> </template> @@ -267,35 +266,14 @@ function onContextmenu(ev: MouseEvent) { } </script> -<style lang="scss" scoped> -.rghtznwe { +<style lang="scss" module> +.root { position: relative; padding: 8px; height: 64px; background: var(--driveFolderBg); border-radius: 4px; - - &, * { - cursor: pointer; - } - - *:not(.checkbox) { - pointer-events: none; - } - - > .checkbox { - position: absolute; - bottom: 8px; - right: 8px; - width: 16px; - height: 16px; - background: #fff; - border: solid 1px #000; - - &.checked { - background: var(--accent); - } - } + cursor: pointer; &.draghover { &:after { @@ -310,24 +288,38 @@ function onContextmenu(ev: MouseEvent) { border-radius: 4px; } } +} - > .name { - margin: 0; - font-size: 0.9em; - color: var(--desktopDriveFolderFg); +.checkbox { + position: absolute; + bottom: 8px; + right: 8px; + width: 16px; + height: 16px; + background: #fff; + border: solid 1px #000; - > i { - margin-right: 4px; - margin-left: 2px; - text-align: left; - } - } - - > .upload { - margin: 4px 4px; - font-size: 0.8em; - text-align: right; - color: var(--desktopDriveFolderFg); + &.checked { + background: var(--accent); } } + +.name { + margin: 0; + font-size: 0.9em; + color: var(--desktopDriveFolderFg); +} + +.icon { + margin-right: 4px; + margin-left: 2px; + text-align: left; +} + +.upload { + margin: 4px 4px; + font-size: 0.8em; + text-align: right; + color: var(--desktopDriveFolderFg); +} </style> diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue index dbbfef5f0..3349603d3 100644 --- a/packages/frontend/src/components/MkDrive.navFolder.vue +++ b/packages/frontend/src/components/MkDrive.navFolder.vue @@ -1,13 +1,13 @@ <template> -<div class="drylbebk" - :class="{ draghover }" +<div + :class="[$style.root, { [$style.draghover]: draghover }]" @click="onClick" @dragover.prevent.stop="onDragover" @dragenter="onDragenter" @dragleave="onDragleave" @drop.stop="onDrop" > - <i v-if="folder == null" class="ti ti-cloud"></i> + <i v-if="folder == null" class="ti ti-cloud" style="margin-right: 4px;"></i> <span>{{ folder == null ? i18n.ts.drive : folder.name }}</span> </div> </template> @@ -130,18 +130,10 @@ function onDrop(ev: DragEvent) { } </script> -<style lang="scss" scoped> -.drylbebk { - > * { - pointer-events: none; - } - +<style lang="scss" module> +.root { &.draghover { background: #eee; } - - > i { - margin-right: 4px; - } } </style> diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index bfec57d6a..52aef450d 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -1,89 +1,90 @@ <template> -<div class="yfudmmck"> - <nav> - <div class="path" @contextmenu.prevent.stop="() => {}"> +<div :class="$style.root"> + <nav :class="$style.nav"> + <div :class="$style.navPath" @contextmenu.prevent.stop="() => {}"> <XNavFolder - :class="{ current: folder == null }" - :parent-folder="folder" + :class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]" + :parentFolder="folder" @move="move" @upload="upload" - @remove-file="removeFile" - @remove-folder="removeFolder" + @removeFile="removeFile" + @removeFolder="removeFolder" /> <template v-for="f in hierarchyFolders"> - <span class="separator"><i class="ti ti-chevron-right"></i></span> + <span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span> <XNavFolder :folder="f" - :parent-folder="folder" + :parentFolder="folder" + :class="[$style.navPathItem]" @move="move" @upload="upload" - @remove-file="removeFile" - @remove-folder="removeFolder" + @removeFile="removeFile" + @removeFolder="removeFolder" /> </template> - <span v-if="folder != null" class="separator"><i class="ti ti-chevron-right"></i></span> - <span v-if="folder != null" class="folder current">{{ folder.name }}</span> + <span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span> + <span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span> </div> - <button class="menu _button" @click="showMenu"><i class="ti ti-dots"></i></button> + <button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button> </nav> <div - ref="main" class="main" - :class="{ uploading: uploadings.length > 0, fetching }" + ref="main" + :class="[$style.main, { [$style.uploading]: uploadings.length > 0, [$style.fetching]: fetching }]" @dragover.prevent.stop="onDragover" @dragenter="onDragenter" @dragleave="onDragleave" @drop.prevent.stop="onDrop" @contextmenu.stop="onContextmenu" > - <div ref="contents" class="contents"> - <div v-show="folders.length > 0" ref="foldersContainer" class="folders"> + <div ref="contents"> + <div v-show="folders.length > 0" ref="foldersContainer" :class="$style.folders"> <XFolder v-for="(f, i) in folders" :key="f.id" v-anim="i" - class="folder" + :class="$style.folder" :folder="f" - :select-mode="select === 'folder'" - :is-selected="selectedFolders.some(x => x.id === f.id)" + :selectMode="select === 'folder'" + :isSelected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder" @move="move" @upload="upload" - @remove-file="removeFile" - @remove-folder="removeFolder" + @removeFile="removeFile" + @removeFolder="removeFolder" @dragstart="isDragSource = true" @dragend="isDragSource = false" /> <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> - <div v-for="(n, i) in 16" :key="i" class="padding"></div> + <div v-for="(n, i) in 16" :key="i" :class="$style.padding"></div> <MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.ts.loadMore }}</MkButton> </div> - <div v-show="files.length > 0" ref="filesContainer" class="files"> + <div v-show="files.length > 0" ref="filesContainer" :class="$style.files"> <XFile v-for="(file, i) in files" :key="file.id" v-anim="i" - class="file" + :class="$style.file" :file="file" - :select-mode="select === 'file'" - :is-selected="selectedFiles.some(x => x.id === file.id)" + :selectMode="select === 'file'" + :isSelected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile" @dragstart="isDragSource = true" @dragend="isDragSource = false" /> <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> - <div v-for="(n, i) in 16" :key="i" class="padding"></div> + <div v-for="(n, i) in 16" :key="i" :class="$style.padding"></div> <MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.ts.loadMore }}</MkButton> </div> - <div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty"> - <p v-if="draghover">{{ i18n.t('empty-draghover') }}</p> - <p v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p> - <p v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</p> + <div v-if="files.length == 0 && folders.length == 0 && !fetching" :class="$style.empty"> + <div v-if="draghover">{{ i18n.t('empty-draghover') }}</div> + <div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</div> + <div v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</div> </div> </div> <MkLoading v-if="fetching"/> </div> - <div v-if="draghover" class="dropzone"></div> - <input ref="fileInput" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/> + <div v-if="draghover" :class="$style.dropzone"></div> + <input ref="fileInput" style="display: none;" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/> </div> </template> @@ -95,7 +96,7 @@ import XNavFolder from '@/components/MkDrive.navFolder.vue'; import XFolder from '@/components/MkDrive.folder.vue'; import XFile from '@/components/MkDrive.file.vue'; import * as os from '@/os'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { defaultStore } from '@/store'; import { i18n } from '@/i18n'; import { uploadFile, uploads } from '@/scripts/upload'; @@ -131,7 +132,7 @@ const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]); const selectedFiles = ref<Misskey.entities.DriveFile[]>([]); const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]); const uploadings = uploads; -const connection = stream.useChannel('drive'); +const connection = useStream().useChannel('drive'); const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい // ドロップされようとしているか @@ -658,147 +659,116 @@ onBeforeUnmount(() => { }); </script> -<style lang="scss" scoped> -.yfudmmck { +<style lang="scss" module> +.root { display: flex; flex-direction: column; height: 100%; +} - > nav { - display: flex; - z-index: 2; - width: 100%; - padding: 0 8px; - box-sizing: border-box; - overflow: auto; - font-size: 0.9em; - box-shadow: 0 1px 0 var(--divider); +.nav { + display: flex; + z-index: 2; + width: 100%; + padding: 0 8px; + box-sizing: border-box; + overflow: auto; + font-size: 0.9em; + box-shadow: 0 1px 0 var(--divider); + user-select: none; +} - &, * { - user-select: none; - } +.navPath { + display: inline-block; + vertical-align: bottom; + line-height: 42px; + white-space: nowrap; +} - > .path { - display: inline-block; - vertical-align: bottom; - line-height: 42px; - white-space: nowrap; +.navPathItem { + display: inline-block; + margin: 0; + padding: 0 8px; + line-height: 42px; + cursor: pointer; - > * { - display: inline-block; - margin: 0; - padding: 0 8px; - line-height: 42px; - cursor: pointer; + &:hover { + text-decoration: underline; + } - * { - pointer-events: none; - } + &.navCurrent { + font-weight: bold; + cursor: default; - &:hover { - text-decoration: underline; - } - - &.current { - font-weight: bold; - cursor: default; - - &:hover { - text-decoration: none; - } - } - - &.separator { - margin: 0; - padding: 0; - opacity: 0.5; - cursor: default; - - > i { - margin: 0; - } - } - } - } - - > .menu { - margin-left: auto; - padding: 0 12px; + &:hover { + text-decoration: none; } } - > .main { - flex: 1; - overflow: auto; - padding: var(--margin); - - &, * { - user-select: none; - } - - &.fetching { - cursor: wait !important; - - * { - pointer-events: none; - } - - > .contents { - opacity: 0.5; - } - } - - &.uploading { - height: calc(100% - 38px - 100px); - } - - > .contents { - - > .folders, - > .files { - display: flex; - flex-wrap: wrap; - - > .folder, - > .file { - flex-grow: 1; - width: 128px; - margin: 4px; - box-sizing: border-box; - } - - > .padding { - flex-grow: 1; - pointer-events: none; - width: 128px + 8px; - } - } - - > .empty { - padding: 16px; - text-align: center; - pointer-events: none; - opacity: 0.5; - - > p { - margin: 0; - } - } - } + &.navSeparator { + margin: 0; + padding: 0; + opacity: 0.5; + cursor: default; } +} - > .dropzone { - position: absolute; - left: 0; - top: 38px; - width: 100%; - height: calc(100% - 38px); - border: dashed 2px var(--focus); +.navMenu { + margin-left: auto; + padding: 0 12px; +} + +.main { + flex: 1; + overflow: auto; + padding: var(--margin); + user-select: none; + + &.fetching { + cursor: wait !important; + opacity: 0.5; pointer-events: none; } - > input { - display: none; + &.uploading { + height: calc(100% - 38px - 100px); } } + +.folders, +.files { + display: flex; + flex-wrap: wrap; +} + +.folder, +.file { + flex-grow: 1; + width: 128px; + margin: 4px; + box-sizing: border-box; +} + +.padding { + flex-grow: 1; + pointer-events: none; + width: 128px + 8px; +} + +.empty { + padding: 16px; + text-align: center; + pointer-events: none; + opacity: 0.5; +} + +.dropzone { + position: absolute; + left: 0; + top: 38px; + width: 100%; + height: calc(100% - 38px); + border: dashed 2px var(--focus); + pointer-events: none; +} </style> diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue index 33379ed5c..490aed6e0 100644 --- a/packages/frontend/src/components/MkDriveFileThumbnail.vue +++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue @@ -1,16 +1,16 @@ <template> -<div ref="thumbnail" class="zdjebgpv"> +<div ref="thumbnail" :class="$style.root"> <ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/> - <i v-else-if="is === 'image'" class="ti ti-photo icon"></i> - <i v-else-if="is === 'video'" class="ti ti-video icon"></i> - <i v-else-if="is === 'audio' || is === 'midi'" class="ti ti-file-music icon"></i> - <i v-else-if="is === 'csv'" class="ti ti-file-text icon"></i> - <i v-else-if="is === 'pdf'" class="ti ti-file-text icon"></i> - <i v-else-if="is === 'textfile'" class="ti ti-file-text icon"></i> - <i v-else-if="is === 'archive'" class="ti ti-file-zip icon"></i> - <i v-else class="ti ti-file icon"></i> + <i v-else-if="is === 'image'" class="ti ti-photo" :class="$style.icon"></i> + <i v-else-if="is === 'video'" class="ti ti-video" :class="$style.icon"></i> + <i v-else-if="is === 'audio' || is === 'midi'" class="ti ti-file-music" :class="$style.icon"></i> + <i v-else-if="is === 'csv'" class="ti ti-file-text" :class="$style.icon"></i> + <i v-else-if="is === 'pdf'" class="ti ti-file-text" :class="$style.icon"></i> + <i v-else-if="is === 'textfile'" class="ti ti-file-text" :class="$style.icon"></i> + <i v-else-if="is === 'archive'" class="ti ti-file-zip" :class="$style.icon"></i> + <i v-else class="ti ti-file" :class="$style.icon"></i> - <i v-if="isThumbnailAvailable && is === 'video'" class="ti ti-video icon-sub"></i> + <i v-if="isThumbnailAvailable && is === 'video'" class="ti ti-video" :class="$style.iconSub"></i> </div> </template> @@ -53,28 +53,28 @@ const isThumbnailAvailable = computed(() => { }); </script> -<style lang="scss" scoped> -.zdjebgpv { +<style lang="scss" module> +.root { position: relative; display: flex; background: var(--panel); border-radius: 8px; overflow: clip; +} - > .icon-sub { - position: absolute; - width: 30%; - height: auto; - margin: 0; - right: 4%; - bottom: 4%; - } +.iconSub { + position: absolute; + width: 30%; + height: auto; + margin: 0; + right: 4%; + bottom: 4%; +} - > .icon { - pointer-events: none; - margin: auto; - font-size: 32px; - color: #777; - } +.icon { + pointer-events: none; + margin: auto; + font-size: 32px; + color: #777; } </style> diff --git a/packages/frontend/src/components/MkDriveSelectDialog.vue b/packages/frontend/src/components/MkDriveSelectDialog.vue index 8d2b19c01..da873cb90 100644 --- a/packages/frontend/src/components/MkDriveSelectDialog.vue +++ b/packages/frontend/src/components/MkDriveSelectDialog.vue @@ -3,8 +3,8 @@ ref="dialog" :width="800" :height="500" - :with-ok-button="true" - :ok-button-disabled="(type === 'file') && (selected.length === 0)" + :withOkButton="true" + :okButtonDisabled="(type === 'file') && (selected.length === 0)" @click="cancel()" @close="cancel()" @ok="ok()" @@ -14,7 +14,7 @@ {{ multiple ? ((type === 'file') ? i18n.ts.selectFiles : i18n.ts.selectFolders) : ((type === 'file') ? i18n.ts.selectFile : i18n.ts.selectFolder) }} <span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span> </template> - <XDrive :multiple="multiple" :select="type" @change-selection="onChangeSelection" @selected="ok()"/> + <XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/> </MkModalWindow> </template> diff --git a/packages/frontend/src/components/MkDriveWindow.vue b/packages/frontend/src/components/MkDriveWindow.vue index 8b2abc15a..64ccbec9c 100644 --- a/packages/frontend/src/components/MkDriveWindow.vue +++ b/packages/frontend/src/components/MkDriveWindow.vue @@ -1,15 +1,15 @@ <template> <MkWindow ref="window" - :initial-width="800" - :initial-height="500" - :can-resize="true" + :initialWidth="800" + :initialHeight="500" + :canResize="true" @closed="emit('closed')" > <template #header> {{ i18n.ts.drive }} </template> - <XDrive :initial-folder="initialFolder"/> + <XDrive :initialFolder="initialFolder"/> </MkWindow> </template> diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 9eaf16374..cf856fd31 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -1,7 +1,8 @@ <template> <div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"> <input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter"> - <div ref="emojisEl" class="emojis"> + <!-- FirefoxのTabフォーカスが想定外の挙動となるためtabindex="-1"を追加 https://github.com/misskey-dev/misskey/issues/10744 --> + <div ref="emojisEl" class="emojis" tabindex="-1"> <section class="result"> <div v-if="searchResultCustom.length > 0" class="body"> <button @@ -69,8 +70,8 @@ <XSection v-for="category in customEmojiCategories" :key="`custom:${category}`" - :initial-shown="false" - :emojis="computed(() => customEmojis.filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).map(e => `:${e.name}:`))" + :initialShown="false" + :emojis="computed(() => customEmojis.filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).filter(filterAvailable).map(e => `:${e.name}:`))" @chosen="chosen" > {{ category || i18n.ts.other }} @@ -101,7 +102,8 @@ import { isTouchUsing } from '@/scripts/touch'; import { deviceKind } from '@/scripts/device-kind'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; -import { customEmojiCategories, customEmojis } from '@/custom-emojis'; +import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis'; +import { $i } from '@/account'; const props = withDefaults(defineProps<{ showPinned?: boolean; @@ -222,7 +224,6 @@ watch(q, () => { if (newQ.includes(' ')) { // AND検索 const keywords = newQ.split(' '); - // 名前にキーワードが含まれている for (const emoji of emojis) { if (keywords.every(keyword => emoji.name.includes(keyword))) { matches.add(emoji); @@ -231,11 +232,12 @@ watch(q, () => { } if (matches.size >= max) return matches; - // 名前またはエイリアスにキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) { - matches.add(emoji); - if (matches.size >= max) break; + for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) { + for (const emoji of emojis) { + if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) { + matches.add(emoji); + if (matches.size >= max) break; + } } } } else { @@ -247,13 +249,14 @@ watch(q, () => { } if (matches.size >= max) return matches; - for (const emoji of emojis) { - if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) { - matches.add(emoji); - if (matches.size >= max) break; + for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) { + for (const emoji of emojis) { + if (index[emoji.char].some(k => k.startsWith(newQ))) { + matches.add(emoji); + if (matches.size >= max) break; + } } } - if (matches.size >= max) return matches; for (const emoji of emojis) { if (emoji.name.includes(newQ)) { @@ -263,10 +266,12 @@ watch(q, () => { } if (matches.size >= max) return matches; - for (const emoji of emojis) { - if (emoji.keywords.some(keyword => keyword.includes(newQ))) { - matches.add(emoji); - if (matches.size >= max) break; + for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) { + for (const emoji of emojis) { + if (index[emoji.char].some(k => k.includes(newQ))) { + matches.add(emoji); + if (matches.size >= max) break; + } } } } @@ -274,10 +279,14 @@ watch(q, () => { return matches; }; - searchResultCustom.value = Array.from(searchCustom()); + searchResultCustom.value = Array.from(searchCustom()).filter(filterAvailable); searchResultUnicode.value = Array.from(searchUnicode()); }); +function filterAvailable(emoji: Misskey.entities.CustomEmoji): boolean { + return (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))); +} + function focus() { if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) { searchEl.value?.focus({ @@ -347,7 +356,7 @@ function done(query?: string): boolean | void { if (query == null || typeof query !== 'string') return; const q2 = query.replace(/:/g, ''); - const exactMatchCustom = customEmojis.value.find(emoji => emoji.name === q2); + const exactMatchCustom = customEmojisMap.get(q2); if (exactMatchCustom) { chosen(exactMatchCustom); return true; diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue index c568d4ed5..cfb65e3b6 100644 --- a/packages/frontend/src/components/MkEmojiPickerDialog.vue +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -2,10 +2,10 @@ <MkModal ref="modal" v-slot="{ type, maxHeight }" - :z-priority="'middle'" - :prefer-type="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'" - :transparent-bg="true" - :manual-showing="manualShowing" + :zPriority="'middle'" + :preferType="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'" + :transparentBg="true" + :manualShowing="manualShowing" :src="src" @click="modal?.close()" @opening="opening" @@ -14,11 +14,11 @@ > <MkEmojiPicker ref="picker" - class="ryghynhb _popup _shadow" - :class="{ drawer: type === 'drawer' }" - :show-pinned="showPinned" - :as-reaction-picker="asReactionPicker" - :as-drawer="type === 'drawer'" + class="_popup _shadow" + :class="{ [$style.drawer]: type === 'drawer' }" + :showPinned="showPinned" + :asReactionPicker="asReactionPicker" + :asDrawer="type === 'drawer'" :max-height="maxHeight" @chosen="chosen" /> @@ -67,12 +67,10 @@ function opening() { } </script> -<style lang="scss" scoped> -.ryghynhb { - &.drawer { - border-radius: 24px; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; - } +<style lang="scss" module> +.drawer { + border-radius: 24px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } </style> diff --git a/packages/frontend/src/components/MkEmojiPickerWindow.vue b/packages/frontend/src/components/MkEmojiPickerWindow.vue index 84970410e..9fecfd608 100644 --- a/packages/frontend/src/components/MkEmojiPickerWindow.vue +++ b/packages/frontend/src/components/MkEmojiPickerWindow.vue @@ -1,13 +1,14 @@ <template> -<MkWindow ref="window" - :initial-width="300" - :initial-height="290" - :can-resize="true" +<MkWindow + ref="window" + :initialWidth="300" + :initialHeight="290" + :canResize="true" :mini="true" :front="true" @closed="emit('closed')" > - <MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" as-window :class="$style.picker" @chosen="chosen"/> + <MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" asWindow :class="$style.picker" @chosen="chosen"/> </MkWindow> </template> diff --git a/packages/frontend/src/components/MkFileCaptionEditWindow.vue b/packages/frontend/src/components/MkFileCaptionEditWindow.vue index 95eef45df..61b87bda7 100644 --- a/packages/frontend/src/components/MkFileCaptionEditWindow.vue +++ b/packages/frontend/src/components/MkFileCaptionEditWindow.vue @@ -3,14 +3,14 @@ ref="dialog" :width="400" :height="450" - :with-ok-button="true" - :ok-button-disabled="false" + :withOkButton="true" + :okButtonDisabled="false" @ok="ok()" @close="dialog.close()" @closed="emit('closed')" > <template #header>{{ i18n.ts.describeFile }}</template> - <MkSpacer :margin-min="20" :margin-max="28"> + <MkSpacer :marginMin="20" :marginMax="28"> <MkDriveFileThumbnail :file="file" fit="contain" style="height: 100px; margin-bottom: 16px;"/> <MkTextarea v-model="caption" autofocus :placeholder="i18n.ts.inputNewDescription"> <template #label>{{ i18n.ts.caption }}</template> diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue index 475e01c8d..5dd07fc7d 100644 --- a/packages/frontend/src/components/MkFoldableSection.vue +++ b/packages/frontend/src/components/MkFoldableSection.vue @@ -1,9 +1,9 @@ <template> -<div class="ssazuxis"> - <header class="_button" :style="{ background: bg }" @click="showBody = !showBody"> - <div class="title"><div><slot name="header"></slot></div></div> - <div class="divider"></div> - <button class="_button"> +<div ref="el" :class="$style.root"> + <header :class="$style.header" class="_button" :style="{ background: bg }" @click="showBody = !showBody"> + <div :class="$style.title"><div><slot name="header"></slot></div></div> + <div :class="$style.divider"></div> + <button class="_button" :class="$style.button"> <template v-if="showBody"><i class="ti ti-chevron-up"></i></template> <template v-else><i class="ti ti-chevron-down"></i></template> </button> @@ -11,9 +11,9 @@ <Transition :name="defaultStore.state.animation ? 'folder-toggle' : ''" @enter="enter" - @after-enter="afterEnter" + @afterEnter="afterEnter" @leave="leave" - @after-leave="afterLeave" + @afterLeave="afterLeave" > <div v-show="showBody"> <slot></slot> @@ -22,84 +22,71 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { onMounted, ref, shallowRef, watch } from 'vue'; import tinycolor from 'tinycolor2'; import { miLocalStorage } from '@/local-storage'; import { defaultStore } from '@/store'; const miLocalStoragePrefix = 'ui:folder:' as const; -export default defineComponent({ - props: { - expanded: { - type: Boolean, - required: false, - default: true, - }, - persistKey: { - type: String, - required: false, - default: null, - }, - }, - data() { - return { - defaultStore, - bg: null, - showBody: (this.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`) === 't') : this.expanded, - }; - }, - watch: { - showBody() { - if (this.persistKey) { - miLocalStorage.setItem(`${miLocalStoragePrefix}${this.persistKey}`, this.showBody ? 't' : 'f'); - } - }, - }, - mounted() { - function getParentBg(el: Element | null): string { - if (el == null || el.tagName === 'BODY') return 'var(--bg)'; - const bg = el.style.background || el.style.backgroundColor; - if (bg) { - return bg; - } else { - return getParentBg(el.parentElement); - } - } - const rawBg = getParentBg(this.$el); - const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); - bg.setAlpha(0.85); - this.bg = bg.toRgbString(); - }, - methods: { - toggleContent(show: boolean) { - this.showBody = show; - }, +const props = withDefaults(defineProps<{ + expanded?: boolean; + persistKey?: string; +}>(), { + expanded: true, +}); - enter(el) { - const elementHeight = el.getBoundingClientRect().height; - el.style.height = 0; - el.offsetHeight; // reflow - el.style.height = elementHeight + 'px'; - }, - afterEnter(el) { - el.style.height = null; - }, - leave(el) { - const elementHeight = el.getBoundingClientRect().height; - el.style.height = elementHeight + 'px'; - el.offsetHeight; // reflow - el.style.height = 0; - }, - afterLeave(el) { - el.style.height = null; - }, - }, +const el = shallowRef<HTMLDivElement>(); +const bg = ref<string | null>(null); +const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded); + +watch(showBody, () => { + if (props.persistKey) { + miLocalStorage.setItem(`${miLocalStoragePrefix}${props.persistKey}`, showBody.value ? 't' : 'f'); + } +}); + +function enter(el: Element) { + const elementHeight = el.getBoundingClientRect().height; + el.style.height = 0; + el.offsetHeight; // reflow + el.style.height = elementHeight + 'px'; +} + +function afterEnter(el: Element) { + el.style.height = null; +} + +function leave(el: Element) { + const elementHeight = el.getBoundingClientRect().height; + el.style.height = elementHeight + 'px'; + el.offsetHeight; // reflow + el.style.height = 0; +} + +function afterLeave(el: Element) { + el.style.height = null; +} + +onMounted(() => { + function getParentBg(el: HTMLElement | null): string { + if (el == null || el.tagName === 'BODY') return 'var(--bg)'; + const bg = el.style.background || el.style.backgroundColor; + if (bg) { + return bg; + } else { + return getParentBg(el.parentElement); + } + } + const rawBg = getParentBg(el.value); + const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); + _bg.setAlpha(0.85); + bg.value = _bg.toRgbString(); }); </script> -<style lang="scss" scoped> +<style lang="scss" module> .folder-toggle-enter-active, .folder-toggle-leave-active { overflow-y: clip; transition: opacity 0.5s, height 0.5s !important; @@ -111,45 +98,41 @@ export default defineComponent({ opacity: 0; } -.ssazuxis { +.root { position: relative; +} - > header { - display: flex; - position: relative; - z-index: 10; - position: sticky; - top: var(--stickyTop, 0px); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(20px)); +.header { + display: flex; + position: relative; + z-index: 10; + position: sticky; + top: var(--stickyTop, 0px); + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(20px)); +} - > .title { - display: grid; - place-content: center; - margin: 0; - padding: 12px 16px 12px 0; - } +.title { + display: grid; + place-content: center; + margin: 0; + padding: 12px 16px 12px 0; +} - > .divider { - flex: 1; - margin: auto; - height: 1px; - background: var(--divider); - } +.divider { + flex: 1; + margin: auto; + height: 1px; + background: var(--divider); +} - > button { - padding: 12px 0 12px 16px; - } - } +.button { + padding: 12px 0 12px 16px; } @container (max-width: 500px) { - .ssazuxis { - > header { - > .title { - padding: 8px 10px 8px 0; - } - } + .title { + padding: 8px 10px 8px 0; } } </style> diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index 10eee6aab..70f0cc5cd 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -5,8 +5,8 @@ <div :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle"> <div :class="$style.headerIcon"><slot name="icon"></slot></div> <div :class="$style.headerText"> - <div :class="$style.headerTextMain"> - <slot name="label"></slot> + <div> + <MkCondensedLine :minScale="2 / 3"><slot name="label"></slot></MkCondensedLine> </div> <div :class="$style.headerTextSub"> <slot name="caption"></slot> @@ -22,18 +22,18 @@ <div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null, overflow: maxHeight ? `auto` : null }" :aria-hidden="!opened"> <Transition - :enter-active-class="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''" - :leave-active-class="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''" - :enter-from-class="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''" - :leave-to-class="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''" + :enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''" @enter="enter" - @after-enter="afterEnter" + @afterEnter="afterEnter" @leave="leave" - @after-leave="afterLeave" + @afterLeave="afterLeave" > <KeepAlive> <div v-show="opened"> - <MkSpacer :margin-min="14" :margin-max="22"> + <MkSpacer :marginMin="14" :marginMax="22"> <slot></slot> </MkSpacer> </div> @@ -185,10 +185,6 @@ onMounted(() => { padding-right: 12px; } -.headerTextMain { - -} - .headerTextSub { color: var(--fgTransparentWeak); font-size: .85em; diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index beee21c64..b732fbb2b 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -1,30 +1,30 @@ <template> <button - class="kpoogebi _button" - :class="{ wait, active: isFollowing || hasPendingFollowRequestFromYou, full, large }" + class="_button" + :class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing || hasPendingFollowRequestFromYou, [$style.full]: full, [$style.large]: large }]" :disabled="wait" @click="onClick" > <template v-if="!wait"> <template v-if="hasPendingFollowRequestFromYou && user.isLocked"> - <span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="ti ti-hourglass-empty"></i> + <span v-if="full" :class="$style.text">{{ i18n.ts.followRequestPending }}</span><i class="ti ti-hourglass-empty"></i> </template> <template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 --> - <span v-if="full">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/> + <span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/> </template> <template v-else-if="isFollowing"> - <span v-if="full">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i> + <span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i> </template> <template v-else-if="!isFollowing && user.isLocked"> - <span v-if="full">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i> + <span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i> </template> <template v-else-if="!isFollowing && !user.isLocked"> - <span v-if="full">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i> + <span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i> </template> </template> <template v-else> - <span v-if="full">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/> + <span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/> </template> </button> </template> @@ -33,7 +33,7 @@ import { onBeforeUnmount, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { i18n } from '@/i18n'; import { claimAchievement } from '@/scripts/achievements'; import { $i } from '@/account'; @@ -50,7 +50,7 @@ const props = withDefaults(defineProps<{ let isFollowing = $ref(props.user.isFollowing); let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou); let wait = $ref(false); -const connection = stream.useChannel('main'); +const connection = useStream().useChannel('main'); if (props.user.isFollowing == null) { os.api('users/show', { @@ -126,13 +126,12 @@ onBeforeUnmount(() => { }); </script> -<style lang="scss" scoped> -.kpoogebi { +<style lang="scss" module> +.root { position: relative; display: inline-block; font-weight: bold; - color: var(--accent); - background: transparent; + color: var(--fgOnWhite); border: solid 1px var(--accent); padding: 0; height: 31px; @@ -196,9 +195,9 @@ onBeforeUnmount(() => { cursor: wait !important; opacity: 0.7; } +} - > span { - margin-right: 6px; - } +.text { + margin-right: 6px; } </style> diff --git a/packages/frontend/src/components/MkForgotPassword.vue b/packages/frontend/src/components/MkForgotPassword.vue index 0befa7e3a..1264c4233 100644 --- a/packages/frontend/src/components/MkForgotPassword.vue +++ b/packages/frontend/src/components/MkForgotPassword.vue @@ -8,27 +8,28 @@ > <template #header>{{ i18n.ts.forgotPassword }}</template> - <form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit"> - <div class="main _gaps_m"> - <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required> - <template #label>{{ i18n.ts.username }}</template> - <template #prefix>@</template> - </MkInput> + <MkSpacer :marginMin="20" :marginMax="28"> + <form v-if="instance.enableEmail" @submit.prevent="onSubmit"> + <div class="_gaps_m"> + <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required> + <template #label>{{ i18n.ts.username }}</template> + <template #prefix>@</template> + </MkInput> - <MkInput v-model="email" type="email" :spellcheck="false" required> - <template #label>{{ i18n.ts.emailAddress }}</template> - <template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template> - </MkInput> + <MkInput v-model="email" type="email" :spellcheck="false" required> + <template #label>{{ i18n.ts.emailAddress }}</template> + <template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template> + </MkInput> - <MkButton type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton> + <MkButton type="submit" rounded :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton> + + <MkInfo>{{ i18n.ts._forgotPassword.ifNoEmail }}</MkInfo> + </div> + </form> + <div v-else> + {{ i18n.ts._forgotPassword.contactAdmin }} </div> - <div class="sub"> - <MkA to="/about" class="_link">{{ i18n.ts._forgotPassword.ifNoEmail }}</MkA> - </div> - </form> - <div v-else class="bafecedb"> - {{ i18n.ts._forgotPassword.contactAdmin }} - </div> + </MkSpacer> </MkModalWindow> </template> @@ -37,6 +38,7 @@ import { } from 'vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; +import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os'; import { instance } from '@/instance'; import { i18n } from '@/i18n'; @@ -62,20 +64,3 @@ async function onSubmit() { dialog.close(); } </script> - -<style lang="scss" scoped> -.bafeceda { - > .main { - padding: 24px; - } - - > .sub { - border-top: solid 0.5px var(--divider); - padding: 24px; - } -} - -.bafecedb { - padding: 24px; -} -</style> diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue index 979df2e7c..6d2b391e6 100644 --- a/packages/frontend/src/components/MkFormDialog.vue +++ b/packages/frontend/src/components/MkFormDialog.vue @@ -2,9 +2,9 @@ <MkModalWindow ref="dialog" :width="450" - :can-close="false" - :with-ok-button="true" - :ok-button-disabled="false" + :canClose="false" + :withOkButton="true" + :okButtonDisabled="false" @click="cancel()" @ok="ok()" @close="cancel()" @@ -14,7 +14,7 @@ {{ title }} </template> - <MkSpacer :margin-min="20" :margin-max="32"> + <MkSpacer :marginMin="20" :marginMax="32"> <div class="_gaps_m"> <template v-for="item in Object.keys(form).filter(item => !form[item].hidden)"> <MkInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1"> @@ -41,7 +41,7 @@ <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template> <option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option> </MkRadios> - <MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter"> + <MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :textConverter="form[item].textConverter"> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template> <template v-if="form[item].description" #caption>{{ form[item].description }}</template> </MkRange> @@ -54,8 +54,8 @@ </MkModalWindow> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { reactive, shallowRef } from 'vue'; import MkInput from './MkInput.vue'; import MkTextarea from './MkTextarea.vue'; import MkSwitch from './MkSwitch.vue'; @@ -66,58 +66,36 @@ import MkRadios from './MkRadios.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkModalWindow, - MkInput, - MkTextarea, - MkSwitch, - MkSelect, - MkRange, - MkButton, - MkRadios, - }, +const props = defineProps<{ + title: string; + form: any; +}>(); - props: { - title: { - type: String, - required: true, - }, - form: { - type: Object, - required: true, - }, - }, +const emit = defineEmits<{ + (ev: 'done', v: { + canceled?: boolean; + result?: any; + }): void; +}>(); - emits: ['done'], +const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +const values = reactive({}); - data() { - return { - values: {}, - i18n, - }; - }, +for (const item in props.form) { + values[item] = props.form[item].default ?? null; +} - created() { - for (const item in this.form) { - this.values[item] = this.form[item].default ?? null; - } - }, +function ok() { + emit('done', { + result: values, + }); + dialog.value.close(); +} - methods: { - ok() { - this.$emit('done', { - result: this.values, - }); - this.$refs.dialog.close(); - }, - - cancel() { - this.$emit('done', { - canceled: true, - }); - this.$refs.dialog.close(); - }, - }, -}); +function cancel() { + emit('done', { + canceled: true, + }); + dialog.value.close(); +} </script> diff --git a/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts b/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts index 57b3e7551..72ac0a58f 100644 --- a/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts +++ b/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts @@ -44,6 +44,10 @@ export const Default = { ], parameters: { layout: 'centered', + chromatic: { + // FIXME: flaky + disableSnapshot: true, + }, }, } satisfies StoryObj<typeof MkGalleryPostPreview>; export const Hover = { diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue index 4f8f7b945..3a39ad963 100644 --- a/packages/frontend/src/components/MkGalleryPostPreview.vue +++ b/packages/frontend/src/components/MkGalleryPostPreview.vue @@ -5,16 +5,13 @@ <ImgWithBlurhash class="img layered" :transition="safe ? null : { - enterActiveClass: $style.transition_toggle_enterActive, + duration: 500, leaveActiveClass: $style.transition_toggle_leaveActive, - enterFromClass: $style.transition_toggle_enterFrom, leaveToClass: $style.transition_toggle_leaveTo, - enterToClass: $style.transition_toggle_enterTo, - leaveFromClass: $style.transition_toggle_leaveFrom, }" :src="post.files[0].thumbnailUrl" :hash="post.files[0].blurhash" - :force-blurhash="!show" + :forceBlurhash="!show" /> </Transition> </div> @@ -53,24 +50,16 @@ function leaveHover(): void { </script> <style lang="scss" module> -.transition_toggle_enterActive, .transition_toggle_leaveActive { - transition: opacity 0.5s; + transition: opacity .5s; position: absolute; top: 0; left: 0; } -.transition_toggle_enterFrom, .transition_toggle_leaveTo { opacity: 0; } - -.transition_toggle_enterTo, -.transition_toggle_leaveFrom { - transition: none; - opacity: 1; -} </style> <style lang="scss" scoped> diff --git a/packages/frontend/src/components/MkImageViewer.vue b/packages/frontend/src/components/MkImageViewer.vue deleted file mode 100644 index a90e27e50..000000000 --- a/packages/frontend/src/components/MkImageViewer.vue +++ /dev/null @@ -1,78 +0,0 @@ -<template> -<MkModal ref="modal" :z-priority="'middle'" @click="modal.close()" @closed="emit('closed')"> - <div class="xubzgfga"> - <header>{{ image.name }}</header> - <img :src="image.url" :alt="image.comment" :title="image.comment" @click="modal.close()"/> - <footer> - <span>{{ image.type }}</span> - <span>{{ bytes(image.size) }}</span> - <span v-if="image.properties && image.properties.width">{{ number(image.properties.width) }}px × {{ number(image.properties.height) }}px</span> - </footer> - </div> -</MkModal> -</template> - -<script lang="ts" setup> -import { } from 'vue'; -import * as misskey from 'misskey-js'; -import bytes from '@/filters/bytes'; -import number from '@/filters/number'; -import MkModal from '@/components/MkModal.vue'; - -const props = withDefaults(defineProps<{ - image: misskey.entities.DriveFile; -}>(), { -}); - -const emit = defineEmits<{ - (ev: 'closed'): void; -}>(); - -const modal = $shallowRef<InstanceType<typeof MkModal>>(); -</script> - -<style lang="scss" scoped> -.xubzgfga { - margin: auto; - display: flex; - flex-direction: column; - height: 100%; - - > header, - > footer { - align-self: center; - display: inline-block; - padding: 6px 9px; - font-size: 90%; - background: rgba(0, 0, 0, 0.5); - border-radius: 6px; - color: #fff; - } - - > header { - margin-bottom: 8px; - opacity: 0.9; - } - - > img { - display: block; - flex: 1; - min-height: 0; - object-fit: contain; - width: 100%; - cursor: zoom-out; - image-orientation: from-image; - } - - > footer { - margin-top: 8px; - opacity: 0.8; - - > span + span { - margin-left: 0.5em; - padding-left: 0.5em; - border-left: solid 1px rgba(255, 255, 255, 0.5); - } - } -} -</style> diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index 6406a3506..672a28f6d 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -1,30 +1,60 @@ <template> -<div :class="[$style.root, { [$style.cover]: cover }]" :title="title ?? ''"> - <img v-if="!loaded && src && !forceBlurhash" :class="$style.loader" :src="src" @load="onLoad"/> - <Transition - mode="in-out" - :enter-active-class="defaultStore.state.animation && (props.transition?.enterActiveClass ?? $style['transition_toggle_enterActive']) || undefined" - :leave-active-class="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style['transition_toggle_leaveActive']) || undefined" - :enter-from-class="defaultStore.state.animation && props.transition?.enterFromClass || undefined" - :leave-to-class="defaultStore.state.animation && props.transition?.leaveToClass || undefined" - :enter-to-class="defaultStore.state.animation && (props.transition?.enterToClass ?? $style['transition_toggle_enterTo']) || undefined" - :leave-from-class="defaultStore.state.animation && (props.transition?.leaveFromClass ?? $style['transition_toggle_leaveFrom']) || undefined" +<div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''"> + <TransitionGroup + :duration="defaultStore.state.animation && props.transition?.duration || undefined" + :enterActiveClass="defaultStore.state.animation && props.transition?.enterActiveClass || undefined" + :leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined" + :enterFromClass="defaultStore.state.animation && props.transition?.enterFromClass || undefined" + :leaveToClass="defaultStore.state.animation && props.transition?.leaveToClass || undefined" + :enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined" + :leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined" > - <canvas v-if="!loaded || forceBlurhash" ref="canvas" :class="$style.canvas" :width="width" :height="height" :title="title ?? undefined"/> - <img v-else :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined"/> - </Transition> + <canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined"/> + <img v-show="!hide" key="img" ref="img" :height="imgHeight" :width="imgWidth" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async"/> + </TransitionGroup> </div> </template> -<script lang="ts" setup> -import { onMounted, shallowRef, useCssModule, watch } from 'vue'; -import { decode } from 'blurhash'; -import { defaultStore } from '@/store'; +<script lang="ts"> +import { $ref } from 'vue/macros'; +import DrawBlurhash from '@/workers/draw-blurhash?worker'; +import TestWebGL2 from '@/workers/test-webgl2?worker'; +import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch'; +import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash'; -const $style = useCssModule(); +const workerPromise = new Promise<WorkerMultiDispatch | null>(resolve => { + // テスト環境で Web Worker インスタンスは作成できない + if (import.meta.env.MODE === 'test') { + resolve(null); + return; + } + const testWorker = new TestWebGL2(); + testWorker.addEventListener('message', event => { + if (event.data.result) { + const workers = new WorkerMultiDispatch( + () => new DrawBlurhash(), + Math.min(navigator.hardwareConcurrency - 1, 4), + ); + resolve(workers); + if (_DEV_) console.log('WebGL2 in worker is supported!'); + } else { + resolve(null); + if (_DEV_) console.log('WebGL2 in worker is not supported...'); + } + testWorker.terminate(); + }); +}); +</script> + +<script lang="ts" setup> +import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue'; +import { v4 as uuid } from 'uuid'; +import { render } from 'buraha'; +import { defaultStore } from '@/store'; const props = withDefaults(defineProps<{ transition?: { + duration?: number | { enter: number; leave: number; }; enterActiveClass?: string; leaveActiveClass?: string; enterFromClass?: string; @@ -51,67 +81,141 @@ const props = withDefaults(defineProps<{ forceBlurhash: false, }); +const viewId = uuid(); const canvas = shallowRef<HTMLCanvasElement>(); +const root = shallowRef<HTMLDivElement>(); +const img = shallowRef<HTMLImageElement>(); let loaded = $ref(false); -let width = $ref(props.width); -let height = $ref(props.height); +let canvasWidth = $ref(64); +let canvasHeight = $ref(64); +let imgWidth = $ref(props.width); +let imgHeight = $ref(props.height); +let bitmapTmp = $ref<CanvasImageSource | undefined>(); +const hide = computed(() => !loaded || props.forceBlurhash); -function onLoad() { - loaded = true; +function waitForDecode() { + if (props.src != null && props.src !== '') { + nextTick() + .then(() => img.value?.decode()) + .then(() => { + loaded = true; + }, error => { + console.error('Error occured during decoding image', img.value, error); + throw Error(error); + }); + } else { + loaded = false; + } } -watch([() => props.width, () => props.height], () => { +watch([() => props.width, () => props.height, root], () => { const ratio = props.width / props.height; if (ratio > 1) { - width = Math.round(64 * ratio); - height = 64; + canvasWidth = Math.round(64 * ratio); + canvasHeight = 64; } else { - width = 64; - height = Math.round(64 / ratio); + canvasWidth = 64; + canvasHeight = Math.round(64 / ratio); } + + const clientWidth = root.value?.clientWidth ?? 300; + imgWidth = clientWidth; + imgHeight = Math.round(clientWidth / ratio); }, { immediate: true, }); -function draw() { - if (props.hash == null || !canvas.value) return; - const pixels = decode(props.hash, width, height); +function drawImage(bitmap: CanvasImageSource) { + // canvasがない(mountedされていない)場合はTmpに保存しておく + if (!canvas.value) { + bitmapTmp = bitmap; + return; + } + + // canvasがあれば描画する + bitmapTmp = undefined; const ctx = canvas.value.getContext('2d'); - const imageData = ctx!.createImageData(width, height); - imageData.data.set(pixels); - ctx!.putImageData(imageData, 0, 0); + if (!ctx) return; + ctx.drawImage(bitmap, 0, 0, canvasWidth, canvasHeight); } -watch([() => props.hash, canvas], () => { +async function draw() { + if (!canvas.value || props.hash == null) return; + + const ctx = canvas.value.getContext('2d'); + if (!ctx) return; + + // avgColorでお茶をにごす + ctx.beginPath(); + ctx.fillStyle = extractAvgColorFromBlurhash(props.hash) ?? '#888'; + ctx.fillRect(0, 0, canvasWidth, canvasHeight); + + const workers = await workerPromise; + if (workers) { + workers.postMessage( + { + id: viewId, + hash: props.hash, + width: canvasWidth, + height: canvasHeight, + }, + undefined, + ); + } else { + try { + const work = document.createElement('canvas'); + work.width = canvasWidth; + work.height = canvasHeight; + render(props.hash, work); + ctx.drawImage(work, 0, 0, canvasWidth, canvasHeight); + } catch (error) { + console.error('Error occured during drawing blurhash', error); + } + } +} + +function workerOnMessage(event: MessageEvent) { + if (event.data.id !== viewId) return; + drawImage(event.data.bitmap as ImageBitmap); +} + +workerPromise.then(worker => { + if (worker) { + worker.addListener(workerOnMessage); + } + + draw(); +}); + +watch(() => props.src, () => { + waitForDecode(); +}); + +watch(() => props.hash, () => { draw(); }); onMounted(() => { - draw(); + // drawImageがmountedより先に呼ばれている場合はここで描画する + if (bitmapTmp) { + drawImage(bitmapTmp); + } + waitForDecode(); +}); + +onUnmounted(() => { + workerPromise.then(worker => { + worker?.removeListener(workerOnMessage); + }); }); </script> <style lang="scss" module> -.transition_toggle_enterActive, -.transition_toggle_leaveActive { +.transition_leaveActive { position: absolute; top: 0; left: 0; } - -.transition_toggle_enterTo, -.transition_toggle_leaveFrom { - opacity: 0; -} - -.loader { - position: absolute; - top: 0; - left: 0; - width: 0; - height: 0; -} - .root { position: relative; width: 100%; diff --git a/packages/frontend/src/components/MkKeyValue.vue b/packages/frontend/src/components/MkKeyValue.vue index ff69c7964..4b6a77563 100644 --- a/packages/frontend/src/components/MkKeyValue.vue +++ b/packages/frontend/src/components/MkKeyValue.vue @@ -1,9 +1,9 @@ <template> -<div class="alqyeyti" :class="{ oneline }"> - <div class="key"> +<div :class="[$style.root, { [$style.oneline]: oneline }]"> + <div :class="$style.key"> <slot name="key"></slot> </div> - <div class="value"> + <div :class="$style.value"> <slot name="value"></slot> <button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="ti ti-copy"></i></button> </div> @@ -30,24 +30,18 @@ const copy_ = () => { }; </script> -<style lang="scss" scoped> -.alqyeyti { - > .key { - font-size: 0.85em; - padding: 0 0 0.25em 0; - opacity: 0.75; - } - +<style lang="scss" module> +.root { &.oneline { display: flex; - > .key { + .key { width: 30%; font-size: 1em; padding: 0 8px 0 0; } - > .value { + .value { width: 70%; white-space: nowrap; overflow: hidden; @@ -55,4 +49,10 @@ const copy_ = () => { } } } + +.key { + font-size: 0.85em; + padding: 0 0 0.25em 0; + opacity: 0.75; +} </style> diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue index 80e5cc827..926277861 100644 --- a/packages/frontend/src/components/MkLaunchPad.vue +++ b/packages/frontend/src/components/MkLaunchPad.vue @@ -1,5 +1,5 @@ <template> -<MkModal ref="modal" v-slot="{ type, maxHeight }" :prefer-type="preferedModalType" :anchor="anchor" :transparent-bg="true" :src="src" @click="modal.close()" @closed="emit('closed')"> +<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal.close()" @closed="emit('closed')"> <div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }"> <div class="main"> <template v-for="item in items"> diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue index 5ca4c5051..5902d6fd2 100644 --- a/packages/frontend/src/components/MkMediaBanner.vue +++ b/packages/frontend/src/components/MkMediaBanner.vue @@ -1,27 +1,27 @@ <template> -<div class="mk-media-banner"> - <div v-if="media.isSensitive && hide" class="sensitive" @click="hide = false"> - <span class="icon"><i class="ti ti-alert-triangle"></i></span> +<div :class="$style.root"> + <div v-if="media.isSensitive && hide" :class="$style.sensitive" @click="hide = false"> + <span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span> <b>{{ i18n.ts.sensitive }}</b> <span>{{ i18n.ts.clickToShow }}</span> </div> - <div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" class="audio"> - <VuePlyr :options="{ volume: 0.5 }"> - <audio controls preload="metadata"> - <source - :src="media.url" - :type="media.type" - /> - </audio> - </VuePlyr> + <div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :class="$style.audio"> + <audio + ref="audioEl" + :src="media.url" + :title="media.name" + controls + preload="metadata" + @volumechange="volumechange" + /> </div> <a - v-else class="download" + v-else :class="$style.download" :href="media.url" :title="media.name" :download="media.name" > - <span class="icon"><i class="ti ti-download"></i></span> + <span style="font-size: 1.6em;"><i class="ti ti-download"></i></span> <b>{{ media.name }}</b> </a> </div> @@ -30,9 +30,7 @@ <script lang="ts" setup> import { onMounted } from 'vue'; import * as misskey from 'misskey-js'; -import VuePlyr from 'vue-plyr'; import { soundConfigStore } from '@/scripts/sound'; -import 'vue-plyr/dist/vue-plyr.css'; import { i18n } from '@/i18n'; const props = withDefaults(defineProps<{ @@ -52,55 +50,34 @@ onMounted(() => { }); </script> -<style lang="scss" scoped> -.mk-media-banner { +<style lang="scss" module> +.root { width: 100%; border-radius: 4px; margin-top: 4px; - // overflow: clip; + overflow: clip; +} - --plyr-color-main: var(--accent); - --plyr-audio-controls-background: var(--bg); - --plyr-audio-controls-color: var(--accentLighten); +.download, +.sensitive { + display: flex; + align-items: center; + font-size: 12px; + padding: 8px 12px; + white-space: nowrap; +} - > .download, - > .sensitive { - display: flex; - align-items: center; - font-size: 12px; - padding: 8px 12px; - white-space: nowrap; +.download { + background: var(--noteAttachedFile); +} - > * { - display: block; - } +.sensitive { + background: #111; + color: #fff; +} - > b { - overflow: hidden; - text-overflow: ellipsis; - } - - > *:not(:last-child) { - margin-right: .2em; - } - - > .icon { - font-size: 1.6em; - } - } - - > .download { - background: var(--noteAttachedFile); - } - - > .sensitive { - background: #111; - color: #fff; - } - - > .audio { - border-radius: 8px; - // overflow: clip; - } +.audio { + border-radius: 8px; + overflow: clip; } </style> diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index 42dc9e79f..b29871c36 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -1,29 +1,39 @@ <template> -<div v-if="hide" :class="$style.hidden" @click="hide = false"> - <ImgWithBlurhash style="filter: brightness(0.5);" :hash="image.blurhash" :title="image.comment" :alt="image.comment" :width="image.properties.width" :height="image.properties.height" :force-blurhash="defaultStore.state.enableDataSaverMode"/> - <div :class="$style.hiddenText"> - <div :class="$style.hiddenTextWrapper"> - <b v-if="image.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b> - <b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b> - <span style="display: block;">{{ i18n.ts.clickToShow }}</span> - </div> - </div> -</div> -<div v-else :class="$style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'"> +<div :class="hide ? $style.hidden : $style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click="onclick"> <a :class="$style.imageContainer" :href="image.url" :title="image.name" > - <ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment || image.name" :title="image.comment || image.name" :width="image.properties.width" :height="image.properties.height" :cover="false"/> + <ImgWithBlurhash + :hash="image.blurhash" + :src="(defaultStore.state.enableDataSaverMode && hide) ? null : url" + :forceBlurhash="hide" + :cover="hide" + :alt="image.comment || image.name" + :title="image.comment || image.name" + :width="image.properties.width" + :height="image.properties.height" + :style="hide ? 'filter: brightness(0.5);' : null" + /> </a> - <div :class="$style.indicators"> - <div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div> - <div v-if="image.comment" :class="$style.indicator">ALT</div> - <div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div> - </div> - <button v-tooltip="i18n.ts.hide" :class="$style.hide" class="_button" @click="hide = true"><i class="ti ti-eye-off"></i></button> - <button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots"></i></button> + <template v-if="hide"> + <div :class="$style.hiddenText"> + <div :class="$style.hiddenTextWrapper"> + <b v-if="image.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b> + <b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b> + <span style="display: block;">{{ i18n.ts.clickToShow }}</span> + </div> + </div> + </template> + <template v-else> + <div :class="$style.indicators"> + <div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div> + <div v-if="image.comment" :class="$style.indicator">ALT</div> + <div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div> + </div> + <button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button> + </template> </div> </template> @@ -53,6 +63,12 @@ const url = $computed(() => (props.raw || defaultStore.state.loadRawImages) : props.image.thumbnailUrl, ); +function onclick() { + if (hide) { + hide = false; + } +} + // Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする watch(() => props.image, () => { hide = (defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore'); @@ -62,9 +78,16 @@ watch(() => props.image, () => { }); function showMenu(ev: MouseEvent) { - os.popupMenu([...(iAmModerator ? [{ - text: i18n.ts.markAsSensitive, + os.popupMenu([{ + text: i18n.ts.hide, icon: 'ti ti-eye-off', + action: () => { + hide = true; + }, + }, ...(iAmModerator ? [{ + text: i18n.ts.markAsSensitive, + icon: 'ti ti-eye-exclamation', + danger: true, action: () => { os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true }); }, @@ -105,34 +128,20 @@ function showMenu(ev: MouseEvent) { background-size: 16px 16px; } -.hide { - display: block; - position: absolute; - border-radius: 6px; - background-color: var(--accentedBg); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - color: var(--accent); - font-size: 0.8em; - padding: 6px 8px; - text-align: center; - top: 12px; - right: 12px; -} - .menu { display: block; position: absolute; - border-radius: 6px; + border-radius: 999px; background-color: rgba(0, 0, 0, 0.3); -webkit-backdrop-filter: var(--blur, blur(15px)); backdrop-filter: var(--blur, blur(15px)); color: #fff; font-size: 0.8em; - padding: 6px 8px; + width: 32px; + height: 32px; text-align: center; - bottom: 12px; - right: 12px; + bottom: 10px; + right: 10px; } .imageContainer { @@ -149,12 +158,10 @@ function showMenu(ev: MouseEvent) { .indicators { display: inline-flex; position: absolute; - top: 12px; - left: 12px; - text-align: center; + top: 10px; + left: 10px; pointer-events: none; opacity: .5; - font-size: 14px; gap: 6px; } @@ -165,7 +172,7 @@ function showMenu(ev: MouseEvent) { color: var(--accentLighten); display: inline-block; font-weight: bold; - font-size: 12px; - padding: 2px 6px; + font-size: 0.8em; + padding: 2px 5px; } </style> diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index e456ff3ee..a0a245005 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -6,7 +6,11 @@ ref="gallery" :class="[ $style.medias, - count <= 4 ? $style['n' + count] : $style.nMany, + count === 1 ? [$style.n1, { + [$style.n116_9]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '16_9', + [$style.n11_1]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '1_1', + [$style.n12_3]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '2_3', + }] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany, ]" > <template v-for="media in mediaList.filter(media => previewable(media))"> @@ -19,7 +23,7 @@ </template> <script lang="ts" setup> -import { onMounted, ref, useCssModule, watch } from 'vue'; +import { onMounted, watch, shallowRef } from 'vue'; import * as misskey from 'misskey-js'; import PhotoSwipeLightbox from 'photoswipe/lightbox'; import PhotoSwipe from 'photoswipe'; @@ -36,13 +40,42 @@ const props = defineProps<{ raw?: boolean; }>(); -const $style = useCssModule(); - -const gallery = ref<HTMLDivElement>(); +const gallery = shallowRef<HTMLDivElement>(); const pswpZIndex = os.claimZIndex('middle'); document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString()); const count = $computed(() => props.mediaList.filter(media => previewable(media)).length); +function calcAspectRatio() { + if (!gallery.value) return; + + let img = props.mediaList[0]; + + if (props.mediaList.length !== 1 || !(img.properties.width && img.properties.height)) { + gallery.value.style.aspectRatio = ''; + return; + } + + // アスペクト比上限設定では、横長の場合は高さを縮小させる + const ratioMax = (ratio: number) => `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`; + + switch (defaultStore.state.mediaListWithOneImageAppearance) { + case '16_9': + gallery.value.style.aspectRatio = ratioMax(16 / 9); + break; + case '1_1': + gallery.value.style.aspectRatio = ratioMax(1); + break; + case '2_3': + gallery.value.style.aspectRatio = ratioMax(2 / 3); + break; + default: + gallery.value.style.aspectRatio = ''; + break; + } +} + +watch([defaultStore.reactiveState.mediaListWithOneImageAppearance, gallery], () => calcAspectRatio()); + onMounted(() => { const lightbox = new PhotoSwipeLightbox({ dataSource: props.mediaList @@ -64,7 +97,7 @@ onMounted(() => { return item; }), gallery: gallery.value, - mainClass: $style.pswp, + mainClass: 'pswp', children: '.image', thumbSelector: '.image', loop: false, @@ -162,12 +195,37 @@ const previewable = (file: misskey.entities.DriveFile): boolean => { display: grid; grid-gap: 8px; - // for webkit height: 100%; + width: 100%; &.n1 { - aspect-ratio: 16/9; grid-template-rows: 1fr; + + // default (expand) + min-height: 64px; + max-height: clamp( + 64px, + 50cqh, + min(360px, 50vh) + ); + + &.n116_9 { + min-height: none; + max-height: none; + aspect-ratio: 16 / 9; // fallback + } + + &.n11_1{ + min-height: none; + max-height: none; + aspect-ratio: 1 / 1; // fallback + } + + &.n12_3 { + min-height: none; + max-height: none; + aspect-ratio: 2 / 3; // fallback + } } &.n2 { @@ -211,7 +269,7 @@ const previewable = (file: misskey.entities.DriveFile): boolean => { border-radius: 8px; } -.pswp { +:global(.pswp) { --pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important; --pswp-bg: var(--modalBg) !important; } diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index a4b76300e..40bae90b5 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -1,26 +1,28 @@ <template> -<div v-if="hide" class="icozogqfvdetwohsdglrbswgrejoxbdj" @click="hide = false"> +<div v-if="hide" :class="$style.hidden" @click="hide = false"> <!-- 【注意】dataSaverMode が有効になっている際には、hide が false になるまでサムネイルや動画を読み込まないようにすること --> - <div> - <b v-if="video.isSensitive"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b> - <b v-else><i class="ti ti-movie"></i> {{ defaultStore.state.enableDataSaverMode && video.size ? bytes(video.size) : i18n.ts.video }}</b> + <div :class="$style.sensitive"> + <b v-if="video.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b> + <b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.enableDataSaverMode && video.size ? bytes(video.size) : i18n.ts.video }}</b> <span>{{ i18n.ts.clickToShow }}</span> </div> </div> -<div v-else class="kkjnbbplepmiyuadieoenjgutgcmtsvu"> - <VuePlyr :options="{ volume: 0.5 }"> - <video - controls - :data-poster="video.thumbnailUrl" +<div v-else :class="$style.visible"> + <video + :class="$style.video" + :poster="video.thumbnailUrl" + :title="video.comment" + :alt="video.comment" + preload="none" + controls + @contextmenu.stop + > + <source + :src="video.url" + :type="video.type" > - <source - size="720" - :src="video.url" - :type="video.type" - /> - </video> - </VuePlyr> - <i class="ti ti-eye-off" @click="hide = true"></i> + </video> + <i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i> </div> </template> @@ -28,9 +30,7 @@ import { ref } from 'vue'; import * as misskey from 'misskey-js'; import bytes from '@/filters/bytes'; -import VuePlyr from 'vue-plyr'; import { defaultStore } from '@/store'; -import 'vue-plyr/dist/vue-plyr.css'; import { i18n } from '@/i18n'; const props = defineProps<{ @@ -40,56 +40,49 @@ const props = defineProps<{ const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore')); </script> -<style lang="scss" scoped> -.kkjnbbplepmiyuadieoenjgutgcmtsvu { +<style lang="scss" module> +.visible { position: relative; - - --plyr-color-main: var(--accent); - - > i { - display: block; - position: absolute; - border-radius: 6px; - background-color: var(--fg); - color: var(--accentLighten); - font-size: 14px; - opacity: .5; - padding: 3px 6px; - text-align: center; - cursor: pointer; - top: 12px; - right: 12px; - } - - > video { - display: flex; - justify-content: center; - align-items: center; - - font-size: 3.5em; - overflow: hidden; - background-position: center; - background-size: cover; - width: 100%; - height: 100%; - } } -.icozogqfvdetwohsdglrbswgrejoxbdj { +.hide { + display: block; + position: absolute; + border-radius: 6px; + background-color: var(--fg); + color: var(--accentLighten); + font-size: 14px; + opacity: .5; + padding: 3px 6px; + text-align: center; + cursor: pointer; + top: 12px; + right: 12px; +} + +.video { + display: flex; + justify-content: center; + align-items: center; + font-size: 3.5em; + overflow: hidden; + background-position: center; + background-size: cover; + width: 100%; + height: 100%; +} + +.hidden { display: flex; justify-content: center; align-items: center; background: #111; color: #fff; +} - > div { - display: table-cell; - text-align: center; - font-size: 12px; - - > b { - display: block; - } - } +.sensitive { + display: table-cell; + text-align: center; + font-size: 12px; } </style> diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue index 481c3710c..bb256c394 100644 --- a/packages/frontend/src/components/MkMention.vue +++ b/packages/frontend/src/components/MkMention.vue @@ -2,7 +2,7 @@ <MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }"> <img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt=""> <span> - <span :class="$style.username">@{{ username }}</span> + <span>@{{ username }}</span> <span v-if="(host != localHost) || defaultStore.state.showFullAcct" :class="$style.host">@{{ toUnicode(host) }}</span> </span> </MkA> diff --git a/packages/frontend/src/components/MkMenu.child.vue b/packages/frontend/src/components/MkMenu.child.vue index e0935efbe..4fedfe701 100644 --- a/packages/frontend/src/components/MkMenu.child.vue +++ b/packages/frontend/src/components/MkMenu.child.vue @@ -1,6 +1,6 @@ <template> <div ref="el" :class="$style.root"> - <MkMenu :items="items" :align="align" :width="width" :as-drawer="false" @close="onChildClosed"/> + <MkMenu :items="items" :align="align" :width="width" :asDrawer="false" @close="onChildClosed"/> </div> </template> diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index e513a65a3..7dd6a8c88 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -49,8 +49,8 @@ <span>{{ i18n.ts.none }}</span> </span> </div> - <div v-if="childMenu" :class="$style.child"> - <XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/> + <div v-if="childMenu"> + <XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned"/> </div> </div> </template> diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index 99df9e815..bb5c6c7aa 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -1,11 +1,31 @@ <template> <Transition :name="transitionName" - :enter-active-class="$style['transition_' + transitionName + '_enterActive']" - :leave-active-class="$style['transition_' + transitionName + '_leaveActive']" - :enter-from-class="$style['transition_' + transitionName + '_enterFrom']" - :leave-to-class="$style['transition_' + transitionName + '_leaveTo']" - :duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened" + :enterActiveClass="normalizeClass({ + [$style.transition_modalDrawer_enterActive]: transitionName === 'modal-drawer', + [$style.transition_modalPopup_enterActive]: transitionName === 'modal-popup', + [$style.transition_modal_enterActive]: transitionName === 'modal', + [$style.transition_send_enterActive]: transitionName === 'send', + })" + :leaveActiveClass="normalizeClass({ + [$style.transition_modalDrawer_leaveActive]: transitionName === 'modal-drawer', + [$style.transition_modalPopup_leaveActive]: transitionName === 'modal-popup', + [$style.transition_modal_leaveActive]: transitionName === 'modal', + [$style.transition_send_leaveActive]: transitionName === 'send', + })" + :enterFromClass="normalizeClass({ + [$style.transition_modalDrawer_enterFrom]: transitionName === 'modal-drawer', + [$style.transition_modalPopup_enterFrom]: transitionName === 'modal-popup', + [$style.transition_modal_enterFrom]: transitionName === 'modal', + [$style.transition_send_enterFrom]: transitionName === 'send', + })" + :leaveToClass="normalizeClass({ + [$style.transition_modalDrawer_leaveTo]: transitionName === 'modal-drawer', + [$style.transition_modalPopup_leaveTo]: transitionName === 'modal-popup', + [$style.transition_modal_leaveTo]: transitionName === 'modal', + [$style.transition_send_leaveTo]: transitionName === 'send', + })" + :duration="transitionDuration" appear @afterLeave="emit('closed')" @enter="emit('opening')" @afterEnter="onOpened" > <div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> <div data-cy-bg :data-cy-transparent="isEnableBgTransparent" class="_modalBg" :class="[$style.bg, { [$style.bgTransparent]: isEnableBgTransparent }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div> @@ -17,7 +37,7 @@ </template> <script lang="ts" setup> -import { nextTick, onMounted, watch, provide } from 'vue'; +import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch } from 'vue'; import * as os from '@/os'; import { isTouchUsing } from '@/scripts/touch'; import { defaultStore } from '@/store'; @@ -38,7 +58,7 @@ type ModalTypes = 'popup' | 'dialog' | 'drawer'; const props = withDefaults(defineProps<{ manualShowing?: boolean | null; anchor?: { x: string; y: string; }; - src?: HTMLElement; + src?: HTMLElement | null; preferType?: ModalTypes | 'auto'; zPriority?: 'low' | 'middle' | 'high'; noOverlap?: boolean; @@ -264,6 +284,10 @@ const onOpened = () => { }, { passive: true }); }; +const alignObserver = new ResizeObserver((entries, observer) => { + align(); +}); + onMounted(() => { watch(() => props.src, async () => { if (props.src) { @@ -278,12 +302,14 @@ onMounted(() => { }, { immediate: true }); nextTick(() => { - new ResizeObserver((entries, observer) => { - align(); - }).observe(content!); + alignObserver.observe(content!); }); }); +onUnmounted(() => { + alignObserver.disconnect(); +}); + defineExpose({ close, }); @@ -339,8 +365,8 @@ defineExpose({ } } -.transition_modal-popup_enterActive, -.transition_modal-popup_leaveActive { +.transition_modalPopup_enterActive, +.transition_modalPopup_leaveActive { > .bg { transition: opacity 0.1s !important; } @@ -350,8 +376,8 @@ defineExpose({ transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important; } } -.transition_modal-popup_enterFrom, -.transition_modal-popup_leaveTo { +.transition_modalPopup_enterFrom, +.transition_modalPopup_leaveTo { > .bg { opacity: 0; } @@ -364,7 +390,7 @@ defineExpose({ } } -.transition_modal-drawer_enterActive { +.transition_modalDrawer_enterActive { > .bg { transition: opacity 0.2s !important; } @@ -373,7 +399,7 @@ defineExpose({ transition: transform 0.2s cubic-bezier(0,.5,0,1) !important; } } -.transition_modal-drawer_leaveActive { +.transition_modalDrawer_leaveActive { > .bg { transition: opacity 0.2s !important; } @@ -382,8 +408,8 @@ defineExpose({ transition: transform 0.2s cubic-bezier(0,.5,0,1) !important; } } -.transition_modal-drawer_enterFrom, -.transition_modal-drawer_leaveTo { +.transition_modalDrawer_enterFrom, +.transition_modalDrawer_leaveTo { > .bg { opacity: 0; } diff --git a/packages/frontend/src/components/MkModalPageWindow.vue b/packages/frontend/src/components/MkModalPageWindow.vue deleted file mode 100644 index b38865f52..000000000 --- a/packages/frontend/src/components/MkModalPageWindow.vue +++ /dev/null @@ -1,182 +0,0 @@ -<template> -<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')"> - <div ref="rootEl" class="hrmcaedk" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }"> - <div class="header" @contextmenu="onContextmenu"> - <button v-if="history.length > 0" v-tooltip="i18n.ts.goBack" class="_button" @click="back()"><i class="ti ti-arrow-left"></i></button> - <span v-else style="display: inline-block; width: 20px"></span> - <span v-if="pageMetadata?.value" class="title"> - <i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i> - <span>{{ pageMetadata?.value.title }}</span> - </span> - <button class="_button" @click="$refs.modal.close()"><i class="ti ti-x"></i></button> - </div> - <div class="body" style="container-type: inline-size;"> - <MkStickyContainer> - <template #header><MkPageHeader v-if="pageMetadata?.value && !pageMetadata?.value.hideHeader" :info="pageMetadata?.value"/></template> - <RouterView :router="router"/> - </MkStickyContainer> - </div> - </div> -</MkModal> -</template> - -<script lang="ts" setup> -import { ComputedRef, provide } from 'vue'; -import MkModal from '@/components/MkModal.vue'; -import { popout as _popout } from '@/scripts/popout'; -import copyToClipboard from '@/scripts/copy-to-clipboard'; -import { url } from '@/config'; -import * as os from '@/os'; -import { mainRouter, routes } from '@/router'; -import { i18n } from '@/i18n'; -import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata'; -import { Router } from '@/nirax'; - -const props = defineProps<{ - initialPath: string; -}>(); - -defineEmits<{ - (ev: 'closed'): void; - (ev: 'click'): void; -}>(); - -const router = new Router(routes, props.initialPath); - -router.addListener('push', ctx => { - -}); - -let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); -let rootEl = $ref(); -let modal = $shallowRef<InstanceType<typeof MkModal>>(); -let path = $ref(props.initialPath); -let width = $ref(860); -let height = $ref(660); -const history = []; - -provide('router', router); -provideMetadataReceiver((info) => { - pageMetadata = info; -}); -provide('shouldOmitHeaderTitle', true); -provide('shouldHeaderThin', true); - -const pageUrl = $computed(() => url + path); -const contextmenu = $computed(() => { - return [{ - type: 'label', - text: path, - }, { - icon: 'ti ti-player-eject', - text: i18n.ts.showInPage, - action: expand, - }, { - icon: 'ti ti-window-maximize', - text: i18n.ts.popout, - action: popout, - }, null, { - icon: 'ti ti-external-link', - text: i18n.ts.openInNewTab, - action: () => { - window.open(pageUrl, '_blank'); - modal.close(); - }, - }, { - icon: 'ti ti-link', - text: i18n.ts.copyLink, - action: () => { - copyToClipboard(pageUrl); - }, - }]; -}); - -function navigate(path, record = true) { - if (record) history.push(router.getCurrentPath()); - router.push(path); -} - -function back() { - navigate(history.pop(), false); -} - -function expand() { - mainRouter.push(path); - modal.close(); -} - -function popout() { - _popout(path, rootEl); - modal.close(); -} - -function onContextmenu(ev: MouseEvent) { - os.contextMenu(contextmenu, ev); -} -</script> - -<style lang="scss" scoped> -.hrmcaedk { - margin: auto; - overflow: hidden; - display: flex; - flex-direction: column; - contain: content; - border-radius: var(--radius); - - --root-margin: 24px; - - @media (max-width: 500px) { - --root-margin: 16px; - } - - > .header { - $height: 52px; - $height-narrow: 42px; - display: flex; - flex-shrink: 0; - height: $height; - line-height: $height; - font-weight: bold; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - background: var(--windowHeader); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - - > button { - height: $height; - width: $height; - - &:hover { - color: var(--fgHighlighted); - } - } - - @media (max-width: 500px) { - height: $height-narrow; - line-height: $height-narrow; - padding-left: 16px; - - > button { - height: $height-narrow; - width: $height-narrow; - } - } - - > .title { - flex: 1; - - > .icon { - margin-right: 0.5em; - } - } - } - - > .body { - overflow: auto; - background: var(--bg); - } -} -</style> diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue index 63c55b904..08569b4d6 100644 --- a/packages/frontend/src/components/MkModalWindow.vue +++ b/packages/frontend/src/components/MkModalWindow.vue @@ -1,5 +1,5 @@ <template> -<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')"> +<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="$emit('closed')"> <div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown"> <div ref="headerEl" :class="$style.header"> <button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button> diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index d95f8de31..7c9ddadbf 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -44,8 +44,8 @@ <div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div> <MkAvatar :class="$style.avatar" :user="appearNote.user" link preview/> <div :class="$style.main"> - <MkNoteHeader :class="$style.header" :note="appearNote" :mini="true"/> - <MkInstanceTicker v-if="showTicker" :class="$style.ticker" :instance="appearNote.user.instance"/> + <MkNoteHeader :note="appearNote" :mini="true"/> + <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> <div style="container-type: inline-size;"> <p v-if="appearNote.cw != null" :class="$style.cw"> <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :i="$i"/> @@ -55,17 +55,17 @@ <div :class="$style.text"> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/> + <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/> <div v-if="translating || translation" :class="$style.translation"> <MkLoading v-if="translating" mini/> - <div v-else :class="$style.translated"> + <div v-else> <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> - <Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/> + <Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/> </div> </div> </div> - <div v-if="appearNote.files.length > 0" :class="$style.files"> - <MkMediaList :media-list="appearNote.files"/> + <div v-if="appearNote.files.length > 0"> + <MkMediaList :mediaList="appearNote.files"/> </div> <MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll"/> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/> @@ -79,7 +79,7 @@ </div> <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> </div> - <MkReactionsViewer :note="appearNote" :max-number="16"> + <MkReactionsViewer :note="appearNote" :maxNumber="16"> <template #more> <button class="_button" :class="$style.reactionDetailsButton" @click="showReactions"> {{ i18n.ts.more }} @@ -205,8 +205,11 @@ const isMyRenote = $i && ($i.id === note.userId); const showContent = ref(false); const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null; const isLong = (appearNote.cw == null && appearNote.text != null && ( + (appearNote.text.includes('$[x2')) || (appearNote.text.includes('$[x3')) || (appearNote.text.includes('$[x4')) || + (appearNote.text.includes('$[scale')) || + (appearNote.text.includes('$[position')) || (appearNote.text.split('\n').length > 9) || (appearNote.text.length > 500) || (appearNote.files.length >= 5) || @@ -274,7 +277,7 @@ function renote(viaKeyboard = false) { const y = rect.top + (el.offsetHeight / 2); os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - + os.api('notes/create', { renoteId: appearNote.id, channelId: appearNote.channelId, @@ -305,7 +308,7 @@ function renote(viaKeyboard = false) { const y = rect.top + (el.offsetHeight / 2); os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - + os.api('notes/create', { renoteId: appearNote.id, }).then(() => { @@ -379,6 +382,8 @@ function undoReact(note): void { function onContextmenu(ev: MouseEvent): void { const isLink = (el: HTMLElement) => { if (el.tagName === 'A') return true; + // 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。 + if (el.tagName === 'AUDIO') return true; if (el.parentElement) { return isLink(el.parentElement); } diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 0d6d329d9..a65039277 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -4,25 +4,25 @@ v-show="!isDeleted" ref="el" v-hotkey="keymap" - class="lxwezrsl" - :tabindex="!isDeleted ? '-1' : null" - :class="{ renote: isRenote }" + :class="$style.root" > - <MkNoteSub v-for="note in conversation" :key="note.id" class="reply-to-more" :note="note"/> - <MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/> - <div v-if="isRenote" class="renote"> - <MkAvatar class="avatar" :user="note.user" link preview/> - <i class="ti ti-repeat"></i> - <I18n :src="i18n.ts.renotedBy" tag="span"> - <template #user> - <MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)"> - <MkUserName :user="note.user"/> - </MkA> - </template> - </I18n> - <div class="info"> - <button ref="renoteTime" class="_button time" @click="showRenoteMenu()"> - <i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i> + <MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/> + <MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/> + <div v-if="isRenote" :class="$style.renote"> + <MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/> + <i class="ti ti-repeat" style="margin-right: 4px;"></i> + <span :class="$style.renoteText"> + <I18n :src="i18n.ts.renotedBy" tag="span"> + <template #user> + <MkA v-user-preview="note.userId" :class="$style.renoteName" :to="userPage(note.user)"> + <MkUserName :user="note.user"/> + </MkA> + </template> + </I18n> + </span> + <div :class="$style.renoteInfo"> + <button ref="renoteTime" class="_button" :class="$style.renoteTime" @click="showRenoteMenu()"> + <i v-if="isMyRenote" class="ti ti-dots" style="margin-right: 4px;"></i> <MkTime :time="note.createdAt"/> </button> <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> @@ -33,16 +33,16 @@ <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> </div> </div> - <article class="article" @contextmenu.stop="onContextmenu"> - <header class="header"> - <MkAvatar class="avatar" :user="appearNote.user" indicator link preview/> - <div class="body"> - <div class="top"> - <MkA v-user-preview="appearNote.user.id" class="name" :to="userPage(appearNote.user)"> + <article :class="$style.note" @contextmenu.stop="onContextmenu"> + <header :class="$style.noteHeader"> + <MkAvatar :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link preview/> + <div :class="$style.noteHeaderBody"> + <div> + <MkA v-user-preview="appearNote.user.id" :class="$style.noteHeaderName" :to="userPage(appearNote.user)"> <MkUserName :nowrap="false" :user="appearNote.user"/> </MkA> - <span v-if="appearNote.user.isBot" class="is-bot">bot</span> - <div class="info"> + <span v-if="appearNote.user.isBot" :class="$style.isBot">bot</span> + <div :class="$style.noteHeaderInfo"> <span v-if="appearNote.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]"> <i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i> <i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i> @@ -51,84 +51,81 @@ <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> </div> </div> - <div class="username"><MkAcct :user="appearNote.user"/></div> - <MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/> + <div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div> + <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> </div> </header> - <div class="main"> - <div class="body"> - <p v-if="appearNote.cw != null" class="cw"> - <Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i"/> - <MkCwButton v-model="showContent" :note="appearNote"/> - </p> - <div v-show="appearNote.cw == null || showContent" class="content"> - <div class="text"> - <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> - <MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/> - <a v-if="appearNote.renote != null" class="rp">RN:</a> - <div v-if="translating || translation" class="translation"> - <MkLoading v-if="translating" mini/> - <div v-else class="translated"> - <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> - <Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/> - </div> - </div> + <div :class="$style.noteContent"> + <p v-if="appearNote.cw != null" :class="$style.cw"> + <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :i="$i"/> + <MkCwButton v-model="showContent" :note="appearNote"/> + </p> + <div v-show="appearNote.cw == null || showContent"> + <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> + <MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> + <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/> + <a v-if="appearNote.renote != null" :class="$style.rn">RN:</a> + <div v-if="translating || translation" :class="$style.translation"> + <MkLoading v-if="translating" mini/> + <div v-else> + <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> + <Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/> </div> - <div v-if="appearNote.files.length > 0" class="files"> - <MkMediaList :media-list="appearNote.files"/> - </div> - <MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/> - <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/> - <div v-if="appearNote.renote" class="renote"><MkNoteSimple :note="appearNote.renote" class="note"/></div> </div> - <MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> + <div v-if="appearNote.files.length > 0"> + <MkMediaList :mediaList="appearNote.files"/> + </div> + <MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" :class="$style.poll"/> + <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/> + <div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div> </div> - <footer class="footer"> - <div class="info"> - <MkA class="created-at" :to="notePage(appearNote)"> - <MkTime :time="appearNote.createdAt" mode="detail"/> - </MkA> - </div> - <MkReactionsViewer ref="reactionsViewer" :note="appearNote"/> - <button class="button _button" @click="reply()"> - <i class="ti ti-arrow-back-up"></i> - <p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p> - </button> - <button - v-if="canRenote" - ref="renoteButton" - class="button _button" - @mousedown="renote()" - > - <i class="ti ti-repeat"></i> - <p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p> - </button> - <button v-else class="button _button" disabled> - <i class="ti ti-ban"></i> - </button> - <button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()"> - <i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> - <i v-else class="ti ti-plus"></i> - </button> - <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> - <i class="ti ti-minus"></i> - </button> - <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="button _button" @mousedown="clip()"> - <i class="ti ti-paperclip"></i> - </button> - <button ref="menuButton" class="button _button" @mousedown="menu()"> - <i class="ti ti-dots"></i> - </button> - </footer> + <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> </div> + <footer> + <div :class="$style.noteFooterInfo"> + <MkA :to="notePage(appearNote)"> + <MkTime :time="appearNote.createdAt" mode="detail"/> + </MkA> + </div> + <MkReactionsViewer ref="reactionsViewer" :note="appearNote"/> + <button class="_button" :class="$style.noteFooterButton" @click="reply()"> + <i class="ti ti-arrow-back-up"></i> + <p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.repliesCount }}</p> + </button> + <button + v-if="canRenote" + ref="renoteButton" + class="_button" + :class="$style.noteFooterButton" + @mousedown="renote()" + > + <i class="ti ti-repeat"></i> + <p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.renoteCount }}</p> + </button> + <button v-else class="_button" :class="$style.noteFooterButton" disabled> + <i class="ti ti-ban"></i> + </button> + <button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()"> + <i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> + <i v-else class="ti ti-plus"></i> + </button> + <button v-if="appearNote.myReaction != null" ref="reactButton" class="_button" :class="[$style.noteFooterButton, $style.reacted]" @click="undoReact(appearNote)"> + <i class="ti ti-minus"></i> + </button> + <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()"> + <i class="ti ti-paperclip"></i> + </button> + <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="menu()"> + <i class="ti ti-dots"></i> + </button> + </footer> </article> - <MkNoteSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/> + <MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true"/> </div> -<div v-else class="_panel muted" @click="muted = false"> +<div v-else class="_panel" :class="$style.muted" @click="muted = false"> <I18n :src="i18n.ts.userSaysSomething" tag="small"> <template #name> - <MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)"> + <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)"> <MkUserName :user="appearNote.user"/> </MkA> </template> @@ -438,318 +435,249 @@ if (appearNote.replyId) { } </script> -<style lang="scss" scoped> -.lxwezrsl { +<style lang="scss" module> +.root { position: relative; transition: box-shadow 0.1s ease; overflow: clip; contain: content; +} - &:focus-visible { - outline: none; +.replyTo { + opacity: 0.7; + padding-bottom: 0; +} - &:after { - content: ""; - pointer-events: none; - display: block; - position: absolute; - z-index: 10; - top: 0; - left: 0; - right: 0; - bottom: 0; - margin: auto; - width: calc(100% - 8px); - height: calc(100% - 8px); - border: dashed 1px var(--focus); - border-radius: var(--radius); - box-sizing: border-box; - } - } +.replyToMore { + opacity: 0.7; +} - &:hover > .article > .main > .footer > .button { +.renote { + display: flex; + align-items: center; + padding: 16px 32px 8px 32px; + line-height: 28px; + white-space: pre; + color: var(--renote); +} + +.renoteAvatar { + flex-shrink: 0; + display: inline-block; + width: 28px; + height: 28px; + margin: 0 8px 0 0; + border-radius: 6px; +} + +.renoteText { + overflow: hidden; + flex-shrink: 1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.renoteName { + font-weight: bold; +} + +.renoteInfo { + margin-left: auto; + font-size: 0.9em; +} + +.renoteTime { + flex-shrink: 0; + color: inherit; +} + +.renote + .note { + padding-top: 8px; +} + +.note { + padding: 32px; + font-size: 1.2em; + + &:hover > .main > .footer > .button { opacity: 1; } - - > .reply-to { - opacity: 0.7; - padding-bottom: 0; - } - - > .reply-to-more { - opacity: 0.7; - } - - > .renote { - display: flex; - align-items: center; - padding: 16px 32px 8px 32px; - line-height: 28px; - white-space: pre; - color: var(--renote); - - > .avatar { - flex-shrink: 0; - display: inline-block; - width: 28px; - height: 28px; - margin: 0 8px 0 0; - border-radius: 6px; - } - - > i { - margin-right: 4px; - } - - > span { - overflow: hidden; - flex-shrink: 1; - text-overflow: ellipsis; - white-space: nowrap; - - > .name { - font-weight: bold; - } - } - - > .info { - margin-left: auto; - font-size: 0.9em; - - > .time { - flex-shrink: 0; - color: inherit; - - > .dropdownIcon { - margin-right: 4px; - } - } - } - } - - > .renote + .article { - padding-top: 8px; - } - - > .article { - padding: 32px; - font-size: 1.2em; - - > .header { - display: flex; - position: relative; - margin-bottom: 16px; - align-items: center; - - > .avatar { - display: block; - flex-shrink: 0; - width: 58px; - height: 58px; - } - - > .body { - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - padding-left: 16px; - font-size: 0.95em; - - > .top { - > .name { - font-weight: bold; - line-height: 1.3; - } - - > .is-bot { - display: inline-block; - margin: 0 0.5em; - padding: 4px 6px; - font-size: 80%; - line-height: 1; - border: solid 0.5px var(--divider); - border-radius: 4px; - } - - > .info { - float: right; - } - } - - > .username { - margin-bottom: 2px; - line-height: 1.3; - word-wrap: anywhere; - } - } - } - - > .main { - > .body { - container-type: inline-size; - - > .cw { - cursor: default; - display: block; - margin: 0; - padding: 0; - overflow-wrap: break-word; - - > .text { - margin-right: 8px; - } - } - - > .content { - > .text { - overflow-wrap: break-word; - - > .reply { - color: var(--accent); - margin-right: 0.5em; - } - - > .rp { - margin-left: 4px; - font-style: oblique; - color: var(--renote); - } - - > .translation { - border: solid 0.5px var(--divider); - border-radius: var(--radius); - padding: 12px; - margin-top: 8px; - } - } - - > .url-preview { - margin-top: 8px; - } - - > .poll { - font-size: 80%; - } - - > .renote { - padding: 8px 0; - - > .note { - padding: 16px; - border: dashed 1px var(--renote); - border-radius: 8px; - } - } - } - - > .channel { - opacity: 0.7; - font-size: 80%; - } - } - - > .footer { - > .info { - margin: 16px 0; - opacity: 0.7; - font-size: 0.9em; - } - - > .button { - margin: 0; - padding: 8px; - opacity: 0.7; - - &:not(:last-child) { - margin-right: 28px; - } - - &:hover { - color: var(--fgHighlighted); - } - - > .count { - display: inline; - margin: 0 0 0 8px; - opacity: 0.7; - } - - &.reacted { - color: var(--accent); - } - } - } - } - } - - > .reply { - border-top: solid 0.5px var(--divider); - } +} + +.noteHeader { + display: flex; + position: relative; + margin-bottom: 16px; + align-items: center; +} + +.noteHeaderAvatar { + display: block; + flex-shrink: 0; + width: 58px; + height: 58px; +} + +.noteHeaderBody { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + padding-left: 16px; + font-size: 0.95em; +} + +.noteHeaderName { + font-weight: bold; + line-height: 1.3; +} + +.isBot { + display: inline-block; + margin: 0 0.5em; + padding: 4px 6px; + font-size: 80%; + line-height: 1; + border: solid 0.5px var(--divider); + border-radius: 4px; +} + +.noteHeaderInfo { + float: right; +} + +.noteHeaderUsername { + margin-bottom: 2px; + line-height: 1.3; + word-wrap: anywhere; +} + +.noteContent { + container-type: inline-size; + overflow-wrap: break-word; +} + +.cw { + cursor: default; + display: block; + margin: 0; + padding: 0; + overflow-wrap: break-word; +} + +.noteReplyTarget { + color: var(--accent); + margin-right: 0.5em; +} + +.rn { + margin-left: 4px; + font-style: oblique; + color: var(--renote); +} + +.translation { + border: solid 0.5px var(--divider); + border-radius: var(--radius); + padding: 12px; + margin-top: 8px; +} + +.poll { + font-size: 80%; +} + +.quote { + padding: 8px 0; +} + +.quoteNote { + padding: 16px; + border: dashed 1px var(--renote); + border-radius: 8px; +} + +.channel { + opacity: 0.7; + font-size: 80%; +} + +.noteFooterInfo { + margin: 16px 0; + opacity: 0.7; + font-size: 0.9em; +} + +.noteFooterButton { + margin: 0; + padding: 8px; + opacity: 0.7; + + &:not(:last-child) { + margin-right: 28px; + } + + &:hover { + color: var(--fgHighlighted); + } +} + +.noteFooterButtonCount { + display: inline; + margin: 0 0 0 8px; + opacity: 0.7; + + &.reacted { + color: var(--accent); + } +} + +.reply { + border-top: solid 0.5px var(--divider); } @container (max-width: 500px) { - .lxwezrsl { + .root { font-size: 0.9em; } } @container (max-width: 450px) { - .lxwezrsl { - > .renote { - padding: 8px 16px 0 16px; - } + .renote { + padding: 8px 16px 0 16px; + } - > .article { - padding: 16px; + .note { + padding: 16px; + } - > .header { - > .avatar { - width: 50px; - height: 50px; - } - } - } + .noteHeaderAvatar { + width: 50px; + height: 50px; } } @container (max-width: 350px) { - .lxwezrsl { - > .article { - > .main { - > .footer { - > .button { - &:not(:last-child) { - margin-right: 18px; - } - } - } - } + .noteFooterButton { + &:not(:last-child) { + margin-right: 18px; } } } @container (max-width: 300px) { - .lxwezrsl { + .root { font-size: 0.825em; + } - > .article { - > .header { - > .avatar { - width: 50px; - height: 50px; - } - } + .noteHeaderAvatar { + width: 50px; + height: 50px; + } - > .main { - > .footer { - > .button { - &:not(:last-child) { - margin-right: 12px; - } - } - } - } + .noteFooterButton { + &:not(:last-child) { + margin-right: 12px; } } } diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue index 6b55c2786..6786f8b25 100644 --- a/packages/frontend/src/components/MkNotePreview.vue +++ b/packages/frontend/src/components/MkNotePreview.vue @@ -6,7 +6,7 @@ <MkUserName :user="$i" :nowrap="true"/> </div> <div> - <div :class="$style.content"> + <div> <Mfm :text="text.trim()" :author="$i" :i="$i"/> </div> </div> diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index bd27a43b6..21be1454a 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -5,7 +5,7 @@ <MkNoteHeader :class="$style.header" :note="note" :mini="true"/> <div> <p v-if="note.cw != null" :class="$style.cw"> - <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :i="$i" :emoji-urls="note.emojis"/> + <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :i="$i" :emojiUrls="note.emojis"/> <MkCwButton v-model="showContent" :note="note"/> </p> <div v-show="note.cw == null || showContent"> diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue index a4e949c89..9cc2b7a96 100644 --- a/packages/frontend/src/components/MkNotes.vue +++ b/packages/frontend/src/components/MkNotes.vue @@ -15,7 +15,7 @@ :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" - :no-gap="noGap" + :noGap="noGap" :ad="true" :class="$style.notes" > diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index efae687e6..d25332b10 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -5,7 +5,19 @@ <MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/> <MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/> <img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/> - <div :class="[$style.subIcon, $style['t_' + notification.type]]"> + <div + :class="[$style.subIcon, { + [$style.t_follow]: notification.type === 'follow', + [$style.t_followRequestAccepted]: notification.type === 'followRequestAccepted', + [$style.t_receiveFollowRequest]: notification.type === 'receiveFollowRequest', + [$style.t_renote]: notification.type === 'renote', + [$style.t_reply]: notification.type === 'reply', + [$style.t_mention]: notification.type === 'mention', + [$style.t_quote]: notification.type === 'quote', + [$style.t_pollEnded]: notification.type === 'pollEnded', + [$style.t_achievementEarned]: notification.type === 'achievementEarned', + }]" + > <i v-if="notification.type === 'follow'" class="ti ti-plus"></i> <i v-else-if="notification.type === 'receiveFollowRequest'" class="ti ti-clock"></i> <i v-else-if="notification.type === 'followRequestAccepted'" class="ti ti-check"></i> @@ -20,8 +32,8 @@ v-else-if="notification.type === 'reaction'" ref="reactionRef" :reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction" - :custom-emojis="notification.note.emojis" - :no-style="true" + :customEmojis="notification.note.emojis" + :noStyle="true" style="width: 100%; height: 100%;" /> </div> @@ -34,7 +46,7 @@ <span v-else>{{ notification.header }}</span> <MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/> </header> - <div :class="$style.content"> + <div> <MkA v-if="notification.type === 'reaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> <i class="ti ti-quote" :class="$style.quote"></i> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/> @@ -243,9 +255,6 @@ useTooltip(reactionRef, (showing) => { font-size: 0.9em; } -.content { -} - .text { display: flex; width: 100%; diff --git a/packages/frontend/src/components/MkNotificationSettingWindow.vue b/packages/frontend/src/components/MkNotificationSettingWindow.vue index f6d0e5681..598d3a055 100644 --- a/packages/frontend/src/components/MkNotificationSettingWindow.vue +++ b/packages/frontend/src/components/MkNotificationSettingWindow.vue @@ -3,15 +3,15 @@ ref="dialog" :width="400" :height="450" - :with-ok-button="true" - :ok-button-disabled="false" + :withOkButton="true" + :okButtonDisabled="false" @ok="ok()" @close="dialog?.close()" @closed="emit('closed')" > <template #header>{{ i18n.ts.notificationSetting }}</template> - <MkSpacer :margin-min="20" :margin-max="28"> + <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps_m"> <template v-if="showGlobalToggle"> <MkSwitch v-model="useGlobalSetting"> diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 1aea95fe0..70224bffa 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -8,9 +8,9 @@ </template> <template #default="{ items: notifications }"> - <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :no-gap="true"> + <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/> - <XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/> + <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel notification"/> </MkDateSeparatedList> </template> </MkPagination> @@ -22,7 +22,7 @@ import MkPagination, { Paging } from '@/components/MkPagination.vue'; import XNotification from '@/components/MkNotification.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkNote from '@/components/MkNote.vue'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { $i } from '@/account'; import { i18n } from '@/i18n'; import { notificationTypes } from '@/const'; @@ -45,7 +45,7 @@ const pagination: Paging = { const onNotification = (notification) => { const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type); if (isMuted || document.visibilityState === 'visible') { - stream.send('readNotification'); + useStream().send('readNotification'); } if (!isMuted) { @@ -56,7 +56,7 @@ const onNotification = (notification) => { let connection; onMounted(() => { - connection = stream.useChannel('main'); + connection = useStream().useChannel('main'); connection.on('notification', onNotification); }); diff --git a/packages/frontend/src/components/MkObjectView.value.vue b/packages/frontend/src/components/MkObjectView.value.vue index e7fc73bce..d48e7886e 100644 --- a/packages/frontend/src/components/MkObjectView.value.vue +++ b/packages/frontend/src/components/MkObjectView.value.vue @@ -28,54 +28,38 @@ </div> </template> -<script lang="ts"> -import { defineComponent, reactive } from 'vue'; +<script lang="ts" setup> +import { reactive } from 'vue'; import number from '@/filters/number'; +import XValue from '@/components/MkObjectView.value.vue'; -export default defineComponent({ - name: 'XValue', +const props = defineProps<{ + value: any; +}>(); - props: { - value: { - required: true, - }, - }, +const collapsed = reactive({}); - setup(props) { - const collapsed = reactive({}); +if (isObject(props.value)) { + for (const key in props.value) { + collapsed[key] = collapsable(props.value[key]); + } +} - if (isObject(props.value)) { - for (const key in props.value) { - collapsed[key] = collapsable(props.value[key]); - } - } +function isObject(v): boolean { + return typeof v === 'object' && !Array.isArray(v) && v !== null; +} - function isObject(v): boolean { - return typeof v === 'object' && !Array.isArray(v) && v !== null; - } +function isArray(v): boolean { + return Array.isArray(v); +} - function isArray(v): boolean { - return Array.isArray(v); - } +function isEmpty(v): boolean { + return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0); +} - function isEmpty(v): boolean { - return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0); - } - - function collapsable(v): boolean { - return (isObject(v) || isArray(v)) && !isEmpty(v); - } - - return { - number, - collapsed, - isObject, - isArray, - isEmpty, - collapsable, - }; - }, -}); +function collapsable(v): boolean { + return (isObject(v) || isArray(v)) && !isEmpty(v); +} </script> <style lang="scss" scoped> diff --git a/packages/frontend/src/components/MkObjectView.vue b/packages/frontend/src/components/MkObjectView.vue index 55578a37f..8b1ed7414 100644 --- a/packages/frontend/src/components/MkObjectView.vue +++ b/packages/frontend/src/components/MkObjectView.vue @@ -1,5 +1,5 @@ <template> -<div class="zhyxdalp"> +<div> <XValue :value="value" :collapsed="false"/> </div> </template> @@ -12,9 +12,3 @@ const props = defineProps<{ value: Record<string, unknown>; }>(); </script> - -<style lang="scss" scoped> -.zhyxdalp { - -} -</style> diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue index e2d68d12c..668f9ff5a 100644 --- a/packages/frontend/src/components/MkOmit.vue +++ b/packages/frontend/src/components/MkOmit.vue @@ -8,7 +8,7 @@ </template> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { onMounted, onUnmounted } from 'vue'; import { i18n } from '@/i18n'; const props = withDefaults(defineProps<{ @@ -21,16 +21,22 @@ let content = $shallowRef<HTMLElement>(); let omitted = $ref(false); let ignoreOmit = $ref(false); -onMounted(() => { - const calcOmit = () => { - if (omitted || ignoreOmit) return; - omitted = content.offsetHeight > props.maxHeight; - }; +const calcOmit = () => { + if (omitted || ignoreOmit) return; + omitted = content.offsetHeight > props.maxHeight; +}; +const omitObserver = new ResizeObserver((entries, observer) => { calcOmit(); - new ResizeObserver((entries, observer) => { - calcOmit(); - }).observe(content); +}); + +onMounted(() => { + calcOmit(); + omitObserver.observe(content); +}); + +onUnmounted(() => { + omitObserver.disconnect(); }); </script> diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index 02ce58451..709b5a52d 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -1,23 +1,23 @@ <template> <MkWindow ref="windowEl" - :initial-width="500" - :initial-height="500" - :can-resize="true" - :close-button="true" - :buttons-left="buttonsLeft" - :buttons-right="buttonsRight" + :initialWidth="500" + :initialHeight="500" + :canResize="true" + :closeButton="true" + :buttonsLeft="buttonsLeft" + :buttonsRight="buttonsRight" :contextmenu="contextmenu" @closed="$emit('closed')" > <template #header> <template v-if="pageMetadata?.value"> - <i v-if="pageMetadata.value.icon" class="icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i> + <i v-if="pageMetadata.value.icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i> <span>{{ pageMetadata.value.title }}</span> </template> </template> - <div :class="$style.root" :style="{ background: pageMetadata?.value?.bg }" style="container-type: inline-size;"> + <div :class="$style.root" style="container-type: inline-size;"> <RouterView :key="reloadCount" :router="router"/> </div> </MkWindow> diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index cd8af560e..740094b11 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -1,9 +1,9 @@ <template> <Transition - :enter-active-class="defaultStore.state.animation ? $style.transition_fade_enterActive : ''" - :leave-active-class="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''" - :enter-from-class="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''" - :leave-to-class="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''" + :enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''" mode="out-in" > <MkLoading v-if="fetching"/> diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index 0810061ff..464e34011 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -1,19 +1,19 @@ <template> -<div class="tivcixzd" :class="{ done: closed || isVoted }"> - <ul> - <li v-for="(choice, i) in note.poll.choices" :key="i" :class="{ voted: choice.voted }" @click="vote(i)"> - <div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> - <span> - <template v-if="choice.isVoted"><i class="ti ti-check"></i></template> +<div :class="{ [$style.done]: closed || isVoted }"> + <ul :class="$style.choices"> + <li v-for="(choice, i) in note.poll.choices" :key="i" :class="$style.choice" @click="vote(i)"> + <div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> + <span :class="$style.fg"> + <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template> <Mfm :text="choice.text" :plain="true"/> - <span v-if="showResult" class="votes">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span> + <span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span> </span> </li> </ul> - <p v-if="!readOnly"> + <p v-if="!readOnly" :class="$style.info"> <span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span> <span> · </span> - <a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a> + <a v-if="!closed && !isVoted" style="color: inherit;" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a> <span v-if="isVoted">{{ i18n.ts._poll.voted }}</span> <span v-else-if="closed">{{ i18n.ts._poll.closed }}</span> <span v-if="remaining > 0"> · {{ timer }}</span> @@ -86,67 +86,51 @@ const vote = async (id) => { }; </script> -<style lang="scss" scoped> -.tivcixzd { - > ul { - display: block; - margin: 0; - padding: 0; - list-style: none; +<style lang="scss" module> +.choices { + display: block; + margin: 0; + padding: 0; + list-style: none; +} - > li { - display: block; - position: relative; - margin: 4px 0; - padding: 4px; - //border: solid 0.5px var(--divider); - background: var(--accentedBg); - border-radius: 4px; - overflow: clip; - cursor: pointer; +.choice { + display: block; + position: relative; + margin: 4px 0; + padding: 4px; + //border: solid 0.5px var(--divider); + background: var(--accentedBg); + border-radius: 4px; + overflow: clip; + cursor: pointer; +} - > .backdrop { - position: absolute; - top: 0; - left: 0; - height: 100%; - background: var(--accent); - background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB)); - transition: width 1s ease; - } +.bg { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: var(--accent); + background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB)); + transition: width 1s ease; +} - > span { - position: relative; - display: inline-block; - padding: 3px 5px; - background: var(--panel); - border-radius: 3px; +.fg { + position: relative; + display: inline-block; + padding: 3px 5px; + background: var(--panel); + border-radius: 3px; +} - > i { - margin-right: 4px; - color: var(--accent); - } +.info { + color: var(--fg); +} - > .votes { - margin-left: 4px; - opacity: 0.7; - } - } - } - } - - > p { - color: var(--fg); - - a { - color: inherit; - } - } - - &.done { - > ul > li { - cursor: default; - } +.done { + .choice { + cursor: default; } } </style> diff --git a/packages/frontend/src/components/MkPollEditor.vue b/packages/frontend/src/components/MkPollEditor.vue index 471ec3916..2da933994 100644 --- a/packages/frontend/src/components/MkPollEditor.vue +++ b/packages/frontend/src/components/MkPollEditor.vue @@ -5,7 +5,7 @@ </p> <ul> <li v-for="(choice, i) in choices" :key="i"> - <MkInput class="input" small :model-value="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:model-value="onInput(i, $event)"> + <MkInput class="input" small :modelValue="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)"> </MkInput> <button class="_button" @click="remove(i)"> <i class="ti ti-x"></i> diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue index 93b9eb401..30af36566 100644 --- a/packages/frontend/src/components/MkPopupMenu.vue +++ b/packages/frontend/src/components/MkPopupMenu.vue @@ -1,6 +1,6 @@ <template> -<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')"> - <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="modal.close()"/> +<MkModal ref="modal" v-slot="{ type, maxHeight }" :zPriority="'high'" :src="src" :transparentBg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')"> + <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="modal.close()"/> </MkModal> </template> diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index c65cb7d6e..5c6556968 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -22,21 +22,21 @@ <span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span> <span :class="$style.headerRightButtonText">{{ i18n.ts._visibility[visibility] }}</span> </button> - <button v-else :class="['_button', $style.headerRightItem, $style.visibility]" disabled> + <button v-else class="_button" :class="[$style.headerRightItem, $style.visibility]" disabled> <span><i class="ti ti-device-tv"></i></span> <span :class="$style.headerRightButtonText">{{ channel.name }}</span> </button> </template> - <button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" :class="['_button', $style.headerRightItem, $style.localOnly, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly"> + <button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly"> <span v-if="!localOnly"><i class="ti ti-rocket"></i></span> <span v-else><i class="ti ti-rocket-off"></i></span> </button> - <button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" :class="['_button', $style.headerRightItem, $style.reactionAcceptance, { [$style.danger]: reactionAcceptance }]" @click="toggleReactionAcceptance"> + <button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance"> <span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span> <span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span> <span v-else><i class="ti ti-icons"></i></span> </button> - <button v-click-anime class="_button" :class="[$style.submit, { [$style.submitPosting]: posting }]" :disabled="!canPost" data-cy-open-post-form-submit @click="post"> + <button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post"> <div :class="$style.submitInner"> <template v-if="posted"></template> <template v-else-if="posting"><MkEllipsis/></template> @@ -66,7 +66,7 @@ <div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div> </div> <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> - <XPostFormAttaches v-model="files" :class="$style.attaches" @detach="detachFile" @change-sensitive="updateFileSensitive" @change-name="updateFileName"/> + <XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/> <MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/> <div v-if="showingOptions" style="padding: 8px 16px;"> @@ -484,8 +484,10 @@ async function toggleReactionAcceptance() { title: i18n.ts.reactionAcceptance, items: [ { value: null, text: i18n.ts.all }, - { value: 'likeOnly' as const, text: i18n.ts.likeOnly }, { value: 'likeOnlyForRemote' as const, text: i18n.ts.likeOnlyForRemote }, + { value: 'nonSensitiveOnly' as const, text: i18n.ts.nonSensitiveOnly }, + { value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, text: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }, + { value: 'likeOnly' as const, text: i18n.ts.likeOnly }, ], default: reactionAcceptance, }); diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index 760c6e5d0..18fa142eb 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -1,16 +1,16 @@ <template> -<div v-show="props.modelValue.length != 0" class="skeikyzd"> - <Sortable :model-value="props.modelValue" class="files" item-key="id" :animation="150" :delay="100" :delay-on-touch-only="true" @update:model-value="v => emit('update:modelValue', v)"> +<div v-show="props.modelValue.length != 0" :class="$style.root"> + <Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)"> <template #item="{element}"> - <div class="file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)"> - <MkDriveFileThumbnail :data-id="element.id" class="thumbnail" :file="element" fit="cover"/> - <div v-if="element.isSensitive" class="sensitive"> - <i class="ti ti-alert-triangle icon"></i> + <div :class="$style.file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)"> + <MkDriveFileThumbnail :data-id="element.id" :class="$style.thumbnail" :file="element" fit="cover"/> + <div v-if="element.isSensitive" :class="$style.sensitive"> + <i class="ti ti-alert-triangle" style="margin: auto;"></i> </div> </div> </template> </Sortable> - <p class="remain">{{ 16 - props.modelValue.length }}/16</p> + <p :class="$style.remain">{{ 16 - props.modelValue.length }}/16</p> </div> </template> @@ -93,7 +93,7 @@ function showFileMenu(file, ev: MouseEvent) { action: () => { rename(file); }, }, { text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, - icon: file.isSensitive ? 'ti ti-eye-off' : 'ti ti-eye', + icon: file.isSensitive ? 'ti ti-eye-exclamation' : 'ti ti-eye', action: () => { toggleSensitive(file); }, }, { text: i18n.ts.describeFile, @@ -108,60 +108,53 @@ function showFileMenu(file, ev: MouseEvent) { } </script> -<style lang="scss" scoped> -.skeikyzd { +<style lang="scss" module> +.root { padding: 8px 16px; position: relative; +} - > .files { - display: flex; - flex-wrap: wrap; +.files { + display: flex; + flex-wrap: wrap; +} - > .file { - position: relative; - width: 64px; - height: 64px; - margin-right: 4px; - border-radius: 4px; - overflow: hidden; - cursor: move; +.file { + position: relative; + width: 64px; + height: 64px; + margin-right: 4px; + border-radius: 4px; + overflow: hidden; + cursor: move; +} - &:hover > .remove { - display: block; - } +.thumbnail { + width: 100%; + height: 100%; + z-index: 1; + color: var(--fg); +} - > .thumbnail { - width: 100%; - height: 100%; - z-index: 1; - color: var(--fg); - } +.sensitive { + display: flex; + position: absolute; + width: 64px; + height: 64px; + top: 0; + left: 0; + z-index: 2; + background: rgba(17, 17, 17, .7); + color: #fff; +} - > .sensitive { - display: flex; - position: absolute; - width: 64px; - height: 64px; - top: 0; - left: 0; - z-index: 2; - background: rgba(17, 17, 17, .7); - color: #fff; - - > .icon { - margin: auto; - } - } - } - } - - > .remain { - display: block; - position: absolute; - top: 8px; - right: 8px; - margin: 0; - padding: 0; - } +.remain { + display: block; + position: absolute; + top: 8px; + right: 8px; + margin: 0; + padding: 0; + font-size: 90%; } </style> diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue index 6326c498d..98af92c6f 100644 --- a/packages/frontend/src/components/MkPostFormDialog.vue +++ b/packages/frontend/src/components/MkPostFormDialog.vue @@ -1,6 +1,6 @@ <template> -<MkModal ref="modal" :prefer-type="'dialog'" @click="modal.close()" @closed="onModalClosed()"> - <MkPostForm ref="form" style="margin: 0 auto auto auto;" v-bind="props" autofocus freeze-after-posted @posted="onPosted" @cancel="modal.close()" @esc="modal.close()"/> +<MkModal ref="modal" :preferType="'dialog'" @click="modal.close()" @closed="onModalClosed()"> + <MkPostForm ref="form" style="margin: 0 auto auto auto;" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal.close()" @esc="modal.close()"/> </MkModal> </template> diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue index b98c814f2..448084d9b 100644 --- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue +++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue @@ -72,28 +72,28 @@ function subscribe() { userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(instance.swPublickey), }) - .then(async subscription => { - pushSubscription = subscription; + .then(async subscription => { + pushSubscription = subscription; - // Register - pushRegistrationInServer = await api('sw/register', { - endpoint: subscription.endpoint, - auth: encode(subscription.getKey('auth')), - publickey: encode(subscription.getKey('p256dh')), - }); - }, async err => { // When subscribe failed + // Register + pushRegistrationInServer = await api('sw/register', { + endpoint: subscription.endpoint, + auth: encode(subscription.getKey('auth')), + publickey: encode(subscription.getKey('p256dh')), + }); + }, async err => { // When subscribe failed // 通知が許可されていなかったとき - if (err?.name === 'NotAllowedError') { - console.info('User denied the notification permission request.'); - return; - } + if (err?.name === 'NotAllowedError') { + console.info('User denied the notification permission request.'); + return; + } - // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが - // 既に存在していることが原因でエラーになった可能性があるので、 - // そのサブスクリプションを解除しておく - // (これは実行されなさそうだけど、おまじない的に古い実装から残してある) - await unsubscribe(); - }), null, null); + // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが + // 既に存在していることが原因でエラーになった可能性があるので、 + // そのサブスクリプションを解除しておく + // (これは実行されなさそうだけど、おまじない的に古い実装から残してある) + await unsubscribe(); + }), null, null); } async function unsubscribe() { diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue index e2240fb4e..84be10078 100644 --- a/packages/frontend/src/components/MkRadios.vue +++ b/packages/frontend/src/components/MkRadios.vue @@ -1,37 +1,27 @@ <script lang="ts"> -import { VNode, defineComponent, h } from 'vue'; +import { VNode, defineComponent, h, ref, watch } from 'vue'; import MkRadio from './MkRadio.vue'; export default defineComponent({ - components: { - MkRadio, - }, props: { modelValue: { required: false, }, }, - data() { - return { - value: this.modelValue, - }; - }, - watch: { - value() { - this.$emit('update:modelValue', this.value); - }, - }, - render() { - console.log(this.$slots, this.$slots.label && this.$slots.label()); - if (!this.$slots.default) return null; - let options = this.$slots.default(); - const label = this.$slots.label && this.$slots.label(); - const caption = this.$slots.caption && this.$slots.caption(); + setup(props, context) { + const value = ref(props.modelValue); + watch(value, () => { + context.emit('update:modelValue', value.value); + }); + if (!context.slots.default) return null; + let options = context.slots.default(); + const label = context.slots.label && context.slots.label(); + const caption = context.slots.caption && context.slots.caption(); // なぜかFragmentになることがあるため if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[]; - return h('div', { + return () => h('div', { class: 'novjtcto', }, [ ...(label ? [h('div', { @@ -42,8 +32,8 @@ export default defineComponent({ }, options.map(option => h(MkRadio, { key: option.key, value: option.props?.value, - modelValue: this.value, - 'onUpdate:modelValue': value => this.value = value, + modelValue: value.value, + 'onUpdate:modelValue': _v => value.value = _v, }, () => option.children)), ), ...(caption ? [h('div', { diff --git a/packages/frontend/src/components/MkReactedUsersDialog.vue b/packages/frontend/src/components/MkReactedUsersDialog.vue index 0c0cc3669..cd2a359d5 100644 --- a/packages/frontend/src/components/MkReactedUsersDialog.vue +++ b/packages/frontend/src/components/MkReactedUsersDialog.vue @@ -8,7 +8,7 @@ > <template #header>{{ i18n.ts.reactionsList }}</template> - <MkSpacer :margin-min="20" :margin-max="28"> + <MkSpacer :marginMin="20" :marginMax="28"> <div v-if="note" class="_gaps"> <div v-if="reactions.length === 0" class="_fullinfo"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> @@ -22,7 +22,7 @@ </button> </div> <MkA v-for="user in users" :key="user.id" :to="userPage(user)" @click="dialog.close()"> - <MkUserCardMini :user="user" :with-chart="false"/> + <MkUserCardMini :user="user" :withChart="false"/> </MkA> </template> </div> diff --git a/packages/frontend/src/components/MkReactionIcon.vue b/packages/frontend/src/components/MkReactionIcon.vue index 29b3f9b85..dfb06f63c 100644 --- a/packages/frontend/src/components/MkReactionIcon.vue +++ b/packages/frontend/src/components/MkReactionIcon.vue @@ -1,6 +1,6 @@ <template> -<MkCustomEmoji v-if="reaction[0] === ':'" :name="reaction" :normal="true" :no-style="noStyle" :url="emojiUrl"/> -<MkEmoji v-else :emoji="reaction" :normal="true" :no-style="noStyle"/> +<MkCustomEmoji v-if="reaction[0] === ':'" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl"/> +<MkEmoji v-else :emoji="reaction" :normal="true" :noStyle="noStyle"/> </template> <script lang="ts" setup> diff --git a/packages/frontend/src/components/MkReactionTooltip.vue b/packages/frontend/src/components/MkReactionTooltip.vue index 4d67dc3da..34afa7223 100644 --- a/packages/frontend/src/components/MkReactionTooltip.vue +++ b/packages/frontend/src/components/MkReactionTooltip.vue @@ -1,7 +1,7 @@ <template> -<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> +<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="340" @closed="emit('closed')"> <div :class="$style.root"> - <MkReactionIcon :reaction="reaction" :class="$style.icon" :no-style="true"/> + <MkReactionIcon :reaction="reaction" :class="$style.icon" :noStyle="true"/> <div :class="$style.name">{{ reaction.replace('@.', '') }}</div> </div> </MkTooltip> diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue index f5e611c62..99960f5d2 100644 --- a/packages/frontend/src/components/MkReactionsViewer.details.vue +++ b/packages/frontend/src/components/MkReactionsViewer.details.vue @@ -1,8 +1,8 @@ <template> -<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> +<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="340" @closed="emit('closed')"> <div :class="$style.root"> <div :class="$style.reaction"> - <MkReactionIcon :reaction="reaction" :class="$style.reactionIcon" :no-style="true"/> + <MkReactionIcon :reaction="reaction" :class="$style.reactionIcon" :noStyle="true"/> <div :class="$style.reactionName">{{ getReactionName(reaction) }}</div> </div> <div :class="$style.users"> diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 9480af510..aabebb3ab 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -6,7 +6,7 @@ :class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.large]: defaultStore.state.largeNoteReactions }]" @click="toggleReaction()" > - <MkReactionIcon :class="$style.icon" :reaction="reaction" :emoji-url="note.reactionEmojis[reaction.substr(1, reaction.length - 2)]"/> + <MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substr(1, reaction.length - 2)]"/> <span :class="$style.count">{{ count }}</span> </button> </template> @@ -22,6 +22,7 @@ import { $i } from '@/account'; import MkReactionEffect from '@/components/MkReactionEffect.vue'; import { claimAchievement } from '@/scripts/achievements'; import { defaultStore } from '@/store'; +import { i18n } from '@/i18n'; const props = defineProps<{ reaction: string; @@ -34,11 +35,19 @@ const buttonEl = shallowRef<HTMLElement>(); const canToggle = computed(() => !props.reaction.match(/@\w/) && $i); -const toggleReaction = () => { +async function toggleReaction() { if (!canToggle.value) return; + // TODO: その絵文字を使う権限があるかどうか確認 + const oldReaction = props.note.myReaction; if (oldReaction) { + const confirm = await os.confirm({ + type: 'warning', + text: oldReaction !== props.reaction ? i18n.ts.changeReactionConfirm : i18n.ts.cancelReactionConfirm, + }); + if (confirm.canceled) return; + os.api('notes/reactions/delete', { noteId: props.note.id, }).then(() => { @@ -58,9 +67,9 @@ const toggleReaction = () => { claimAchievement('reactWithoutRead'); } } -}; +} -const anime = () => { +function anime() { if (document.hidden) return; if (!defaultStore.state.animation) return; @@ -68,7 +77,7 @@ const anime = () => { const x = rect.left + 16; const y = rect.top + (buttonEl.value.offsetHeight / 2); os.popup(MkReactionEffect, { reaction: props.reaction, x, y }, {}, 'end'); -}; +} watch(() => props.count, (newCount, oldCount) => { if (oldCount < newCount) anime(); diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue index 3219c8a92..ce146463e 100644 --- a/packages/frontend/src/components/MkReactionsViewer.vue +++ b/packages/frontend/src/components/MkReactionsViewer.vue @@ -1,13 +1,13 @@ <template> <TransitionGroup - :enter-active-class="defaultStore.state.animation ? $style.transition_x_enterActive : ''" - :leave-active-class="defaultStore.state.animation ? $style.transition_x_leaveActive : ''" - :enter-from-class="defaultStore.state.animation ? $style.transition_x_enterFrom : ''" - :leave-to-class="defaultStore.state.animation ? $style.transition_x_leaveTo : ''" - :move-class="defaultStore.state.animation ? $style.transition_x_move : ''" + :enterActiveClass="defaultStore.state.animation ? $style.transition_x_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_x_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_x_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_x_leaveTo : ''" + :moveClass="defaultStore.state.animation ? $style.transition_x_move : ''" tag="div" :class="$style.root" > - <XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note"/> + <XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note"/> <slot v-if="hasMoreReactions" name="more"/> </TransitionGroup> </template> diff --git a/packages/frontend/src/components/MkRenotedUsersDialog.vue b/packages/frontend/src/components/MkRenotedUsersDialog.vue index 56025535f..814a68d4d 100644 --- a/packages/frontend/src/components/MkRenotedUsersDialog.vue +++ b/packages/frontend/src/components/MkRenotedUsersDialog.vue @@ -8,7 +8,7 @@ > <template #header>{{ i18n.ts.renotesList }}</template> - <MkSpacer :margin-min="20" :margin-max="28"> + <MkSpacer :marginMin="20" :marginMax="28"> <div v-if="renotes" class="_gaps"> <div v-if="renotes.length === 0" class="_fullinfo"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> @@ -16,7 +16,7 @@ </div> <template v-else> <MkA v-for="user in users" :key="user.id" :to="userPage(user)" @click="dialog.close()"> - <MkUserCardMini :user="user" :with-chart="false"/> + <MkUserCardMini :user="user" :withChart="false"/> </MkA> </template> </div> diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue index 8bd027980..9f56189f3 100644 --- a/packages/frontend/src/components/MkRetentionLineChart.vue +++ b/packages/frontend/src/components/MkRetentionLineChart.vue @@ -124,7 +124,3 @@ onMounted(async () => { }); }); </script> - -<style lang="scss" scoped> - -</style> diff --git a/packages/frontend/src/components/MkRippleEffect.vue b/packages/frontend/src/components/MkRippleEffect.vue index 9d93211d5..60c3a4738 100644 --- a/packages/frontend/src/components/MkRippleEffect.vue +++ b/packages/frontend/src/components/MkRippleEffect.vue @@ -1,7 +1,7 @@ <template> -<div class="vswabwbm" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }"> +<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }"> <svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"> - <circle fill="none" cx="64" cy="64"> + <circle fill="none" cx="64" cy="64" style="stroke: var(--accent);"> <animate attributeName="r" begin="0s" dur="0.5s" @@ -22,7 +22,7 @@ /> </circle> <g fill="none" fill-rule="evenodd"> - <circle v-for="(particle, i) in particles" :key="i" :fill="particle.color"> + <circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--accent);"> <animate attributeName="r" begin="0s" dur="0.8s" @@ -100,17 +100,11 @@ onMounted(() => { }); </script> -<style lang="scss" scoped> -.vswabwbm { +<style lang="scss" module> +.root { pointer-events: none; position: fixed; width: 128px; height: 128px; - - > svg { - > circle { - stroke: var(--accent); - } - } } </style> diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue index 2f5866f34..9fbe1ec99 100644 --- a/packages/frontend/src/components/MkRolePreview.vue +++ b/packages/frontend/src/components/MkRolePreview.vue @@ -12,8 +12,10 @@ </template> </span> <span :class="$style.name">{{ role.name }}</span> - <span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span> - <span v-else-if="role.target === 'conditional'" :class="$style.users">({{ i18n.ts._role.conditional }})</span> + <template v-if="detailed"> + <span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span> + <span v-else-if="role.target === 'conditional'" :class="$style.users">({{ i18n.ts._role.conditional }})</span> + </template> </div> <div :class="$style.description">{{ role.description }}</div> </MkA> @@ -23,10 +25,13 @@ import { } from 'vue'; import { i18n } from '@/i18n'; -const props = defineProps<{ +const props = withDefaults(defineProps<{ role: any; forModeration: boolean; -}>(); + detailed: boolean; +}>(), { + detailed: true, +}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkSample.vue b/packages/frontend/src/components/MkSample.vue deleted file mode 100644 index 922b862b4..000000000 --- a/packages/frontend/src/components/MkSample.vue +++ /dev/null @@ -1,118 +0,0 @@ -<template> -<div class=""> - <div class=""> - <MkInput v-model="text"> - <template #label>Text</template> - </MkInput> - <MkSwitch v-model="flag"> - <span>Switch is now {{ flag ? 'on' : 'off' }}</span> - </MkSwitch> - <div style="margin: 32px 0;"> - <MkRadio v-model="radio" value="misskey">Misskey</MkRadio> - <MkRadio v-model="radio" value="mastodon">Mastodon</MkRadio> - <MkRadio v-model="radio" value="pleroma">Pleroma</MkRadio> - </div> - <MkButton inline>This is</MkButton> - <MkButton inline primary>the button</MkButton> - </div> - <div class="" style="pointer-events: none;"> - <Mfm :text="mfm"/> - </div> - <div class=""> - <MkButton inline primary @click="openMenu">Open menu</MkButton> - <MkButton inline primary @click="openDialog">Open dialog</MkButton> - <MkButton inline primary @click="openForm">Open form</MkButton> - <MkButton inline primary @click="openDrive">Open drive</MkButton> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@/components/MkButton.vue'; -import MkInput from '@/components/MkInput.vue'; -import MkSwitch from '@/components/MkSwitch.vue'; -import MkTextarea from '@/components/MkTextarea.vue'; -import MkRadio from '@/components/MkRadio.vue'; -import * as os from '@/os'; -import * as config from '@/config'; -import { $i } from '@/account'; - -export default defineComponent({ - components: { - MkButton, - MkInput, - MkSwitch, - MkTextarea, - MkRadio, - }, - - data() { - return { - text: '', - flag: true, - radio: 'misskey', - $i, - mfm: `Hello world! This is an @example mention. BTW you are @${this.$i ? this.$i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`, - }; - }, - - methods: { - async openDialog() { - os.alert({ - type: 'warning', - title: 'Oh my Aichan', - text: 'Lorem ipsum dolor sit amet, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', - }); - }, - - async openForm() { - os.form('Example form', { - foo: { - type: 'boolean', - default: true, - label: 'This is a boolean property', - }, - bar: { - type: 'number', - default: 300, - label: 'This is a number property', - }, - baz: { - type: 'string', - default: 'Misskey makes you happy.', - label: 'This is a string property', - }, - }); - }, - - async openDrive() { - os.selectDriveFile(false); - }, - - async selectUser() { - os.selectUser(); - }, - - async openMenu(ev) { - os.popupMenu([{ - type: 'label', - text: 'Fruits', - }, { - text: 'Create some apples', - action: () => {}, - }, { - text: 'Read some oranges', - action: () => {}, - }, { - text: 'Update some melons', - action: () => {}, - }, null, { - text: 'Delete some bananas', - danger: true, - action: () => {}, - }], ev.currentTarget ?? ev.target); - }, - }, -}); -</script> diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index ffc5e82b5..b1a509b9e 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -1,16 +1,16 @@ <template> -<form class="eppvobhk" :class="{ signing, totpLogin }" @submit.prevent="onSubmit"> - <div class="auth _gaps_m"> - <div v-show="withAvatar" class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div> +<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit"> + <div class="_gaps_m"> + <div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div> <MkInfo v-if="message"> {{ message }} </MkInfo> <div v-if="!totpLogin" class="normal-signin _gaps_m"> - <MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username" autofocus required data-cy-signin-username @update:model-value="onUsernameChange"> + <MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange"> <template #prefix>@</template> <template #suffix>@{{ host }}</template> </MkInput> - <MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password" :with-password-toggle="true" required data-cy-signin-password> + <MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password" :withPasswordToggle="true" required data-cy-signin-password> <template #prefix><i class="ti ti-lock"></i></template> <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> </MkInput> @@ -28,7 +28,7 @@ </div> <div class="twofa-group totp-group"> <p style="margin-bottom:0;">{{ i18n.ts.twoStepAuthentication }}</p> - <MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :with-password-toggle="true" required> + <MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required> <template #label>{{ i18n.ts.password }}</template> <template #prefix><i class="ti ti-lock"></i></template> </MkInput> @@ -236,18 +236,14 @@ function resetPassword() { } </script> -<style lang="scss" scoped> -.eppvobhk { - > .auth { - > .avatar { - margin: 0 auto 0 auto; - width: 64px; - height: 64px; - background: #ddd; - background-position: center; - background-size: cover; - border-radius: 100%; - } - } +<style lang="scss" module> +.avatar { + margin: 0 auto 0 auto; + width: 64px; + height: 64px; + background: #ddd; + background-position: center; + background-size: cover; + border-radius: 100%; } </style> diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue index 08e41d6ae..eb5876e58 100644 --- a/packages/frontend/src/components/MkSigninDialog.vue +++ b/packages/frontend/src/components/MkSigninDialog.vue @@ -8,8 +8,8 @@ > <template #header>{{ i18n.ts.login }}</template> - <MkSpacer :margin-min="20" :margin-max="28"> - <MkSignin :auto-set="autoSet" :message="message" @login="onLogin"/> + <MkSpacer :marginMin="20" :marginMax="28"> + <MkSignin :autoSet="autoSet" :message="message" @login="onLogin"/> </MkSpacer> </MkModalWindow> </template> diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 0e8bdb321..472269aba 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -3,13 +3,13 @@ <div :class="$style.banner"> <i class="ti ti-user-edit"></i> </div> - <MkSpacer :margin-min="20" :margin-max="32"> + <MkSpacer :marginMin="20" :marginMax="32"> <form class="_gaps_m" autocomplete="new-password" @submit.prevent="onSubmit"> <MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required> <template #label>{{ i18n.ts.invitationCode }}</template> <template #prefix><i class="ti ti-key"></i></template> </MkInput> - <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:model-value="onChangeUsername"> + <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" autocomplete="username" required data-cy-signup-username @update:modelValue="onChangeUsername"> <template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template> <template #prefix>@</template> <template #suffix>@{{ host }}</template> @@ -24,7 +24,7 @@ <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span> </template> </MkInput> - <MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:model-value="onChangeEmail"> + <MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail"> <template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template> <template #prefix><i class="ti ti-mail"></i></template> <template #caption> @@ -39,7 +39,7 @@ <span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> </template> </MkInput> - <MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:model-value="onChangePassword"> + <MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword"> <template #label>{{ i18n.ts.password }}</template> <template #prefix><i class="ti ti-lock"></i></template> <template #caption> @@ -48,7 +48,7 @@ <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span> </template> </MkInput> - <MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:model-value="onChangePasswordRetype"> + <MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype"> <template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template> <template #prefix><i class="ti ti-lock"></i></template> <template #caption> diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue index 6da81c3bc..b6ffba6cc 100644 --- a/packages/frontend/src/components/MkSignupDialog.rules.vue +++ b/packages/frontend/src/components/MkSignupDialog.rules.vue @@ -3,7 +3,7 @@ <div :class="$style.banner"> <i class="ti ti-checklist"></i> </div> - <MkSpacer :margin-min="20" :margin-max="28"> + <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps_m"> <div v-if="instance.disableRegistration"> <MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo> @@ -11,7 +11,7 @@ <div style="text-align: center;">{{ i18n.ts.pleaseConfirmBelowBeforeSignup }}</div> - <MkFolder v-if="availableServerRules" :default-open="true"> + <MkFolder v-if="availableServerRules" :defaultOpen="true"> <template #label>{{ i18n.ts.serverRules }}</template> <template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--success)"></i></template> @@ -22,7 +22,7 @@ <MkSwitch v-model="agreeServerRules" style="margin-top: 16px;">{{ i18n.ts.agree }}</MkSwitch> </MkFolder> - <MkFolder v-if="availableTos" :default-open="true"> + <MkFolder v-if="availableTos" :defaultOpen="true"> <template #label>{{ i18n.ts.termsOfService }}</template> <template #suffix><i v-if="agreeTos" class="ti ti-check" style="color: var(--success)"></i></template> @@ -31,7 +31,7 @@ <MkSwitch v-model="agreeTos" style="margin-top: 16px;">{{ i18n.ts.agree }}</MkSwitch> </MkFolder> - <MkFolder :default-open="true"> + <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template> <template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--success)"></i></template> diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue index 17f8b8642..d8d002fdb 100644 --- a/packages/frontend/src/components/MkSignupDialog.vue +++ b/packages/frontend/src/components/MkSignupDialog.vue @@ -11,16 +11,16 @@ <div style="overflow-x: clip;"> <Transition mode="out-in" - :enter-active-class="$style.transition_x_enterActive" - :leave-active-class="$style.transition_x_leaveActive" - :enter-from-class="$style.transition_x_enterFrom" - :leave-to-class="$style.transition_x_leaveTo" + :enterActiveClass="$style.transition_x_enterActive" + :leaveActiveClass="$style.transition_x_leaveActive" + :enterFromClass="$style.transition_x_enterFrom" + :leaveToClass="$style.transition_x_leaveTo" > <template v-if="!isAcceptedServerRule"> <XServerRules @done="isAcceptedServerRule = true" @cancel="dialog.close()"/> </template> <template v-else> - <XSignup :auto-set="autoSet" @signup="onSignup" @signup-email-pending="onSignupEmailPending"/> + <XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/> </template> </Transition> </div> diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index 1ac7107aa..3a050889c 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -1,15 +1,15 @@ <template> <div :class="[$style.root, { [$style.collapsed]: collapsed }]"> - <div :class="$style.body"> + <div> <span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span> <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :emoji-urls="note.emojis"/> + <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :emojiUrls="note.emojis"/> <MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> </div> <details v-if="note.files.length > 0"> <summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary> - <MkMediaList :media-list="note.files"/> + <MkMediaList :mediaList="note.files"/> </details> <details v-if="note.poll"> <summary>{{ i18n.ts.poll }}</summary> @@ -76,10 +76,6 @@ const collapsed = $ref( } } -.body { - -} - .reply { margin-right: 6px; color: var(--accent); diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index 2a8e43c57..72b70416d 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -23,22 +23,13 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; -export default defineComponent({ - props: { - def: { - type: Array, - required: true, - }, - grid: { - type: Boolean, - required: false, - default: false, - }, - }, -}); +defineProps<{ + def: any[]; + grid?: boolean; +}>(); </script> <style lang="scss" scoped> diff --git a/packages/frontend/src/components/MkTab.vue b/packages/frontend/src/components/MkTab.vue index 6f819bbbd..7274f9b31 100644 --- a/packages/frontend/src/components/MkTab.vue +++ b/packages/frontend/src/components/MkTab.vue @@ -7,17 +7,17 @@ export default defineComponent({ required: true, }, }, - render() { - const options = this.$slots.default(); + setup(props, { emit, slots }) { + const options = slots.default(); - return h('div', { + return () => h('div', { class: 'pxhvhrfw', }, options.map(option => withDirectives(h('button', { - class: ['_button', { active: this.modelValue === option.props.value }], + class: ['_button', { active: props.modelValue === option.props.value }], key: option.key, - disabled: this.modelValue === option.props.value, + disabled: props.modelValue === option.props.value, onClick: () => { - this.$emit('update:modelValue', option.props.value); + emit('update:modelValue', option.props.value); }, }, option.children), [ [resolveDirective('click-anime')], diff --git a/packages/frontend/src/components/MkTagCloud.vue b/packages/frontend/src/components/MkTagCloud.vue index 4e8d5bab7..6e4e054aa 100644 --- a/packages/frontend/src/components/MkTagCloud.vue +++ b/packages/frontend/src/components/MkTagCloud.vue @@ -1,7 +1,7 @@ <template> -<div ref="rootEl" class="meijqfqm"> - <canvas :id="idForCanvas" ref="canvasEl" class="canvas" :width="width" height="300" @contextmenu.prevent="() => {}"></canvas> - <div :id="idForTags" ref="tagsEl" class="tags"> +<div ref="rootEl" :class="$style.root"> + <canvas :id="idForCanvas" ref="canvasEl" style="display: block;" :width="width" height="300" @contextmenu.prevent="() => {}"></canvas> + <div :id="idForTags" ref="tagsEl" :class="$style.tags"> <ul> <slot></slot> </ul> @@ -70,21 +70,17 @@ defineExpose({ }); </script> -<style lang="scss" scoped> -.meijqfqm { +<style lang="scss" module> +.root { position: relative; overflow: clip; display: grid; place-items: center; +} - > .canvas { - display: block; - } - - > .tags { - position: absolute; - top: 999px; - left: 999px; - } +.tags { + position: absolute; + top: 999px; + left: 999px; } </style> diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue index 82b631edd..83b2ed244 100644 --- a/packages/frontend/src/components/MkTextarea.vue +++ b/packages/frontend/src/components/MkTextarea.vue @@ -1,12 +1,12 @@ <template> -<div class="adhpbeos"> - <div class="label" @click="focus"><slot name="label"></slot></div> - <div class="input" :class="{ disabled, focused, tall, pre }"> +<div> + <div :class="$style.label" @click="focus"><slot name="label"></slot></div> + <div :class="{ [$style.disabled]: disabled, [$style.focused]: focused, [$style.tall]: tall, [$style.pre]: pre }" style="position: relative;"> <textarea ref="inputEl" v-model="v" v-adaptive-border - :class="{ code, _monospace: code }" + :class="[$style.textarea, { _monospace: code }]" :disabled="disabled" :required="required" :readonly="readonly" @@ -20,243 +20,173 @@ @input="onInput" ></textarea> </div> - <div class="caption"><slot name="caption"></slot></div> + <div :class="$style.caption"><slot name="caption"></slot></div> - <MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> + <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> </template> -<script lang="ts"> -import { defineComponent, onMounted, nextTick, ref, watch, computed, toRefs } from 'vue'; +<script lang="ts" setup> +import { onMounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue'; import { debounce } from 'throttle-debounce'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, - }, +const props = defineProps<{ + modelValue: string | null; + required?: boolean; + readonly?: boolean; + disabled?: boolean; + pattern?: string; + placeholder?: string; + autofocus?: boolean; + autocomplete?: string; + spellcheck?: boolean; + debounce?: boolean; + manualSave?: boolean; + code?: boolean; + tall?: boolean; + pre?: boolean; +}>(); - props: { - modelValue: { - required: true, - }, - type: { - type: String, - required: false, - }, - required: { - type: Boolean, - required: false, - }, - readonly: { - type: Boolean, - required: false, - }, - disabled: { - type: Boolean, - required: false, - }, - pattern: { - type: String, - required: false, - }, - placeholder: { - type: String, - required: false, - }, - autofocus: { - type: Boolean, - required: false, - default: false, - }, - autocomplete: { - required: false, - }, - spellcheck: { - required: false, - }, - code: { - type: Boolean, - required: false, - }, - tall: { - type: Boolean, - required: false, - default: false, - }, - pre: { - type: Boolean, - required: false, - default: false, - }, - debounce: { - type: Boolean, - required: false, - default: false, - }, - manualSave: { - type: Boolean, - required: false, - default: false, - }, - }, +const emit = defineEmits<{ + (ev: 'change', _ev: KeyboardEvent): void; + (ev: 'keydown', _ev: KeyboardEvent): void; + (ev: 'enter'): void; + (ev: 'update:modelValue', value: string): void; +}>(); - emits: ['change', 'keydown', 'enter', 'update:modelValue'], +const { modelValue, autofocus } = toRefs(props); +const v = ref<string>(modelValue.value ?? ''); +const focused = ref(false); +const changed = ref(false); +const invalid = ref(false); +const filled = computed(() => v.value !== '' && v.value != null); +const inputEl = shallowRef<HTMLTextAreaElement>(); - setup(props, context) { - const { modelValue, autofocus } = toRefs(props); - const v = ref(modelValue.value); - const focused = ref(false); - const changed = ref(false); - const invalid = ref(false); - const filled = computed(() => v.value !== '' && v.value != null); - const inputEl = ref(null); +const focus = () => inputEl.value.focus(); +const onInput = (ev) => { + changed.value = true; + emit('change', ev); +}; +const onKeydown = (ev: KeyboardEvent) => { + if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return; - const focus = () => inputEl.value.focus(); - const onInput = (ev) => { - changed.value = true; - context.emit('change', ev); - }; - const onKeydown = (ev: KeyboardEvent) => { - if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return; + emit('keydown', ev); - context.emit('keydown', ev); + if (ev.code === 'Enter') { + emit('enter'); + } +}; - if (ev.code === 'Enter') { - context.emit('enter'); - } - }; +const updated = () => { + changed.value = false; + emit('update:modelValue', v.value ?? ''); +}; - const updated = () => { - changed.value = false; - context.emit('update:modelValue', v.value); - }; +const debouncedUpdated = debounce(1000, updated); - const debouncedUpdated = debounce(1000, updated); +watch(modelValue, newValue => { + v.value = newValue; +}); - watch(modelValue, newValue => { - v.value = newValue; - }); +watch(v, newValue => { + if (!props.manualSave) { + if (props.debounce) { + debouncedUpdated(); + } else { + updated(); + } + } - watch(v, newValue => { - if (!props.manualSave) { - if (props.debounce) { - debouncedUpdated(); - } else { - updated(); - } - } + invalid.value = inputEl.value.validity.badInput; +}); - invalid.value = inputEl.value.validity.badInput; - }); - - onMounted(() => { - nextTick(() => { - if (autofocus.value) { - focus(); - } - }); - }); - - return { - v, - focused, - invalid, - changed, - filled, - inputEl, - focus, - onInput, - onKeydown, - updated, - i18n, - }; - }, +onMounted(() => { + nextTick(() => { + if (autofocus.value) { + focus(); + } + }); }); </script> -<style lang="scss" scoped> -.adhpbeos { - > .label { - font-size: 0.85em; - padding: 0 0 8px 0; - user-select: none; +<style lang="scss" module> +.label { + font-size: 0.85em; + padding: 0 0 8px 0; + user-select: none; - &:empty { - display: none; - } - } - - > .caption { - font-size: 0.85em; - padding: 8px 0 0 0; - color: var(--fgTransparentWeak); - - &:empty { - display: none; - } - } - - > .input { - position: relative; - - > textarea { - appearance: none; - -webkit-appearance: none; - display: block; - width: 100%; - min-width: 100%; - max-width: 100%; - min-height: 130px; - margin: 0; - padding: 12px; - font: inherit; - font-weight: normal; - font-size: 1em; - color: var(--fg); - background: var(--panel); - border: solid 1px var(--panel); - border-radius: 6px; - outline: none; - box-shadow: none; - box-sizing: border-box; - transition: border-color 0.1s ease-out; - - &:hover { - border-color: var(--inputBorderHover) !important; - } - } - - &.focused { - > textarea { - border-color: var(--accent) !important; - } - } - - &.disabled { - opacity: 0.7; - - &, * { - cursor: not-allowed !important; - } - } - - &.tall { - > textarea { - min-height: 200px; - } - } - - &.pre { - > textarea { - white-space: pre; - } - } - } - - > .save { - margin: 8px 0 0 0; + &:empty { + display: none; } } + +.caption { + font-size: 0.85em; + padding: 8px 0 0 0; + color: var(--fgTransparentWeak); + + &:empty { + display: none; + } +} + +.textarea { + appearance: none; + -webkit-appearance: none; + display: block; + width: 100%; + min-width: 100%; + max-width: 100%; + min-height: 130px; + margin: 0; + padding: 12px; + font: inherit; + font-weight: normal; + font-size: 1em; + color: var(--fg); + background: var(--panel); + border: solid 1px var(--panel); + border-radius: 6px; + outline: none; + box-shadow: none; + box-sizing: border-box; + transition: border-color 0.1s ease-out; + + &:hover { + border-color: var(--inputBorderHover) !important; + } +} + +.focused { + > .textarea { + border-color: var(--accent) !important; + } +} + +.disabled { + opacity: 0.7; + cursor: not-allowed !important; + + > .textarea { + cursor: not-allowed !important; + } +} + +.tall { + > .textarea { + min-height: 200px; + } +} + +.pre { + > .textarea { + white-space: pre; + } +} + +.save { + margin: 8px 0 0 0; +} </style> diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index fb0a3a4b6..2595ebc45 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -1,11 +1,11 @@ <template> -<MkNotes ref="tlComponent" :no-gap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)"/> +<MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)"/> </template> <script lang="ts" setup> import { computed, provide, onUnmounted } from 'vue'; import MkNotes from '@/components/MkNotes.vue'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import * as sound from '@/scripts/sound'; import { $i } from '@/account'; import { defaultStore } from '@/store'; @@ -46,17 +46,13 @@ const onUserRemoved = () => { tlComponent.pagingComponent?.reload(); }; -const onChangeFollowing = () => { - if (!tlComponent.pagingComponent?.backed) { - tlComponent.pagingComponent?.reload(); - } -}; - let endpoint; let query; let connection; let connection2; +const stream = useStream(); + if (props.src === 'antenna') { endpoint = 'antennas/notes'; query = { @@ -68,23 +64,41 @@ if (props.src === 'antenna') { connection.on('note', prepend); } else if (props.src === 'home') { endpoint = 'notes/timeline'; - connection = stream.useChannel('homeTimeline'); + query = { + withReplies: defaultStore.state.showTimelineReplies, + }; + connection = stream.useChannel('homeTimeline', { + withReplies: defaultStore.state.showTimelineReplies, + }); connection.on('note', prepend); connection2 = stream.useChannel('main'); - connection2.on('follow', onChangeFollowing); - connection2.on('unfollow', onChangeFollowing); } else if (props.src === 'local') { endpoint = 'notes/local-timeline'; - connection = stream.useChannel('localTimeline'); + query = { + withReplies: defaultStore.state.showTimelineReplies, + }; + connection = stream.useChannel('localTimeline', { + withReplies: defaultStore.state.showTimelineReplies, + }); connection.on('note', prepend); } else if (props.src === 'social') { endpoint = 'notes/hybrid-timeline'; - connection = stream.useChannel('hybridTimeline'); + query = { + withReplies: defaultStore.state.showTimelineReplies, + }; + connection = stream.useChannel('hybridTimeline', { + withReplies: defaultStore.state.showTimelineReplies, + }); connection.on('note', prepend); } else if (props.src === 'global') { endpoint = 'notes/global-timeline'; - connection = stream.useChannel('globalTimeline'); + query = { + withReplies: defaultStore.state.showTimelineReplies, + }; + connection = stream.useChannel('globalTimeline', { + withReplies: defaultStore.state.showTimelineReplies, + }); connection.on('note', prepend); } else if (props.src === 'mentions') { endpoint = 'notes/mentions'; diff --git a/packages/frontend/src/components/MkToast.vue b/packages/frontend/src/components/MkToast.vue index ad53c7f28..e135f5647 100644 --- a/packages/frontend/src/components/MkToast.vue +++ b/packages/frontend/src/components/MkToast.vue @@ -1,11 +1,11 @@ <template> <div> <Transition - :enter-active-class="defaultStore.state.animation ? $style.transition_toast_enterActive : ''" - :leave-active-class="defaultStore.state.animation ? $style.transition_toast_leaveActive : ''" - :enter-from-class="defaultStore.state.animation ? $style.transition_toast_enterFrom : ''" - :leave-to-class="defaultStore.state.animation ? $style.transition_toast_leaveTo : ''" - appear @after-leave="emit('closed')" + :enterActiveClass="defaultStore.state.animation ? $style.transition_toast_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_toast_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_toast_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_toast_leaveTo : ''" + appear @afterLeave="emit('closed')" > <div v-if="showing" class="_acrylic" :class="$style.root" :style="{ zIndex }"> <div style="padding: 16px 24px;"> diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue index 56be04440..3ddd81aae 100644 --- a/packages/frontend/src/components/MkTokenGenerateWindow.vue +++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue @@ -3,16 +3,16 @@ ref="dialog" :width="400" :height="450" - :with-ok-button="true" - :ok-button-disabled="false" - :can-close="false" + :withOkButton="true" + :okButtonDisabled="false" + :canClose="false" @close="dialog.close()" @closed="$emit('closed')" @ok="ok()" > <template #header>{{ title || i18n.ts.generateAccessToken }}</template> - <MkSpacer :margin-min="20" :margin-max="28"> + <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps_m"> <div v-if="information"> <MkInfo warn>{{ information }}</MkInfo> diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue index 2d34b090e..91c9b70a5 100644 --- a/packages/frontend/src/components/MkTooltip.vue +++ b/packages/frontend/src/components/MkTooltip.vue @@ -1,10 +1,10 @@ <template> <Transition - :enter-active-class="defaultStore.state.animation ? $style.transition_tooltip_enterActive : ''" - :leave-active-class="defaultStore.state.animation ? $style.transition_tooltip_leaveActive : ''" - :enter-from-class="defaultStore.state.animation ? $style.transition_tooltip_enterFrom : ''" - :leave-to-class="defaultStore.state.animation ? $style.transition_tooltip_leaveTo : ''" - appear @after-leave="emit('closed')" + :enterActiveClass="defaultStore.state.animation ? $style.transition_tooltip_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_tooltip_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_tooltip_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_tooltip_leaveTo : ''" + appear @afterLeave="emit('closed')" > <div v-show="showing" ref="el" :class="$style.root" class="_acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }"> <slot> @@ -41,6 +41,9 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); +// タイミングによっては最初から showing = false な場合があり、その場合に closed 扱いにしないと永久にDOMに残ることになる +if (!props.showing) emit('closed'); + const el = shallowRef<HTMLElement>(); const zIndex = os.claimZIndex('high'); @@ -66,10 +69,8 @@ onMounted(() => { setPosition(); const loop = () => { - loopHandler = window.requestAnimationFrame(() => { - setPosition(); - loop(); - }); + setPosition(); + loopHandler = window.requestAnimationFrame(loop); }; loop(); diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue index eed7fa71f..3a0b2abb4 100644 --- a/packages/frontend/src/components/MkUpdated.vue +++ b/packages/frontend/src/components/MkUpdated.vue @@ -1,5 +1,5 @@ <template> -<MkModal ref="modal" :z-priority="'middle'" @click="$refs.modal.close()" @closed="$emit('closed')"> +<MkModal ref="modal" :zPriority="'middle'" @click="$refs.modal.close()" @closed="$emit('closed')"> <div :class="$style.root"> <div :class="$style.title"><MkSparkle>{{ i18n.ts.misskeyUpdated }}</MkSparkle></div> <div :class="$style.version">✨{{ version }}🚀</div> diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 9c5622b1c..fcad5b806 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -22,7 +22,7 @@ </div> </template> <template v-else-if="tweetId && tweetExpanded"> - <div ref="twitter" :class="$style.twitter"> + <div ref="twitter"> <iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe> </div> <div :class="$style.action"> @@ -31,7 +31,7 @@ </MkButton> </div> </template> -<div v-else :class="$style.urlPreview"> +<div v-else> <component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url"> <div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`"> </div> @@ -41,14 +41,14 @@ <h1 v-else-if="fetching" :class="$style.title"><MkEllipsis/></h1> <h1 v-else :class="$style.title" :title="title ?? undefined">{{ title }}</h1> </header> - <p v-if="unknownUrl" :class="$style.text">{{ i18n.ts.cannotLoad }}</p> + <p v-if="unknownUrl" :class="$style.text">{{ i18n.ts.failedToPreviewUrl }}</p> <p v-else-if="fetching" :class="$style.text"><MkEllipsis/></p> <p v-else-if="description" :class="$style.text" :title="description">{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p> <footer :class="$style.footer"> <img v-if="icon" :class="$style.siteIcon" :src="icon"/> - <p v-if="unknownUrl" :class="$style.siteName">?</p> + <p v-if="unknownUrl" :class="$style.siteName">{{ requestUrl.host }}</p> <p v-else-if="fetching" :class="$style.siteName"><MkEllipsis/></p> - <p v-else :class="$style.siteName" :title="sitename ?? undefined">{{ sitename }}</p> + <p v-else :class="$style.siteName" :title="sitename ?? requestUrl.host">{{ sitename ?? requestUrl.host }}</p> </footer> </article> </component> @@ -128,17 +128,33 @@ if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/ requestUrl.hash = ''; -window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`).then(res => { - res.json().then((info: SummalyResult) => { +window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`) + .then(res => { + if (!res.ok) { + fetching = false; + unknownUrl = true; + return; + } + + return res.json(); + }) + .then((info: SummalyResult) => { + if (info.url == null) { + fetching = false; + unknownUrl = true; + return; + } + + fetching = false; + unknownUrl = false; + title = info.title; description = info.description; thumbnail = info.thumbnail; icon = info.icon; sitename = info.sitename; - fetching = false; player = info.player; }); -}); function adjustTweetHeight(message: any) { if (message.origin !== 'https://platform.twitter.com') return; @@ -194,13 +210,6 @@ onUnmounted(() => { width: 100%; } -.twitter { - -} - -.urlPreview { -} - .link { position: relative; display: block; diff --git a/packages/frontend/src/components/MkUrlPreviewPopup.vue b/packages/frontend/src/components/MkUrlPreviewPopup.vue index e244be3e9..36a9e2f73 100644 --- a/packages/frontend/src/components/MkUrlPreviewPopup.vue +++ b/packages/frontend/src/components/MkUrlPreviewPopup.vue @@ -1,6 +1,6 @@ <template> -<div class="fgmtyycl" :style="{ zIndex, top: top + 'px', left: left + 'px' }"> - <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" @after-leave="emit('closed')"> +<div :class="$style.root" :style="{ zIndex, top: top + 'px', left: left + 'px' }"> + <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" @afterLeave="emit('closed')"> <MkUrlPreview v-if="showing" class="_popup _shadow" :url="url"/> </Transition> </div> @@ -36,8 +36,8 @@ onMounted(() => { }); </script> -<style lang="scss" scoped> -.fgmtyycl { +<style lang="scss" module> +.root { position: absolute; width: 500px; max-width: calc(90vw - 12px); diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue index f560ebcd8..172b51751 100644 --- a/packages/frontend/src/components/MkUserInfo.vue +++ b/packages/frontend/src/components/MkUserInfo.vue @@ -8,7 +8,7 @@ </div> <span v-if="$i && $i.id !== user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span> <div :class="$style.description"> - <div v-if="user.description" class="mfm"> + <div v-if="user.description" :class="$style.mfm"> <Mfm :text="user.description" :author="user" :i="$i"/> </div> <span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span> @@ -105,7 +105,7 @@ defineProps<{ .mfm { display: -webkit-box; -webkit-line-clamp: 3; - -webkit-box-orient: vertical; + -webkit-box-orient: vertical; overflow: hidden; } diff --git a/packages/frontend/src/components/MkUserOnlineIndicator.vue b/packages/frontend/src/components/MkUserOnlineIndicator.vue index 251ab5d79..a2c2b53b0 100644 --- a/packages/frontend/src/components/MkUserOnlineIndicator.vue +++ b/packages/frontend/src/components/MkUserOnlineIndicator.vue @@ -1,5 +1,13 @@ <template> -<div v-tooltip="text" :class="[$style.root, $style['status_' + user.onlineStatus]]"></div> +<div + v-tooltip="text" + :class="[$style.root, { + [$style.status_online]: user.onlineStatus === 'online', + [$style.status_active]: user.onlineStatus === 'active', + [$style.status_offline]: user.onlineStatus === 'offline', + [$style.status_unknown]: user.onlineStatus === 'unknown', + }]" +></div> </template> <script lang="ts" setup> diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index 8ca035544..c3b777a12 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -1,10 +1,10 @@ <template> <Transition - :enter-active-class="defaultStore.state.animation ? $style.transition_popup_enterActive : ''" - :leave-active-class="defaultStore.state.animation ? $style.transition_popup_leaveActive : ''" - :enter-from-class="defaultStore.state.animation ? $style.transition_popup_enterFrom : ''" - :leave-to-class="defaultStore.state.animation ? $style.transition_popup_leaveTo : ''" - appear @after-leave="emit('closed')" + :enterActiveClass="defaultStore.state.animation ? $style.transition_popup_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_popup_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_popup_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_popup_leaveTo : ''" + appear @afterLeave="emit('closed')" > <div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }"> <div v-if="user != null"> @@ -22,7 +22,7 @@ <div :class="$style.username"><MkAcct :user="user"/></div> </div> <div :class="$style.description"> - <Mfm v-if="user.description" :text="user.description" :author="user" :i="$i"/> + <Mfm v-if="user.description" :class="$style.mfm" :text="user.description" :author="user" :i="$i"/> <div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div> </div> <div :class="$style.status"> @@ -192,6 +192,13 @@ onMounted(() => { border-bottom: solid 1px var(--divider); } +.mfm { + display: -webkit-box; + -webkit-line-clamp: 5; + -webkit-box-orient: vertical; + overflow: hidden; +} + .status { padding: 16px 26px 16px 26px; } diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue index dc78bbf42..792ff7afd 100644 --- a/packages/frontend/src/components/MkUserSelectDialog.vue +++ b/packages/frontend/src/components/MkUserSelectDialog.vue @@ -1,22 +1,22 @@ <template> <MkModalWindow ref="dialogEl" - :with-ok-button="true" - :ok-button-disabled="selected == null" + :withOkButton="true" + :okButtonDisabled="selected == null" @click="cancel()" @close="cancel()" @ok="ok()" @closed="$emit('closed')" > <template #header>{{ i18n.ts.selectUser }}</template> - <div :class="$style.root"> + <div> <div :class="$style.form"> - <FormSplit :min-width="170"> - <MkInput v-model="username" :autofocus="true" @update:model-value="search"> + <FormSplit :minWidth="170"> + <MkInput v-model="username" :autofocus="true" @update:modelValue="search"> <template #label>{{ i18n.ts.username }}</template> <template #prefix>@</template> </MkInput> - <MkInput v-model="host" :datalist="[hostname]" @update:model-value="search"> + <MkInput v-model="host" :datalist="[hostname]" @update:modelValue="search"> <template #label>{{ i18n.ts.host }}</template> <template #prefix>@</template> </MkInput> @@ -126,8 +126,6 @@ onMounted(() => { </script> <style lang="scss" module> -.root { -} .form { padding: 0 var(--root-margin); diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue index a2a195cb0..789f88a8f 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue @@ -2,7 +2,7 @@ <div class="_gaps"> <div style="text-align: center;">{{ i18n.ts._initialAccountSetting.followUsers }}</div> - <MkFolder :default-open="true"> + <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts.recommended }}</template> <MkPagination :pagination="pinnedUsers"> @@ -14,7 +14,7 @@ </MkPagination> </MkFolder> - <MkFolder :default-open="true"> + <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts.popularUsers }}</template> <MkPagination :pagination="popularUsers"> diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue index e9f4f68df..5cea67ccf 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue @@ -4,6 +4,7 @@ <MkFolder> <template #label>{{ i18n.ts.makeFollowManuallyApprove }}</template> + <template #icon><i class="ti ti-lock"></i></template> <template #suffix>{{ isLocked ? i18n.ts.on : i18n.ts.off }}</template> <MkSwitch v-model="isLocked">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch> @@ -11,6 +12,7 @@ <MkFolder> <template #label>{{ i18n.ts.hideOnlineStatus }}</template> + <template #icon><i class="ti ti-eye-off"></i></template> <template #suffix>{{ hideOnlineStatus ? i18n.ts.on : i18n.ts.off }}</template> <MkSwitch v-model="hideOnlineStatus">{{ i18n.ts.hideOnlineStatus }}<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template></MkSwitch> @@ -18,6 +20,7 @@ <MkFolder> <template #label>{{ i18n.ts.noCrawle }}</template> + <template #icon><i class="ti ti-world-x"></i></template> <template #suffix>{{ noCrawle ? i18n.ts.on : i18n.ts.off }}</template> <MkSwitch v-model="noCrawle">{{ i18n.ts.noCrawle }}<template #caption>{{ i18n.ts.noCrawleDescription }}</template></MkSwitch> @@ -25,6 +28,7 @@ <MkFolder> <template #label>{{ i18n.ts.preventAiLearning }}</template> + <template #icon><i class="ti ti-photo-shield"></i></template> <template #suffix>{{ preventAiLearning ? i18n.ts.on : i18n.ts.off }}</template> <MkSwitch v-model="preventAiLearning">{{ i18n.ts.preventAiLearning }}<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template></MkSwitch> diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue index f26ea1121..3107209b9 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue @@ -12,11 +12,11 @@ </div> </FormSlot> - <MkInput v-model="name" :max="30" manual-save data-cy-user-setup-user-name> + <MkInput v-model="name" :max="30" manualSave data-cy-user-setup-user-name> <template #label>{{ i18n.ts._profile.name }}</template> </MkInput> - <MkTextarea v-model="description" :max="500" tall manual-save data-cy-user-setup-user-description> + <MkTextarea v-model="description" :max="500" tall manualSave data-cy-user-setup-user-description> <template #label>{{ i18n.ts._profile.description }}</template> </MkTextarea> @@ -37,8 +37,8 @@ import { chooseFileFromPc } from '@/scripts/select-file'; import * as os from '@/os'; import { $i } from '@/account'; -const name = ref(''); -const description = ref(''); +const name = ref($i.name ?? ''); +const description = ref($i.description ?? ''); watch(name, () => { os.apiWithDialog('i/update', { diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue index 4e80a5c0f..566441213 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.vue @@ -7,10 +7,10 @@ @close="close(true)" @closed="emit('closed')" > - <template v-if="page === 1" #header>{{ i18n.ts._initialAccountSetting.profileSetting }}</template> - <template v-else-if="page === 2" #header>{{ i18n.ts._initialAccountSetting.privacySetting }}</template> - <template v-else-if="page === 3" #header>{{ i18n.ts.follow }}</template> - <template v-else-if="page === 4" #header>{{ i18n.ts.pushNotification }}</template> + <template v-if="page === 1" #header><i class="ti ti-user-edit"></i> {{ i18n.ts._initialAccountSetting.profileSetting }}</template> + <template v-else-if="page === 2" #header><i class="ti ti-lock"></i> {{ i18n.ts._initialAccountSetting.privacySetting }}</template> + <template v-else-if="page === 3" #header><i class="ti ti-user-plus"></i> {{ i18n.ts.follow }}</template> + <template v-else-if="page === 4" #header><i class="ti ti-bell-plus"></i> {{ i18n.ts.pushNotification }}</template> <template v-else-if="page === 5" #header>{{ i18n.ts.done }}</template> <template v-else #header>{{ i18n.ts.initialAccountSetting }}</template> @@ -20,65 +20,80 @@ </div> <Transition mode="out-in" - :enter-active-class="$style.transition_x_enterActive" - :leave-active-class="$style.transition_x_leaveActive" - :enter-from-class="$style.transition_x_enterFrom" - :leave-to-class="$style.transition_x_leaveTo" + :enterActiveClass="$style.transition_x_enterActive" + :leaveActiveClass="$style.transition_x_leaveActive" + :enterFromClass="$style.transition_x_enterFrom" + :leaveToClass="$style.transition_x_leaveTo" > <template v-if="page === 0"> <div :class="$style.centerPage"> - <MkSpacer :margin-min="20" :margin-max="28"> + <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/> + <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> <div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.accountCreated }}</div> <div>{{ i18n.ts._initialAccountSetting.letsStartAccountSetup }}</div> <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts._initialAccountSetting.profileSetting }} <i class="ti ti-arrow-right"></i></MkButton> + <MkButton style="margin: 0 auto;" transparent rounded @click="later(true)">{{ i18n.ts.later }}</MkButton> </div> </MkSpacer> </div> </template> <template v-else-if="page === 1"> <div style="height: 100cqh; overflow: auto;"> - <MkSpacer :margin-min="20" :margin-max="28"> + <MkSpacer :marginMin="20" :marginMax="28"> <XProfile/> - <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + <div class="_buttonsCenter" style="margin-top: 16px;"> + <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> </MkSpacer> </div> </template> <template v-else-if="page === 2"> <div style="height: 100cqh; overflow: auto;"> - <MkSpacer :margin-min="20" :margin-max="28"> + <MkSpacer :marginMin="20" :marginMax="28"> <XPrivacy/> - <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + <div class="_buttonsCenter" style="margin-top: 16px;"> + <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> </MkSpacer> </div> </template> <template v-else-if="page === 3"> <div style="height: 100cqh; overflow: auto;"> - <MkSpacer :margin-min="20" :margin-max="28"> + <MkSpacer :marginMin="20" :marginMax="28"> <XFollow/> </MkSpacer> <div :class="$style.pageFooter"> - <MkButton primary rounded gradate style="margin: 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + <div class="_buttonsCenter"> + <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate style="" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> </div> </div> </template> <template v-else-if="page === 4"> <div :class="$style.centerPage"> - <MkSpacer :margin-min="20" :margin-max="28"> + <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> <i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> <div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div> <div style="padding: 0 16px;">{{ i18n.t('_initialAccountSetting.pushNotificationDescription', { name: instance.name ?? host }) }}</div> - <MkPushNotificationAllowButton primary show-only-to-register style="margin: 0 auto;"/> - <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + <MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/> + <div class="_buttonsCenter" style="margin-top: 16px;"> + <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> </div> </MkSpacer> </div> </template> <template v-else-if="page === 5"> <div :class="$style.centerPage"> - <MkSpacer :margin-min="20" :margin-max="28"> + <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/> + <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> <div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div> @@ -89,7 +104,10 @@ </template> </I18n> <div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div> - <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="close(false)">{{ i18n.ts.close }}</MkButton> + <div class="_buttonsCenter" style="margin-top: 16px;"> + <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate data-cy-user-setup-continue @click="close(false)">{{ i18n.ts.close }}</MkButton> + </div> </div> </MkSpacer> </div> @@ -106,6 +124,7 @@ import MkButton from '@/components/MkButton.vue'; import XProfile from '@/components/MkUserSetupDialog.Profile.vue'; import XFollow from '@/components/MkUserSetupDialog.Follow.vue'; import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue'; +import MkAnimBg from '@/components/MkAnimBg.vue'; import { i18n } from '@/i18n'; import { instance } from '@/instance'; import { host } from '@/config'; @@ -137,6 +156,19 @@ async function close(skip: boolean) { dialog.value.close(); defaultStore.set('accountSetupWizard', -1); } + +async function later(later: boolean) { + if (later) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts._initialAccountSetting.laterAreYouSure, + }); + if (canceled) return; + } + + dialog.value.close(); + defaultStore.set('accountSetupWizard', 0); +} </script> <style lang="scss" module> @@ -183,7 +215,7 @@ async function close(skip: boolean) { left: 0; padding: 12px; border-top: solid 0.5px var(--divider); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + -webkit-backdrop-filter: blur(15px); + backdrop-filter: blur(15px); } </style> diff --git a/packages/frontend/src/components/MkUsersTooltip.vue b/packages/frontend/src/components/MkUsersTooltip.vue index d0f95fced..0b80c2edc 100644 --- a/packages/frontend/src/components/MkUsersTooltip.vue +++ b/packages/frontend/src/components/MkUsersTooltip.vue @@ -1,11 +1,11 @@ <template> -<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="250" @closed="emit('closed')"> +<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="250" @closed="emit('closed')"> <div :class="$style.root"> <div v-for="u in users" :key="u.id" :class="$style.user"> <MkAvatar :class="$style.avatar" :user="u"/> - <MkUserName :class="$style.name" :user="u" :nowrap="true"/> + <MkUserName :user="u" :nowrap="true"/> </div> - <div v-if="users.length < count" :class="$style.omitted">+{{ count - users.length }}</div> + <div v-if="users.length < count">+{{ count - users.length }}</div> </div> </MkTooltip> </template> @@ -43,14 +43,6 @@ const emit = defineEmits<{ } } -.name { - -} - -.omitted { - -} - .avatar { width: 24px; height: 24px; diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue index c181d84bc..c8dbe9094 100644 --- a/packages/frontend/src/components/MkVisibilityPicker.vue +++ b/packages/frontend/src/components/MkVisibilityPicker.vue @@ -1,5 +1,5 @@ <template> -<MkModal ref="modal" v-slot="{ type }" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')"> +<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')"> <div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }"> <div :class="[$style.label, $style.item]"> {{ i18n.ts.visibility }} diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue index 622676812..9566cc651 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -39,7 +39,7 @@ <MkTimeline src="local"/> </div> </div> - <div :class="[$style.activeUsersChart, $style.panel]"> + <div :class="$style.panel"> <XActiveUsersChart/> </div> </div> @@ -220,8 +220,4 @@ function exploreOtherServers() { height: 350px; overflow: auto; } - -.activeUsersChart { - -} </style> diff --git a/packages/frontend/src/components/MkWaitingDialog.vue b/packages/frontend/src/components/MkWaitingDialog.vue index da98da29d..1b6ab1f13 100644 --- a/packages/frontend/src/components/MkWaitingDialog.vue +++ b/packages/frontend/src/components/MkWaitingDialog.vue @@ -1,5 +1,5 @@ <template> -<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')"> +<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')"> <div :class="[$style.root, { [$style.iconOnly]: (text == null) || success }]"> <i v-if="success" :class="[$style.icon, $style.success]" class="ti ti-check"></i> <MkLoading v-else :class="[$style.icon, $style.waiting]" :em="true"/> diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue index ad1c02a48..30547c744 100644 --- a/packages/frontend/src/components/MkWidgets.vue +++ b/packages/frontend/src/components/MkWidgets.vue @@ -1,7 +1,7 @@ <template> <div :class="$style.root"> <template v-if="edit"> - <header :class="$style['edit-header']"> + <header :class="$style.editHeader"> <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select> <template #label>{{ i18n.ts.selectWidget }}</template> <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option> @@ -10,26 +10,26 @@ <MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton> </header> <Sortable - :model-value="props.widgets" - item-key="id" + :modelValue="props.widgets" + itemKey="id" handle=".handle" :animation="150" :group="{ name: 'SortableMkWidgets' }" - :class="$style['edit-editing']" - @update:model-value="v => emit('updateWidgets', v)" + :class="$style.editEditing" + @update:modelValue="v => emit('updateWidgets', v)" > <template #item="{element}"> - <div :class="[$style.widget, $style['customize-container']]" data-cy-customize-container> - <button :class="$style['customize-container-config']" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button> - <button :class="$style['customize-container-remove']" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button> + <div :class="[$style.widget, $style.customizeContainer]" data-cy-customize-container> + <button :class="$style.customizeContainerConfig" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button> + <button :class="$style.customizeContainerRemove" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button> <div class="handle"> - <component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style['customize-container-handle-widget']" :widget="element" @update-props="updateWidget(element.id, $event)"/> + <component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style.customizeContainerHandleWidget" :widget="element" @updateProps="updateWidget(element.id, $event)"/> </div> </div> </template> </Sortable> </template> - <component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @update-props="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/> + <component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @updateProps="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/> </div> </template> @@ -130,7 +130,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { } .edit { - &-header { + &Header { margin: 16px 0; > * { @@ -139,17 +139,17 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { } } - &-editing { + &Editing { min-height: 100px; } } -.customize-container { +.customizeContainer { position: relative; cursor: move; - &-config, - &-remove { + &Config, + &Remove { position: absolute; z-index: 10000; top: 8px; @@ -160,17 +160,17 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { border-radius: 4px; } - &-config { + &Config { right: 8px + 8px + 32px; } - &-remove { + &Remove { right: 8px; } - &-handle { + &Handle { - &-widget { + &Widget { pointer-events: none; } } diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue index b662479b2..dafabf2ba 100644 --- a/packages/frontend/src/components/MkWindow.vue +++ b/packages/frontend/src/components/MkWindow.vue @@ -1,11 +1,11 @@ <template> <Transition - :enter-active-class="defaultStore.state.animation ? $style.transition_window_enterActive : ''" - :leave-active-class="defaultStore.state.animation ? $style.transition_window_leaveActive : ''" - :enter-from-class="defaultStore.state.animation ? $style.transition_window_enterFrom : ''" - :leave-to-class="defaultStore.state.animation ? $style.transition_window_leaveTo : ''" + :enterActiveClass="defaultStore.state.animation ? $style.transition_window_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_window_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''" appear - @after-leave="$emit('closed')" + @afterLeave="$emit('closed')" > <div v-if="showing" ref="rootEl" :class="[$style.root, { [$style.maximized]: maximized }]"> <div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown"> diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue index 4d765fe2f..0edfd98ef 100644 --- a/packages/frontend/src/components/MkYouTubePlayer.vue +++ b/packages/frontend/src/components/MkYouTubePlayer.vue @@ -1,5 +1,5 @@ <template> -<MkWindow :initial-width="640" :initial-height="402" :can-resize="true" :close-button="true"> +<MkWindow :initialWidth="640" :initialHeight="402" :canResize="true" :closeButton="true"> <template #header> <i class="icon ti ti-brand-youtube" style="margin-right: 0.5em;"></i> <span>{{ title ?? 'YouTube' }}</span> diff --git a/packages/frontend/src/components/form/link.vue b/packages/frontend/src/components/form/link.vue index a1775c0bd..22b5edc3c 100644 --- a/packages/frontend/src/components/form/link.vue +++ b/packages/frontend/src/components/form/link.vue @@ -1,19 +1,19 @@ <template> -<div class="ffcbddfc" :class="{ inline }"> - <a v-if="external" class="main _button" :href="to" target="_blank"> - <span class="icon"><slot name="icon"></slot></span> - <span class="text"><slot></slot></span> - <span class="right"> - <span class="text"><slot name="suffix"></slot></span> - <i class="ti ti-external-link icon"></i> +<div :class="[$style.root, { [$style.inline]: inline }]"> + <a v-if="external" :class="$style.main" class="_button" :href="to" target="_blank"> + <span :class="$style.icon"><slot name="icon"></slot></span> + <span :class="$style.text"><slot></slot></span> + <span :class="$style.suffix"> + <span :class="$style.suffixText"><slot name="suffix"></slot></span> + <i class="ti ti-external-link"></i> </span> </a> - <MkA v-else class="main _button" :class="{ active }" :to="to" :behavior="behavior"> - <span class="icon"><slot name="icon"></slot></span> - <span class="text"><slot></slot></span> - <span class="right"> - <span class="text"><slot name="suffix"></slot></span> - <i class="ti ti-chevron-right icon"></i> + <MkA v-else :class="[$style.main, { [$style.active]: active }]" class="_button" :to="to" :behavior="behavior"> + <span :class="$style.icon"><slot name="icon"></slot></span> + <span :class="$style.text"><slot></slot></span> + <span :class="$style.suffix"> + <span :class="$style.suffixText"><slot name="suffix"></slot></span> + <i class="ti ti-chevron-right"></i> </span> </MkA> </div> @@ -26,70 +26,70 @@ const props = defineProps<{ to: string; active?: boolean; external?: boolean; - behavior?: null | 'window' | 'browser' | 'modalWindow'; + behavior?: null | 'window' | 'browser'; inline?: boolean; }>(); </script> -<style lang="scss" scoped> -.ffcbddfc { +<style lang="scss" module> +.root { display: block; &.inline { display: inline-block; } +} - > .main { - display: flex; - align-items: center; - width: 100%; - box-sizing: border-box; - padding: 10px 14px; - background: var(--buttonBg); - border-radius: 6px; - font-size: 0.9em; +.main { + display: flex; + align-items: center; + width: 100%; + box-sizing: border-box; + padding: 10px 14px; + background: var(--buttonBg); + border-radius: 6px; + font-size: 0.9em; - &:hover { - text-decoration: none; - background: var(--buttonHoverBg); - } + &:hover { + text-decoration: none; + background: var(--buttonHoverBg); + } - &.active { - color: var(--accent); - background: var(--buttonHoverBg); - } + &.active { + color: var(--accent); + background: var(--buttonHoverBg); + } +} - > .icon { - margin-right: 0.75em; - flex-shrink: 0; - text-align: center; - color: var(--fgTransparentWeak); +.icon { + margin-right: 0.75em; + flex-shrink: 0; + text-align: center; + color: var(--fgTransparentWeak); - &:empty { - display: none; + &:empty { + display: none; - & + .text { - padding-left: 4px; - } - } - } - - > .text { - flex-shrink: 1; - white-space: normal; - padding-right: 12px; - text-align: center; - } - - > .right { - margin-left: auto; - opacity: 0.7; - white-space: nowrap; - - > .text:not(:empty) { - margin-right: 0.75em; - } + & + .text { + padding-left: 4px; } } } + +.text { + flex-shrink: 1; + white-space: normal; + padding-right: 12px; + text-align: center; +} + +.suffix { + margin-left: auto; + opacity: 0.7; + white-space: nowrap; + + > .suffixText:not(:empty) { + margin-right: 0.75em; + } +} </style> diff --git a/packages/frontend/src/components/form/slot.vue b/packages/frontend/src/components/form/slot.vue index 79ce8fe51..809d80620 100644 --- a/packages/frontend/src/components/form/slot.vue +++ b/packages/frontend/src/components/form/slot.vue @@ -1,10 +1,10 @@ <template> -<div class="adhpbeou"> - <div class="label" @click="focus"><slot name="label"></slot></div> - <div class="content"> +<div> + <div :class="$style.label" @click="focus"><slot name="label"></slot></div> + <div> <slot></slot> </div> - <div class="caption"><slot name="caption"></slot></div> + <div :class="$style.caption"><slot name="caption"></slot></div> </div> </template> @@ -16,26 +16,24 @@ function focus() { } </script> -<style lang="scss" scoped> -.adhpbeou { - > .label { - font-size: 0.85em; - padding: 0 0 8px 0; - user-select: none; +<style lang="scss" module> +.label { + font-size: 0.85em; + padding: 0 0 8px 0; + user-select: none; - &:empty { - display: none; - } + &:empty { + display: none; } +} - > .caption { - font-size: 0.85em; - padding: 8px 0 0 0; - color: var(--fgTransparentWeak); +.caption { + font-size: 0.85em; + padding: 8px 0 0 0; + color: var(--fgTransparentWeak); - &:empty { - display: none; - } + &:empty { + display: none; } } </style> diff --git a/packages/frontend/src/components/form/suspense.vue b/packages/frontend/src/components/form/suspense.vue index 3a44c3da3..b3d8c22b2 100644 --- a/packages/frontend/src/components/form/suspense.vue +++ b/packages/frontend/src/components/form/suspense.vue @@ -1,102 +1,66 @@ <template> -<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in"> - <div v-if="pending"> - <MkLoading/> +<div v-if="pending"> + <MkLoading/> +</div> +<div v-else-if="resolved"> + <slot :result="result"></slot> +</div> +<div v-else> + <div :class="$style.error"> + <div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</div> + <MkButton inline style="margin-top: 16px;" @click="retry"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton> </div> - <div v-else-if="resolved"> - <slot :result="result"></slot> - </div> - <div v-else> - <div class="wszdbhzo"> - <div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</div> - <MkButton inline class="retry" @click="retry"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton> - </div> - </div> -</Transition> +</div> </template> -<script lang="ts"> -import { defineComponent, PropType, ref, watch } from 'vue'; +<script lang="ts" setup> +import { ref, watch } from 'vue'; import MkButton from '@/components/MkButton.vue'; import { defaultStore } from '@/store'; import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, - }, +const props = defineProps<{ + p: () => Promise<any>; +}>(); - props: { - p: { - type: Function as PropType<() => Promise<any>>, - required: true, - }, - }, +const pending = ref(true); +const resolved = ref(false); +const rejected = ref(false); +const result = ref(null); - setup(props, context) { - const pending = ref(true); - const resolved = ref(false); - const rejected = ref(false); - const result = ref(null); +const process = () => { + if (props.p == null) { + return; + } + const promise = props.p(); + pending.value = true; + resolved.value = false; + rejected.value = false; + promise.then((_result) => { + pending.value = false; + resolved.value = true; + result.value = _result; + }); + promise.catch(() => { + pending.value = false; + rejected.value = true; + }); +}; - const process = () => { - if (props.p == null) { - return; - } - const promise = props.p(); - pending.value = true; - resolved.value = false; - rejected.value = false; - promise.then((_result) => { - pending.value = false; - resolved.value = true; - result.value = _result; - }); - promise.catch(() => { - pending.value = false; - rejected.value = true; - }); - }; - - watch(() => props.p, () => { - process(); - }, { - immediate: true, - }); - - const retry = () => { - process(); - }; - - return { - pending, - resolved, - rejected, - result, - retry, - defaultStore, - i18n, - }; - }, +watch(() => props.p, () => { + process(); +}, { + immediate: true, }); + +const retry = () => { + process(); +}; </script> -<style lang="scss" scoped> -.fade-enter-active, -.fade-leave-active { - transition: opacity 0.125s ease; -} -.fade-enter-from, -.fade-leave-to { - opacity: 0; -} - -.wszdbhzo { +<style lang="scss" module> +.error { padding: 16px; text-align: center; - - > .retry { - margin-top: 16px; - } } </style> diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue index 40d134dff..4e608c6ef 100644 --- a/packages/frontend/src/components/global/MkA.vue +++ b/packages/frontend/src/components/global/MkA.vue @@ -15,7 +15,7 @@ import { useRouter } from '@/router'; const props = withDefaults(defineProps<{ to: string; activeClass?: null | string; - behavior?: null | 'window' | 'browser' | 'modalWindow'; + behavior?: null | 'window' | 'browser'; }>(), { activeClass: null, behavior: null, @@ -70,14 +70,6 @@ function openWindow() { os.pageWindow(props.to); } -function modalWindow() { - os.modalPageWindow(props.to); -} - -function popout() { - popout_(props.to); -} - function nav(ev: MouseEvent) { if (props.behavior === 'browser') { location.href = props.to; @@ -87,8 +79,6 @@ function nav(ev: MouseEvent) { if (props.behavior) { if (props.behavior === 'window') { return openWindow(); - } else if (props.behavior === 'modalWindow') { - return modalWindow(); } } diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue index 59358aef7..f93659f5e 100644 --- a/packages/frontend/src/components/global/MkAcct.vue +++ b/packages/frontend/src/components/global/MkAcct.vue @@ -1,5 +1,5 @@ <template> -<MkCondensedLine v-if="defaultStore.state.enableCondensedLineForAcct" :min-scale="2 / 3"> +<MkCondensedLine v-if="defaultStore.state.enableCondensedLineForAcct" :minScale="2 / 3"> <span>@{{ user.username }}</span> <span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span> </MkCondensedLine> diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index aa975600f..8b25ab1b6 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -1,6 +1,14 @@ <template> <div v-if="chosen && !shouldHide" :class="$style.root"> - <div v-if="!showMenu" :class="[$style.main, $style['form_' + chosen.place]]"> + <div + v-if="!showMenu" + :class="[$style.main, { + [$style.form_square]: chosen.place === 'square', + [$style.form_horizontal]: chosen.place === 'horizontal', + [$style.form_horizontalBig]: chosen.place === 'horizontal-big', + [$style.form_vertical]: chosen.place === 'vertical', + }]" + > <a :href="chosen.url" target="_blank" :class="$style.link"> <img :src="chosen.imageUrl" :class="$style.img"> <button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button> @@ -122,7 +130,7 @@ function reduceFrequency(): void { } } - &.form_horizontal-big { + &.form_horizontalBig { padding: 8px; > .link, diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index 42abdcbdc..422b35c9d 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -1,6 +1,6 @@ <template> <component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick"> - <img :class="$style.inner" :src="url" decoding="async"/> + <MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user?.avatarBlurhash" :cover="true"/> <MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/> <div v-if="user.isCat" :class="[$style.ears]"> <div :class="$style.earLeft"> @@ -24,6 +24,7 @@ <script lang="ts" setup> import { watch } from 'vue'; import * as misskey from 'misskey-js'; +import MkImgWithBlurhash from '../MkImgWithBlurhash.vue'; import MkA from './MkA.vue'; import { getStaticImageUrl } from '@/scripts/media-proxy'; import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash'; diff --git a/packages/frontend/src/components/global/MkCondensedLine.vue b/packages/frontend/src/components/global/MkCondensedLine.vue index 1d46ff1ec..4b2e8e475 100644 --- a/packages/frontend/src/components/global/MkCondensedLine.vue +++ b/packages/frontend/src/components/global/MkCondensedLine.vue @@ -13,13 +13,20 @@ interface Props { const contentSymbol = Symbol(); const observer = new ResizeObserver((entries) => { + const results: { + container: HTMLSpanElement; + transform: string; + }[] = []; for (const entry of entries) { const content = (entry.target[contentSymbol] ? entry.target : entry.target.firstElementChild) as HTMLSpanElement; const props: Required<Props> = content[contentSymbol]; const container = content.parentElement as HTMLSpanElement; const contentWidth = content.getBoundingClientRect().width; const containerWidth = container.getBoundingClientRect().width; - container.style.transform = `scaleX(${Math.max(props.minScale, Math.min(1, containerWidth / contentWidth))})`; + results.push({ container, transform: `scaleX(${Math.max(props.minScale, Math.min(1, containerWidth / contentWidth))})` }); + } + for (const result of results) { + result.container.style.transform = result.transform; } }); </script> diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index 0cb31ffcb..e8a7f17cc 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -7,7 +7,7 @@ import { computed } from 'vue'; import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy'; import { defaultStore } from '@/store'; -import { customEmojis } from '@/custom-emojis'; +import { customEmojisMap } from '@/custom-emojis'; const props = defineProps<{ name: string; @@ -26,7 +26,7 @@ const rawUrl = computed(() => { return props.url; } if (isLocal.value) { - return customEmojis.value.find(x => x.name === customEmojiName.value)?.url ?? null; + return customEmojisMap.get(customEmojiName.value)?.url ?? null; } return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`; }); diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts index f6811b674..685b3b8b8 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { StoryObj } from '@storybook/vue3'; -import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.vue'; import { within } from '@storybook/testing-library'; import { expect } from '@storybook/jest'; +import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.ts'; export const Default = { render(args) { return { diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts new file mode 100644 index 000000000..2a50a3439 --- /dev/null +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -0,0 +1,367 @@ +import { VNode, h } from 'vue'; +import * as mfm from 'mfm-js'; +import * as Misskey from 'misskey-js'; +import MkUrl from '@/components/global/MkUrl.vue'; +import MkLink from '@/components/MkLink.vue'; +import MkMention from '@/components/MkMention.vue'; +import MkEmoji from '@/components/global/MkEmoji.vue'; +import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue'; +import MkCode from '@/components/MkCode.vue'; +import MkGoogle from '@/components/MkGoogle.vue'; +import MkSparkle from '@/components/MkSparkle.vue'; +import MkA from '@/components/global/MkA.vue'; +import { host } from '@/config'; +import { defaultStore } from '@/store'; + +const QUOTE_STYLE = ` +display: block; +margin: 8px; +padding: 6px 0 6px 12px; +color: var(--fg); +border-left: solid 3px var(--fg); +opacity: 0.7; +`.split('\n').join(' '); + +export default function(props: { + text: string; + plain?: boolean; + nowrap?: boolean; + author?: Misskey.entities.UserLite; + i?: Misskey.entities.UserLite; + isNote?: boolean; + emojiUrls?: string[]; + rootScale?: number; +}) { + const isNote = props.isNote !== undefined ? props.isNote : true; + + if (props.text == null || props.text === '') return; + + const ast = (props.plain ? mfm.parseSimple : mfm.parse)(props.text); + + const validTime = (t: string | null | undefined) => { + if (t == null) return null; + return t.match(/^[0-9.]+s$/) ? t : null; + }; + + const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm; + + /** + * Gen Vue Elements from MFM AST + * @param ast MFM AST + * @param scale How times large the text is + */ + const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => { + switch (token.type) { + case 'text': { + const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); + + if (!props.plain) { + const res: (VNode | string)[] = []; + for (const t of text.split('\n')) { + res.push(h('br')); + res.push(t); + } + res.shift(); + return res; + } else { + return [text.replace(/\n/g, ' ')]; + } + } + + case 'bold': { + return [h('b', genEl(token.children, scale))]; + } + + case 'strike': { + return [h('del', genEl(token.children, scale))]; + } + + case 'italic': { + return h('i', { + style: 'font-style: oblique;', + }, genEl(token.children, scale)); + } + + case 'fn': { + // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる + let style; + switch (token.props.name) { + case 'tada': { + const speed = validTime(token.props.args.speed) ?? '1s'; + style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : ''); + break; + } + case 'jelly': { + const speed = validTime(token.props.args.speed) ?? '1s'; + style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : ''); + break; + } + case 'twitch': { + const speed = validTime(token.props.args.speed) ?? '0.5s'; + style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : ''; + break; + } + case 'shake': { + const speed = validTime(token.props.args.speed) ?? '0.5s'; + style = useAnim ? `animation: mfm-shake ${speed} ease infinite;` : ''; + break; + } + case 'spin': { + const direction = + token.props.args.left ? 'reverse' : + token.props.args.alternate ? 'alternate' : + 'normal'; + const anime = + token.props.args.x ? 'mfm-spinX' : + token.props.args.y ? 'mfm-spinY' : + 'mfm-spin'; + const speed = validTime(token.props.args.speed) ?? '1.5s'; + style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : ''; + break; + } + case 'jump': { + const speed = validTime(token.props.args.speed) ?? '0.75s'; + style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : ''; + break; + } + case 'bounce': { + const speed = validTime(token.props.args.speed) ?? '0.75s'; + style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : ''; + break; + } + case 'flip': { + const transform = + (token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' : + token.props.args.v ? 'scaleY(-1)' : + 'scaleX(-1)'; + style = `transform: ${transform};`; + break; + } + case 'x2': { + return h('span', { + class: defaultStore.state.advancedMfm ? 'mfm-x2' : '', + }, genEl(token.children, scale * 2)); + } + case 'x3': { + return h('span', { + class: defaultStore.state.advancedMfm ? 'mfm-x3' : '', + }, genEl(token.children, scale * 3)); + } + case 'x4': { + return h('span', { + class: defaultStore.state.advancedMfm ? 'mfm-x4' : '', + }, genEl(token.children, scale * 4)); + } + case 'font': { + const family = + token.props.args.serif ? 'serif' : + token.props.args.monospace ? 'monospace' : + token.props.args.cursive ? 'cursive' : + token.props.args.fantasy ? 'fantasy' : + token.props.args.emoji ? 'emoji' : + token.props.args.math ? 'math' : + null; + if (family) style = `font-family: ${family};`; + break; + } + case 'blur': { + return h('span', { + class: '_mfm_blur_', + }, genEl(token.children, scale)); + } + case 'rainbow': { + const speed = validTime(token.props.args.speed) ?? '1s'; + style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite;` : ''; + break; + } + case 'sparkle': { + if (!useAnim) { + return genEl(token.children, scale); + } + return h(MkSparkle, {}, genEl(token.children, scale)); + } + case 'rotate': { + const degrees = parseFloat(token.props.args.deg ?? '90'); + style = `transform: rotate(${degrees}deg); transform-origin: center center;`; + break; + } + case 'position': { + if (!defaultStore.state.advancedMfm) break; + const x = parseFloat(token.props.args.x ?? '0'); + const y = parseFloat(token.props.args.y ?? '0'); + style = `transform: translateX(${x}em) translateY(${y}em);`; + break; + } + case 'scale': { + if (!defaultStore.state.advancedMfm) { + style = ''; + break; + } + const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5); + const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5); + style = `transform: scale(${x}, ${y});`; + scale = scale * Math.max(x, y); + break; + } + case 'fg': { + let color = token.props.args.color; + if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00'; + style = `color: #${color};`; + break; + } + case 'bg': { + let color = token.props.args.color; + if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00'; + style = `background-color: #${color};`; + break; + } + } + if (style == null) { + return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']); + } else { + return h('span', { + style: 'display: inline-block; ' + style, + }, genEl(token.children, scale)); + } + } + + case 'small': { + return [h('small', { + style: 'opacity: 0.7;', + }, genEl(token.children, scale))]; + } + + case 'center': { + return [h('div', { + style: 'text-align:center;', + }, genEl(token.children, scale))]; + } + + case 'url': { + return [h(MkUrl, { + key: Math.random(), + url: token.props.url, + rel: 'nofollow noopener', + })]; + } + + case 'link': { + return [h(MkLink, { + key: Math.random(), + url: token.props.url, + rel: 'nofollow noopener', + }, genEl(token.children, scale))]; + } + + case 'mention': { + return [h(MkMention, { + key: Math.random(), + host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) || host, + username: token.props.username, + })]; + } + + case 'hashtag': { + return [h(MkA, { + key: Math.random(), + to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`, + style: 'color:var(--hashtag);', + }, `#${token.props.hashtag}`)]; + } + + case 'blockCode': { + return [h(MkCode, { + key: Math.random(), + code: token.props.code, + lang: token.props.lang, + })]; + } + + case 'inlineCode': { + return [h(MkCode, { + key: Math.random(), + code: token.props.code, + inline: true, + })]; + } + + case 'quote': { + if (!props.nowrap) { + return [h('div', { + style: QUOTE_STYLE, + }, genEl(token.children, scale))]; + } else { + return [h('span', { + style: QUOTE_STYLE, + }, genEl(token.children, scale))]; + } + } + + case 'emojiCode': { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (props.author?.host == null) { + return [h(MkCustomEmoji, { + key: Math.random(), + name: token.props.name, + normal: props.plain, + host: null, + useOriginalSize: scale >= 2.5, + })]; + } else { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (props.emojiUrls && (props.emojiUrls[token.props.name] == null)) { + return [h('span', `:${token.props.name}:`)]; + } else { + return [h(MkCustomEmoji, { + key: Math.random(), + name: token.props.name, + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + url: props.emojiUrls ? props.emojiUrls[token.props.name] : null, + normal: props.plain, + host: props.author.host, + useOriginalSize: scale >= 2.5, + })]; + } + } + } + + case 'unicodeEmoji': { + return [h(MkEmoji, { + key: Math.random(), + emoji: token.props.emoji, + })]; + } + + case 'mathInline': { + return [h('code', token.props.formula)]; + } + + case 'mathBlock': { + return [h('code', token.props.formula)]; + } + + case 'search': { + return [h(MkGoogle, { + key: Math.random(), + q: token.props.query, + })]; + } + + case 'plain': { + return [h('span', genEl(token.children, scale))]; + } + + default: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + console.error('unrecognized ast type:', (token as any).type); + + return []; + } + } + }).flat(Infinity) as (VNode | string)[]; + + return h('span', { + // https://codeday.me/jp/qa/20190424/690106.html + style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;', + }, genEl(ast, props.rootScale ?? 1)); +} diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue deleted file mode 100644 index 28a0d1c98..000000000 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue +++ /dev/null @@ -1,171 +0,0 @@ -<template> -<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :is-note="isNote" :class="[$style.root, { [$style.nowrap]: nowrap }]"/> -</template> - -<script lang="ts" setup> -import { } from 'vue'; -import MfmCore from '@/components/mfm'; - -const props = withDefaults(defineProps<{ - text: string; - plain?: boolean; - nowrap?: boolean; - author?: any; - isNote?: boolean; -}>(), { - plain: false, - nowrap: false, - author: null, - isNote: true, -}); -</script> - -<style lang="scss"> -._mfm_blur_ { - filter: blur(6px); - transition: filter 0.3s; - - &:hover { - filter: blur(0px); - } -} - -.mfm-x2 { - --mfm-zoom-size: 200%; -} - -.mfm-x3 { - --mfm-zoom-size: 400%; -} - -.mfm-x4 { - --mfm-zoom-size: 600%; -} - -.mfm-x2, .mfm-x3, .mfm-x4 { - font-size: var(--mfm-zoom-size); - - .mfm-x2, .mfm-x3, .mfm-x4 { - /* only half effective */ - font-size: calc(var(--mfm-zoom-size) / 2 + 50%); - - .mfm-x2, .mfm-x3, .mfm-x4 { - /* disabled */ - font-size: 100%; - } - } -} - -@keyframes mfm-spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -@keyframes mfm-spinX { - 0% { transform: perspective(128px) rotateX(0deg); } - 100% { transform: perspective(128px) rotateX(360deg); } -} - -@keyframes mfm-spinY { - 0% { transform: perspective(128px) rotateY(0deg); } - 100% { transform: perspective(128px) rotateY(360deg); } -} - -@keyframes mfm-jump { - 0% { transform: translateY(0); } - 25% { transform: translateY(-16px); } - 50% { transform: translateY(0); } - 75% { transform: translateY(-8px); } - 100% { transform: translateY(0); } -} - -@keyframes mfm-bounce { - 0% { transform: translateY(0) scale(1, 1); } - 25% { transform: translateY(-16px) scale(1, 1); } - 50% { transform: translateY(0) scale(1, 1); } - 75% { transform: translateY(0) scale(1.5, 0.75); } - 100% { transform: translateY(0) scale(1, 1); } -} - -// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`; -// let css = ''; -// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; } -@keyframes mfm-twitch { - 0% { transform: translate(7px, -2px) } - 5% { transform: translate(-3px, 1px) } - 10% { transform: translate(-7px, -1px) } - 15% { transform: translate(0px, -1px) } - 20% { transform: translate(-8px, 6px) } - 25% { transform: translate(-4px, -3px) } - 30% { transform: translate(-4px, -6px) } - 35% { transform: translate(-8px, -8px) } - 40% { transform: translate(4px, 6px) } - 45% { transform: translate(-3px, 1px) } - 50% { transform: translate(2px, -10px) } - 55% { transform: translate(-7px, 0px) } - 60% { transform: translate(-2px, 4px) } - 65% { transform: translate(3px, -8px) } - 70% { transform: translate(6px, 7px) } - 75% { transform: translate(-7px, -2px) } - 80% { transform: translate(-7px, -8px) } - 85% { transform: translate(9px, 3px) } - 90% { transform: translate(-3px, -2px) } - 95% { transform: translate(-10px, 2px) } - 100% { transform: translate(-2px, -6px) } -} - -// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`; -// let css = ''; -// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; } -@keyframes mfm-shake { - 0% { transform: translate(-3px, -1px) rotate(-8deg) } - 5% { transform: translate(0px, -1px) rotate(-10deg) } - 10% { transform: translate(1px, -3px) rotate(0deg) } - 15% { transform: translate(1px, 1px) rotate(11deg) } - 20% { transform: translate(-2px, 1px) rotate(1deg) } - 25% { transform: translate(-1px, -2px) rotate(-2deg) } - 30% { transform: translate(-1px, 2px) rotate(-3deg) } - 35% { transform: translate(2px, 1px) rotate(6deg) } - 40% { transform: translate(-2px, -3px) rotate(-9deg) } - 45% { transform: translate(0px, -1px) rotate(-12deg) } - 50% { transform: translate(1px, 2px) rotate(10deg) } - 55% { transform: translate(0px, -3px) rotate(8deg) } - 60% { transform: translate(1px, -1px) rotate(8deg) } - 65% { transform: translate(0px, -1px) rotate(-7deg) } - 70% { transform: translate(-1px, -3px) rotate(6deg) } - 75% { transform: translate(0px, -2px) rotate(4deg) } - 80% { transform: translate(-2px, -1px) rotate(3deg) } - 85% { transform: translate(1px, -3px) rotate(-10deg) } - 90% { transform: translate(1px, 0px) rotate(3deg) } - 95% { transform: translate(-2px, 0px) rotate(-3deg) } - 100% { transform: translate(2px, 1px) rotate(2deg) } -} - -@keyframes mfm-rubberBand { - from { transform: scale3d(1, 1, 1); } - 30% { transform: scale3d(1.25, 0.75, 1); } - 40% { transform: scale3d(0.75, 1.25, 1); } - 50% { transform: scale3d(1.15, 0.85, 1); } - 65% { transform: scale3d(0.95, 1.05, 1); } - 75% { transform: scale3d(1.05, 0.95, 1); } - to { transform: scale3d(1, 1, 1); } -} - -@keyframes mfm-rainbow { - 0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); } - 100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); } -} -</style> - -<style lang="scss" module> -.root { - white-space: pre-wrap; - - &.nowrap { - white-space: pre; - word-wrap: normal; // https://codeday.me/jp/qa/20190424/690106.html - overflow: hidden; - text-overflow: ellipsis; - } -} -</style> diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue index 9e1da64e6..d71343baf 100644 --- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue +++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue @@ -15,8 +15,8 @@ {{ t.title }} </div> <Transition - v-else mode="in-out" @enter="enter" @after-enter="afterEnter" @leave="leave" - @after-leave="afterLeave" + v-else mode="in-out" @enter="enter" @afterEnter="afterEnter" @leave="leave" + @afterLeave="afterLeave" > <div v-show="t.key === tab" :class="[$style.tabTitle, $style.animate]">{{ t.title }}</div> </Transition> diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index b91d378b1..0a21d39bc 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -21,7 +21,7 @@ </div> </div> </div> - <XTabs v-if="!narrow || hideTitle" :class="$style.tabs" :tab="tab" :tabs="tabs" :root-el="el" @update:tab="key => emit('update:tab', key)" @tab-click="onTabClick"/> + <XTabs v-if="!narrow || hideTitle" :class="$style.tabs" :tab="tab" :tabs="tabs" :rootEl="el" @update:tab="key => emit('update:tab', key)" @tabClick="onTabClick"/> </template> <div v-if="(!thin_ && narrow && !hideTitle) || (actions && actions.length > 0)" :class="$style.buttonsRight"> <template v-for="action in actions"> @@ -30,7 +30,7 @@ </div> </div> <div v-if="(narrow && !hideTitle) && hasTabs" :class="[$style.lower, { [$style.slim]: narrow, [$style.thin]: thin_ }]"> - <XTabs :class="$style.tabs" :tab="tab" :tabs="tabs" :root-el="el" @update:tab="key => emit('update:tab', key)" @tab-click="onTabClick"/> + <XTabs :class="$style.tabs" :tab="tab" :tabs="tabs" :rootEl="el" @update:tab="key => emit('update:tab', key)" @tabClick="onTabClick"/> </div> </div> </template> diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue index 44c02088d..e5dba54b4 100644 --- a/packages/frontend/src/components/global/MkStickyContainer.vue +++ b/packages/frontend/src/components/global/MkStickyContainer.vue @@ -14,6 +14,7 @@ <script lang="ts" setup> import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue'; +import { $$ } from 'vue/macros'; import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const'; const rootEl = $shallowRef<HTMLElement>(); @@ -83,8 +84,8 @@ onMounted(() => { onUnmounted(() => { observer.disconnect(); }); + +defineExpose({ + rootEl: $$(rootEl), +}); </script> - -<style lang="scss" module> - -</style> diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index 261cc0ee1..dfc3c8979 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -58,7 +58,6 @@ function tick() { if (props.mode === 'relative' || props.mode === 'detail') { tick(); - onUnmounted(() => { window.clearTimeout(tickId); }); diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue index 2a9278030..c1efd9a06 100644 --- a/packages/frontend/src/components/global/MkUrl.vue +++ b/packages/frontend/src/components/global/MkUrl.vue @@ -6,7 +6,7 @@ <template v-if="!self"> <span :class="$style.schema">{{ schema }}//</span> <span :class="$style.hostname">{{ hostname }}</span> - <span v-if="port != ''" :class="$style.port">:{{ port }}</span> + <span v-if="port != ''">:{{ port }}</span> </template> <template v-if="pathname === '/' && self"> <span :class="$style.self">{{ hostname }}</span> diff --git a/packages/frontend/src/components/global/MkUserName.vue b/packages/frontend/src/components/global/MkUserName.vue index 4186a4a4f..c9e85c546 100644 --- a/packages/frontend/src/components/global/MkUserName.vue +++ b/packages/frontend/src/components/global/MkUserName.vue @@ -1,5 +1,5 @@ <template> -<Mfm :text="user.name ?? user.username" :author="user" :plain="true" :nowrap="nowrap" :emoji-urls="user.emojis"/> +<Mfm :text="user.name ?? user.username" :author="user" :plain="true" :nowrap="nowrap" :emojiUrls="user.emojis"/> </template> <script lang="ts" setup> diff --git a/packages/frontend/src/components/global/i18n.ts b/packages/frontend/src/components/global/i18n.ts index 1fd293ba1..2708b759a 100644 --- a/packages/frontend/src/components/global/i18n.ts +++ b/packages/frontend/src/components/global/i18n.ts @@ -1,42 +1,24 @@ -import { h, defineComponent } from 'vue'; +import { h } from 'vue'; -export default defineComponent({ - props: { - src: { - type: String, - required: true, - }, - tag: { - type: String, - required: false, - default: 'span', - }, - textTag: { - type: String, - required: false, - default: null, - }, - }, - render() { - let str = this.src; - const parsed = [] as (string | { arg: string; })[]; - while (true) { - const nextBracketOpen = str.indexOf('{'); - const nextBracketClose = str.indexOf('}'); +export default function(props: { src: string; tag?: string; textTag?: string; }, { slots }) { + let str = props.src; + const parsed = [] as (string | { arg: string; })[]; + while (true) { + const nextBracketOpen = str.indexOf('{'); + const nextBracketClose = str.indexOf('}'); - if (nextBracketOpen === -1) { - parsed.push(str); - break; - } else { - if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen)); - parsed.push({ - arg: str.substring(nextBracketOpen + 1, nextBracketClose), - }); - } - - str = str.substr(nextBracketClose + 1); + if (nextBracketOpen === -1) { + parsed.push(str); + break; + } else { + if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen)); + parsed.push({ + arg: str.substring(nextBracketOpen + 1, nextBracketClose), + }); } - return h(this.tag, parsed.map(x => typeof x === 'string' ? (this.textTag ? h(this.textTag, x) : x) : this.$slots[x.arg]())); - }, -}); + str = str.substr(nextBracketClose + 1); + } + + return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]())); +} diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index 4ef8111da..ee2a2bc7b 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -1,6 +1,6 @@ import { App } from 'vue'; -import Mfm from './global/MkMisskeyFlavoredMarkdown.vue'; +import Mfm from './global/MkMisskeyFlavoredMarkdown.ts'; import MkA from './global/MkA.vue'; import MkAcct from './global/MkAcct.vue'; import MkAvatar from './global/MkAvatar.vue'; diff --git a/packages/frontend/src/components/mfm.ts b/packages/frontend/src/components/mfm.ts deleted file mode 100644 index c3c07b583..000000000 --- a/packages/frontend/src/components/mfm.ts +++ /dev/null @@ -1,390 +0,0 @@ -import { VNode, defineComponent, h } from 'vue'; -import * as mfm from 'mfm-js'; -import MkUrl from '@/components/global/MkUrl.vue'; -import MkLink from '@/components/MkLink.vue'; -import MkMention from '@/components/MkMention.vue'; -import MkEmoji from '@/components/global/MkEmoji.vue'; -import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue'; -import MkCode from '@/components/MkCode.vue'; -import MkGoogle from '@/components/MkGoogle.vue'; -import MkSparkle from '@/components/MkSparkle.vue'; -import MkA from '@/components/global/MkA.vue'; -import { host } from '@/config'; -import { defaultStore } from '@/store'; - -const QUOTE_STYLE = ` -display: block; -margin: 8px; -padding: 6px 0 6px 12px; -color: var(--fg); -border-left: solid 3px var(--fg); -opacity: 0.7; -`.split('\n').join(' '); - -export default defineComponent({ - props: { - text: { - type: String, - required: true, - }, - plain: { - type: Boolean, - default: false, - }, - nowrap: { - type: Boolean, - default: false, - }, - author: { - type: Object, - default: null, - }, - i: { - type: Object, - default: null, - }, - isNote: { - type: Boolean, - default: true, - }, - emojiUrls: { - type: Object, - default: null, - }, - rootScale: { - type: Number, - default: 1, - } - }, - - render() { - if (this.text == null || this.text === '') return; - - const ast = (this.plain ? mfm.parseSimple : mfm.parse)(this.text); - - const validTime = (t: string | null | undefined) => { - if (t == null) return null; - return t.match(/^[0-9.]+s$/) ? t : null; - }; - - const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm; - - /** - * Gen Vue Elements from MFM AST - * @param ast MFM AST - * @param scale How times large the text is - */ - const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => { - switch (token.type) { - case 'text': { - const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); - - if (!this.plain) { - const res: (VNode | string)[] = []; - for (const t of text.split('\n')) { - res.push(h('br')); - res.push(t); - } - res.shift(); - return res; - } else { - return [text.replace(/\n/g, ' ')]; - } - } - - case 'bold': { - return [h('b', genEl(token.children, scale))]; - } - - case 'strike': { - return [h('del', genEl(token.children, scale))]; - } - - case 'italic': { - return h('i', { - style: 'font-style: oblique;', - }, genEl(token.children, scale)); - } - - case 'fn': { - // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる - let style; - switch (token.props.name) { - case 'tada': { - const speed = validTime(token.props.args.speed) ?? '1s'; - style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : ''); - break; - } - case 'jelly': { - const speed = validTime(token.props.args.speed) ?? '1s'; - style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : ''); - break; - } - case 'twitch': { - const speed = validTime(token.props.args.speed) ?? '0.5s'; - style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : ''; - break; - } - case 'shake': { - const speed = validTime(token.props.args.speed) ?? '0.5s'; - style = useAnim ? `animation: mfm-shake ${speed} ease infinite;` : ''; - break; - } - case 'spin': { - const direction = - token.props.args.left ? 'reverse' : - token.props.args.alternate ? 'alternate' : - 'normal'; - const anime = - token.props.args.x ? 'mfm-spinX' : - token.props.args.y ? 'mfm-spinY' : - 'mfm-spin'; - const speed = validTime(token.props.args.speed) ?? '1.5s'; - style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : ''; - break; - } - case 'jump': { - const speed = validTime(token.props.args.speed) ?? '0.75s'; - style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : ''; - break; - } - case 'bounce': { - const speed = validTime(token.props.args.speed) ?? '0.75s'; - style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : ''; - break; - } - case 'flip': { - const transform = - (token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' : - token.props.args.v ? 'scaleY(-1)' : - 'scaleX(-1)'; - style = `transform: ${transform};`; - break; - } - case 'x2': { - return h('span', { - class: defaultStore.state.advancedMfm ? 'mfm-x2' : '', - }, genEl(token.children, scale * 2)); - } - case 'x3': { - return h('span', { - class: defaultStore.state.advancedMfm ? 'mfm-x3' : '', - }, genEl(token.children, scale * 3)); - } - case 'x4': { - return h('span', { - class: defaultStore.state.advancedMfm ? 'mfm-x4' : '', - }, genEl(token.children, scale * 4)); - } - case 'font': { - const family = - token.props.args.serif ? 'serif' : - token.props.args.monospace ? 'monospace' : - token.props.args.cursive ? 'cursive' : - token.props.args.fantasy ? 'fantasy' : - token.props.args.emoji ? 'emoji' : - token.props.args.math ? 'math' : - null; - if (family) style = `font-family: ${family};`; - break; - } - case 'blur': { - return h('span', { - class: '_mfm_blur_', - }, genEl(token.children, scale)); - } - case 'rainbow': { - const speed = validTime(token.props.args.speed) ?? '1s'; - style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite;` : ''; - break; - } - case 'sparkle': { - if (!useAnim) { - return genEl(token.children, scale); - } - return h(MkSparkle, {}, genEl(token.children, scale)); - } - case 'rotate': { - const degrees = parseFloat(token.props.args.deg ?? '90'); - style = `transform: rotate(${degrees}deg); transform-origin: center center;`; - break; - } - case 'position': { - if (!defaultStore.state.advancedMfm) break; - const x = parseFloat(token.props.args.x ?? '0'); - const y = parseFloat(token.props.args.y ?? '0'); - style = `transform: translateX(${x}em) translateY(${y}em);`; - break; - } - case 'scale': { - if (!defaultStore.state.advancedMfm) { - style = ''; - break; - } - const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5); - const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5); - style = `transform: scale(${x}, ${y});`; - scale = scale * Math.max(x, y); - break; - } - case 'fg': { - let color = token.props.args.color; - if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00'; - style = `color: #${color};`; - break; - } - case 'bg': { - let color = token.props.args.color; - if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00'; - style = `background-color: #${color};`; - break; - } - } - if (style == null) { - return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']); - } else { - return h('span', { - style: 'display: inline-block; ' + style, - }, genEl(token.children, scale)); - } - } - - case 'small': { - return [h('small', { - style: 'opacity: 0.7;', - }, genEl(token.children, scale))]; - } - - case 'center': { - return [h('div', { - style: 'text-align:center;', - }, genEl(token.children, scale))]; - } - - case 'url': { - return [h(MkUrl, { - key: Math.random(), - url: token.props.url, - rel: 'nofollow noopener', - })]; - } - - case 'link': { - return [h(MkLink, { - key: Math.random(), - url: token.props.url, - rel: 'nofollow noopener', - }, genEl(token.children, scale))]; - } - - case 'mention': { - return [h(MkMention, { - key: Math.random(), - host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host, - username: token.props.username, - })]; - } - - case 'hashtag': { - return [h(MkA, { - key: Math.random(), - to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`, - style: 'color:var(--hashtag);', - }, `#${token.props.hashtag}`)]; - } - - case 'blockCode': { - return [h(MkCode, { - key: Math.random(), - code: token.props.code, - lang: token.props.lang, - })]; - } - - case 'inlineCode': { - return [h(MkCode, { - key: Math.random(), - code: token.props.code, - inline: true, - })]; - } - - case 'quote': { - if (!this.nowrap) { - return [h('div', { - style: QUOTE_STYLE, - }, genEl(token.children, scale))]; - } else { - return [h('span', { - style: QUOTE_STYLE, - }, genEl(token.children, scale))]; - } - } - - case 'emojiCode': { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (this.author?.host == null) { - return [h(MkCustomEmoji, { - key: Math.random(), - name: token.props.name, - normal: this.plain, - host: null, - useOriginalSize: scale >= 2.5, - })]; - } else { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (this.emojiUrls && (this.emojiUrls[token.props.name] == null)) { - return [h('span', `:${token.props.name}:`)]; - } else { - return [h(MkCustomEmoji, { - key: Math.random(), - name: token.props.name, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - url: this.emojiUrls ? this.emojiUrls[token.props.name] : null, - normal: this.plain, - host: this.author.host, - useOriginalSize: scale >= 2.5, - })]; - } - } - } - - case 'unicodeEmoji': { - return [h(MkEmoji, { - key: Math.random(), - emoji: token.props.emoji, - })]; - } - - case 'mathInline': { - return [h('code', token.props.formula)]; - } - - case 'mathBlock': { - return [h('code', token.props.formula)]; - } - - case 'search': { - return [h(MkGoogle, { - key: Math.random(), - q: token.props.query, - })]; - } - - case 'plain': { - return [h('span', genEl(token.children, scale))]; - } - - default: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - console.error('unrecognized ast type:', (token as any).type); - - return []; - } - } - }).flat(Infinity) as (VNode | string)[]; - - // Parse ast to DOM - return h('span', genEl(ast, this.rootScale)); - }, -}); diff --git a/packages/frontend/src/components/page/block.type.ts b/packages/frontend/src/components/page/block.type.ts new file mode 100644 index 000000000..71249a8af --- /dev/null +++ b/packages/frontend/src/components/page/block.type.ts @@ -0,0 +1,29 @@ +export type BlockBase = { + id: string; + type: string; +}; + +export type TextBlock = BlockBase & { + type: 'text'; + text: string; +}; + +export type SectionBlock = BlockBase & { + type: 'section'; + title: string; + children: Block[]; +}; + +export type ImageBlock = BlockBase & { + type: 'image'; + fileId: string | null; +}; + +export type NoteBlock = BlockBase & { + type: 'note'; + detailed: boolean; + note: string | null; +}; + +export type Block = + TextBlock | SectionBlock | ImageBlock | NoteBlock; diff --git a/packages/frontend/src/components/page/page.block.vue b/packages/frontend/src/components/page/page.block.vue index f3e776460..2bf3d12da 100644 --- a/packages/frontend/src/components/page/page.block.vue +++ b/packages/frontend/src/components/page/page.block.vue @@ -1,44 +1,29 @@ <template> -<component :is="'x-' + block.type" :key="block.id" :block="block" :hpml="hpml" :h="h"/> +<component :is="getComponent(block.type)" :key="block.id" :page="page" :block="block" :h="h"/> </template> -<script lang="ts"> -import { defineComponent, PropType } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; +import * as Misskey from 'misskey-js'; import XText from './page.text.vue'; import XSection from './page.section.vue'; import XImage from './page.image.vue'; -import XButton from './page.button.vue'; -import XNumberInput from './page.number-input.vue'; -import XTextInput from './page.text-input.vue'; -import XTextareaInput from './page.textarea-input.vue'; -import XSwitch from './page.switch.vue'; -import XIf from './page.if.vue'; -import XTextarea from './page.textarea.vue'; -import XPost from './page.post.vue'; -import XCounter from './page.counter.vue'; -import XRadioButton from './page.radio-button.vue'; -import XCanvas from './page.canvas.vue'; import XNote from './page.note.vue'; -import { Hpml } from '@/scripts/hpml/evaluator'; -import { Block } from '@/scripts/hpml/block'; +import { Block } from './block.type'; -export default defineComponent({ - components: { - XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton, XCanvas, XNote, - }, - props: { - block: { - type: Object as PropType<Block>, - required: true, - }, - hpml: { - type: Object as PropType<Hpml>, - required: true, - }, - h: { - type: Number, - required: true, - }, - }, -}); +function getComponent(type: string) { + switch (type) { + case 'text': return XText; + case 'section': return XSection; + case 'image': return XImage; + case 'note': return XNote; + default: return null; + } +} + +defineProps<{ + block: Block, + h: number, + page: Misskey.entities.Page, +}>(); </script> diff --git a/packages/frontend/src/components/page/page.button.vue b/packages/frontend/src/components/page/page.button.vue deleted file mode 100644 index 83931021d..000000000 --- a/packages/frontend/src/components/page/page.button.vue +++ /dev/null @@ -1,66 +0,0 @@ -<template> -<div> - <MkButton class="kudkigyw" :primary="block.primary" @click="click()">{{ hpml.interpolate(block.text) }}</MkButton> -</div> -</template> - -<script lang="ts"> -import { defineComponent, PropType, unref } from 'vue'; -import MkButton from '../MkButton.vue'; -import * as os from '@/os'; -import { ButtonBlock } from '@/scripts/hpml/block'; -import { Hpml } from '@/scripts/hpml/evaluator'; - -export default defineComponent({ - components: { - MkButton, - }, - props: { - block: { - type: Object as PropType<ButtonBlock>, - required: true, - }, - hpml: { - type: Object as PropType<Hpml>, - required: true, - }, - }, - methods: { - click() { - if (this.block.action === 'dialog') { - this.hpml.eval(); - os.alert({ - text: this.hpml.interpolate(this.block.content), - }); - } else if (this.block.action === 'resetRandom') { - this.hpml.updateRandomSeed(Math.random()); - this.hpml.eval(); - } else if (this.block.action === 'pushEvent') { - os.api('page-push', { - pageId: this.hpml.page.id, - event: this.block.event, - ...(this.block.var ? { - var: unref(this.hpml.vars)[this.block.var], - } : {}), - }); - - os.alert({ - type: 'success', - text: this.hpml.interpolate(this.block.message), - }); - } else if (this.block.action === 'callAiScript') { - this.hpml.callAiScript(this.block.fn); - } - }, - }, -}); -</script> - -<style lang="scss" scoped> -.kudkigyw { - display: inline-block; - min-width: 200px; - max-width: 450px; - margin: 8px 0; -} -</style> diff --git a/packages/frontend/src/components/page/page.canvas.vue b/packages/frontend/src/components/page/page.canvas.vue deleted file mode 100644 index 82ff36ec3..000000000 --- a/packages/frontend/src/components/page/page.canvas.vue +++ /dev/null @@ -1,48 +0,0 @@ -<template> -<div class="ysrxegms"> - <canvas ref="canvas" :width="block.width" :height="block.height"/> -</div> -</template> - -<script lang="ts"> -import { defineComponent, onMounted, PropType, Ref, ref } from 'vue'; -import { CanvasBlock } from '@/scripts/hpml/block'; -import { Hpml } from '@/scripts/hpml/evaluator'; - -export default defineComponent({ - props: { - block: { - type: Object as PropType<CanvasBlock>, - required: true, - }, - hpml: { - type: Object as PropType<Hpml>, - required: true, - }, - }, - setup(props, ctx) { - const canvas: Ref<any> = ref(null); - - onMounted(() => { - props.hpml.registerCanvas(props.block.name, canvas.value); - }); - - return { - canvas, - }; - }, -}); -</script> - -<style lang="scss" scoped> -.ysrxegms { - display: inline-block; - vertical-align: bottom; - overflow: auto; - max-width: 100%; - - > canvas { - display: block; - } -} -</style> diff --git a/packages/frontend/src/components/page/page.counter.vue b/packages/frontend/src/components/page/page.counter.vue deleted file mode 100644 index 63fde6a12..000000000 --- a/packages/frontend/src/components/page/page.counter.vue +++ /dev/null @@ -1,51 +0,0 @@ -<template> -<div> - <MkButton class="llumlmnx" @click="click()">{{ hpml.interpolate(block.text) }}</MkButton> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent, PropType } from 'vue'; -import MkButton from '../MkButton.vue'; -import { CounterVarBlock } from '@/scripts/hpml/block'; -import { Hpml } from '@/scripts/hpml/evaluator'; - -export default defineComponent({ - components: { - MkButton, - }, - props: { - block: { - type: Object as PropType<CounterVarBlock>, - required: true, - }, - hpml: { - type: Object as PropType<Hpml>, - required: true, - }, - }, - setup(props, ctx) { - const value = computed(() => { - return props.hpml.vars.value[props.block.name]; - }); - - function click() { - props.hpml.updatePageVar(props.block.name, value.value + (props.block.inc || 1)); - props.hpml.eval(); - } - - return { - click, - }; - }, -}); -</script> - -<style lang="scss" scoped> -.llumlmnx { - display: inline-block; - min-width: 300px; - max-width: 450px; - margin: 8px 0; -} -</style> diff --git a/packages/frontend/src/components/page/page.if.vue b/packages/frontend/src/components/page/page.if.vue deleted file mode 100644 index 372a15f0c..000000000 --- a/packages/frontend/src/components/page/page.if.vue +++ /dev/null @@ -1,31 +0,0 @@ -<template> -<div v-show="hpml.vars.value[block.var]"> - <XBlock v-for="child in block.children" :key="child.id" :block="child" :hpml="hpml" :h="h"/> -</div> -</template> - -<script lang="ts"> -import { IfBlock } from '@/scripts/hpml/block'; -import { Hpml } from '@/scripts/hpml/evaluator'; -import { defineComponent, defineAsyncComponent, PropType } from 'vue'; - -export default defineComponent({ - components: { - XBlock: defineAsyncComponent(() => import('./page.block.vue')), - }, - props: { - block: { - type: Object as PropType<IfBlock>, - required: true, - }, - hpml: { - type: Object as PropType<Hpml>, - required: true, - }, - h: { - type: Number, - required: true, - }, - }, -}); -</script> diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue index 6ea81d257..2edcfb8b1 100644 --- a/packages/frontend/src/components/page/page.image.vue +++ b/packages/frontend/src/components/page/page.image.vue @@ -5,15 +5,15 @@ </template> <script lang="ts" setup> -import { PropType } from 'vue'; +import { } from 'vue'; +import * as Misskey from 'misskey-js'; +import { ImageBlock } from './block.type'; import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; -import { ImageBlock } from '@/scripts/hpml/block'; -import { Hpml } from '@/scripts/hpml/evaluator'; const props = defineProps<{ - block: PropType<ImageBlock>, - hpml: PropType<Hpml>, + block: ImageBlock, + page: Misskey.entities.Page, }>(); -const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId); +const image = props.page.attachedFiles.find(x => x.id === props.block.fileId); </script> diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue index 8c65dabf0..7133a7f5a 100644 --- a/packages/frontend/src/components/page/page.note.vue +++ b/packages/frontend/src/components/page/page.note.vue @@ -1,47 +1,29 @@ <template> -<div class="voxdxuby"> +<div style="margin: 1em 0;"> <MkNote v-if="note && !block.detailed" :key="note.id + ':normal'" v-model:note="note"/> <MkNoteDetailed v-if="note && block.detailed" :key="note.id + ':detail'" v-model:note="note"/> </div> </template> -<script lang="ts"> -import { defineComponent, onMounted, PropType, Ref, ref } from 'vue'; +<script lang="ts" setup> +import { onMounted, Ref, ref } from 'vue'; +import * as Misskey from 'misskey-js'; +import { NoteBlock } from './block.type'; import MkNote from '@/components/MkNote.vue'; import MkNoteDetailed from '@/components/MkNoteDetailed.vue'; import * as os from '@/os'; -import { NoteBlock } from '@/scripts/hpml/block'; -export default defineComponent({ - components: { - MkNote, - MkNoteDetailed, - }, - props: { - block: { - type: Object as PropType<NoteBlock>, - required: true, - }, - }, - setup(props, ctx) { - const note: Ref<Record<string, any> | null> = ref(null); +const props = defineProps<{ + block: NoteBlock, + page: Misskey.entities.Page, +}>(); - onMounted(() => { - os.api('notes/show', { noteId: props.block.note }) - .then(result => { - note.value = result; - }); +const note: Ref<Misskey.entities.Note | null> = ref(null); + +onMounted(() => { + os.api('notes/show', { noteId: props.block.note }) + .then(result => { + note.value = result; }); - - return { - note, - }; - }, }); </script> - -<style lang="scss" scoped> -.voxdxuby { - margin: 1em 0; -} -</style> diff --git a/packages/frontend/src/components/page/page.number-input.vue b/packages/frontend/src/components/page/page.number-input.vue deleted file mode 100644 index 72c1b6deb..000000000 --- a/packages/frontend/src/components/page/page.number-input.vue +++ /dev/null @@ -1,54 +0,0 @@ -<template> -<div> - <MkInput class="kudkigyw" :model-value="value" type="number" @update:model-value="updateValue($event)"> - <template #label>{{ hpml.interpolate(block.text) }}</template> - </MkInput> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent, PropType } from 'vue'; -import MkInput from '../MkInput.vue'; -import { Hpml } from '@/scripts/hpml/evaluator'; -import { NumberInputVarBlock } from '@/scripts/hpml/block'; - -export default defineComponent({ - components: { - MkInput, - }, - props: { - block: { - type: Object as PropType<NumberInputVarBlock>, - required: true, - }, - hpml: { - type: Object as PropType<Hpml>, - required: true, - }, - }, - setup(props, ctx) { - const value = computed(() => { - return props.hpml.vars.value[props.block.name]; - }); - - function updateValue(newValue) { - props.hpml.updatePageVar(props.block.name, newValue); - props.hpml.eval(); - } - - return { - value, - updateValue, - }; - }, -}); -</script> - -<style lang="scss" scoped> -.kudkigyw { - display: inline-block; - min-width: 300px; - max-width: 450px; - margin: 8px 0; -} -</style> diff --git a/packages/frontend/src/components/page/page.post.vue b/packages/frontend/src/components/page/page.post.vue deleted file mode 100644 index 55da610cb..000000000 --- a/packages/frontend/src/components/page/page.post.vue +++ /dev/null @@ -1,111 +0,0 @@ -<template> -<div class="ngbfujlo"> - <MkTextarea :model-value="text" readonly style="margin: 0;"></MkTextarea> - <MkButton class="button" primary :disabled="posting || posted" @click="post()"> - <i v-if="posted" class="ti ti-check"></i> - <i v-else class="ti ti-send"></i> - </MkButton> -</div> -</template> - -<script lang="ts"> -import { defineComponent, PropType } from 'vue'; -import MkTextarea from '../MkTextarea.vue'; -import MkButton from '../MkButton.vue'; -import { apiUrl } from '@/config'; -import * as os from '@/os'; -import { PostBlock } from '@/scripts/hpml/block'; -import { Hpml } from '@/scripts/hpml/evaluator'; -import { defaultStore } from '@/store'; -import { $i } from '@/account'; - -export default defineComponent({ - components: { - MkTextarea, - MkButton, - }, - props: { - block: { - type: Object as PropType<PostBlock>, - required: true, - }, - hpml: { - type: Object as PropType<Hpml>, - required: true, - }, - }, - data() { - return { - text: this.hpml.interpolate(this.block.text), - posted: false, - posting: false, - }; - }, - watch: { - 'hpml.vars': { - handler() { - this.text = this.hpml.interpolate(this.block.text); - }, - deep: true, - }, - }, - methods: { - upload() { - const promise = new Promise((ok) => { - const canvas = this.hpml.canvases[this.block.canvasId]; - canvas.toBlob(blob => { - const formData = new FormData(); - formData.append('file', blob); - formData.append('i', $i.token); - if (defaultStore.state.uploadFolder) { - formData.append('folderId', defaultStore.state.uploadFolder); - } - - window.fetch(apiUrl + '/drive/files/create', { - method: 'POST', - body: formData, - }) - .then(response => response.json()) - .then(f => { - ok(f); - }); - }); - }); - os.promiseDialog(promise); - return promise; - }, - async post() { - this.posting = true; - const file = this.block.attachCanvasImage ? await this.upload() : null; - os.apiWithDialog('notes/create', { - text: this.text === '' ? null : this.text, - fileIds: file ? [file.id] : undefined, - }).then(() => { - this.posted = true; - }); - }, - }, -}); -</script> - -<style lang="scss" scoped> -.ngbfujlo { - position: relative; - padding: 32px; - border-radius: 6px; - box-shadow: 0 2px 8px var(--shadow); - z-index: 1; - - > .button { - margin-top: 32px; - } - - @media (max-width: 600px) { - padding: 16px; - - > .button { - margin-top: 16px; - } - } -} -</style> diff --git a/packages/frontend/src/components/page/page.radio-button.vue b/packages/frontend/src/components/page/page.radio-button.vue deleted file mode 100644 index ce8f252e4..000000000 --- a/packages/frontend/src/components/page/page.radio-button.vue +++ /dev/null @@ -1,44 +0,0 @@ -<template> -<div> - <div>{{ hpml.interpolate(block.title) }}</div> - <MkRadio v-for="item in block.values" :key="item" :modelValue="value" :value="item" @update:model-value="updateValue($event)">{{ item }}</MkRadio> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent, PropType } from 'vue'; -import MkRadio from '../MkRadio.vue'; -import { Hpml } from '@/scripts/hpml/evaluator'; -import { RadioButtonVarBlock } from '@/scripts/hpml/block'; - -export default defineComponent({ - components: { - MkRadio, - }, - props: { - block: { - type: Object as PropType<RadioButtonVarBlock>, - required: true, - }, - hpml: { - type: Object as PropType<Hpml>, - required: true, - }, - }, - setup(props, ctx) { - const value = computed(() => { - return props.hpml.vars.value[props.block.name]; - }); - - function updateValue(newValue: string) { - props.hpml.updatePageVar(props.block.name, newValue); - props.hpml.eval(); - } - - return { - value, - updateValue, - }; - }, -}); -</script> diff --git a/packages/frontend/src/components/page/page.section.vue b/packages/frontend/src/components/page/page.section.vue index 50181b390..83a16ae0a 100644 --- a/packages/frontend/src/components/page/page.section.vue +++ b/packages/frontend/src/components/page/page.section.vue @@ -1,59 +1,49 @@ <template> -<section class="sdgxphyu"> - <component :is="'h' + h">{{ block.title }}</component> +<section> + <component + :is="'h' + h" + :class="{ + 'h2': h === 2, + 'h3': h === 3, + 'h4': h === 4, + }" + > + {{ block.title }} + </component> - <div class="children"> - <XBlock v-for="child in block.children" :key="child.id" :block="child" :hpml="hpml" :h="h + 1"/> + <div class="_gaps"> + <XBlock v-for="child in block.children" :key="child.id" :page="page" :block="child" :h="h + 1"/> </div> </section> </template> -<script lang="ts"> -import { defineComponent, defineAsyncComponent, PropType } from 'vue'; -import { SectionBlock } from '@/scripts/hpml/block'; -import { Hpml } from '@/scripts/hpml/evaluator'; +<script lang="ts" setup> +import { defineAsyncComponent } from 'vue'; +import * as Misskey from 'misskey-js'; +import { SectionBlock } from './block.type'; -export default defineComponent({ - components: { - XBlock: defineAsyncComponent(() => import('./page.block.vue')), - }, - props: { - block: { - type: Object as PropType<SectionBlock>, - required: true, - }, - hpml: { - type: Object as PropType<Hpml>, - required: true, - }, - h: { - required: true, - }, - }, -}); +const XBlock = defineAsyncComponent(() => import('./page.block.vue')); + +defineProps<{ + block: SectionBlock, + h: number, + page: Misskey.entities.Page, +}>(); </script> -<style lang="scss" scoped> -.sdgxphyu { - margin: 1.5em 0; +<style lang="scss" module> +.h2 { + font-size: 1.35em; + margin: 0 0 0.5em 0; +} - > h2 { - font-size: 1.35em; - margin: 0 0 0.5em 0; - } +.h3 { + font-size: 1em; + margin: 0 0 0.5em 0; +} - > h3 { - font-size: 1em; - margin: 0 0 0.5em 0; - } - - > h4 { - font-size: 1em; - margin: 0 0 0.5em 0; - } - - > .children { - //padding 16px - } +.h4 { + font-size: 1em; + margin: 0 0 0.5em 0; } </style> diff --git a/packages/frontend/src/components/page/page.switch.vue b/packages/frontend/src/components/page/page.switch.vue deleted file mode 100644 index b5f346451..000000000 --- a/packages/frontend/src/components/page/page.switch.vue +++ /dev/null @@ -1,54 +0,0 @@ -<template> -<div class="hkcxmtwj"> - <MkSwitch :model-value="value" @update:model-value="updateValue($event)">{{ hpml.interpolate(block.text) }}</MkSwitch> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent, PropType } from 'vue'; -import MkSwitch from '../MkSwitch.vue'; -import { Hpml } from '@/scripts/hpml/evaluator'; -import { SwitchVarBlock } from '@/scripts/hpml/block'; - -export default defineComponent({ - components: { - MkSwitch, - }, - props: { - block: { - type: Object as PropType<SwitchVarBlock>, - required: true, - }, - hpml: { - type: Object as PropType<Hpml>, - required: true, - }, - }, - setup(props, ctx) { - const value = computed(() => { - return props.hpml.vars.value[props.block.name]; - }); - - function updateValue(newValue: boolean) { - props.hpml.updatePageVar(props.block.name, newValue); - props.hpml.eval(); - } - - return { - value, - updateValue, - }; - }, -}); -</script> - -<style lang="scss" scoped> -.hkcxmtwj { - display: inline-block; - margin: 16px auto; - - & + .hkcxmtwj { - margin-left: 16px; - } -} -</style> diff --git a/packages/frontend/src/components/page/page.text-input.vue b/packages/frontend/src/components/page/page.text-input.vue deleted file mode 100644 index d020a99de..000000000 --- a/packages/frontend/src/components/page/page.text-input.vue +++ /dev/null @@ -1,54 +0,0 @@ -<template> -<div> - <MkInput class="kudkigyw" :model-value="value" type="text" @update:model-value="updateValue($event)"> - <template #label>{{ hpml.interpolate(block.text) }}</template> - </MkInput> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent, PropType } from 'vue'; -import MkInput from '../MkInput.vue'; -import { Hpml } from '@/scripts/hpml/evaluator'; -import { TextInputVarBlock } from '@/scripts/hpml/block'; - -export default defineComponent({ - components: { - MkInput, - }, - props: { - block: { - type: Object as PropType<TextInputVarBlock>, - required: true, - }, - hpml: { - type: Object as PropType<Hpml>, - required: true, - }, - }, - setup(props, ctx) { - const value = computed(() => { - return props.hpml.vars.value[props.block.name]; - }); - - function updateValue(newValue) { - props.hpml.updatePageVar(props.block.name, newValue); - props.hpml.eval(); - } - - return { - value, - updateValue, - }; - }, -}); -</script> - -<style lang="scss" scoped> -.kudkigyw { - display: inline-block; - min-width: 300px; - max-width: 450px; - margin: 8px 0; -} -</style> diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue index e0e4959ef..48ce4b0e1 100644 --- a/packages/frontend/src/components/page/page.text.vue +++ b/packages/frontend/src/components/page/page.text.vue @@ -1,70 +1,24 @@ <template> -<div class="mrdgzndn"> - <Mfm :key="text" :text="text" :is-note="false" :i="$i"/> - <MkUrlPreview v-for="url in urls" :key="url" :url="url" class="url"/> +<div class="_gaps"> + <Mfm :text="block.text" :isNote="false" :i="$i"/> + <MkUrlPreview v-for="url in urls" :key="url" :url="url"/> </div> </template> -<script lang="ts"> -import { defineAsyncComponent, defineComponent, PropType } from 'vue'; +<script lang="ts" setup> +import { defineAsyncComponent } from 'vue'; import * as mfm from 'mfm-js'; -import { TextBlock } from '@/scripts/hpml/block'; -import { Hpml } from '@/scripts/hpml/evaluator'; +import * as Misskey from 'misskey-js'; +import { TextBlock } from './block.type'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; import { $i } from '@/account'; -export default defineComponent({ - components: { - MkUrlPreview: defineAsyncComponent(() => import('@/components/MkUrlPreview.vue')), - }, - props: { - block: { - type: Object as PropType<TextBlock>, - required: true, - }, - hpml: { - type: Object as PropType<Hpml>, - required: true, - }, - }, - data() { - return { - text: this.hpml.interpolate(this.block.text), - $i, - }; - }, - computed: { - urls(): string[] { - if (this.text) { - return extractUrlFromMfm(mfm.parse(this.text)); - } else { - return []; - } - }, - }, - watch: { - 'hpml.vars': { - handler() { - this.text = this.hpml.interpolate(this.block.text); - }, - deep: true, - }, - }, -}); +const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue')); + +const props = defineProps<{ + block: TextBlock, + page: Misskey.entities.Page, +}>(); + +const urls = props.block.text ? extractUrlFromMfm(mfm.parse(props.block.text)) : []; </script> - -<style lang="scss" scoped> -.mrdgzndn { - &:not(:first-child) { - margin-top: 0.5em; - } - - &:not(:last-child) { - margin-bottom: 0.5em; - } - - > .url { - margin: 0.5em 0; - } -} -</style> diff --git a/packages/frontend/src/components/page/page.textarea-input.vue b/packages/frontend/src/components/page/page.textarea-input.vue deleted file mode 100644 index db3a96dd1..000000000 --- a/packages/frontend/src/components/page/page.textarea-input.vue +++ /dev/null @@ -1,45 +0,0 @@ -<template> -<div> - <MkTextarea :model-value="value" @update:model-value="updateValue($event)"> - <template #label>{{ hpml.interpolate(block.text) }}</template> - </MkTextarea> -</div> -</template> - -<script lang="ts"> -import { computed, defineComponent, PropType } from 'vue'; -import MkTextarea from '../MkTextarea.vue'; -import { Hpml } from '@/scripts/hpml/evaluator'; -import { TextInputVarBlock } from '@/scripts/hpml/block'; - -export default defineComponent({ - components: { - MkTextarea, - }, - props: { - block: { - type: Object as PropType<TextInputVarBlock>, - required: true, - }, - hpml: { - type: Object as PropType<Hpml>, - required: true, - }, - }, - setup(props, ctx) { - const value = computed(() => { - return props.hpml.vars.value[props.block.name]; - }); - - function updateValue(newValue) { - props.hpml.updatePageVar(props.block.name, newValue); - props.hpml.eval(); - } - - return { - value, - updateValue, - }; - }, -}); -</script> diff --git a/packages/frontend/src/components/page/page.textarea.vue b/packages/frontend/src/components/page/page.textarea.vue deleted file mode 100644 index 9b82412e8..000000000 --- a/packages/frontend/src/components/page/page.textarea.vue +++ /dev/null @@ -1,39 +0,0 @@ -<template> -<MkTextarea :model-value="text" readonly></MkTextarea> -</template> - -<script lang="ts"> -import { TextBlock } from '@/scripts/hpml/block'; -import { Hpml } from '@/scripts/hpml/evaluator'; -import { defineComponent, PropType } from 'vue'; -import MkTextarea from '../MkTextarea.vue'; - -export default defineComponent({ - components: { - MkTextarea, - }, - props: { - block: { - type: Object as PropType<TextBlock>, - required: true, - }, - hpml: { - type: Object as PropType<Hpml>, - required: true, - }, - }, - data() { - return { - text: this.hpml.interpolate(this.block.text), - }; - }, - watch: { - 'hpml.vars': { - handler() { - this.text = this.hpml.interpolate(this.block.text); - }, - deep: true, - }, - }, -}); -</script> diff --git a/packages/frontend/src/components/page/page.vue b/packages/frontend/src/components/page/page.vue index 5f1f62581..c2c269322 100644 --- a/packages/frontend/src/components/page/page.vue +++ b/packages/frontend/src/components/page/page.vue @@ -1,56 +1,25 @@ <template> -<div v-if="hpml" class="iroscrza" :class="{ center: page.alignCenter, serif: page.font === 'serif' }"> - <XBlock v-for="child in page.content" :key="child.id" :block="child" :hpml="hpml" :h="2"/> +<div :class="{ [$style.center]: page.alignCenter, [$style.serif]: page.font === 'serif' }"> + <XBlock v-for="child in page.content" :key="child.id" :page="page" :block="child" :h="2"/> </div> </template> -<script lang="ts"> -import { defineComponent, onMounted, nextTick, PropType } from 'vue'; +<script lang="ts" setup> +import { onMounted, nextTick } from 'vue'; +import * as Misskey from 'misskey-js'; import XBlock from './page.block.vue'; -import { Hpml } from '@/scripts/hpml/evaluator'; -import { url } from '@/config'; -import { $i } from '@/account'; -export default defineComponent({ - components: { - XBlock, - }, - props: { - page: { - type: Object as PropType<Record<string, any>>, - required: true, - }, - }, - setup(props, ctx) { - const hpml = new Hpml(props.page, { - randomSeed: Math.random(), - visitor: $i, - url: url, - }); - - onMounted(() => { - nextTick(() => { - hpml.eval(); - }); - }); - - return { - hpml, - }; - }, -}); +defineProps<{ + page: Misskey.entities.Page, +}>(); </script> -<style lang="scss" scoped> -.iroscrza { - &.serif { - > div { - font-family: serif; - } - } +<style lang="scss" module> +.serif { + font-family: serif; +} - &.center { - text-align: center; - } +.center { + text-align: center; } </style> diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts index a89a420d7..9b738b2fd 100644 --- a/packages/frontend/src/custom-emojis.ts +++ b/packages/frontend/src/custom-emojis.ts @@ -1,8 +1,7 @@ -import { shallowRef, computed, markRaw } from 'vue'; +import { shallowRef, computed, markRaw, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { api, apiGet } from './os'; -import { miLocalStorage } from './local-storage'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { get, set } from '@/scripts/idb-proxy'; const storageCache = await get('emojis'); @@ -17,6 +16,17 @@ export const customEmojiCategories = computed<[ ...string[], null ]>(() => { return markRaw([...Array.from(categories), null]); }); +export const customEmojisMap = new Map<string, Misskey.entities.CustomEmoji>(); +watch(customEmojis, emojis => { + customEmojisMap.clear(); + for (const emoji of emojis) { + customEmojisMap.set(emoji.name, emoji); + } +}, { immediate: true }); + +// TODO: ここら辺副作用なのでいい感じにする +const stream = useStream(); + stream.on('emojiAdded', emojiData => { customEmojis.value = [emojiData.emoji, ...customEmojis.value]; set('emojis', customEmojis.value); @@ -34,10 +44,9 @@ stream.on('emojiDeleted', emojiData => { export async function fetchCustomEmojis(force = false) { const now = Date.now(); - const needsMigration = miLocalStorage.getItem('emojis') != null; let res; - if (force || needsMigration) { + if (force) { res = await api('emojis', {}); } else { const lastFetchedAt = await get('lastEmojisFetchedAt'); @@ -48,10 +57,6 @@ export async function fetchCustomEmojis(force = false) { customEmojis.value = res.emojis; set('emojis', res.emojis); set('lastEmojisFetchedAt', now); - if (needsMigration) { - miLocalStorage.removeItem('emojis'); - miLocalStorage.removeItem('lastEmojisFetchedAt'); - } } let cachedTags; diff --git a/packages/frontend/src/directives/tooltip.ts b/packages/frontend/src/directives/tooltip.ts index 5d13497b5..373141fa3 100644 --- a/packages/frontend/src/directives/tooltip.ts +++ b/packages/frontend/src/directives/tooltip.ts @@ -5,7 +5,7 @@ import { defineAsyncComponent, Directive, ref } from 'vue'; import { isTouchUsing } from '@/scripts/touch'; import { popup, alert } from '@/os'; -const start = isTouchUsing ? 'touchstart' : 'mouseover'; +const start = isTouchUsing ? 'touchstart' : 'mouseenter'; const end = isTouchUsing ? 'touchend' : 'mouseleave'; export default { @@ -63,16 +63,24 @@ export default { ev.preventDefault(); }); - el.addEventListener(start, () => { + el.addEventListener(start, (ev) => { window.clearTimeout(self.showTimer); window.clearTimeout(self.hideTimer); - self.showTimer = window.setTimeout(self.show, delay); + if (delay === 0) { + self.show(); + } else { + self.showTimer = window.setTimeout(self.show, delay); + } }, { passive: true }); el.addEventListener(end, () => { window.clearTimeout(self.showTimer); window.clearTimeout(self.hideTimer); - self.hideTimer = window.setTimeout(self.close, delay); + if (delay === 0) { + self.close(); + } else { + self.hideTimer = window.setTimeout(self.close, delay); + } }, { passive: true }); el.addEventListener('click', () => { diff --git a/packages/frontend/src/emojilist.json b/packages/frontend/src/emojilist.json index 402e82e33..fde06a4aa 100644 --- a/packages/frontend/src/emojilist.json +++ b/packages/frontend/src/emojilist.json @@ -1,1785 +1,1784 @@ [ - { "category": "face", "char": "😀", "name": "grinning", "keywords": ["face", "smile", "happy", "joy", ": D", "grin"] }, - { "category": "face", "char": "😬", "name": "grimacing", "keywords": ["face", "grimace", "teeth"] }, - { "category": "face", "char": "😁", "name": "grin", "keywords": ["face", "happy", "smile", "joy", "kawaii"] }, - { "category": "face", "char": "😂", "name": "joy", "keywords": ["face", "cry", "tears", "weep", "happy", "happytears", "haha"] }, - { "category": "face", "char": "🤣", "name": "rofl", "keywords": ["face", "rolling", "floor", "laughing", "lol", "haha"] }, - { "category": "face", "char": "🥳", "name": "partying", "keywords": ["face", "celebration", "woohoo"] }, - { "category": "face", "char": "😃", "name": "smiley", "keywords": ["face", "happy", "joy", "haha", ": D", ": )", "smile", "funny"] }, - { "category": "face", "char": "😄", "name": "smile", "keywords": ["face", "happy", "joy", "funny", "haha", "laugh", "like", ": D", ": )"] }, - { "category": "face", "char": "😅", "name": "sweat_smile", "keywords": ["face", "hot", "happy", "laugh", "sweat", "smile", "relief"] }, - { "category": "face", "char": "🥲", "name": "smiling_face_with_tear", "keywords": ["face"] }, - { "category": "face", "char": "😆", "name": "laughing", "keywords": ["happy", "joy", "lol", "satisfied", "haha", "face", "glad", "XD", "laugh"] }, - { "category": "face", "char": "😇", "name": "innocent", "keywords": ["face", "angel", "heaven", "halo"] }, - { "category": "face", "char": "😉", "name": "wink", "keywords": ["face", "happy", "mischievous", "secret", ";)", "smile", "eye"] }, - { "category": "face", "char": "😊", "name": "blush", "keywords": ["face", "smile", "happy", "flushed", "crush", "embarrassed", "shy", "joy"] }, - { "category": "face", "char": "🙂", "name": "slightly_smiling_face", "keywords": ["face", "smile"] }, - { "category": "face", "char": "🙃", "name": "upside_down_face", "keywords": ["face", "flipped", "silly", "smile"] }, - { "category": "face", "char": "☺️", "name": "relaxed", "keywords": ["face", "blush", "massage", "happiness"] }, - { "category": "face", "char": "😋", "name": "yum", "keywords": ["happy", "joy", "tongue", "smile", "face", "silly", "yummy", "nom", "delicious", "savouring"] }, - { "category": "face", "char": "😌", "name": "relieved", "keywords": ["face", "relaxed", "phew", "massage", "happiness"] }, - { "category": "face", "char": "😍", "name": "heart_eyes", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "crush", "heart"] }, - { "category": "face", "char": "🥰", "name": "smiling_face_with_three_hearts", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "crush", "hearts", "adore"] }, - { "category": "face", "char": "😘", "name": "kissing_heart", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "kiss"] }, - { "category": "face", "char": "😗", "name": "kissing", "keywords": ["love", "like", "face", "3", "valentines", "infatuation", "kiss"] }, - { "category": "face", "char": "😙", "name": "kissing_smiling_eyes", "keywords": ["face", "affection", "valentines", "infatuation", "kiss"] }, - { "category": "face", "char": "😚", "name": "kissing_closed_eyes", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "kiss"] }, - { "category": "face", "char": "😜", "name": "stuck_out_tongue_winking_eye", "keywords": ["face", "prank", "childish", "playful", "mischievous", "smile", "wink", "tongue"] }, - { "category": "face", "char": "🤪", "name": "zany", "keywords": ["face", "goofy", "crazy"] }, - { "category": "face", "char": "🤨", "name": "raised_eyebrow", "keywords": ["face", "distrust", "scepticism", "disapproval", "disbelief", "surprise"] }, - { "category": "face", "char": "🧐", "name": "monocle", "keywords": ["face", "stuffy", "wealthy"] }, - { "category": "face", "char": "😝", "name": "stuck_out_tongue_closed_eyes", "keywords": ["face", "prank", "playful", "mischievous", "smile", "tongue"] }, - { "category": "face", "char": "😛", "name": "stuck_out_tongue", "keywords": ["face", "prank", "childish", "playful", "mischievous", "smile", "tongue"] }, - { "category": "face", "char": "🤑", "name": "money_mouth_face", "keywords": ["face", "rich", "dollar", "money"] }, - { "category": "face", "char": "🤓", "name": "nerd_face", "keywords": ["face", "nerdy", "geek", "dork"] }, - { "category": "face", "char": "🥸", "name": "disguised_face", "keywords": ["face", "nose", "glasses", "incognito"] }, - { "category": "face", "char": "😎", "name": "sunglasses", "keywords": ["face", "cool", "smile", "summer", "beach", "sunglass"] }, - { "category": "face", "char": "🤩", "name": "star_struck", "keywords": ["face", "smile", "starry", "eyes", "grinning"] }, - { "category": "face", "char": "🤡", "name": "clown_face", "keywords": ["face"] }, - { "category": "face", "char": "🤠", "name": "cowboy_hat_face", "keywords": ["face", "cowgirl", "hat"] }, - { "category": "face", "char": "🤗", "name": "hugs", "keywords": ["face", "smile", "hug"] }, - { "category": "face", "char": "😏", "name": "smirk", "keywords": ["face", "smile", "mean", "prank", "smug", "sarcasm"] }, - { "category": "face", "char": "😶", "name": "no_mouth", "keywords": ["face", "hellokitty"] }, - { "category": "face", "char": "😐", "name": "neutral_face", "keywords": ["indifference", "meh", ": |", "neutral"] }, - { "category": "face", "char": "😑", "name": "expressionless", "keywords": ["face", "indifferent", "-_-", "meh", "deadpan"] }, - { "category": "face", "char": "😒", "name": "unamused", "keywords": ["indifference", "bored", "straight face", "serious", "sarcasm", "unimpressed", "skeptical", "dubious", "side_eye"] }, - { "category": "face", "char": "🙄", "name": "roll_eyes", "keywords": ["face", "eyeroll", "frustrated"] }, - { "category": "face", "char": "🤔", "name": "thinking", "keywords": ["face", "hmmm", "think", "consider"] }, - { "category": "face", "char": "🤥", "name": "lying_face", "keywords": ["face", "lie", "pinocchio"] }, - { "category": "face", "char": "🤭", "name": "hand_over_mouth", "keywords": ["face", "whoops", "shock", "surprise"] }, - { "category": "face", "char": "🤫", "name": "shushing", "keywords": ["face", "quiet", "shhh"] }, - { "category": "face", "char": "🤬", "name": "symbols_over_mouth", "keywords": ["face", "swearing", "cursing", "cussing", "profanity", "expletive"] }, - { "category": "face", "char": "🤯", "name": "exploding_head", "keywords": ["face", "shocked", "mind", "blown"] }, - { "category": "face", "char": "😳", "name": "flushed", "keywords": ["face", "blush", "shy", "flattered"] }, - { "category": "face", "char": "😞", "name": "disappointed", "keywords": ["face", "sad", "upset", "depressed", ": ("] }, - { "category": "face", "char": "😟", "name": "worried", "keywords": ["face", "concern", "nervous", ": ("] }, - { "category": "face", "char": "😠", "name": "angry", "keywords": ["mad", "face", "annoyed", "frustrated"] }, - { "category": "face", "char": "😡", "name": "rage", "keywords": ["angry", "mad", "hate", "despise"] }, - { "category": "face", "char": "😔", "name": "pensive", "keywords": ["face", "sad", "depressed", "upset"] }, - { "category": "face", "char": "😕", "name": "confused", "keywords": ["face", "indifference", "huh", "weird", "hmmm", ": /"] }, - { "category": "face", "char": "🙁", "name": "slightly_frowning_face", "keywords": ["face", "frowning", "disappointed", "sad", "upset"] }, - { "category": "face", "char": "☹", "name": "frowning_face", "keywords": ["face", "sad", "upset", "frown"] }, - { "category": "face", "char": "😣", "name": "persevere", "keywords": ["face", "sick", "no", "upset", "oops"] }, - { "category": "face", "char": "😖", "name": "confounded", "keywords": ["face", "confused", "sick", "unwell", "oops", ": S"] }, - { "category": "face", "char": "😫", "name": "tired_face", "keywords": ["sick", "whine", "upset", "frustrated"] }, - { "category": "face", "char": "😩", "name": "weary", "keywords": ["face", "tired", "sleepy", "sad", "frustrated", "upset"] }, - { "category": "face", "char": "🥺", "name": "pleading", "keywords": ["face", "begging", "mercy"] }, - { "category": "face", "char": "😤", "name": "triumph", "keywords": ["face", "gas", "phew", "proud", "pride"] }, - { "category": "face", "char": "😮", "name": "open_mouth", "keywords": ["face", "surprise", "impressed", "wow", "whoa", ": O"] }, - { "category": "face", "char": "😱", "name": "scream", "keywords": ["face", "munch", "scared", "omg"] }, - { "category": "face", "char": "😨", "name": "fearful", "keywords": ["face", "scared", "terrified", "nervous", "oops", "huh"] }, - { "category": "face", "char": "😰", "name": "cold_sweat", "keywords": ["face", "nervous", "sweat"] }, - { "category": "face", "char": "😯", "name": "hushed", "keywords": ["face", "woo", "shh"] }, - { "category": "face", "char": "😦", "name": "frowning", "keywords": ["face", "aw", "what"] }, - { "category": "face", "char": "😧", "name": "anguished", "keywords": ["face", "stunned", "nervous"] }, - { "category": "face", "char": "😢", "name": "cry", "keywords": ["face", "tears", "sad", "depressed", "upset", ": '("] }, - { "category": "face", "char": "😥", "name": "disappointed_relieved", "keywords": ["face", "phew", "sweat", "nervous"] }, - { "category": "face", "char": "🤤", "name": "drooling_face", "keywords": ["face"] }, - { "category": "face", "char": "😪", "name": "sleepy", "keywords": ["face", "tired", "rest", "nap"] }, - { "category": "face", "char": "😓", "name": "sweat", "keywords": ["face", "hot", "sad", "tired", "exercise"] }, - { "category": "face", "char": "🥵", "name": "hot", "keywords": ["face", "feverish", "heat", "red", "sweating"] }, - { "category": "face", "char": "🥶", "name": "cold", "keywords": ["face", "blue", "freezing", "frozen", "frostbite", "icicles"] }, - { "category": "face", "char": "😭", "name": "sob", "keywords": ["face", "cry", "tears", "sad", "upset", "depressed"] }, - { "category": "face", "char": "😵", "name": "dizzy_face", "keywords": ["spent", "unconscious", "xox", "dizzy"] }, - { "category": "face", "char": "😲", "name": "astonished", "keywords": ["face", "xox", "surprised", "poisoned"] }, - { "category": "face", "char": "🤐", "name": "zipper_mouth_face", "keywords": ["face", "sealed", "zipper", "secret"] }, - { "category": "face", "char": "🤢", "name": "nauseated_face", "keywords": ["face", "vomit", "gross", "green", "sick", "throw up", "ill"] }, - { "category": "face", "char": "🤧", "name": "sneezing_face", "keywords": ["face", "gesundheit", "sneeze", "sick", "allergy"] }, - { "category": "face", "char": "🤮", "name": "vomiting", "keywords": ["face", "sick"] }, - { "category": "face", "char": "😷", "name": "mask", "keywords": ["face", "sick", "ill", "disease"] }, - { "category": "face", "char": "🤒", "name": "face_with_thermometer", "keywords": ["sick", "temperature", "thermometer", "cold", "fever"] }, - { "category": "face", "char": "🤕", "name": "face_with_head_bandage", "keywords": ["injured", "clumsy", "bandage", "hurt"] }, - { "category": "face", "char": "🥴", "name": "woozy", "keywords": ["face", "dizzy", "intoxicated", "tipsy", "wavy"] }, - { "category": "face", "char": "🥱", "name": "yawning", "keywords": ["face", "tired", "yawning"] }, - { "category": "face", "char": "😴", "name": "sleeping", "keywords": ["face", "tired", "sleepy", "night", "zzz"] }, - { "category": "face", "char": "💤", "name": "zzz", "keywords": ["sleepy", "tired", "dream"] }, - { "category": "face", "char": "\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F", "name": "face_in_clouds", "keywords": [] }, - { "category": "face", "char": "\uD83D\uDE2E\u200D\uD83D\uDCA8", "name": "face_exhaling", "keywords": [] }, - { "category": "face", "char": "\uD83D\uDE35\u200D\uD83D\uDCAB", "name": "face_with_spiral_eyes", "keywords": [] }, - { "category": "face", "char": "\uD83E\uDEE0", "name": "melting_face", "keywords": ["disappear", "dissolve", "liquid", "melt", "toketa"] }, - { "category": "face", "char": "\uD83E\uDEE2", "name": "face_with_open_eyes_and_hand_over_mouth", "keywords": ["amazement", "awe", "disbelief", "embarrass", "scared", "surprise", "ohoho"] }, - { "category": "face", "char": "\uD83E\uDEE3", "name": "face_with_peeking_eye", "keywords": ["captivated", "peep", "stare", "chunibyo"] }, - { "category": "face", "char": "\uD83E\uDEE1", "name": "saluting_face", "keywords": ["ok", "salute", "sunny", "troops", "yes", "raja"] }, - { "category": "face", "char": "\uD83E\uDEE5", "name": "dotted_line_face", "keywords": ["depressed", "disappear", "hide", "introvert", "invisible", "tensen"] }, - { "category": "face", "char": "\uD83E\uDEE4", "name": "face_with_diagonal_mouth", "keywords": ["disappointed", "meh", "skeptical", "unsure"] }, - { "category": "face", "char": "\uD83E\uDD79", "name": "face_holding_back_tears", "keywords": ["angry", "cry", "proud", "resist", "sad"] }, - { "category": "face", "char": "💩", "name": "poop", "keywords": ["hankey", "shitface", "fail", "turd", "shit"] }, - { "category": "face", "char": "😈", "name": "smiling_imp", "keywords": ["devil", "horns"] }, - { "category": "face", "char": "👿", "name": "imp", "keywords": ["devil", "angry", "horns"] }, - { "category": "face", "char": "👹", "name": "japanese_ogre", "keywords": ["monster", "red", "mask", "halloween", "scary", "creepy", "devil", "demon", "japanese", "ogre"] }, - { "category": "face", "char": "👺", "name": "japanese_goblin", "keywords": ["red", "evil", "mask", "monster", "scary", "creepy", "japanese", "goblin"] }, - { "category": "face", "char": "💀", "name": "skull", "keywords": ["dead", "skeleton", "creepy", "death"] }, - { "category": "face", "char": "👻", "name": "ghost", "keywords": ["halloween", "spooky", "scary"] }, - { "category": "face", "char": "👽", "name": "alien", "keywords": ["UFO", "paul", "weird", "outer_space"] }, - { "category": "face", "char": "🤖", "name": "robot", "keywords": ["computer", "machine", "bot"] }, - { "category": "face", "char": "😺", "name": "smiley_cat", "keywords": ["animal", "cats", "happy", "smile"] }, - { "category": "face", "char": "😸", "name": "smile_cat", "keywords": ["animal", "cats", "smile"] }, - { "category": "face", "char": "😹", "name": "joy_cat", "keywords": ["animal", "cats", "haha", "happy", "tears"] }, - { "category": "face", "char": "😻", "name": "heart_eyes_cat", "keywords": ["animal", "love", "like", "affection", "cats", "valentines", "heart"] }, - { "category": "face", "char": "😼", "name": "smirk_cat", "keywords": ["animal", "cats", "smirk"] }, - { "category": "face", "char": "😽", "name": "kissing_cat", "keywords": ["animal", "cats", "kiss"] }, - { "category": "face", "char": "🙀", "name": "scream_cat", "keywords": ["animal", "cats", "munch", "scared", "scream"] }, - { "category": "face", "char": "😿", "name": "crying_cat_face", "keywords": ["animal", "tears", "weep", "sad", "cats", "upset", "cry"] }, - { "category": "face", "char": "😾", "name": "pouting_cat", "keywords": ["animal", "cats"] }, - { "category": "people", "char": "🤲", "name": "palms_up", "keywords": ["hands", "gesture", "cupped", "prayer"] }, - { "category": "people", "char": "🙌", "name": "raised_hands", "keywords": ["gesture", "hooray", "yea", "celebration", "hands"] }, - { "category": "people", "char": "👏", "name": "clap", "keywords": ["hands", "praise", "applause", "congrats", "yay"] }, - { "category": "people", "char": "👋", "name": "wave", "keywords": ["hands", "gesture", "goodbye", "solong", "farewell", "hello", "hi", "palm"] }, - { "category": "people", "char": "🤙", "name": "call_me_hand", "keywords": ["hands", "gesture"] }, - { "category": "people", "char": "👍", "name": "+1", "keywords": ["thumbsup", "yes", "awesome", "good", "agree", "accept", "cool", "hand", "like"] }, - { "category": "people", "char": "👎", "name": "-1", "keywords": ["thumbsdown", "no", "dislike", "hand"] }, - { "category": "people", "char": "👊", "name": "facepunch", "keywords": ["angry", "violence", "fist", "hit", "attack", "hand"] }, - { "category": "people", "char": "✊", "name": "fist", "keywords": ["fingers", "hand", "grasp"] }, - { "category": "people", "char": "🤛", "name": "fist_left", "keywords": ["hand", "fistbump"] }, - { "category": "people", "char": "🤜", "name": "fist_right", "keywords": ["hand", "fistbump"] }, - { "category": "people", "char": "✌", "name": "v", "keywords": ["fingers", "ohyeah", "hand", "peace", "victory", "two"] }, - { "category": "people", "char": "👌", "name": "ok_hand", "keywords": ["fingers", "limbs", "perfect", "ok", "okay"] }, - { "category": "people", "char": "✋", "name": "raised_hand", "keywords": ["fingers", "stop", "highfive", "palm", "ban"] }, - { "category": "people", "char": "🤚", "name": "raised_back_of_hand", "keywords": ["fingers", "raised", "backhand"] }, - { "category": "people", "char": "👐", "name": "open_hands", "keywords": ["fingers", "butterfly", "hands", "open"] }, - { "category": "people", "char": "💪", "name": "muscle", "keywords": ["arm", "flex", "hand", "summer", "strong", "biceps"] }, - { "category": "people", "char": "🦾", "name": "mechanical_arm", "keywords": ["flex", "hand", "strong", "biceps"] }, - { "category": "people", "char": "🙏", "name": "pray", "keywords": ["please", "hope", "wish", "namaste", "highfive"] }, - { "category": "people", "char": "🦶", "name": "foot", "keywords": ["kick", "stomp"] }, - { "category": "people", "char": "🦵", "name": "leg", "keywords": ["kick", "limb"] }, - { "category": "people", "char": "🦿", "name": "mechanical_leg", "keywords": ["kick", "limb"] }, - { "category": "people", "char": "🤝", "name": "handshake", "keywords": ["agreement", "shake"] }, - { "category": "people", "char": "☝", "name": "point_up", "keywords": ["hand", "fingers", "direction", "up"] }, - { "category": "people", "char": "👆", "name": "point_up_2", "keywords": ["fingers", "hand", "direction", "up"] }, - { "category": "people", "char": "👇", "name": "point_down", "keywords": ["fingers", "hand", "direction", "down"] }, - { "category": "people", "char": "👈", "name": "point_left", "keywords": ["direction", "fingers", "hand", "left"] }, - { "category": "people", "char": "👉", "name": "point_right", "keywords": ["fingers", "hand", "direction", "right"] }, - { "category": "people", "char": "🖕", "name": "fu", "keywords": ["hand", "fingers", "rude", "middle", "flipping"] }, - { "category": "people", "char": "🖐", "name": "raised_hand_with_fingers_splayed", "keywords": ["hand", "fingers", "palm"] }, - { "category": "people", "char": "🤟", "name": "love_you", "keywords": ["hand", "fingers", "gesture"] }, - { "category": "people", "char": "🤘", "name": "metal", "keywords": ["hand", "fingers", "evil_eye", "sign_of_horns", "rock_on"] }, - { "category": "people", "char": "🤞", "name": "crossed_fingers", "keywords": ["good", "lucky"] }, - { "category": "people", "char": "🖖", "name": "vulcan_salute", "keywords": ["hand", "fingers", "spock", "star trek"] }, - { "category": "people", "char": "✍", "name": "writing_hand", "keywords": ["lower_left_ballpoint_pen", "stationery", "write", "compose"] }, - { "category": "people", "char": "\uD83E\uDEF0", "name": "hand_with_index_finger_and_thumb_crossed", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDEF1", "name": "rightwards_hand", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDEF2", "name": "leftwards_hand", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDEF3", "name": "palm_down_hand", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDEF4", "name": "palm_up_hand", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDEF5", "name": "index_pointing_at_the_viewer", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDEF6", "name": "heart_hands", "keywords": ["moemoekyun"] }, - { "category": "people", "char": "🤏", "name": "pinching_hand", "keywords": ["hand", "fingers"] }, - { "category": "people", "char": "🤌", "name": "pinched_fingers", "keywords": ["hand", "fingers"] }, - { "category": "people", "char": "🤳", "name": "selfie", "keywords": ["camera", "phone"] }, - { "category": "people", "char": "💅", "name": "nail_care", "keywords": ["beauty", "manicure", "finger", "fashion", "nail"] }, - { "category": "people", "char": "👄", "name": "lips", "keywords": ["mouth", "kiss"] }, - { "category": "people", "char": "\uD83E\uDEE6", "name": "biting_lip", "keywords": [] }, - { "category": "people", "char": "🦷", "name": "tooth", "keywords": ["teeth", "dentist"] }, - { "category": "people", "char": "👅", "name": "tongue", "keywords": ["mouth", "playful"] }, - { "category": "people", "char": "👂", "name": "ear", "keywords": ["face", "hear", "sound", "listen"] }, - { "category": "people", "char": "🦻", "name": "ear_with_hearing_aid", "keywords": ["face", "hear", "sound", "listen"] }, - { "category": "people", "char": "👃", "name": "nose", "keywords": ["smell", "sniff"] }, - { "category": "people", "char": "👁", "name": "eye", "keywords": ["face", "look", "see", "watch", "stare"] }, - { "category": "people", "char": "👀", "name": "eyes", "keywords": ["look", "watch", "stalk", "peek", "see"] }, - { "category": "people", "char": "🧠", "name": "brain", "keywords": ["smart", "intelligent"] }, - { "category": "people", "char": "🫀", "name": "anatomical_heart", "keywords": [] }, - { "category": "people", "char": "🫁", "name": "lungs", "keywords": [] }, - { "category": "people", "char": "👤", "name": "bust_in_silhouette", "keywords": ["user", "person", "human"] }, - { "category": "people", "char": "👥", "name": "busts_in_silhouette", "keywords": ["user", "person", "human", "group", "team"] }, - { "category": "people", "char": "🗣", "name": "speaking_head", "keywords": ["user", "person", "human", "sing", "say", "talk"] }, - { "category": "people", "char": "👶", "name": "baby", "keywords": ["child", "boy", "girl", "toddler"] }, - { "category": "people", "char": "🧒", "name": "child", "keywords": ["gender-neutral", "young"] }, - { "category": "people", "char": "👦", "name": "boy", "keywords": ["man", "male", "guy", "teenager"] }, - { "category": "people", "char": "👧", "name": "girl", "keywords": ["female", "woman", "teenager"] }, - { "category": "people", "char": "🧑", "name": "adult", "keywords": ["gender-neutral", "person"] }, - { "category": "people", "char": "👨", "name": "man", "keywords": ["mustache", "father", "dad", "guy", "classy", "sir", "moustache"] }, - { "category": "people", "char": "👩", "name": "woman", "keywords": ["female", "girls", "lady"] }, - { "category": "people", "char": "🧑🦱", "name": "curly_hair", "keywords": ["curly", "afro", "braids", "ringlets"] }, - { "category": "people", "char": "👩🦱", "name": "curly_hair_woman", "keywords": ["woman", "female", "girl", "curly", "afro", "braids", "ringlets"] }, - { "category": "people", "char": "👨🦱", "name": "curly_hair_man", "keywords": ["man", "male", "boy", "guy", "curly", "afro", "braids", "ringlets"] }, - { "category": "people", "char": "🧑🦰", "name": "red_hair", "keywords": ["redhead"] }, - { "category": "people", "char": "👩🦰", "name": "red_hair_woman", "keywords": ["woman", "female", "girl", "ginger", "redhead"] }, - { "category": "people", "char": "👨🦰", "name": "red_hair_man", "keywords": ["man", "male", "boy", "guy", "ginger", "redhead"] }, - { "category": "people", "char": "👱♀️", "name": "blonde_woman", "keywords": ["woman", "female", "girl", "blonde", "person"] }, - { "category": "people", "char": "👱", "name": "blonde_man", "keywords": ["man", "male", "boy", "blonde", "guy", "person"] }, - { "category": "people", "char": "🧑🦳", "name": "white_hair", "keywords": ["gray", "old", "white"] }, - { "category": "people", "char": "👩🦳", "name": "white_hair_woman", "keywords": ["woman", "female", "girl", "gray", "old", "white"] }, - { "category": "people", "char": "👨🦳", "name": "white_hair_man", "keywords": ["man", "male", "boy", "guy", "gray", "old", "white"] }, - { "category": "people", "char": "🧑🦲", "name": "bald", "keywords": ["bald", "chemotherapy", "hairless", "shaven"] }, - { "category": "people", "char": "👩🦲", "name": "bald_woman", "keywords": ["woman", "female", "girl", "bald", "chemotherapy", "hairless", "shaven"] }, - { "category": "people", "char": "👨🦲", "name": "bald_man", "keywords": ["man", "male", "boy", "guy", "bald", "chemotherapy", "hairless", "shaven"] }, - { "category": "people", "char": "🧔", "name": "bearded_person", "keywords": ["person", "bewhiskered"] }, - { "category": "people", "char": "🧓", "name": "older_adult", "keywords": ["human", "elder", "senior", "gender-neutral"] }, - { "category": "people", "char": "👴", "name": "older_man", "keywords": ["human", "male", "men", "old", "elder", "senior"] }, - { "category": "people", "char": "👵", "name": "older_woman", "keywords": ["human", "female", "women", "lady", "old", "elder", "senior"] }, - { "category": "people", "char": "👲", "name": "man_with_gua_pi_mao", "keywords": ["male", "boy", "chinese"] }, - { "category": "people", "char": "🧕", "name": "woman_with_headscarf", "keywords": ["female", "hijab", "mantilla", "tichel"] }, - { "category": "people", "char": "👳♀️", "name": "woman_with_turban", "keywords": ["female", "indian", "hinduism", "arabs", "woman"] }, - { "category": "people", "char": "👳", "name": "man_with_turban", "keywords": ["male", "indian", "hinduism", "arabs"] }, - { "category": "people", "char": "👮♀️", "name": "policewoman", "keywords": ["woman", "police", "law", "legal", "enforcement", "arrest", "911", "female"] }, - { "category": "people", "char": "👮", "name": "policeman", "keywords": ["man", "police", "law", "legal", "enforcement", "arrest", "911"] }, - { "category": "people", "char": "👷♀️", "name": "construction_worker_woman", "keywords": ["female", "human", "wip", "build", "construction", "worker", "labor", "woman"] }, - { "category": "people", "char": "👷", "name": "construction_worker_man", "keywords": ["male", "human", "wip", "guy", "build", "construction", "worker", "labor"] }, - { "category": "people", "char": "💂♀️", "name": "guardswoman", "keywords": ["uk", "gb", "british", "female", "royal", "woman"] }, - { "category": "people", "char": "💂", "name": "guardsman", "keywords": ["uk", "gb", "british", "male", "guy", "royal"] }, - { "category": "people", "char": "🕵️♀️", "name": "female_detective", "keywords": ["human", "spy", "detective", "female", "woman"] }, - { "category": "people", "char": "🕵", "name": "male_detective", "keywords": ["human", "spy", "detective"] }, - { "category": "people", "char": "🧑⚕️", "name": "health_worker", "keywords": ["doctor", "nurse", "therapist", "healthcare", "human"] }, - { "category": "people", "char": "👩⚕️", "name": "woman_health_worker", "keywords": ["doctor", "nurse", "therapist", "healthcare", "woman", "human"] }, - { "category": "people", "char": "👨⚕️", "name": "man_health_worker", "keywords": ["doctor", "nurse", "therapist", "healthcare", "man", "human"] }, - { "category": "people", "char": "🧑🌾", "name": "farmer", "keywords": ["rancher", "gardener", "human"] }, - { "category": "people", "char": "👩🌾", "name": "woman_farmer", "keywords": ["rancher", "gardener", "woman", "human"] }, - { "category": "people", "char": "👨🌾", "name": "man_farmer", "keywords": ["rancher", "gardener", "man", "human"] }, - { "category": "people", "char": "🧑🍳", "name": "cook", "keywords": ["chef", "human"] }, - { "category": "people", "char": "👩🍳", "name": "woman_cook", "keywords": ["chef", "woman", "human"] }, - { "category": "people", "char": "👨🍳", "name": "man_cook", "keywords": ["chef", "man", "human"] }, - { "category": "people", "char": "🧑🎓", "name": "student", "keywords": ["graduate", "human"] }, - { "category": "people", "char": "👩🎓", "name": "woman_student", "keywords": ["graduate", "woman", "human"] }, - { "category": "people", "char": "👨🎓", "name": "man_student", "keywords": ["graduate", "man", "human"] }, - { "category": "people", "char": "🧑🎤", "name": "singer", "keywords": ["rockstar", "entertainer", "human"] }, - { "category": "people", "char": "👩🎤", "name": "woman_singer", "keywords": ["rockstar", "entertainer", "woman", "human"] }, - { "category": "people", "char": "👨🎤", "name": "man_singer", "keywords": ["rockstar", "entertainer", "man", "human"] }, - { "category": "people", "char": "🧑🏫", "name": "teacher", "keywords": ["instructor", "professor", "human"] }, - { "category": "people", "char": "👩🏫", "name": "woman_teacher", "keywords": ["instructor", "professor", "woman", "human"] }, - { "category": "people", "char": "👨🏫", "name": "man_teacher", "keywords": ["instructor", "professor", "man", "human"] }, - { "category": "people", "char": "🧑🏭", "name": "factory_worker", "keywords": ["assembly", "industrial", "human"] }, - { "category": "people", "char": "👩🏭", "name": "woman_factory_worker", "keywords": ["assembly", "industrial", "woman", "human"] }, - { "category": "people", "char": "👨🏭", "name": "man_factory_worker", "keywords": ["assembly", "industrial", "man", "human"] }, - { "category": "people", "char": "🧑💻", "name": "technologist", "keywords": ["coder", "developer", "engineer", "programmer", "software", "human", "laptop", "computer"] }, - { "category": "people", "char": "👩💻", "name": "woman_technologist", "keywords": ["coder", "developer", "engineer", "programmer", "software", "woman", "human", "laptop", "computer"] }, - { "category": "people", "char": "👨💻", "name": "man_technologist", "keywords": ["coder", "developer", "engineer", "programmer", "software", "man", "human", "laptop", "computer"] }, - { "category": "people", "char": "🧑💼", "name": "office_worker", "keywords": ["business", "manager", "human"] }, - { "category": "people", "char": "👩💼", "name": "woman_office_worker", "keywords": ["business", "manager", "woman", "human"] }, - { "category": "people", "char": "👨💼", "name": "man_office_worker", "keywords": ["business", "manager", "man", "human"] }, - { "category": "people", "char": "🧑🔧", "name": "mechanic", "keywords": ["plumber", "human", "wrench"] }, - { "category": "people", "char": "👩🔧", "name": "woman_mechanic", "keywords": ["plumber", "woman", "human", "wrench"] }, - { "category": "people", "char": "👨🔧", "name": "man_mechanic", "keywords": ["plumber", "man", "human", "wrench"] }, - { "category": "people", "char": "🧑🔬", "name": "scientist", "keywords": ["biologist", "chemist", "engineer", "physicist", "human"] }, - { "category": "people", "char": "👩🔬", "name": "woman_scientist", "keywords": ["biologist", "chemist", "engineer", "physicist", "woman", "human"] }, - { "category": "people", "char": "👨🔬", "name": "man_scientist", "keywords": ["biologist", "chemist", "engineer", "physicist", "man", "human"] }, - { "category": "people", "char": "🧑🎨", "name": "artist", "keywords": ["painter", "human"] }, - { "category": "people", "char": "👩🎨", "name": "woman_artist", "keywords": ["painter", "woman", "human"] }, - { "category": "people", "char": "👨🎨", "name": "man_artist", "keywords": ["painter", "man", "human"] }, - { "category": "people", "char": "🧑🚒", "name": "firefighter", "keywords": ["fireman", "human"] }, - { "category": "people", "char": "👩🚒", "name": "woman_firefighter", "keywords": ["fireman", "woman", "human"] }, - { "category": "people", "char": "👨🚒", "name": "man_firefighter", "keywords": ["fireman", "man", "human"] }, - { "category": "people", "char": "🧑✈️", "name": "pilot", "keywords": ["aviator", "plane", "human"] }, - { "category": "people", "char": "👩✈️", "name": "woman_pilot", "keywords": ["aviator", "plane", "woman", "human"] }, - { "category": "people", "char": "👨✈️", "name": "man_pilot", "keywords": ["aviator", "plane", "man", "human"] }, - { "category": "people", "char": "🧑🚀", "name": "astronaut", "keywords": ["space", "rocket", "human"] }, - { "category": "people", "char": "👩🚀", "name": "woman_astronaut", "keywords": ["space", "rocket", "woman", "human"] }, - { "category": "people", "char": "👨🚀", "name": "man_astronaut", "keywords": ["space", "rocket", "man", "human"] }, - { "category": "people", "char": "🧑⚖️", "name": "judge", "keywords": ["justice", "court", "human"] }, - { "category": "people", "char": "👩⚖️", "name": "woman_judge", "keywords": ["justice", "court", "woman", "human"] }, - { "category": "people", "char": "👨⚖️", "name": "man_judge", "keywords": ["justice", "court", "man", "human"] }, - { "category": "people", "char": "🦸♀️", "name": "woman_superhero", "keywords": ["woman", "female", "good", "heroine", "superpowers"] }, - { "category": "people", "char": "🦸♂️", "name": "man_superhero", "keywords": ["man", "male", "good", "hero", "superpowers"] }, - { "category": "people", "char": "🦹♀️", "name": "woman_supervillain", "keywords": ["woman", "female", "evil", "bad", "criminal", "heroine", "superpowers"] }, - { "category": "people", "char": "🦹♂️", "name": "man_supervillain", "keywords": ["man", "male", "evil", "bad", "criminal", "hero", "superpowers"] }, - { "category": "people", "char": "🤶", "name": "mrs_claus", "keywords": ["woman", "female", "xmas", "mother christmas"] }, - { "category": "people", "char": "\uD83E\uDDD1\u200D\uD83C\uDF84", "name": "mx_claus", "keywords": ["xmas", "christmas"] }, - { "category": "people", "char": "🎅", "name": "santa", "keywords": ["festival", "man", "male", "xmas", "father christmas"] }, - { "category": "people", "char": "🥷", "name": "ninja", "keywords": [] }, - { "category": "people", "char": "🧙♀️", "name": "sorceress", "keywords": ["woman", "female", "mage", "witch"] }, - { "category": "people", "char": "🧙♂️", "name": "wizard", "keywords": ["man", "male", "mage", "sorcerer"] }, - { "category": "people", "char": "🧝♀️", "name": "woman_elf", "keywords": ["woman", "female"] }, - { "category": "people", "char": "🧝♂️", "name": "man_elf", "keywords": ["man", "male"] }, - { "category": "people", "char": "🧛♀️", "name": "woman_vampire", "keywords": ["woman", "female"] }, - { "category": "people", "char": "🧛♂️", "name": "man_vampire", "keywords": ["man", "male", "dracula"] }, - { "category": "people", "char": "🧟♀️", "name": "woman_zombie", "keywords": ["woman", "female", "undead", "walking dead"] }, - { "category": "people", "char": "🧟♂️", "name": "man_zombie", "keywords": ["man", "male", "dracula", "undead", "walking dead"] }, - { "category": "people", "char": "🧞♀️", "name": "woman_genie", "keywords": ["woman", "female"] }, - { "category": "people", "char": "🧞♂️", "name": "man_genie", "keywords": ["man", "male"] }, - { "category": "people", "char": "🧜♀️", "name": "mermaid", "keywords": ["woman", "female", "merwoman", "ariel"] }, - { "category": "people", "char": "🧜♂️", "name": "merman", "keywords": ["man", "male", "triton"] }, - { "category": "people", "char": "🧚♀️", "name": "woman_fairy", "keywords": ["woman", "female"] }, - { "category": "people", "char": "🧚♂️", "name": "man_fairy", "keywords": ["man", "male"] }, - { "category": "people", "char": "👼", "name": "angel", "keywords": ["heaven", "wings", "halo"] }, - { "category": "people", "char": "\uD83E\uDDCC", "name": "troll", "keywords": [] }, - { "category": "people", "char": "🤰", "name": "pregnant_woman", "keywords": ["baby"] }, - { "category": "people", "char": "\uD83E\uDEC3", "name": "pregnant_man", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDEC4", "name": "pregnant_person", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDEC5", "name": "person_with_crown", "keywords": [] }, - { "category": "people", "char": "🤱", "name": "breastfeeding", "keywords": ["nursing", "baby"] }, - { "category": "people", "char": "\uD83D\uDC69\u200D\uD83C\uDF7C", "name": "woman_feeding_baby", "keywords": [] }, - { "category": "people", "char": "\uD83D\uDC68\u200D\uD83C\uDF7C", "name": "man_feeding_baby", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDDD1\u200D\uD83C\uDF7C", "name": "person_feeding_baby", "keywords": [] }, - { "category": "people", "char": "👸", "name": "princess", "keywords": ["girl", "woman", "female", "blond", "crown", "royal", "queen"] }, - { "category": "people", "char": "🤴", "name": "prince", "keywords": ["boy", "man", "male", "crown", "royal", "king"] }, - { "category": "people", "char": "👰", "name": "person_with_veil", "keywords": ["couple", "marriage", "wedding", "woman", "bride"] }, - { "category": "people", "char": "👰", "name": "bride_with_veil", "keywords": ["couple", "marriage", "wedding", "woman", "bride"] }, - { "category": "people", "char": "🤵", "name": "person_in_tuxedo", "keywords": ["couple", "marriage", "wedding", "groom"] }, - { "category": "people", "char": "🤵", "name": "man_in_tuxedo", "keywords": ["couple", "marriage", "wedding", "groom"] }, - { "category": "people", "char": "🏃♀️", "name": "running_woman", "keywords": ["woman", "walking", "exercise", "race", "running", "female"] }, - { "category": "people", "char": "🏃", "name": "running_man", "keywords": ["man", "walking", "exercise", "race", "running"] }, - { "category": "people", "char": "🚶♀️", "name": "walking_woman", "keywords": ["human", "feet", "steps", "woman", "female"] }, - { "category": "people", "char": "🚶", "name": "walking_man", "keywords": ["human", "feet", "steps"] }, - { "category": "people", "char": "💃", "name": "dancer", "keywords": ["female", "girl", "woman", "fun"] }, - { "category": "people", "char": "🕺", "name": "man_dancing", "keywords": ["male", "boy", "fun", "dancer"] }, - { "category": "people", "char": "👯", "name": "dancing_women", "keywords": ["female", "bunny", "women", "girls"] }, - { "category": "people", "char": "👯♂️", "name": "dancing_men", "keywords": ["male", "bunny", "men", "boys"] }, - { "category": "people", "char": "👫", "name": "couple", "keywords": ["pair", "people", "human", "love", "date", "dating", "like", "affection", "valentines", "marriage"] }, - { "category": "people", "char": "\uD83E\uDDD1\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1", "name": "people_holding_hands", "keywords": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "human"] }, - { "category": "people", "char": "👬", "name": "two_men_holding_hands", "keywords": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "man", "human"] }, - { "category": "people", "char": "👭", "name": "two_women_holding_hands", "keywords": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "female", "human"] }, - { "category": "people", "char": "🫂", "name": "people_hugging", "keywords": [] }, - { "category": "people", "char": "🙇♀️", "name": "bowing_woman", "keywords": ["woman", "female", "girl"] }, - { "category": "people", "char": "🙇", "name": "bowing_man", "keywords": ["man", "male", "boy"] }, - { "category": "people", "char": "🤦♂️", "name": "man_facepalming", "keywords": ["man", "male", "boy", "disbelief"] }, - { "category": "people", "char": "🤦♀️", "name": "woman_facepalming", "keywords": ["woman", "female", "girl", "disbelief"] }, - { "category": "people", "char": "🤷", "name": "woman_shrugging", "keywords": ["woman", "female", "girl", "confused", "indifferent", "doubt"] }, - { "category": "people", "char": "🤷♂️", "name": "man_shrugging", "keywords": ["man", "male", "boy", "confused", "indifferent", "doubt"] }, - { "category": "people", "char": "💁", "name": "tipping_hand_woman", "keywords": ["female", "girl", "woman", "human", "information"] }, - { "category": "people", "char": "💁♂️", "name": "tipping_hand_man", "keywords": ["male", "boy", "man", "human", "information"] }, - { "category": "people", "char": "🙅", "name": "no_good_woman", "keywords": ["female", "girl", "woman", "nope"] }, - { "category": "people", "char": "🙅♂️", "name": "no_good_man", "keywords": ["male", "boy", "man", "nope"] }, - { "category": "people", "char": "🙆", "name": "ok_woman", "keywords": ["women", "girl", "female", "pink", "human", "woman"] }, - { "category": "people", "char": "🙆♂️", "name": "ok_man", "keywords": ["men", "boy", "male", "blue", "human", "man"] }, - { "category": "people", "char": "🙋", "name": "raising_hand_woman", "keywords": ["female", "girl", "woman"] }, - { "category": "people", "char": "🙋♂️", "name": "raising_hand_man", "keywords": ["male", "boy", "man"] }, - { "category": "people", "char": "🙎", "name": "pouting_woman", "keywords": ["female", "girl", "woman"] }, - { "category": "people", "char": "🙎♂️", "name": "pouting_man", "keywords": ["male", "boy", "man"] }, - { "category": "people", "char": "🙍", "name": "frowning_woman", "keywords": ["female", "girl", "woman", "sad", "depressed", "discouraged", "unhappy"] }, - { "category": "people", "char": "🙍♂️", "name": "frowning_man", "keywords": ["male", "boy", "man", "sad", "depressed", "discouraged", "unhappy"] }, - { "category": "people", "char": "💇", "name": "haircut_woman", "keywords": ["female", "girl", "woman"] }, - { "category": "people", "char": "💇♂️", "name": "haircut_man", "keywords": ["male", "boy", "man"] }, - { "category": "people", "char": "💆", "name": "massage_woman", "keywords": ["female", "girl", "woman", "head"] }, - { "category": "people", "char": "💆♂️", "name": "massage_man", "keywords": ["male", "boy", "man", "head"] }, - { "category": "people", "char": "🧖♀️", "name": "woman_in_steamy_room", "keywords": ["female", "woman", "spa", "steamroom", "sauna"] }, - { "category": "people", "char": "🧖♂️", "name": "man_in_steamy_room", "keywords": ["male", "man", "spa", "steamroom", "sauna"] }, - { "category": "people", "char": "🧏♀️", "name": "woman_deaf", "keywords": ["woman", "female"] }, - { "category": "people", "char": "🧏♂️", "name": "man_deaf", "keywords": ["man", "male"] }, - { "category": "people", "char": "🧍♀️", "name": "woman_standing", "keywords": ["woman", "female"] }, - { "category": "people", "char": "🧍♂️", "name": "man_standing", "keywords": ["man", "male"] }, - { "category": "people", "char": "🧎♀️", "name": "woman_kneeling", "keywords": ["woman", "female"] }, - { "category": "people", "char": "🧎♂️", "name": "man_kneeling", "keywords": ["man", "male"] }, - { "category": "people", "char": "🧑🦯", "name": "person_with_probing_cane", "keywords": ["accessibility", "blind"] }, - { "category": "people", "char": "👩🦯", "name": "woman_with_probing_cane", "keywords": ["woman", "female", "accessibility", "blind"] }, - { "category": "people", "char": "👨🦯", "name": "man_with_probing_cane", "keywords": ["man", "male", "accessibility", "blind"] }, - { "category": "people", "char": "🧑🦼", "name": "person_in_motorized_wheelchair", "keywords": ["accessibility"] }, - { "category": "people", "char": "👩🦼", "name": "woman_in_motorized_wheelchair", "keywords": ["woman", "female", "accessibility"] }, - { "category": "people", "char": "👨🦼", "name": "man_in_motorized_wheelchair", "keywords": ["man", "male", "accessibility"] }, - { "category": "people", "char": "🧑🦽", "name": "person_in_manual_wheelchair", "keywords": ["accessibility"] }, - { "category": "people", "char": "👩🦽", "name": "woman_in_manual_wheelchair", "keywords": ["woman", "female", "accessibility"] }, - { "category": "people", "char": "👨🦽", "name": "man_in_manual_wheelchair", "keywords": ["man", "male", "accessibility"] }, - { "category": "people", "char": "💑", "name": "couple_with_heart_woman_man", "keywords": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"] }, - { "category": "people", "char": "👩❤️👩", "name": "couple_with_heart_woman_woman", "keywords": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"] }, - { "category": "people", "char": "👨❤️👨", "name": "couple_with_heart_man_man", "keywords": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"] }, - { "category": "people", "char": "💏", "name": "couplekiss_man_woman", "keywords": ["pair", "valentines", "love", "like", "dating", "marriage"] }, - { "category": "people", "char": "👩❤️💋👩", "name": "couplekiss_woman_woman", "keywords": ["pair", "valentines", "love", "like", "dating", "marriage"] }, - { "category": "people", "char": "👨❤️💋👨", "name": "couplekiss_man_man", "keywords": ["pair", "valentines", "love", "like", "dating", "marriage"] }, - { "category": "people", "char": "👪", "name": "family_man_woman_boy", "keywords": ["home", "parents", "child", "mom", "dad", "father", "mother", "people", "human"] }, - { "category": "people", "char": "👨👩👧", "name": "family_man_woman_girl", "keywords": ["home", "parents", "people", "human", "child"] }, - { "category": "people", "char": "👨👩👧👦", "name": "family_man_woman_girl_boy", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👨👩👦👦", "name": "family_man_woman_boy_boy", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👨👩👧👧", "name": "family_man_woman_girl_girl", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👩👩👦", "name": "family_woman_woman_boy", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👩👩👧", "name": "family_woman_woman_girl", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👩👩👧👦", "name": "family_woman_woman_girl_boy", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👩👩👦👦", "name": "family_woman_woman_boy_boy", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👩👩👧👧", "name": "family_woman_woman_girl_girl", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👨👨👦", "name": "family_man_man_boy", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👨👨👧", "name": "family_man_man_girl", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👨👨👧👦", "name": "family_man_man_girl_boy", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👨👨👦👦", "name": "family_man_man_boy_boy", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👨👨👧👧", "name": "family_man_man_girl_girl", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👩👦", "name": "family_woman_boy", "keywords": ["home", "parent", "people", "human", "child"] }, - { "category": "people", "char": "👩👧", "name": "family_woman_girl", "keywords": ["home", "parent", "people", "human", "child"] }, - { "category": "people", "char": "👩👧👦", "name": "family_woman_girl_boy", "keywords": ["home", "parent", "people", "human", "children"] }, - { "category": "people", "char": "👩👦👦", "name": "family_woman_boy_boy", "keywords": ["home", "parent", "people", "human", "children"] }, - { "category": "people", "char": "👩👧👧", "name": "family_woman_girl_girl", "keywords": ["home", "parent", "people", "human", "children"] }, - { "category": "people", "char": "👨👦", "name": "family_man_boy", "keywords": ["home", "parent", "people", "human", "child"] }, - { "category": "people", "char": "👨👧", "name": "family_man_girl", "keywords": ["home", "parent", "people", "human", "child"] }, - { "category": "people", "char": "👨👧👦", "name": "family_man_girl_boy", "keywords": ["home", "parent", "people", "human", "children"] }, - { "category": "people", "char": "👨👦👦", "name": "family_man_boy_boy", "keywords": ["home", "parent", "people", "human", "children"] }, - { "category": "people", "char": "👨👧👧", "name": "family_man_girl_girl", "keywords": ["home", "parent", "people", "human", "children"] }, - { "category": "people", "char": "🧶", "name": "yarn", "keywords": ["ball", "crochet", "knit"] }, - { "category": "people", "char": "🧵", "name": "thread", "keywords": ["needle", "sewing", "spool", "string"] }, - { "category": "people", "char": "🧥", "name": "coat", "keywords": ["jacket"] }, - { "category": "people", "char": "🥼", "name": "labcoat", "keywords": ["doctor", "experiment", "scientist", "chemist"] }, - { "category": "people", "char": "👚", "name": "womans_clothes", "keywords": ["fashion", "shopping_bags", "female"] }, - { "category": "people", "char": "👕", "name": "tshirt", "keywords": ["fashion", "cloth", "casual", "shirt", "tee"] }, - { "category": "people", "char": "👖", "name": "jeans", "keywords": ["fashion", "shopping"] }, - { "category": "people", "char": "👔", "name": "necktie", "keywords": ["shirt", "suitup", "formal", "fashion", "cloth", "business"] }, - { "category": "people", "char": "👗", "name": "dress", "keywords": ["clothes", "fashion", "shopping"] }, - { "category": "people", "char": "👙", "name": "bikini", "keywords": ["swimming", "female", "woman", "girl", "fashion", "beach", "summer"] }, - { "category": "people", "char": "🩱", "name": "one_piece_swimsuit", "keywords": ["swimming", "female", "woman", "girl", "fashion", "beach", "summer"] }, - { "category": "people", "char": "👘", "name": "kimono", "keywords": ["dress", "fashion", "women", "female", "japanese"] }, - { "category": "people", "char": "🥻", "name": "sari", "keywords": ["dress", "fashion", "women", "female"] }, - { "category": "people", "char": "🩲", "name": "briefs", "keywords": ["dress", "fashion"] }, - { "category": "people", "char": "🩳", "name": "shorts", "keywords": ["dress", "fashion"] }, - { "category": "people", "char": "💄", "name": "lipstick", "keywords": ["female", "girl", "fashion", "woman"] }, - { "category": "people", "char": "💋", "name": "kiss", "keywords": ["face", "lips", "love", "like", "affection", "valentines"] }, - { "category": "people", "char": "👣", "name": "footprints", "keywords": ["feet", "tracking", "walking", "beach"] }, - { "category": "people", "char": "🥿", "name": "flat_shoe", "keywords": ["ballet", "slip-on", "slipper"] }, - { "category": "people", "char": "👠", "name": "high_heel", "keywords": ["fashion", "shoes", "female", "pumps", "stiletto"] }, - { "category": "people", "char": "👡", "name": "sandal", "keywords": ["shoes", "fashion", "flip flops"] }, - { "category": "people", "char": "👢", "name": "boot", "keywords": ["shoes", "fashion"] }, - { "category": "people", "char": "👞", "name": "mans_shoe", "keywords": ["fashion", "male"] }, - { "category": "people", "char": "👟", "name": "athletic_shoe", "keywords": ["shoes", "sports", "sneakers"] }, - { "category": "people", "char": "🩴", "name": "thong_sandal", "keywords": [] }, - { "category": "people", "char": "🩰", "name": "ballet_shoes", "keywords": ["shoes", "sports"] }, - { "category": "people", "char": "🧦", "name": "socks", "keywords": ["stockings", "clothes"] }, - { "category": "people", "char": "🧤", "name": "gloves", "keywords": ["hands", "winter", "clothes"] }, - { "category": "people", "char": "🧣", "name": "scarf", "keywords": ["neck", "winter", "clothes"] }, - { "category": "people", "char": "👒", "name": "womans_hat", "keywords": ["fashion", "accessories", "female", "lady", "spring"] }, - { "category": "people", "char": "🎩", "name": "tophat", "keywords": ["magic", "gentleman", "classy", "circus"] }, - { "category": "people", "char": "🧢", "name": "billed_hat", "keywords": ["cap", "baseball"] }, - { "category": "people", "char": "⛑", "name": "rescue_worker_helmet", "keywords": ["construction", "build"] }, - { "category": "people", "char": "🪖", "name": "military_helmet", "keywords": [] }, - { "category": "people", "char": "🎓", "name": "mortar_board", "keywords": ["school", "college", "degree", "university", "graduation", "cap", "hat", "legal", "learn", "education"] }, - { "category": "people", "char": "👑", "name": "crown", "keywords": ["king", "kod", "leader", "royalty", "lord"] }, - { "category": "people", "char": "🎒", "name": "school_satchel", "keywords": ["student", "education", "bag", "backpack"] }, - { "category": "people", "char": "🧳", "name": "luggage", "keywords": ["packing", "travel"] }, - { "category": "people", "char": "👝", "name": "pouch", "keywords": ["bag", "accessories", "shopping"] }, - { "category": "people", "char": "👛", "name": "purse", "keywords": ["fashion", "accessories", "money", "sales", "shopping"] }, - { "category": "people", "char": "👜", "name": "handbag", "keywords": ["fashion", "accessory", "accessories", "shopping"] }, - { "category": "people", "char": "💼", "name": "briefcase", "keywords": ["business", "documents", "work", "law", "legal", "job", "career"] }, - { "category": "people", "char": "👓", "name": "eyeglasses", "keywords": ["fashion", "accessories", "eyesight", "nerdy", "dork", "geek"] }, - { "category": "people", "char": "🕶", "name": "dark_sunglasses", "keywords": ["face", "cool", "accessories"] }, - { "category": "people", "char": "🥽", "name": "goggles", "keywords": ["eyes", "protection", "safety"] }, - { "category": "people", "char": "💍", "name": "ring", "keywords": ["wedding", "propose", "marriage", "valentines", "diamond", "fashion", "jewelry", "gem", "engagement"] }, - { "category": "people", "char": "🌂", "name": "closed_umbrella", "keywords": ["weather", "rain", "drizzle"] }, - { "category": "animals_and_nature", "char": "🐶", "name": "dog", "keywords": ["animal", "friend", "nature", "woof", "puppy", "pet", "faithful"] }, - { "category": "animals_and_nature", "char": "🐱", "name": "cat", "keywords": ["animal", "meow", "nature", "pet", "kitten"] }, - { "category": "animals_and_nature", "char": "🐈⬛", "name": "black_cat", "keywords": ["animal", "meow", "nature", "pet", "kitten"] }, - { "category": "animals_and_nature", "char": "🐭", "name": "mouse", "keywords": ["animal", "nature", "cheese_wedge", "rodent"] }, - { "category": "animals_and_nature", "char": "🐹", "name": "hamster", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐰", "name": "rabbit", "keywords": ["animal", "nature", "pet", "spring", "magic", "bunny"] }, - { "category": "animals_and_nature", "char": "🦊", "name": "fox_face", "keywords": ["animal", "nature", "face"] }, - { "category": "animals_and_nature", "char": "🐻", "name": "bear", "keywords": ["animal", "nature", "wild"] }, - { "category": "animals_and_nature", "char": "🐼", "name": "panda_face", "keywords": ["animal", "nature", "panda"] }, - { "category": "animals_and_nature", "char": "🐨", "name": "koala", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐯", "name": "tiger", "keywords": ["animal", "cat", "danger", "wild", "nature", "roar"] }, - { "category": "animals_and_nature", "char": "🦁", "name": "lion", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐮", "name": "cow", "keywords": ["beef", "ox", "animal", "nature", "moo", "milk"] }, - { "category": "animals_and_nature", "char": "🐷", "name": "pig", "keywords": ["animal", "oink", "nature"] }, - { "category": "animals_and_nature", "char": "🐽", "name": "pig_nose", "keywords": ["animal", "oink"] }, - { "category": "animals_and_nature", "char": "🐸", "name": "frog", "keywords": ["animal", "nature", "croak", "toad"] }, - { "category": "animals_and_nature", "char": "🦑", "name": "squid", "keywords": ["animal", "nature", "ocean", "sea"] }, - { "category": "animals_and_nature", "char": "🐙", "name": "octopus", "keywords": ["animal", "creature", "ocean", "sea", "nature", "beach"] }, - { "category": "animals_and_nature", "char": "🦐", "name": "shrimp", "keywords": ["animal", "ocean", "nature", "seafood"] }, - { "category": "animals_and_nature", "char": "🐵", "name": "monkey_face", "keywords": ["animal", "nature", "circus"] }, - { "category": "animals_and_nature", "char": "🦍", "name": "gorilla", "keywords": ["animal", "nature", "circus"] }, - { "category": "animals_and_nature", "char": "🙈", "name": "see_no_evil", "keywords": ["monkey", "animal", "nature", "haha"] }, - { "category": "animals_and_nature", "char": "🙉", "name": "hear_no_evil", "keywords": ["animal", "monkey", "nature"] }, - { "category": "animals_and_nature", "char": "🙊", "name": "speak_no_evil", "keywords": ["monkey", "animal", "nature", "omg"] }, - { "category": "animals_and_nature", "char": "🐒", "name": "monkey", "keywords": ["animal", "nature", "banana", "circus"] }, - { "category": "animals_and_nature", "char": "🐔", "name": "chicken", "keywords": ["animal", "cluck", "nature", "bird"] }, - { "category": "animals_and_nature", "char": "🐧", "name": "penguin", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐦", "name": "bird", "keywords": ["animal", "nature", "fly", "tweet", "spring"] }, - { "category": "animals_and_nature", "char": "🐤", "name": "baby_chick", "keywords": ["animal", "chicken", "bird"] }, - { "category": "animals_and_nature", "char": "🐣", "name": "hatching_chick", "keywords": ["animal", "chicken", "egg", "born", "baby", "bird"] }, - { "category": "animals_and_nature", "char": "🐥", "name": "hatched_chick", "keywords": ["animal", "chicken", "baby", "bird"] }, - { "category": "animals_and_nature", "char": "🦆", "name": "duck", "keywords": ["animal", "nature", "bird", "mallard"] }, - { "category": "animals_and_nature", "char": "🦅", "name": "eagle", "keywords": ["animal", "nature", "bird"] }, - { "category": "animals_and_nature", "char": "🦉", "name": "owl", "keywords": ["animal", "nature", "bird", "hoot"] }, - { "category": "animals_and_nature", "char": "🦇", "name": "bat", "keywords": ["animal", "nature", "blind", "vampire"] }, - { "category": "animals_and_nature", "char": "🐺", "name": "wolf", "keywords": ["animal", "nature", "wild"] }, - { "category": "animals_and_nature", "char": "🐗", "name": "boar", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐴", "name": "horse", "keywords": ["animal", "brown", "nature"] }, - { "category": "animals_and_nature", "char": "🦄", "name": "unicorn", "keywords": ["animal", "nature", "mystical"] }, - { "category": "animals_and_nature", "char": "🐝", "name": "honeybee", "keywords": ["animal", "insect", "nature", "bug", "spring", "honey"] }, - { "category": "animals_and_nature", "char": "🐛", "name": "bug", "keywords": ["animal", "insect", "nature", "worm"] }, - { "category": "animals_and_nature", "char": "🦋", "name": "butterfly", "keywords": ["animal", "insect", "nature", "caterpillar"] }, - { "category": "animals_and_nature", "char": "🐌", "name": "snail", "keywords": ["slow", "animal", "shell"] }, - { "category": "animals_and_nature", "char": "🐞", "name": "lady_beetle", "keywords": ["animal", "insect", "nature", "ladybug"] }, - { "category": "animals_and_nature", "char": "🐜", "name": "ant", "keywords": ["animal", "insect", "nature", "bug"] }, - { "category": "animals_and_nature", "char": "🦗", "name": "grasshopper", "keywords": ["animal", "cricket", "chirp"] }, - { "category": "animals_and_nature", "char": "🕷", "name": "spider", "keywords": ["animal", "arachnid"] }, - { "category": "animals_and_nature", "char": "🪲", "name": "beetle", "keywords": ["animal"] }, - { "category": "animals_and_nature", "char": "🪳", "name": "cockroach", "keywords": ["animal"] }, - { "category": "animals_and_nature", "char": "🪰", "name": "fly", "keywords": ["animal"] }, - { "category": "animals_and_nature", "char": "🪱", "name": "worm", "keywords": ["animal"] }, - { "category": "animals_and_nature", "char": "🦂", "name": "scorpion", "keywords": ["animal", "arachnid"] }, - { "category": "animals_and_nature", "char": "🦀", "name": "crab", "keywords": ["animal", "crustacean"] }, - { "category": "animals_and_nature", "char": "🐍", "name": "snake", "keywords": ["animal", "evil", "nature", "hiss", "python"] }, - { "category": "animals_and_nature", "char": "🦎", "name": "lizard", "keywords": ["animal", "nature", "reptile"] }, - { "category": "animals_and_nature", "char": "🦖", "name": "t-rex", "keywords": ["animal", "nature", "dinosaur", "tyrannosaurus", "extinct"] }, - { "category": "animals_and_nature", "char": "🦕", "name": "sauropod", "keywords": ["animal", "nature", "dinosaur", "brachiosaurus", "brontosaurus", "diplodocus", "extinct"] }, - { "category": "animals_and_nature", "char": "🐢", "name": "turtle", "keywords": ["animal", "slow", "nature", "tortoise"] }, - { "category": "animals_and_nature", "char": "🐠", "name": "tropical_fish", "keywords": ["animal", "swim", "ocean", "beach", "nemo"] }, - { "category": "animals_and_nature", "char": "🐟", "name": "fish", "keywords": ["animal", "food", "nature"] }, - { "category": "animals_and_nature", "char": "🐡", "name": "blowfish", "keywords": ["animal", "nature", "food", "sea", "ocean"] }, - { "category": "animals_and_nature", "char": "🐬", "name": "dolphin", "keywords": ["animal", "nature", "fish", "sea", "ocean", "flipper", "fins", "beach"] }, - { "category": "animals_and_nature", "char": "🦈", "name": "shark", "keywords": ["animal", "nature", "fish", "sea", "ocean", "jaws", "fins", "beach"] }, - { "category": "animals_and_nature", "char": "🐳", "name": "whale", "keywords": ["animal", "nature", "sea", "ocean"] }, - { "category": "animals_and_nature", "char": "🐋", "name": "whale2", "keywords": ["animal", "nature", "sea", "ocean"] }, - { "category": "animals_and_nature", "char": "🐊", "name": "crocodile", "keywords": ["animal", "nature", "reptile", "lizard", "alligator"] }, - { "category": "animals_and_nature", "char": "🐆", "name": "leopard", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦓", "name": "zebra", "keywords": ["animal", "nature", "stripes", "safari"] }, - { "category": "animals_and_nature", "char": "🐅", "name": "tiger2", "keywords": ["animal", "nature", "roar"] }, - { "category": "animals_and_nature", "char": "🐃", "name": "water_buffalo", "keywords": ["animal", "nature", "ox", "cow"] }, - { "category": "animals_and_nature", "char": "🐂", "name": "ox", "keywords": ["animal", "cow", "beef"] }, - { "category": "animals_and_nature", "char": "🐄", "name": "cow2", "keywords": ["beef", "ox", "animal", "nature", "moo", "milk"] }, - { "category": "animals_and_nature", "char": "🦌", "name": "deer", "keywords": ["animal", "nature", "horns", "venison"] }, - { "category": "animals_and_nature", "char": "🐪", "name": "dromedary_camel", "keywords": ["animal", "hot", "desert", "hump"] }, - { "category": "animals_and_nature", "char": "🐫", "name": "camel", "keywords": ["animal", "nature", "hot", "desert", "hump"] }, - { "category": "animals_and_nature", "char": "🦒", "name": "giraffe", "keywords": ["animal", "nature", "spots", "safari"] }, - { "category": "animals_and_nature", "char": "🐘", "name": "elephant", "keywords": ["animal", "nature", "nose", "th", "circus"] }, - { "category": "animals_and_nature", "char": "🦏", "name": "rhinoceros", "keywords": ["animal", "nature", "horn"] }, - { "category": "animals_and_nature", "char": "🐐", "name": "goat", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐏", "name": "ram", "keywords": ["animal", "sheep", "nature"] }, - { "category": "animals_and_nature", "char": "🐑", "name": "sheep", "keywords": ["animal", "nature", "wool", "shipit"] }, - { "category": "animals_and_nature", "char": "🐎", "name": "racehorse", "keywords": ["animal", "gamble", "luck"] }, - { "category": "animals_and_nature", "char": "🐖", "name": "pig2", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐀", "name": "rat", "keywords": ["animal", "mouse", "rodent"] }, - { "category": "animals_and_nature", "char": "🐁", "name": "mouse2", "keywords": ["animal", "nature", "rodent"] }, - { "category": "animals_and_nature", "char": "🐓", "name": "rooster", "keywords": ["animal", "nature", "chicken"] }, - { "category": "animals_and_nature", "char": "🦃", "name": "turkey", "keywords": ["animal", "bird"] }, - { "category": "animals_and_nature", "char": "🕊", "name": "dove", "keywords": ["animal", "bird"] }, - { "category": "animals_and_nature", "char": "🐕", "name": "dog2", "keywords": ["animal", "nature", "friend", "doge", "pet", "faithful"] }, - { "category": "animals_and_nature", "char": "🐩", "name": "poodle", "keywords": ["dog", "animal", "101", "nature", "pet"] }, - { "category": "animals_and_nature", "char": "🐈", "name": "cat2", "keywords": ["animal", "meow", "pet", "cats"] }, - { "category": "animals_and_nature", "char": "🐇", "name": "rabbit2", "keywords": ["animal", "nature", "pet", "magic", "spring"] }, - { "category": "animals_and_nature", "char": "🐿", "name": "chipmunk", "keywords": ["animal", "nature", "rodent", "squirrel"] }, - { "category": "animals_and_nature", "char": "🦔", "name": "hedgehog", "keywords": ["animal", "nature", "spiny"] }, - { "category": "animals_and_nature", "char": "🦝", "name": "raccoon", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦙", "name": "llama", "keywords": ["animal", "nature", "alpaca"] }, - { "category": "animals_and_nature", "char": "🦛", "name": "hippopotamus", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦘", "name": "kangaroo", "keywords": ["animal", "nature", "australia", "joey", "hop", "marsupial"] }, - { "category": "animals_and_nature", "char": "🦡", "name": "badger", "keywords": ["animal", "nature", "honey"] }, - { "category": "animals_and_nature", "char": "🦢", "name": "swan", "keywords": ["animal", "nature", "bird"] }, - { "category": "animals_and_nature", "char": "🦚", "name": "peacock", "keywords": ["animal", "nature", "peahen", "bird"] }, - { "category": "animals_and_nature", "char": "🦜", "name": "parrot", "keywords": ["animal", "nature", "bird", "pirate", "talk"] }, - { "category": "animals_and_nature", "char": "🦞", "name": "lobster", "keywords": ["animal", "nature", "bisque", "claws", "seafood"] }, - { "category": "animals_and_nature", "char": "🦠", "name": "microbe", "keywords": ["amoeba", "bacteria", "germs"] }, - { "category": "animals_and_nature", "char": "🦟", "name": "mosquito", "keywords": ["animal", "nature", "insect", "malaria"] }, - { "category": "animals_and_nature", "char": "🦬", "name": "bison", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦣", "name": "mammoth", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦫", "name": "beaver", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐻❄️", "name": "polar_bear", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦤", "name": "dodo", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🪶", "name": "feather", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦭", "name": "seal", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐾", "name": "paw_prints", "keywords": ["animal", "tracking", "footprints", "dog", "cat", "pet", "feet"] }, - { "category": "animals_and_nature", "char": "🐉", "name": "dragon", "keywords": ["animal", "myth", "nature", "chinese", "green"] }, - { "category": "animals_and_nature", "char": "🐲", "name": "dragon_face", "keywords": ["animal", "myth", "nature", "chinese", "green"] }, - { "category": "animals_and_nature", "char": "🦧", "name": "orangutan", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦮", "name": "guide_dog", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐕🦺", "name": "service_dog", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦥", "name": "sloth", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦦", "name": "otter", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦨", "name": "skunk", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦩", "name": "flamingo", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🌵", "name": "cactus", "keywords": ["vegetable", "plant", "nature"] }, - { "category": "animals_and_nature", "char": "🎄", "name": "christmas_tree", "keywords": ["festival", "vacation", "december", "xmas", "celebration"] }, - { "category": "animals_and_nature", "char": "🌲", "name": "evergreen_tree", "keywords": ["plant", "nature"] }, - { "category": "animals_and_nature", "char": "🌳", "name": "deciduous_tree", "keywords": ["plant", "nature"] }, - { "category": "animals_and_nature", "char": "🌴", "name": "palm_tree", "keywords": ["plant", "vegetable", "nature", "summer", "beach", "mojito", "tropical"] }, - { "category": "animals_and_nature", "char": "🌱", "name": "seedling", "keywords": ["plant", "nature", "grass", "lawn", "spring"] }, - { "category": "animals_and_nature", "char": "🌿", "name": "herb", "keywords": ["vegetable", "plant", "medicine", "weed", "grass", "lawn"] }, - { "category": "animals_and_nature", "char": "☘", "name": "shamrock", "keywords": ["vegetable", "plant", "nature", "irish", "clover"] }, - { "category": "animals_and_nature", "char": "🍀", "name": "four_leaf_clover", "keywords": ["vegetable", "plant", "nature", "lucky", "irish"] }, - { "category": "animals_and_nature", "char": "🎍", "name": "bamboo", "keywords": ["plant", "nature", "vegetable", "panda", "pine_decoration"] }, - { "category": "animals_and_nature", "char": "🎋", "name": "tanabata_tree", "keywords": ["plant", "nature", "branch", "summer"] }, - { "category": "animals_and_nature", "char": "🍃", "name": "leaves", "keywords": ["nature", "plant", "tree", "vegetable", "grass", "lawn", "spring"] }, - { "category": "animals_and_nature", "char": "🍂", "name": "fallen_leaf", "keywords": ["nature", "plant", "vegetable", "leaves"] }, - { "category": "animals_and_nature", "char": "🍁", "name": "maple_leaf", "keywords": ["nature", "plant", "vegetable", "ca", "fall"] }, - { "category": "animals_and_nature", "char": "🌾", "name": "ear_of_rice", "keywords": ["nature", "plant"] }, - { "category": "animals_and_nature", "char": "🌺", "name": "hibiscus", "keywords": ["plant", "vegetable", "flowers", "beach"] }, - { "category": "animals_and_nature", "char": "🌻", "name": "sunflower", "keywords": ["nature", "plant", "fall"] }, - { "category": "animals_and_nature", "char": "🌹", "name": "rose", "keywords": ["flowers", "valentines", "love", "spring"] }, - { "category": "animals_and_nature", "char": "🥀", "name": "wilted_flower", "keywords": ["plant", "nature", "flower"] }, - { "category": "animals_and_nature", "char": "🌷", "name": "tulip", "keywords": ["flowers", "plant", "nature", "summer", "spring"] }, - { "category": "animals_and_nature", "char": "🌼", "name": "blossom", "keywords": ["nature", "flowers", "yellow"] }, - { "category": "animals_and_nature", "char": "🌸", "name": "cherry_blossom", "keywords": ["nature", "plant", "spring", "flower"] }, - { "category": "animals_and_nature", "char": "💐", "name": "bouquet", "keywords": ["flowers", "nature", "spring"] }, - { "category": "animals_and_nature", "char": "🍄", "name": "mushroom", "keywords": ["plant", "vegetable"] }, - { "category": "animals_and_nature", "char": "🪴", "name": "potted_plant", "keywords": ["plant"] }, - { "category": "animals_and_nature", "char": "🌰", "name": "chestnut", "keywords": ["food", "squirrel"] }, - { "category": "animals_and_nature", "char": "🎃", "name": "jack_o_lantern", "keywords": ["halloween", "light", "pumpkin", "creepy", "fall"] }, - { "category": "animals_and_nature", "char": "🐚", "name": "shell", "keywords": ["nature", "sea", "beach"] }, - { "category": "animals_and_nature", "char": "🕸", "name": "spider_web", "keywords": ["animal", "insect", "arachnid", "silk"] }, - { "category": "animals_and_nature", "char": "🌎", "name": "earth_americas", "keywords": ["globe", "world", "USA", "international"] }, - { "category": "animals_and_nature", "char": "🌍", "name": "earth_africa", "keywords": ["globe", "world", "international"] }, - { "category": "animals_and_nature", "char": "🌏", "name": "earth_asia", "keywords": ["globe", "world", "east", "international"] }, - { "category": "animals_and_nature", "char": "🪐", "name": "ringed_planet", "keywords": ["saturn"] }, - { "category": "animals_and_nature", "char": "🌕", "name": "full_moon", "keywords": ["nature", "yellow", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌖", "name": "waning_gibbous_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep", "waxing_gibbous_moon"] }, - { "category": "animals_and_nature", "char": "🌗", "name": "last_quarter_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌘", "name": "waning_crescent_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌑", "name": "new_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌒", "name": "waxing_crescent_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌓", "name": "first_quarter_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌔", "name": "waxing_gibbous_moon", "keywords": ["nature", "night", "sky", "gray", "twilight", "planet", "space", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌚", "name": "new_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌝", "name": "full_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌛", "name": "first_quarter_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌜", "name": "last_quarter_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌞", "name": "sun_with_face", "keywords": ["nature", "morning", "sky"] }, - { "category": "animals_and_nature", "char": "🌙", "name": "crescent_moon", "keywords": ["night", "sleep", "sky", "evening", "magic"] }, - { "category": "animals_and_nature", "char": "⭐", "name": "star", "keywords": ["night", "yellow"] }, - { "category": "animals_and_nature", "char": "🌟", "name": "star2", "keywords": ["night", "sparkle", "awesome", "good", "magic"] }, - { "category": "animals_and_nature", "char": "💫", "name": "dizzy", "keywords": ["star", "sparkle", "shoot", "magic"] }, - { "category": "animals_and_nature", "char": "✨", "name": "sparkles", "keywords": ["stars", "shine", "shiny", "cool", "awesome", "good", "magic"] }, - { "category": "animals_and_nature", "char": "☄", "name": "comet", "keywords": ["space"] }, - { "category": "animals_and_nature", "char": "☀️", "name": "sunny", "keywords": ["weather", "nature", "brightness", "summer", "beach", "spring"] }, - { "category": "animals_and_nature", "char": "🌤", "name": "sun_behind_small_cloud", "keywords": ["weather"] }, - { "category": "animals_and_nature", "char": "⛅", "name": "partly_sunny", "keywords": ["weather", "nature", "cloudy", "morning", "fall", "spring"] }, - { "category": "animals_and_nature", "char": "🌥", "name": "sun_behind_large_cloud", "keywords": ["weather"] }, - { "category": "animals_and_nature", "char": "🌦", "name": "sun_behind_rain_cloud", "keywords": ["weather"] }, - { "category": "animals_and_nature", "char": "☁️", "name": "cloud", "keywords": ["weather", "sky"] }, - { "category": "animals_and_nature", "char": "🌧", "name": "cloud_with_rain", "keywords": ["weather"] }, - { "category": "animals_and_nature", "char": "⛈", "name": "cloud_with_lightning_and_rain", "keywords": ["weather", "lightning"] }, - { "category": "animals_and_nature", "char": "🌩", "name": "cloud_with_lightning", "keywords": ["weather", "thunder"] }, - { "category": "animals_and_nature", "char": "⚡", "name": "zap", "keywords": ["thunder", "weather", "lightning bolt", "fast"] }, - { "category": "animals_and_nature", "char": "🔥", "name": "fire", "keywords": ["hot", "cook", "flame"] }, - { "category": "animals_and_nature", "char": "💥", "name": "boom", "keywords": ["bomb", "explode", "explosion", "collision", "blown"] }, - { "category": "animals_and_nature", "char": "❄️", "name": "snowflake", "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas"] }, - { "category": "animals_and_nature", "char": "🌨", "name": "cloud_with_snow", "keywords": ["weather"] }, - { "category": "animals_and_nature", "char": "⛄", "name": "snowman", "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas", "frozen", "without_snow"] }, - { "category": "animals_and_nature", "char": "☃", "name": "snowman_with_snow", "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas", "frozen"] }, - { "category": "animals_and_nature", "char": "🌬", "name": "wind_face", "keywords": ["gust", "air"] }, - { "category": "animals_and_nature", "char": "💨", "name": "dash", "keywords": ["wind", "air", "fast", "shoo", "fart", "smoke", "puff"] }, - { "category": "animals_and_nature", "char": "🌪", "name": "tornado", "keywords": ["weather", "cyclone", "twister"] }, - { "category": "animals_and_nature", "char": "🌫", "name": "fog", "keywords": ["weather"] }, - { "category": "animals_and_nature", "char": "☂", "name": "open_umbrella", "keywords": ["weather", "spring"] }, - { "category": "animals_and_nature", "char": "☔", "name": "umbrella", "keywords": ["rainy", "weather", "spring"] }, - { "category": "animals_and_nature", "char": "💧", "name": "droplet", "keywords": ["water", "drip", "faucet", "spring"] }, - { "category": "animals_and_nature", "char": "💦", "name": "sweat_drops", "keywords": ["water", "drip", "oops"] }, - { "category": "animals_and_nature", "char": "🌊", "name": "ocean", "keywords": ["sea", "water", "wave", "nature", "tsunami", "disaster"] }, - { "category": "animals_and_nature", "char": "\uD83E\uDEB7", "name": "lotus", "keywords": [] }, - { "category": "animals_and_nature", "char": "\uD83E\uDEB8", "name": "coral", "keywords": [] }, - { "category": "animals_and_nature", "char": "\uD83E\uDEB9", "name": "empty_nest", "keywords": [] }, - { "category": "animals_and_nature", "char": "\uD83E\uDEBA", "name": "nest_with_eggs", "keywords": [] }, - { "category": "food_and_drink", "char": "🍏", "name": "green_apple", "keywords": ["fruit", "nature"] }, - { "category": "food_and_drink", "char": "🍎", "name": "apple", "keywords": ["fruit", "mac", "school"] }, - { "category": "food_and_drink", "char": "🍐", "name": "pear", "keywords": ["fruit", "nature", "food"] }, - { "category": "food_and_drink", "char": "🍊", "name": "tangerine", "keywords": ["food", "fruit", "nature", "orange"] }, - { "category": "food_and_drink", "char": "🍋", "name": "lemon", "keywords": ["fruit", "nature"] }, - { "category": "food_and_drink", "char": "🍌", "name": "banana", "keywords": ["fruit", "food", "monkey"] }, - { "category": "food_and_drink", "char": "🍉", "name": "watermelon", "keywords": ["fruit", "food", "picnic", "summer"] }, - { "category": "food_and_drink", "char": "🍇", "name": "grapes", "keywords": ["fruit", "food", "wine"] }, - { "category": "food_and_drink", "char": "🍓", "name": "strawberry", "keywords": ["fruit", "food", "nature"] }, - { "category": "food_and_drink", "char": "🍈", "name": "melon", "keywords": ["fruit", "nature", "food"] }, - { "category": "food_and_drink", "char": "🍒", "name": "cherries", "keywords": ["food", "fruit"] }, - { "category": "food_and_drink", "char": "🍑", "name": "peach", "keywords": ["fruit", "nature", "food"] }, - { "category": "food_and_drink", "char": "🍍", "name": "pineapple", "keywords": ["fruit", "nature", "food"] }, - { "category": "food_and_drink", "char": "🥥", "name": "coconut", "keywords": ["fruit", "nature", "food", "palm"] }, - { "category": "food_and_drink", "char": "🥝", "name": "kiwi_fruit", "keywords": ["fruit", "food"] }, - { "category": "food_and_drink", "char": "🥭", "name": "mango", "keywords": ["fruit", "food", "tropical"] }, - { "category": "food_and_drink", "char": "🥑", "name": "avocado", "keywords": ["fruit", "food"] }, - { "category": "food_and_drink", "char": "🥦", "name": "broccoli", "keywords": ["fruit", "food", "vegetable"] }, - { "category": "food_and_drink", "char": "🍅", "name": "tomato", "keywords": ["fruit", "vegetable", "nature", "food"] }, - { "category": "food_and_drink", "char": "🍆", "name": "eggplant", "keywords": ["vegetable", "nature", "food", "aubergine"] }, - { "category": "food_and_drink", "char": "🥒", "name": "cucumber", "keywords": ["fruit", "food", "pickle"] }, - { "category": "food_and_drink", "char": "🫐", "name": "blueberries", "keywords": ["fruit", "food"] }, - { "category": "food_and_drink", "char": "🫒", "name": "olive", "keywords": ["fruit", "food"] }, - { "category": "food_and_drink", "char": "🫑", "name": "bell_pepper", "keywords": ["fruit", "food"] }, - { "category": "food_and_drink", "char": "🥕", "name": "carrot", "keywords": ["vegetable", "food", "orange"] }, - { "category": "food_and_drink", "char": "🌶", "name": "hot_pepper", "keywords": ["food", "spicy", "chilli", "chili"] }, - { "category": "food_and_drink", "char": "🥔", "name": "potato", "keywords": ["food", "tuber", "vegatable", "starch"] }, - { "category": "food_and_drink", "char": "🌽", "name": "corn", "keywords": ["food", "vegetable", "plant"] }, - { "category": "food_and_drink", "char": "🥬", "name": "leafy_greens", "keywords": ["food", "vegetable", "plant", "bok choy", "cabbage", "kale", "lettuce"] }, - { "category": "food_and_drink", "char": "🍠", "name": "sweet_potato", "keywords": ["food", "nature"] }, - { "category": "food_and_drink", "char": "🥜", "name": "peanuts", "keywords": ["food", "nut"] }, - { "category": "food_and_drink", "char": "🧄", "name": "garlic", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🧅", "name": "onion", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🍯", "name": "honey_pot", "keywords": ["bees", "sweet", "kitchen"] }, - { "category": "food_and_drink", "char": "🥐", "name": "croissant", "keywords": ["food", "bread", "french"] }, - { "category": "food_and_drink", "char": "🍞", "name": "bread", "keywords": ["food", "wheat", "breakfast", "toast"] }, - { "category": "food_and_drink", "char": "🥖", "name": "baguette_bread", "keywords": ["food", "bread", "french"] }, - { "category": "food_and_drink", "char": "🥯", "name": "bagel", "keywords": ["food", "bread", "bakery", "schmear"] }, - { "category": "food_and_drink", "char": "🥨", "name": "pretzel", "keywords": ["food", "bread", "twisted"] }, - { "category": "food_and_drink", "char": "🧀", "name": "cheese", "keywords": ["food", "chadder"] }, - { "category": "food_and_drink", "char": "🥚", "name": "egg", "keywords": ["food", "chicken", "breakfast"] }, - { "category": "food_and_drink", "char": "🥓", "name": "bacon", "keywords": ["food", "breakfast", "pork", "pig", "meat"] }, - { "category": "food_and_drink", "char": "🥩", "name": "steak", "keywords": ["food", "cow", "meat", "cut", "chop", "lambchop", "porkchop"] }, - { "category": "food_and_drink", "char": "🥞", "name": "pancakes", "keywords": ["food", "breakfast", "flapjacks", "hotcakes"] }, - { "category": "food_and_drink", "char": "🍗", "name": "poultry_leg", "keywords": ["food", "meat", "drumstick", "bird", "chicken", "turkey"] }, - { "category": "food_and_drink", "char": "🍖", "name": "meat_on_bone", "keywords": ["good", "food", "drumstick"] }, - { "category": "food_and_drink", "char": "🦴", "name": "bone", "keywords": ["skeleton"] }, - { "category": "food_and_drink", "char": "🍤", "name": "fried_shrimp", "keywords": ["food", "animal", "appetizer", "summer"] }, - { "category": "food_and_drink", "char": "🍳", "name": "fried_egg", "keywords": ["food", "breakfast", "kitchen", "egg"] }, - { "category": "food_and_drink", "char": "🍔", "name": "hamburger", "keywords": ["meat", "fast food", "beef", "cheeseburger", "mcdonalds", "burger king"] }, - { "category": "food_and_drink", "char": "🍟", "name": "fries", "keywords": ["chips", "snack", "fast food"] }, - { "category": "food_and_drink", "char": "🥙", "name": "stuffed_flatbread", "keywords": ["food", "flatbread", "stuffed", "gyro"] }, - { "category": "food_and_drink", "char": "🌭", "name": "hotdog", "keywords": ["food", "frankfurter"] }, - { "category": "food_and_drink", "char": "🍕", "name": "pizza", "keywords": ["food", "party"] }, - { "category": "food_and_drink", "char": "🥪", "name": "sandwich", "keywords": ["food", "lunch", "bread"] }, - { "category": "food_and_drink", "char": "🥫", "name": "canned_food", "keywords": ["food", "soup"] }, - { "category": "food_and_drink", "char": "🍝", "name": "spaghetti", "keywords": ["food", "italian", "noodle"] }, - { "category": "food_and_drink", "char": "🌮", "name": "taco", "keywords": ["food", "mexican"] }, - { "category": "food_and_drink", "char": "🌯", "name": "burrito", "keywords": ["food", "mexican"] }, - { "category": "food_and_drink", "char": "🥗", "name": "green_salad", "keywords": ["food", "healthy", "lettuce"] }, - { "category": "food_and_drink", "char": "🥘", "name": "shallow_pan_of_food", "keywords": ["food", "cooking", "casserole", "paella"] }, - { "category": "food_and_drink", "char": "🍜", "name": "ramen", "keywords": ["food", "japanese", "noodle", "chopsticks"] }, - { "category": "food_and_drink", "char": "🍲", "name": "stew", "keywords": ["food", "meat", "soup"] }, - { "category": "food_and_drink", "char": "🍥", "name": "fish_cake", "keywords": ["food", "japan", "sea", "beach", "narutomaki", "pink", "swirl", "kamaboko", "surimi", "ramen"] }, - { "category": "food_and_drink", "char": "🥠", "name": "fortune_cookie", "keywords": ["food", "prophecy"] }, - { "category": "food_and_drink", "char": "🍣", "name": "sushi", "keywords": ["food", "fish", "japanese", "rice"] }, - { "category": "food_and_drink", "char": "🍱", "name": "bento", "keywords": ["food", "japanese", "box"] }, - { "category": "food_and_drink", "char": "🍛", "name": "curry", "keywords": ["food", "spicy", "hot", "indian"] }, - { "category": "food_and_drink", "char": "🍙", "name": "rice_ball", "keywords": ["food", "japanese"] }, - { "category": "food_and_drink", "char": "🍚", "name": "rice", "keywords": ["food", "china", "asian"] }, - { "category": "food_and_drink", "char": "🍘", "name": "rice_cracker", "keywords": ["food", "japanese"] }, - { "category": "food_and_drink", "char": "🍢", "name": "oden", "keywords": ["food", "japanese"] }, - { "category": "food_and_drink", "char": "🍡", "name": "dango", "keywords": ["food", "dessert", "sweet", "japanese", "barbecue", "meat"] }, - { "category": "food_and_drink", "char": "🍧", "name": "shaved_ice", "keywords": ["hot", "dessert", "summer"] }, - { "category": "food_and_drink", "char": "🍨", "name": "ice_cream", "keywords": ["food", "hot", "dessert"] }, - { "category": "food_and_drink", "char": "🍦", "name": "icecream", "keywords": ["food", "hot", "dessert", "summer"] }, - { "category": "food_and_drink", "char": "🥧", "name": "pie", "keywords": ["food", "dessert", "pastry"] }, - { "category": "food_and_drink", "char": "🍰", "name": "cake", "keywords": ["food", "dessert"] }, - { "category": "food_and_drink", "char": "🧁", "name": "cupcake", "keywords": ["food", "dessert", "bakery", "sweet"] }, - { "category": "food_and_drink", "char": "🥮", "name": "moon_cake", "keywords": ["food", "autumn"] }, - { "category": "food_and_drink", "char": "🎂", "name": "birthday", "keywords": ["food", "dessert", "cake"] }, - { "category": "food_and_drink", "char": "🍮", "name": "custard", "keywords": ["dessert", "food"] }, - { "category": "food_and_drink", "char": "🍬", "name": "candy", "keywords": ["snack", "dessert", "sweet", "lolly"] }, - { "category": "food_and_drink", "char": "🍭", "name": "lollipop", "keywords": ["food", "snack", "candy", "sweet"] }, - { "category": "food_and_drink", "char": "🍫", "name": "chocolate_bar", "keywords": ["food", "snack", "dessert", "sweet"] }, - { "category": "food_and_drink", "char": "🍿", "name": "popcorn", "keywords": ["food", "movie theater", "films", "snack"] }, - { "category": "food_and_drink", "char": "🥟", "name": "dumpling", "keywords": ["food", "empanada", "pierogi", "potsticker"] }, - { "category": "food_and_drink", "char": "🍩", "name": "doughnut", "keywords": ["food", "dessert", "snack", "sweet", "donut"] }, - { "category": "food_and_drink", "char": "🍪", "name": "cookie", "keywords": ["food", "snack", "oreo", "chocolate", "sweet", "dessert"] }, - { "category": "food_and_drink", "char": "🧇", "name": "waffle", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🧆", "name": "falafel", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🧈", "name": "butter", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🦪", "name": "oyster", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🫓", "name": "flatbread", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🫔", "name": "tamale", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🫕", "name": "fondue", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🥛", "name": "milk_glass", "keywords": ["beverage", "drink", "cow"] }, - { "category": "food_and_drink", "char": "🍺", "name": "beer", "keywords": ["relax", "beverage", "drink", "drunk", "party", "pub", "summer", "alcohol", "booze"] }, - { "category": "food_and_drink", "char": "🍻", "name": "beers", "keywords": ["relax", "beverage", "drink", "drunk", "party", "pub", "summer", "alcohol", "booze"] }, - { "category": "food_and_drink", "char": "🥂", "name": "clinking_glasses", "keywords": ["beverage", "drink", "party", "alcohol", "celebrate", "cheers", "wine", "champagne", "toast"] }, - { "category": "food_and_drink", "char": "🍷", "name": "wine_glass", "keywords": ["drink", "beverage", "drunk", "alcohol", "booze"] }, - { "category": "food_and_drink", "char": "🥃", "name": "tumbler_glass", "keywords": ["drink", "beverage", "drunk", "alcohol", "liquor", "booze", "bourbon", "scotch", "whisky", "glass", "shot"] }, - { "category": "food_and_drink", "char": "🍸", "name": "cocktail", "keywords": ["drink", "drunk", "alcohol", "beverage", "booze", "mojito"] }, - { "category": "food_and_drink", "char": "🍹", "name": "tropical_drink", "keywords": ["beverage", "cocktail", "summer", "beach", "alcohol", "booze", "mojito"] }, - { "category": "food_and_drink", "char": "🍾", "name": "champagne", "keywords": ["drink", "wine", "bottle", "celebration"] }, - { "category": "food_and_drink", "char": "🍶", "name": "sake", "keywords": ["wine", "drink", "drunk", "beverage", "japanese", "alcohol", "booze"] }, - { "category": "food_and_drink", "char": "🍵", "name": "tea", "keywords": ["drink", "bowl", "breakfast", "green", "british"] }, - { "category": "food_and_drink", "char": "🥤", "name": "cup_with_straw", "keywords": ["drink", "soda"] }, - { "category": "food_and_drink", "char": "☕", "name": "coffee", "keywords": ["beverage", "caffeine", "latte", "espresso"] }, - { "category": "food_and_drink", "char": "🫖", "name": "teapot", "keywords": [] }, - { "category": "food_and_drink", "char": "🧋", "name": "bubble_tea", "keywords": ["tapioca"] }, - { "category": "food_and_drink", "char": "🍼", "name": "baby_bottle", "keywords": ["food", "container", "milk"] }, - { "category": "food_and_drink", "char": "🧃", "name": "beverage_box", "keywords": ["food", "drink"] }, - { "category": "food_and_drink", "char": "🧉", "name": "mate", "keywords": ["food", "drink"] }, - { "category": "food_and_drink", "char": "🧊", "name": "ice_cube", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🧂", "name": "salt", "keywords": ["condiment", "shaker"] }, - { "category": "food_and_drink", "char": "🥄", "name": "spoon", "keywords": ["cutlery", "kitchen", "tableware"] }, - { "category": "food_and_drink", "char": "🍴", "name": "fork_and_knife", "keywords": ["cutlery", "kitchen"] }, - { "category": "food_and_drink", "char": "🍽", "name": "plate_with_cutlery", "keywords": ["food", "eat", "meal", "lunch", "dinner", "restaurant"] }, - { "category": "food_and_drink", "char": "🥣", "name": "bowl_with_spoon", "keywords": ["food", "breakfast", "cereal", "oatmeal", "porridge"] }, - { "category": "food_and_drink", "char": "🥡", "name": "takeout_box", "keywords": ["food", "leftovers"] }, - { "category": "food_and_drink", "char": "🥢", "name": "chopsticks", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "\uD83E\uDED7", "name": "pouring_liquid", "keywords": [] }, - { "category": "food_and_drink", "char": "\uD83E\uDED8", "name": "beans", "keywords": [] }, - { "category": "food_and_drink", "char": "\uD83E\uDED9", "name": "jar", "keywords": [] }, - { "category": "activity", "char": "⚽", "name": "soccer", "keywords": ["sports", "football"] }, - { "category": "activity", "char": "🏀", "name": "basketball", "keywords": ["sports", "balls", "NBA"] }, - { "category": "activity", "char": "🏈", "name": "football", "keywords": ["sports", "balls", "NFL"] }, - { "category": "activity", "char": "⚾", "name": "baseball", "keywords": ["sports", "balls"] }, - { "category": "activity", "char": "🥎", "name": "softball", "keywords": ["sports", "balls"] }, - { "category": "activity", "char": "🎾", "name": "tennis", "keywords": ["sports", "balls", "green"] }, - { "category": "activity", "char": "🏐", "name": "volleyball", "keywords": ["sports", "balls"] }, - { "category": "activity", "char": "🏉", "name": "rugby_football", "keywords": ["sports", "team"] }, - { "category": "activity", "char": "🥏", "name": "flying_disc", "keywords": ["sports", "frisbee", "ultimate"] }, - { "category": "activity", "char": "🎱", "name": "8ball", "keywords": ["pool", "hobby", "game", "luck", "magic"] }, - { "category": "activity", "char": "⛳", "name": "golf", "keywords": ["sports", "business", "flag", "hole", "summer"] }, - { "category": "activity", "char": "🏌️♀️", "name": "golfing_woman", "keywords": ["sports", "business", "woman", "female"] }, - { "category": "activity", "char": "🏌", "name": "golfing_man", "keywords": ["sports", "business"] }, - { "category": "activity", "char": "🏓", "name": "ping_pong", "keywords": ["sports", "pingpong"] }, - { "category": "activity", "char": "🏸", "name": "badminton", "keywords": ["sports"] }, - { "category": "activity", "char": "🥅", "name": "goal_net", "keywords": ["sports"] }, - { "category": "activity", "char": "🏒", "name": "ice_hockey", "keywords": ["sports"] }, - { "category": "activity", "char": "🏑", "name": "field_hockey", "keywords": ["sports"] }, - { "category": "activity", "char": "🥍", "name": "lacrosse", "keywords": ["sports", "ball", "stick"] }, - { "category": "activity", "char": "🏏", "name": "cricket", "keywords": ["sports"] }, - { "category": "activity", "char": "🎿", "name": "ski", "keywords": ["sports", "winter", "cold", "snow"] }, - { "category": "activity", "char": "⛷", "name": "skier", "keywords": ["sports", "winter", "snow"] }, - { "category": "activity", "char": "🏂", "name": "snowboarder", "keywords": ["sports", "winter"] }, - { "category": "activity", "char": "🤺", "name": "person_fencing", "keywords": ["sports", "fencing", "sword"] }, - { "category": "activity", "char": "🤼♀️", "name": "women_wrestling", "keywords": ["sports", "wrestlers"] }, - { "category": "activity", "char": "🤼♂️", "name": "men_wrestling", "keywords": ["sports", "wrestlers"] }, - { "category": "activity", "char": "🤸♀️", "name": "woman_cartwheeling", "keywords": ["gymnastics"] }, - { "category": "activity", "char": "🤸♂️", "name": "man_cartwheeling", "keywords": ["gymnastics"] }, - { "category": "activity", "char": "🤾♀️", "name": "woman_playing_handball", "keywords": ["sports"] }, - { "category": "activity", "char": "🤾♂️", "name": "man_playing_handball", "keywords": ["sports"] }, - { "category": "activity", "char": "⛸", "name": "ice_skate", "keywords": ["sports"] }, - { "category": "activity", "char": "🥌", "name": "curling_stone", "keywords": ["sports"] }, - { "category": "activity", "char": "🛹", "name": "skateboard", "keywords": ["board"] }, - { "category": "activity", "char": "🛷", "name": "sled", "keywords": ["sleigh", "luge", "toboggan"] }, - { "category": "activity", "char": "🏹", "name": "bow_and_arrow", "keywords": ["sports"] }, - { "category": "activity", "char": "🎣", "name": "fishing_pole_and_fish", "keywords": ["food", "hobby", "summer"] }, - { "category": "activity", "char": "🥊", "name": "boxing_glove", "keywords": ["sports", "fighting"] }, - { "category": "activity", "char": "🥋", "name": "martial_arts_uniform", "keywords": ["judo", "karate", "taekwondo"] }, - { "category": "activity", "char": "🚣♀️", "name": "rowing_woman", "keywords": ["sports", "hobby", "water", "ship", "woman", "female"] }, - { "category": "activity", "char": "🚣", "name": "rowing_man", "keywords": ["sports", "hobby", "water", "ship"] }, - { "category": "activity", "char": "🧗♀️", "name": "climbing_woman", "keywords": ["sports", "hobby", "woman", "female", "rock"] }, - { "category": "activity", "char": "🧗♂️", "name": "climbing_man", "keywords": ["sports", "hobby", "man", "male", "rock"] }, - { "category": "activity", "char": "🏊♀️", "name": "swimming_woman", "keywords": ["sports", "exercise", "human", "athlete", "water", "summer", "woman", "female"] }, - { "category": "activity", "char": "🏊", "name": "swimming_man", "keywords": ["sports", "exercise", "human", "athlete", "water", "summer"] }, - { "category": "activity", "char": "🤽♀️", "name": "woman_playing_water_polo", "keywords": ["sports", "pool"] }, - { "category": "activity", "char": "🤽♂️", "name": "man_playing_water_polo", "keywords": ["sports", "pool"] }, - { "category": "activity", "char": "🧘♀️", "name": "woman_in_lotus_position", "keywords": ["woman", "female", "meditation", "yoga", "serenity", "zen", "mindfulness"] }, - { "category": "activity", "char": "🧘♂️", "name": "man_in_lotus_position", "keywords": ["man", "male", "meditation", "yoga", "serenity", "zen", "mindfulness"] }, - { "category": "activity", "char": "🏄♀️", "name": "surfing_woman", "keywords": ["sports", "ocean", "sea", "summer", "beach", "woman", "female"] }, - { "category": "activity", "char": "🏄", "name": "surfing_man", "keywords": ["sports", "ocean", "sea", "summer", "beach"] }, - { "category": "activity", "char": "🛀", "name": "bath", "keywords": ["clean", "shower", "bathroom"] }, - { "category": "activity", "char": "⛹️♀️", "name": "basketball_woman", "keywords": ["sports", "human", "woman", "female"] }, - { "category": "activity", "char": "⛹", "name": "basketball_man", "keywords": ["sports", "human"] }, - { "category": "activity", "char": "🏋️♀️", "name": "weight_lifting_woman", "keywords": ["sports", "training", "exercise", "woman", "female"] }, - { "category": "activity", "char": "🏋", "name": "weight_lifting_man", "keywords": ["sports", "training", "exercise"] }, - { "category": "activity", "char": "🚴♀️", "name": "biking_woman", "keywords": ["sports", "bike", "exercise", "hipster", "woman", "female"] }, - { "category": "activity", "char": "🚴", "name": "biking_man", "keywords": ["sports", "bike", "exercise", "hipster"] }, - { "category": "activity", "char": "🚵♀️", "name": "mountain_biking_woman", "keywords": ["transportation", "sports", "human", "race", "bike", "woman", "female"] }, - { "category": "activity", "char": "🚵", "name": "mountain_biking_man", "keywords": ["transportation", "sports", "human", "race", "bike"] }, - { "category": "activity", "char": "🏇", "name": "horse_racing", "keywords": ["animal", "betting", "competition", "gambling", "luck"] }, - { "category": "activity", "char": "🤿", "name": "diving_mask", "keywords": ["sports"] }, - { "category": "activity", "char": "🪀", "name": "yo_yo", "keywords": ["sports"] }, - { "category": "activity", "char": "🪁", "name": "kite", "keywords": ["sports"] }, - { "category": "activity", "char": "🦺", "name": "safety_vest", "keywords": ["sports"] }, - { "category": "activity", "char": "🪡", "name": "sewing_needle", "keywords": [] }, - { "category": "activity", "char": "🪢", "name": "knot", "keywords": [] }, - { "category": "activity", "char": "🕴", "name": "business_suit_levitating", "keywords": ["suit", "business", "levitate", "hover", "jump"] }, - { "category": "activity", "char": "🏆", "name": "trophy", "keywords": ["win", "award", "contest", "place", "ftw", "ceremony"] }, - { "category": "activity", "char": "🎽", "name": "running_shirt_with_sash", "keywords": ["play", "pageant"] }, - { "category": "activity", "char": "🏅", "name": "medal_sports", "keywords": ["award", "winning"] }, - { "category": "activity", "char": "🎖", "name": "medal_military", "keywords": ["award", "winning", "army"] }, - { "category": "activity", "char": "🥇", "name": "1st_place_medal", "keywords": ["award", "winning", "first"] }, - { "category": "activity", "char": "🥈", "name": "2nd_place_medal", "keywords": ["award", "second"] }, - { "category": "activity", "char": "🥉", "name": "3rd_place_medal", "keywords": ["award", "third"] }, - { "category": "activity", "char": "🎗", "name": "reminder_ribbon", "keywords": ["sports", "cause", "support", "awareness"] }, - { "category": "activity", "char": "🏵", "name": "rosette", "keywords": ["flower", "decoration", "military"] }, - { "category": "activity", "char": "🎫", "name": "ticket", "keywords": ["event", "concert", "pass"] }, - { "category": "activity", "char": "🎟", "name": "tickets", "keywords": ["sports", "concert", "entrance"] }, - { "category": "activity", "char": "🎭", "name": "performing_arts", "keywords": ["acting", "theater", "drama"] }, - { "category": "activity", "char": "🎨", "name": "art", "keywords": ["design", "paint", "draw", "colors"] }, - { "category": "activity", "char": "🎪", "name": "circus_tent", "keywords": ["festival", "carnival", "party"] }, - { "category": "activity", "char": "🤹♀️", "name": "woman_juggling", "keywords": ["juggle", "balance", "skill", "multitask"] }, - { "category": "activity", "char": "🤹♂️", "name": "man_juggling", "keywords": ["juggle", "balance", "skill", "multitask"] }, - { "category": "activity", "char": "🎤", "name": "microphone", "keywords": ["sound", "music", "PA", "sing", "talkshow"] }, - { "category": "activity", "char": "🎧", "name": "headphones", "keywords": ["music", "score", "gadgets"] }, - { "category": "activity", "char": "🎼", "name": "musical_score", "keywords": ["treble", "clef", "compose"] }, - { "category": "activity", "char": "🎹", "name": "musical_keyboard", "keywords": ["piano", "instrument", "compose"] }, - { "category": "activity", "char": "🥁", "name": "drum", "keywords": ["music", "instrument", "drumsticks", "snare"] }, - { "category": "activity", "char": "🎷", "name": "saxophone", "keywords": ["music", "instrument", "jazz", "blues"] }, - { "category": "activity", "char": "🎺", "name": "trumpet", "keywords": ["music", "brass"] }, - { "category": "activity", "char": "🎸", "name": "guitar", "keywords": ["music", "instrument"] }, - { "category": "activity", "char": "🎻", "name": "violin", "keywords": ["music", "instrument", "orchestra", "symphony"] }, - { "category": "activity", "char": "🪕", "name": "banjo", "keywords": ["music", "instrument"] }, - { "category": "activity", "char": "🪗", "name": "accordion", "keywords": ["music", "instrument"] }, - { "category": "activity", "char": "🪘", "name": "long_drum", "keywords": ["music", "instrument"] }, - { "category": "activity", "char": "🎬", "name": "clapper", "keywords": ["movie", "film", "record"] }, - { "category": "activity", "char": "🎮", "name": "video_game", "keywords": ["play", "console", "PS4", "controller"] }, - { "category": "activity", "char": "👾", "name": "space_invader", "keywords": ["game", "arcade", "play"] }, - { "category": "activity", "char": "🎯", "name": "dart", "keywords": ["game", "play", "bar", "target", "bullseye"] }, - { "category": "activity", "char": "🎲", "name": "game_die", "keywords": ["dice", "random", "tabletop", "play", "luck"] }, - { "category": "activity", "char": "♟️", "name": "chess_pawn", "keywords": ["expendable"] }, - { "category": "activity", "char": "🎰", "name": "slot_machine", "keywords": ["bet", "gamble", "vegas", "fruit machine", "luck", "casino"] }, - { "category": "activity", "char": "🧩", "name": "jigsaw", "keywords": ["interlocking", "puzzle", "piece"] }, - { "category": "activity", "char": "🎳", "name": "bowling", "keywords": ["sports", "fun", "play"] }, - { "category": "activity", "char": "🪄", "name": "magic_wand", "keywords": [] }, - { "category": "activity", "char": "🪅", "name": "pinata", "keywords": [] }, - { "category": "activity", "char": "🪆", "name": "nesting_dolls", "keywords": [] }, - { "category": "activity", "char": "\uD83E\uDEAC", "name": "hamsa", "keywords": [] }, - { "category": "activity", "char": "\uD83E\uDEA9", "name": "mirror_ball", "keywords": [] }, - { "category": "travel_and_places", "char": "🚗", "name": "red_car", "keywords": ["red", "transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🚕", "name": "taxi", "keywords": ["uber", "vehicle", "cars", "transportation"] }, - { "category": "travel_and_places", "char": "🚙", "name": "blue_car", "keywords": ["transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🚌", "name": "bus", "keywords": ["car", "vehicle", "transportation"] }, - { "category": "travel_and_places", "char": "🚎", "name": "trolleybus", "keywords": ["bart", "transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🏎", "name": "racing_car", "keywords": ["sports", "race", "fast", "formula", "f1"] }, - { "category": "travel_and_places", "char": "🚓", "name": "police_car", "keywords": ["vehicle", "cars", "transportation", "law", "legal", "enforcement"] }, - { "category": "travel_and_places", "char": "🚑", "name": "ambulance", "keywords": ["health", "911", "hospital"] }, - { "category": "travel_and_places", "char": "🚒", "name": "fire_engine", "keywords": ["transportation", "cars", "vehicle"] }, - { "category": "travel_and_places", "char": "🚐", "name": "minibus", "keywords": ["vehicle", "car", "transportation"] }, - { "category": "travel_and_places", "char": "🚚", "name": "truck", "keywords": ["cars", "transportation"] }, - { "category": "travel_and_places", "char": "🚛", "name": "articulated_lorry", "keywords": ["vehicle", "cars", "transportation", "express"] }, - { "category": "travel_and_places", "char": "🚜", "name": "tractor", "keywords": ["vehicle", "car", "farming", "agriculture"] }, - { "category": "travel_and_places", "char": "🛴", "name": "kick_scooter", "keywords": ["vehicle", "kick", "razor"] }, - { "category": "travel_and_places", "char": "🏍", "name": "motorcycle", "keywords": ["race", "sports", "fast"] }, - { "category": "travel_and_places", "char": "🚲", "name": "bike", "keywords": ["sports", "bicycle", "exercise", "hipster"] }, - { "category": "travel_and_places", "char": "🛵", "name": "motor_scooter", "keywords": ["vehicle", "vespa", "sasha"] }, - { "category": "travel_and_places", "char": "🦽", "name": "manual_wheelchair", "keywords": ["vehicle"] }, - { "category": "travel_and_places", "char": "🦼", "name": "motorized_wheelchair", "keywords": ["vehicle"] }, - { "category": "travel_and_places", "char": "🛺", "name": "auto_rickshaw", "keywords": ["vehicle"] }, - { "category": "travel_and_places", "char": "🪂", "name": "parachute", "keywords": ["vehicle"] }, - { "category": "travel_and_places", "char": "🚨", "name": "rotating_light", "keywords": ["police", "ambulance", "911", "emergency", "alert", "error", "pinged", "law", "legal"] }, - { "category": "travel_and_places", "char": "🚔", "name": "oncoming_police_car", "keywords": ["vehicle", "law", "legal", "enforcement", "911"] }, - { "category": "travel_and_places", "char": "🚍", "name": "oncoming_bus", "keywords": ["vehicle", "transportation"] }, - { "category": "travel_and_places", "char": "🚘", "name": "oncoming_automobile", "keywords": ["car", "vehicle", "transportation"] }, - { "category": "travel_and_places", "char": "🚖", "name": "oncoming_taxi", "keywords": ["vehicle", "cars", "uber"] }, - { "category": "travel_and_places", "char": "🚡", "name": "aerial_tramway", "keywords": ["transportation", "vehicle", "ski"] }, - { "category": "travel_and_places", "char": "🚠", "name": "mountain_cableway", "keywords": ["transportation", "vehicle", "ski"] }, - { "category": "travel_and_places", "char": "🚟", "name": "suspension_railway", "keywords": ["vehicle", "transportation"] }, - { "category": "travel_and_places", "char": "🚃", "name": "railway_car", "keywords": ["transportation", "vehicle", "train"] }, - { "category": "travel_and_places", "char": "🚋", "name": "train", "keywords": ["transportation", "vehicle", "carriage", "public", "travel"] }, - { "category": "travel_and_places", "char": "🚝", "name": "monorail", "keywords": ["transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🚄", "name": "bullettrain_side", "keywords": ["transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🚅", "name": "bullettrain_front", "keywords": ["transportation", "vehicle", "speed", "fast", "public", "travel"] }, - { "category": "travel_and_places", "char": "🚈", "name": "light_rail", "keywords": ["transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🚞", "name": "mountain_railway", "keywords": ["transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🚂", "name": "steam_locomotive", "keywords": ["transportation", "vehicle", "train"] }, - { "category": "travel_and_places", "char": "🚆", "name": "train2", "keywords": ["transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🚇", "name": "metro", "keywords": ["transportation", "blue-square", "mrt", "underground", "tube"] }, - { "category": "travel_and_places", "char": "🚊", "name": "tram", "keywords": ["transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🚉", "name": "station", "keywords": ["transportation", "vehicle", "public"] }, - { "category": "travel_and_places", "char": "🛸", "name": "flying_saucer", "keywords": ["transportation", "vehicle", "ufo"] }, - { "category": "travel_and_places", "char": "🚁", "name": "helicopter", "keywords": ["transportation", "vehicle", "fly"] }, - { "category": "travel_and_places", "char": "🛩", "name": "small_airplane", "keywords": ["flight", "transportation", "fly", "vehicle"] }, - { "category": "travel_and_places", "char": "✈️", "name": "airplane", "keywords": ["vehicle", "transportation", "flight", "fly"] }, - { "category": "travel_and_places", "char": "🛫", "name": "flight_departure", "keywords": ["airport", "flight", "landing"] }, - { "category": "travel_and_places", "char": "🛬", "name": "flight_arrival", "keywords": ["airport", "flight", "boarding"] }, - { "category": "travel_and_places", "char": "⛵", "name": "sailboat", "keywords": ["ship", "summer", "transportation", "water", "sailing"] }, - { "category": "travel_and_places", "char": "🛥", "name": "motor_boat", "keywords": ["ship"] }, - { "category": "travel_and_places", "char": "🚤", "name": "speedboat", "keywords": ["ship", "transportation", "vehicle", "summer"] }, - { "category": "travel_and_places", "char": "⛴", "name": "ferry", "keywords": ["boat", "ship", "yacht"] }, - { "category": "travel_and_places", "char": "🛳", "name": "passenger_ship", "keywords": ["yacht", "cruise", "ferry"] }, - { "category": "travel_and_places", "char": "🚀", "name": "rocket", "keywords": ["launch", "ship", "staffmode", "NASA", "outer space", "outer_space", "fly"] }, - { "category": "travel_and_places", "char": "🛰", "name": "artificial_satellite", "keywords": ["communication", "gps", "orbit", "spaceflight", "NASA", "ISS"] }, - { "category": "travel_and_places", "char": "🛻", "name": "pickup_truck", "keywords": ["car"] }, - { "category": "travel_and_places", "char": "🛼", "name": "roller_skate", "keywords": [] }, - { "category": "travel_and_places", "char": "💺", "name": "seat", "keywords": ["sit", "airplane", "transport", "bus", "flight", "fly"] }, - { "category": "travel_and_places", "char": "🛶", "name": "canoe", "keywords": ["boat", "paddle", "water", "ship"] }, - { "category": "travel_and_places", "char": "⚓", "name": "anchor", "keywords": ["ship", "ferry", "sea", "boat"] }, - { "category": "travel_and_places", "char": "🚧", "name": "construction", "keywords": ["wip", "progress", "caution", "warning"] }, - { "category": "travel_and_places", "char": "⛽", "name": "fuelpump", "keywords": ["gas station", "petroleum"] }, - { "category": "travel_and_places", "char": "🚏", "name": "busstop", "keywords": ["transportation", "wait"] }, - { "category": "travel_and_places", "char": "🚦", "name": "vertical_traffic_light", "keywords": ["transportation", "driving"] }, - { "category": "travel_and_places", "char": "🚥", "name": "traffic_light", "keywords": ["transportation", "signal"] }, - { "category": "travel_and_places", "char": "🏁", "name": "checkered_flag", "keywords": ["contest", "finishline", "race", "gokart"] }, - { "category": "travel_and_places", "char": "🚢", "name": "ship", "keywords": ["transportation", "titanic", "deploy"] }, - { "category": "travel_and_places", "char": "🎡", "name": "ferris_wheel", "keywords": ["photo", "carnival", "londoneye"] }, - { "category": "travel_and_places", "char": "🎢", "name": "roller_coaster", "keywords": ["carnival", "playground", "photo", "fun"] }, - { "category": "travel_and_places", "char": "🎠", "name": "carousel_horse", "keywords": ["photo", "carnival"] }, - { "category": "travel_and_places", "char": "🏗", "name": "building_construction", "keywords": ["wip", "working", "progress"] }, - { "category": "travel_and_places", "char": "🌁", "name": "foggy", "keywords": ["photo", "mountain"] }, - { "category": "travel_and_places", "char": "🏭", "name": "factory", "keywords": ["building", "industry", "pollution", "smoke"] }, - { "category": "travel_and_places", "char": "⛲", "name": "fountain", "keywords": ["photo", "summer", "water", "fresh"] }, - { "category": "travel_and_places", "char": "🎑", "name": "rice_scene", "keywords": ["photo", "japan", "asia", "tsukimi"] }, - { "category": "travel_and_places", "char": "⛰", "name": "mountain", "keywords": ["photo", "nature", "environment"] }, - { "category": "travel_and_places", "char": "🏔", "name": "mountain_snow", "keywords": ["photo", "nature", "environment", "winter", "cold"] }, - { "category": "travel_and_places", "char": "🗻", "name": "mount_fuji", "keywords": ["photo", "mountain", "nature", "japanese"] }, - { "category": "travel_and_places", "char": "🌋", "name": "volcano", "keywords": ["photo", "nature", "disaster"] }, - { "category": "travel_and_places", "char": "🗾", "name": "japan", "keywords": ["nation", "country", "japanese", "asia"] }, - { "category": "travel_and_places", "char": "🏕", "name": "camping", "keywords": ["photo", "outdoors", "tent"] }, - { "category": "travel_and_places", "char": "⛺", "name": "tent", "keywords": ["photo", "camping", "outdoors"] }, - { "category": "travel_and_places", "char": "🏞", "name": "national_park", "keywords": ["photo", "environment", "nature"] }, - { "category": "travel_and_places", "char": "🛣", "name": "motorway", "keywords": ["road", "cupertino", "interstate", "highway"] }, - { "category": "travel_and_places", "char": "🛤", "name": "railway_track", "keywords": ["train", "transportation"] }, - { "category": "travel_and_places", "char": "🌅", "name": "sunrise", "keywords": ["morning", "view", "vacation", "photo"] }, - { "category": "travel_and_places", "char": "🌄", "name": "sunrise_over_mountains", "keywords": ["view", "vacation", "photo"] }, - { "category": "travel_and_places", "char": "🏜", "name": "desert", "keywords": ["photo", "warm", "saharah"] }, - { "category": "travel_and_places", "char": "🏖", "name": "beach_umbrella", "keywords": ["weather", "summer", "sunny", "sand", "mojito"] }, - { "category": "travel_and_places", "char": "🏝", "name": "desert_island", "keywords": ["photo", "tropical", "mojito"] }, - { "category": "travel_and_places", "char": "🌇", "name": "city_sunrise", "keywords": ["photo", "good morning", "dawn"] }, - { "category": "travel_and_places", "char": "🌆", "name": "city_sunset", "keywords": ["photo", "evening", "sky", "buildings"] }, - { "category": "travel_and_places", "char": "🏙", "name": "cityscape", "keywords": ["photo", "night life", "urban"] }, - { "category": "travel_and_places", "char": "🌃", "name": "night_with_stars", "keywords": ["evening", "city", "downtown"] }, - { "category": "travel_and_places", "char": "🌉", "name": "bridge_at_night", "keywords": ["photo", "sanfrancisco"] }, - { "category": "travel_and_places", "char": "🌌", "name": "milky_way", "keywords": ["photo", "space", "stars"] }, - { "category": "travel_and_places", "char": "🌠", "name": "stars", "keywords": ["night", "photo"] }, - { "category": "travel_and_places", "char": "🎇", "name": "sparkler", "keywords": ["stars", "night", "shine"] }, - { "category": "travel_and_places", "char": "🎆", "name": "fireworks", "keywords": ["photo", "festival", "carnival", "congratulations"] }, - { "category": "travel_and_places", "char": "🌈", "name": "rainbow", "keywords": ["nature", "happy", "unicorn_face", "photo", "sky", "spring"] }, - { "category": "travel_and_places", "char": "🏘", "name": "houses", "keywords": ["buildings", "photo"] }, - { "category": "travel_and_places", "char": "🏰", "name": "european_castle", "keywords": ["building", "royalty", "history"] }, - { "category": "travel_and_places", "char": "🏯", "name": "japanese_castle", "keywords": ["photo", "building"] }, - { "category": "travel_and_places", "char": "🗼", "name": "tokyo_tower", "keywords": ["photo", "japanese"] }, - { "category": "travel_and_places", "char": "", "name": "shibuya_109", "keywords": ["photo", "japanese"] }, - { "category": "travel_and_places", "char": "🏟", "name": "stadium", "keywords": ["photo", "place", "sports", "concert", "venue"] }, - { "category": "travel_and_places", "char": "🗽", "name": "statue_of_liberty", "keywords": ["american", "newyork"] }, - { "category": "travel_and_places", "char": "🏠", "name": "house", "keywords": ["building", "home"] }, - { "category": "travel_and_places", "char": "🏡", "name": "house_with_garden", "keywords": ["home", "plant", "nature"] }, - { "category": "travel_and_places", "char": "🏚", "name": "derelict_house", "keywords": ["abandon", "evict", "broken", "building"] }, - { "category": "travel_and_places", "char": "🏢", "name": "office", "keywords": ["building", "bureau", "work"] }, - { "category": "travel_and_places", "char": "🏬", "name": "department_store", "keywords": ["building", "shopping", "mall"] }, - { "category": "travel_and_places", "char": "🏣", "name": "post_office", "keywords": ["building", "envelope", "communication"] }, - { "category": "travel_and_places", "char": "🏤", "name": "european_post_office", "keywords": ["building", "email"] }, - { "category": "travel_and_places", "char": "🏥", "name": "hospital", "keywords": ["building", "health", "surgery", "doctor"] }, - { "category": "travel_and_places", "char": "🏦", "name": "bank", "keywords": ["building", "money", "sales", "cash", "business", "enterprise"] }, - { "category": "travel_and_places", "char": "🏨", "name": "hotel", "keywords": ["building", "accomodation", "checkin"] }, - { "category": "travel_and_places", "char": "🏪", "name": "convenience_store", "keywords": ["building", "shopping", "groceries"] }, - { "category": "travel_and_places", "char": "🏫", "name": "school", "keywords": ["building", "student", "education", "learn", "teach"] }, - { "category": "travel_and_places", "char": "🏩", "name": "love_hotel", "keywords": ["like", "affection", "dating"] }, - { "category": "travel_and_places", "char": "💒", "name": "wedding", "keywords": ["love", "like", "affection", "couple", "marriage", "bride", "groom"] }, - { "category": "travel_and_places", "char": "🏛", "name": "classical_building", "keywords": ["art", "culture", "history"] }, - { "category": "travel_and_places", "char": "⛪", "name": "church", "keywords": ["building", "religion", "christ"] }, - { "category": "travel_and_places", "char": "🕌", "name": "mosque", "keywords": ["islam", "worship", "minaret"] }, - { "category": "travel_and_places", "char": "🕍", "name": "synagogue", "keywords": ["judaism", "worship", "temple", "jewish"] }, - { "category": "travel_and_places", "char": "🕋", "name": "kaaba", "keywords": ["mecca", "mosque", "islam"] }, - { "category": "travel_and_places", "char": "⛩", "name": "shinto_shrine", "keywords": ["temple", "japan", "kyoto"] }, - { "category": "travel_and_places", "char": "🛕", "name": "hindu_temple", "keywords": ["temple"] }, - { "category": "travel_and_places", "char": "🪨", "name": "rock", "keywords": [] }, - { "category": "travel_and_places", "char": "🪵", "name": "wood", "keywords": [] }, - { "category": "travel_and_places", "char": "🛖", "name": "hut", "keywords": [] }, - { "category": "travel_and_places", "char": "\uD83D\uDEDD", "name": "playground_slide", "keywords": [] }, - { "category": "travel_and_places", "char": "\uD83D\uDEDE", "name": "wheel", "keywords": [] }, - { "category": "travel_and_places", "char": "\uD83D\uDEDF", "name": "ring_buoy", "keywords": [] }, - { "category": "objects", "char": "⌚", "name": "watch", "keywords": ["time", "accessories"] }, - { "category": "objects", "char": "📱", "name": "iphone", "keywords": ["technology", "apple", "gadgets", "dial"] }, - { "category": "objects", "char": "📲", "name": "calling", "keywords": ["iphone", "incoming"] }, - { "category": "objects", "char": "💻", "name": "computer", "keywords": ["technology", "laptop", "screen", "display", "monitor"] }, - { "category": "objects", "char": "⌨", "name": "keyboard", "keywords": ["technology", "computer", "type", "input", "text"] }, - { "category": "objects", "char": "🖥", "name": "desktop_computer", "keywords": ["technology", "computing", "screen"] }, - { "category": "objects", "char": "🖨", "name": "printer", "keywords": ["paper", "ink"] }, - { "category": "objects", "char": "🖱", "name": "computer_mouse", "keywords": ["click"] }, - { "category": "objects", "char": "🖲", "name": "trackball", "keywords": ["technology", "trackpad"] }, - { "category": "objects", "char": "🕹", "name": "joystick", "keywords": ["game", "play"] }, - { "category": "objects", "char": "🗜", "name": "clamp", "keywords": ["tool"] }, - { "category": "objects", "char": "💽", "name": "minidisc", "keywords": ["technology", "record", "data", "disk", "90s"] }, - { "category": "objects", "char": "💾", "name": "floppy_disk", "keywords": ["oldschool", "technology", "save", "90s", "80s"] }, - { "category": "objects", "char": "💿", "name": "cd", "keywords": ["technology", "dvd", "disk", "disc", "90s"] }, - { "category": "objects", "char": "📀", "name": "dvd", "keywords": ["cd", "disk", "disc"] }, - { "category": "objects", "char": "📼", "name": "vhs", "keywords": ["record", "video", "oldschool", "90s", "80s"] }, - { "category": "objects", "char": "📷", "name": "camera", "keywords": ["gadgets", "photography"] }, - { "category": "objects", "char": "📸", "name": "camera_flash", "keywords": ["photography", "gadgets"] }, - { "category": "objects", "char": "📹", "name": "video_camera", "keywords": ["film", "record"] }, - { "category": "objects", "char": "🎥", "name": "movie_camera", "keywords": ["film", "record"] }, - { "category": "objects", "char": "📽", "name": "film_projector", "keywords": ["video", "tape", "record", "movie"] }, - { "category": "objects", "char": "🎞", "name": "film_strip", "keywords": ["movie"] }, - { "category": "objects", "char": "📞", "name": "telephone_receiver", "keywords": ["technology", "communication", "dial"] }, - { "category": "objects", "char": "☎️", "name": "phone", "keywords": ["technology", "communication", "dial", "telephone"] }, - { "category": "objects", "char": "📟", "name": "pager", "keywords": ["bbcall", "oldschool", "90s"] }, - { "category": "objects", "char": "📠", "name": "fax", "keywords": ["communication", "technology"] }, - { "category": "objects", "char": "📺", "name": "tv", "keywords": ["technology", "program", "oldschool", "show", "television"] }, - { "category": "objects", "char": "📻", "name": "radio", "keywords": ["communication", "music", "podcast", "program"] }, - { "category": "objects", "char": "🎙", "name": "studio_microphone", "keywords": ["sing", "recording", "artist", "talkshow"] }, - { "category": "objects", "char": "🎚", "name": "level_slider", "keywords": ["scale"] }, - { "category": "objects", "char": "🎛", "name": "control_knobs", "keywords": ["dial"] }, - { "category": "objects", "char": "🧭", "name": "compass", "keywords": ["magnetic", "navigation", "orienteering"] }, - { "category": "objects", "char": "⏱", "name": "stopwatch", "keywords": ["time", "deadline"] }, - { "category": "objects", "char": "⏲", "name": "timer_clock", "keywords": ["alarm"] }, - { "category": "objects", "char": "⏰", "name": "alarm_clock", "keywords": ["time", "wake"] }, - { "category": "objects", "char": "🕰", "name": "mantelpiece_clock", "keywords": ["time"] }, - { "category": "objects", "char": "⏳", "name": "hourglass_flowing_sand", "keywords": ["oldschool", "time", "countdown"] }, - { "category": "objects", "char": "⌛", "name": "hourglass", "keywords": ["time", "clock", "oldschool", "limit", "exam", "quiz", "test"] }, - { "category": "objects", "char": "📡", "name": "satellite", "keywords": ["communication", "future", "radio", "space"] }, - { "category": "objects", "char": "🔋", "name": "battery", "keywords": ["power", "energy", "sustain"] }, - { "category": "objects", "char": "\uD83E\uDEAB", "name": "battery", "keywords": [] }, - { "category": "objects", "char": "🔌", "name": "electric_plug", "keywords": ["charger", "power"] }, - { "category": "objects", "char": "💡", "name": "bulb", "keywords": ["light", "electricity", "idea"] }, - { "category": "objects", "char": "🔦", "name": "flashlight", "keywords": ["dark", "camping", "sight", "night"] }, - { "category": "objects", "char": "🕯", "name": "candle", "keywords": ["fire", "wax"] }, - { "category": "objects", "char": "🧯", "name": "fire_extinguisher", "keywords": ["quench"] }, - { "category": "objects", "char": "🗑", "name": "wastebasket", "keywords": ["bin", "trash", "rubbish", "garbage", "toss"] }, - { "category": "objects", "char": "🛢", "name": "oil_drum", "keywords": ["barrell"] }, - { "category": "objects", "char": "💸", "name": "money_with_wings", "keywords": ["dollar", "bills", "payment", "sale"] }, - { "category": "objects", "char": "💵", "name": "dollar", "keywords": ["money", "sales", "bill", "currency"] }, - { "category": "objects", "char": "💴", "name": "yen", "keywords": ["money", "sales", "japanese", "dollar", "currency"] }, - { "category": "objects", "char": "💶", "name": "euro", "keywords": ["money", "sales", "dollar", "currency"] }, - { "category": "objects", "char": "💷", "name": "pound", "keywords": ["british", "sterling", "money", "sales", "bills", "uk", "england", "currency"] }, - { "category": "objects", "char": "💰", "name": "moneybag", "keywords": ["dollar", "payment", "coins", "sale"] }, - { "category": "objects", "char": "🪙", "name": "coin", "keywords": ["dollar", "payment", "coins", "sale"] }, - { "category": "objects", "char": "💳", "name": "credit_card", "keywords": ["money", "sales", "dollar", "bill", "payment", "shopping"] }, - { "category": "objects", "char": "\uD83E\uDEAB", "name": "identification_card", "keywords": [] }, - { "category": "objects", "char": "💎", "name": "gem", "keywords": ["blue", "ruby", "diamond", "jewelry"] }, - { "category": "objects", "char": "⚖", "name": "balance_scale", "keywords": ["law", "fairness", "weight"] }, - { "category": "objects", "char": "🧰", "name": "toolbox", "keywords": ["tools", "diy", "fix", "maintainer", "mechanic"] }, - { "category": "objects", "char": "🔧", "name": "wrench", "keywords": ["tools", "diy", "ikea", "fix", "maintainer"] }, - { "category": "objects", "char": "🔨", "name": "hammer", "keywords": ["tools", "build", "create"] }, - { "category": "objects", "char": "⚒", "name": "hammer_and_pick", "keywords": ["tools", "build", "create"] }, - { "category": "objects", "char": "🛠", "name": "hammer_and_wrench", "keywords": ["tools", "build", "create"] }, - { "category": "objects", "char": "⛏", "name": "pick", "keywords": ["tools", "dig"] }, - { "category": "objects", "char": "🪓", "name": "axe", "keywords": ["tools"] }, - { "category": "objects", "char": "🦯", "name": "probing_cane", "keywords": ["tools"] }, - { "category": "objects", "char": "🔩", "name": "nut_and_bolt", "keywords": ["handy", "tools", "fix"] }, - { "category": "objects", "char": "⚙", "name": "gear", "keywords": ["cog"] }, - { "category": "objects", "char": "🪃", "name": "boomerang", "keywords": ["tool"] }, - { "category": "objects", "char": "🪚", "name": "carpentry_saw", "keywords": ["tool"] }, - { "category": "objects", "char": "🪛", "name": "screwdriver", "keywords": ["tool"] }, - { "category": "objects", "char": "🪝", "name": "hook", "keywords": ["tool"] }, - { "category": "objects", "char": "🪜", "name": "ladder", "keywords": ["tool"] }, - { "category": "objects", "char": "🧱", "name": "brick", "keywords": ["bricks"] }, - { "category": "objects", "char": "⛓", "name": "chains", "keywords": ["lock", "arrest"] }, - { "category": "objects", "char": "🧲", "name": "magnet", "keywords": ["attraction", "magnetic"] }, - { "category": "objects", "char": "🔫", "name": "gun", "keywords": ["violence", "weapon", "pistol", "revolver"] }, - { "category": "objects", "char": "💣", "name": "bomb", "keywords": ["boom", "explode", "explosion", "terrorism"] }, - { "category": "objects", "char": "🧨", "name": "firecracker", "keywords": ["dynamite", "boom", "explode", "explosion", "explosive"] }, - { "category": "objects", "char": "🔪", "name": "hocho", "keywords": ["knife", "blade", "cutlery", "kitchen", "weapon"] }, - { "category": "objects", "char": "🗡", "name": "dagger", "keywords": ["weapon"] }, - { "category": "objects", "char": "⚔", "name": "crossed_swords", "keywords": ["weapon"] }, - { "category": "objects", "char": "🛡", "name": "shield", "keywords": ["protection", "security"] }, - { "category": "objects", "char": "🚬", "name": "smoking", "keywords": ["kills", "tobacco", "cigarette", "joint", "smoke"] }, - { "category": "objects", "char": "☠", "name": "skull_and_crossbones", "keywords": ["poison", "danger", "deadly", "scary", "death", "pirate", "evil"] }, - { "category": "objects", "char": "⚰", "name": "coffin", "keywords": ["vampire", "dead", "die", "death", "rip", "graveyard", "cemetery", "casket", "funeral", "box"] }, - { "category": "objects", "char": "⚱", "name": "funeral_urn", "keywords": ["dead", "die", "death", "rip", "ashes"] }, - { "category": "objects", "char": "🏺", "name": "amphora", "keywords": ["vase", "jar"] }, - { "category": "objects", "char": "🔮", "name": "crystal_ball", "keywords": ["disco", "party", "magic", "circus", "fortune_teller"] }, - { "category": "objects", "char": "📿", "name": "prayer_beads", "keywords": ["dhikr", "religious"] }, - { "category": "objects", "char": "🧿", "name": "nazar_amulet", "keywords": ["bead", "charm"] }, - { "category": "objects", "char": "💈", "name": "barber", "keywords": ["hair", "salon", "style"] }, - { "category": "objects", "char": "⚗", "name": "alembic", "keywords": ["distilling", "science", "experiment", "chemistry"] }, - { "category": "objects", "char": "🔭", "name": "telescope", "keywords": ["stars", "space", "zoom", "science", "astronomy"] }, - { "category": "objects", "char": "🔬", "name": "microscope", "keywords": ["laboratory", "experiment", "zoomin", "science", "study"] }, - { "category": "objects", "char": "🕳", "name": "hole", "keywords": ["embarrassing"] }, - { "category": "objects", "char": "💊", "name": "pill", "keywords": ["health", "medicine", "doctor", "pharmacy", "drug"] }, - { "category": "objects", "char": "💉", "name": "syringe", "keywords": ["health", "hospital", "drugs", "blood", "medicine", "needle", "doctor", "nurse"] }, - { "category": "objects", "char": "🩸", "name": "drop_of_blood", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] }, - { "category": "objects", "char": "🩹", "name": "adhesive_bandage", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] }, - { "category": "objects", "char": "🩺", "name": "stethoscope", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] }, - { "category": "objects", "char": "🪒", "name": "razor", "keywords": ["health"] }, - { "category": "objects", "char": "\uD83E\uDE7B", "name": "xray", "keywords": [] }, - { "category": "objects", "char": "\uD83E\uDE7C", "name": "crutch", "keywords": [] }, - { "category": "objects", "char": "🧬", "name": "dna", "keywords": ["biologist", "genetics", "life"] }, - { "category": "objects", "char": "🧫", "name": "petri_dish", "keywords": ["bacteria", "biology", "culture", "lab"] }, - { "category": "objects", "char": "🧪", "name": "test_tube", "keywords": ["chemistry", "experiment", "lab", "science"] }, - { "category": "objects", "char": "🌡", "name": "thermometer", "keywords": ["weather", "temperature", "hot", "cold"] }, - { "category": "objects", "char": "🧹", "name": "broom", "keywords": ["cleaning", "sweeping", "witch"] }, - { "category": "objects", "char": "🧺", "name": "basket", "keywords": ["laundry"] }, - { "category": "objects", "char": "🧻", "name": "toilet_paper", "keywords": ["roll"] }, - { "category": "objects", "char": "🏷", "name": "label", "keywords": ["sale", "tag"] }, - { "category": "objects", "char": "🔖", "name": "bookmark", "keywords": ["favorite", "label", "save"] }, - { "category": "objects", "char": "🚽", "name": "toilet", "keywords": ["restroom", "wc", "washroom", "bathroom", "potty"] }, - { "category": "objects", "char": "🚿", "name": "shower", "keywords": ["clean", "water", "bathroom"] }, - { "category": "objects", "char": "🛁", "name": "bathtub", "keywords": ["clean", "shower", "bathroom"] }, - { "category": "objects", "char": "🧼", "name": "soap", "keywords": ["bar", "bathing", "cleaning", "lather"] }, - { "category": "objects", "char": "🧽", "name": "sponge", "keywords": ["absorbing", "cleaning", "porous"] }, - { "category": "objects", "char": "🧴", "name": "lotion_bottle", "keywords": ["moisturizer", "sunscreen"] }, - { "category": "objects", "char": "🔑", "name": "key", "keywords": ["lock", "door", "password"] }, - { "category": "objects", "char": "🗝", "name": "old_key", "keywords": ["lock", "door", "password"] }, - { "category": "objects", "char": "🛋", "name": "couch_and_lamp", "keywords": ["read", "chill"] }, - { "category": "objects", "char": "🪔", "name": "diya_Lamp", "keywords": ["light", "oil"] }, - { "category": "objects", "char": "🛌", "name": "sleeping_bed", "keywords": ["bed", "rest"] }, - { "category": "objects", "char": "🛏", "name": "bed", "keywords": ["sleep", "rest"] }, - { "category": "objects", "char": "🚪", "name": "door", "keywords": ["house", "entry", "exit"] }, - { "category": "objects", "char": "🪑", "name": "chair", "keywords": ["house", "desk"] }, - { "category": "objects", "char": "🛎", "name": "bellhop_bell", "keywords": ["service"] }, - { "category": "objects", "char": "🧸", "name": "teddy_bear", "keywords": ["plush", "stuffed"] }, - { "category": "objects", "char": "🖼", "name": "framed_picture", "keywords": ["photography"] }, - { "category": "objects", "char": "🗺", "name": "world_map", "keywords": ["location", "direction"] }, - { "category": "objects", "char": "🛗", "name": "elevator", "keywords": ["household"] }, - { "category": "objects", "char": "🪞", "name": "mirror", "keywords": ["household"] }, - { "category": "objects", "char": "🪟", "name": "window", "keywords": ["household"] }, - { "category": "objects", "char": "🪠", "name": "plunger", "keywords": ["household"] }, - { "category": "objects", "char": "🪤", "name": "mouse_trap", "keywords": ["household"] }, - { "category": "objects", "char": "🪣", "name": "bucket", "keywords": ["household"] }, - { "category": "objects", "char": "🪥", "name": "toothbrush", "keywords": ["household"] }, - { "category": "objects", "char": "\uD83E\uDEE7", "name": "bubbles", "keywords": [] }, - { "category": "objects", "char": "⛱", "name": "parasol_on_ground", "keywords": ["weather", "summer"] }, - { "category": "objects", "char": "🗿", "name": "moyai", "keywords": ["rock", "easter island", "moai"] }, - { "category": "objects", "char": "🛍", "name": "shopping", "keywords": ["mall", "buy", "purchase"] }, - { "category": "objects", "char": "🛒", "name": "shopping_cart", "keywords": ["trolley"] }, - { "category": "objects", "char": "🎈", "name": "balloon", "keywords": ["party", "celebration", "birthday", "circus"] }, - { "category": "objects", "char": "🎏", "name": "flags", "keywords": ["fish", "japanese", "koinobori", "carp", "banner"] }, - { "category": "objects", "char": "🎀", "name": "ribbon", "keywords": ["decoration", "pink", "girl", "bowtie"] }, - { "category": "objects", "char": "🎁", "name": "gift", "keywords": ["present", "birthday", "christmas", "xmas"] }, - { "category": "objects", "char": "🎊", "name": "confetti_ball", "keywords": ["festival", "party", "birthday", "circus"] }, - { "category": "objects", "char": "🎉", "name": "tada", "keywords": ["party", "congratulations", "birthday", "magic", "circus", "celebration"] }, - { "category": "objects", "char": "🎎", "name": "dolls", "keywords": ["japanese", "toy", "kimono"] }, - { "category": "objects", "char": "🎐", "name": "wind_chime", "keywords": ["nature", "ding", "spring", "bell"] }, - { "category": "objects", "char": "🎌", "name": "crossed_flags", "keywords": ["japanese", "nation", "country", "border"] }, - { "category": "objects", "char": "🏮", "name": "izakaya_lantern", "keywords": ["light", "paper", "halloween", "spooky"] }, - { "category": "objects", "char": "🧧", "name": "red_envelope", "keywords": ["gift"] }, - { "category": "objects", "char": "✉️", "name": "email", "keywords": ["letter", "postal", "inbox", "communication"] }, - { "category": "objects", "char": "📩", "name": "envelope_with_arrow", "keywords": ["email", "communication"] }, - { "category": "objects", "char": "📨", "name": "incoming_envelope", "keywords": ["email", "inbox"] }, - { "category": "objects", "char": "📧", "name": "e-mail", "keywords": ["communication", "inbox"] }, - { "category": "objects", "char": "💌", "name": "love_letter", "keywords": ["email", "like", "affection", "envelope", "valentines"] }, - { "category": "objects", "char": "📮", "name": "postbox", "keywords": ["email", "letter", "envelope"] }, - { "category": "objects", "char": "📪", "name": "mailbox_closed", "keywords": ["email", "communication", "inbox"] }, - { "category": "objects", "char": "📫", "name": "mailbox", "keywords": ["email", "inbox", "communication"] }, - { "category": "objects", "char": "📬", "name": "mailbox_with_mail", "keywords": ["email", "inbox", "communication"] }, - { "category": "objects", "char": "📭", "name": "mailbox_with_no_mail", "keywords": ["email", "inbox"] }, - { "category": "objects", "char": "📦", "name": "package", "keywords": ["mail", "gift", "cardboard", "box", "moving"] }, - { "category": "objects", "char": "📯", "name": "postal_horn", "keywords": ["instrument", "music"] }, - { "category": "objects", "char": "📥", "name": "inbox_tray", "keywords": ["email", "documents"] }, - { "category": "objects", "char": "📤", "name": "outbox_tray", "keywords": ["inbox", "email"] }, - { "category": "objects", "char": "📜", "name": "scroll", "keywords": ["documents", "ancient", "history", "paper"] }, - { "category": "objects", "char": "📃", "name": "page_with_curl", "keywords": ["documents", "office", "paper"] }, - { "category": "objects", "char": "📑", "name": "bookmark_tabs", "keywords": ["favorite", "save", "order", "tidy"] }, - { "category": "objects", "char": "🧾", "name": "receipt", "keywords": ["accounting", "expenses"] }, - { "category": "objects", "char": "📊", "name": "bar_chart", "keywords": ["graph", "presentation", "stats"] }, - { "category": "objects", "char": "📈", "name": "chart_with_upwards_trend", "keywords": ["graph", "presentation", "stats", "recovery", "business", "economics", "money", "sales", "good", "success"] }, - { "category": "objects", "char": "📉", "name": "chart_with_downwards_trend", "keywords": ["graph", "presentation", "stats", "recession", "business", "economics", "money", "sales", "bad", "failure"] }, - { "category": "objects", "char": "📄", "name": "page_facing_up", "keywords": ["documents", "office", "paper", "information"] }, - { "category": "objects", "char": "📅", "name": "date", "keywords": ["calendar", "schedule"] }, - { "category": "objects", "char": "📆", "name": "calendar", "keywords": ["schedule", "date", "planning"] }, - { "category": "objects", "char": "🗓", "name": "spiral_calendar", "keywords": ["date", "schedule", "planning"] }, - { "category": "objects", "char": "📇", "name": "card_index", "keywords": ["business", "stationery"] }, - { "category": "objects", "char": "🗃", "name": "card_file_box", "keywords": ["business", "stationery"] }, - { "category": "objects", "char": "🗳", "name": "ballot_box", "keywords": ["election", "vote"] }, - { "category": "objects", "char": "🗄", "name": "file_cabinet", "keywords": ["filing", "organizing"] }, - { "category": "objects", "char": "📋", "name": "clipboard", "keywords": ["stationery", "documents"] }, - { "category": "objects", "char": "🗒", "name": "spiral_notepad", "keywords": ["memo", "stationery"] }, - { "category": "objects", "char": "📁", "name": "file_folder", "keywords": ["documents", "business", "office"] }, - { "category": "objects", "char": "📂", "name": "open_file_folder", "keywords": ["documents", "load"] }, - { "category": "objects", "char": "🗂", "name": "card_index_dividers", "keywords": ["organizing", "business", "stationery"] }, - { "category": "objects", "char": "🗞", "name": "newspaper_roll", "keywords": ["press", "headline"] }, - { "category": "objects", "char": "📰", "name": "newspaper", "keywords": ["press", "headline"] }, - { "category": "objects", "char": "📓", "name": "notebook", "keywords": ["stationery", "record", "notes", "paper", "study"] }, - { "category": "objects", "char": "📕", "name": "closed_book", "keywords": ["read", "library", "knowledge", "textbook", "learn"] }, - { "category": "objects", "char": "📗", "name": "green_book", "keywords": ["read", "library", "knowledge", "study"] }, - { "category": "objects", "char": "📘", "name": "blue_book", "keywords": ["read", "library", "knowledge", "learn", "study"] }, - { "category": "objects", "char": "📙", "name": "orange_book", "keywords": ["read", "library", "knowledge", "textbook", "study"] }, - { "category": "objects", "char": "📔", "name": "notebook_with_decorative_cover", "keywords": ["classroom", "notes", "record", "paper", "study"] }, - { "category": "objects", "char": "📒", "name": "ledger", "keywords": ["notes", "paper"] }, - { "category": "objects", "char": "📚", "name": "books", "keywords": ["literature", "library", "study"] }, - { "category": "objects", "char": "📖", "name": "open_book", "keywords": ["book", "read", "library", "knowledge", "literature", "learn", "study"] }, - { "category": "objects", "char": "🧷", "name": "safety_pin", "keywords": ["diaper"] }, - { "category": "objects", "char": "🔗", "name": "link", "keywords": ["rings", "url"] }, - { "category": "objects", "char": "📎", "name": "paperclip", "keywords": ["documents", "stationery"] }, - { "category": "objects", "char": "🖇", "name": "paperclips", "keywords": ["documents", "stationery"] }, - { "category": "objects", "char": "✂️", "name": "scissors", "keywords": ["stationery", "cut"] }, - { "category": "objects", "char": "📐", "name": "triangular_ruler", "keywords": ["stationery", "math", "architect", "sketch"] }, - { "category": "objects", "char": "📏", "name": "straight_ruler", "keywords": ["stationery", "calculate", "length", "math", "school", "drawing", "architect", "sketch"] }, - { "category": "objects", "char": "🧮", "name": "abacus", "keywords": ["calculation"] }, - { "category": "objects", "char": "📌", "name": "pushpin", "keywords": ["stationery", "mark", "here"] }, - { "category": "objects", "char": "📍", "name": "round_pushpin", "keywords": ["stationery", "location", "map", "here"] }, - { "category": "objects", "char": "🚩", "name": "triangular_flag_on_post", "keywords": ["mark", "milestone", "place"] }, - { "category": "objects", "char": "🏳", "name": "white_flag", "keywords": ["losing", "loser", "lost", "surrender", "give up", "fail"] }, - { "category": "objects", "char": "🏴", "name": "black_flag", "keywords": ["pirate"] }, - { "category": "objects", "char": "🏳️🌈", "name": "rainbow_flag", "keywords": ["flag", "rainbow", "pride", "gay", "lgbt", "glbt", "queer", "homosexual", "lesbian", "bisexual", "transgender"] }, - { "category": "objects", "char": "🏳️⚧️", "name": "transgender_flag", "keywords": ["flag", "transgender"] }, - { "category": "objects", "char": "🔐", "name": "closed_lock_with_key", "keywords": ["security", "privacy"] }, - { "category": "objects", "char": "🔒", "name": "lock", "keywords": ["security", "password", "padlock"] }, - { "category": "objects", "char": "🔓", "name": "unlock", "keywords": ["privacy", "security"] }, - { "category": "objects", "char": "🔏", "name": "lock_with_ink_pen", "keywords": ["security", "secret"] }, - { "category": "objects", "char": "🖊", "name": "pen", "keywords": ["stationery", "writing", "write"] }, - { "category": "objects", "char": "🖋", "name": "fountain_pen", "keywords": ["stationery", "writing", "write"] }, - { "category": "objects", "char": "✒️", "name": "black_nib", "keywords": ["pen", "stationery", "writing", "write"] }, - { "category": "objects", "char": "📝", "name": "memo", "keywords": ["write", "documents", "stationery", "pencil", "paper", "writing", "legal", "exam", "quiz", "test", "study", "compose"] }, - { "category": "objects", "char": "✏️", "name": "pencil2", "keywords": ["stationery", "write", "paper", "writing", "school", "study"] }, - { "category": "objects", "char": "🖍", "name": "crayon", "keywords": ["drawing", "creativity"] }, - { "category": "objects", "char": "🖌", "name": "paintbrush", "keywords": ["drawing", "creativity", "art"] }, - { "category": "objects", "char": "🔍", "name": "mag", "keywords": ["search", "zoom", "find", "detective"] }, - { "category": "objects", "char": "🔎", "name": "mag_right", "keywords": ["search", "zoom", "find", "detective"] }, - { "category": "objects", "char": "🪦", "name": "headstone", "keywords": [] }, - { "category": "objects", "char": "🪧", "name": "placard", "keywords": [] }, - { "category": "symbols", "char": "💯", "name": "100", "keywords": ["score", "perfect", "numbers", "century", "exam", "quiz", "test", "pass", "hundred"] }, - { "category": "symbols", "char": "🔢", "name": "1234", "keywords": ["numbers", "blue-square"] }, - { "category": "symbols", "char": "❤️", "name": "heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "🧡", "name": "orange_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "💛", "name": "yellow_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "💚", "name": "green_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "💙", "name": "blue_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "💜", "name": "purple_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "🤎", "name": "brown_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "🖤", "name": "black_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "🤍", "name": "white_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "💔", "name": "broken_heart", "keywords": ["sad", "sorry", "break", "heart", "heartbreak"] }, - { "category": "symbols", "char": "❣", "name": "heavy_heart_exclamation", "keywords": ["decoration", "love"] }, - { "category": "symbols", "char": "💕", "name": "two_hearts", "keywords": ["love", "like", "affection", "valentines", "heart"] }, - { "category": "symbols", "char": "💞", "name": "revolving_hearts", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "💓", "name": "heartbeat", "keywords": ["love", "like", "affection", "valentines", "pink", "heart"] }, - { "category": "symbols", "char": "💗", "name": "heartpulse", "keywords": ["like", "love", "affection", "valentines", "pink"] }, - { "category": "symbols", "char": "💖", "name": "sparkling_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "💘", "name": "cupid", "keywords": ["love", "like", "heart", "affection", "valentines"] }, - { "category": "symbols", "char": "💝", "name": "gift_heart", "keywords": ["love", "valentines"] }, - { "category": "symbols", "char": "💟", "name": "heart_decoration", "keywords": ["purple-square", "love", "like"] }, - { "category": "symbols", "char": "\u2764\uFE0F\u200D\uD83D\uDD25", "name": "heart_on_fire", "keywords": [] }, - { "category": "symbols", "char": "\u2764\uFE0F\u200D\uD83E\uDE79", "name": "mending_heart", "keywords": [] }, - { "category": "symbols", "char": "☮", "name": "peace_symbol", "keywords": ["hippie"] }, - { "category": "symbols", "char": "✝", "name": "latin_cross", "keywords": ["christianity"] }, - { "category": "symbols", "char": "☪", "name": "star_and_crescent", "keywords": ["islam"] }, - { "category": "symbols", "char": "🕉", "name": "om", "keywords": ["hinduism", "buddhism", "sikhism", "jainism"] }, - { "category": "symbols", "char": "☸", "name": "wheel_of_dharma", "keywords": ["hinduism", "buddhism", "sikhism", "jainism"] }, - { "category": "symbols", "char": "✡", "name": "star_of_david", "keywords": ["judaism"] }, - { "category": "symbols", "char": "🔯", "name": "six_pointed_star", "keywords": ["purple-square", "religion", "jewish", "hexagram"] }, - { "category": "symbols", "char": "🕎", "name": "menorah", "keywords": ["hanukkah", "candles", "jewish"] }, - { "category": "symbols", "char": "☯", "name": "yin_yang", "keywords": ["balance"] }, - { "category": "symbols", "char": "☦", "name": "orthodox_cross", "keywords": ["suppedaneum", "religion"] }, - { "category": "symbols", "char": "🛐", "name": "place_of_worship", "keywords": ["religion", "church", "temple", "prayer"] }, - { "category": "symbols", "char": "⛎", "name": "ophiuchus", "keywords": ["sign", "purple-square", "constellation", "astrology"] }, - { "category": "symbols", "char": "♈", "name": "aries", "keywords": ["sign", "purple-square", "zodiac", "astrology"] }, - { "category": "symbols", "char": "♉", "name": "taurus", "keywords": ["purple-square", "sign", "zodiac", "astrology"] }, - { "category": "symbols", "char": "♊", "name": "gemini", "keywords": ["sign", "zodiac", "purple-square", "astrology"] }, - { "category": "symbols", "char": "♋", "name": "cancer", "keywords": ["sign", "zodiac", "purple-square", "astrology"] }, - { "category": "symbols", "char": "♌", "name": "leo", "keywords": ["sign", "purple-square", "zodiac", "astrology"] }, - { "category": "symbols", "char": "♍", "name": "virgo", "keywords": ["sign", "zodiac", "purple-square", "astrology"] }, - { "category": "symbols", "char": "♎", "name": "libra", "keywords": ["sign", "purple-square", "zodiac", "astrology"] }, - { "category": "symbols", "char": "♏", "name": "scorpius", "keywords": ["sign", "zodiac", "purple-square", "astrology", "scorpio"] }, - { "category": "symbols", "char": "♐", "name": "sagittarius", "keywords": ["sign", "zodiac", "purple-square", "astrology"] }, - { "category": "symbols", "char": "♑", "name": "capricorn", "keywords": ["sign", "zodiac", "purple-square", "astrology"] }, - { "category": "symbols", "char": "♒", "name": "aquarius", "keywords": ["sign", "purple-square", "zodiac", "astrology"] }, - { "category": "symbols", "char": "♓", "name": "pisces", "keywords": ["purple-square", "sign", "zodiac", "astrology"] }, - { "category": "symbols", "char": "🆔", "name": "id", "keywords": ["purple-square", "words"] }, - { "category": "symbols", "char": "⚛", "name": "atom_symbol", "keywords": ["science", "physics", "chemistry"] }, - { "category": "symbols", "char": "⚧️", "name": "transgender_symbol", "keywords": ["purple-square", "woman", "female", "toilet", "loo", "restroom", "gender"] }, - { "category": "symbols", "char": "🈳", "name": "u7a7a", "keywords": ["kanji", "japanese", "chinese", "empty", "sky", "blue-square", "aki"] }, - { "category": "symbols", "char": "🈹", "name": "u5272", "keywords": ["cut", "divide", "chinese", "kanji", "pink-square", "waribiki"] }, - { "category": "symbols", "char": "☢", "name": "radioactive", "keywords": ["nuclear", "danger"] }, - { "category": "symbols", "char": "☣", "name": "biohazard", "keywords": ["danger"] }, - { "category": "symbols", "char": "📴", "name": "mobile_phone_off", "keywords": ["mute", "orange-square", "silence", "quiet"] }, - { "category": "symbols", "char": "📳", "name": "vibration_mode", "keywords": ["orange-square", "phone"] }, - { "category": "symbols", "char": "🈶", "name": "u6709", "keywords": ["orange-square", "chinese", "have", "kanji", "ari"] }, - { "category": "symbols", "char": "🈚", "name": "u7121", "keywords": ["nothing", "chinese", "kanji", "japanese", "orange-square", "nashi"] }, - { "category": "symbols", "char": "🈸", "name": "u7533", "keywords": ["chinese", "japanese", "kanji", "orange-square", "moushikomi"] }, - { "category": "symbols", "char": "🈺", "name": "u55b6", "keywords": ["japanese", "opening hours", "orange-square", "eigyo"] }, - { "category": "symbols", "char": "🈷️", "name": "u6708", "keywords": ["chinese", "month", "moon", "japanese", "orange-square", "kanji", "tsuki", "tsukigime", "getsugaku"] }, - { "category": "symbols", "char": "✴️", "name": "eight_pointed_black_star", "keywords": ["orange-square", "shape", "polygon"] }, - { "category": "symbols", "char": "🆚", "name": "vs", "keywords": ["words", "orange-square"] }, - { "category": "symbols", "char": "🉑", "name": "accept", "keywords": ["ok", "good", "chinese", "kanji", "agree", "yes", "orange-circle"] }, - { "category": "symbols", "char": "💮", "name": "white_flower", "keywords": ["japanese", "spring"] }, - { "category": "symbols", "char": "🉐", "name": "ideograph_advantage", "keywords": ["chinese", "kanji", "obtain", "get", "circle"] }, - { "category": "symbols", "char": "㊙️", "name": "secret", "keywords": ["privacy", "chinese", "sshh", "kanji", "red-circle"] }, - { "category": "symbols", "char": "㊗️", "name": "congratulations", "keywords": ["chinese", "kanji", "japanese", "red-circle"] }, - { "category": "symbols", "char": "🈴", "name": "u5408", "keywords": ["japanese", "chinese", "join", "kanji", "red-square", "goukaku", "pass"] }, - { "category": "symbols", "char": "🈵", "name": "u6e80", "keywords": ["full", "chinese", "japanese", "red-square", "kanji", "man"] }, - { "category": "symbols", "char": "🈲", "name": "u7981", "keywords": ["kanji", "japanese", "chinese", "forbidden", "limit", "restricted", "red-square", "kinshi"] }, - { "category": "symbols", "char": "🅰️", "name": "a", "keywords": ["red-square", "alphabet", "letter"] }, - { "category": "symbols", "char": "🅱️", "name": "b", "keywords": ["red-square", "alphabet", "letter"] }, - { "category": "symbols", "char": "🆎", "name": "ab", "keywords": ["red-square", "alphabet"] }, - { "category": "symbols", "char": "🆑", "name": "cl", "keywords": ["alphabet", "words", "red-square"] }, - { "category": "symbols", "char": "🅾️", "name": "o2", "keywords": ["alphabet", "red-square", "letter"] }, - { "category": "symbols", "char": "🆘", "name": "sos", "keywords": ["help", "red-square", "words", "emergency", "911"] }, - { "category": "symbols", "char": "⛔", "name": "no_entry", "keywords": ["limit", "security", "privacy", "bad", "denied", "stop", "circle"] }, - { "category": "symbols", "char": "📛", "name": "name_badge", "keywords": ["fire", "forbid"] }, - { "category": "symbols", "char": "🚫", "name": "no_entry_sign", "keywords": ["forbid", "stop", "limit", "denied", "disallow", "circle"] }, - { "category": "symbols", "char": "❌", "name": "x", "keywords": ["no", "delete", "remove", "cancel", "red"] }, - { "category": "symbols", "char": "⭕", "name": "o", "keywords": ["circle", "round"] }, - { "category": "symbols", "char": "🛑", "name": "stop_sign", "keywords": ["stop"] }, - { "category": "symbols", "char": "💢", "name": "anger", "keywords": ["angry", "mad"] }, - { "category": "symbols", "char": "♨️", "name": "hotsprings", "keywords": ["bath", "warm", "relax"] }, - { "category": "symbols", "char": "🚷", "name": "no_pedestrians", "keywords": ["rules", "crossing", "walking", "circle"] }, - { "category": "symbols", "char": "🚯", "name": "do_not_litter", "keywords": ["trash", "bin", "garbage", "circle"] }, - { "category": "symbols", "char": "🚳", "name": "no_bicycles", "keywords": ["cyclist", "prohibited", "circle"] }, - { "category": "symbols", "char": "🚱", "name": "non-potable_water", "keywords": ["drink", "faucet", "tap", "circle"] }, - { "category": "symbols", "char": "🔞", "name": "underage", "keywords": ["18", "drink", "pub", "night", "minor", "circle"] }, - { "category": "symbols", "char": "📵", "name": "no_mobile_phones", "keywords": ["iphone", "mute", "circle"] }, - { "category": "symbols", "char": "❗", "name": "exclamation", "keywords": ["heavy_exclamation_mark", "danger", "surprise", "punctuation", "wow", "warning"] }, - { "category": "symbols", "char": "❕", "name": "grey_exclamation", "keywords": ["surprise", "punctuation", "gray", "wow", "warning"] }, - { "category": "symbols", "char": "❓", "name": "question", "keywords": ["doubt", "confused"] }, - { "category": "symbols", "char": "❔", "name": "grey_question", "keywords": ["doubts", "gray", "huh", "confused"] }, - { "category": "symbols", "char": "‼️", "name": "bangbang", "keywords": ["exclamation", "surprise"] }, - { "category": "symbols", "char": "⁉️", "name": "interrobang", "keywords": ["wat", "punctuation", "surprise"] }, - { "category": "symbols", "char": "🔅", "name": "low_brightness", "keywords": ["sun", "afternoon", "warm", "summer"] }, - { "category": "symbols", "char": "🔆", "name": "high_brightness", "keywords": ["sun", "light"] }, - { "category": "symbols", "char": "🔱", "name": "trident", "keywords": ["weapon", "spear"] }, - { "category": "symbols", "char": "⚜", "name": "fleur_de_lis", "keywords": ["decorative", "scout"] }, - { "category": "symbols", "char": "〽️", "name": "part_alternation_mark", "keywords": ["graph", "presentation", "stats", "business", "economics", "bad"] }, - { "category": "symbols", "char": "⚠️", "name": "warning", "keywords": ["exclamation", "wip", "alert", "error", "problem", "issue"] }, - { "category": "symbols", "char": "🚸", "name": "children_crossing", "keywords": ["school", "warning", "danger", "sign", "driving", "yellow-diamond"] }, - { "category": "symbols", "char": "🔰", "name": "beginner", "keywords": ["badge", "shield"] }, - { "category": "symbols", "char": "♻️", "name": "recycle", "keywords": ["arrow", "environment", "garbage", "trash"] }, - { "category": "symbols", "char": "🈯", "name": "u6307", "keywords": ["chinese", "point", "green-square", "kanji", "reserved", "shiteiseki"] }, - { "category": "symbols", "char": "💹", "name": "chart", "keywords": ["green-square", "graph", "presentation", "stats"] }, - { "category": "symbols", "char": "❇️", "name": "sparkle", "keywords": ["stars", "green-square", "awesome", "good", "fireworks"] }, - { "category": "symbols", "char": "✳️", "name": "eight_spoked_asterisk", "keywords": ["star", "sparkle", "green-square"] }, - { "category": "symbols", "char": "❎", "name": "negative_squared_cross_mark", "keywords": ["x", "green-square", "no", "deny"] }, - { "category": "symbols", "char": "✅", "name": "white_check_mark", "keywords": ["green-square", "ok", "agree", "vote", "election", "answer", "tick"] }, - { "category": "symbols", "char": "💠", "name": "diamond_shape_with_a_dot_inside", "keywords": ["jewel", "blue", "gem", "crystal", "fancy"] }, - { "category": "symbols", "char": "🌀", "name": "cyclone", "keywords": ["weather", "swirl", "blue", "cloud", "vortex", "spiral", "whirlpool", "spin", "tornado", "hurricane", "typhoon"] }, - { "category": "symbols", "char": "➿", "name": "loop", "keywords": ["tape", "cassette"] }, - { "category": "symbols", "char": "🌐", "name": "globe_with_meridians", "keywords": ["earth", "international", "world", "internet", "interweb", "i18n"] }, - { "category": "symbols", "char": "Ⓜ️", "name": "m", "keywords": ["alphabet", "blue-circle", "letter"] }, - { "category": "symbols", "char": "🏧", "name": "atm", "keywords": ["money", "sales", "cash", "blue-square", "payment", "bank"] }, - { "category": "symbols", "char": "🈂️", "name": "sa", "keywords": ["japanese", "blue-square", "katakana"] }, - { "category": "symbols", "char": "🛂", "name": "passport_control", "keywords": ["custom", "blue-square"] }, - { "category": "symbols", "char": "🛃", "name": "customs", "keywords": ["passport", "border", "blue-square"] }, - { "category": "symbols", "char": "🛄", "name": "baggage_claim", "keywords": ["blue-square", "airport", "transport"] }, - { "category": "symbols", "char": "🛅", "name": "left_luggage", "keywords": ["blue-square", "travel"] }, - { "category": "symbols", "char": "♿", "name": "wheelchair", "keywords": ["blue-square", "disabled", "a11y", "accessibility"] }, - { "category": "symbols", "char": "🚭", "name": "no_smoking", "keywords": ["cigarette", "blue-square", "smell", "smoke"] }, - { "category": "symbols", "char": "🚾", "name": "wc", "keywords": ["toilet", "restroom", "blue-square"] }, - { "category": "symbols", "char": "🅿️", "name": "parking", "keywords": ["cars", "blue-square", "alphabet", "letter"] }, - { "category": "symbols", "char": "🚰", "name": "potable_water", "keywords": ["blue-square", "liquid", "restroom", "cleaning", "faucet"] }, - { "category": "symbols", "char": "🚹", "name": "mens", "keywords": ["toilet", "restroom", "wc", "blue-square", "gender", "male"] }, - { "category": "symbols", "char": "🚺", "name": "womens", "keywords": ["purple-square", "woman", "female", "toilet", "loo", "restroom", "gender"] }, - { "category": "symbols", "char": "🚼", "name": "baby_symbol", "keywords": ["orange-square", "child"] }, - { "category": "symbols", "char": "🚻", "name": "restroom", "keywords": ["blue-square", "toilet", "refresh", "wc", "gender"] }, - { "category": "symbols", "char": "🚮", "name": "put_litter_in_its_place", "keywords": ["blue-square", "sign", "human", "info"] }, - { "category": "symbols", "char": "🎦", "name": "cinema", "keywords": ["blue-square", "record", "film", "movie", "curtain", "stage", "theater"] }, - { "category": "symbols", "char": "📶", "name": "signal_strength", "keywords": ["blue-square", "reception", "phone", "internet", "connection", "wifi", "bluetooth", "bars"] }, - { "category": "symbols", "char": "🈁", "name": "koko", "keywords": ["blue-square", "here", "katakana", "japanese", "destination"] }, - { "category": "symbols", "char": "🆖", "name": "ng", "keywords": ["blue-square", "words", "shape", "icon"] }, - { "category": "symbols", "char": "🆗", "name": "ok", "keywords": ["good", "agree", "yes", "blue-square"] }, - { "category": "symbols", "char": "🆙", "name": "up", "keywords": ["blue-square", "above", "high"] }, - { "category": "symbols", "char": "🆒", "name": "cool", "keywords": ["words", "blue-square"] }, - { "category": "symbols", "char": "🆕", "name": "new", "keywords": ["blue-square", "words", "start"] }, - { "category": "symbols", "char": "🆓", "name": "free", "keywords": ["blue-square", "words"] }, - { "category": "symbols", "char": "0️⃣", "name": "zero", "keywords": ["0", "numbers", "blue-square", "null"] }, - { "category": "symbols", "char": "1️⃣", "name": "one", "keywords": ["blue-square", "numbers", "1"] }, - { "category": "symbols", "char": "2️⃣", "name": "two", "keywords": ["numbers", "2", "prime", "blue-square"] }, - { "category": "symbols", "char": "3️⃣", "name": "three", "keywords": ["3", "numbers", "prime", "blue-square"] }, - { "category": "symbols", "char": "4️⃣", "name": "four", "keywords": ["4", "numbers", "blue-square"] }, - { "category": "symbols", "char": "5️⃣", "name": "five", "keywords": ["5", "numbers", "blue-square", "prime"] }, - { "category": "symbols", "char": "6️⃣", "name": "six", "keywords": ["6", "numbers", "blue-square"] }, - { "category": "symbols", "char": "7️⃣", "name": "seven", "keywords": ["7", "numbers", "blue-square", "prime"] }, - { "category": "symbols", "char": "8️⃣", "name": "eight", "keywords": ["8", "blue-square", "numbers"] }, - { "category": "symbols", "char": "9️⃣", "name": "nine", "keywords": ["blue-square", "numbers", "9"] }, - { "category": "symbols", "char": "🔟", "name": "keycap_ten", "keywords": ["numbers", "10", "blue-square"] }, - { "category": "symbols", "char": "*⃣", "name": "asterisk", "keywords": ["star", "keycap"] }, - { "category": "symbols", "char": "⏏️", "name": "eject_button", "keywords": ["blue-square"] }, - { "category": "symbols", "char": "▶️", "name": "arrow_forward", "keywords": ["blue-square", "right", "direction", "play"] }, - { "category": "symbols", "char": "⏸", "name": "pause_button", "keywords": ["pause", "blue-square"] }, - { "category": "symbols", "char": "⏭", "name": "next_track_button", "keywords": ["forward", "next", "blue-square"] }, - { "category": "symbols", "char": "⏹", "name": "stop_button", "keywords": ["blue-square"] }, - { "category": "symbols", "char": "⏺", "name": "record_button", "keywords": ["blue-square"] }, - { "category": "symbols", "char": "⏯", "name": "play_or_pause_button", "keywords": ["blue-square", "play", "pause"] }, - { "category": "symbols", "char": "⏮", "name": "previous_track_button", "keywords": ["backward"] }, - { "category": "symbols", "char": "⏩", "name": "fast_forward", "keywords": ["blue-square", "play", "speed", "continue"] }, - { "category": "symbols", "char": "⏪", "name": "rewind", "keywords": ["play", "blue-square"] }, - { "category": "symbols", "char": "🔀", "name": "twisted_rightwards_arrows", "keywords": ["blue-square", "shuffle", "music", "random"] }, - { "category": "symbols", "char": "🔁", "name": "repeat", "keywords": ["loop", "record"] }, - { "category": "symbols", "char": "🔂", "name": "repeat_one", "keywords": ["blue-square", "loop"] }, - { "category": "symbols", "char": "◀️", "name": "arrow_backward", "keywords": ["blue-square", "left", "direction"] }, - { "category": "symbols", "char": "🔼", "name": "arrow_up_small", "keywords": ["blue-square", "triangle", "direction", "point", "forward", "top"] }, - { "category": "symbols", "char": "🔽", "name": "arrow_down_small", "keywords": ["blue-square", "direction", "bottom"] }, - { "category": "symbols", "char": "⏫", "name": "arrow_double_up", "keywords": ["blue-square", "direction", "top"] }, - { "category": "symbols", "char": "⏬", "name": "arrow_double_down", "keywords": ["blue-square", "direction", "bottom"] }, - { "category": "symbols", "char": "➡️", "name": "arrow_right", "keywords": ["blue-square", "next"] }, - { "category": "symbols", "char": "⬅️", "name": "arrow_left", "keywords": ["blue-square", "previous", "back"] }, - { "category": "symbols", "char": "⬆️", "name": "arrow_up", "keywords": ["blue-square", "continue", "top", "direction"] }, - { "category": "symbols", "char": "⬇️", "name": "arrow_down", "keywords": ["blue-square", "direction", "bottom"] }, - { "category": "symbols", "char": "↗️", "name": "arrow_upper_right", "keywords": ["blue-square", "point", "direction", "diagonal", "northeast"] }, - { "category": "symbols", "char": "↘️", "name": "arrow_lower_right", "keywords": ["blue-square", "direction", "diagonal", "southeast"] }, - { "category": "symbols", "char": "↙️", "name": "arrow_lower_left", "keywords": ["blue-square", "direction", "diagonal", "southwest"] }, - { "category": "symbols", "char": "↖️", "name": "arrow_upper_left", "keywords": ["blue-square", "point", "direction", "diagonal", "northwest"] }, - { "category": "symbols", "char": "↕️", "name": "arrow_up_down", "keywords": ["blue-square", "direction", "way", "vertical"] }, - { "category": "symbols", "char": "↔️", "name": "left_right_arrow", "keywords": ["shape", "direction", "horizontal", "sideways"] }, - { "category": "symbols", "char": "🔄", "name": "arrows_counterclockwise", "keywords": ["blue-square", "sync", "cycle"] }, - { "category": "symbols", "char": "↪️", "name": "arrow_right_hook", "keywords": ["blue-square", "return", "rotate", "direction"] }, - { "category": "symbols", "char": "↩️", "name": "leftwards_arrow_with_hook", "keywords": ["back", "return", "blue-square", "undo", "enter"] }, - { "category": "symbols", "char": "⤴️", "name": "arrow_heading_up", "keywords": ["blue-square", "direction", "top"] }, - { "category": "symbols", "char": "⤵️", "name": "arrow_heading_down", "keywords": ["blue-square", "direction", "bottom"] }, - { "category": "symbols", "char": "#️⃣", "name": "hash", "keywords": ["symbol", "blue-square", "twitter"] }, - { "category": "symbols", "char": "ℹ️", "name": "information_source", "keywords": ["blue-square", "alphabet", "letter"] }, - { "category": "symbols", "char": "🔤", "name": "abc", "keywords": ["blue-square", "alphabet"] }, - { "category": "symbols", "char": "🔡", "name": "abcd", "keywords": ["blue-square", "alphabet"] }, - { "category": "symbols", "char": "🔠", "name": "capital_abcd", "keywords": ["alphabet", "words", "blue-square"] }, - { "category": "symbols", "char": "🔣", "name": "symbols", "keywords": ["blue-square", "music", "note", "ampersand", "percent", "glyphs", "characters"] }, - { "category": "symbols", "char": "🎵", "name": "musical_note", "keywords": ["score", "tone", "sound"] }, - { "category": "symbols", "char": "🎶", "name": "notes", "keywords": ["music", "score"] }, - { "category": "symbols", "char": "〰️", "name": "wavy_dash", "keywords": ["draw", "line", "moustache", "mustache", "squiggle", "scribble"] }, - { "category": "symbols", "char": "➰", "name": "curly_loop", "keywords": ["scribble", "draw", "shape", "squiggle"] }, - { "category": "symbols", "char": "✔️", "name": "heavy_check_mark", "keywords": ["ok", "nike", "answer", "yes", "tick"] }, - { "category": "symbols", "char": "🔃", "name": "arrows_clockwise", "keywords": ["sync", "cycle", "round", "repeat"] }, - { "category": "symbols", "char": "➕", "name": "heavy_plus_sign", "keywords": ["math", "calculation", "addition", "more", "increase"] }, - { "category": "symbols", "char": "➖", "name": "heavy_minus_sign", "keywords": ["math", "calculation", "subtract", "less"] }, - { "category": "symbols", "char": "➗", "name": "heavy_division_sign", "keywords": ["divide", "math", "calculation"] }, - { "category": "symbols", "char": "✖️", "name": "heavy_multiplication_x", "keywords": ["math", "calculation"] }, - { "category": "symbols", "char": "\uD83D\uDFF0", "name": "heavy_equals_sign", "keywords": [] }, - { "category": "symbols", "char": "♾", "name": "infinity", "keywords": ["forever"] }, - { "category": "symbols", "char": "💲", "name": "heavy_dollar_sign", "keywords": ["money", "sales", "payment", "currency", "buck"] }, - { "category": "symbols", "char": "💱", "name": "currency_exchange", "keywords": ["money", "sales", "dollar", "travel"] }, - { "category": "symbols", "char": "©️", "name": "copyright", "keywords": ["ip", "license", "circle", "law", "legal"] }, - { "category": "symbols", "char": "®️", "name": "registered", "keywords": ["alphabet", "circle"] }, - { "category": "symbols", "char": "™️", "name": "tm", "keywords": ["trademark", "brand", "law", "legal"] }, - { "category": "symbols", "char": "🔚", "name": "end", "keywords": ["words", "arrow"] }, - { "category": "symbols", "char": "🔙", "name": "back", "keywords": ["arrow", "words", "return"] }, - { "category": "symbols", "char": "🔛", "name": "on", "keywords": ["arrow", "words"] }, - { "category": "symbols", "char": "🔝", "name": "top", "keywords": ["words", "blue-square"] }, - { "category": "symbols", "char": "🔜", "name": "soon", "keywords": ["arrow", "words"] }, - { "category": "symbols", "char": "☑️", "name": "ballot_box_with_check", "keywords": ["ok", "agree", "confirm", "black-square", "vote", "election", "yes", "tick"] }, - { "category": "symbols", "char": "🔘", "name": "radio_button", "keywords": ["input", "old", "music", "circle"] }, - { "category": "symbols", "char": "⚫", "name": "black_circle", "keywords": ["shape", "button", "round"] }, - { "category": "symbols", "char": "⚪", "name": "white_circle", "keywords": ["shape", "round"] }, - { "category": "symbols", "char": "🔴", "name": "red_circle", "keywords": ["shape", "error", "danger"] }, - { "category": "symbols", "char": "🟠", "name": "orange_circle", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟡", "name": "yellow_circle", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟢", "name": "green_circle", "keywords": ["shape"] }, - { "category": "symbols", "char": "🔵", "name": "large_blue_circle", "keywords": ["shape", "icon", "button"] }, - { "category": "symbols", "char": "🟣", "name": "purple_circle", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟤", "name": "brown_circle", "keywords": ["shape"] }, - { "category": "symbols", "char": "🔸", "name": "small_orange_diamond", "keywords": ["shape", "jewel", "gem"] }, - { "category": "symbols", "char": "🔹", "name": "small_blue_diamond", "keywords": ["shape", "jewel", "gem"] }, - { "category": "symbols", "char": "🔶", "name": "large_orange_diamond", "keywords": ["shape", "jewel", "gem"] }, - { "category": "symbols", "char": "🔷", "name": "large_blue_diamond", "keywords": ["shape", "jewel", "gem"] }, - { "category": "symbols", "char": "🔺", "name": "small_red_triangle", "keywords": ["shape", "direction", "up", "top"] }, - { "category": "symbols", "char": "▪️", "name": "black_small_square", "keywords": ["shape", "icon"] }, - { "category": "symbols", "char": "▫️", "name": "white_small_square", "keywords": ["shape", "icon"] }, - { "category": "symbols", "char": "⬛", "name": "black_large_square", "keywords": ["shape", "icon", "button"] }, - { "category": "symbols", "char": "⬜", "name": "white_large_square", "keywords": ["shape", "icon", "stone", "button"] }, - { "category": "symbols", "char": "🟥", "name": "red_square", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟧", "name": "orange_square", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟨", "name": "yellow_square", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟩", "name": "green_square", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟦", "name": "blue_square", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟪", "name": "purple_square", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟫", "name": "brown_square", "keywords": ["shape"] }, - { "category": "symbols", "char": "🔻", "name": "small_red_triangle_down", "keywords": ["shape", "direction", "bottom"] }, - { "category": "symbols", "char": "◼️", "name": "black_medium_square", "keywords": ["shape", "button", "icon"] }, - { "category": "symbols", "char": "◻️", "name": "white_medium_square", "keywords": ["shape", "stone", "icon"] }, - { "category": "symbols", "char": "◾", "name": "black_medium_small_square", "keywords": ["icon", "shape", "button"] }, - { "category": "symbols", "char": "◽", "name": "white_medium_small_square", "keywords": ["shape", "stone", "icon", "button"] }, - { "category": "symbols", "char": "🔲", "name": "black_square_button", "keywords": ["shape", "input", "frame"] }, - { "category": "symbols", "char": "🔳", "name": "white_square_button", "keywords": ["shape", "input"] }, - { "category": "symbols", "char": "🔈", "name": "speaker", "keywords": ["sound", "volume", "silence", "broadcast"] }, - { "category": "symbols", "char": "🔉", "name": "sound", "keywords": ["volume", "speaker", "broadcast"] }, - { "category": "symbols", "char": "🔊", "name": "loud_sound", "keywords": ["volume", "noise", "noisy", "speaker", "broadcast"] }, - { "category": "symbols", "char": "🔇", "name": "mute", "keywords": ["sound", "volume", "silence", "quiet"] }, - { "category": "symbols", "char": "📣", "name": "mega", "keywords": ["sound", "speaker", "volume"] }, - { "category": "symbols", "char": "📢", "name": "loudspeaker", "keywords": ["volume", "sound"] }, - { "category": "symbols", "char": "🔔", "name": "bell", "keywords": ["sound", "notification", "christmas", "xmas", "chime"] }, - { "category": "symbols", "char": "🔕", "name": "no_bell", "keywords": ["sound", "volume", "mute", "quiet", "silent"] }, - { "category": "symbols", "char": "🃏", "name": "black_joker", "keywords": ["poker", "cards", "game", "play", "magic"] }, - { "category": "symbols", "char": "🀄", "name": "mahjong", "keywords": ["game", "play", "chinese", "kanji"] }, - { "category": "symbols", "char": "♠️", "name": "spades", "keywords": ["poker", "cards", "suits", "magic"] }, - { "category": "symbols", "char": "♣️", "name": "clubs", "keywords": ["poker", "cards", "magic", "suits"] }, - { "category": "symbols", "char": "♥️", "name": "hearts", "keywords": ["poker", "cards", "magic", "suits"] }, - { "category": "symbols", "char": "♦️", "name": "diamonds", "keywords": ["poker", "cards", "magic", "suits"] }, - { "category": "symbols", "char": "🎴", "name": "flower_playing_cards", "keywords": ["game", "sunset", "red"] }, - { "category": "symbols", "char": "💭", "name": "thought_balloon", "keywords": ["bubble", "cloud", "speech", "thinking", "dream"] }, - { "category": "symbols", "char": "🗯", "name": "right_anger_bubble", "keywords": ["caption", "speech", "thinking", "mad"] }, - { "category": "symbols", "char": "💬", "name": "speech_balloon", "keywords": ["bubble", "words", "message", "talk", "chatting"] }, - { "category": "symbols", "char": "🗨", "name": "left_speech_bubble", "keywords": ["words", "message", "talk", "chatting"] }, - { "category": "symbols", "char": "🕐", "name": "clock1", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕑", "name": "clock2", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕒", "name": "clock3", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕓", "name": "clock4", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕔", "name": "clock5", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕕", "name": "clock6", "keywords": ["time", "late", "early", "schedule", "dawn", "dusk"] }, - { "category": "symbols", "char": "🕖", "name": "clock7", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕗", "name": "clock8", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕘", "name": "clock9", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕙", "name": "clock10", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕚", "name": "clock11", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕛", "name": "clock12", "keywords": ["time", "noon", "midnight", "midday", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕜", "name": "clock130", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕝", "name": "clock230", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕞", "name": "clock330", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕟", "name": "clock430", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕠", "name": "clock530", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕡", "name": "clock630", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕢", "name": "clock730", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕣", "name": "clock830", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕤", "name": "clock930", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕥", "name": "clock1030", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕦", "name": "clock1130", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕧", "name": "clock1230", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "flags", "char": "🇦🇫", "name": "afghanistan", "keywords": ["af", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇽", "name": "aland_islands", "keywords": ["Åland", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇱", "name": "albania", "keywords": ["al", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇩🇿", "name": "algeria", "keywords": ["dz", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇸", "name": "american_samoa", "keywords": ["american", "ws", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇩", "name": "andorra", "keywords": ["ad", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇴", "name": "angola", "keywords": ["ao", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇮", "name": "anguilla", "keywords": ["ai", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇶", "name": "antarctica", "keywords": ["aq", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇬", "name": "antigua_barbuda", "keywords": ["antigua", "barbuda", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇷", "name": "argentina", "keywords": ["ar", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇲", "name": "armenia", "keywords": ["am", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇼", "name": "aruba", "keywords": ["aw", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇨", "name": "ascension_island", "keywords": ["flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇺", "name": "australia", "keywords": ["au", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇹", "name": "austria", "keywords": ["at", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇿", "name": "azerbaijan", "keywords": ["az", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇸", "name": "bahamas", "keywords": ["bs", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇭", "name": "bahrain", "keywords": ["bh", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇩", "name": "bangladesh", "keywords": ["bd", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇧", "name": "barbados", "keywords": ["bb", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇾", "name": "belarus", "keywords": ["by", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇪", "name": "belgium", "keywords": ["be", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇿", "name": "belize", "keywords": ["bz", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇯", "name": "benin", "keywords": ["bj", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇲", "name": "bermuda", "keywords": ["bm", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇹", "name": "bhutan", "keywords": ["bt", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇴", "name": "bolivia", "keywords": ["bo", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇶", "name": "caribbean_netherlands", "keywords": ["bonaire", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇦", "name": "bosnia_herzegovina", "keywords": ["bosnia", "herzegovina", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇼", "name": "botswana", "keywords": ["bw", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇷", "name": "brazil", "keywords": ["br", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇴", "name": "british_indian_ocean_territory", "keywords": ["british", "indian", "ocean", "territory", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇻🇬", "name": "british_virgin_islands", "keywords": ["british", "virgin", "islands", "bvi", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇳", "name": "brunei", "keywords": ["bn", "darussalam", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇬", "name": "bulgaria", "keywords": ["bg", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇫", "name": "burkina_faso", "keywords": ["burkina", "faso", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇮", "name": "burundi", "keywords": ["bi", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇻", "name": "cape_verde", "keywords": ["cabo", "verde", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇭", "name": "cambodia", "keywords": ["kh", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇲", "name": "cameroon", "keywords": ["cm", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇦", "name": "canada", "keywords": ["ca", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇨", "name": "canary_islands", "keywords": ["canary", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇾", "name": "cayman_islands", "keywords": ["cayman", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇫", "name": "central_african_republic", "keywords": ["central", "african", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇩", "name": "chad", "keywords": ["td", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇱", "name": "chile", "keywords": ["flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇳", "name": "cn", "keywords": ["china", "chinese", "prc", "flag", "country", "nation", "banner"] }, - { "category": "flags", "char": "🇨🇽", "name": "christmas_island", "keywords": ["christmas", "island", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇨", "name": "cocos_islands", "keywords": ["cocos", "keeling", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇴", "name": "colombia", "keywords": ["co", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇲", "name": "comoros", "keywords": ["km", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇬", "name": "congo_brazzaville", "keywords": ["congo", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇩", "name": "congo_kinshasa", "keywords": ["congo", "democratic", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇰", "name": "cook_islands", "keywords": ["cook", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇷", "name": "costa_rica", "keywords": ["costa", "rica", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇭🇷", "name": "croatia", "keywords": ["hr", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇺", "name": "cuba", "keywords": ["cu", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇼", "name": "curacao", "keywords": ["curaçao", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇾", "name": "cyprus", "keywords": ["cy", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇿", "name": "czech_republic", "keywords": ["cz", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇩🇰", "name": "denmark", "keywords": ["dk", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇩🇯", "name": "djibouti", "keywords": ["dj", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇩🇲", "name": "dominica", "keywords": ["dm", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇩🇴", "name": "dominican_republic", "keywords": ["dominican", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇪🇨", "name": "ecuador", "keywords": ["ec", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇪🇬", "name": "egypt", "keywords": ["eg", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇻", "name": "el_salvador", "keywords": ["el", "salvador", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇶", "name": "equatorial_guinea", "keywords": ["equatorial", "gn", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇪🇷", "name": "eritrea", "keywords": ["er", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇪🇪", "name": "estonia", "keywords": ["ee", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇪🇹", "name": "ethiopia", "keywords": ["et", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇪🇺", "name": "eu", "keywords": ["european", "union", "flag", "banner"] }, - { "category": "flags", "char": "🇫🇰", "name": "falkland_islands", "keywords": ["falkland", "islands", "malvinas", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇫🇴", "name": "faroe_islands", "keywords": ["faroe", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇫🇯", "name": "fiji", "keywords": ["fj", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇫🇮", "name": "finland", "keywords": ["fi", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇫🇷", "name": "fr", "keywords": ["banner", "flag", "nation", "france", "french", "country"] }, - { "category": "flags", "char": "🇬🇫", "name": "french_guiana", "keywords": ["french", "guiana", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇫", "name": "french_polynesia", "keywords": ["french", "polynesia", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇫", "name": "french_southern_territories", "keywords": ["french", "southern", "territories", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇦", "name": "gabon", "keywords": ["ga", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇲", "name": "gambia", "keywords": ["gm", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇪", "name": "georgia", "keywords": ["ge", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇩🇪", "name": "de", "keywords": ["german", "nation", "flag", "country", "banner"] }, - { "category": "flags", "char": "🇬🇭", "name": "ghana", "keywords": ["gh", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇮", "name": "gibraltar", "keywords": ["gi", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇷", "name": "greece", "keywords": ["gr", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇱", "name": "greenland", "keywords": ["gl", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇩", "name": "grenada", "keywords": ["gd", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇵", "name": "guadeloupe", "keywords": ["gp", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇺", "name": "guam", "keywords": ["gu", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇹", "name": "guatemala", "keywords": ["gt", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇬", "name": "guernsey", "keywords": ["gg", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇳", "name": "guinea", "keywords": ["gn", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇼", "name": "guinea_bissau", "keywords": ["gw", "bissau", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇾", "name": "guyana", "keywords": ["gy", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇭🇹", "name": "haiti", "keywords": ["ht", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇭🇳", "name": "honduras", "keywords": ["hn", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇭🇰", "name": "hong_kong", "keywords": ["hong", "kong", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇭🇺", "name": "hungary", "keywords": ["hu", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇸", "name": "iceland", "keywords": ["is", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇳", "name": "india", "keywords": ["in", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇩", "name": "indonesia", "keywords": ["flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇷", "name": "iran", "keywords": ["iran, ", "islamic", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇶", "name": "iraq", "keywords": ["iq", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇪", "name": "ireland", "keywords": ["ie", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇲", "name": "isle_of_man", "keywords": ["isle", "man", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇱", "name": "israel", "keywords": ["il", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇹", "name": "it", "keywords": ["italy", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇮", "name": "cote_divoire", "keywords": ["ivory", "coast", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇯🇲", "name": "jamaica", "keywords": ["jm", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇯🇵", "name": "jp", "keywords": ["japanese", "nation", "flag", "country", "banner"] }, - { "category": "flags", "char": "🇯🇪", "name": "jersey", "keywords": ["je", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇯🇴", "name": "jordan", "keywords": ["jo", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇿", "name": "kazakhstan", "keywords": ["kz", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇪", "name": "kenya", "keywords": ["ke", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇮", "name": "kiribati", "keywords": ["ki", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇽🇰", "name": "kosovo", "keywords": ["xk", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇼", "name": "kuwait", "keywords": ["kw", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇬", "name": "kyrgyzstan", "keywords": ["kg", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇦", "name": "laos", "keywords": ["lao", "democratic", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇻", "name": "latvia", "keywords": ["lv", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇧", "name": "lebanon", "keywords": ["lb", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇸", "name": "lesotho", "keywords": ["ls", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇷", "name": "liberia", "keywords": ["lr", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇾", "name": "libya", "keywords": ["ly", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇮", "name": "liechtenstein", "keywords": ["li", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇹", "name": "lithuania", "keywords": ["lt", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇺", "name": "luxembourg", "keywords": ["lu", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇴", "name": "macau", "keywords": ["macao", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇰", "name": "macedonia", "keywords": ["macedonia, ", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇬", "name": "madagascar", "keywords": ["mg", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇼", "name": "malawi", "keywords": ["mw", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇾", "name": "malaysia", "keywords": ["my", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇻", "name": "maldives", "keywords": ["mv", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇱", "name": "mali", "keywords": ["ml", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇹", "name": "malta", "keywords": ["mt", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇭", "name": "marshall_islands", "keywords": ["marshall", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇶", "name": "martinique", "keywords": ["mq", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇷", "name": "mauritania", "keywords": ["mr", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇺", "name": "mauritius", "keywords": ["mu", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇾🇹", "name": "mayotte", "keywords": ["yt", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇽", "name": "mexico", "keywords": ["mx", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇫🇲", "name": "micronesia", "keywords": ["micronesia, ", "federated", "states", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇩", "name": "moldova", "keywords": ["moldova, ", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇨", "name": "monaco", "keywords": ["mc", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇳", "name": "mongolia", "keywords": ["mn", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇪", "name": "montenegro", "keywords": ["me", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇸", "name": "montserrat", "keywords": ["ms", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇦", "name": "morocco", "keywords": ["ma", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇿", "name": "mozambique", "keywords": ["mz", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇲", "name": "myanmar", "keywords": ["mm", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇦", "name": "namibia", "keywords": ["na", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇷", "name": "nauru", "keywords": ["nr", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇵", "name": "nepal", "keywords": ["np", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇱", "name": "netherlands", "keywords": ["nl", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇨", "name": "new_caledonia", "keywords": ["new", "caledonia", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇿", "name": "new_zealand", "keywords": ["new", "zealand", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇮", "name": "nicaragua", "keywords": ["ni", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇪", "name": "niger", "keywords": ["ne", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇬", "name": "nigeria", "keywords": ["flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇺", "name": "niue", "keywords": ["nu", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇫", "name": "norfolk_island", "keywords": ["norfolk", "island", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇵", "name": "northern_mariana_islands", "keywords": ["northern", "mariana", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇵", "name": "north_korea", "keywords": ["north", "korea", "nation", "flag", "country", "banner"] }, - { "category": "flags", "char": "🇳🇴", "name": "norway", "keywords": ["no", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇴🇲", "name": "oman", "keywords": ["om_symbol", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇰", "name": "pakistan", "keywords": ["pk", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇼", "name": "palau", "keywords": ["pw", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇸", "name": "palestinian_territories", "keywords": ["palestine", "palestinian", "territories", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇦", "name": "panama", "keywords": ["pa", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇬", "name": "papua_new_guinea", "keywords": ["papua", "new", "guinea", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇾", "name": "paraguay", "keywords": ["py", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇪", "name": "peru", "keywords": ["pe", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇭", "name": "philippines", "keywords": ["ph", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇳", "name": "pitcairn_islands", "keywords": ["pitcairn", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇱", "name": "poland", "keywords": ["pl", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇹", "name": "portugal", "keywords": ["pt", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇷", "name": "puerto_rico", "keywords": ["puerto", "rico", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇶🇦", "name": "qatar", "keywords": ["qa", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇷🇪", "name": "reunion", "keywords": ["réunion", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇷🇴", "name": "romania", "keywords": ["ro", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇷🇺", "name": "ru", "keywords": ["russian", "federation", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇷🇼", "name": "rwanda", "keywords": ["rw", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇱", "name": "st_barthelemy", "keywords": ["saint", "barthélemy", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇭", "name": "st_helena", "keywords": ["saint", "helena", "ascension", "tristan", "cunha", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇳", "name": "st_kitts_nevis", "keywords": ["saint", "kitts", "nevis", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇨", "name": "st_lucia", "keywords": ["saint", "lucia", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇲", "name": "st_pierre_miquelon", "keywords": ["saint", "pierre", "miquelon", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇻🇨", "name": "st_vincent_grenadines", "keywords": ["saint", "vincent", "grenadines", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇼🇸", "name": "samoa", "keywords": ["ws", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇲", "name": "san_marino", "keywords": ["san", "marino", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇹", "name": "sao_tome_principe", "keywords": ["sao", "tome", "principe", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇦", "name": "saudi_arabia", "keywords": ["flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇳", "name": "senegal", "keywords": ["sn", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇷🇸", "name": "serbia", "keywords": ["rs", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇨", "name": "seychelles", "keywords": ["sc", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇱", "name": "sierra_leone", "keywords": ["sierra", "leone", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇬", "name": "singapore", "keywords": ["sg", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇽", "name": "sint_maarten", "keywords": ["sint", "maarten", "dutch", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇰", "name": "slovakia", "keywords": ["sk", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇮", "name": "slovenia", "keywords": ["si", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇧", "name": "solomon_islands", "keywords": ["solomon", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇴", "name": "somalia", "keywords": ["so", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇿🇦", "name": "south_africa", "keywords": ["south", "africa", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇸", "name": "south_georgia_south_sandwich_islands", "keywords": ["south", "georgia", "sandwich", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇷", "name": "kr", "keywords": ["south", "korea", "nation", "flag", "country", "banner"] }, - { "category": "flags", "char": "🇸🇸", "name": "south_sudan", "keywords": ["south", "sd", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇪🇸", "name": "es", "keywords": ["spain", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇰", "name": "sri_lanka", "keywords": ["sri", "lanka", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇩", "name": "sudan", "keywords": ["sd", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇷", "name": "suriname", "keywords": ["sr", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇿", "name": "swaziland", "keywords": ["sz", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇪", "name": "sweden", "keywords": ["se", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇭", "name": "switzerland", "keywords": ["ch", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇾", "name": "syria", "keywords": ["syrian", "arab", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇼", "name": "taiwan", "keywords": ["tw", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇯", "name": "tajikistan", "keywords": ["tj", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇿", "name": "tanzania", "keywords": ["tanzania, ", "united", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇭", "name": "thailand", "keywords": ["th", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇱", "name": "timor_leste", "keywords": ["timor", "leste", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇬", "name": "togo", "keywords": ["tg", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇰", "name": "tokelau", "keywords": ["tk", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇴", "name": "tonga", "keywords": ["to", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇹", "name": "trinidad_tobago", "keywords": ["trinidad", "tobago", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇦", "name": "tristan_da_cunha", "keywords": ["flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇳", "name": "tunisia", "keywords": ["tn", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇷", "name": "tr", "keywords": ["turkey", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇲", "name": "turkmenistan", "keywords": ["flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇨", "name": "turks_caicos_islands", "keywords": ["turks", "caicos", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇻", "name": "tuvalu", "keywords": ["flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇺🇬", "name": "uganda", "keywords": ["ug", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇺🇦", "name": "ukraine", "keywords": ["ua", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇪", "name": "united_arab_emirates", "keywords": ["united", "arab", "emirates", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇧", "name": "uk", "keywords": ["united", "kingdom", "great", "britain", "northern", "ireland", "flag", "nation", "country", "banner", "british", "UK", "english", "england", "union jack"] }, - { "category": "flags", "char": "🏴", "name": "england", "keywords": ["flag", "english"] }, - { "category": "flags", "char": "🏴", "name": "scotland", "keywords": ["flag", "scottish"] }, - { "category": "flags", "char": "🏴", "name": "wales", "keywords": ["flag", "welsh"] }, - { "category": "flags", "char": "🇺🇸", "name": "us", "keywords": ["united", "states", "america", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇻🇮", "name": "us_virgin_islands", "keywords": ["virgin", "islands", "us", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇺🇾", "name": "uruguay", "keywords": ["uy", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇺🇿", "name": "uzbekistan", "keywords": ["uz", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇻🇺", "name": "vanuatu", "keywords": ["vu", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇻🇦", "name": "vatican_city", "keywords": ["vatican", "city", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇻🇪", "name": "venezuela", "keywords": ["ve", "bolivarian", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇻🇳", "name": "vietnam", "keywords": ["viet", "nam", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇼🇫", "name": "wallis_futuna", "keywords": ["wallis", "futuna", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇪🇭", "name": "western_sahara", "keywords": ["western", "sahara", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇾🇪", "name": "yemen", "keywords": ["ye", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇿🇲", "name": "zambia", "keywords": ["zm", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇿🇼", "name": "zimbabwe", "keywords": ["zw", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇺🇳", "name": "united_nations", "keywords": ["un", "flag", "banner"] }, - { "category": "flags", "char": "🏴☠️", "name": "pirate_flag", "keywords": ["skull", "crossbones", "flag", "banner"] } + ["😀", "grinning", 0], + ["😬", "grimacing", 0], + ["😁", "grin", 0], + ["😂", "joy", 0], + ["🤣", "rofl", 0], + ["🥳", "partying", 0], + ["😃", "smiley", 0], + ["😄", "smile", 0], + ["😅", "sweat_smile", 0], + ["🥲", "smiling_face_with_tear", 0], + ["😆", "laughing", 0], + ["😇", "innocent", 0], + ["😉", "wink", 0], + ["😊", "blush", 0], + ["🙂", "slightly_smiling_face", 0], + ["🙃", "upside_down_face", 0], + ["☺️", "relaxed", 0], + ["😋", "yum", 0], + ["😌", "relieved", 0], + ["😍", "heart_eyes", 0], + ["🥰", "smiling_face_with_three_hearts", 0], + ["😘", "kissing_heart", 0], + ["😗", "kissing", 0], + ["😙", "kissing_smiling_eyes", 0], + ["😚", "kissing_closed_eyes", 0], + ["😜", "stuck_out_tongue_winking_eye", 0], + ["🤪", "zany", 0], + ["🤨", "raised_eyebrow", 0], + ["🧐", "monocle", 0], + ["😝", "stuck_out_tongue_closed_eyes", 0], + ["😛", "stuck_out_tongue", 0], + ["🤑", "money_mouth_face", 0], + ["🤓", "nerd_face", 0], + ["🥸", "disguised_face", 0], + ["😎", "sunglasses", 0], + ["🤩", "star_struck", 0], + ["🤡", "clown_face", 0], + ["🤠", "cowboy_hat_face", 0], + ["🤗", "hugs", 0], + ["😏", "smirk", 0], + ["😶", "no_mouth", 0], + ["😐", "neutral_face", 0], + ["😑", "expressionless", 0], + ["😒", "unamused", 0], + ["🙄", "roll_eyes", 0], + ["🤔", "thinking", 0], + ["🤥", "lying_face", 0], + ["🤭", "hand_over_mouth", 0], + ["🤫", "shushing", 0], + ["🤬", "symbols_over_mouth", 0], + ["🤯", "exploding_head", 0], + ["😳", "flushed", 0], + ["😞", "disappointed", 0], + ["😟", "worried", 0], + ["😠", "angry", 0], + ["😡", "rage", 0], + ["😔", "pensive", 0], + ["😕", "confused", 0], + ["🙁", "slightly_frowning_face", 0], + ["☹", "frowning_face", 0], + ["😣", "persevere", 0], + ["😖", "confounded", 0], + ["😫", "tired_face", 0], + ["😩", "weary", 0], + ["🥺", "pleading", 0], + ["😤", "triumph", 0], + ["😮", "open_mouth", 0], + ["😱", "scream", 0], + ["😨", "fearful", 0], + ["😰", "cold_sweat", 0], + ["😯", "hushed", 0], + ["😦", "frowning", 0], + ["😧", "anguished", 0], + ["😢", "cry", 0], + ["😥", "disappointed_relieved", 0], + ["🤤", "drooling_face", 0], + ["😪", "sleepy", 0], + ["😓", "sweat", 0], + ["🥵", "hot", 0], + ["🥶", "cold", 0], + ["😭", "sob", 0], + ["😵", "dizzy_face", 0], + ["😲", "astonished", 0], + ["🤐", "zipper_mouth_face", 0], + ["🤢", "nauseated_face", 0], + ["🤧", "sneezing_face", 0], + ["🤮", "vomiting", 0], + ["😷", "mask", 0], + ["🤒", "face_with_thermometer", 0], + ["🤕", "face_with_head_bandage", 0], + ["🥴", "woozy", 0], + ["🥱", "yawning", 0], + ["😴", "sleeping", 0], + ["💤", "zzz", 0], + ["😶🌫️", "face_in_clouds", 0], + ["😮💨", "face_exhaling", 0], + ["😵💫", "face_with_spiral_eyes", 0], + ["🫠", "melting_face", 0], + ["🫢", "face_with_open_eyes_and_hand_over_mouth", 0], + ["🫣", "face_with_peeking_eye", 0], + ["🫡", "saluting_face", 0], + ["🫥", "dotted_line_face", 0], + ["🫤", "face_with_diagonal_mouth", 0], + ["🥹", "face_holding_back_tears", 0], + ["💩", "poop", 0], + ["😈", "smiling_imp", 0], + ["👿", "imp", 0], + ["👹", "japanese_ogre", 0], + ["👺", "japanese_goblin", 0], + ["💀", "skull", 0], + ["👻", "ghost", 0], + ["👽", "alien", 0], + ["🤖", "robot", 0], + ["😺", "smiley_cat", 0], + ["😸", "smile_cat", 0], + ["😹", "joy_cat", 0], + ["😻", "heart_eyes_cat", 0], + ["😼", "smirk_cat", 0], + ["😽", "kissing_cat", 0], + ["🙀", "scream_cat", 0], + ["😿", "crying_cat_face", 0], + ["😾", "pouting_cat", 0], + ["🤲", "palms_up", 1], + ["🙌", "raised_hands", 1], + ["👏", "clap", 1], + ["👋", "wave", 1], + ["🤙", "call_me_hand", 1], + ["👍", "+1", 1], + ["👎", "-1", 1], + ["👊", "facepunch", 1], + ["✊", "fist", 1], + ["🤛", "fist_left", 1], + ["🤜", "fist_right", 1], + ["✌", "v", 1], + ["👌", "ok_hand", 1], + ["✋", "raised_hand", 1], + ["🤚", "raised_back_of_hand", 1], + ["👐", "open_hands", 1], + ["💪", "muscle", 1], + ["🦾", "mechanical_arm", 1], + ["🙏", "pray", 1], + ["🦶", "foot", 1], + ["🦵", "leg", 1], + ["🦿", "mechanical_leg", 1], + ["🤝", "handshake", 1], + ["☝", "point_up", 1], + ["👆", "point_up_2", 1], + ["👇", "point_down", 1], + ["👈", "point_left", 1], + ["👉", "point_right", 1], + ["🖕", "fu", 1], + ["🖐", "raised_hand_with_fingers_splayed", 1], + ["🤟", "love_you", 1], + ["🤘", "metal", 1], + ["🤞", "crossed_fingers", 1], + ["🖖", "vulcan_salute", 1], + ["✍", "writing_hand", 1], + ["🫰", "hand_with_index_finger_and_thumb_crossed", 1], + ["🫱", "rightwards_hand", 1], + ["🫲", "leftwards_hand", 1], + ["🫳", "palm_down_hand", 1], + ["🫴", "palm_up_hand", 1], + ["🫵", "index_pointing_at_the_viewer", 1], + ["🫶", "heart_hands", 1], + ["🤏", "pinching_hand", 1], + ["🤌", "pinched_fingers", 1], + ["🤳", "selfie", 1], + ["💅", "nail_care", 1], + ["👄", "lips", 1], + ["🫦", "biting_lip", 1], + ["🦷", "tooth", 1], + ["👅", "tongue", 1], + ["👂", "ear", 1], + ["🦻", "ear_with_hearing_aid", 1], + ["👃", "nose", 1], + ["👁", "eye", 1], + ["👀", "eyes", 1], + ["🧠", "brain", 1], + ["🫀", "anatomical_heart", 1], + ["🫁", "lungs", 1], + ["👤", "bust_in_silhouette", 1], + ["👥", "busts_in_silhouette", 1], + ["🗣", "speaking_head", 1], + ["👶", "baby", 1], + ["🧒", "child", 1], + ["👦", "boy", 1], + ["👧", "girl", 1], + ["🧑", "adult", 1], + ["👨", "man", 1], + ["👩", "woman", 1], + ["🧑🦱", "curly_hair", 1], + ["👩🦱", "curly_hair_woman", 1], + ["👨🦱", "curly_hair_man", 1], + ["🧑🦰", "red_hair", 1], + ["👩🦰", "red_hair_woman", 1], + ["👨🦰", "red_hair_man", 1], + ["👱♀️", "blonde_woman", 1], + ["👱", "blonde_man", 1], + ["🧑🦳", "white_hair", 1], + ["👩🦳", "white_hair_woman", 1], + ["👨🦳", "white_hair_man", 1], + ["🧑🦲", "bald", 1], + ["👩🦲", "bald_woman", 1], + ["👨🦲", "bald_man", 1], + ["🧔", "bearded_person", 1], + ["🧓", "older_adult", 1], + ["👴", "older_man", 1], + ["👵", "older_woman", 1], + ["👲", "man_with_gua_pi_mao", 1], + ["🧕", "woman_with_headscarf", 1], + ["👳♀️", "woman_with_turban", 1], + ["👳", "man_with_turban", 1], + ["👮♀️", "policewoman", 1], + ["👮", "policeman", 1], + ["👷♀️", "construction_worker_woman", 1], + ["👷", "construction_worker_man", 1], + ["💂♀️", "guardswoman", 1], + ["💂", "guardsman", 1], + ["🕵️♀️", "female_detective", 1], + ["🕵", "male_detective", 1], + ["🧑⚕️", "health_worker", 1], + ["👩⚕️", "woman_health_worker", 1], + ["👨⚕️", "man_health_worker", 1], + ["🧑🌾", "farmer", 1], + ["👩🌾", "woman_farmer", 1], + ["👨🌾", "man_farmer", 1], + ["🧑🍳", "cook", 1], + ["👩🍳", "woman_cook", 1], + ["👨🍳", "man_cook", 1], + ["🧑🎓", "student", 1], + ["👩🎓", "woman_student", 1], + ["👨🎓", "man_student", 1], + ["🧑🎤", "singer", 1], + ["👩🎤", "woman_singer", 1], + ["👨🎤", "man_singer", 1], + ["🧑🏫", "teacher", 1], + ["👩🏫", "woman_teacher", 1], + ["👨🏫", "man_teacher", 1], + ["🧑🏭", "factory_worker", 1], + ["👩🏭", "woman_factory_worker", 1], + ["👨🏭", "man_factory_worker", 1], + ["🧑💻", "technologist", 1], + ["👩💻", "woman_technologist", 1], + ["👨💻", "man_technologist", 1], + ["🧑💼", "office_worker", 1], + ["👩💼", "woman_office_worker", 1], + ["👨💼", "man_office_worker", 1], + ["🧑🔧", "mechanic", 1], + ["👩🔧", "woman_mechanic", 1], + ["👨🔧", "man_mechanic", 1], + ["🧑🔬", "scientist", 1], + ["👩🔬", "woman_scientist", 1], + ["👨🔬", "man_scientist", 1], + ["🧑🎨", "artist", 1], + ["👩🎨", "woman_artist", 1], + ["👨🎨", "man_artist", 1], + ["🧑🚒", "firefighter", 1], + ["👩🚒", "woman_firefighter", 1], + ["👨🚒", "man_firefighter", 1], + ["🧑✈️", "pilot", 1], + ["👩✈️", "woman_pilot", 1], + ["👨✈️", "man_pilot", 1], + ["🧑🚀", "astronaut", 1], + ["👩🚀", "woman_astronaut", 1], + ["👨🚀", "man_astronaut", 1], + ["🧑⚖️", "judge", 1], + ["👩⚖️", "woman_judge", 1], + ["👨⚖️", "man_judge", 1], + ["🦸♀️", "woman_superhero", 1], + ["🦸♂️", "man_superhero", 1], + ["🦹♀️", "woman_supervillain", 1], + ["🦹♂️", "man_supervillain", 1], + ["🤶", "mrs_claus", 1], + ["🧑🎄", "mx_claus", 1], + ["🎅", "santa", 1], + ["🥷", "ninja", 1], + ["🧙♀️", "sorceress", 1], + ["🧙♂️", "wizard", 1], + ["🧝♀️", "woman_elf", 1], + ["🧝♂️", "man_elf", 1], + ["🧛♀️", "woman_vampire", 1], + ["🧛♂️", "man_vampire", 1], + ["🧟♀️", "woman_zombie", 1], + ["🧟♂️", "man_zombie", 1], + ["🧞♀️", "woman_genie", 1], + ["🧞♂️", "man_genie", 1], + ["🧜♀️", "mermaid", 1], + ["🧜♂️", "merman", 1], + ["🧚♀️", "woman_fairy", 1], + ["🧚♂️", "man_fairy", 1], + ["👼", "angel", 1], + ["🧌", "troll", 1], + ["🤰", "pregnant_woman", 1], + ["🫃", "pregnant_man", 1], + ["🫄", "pregnant_person", 1], + ["🫅", "person_with_crown", 1], + ["🤱", "breastfeeding", 1], + ["👩🍼", "woman_feeding_baby", 1], + ["👨🍼", "man_feeding_baby", 1], + ["🧑🍼", "person_feeding_baby", 1], + ["👸", "princess", 1], + ["🤴", "prince", 1], + ["👰", "person_with_veil", 1], + ["👰", "bride_with_veil", 1], + ["🤵", "person_in_tuxedo", 1], + ["🤵", "man_in_tuxedo", 1], + ["🏃♀️", "running_woman", 1], + ["🏃", "running_man", 1], + ["🚶♀️", "walking_woman", 1], + ["🚶", "walking_man", 1], + ["💃", "dancer", 1], + ["🕺", "man_dancing", 1], + ["👯", "dancing_women", 1], + ["👯♂️", "dancing_men", 1], + ["👫", "couple", 1], + ["🧑🤝🧑", "people_holding_hands", 1], + ["👬", "two_men_holding_hands", 1], + ["👭", "two_women_holding_hands", 1], + ["🫂", "people_hugging", 1], + ["🙇♀️", "bowing_woman", 1], + ["🙇", "bowing_man", 1], + ["🤦♂️", "man_facepalming", 1], + ["🤦♀️", "woman_facepalming", 1], + ["🤷", "woman_shrugging", 1], + ["🤷♂️", "man_shrugging", 1], + ["💁", "tipping_hand_woman", 1], + ["💁♂️", "tipping_hand_man", 1], + ["🙅", "no_good_woman", 1], + ["🙅♂️", "no_good_man", 1], + ["🙆", "ok_woman", 1], + ["🙆♂️", "ok_man", 1], + ["🙋", "raising_hand_woman", 1], + ["🙋♂️", "raising_hand_man", 1], + ["🙎", "pouting_woman", 1], + ["🙎♂️", "pouting_man", 1], + ["🙍", "frowning_woman", 1], + ["🙍♂️", "frowning_man", 1], + ["💇", "haircut_woman", 1], + ["💇♂️", "haircut_man", 1], + ["💆", "massage_woman", 1], + ["💆♂️", "massage_man", 1], + ["🧖♀️", "woman_in_steamy_room", 1], + ["🧖♂️", "man_in_steamy_room", 1], + ["🧏♀️", "woman_deaf", 1], + ["🧏♂️", "man_deaf", 1], + ["🧍♀️", "woman_standing", 1], + ["🧍♂️", "man_standing", 1], + ["🧎♀️", "woman_kneeling", 1], + ["🧎♂️", "man_kneeling", 1], + ["🧑🦯", "person_with_probing_cane", 1], + ["👩🦯", "woman_with_probing_cane", 1], + ["👨🦯", "man_with_probing_cane", 1], + ["🧑🦼", "person_in_motorized_wheelchair", 1], + ["👩🦼", "woman_in_motorized_wheelchair", 1], + ["👨🦼", "man_in_motorized_wheelchair", 1], + ["🧑🦽", "person_in_manual_wheelchair", 1], + ["👩🦽", "woman_in_manual_wheelchair", 1], + ["👨🦽", "man_in_manual_wheelchair", 1], + ["💑", "couple_with_heart_woman_man", 1], + ["👩❤️👩", "couple_with_heart_woman_woman", 1], + ["👨❤️👨", "couple_with_heart_man_man", 1], + ["💏", "couplekiss_man_woman", 1], + ["👩❤️💋👩", "couplekiss_woman_woman", 1], + ["👨❤️💋👨", "couplekiss_man_man", 1], + ["👪", "family_man_woman_boy", 1], + ["👨👩👧", "family_man_woman_girl", 1], + ["👨👩👧👦", "family_man_woman_girl_boy", 1], + ["👨👩👦👦", "family_man_woman_boy_boy", 1], + ["👨👩👧👧", "family_man_woman_girl_girl", 1], + ["👩👩👦", "family_woman_woman_boy", 1], + ["👩👩👧", "family_woman_woman_girl", 1], + ["👩👩👧👦", "family_woman_woman_girl_boy", 1], + ["👩👩👦👦", "family_woman_woman_boy_boy", 1], + ["👩👩👧👧", "family_woman_woman_girl_girl", 1], + ["👨👨👦", "family_man_man_boy", 1], + ["👨👨👧", "family_man_man_girl", 1], + ["👨👨👧👦", "family_man_man_girl_boy", 1], + ["👨👨👦👦", "family_man_man_boy_boy", 1], + ["👨👨👧👧", "family_man_man_girl_girl", 1], + ["👩👦", "family_woman_boy", 1], + ["👩👧", "family_woman_girl", 1], + ["👩👧👦", "family_woman_girl_boy", 1], + ["👩👦👦", "family_woman_boy_boy", 1], + ["👩👧👧", "family_woman_girl_girl", 1], + ["👨👦", "family_man_boy", 1], + ["👨👧", "family_man_girl", 1], + ["👨👧👦", "family_man_girl_boy", 1], + ["👨👦👦", "family_man_boy_boy", 1], + ["👨👧👧", "family_man_girl_girl", 1], + ["🧶", "yarn", 1], + ["🧵", "thread", 1], + ["🧥", "coat", 1], + ["🥼", "labcoat", 1], + ["👚", "womans_clothes", 1], + ["👕", "tshirt", 1], + ["👖", "jeans", 1], + ["👔", "necktie", 1], + ["👗", "dress", 1], + ["👙", "bikini", 1], + ["🩱", "one_piece_swimsuit", 1], + ["👘", "kimono", 1], + ["🥻", "sari", 1], + ["🩲", "briefs", 1], + ["🩳", "shorts", 1], + ["💄", "lipstick", 1], + ["💋", "kiss", 1], + ["👣", "footprints", 1], + ["🥿", "flat_shoe", 1], + ["👠", "high_heel", 1], + ["👡", "sandal", 1], + ["👢", "boot", 1], + ["👞", "mans_shoe", 1], + ["👟", "athletic_shoe", 1], + ["🩴", "thong_sandal", 1], + ["🩰", "ballet_shoes", 1], + ["🧦", "socks", 1], + ["🧤", "gloves", 1], + ["🧣", "scarf", 1], + ["👒", "womans_hat", 1], + ["🎩", "tophat", 1], + ["🧢", "billed_hat", 1], + ["⛑", "rescue_worker_helmet", 1], + ["🪖", "military_helmet", 1], + ["🎓", "mortar_board", 1], + ["👑", "crown", 1], + ["🎒", "school_satchel", 1], + ["🧳", "luggage", 1], + ["👝", "pouch", 1], + ["👛", "purse", 1], + ["👜", "handbag", 1], + ["💼", "briefcase", 1], + ["👓", "eyeglasses", 1], + ["🕶", "dark_sunglasses", 1], + ["🥽", "goggles", 1], + ["💍", "ring", 1], + ["🌂", "closed_umbrella", 1], + ["🐶", "dog", 2], + ["🐱", "cat", 2], + ["🐈⬛", "black_cat", 2], + ["🐭", "mouse", 2], + ["🐹", "hamster", 2], + ["🐰", "rabbit", 2], + ["🦊", "fox_face", 2], + ["🐻", "bear", 2], + ["🐼", "panda_face", 2], + ["🐨", "koala", 2], + ["🐯", "tiger", 2], + ["🦁", "lion", 2], + ["🐮", "cow", 2], + ["🐷", "pig", 2], + ["🐽", "pig_nose", 2], + ["🐸", "frog", 2], + ["🦑", "squid", 2], + ["🐙", "octopus", 2], + ["🦐", "shrimp", 2], + ["🐵", "monkey_face", 2], + ["🦍", "gorilla", 2], + ["🙈", "see_no_evil", 2], + ["🙉", "hear_no_evil", 2], + ["🙊", "speak_no_evil", 2], + ["🐒", "monkey", 2], + ["🐔", "chicken", 2], + ["🐧", "penguin", 2], + ["🐦", "bird", 2], + ["🐤", "baby_chick", 2], + ["🐣", "hatching_chick", 2], + ["🐥", "hatched_chick", 2], + ["🦆", "duck", 2], + ["🦅", "eagle", 2], + ["🦉", "owl", 2], + ["🦇", "bat", 2], + ["🐺", "wolf", 2], + ["🐗", "boar", 2], + ["🐴", "horse", 2], + ["🦄", "unicorn", 2], + ["🐝", "honeybee", 2], + ["🐛", "bug", 2], + ["🦋", "butterfly", 2], + ["🐌", "snail", 2], + ["🐞", "lady_beetle", 2], + ["🐜", "ant", 2], + ["🦗", "grasshopper", 2], + ["🕷", "spider", 2], + ["🪲", "beetle", 2], + ["🪳", "cockroach", 2], + ["🪰", "fly", 2], + ["🪱", "worm", 2], + ["🦂", "scorpion", 2], + ["🦀", "crab", 2], + ["🐍", "snake", 2], + ["🦎", "lizard", 2], + ["🦖", "t-rex", 2], + ["🦕", "sauropod", 2], + ["🐢", "turtle", 2], + ["🐠", "tropical_fish", 2], + ["🐟", "fish", 2], + ["🐡", "blowfish", 2], + ["🐬", "dolphin", 2], + ["🦈", "shark", 2], + ["🐳", "whale", 2], + ["🐋", "whale2", 2], + ["🐊", "crocodile", 2], + ["🐆", "leopard", 2], + ["🦓", "zebra", 2], + ["🐅", "tiger2", 2], + ["🐃", "water_buffalo", 2], + ["🐂", "ox", 2], + ["🐄", "cow2", 2], + ["🦌", "deer", 2], + ["🐪", "dromedary_camel", 2], + ["🐫", "camel", 2], + ["🦒", "giraffe", 2], + ["🐘", "elephant", 2], + ["🦏", "rhinoceros", 2], + ["🐐", "goat", 2], + ["🐏", "ram", 2], + ["🐑", "sheep", 2], + ["🐎", "racehorse", 2], + ["🐖", "pig2", 2], + ["🐀", "rat", 2], + ["🐁", "mouse2", 2], + ["🐓", "rooster", 2], + ["🦃", "turkey", 2], + ["🕊", "dove", 2], + ["🐕", "dog2", 2], + ["🐩", "poodle", 2], + ["🐈", "cat2", 2], + ["🐇", "rabbit2", 2], + ["🐿", "chipmunk", 2], + ["🦔", "hedgehog", 2], + ["🦝", "raccoon", 2], + ["🦙", "llama", 2], + ["🦛", "hippopotamus", 2], + ["🦘", "kangaroo", 2], + ["🦡", "badger", 2], + ["🦢", "swan", 2], + ["🦚", "peacock", 2], + ["🦜", "parrot", 2], + ["🦞", "lobster", 2], + ["🦠", "microbe", 2], + ["🦟", "mosquito", 2], + ["🦬", "bison", 2], + ["🦣", "mammoth", 2], + ["🦫", "beaver", 2], + ["🐻❄️", "polar_bear", 2], + ["🦤", "dodo", 2], + ["🪶", "feather", 2], + ["🦭", "seal", 2], + ["🐾", "paw_prints", 2], + ["🐉", "dragon", 2], + ["🐲", "dragon_face", 2], + ["🦧", "orangutan", 2], + ["🦮", "guide_dog", 2], + ["🐕🦺", "service_dog", 2], + ["🦥", "sloth", 2], + ["🦦", "otter", 2], + ["🦨", "skunk", 2], + ["🦩", "flamingo", 2], + ["🌵", "cactus", 2], + ["🎄", "christmas_tree", 2], + ["🌲", "evergreen_tree", 2], + ["🌳", "deciduous_tree", 2], + ["🌴", "palm_tree", 2], + ["🌱", "seedling", 2], + ["🌿", "herb", 2], + ["☘", "shamrock", 2], + ["🍀", "four_leaf_clover", 2], + ["🎍", "bamboo", 2], + ["🎋", "tanabata_tree", 2], + ["🍃", "leaves", 2], + ["🍂", "fallen_leaf", 2], + ["🍁", "maple_leaf", 2], + ["🌾", "ear_of_rice", 2], + ["🌺", "hibiscus", 2], + ["🌻", "sunflower", 2], + ["🌹", "rose", 2], + ["🥀", "wilted_flower", 2], + ["🌷", "tulip", 2], + ["🌼", "blossom", 2], + ["🌸", "cherry_blossom", 2], + ["💐", "bouquet", 2], + ["🍄", "mushroom", 2], + ["🪴", "potted_plant", 2], + ["🌰", "chestnut", 2], + ["🎃", "jack_o_lantern", 2], + ["🐚", "shell", 2], + ["🕸", "spider_web", 2], + ["🌎", "earth_americas", 2], + ["🌍", "earth_africa", 2], + ["🌏", "earth_asia", 2], + ["🪐", "ringed_planet", 2], + ["🌕", "full_moon", 2], + ["🌖", "waning_gibbous_moon", 2], + ["🌗", "last_quarter_moon", 2], + ["🌘", "waning_crescent_moon", 2], + ["🌑", "new_moon", 2], + ["🌒", "waxing_crescent_moon", 2], + ["🌓", "first_quarter_moon", 2], + ["🌔", "waxing_gibbous_moon", 2], + ["🌚", "new_moon_with_face", 2], + ["🌝", "full_moon_with_face", 2], + ["🌛", "first_quarter_moon_with_face", 2], + ["🌜", "last_quarter_moon_with_face", 2], + ["🌞", "sun_with_face", 2], + ["🌙", "crescent_moon", 2], + ["⭐", "star", 2], + ["🌟", "star2", 2], + ["💫", "dizzy", 2], + ["✨", "sparkles", 2], + ["☄", "comet", 2], + ["☀️", "sunny", 2], + ["🌤", "sun_behind_small_cloud", 2], + ["⛅", "partly_sunny", 2], + ["🌥", "sun_behind_large_cloud", 2], + ["🌦", "sun_behind_rain_cloud", 2], + ["☁️", "cloud", 2], + ["🌧", "cloud_with_rain", 2], + ["⛈", "cloud_with_lightning_and_rain", 2], + ["🌩", "cloud_with_lightning", 2], + ["⚡", "zap", 2], + ["🔥", "fire", 2], + ["💥", "boom", 2], + ["❄️", "snowflake", 2], + ["🌨", "cloud_with_snow", 2], + ["⛄", "snowman", 2], + ["☃", "snowman_with_snow", 2], + ["🌬", "wind_face", 2], + ["💨", "dash", 2], + ["🌪", "tornado", 2], + ["🌫", "fog", 2], + ["☂", "open_umbrella", 2], + ["☔", "umbrella", 2], + ["💧", "droplet", 2], + ["💦", "sweat_drops", 2], + ["🌊", "ocean", 2], + ["🪷", "lotus", 2], + ["🪸", "coral", 2], + ["🪹", "empty_nest", 2], + ["🪺", "nest_with_eggs", 2], + ["🍏", "green_apple", 3], + ["🍎", "apple", 3], + ["🍐", "pear", 3], + ["🍊", "tangerine", 3], + ["🍋", "lemon", 3], + ["🍌", "banana", 3], + ["🍉", "watermelon", 3], + ["🍇", "grapes", 3], + ["🍓", "strawberry", 3], + ["🍈", "melon", 3], + ["🍒", "cherries", 3], + ["🍑", "peach", 3], + ["🍍", "pineapple", 3], + ["🥥", "coconut", 3], + ["🥝", "kiwi_fruit", 3], + ["🥭", "mango", 3], + ["🥑", "avocado", 3], + ["🥦", "broccoli", 3], + ["🍅", "tomato", 3], + ["🍆", "eggplant", 3], + ["🥒", "cucumber", 3], + ["🫐", "blueberries", 3], + ["🫒", "olive", 3], + ["🫑", "bell_pepper", 3], + ["🥕", "carrot", 3], + ["🌶", "hot_pepper", 3], + ["🥔", "potato", 3], + ["🌽", "corn", 3], + ["🥬", "leafy_greens", 3], + ["🍠", "sweet_potato", 3], + ["🥜", "peanuts", 3], + ["🧄", "garlic", 3], + ["🧅", "onion", 3], + ["🍯", "honey_pot", 3], + ["🥐", "croissant", 3], + ["🍞", "bread", 3], + ["🥖", "baguette_bread", 3], + ["🥯", "bagel", 3], + ["🥨", "pretzel", 3], + ["🧀", "cheese", 3], + ["🥚", "egg", 3], + ["🥓", "bacon", 3], + ["🥩", "steak", 3], + ["🥞", "pancakes", 3], + ["🍗", "poultry_leg", 3], + ["🍖", "meat_on_bone", 3], + ["🦴", "bone", 3], + ["🍤", "fried_shrimp", 3], + ["🍳", "fried_egg", 3], + ["🍔", "hamburger", 3], + ["🍟", "fries", 3], + ["🥙", "stuffed_flatbread", 3], + ["🌭", "hotdog", 3], + ["🍕", "pizza", 3], + ["🥪", "sandwich", 3], + ["🥫", "canned_food", 3], + ["🍝", "spaghetti", 3], + ["🌮", "taco", 3], + ["🌯", "burrito", 3], + ["🥗", "green_salad", 3], + ["🥘", "shallow_pan_of_food", 3], + ["🍜", "ramen", 3], + ["🍲", "stew", 3], + ["🍥", "fish_cake", 3], + ["🥠", "fortune_cookie", 3], + ["🍣", "sushi", 3], + ["🍱", "bento", 3], + ["🍛", "curry", 3], + ["🍙", "rice_ball", 3], + ["🍚", "rice", 3], + ["🍘", "rice_cracker", 3], + ["🍢", "oden", 3], + ["🍡", "dango", 3], + ["🍧", "shaved_ice", 3], + ["🍨", "ice_cream", 3], + ["🍦", "icecream", 3], + ["🥧", "pie", 3], + ["🍰", "cake", 3], + ["🧁", "cupcake", 3], + ["🥮", "moon_cake", 3], + ["🎂", "birthday", 3], + ["🍮", "custard", 3], + ["🍬", "candy", 3], + ["🍭", "lollipop", 3], + ["🍫", "chocolate_bar", 3], + ["🍿", "popcorn", 3], + ["🥟", "dumpling", 3], + ["🍩", "doughnut", 3], + ["🍪", "cookie", 3], + ["🧇", "waffle", 3], + ["🧆", "falafel", 3], + ["🧈", "butter", 3], + ["🦪", "oyster", 3], + ["🫓", "flatbread", 3], + ["🫔", "tamale", 3], + ["🫕", "fondue", 3], + ["🥛", "milk_glass", 3], + ["🍺", "beer", 3], + ["🍻", "beers", 3], + ["🥂", "clinking_glasses", 3], + ["🍷", "wine_glass", 3], + ["🥃", "tumbler_glass", 3], + ["🍸", "cocktail", 3], + ["🍹", "tropical_drink", 3], + ["🍾", "champagne", 3], + ["🍶", "sake", 3], + ["🍵", "tea", 3], + ["🥤", "cup_with_straw", 3], + ["☕", "coffee", 3], + ["🫖", "teapot", 3], + ["🧋", "bubble_tea", 3], + ["🍼", "baby_bottle", 3], + ["🧃", "beverage_box", 3], + ["🧉", "mate", 3], + ["🧊", "ice_cube", 3], + ["🧂", "salt", 3], + ["🥄", "spoon", 3], + ["🍴", "fork_and_knife", 3], + ["🍽", "plate_with_cutlery", 3], + ["🥣", "bowl_with_spoon", 3], + ["🥡", "takeout_box", 3], + ["🥢", "chopsticks", 3], + ["🫗", "pouring_liquid", 3], + ["🫘", "beans", 3], + ["🫙", "jar", 3], + ["⚽", "soccer", 4], + ["🏀", "basketball", 4], + ["🏈", "football", 4], + ["⚾", "baseball", 4], + ["🥎", "softball", 4], + ["🎾", "tennis", 4], + ["🏐", "volleyball", 4], + ["🏉", "rugby_football", 4], + ["🥏", "flying_disc", 4], + ["🎱", "8ball", 4], + ["⛳", "golf", 4], + ["🏌️♀️", "golfing_woman", 4], + ["🏌", "golfing_man", 4], + ["🏓", "ping_pong", 4], + ["🏸", "badminton", 4], + ["🥅", "goal_net", 4], + ["🏒", "ice_hockey", 4], + ["🏑", "field_hockey", 4], + ["🥍", "lacrosse", 4], + ["🏏", "cricket", 4], + ["🎿", "ski", 4], + ["⛷", "skier", 4], + ["🏂", "snowboarder", 4], + ["🤺", "person_fencing", 4], + ["🤼♀️", "women_wrestling", 4], + ["🤼♂️", "men_wrestling", 4], + ["🤸♀️", "woman_cartwheeling", 4], + ["🤸♂️", "man_cartwheeling", 4], + ["🤾♀️", "woman_playing_handball", 4], + ["🤾♂️", "man_playing_handball", 4], + ["⛸", "ice_skate", 4], + ["🥌", "curling_stone", 4], + ["🛹", "skateboard", 4], + ["🛷", "sled", 4], + ["🏹", "bow_and_arrow", 4], + ["🎣", "fishing_pole_and_fish", 4], + ["🥊", "boxing_glove", 4], + ["🥋", "martial_arts_uniform", 4], + ["🚣♀️", "rowing_woman", 4], + ["🚣", "rowing_man", 4], + ["🧗♀️", "climbing_woman", 4], + ["🧗♂️", "climbing_man", 4], + ["🏊♀️", "swimming_woman", 4], + ["🏊", "swimming_man", 4], + ["🤽♀️", "woman_playing_water_polo", 4], + ["🤽♂️", "man_playing_water_polo", 4], + ["🧘♀️", "woman_in_lotus_position", 4], + ["🧘♂️", "man_in_lotus_position", 4], + ["🏄♀️", "surfing_woman", 4], + ["🏄", "surfing_man", 4], + ["🛀", "bath", 4], + ["⛹️♀️", "basketball_woman", 4], + ["⛹", "basketball_man", 4], + ["🏋️♀️", "weight_lifting_woman", 4], + ["🏋", "weight_lifting_man", 4], + ["🚴♀️", "biking_woman", 4], + ["🚴", "biking_man", 4], + ["🚵♀️", "mountain_biking_woman", 4], + ["🚵", "mountain_biking_man", 4], + ["🏇", "horse_racing", 4], + ["🤿", "diving_mask", 4], + ["🪀", "yo_yo", 4], + ["🪁", "kite", 4], + ["🦺", "safety_vest", 4], + ["🪡", "sewing_needle", 4], + ["🪢", "knot", 4], + ["🕴", "business_suit_levitating", 4], + ["🏆", "trophy", 4], + ["🎽", "running_shirt_with_sash", 4], + ["🏅", "medal_sports", 4], + ["🎖", "medal_military", 4], + ["🥇", "1st_place_medal", 4], + ["🥈", "2nd_place_medal", 4], + ["🥉", "3rd_place_medal", 4], + ["🎗", "reminder_ribbon", 4], + ["🏵", "rosette", 4], + ["🎫", "ticket", 4], + ["🎟", "tickets", 4], + ["🎭", "performing_arts", 4], + ["🎨", "art", 4], + ["🎪", "circus_tent", 4], + ["🤹♀️", "woman_juggling", 4], + ["🤹♂️", "man_juggling", 4], + ["🎤", "microphone", 4], + ["🎧", "headphones", 4], + ["🎼", "musical_score", 4], + ["🎹", "musical_keyboard", 4], + ["🥁", "drum", 4], + ["🎷", "saxophone", 4], + ["🎺", "trumpet", 4], + ["🎸", "guitar", 4], + ["🎻", "violin", 4], + ["🪕", "banjo", 4], + ["🪗", "accordion", 4], + ["🪘", "long_drum", 4], + ["🎬", "clapper", 4], + ["🎮", "video_game", 4], + ["👾", "space_invader", 4], + ["🎯", "dart", 4], + ["🎲", "game_die", 4], + ["♟️", "chess_pawn", 4], + ["🎰", "slot_machine", 4], + ["🧩", "jigsaw", 4], + ["🎳", "bowling", 4], + ["🪄", "magic_wand", 4], + ["🪅", "pinata", 4], + ["🪆", "nesting_dolls", 4], + ["🪬", "hamsa", 4], + ["🪩", "mirror_ball", 4], + ["🚗", "red_car", 5], + ["🚕", "taxi", 5], + ["🚙", "blue_car", 5], + ["🚌", "bus", 5], + ["🚎", "trolleybus", 5], + ["🏎", "racing_car", 5], + ["🚓", "police_car", 5], + ["🚑", "ambulance", 5], + ["🚒", "fire_engine", 5], + ["🚐", "minibus", 5], + ["🚚", "truck", 5], + ["🚛", "articulated_lorry", 5], + ["🚜", "tractor", 5], + ["🛴", "kick_scooter", 5], + ["🏍", "motorcycle", 5], + ["🚲", "bike", 5], + ["🛵", "motor_scooter", 5], + ["🦽", "manual_wheelchair", 5], + ["🦼", "motorized_wheelchair", 5], + ["🛺", "auto_rickshaw", 5], + ["🪂", "parachute", 5], + ["🚨", "rotating_light", 5], + ["🚔", "oncoming_police_car", 5], + ["🚍", "oncoming_bus", 5], + ["🚘", "oncoming_automobile", 5], + ["🚖", "oncoming_taxi", 5], + ["🚡", "aerial_tramway", 5], + ["🚠", "mountain_cableway", 5], + ["🚟", "suspension_railway", 5], + ["🚃", "railway_car", 5], + ["🚋", "train", 5], + ["🚝", "monorail", 5], + ["🚄", "bullettrain_side", 5], + ["🚅", "bullettrain_front", 5], + ["🚈", "light_rail", 5], + ["🚞", "mountain_railway", 5], + ["🚂", "steam_locomotive", 5], + ["🚆", "train2", 5], + ["🚇", "metro", 5], + ["🚊", "tram", 5], + ["🚉", "station", 5], + ["🛸", "flying_saucer", 5], + ["🚁", "helicopter", 5], + ["🛩", "small_airplane", 5], + ["✈️", "airplane", 5], + ["🛫", "flight_departure", 5], + ["🛬", "flight_arrival", 5], + ["⛵", "sailboat", 5], + ["🛥", "motor_boat", 5], + ["🚤", "speedboat", 5], + ["⛴", "ferry", 5], + ["🛳", "passenger_ship", 5], + ["🚀", "rocket", 5], + ["🛰", "artificial_satellite", 5], + ["🛻", "pickup_truck", 5], + ["🛼", "roller_skate", 5], + ["💺", "seat", 5], + ["🛶", "canoe", 5], + ["⚓", "anchor", 5], + ["🚧", "construction", 5], + ["⛽", "fuelpump", 5], + ["🚏", "busstop", 5], + ["🚦", "vertical_traffic_light", 5], + ["🚥", "traffic_light", 5], + ["🏁", "checkered_flag", 5], + ["🚢", "ship", 5], + ["🎡", "ferris_wheel", 5], + ["🎢", "roller_coaster", 5], + ["🎠", "carousel_horse", 5], + ["🏗", "building_construction", 5], + ["🌁", "foggy", 5], + ["🏭", "factory", 5], + ["⛲", "fountain", 5], + ["🎑", "rice_scene", 5], + ["⛰", "mountain", 5], + ["🏔", "mountain_snow", 5], + ["🗻", "mount_fuji", 5], + ["🌋", "volcano", 5], + ["🗾", "japan", 5], + ["🏕", "camping", 5], + ["⛺", "tent", 5], + ["🏞", "national_park", 5], + ["🛣", "motorway", 5], + ["🛤", "railway_track", 5], + ["🌅", "sunrise", 5], + ["🌄", "sunrise_over_mountains", 5], + ["🏜", "desert", 5], + ["🏖", "beach_umbrella", 5], + ["🏝", "desert_island", 5], + ["🌇", "city_sunrise", 5], + ["🌆", "city_sunset", 5], + ["🏙", "cityscape", 5], + ["🌃", "night_with_stars", 5], + ["🌉", "bridge_at_night", 5], + ["🌌", "milky_way", 5], + ["🌠", "stars", 5], + ["🎇", "sparkler", 5], + ["🎆", "fireworks", 5], + ["🌈", "rainbow", 5], + ["🏘", "houses", 5], + ["🏰", "european_castle", 5], + ["🏯", "japanese_castle", 5], + ["🗼", "tokyo_tower", 5], + ["", "shibuya_109", 5], + ["🏟", "stadium", 5], + ["🗽", "statue_of_liberty", 5], + ["🏠", "house", 5], + ["🏡", "house_with_garden", 5], + ["🏚", "derelict_house", 5], + ["🏢", "office", 5], + ["🏬", "department_store", 5], + ["🏣", "post_office", 5], + ["🏤", "european_post_office", 5], + ["🏥", "hospital", 5], + ["🏦", "bank", 5], + ["🏨", "hotel", 5], + ["🏪", "convenience_store", 5], + ["🏫", "school", 5], + ["🏩", "love_hotel", 5], + ["💒", "wedding", 5], + ["🏛", "classical_building", 5], + ["⛪", "church", 5], + ["🕌", "mosque", 5], + ["🕍", "synagogue", 5], + ["🕋", "kaaba", 5], + ["⛩", "shinto_shrine", 5], + ["🛕", "hindu_temple", 5], + ["🪨", "rock", 5], + ["🪵", "wood", 5], + ["🛖", "hut", 5], + ["🛝", "playground_slide", 5], + ["🛞", "wheel", 5], + ["🛟", "ring_buoy", 5], + ["⌚", "watch", 6], + ["📱", "iphone", 6], + ["📲", "calling", 6], + ["💻", "computer", 6], + ["⌨", "keyboard", 6], + ["🖥", "desktop_computer", 6], + ["🖨", "printer", 6], + ["🖱", "computer_mouse", 6], + ["🖲", "trackball", 6], + ["🕹", "joystick", 6], + ["🗜", "clamp", 6], + ["💽", "minidisc", 6], + ["💾", "floppy_disk", 6], + ["💿", "cd", 6], + ["📀", "dvd", 6], + ["📼", "vhs", 6], + ["📷", "camera", 6], + ["📸", "camera_flash", 6], + ["📹", "video_camera", 6], + ["🎥", "movie_camera", 6], + ["📽", "film_projector", 6], + ["🎞", "film_strip", 6], + ["📞", "telephone_receiver", 6], + ["☎️", "phone", 6], + ["📟", "pager", 6], + ["📠", "fax", 6], + ["📺", "tv", 6], + ["📻", "radio", 6], + ["🎙", "studio_microphone", 6], + ["🎚", "level_slider", 6], + ["🎛", "control_knobs", 6], + ["🧭", "compass", 6], + ["⏱", "stopwatch", 6], + ["⏲", "timer_clock", 6], + ["⏰", "alarm_clock", 6], + ["🕰", "mantelpiece_clock", 6], + ["⏳", "hourglass_flowing_sand", 6], + ["⌛", "hourglass", 6], + ["📡", "satellite", 6], + ["🔋", "battery", 6], + ["🪫", "battery", 6], + ["🔌", "electric_plug", 6], + ["💡", "bulb", 6], + ["🔦", "flashlight", 6], + ["🕯", "candle", 6], + ["🧯", "fire_extinguisher", 6], + ["🗑", "wastebasket", 6], + ["🛢", "oil_drum", 6], + ["💸", "money_with_wings", 6], + ["💵", "dollar", 6], + ["💴", "yen", 6], + ["💶", "euro", 6], + ["💷", "pound", 6], + ["💰", "moneybag", 6], + ["🪙", "coin", 6], + ["💳", "credit_card", 6], + ["🪫", "identification_card", 6], + ["💎", "gem", 6], + ["⚖", "balance_scale", 6], + ["🧰", "toolbox", 6], + ["🔧", "wrench", 6], + ["🔨", "hammer", 6], + ["⚒", "hammer_and_pick", 6], + ["🛠", "hammer_and_wrench", 6], + ["⛏", "pick", 6], + ["🪓", "axe", 6], + ["🦯", "probing_cane", 6], + ["🔩", "nut_and_bolt", 6], + ["⚙", "gear", 6], + ["🪃", "boomerang", 6], + ["🪚", "carpentry_saw", 6], + ["🪛", "screwdriver", 6], + ["🪝", "hook", 6], + ["🪜", "ladder", 6], + ["🧱", "brick", 6], + ["⛓", "chains", 6], + ["🧲", "magnet", 6], + ["🔫", "gun", 6], + ["💣", "bomb", 6], + ["🧨", "firecracker", 6], + ["🔪", "hocho", 6], + ["🗡", "dagger", 6], + ["⚔", "crossed_swords", 6], + ["🛡", "shield", 6], + ["🚬", "smoking", 6], + ["☠", "skull_and_crossbones", 6], + ["⚰", "coffin", 6], + ["⚱", "funeral_urn", 6], + ["🏺", "amphora", 6], + ["🔮", "crystal_ball", 6], + ["📿", "prayer_beads", 6], + ["🧿", "nazar_amulet", 6], + ["💈", "barber", 6], + ["⚗", "alembic", 6], + ["🔭", "telescope", 6], + ["🔬", "microscope", 6], + ["🕳", "hole", 6], + ["💊", "pill", 6], + ["💉", "syringe", 6], + ["🩸", "drop_of_blood", 6], + ["🩹", "adhesive_bandage", 6], + ["🩺", "stethoscope", 6], + ["🪒", "razor", 6], + ["🩻", "xray", 6], + ["🩼", "crutch", 6], + ["🧬", "dna", 6], + ["🧫", "petri_dish", 6], + ["🧪", "test_tube", 6], + ["🌡", "thermometer", 6], + ["🧹", "broom", 6], + ["🧺", "basket", 6], + ["🧻", "toilet_paper", 6], + ["🏷", "label", 6], + ["🔖", "bookmark", 6], + ["🚽", "toilet", 6], + ["🚿", "shower", 6], + ["🛁", "bathtub", 6], + ["🧼", "soap", 6], + ["🧽", "sponge", 6], + ["🧴", "lotion_bottle", 6], + ["🔑", "key", 6], + ["🗝", "old_key", 6], + ["🛋", "couch_and_lamp", 6], + ["🪔", "diya_Lamp", 6], + ["🛌", "sleeping_bed", 6], + ["🛏", "bed", 6], + ["🚪", "door", 6], + ["🪑", "chair", 6], + ["🛎", "bellhop_bell", 6], + ["🧸", "teddy_bear", 6], + ["🖼", "framed_picture", 6], + ["🗺", "world_map", 6], + ["🛗", "elevator", 6], + ["🪞", "mirror", 6], + ["🪟", "window", 6], + ["🪠", "plunger", 6], + ["🪤", "mouse_trap", 6], + ["🪣", "bucket", 6], + ["🪥", "toothbrush", 6], + ["🫧", "bubbles", 6], + ["⛱", "parasol_on_ground", 6], + ["🗿", "moyai", 6], + ["🛍", "shopping", 6], + ["🛒", "shopping_cart", 6], + ["🎈", "balloon", 6], + ["🎏", "flags", 6], + ["🎀", "ribbon", 6], + ["🎁", "gift", 6], + ["🎊", "confetti_ball", 6], + ["🎉", "tada", 6], + ["🎎", "dolls", 6], + ["🎐", "wind_chime", 6], + ["🎌", "crossed_flags", 6], + ["🏮", "izakaya_lantern", 6], + ["🧧", "red_envelope", 6], + ["✉️", "email", 6], + ["📩", "envelope_with_arrow", 6], + ["📨", "incoming_envelope", 6], + ["📧", "e-mail", 6], + ["💌", "love_letter", 6], + ["📮", "postbox", 6], + ["📪", "mailbox_closed", 6], + ["📫", "mailbox", 6], + ["📬", "mailbox_with_mail", 6], + ["📭", "mailbox_with_no_mail", 6], + ["📦", "package", 6], + ["📯", "postal_horn", 6], + ["📥", "inbox_tray", 6], + ["📤", "outbox_tray", 6], + ["📜", "scroll", 6], + ["📃", "page_with_curl", 6], + ["📑", "bookmark_tabs", 6], + ["🧾", "receipt", 6], + ["📊", "bar_chart", 6], + ["📈", "chart_with_upwards_trend", 6], + ["📉", "chart_with_downwards_trend", 6], + ["📄", "page_facing_up", 6], + ["📅", "date", 6], + ["📆", "calendar", 6], + ["🗓", "spiral_calendar", 6], + ["📇", "card_index", 6], + ["🗃", "card_file_box", 6], + ["🗳", "ballot_box", 6], + ["🗄", "file_cabinet", 6], + ["📋", "clipboard", 6], + ["🗒", "spiral_notepad", 6], + ["📁", "file_folder", 6], + ["📂", "open_file_folder", 6], + ["🗂", "card_index_dividers", 6], + ["🗞", "newspaper_roll", 6], + ["📰", "newspaper", 6], + ["📓", "notebook", 6], + ["📕", "closed_book", 6], + ["📗", "green_book", 6], + ["📘", "blue_book", 6], + ["📙", "orange_book", 6], + ["📔", "notebook_with_decorative_cover", 6], + ["📒", "ledger", 6], + ["📚", "books", 6], + ["📖", "open_book", 6], + ["🧷", "safety_pin", 6], + ["🔗", "link", 6], + ["📎", "paperclip", 6], + ["🖇", "paperclips", 6], + ["✂️", "scissors", 6], + ["📐", "triangular_ruler", 6], + ["📏", "straight_ruler", 6], + ["🧮", "abacus", 6], + ["📌", "pushpin", 6], + ["📍", "round_pushpin", 6], + ["🚩", "triangular_flag_on_post", 6], + ["🏳", "white_flag", 6], + ["🏴", "black_flag", 6], + ["🏳️🌈", "rainbow_flag", 6], + ["🏳️⚧️", "transgender_flag", 6], + ["🔐", "closed_lock_with_key", 6], + ["🔒", "lock", 6], + ["🔓", "unlock", 6], + ["🔏", "lock_with_ink_pen", 6], + ["🖊", "pen", 6], + ["🖋", "fountain_pen", 6], + ["✒️", "black_nib", 6], + ["📝", "memo", 6], + ["✏️", "pencil2", 6], + ["🖍", "crayon", 6], + ["🖌", "paintbrush", 6], + ["🔍", "mag", 6], + ["🔎", "mag_right", 6], + ["🪦", "headstone", 6], + ["🪧", "placard", 6], + ["💯", "100", 7], + ["🔢", "1234", 7], + ["❤️", "heart", 7], + ["🧡", "orange_heart", 7], + ["💛", "yellow_heart", 7], + ["💚", "green_heart", 7], + ["💙", "blue_heart", 7], + ["💜", "purple_heart", 7], + ["🤎", "brown_heart", 7], + ["🖤", "black_heart", 7], + ["🤍", "white_heart", 7], + ["💔", "broken_heart", 7], + ["❣", "heavy_heart_exclamation", 7], + ["💕", "two_hearts", 7], + ["💞", "revolving_hearts", 7], + ["💓", "heartbeat", 7], + ["💗", "heartpulse", 7], + ["💖", "sparkling_heart", 7], + ["💘", "cupid", 7], + ["💝", "gift_heart", 7], + ["💟", "heart_decoration", 7], + ["❤️🔥", "heart_on_fire", 7], + ["❤️🩹", "mending_heart", 7], + ["☮", "peace_symbol", 7], + ["✝", "latin_cross", 7], + ["☪", "star_and_crescent", 7], + ["🕉", "om", 7], + ["☸", "wheel_of_dharma", 7], + ["✡", "star_of_david", 7], + ["🔯", "six_pointed_star", 7], + ["🕎", "menorah", 7], + ["☯", "yin_yang", 7], + ["☦", "orthodox_cross", 7], + ["🛐", "place_of_worship", 7], + ["⛎", "ophiuchus", 7], + ["♈", "aries", 7], + ["♉", "taurus", 7], + ["♊", "gemini", 7], + ["♋", "cancer", 7], + ["♌", "leo", 7], + ["♍", "virgo", 7], + ["♎", "libra", 7], + ["♏", "scorpius", 7], + ["♐", "sagittarius", 7], + ["♑", "capricorn", 7], + ["♒", "aquarius", 7], + ["♓", "pisces", 7], + ["🆔", "id", 7], + ["⚛", "atom_symbol", 7], + ["⚧️", "transgender_symbol", 7], + ["🈳", "u7a7a", 7], + ["🈹", "u5272", 7], + ["☢", "radioactive", 7], + ["☣", "biohazard", 7], + ["📴", "mobile_phone_off", 7], + ["📳", "vibration_mode", 7], + ["🈶", "u6709", 7], + ["🈚", "u7121", 7], + ["🈸", "u7533", 7], + ["🈺", "u55b6", 7], + ["🈷️", "u6708", 7], + ["✴️", "eight_pointed_black_star", 7], + ["🆚", "vs", 7], + ["🉑", "accept", 7], + ["💮", "white_flower", 7], + ["🉐", "ideograph_advantage", 7], + ["㊙️", "secret", 7], + ["㊗️", "congratulations", 7], + ["🈴", "u5408", 7], + ["🈵", "u6e80", 7], + ["🈲", "u7981", 7], + ["🅰️", "a", 7], + ["🅱️", "b", 7], + ["🆎", "ab", 7], + ["🆑", "cl", 7], + ["🅾️", "o2", 7], + ["🆘", "sos", 7], + ["⛔", "no_entry", 7], + ["📛", "name_badge", 7], + ["🚫", "no_entry_sign", 7], + ["❌", "x", 7], + ["⭕", "o", 7], + ["🛑", "stop_sign", 7], + ["💢", "anger", 7], + ["♨️", "hotsprings", 7], + ["🚷", "no_pedestrians", 7], + ["🚯", "do_not_litter", 7], + ["🚳", "no_bicycles", 7], + ["🚱", "non-potable_water", 7], + ["🔞", "underage", 7], + ["📵", "no_mobile_phones", 7], + ["❗", "exclamation", 7], + ["❕", "grey_exclamation", 7], + ["❓", "question", 7], + ["❔", "grey_question", 7], + ["‼️", "bangbang", 7], + ["⁉️", "interrobang", 7], + ["🔅", "low_brightness", 7], + ["🔆", "high_brightness", 7], + ["🔱", "trident", 7], + ["⚜", "fleur_de_lis", 7], + ["〽️", "part_alternation_mark", 7], + ["⚠️", "warning", 7], + ["🚸", "children_crossing", 7], + ["🔰", "beginner", 7], + ["♻️", "recycle", 7], + ["🈯", "u6307", 7], + ["💹", "chart", 7], + ["❇️", "sparkle", 7], + ["✳️", "eight_spoked_asterisk", 7], + ["❎", "negative_squared_cross_mark", 7], + ["✅", "white_check_mark", 7], + ["💠", "diamond_shape_with_a_dot_inside", 7], + ["🌀", "cyclone", 7], + ["➿", "loop", 7], + ["🌐", "globe_with_meridians", 7], + ["Ⓜ️", "m", 7], + ["🏧", "atm", 7], + ["🈂️", "sa", 7], + ["🛂", "passport_control", 7], + ["🛃", "customs", 7], + ["🛄", "baggage_claim", 7], + ["🛅", "left_luggage", 7], + ["♿", "wheelchair", 7], + ["🚭", "no_smoking", 7], + ["🚾", "wc", 7], + ["🅿️", "parking", 7], + ["🚰", "potable_water", 7], + ["🚹", "mens", 7], + ["🚺", "womens", 7], + ["🚼", "baby_symbol", 7], + ["🚻", "restroom", 7], + ["🚮", "put_litter_in_its_place", 7], + ["🎦", "cinema", 7], + ["📶", "signal_strength", 7], + ["🈁", "koko", 7], + ["🆖", "ng", 7], + ["🆗", "ok", 7], + ["🆙", "up", 7], + ["🆒", "cool", 7], + ["🆕", "new", 7], + ["🆓", "free", 7], + ["0️⃣", "zero", 7], + ["1️⃣", "one", 7], + ["2️⃣", "two", 7], + ["3️⃣", "three", 7], + ["4️⃣", "four", 7], + ["5️⃣", "five", 7], + ["6️⃣", "six", 7], + ["7️⃣", "seven", 7], + ["8️⃣", "eight", 7], + ["9️⃣", "nine", 7], + ["🔟", "keycap_ten", 7], + ["*⃣", "asterisk", 7], + ["⏏️", "eject_button", 7], + ["▶️", "arrow_forward", 7], + ["⏸", "pause_button", 7], + ["⏭", "next_track_button", 7], + ["⏹", "stop_button", 7], + ["⏺", "record_button", 7], + ["⏯", "play_or_pause_button", 7], + ["⏮", "previous_track_button", 7], + ["⏩", "fast_forward", 7], + ["⏪", "rewind", 7], + ["🔀", "twisted_rightwards_arrows", 7], + ["🔁", "repeat", 7], + ["🔂", "repeat_one", 7], + ["◀️", "arrow_backward", 7], + ["🔼", "arrow_up_small", 7], + ["🔽", "arrow_down_small", 7], + ["⏫", "arrow_double_up", 7], + ["⏬", "arrow_double_down", 7], + ["➡️", "arrow_right", 7], + ["⬅️", "arrow_left", 7], + ["⬆️", "arrow_up", 7], + ["⬇️", "arrow_down", 7], + ["↗️", "arrow_upper_right", 7], + ["↘️", "arrow_lower_right", 7], + ["↙️", "arrow_lower_left", 7], + ["↖️", "arrow_upper_left", 7], + ["↕️", "arrow_up_down", 7], + ["↔️", "left_right_arrow", 7], + ["🔄", "arrows_counterclockwise", 7], + ["↪️", "arrow_right_hook", 7], + ["↩️", "leftwards_arrow_with_hook", 7], + ["⤴️", "arrow_heading_up", 7], + ["⤵️", "arrow_heading_down", 7], + ["#️⃣", "hash", 7], + ["ℹ️", "information_source", 7], + ["🔤", "abc", 7], + ["🔡", "abcd", 7], + ["🔠", "capital_abcd", 7], + ["🔣", "symbols", 7], + ["🎵", "musical_note", 7], + ["🎶", "notes", 7], + ["〰️", "wavy_dash", 7], + ["➰", "curly_loop", 7], + ["✔️", "heavy_check_mark", 7], + ["🔃", "arrows_clockwise", 7], + ["➕", "heavy_plus_sign", 7], + ["➖", "heavy_minus_sign", 7], + ["➗", "heavy_division_sign", 7], + ["✖️", "heavy_multiplication_x", 7], + ["🟰", "heavy_equals_sign", 7], + ["♾", "infinity", 7], + ["💲", "heavy_dollar_sign", 7], + ["💱", "currency_exchange", 7], + ["©️", "copyright", 7], + ["®️", "registered", 7], + ["™️", "tm", 7], + ["🔚", "end", 7], + ["🔙", "back", 7], + ["🔛", "on", 7], + ["🔝", "top", 7], + ["🔜", "soon", 7], + ["☑️", "ballot_box_with_check", 7], + ["🔘", "radio_button", 7], + ["⚫", "black_circle", 7], + ["⚪", "white_circle", 7], + ["🔴", "red_circle", 7], + ["🟠", "orange_circle", 7], + ["🟡", "yellow_circle", 7], + ["🟢", "green_circle", 7], + ["🔵", "large_blue_circle", 7], + ["🟣", "purple_circle", 7], + ["🟤", "brown_circle", 7], + ["🔸", "small_orange_diamond", 7], + ["🔹", "small_blue_diamond", 7], + ["🔶", "large_orange_diamond", 7], + ["🔷", "large_blue_diamond", 7], + ["🔺", "small_red_triangle", 7], + ["▪️", "black_small_square", 7], + ["▫️", "white_small_square", 7], + ["⬛", "black_large_square", 7], + ["⬜", "white_large_square", 7], + ["🟥", "red_square", 7], + ["🟧", "orange_square", 7], + ["🟨", "yellow_square", 7], + ["🟩", "green_square", 7], + ["🟦", "blue_square", 7], + ["🟪", "purple_square", 7], + ["🟫", "brown_square", 7], + ["🔻", "small_red_triangle_down", 7], + ["◼️", "black_medium_square", 7], + ["◻️", "white_medium_square", 7], + ["◾", "black_medium_small_square", 7], + ["◽", "white_medium_small_square", 7], + ["🔲", "black_square_button", 7], + ["🔳", "white_square_button", 7], + ["🔈", "speaker", 7], + ["🔉", "sound", 7], + ["🔊", "loud_sound", 7], + ["🔇", "mute", 7], + ["📣", "mega", 7], + ["📢", "loudspeaker", 7], + ["🔔", "bell", 7], + ["🔕", "no_bell", 7], + ["🃏", "black_joker", 7], + ["🀄", "mahjong", 7], + ["♠️", "spades", 7], + ["♣️", "clubs", 7], + ["♥️", "hearts", 7], + ["♦️", "diamonds", 7], + ["🎴", "flower_playing_cards", 7], + ["💭", "thought_balloon", 7], + ["🗯", "right_anger_bubble", 7], + ["💬", "speech_balloon", 7], + ["🗨", "left_speech_bubble", 7], + ["🕐", "clock1", 7], + ["🕑", "clock2", 7], + ["🕒", "clock3", 7], + ["🕓", "clock4", 7], + ["🕔", "clock5", 7], + ["🕕", "clock6", 7], + ["🕖", "clock7", 7], + ["🕗", "clock8", 7], + ["🕘", "clock9", 7], + ["🕙", "clock10", 7], + ["🕚", "clock11", 7], + ["🕛", "clock12", 7], + ["🕜", "clock130", 7], + ["🕝", "clock230", 7], + ["🕞", "clock330", 7], + ["🕟", "clock430", 7], + ["🕠", "clock530", 7], + ["🕡", "clock630", 7], + ["🕢", "clock730", 7], + ["🕣", "clock830", 7], + ["🕤", "clock930", 7], + ["🕥", "clock1030", 7], + ["🕦", "clock1130", 7], + ["🕧", "clock1230", 7], + ["🇦🇫", "afghanistan", 8], + ["🇦🇽", "aland_islands", 8], + ["🇦🇱", "albania", 8], + ["🇩🇿", "algeria", 8], + ["🇦🇸", "american_samoa", 8], + ["🇦🇩", "andorra", 8], + ["🇦🇴", "angola", 8], + ["🇦🇮", "anguilla", 8], + ["🇦🇶", "antarctica", 8], + ["🇦🇬", "antigua_barbuda", 8], + ["🇦🇷", "argentina", 8], + ["🇦🇲", "armenia", 8], + ["🇦🇼", "aruba", 8], + ["🇦🇨", "ascension_island", 8], + ["🇦🇺", "australia", 8], + ["🇦🇹", "austria", 8], + ["🇦🇿", "azerbaijan", 8], + ["🇧🇸", "bahamas", 8], + ["🇧🇭", "bahrain", 8], + ["🇧🇩", "bangladesh", 8], + ["🇧🇧", "barbados", 8], + ["🇧🇾", "belarus", 8], + ["🇧🇪", "belgium", 8], + ["🇧🇿", "belize", 8], + ["🇧🇯", "benin", 8], + ["🇧🇲", "bermuda", 8], + ["🇧🇹", "bhutan", 8], + ["🇧🇴", "bolivia", 8], + ["🇧🇶", "caribbean_netherlands", 8], + ["🇧🇦", "bosnia_herzegovina", 8], + ["🇧🇼", "botswana", 8], + ["🇧🇷", "brazil", 8], + ["🇮🇴", "british_indian_ocean_territory", 8], + ["🇻🇬", "british_virgin_islands", 8], + ["🇧🇳", "brunei", 8], + ["🇧🇬", "bulgaria", 8], + ["🇧🇫", "burkina_faso", 8], + ["🇧🇮", "burundi", 8], + ["🇨🇻", "cape_verde", 8], + ["🇰🇭", "cambodia", 8], + ["🇨🇲", "cameroon", 8], + ["🇨🇦", "canada", 8], + ["🇮🇨", "canary_islands", 8], + ["🇰🇾", "cayman_islands", 8], + ["🇨🇫", "central_african_republic", 8], + ["🇹🇩", "chad", 8], + ["🇨🇱", "chile", 8], + ["🇨🇳", "cn", 8], + ["🇨🇽", "christmas_island", 8], + ["🇨🇨", "cocos_islands", 8], + ["🇨🇴", "colombia", 8], + ["🇰🇲", "comoros", 8], + ["🇨🇬", "congo_brazzaville", 8], + ["🇨🇩", "congo_kinshasa", 8], + ["🇨🇰", "cook_islands", 8], + ["🇨🇷", "costa_rica", 8], + ["🇭🇷", "croatia", 8], + ["🇨🇺", "cuba", 8], + ["🇨🇼", "curacao", 8], + ["🇨🇾", "cyprus", 8], + ["🇨🇿", "czech_republic", 8], + ["🇩🇰", "denmark", 8], + ["🇩🇯", "djibouti", 8], + ["🇩🇲", "dominica", 8], + ["🇩🇴", "dominican_republic", 8], + ["🇪🇨", "ecuador", 8], + ["🇪🇬", "egypt", 8], + ["🇸🇻", "el_salvador", 8], + ["🇬🇶", "equatorial_guinea", 8], + ["🇪🇷", "eritrea", 8], + ["🇪🇪", "estonia", 8], + ["🇪🇹", "ethiopia", 8], + ["🇪🇺", "eu", 8], + ["🇫🇰", "falkland_islands", 8], + ["🇫🇴", "faroe_islands", 8], + ["🇫🇯", "fiji", 8], + ["🇫🇮", "finland", 8], + ["🇫🇷", "fr", 8], + ["🇬🇫", "french_guiana", 8], + ["🇵🇫", "french_polynesia", 8], + ["🇹🇫", "french_southern_territories", 8], + ["🇬🇦", "gabon", 8], + ["🇬🇲", "gambia", 8], + ["🇬🇪", "georgia", 8], + ["🇩🇪", "de", 8], + ["🇬🇭", "ghana", 8], + ["🇬🇮", "gibraltar", 8], + ["🇬🇷", "greece", 8], + ["🇬🇱", "greenland", 8], + ["🇬🇩", "grenada", 8], + ["🇬🇵", "guadeloupe", 8], + ["🇬🇺", "guam", 8], + ["🇬🇹", "guatemala", 8], + ["🇬🇬", "guernsey", 8], + ["🇬🇳", "guinea", 8], + ["🇬🇼", "guinea_bissau", 8], + ["🇬🇾", "guyana", 8], + ["🇭🇹", "haiti", 8], + ["🇭🇳", "honduras", 8], + ["🇭🇰", "hong_kong", 8], + ["🇭🇺", "hungary", 8], + ["🇮🇸", "iceland", 8], + ["🇮🇳", "india", 8], + ["🇮🇩", "indonesia", 8], + ["🇮🇷", "iran", 8], + ["🇮🇶", "iraq", 8], + ["🇮🇪", "ireland", 8], + ["🇮🇲", "isle_of_man", 8], + ["🇮🇱", "israel", 8], + ["🇮🇹", "it", 8], + ["🇨🇮", "cote_divoire", 8], + ["🇯🇲", "jamaica", 8], + ["🇯🇵", "jp", 8], + ["🇯🇪", "jersey", 8], + ["🇯🇴", "jordan", 8], + ["🇰🇿", "kazakhstan", 8], + ["🇰🇪", "kenya", 8], + ["🇰🇮", "kiribati", 8], + ["🇽🇰", "kosovo", 8], + ["🇰🇼", "kuwait", 8], + ["🇰🇬", "kyrgyzstan", 8], + ["🇱🇦", "laos", 8], + ["🇱🇻", "latvia", 8], + ["🇱🇧", "lebanon", 8], + ["🇱🇸", "lesotho", 8], + ["🇱🇷", "liberia", 8], + ["🇱🇾", "libya", 8], + ["🇱🇮", "liechtenstein", 8], + ["🇱🇹", "lithuania", 8], + ["🇱🇺", "luxembourg", 8], + ["🇲🇴", "macau", 8], + ["🇲🇰", "macedonia", 8], + ["🇲🇬", "madagascar", 8], + ["🇲🇼", "malawi", 8], + ["🇲🇾", "malaysia", 8], + ["🇲🇻", "maldives", 8], + ["🇲🇱", "mali", 8], + ["🇲🇹", "malta", 8], + ["🇲🇭", "marshall_islands", 8], + ["🇲🇶", "martinique", 8], + ["🇲🇷", "mauritania", 8], + ["🇲🇺", "mauritius", 8], + ["🇾🇹", "mayotte", 8], + ["🇲🇽", "mexico", 8], + ["🇫🇲", "micronesia", 8], + ["🇲🇩", "moldova", 8], + ["🇲🇨", "monaco", 8], + ["🇲🇳", "mongolia", 8], + ["🇲🇪", "montenegro", 8], + ["🇲🇸", "montserrat", 8], + ["🇲🇦", "morocco", 8], + ["🇲🇿", "mozambique", 8], + ["🇲🇲", "myanmar", 8], + ["🇳🇦", "namibia", 8], + ["🇳🇷", "nauru", 8], + ["🇳🇵", "nepal", 8], + ["🇳🇱", "netherlands", 8], + ["🇳🇨", "new_caledonia", 8], + ["🇳🇿", "new_zealand", 8], + ["🇳🇮", "nicaragua", 8], + ["🇳🇪", "niger", 8], + ["🇳🇬", "nigeria", 8], + ["🇳🇺", "niue", 8], + ["🇳🇫", "norfolk_island", 8], + ["🇲🇵", "northern_mariana_islands", 8], + ["🇰🇵", "north_korea", 8], + ["🇳🇴", "norway", 8], + ["🇴🇲", "oman", 8], + ["🇵🇰", "pakistan", 8], + ["🇵🇼", "palau", 8], + ["🇵🇸", "palestinian_territories", 8], + ["🇵🇦", "panama", 8], + ["🇵🇬", "papua_new_guinea", 8], + ["🇵🇾", "paraguay", 8], + ["🇵🇪", "peru", 8], + ["🇵🇭", "philippines", 8], + ["🇵🇳", "pitcairn_islands", 8], + ["🇵🇱", "poland", 8], + ["🇵🇹", "portugal", 8], + ["🇵🇷", "puerto_rico", 8], + ["🇶🇦", "qatar", 8], + ["🇷🇪", "reunion", 8], + ["🇷🇴", "romania", 8], + ["🇷🇺", "ru", 8], + ["🇷🇼", "rwanda", 8], + ["🇧🇱", "st_barthelemy", 8], + ["🇸🇭", "st_helena", 8], + ["🇰🇳", "st_kitts_nevis", 8], + ["🇱🇨", "st_lucia", 8], + ["🇵🇲", "st_pierre_miquelon", 8], + ["🇻🇨", "st_vincent_grenadines", 8], + ["🇼🇸", "samoa", 8], + ["🇸🇲", "san_marino", 8], + ["🇸🇹", "sao_tome_principe", 8], + ["🇸🇦", "saudi_arabia", 8], + ["🇸🇳", "senegal", 8], + ["🇷🇸", "serbia", 8], + ["🇸🇨", "seychelles", 8], + ["🇸🇱", "sierra_leone", 8], + ["🇸🇬", "singapore", 8], + ["🇸🇽", "sint_maarten", 8], + ["🇸🇰", "slovakia", 8], + ["🇸🇮", "slovenia", 8], + ["🇸🇧", "solomon_islands", 8], + ["🇸🇴", "somalia", 8], + ["🇿🇦", "south_africa", 8], + ["🇬🇸", "south_georgia_south_sandwich_islands", 8], + ["🇰🇷", "kr", 8], + ["🇸🇸", "south_sudan", 8], + ["🇪🇸", "es", 8], + ["🇱🇰", "sri_lanka", 8], + ["🇸🇩", "sudan", 8], + ["🇸🇷", "suriname", 8], + ["🇸🇿", "swaziland", 8], + ["🇸🇪", "sweden", 8], + ["🇨🇭", "switzerland", 8], + ["🇸🇾", "syria", 8], + ["🇹🇼", "taiwan", 8], + ["🇹🇯", "tajikistan", 8], + ["🇹🇿", "tanzania", 8], + ["🇹🇭", "thailand", 8], + ["🇹🇱", "timor_leste", 8], + ["🇹🇬", "togo", 8], + ["🇹🇰", "tokelau", 8], + ["🇹🇴", "tonga", 8], + ["🇹🇹", "trinidad_tobago", 8], + ["🇹🇦", "tristan_da_cunha", 8], + ["🇹🇳", "tunisia", 8], + ["🇹🇷", "tr", 8], + ["🇹🇲", "turkmenistan", 8], + ["🇹🇨", "turks_caicos_islands", 8], + ["🇹🇻", "tuvalu", 8], + ["🇺🇬", "uganda", 8], + ["🇺🇦", "ukraine", 8], + ["🇦🇪", "united_arab_emirates", 8], + ["🇬🇧", "uk", 8], + ["🏴", "england", 8], + ["🏴", "scotland", 8], + ["🏴", "wales", 8], + ["🇺🇸", "us", 8], + ["🇻🇮", "us_virgin_islands", 8], + ["🇺🇾", "uruguay", 8], + ["🇺🇿", "uzbekistan", 8], + ["🇻🇺", "vanuatu", 8], + ["🇻🇦", "vatican_city", 8], + ["🇻🇪", "venezuela", 8], + ["🇻🇳", "vietnam", 8], + ["🇼🇫", "wallis_futuna", 8], + ["🇪🇭", "western_sahara", 8], + ["🇾🇪", "yemen", 8], + ["🇿🇲", "zambia", 8], + ["🇿🇼", "zimbabwe", 8], + ["🇺🇳", "united_nations", 8], + ["🏴☠️", "pirate_flag", 8] ] - diff --git a/packages/frontend/src/i18n.ts b/packages/frontend/src/i18n.ts index 220c6210c..30771ec1b 100644 --- a/packages/frontend/src/i18n.ts +++ b/packages/frontend/src/i18n.ts @@ -1,8 +1,9 @@ import { markRaw } from 'vue'; +import type { Locale } from '../../../locales'; import { locale } from '@/config'; import { I18n } from '@/scripts/i18n'; -export const i18n = markRaw(new I18n(locale)); +export const i18n = markRaw(new I18n<Locale>(locale)); export function updateI18n(newLocale) { i18n.ts = newLocale; diff --git a/packages/frontend/src/init.ts b/packages/frontend/src/init.ts deleted file mode 100644 index 49e7bb400..000000000 --- a/packages/frontend/src/init.ts +++ /dev/null @@ -1,527 +0,0 @@ -/** - * Client entry point - */ -// https://vitejs.dev/config/build-options.html#build-modulepreload -import 'vite/modulepreload-polyfill'; - -import '@/style.scss'; - -import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; -import { compareVersions } from 'compare-versions'; -import JSON5 from 'json5'; - -import widgets from '@/widgets'; -import directives from '@/directives'; -import components from '@/components'; -import { version, ui, lang, updateLocale } from '@/config'; -import { applyTheme } from '@/scripts/theme'; -import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; -import { i18n, updateI18n } from '@/i18n'; -import { confirm, alert, post, popup, toast } from '@/os'; -import { stream } from '@/stream'; -import * as sound from '@/scripts/sound'; -import { $i, refreshAccount, login, updateAccount, signout } from '@/account'; -import { defaultStore, ColdDeviceStorage } from '@/store'; -import { fetchInstance, instance } from '@/instance'; -import { makeHotkey } from '@/scripts/hotkey'; -import { deviceKind } from '@/scripts/device-kind'; -import { initializeSw } from '@/scripts/initialize-sw'; -import { reloadChannel } from '@/scripts/unison-reload'; -import { reactionPicker } from '@/scripts/reaction-picker'; -import { getUrlWithoutLoginId } from '@/scripts/login-id'; -import { getAccountFromId } from '@/scripts/get-account-from-id'; -import { deckStore } from '@/ui/deck/deck-store'; -import { miLocalStorage } from '@/local-storage'; -import { claimAchievement, claimedAchievements } from '@/scripts/achievements'; -import { fetchCustomEmojis } from '@/custom-emojis'; -import { mainRouter } from '@/router'; - -console.info(`Misskey v${version}`); - -if (_DEV_) { - console.warn('Development mode!!!'); - - console.info(`vue ${vueVersion}`); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (window as any).$i = $i; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (window as any).$store = defaultStore; - - window.addEventListener('error', event => { - console.error(event); - /* - alert({ - type: 'error', - title: 'DEV: Unhandled error', - text: event.message - }); - */ - }); - - window.addEventListener('unhandledrejection', event => { - console.error(event); - /* - alert({ - type: 'error', - title: 'DEV: Unhandled promise rejection', - text: event.reason - }); - */ - }); -} - -//#region Detect language & fetch translations -const localeVersion = miLocalStorage.getItem('localeVersion'); -const localeOutdated = (localeVersion == null || localeVersion !== version); -if (localeOutdated) { - const res = await window.fetch(`/assets/locales/${lang}.${version}.json`); - if (res.status === 200) { - const newLocale = await res.text(); - const parsedNewLocale = JSON.parse(newLocale); - miLocalStorage.setItem('locale', newLocale); - miLocalStorage.setItem('localeVersion', version); - updateLocale(parsedNewLocale); - updateI18n(parsedNewLocale); - } -} -//#endregion - -// タッチデバイスでCSSの:hoverを機能させる -document.addEventListener('touchend', () => {}, { passive: true }); - -// 一斉リロード -reloadChannel.addEventListener('message', path => { - if (path !== null) location.href = path; - else location.reload(); -}); - -// If mobile, insert the viewport meta tag -if (['smartphone', 'tablet'].includes(deviceKind)) { - const viewport = document.getElementsByName('viewport').item(0); - viewport.setAttribute('content', - `${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`); -} - -//#region Set lang attr -const html = document.documentElement; -html.setAttribute('lang', lang); -//#endregion - -//#region loginId -const params = new URLSearchParams(location.search); -const loginId = params.get('loginId'); - -if (loginId) { - const target = getUrlWithoutLoginId(location.href); - - if (!$i || $i.id !== loginId) { - const account = await getAccountFromId(loginId); - if (account) { - await login(account.token, target); - } - } - - history.replaceState({ misskey: 'loginId' }, '', target); -} - -//#endregion - -//#region Fetch user -if ($i && $i.token) { - if (_DEV_) { - console.log('account cache found. refreshing...'); - } - - refreshAccount(); -} else { - if (_DEV_) { - console.log('no account cache found.'); - } - - // 連携ログインの場合用にCookieを参照する - const i = (document.cookie.match(/igi=(\w+)/) ?? [null, null])[1]; - - if (i != null && i !== 'null') { - if (_DEV_) { - console.log('signing...'); - } - - try { - document.body.innerHTML = '<div>Please wait...</div>'; - await login(i); - } catch (err) { - // Render the error screen - // TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな) - document.body.innerHTML = '<div id="err">Oops!</div>'; - } - } else { - if (_DEV_) { - console.log('not signed in'); - } - } -} -//#endregion - -const fetchInstanceMetaPromise = fetchInstance(); - -fetchInstanceMetaPromise.then(() => { - miLocalStorage.setItem('v', instance.version); - - // Init service worker - initializeSw(); -}); - -try { - await fetchCustomEmojis(); -} catch (err) { /* empty */ } - -const app = createApp( - new URLSearchParams(window.location.search).has('zen') ? defineAsyncComponent(() => import('@/ui/zen.vue')) : - !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : - ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : - ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : - defineAsyncComponent(() => import('@/ui/universal.vue')), -); - -if (_DEV_) { - app.config.performance = true; -} - -widgets(app); -directives(app); -components(app); - -const splash = document.getElementById('splash'); -// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す)) -if (splash) splash.addEventListener('transitionend', () => { - splash.remove(); -}); - -await deckStore.ready; - -// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 -// なぜかinit.tsの内容が2回実行されることがあるため、mountするdivを1つに制限する -const rootEl = ((): HTMLElement => { - const MISSKEY_MOUNT_DIV_ID = 'misskey_app'; - - const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID); - - if (currentRoot) { - console.warn('multiple import detected'); - return currentRoot; - } - - const root = document.createElement('div'); - root.id = MISSKEY_MOUNT_DIV_ID; - document.body.appendChild(root); - return root; -})(); - -app.mount(rootEl); - -// boot.jsのやつを解除 -window.onerror = null; -window.onunhandledrejection = null; - -reactionPicker.init(); - -if (splash) { - splash.style.opacity = '0'; - splash.style.pointerEvents = 'none'; -} - -// クライアントが更新されたか? -const lastVersion = miLocalStorage.getItem('lastVersion'); -if (lastVersion !== version) { - miLocalStorage.setItem('lastVersion', version); - - // テーマリビルドするため - miLocalStorage.removeItem('theme'); - - try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため - if (lastVersion != null && compareVersions(version, lastVersion) === 1) { - // ログインしてる場合だけ - if ($i) { - popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed'); - } - } - } catch (err) { /* empty */ } -} - -await defaultStore.ready; - -// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため) -watch(defaultStore.reactiveState.darkMode, (darkMode) => { - applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme')); -}, { immediate: miLocalStorage.getItem('theme') == null }); - -const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme')); -const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme')); - -watch(darkTheme, (theme) => { - if (defaultStore.state.darkMode) { - applyTheme(theme); - } -}); - -watch(lightTheme, (theme) => { - if (!defaultStore.state.darkMode) { - applyTheme(theme); - } -}); - -//#region Sync dark mode -if (ColdDeviceStorage.get('syncDeviceDarkMode')) { - defaultStore.set('darkMode', isDeviceDarkmode()); -} - -window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => { - if (ColdDeviceStorage.get('syncDeviceDarkMode')) { - defaultStore.set('darkMode', mql.matches); - } -}); -//#endregion - -fetchInstanceMetaPromise.then(() => { - if (defaultStore.state.themeInitial) { - if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme)); - if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme)); - defaultStore.set('themeInitial', false); - } -}); - -watch(defaultStore.reactiveState.useBlurEffectForModal, v => { - document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none'); -}, { immediate: true }); - -watch(defaultStore.reactiveState.useBlurEffect, v => { - if (v) { - document.documentElement.style.removeProperty('--blur'); - } else { - document.documentElement.style.setProperty('--blur', 'none'); - } -}, { immediate: true }); - -let reloadDialogShowing = false; -stream.on('_disconnected_', async () => { - if (defaultStore.state.serverDisconnectedBehavior === 'reload') { - location.reload(); - } else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') { - if (reloadDialogShowing) return; - reloadDialogShowing = true; - const { canceled } = await confirm({ - type: 'warning', - title: i18n.ts.disconnectedFromServer, - text: i18n.ts.reloadConfirm, - }); - reloadDialogShowing = false; - if (!canceled) { - location.reload(); - } - } -}); - -for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { - import('./plugin').then(async ({ install }) => { - // Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740 - await new Promise(r => setTimeout(r, 0)); - install(plugin); - }); -} - -const hotkeys = { - 'd': (): void => { - defaultStore.set('darkMode', !defaultStore.state.darkMode); - }, - 's': (): void => { - mainRouter.push('/search'); - }, -}; - -if ($i) { - // only add post shortcuts if logged in - hotkeys['p|n'] = post; - - if (defaultStore.state.accountSetupWizard !== -1) { - // このウィザードが実装される前に登録したユーザーには表示させないため - // TODO: そのうち消す - if (Date.now() - new Date($i.createdAt).getTime() < 1000 * 60 * 60 * 24) { - popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed'); - } else { - defaultStore.set('accountSetupWizard', -1); - } - } - - if ($i.isDeleted) { - alert({ - type: 'warning', - text: i18n.ts.accountDeletionInProgress, - }); - } - - const now = new Date(); - const m = now.getMonth() + 1; - const d = now.getDate(); - - if ($i.birthday) { - const bm = parseInt($i.birthday.split('-')[1]); - const bd = parseInt($i.birthday.split('-')[2]); - if (m === bm && d === bd) { - claimAchievement('loggedInOnBirthday'); - } - } - - if (m === 1 && d === 1) { - claimAchievement('loggedInOnNewYearsDay'); - } - - if ($i.loggedInDays >= 3) claimAchievement('login3'); - if ($i.loggedInDays >= 7) claimAchievement('login7'); - if ($i.loggedInDays >= 15) claimAchievement('login15'); - if ($i.loggedInDays >= 30) claimAchievement('login30'); - if ($i.loggedInDays >= 60) claimAchievement('login60'); - if ($i.loggedInDays >= 100) claimAchievement('login100'); - if ($i.loggedInDays >= 200) claimAchievement('login200'); - if ($i.loggedInDays >= 300) claimAchievement('login300'); - if ($i.loggedInDays >= 400) claimAchievement('login400'); - if ($i.loggedInDays >= 500) claimAchievement('login500'); - if ($i.loggedInDays >= 600) claimAchievement('login600'); - if ($i.loggedInDays >= 700) claimAchievement('login700'); - if ($i.loggedInDays >= 800) claimAchievement('login800'); - if ($i.loggedInDays >= 900) claimAchievement('login900'); - if ($i.loggedInDays >= 1000) claimAchievement('login1000'); - - if ($i.notesCount > 0) claimAchievement('notes1'); - if ($i.notesCount >= 10) claimAchievement('notes10'); - if ($i.notesCount >= 100) claimAchievement('notes100'); - if ($i.notesCount >= 500) claimAchievement('notes500'); - if ($i.notesCount >= 1000) claimAchievement('notes1000'); - if ($i.notesCount >= 5000) claimAchievement('notes5000'); - if ($i.notesCount >= 10000) claimAchievement('notes10000'); - if ($i.notesCount >= 20000) claimAchievement('notes20000'); - if ($i.notesCount >= 30000) claimAchievement('notes30000'); - if ($i.notesCount >= 40000) claimAchievement('notes40000'); - if ($i.notesCount >= 50000) claimAchievement('notes50000'); - if ($i.notesCount >= 60000) claimAchievement('notes60000'); - if ($i.notesCount >= 70000) claimAchievement('notes70000'); - if ($i.notesCount >= 80000) claimAchievement('notes80000'); - if ($i.notesCount >= 90000) claimAchievement('notes90000'); - if ($i.notesCount >= 100000) claimAchievement('notes100000'); - - if ($i.followersCount > 0) claimAchievement('followers1'); - if ($i.followersCount >= 10) claimAchievement('followers10'); - if ($i.followersCount >= 50) claimAchievement('followers50'); - if ($i.followersCount >= 100) claimAchievement('followers100'); - if ($i.followersCount >= 300) claimAchievement('followers300'); - if ($i.followersCount >= 500) claimAchievement('followers500'); - if ($i.followersCount >= 1000) claimAchievement('followers1000'); - - if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) { - claimAchievement('passedSinceAccountCreated1'); - } - if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) { - claimAchievement('passedSinceAccountCreated2'); - } - if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) { - claimAchievement('passedSinceAccountCreated3'); - } - - if (claimedAchievements.length >= 30) { - claimAchievement('collectAchievements30'); - } - - window.setInterval(() => { - if (Math.floor(Math.random() * 20000) === 0) { - claimAchievement('justPlainLucky'); - } - }, 1000 * 10); - - window.setTimeout(() => { - claimAchievement('client30min'); - }, 1000 * 60 * 30); - - window.setTimeout(() => { - claimAchievement('client60min'); - }, 1000 * 60 * 60); - - const lastUsed = miLocalStorage.getItem('lastUsed'); - if (lastUsed) { - const lastUsedDate = parseInt(lastUsed, 10); - // 二時間以上前なら - if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) { - toast(i18n.t('welcomeBackWithName', { - name: $i.name || $i.username, - })); - } - } - miLocalStorage.setItem('lastUsed', Date.now().toString()); - - const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt'); - const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo'); - if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) { - if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) { - popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed'); - } - } - - if ('Notification' in window) { - // 許可を得ていなかったらリクエスト - if (Notification.permission === 'default') { - Notification.requestPermission(); - } - } - - const main = markRaw(stream.useChannel('main', null, 'System')); - - // 自分の情報が更新されたとき - main.on('meUpdated', i => { - updateAccount(i); - }); - - main.on('readAllNotifications', () => { - updateAccount({ hasUnreadNotification: false }); - }); - - main.on('unreadNotification', () => { - updateAccount({ hasUnreadNotification: true }); - }); - - main.on('unreadMention', () => { - updateAccount({ hasUnreadMentions: true }); - }); - - main.on('readAllUnreadMentions', () => { - updateAccount({ hasUnreadMentions: false }); - }); - - main.on('unreadSpecifiedNote', () => { - updateAccount({ hasUnreadSpecifiedNotes: true }); - }); - - main.on('readAllUnreadSpecifiedNotes', () => { - updateAccount({ hasUnreadSpecifiedNotes: false }); - }); - - main.on('readAllAntennas', () => { - updateAccount({ hasUnreadAntenna: false }); - }); - - main.on('unreadAntenna', () => { - updateAccount({ hasUnreadAntenna: true }); - sound.play('antenna'); - }); - - main.on('readAllAnnouncements', () => { - updateAccount({ hasUnreadAnnouncement: false }); - }); - - // トークンが再生成されたとき - // このままではMisskeyが利用できないので強制的にサインアウトさせる - main.on('myTokenRegenerated', () => { - signout(); - }); -} - -// shortcut -document.addEventListener('keydown', makeHotkey(hotkeys)); diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts index 441a35747..ca4f21f79 100644 --- a/packages/frontend/src/local-storage.ts +++ b/packages/frontend/src/local-storage.ts @@ -13,7 +13,7 @@ type Keys = 'hashtags' | 'wallpaper' | 'theme' | - 'colorSchema' | + 'colorScheme' | 'useSystemFont' | 'fontSize' | 'ui' | diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index c4f9d47d7..c44d34804 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -172,12 +172,6 @@ export function pageWindow(path: string) { }, {}, 'closed'); } -export function modalPageWindow(path: string) { - popup(defineAsyncComponent(() => import('@/components/MkModalPageWindow.vue')), { - initialPath: path, - }, {}, 'closed'); -} - export function toast(message: string) { popup(MkToast, { message, diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue index f53fec7d9..f27d2df33 100644 --- a/packages/frontend/src/pages/_error_.vue +++ b/packages/frontend/src/pages/_error_.vue @@ -1,18 +1,20 @@ <template> <MkLoading v-if="!loaded"/> <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear> - <div v-show="loaded" class="mjndxjch"> - <img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> - <p><b><i class="ti ti-alert-triangle"></i> {{ i18n.ts.pageLoadError }}</b></p> - <p v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</p> - <p v-else-if="serverIsDead">{{ i18n.ts.serverIsDead }}</p> - <template v-else> - <p>{{ i18n.ts.newVersionOfClientAvailable }}</p> - <p>{{ i18n.ts.youShouldUpgradeClient }}</p> - <MkButton class="button primary" @click="reload">{{ i18n.ts.reload }}</MkButton> - </template> - <p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.ts.troubleshooting }}</MkA></p> - <p v-if="error" class="error">ERROR: {{ error }}</p> + <div v-show="loaded" :class="$style.root"> + <img src="https://xn--931a.moe/assets/error.jpg" class="_ghost" :class="$style.img"/> + <div class="_gaps"> + <p><b><i class="ti ti-alert-triangle"></i> {{ i18n.ts.pageLoadError }}</b></p> + <p v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</p> + <p v-else-if="serverIsDead">{{ i18n.ts.serverIsDead }}</p> + <template v-else> + <p>{{ i18n.ts.newVersionOfClientAvailable }}</p> + <p>{{ i18n.ts.youShouldUpgradeClient }}</p> + <MkButton style="margin: 8px auto;" @click="reload">{{ i18n.ts.reload }}</MkButton> + </template> + <p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.ts.troubleshooting }}</MkA></p> + <p v-if="error" style="opacity: 0.7;">ERROR: {{ error }}</p> + </div> </div> </Transition> </template> @@ -64,28 +66,16 @@ definePageMetadata({ }); </script> -<style lang="scss" scoped> -.mjndxjch { +<style lang="scss" module> +.root { padding: 32px; text-align: center; +} - > p { - margin: 0 0 12px 0; - } - - > .button { - margin: 8px auto; - } - - > img { - vertical-align: bottom; - height: 128px; - margin-bottom: 24px; - border-radius: 16px; - } - - > .error { - opacity: 0.7; - } +.img { + vertical-align: bottom; + height: 128px; + margin-bottom: 24px; + border-radius: 16px; } </style> diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index 9e0594db3..0017145fa 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -2,7 +2,7 @@ <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <div style="overflow: clip;"> - <MkSpacer :content-max="600" :margin-min="20"> + <MkSpacer :contentMax="600" :marginMin="20"> <div class="_gaps_m znqjceqz"> <div v-panel class="about"> <div ref="containerEl" class="container" :class="{ playing: easterEggEngine != null }"> @@ -10,8 +10,8 @@ <div class="misskey">Misskey</div> <div class="version">v{{ version }}</div> <span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"> - <MkCustomEmoji v-if="emoji.emoji[0] === ':'" class="emoji" :name="emoji.emoji" :normal="true" :no-style="true"/> - <MkEmoji v-else class="emoji" :emoji="emoji.emoji" :normal="true" :no-style="true"/> + <MkCustomEmoji v-if="emoji.emoji[0] === ':'" class="emoji" :name="emoji.emoji" :normal="true" :noStyle="true"/> + <MkEmoji v-else class="emoji" :emoji="emoji.emoji" :normal="true" :noStyle="true"/> </span> </div> <button v-if="thereIsTreasure" class="_button treasure" @click="getTreasure"><img src="/fluent-emoji/1f3c6.png" class="treasureImg"></button> @@ -86,8 +86,13 @@ </FormSection> <FormSection> <template #label>Special thanks</template> - <div style="text-align: center;"> - <a style="display: inline-block;" class="dcadvirth" title="DC Advirth" href="https://www.dotchain.ltd/advirth" target="_blank"><img width="200" src="https://misskey-hub.net/sponsors/dcadvirth.png" alt="DC Advirth"></a> + <div class="_gaps" style="text-align: center;"> + <div> + <a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img width="200" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a> + </div> + <div> + <a style="display: inline-block;" class="dcadvirth" title="DC Advirth" href="https://www.dotchain.ltd/advirth" target="_blank"><img width="200" src="https://misskey-hub.net/sponsors/dcadvirth.png" alt="DC Advirth"></a> + </div> </div> </FormSection> </div> @@ -144,6 +149,12 @@ const patronsWithIcon = [{ }, { name: 'かみらえっと', icon: 'https://misskey-hub.net/patrons/be1326bda7d940a482f3758ffd9ffaf6.jpg', +}, { + name: 'へてて', + icon: 'https://misskey-hub.net/patrons/0431eacd7c6843d09de8ea9984307e86.jpg', +}, { + name: 'spinlock', + icon: 'https://misskey-hub.net/patrons/6a1cebc819d540a78bf20e9e3115baa8.jpg', }]; const patrons = [ diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue index 2d82fcf27..3744bed10 100644 --- a/packages/frontend/src/pages/about.emojis.vue +++ b/packages/frontend/src/pages/about.emojis.vue @@ -1,5 +1,5 @@ <template> -<div class="driuhtrh _gaps"> +<div class="_gaps"> <MkButton v-if="$i && ($i.isModerator || $i.policies.canManageCustomEmojis)" primary link to="/custom-emojis-manager">{{ i18n.ts.manageCustomEmojis }}</MkButton> <div class="query"> @@ -14,17 +14,17 @@ --> </div> - <MkFoldableSection v-if="searchEmojis" class="emojis"> + <MkFoldableSection v-if="searchEmojis"> <template #header>{{ i18n.ts.searchResult }}</template> - <div class="zuvgdzyt"> - <XEmoji v-for="emoji in searchEmojis" :key="emoji.name" class="emoji" :emoji="emoji"/> + <div :class="$style.emojis"> + <XEmoji v-for="emoji in searchEmojis" :key="emoji.name" :emoji="emoji"/> </div> </MkFoldableSection> - <MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category" class="emojis"> + <MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category"> <template #header>{{ category || i18n.ts.other }}</template> - <div class="zuvgdzyt"> - <XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji" :emoji="emoji"/> + <div :class="$style.emojis"> + <XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" :emoji="emoji"/> </div> </MkFoldableSection> </div> @@ -57,7 +57,7 @@ function search() { if (queryarry) { searchEmojis = customEmojis.value.filter(emoji => - queryarry.includes(`:${emoji.name}:`) + queryarry.includes(`:${emoji.name}:`), ); } else { searchEmojis = customEmojis.value.filter(emoji => emoji.name.includes(q) || emoji.aliases.includes(q)); @@ -84,36 +84,10 @@ watch($$(selectedTags), () => { }, { deep: true }); </script> -<style lang="scss" scoped> -.driuhtrh { - background: var(--bg); - - > .query { - background: var(--bg); - - > .tags { - > .tag { - display: inline-block; - margin: 8px 8px 0 0; - padding: 4px 8px; - font-size: 0.9em; - background: var(--accentedBg); - border-radius: 5px; - - &.active { - background: var(--accent); - color: var(--fgOnAccent); - } - } - } - } - - > .emojis { - .zuvgdzyt { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); - grid-gap: 12px; - } - } +<style lang="scss" module> +.emojis { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); + grid-gap: 12px; } </style> diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue index 8fe613a9a..a8c6c05d8 100644 --- a/packages/frontend/src/pages/about.federation.vue +++ b/packages/frontend/src/pages/about.federation.vue @@ -1,6 +1,6 @@ <template> -<div class="taeiyria"> - <div class="query"> +<div> + <div> <MkInput v-model="host" :debounce="true" class=""> <template #prefix><i class="ti ti-search"></i></template> <template #label>{{ i18n.ts.host }}</template> @@ -35,8 +35,8 @@ </div> <MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination"> - <div class="dqokceoi"> - <MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`"> + <div :class="$style.items"> + <MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" :class="$style.item" :to="`/instance-info/${instance.host}`"> <MkInstanceCardMini :instance="instance"/> </MkA> </div> @@ -82,21 +82,14 @@ function getStatus(instance) { } </script> -<style lang="scss" scoped> -.taeiyria { - > .query { - background: var(--bg); - margin-bottom: 16px; - } -} - -.dqokceoi { +<style lang="scss" module> +.items { display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-gap: 12px; +} - > .instance:hover { - text-decoration: none; - } +.item:hover { + text-decoration: none; } </style> diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index 8e2999042..693d369b8 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20"> + <MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20"> <div class="_gaps_m"> <div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"> <div style="overflow: clip;"> @@ -80,13 +80,13 @@ </FormSection> </div> </MkSpacer> - <MkSpacer v-else-if="tab === 'emojis'" :content-max="1000" :margin-min="20"> + <MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20"> <XEmojis/> </MkSpacer> - <MkSpacer v-else-if="tab === 'federation'" :content-max="1000" :margin-min="20"> + <MkSpacer v-else-if="tab === 'federation'" :contentMax="1000" :marginMin="20"> <XFederation/> </MkSpacer> - <MkSpacer v-else-if="tab === 'charts'" :content-max="1000" :margin-min="20"> + <MkSpacer v-else-if="tab === 'charts'" :contentMax="1000" :marginMin="20"> <MkInstanceStats/> </MkSpacer> </MkStickyContainer> diff --git a/packages/frontend/src/pages/achievements.vue b/packages/frontend/src/pages/achievements.vue index 1eef7a53f..dc47d8dde 100644 --- a/packages/frontend/src/pages/achievements.vue +++ b/packages/frontend/src/pages/achievements.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader/></template> - <MkSpacer :content-max="1200"> + <MkSpacer :contentMax="1200"> <MkAchievements :user="$i"/> </MkSpacer> </MkStickyContainer> diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue index 1d309a737..24c863ba6 100644 --- a/packages/frontend/src/pages/admin-file.vue +++ b/packages/frontend/src/pages/admin-file.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer v-if="file" :content-max="600" :margin-min="16" :margin-max="32"> + <MkSpacer v-if="file" :contentMax="600" :marginMin="16" :marginMax="32"> <div v-if="tab === 'overview'" class="cxqhhsmd _gaps_m"> <a class="thumbnail" :href="file.url" target="_blank"> <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> @@ -32,7 +32,7 @@ <MkUserCardMini :user="file.user"/> </MkA> <div> - <MkSwitch v-model="isSensitive" @update:model-value="toggleIsSensitive">NSFW</MkSwitch> + <MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">NSFW</MkSwitch> </div> <div> diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue index 343d2c4c5..9530b27ba 100644 --- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue +++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue @@ -1,5 +1,5 @@ <template> -<div :class="$style.root" class="_gaps"> +<div class="_gaps"> <div :class="$style.header"> <MkSelect v-model="type" :class="$style.typeSelect"> <option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option> @@ -24,12 +24,12 @@ </button> </div> - <div v-if="type === 'and' || type === 'or'" :class="$style.values" class="_gaps"> - <Sortable v-model="v.values" tag="div" class="_gaps" item-key="id" handle=".drag-handle" :group="{ name: 'roleFormula' }" :animation="150" :swap-threshold="0.5"> + <div v-if="type === 'and' || type === 'or'" class="_gaps"> + <Sortable v-model="v.values" tag="div" class="_gaps" itemKey="id" handle=".drag-handle" :group="{ name: 'roleFormula' }" :animation="150" :swapThreshold="0.5"> <template #item="{element}"> <div :class="$style.item"> <!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 --> - <RolesEditorFormula :model-value="element" draggable @update:model-value="updated => valuesItemUpdated(updated)" @remove="removeItem(element)"/> + <RolesEditorFormula :modelValue="element" draggable @update:modelValue="updated => valuesItemUpdated(updated)" @remove="removeItem(element)"/> </div> </template> </Sortable> @@ -118,10 +118,6 @@ function removeSelf() { </script> <style lang="scss" module> -.root { - -} - .header { display: flex; } @@ -148,8 +144,4 @@ function removeSelf() { border-color: var(--accent); } } - -.values { - -} </style> diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue index 9e8af4302..3bc5ee972 100644 --- a/packages/frontend/src/pages/admin/abuses.vue +++ b/packages/frontend/src/pages/admin/abuses.vue @@ -1,8 +1,8 @@ <template> <MkStickyContainer> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="900"> - <div class="lcixvhis"> + <MkSpacer :contentMax="900"> + <div> <div class="reports"> <div class=""> <div class="inputs" style="display: flex;"> @@ -87,9 +87,3 @@ definePageMetadata({ icon: 'ti ti-exclamation-circle', }); </script> - -<style lang="scss" scoped> -.lcixvhis { - margin: var(--margin); -} -</style> diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue index 803e8cb7b..2c9e18b0b 100644 --- a/packages/frontend/src/pages/admin/ads.vue +++ b/packages/frontend/src/pages/admin/ads.vue @@ -1,9 +1,9 @@ <template> <MkStickyContainer> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="900"> - <div class="uqshojas"> - <div v-for="ad in ads" class="_panel _gaps_m ad"> + <MkSpacer :contentMax="900"> + <div> + <div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad"> <MkAd v-if="ad.url" :specify="ad"/> <MkInput v-model="ad.url" type="url"> <template #label>URL</template> @@ -196,14 +196,12 @@ definePageMetadata({ }); </script> -<style lang="scss" scoped> -.uqshojas { - > .ad { - padding: 32px; +<style lang="scss" module> +.ad { + padding: 32px; - &:not(:last-child) { - margin-bottom: var(--margin); - } + &:not(:last-child) { + margin-bottom: var(--margin); } } </style> diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index b76e4b911..3cb32c1d9 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -1,8 +1,8 @@ <template> <MkStickyContainer> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="900"> - <div class="ztgjmzrw _gaps_m"> + <MkSpacer :contentMax="900"> + <div class="_gaps_m"> <section v-for="announcement in announcements" class=""> <div class="_panel _gaps_m" style="padding: 24px;"> <MkInput v-model="announcement.title"> @@ -113,9 +113,3 @@ definePageMetadata({ icon: 'ti ti-speakerphone', }); </script> - -<style lang="scss" scoped> -.ztgjmzrw { - margin: var(--margin); -} -</style> diff --git a/packages/frontend/src/pages/admin/database.vue b/packages/frontend/src/pages/admin/database.vue index 5a0d3d5e5..131e586af 100644 --- a/packages/frontend/src/pages/admin/database.vue +++ b/packages/frontend/src/pages/admin/database.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="800" :margin-min="16" :margin-max="32"> + <MkSpacer :contentMax="800" :marginMin="16" :marginMax="32"> <FormSuspense v-slot="{ result: database }" :p="databasePromiseFactory"> <MkKeyValue v-for="table in database" :key="table[0]" oneline style="margin: 1em 0;"> <template #key>{{ table[0] }}</template> diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue index d51bf6230..4f5bb379a 100644 --- a/packages/frontend/src/pages/admin/email-settings.vue +++ b/packages/frontend/src/pages/admin/email-settings.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><XHeader :tabs="headerTabs"/></template> - <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> <div class="_gaps_m"> <MkSwitch v-model="enableEmail"> @@ -18,7 +18,7 @@ <template #label>{{ i18n.ts.smtpConfig }}</template> <div class="_gaps_m"> - <FormSplit :min-width="280"> + <FormSplit :minWidth="280"> <MkInput v-model="smtpHost"> <template #label>{{ i18n.ts.smtpHost }}</template> </MkInput> @@ -26,7 +26,7 @@ <template #label>{{ i18n.ts.smtpPort }}</template> </MkInput> </FormSplit> - <FormSplit :min-width="280"> + <FormSplit :minWidth="280"> <MkInput v-model="smtpUser"> <template #label>{{ i18n.ts.smtpUser }}</template> </MkInput> @@ -47,7 +47,7 @@ </MkSpacer> <template #footer> <div :class="$style.footer"> - <MkSpacer :content-max="700" :margin-min="16" :margin-max="16"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> <div class="_buttons"> <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> <MkButton rounded @click="testEmail"><i class="ti ti-send"></i> {{ i18n.ts.testEmail }}</MkButton> diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue index 6af161043..a8d6dcf3d 100644 --- a/packages/frontend/src/pages/admin/federation.vue +++ b/packages/frontend/src/pages/admin/federation.vue @@ -2,9 +2,9 @@ <div> <MkStickyContainer> <template #header><XHeader :actions="headerActions"/></template> - <MkSpacer :content-max="900"> - <div class="taeiyrib"> - <div class="query"> + <MkSpacer :contentMax="900"> + <div class="_gaps"> + <div> <MkInput v-model="host" :debounce="true" class=""> <template #prefix><i class="ti ti-search"></i></template> <template #label>{{ i18n.ts.host }}</template> @@ -39,8 +39,8 @@ </div> <MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination"> - <div class="dqokceoj"> - <MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`"> + <div :class="$style.instances"> + <MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" :class="$style.instance" :to="`/instance-info/${instance.host}`"> <MkInstanceCardMini :instance="instance"/> </MkA> </div> @@ -100,21 +100,14 @@ definePageMetadata(computed(() => ({ }))); </script> -<style lang="scss" scoped> -.taeiyrib { - > .query { - background: var(--bg); - margin-bottom: 16px; - } -} - -.dqokceoj { +<style lang="scss" module> +.instances { display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-gap: 12px; +} - > .instance:hover { - text-decoration: none; - } +.instance:hover { + text-decoration: none; } </style> diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue index c18943724..b204a1a64 100644 --- a/packages/frontend/src/pages/admin/files.vue +++ b/packages/frontend/src/pages/admin/files.vue @@ -2,30 +2,28 @@ <div> <MkStickyContainer> <template #header><XHeader :actions="headerActions"/></template> - <MkSpacer :content-max="900"> - <div class="xrmjdkdw"> - <div> - <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> - <MkSelect v-model="origin" style="margin: 0; flex: 1;"> - <template #label>{{ i18n.ts.instance }}</template> - <option value="combined">{{ i18n.ts.all }}</option> - <option value="local">{{ i18n.ts.local }}</option> - <option value="remote">{{ i18n.ts.remote }}</option> - </MkSelect> - <MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'"> - <template #label>{{ i18n.ts.host }}</template> - </MkInput> - </div> - <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap; padding-top: 1.2em;"> - <MkInput v-model="userId" :debounce="true" type="search" style="margin: 0; flex: 1;"> - <template #label>User ID</template> - </MkInput> - <MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;"> - <template #label>MIME type</template> - </MkInput> - </div> - <MkFileListForAdmin :pagination="pagination" :view-mode="viewMode"/> + <MkSpacer :contentMax="900"> + <div class="_gaps"> + <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> + <MkSelect v-model="origin" style="margin: 0; flex: 1;"> + <template #label>{{ i18n.ts.instance }}</template> + <option value="combined">{{ i18n.ts.all }}</option> + <option value="local">{{ i18n.ts.local }}</option> + <option value="remote">{{ i18n.ts.remote }}</option> + </MkSelect> + <MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'"> + <template #label>{{ i18n.ts.host }}</template> + </MkInput> </div> + <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> + <MkInput v-model="userId" :debounce="true" type="search" style="margin: 0; flex: 1;"> + <template #label>User ID</template> + </MkInput> + <MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;"> + <template #label>MIME type</template> + </MkInput> + </div> + <MkFileListForAdmin :pagination="pagination" :viewMode="viewMode"/> </div> </MkSpacer> </MkStickyContainer> @@ -109,9 +107,3 @@ definePageMetadata(computed(() => ({ icon: 'ti ti-cloud', }))); </script> - -<style lang="scss" scoped> -.xrmjdkdw { - margin: var(--margin); -} -</style> diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 963393d7e..5cbbcaa44 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -1,7 +1,7 @@ <template> <div ref="el" class="hiyeyicy" :class="{ wide: !narrow }"> <div v-if="!narrow || currentPage?.route.name == null" class="nav"> - <MkSpacer :content-max="700" :margin-min="16"> + <MkSpacer :contentMax="700" :marginMin="16"> <div class="lxpfedzu"> <div class="banner"> <img :src="instance.iconUrl || '/favicon.ico'" alt="" class="icon"/> diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue index 7a4937093..e5f3816c8 100644 --- a/packages/frontend/src/pages/admin/instance-block.vue +++ b/packages/frontend/src/pages/admin/instance-block.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> <MkTextarea v-model="blockedHosts"> <span>{{ i18n.ts.blockedInstances }}</span> diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index bf788e360..e36c9ac91 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -2,7 +2,7 @@ <div> <MkStickyContainer> <template #header><XHeader :tabs="headerTabs"/></template> - <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> <div class="_gaps_m"> <MkSwitch v-model="enableRegistration"> @@ -34,7 +34,7 @@ </MkSpacer> <template #footer> <div :class="$style.footer"> - <MkSpacer :content-max="700" :margin-min="16" :margin-max="16"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </MkSpacer> </div> diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue index 704b27c17..e569aad1b 100644 --- a/packages/frontend/src/pages/admin/object-storage.vue +++ b/packages/frontend/src/pages/admin/object-storage.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><XHeader :tabs="headerTabs"/></template> - <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> <div class="_gaps_m"> <MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch> @@ -33,7 +33,7 @@ <template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template> </MkInput> - <FormSplit :min-width="280"> + <FormSplit :minWidth="280"> <MkInput v-model="objectStorageAccessKey"> <template #prefix><i class="ti ti-key"></i></template> <template #label>Access key</template> @@ -69,7 +69,7 @@ </MkSpacer> <template #footer> <div :class="$style.footer"> - <MkSpacer :content-max="700" :margin-min="16" :margin-max="16"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </MkSpacer> </div> diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/other-settings.vue index 62dff6ce7..15d720a07 100644 --- a/packages/frontend/src/pages/admin/other-settings.vue +++ b/packages/frontend/src/pages/admin/other-settings.vue @@ -1,9 +1,17 @@ <template> <MkStickyContainer> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> - none + <div class="_gaps_s"> + <MkSwitch v-model="enableChartsForRemoteUser"> + <template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template> + </MkSwitch> + + <MkSwitch v-model="enableChartsForFederatedInstances"> + <template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template> + </MkSwitch> + </div> </FormSuspense> </MkSpacer> </MkStickyContainer> @@ -17,13 +25,22 @@ import * as os from '@/os'; import { fetchInstance } from '@/instance'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import MkSwitch from '@/components/MkSwitch.vue'; + +let enableChartsForRemoteUser: boolean = $ref(false); +let enableChartsForFederatedInstances: boolean = $ref(false); async function init() { - await os.api('admin/meta'); + const meta = await os.api('admin/meta'); + enableChartsForRemoteUser = meta.enableChartsForRemoteUser; + enableChartsForFederatedInstances = meta.enableChartsForFederatedInstances; } function save() { - os.apiWithDialog('admin/update-meta').then(() => { + os.apiWithDialog('admin/update-meta', { + enableChartsForRemoteUser, + enableChartsForFederatedInstances, + }).then(() => { fetchInstance(); }); } diff --git a/packages/frontend/src/pages/admin/overview.instances.vue b/packages/frontend/src/pages/admin/overview.instances.vue index 6c2ffd474..d349b3232 100644 --- a/packages/frontend/src/pages/admin/overview.instances.vue +++ b/packages/frontend/src/pages/admin/overview.instances.vue @@ -1,9 +1,9 @@ <template> -<div class="wbrkwale"> +<div> <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in"> <MkLoading v-if="fetching"/> - <div v-else class="instances"> - <MkA v-for="(instance, i) in instances" :key="instance.id" v-tooltip.mfm.noDelay="`${instance.name}\n${instance.host}\n${instance.softwareName} ${instance.softwareVersion}`" :to="`/instance-info/${instance.host}`" class="instance"> + <div v-else :class="$style.instances"> + <MkA v-for="(instance, i) in instances" :key="instance.id" v-tooltip.mfm.noDelay="`${instance.name}\n${instance.host}\n${instance.softwareName} ${instance.softwareVersion}`" :to="`/instance-info/${instance.host}`" :class="$style.instance"> <MkInstanceCardMini :instance="instance"/> </MkA> </div> @@ -36,16 +36,14 @@ useInterval(fetch, 1000 * 60, { }); </script> -<style lang="scss" scoped> -.wbrkwale { - > .instances { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - grid-gap: 12px; +<style lang="scss" module> +.instances { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + grid-gap: 12px; +} - > .instance:hover { - text-decoration: none; - } - } +.instance:hover { + text-decoration: none; } </style> diff --git a/packages/frontend/src/pages/admin/overview.pie.vue b/packages/frontend/src/pages/admin/overview.pie.vue index 08a29bf55..af7bc7055 100644 --- a/packages/frontend/src/pages/admin/overview.pie.vue +++ b/packages/frontend/src/pages/admin/overview.pie.vue @@ -67,7 +67,3 @@ onMounted(() => { }); }); </script> - -<style lang="scss" scoped> - -</style> diff --git a/packages/frontend/src/pages/admin/overview.queue.chart.vue b/packages/frontend/src/pages/admin/overview.queue.chart.vue index 6a11e8b76..a3c8659ce 100644 --- a/packages/frontend/src/pages/admin/overview.queue.chart.vue +++ b/packages/frontend/src/pages/admin/overview.queue.chart.vue @@ -132,7 +132,3 @@ defineExpose({ pushData, }); </script> - -<style lang="scss" scoped> - -</style> diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue index 1f56a2826..69ca89e22 100644 --- a/packages/frontend/src/pages/admin/overview.queue.vue +++ b/packages/frontend/src/pages/admin/overview.queue.vue @@ -33,9 +33,9 @@ import { markRaw, onMounted, onUnmounted, ref } from 'vue'; import XChart from './overview.queue.chart.vue'; import number from '@/filters/number'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; -const connection = markRaw(stream.useChannel('queueStats')); +const connection = markRaw(useStream().useChannel('queueStats')); const activeSincePrevTick = ref(0); const active = ref(0); diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue index 5c96c07bf..e8295c81b 100644 --- a/packages/frontend/src/pages/admin/overview.vue +++ b/packages/frontend/src/pages/admin/overview.vue @@ -1,6 +1,6 @@ <template> -<MkSpacer :content-max="1000"> - <div ref="rootEl" class="edbbcaef"> +<MkSpacer :contentMax="1000"> + <div ref="rootEl" :class="$style.root"> <MkFoldableSection class="item"> <template #header>Stats</template> <XStats/> @@ -72,7 +72,7 @@ import XRetention from './overview.retention.vue'; import XModerators from './overview.moderators.vue'; import XHeatmap from './overview.heatmap.vue'; import * as os from '@/os'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; @@ -87,7 +87,7 @@ let federationSubActive = $ref<number | null>(null); let federationSubActiveDiff = $ref<number | null>(null); let newUsers = $ref(null); let activeInstances = $shallowRef(null); -const queueStatsConnection = markRaw(stream.useChannel('queueStats')); +const queueStatsConnection = markRaw(useStream().useChannel('queueStats')); const now = new Date(); const filesPagination = { endpoint: 'admin/drive/files' as const, @@ -176,8 +176,8 @@ definePageMetadata({ }); </script> -<style lang="scss" scoped> -.edbbcaef { +<style lang="scss" module> +.root { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); grid-gap: 16px; diff --git a/packages/frontend/src/pages/admin/proxy-account.vue b/packages/frontend/src/pages/admin/proxy-account.vue index 6ad566187..c81f50a0d 100644 --- a/packages/frontend/src/pages/admin/proxy-account.vue +++ b/packages/frontend/src/pages/admin/proxy-account.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> <MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo> <MkKeyValue> diff --git a/packages/frontend/src/pages/admin/queue.chart.chart.vue b/packages/frontend/src/pages/admin/queue.chart.chart.vue index 1a1f6a9db..9bc0eee21 100644 --- a/packages/frontend/src/pages/admin/queue.chart.chart.vue +++ b/packages/frontend/src/pages/admin/queue.chart.chart.vue @@ -132,7 +132,3 @@ defineExpose({ pushData, }); </script> - -<style lang="scss" scoped> - -</style> diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue index 100d1ea54..8e6856fdd 100644 --- a/packages/frontend/src/pages/admin/queue.chart.vue +++ b/packages/frontend/src/pages/admin/queue.chart.vue @@ -1,35 +1,35 @@ <template> -<div class="pumxzjhg _gaps"> +<div class="_gaps"> <div :class="$style.status"> - <div class="item _panel"><div class="label">Process</div>{{ number(activeSincePrevTick) }}</div> - <div class="item _panel"><div class="label">Active</div>{{ number(active) }}</div> - <div class="item _panel"><div class="label">Waiting</div>{{ number(waiting) }}</div> - <div class="item _panel"><div class="label">Delayed</div>{{ number(delayed) }}</div> + <div :class="$style.statusItem" class="_panel"><div :class="$style.statusLabel">Process</div>{{ number(activeSincePrevTick) }}</div> + <div :class="$style.statusItem" class="_panel"><div :class="$style.statusLabel">Active</div>{{ number(active) }}</div> + <div :class="$style.statusItem" class="_panel"><div :class="$style.statusLabel">Waiting</div>{{ number(waiting) }}</div> + <div :class="$style.statusItem" class="_panel"><div :class="$style.statusLabel">Delayed</div>{{ number(delayed) }}</div> </div> - <div class="charts"> - <div class="chart"> - <div class="title">Process</div> + <div :class="$style.charts"> + <div :class="$style.chart"> + <div :class="$style.chartTitle">Process</div> <XChart ref="chartProcess" type="process"/> </div> - <div class="chart"> - <div class="title">Active</div> + <div :class="$style.chart"> + <div :class="$style.chartTitle">Active</div> <XChart ref="chartActive" type="active"/> </div> - <div class="chart"> - <div class="title">Delayed</div> + <div :class="$style.chart"> + <div :class="$style.chartTitle">Delayed</div> <XChart ref="chartDelayed" type="delayed"/> </div> - <div class="chart"> - <div class="title">Waiting</div> + <div :class="$style.chart"> + <div :class="$style.chartTitle">Waiting</div> <XChart ref="chartWaiting" type="waiting"/> </div> </div> - <MkFolder :default-open="true" :max-height="250"> + <MkFolder :defaultOpen="true" :max-height="250"> <template #icon><i class="ti ti-alert-triangle"></i></template> <template #label>Errored instances</template> <template #suffix>({{ number(jobs.reduce((a, b) => a + b[1], 0)) }} jobs)</template> - - <div :class="$style.jobs"> + + <div> <div v-if="jobs.length > 0"> <div v-for="job in jobs" :key="job[0]"> <MkA :to="`/instance-info/${job[0]}`" behavior="window">{{ job[0] }}</MkA> @@ -47,11 +47,11 @@ import { markRaw, onMounted, onUnmounted, ref } from 'vue'; import XChart from './queue.chart.chart.vue'; import number from '@/filters/number'; import * as os from '@/os'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { i18n } from '@/i18n'; import MkFolder from '@/components/MkFolder.vue'; -const connection = markRaw(stream.useChannel('queueStats')); +const connection = markRaw(useStream().useChannel('queueStats')); const activeSincePrevTick = ref(0); const active = ref(0); @@ -118,45 +118,36 @@ onUnmounted(() => { }); </script> -<style lang="scss" scoped> -.pumxzjhg { - > .charts { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; - - > .chart { - min-width: 0; - padding: 16px; - background: var(--panel); - border-radius: var(--radius); - - > .title { - margin-bottom: 8px; - } - } - } -} -</style> - <style lang="scss" module> +.charts { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.chart { + min-width: 0; + padding: 16px; + background: var(--panel); + border-radius: var(--radius); +} + +.chartTitle { + margin-bottom: 8px; +} + .status { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); grid-gap: 10px; - - &:global { - > .item { - padding: 12px 16px; - - > .label { - font-size: 80%; - opacity: 0.6; - } - } - } } -.jobs { +.statusItem { + padding: 12px 16px; +} + +.statusLabel { + font-size: 80%; + opacity: 0.6; } </style> diff --git a/packages/frontend/src/pages/admin/queue.vue b/packages/frontend/src/pages/admin/queue.vue index 509d329eb..1282a4b49 100644 --- a/packages/frontend/src/pages/admin/queue.vue +++ b/packages/frontend/src/pages/admin/queue.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="800"> + <MkSpacer :contentMax="800"> <XQueue v-if="tab === 'deliver'" domain="deliver"/> <XQueue v-else-if="tab === 'inbox'" domain="inbox"/> <br> diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue index 7ebcdfc58..119439c95 100644 --- a/packages/frontend/src/pages/admin/relays.vue +++ b/packages/frontend/src/pages/admin/relays.vue @@ -1,14 +1,14 @@ <template> <MkStickyContainer> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="800"> + <MkSpacer :contentMax="800"> <div class="_gaps"> <div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel" style="padding: 16px;"> <div>{{ relay.inbox }}</div> - <div class="status"> - <i v-if="relay.status === 'accepted'" class="ti ti-check icon accepted"></i> - <i v-else-if="relay.status === 'rejected'" class="ti ti-ban icon rejected"></i> - <i v-else class="ti ti-clock icon requesting"></i> + <div style="margin: 8px 0;"> + <i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--success);"></i> + <i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--error);"></i> + <i v-else class="ti ti-clock" :class="$style.icon"></i> <span>{{ i18n.t(`_relayStatus.${relay.status}`) }}</span> </div> <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton> @@ -83,23 +83,9 @@ definePageMetadata({ }); </script> -<style lang="scss" scoped> -.relaycxt { - > .status { - margin: 8px 0; - - > .icon { - width: 1em; - margin-right: 0.75em; - - &.accepted { - color: var(--success); - } - - &.rejected { - color: var(--error); - } - } - } +<style lang="scss" module> +.icon { + width: 1em; + margin-right: 0.75em; } </style> diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue index c211ef2f0..c7a34ac77 100644 --- a/packages/frontend/src/pages/admin/roles.edit.vue +++ b/packages/frontend/src/pages/admin/roles.edit.vue @@ -2,12 +2,12 @@ <div> <MkStickyContainer> <template #header><XHeader :tabs="headerTabs"/></template> - <MkSpacer :content-max="600" :margin-min="16" :margin-max="32"> + <MkSpacer :contentMax="600" :marginMin="16" :marginMax="32"> <XEditor v-if="data" v-model="data"/> </MkSpacer> <template #footer> <div :class="$style.footer"> - <MkSpacer :content-max="600" :margin-min="16" :margin-max="16"> + <MkSpacer :contentMax="600" :marginMin="16" :marginMax="16"> <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </MkSpacer> </div> diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index 49942c87c..a1fa9d293 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -36,7 +36,7 @@ <option value="conditional">{{ i18n.ts._role.conditional }}</option> </MkSelect> - <MkFolder v-if="role.target === 'conditional'" default-open> + <MkFolder v-if="role.target === 'conditional'" defaultOpen> <template #label>{{ i18n.ts._role.condition }}</template> <div class="_gaps"> <RolesEditorFormula v-model="role.condFormula"/> @@ -81,11 +81,11 @@ <MkSwitch v-model="role.policies.rateLimitFactor.useDefault" :readonly="readonly"> <template #label>{{ i18n.ts._role.useBaseValue }}</template> </MkSwitch> - <MkRange :model-value="role.policies.rateLimitFactor.value * 100" :min="0" :max="400" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => role.policies.rateLimitFactor.value = (v / 100)"> + <MkRange :modelValue="role.policies.rateLimitFactor.value * 100" :min="0" :max="400" :step="10" :textConverter="(v) => `${v}%`" @update:modelValue="v => role.policies.rateLimitFactor.value = (v / 100)"> <template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template> <template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template> </MkRange> - <MkRange v-model="role.policies.rateLimitFactor.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.rateLimitFactor.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -105,7 +105,7 @@ <MkSwitch v-model="role.policies.gtlAvailable.value" :disabled="role.policies.gtlAvailable.useDefault" :readonly="readonly"> <template #label>{{ i18n.ts.enable }}</template> </MkSwitch> - <MkRange v-model="role.policies.gtlAvailable.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.gtlAvailable.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -125,7 +125,7 @@ <MkSwitch v-model="role.policies.ltlAvailable.value" :disabled="role.policies.ltlAvailable.useDefault" :readonly="readonly"> <template #label>{{ i18n.ts.enable }}</template> </MkSwitch> - <MkRange v-model="role.policies.ltlAvailable.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.ltlAvailable.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -145,7 +145,7 @@ <MkSwitch v-model="role.policies.canPublicNote.value" :disabled="role.policies.canPublicNote.useDefault" :readonly="readonly"> <template #label>{{ i18n.ts.enable }}</template> </MkSwitch> - <MkRange v-model="role.policies.canPublicNote.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.canPublicNote.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -165,7 +165,7 @@ <MkSwitch v-model="role.policies.canInvite.value" :disabled="role.policies.canInvite.useDefault" :readonly="readonly"> <template #label>{{ i18n.ts.enable }}</template> </MkSwitch> - <MkRange v-model="role.policies.canInvite.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.canInvite.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -185,7 +185,7 @@ <MkSwitch v-model="role.policies.canManageCustomEmojis.value" :disabled="role.policies.canManageCustomEmojis.useDefault" :readonly="readonly"> <template #label>{{ i18n.ts.enable }}</template> </MkSwitch> - <MkRange v-model="role.policies.canManageCustomEmojis.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.canManageCustomEmojis.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -205,7 +205,7 @@ <MkSwitch v-model="role.policies.canSearchNotes.value" :disabled="role.policies.canSearchNotes.useDefault" :readonly="readonly"> <template #label>{{ i18n.ts.enable }}</template> </MkSwitch> - <MkRange v-model="role.policies.canSearchNotes.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.canSearchNotes.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -225,7 +225,7 @@ <MkInput v-model="role.policies.driveCapacityMb.value" :disabled="role.policies.driveCapacityMb.useDefault" type="number" :readonly="readonly"> <template #suffix>MB</template> </MkInput> - <MkRange v-model="role.policies.driveCapacityMb.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.driveCapacityMb.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -245,7 +245,7 @@ <MkSwitch v-model="role.policies.alwaysMarkNsfw.value" :disabled="role.policies.alwaysMarkNsfw.useDefault" :readonly="readonly"> <template #label>{{ i18n.ts.enable }}</template> </MkSwitch> - <MkRange v-model="role.policies.alwaysMarkNsfw.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.alwaysMarkNsfw.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -264,7 +264,7 @@ </MkSwitch> <MkInput v-model="role.policies.pinLimit.value" :disabled="role.policies.pinLimit.useDefault" type="number" :readonly="readonly"> </MkInput> - <MkRange v-model="role.policies.pinLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.pinLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -283,7 +283,7 @@ </MkSwitch> <MkInput v-model="role.policies.antennaLimit.value" :disabled="role.policies.antennaLimit.useDefault" type="number" :readonly="readonly"> </MkInput> - <MkRange v-model="role.policies.antennaLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.antennaLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -303,7 +303,7 @@ <MkInput v-model="role.policies.wordMuteLimit.value" :disabled="role.policies.wordMuteLimit.useDefault" type="number" :readonly="readonly"> <template #suffix>chars</template> </MkInput> - <MkRange v-model="role.policies.wordMuteLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.wordMuteLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -322,7 +322,7 @@ </MkSwitch> <MkInput v-model="role.policies.webhookLimit.value" :disabled="role.policies.webhookLimit.useDefault" type="number" :readonly="readonly"> </MkInput> - <MkRange v-model="role.policies.webhookLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.webhookLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -341,7 +341,7 @@ </MkSwitch> <MkInput v-model="role.policies.clipLimit.value" :disabled="role.policies.clipLimit.useDefault" type="number" :readonly="readonly"> </MkInput> - <MkRange v-model="role.policies.clipLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.clipLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -360,7 +360,7 @@ </MkSwitch> <MkInput v-model="role.policies.noteEachClipsLimit.value" :disabled="role.policies.noteEachClipsLimit.useDefault" type="number" :readonly="readonly"> </MkInput> - <MkRange v-model="role.policies.noteEachClipsLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.noteEachClipsLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -379,7 +379,7 @@ </MkSwitch> <MkInput v-model="role.policies.userListLimit.value" :disabled="role.policies.userListLimit.useDefault" type="number" :readonly="readonly"> </MkInput> - <MkRange v-model="role.policies.userListLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.userListLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -398,7 +398,7 @@ </MkSwitch> <MkInput v-model="role.policies.userEachUserListsLimit.value" :disabled="role.policies.userEachUserListsLimit.useDefault" type="number" :readonly="readonly"> </MkInput> - <MkRange v-model="role.policies.userEachUserListsLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.userEachUserListsLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> @@ -418,7 +418,7 @@ <MkSwitch v-model="role.policies.canHideAds.value" :disabled="role.policies.canHideAds.useDefault" :readonly="readonly"> <template #label>{{ i18n.ts.enable }}</template> </MkSwitch> - <MkRange v-model="role.policies.canHideAds.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <MkRange v-model="role.policies.canHideAds.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> <template #label>{{ i18n.ts._role.priority }}</template> </MkRange> </div> diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index 6eac90257..4ed6abf20 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -2,7 +2,7 @@ <div> <MkStickyContainer> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700"> + <MkSpacer :contentMax="700"> <div class="_gaps"> <div class="_buttons"> <MkButton primary rounded @click="edit"><i class="ti ti-pencil"></i> {{ i18n.ts.edit }}</MkButton> @@ -11,9 +11,9 @@ <MkFolder> <template #icon><i class="ti ti-info-circle"></i></template> <template #label>{{ i18n.ts.info }}</template> - <XEditor :model-value="role" readonly/> + <XEditor :modelValue="role" readonly/> </MkFolder> - <MkFolder v-if="role.target === 'manual'" default-open> + <MkFolder v-if="role.target === 'manual'" defaultOpen> <template #icon><i class="ti ti-users"></i></template> <template #label>{{ i18n.ts.users }}</template> <template #suffix>{{ role.usersCount }}</template> diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index e8dbe1c5f..6634d9cba 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -2,7 +2,7 @@ <div> <MkStickyContainer> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700"> + <MkSpacer :contentMax="700"> <div class="_gaps"> <MkFolder> <template #label>{{ i18n.ts._role.baseRole }}</template> @@ -14,7 +14,7 @@ <MkFolder v-if="matchQuery([i18n.ts._role._options.rateLimitFactor, 'rateLimitFactor'])"> <template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template> <template #suffix>{{ Math.floor(policies.rateLimitFactor * 100) }}%</template> - <MkRange :model-value="policies.rateLimitFactor * 100" :min="30" :max="300" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => policies.rateLimitFactor = (v / 100)"> + <MkRange :modelValue="policies.rateLimitFactor * 100" :min="30" :max="300" :step="10" :textConverter="(v) => `${v}%`" @update:modelValue="v => policies.rateLimitFactor = (v / 100)"> <template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template> </MkRange> </MkFolder> @@ -156,13 +156,13 @@ <MkFoldableSection> <template #header>Manual roles</template> <div class="_gaps_s"> - <MkRolePreview v-for="role in roles.filter(x => x.target === 'manual')" :key="role.id" :role="role" :for-moderation="true"/> + <MkRolePreview v-for="role in roles.filter(x => x.target === 'manual')" :key="role.id" :role="role" :forModeration="true"/> </div> </MkFoldableSection> <MkFoldableSection> <template #header>Conditional roles</template> <div class="_gaps_s"> - <MkRolePreview v-for="role in roles.filter(x => x.target === 'conditional')" :key="role.id" :role="role" :for-moderation="true"/> + <MkRolePreview v-for="role in roles.filter(x => x.target === 'conditional')" :key="role.id" :role="role" :forModeration="true"/> </div> </MkFoldableSection> </div> diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index cd8ef9e68..efb9f81f2 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> <div class="_gaps_m"> <MkFolder> @@ -33,7 +33,7 @@ <option value="remote">{{ i18n.ts.remoteOnly }}</option> </MkRadios> - <MkRange v-model="sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :text-converter="(v) => `${v + 1}`"> + <MkRange v-model="sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :textConverter="(v) => `${v + 1}`"> <template #label>{{ i18n.ts._sensitiveMediaDetection.sensitivity }}</template> <template #caption>{{ i18n.ts._sensitiveMediaDetection.sensitivityDescription }}</template> </MkRange> @@ -65,7 +65,7 @@ <div class="_gaps_m"> <span>{{ i18n.ts.activeEmailValidationDescription }}</span> - <MkSwitch v-model="enableActiveEmailValidation" @update:model-value="save"> + <MkSwitch v-model="enableActiveEmailValidation" @update:modelValue="save"> <template #label>Enable</template> </MkSwitch> </div> @@ -77,7 +77,7 @@ <template v-else #suffix>Disabled</template> <div class="_gaps_m"> - <MkSwitch v-model="enableIpLogging" @update:model-value="save"> + <MkSwitch v-model="enableIpLogging" @update:modelValue="save"> <template #label>Enable</template> </MkSwitch> </div> diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue index 85781c0bd..fdba4f464 100644 --- a/packages/frontend/src/pages/admin/server-rules.vue +++ b/packages/frontend/src/pages/admin/server-rules.vue @@ -2,13 +2,13 @@ <div> <MkStickyContainer> <template #header><XHeader :tabs="headerTabs"/></template> - <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <div class="_gaps_m"> <div>{{ i18n.ts._serverRules.description }}</div> <Sortable v-model="serverRules" class="_gaps_m" - :item-key="(_, i) => i" + :itemKey="(_, i) => i" :animation="150" :handle="'.' + $style.itemHandle" @start="e => e.item.classList.add('active')" @@ -27,7 +27,7 @@ </Sortable> <div :class="$style.commands"> <MkButton rounded @click="serverRules.push('')"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> - <MkButton primary rounded :class="$style.buttonSave" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </div> </div> </MkSpacer> diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 7ec3c381f..39d5ae860 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -2,7 +2,7 @@ <div> <MkStickyContainer> <template #header><XHeader :tabs="headerTabs"/></template> - <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> <div class="_gaps_m"> <MkInput v-model="name"> @@ -13,7 +13,7 @@ <template #label>{{ i18n.ts.instanceDescription }}</template> </MkTextarea> - <FormSplit :min-width="300"> + <FormSplit :minWidth="300"> <MkInput v-model="maintainerName"> <template #label>{{ i18n.ts.maintainerName }}</template> </MkInput> @@ -29,18 +29,6 @@ <template #caption>{{ i18n.ts.pinnedUsersDescription }}</template> </MkTextarea> - <FormSection> - <div class="_gaps_s"> - <MkSwitch v-model="enableChartsForRemoteUser"> - <template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template> - </MkSwitch> - - <MkSwitch v-model="enableChartsForFederatedInstances"> - <template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template> - </MkSwitch> - </div> - </FormSection> - <FormSection> <template #label>{{ i18n.ts.theme }}</template> @@ -128,7 +116,7 @@ </MkSpacer> <template #footer> <div :class="$style.footer"> - <MkSpacer :content-max="700" :margin-min="16" :margin-max="16"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </MkSpacer> </div> @@ -166,8 +154,6 @@ let defaultDarkTheme: any = $ref(null); let pinnedUsers: string = $ref(''); let cacheRemoteFiles: boolean = $ref(false); let enableServiceWorker: boolean = $ref(false); -let enableChartsForRemoteUser: boolean = $ref(false); -let enableChartsForFederatedInstances: boolean = $ref(false); let swPublicKey: any = $ref(null); let swPrivateKey: any = $ref(null); let deeplAuthKey: string = $ref(''); @@ -188,8 +174,6 @@ async function init() { pinnedUsers = meta.pinnedUsers.join('\n'); cacheRemoteFiles = meta.cacheRemoteFiles; enableServiceWorker = meta.enableServiceWorker; - enableChartsForRemoteUser = meta.enableChartsForRemoteUser; - enableChartsForFederatedInstances = meta.enableChartsForFederatedInstances; swPublicKey = meta.swPublickey; swPrivateKey = meta.swPrivateKey; deeplAuthKey = meta.deeplAuthKey; @@ -211,8 +195,6 @@ function save() { pinnedUsers: pinnedUsers.split('\n'), cacheRemoteFiles, enableServiceWorker, - enableChartsForRemoteUser, - enableChartsForFederatedInstances, swPublicKey, swPrivateKey, deeplAuthKey, diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue index 819ced826..1af661a47 100644 --- a/packages/frontend/src/pages/admin/users.vue +++ b/packages/frontend/src/pages/admin/users.vue @@ -2,7 +2,7 @@ <div> <MkStickyContainer> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="900"> + <MkSpacer :contentMax="900"> <div class="_gaps"> <div :class="$style.inputs"> <MkSelect v-model="sort" style="flex: 1;"> @@ -28,11 +28,11 @@ </MkSelect> </div> <div :class="$style.inputs"> - <MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false" @update:model-value="$refs.users.reload()"> + <MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false" @update:modelValue="$refs.users.reload()"> <template #prefix>@</template> <template #label>{{ i18n.ts.username }}</template> </MkInput> - <MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:model-value="$refs.users.reload()"> + <MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()"> <template #prefix>@</template> <template #label>{{ i18n.ts.host }}</template> </MkInput> diff --git a/packages/frontend/src/pages/ads.vue b/packages/frontend/src/pages/ads.vue index 728ef3c0b..4cf2e4b2e 100644 --- a/packages/frontend/src/pages/ads.vue +++ b/packages/frontend/src/pages/ads.vue @@ -2,7 +2,7 @@ <MkStickyContainer> <template #header><MkPageHeader/></template> - <MkSpacer :content-max="500"> + <MkSpacer :contentMax="500"> <div class="_gaps"> <MkAd v-for="ad in instance.ads" :key="ad.id" :specify="ad"/> </div> diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue index 16a0ee837..3dfb9e555 100644 --- a/packages/frontend/src/pages/announcements.vue +++ b/packages/frontend/src/pages/announcements.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="800"> + <MkSpacer :contentMax="800"> <MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _gaps_m"> <section v-for="(announcement, i) in items" :key="announcement.id" class="announcement _panel"> <div class="header"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div> diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue index 62e8178af..a22714791 100644 --- a/packages/frontend/src/pages/antenna-timeline.vue +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -1,19 +1,20 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <div ref="rootEl" v-hotkey.global="keymap" class="tqmomfks"> - <div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> - <div class="tl"> - <MkTimeline - ref="tlEl" :key="antennaId" - class="tl" - src="antenna" - :antenna="antennaId" - :sound="true" - @queue="queueUpdated" - /> + <MkSpacer :contentMax="800"> + <div ref="rootEl" v-hotkey.global="keymap"> + <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> + <div :class="$style.tl"> + <MkTimeline + ref="tlEl" :key="antennaId" + src="antenna" + :antenna="antennaId" + :sound="true" + @queue="queueUpdated" + /> + </div> </div> - </div> + </MkSpacer> </MkStickyContainer> </template> @@ -89,36 +90,29 @@ definePageMetadata(computed(() => antenna ? { } : null)); </script> -<style lang="scss" scoped> -.tqmomfks { - padding: var(--margin); +<style lang="scss" module> +.new { + position: sticky; + top: calc(var(--stickyTop, 0px) + 16px); + z-index: 1000; + width: 100%; + margin: calc(-0.675em - 8px) 0; - > .new { - position: sticky; - top: calc(var(--stickyTop, 0px) + 16px); - z-index: 1000; - width: 100%; - margin: calc(-0.675em - 8px - var(--margin)) 0 calc(-0.675em - 8px); - - > button { - display: block; - margin: var(--margin) auto 0 auto; - padding: 8px 16px; - border-radius: 32px; - } - } - - > .tl { - background: var(--bg); - border-radius: var(--radius); - overflow: clip; + &:first-child { + margin-top: calc(-0.675em - 8px - var(--margin)); } } -@container (min-width: 800px) { - .tqmomfks { - max-width: 800px; - margin: 0 auto; - } +.newButton { + display: block; + margin: var(--margin) auto 0 auto; + padding: 8px 16px; + border-radius: 32px; +} + +.tl { + background: var(--bg); + border-radius: var(--radius); + overflow: clip; } </style> diff --git a/packages/frontend/src/pages/api-console.vue b/packages/frontend/src/pages/api-console.vue index 7d2828e91..3a3cb3d7d 100644 --- a/packages/frontend/src/pages/api-console.vue +++ b/packages/frontend/src/pages/api-console.vue @@ -1,10 +1,10 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700"> + <MkSpacer :contentMax="700"> <div class="_gaps_m"> <div class="_gaps_m"> - <MkInput v-model="endpoint" :datalist="endpoints" @update:model-value="onEndpointChange()"> + <MkInput v-model="endpoint" :datalist="endpoints" @update:modelValue="onEndpointChange()"> <template #label>Endpoint</template> </MkInput> <MkTextarea v-model="body" code> diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue index 2f40e7ded..54e76805b 100644 --- a/packages/frontend/src/pages/auth.vue +++ b/packages/frontend/src/pages/auth.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="500"> + <MkSpacer :contentMax="500"> <div v-if="state == 'fetch-session-error'"> <p>{{ i18n.ts.somethingHappened }}</p> </div> diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index a74ab4047..0a358a141 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700"> + <MkSpacer :contentMax="700"> <div v-if="channelId == null || channel != null" class="_gaps_m"> <MkInput v-model="name"> <template #label>{{ i18n.ts.name }}</template> @@ -23,7 +23,7 @@ </div> </div> - <MkFolder :default-open="true"> + <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts.pinnedNotes }}</template> <div class="_gaps"> @@ -31,7 +31,7 @@ <Sortable v-model="pinnedNotes" - item-key="id" + itemKey="id" :handle="'.' + $style.pinnedNoteHandle" :animation="150" > diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 9aa564a7d..bcc0fc686 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -1,10 +1,12 @@ <template> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700" :class="$style.main"> + <MkSpacer :contentMax="700" :class="$style.main"> <div v-if="channel && tab === 'overview'" class="_gaps"> <div class="_panel" :class="$style.bannerContainer"> <XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/> + <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ti ti-star"></i></MkButton> + <MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ti ti-star"></i></MkButton> <div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" :class="$style.banner"> <div :class="$style.bannerStatus"> <div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div> @@ -13,13 +15,10 @@ <div :class="$style.bannerFade"></div> </div> <div v-if="channel.description" :class="$style.description"> - <Mfm :text="channel.description" :is-note="false" :i="$i"/> + <Mfm :text="channel.description" :isNote="false" :i="$i"/> </div> </div> - <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" as-like class="button" rounded primary @click="unfavorite()"><i class="ti ti-star"></i></MkButton> - <MkButton v-else v-tooltip="i18n.ts.favorite" as-like class="button" rounded @click="favorite()"><i class="ti ti-star"></i></MkButton> - <MkFoldableSection> <template #header><i class="ti ti-pin ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template> <div v-if="channel.pinnedNotes.length > 0" class="_gaps"> @@ -52,7 +51,7 @@ </MkSpacer> <template #footer> <div :class="$style.footer"> - <MkSpacer :content-max="700" :margin-min="16" :margin-max="16"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> <div class="_buttonsCenter"> <MkButton inline rounded primary gradate @click="openPostForm()"><i class="ti ti-pencil"></i> {{ i18n.ts.postToTheChannel }}</MkButton> </div> @@ -229,6 +228,13 @@ definePageMetadata(computed(() => channel ? { left: 16px; } +.favorite { + position: absolute; + z-index: 1; + top: 16px; + right: 16px; +} + .banner { position: relative; height: 200px; diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue index e670cdd86..0c4ccc1bc 100644 --- a/packages/frontend/src/pages/channels.vue +++ b/packages/frontend/src/pages/channels.vue @@ -1,13 +1,13 @@ <template> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700"> + <MkSpacer :contentMax="700"> <div v-if="tab === 'search'"> <div class="_gaps"> <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search"> <template #prefix><i class="ti ti-search"></i></template> </MkInput> - <MkRadios v-model="searchType" @update:model-value="search()"> + <MkRadios v-model="searchType" @update:modelValue="search()"> <option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option> <option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option> </MkRadios> diff --git a/packages/frontend/src/pages/clicker.vue b/packages/frontend/src/pages/clicker.vue index 24eae32e1..69ecc9e77 100644 --- a/packages/frontend/src/pages/clicker.vue +++ b/packages/frontend/src/pages/clicker.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader/></template> - <MkSpacer :content-max="800"> + <MkSpacer :contentMax="800"> <MkClickerGame/> </MkSpacer> </MkStickyContainer> diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index e3ac3f4c9..d5313099d 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -1,16 +1,16 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions"/></template> - <MkSpacer :content-max="800"> - <div v-if="clip"> - <div class="okzinsic _panel"> - <div v-if="clip.description" class="description"> - <Mfm :text="clip.description" :is-note="false" :i="$i"/> + <MkSpacer :contentMax="800"> + <div v-if="clip" class="_gaps"> + <div class="_panel"> + <div v-if="clip.description" :class="$style.description"> + <Mfm :text="clip.description" :isNote="false" :i="$i"/> </div> - <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" as-like class="button" rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton> - <MkButton v-else v-tooltip="i18n.ts.favorite" as-like class="button" rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton> - <div class="user"> - <MkAvatar :user="clip.user" class="avatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/> + <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton> + <MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton> + <div :class="$style.user"> + <MkAvatar :user="clip.user" :class="$style.avatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/> </div> </div> @@ -147,25 +147,20 @@ definePageMetadata(computed(() => clip ? { } : null)); </script> -<style lang="scss" scoped> -.okzinsic { - position: relative; - margin-bottom: var(--margin); +<style lang="scss" module> +.description { + padding: 16px; +} - > .description { - padding: 16px; - } +.user { + --height: 32px; + padding: 16px; + border-top: solid 0.5px var(--divider); + line-height: var(--height); +} - > .user { - $height: 32px; - padding: 16px; - border-top: solid 0.5px var(--divider); - line-height: $height; - - > .avatar { - width: $height; - height: $height; - } - } +.avatar { + width: var(--height); + height: var(--height); } </style> diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index 3f13f0787..3da6a0d9c 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -2,7 +2,7 @@ <div> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="900"> + <MkSpacer :contentMax="900"> <div class="ogwlenmc"> <div v-if="tab === 'local'" class="local"> <MkInput v-model="query" :debounce="true" type="search"> @@ -123,15 +123,14 @@ const toggleSelect = (emoji) => { }; const add = async (ev: MouseEvent) => { - const files = await selectFiles(ev.currentTarget ?? ev.target, null); - - const promise = Promise.all(files.map(file => os.api('admin/emoji/add', { - fileId: file.id, - }))); - promise.then(() => { - emojisPaginationComponent.value.reload(); - }); - os.promiseDialog(promise); + os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { + }, { + done: result => { + if (result.created) { + emojisPaginationComponent.value.prepend(result.created); + } + }, + }, 'closed'); }; const edit = (emoji) => { diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index 84bc153b7..3208c9273 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -1,84 +1,171 @@ <template> <MkModalWindow ref="dialog" - :width="370" - :with-ok-button="true" - @close="$refs.dialog.close()" + :width="400" + @close="dialog.close()" @closed="$emit('closed')" - @ok="ok()" > - <template #header>:{{ emoji.name }}:</template> + <template v-if="emoji" #header>:{{ emoji.name }}:</template> + <template v-else #header>New emoji</template> - <MkSpacer :margin-min="20" :margin-max="28"> - <div class="yigymqpb _gaps_m"> - <img :src="`/emoji/${emoji.name}.webp`" class="img"/> - <MkInput v-model="name"> - <template #label>{{ i18n.ts.name }}</template> - </MkInput> - <MkInput v-model="category" :datalist="customEmojiCategories"> - <template #label>{{ i18n.ts.category }}</template> - </MkInput> - <MkInput v-model="aliases"> - <template #label>{{ i18n.ts.tags }}</template> - <template #caption>{{ i18n.ts.setMultipleBySeparatingWithSpace }}</template> - </MkInput> - <MkInput v-model="license"> - <template #label>{{ i18n.ts.license }}</template> - </MkInput> - <MkButton danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + <div> + <MkSpacer :marginMin="20" :marginMax="28"> + <div class="_gaps_m"> + <div v-if="imgUrl != null" :class="$style.imgs"> + <div style="background: #000;" :class="$style.imgContainer"> + <img :src="imgUrl" :class="$style.img"/> + </div> + <div style="background: #222;" :class="$style.imgContainer"> + <img :src="imgUrl" :class="$style.img"/> + </div> + <div style="background: #ddd;" :class="$style.imgContainer"> + <img :src="imgUrl" :class="$style.img"/> + </div> + <div style="background: #fff;" :class="$style.imgContainer"> + <img :src="imgUrl" :class="$style.img"/> + </div> + </div> + <MkButton rounded style="margin: 0 auto;" @click="changeImage">{{ i18n.ts.selectFile }}</MkButton> + <MkInput v-model="name"> + <template #label>{{ i18n.ts.name }}</template> + </MkInput> + <MkInput v-model="category" :datalist="customEmojiCategories"> + <template #label>{{ i18n.ts.category }}</template> + </MkInput> + <MkInput v-model="aliases"> + <template #label>{{ i18n.ts.tags }}</template> + <template #caption>{{ i18n.ts.setMultipleBySeparatingWithSpace }}</template> + </MkInput> + <MkInput v-model="license"> + <template #label>{{ i18n.ts.license }}</template> + </MkInput> + <MkFolder> + <template #label>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReaction }}</template> + <template #suffix>{{ rolesThatCanBeUsedThisEmojiAsReaction.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisEmojiAsReaction.length }}</template> + + <div class="_gaps"> + <MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> + + <div v-for="role in rolesThatCanBeUsedThisEmojiAsReaction" :key="role.id" :class="$style.roleItem"> + <MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/> + <button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button> + <button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button> + </div> + + <MkInfo>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription }}</MkInfo> + <MkInfo warn>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn }}</MkInfo> + </div> + </MkFolder> + <MkSwitch v-model="isSensitive">isSensitive</MkSwitch> + <MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch> + <MkButton danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </div> + </MkSpacer> + <div :class="$style.footer"> + <MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton> </div> - </MkSpacer> + </div> </MkModalWindow> </template> <script lang="ts" setup> -import { } from 'vue'; +import { computed, watch } from 'vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; +import MkInfo from '@/components/MkInfo.vue'; +import MkFolder from '@/components/MkFolder.vue'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { customEmojiCategories } from '@/custom-emojis'; +import MkSwitch from '@/components/MkSwitch.vue'; +import { selectFile, selectFiles } from '@/scripts/select-file'; +import MkRolePreview from '@/components/MkRolePreview.vue'; const props = defineProps<{ - emoji: any, + emoji?: any, }>(); let dialog = $ref(null); -let name: string = $ref(props.emoji.name); -let category: string = $ref(props.emoji.category); -let aliases: string = $ref(props.emoji.aliases.join(' ')); -let license: string = $ref(props.emoji.license ?? ''); +let name: string = $ref(props.emoji ? props.emoji.name : ''); +let category: string = $ref(props.emoji ? props.emoji.category : ''); +let aliases: string = $ref(props.emoji ? props.emoji.aliases.join(' ') : ''); +let license: string = $ref(props.emoji ? (props.emoji.license ?? '') : ''); +let isSensitive = $ref(props.emoji ? props.emoji.isSensitive : false); +let localOnly = $ref(props.emoji ? props.emoji.localOnly : false); +let roleIdsThatCanBeUsedThisEmojiAsReaction = $ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []); +let rolesThatCanBeUsedThisEmojiAsReaction = $ref([]); +let file = $ref(); + +watch($$(roleIdsThatCanBeUsedThisEmojiAsReaction), async () => { + rolesThatCanBeUsedThisEmojiAsReaction = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); +}, { immediate: true }); + +const imgUrl = computed(() => file ? file.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null); const emit = defineEmits<{ - (ev: 'done', v: { deleted?: boolean, updated?: any }): void, + (ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void, (ev: 'closed'): void }>(); -function ok() { - update(); +async function changeImage(ev) { + file = await selectFile(ev.currentTarget ?? ev.target, null); } -async function update() { - await os.apiWithDialog('admin/emoji/update', { - id: props.emoji.id, +async function addRole() { + const roles = await os.api('admin/roles/list'); + const currentRoleIds = rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id); + + const { canceled, result: role } = await os.select({ + items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })), + }); + if (canceled) return; + + rolesThatCanBeUsedThisEmojiAsReaction.push(role); +} + +async function removeRole(role, ev) { + rolesThatCanBeUsedThisEmojiAsReaction = rolesThatCanBeUsedThisEmojiAsReaction.filter(x => x.id !== role.id); +} + +async function done() { + const params = { name, - category, - aliases: aliases.split(' '), + category: category === '' ? null : category, + aliases: aliases.split(' ').filter(x => x !== ''), license: license === '' ? null : license, - }); + isSensitive, + localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id), + }; - emit('done', { - updated: { + if (file) { + params.fileId = file.id; + } + + if (props.emoji) { + await os.apiWithDialog('admin/emoji/update', { id: props.emoji.id, - name, - category, - aliases: aliases.split(' '), - license: license === '' ? null : license, - }, - }); + ...params, + }); - dialog.close(); + emit('done', { + updated: { + id: props.emoji.id, + ...params, + }, + }); + + dialog.close(); + } else { + const created = await os.apiWithDialog('admin/emoji/add', params); + + emit('done', { + created: created, + }); + + dialog.close(); + } } async function del() { @@ -99,12 +186,48 @@ async function del() { } </script> -<style lang="scss" scoped> -.yigymqpb { - > .img { - display: block; - height: 64px; - margin: 0 auto; - } +<style lang="scss" module> +.imgs { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: center; +} + +.imgContainer { + padding: 8px; + border-radius: 6px; +} + +.img { + display: block; + height: 64px; + width: 64px; + object-fit: contain; +} + +.roleItem { + display: flex; +} + +.role { + flex: 1; +} + +.roleUnassign { + width: 32px; + height: 32px; + margin-left: 8px; + align-self: center; +} + +.footer { + position: sticky; + bottom: 0; + left: 0; + padding: 12px; + border-top: solid 0.5px var(--divider); + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); } </style> diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue index bdd21b29e..e9fab6a31 100644 --- a/packages/frontend/src/pages/emojis.emoji.vue +++ b/packages/frontend/src/pages/emojis.emoji.vue @@ -1,9 +1,9 @@ <template> -<button class="zuvgdzyu _button" @click="menu"> - <img :src="emoji.url" class="img" loading="lazy"/> - <div class="body"> - <div class="name _monospace">{{ emoji.name }}</div> - <div class="info">{{ emoji.aliases.join(' ') }}</div> +<button class="_button" :class="$style.root" @click="menu"> + <img :src="emoji.url" :class="$style.img" loading="lazy"/> + <div :class="$style.body"> + <div :class="$style.name" class="_monospace">{{ emoji.name }}</div> + <div :class="$style.info">{{ emoji.aliases.join(' ') }}</div> </div> </button> </template> @@ -49,8 +49,8 @@ function menu(ev) { } </script> -<style lang="scss" scoped> -.zuvgdzyu { +<style lang="scss" module> +.root { display: flex; align-items: center; padding: 12px; @@ -61,29 +61,29 @@ function menu(ev) { &:hover { border-color: var(--accent); } +} - > .img { - width: 42px; - height: 42px; - object-fit: contain; - } +.img { + width: 42px; + height: 42px; + object-fit: contain; +} - > .body { - padding: 0 0 0 8px; - white-space: nowrap; - overflow: hidden; +.body { + padding: 0 0 0 8px; + white-space: nowrap; + overflow: hidden; +} - > .name { - text-overflow: ellipsis; - overflow: hidden; - } +.name { + text-overflow: ellipsis; + overflow: hidden; +} - > .info { - opacity: 0.5; - font-size: 0.9em; - text-overflow: ellipsis; - overflow: hidden; - } - } +.info { + opacity: 0.5; + font-size: 0.9em; + text-overflow: ellipsis; + overflow: hidden; } </style> diff --git a/packages/frontend/src/pages/explore.featured.vue b/packages/frontend/src/pages/explore.featured.vue index a972ae04e..5c7131373 100644 --- a/packages/frontend/src/pages/explore.featured.vue +++ b/packages/frontend/src/pages/explore.featured.vue @@ -1,5 +1,5 @@ <template> -<MkSpacer :content-max="800"> +<MkSpacer :contentMax="800"> <MkTab v-model="tab" style="margin-bottom: var(--margin);"> <option value="notes">{{ i18n.ts.notes }}</option> <option value="polls">{{ i18n.ts.poll }}</option> diff --git a/packages/frontend/src/pages/explore.roles.vue b/packages/frontend/src/pages/explore.roles.vue index 6ac469f7b..c855d79f4 100644 --- a/packages/frontend/src/pages/explore.roles.vue +++ b/packages/frontend/src/pages/explore.roles.vue @@ -1,7 +1,7 @@ <template> -<MkSpacer :content-max="700"> +<MkSpacer :contentMax="700"> <div class="_gaps_s"> - <MkRolePreview v-for="role in roles" :key="role.id" :role="role" :for-moderation="false"/> + <MkRolePreview v-for="role in roles" :key="role.id" :role="role" :forModeration="false"/> </div> </MkSpacer> </template> diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue index f9c833dd2..785dbaa34 100644 --- a/packages/frontend/src/pages/explore.users.vue +++ b/packages/frontend/src/pages/explore.users.vue @@ -1,24 +1,24 @@ <template> -<MkSpacer :content-max="1200"> +<MkSpacer :contentMax="1200"> <MkTab v-model="origin" style="margin-bottom: var(--margin);"> <option value="local">{{ i18n.ts.local }}</option> <option value="remote">{{ i18n.ts.remote }}</option> </MkTab> <div v-if="origin === 'local'"> <template v-if="tag == null"> - <MkFoldableSection class="_margin" persist-key="explore-pinned-users"> + <MkFoldableSection class="_margin" persistKey="explore-pinned-users"> <template #header><i class="ti ti-bookmark ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template> <MkUserList :pagination="pinnedUsers"/> </MkFoldableSection> - <MkFoldableSection class="_margin" persist-key="explore-popular-users"> + <MkFoldableSection class="_margin" persistKey="explore-popular-users"> <template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template> <MkUserList :pagination="popularUsers"/> </MkFoldableSection> - <MkFoldableSection class="_margin" persist-key="explore-recently-updated-users"> + <MkFoldableSection class="_margin" persistKey="explore-recently-updated-users"> <template #header><i class="ti ti-message ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template> <MkUserList :pagination="recentlyUpdatedUsers"/> </MkFoldableSection> - <MkFoldableSection class="_margin" persist-key="explore-recently-registered-users"> + <MkFoldableSection class="_margin" persistKey="explore-recently-registered-users"> <template #header><i class="ti ti-plus ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyRegisteredUsers }}</template> <MkUserList :pagination="recentlyRegisteredUsers"/> </MkFoldableSection> diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue index 0dc9b9dc8..460bf65d1 100644 --- a/packages/frontend/src/pages/favorites.vue +++ b/packages/frontend/src/pages/favorites.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader/></template> - <MkSpacer :content-max="800"> + <MkSpacer :contentMax="800"> <MkPagination :pagination="pagination"> <template #empty> <div class="_fullinfo"> @@ -11,7 +11,7 @@ </template> <template #default="{ items }"> - <MkDateSeparatedList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false"> + <MkDateSeparatedList v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false"> <MkNote :key="item.id" :note="item.note" :class="$style.note"/> </MkDateSeparatedList> </template> diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index 816825e5b..6a16cd1c4 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700"> + <MkSpacer :contentMax="700"> <div class="_gaps"> <MkInput v-model="title"> <template #label>{{ i18n.ts._play.title }}</template> @@ -33,7 +33,7 @@ import MkTextarea from '@/components/MkTextarea.vue'; import MkInput from '@/components/MkInput.vue'; import { useRouter } from '@/router'; -const PRESET_DEFAULT = `/// @ 0.13.2 +const PRESET_DEFAULT = `/// @ 0.13.3 var name = "" @@ -51,7 +51,7 @@ Ui:render([ ]) `; -const PRESET_OMIKUJI = `/// @ 0.13.2 +const PRESET_OMIKUJI = `/// @ 0.13.3 // ユーザーごとに日替わりのおみくじのプリセット // 選択肢 @@ -94,7 +94,7 @@ Ui:render([ ]) `; -const PRESET_SHUFFLE = `/// @ 0.13.2 +const PRESET_SHUFFLE = `/// @ 0.13.3 // 巻き戻し可能な文字シャッフルのプリセット let string = "ペペロンチーノ" @@ -173,7 +173,7 @@ var cursor = 0 do() `; -const PRESET_QUIZ = `/// @ 0.13.2 +const PRESET_QUIZ = `/// @ 0.13.3 let title = '地理クイズ' let qas = [{ @@ -286,7 +286,7 @@ qaEls.push(Ui:C:container({ Ui:render(qaEls) `; -const PRESET_TIMELINE = `/// @ 0.13.2 +const PRESET_TIMELINE = `/// @ 0.13.3 // APIリクエストを行いローカルタイムラインを表示するプリセット @fetch() { @@ -442,7 +442,3 @@ definePageMetadata(computed(() => flash ? { title: i18n.ts._play.new, })); </script> - -<style lang="scss" scoped> - -</style> diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue index f1dca5f24..1f933c234 100644 --- a/packages/frontend/src/pages/flash/flash-index.vue +++ b/packages/frontend/src/pages/flash/flash-index.vue @@ -1,30 +1,30 @@ <template> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700"> - <div v-if="tab === 'featured'" class=""> + <MkSpacer :contentMax="700"> + <div v-if="tab === 'featured'"> <MkPagination v-slot="{items}" :pagination="featuredFlashsPagination"> <div class="_gaps_s"> - <MkFlashPreview v-for="flash in items" :key="flash.id" class="" :flash="flash"/> + <MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/> </div> </MkPagination> </div> - <div v-else-if="tab === 'my'" class="my"> + <div v-else-if="tab === 'my'"> <div class="_gaps"> - <MkButton class="new" gradate rounded style="margin: 0 auto;" @click="create()"><i class="ti ti-plus"></i></MkButton> + <MkButton gradate rounded style="margin: 0 auto;" @click="create()"><i class="ti ti-plus"></i></MkButton> <MkPagination v-slot="{items}" :pagination="myFlashsPagination"> <div class="_gaps_s"> - <MkFlashPreview v-for="flash in items" :key="flash.id" class="" :flash="flash"/> + <MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/> </div> </MkPagination> </div> </div> - <div v-else-if="tab === 'liked'" class=""> + <div v-else-if="tab === 'liked'"> <MkPagination v-slot="{items}" :pagination="likedFlashsPagination"> <div class="_gaps_s"> - <MkFlashPreview v-for="like in items" :key="like.flash.id" class="" :flash="like.flash"/> + <MkFlashPreview v-for="like in items" :key="like.flash.id" :flash="like.flash"/> </div> </MkPagination> </div> @@ -87,21 +87,3 @@ definePageMetadata(computed(() => ({ icon: 'ti ti-player-play', }))); </script> - -<style lang="scss" scoped> -.rknalgpo { - &.my .ckltabjg:first-child { - margin-top: 16px; - } - - .ckltabjg:not(:last-child) { - margin-bottom: 8px; - } - - @media (min-width: 500px) { - .ckltabjg:not(:last-child) { - margin-bottom: 16px; - } - } -} -</style> diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index 961ef4b75..2e1532b9f 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700"> + <MkSpacer :contentMax="700"> <Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in"> <div v-if="flash" :key="flash.id"> <Transition :name="defaultStore.state.animation ? 'zoom' : ''" mode="out-in"> @@ -10,8 +10,8 @@ <MkAsUi v-if="root" :component="root" :components="components"/> </div> <div class="actions _panel"> - <MkButton v-if="flash.isLiked" v-tooltip="i18n.ts.unlike" as-like class="button" rounded primary @click="unlike()"><i class="ti ti-heart"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton> - <MkButton v-else v-tooltip="i18n.ts.like" as-like class="button" rounded @click="like()"><i class="ti ti-heart"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton> + <MkButton v-if="flash.isLiked" v-tooltip="i18n.ts.unlike" asLike class="button" rounded primary @click="unlike()"><i class="ti ti-heart"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton> + <MkButton v-else v-tooltip="i18n.ts.like" asLike class="button" rounded @click="like()"><i class="ti ti-heart"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton> <MkButton v-tooltip="i18n.ts.shareWithNote" class="button" rounded @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></MkButton> <MkButton v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ti ti-share ti-fw"></i></MkButton> </div> @@ -27,7 +27,7 @@ </div> </div> </Transition> - <MkFolder :default-open="false" :max-height="280" class="_margin"> + <MkFolder :defaultOpen="false" :max-height="280" class="_margin"> <template #icon><i class="ti ti-code"></i></template> <template #label>{{ i18n.ts._play.viewSource }}</template> diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue index a51d1c78a..1452942a1 100644 --- a/packages/frontend/src/pages/follow-requests.vue +++ b/packages/frontend/src/pages/follow-requests.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader/></template> - <MkSpacer :content-max="800"> + <MkSpacer :contentMax="800"> <MkPagination ref="paginationComponent" :pagination="pagination"> <template #empty> <div class="_fullinfo"> diff --git a/packages/frontend/src/pages/follow.vue b/packages/frontend/src/pages/follow.vue index 828246d67..d14b66336 100644 --- a/packages/frontend/src/pages/follow.vue +++ b/packages/frontend/src/pages/follow.vue @@ -1,5 +1,5 @@ <template> -<div class="mk-follow-page"> +<div> </div> </template> diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue index cafcee0c3..f381636a7 100644 --- a/packages/frontend/src/pages/gallery/edit.vue +++ b/packages/frontend/src/pages/gallery/edit.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="800" :margin-min="16" :margin-max="32"> + <MkSpacer :contentMax="800" :marginMin="16" :marginMax="32"> <FormSuspense :p="init" class="_gaps"> <MkInput v-model="title"> <template #label>{{ i18n.ts.title }}</template> diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue index fc9cc7ae9..3c9c21a2f 100644 --- a/packages/frontend/src/pages/gallery/index.vue +++ b/packages/frontend/src/pages/gallery/index.vue @@ -1,21 +1,21 @@ <template> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="1400"> + <MkSpacer :contentMax="1400"> <div class="_root"> <div v-if="tab === 'explore'"> <MkFoldableSection class="_margin"> <template #header><i class="ti ti-clock"></i>{{ i18n.ts.recentPosts }}</template> - <MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true"> - <div class="vfpdbgtk"> + <MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disableAutoLoad="true"> + <div :class="$style.items"> <MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> </div> </MkPagination> </MkFoldableSection> <MkFoldableSection class="_margin"> <template #header><i class="ti ti-comet"></i>{{ i18n.ts.popularPosts }}</template> - <MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true"> - <div class="vfpdbgtk"> + <MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disableAutoLoad="true"> + <div :class="$style.items"> <MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> </div> </MkPagination> @@ -23,7 +23,7 @@ </div> <div v-else-if="tab === 'liked'"> <MkPagination v-slot="{items}" :pagination="likedPostsPagination"> - <div class="vfpdbgtk"> + <div :class="$style.items"> <MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/> </div> </MkPagination> @@ -31,7 +31,7 @@ <div v-else-if="tab === 'my'"> <MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="ti ti-plus"></i> {{ i18n.ts.postToGallery }}</MkA> <MkPagination v-slot="{items}" :pagination="myPostsPagination"> - <div class="vfpdbgtk"> + <div :class="$style.items"> <MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> </div> </MkPagination> @@ -119,15 +119,11 @@ definePageMetadata({ }); </script> -<style lang="scss" scoped> -.vfpdbgtk { +<style lang="scss" module> +.items { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); grid-gap: 12px; margin: 0 var(--margin); - - > .post { - - } } </style> diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index e0f3c105e..dfa6c0bac 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="1000" :margin-min="16" :margin-max="32"> + <MkSpacer :contentMax="1000" :marginMin="16" :marginMax="32"> <div class="_root"> <Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in"> <div v-if="post" class="rkxwuolj"> diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index ba5fda137..83997b255 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer v-if="instance" :content-max="600" :margin-min="16" :margin-max="32"> + <MkSpacer v-if="instance" :contentMax="600" :marginMin="16" :marginMax="32"> <div v-if="tab === 'overview'" class="_gaps_m"> <div class="fnfelxur"> <img :src="faviconUrl" alt="" class="icon"/> @@ -29,8 +29,8 @@ <FormSection v-if="iAmModerator"> <template #label>Moderation</template> <div class="_gaps_s"> - <MkSwitch v-model="suspended" @update:model-value="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch> - <MkSwitch v-model="isBlocked" @update:model-value="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch> + <MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch> + <MkSwitch v-model="isBlocked" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch> <MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton> </div> </FormSection> diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue new file mode 100644 index 000000000..f92c06d1c --- /dev/null +++ b/packages/frontend/src/pages/list.vue @@ -0,0 +1,148 @@ +<template> +<MkStickyContainer> + <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200"> + <div :class="$style.root"> + <img :class="$style.img" src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> + <p :class="$style.text"> + <i class="ti ti-alert-triangle"></i> + {{ i18n.ts.nothing }} + </p> + </div> + </MKSpacer> + <MkSpacer v-else-if="list" :contentMax="700" :class="$style.main"> + <div v-if="list" class="members _margin"> + <div :class="$style.member_text">{{ i18n.ts.members }}</div> + <div class="_gaps_s"> + <div v-for="user in users" :key="user.id" :class="$style.userItem"> + <MkA :class="$style.userItemBody" :to="`${userPage(user)}`"> + <MkUserCardMini :user="user"/> + </MkA> + </div> + </div> + </div> + <MkButton v-if="list.isLiked" v-tooltip="i18n.ts.unlike" inline :class="$style.button" asLike primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="list.likedCount > 0" class="count">{{ list.likedCount }}</span></MkButton> + <MkButton v-if="!list.isLiked" v-tooltip="i18n.ts.like" inline :class="$style.button" asLike @click="like()"><i class="ti ti-heart"></i><span v-if="1 > 0" class="count">{{ list.likedCount }}</span></MkButton> + <MkButton inline @click="create()"><i class="ti ti-download" :class="$style.import"></i>{{ i18n.ts.import }}</MkButton> + </MkSpacer> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import { watch, computed } from 'vue'; +import * as os from '@/os'; +import { userPage } from '@/filters/user'; +import { i18n } from '@/i18n'; +import MkUserCardMini from '@/components/MkUserCardMini.vue'; +import MkButton from '@/components/MkButton.vue'; +import { definePageMetadata } from '@/scripts/page-metadata'; + +const props = defineProps<{ + listId: string; +}>(); + +let list = $ref(null); +let error = $ref(); +let users = $ref([]); + +function fetchList(): void { + os.api('users/lists/show', { + listId: props.listId, + forPublic: true, + }).then(_list => { + list = _list; + os.api('users/show', { + userIds: list.userIds, + }).then(_users => { + users = _users; + }); + }).catch(err => { + error = err; + }); +} + +function like() { + os.apiWithDialog('users/lists/favorite', { + listId: list.id, + }).then(() => { + list.isLiked = true; + list.likedCount++; + }); +} + +function unlike() { + os.apiWithDialog('users/lists/unfavorite', { + listId: list.id, + }).then(() => { + list.isLiked = false; + list.likedCount--; + }); +} + +async function create() { + const { canceled, result: name } = await os.inputText({ + title: i18n.ts.enterListName, + }); + if (canceled) return; + await os.apiWithDialog('users/lists/create-from-public', { name: name, listId: list.id }); +} + +watch(() => props.listId, fetchList, { immediate: true }); + +const headerActions = $computed(() => []); + +const headerTabs = $computed(() => []); + +definePageMetadata(computed(() => list ? { + title: list.name, + icon: 'ti ti-list', +} : null)); +</script> +<style lang="scss" module> +.main { + min-height: calc(100cqh - (var(--stickyTop, 0px) + var(--stickyBottom, 0px))); +} + +.userItem { + display: flex; +} + +.userItemBody { + flex: 1; + min-width: 0; + margin-right: 8px; + + &:hover { + text-decoration: none; + } +} +.member_text { + margin: 5px; +} + +.root { + padding: 32px; + text-align: center; + align-items: center; +} + +.text { + margin: 0 0 8px 0; +} + +.img { + vertical-align: bottom; + width: 128px; + height: 128px; + margin-bottom: 16px; + border-radius: 16px; +} + +.button { + margin-right: 10px; +} + +.import { + margin-right: 4px; +} +</style> diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue index 8e0624f55..553946cd9 100644 --- a/packages/frontend/src/pages/miauth.vue +++ b/packages/frontend/src/pages/miauth.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="800"> + <MkSpacer :contentMax="800"> <div v-if="$i"> <div v-if="state == 'waiting'"> <MkLoading/> diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue index c35af3e22..355d18fdb 100644 --- a/packages/frontend/src/pages/my-antennas/create.vue +++ b/packages/frontend/src/pages/my-antennas/create.vue @@ -1,5 +1,5 @@ <template> -<div class="geegznzt"> +<div> <XAntenna :antenna="draft" @created="onAntennaCreated"/> </div> </template> @@ -38,7 +38,3 @@ definePageMetadata({ icon: 'ti ti-antenna', }); </script> - -<style lang="scss" scoped> - -</style> diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue index 913fbde8e..da9b2de48 100644 --- a/packages/frontend/src/pages/my-antennas/edit.vue +++ b/packages/frontend/src/pages/my-antennas/edit.vue @@ -36,7 +36,3 @@ definePageMetadata({ icon: 'ti ti-antenna', }); </script> - -<style lang="scss" scoped> - -</style> diff --git a/packages/frontend/src/pages/my-antennas/editor.vue b/packages/frontend/src/pages/my-antennas/editor.vue index 26b7bcc71..ed92208c4 100644 --- a/packages/frontend/src/pages/my-antennas/editor.vue +++ b/packages/frontend/src/pages/my-antennas/editor.vue @@ -1,6 +1,6 @@ <template> -<MkSpacer :content-max="700"> - <div class="shaynizk"> +<MkSpacer :contentMax="700"> + <div> <div class="_gaps_m"> <MkInput v-model="name"> <template #label>{{ i18n.ts.name }}</template> @@ -33,7 +33,7 @@ <MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch> <MkSwitch v-model="notify">{{ i18n.ts.notifyAntenna }}</MkSwitch> </div> - <div class="actions"> + <div :class="$style.actions"> <MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> <MkButton v-if="antenna.id != null" inline danger @click="deleteAntenna()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> </div> @@ -128,12 +128,10 @@ function addUser() { } </script> -<style lang="scss" scoped> -.shaynizk { - > .actions { - margin-top: 16px; - padding: 24px 0; - border-top: solid 0.5px var(--divider); - } +<style lang="scss" module> +.actions { + margin-top: 16px; + padding: 24px 0; + border-top: solid 0.5px var(--divider); } </style> diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue index f1764b1aa..2ca026b9a 100644 --- a/packages/frontend/src/pages/my-antennas/index.vue +++ b/packages/frontend/src/pages/my-antennas/index.vue @@ -1,18 +1,20 @@ -<template><MkStickyContainer> +<template> +<MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700"> - <div class="ieepwinx"> - <MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> + <MkSpacer :contentMax="700"> + <div class="ieepwinx"> + <MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> - <div class=""> - <MkPagination v-slot="{items}" ref="list" :pagination="pagination"> - <MkA v-for="antenna in items" :key="antenna.id" class="ljoevbzj" :to="`/my/antennas/${antenna.id}`"> - <div class="name">{{ antenna.name }}</div> - </MkA> - </MkPagination> + <div class=""> + <MkPagination v-slot="{items}" ref="list" :pagination="pagination"> + <MkA v-for="antenna in items" :key="antenna.id" class="ljoevbzj" :to="`/my/antennas/${antenna.id}`"> + <div class="name">{{ antenna.name }}</div> + </MkA> + </MkPagination> + </div> </div> - </div> -</MkSpacer></MkStickyContainer> + </MkSpacer> +</MkStickyContainer> </template> <script lang="ts" setup> diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue index ccffa7b56..a769f8ee9 100644 --- a/packages/frontend/src/pages/my-clips/index.vue +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700"> + <MkSpacer :contentMax="700"> <div v-if="tab === 'my'" class="_gaps"> <MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue index 47437f3e5..cee241c48 100644 --- a/packages/frontend/src/pages/my-lists/index.vue +++ b/packages/frontend/src/pages/my-lists/index.vue @@ -1,15 +1,17 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700"> - <div class="qkcjvfiv"> - <MkButton primary class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.createList }}</MkButton> + <MkSpacer :contentMax="700"> + <div class="_gaps"> + <MkButton primary rounded style="margin: 0 auto;" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.createList }}</MkButton> - <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="lists"> - <MkA v-for="list in items" :key="list.id" class="list _panel" :to="`/my/lists/${ list.id }`"> - <div class="name">{{ list.name }}</div> - <MkAvatars :user-ids="list.userIds"/> - </MkA> + <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination"> + <div class="_gaps"> + <MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`"> + <div style="margin-bottom: 4px;">{{ list.name }}</div> + <MkAvatars :userIds="list.userIds"/> + </MkA> + </div> </MkPagination> </div> </MkSpacer> @@ -58,28 +60,17 @@ definePageMetadata({ }); </script> -<style lang="scss" scoped> -.qkcjvfiv { - > .add { - margin: 0 auto var(--margin) auto; - } +<style lang="scss" module> +.list { + display: block; + padding: 16px; + border: solid 1px var(--divider); + border-radius: 6px; + margin-bottom: 8px; - > .lists { - > .list { - display: block; - padding: 16px; - border: solid 1px var(--divider); - border-radius: 6px; - - &:hover { - border: solid 1px var(--accent); - text-decoration: none; - } - - > .name { - margin-bottom: 4px; - } - } + &:hover { + border: solid 1px var(--accent); + text-decoration: none; } } </style> diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index 86201e8e0..dd431e8dc 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -1,35 +1,43 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700" :class="$style.main"> - <div v-if="list" class="members _margin"> - <div class="">{{ i18n.ts.members }}</div> - <div class="_gaps_s"> - <div v-for="user in users" :key="user.id" :class="$style.userItem"> - <MkA :class="$style.userItemBody" :to="`${userPage(user)}`"> - <MkUserCardMini :user="user"/> - </MkA> - <button class="_button" :class="$style.remove" @click="removeUser(user, $event)"><i class="ti ti-x"></i></button> + <MkSpacer :contentMax="700" :class="$style.main"> + <div v-if="list" class="_gaps"> + <MkFolder> + <template #label>{{ i18n.ts.settings }}</template> + + <div class="_gaps"> + <MkInput v-model="name"> + <template #label>{{ i18n.ts.name }}</template> + </MkInput> + <MkSwitch v-model="isPublic">{{ i18n.ts.public }}</MkSwitch> + <div class="_buttons"> + <MkButton rounded primary @click="updateSettings">{{ i18n.ts.save }}</MkButton> + <MkButton rounded danger @click="deleteList()">{{ i18n.ts.delete }}</MkButton> + </div> </div> - </div> + </MkFolder> + + <MkFolder defaultOpen> + <template #label>{{ i18n.ts.members }}</template> + + <div class="_gaps_s"> + <MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton> + <div v-for="user in users" :key="user.id" :class="$style.userItem"> + <MkA :class="$style.userItemBody" :to="`${userPage(user)}`"> + <MkUserCardMini :user="user"/> + </MkA> + <button class="_button" :class="$style.remove" @click="removeUser(user, $event)"><i class="ti ti-x"></i></button> + </div> + </div> + </MkFolder> </div> </MkSpacer> - <template #footer> - <div :class="$style.footer"> - <MkSpacer :content-max="700" :margin-min="16" :margin-max="16"> - <div class="_buttons"> - <MkButton inline rounded primary @click="addUser()">{{ i18n.ts.addUser }}</MkButton> - <MkButton inline rounded @click="renameList()">{{ i18n.ts.rename }}</MkButton> - <MkButton inline rounded danger @click="deleteList()">{{ i18n.ts.delete }}</MkButton> - </div> - </MkSpacer> - </div> - </template> </MkStickyContainer> </template> <script lang="ts" setup> -import { computed, watch } from 'vue'; +import { computed, ref, watch } from 'vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os'; import { mainRouter } from '@/router'; @@ -37,6 +45,9 @@ import { definePageMetadata } from '@/scripts/page-metadata'; import { i18n } from '@/i18n'; import { userPage } from '@/filters/user'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import MkInput from '@/components/MkInput.vue'; import { userListsCache } from '@/cache'; const props = defineProps<{ @@ -45,12 +56,17 @@ const props = defineProps<{ let list = $ref(null); let users = $ref([]); +const isPublic = ref(false); +const name = ref(''); function fetchList() { os.api('users/lists/show', { listId: props.listId, }).then(_list => { list = _list; + name.value = list.name; + isPublic.value = list.isPublic; + os.api('users/show', { userIds: list.userIds, }).then(_users => { @@ -86,23 +102,6 @@ async function removeUser(user, ev) { }], ev.currentTarget ?? ev.target); } -async function renameList() { - const { canceled, result: name } = await os.inputText({ - title: i18n.ts.enterListName, - default: list.name, - }); - if (canceled) return; - - await os.api('users/lists/update', { - listId: list.id, - name: name, - }); - - userListsCache.delete(); - - list.name = name; -} - async function deleteList() { const { canceled } = await os.confirm({ type: 'warning', @@ -117,6 +116,19 @@ async function deleteList() { mainRouter.push('/my/lists'); } +async function updateSettings() { + await os.apiWithDialog('users/lists/update', { + listId: list.id, + name: name.value, + isPublic: isPublic.value, + }); + + userListsCache.delete(); + + list.name = name.value; + list.isPublic = isPublic.value; +} + watch(() => props.listId, fetchList, { immediate: true }); const headerActions = $computed(() => []); diff --git a/packages/frontend/src/pages/not-found.vue b/packages/frontend/src/pages/not-found.vue index e58e44ef7..2c9d94901 100644 --- a/packages/frontend/src/pages/not-found.vue +++ b/packages/frontend/src/pages/not-found.vue @@ -1,5 +1,5 @@ <template> -<div class="ipledcug"> +<div> <div class="_fullinfo"> <img src="https://xn--931a.moe/assets/not-found.jpg" class="_ghost"/> <div>{{ i18n.ts.notFoundDescription }}</div> diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index d9baa1096..c519cefba 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -1,33 +1,33 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="800"> - <div class="fcuexfpr"> + <MkSpacer :contentMax="800"> + <div> <Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in"> - <div v-if="note" class="note"> + <div v-if="note"> <div v-if="showNext" class="_margin"> - <MkNotes class="" :pagination="nextPagination" :no-gap="true"/> + <MkNotes class="" :pagination="nextPagination" :noGap="true"/> </div> - <div class="main _margin"> - <MkButton v-if="!showNext && hasNext" class="load next" @click="showNext = true"><i class="ti ti-chevron-up"></i></MkButton> - <div class="note _margin _gaps_s"> + <div class="_margin"> + <MkButton v-if="!showNext && hasNext" :class="$style.loadNext" @click="showNext = true"><i class="ti ti-chevron-up"></i></MkButton> + <div class="_margin _gaps_s"> <MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/> - <MkNoteDetailed :key="note.id" v-model:note="note" class="note"/> + <MkNoteDetailed :key="note.id" v-model:note="note" :class="$style.note"/> </div> - <div v-if="clips && clips.length > 0" class="clips _margin"> - <div class="title">{{ i18n.ts.clip }}</div> + <div v-if="clips && clips.length > 0" class="_margin"> + <div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div> <div class="_gaps"> <MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`"> <MkClipPreview :clip="item"/> </MkA> </div> </div> - <MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><i class="ti ti-chevron-down"></i></MkButton> + <MkButton v-if="!showPrev && hasPrev" :class="$style.loadPrev" @click="showPrev = true"><i class="ti ti-chevron-down"></i></MkButton> </div> <div v-if="showPrev" class="_margin"> - <MkNotes class="" :pagination="prevPagination" :no-gap="true"/> + <MkNotes class="" :pagination="prevPagination" :noGap="true"/> </div> </div> <MkError v-else-if="error" @retry="fetchNote()"/> @@ -137,7 +137,7 @@ definePageMetadata(computed(() => note ? { } : null)); </script> -<style lang="scss" scoped> +<style lang="scss" module> .fade-enter-active, .fade-leave-active { transition: opacity 0.125s ease; @@ -147,39 +147,23 @@ definePageMetadata(computed(() => note ? { opacity: 0; } -.fcuexfpr { - background: var(--bg); +.loadNext, +.loadPrev { + min-width: 0; + margin: 0 auto; + border-radius: 999px; +} - > .note { - > .main { - > .load { - min-width: 0; - margin: 0 auto; - border-radius: 999px; +.loadNext { + margin-bottom: var(--margin); +} - &.next { - margin-bottom: var(--margin); - } +.loadPrev { + margin-top: var(--margin); +} - &.prev { - margin-top: var(--margin); - } - } - - > .note { - > .note { - border-radius: var(--radius); - background: var(--panel); - } - } - - > .clips { - > .title { - font-weight: bold; - padding: 12px; - } - } - } - } +.note { + border-radius: var(--radius); + background: var(--panel); } </style> diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index 1789606cd..8196f9186 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -1,9 +1,9 @@ <template> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="800"> + <MkSpacer :contentMax="800"> <div v-if="tab === 'all'"> - <XNotifications class="notifications" :include-types="includeTypes"/> + <XNotifications class="notifications" :includeTypes="includeTypes"/> </div> <div v-else-if="tab === 'mentions'"> <MkNotes :pagination="mentionsPagination"/> diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue index 1b292e8f3..eca3feda6 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue @@ -8,8 +8,8 @@ </button> </template> - <section class="oyyftmcf"> - <MkDriveFileThumbnail v-if="file" class="preview" :file="file" fit="contain" @click="choose()"/> + <section> + <MkDriveFileThumbnail v-if="file" style="height: 150px;" :file="file" fit="contain" @click="choose()"/> </section> </XContainer> </template> @@ -54,11 +54,3 @@ onMounted(async () => { } }); </script> - -<style lang="scss" scoped> -.oyyftmcf { - > .preview { - height: 150px; - } -} -</style> diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue index bf21ae3c6..3b15c1774 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue @@ -3,8 +3,8 @@ <XContainer :draggable="true" @remove="() => $emit('remove')"> <template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template> - <section class="vckmsadr"> - <textarea v-model="text"></textarea> + <section> + <textarea v-model="text" :class="$style.textarea"></textarea> </section> </XContainer> </template> @@ -33,23 +33,21 @@ watch($$(text), () => { }); </script> -<style lang="scss" scoped> -.vckmsadr { - > textarea { - display: block; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - width: 100%; - min-width: 100%; - min-height: 150px; - border: none; - box-shadow: none; - padding: 16px; - background: transparent; - color: var(--fg); - font-size: 14px; - box-sizing: border-box; - } +<style lang="scss" module> +.textarea { + display: block; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 100%; + min-width: 100%; + min-height: 150px; + border: none; + box-shadow: none; + padding: 16px; + background: transparent; + color: var(--fg); + font-size: 14px; + box-sizing: border-box; } </style> diff --git a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue index 97bdcfe80..fc945b3d6 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue @@ -1,57 +1,59 @@ <template> -<Sortable :model-value="modelValue" tag="div" item-key="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swap-threshold="0.5" @update:model-value="v => $emit('update:modelValue', v)"> +<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => $emit('update:modelValue', v)"> <template #item="{element}"> <div :class="$style.item"> <!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 --> - <component :is="'x-' + element.type" :model-value="element" @update:model-value="updateItem" @remove="() => removeItem(element)"/> + <component :is="getComponent(element.type)" :modelValue="element" @update:modelValue="updateItem" @remove="() => removeItem(element)"/> </div> </template> </Sortable> </template> -<script lang="ts"> -import { defineComponent, defineAsyncComponent } from 'vue'; +<script lang="ts" setup> +import { defineAsyncComponent } from 'vue'; import XSection from './els/page-editor.el.section.vue'; import XText from './els/page-editor.el.text.vue'; import XImage from './els/page-editor.el.image.vue'; import XNote from './els/page-editor.el.note.vue'; -export default defineComponent({ - components: { - Sortable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)), - XSection, XText, XImage, XNote, - }, +function getComponent(type: string) { + switch (type) { + case 'section': return XSection; + case 'text': return XText; + case 'image': return XImage; + case 'note': return XNote; + default: return null; + } +} - props: { - modelValue: { - type: Array, - required: true, - }, - }, +const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); - emits: ['update:modelValue'], +const props = defineProps<{ + modelValue: any[]; +}>(); - methods: { - updateItem(v) { - const i = this.modelValue.findIndex(x => x.id === v.id); - const newValue = [ - ...this.modelValue.slice(0, i), - v, - ...this.modelValue.slice(i + 1), - ]; - this.$emit('update:modelValue', newValue); - }, +const emit = defineEmits<{ + (ev: 'update:modelValue', value: any[]): void; +}>(); - removeItem(el) { - const i = this.modelValue.findIndex(x => x.id === el.id); - const newValue = [ - ...this.modelValue.slice(0, i), - ...this.modelValue.slice(i + 1), - ]; - this.$emit('update:modelValue', newValue); - }, - }, -}); +function updateItem(v) { + const i = props.modelValue.findIndex(x => x.id === v.id); + const newValue = [ + ...props.modelValue.slice(0, i), + v, + ...props.modelValue.slice(i + 1), + ]; + emit('update:modelValue', newValue); +} + +function removeItem(el) { + const i = props.modelValue.findIndex(x => x.id === el.id); + const newValue = [ + ...props.modelValue.slice(0, i), + ...props.modelValue.slice(i + 1), + ]; + emit('update:modelValue', newValue); +} </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/page-editor/page-editor.container.vue b/packages/frontend/src/pages/page-editor/page-editor.container.vue index dd733403a..0842b4fd2 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.container.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.container.vue @@ -1,5 +1,5 @@ <template> -<div class="cpjygsrt" :class="{ error: error != null, warn: warn != null }"> +<div class="cpjygsrt"> <header> <div class="title"><slot name="header"></slot></div> <div class="buttons"> @@ -16,58 +16,40 @@ </button> </div> </header> - <p v-show="showBody" v-if="error != null" class="error">{{ i18n.t('_pages.script.typeError', { slot: error.arg + 1, expect: i18n.t(`script.types.${error.expect}`), actual: i18n.t(`script.types.${error.actual}`) }) }}</p> - <p v-show="showBody" v-if="warn != null" class="warn">{{ i18n.t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p> <div v-show="showBody" class="body"> <slot></slot> </div> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { ref } from 'vue'; import { i18n } from '@/i18n'; -export default defineComponent({ - props: { - expanded: { - type: Boolean, - default: true, - }, - removable: { - type: Boolean, - default: true, - }, - draggable: { - type: Boolean, - default: false, - }, - error: { - required: false, - default: null, - }, - warn: { - required: false, - default: null, - }, - }, - emits: ['toggle', 'remove'], - data() { - return { - showBody: this.expanded, - i18n, - }; - }, - methods: { - toggleContent(show: boolean) { - this.showBody = show; - this.$emit('toggle', show); - }, - remove() { - this.$emit('remove'); - }, - }, +const props = withDefaults(defineProps<{ + expanded?: boolean; + removable?: boolean; + draggable?: boolean; +}>(), { + expanded: true, + removable: true, }); + +const emit = defineEmits<{ + (ev: 'toggle', show: boolean): void; + (ev: 'remove'): void; +}>(); + +const showBody = ref(props.expanded); + +function toggleContent(show: boolean) { + showBody.value = show; + emit('toggle', show); +} + +function remove() { + emit('remove'); +} </script> <style lang="scss" scoped> @@ -128,20 +110,6 @@ export default defineComponent({ } } - > .warn { - color: #b19e49; - margin: 0; - padding: 16px 16px 0 16px; - font-size: 14px; - } - - > .error { - color: #f00; - margin: 0; - padding: 16px 16px 0 16px; - font-size: 14px; - } - > .body { ::v-deep(.juejbjww), ::v-deep(.eiipwacr) { &:not(.inline):first-child { diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue index bcf30e23a..bd54699dc 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700"> + <MkSpacer :contentMax="700"> <div class="jqqmcavi"> <MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="ti ti-external-link"></i> {{ i18n.ts._pages.viewPage }}</MkButton> <MkButton v-if="!readonly" inline primary class="button" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index 5a0f58c8d..27a4cd059 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700"> + <MkSpacer :contentMax="700"> <Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in"> <div v-if="page" :key="page.id" class="xcukqgmh"> <div class="main"> @@ -18,8 +18,8 @@ </div> <div class="actions"> <div class="like"> - <MkButton v-if="page.isLiked" v-tooltip="i18n.ts._pages.unlike" class="button" as-like primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton> - <MkButton v-else v-tooltip="i18n.ts._pages.like" class="button" as-like @click="like()"><i class="ti ti-heart"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton> + <MkButton v-if="page.isLiked" v-tooltip="i18n.ts._pages.unlike" class="button" asLike primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton> + <MkButton v-else v-tooltip="i18n.ts._pages.like" class="button" asLike @click="like()"><i class="ti ti-heart"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton> </div> <div class="other"> <button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></button> diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue index 0427332ab..4f67bda11 100644 --- a/packages/frontend/src/pages/pages.vue +++ b/packages/frontend/src/pages/pages.vue @@ -1,23 +1,29 @@ <template> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="700"> - <div v-if="tab === 'featured'" class="rknalgpo"> + <MkSpacer :contentMax="700"> + <div v-if="tab === 'featured'"> <MkPagination v-slot="{items}" :pagination="featuredPagesPagination"> - <MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/> + <div class="_gaps"> + <MkPagePreview v-for="page in items" :key="page.id" :page="page"/> + </div> </MkPagination> </div> - <div v-else-if="tab === 'my'" class="rknalgpo my"> + <div v-else-if="tab === 'my'" class="_gaps"> <MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton> <MkPagination v-slot="{items}" :pagination="myPagesPagination"> - <MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/> + <div class="_gaps"> + <MkPagePreview v-for="page in items" :key="page.id" :page="page"/> + </div> </MkPagination> </div> - <div v-else-if="tab === 'liked'" class="rknalgpo"> + <div v-else-if="tab === 'liked'"> <MkPagination v-slot="{items}" :pagination="likedPagesPagination"> - <MkPagePreview v-for="like in items" :key="like.page.id" class="ckltabjg" :page="like.page"/> + <div class="_gaps"> + <MkPagePreview v-for="like in items" :key="like.page.id" :page="like.page"/> + </div> </MkPagination> </div> </MkSpacer> @@ -79,21 +85,3 @@ definePageMetadata(computed(() => ({ icon: 'ti ti-note', }))); </script> - -<style lang="scss" scoped> -.rknalgpo { - &.my .ckltabjg:first-child { - margin-top: 16px; - } - - .ckltabjg:not(:last-child) { - margin-bottom: 8px; - } - - @media (min-width: 500px) { - .ckltabjg:not(:last-child) { - margin-bottom: 16px; - } - } -} -</style> diff --git a/packages/frontend/src/pages/preview.vue b/packages/frontend/src/pages/preview.vue deleted file mode 100644 index 354f686e4..000000000 --- a/packages/frontend/src/pages/preview.vue +++ /dev/null @@ -1,27 +0,0 @@ -<template> -<div class="graojtoi"> - <MkSample/> -</div> -</template> - -<script lang="ts" setup> -import { computed } from 'vue'; -import MkSample from '@/components/MkSample.vue'; -import { i18n } from '@/i18n'; -import { definePageMetadata } from '@/scripts/page-metadata'; - -const headerActions = $computed(() => []); - -const headerTabs = $computed(() => []); - -definePageMetadata(computed(() => ({ - title: i18n.ts.preview, - icon: 'ti ti-eye', -}))); -</script> - -<style lang="scss" scoped> -.graojtoi { - padding: var(--margin); -} -</style> diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue index c687b89ea..b1d41fe2c 100644 --- a/packages/frontend/src/pages/registry.keys.vue +++ b/packages/frontend/src/pages/registry.keys.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="600" :margin-min="16"> + <MkSpacer :contentMax="600" :marginMin="16"> <div class="_gaps_m"> <FormSplit> <MkKeyValue> @@ -93,6 +93,3 @@ definePageMetadata({ icon: 'ti ti-adjustments', }); </script> - -<style lang="scss" scoped> -</style> diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue index 00e2ca5e0..513a2f8fe 100644 --- a/packages/frontend/src/pages/registry.value.vue +++ b/packages/frontend/src/pages/registry.value.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="600" :margin-min="16"> + <MkSpacer :contentMax="600" :marginMin="16"> <div class="_gaps_m"> <FormInfo warn>{{ i18n.ts.editTheseSettingsMayBreakAccount }}</FormInfo> @@ -118,6 +118,3 @@ definePageMetadata({ icon: 'ti ti-adjustments', }); </script> - -<style lang="scss" scoped> -</style> diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue index 5a029cb0c..6bfb9bce5 100644 --- a/packages/frontend/src/pages/registry.vue +++ b/packages/frontend/src/pages/registry.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="600" :margin-min="16"> + <MkSpacer :contentMax="600" :marginMin="16"> <MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton> <FormSection v-if="scopes"> @@ -68,6 +68,3 @@ definePageMetadata({ icon: 'ti ti-adjustments', }); </script> - -<style lang="scss" scoped> -</style> diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue index 38c88cc65..9d5730731 100644 --- a/packages/frontend/src/pages/reset-password.vue +++ b/packages/frontend/src/pages/reset-password.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer v-if="token" :content-max="700" :margin-min="16" :margin-max="32"> + <MkSpacer v-if="token" :contentMax="700" :marginMin="16" :marginMax="32"> <div class="_gaps_m"> <MkInput v-model="password" type="password"> <template #prefix><i class="ti ti-lock"></i></template> @@ -53,7 +53,3 @@ definePageMetadata({ icon: 'ti ti-lock', }); </script> - -<style lang="scss" scoped> - -</style> diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue index fe39c594b..e85ab0917 100644 --- a/packages/frontend/src/pages/role.vue +++ b/packages/frontend/src/pages/role.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs"/></template> - <MKSpacer v-if="!(typeof error === 'undefined')" :content-max="1200"> + <MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200"> <div :class="$style.root"> <img :class="$style.img" src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> <p :class="$style.text"> @@ -10,17 +10,18 @@ </p> </div> </MKSpacer> - <MkSpacer v-else-if="tab === 'users'" :content-max="1200"> + <MkSpacer v-else-if="tab === 'users'" :contentMax="1200"> <div class="_gaps_s"> <div v-if="role">{{ role.description }}</div> <MkUserList :pagination="users" :extractor="(item) => item.user"/> </div> </MkSpacer> - <MkSpacer v-else-if="tab === 'timeline'" :content-max="700"> + <MkSpacer v-else-if="tab === 'timeline'" :contentMax="700"> <MkTimeline ref="timeline" src="role" :role="props.role"/> </MkSpacer> </MkStickyContainer> </template> + <script lang="ts" setup> import { computed, watch } from 'vue'; import * as os from '@/os'; @@ -80,6 +81,7 @@ definePageMetadata(computed(() => ({ icon: 'ti ti-badge', }))); </script> + <style lang="scss" module> .root { padding: 32px; diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue index fb78546cb..22eb00dad 100644 --- a/packages/frontend/src/pages/scratchpad.vue +++ b/packages/frontend/src/pages/scratchpad.vue @@ -1,8 +1,8 @@ <template> -<MkSpacer :content-max="800"> +<MkSpacer :contentMax="800"> <div :class="$style.root"> <div :class="$style.editor" class="_panel"> - <PrismEditor v-model="code" class="_code code" :highlight="highlighter" :line-numbers="false"/> + <PrismEditor v-model="code" class="_code code" :highlight="highlighter" :lineNumbers="false"/> <MkButton style="position: absolute; top: 8px; right: 8px;" primary @click="run()"><i class="ti ti-player-play"></i></MkButton> </div> diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue index 23a8978fd..bd1389ffe 100644 --- a/packages/frontend/src/pages/search.user.vue +++ b/packages/frontend/src/pages/search.user.vue @@ -4,7 +4,7 @@ <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search"> <template #prefix><i class="ti ti-search"></i></template> </MkInput> - <MkRadios v-model="searchOrigin" @update:model-value="search()"> + <MkRadios v-model="searchOrigin" @update:modelValue="search()"> <option value="combined">{{ i18n.ts.all }}</option> <option value="local">{{ i18n.ts.local }}</option> <option value="remote">{{ i18n.ts.remote }}</option> diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue index 9f3d8da56..dcaf42e64 100644 --- a/packages/frontend/src/pages/search.vue +++ b/packages/frontend/src/pages/search.vue @@ -2,7 +2,7 @@ <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer v-if="tab === 'note'" :content-max="800"> + <MkSpacer v-if="tab === 'note'" :contentMax="800"> <div v-if="notesSearchAvailable"> <XNote/> </div> @@ -11,7 +11,7 @@ </div> </MkSpacer> - <MkSpacer v-else-if="tab === 'user'" :content-max="800"> + <MkSpacer v-else-if="tab === 'user'" :contentMax="800"> <XUser/> </MkSpacer> </MkStickyContainer> diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue index 1d836db5f..6a798b562 100644 --- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue +++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue @@ -1,8 +1,8 @@ <template> <MkModal ref="dialogEl" - :prefer-type="'dialog'" - :z-priority="'low'" + :preferType="'dialog'" + :zPriority="'low'" @click="cancel" @close="cancel" @closed="emit('closed')" diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue index 891934d70..aff7765ed 100644 --- a/packages/frontend/src/pages/settings/2fa.vue +++ b/packages/frontend/src/pages/settings/2fa.vue @@ -51,7 +51,7 @@ </div> </MkFolder> - <MkSwitch :disabled="!$i.twoFactorEnabled || $i.securityKeysList.length === 0" :model-value="usePasswordLessLogin" @update:model-value="v => updatePasswordLessLogin(v)"> + <MkSwitch :disabled="!$i.twoFactorEnabled || $i.securityKeysList.length === 0" :modelValue="usePasswordLessLogin" @update:modelValue="v => updatePasswordLessLogin(v)"> <template #label>{{ i18n.ts.passwordLessLogin }}</template> <template #caption>{{ i18n.ts.passwordLessLoginDescription }}</template> </MkSwitch> diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue index a58e74fe6..78479be97 100644 --- a/packages/frontend/src/pages/settings/accounts.vue +++ b/packages/frontend/src/pages/settings/accounts.vue @@ -15,13 +15,13 @@ <script lang="ts" setup> import { defineAsyncComponent, ref } from 'vue'; +import type * as Misskey from 'misskey-js'; import FormSuspense from '@/components/form/suspense.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os'; import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; -import type * as Misskey from 'misskey-js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; const storedAccounts = ref<any>(null); diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue index 599d6329e..fbb78200d 100644 --- a/packages/frontend/src/pages/settings/apps.vue +++ b/packages/frontend/src/pages/settings/apps.vue @@ -9,11 +9,11 @@ </template> <template #default="{items}"> <div class="_gaps"> - <div v-for="token in items" :key="token.id" class="_panel bfomjevm"> - <img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/> - <div class="body"> - <div class="name">{{ token.name }}</div> - <div class="description">{{ token.description }}</div> + <div v-for="token in items" :key="token.id" class="_panel" :class="$style.app"> + <img v-if="token.iconUrl" :class="$style.appIcon" :src="token.iconUrl" alt=""/> + <div :class="$style.appBody"> + <div :class="$style.appName">{{ token.name }}</div> + <div>{{ token.description }}</div> <MkKeyValue oneline> <template #key>{{ i18n.ts.installedDate }}</template> <template #value><MkTime :time="token.createdAt"/></template> @@ -28,7 +28,7 @@ <li v-for="p in token.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li> </ul> </details> - <div class="actions"> + <div> <MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton> </div> </div> @@ -75,27 +75,27 @@ definePageMetadata({ }); </script> -<style lang="scss" scoped> -.bfomjevm { +<style lang="scss" module> +.app { display: flex; padding: 16px; +} - > .icon { - display: block; - flex-shrink: 0; - margin: 0 12px 0 0; - width: 50px; - height: 50px; - border-radius: 8px; - } +.appIcon { + display: block; + flex-shrink: 0; + margin: 0 12px 0 0; + width: 50px; + height: 50px; + border-radius: 8px; +} - > .body { - width: calc(100% - 62px); - position: relative; +.appBody { + width: calc(100% - 62px); + position: relative; +} - > .name { - font-weight: bold; - } - } +.appName { + font-weight: bold; } </style> diff --git a/packages/frontend/src/pages/settings/custom-css.vue b/packages/frontend/src/pages/settings/custom-css.vue index 456c3742c..970d5689b 100644 --- a/packages/frontend/src/pages/settings/custom-css.vue +++ b/packages/frontend/src/pages/settings/custom-css.vue @@ -2,7 +2,7 @@ <div class="_gaps_m"> <FormInfo warn>{{ i18n.ts.customCssWarn }}</FormInfo> - <MkTextarea v-model="localCustomCss" manual-save tall class="_monospace" style="tab-size: 2;"> + <MkTextarea v-model="localCustomCss" manualSave tall class="_monospace" style="tab-size: 2;"> <template #label>CSS</template> </MkTextarea> </div> diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index 73c2b2e60..8d7b30dc6 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -4,8 +4,8 @@ <template #label>{{ i18n.ts.usageAmount }}</template> <div class="_gaps_m"> - <div class="uawsfosz"> - <div class="meter"><div :style="meterStyle"></div></div> + <div> + <div :class="$style.meter"><div :class="$style.meterValue" :style="meterStyle"></div></div> </div> <FormSplit> <MkKeyValue> @@ -22,7 +22,7 @@ <FormSection> <template #label>{{ i18n.ts.statistics }}</template> - <MkChart src="per-user-drive" :args="{ user: $i }" span="day" :limit="7 * 5" :bar="true" :stacked="true" :detailed="false" :aspect-ratio="6"/> + <MkChart src="per-user-drive" :args="{ user: $i }" span="day" :limit="7 * 5" :bar="true" :stacked="true" :detailed="false" :aspectRatio="6"/> </FormSection> <FormSection> @@ -39,10 +39,10 @@ <template #label>{{ i18n.ts.keepOriginalUploading }}</template> <template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template> </MkSwitch> - <MkSwitch v-model="alwaysMarkNsfw" @update:model-value="saveProfile()"> + <MkSwitch v-model="alwaysMarkNsfw" @update:modelValue="saveProfile()"> <template #label>{{ i18n.ts.alwaysMarkSensitive }}</template> </MkSwitch> - <MkSwitch v-model="autoSensitive" @update:model-value="saveProfile()"> + <MkSwitch v-model="autoSensitive" @update:modelValue="saveProfile()"> <template #label>{{ i18n.ts.enableAutoSensitive }}<span class="_beta">{{ i18n.ts.beta }}</span></template> <template #caption>{{ i18n.ts.enableAutoSensitiveDescription }}</template> </MkSwitch> @@ -139,22 +139,16 @@ definePageMetadata({ }); </script> -<style lang="scss" scoped> +<style lang="scss" module> +.meter { + height: 10px; + background: rgba(0, 0, 0, 0.1); + border-radius: 999px; + overflow: clip; +} -@use "sass:math"; - -.uawsfosz { - - > .meter { - $size: 12px; - background: rgba(0, 0, 0, 0.1); - border-radius: math.div($size, 2); - overflow: hidden; - - > div { - height: $size; - border-radius: math.div($size, 2); - } - } +.meterValue { + height: 100%; + border-radius: 999px; } </style> diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue index b1e6f223b..d015cec15 100644 --- a/packages/frontend/src/pages/settings/email.vue +++ b/packages/frontend/src/pages/settings/email.vue @@ -2,7 +2,7 @@ <div v-if="instance.enableEmail" class="_gaps_m"> <FormSection first> <template #label>{{ i18n.ts.emailAddress }}</template> - <MkInput v-model="emailAddress" type="email" manual-save> + <MkInput v-model="emailAddress" type="email" manualSave> <template #prefix><i class="ti ti-mail"></i></template> <template v-if="$i.email && !$i.emailVerified" #caption>{{ i18n.ts.verificationEmailSent }}</template> <template v-else-if="emailAddress === $i.email && $i.emailVerified" #caption><i class="ti ti-check" style="color: var(--success);"></i> {{ i18n.ts.emailVerified }}</template> @@ -10,7 +10,7 @@ </FormSection> <FormSection> - <MkSwitch :model-value="$i.receiveAnnouncementEmail" @update:model-value="onChangeReceiveAnnouncementEmail"> + <MkSwitch :modelValue="$i.receiveAnnouncementEmail" @update:modelValue="onChangeReceiveAnnouncementEmail"> {{ i18n.ts.receiveAnnouncementFromInstance }} </MkSwitch> </FormSection> diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index ba0f3274f..20b36f0fc 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -24,6 +24,7 @@ <div class="_gaps_s"> <MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch> <MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch> + <MkSwitch v-model="showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch> </div> </FormSection> @@ -56,7 +57,7 @@ <option value="ignore">{{ i18n.ts._nsfw.ignore }}</option> <option value="force">{{ i18n.ts._nsfw.force }}</option> </MkSelect> - <!-- + <MkRadios v-model="mediaListWithOneImageAppearance"> <template #label>{{ i18n.ts.mediaListWithOneImageAppearance }}</template> <option value="expand">{{ i18n.ts.default }}</option> @@ -64,7 +65,6 @@ <option value="1_1">{{ i18n.t('limitTo', { x: '1:1' }) }}</option> <option value="2_3">{{ i18n.t('limitTo', { x: '2:3' }) }}</option> </MkRadios> - --> </div> </FormSection> @@ -145,12 +145,20 @@ </FormSection> <FormSection> - <MkSwitch v-model="aiChanMode">{{ i18n.ts.aiChanMode }}</MkSwitch> + <template #label>{{ i18n.ts.other }}</template> + + <div class="_gaps"> + <MkFolder> + <template #label>{{ i18n.ts.additionalEmojiDictionary }}</template> + <div v-for="lang in emojiIndexLangs" class="_buttons"> + <MkButton @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ lang }}{{ defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton> + <MkButton v-if="defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton> + </div> + </MkFolder> + <FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink> + <FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink> + </div> </FormSection> - - <FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink> - - <FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink> </div> </template> @@ -160,6 +168,8 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkRange from '@/components/MkRange.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import MkButton from '@/components/MkButton.vue'; import FormSection from '@/components/form/section.vue'; import FormLink from '@/components/form/link.vue'; import MkLink from '@/components/MkLink.vue'; @@ -212,10 +222,10 @@ const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker')) const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll')); const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu')); const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars')); -const aiChanMode = computed(defaultStore.makeGetterSetter('aiChanMode')); const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('mediaListWithOneImageAppearance')); const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition')); const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis')); +const showTimelineReplies = computed(defaultStore.makeGetterSetter('showTimelineReplies')); watch(lang, () => { miLocalStorage.setItem('lang', lang.value as string); @@ -244,7 +254,6 @@ watch([ useSystemFont, enableInfiniteScroll, squareAvatars, - aiChanMode, showNoteActionsOnlyHover, showGapBetweenNotesInTimeline, instanceTicker, @@ -253,6 +262,34 @@ watch([ await reloadAsk(); }); +const emojiIndexLangs = ['en-US']; + +function downloadEmojiIndex(lang: string) { + async function main() { + const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes; + function download() { + switch (lang) { + case 'en-US': return import('../../unicode-emoji-indexes/en-US.json').then(x => x.default); + default: throw new Error('unrecognized lang: ' + lang); + } + } + currentIndexes[lang] = await download(); + await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes); + } + + os.promiseDialog(main()); +} + +function removeEmojiIndex(lang: string) { + async function main() { + const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes; + delete currentIndexes[lang]; + await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes); + } + + os.promiseDialog(main()); +} + const headerActions = $computed(() => []); const headerTabs = $computed(() => []); diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index 34a962ef4..b4f056d8a 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="900" :margin-min="20" :margin-max="32"> + <MkSpacer :contentMax="900" :marginMin="20" :marginMax="32"> <div ref="el" class="vvcocwet" :class="{ wide: !narrow }"> <div class="body"> <div v-if="!narrow || currentPage?.route.name == null" class="nav"> diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue index 541992875..102bc6852 100644 --- a/packages/frontend/src/pages/settings/migration.vue +++ b/packages/frontend/src/pages/settings/migration.vue @@ -3,7 +3,7 @@ <FormInfo warn> {{ i18n.ts.thisIsExperimentalFeature }} </FormInfo> - <MkFolder :default-open="true"> + <MkFolder :defaultOpen="true"> <template #icon><i class="ti ti-plane-arrival"></i></template> <template #label>{{ i18n.ts._accountMigration.moveFrom }}</template> <template #caption>{{ i18n.ts._accountMigration.moveFromSub }}</template> @@ -25,7 +25,7 @@ </div> </MkFolder> - <MkFolder :default-open="!!$i?.movedTo"> + <MkFolder :defaultOpen="!!$i?.movedTo"> <template #icon><i class="ti ti-plane-departure"></i></template> <template #label>{{ i18n.ts._accountMigration.moveTo }}</template> @@ -48,7 +48,7 @@ <FormInfo>{{ i18n.ts._accountMigration.postMigrationNote }}</FormInfo> <FormInfo warn>{{ i18n.ts._accountMigration.movedAndCannotBeUndone }}</FormInfo> <div>{{ i18n.ts._accountMigration.movedTo }}</div> - <MkUserInfo v-if="movedTo" :user="movedTo" class="_panel _shadow" /> + <MkUserInfo v-if="movedTo" :user="movedTo" class="_panel _shadow"/> </template> </div> </MkFolder> @@ -57,6 +57,8 @@ <script lang="ts" setup> import { ref } from 'vue'; +import { toString } from 'misskey-js/built/acct'; +import { UserDetailed } from 'misskey-js/built/entities'; import FormInfo from '@/components/MkInfo.vue'; import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; @@ -66,8 +68,6 @@ import * as os from '@/os'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import { $i } from '@/account'; -import { toString } from 'misskey-js/built/acct'; -import { UserDetailed } from 'misskey-js/built/entities'; import { unisonReload } from '@/scripts/unison-reload'; const moveToAccount = ref(''); diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue index b3b33b802..8780bfbc1 100644 --- a/packages/frontend/src/pages/settings/navbar.vue +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -2,10 +2,10 @@ <div class="_gaps_m"> <FormSlot> <template #label>{{ i18n.ts.navbar }}</template> - <MkContainer :show-header="false"> + <MkContainer :showHeader="false"> <Sortable v-model="items" - item-key="id" + itemKey="id" :animation="150" :handle="'.' + $style.itemHandle" @start="e => e.item.classList.add('active')" diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index 2cf2f6d7f..2552db419 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -12,7 +12,7 @@ <div class="_gaps_m"> <MkPushNotificationAllowButton ref="allowButton"/> - <MkSwitch :disabled="!pushRegistrationInServer" :model-value="sendReadMessage" @update:model-value="onChangeSendReadMessage"> + <MkSwitch :disabled="!pushRegistrationInServer" :modelValue="sendReadMessage" @update:modelValue="onChangeSendReadMessage"> <template #label>{{ i18n.ts.sendPushNotificationReadMessage }}</template> <template #caption> <I18n :src="i18n.ts.sendPushNotificationReadMessageCaption"> diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index 776305d72..0b73780a8 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -53,6 +53,17 @@ </MkSwitch> </div> </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-code"></i></template> + <template #label>{{ i18n.ts.developer }}</template> + + <div class="_gaps_m"> + <MkSwitch v-model="devMode"> + <template #label>{{ i18n.ts.devMode }}</template> + </MkSwitch> + </div> + </MkFolder> </div> </FormSection> @@ -80,6 +91,7 @@ import FormSection from '@/components/form/section.vue'; const reportError = computed(defaultStore.makeGetterSetter('reportError')); const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct')); +const devMode = computed(defaultStore.makeGetterSetter('devMode')); function onChangeInjectFeaturedNote(v) { os.api('i/update', { diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue index 8b57dceef..75fae014f 100644 --- a/packages/frontend/src/pages/settings/plugin.vue +++ b/packages/frontend/src/pages/settings/plugin.vue @@ -8,7 +8,7 @@ <div v-for="plugin in plugins" :key="plugin.id" class="_panel _gaps_s" style="padding: 20px;"> <span style="display: flex;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span> - <MkSwitch :model-value="plugin.active" @update:model-value="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</MkSwitch> + <MkSwitch :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</MkSwitch> <MkKeyValue> <template #key>{{ i18n.ts.author }}</template> @@ -94,7 +94,3 @@ definePageMetadata({ icon: 'ti ti-plug', }); </script> - -<style lang="scss" scoped> - -</style> diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index 6613ce4c1..e34901cd1 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -32,7 +32,7 @@ </template> <script lang="ts" setup> -import { computed, onMounted, onUnmounted, useCssModule } from 'vue'; +import { computed, onMounted, onUnmounted } from 'vue'; import { v4 as uuid } from 'uuid'; import FormSection from '@/components/form/section.vue'; import MkButton from '@/components/MkButton.vue'; @@ -40,7 +40,7 @@ import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os'; import { ColdDeviceStorage, defaultStore } from '@/store'; import { unisonReload } from '@/scripts/unison-reload'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { $i } from '@/account'; import { i18n } from '@/i18n'; import { version, host } from '@/config'; @@ -48,8 +48,6 @@ import { definePageMetadata } from '@/scripts/page-metadata'; import { miLocalStorage } from '@/local-storage'; const { t, ts } = i18n; -useCssModule(); - const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ 'menu', 'visibility', @@ -125,7 +123,7 @@ type Profile = { }; }; -const connection = $i && stream.useChannel('main'); +const connection = $i && useStream().useChannel('main'); let profiles = $ref<Record<string, Profile> | null>(null); diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index a1af0ba80..7fd4d6d34 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -1,14 +1,14 @@ <template> <div class="_gaps_m"> - <MkSwitch v-model="isLocked" @update:model-value="save()">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch> - <MkSwitch v-if="isLocked" v-model="autoAcceptFollowed" @update:model-value="save()">{{ i18n.ts.autoAcceptFollowed }}</MkSwitch> + <MkSwitch v-model="isLocked" @update:modelValue="save()">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch> + <MkSwitch v-if="isLocked" v-model="autoAcceptFollowed" @update:modelValue="save()">{{ i18n.ts.autoAcceptFollowed }}</MkSwitch> - <MkSwitch v-model="publicReactions" @update:model-value="save()"> + <MkSwitch v-model="publicReactions" @update:modelValue="save()"> {{ i18n.ts.makeReactionsPublic }} <template #caption>{{ i18n.ts.makeReactionsPublicDescription }}</template> </MkSwitch> - <MkSelect v-model="ffVisibility" @update:model-value="save()"> + <MkSelect v-model="ffVisibility" @update:modelValue="save()"> <template #label>{{ i18n.ts.ffVisibility }}</template> <option value="public">{{ i18n.ts._ffVisibility.public }}</option> <option value="followers">{{ i18n.ts._ffVisibility.followers }}</option> @@ -16,26 +16,26 @@ <template #caption>{{ i18n.ts.ffVisibilityDescription }}</template> </MkSelect> - <MkSwitch v-model="hideOnlineStatus" @update:model-value="save()"> + <MkSwitch v-model="hideOnlineStatus" @update:modelValue="save()"> {{ i18n.ts.hideOnlineStatus }} <template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template> </MkSwitch> - <MkSwitch v-model="noCrawle" @update:model-value="save()"> + <MkSwitch v-model="noCrawle" @update:modelValue="save()"> {{ i18n.ts.noCrawle }} <template #caption>{{ i18n.ts.noCrawleDescription }}</template> </MkSwitch> - <MkSwitch v-model="preventAiLearning" @update:model-value="save()"> + <MkSwitch v-model="preventAiLearning" @update:modelValue="save()"> {{ i18n.ts.preventAiLearning }}<span class="_beta">{{ i18n.ts.beta }}</span> <template #caption>{{ i18n.ts.preventAiLearningDescription }}</template> </MkSwitch> - <MkSwitch v-model="isExplorable" @update:model-value="save()"> + <MkSwitch v-model="isExplorable" @update:modelValue="save()"> {{ i18n.ts.makeExplorable }} <template #caption>{{ i18n.ts.makeExplorableDescription }}</template> </MkSwitch> <FormSection> <div class="_gaps_m"> - <MkSwitch v-model="rememberNoteVisibility" @update:model-value="save()">{{ i18n.ts.rememberNoteVisibility }}</MkSwitch> + <MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">{{ i18n.ts.rememberNoteVisibility }}</MkSwitch> <MkFolder v-if="!rememberNoteVisibility"> <template #label>{{ i18n.ts.defaultNoteVisibility }}</template> <template v-if="defaultNoteVisibility === 'public'" #suffix>{{ i18n.ts._visibility.public }}</template> @@ -56,7 +56,7 @@ </div> </FormSection> - <MkSwitch v-model="keepCw" @update:model-value="save()">{{ i18n.ts.keepCw }}</MkSwitch> + <MkSwitch v-model="keepCw" @update:modelValue="save()">{{ i18n.ts.keepCw }}</MkSwitch> </div> </template> diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 6ffd68261..58217d047 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -1,28 +1,28 @@ <template> <div class="_gaps_m"> - <div class="llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }"> - <div class="avatar"> - <MkAvatar class="avatar" :user="$i" @click="changeAvatar"/> - <MkButton primary rounded class="avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton> + <div :class="$style.avatarAndBanner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }"> + <div :class="$style.avatarContainer"> + <MkAvatar :class="$style.avatar" :user="$i" @click="changeAvatar"/> + <MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton> </div> - <MkButton primary rounded class="bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton> + <MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton> </div> - <MkInput v-model="profile.name" :max="30" manual-save> + <MkInput v-model="profile.name" :max="30" manualSave> <template #label>{{ i18n.ts._profile.name }}</template> </MkInput> - <MkTextarea v-model="profile.description" :max="500" tall manual-save> + <MkTextarea v-model="profile.description" :max="500" tall manualSave> <template #label>{{ i18n.ts._profile.description }}</template> <template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template> </MkTextarea> - <MkInput v-model="profile.location" manual-save> + <MkInput v-model="profile.location" manualSave> <template #label>{{ i18n.ts.location }}</template> <template #prefix><i class="ti ti-map-pin"></i></template> </MkInput> - <MkInput v-model="profile.birthday" type="date" manual-save> + <MkInput v-model="profile.birthday" type="date" manualSave> <template #label>{{ i18n.ts.birthday }}</template> <template #prefix><i class="ti ti-cake"></i></template> </MkInput> @@ -48,7 +48,7 @@ <Sortable v-model="fields" class="_gaps_s" - item-key="id" + itemKey="id" :animation="150" :handle="'.' + $style.dragItemHandle" @start="e => e.item.classList.add('active')" @@ -59,7 +59,7 @@ <button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ti ti-menu"></i></button> <button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(index)"><i class="ti ti-x"></i></button> <div :class="$style.dragItemForm"> - <FormSplit :min-width="200"> + <FormSplit :minWidth="200"> <MkInput v-model="element.name" small> <template #label>{{ i18n.ts._profile.metadataLabel }}</template> </MkInput> @@ -88,11 +88,11 @@ <MkSelect v-model="reactionAcceptance"> <template #label>{{ i18n.ts.reactionAcceptance }}</template> <option :value="null">{{ i18n.ts.all }}</option> - <option value="likeOnly">{{ i18n.ts.likeOnly }}</option> <option value="likeOnlyForRemote">{{ i18n.ts.likeOnlyForRemote }}</option> + <option value="nonSensitiveOnly">{{ i18n.ts.nonSensitiveOnly }}</option> + <option value="nonSensitiveOnlyForLocalLikeOnlyForRemote">{{ i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }}</option> + <option value="likeOnly">{{ i18n.ts.likeOnly }}</option> </MkSelect> - - <MkSwitch v-model="profile.showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch> </div> </template> @@ -248,36 +248,35 @@ definePageMetadata({ }); </script> -<style lang="scss" scoped> -.llvierxe { +<style lang="scss" module> +.avatarAndBanner { position: relative; background-size: cover; background-position: center; border: solid 1px var(--divider); border-radius: 10px; overflow: clip; - - > .avatar { - display: inline-block; - text-align: center; - padding: 16px; - - > .avatar { - display: inline-block; - width: 72px; - height: 72px; - margin: 0 auto 16px auto; - } - } - - > .bannerEdit { - position: absolute; - top: 16px; - right: 16px; - } } -</style> -<style lang="scss" module> + +.avatarContainer { + display: inline-block; + text-align: center; + padding: 16px; +} + +.avatar { + display: inline-block; + width: 72px; + height: 72px; + margin: 0 auto 16px auto; +} + +.bannerEdit { + position: absolute; + top: 16px; + right: 16px; +} + .metadataRoot { container-type: inline-size; } diff --git a/packages/frontend/src/pages/settings/reaction.vue b/packages/frontend/src/pages/settings/reaction.vue index ed913731d..cb483e34b 100644 --- a/packages/frontend/src/pages/settings/reaction.vue +++ b/packages/frontend/src/pages/settings/reaction.vue @@ -3,15 +3,15 @@ <FromSlot> <template #label>{{ i18n.ts.reactionSettingDescription }}</template> <div v-panel style="border-radius: 6px;"> - <Sortable v-model="reactions" class="zoaiodol" :item-key="item => item" :animation="150" :delay="100" :delay-on-touch-only="true"> + <Sortable v-model="reactions" :class="$style.reactions" :itemKey="item => item" :animation="150" :delay="100" :delayOnTouchOnly="true"> <template #item="{element}"> - <button class="_button item" @click="remove(element, $event)"> + <button class="_button" :class="$style.reactionsItem" @click="remove(element, $event)"> <MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/> <MkEmoji v-else :emoji="element" :normal="true"/> </button> </template> <template #footer> - <button class="_button add" @click="chooseEmoji"><i class="ti ti-plus"></i></button> + <button class="_button" :class="$style.reactionsAdd" @click="chooseEmoji"><i class="ti ti-plus"></i></button> </template> </Sortable> </div> @@ -135,20 +135,20 @@ definePageMetadata({ }); </script> -<style lang="scss" scoped> -.zoaiodol { +<style lang="scss" module> +.reactions { padding: 12px; font-size: 1.1em; +} - > .item { - display: inline-block; - padding: 8px; - cursor: move; - } +.reactionsItem { + display: inline-block; + padding: 8px; + cursor: move; +} - > .add { - display: inline-block; - padding: 8px; - } +.reactionsAdd { + display: inline-block; + padding: 8px; } </style> diff --git a/packages/frontend/src/pages/settings/roles.vue b/packages/frontend/src/pages/settings/roles.vue index ba510dced..05753c9b6 100644 --- a/packages/frontend/src/pages/settings/roles.vue +++ b/packages/frontend/src/pages/settings/roles.vue @@ -3,7 +3,7 @@ <FormSection first> <template #label>{{ i18n.ts.rolesAssignedToMe }}</template> <div class="_gaps_s"> - <MkRolePreview v-for="role in $i.roles" :key="role.id" :role="role" :for-moderation="false"/> + <MkRolePreview v-for="role in $i.roles" :key="role.id" :role="role" :forModeration="false"/> </div> </FormSection> <FormSection> diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue index 0cc2df09c..2da84763a 100644 --- a/packages/frontend/src/pages/settings/security.vue +++ b/packages/frontend/src/pages/settings/security.vue @@ -9,7 +9,7 @@ <FormSection> <template #label>{{ i18n.ts.signinHistory }}</template> - <MkPagination :pagination="pagination" disable-auto-load> + <MkPagination :pagination="pagination" disableAutoLoad> <template #default="{items}"> <div> <div v-for="item in items" :key="item.id" v-panel class="timnmucd"> diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue index aa9f52800..c1a333548 100644 --- a/packages/frontend/src/pages/settings/sounds.sound.vue +++ b/packages/frontend/src/pages/settings/sounds.sound.vue @@ -4,7 +4,7 @@ <template #label>{{ i18n.ts.sound }}</template> <option v-for="x in soundsTypes" :key="x" :value="x">{{ x == null ? i18n.ts.none : x }}</option> </MkSelect> - <MkRange v-model="volume" :min="0" :max="1" :step="0.05" :text-converter="(v) => `${Math.floor(v * 100)}%`"> + <MkRange v-model="volume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`"> <template #label>{{ i18n.ts.volume }}</template> </MkRange> diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue index 724fe4347..c2bf3f8cd 100644 --- a/packages/frontend/src/pages/settings/sounds.vue +++ b/packages/frontend/src/pages/settings/sounds.vue @@ -1,6 +1,6 @@ <template> <div class="_gaps_m"> - <MkRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :text-converter="(v) => `${Math.floor(v * 100)}%`"> + <MkRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`"> <template #label>{{ i18n.ts.masterVolume }}</template> </MkRange> diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue index 81ff873e9..c73ff7c07 100644 --- a/packages/frontend/src/pages/settings/statusbar.statusbar.vue +++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue @@ -7,7 +7,7 @@ <option value="userList">User list timeline</option> </MkSelect> - <MkInput v-model="statusbar.name" manual-save> + <MkInput v-model="statusbar.name" manualSave> <template #label>{{ i18n.ts.label }}</template> </MkInput> @@ -25,13 +25,13 @@ </MkRadios> <template v-if="statusbar.type === 'rss'"> - <MkInput v-model="statusbar.props.url" manual-save type="url"> + <MkInput v-model="statusbar.props.url" manualSave type="url"> <template #label>URL</template> </MkInput> <MkSwitch v-model="statusbar.props.shuffle"> <template #label>{{ i18n.ts.shuffle }}</template> </MkSwitch> - <MkInput v-model="statusbar.props.refreshIntervalSec" manual-save type="number"> + <MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number"> <template #label>{{ i18n.ts.refreshInterval }}</template> </MkInput> <MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1"> @@ -43,7 +43,7 @@ </MkSwitch> </template> <template v-else-if="statusbar.type === 'federation'"> - <MkInput v-model="statusbar.props.refreshIntervalSec" manual-save type="number"> + <MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number"> <template #label>{{ i18n.ts.refreshInterval }}</template> </MkInput> <MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1"> @@ -62,7 +62,7 @@ <template #label>{{ i18n.ts.userList }}</template> <option v-for="list in userLists" :value="list.id">{{ list.name }}</option> </MkSelect> - <MkInput v-model="statusbar.props.refreshIntervalSec" manual-save type="number"> + <MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number"> <template #label>{{ i18n.ts.refreshInterval }}</template> </MkInput> <MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1"> diff --git a/packages/frontend/src/pages/settings/statusbar.vue b/packages/frontend/src/pages/settings/statusbar.vue index f5a090a63..bfb69936e 100644 --- a/packages/frontend/src/pages/settings/statusbar.vue +++ b/packages/frontend/src/pages/settings/statusbar.vue @@ -3,7 +3,7 @@ <MkFolder v-for="x in statusbars" :key="x.id"> <template #label>{{ x.type ?? i18n.ts.notSet }}</template> <template #suffix>{{ x.name }}</template> - <XStatusbar :_id="x.id" :user-lists="userLists"/> + <XStatusbar :_id="x.id" :userLists="userLists"/> </MkFolder> <MkButton primary @click="add">{{ i18n.ts.add }}</MkButton> </div> diff --git a/packages/frontend/src/pages/settings/theme.manage.vue b/packages/frontend/src/pages/settings/theme.manage.vue index d1821a00d..025543511 100644 --- a/packages/frontend/src/pages/settings/theme.manage.vue +++ b/packages/frontend/src/pages/settings/theme.manage.vue @@ -10,13 +10,13 @@ </optgroup> </MkSelect> <template v-if="selectedTheme"> - <MkInput readonly :model-value="selectedTheme.author"> + <MkInput readonly :modelValue="selectedTheme.author"> <template #label>{{ i18n.ts.author }}</template> </MkInput> - <MkTextarea v-if="selectedTheme.desc" readonly :model-value="selectedTheme.desc"> + <MkTextarea v-if="selectedTheme.desc" readonly :modelValue="selectedTheme.desc"> <template #label>{{ i18n.ts._theme.description }}</template> </MkTextarea> - <MkTextarea readonly tall :model-value="selectedThemeCode"> + <MkTextarea readonly tall :modelValue="selectedThemeCode"> <template #label>{{ i18n.ts._theme.code }}</template> <template #caption><button class="_textButton" @click="copyThemeCode()">{{ i18n.ts.copy }}</button></template> </MkTextarea> diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue index 78e071016..e0ac89923 100644 --- a/packages/frontend/src/pages/share.vue +++ b/packages/frontend/src/pages/share.vue @@ -1,22 +1,25 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="800"> + <MkSpacer :contentMax="800"> <MkPostForm v-if="state === 'writing'" fixed :instant="true" - :initial-text="initialText" - :initial-visibility="visibility" - :initial-files="files" - :initial-local-only="localOnly" + :initialText="initialText" + :initialVisibility="visibility" + :initialFiles="files" + :initialLocalOnly="localOnly" :reply="reply" :renote="renote" - :initial-visible-users="visibleUsers" + :initialVisibleUsers="visibleUsers" class="_panel" @posted="state = 'posted'" /> - <MkButton v-else-if="state === 'posted'" primary class="close" @click="close()">{{ i18n.ts.close }}</MkButton> + <div v-else-if="state === 'posted'" class="_buttonsCenter"> + <MkButton primary @click="close">{{ i18n.ts.close }}</MkButton> + <MkButton @click="goToMisskey">{{ i18n.ts.goToMisskey }}</MkButton> + </div> </MkSpacer> </MkStickyContainer> </template> @@ -148,10 +151,14 @@ function close(): void { // 閉じなければ100ms後タイムラインに window.setTimeout(() => { - mainRouter.push('/'); + location.href = '/'; }, 100); } +function goToMisskey(): void { + location.href = '/'; +} + const headerActions = $computed(() => []); const headerTabs = $computed(() => []); @@ -161,9 +168,3 @@ definePageMetadata({ icon: 'ti ti-share', }); </script> - -<style lang="scss" scoped> -.close { - margin: 16px auto; -} -</style> diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue index 545953231..61d7eb24f 100644 --- a/packages/frontend/src/pages/signup-complete.vue +++ b/packages/frontend/src/pages/signup-complete.vue @@ -1,41 +1,80 @@ <template> <div> - {{ i18n.ts.processing }} + <MkAnimBg style="position: fixed; top: 0;"/> + <div :class="$style.formContainer"> + <form :class="$style.form" class="_panel" @submit.prevent="submit()"> + <div :class="$style.banner"> + <i class="ti ti-user-check"></i> + </div> + <div class="_gaps_m" style="padding: 32px;"> + <div>{{ i18n.t('clickToFinishEmailVerification', { ok: i18n.ts.gotIt }) }}</div> + <div> + <MkButton gradate large rounded type="submit" :disabled="submitting" data-cy-admin-ok style="margin: 0 auto;"> + {{ submitting ? i18n.ts.processing : i18n.ts.gotIt }}<MkEllipsis v-if="submitting"/> + </MkButton> + </div> + </div> + </form> + </div> </div> </template> <script lang="ts" setup> -import { onMounted } from 'vue'; -import * as os from '@/os'; +import { } from 'vue'; +import MkButton from '@/components/MkButton.vue'; +import MkAnimBg from '@/components/MkAnimBg.vue'; import { login } from '@/account'; import { i18n } from '@/i18n'; -import { definePageMetadata } from '@/scripts/page-metadata'; +import * as os from '@/os'; + +let submitting = $ref(false); const props = defineProps<{ code: string; }>(); -onMounted(async () => { - await os.alert({ - type: 'info', - text: i18n.t('clickToFinishEmailVerification', { ok: i18n.ts.gotIt }), - }); - const res = await os.apiWithDialog('signup-pending', { +function submit() { + if (submitting) return; + submitting = true; + + os.api('signup-pending', { code: props.code, + }).then(res => { + return login(res.i, '/'); + }).catch(() => { + submitting = false; + + os.alert({ + type: 'error', + text: i18n.ts.somethingHappened, + }); }); - login(res.i, '/'); -}); - -const headerActions = $computed(() => []); - -const headerTabs = $computed(() => []); - -definePageMetadata({ - title: i18n.ts.signup, - icon: 'ti ti-user', -}); +} </script> -<style lang="scss" scoped> +<style lang="scss" module> +.formContainer { + min-height: 100svh; + padding: 32px 32px 64px 32px; + box-sizing: border-box; +display: grid; +place-content: center; +} +.form { + position: relative; + z-index: 10; + border-radius: var(--radius); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); + overflow: clip; + max-width: 500px; +} + +.banner { + padding: 16px; + text-align: center; + font-size: 26px; + background-color: var(--accentedBg); + color: var(--accent); +} </style> diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue index 511052c42..104e73886 100644 --- a/packages/frontend/src/pages/tag.vue +++ b/packages/frontend/src/pages/tag.vue @@ -1,16 +1,28 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="800"> - <MkNotes class="" :pagination="pagination"/> + <MkSpacer :contentMax="800"> + <MkNotes ref="notes" class="" :pagination="pagination"/> </MkSpacer> + <template v-if="$i" #footer> + <div :class="$style.footer"> + <MkSpacer :contentMax="800" :marginMin="16" :marginMax="16"> + <MkButton rounded primary :class="$style.button" @click="post()"><i class="ti ti-pencil"></i>{{ i18n.ts.postToHashtag }}</MkButton> + </MkSpacer> + </div> + </template> </MkStickyContainer> </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, ref } from 'vue'; import MkNotes from '@/components/MkNotes.vue'; +import MkButton from '@/components/MkButton.vue'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { i18n } from '@/i18n'; +import { $i } from '@/account'; +import { defaultStore } from '@/store'; +import * as os from '@/os'; const props = defineProps<{ tag: string; @@ -23,6 +35,16 @@ const pagination = { tag: props.tag, })), }; +const notes = ref<InstanceType<typeof MkNotes>>(); + +async function post() { + defaultStore.set('postFormHashtags', props.tag); + defaultStore.set('postFormWithHashtags', true); + await os.post(); + defaultStore.set('postFormHashtags', ''); + defaultStore.set('postFormWithHashtags', false); + notes.value?.pagingComponent?.reload(); +} const headerActions = $computed(() => []); @@ -33,3 +55,16 @@ definePageMetadata(computed(() => ({ icon: 'ti ti-hash', }))); </script> + +<style lang="scss" module> +.footer { + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); + border-top: solid 0.5px var(--divider); + display: flex; +} + +.button { + margin: 0 auto var(--margin) auto; +} +</style> diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue index 56fdfdf78..f942b5005 100644 --- a/packages/frontend/src/pages/theme-editor.vue +++ b/packages/frontend/src/pages/theme-editor.vue @@ -1,9 +1,9 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="800" :margin-min="16" :margin-max="32"> + <MkSpacer :contentMax="800" :marginMin="16" :marginMax="32"> <div class="cwepdizn _gaps_m"> - <MkFolder :default-open="true"> + <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts.backgroundColor }}</template> <div class="cwepdizn-colors"> <div class="row"> @@ -19,7 +19,7 @@ </div> </MkFolder> - <MkFolder :default-open="true"> + <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts.accentColor }}</template> <div class="cwepdizn-colors"> <div class="row"> @@ -30,7 +30,7 @@ </div> </MkFolder> - <MkFolder :default-open="true"> + <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts.textColor }}</template> <div class="cwepdizn-colors"> <div class="row"> @@ -41,7 +41,7 @@ </div> </MkFolder> - <MkFolder :default-open="false"> + <MkFolder :defaultOpen="false"> <template #icon><i class="ti ti-code"></i></template> <template #label>{{ i18n.ts.editCode }}</template> @@ -53,7 +53,7 @@ </div> </MkFolder> - <MkFolder :default-open="false"> + <MkFolder :defaultOpen="false"> <template #label>{{ i18n.ts.addDescription }}</template> <div class="_gaps_m"> diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 1bf4cdc99..a441c6f72 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -1,12 +1,12 @@ <template> <MkStickyContainer> - <template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :display-my-avatar="true"/></template> - <MkSpacer :content-max="800"> + <template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template> + <MkSpacer :contentMax="800"> <div ref="rootEl" v-hotkey.global="keymap"> <XTutorial v-if="$i && defaultStore.reactiveState.timelineTutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/> <MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/> - <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> + <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> <div :class="$style.tl"> <MkTimeline ref="tlComponent" @@ -187,13 +187,13 @@ definePageMetadata(computed(() => ({ &:first-child { margin-top: calc(-0.675em - 8px - var(--margin)); } +} - > button { - display: block; - margin: var(--margin) auto 0 auto; - padding: 8px 16px; - border-radius: 32px; - } +.newButton { + display: block; + margin: var(--margin) auto 0 auto; + padding: 8px 16px; + border-radius: 32px; } .postForm { diff --git a/packages/frontend/src/pages/user-info.vue b/packages/frontend/src/pages/user-info.vue index 94718d153..56e8737e1 100644 --- a/packages/frontend/src/pages/user-info.vue +++ b/packages/frontend/src/pages/user-info.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="600" :margin-min="16" :margin-max="32"> + <MkSpacer :contentMax="600" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> <div v-if="tab === 'overview'" class="_gaps_m"> <div class="aeakzknw"> @@ -88,7 +88,7 @@ </div> <div v-else-if="tab === 'moderation'" class="_gaps_m"> - <MkSwitch v-model="suspended" @update:model-value="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch> + <MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch> <div> <MkButton v-if="user.host == null && iAmModerator" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton> @@ -112,7 +112,7 @@ <MkButton v-if="user.host == null && iAmModerator" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton> <div v-for="role in info.roles" :key="role.id" :class="$style.roleItem"> - <MkRolePreview :class="$style.role" :role="role" :for-moderation="true"/> + <MkRolePreview :class="$style.role" :role="role" :forModeration="true"/> <button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button> <button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button> </div> @@ -135,10 +135,10 @@ <MkFolder> <template #icon><i class="ti ti-cloud"></i></template> <template #label>{{ i18n.ts.files }}</template> - <MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/> + <MkFileListForAdmin :pagination="filesPagination" viewMode="grid"/> </MkFolder> - <MkTextarea v-model="moderationNote" manual-save> + <MkTextarea v-model="moderationNote" manualSave> <template #label>Moderation note</template> </MkTextarea> </div> diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index acf7ea9b2..f66670e1f 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -1,19 +1,20 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <div ref="rootEl" class="eqqrhokj"> - <div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> - <div class="tl"> - <MkTimeline - ref="tlEl" :key="listId" - class="tl" - src="list" - :list="listId" - :sound="true" - @queue="queueUpdated" - /> + <MkSpacer :contentMax="800"> + <div ref="rootEl"> + <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> + <div :class="$style.tl"> + <MkTimeline + ref="tlEl" :key="listId" + src="list" + :list="listId" + :sound="true" + @queue="queueUpdated" + /> + </div> </div> - </div> + </MkSpacer> </MkStickyContainer> </template> @@ -82,36 +83,29 @@ definePageMetadata(computed(() => list ? { } : null)); </script> -<style lang="scss" scoped> -.eqqrhokj { - padding: var(--margin); +<style lang="scss" module> +.new { + position: sticky; + top: calc(var(--stickyTop, 0px) + 16px); + z-index: 1000; + width: 100%; + margin: calc(-0.675em - 8px) 0; - > .new { - position: sticky; - top: calc(var(--stickyTop, 0px) + 16px); - z-index: 1000; - width: 100%; - margin: calc(-0.675em - 8px - var(--margin)) 0 calc(-0.675em - 8px); - - > button { - display: block; - margin: var(--margin) auto 0 auto; - padding: 8px 16px; - border-radius: 32px; - } - } - - > .tl { - background: var(--bg); - border-radius: var(--radius); - overflow: clip; + &:first-child { + margin-top: calc(-0.675em - 8px - var(--margin)); } } -@container (min-width: 800px) { - .eqqrhokj { - max-width: 800px; - margin: 0 auto; - } +.newButton { + display: block; + margin: var(--margin) auto 0 auto; + padding: 8px 16px; + border-radius: 32px; +} + +.tl { + background: var(--bg); + border-radius: var(--radius); + overflow: clip; } </style> diff --git a/packages/frontend/src/pages/user-tag.vue b/packages/frontend/src/pages/user-tag.vue index fac7593e9..01ef1126c 100644 --- a/packages/frontend/src/pages/user-tag.vue +++ b/packages/frontend/src/pages/user-tag.vue @@ -2,7 +2,7 @@ <MkStickyContainer> <template #header><MkPageHeader/></template> - <MkSpacer :content-max="1200"> + <MkSpacer :contentMax="1200"> <div class="_gaps_s"> <MkUserList :pagination="tagUsers"/> </div> diff --git a/packages/frontend/src/pages/user/achievements.vue b/packages/frontend/src/pages/user/achievements.vue index 1b3a6e24b..7d5993c26 100644 --- a/packages/frontend/src/pages/user/achievements.vue +++ b/packages/frontend/src/pages/user/achievements.vue @@ -1,6 +1,6 @@ <template> -<MkSpacer :content-max="1200"> - <MkAchievements :user="user" :with-locked="false" :with-description="$i != null && (props.user.id === $i.id)"/> +<MkSpacer :contentMax="1200"> + <MkAchievements :user="user" :withLocked="false" :withDescription="$i != null && (props.user.id === $i.id)"/> </MkSpacer> </template> diff --git a/packages/frontend/src/pages/user/activity.vue b/packages/frontend/src/pages/user/activity.vue index cd538ad61..655371ac1 100644 --- a/packages/frontend/src/pages/user/activity.vue +++ b/packages/frontend/src/pages/user/activity.vue @@ -1,5 +1,5 @@ <template> -<MkSpacer :content-max="700"> +<MkSpacer :contentMax="700"> <div class="_gaps"> <MkFoldableSection class="item"> <template #header><i class="ti ti-activity"></i> Heatmap</template> @@ -34,7 +34,3 @@ const props = defineProps<{ }>(); </script> - -<style lang="scss" scoped> - -</style> diff --git a/packages/frontend/src/pages/user/clips.vue b/packages/frontend/src/pages/user/clips.vue index 95f8cbc29..08b7b9a71 100644 --- a/packages/frontend/src/pages/user/clips.vue +++ b/packages/frontend/src/pages/user/clips.vue @@ -1,10 +1,10 @@ <template> -<MkSpacer :content-max="700"> - <div class="pages-user-clips"> - <MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="list"> - <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _margin"> +<MkSpacer :contentMax="700"> + <div> + <MkPagination v-slot="{items}" ref="list" :pagination="pagination"> + <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" :class="$style.item" class="_panel _margin"> <b>{{ item.name }}</b> - <div v-if="item.description" class="description">{{ item.description }}</div> + <div v-if="item.description" :class="$style.description">{{ item.description }}</div> </MkA> </MkPagination> </div> @@ -29,19 +29,15 @@ const pagination = { }; </script> -<style lang="scss" scoped> -.pages-user-clips { - > .list { - > .item { - display: block; - padding: 16px; +<style lang="scss" module> +.item { + display: block; + padding: 16px; +} - > .description { - margin-top: 8px; - padding-top: 8px; - border-top: solid 0.5px var(--divider); - } - } - } +.description { + margin-top: 8px; + padding-top: 8px; + border-top: solid 0.5px var(--divider); } </style> diff --git a/packages/frontend/src/pages/user/follow-list.vue b/packages/frontend/src/pages/user/follow-list.vue index d42acd838..4e76ddfe7 100644 --- a/packages/frontend/src/pages/user/follow-list.vue +++ b/packages/frontend/src/pages/user/follow-list.vue @@ -1,8 +1,8 @@ <template> <div> - <MkPagination v-slot="{items}" ref="list" :pagination="type === 'following' ? followingPagination : followersPagination" class="mk-following-or-followers"> - <div class="users"> - <MkUserInfo v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" class="user" :user="user"/> + <MkPagination v-slot="{items}" ref="list" :pagination="type === 'following' ? followingPagination : followersPagination"> + <div :class="$style.users"> + <MkUserInfo v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" :user="user"/> </div> </MkPagination> </div> @@ -36,12 +36,10 @@ const followersPagination = { }; </script> -<style lang="scss" scoped> -.mk-following-or-followers { - > .users { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); - grid-gap: var(--margin); - } +<style lang="scss" module> +.users { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + grid-gap: var(--margin); } </style> diff --git a/packages/frontend/src/pages/user/followers.vue b/packages/frontend/src/pages/user/followers.vue index 20573e67e..b330f7863 100644 --- a/packages/frontend/src/pages/user/followers.vue +++ b/packages/frontend/src/pages/user/followers.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="1000"> + <MkSpacer :contentMax="1000"> <Transition name="fade" mode="out-in"> <div v-if="user"> <XFollowList :user="user" type="followers"/> @@ -56,6 +56,3 @@ definePageMetadata(computed(() => user ? { avatar: user, } : null)); </script> - -<style lang="scss" scoped> -</style> diff --git a/packages/frontend/src/pages/user/following.vue b/packages/frontend/src/pages/user/following.vue index 3825f138c..9544cf76c 100644 --- a/packages/frontend/src/pages/user/following.vue +++ b/packages/frontend/src/pages/user/following.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="1000"> + <MkSpacer :contentMax="1000"> <Transition name="fade" mode="out-in"> <div v-if="user"> <XFollowList :user="user" type="following"/> @@ -56,6 +56,3 @@ definePageMetadata(computed(() => user ? { avatar: user, } : null)); </script> - -<style lang="scss" scoped> -</style> diff --git a/packages/frontend/src/pages/user/gallery.vue b/packages/frontend/src/pages/user/gallery.vue index b80e83fb1..b4bbab16f 100644 --- a/packages/frontend/src/pages/user/gallery.vue +++ b/packages/frontend/src/pages/user/gallery.vue @@ -1,7 +1,7 @@ <template> -<MkSpacer :content-max="700"> +<MkSpacer :contentMax="700"> <MkPagination v-slot="{items}" :pagination="pagination"> - <div class="jrnovfpt"> + <div :class="$style.root"> <MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> </div> </MkPagination> @@ -28,8 +28,8 @@ const pagination = { }; </script> -<style lang="scss" scoped> -.jrnovfpt { +<style lang="scss" module> +.root { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); grid-gap: 12px; diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 9c133346d..2e69eb367 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -1,5 +1,5 @@ <template> -<MkSpacer :content-max="narrow ? 800 : 1100"> +<MkSpacer :contentMax="narrow ? 800 : 1100"> <div ref="rootEl" class="ftskorzw" :class="{ wide: !narrow }" style="container-type: inline-size;"> <div class="main _gaps"> <!-- TODO --> @@ -7,7 +7,7 @@ <!-- <div class="punished" v-if="user.isSilenced"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> --> <div class="profile _gaps"> - <MkAccountMoved v-if="user.movedTo" :moved-to="user.movedTo"/> + <MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/> <MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/> <div :key="user.id" class="main _panel"> @@ -49,7 +49,7 @@ </span> </div> <div v-if="iAmModerator" class="moderationNote"> - <MkTextarea v-if="editModerationNote || (moderationNote != null && moderationNote !== '')" v-model="moderationNote" manual-save> + <MkTextarea v-if="editModerationNote || (moderationNote != null && moderationNote !== '')" v-model="moderationNote" manualSave> <template #label>Moderation note</template> </MkTextarea> <div v-else> @@ -69,7 +69,7 @@ </div> <div class="description"> <MkOmit> - <Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i"/> + <Mfm v-if="user.description" :text="user.description" :isNote="false" :author="user" :i="$i"/> <p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p> </MkOmit> </div> @@ -123,7 +123,7 @@ <XPhotos :key="user.id" :user="user"/> <XActivity :key="user.id" :user="user"/> </template> - <MkNotes v-if="!disableNotes" :class="$style.tl" :no-gap="true" :pagination="pagination"/> + <MkNotes v-if="!disableNotes" :class="$style.tl" :noGap="true" :pagination="pagination"/> </div> </div> <div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;"> diff --git a/packages/frontend/src/pages/user/index.activity.vue b/packages/frontend/src/pages/user/index.activity.vue index 2d9ee85bc..64d36307e 100644 --- a/packages/frontend/src/pages/user/index.activity.vue +++ b/packages/frontend/src/pages/user/index.activity.vue @@ -9,7 +9,7 @@ </template> <div style="padding: 8px;"> - <MkChart :src="chartSrc" :args="{ user, withoutAll: true }" span="day" :limit="limit" :bar="true" :stacked="true" :detailed="false" :aspect-ratio="5"/> + <MkChart :src="chartSrc" :args="{ user, withoutAll: true }" span="day" :limit="limit" :bar="true" :stacked="true" :detailed="false" :aspectRatio="5"/> </div> </MkContainer> </template> diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue index d8fc25391..91c580ce9 100644 --- a/packages/frontend/src/pages/user/index.timeline.vue +++ b/packages/frontend/src/pages/user/index.timeline.vue @@ -1,5 +1,5 @@ <template> -<MkSpacer :content-max="800" style="padding-top: 0"> +<MkSpacer :contentMax="800" style="padding-top: 0"> <MkStickyContainer> <template #header> <MkTab v-model="include" :class="$style.tab"> @@ -8,7 +8,7 @@ <option value="files">{{ i18n.ts.withFiles }}</option> </MkTab> </template> - <MkNotes :no-gap="true" :pagination="pagination" :class="$style.tl"/> + <MkNotes :noGap="true" :pagination="pagination" :class="$style.tl"/> </MkStickyContainer> </MkSpacer> </template> diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue index 03a226cc0..6aba815e9 100644 --- a/packages/frontend/src/pages/user/index.vue +++ b/packages/frontend/src/pages/user/index.vue @@ -2,20 +2,19 @@ <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <div> - <Transition name="fade" mode="out-in"> - <div v-if="user"> - <XHome v-if="tab === 'home'" :user="user"/> - <XTimeline v-else-if="tab === 'notes'" :user="user" /> - <XActivity v-else-if="tab === 'activity'" :user="user"/> - <XAchievements v-else-if="tab === 'achievements'" :user="user"/> - <XReactions v-else-if="tab === 'reactions'" :user="user"/> - <XClips v-else-if="tab === 'clips'" :user="user"/> - <XPages v-else-if="tab === 'pages'" :user="user"/> - <XGallery v-else-if="tab === 'gallery'" :user="user"/> - </div> - <MkError v-else-if="error" @retry="fetchUser()"/> - <MkLoading v-else/> - </Transition> + <div v-if="user"> + <XHome v-if="tab === 'home'" :user="user"/> + <XTimeline v-else-if="tab === 'notes'" :user="user"/> + <XActivity v-else-if="tab === 'activity'" :user="user"/> + <XAchievements v-else-if="tab === 'achievements'" :user="user"/> + <XReactions v-else-if="tab === 'reactions'" :user="user"/> + <XClips v-else-if="tab === 'clips'" :user="user"/> + <XLists v-else-if="tab === 'lists'" :user="user"/> + <XPages v-else-if="tab === 'pages'" :user="user"/> + <XGallery v-else-if="tab === 'gallery'" :user="user"/> + </div> + <MkError v-else-if="error" @retry="fetchUser()"/> + <MkLoading v-else/> </div> </MkStickyContainer> </template> @@ -36,6 +35,7 @@ const XActivity = defineAsyncComponent(() => import('./activity.vue')); const XAchievements = defineAsyncComponent(() => import('./achievements.vue')); const XReactions = defineAsyncComponent(() => import('./reactions.vue')); const XClips = defineAsyncComponent(() => import('./clips.vue')); +const XLists = defineAsyncComponent(() => import('./lists.vue')); const XPages = defineAsyncComponent(() => import('./pages.vue')); const XGallery = defineAsyncComponent(() => import('./gallery.vue')); @@ -90,6 +90,10 @@ const headerTabs = $computed(() => user ? [{ key: 'clips', title: i18n.ts.clips, icon: 'ti ti-paperclip', +}, { + key: 'lists', + title: i18n.ts.lists, + icon: 'ti ti-list', }, { key: 'pages', title: i18n.ts.pages, @@ -112,14 +116,3 @@ definePageMetadata(computed(() => user ? { }, } : null)); </script> - -<style lang="scss" scoped> -.fade-enter-active, -.fade-leave-active { - transition: opacity 0.125s ease; -} -.fade-enter-from, -.fade-leave-to { - opacity: 0; -} -</style> diff --git a/packages/frontend/src/pages/user/lists.vue b/packages/frontend/src/pages/user/lists.vue new file mode 100644 index 000000000..78f03d2b3 --- /dev/null +++ b/packages/frontend/src/pages/user/lists.vue @@ -0,0 +1,51 @@ +<template> +<MkStickyContainer> + <MkSpacer :contentMax="700"> + <div> + <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="lists"> + <MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/list/${ list.id }`"> + <div>{{ list.name }}</div> + <MkAvatars :userIds="list.userIds"/> + </MkA> + </MkPagination> + </div> + </MkSpacer> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import {} from 'vue'; +import * as misskey from 'misskey-js'; +import MkPagination from '@/components/MkPagination.vue'; +import MkStickyContainer from '@/components/global/MkStickyContainer.vue'; +import MkSpacer from '@/components/global/MkSpacer.vue'; +import MkAvatars from '@/components/MkAvatars.vue'; + +const props = defineProps<{ + user: misskey.entities.UserDetailed; +}>(); + +const pagination = { + endpoint: 'users/lists/list' as const, + noPaging: true, + limit: 10, + params: { + userId: props.user.id, + }, +}; +</script> + +<style lang="scss" module> +.list { + display: block; + padding: 16px; + border: solid 1px var(--divider); + border-radius: 6px; + margin-bottom: 8px; + + &:hover { + border: solid 1px var(--accent); + text-decoration: none; + } +} +</style> diff --git a/packages/frontend/src/pages/user/pages.vue b/packages/frontend/src/pages/user/pages.vue index 7ea1d75f4..a2975c707 100644 --- a/packages/frontend/src/pages/user/pages.vue +++ b/packages/frontend/src/pages/user/pages.vue @@ -1,5 +1,5 @@ <template> -<MkSpacer :content-max="700"> +<MkSpacer :contentMax="700"> <MkPagination v-slot="{items}" ref="list" :pagination="pagination"> <MkPagePreview v-for="page in items" :key="page.id" :page="page" class="_margin"/> </MkPagination> @@ -24,7 +24,3 @@ const pagination = { })), }; </script> - -<style lang="scss" scoped> - -</style> diff --git a/packages/frontend/src/pages/user/reactions.vue b/packages/frontend/src/pages/user/reactions.vue index 24129ec02..228160339 100644 --- a/packages/frontend/src/pages/user/reactions.vue +++ b/packages/frontend/src/pages/user/reactions.vue @@ -1,11 +1,11 @@ <template> -<MkSpacer :content-max="700"> +<MkSpacer :contentMax="700"> <MkPagination v-slot="{items}" ref="list" :pagination="pagination"> - <div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _margin afdcfbfb"> - <div class="header"> - <MkAvatar class="avatar" :user="user"/> - <MkReactionIcon class="reaction" :reaction="item.type" :no-style="true"/> - <MkTime :time="item.createdAt" class="createdAt"/> + <div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="_panel _margin"> + <div :class="$style.header"> + <MkAvatar :class="$style.avatar" :user="user"/> + <MkReactionIcon :class="$style.reaction" :reaction="item.type" :noStyle="true"/> + <MkTime :time="item.createdAt" :class="$style.createdAt"/> </div> <MkNote :key="item.id" :note="item.note"/> </div> @@ -33,29 +33,27 @@ const pagination = { }; </script> -<style lang="scss" scoped> -.afdcfbfb { - > .header { - display: flex; - align-items: center; - padding: 8px 16px; - margin-bottom: 8px; - border-bottom: solid 2px var(--divider); +<style lang="scss" module> +.header { + display: flex; + align-items: center; + padding: 8px 16px; + margin-bottom: 8px; + border-bottom: solid 2px var(--divider); +} - > .avatar { - width: 24px; - height: 24px; - margin-right: 8px; - } +.avatar { + width: 24px; + height: 24px; + margin-right: 8px; +} - > .reaction { - width: 32px; - height: 32px; - } +.reaction { + width: 32px; + height: 32px; +} - > .createdAt { - margin-left: auto; - } - } +.createdAt { + margin-left: auto; } </style> diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue index 929152bd5..f082b4b3c 100644 --- a/packages/frontend/src/pages/welcome.entrance.a.vue +++ b/packages/frontend/src/pages/welcome.entrance.a.vue @@ -6,11 +6,11 @@ <div class="shape2"></div> <img src="/client-assets/misskey.svg" class="misskey"/> <div class="emojis"> - <MkEmoji :normal="true" :no-style="true" emoji="👍"/> - <MkEmoji :normal="true" :no-style="true" emoji="❤"/> - <MkEmoji :normal="true" :no-style="true" emoji="😆"/> - <MkEmoji :normal="true" :no-style="true" emoji="🎉"/> - <MkEmoji :normal="true" :no-style="true" emoji="🍮"/> + <MkEmoji :normal="true" :noStyle="true" emoji="👍"/> + <MkEmoji :normal="true" :noStyle="true" emoji="❤"/> + <MkEmoji :normal="true" :noStyle="true" emoji="😆"/> + <MkEmoji :normal="true" :noStyle="true" emoji="🎉"/> + <MkEmoji :normal="true" :noStyle="true" emoji="🍮"/> </div> <div class="contents"> <MkVisitorDashboard/> diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue index 7728d97a6..2081cb9c9 100644 --- a/packages/frontend/src/pages/welcome.setup.vue +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -1,27 +1,32 @@ <template> -<form :class="$style.root" class="_panel" @submit.prevent="submit()"> - <div :class="$style.title"> - <div>Welcome to Misskey!</div> - <div :class="$style.version">v{{ version }}</div> +<div> + <MkAnimBg style="position: fixed; top: 0;"/> + <div :class="$style.formContainer"> + <form :class="$style.form" class="_panel" @submit.prevent="submit()"> + <div :class="$style.title"> + <div>Welcome to Misskey!</div> + <div :class="$style.version">v{{ version }}</div> + </div> + <div class="_gaps_m" style="padding: 32px;"> + <div>{{ i18n.ts.intro }}</div> + <MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-admin-username> + <template #label>{{ i18n.ts.username }}</template> + <template #prefix>@</template> + <template #suffix>@{{ host }}</template> + </MkInput> + <MkInput v-model="password" type="password" data-cy-admin-password> + <template #label>{{ i18n.ts.password }}</template> + <template #prefix><i class="ti ti-lock"></i></template> + </MkInput> + <div> + <MkButton gradate large rounded type="submit" :disabled="submitting" data-cy-admin-ok style="margin: 0 auto;"> + {{ submitting ? i18n.ts.processing : i18n.ts.done }}<MkEllipsis v-if="submitting"/> + </MkButton> + </div> + </div> + </form> </div> - <div class="_gaps_m" style="padding: 32px;"> - <div>{{ i18n.ts.intro }}</div> - <MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-admin-username> - <template #label>{{ i18n.ts.username }}</template> - <template #prefix>@</template> - <template #suffix>@{{ host }}</template> - </MkInput> - <MkInput v-model="password" type="password" data-cy-admin-password> - <template #label>{{ i18n.ts.password }}</template> - <template #prefix><i class="ti ti-lock"></i></template> - </MkInput> - <div> - <MkButton gradate large rounded type="submit" :disabled="submitting" data-cy-admin-ok style="margin: 0 auto;"> - {{ submitting ? i18n.ts.processing : i18n.ts.done }}<MkEllipsis v-if="submitting"/> - </MkButton> - </div> - </div> -</form> +</div> </template> <script lang="ts" setup> @@ -32,6 +37,7 @@ import { host, version } from '@/config'; import * as os from '@/os'; import { login } from '@/account'; import { i18n } from '@/i18n'; +import MkAnimBg from '@/components/MkAnimBg.vue'; let username = $ref(''); let password = $ref(''); @@ -58,12 +64,21 @@ function submit() { </script> <style lang="scss" module> -.root { +.formContainer { + min-height: 100svh; + padding: 32px 32px 64px 32px; + box-sizing: border-box; +display: grid; +place-content: center; +} + +.form { + position: relative; + z-index: 10; border-radius: var(--radius); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); - overflow: hidden; + overflow: clip; max-width: 500px; - margin: 32px auto; } .title { diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue index 6ec6e3f86..a93d103d4 100644 --- a/packages/frontend/src/pages/welcome.timeline.vue +++ b/packages/frontend/src/pages/welcome.timeline.vue @@ -3,16 +3,16 @@ <div ref="scrollEl" :class="[$style.scrollbox, { [$style.scroll]: isScrolling }]"> <div v-for="note in notes" :key="note.id" :class="$style.note"> <div class="_panel" :class="$style.content"> - <div :class="$style.body"> + <div> <MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i"/> <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> </div> <div v-if="note.files.length > 0" :class="$style.richcontent"> - <MkMediaList :media-list="note.files"/> + <MkMediaList :mediaList="note.files"/> </div> <div v-if="note.poll"> - <MkPoll :note="note" :read-only="true"/> + <MkPoll :note="note" :readOnly="true"/> </div> </div> <MkReactionsViewer ref="reactionsViewer" :note="note"/> diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts index 2616a8a1d..d97bd4be6 100644 --- a/packages/frontend/src/pizzax.ts +++ b/packages/frontend/src/pizzax.ts @@ -6,7 +6,7 @@ import { $i } from './account'; import { api } from './os'; import { get, set } from './scripts/idb-proxy'; import { defaultStore } from './store'; -import { stream } from './stream'; +import { useStream } from './stream'; import { deepClone } from './scripts/clone'; type StateDef = Record<string, { @@ -26,8 +26,6 @@ type PizzaxChannelMessage<T extends StateDef> = { userId?: string; }; -const connection = $i && stream.useChannel('main'); - export class Storage<T extends StateDef> { public readonly ready: Promise<void>; public readonly loaded: Promise<void>; @@ -105,8 +103,10 @@ export class Storage<T extends StateDef> { }); if ($i) { + const connection = useStream().useChannel('main'); + // streamingのuser storage updateイベントを監視して更新 - connection?.on('registryUpdated', ({ scope, key, value }: { scope?: string[], key: keyof T, value: T[typeof key]['default'] }) => { + connection.on('registryUpdated', ({ scope, key, value }: { scope?: string[], key: keyof T, value: T[typeof key]['default'] }) => { if (!scope || scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.state[key] === value) return; this.reactiveState[key].value = this.state[key] = value; diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index e46c1eeb7..6b11137d7 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -30,6 +30,10 @@ export const routes = [{ name: 'note', path: '/notes/:noteId', component: page(() => import('./pages/note.vue')), +}, { + name: 'list', + path: '/list/:listId', + component: page(() => import('./pages/list.vue')), }, { path: '/clips/:clipId', component: page(() => import('./pages/clip.vue')), @@ -242,9 +246,6 @@ export const routes = [{ }, { path: '/scratchpad', component: page(() => import('./pages/scratchpad.vue')), -}, { - path: '/preview', - component: page(() => import('./pages/preview.vue')), }, { path: '/auth/:token', component: page(() => import('./pages/auth.vue')), diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts index 2e853b58b..79661b7ce 100644 --- a/packages/frontend/src/scripts/emojilist.ts +++ b/packages/frontend/src/scripts/emojilist.ts @@ -2,7 +2,6 @@ export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', ' export type UnicodeEmojiDef = { name: string; - keywords: string[]; char: string; category: typeof unicodeEmojiCategories[number]; } @@ -10,11 +9,16 @@ export type UnicodeEmojiDef = { // initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb import _emojilist from '../emojilist.json'; -export const emojilist = _emojilist as UnicodeEmojiDef[]; +export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({ + name: x[1] as string, + char: x[0] as string, + category: unicodeEmojiCategories[x[2]], +})); const _indexByChar = new Map<string, number>(); const _charGroupByCategory = new Map<string, string[]>(); -emojilist.forEach((emo, i) => { +for (let i = 0; i < emojilist.length; i++) { + const emo = emojilist[i]; _indexByChar.set(emo.char, i); if (_charGroupByCategory.has(emo.category)) { @@ -22,14 +26,14 @@ emojilist.forEach((emo, i) => { } else { _charGroupByCategory.set(emo.category, [emo.char]); } -}); +} export const emojiCharByCategory = _charGroupByCategory; -export function getEmojiName(char: string): string | undefined { +export function getEmojiName(char: string): string | null { const idx = _indexByChar.get(char); - if (typeof idx === 'undefined') { - return undefined; + if (idx == null) { + return null; } else { return emojilist[idx].name; } diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index ed01b4905..060c8a1a1 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -73,7 +73,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile) { action: () => rename(file), }, { text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, - icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-off', + icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation', action: () => toggleSensitive(file), }, { text: i18n.ts.describeFile, diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index c8a610025..960f26ca6 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -7,7 +7,7 @@ import { instance } from '@/instance'; import * as os from '@/os'; import copyToClipboard from '@/scripts/copy-to-clipboard'; import { url } from '@/config'; -import { noteActions } from '@/store'; +import { defaultStore, noteActions } from '@/store'; import { miLocalStorage } from '@/local-storage'; import { getUserMenu } from '@/scripts/get-user-menu'; import { clipsCache } from '@/cache'; @@ -396,5 +396,15 @@ export function getNoteMenu(props: { }))]); } + if (defaultStore.state.devMode) { + menu = menu.concat([null, { + icon: 'ti ti-id', + text: i18n.ts.copyNoteId, + action: () => { + copyToClipboard(appearNote.id); + }, + }]); + } + return menu; } diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 6ff9fb63f..b055d2647 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -4,7 +4,7 @@ import { i18n } from '@/i18n'; import copyToClipboard from '@/scripts/copy-to-clipboard'; import { host } from '@/config'; import * as os from '@/os'; -import { userActions } from '@/store'; +import { defaultStore, userActions } from '@/store'; import { $i, iAmModerator } from '@/account'; import { mainRouter } from '@/router'; import { Router } from '@/nirax'; @@ -240,6 +240,16 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router }]); } + if (defaultStore.state.devMode) { + menu = menu.concat([null, { + icon: 'ti ti-id', + text: i18n.ts.copyUserId, + action: () => { + copyToClipboard(user.id); + }, + }]); + } + if ($i && meId === user.id) { menu = menu.concat([null, { icon: 'ti ti-pencil', diff --git a/packages/frontend/src/scripts/hpml/block.ts b/packages/frontend/src/scripts/hpml/block.ts deleted file mode 100644 index 804c5c112..000000000 --- a/packages/frontend/src/scripts/hpml/block.ts +++ /dev/null @@ -1,109 +0,0 @@ -// blocks - -export type BlockBase = { - id: string; - type: string; -}; - -export type TextBlock = BlockBase & { - type: 'text'; - text: string; -}; - -export type SectionBlock = BlockBase & { - type: 'section'; - title: string; - children: (Block | VarBlock)[]; -}; - -export type ImageBlock = BlockBase & { - type: 'image'; - fileId: string | null; -}; - -export type ButtonBlock = BlockBase & { - type: 'button'; - text: any; - primary: boolean; - action: string; - content: string; - event: string; - message: string; - var: string; - fn: string; -}; - -export type IfBlock = BlockBase & { - type: 'if'; - var: string; - children: Block[]; -}; - -export type TextareaBlock = BlockBase & { - type: 'textarea'; - text: string; -}; - -export type PostBlock = BlockBase & { - type: 'post'; - text: string; - attachCanvasImage: boolean; - canvasId: string; -}; - -export type CanvasBlock = BlockBase & { - type: 'canvas'; - name: string; // canvas id - width: number; - height: number; -}; - -export type NoteBlock = BlockBase & { - type: 'note'; - detailed: boolean; - note: string | null; -}; - -export type Block = - TextBlock | SectionBlock | ImageBlock | ButtonBlock | IfBlock | TextareaBlock | PostBlock | CanvasBlock | NoteBlock | VarBlock; - -// variable blocks - -export type VarBlockBase = BlockBase & { - name: string; -}; - -export type NumberInputVarBlock = VarBlockBase & { - type: 'numberInput'; - text: string; -}; - -export type TextInputVarBlock = VarBlockBase & { - type: 'textInput'; - text: string; -}; - -export type SwitchVarBlock = VarBlockBase & { - type: 'switch'; - text: string; -}; - -export type RadioButtonVarBlock = VarBlockBase & { - type: 'radioButton'; - title: string; - values: string[]; -}; - -export type CounterVarBlock = VarBlockBase & { - type: 'counter'; - text: string; - inc: number; -}; - -export type VarBlock = - NumberInputVarBlock | TextInputVarBlock | SwitchVarBlock | RadioButtonVarBlock | CounterVarBlock; - -const varBlock = ['numberInput', 'textInput', 'switch', 'radioButton', 'counter']; -export function isVarBlock(block: Block): block is VarBlock { - return varBlock.includes(block.type); -} diff --git a/packages/frontend/src/scripts/hpml/evaluator.ts b/packages/frontend/src/scripts/hpml/evaluator.ts deleted file mode 100644 index 9adfba7f2..000000000 --- a/packages/frontend/src/scripts/hpml/evaluator.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { ref, Ref, unref } from 'vue'; -import { collectPageVars } from '../collect-page-vars'; -import { initHpmlLib } from './lib'; -import { Expr, isLiteralValue, Variable } from './expr'; -import { PageVar, envVarsDef, Fn, HpmlScope, HpmlError } from '.'; -import { version } from '@/config'; - -/** - * Hpml evaluator - */ -export class Hpml { - private variables: Variable[]; - private pageVars: PageVar[]; - private envVars: Record<keyof typeof envVarsDef, any>; - public pageVarUpdatedCallback?: values.VFn; - public canvases: Record<string, HTMLCanvasElement> = {}; - public vars: Ref<Record<string, any>> = ref({}); - public page: Record<string, any>; - - private opts: { - randomSeed: string; visitor?: any; url?: string; - }; - - constructor(page: Hpml['page'], opts: Hpml['opts']) { - this.page = page; - this.variables = this.page.variables; - this.pageVars = collectPageVars(this.page.content); - this.opts = opts; - - const date = new Date(); - - this.envVars = { - AI: 'kawaii', - VERSION: version, - URL: this.page ? `${opts.url}/@${this.page.user.username}/pages/${this.page.name}` : '', - LOGIN: opts.visitor != null, - NAME: opts.visitor ? opts.visitor.name || opts.visitor.username : '', - USERNAME: opts.visitor ? opts.visitor.username : '', - USERID: opts.visitor ? opts.visitor.id : '', - NOTES_COUNT: opts.visitor ? opts.visitor.notesCount : 0, - FOLLOWERS_COUNT: opts.visitor ? opts.visitor.followersCount : 0, - FOLLOWING_COUNT: opts.visitor ? opts.visitor.followingCount : 0, - IS_CAT: opts.visitor ? opts.visitor.isCat : false, - SEED: opts.randomSeed ? opts.randomSeed : '', - YMD: `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`, - AISCRIPT_DISABLED: true, - NULL: null, - }; - - this.eval(); - } - - public eval() { - try { - this.vars.value = this.evaluateVars(); - } catch (err) { - //this.onError(e); - } - } - - public interpolate(str: string) { - if (str == null) return null; - return str.replace(/{(.+?)}/g, match => { - const v = unref(this.vars)[match.slice(1, -1).trim()]; - return v == null ? 'NULL' : v.toString(); - }); - } - - public registerCanvas(id: string, canvas: any) { - this.canvases[id] = canvas; - } - - public updatePageVar(name: string, value: any) { - const pageVar = this.pageVars.find(v => v.name === name); - if (pageVar !== undefined) { - pageVar.value = value; - } else { - throw new HpmlError(`No such page var '${name}'`); - } - } - - public updateRandomSeed(seed: string) { - this.opts.randomSeed = seed; - this.envVars.SEED = seed; - } - - private _interpolateScope(str: string, scope: HpmlScope) { - return str.replace(/{(.+?)}/g, match => { - const v = scope.getState(match.slice(1, -1).trim()); - return v == null ? 'NULL' : v.toString(); - }); - } - - public evaluateVars(): Record<string, any> { - const values: Record<string, any> = {}; - - for (const [k, v] of Object.entries(this.envVars)) { - values[k] = v; - } - - for (const v of this.pageVars) { - values[v.name] = v.value; - } - - for (const v of this.variables) { - values[v.name] = this.evaluate(v, new HpmlScope([values])); - } - - return values; - } - - private evaluate(expr: Expr, scope: HpmlScope): any { - if (isLiteralValue(expr)) { - if (expr.type === null) { - return null; - } - - if (expr.type === 'number') { - return parseInt((expr.value as any), 10); - } - - if (expr.type === 'text' || expr.type === 'multiLineText') { - return this._interpolateScope(expr.value || '', scope); - } - - if (expr.type === 'textList') { - return this._interpolateScope(expr.value || '', scope).trim().split('\n'); - } - - if (expr.type === 'ref') { - return scope.getState(expr.value); - } - - // Define user function - if (expr.type === 'fn') { - return { - slots: expr.value.slots.map(x => x.name), - exec: (slotArg: Record<string, any>) => { - return this.evaluate(expr.value.expression, scope.createChildScope(slotArg, expr.id)); - }, - } as Fn; - } - return; - } - - // Call user function - if (expr.type.startsWith('fn:')) { - const fnName = expr.type.split(':')[1]; - const fn = scope.getState(fnName); - const args = {} as Record<string, any>; - for (let i = 0; i < fn.slots.length; i++) { - const name = fn.slots[i]; - args[name] = this.evaluate(expr.args[i], scope); - } - return fn.exec(args); - } - - if (expr.args === undefined) return null; - - const funcs = initHpmlLib(expr, scope, this.opts.randomSeed, this.opts.visitor); - - // Call function - const fnName = expr.type; - const fn = (funcs as any)[fnName]; - if (fn == null) { - throw new HpmlError(`No such function '${fnName}'`); - } else { - return fn(...expr.args.map(x => this.evaluate(x, scope))); - } - } -} diff --git a/packages/frontend/src/scripts/hpml/expr.ts b/packages/frontend/src/scripts/hpml/expr.ts deleted file mode 100644 index 18c7c2a14..000000000 --- a/packages/frontend/src/scripts/hpml/expr.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { literalDefs, Type } from '.'; - -export type ExprBase = { - id: string; -}; - -// value - -export type EmptyValue = ExprBase & { - type: null; - value: null; -}; - -export type TextValue = ExprBase & { - type: 'text'; - value: string; -}; - -export type MultiLineTextValue = ExprBase & { - type: 'multiLineText'; - value: string; -}; - -export type TextListValue = ExprBase & { - type: 'textList'; - value: string; -}; - -export type NumberValue = ExprBase & { - type: 'number'; - value: number; -}; - -export type RefValue = ExprBase & { - type: 'ref'; - value: string; // value is variable name -}; - -export type AiScriptRefValue = ExprBase & { - type: 'aiScriptVar'; - value: string; // value is variable name -}; - -export type UserFnValue = ExprBase & { - type: 'fn'; - value: UserFnInnerValue; -}; -type UserFnInnerValue = { - slots: { - name: string; - type: Type; - }[]; - expression: Expr; -}; - -export type Value = - EmptyValue | TextValue | MultiLineTextValue | TextListValue | NumberValue | RefValue | AiScriptRefValue | UserFnValue; - -export function isLiteralValue(expr: Expr): expr is Value { - if (expr.type == null) return true; - if (literalDefs[expr.type]) return true; - return false; -} - -// call function - -export type CallFn = ExprBase & { // "fn:hoge" or string - type: string; - args: Expr[]; - value: null; -}; - -// variable -export type Variable = (Value | CallFn) & { - name: string; -}; - -// expression -export type Expr = Variable | Value | CallFn; diff --git a/packages/frontend/src/scripts/hpml/index.ts b/packages/frontend/src/scripts/hpml/index.ts deleted file mode 100644 index 994f286b9..000000000 --- a/packages/frontend/src/scripts/hpml/index.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Hpml - */ - -import { Hpml } from './evaluator'; -import { funcDefs } from './lib'; - -export type Fn = { - slots: string[]; - exec: (args: Record<string, any>) => ReturnType<Hpml['evaluate']>; -}; - -export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null; - -export const literalDefs: Record<string, { out: any; category: string; icon: any; }> = { - text: { out: 'string', category: 'value', icon: 'ti ti-quote' }, - multiLineText: { out: 'string', category: 'value', icon: 'ti ti-align-left' }, - textList: { out: 'stringArray', category: 'value', icon: 'ti ti-list' }, - number: { out: 'number', category: 'value', icon: 'ti ti-list-numbers' }, - ref: { out: null, category: 'value', icon: 'ti ti-wand' }, - aiScriptVar: { out: null, category: 'value', icon: 'ti ti-wand' }, - fn: { out: 'function', category: 'value', icon: 'ti ti-math-function' }, -}; - -export const blockDefs = [ - ...Object.entries(literalDefs).map(([k, v]) => ({ - type: k, out: v.out, category: v.category, icon: v.icon, - })), - ...Object.entries(funcDefs).map(([k, v]) => ({ - type: k, out: v.out, category: v.category, icon: v.icon, - })), -]; - -export type PageVar = { name: string; value: any; type: Type; }; - -export const envVarsDef: Record<string, Type> = { - AI: 'string', - URL: 'string', - VERSION: 'string', - LOGIN: 'boolean', - NAME: 'string', - USERNAME: 'string', - USERID: 'string', - NOTES_COUNT: 'number', - FOLLOWERS_COUNT: 'number', - FOLLOWING_COUNT: 'number', - IS_CAT: 'boolean', - SEED: null, - YMD: 'string', - AISCRIPT_DISABLED: 'boolean', - NULL: null, -}; - -export class HpmlScope { - private layerdStates: Record<string, any>[]; - public name: string; - - constructor(layerdStates: HpmlScope['layerdStates'], name?: HpmlScope['name']) { - this.layerdStates = layerdStates; - this.name = name ?? 'anonymous'; - } - - public createChildScope(states: Record<string, any>, name?: HpmlScope['name']): HpmlScope { - const layer = [states, ...this.layerdStates]; - return new HpmlScope(layer, name); - } - - /** - * 指定した名前の変数の値を取得します - * @param name 変数名 - */ - public getState(name: string): any { - for (const later of this.layerdStates) { - const state = later[name]; - if (state !== undefined) { - return state; - } - } - - throw new HpmlError( - `No such variable '${name}' in scope '${this.name}'`, { - scope: this.layerdStates, - }); - } -} - -export class HpmlError extends Error { - public info?: any; - - constructor(message: string, info?: any) { - super(message); - - this.info = info; - - // Maintains proper stack trace for where our error was thrown (only available on V8) - if (Error.captureStackTrace) { - Error.captureStackTrace(this, HpmlError); - } - } -} diff --git a/packages/frontend/src/scripts/hpml/lib.ts b/packages/frontend/src/scripts/hpml/lib.ts deleted file mode 100644 index 88db82dd2..000000000 --- a/packages/frontend/src/scripts/hpml/lib.ts +++ /dev/null @@ -1,245 +0,0 @@ -import seedrandom from 'seedrandom'; -import { Hpml } from './evaluator'; -import { Expr } from './expr'; -import { Fn, HpmlScope } from '.'; - -/* TODO: https://www.chartjs.org/docs/latest/configuration/canvas-background.html#color -// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs -Chart.pluginService.register({ - beforeDraw: (chart, easing) => { - if (chart.config.options.chartArea && chart.config.options.chartArea.backgroundColor) { - const ctx = chart.chart.ctx; - ctx.save(); - ctx.fillStyle = chart.config.options.chartArea.backgroundColor; - ctx.fillRect(0, 0, chart.chart.width, chart.chart.height); - ctx.restore(); - } - } -}); -*/ - -export function initAiLib(hpml: Hpml) { - return { - 'MkPages:updated': values.FN_NATIVE(([callback]) => { - hpml.pageVarUpdatedCallback = (callback as values.VFn); - }), - 'MkPages:get_canvas': values.FN_NATIVE(([id]) => { - utils.assertString(id); - const canvas = hpml.canvases[id.value]; - const ctx = canvas.getContext('2d'); - return values.OBJ(new Map([ - ['clear_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.clearRect(x.value, y.value, width.value, height.value); })], - ['fill_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.fillRect(x.value, y.value, width.value, height.value); })], - ['stroke_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.strokeRect(x.value, y.value, width.value, height.value); })], - ['fill_text', values.FN_NATIVE(([text, x, y, width]) => { ctx.fillText(text.value, x.value, y.value, width ? width.value : undefined); })], - ['stroke_text', values.FN_NATIVE(([text, x, y, width]) => { ctx.strokeText(text.value, x.value, y.value, width ? width.value : undefined); })], - ['set_line_width', values.FN_NATIVE(([width]) => { ctx.lineWidth = width.value; })], - ['set_font', values.FN_NATIVE(([font]) => { ctx.font = font.value; })], - ['set_fill_style', values.FN_NATIVE(([style]) => { ctx.fillStyle = style.value; })], - ['set_stroke_style', values.FN_NATIVE(([style]) => { ctx.strokeStyle = style.value; })], - ['begin_path', values.FN_NATIVE(() => { ctx.beginPath(); })], - ['close_path', values.FN_NATIVE(() => { ctx.closePath(); })], - ['move_to', values.FN_NATIVE(([x, y]) => { ctx.moveTo(x.value, y.value); })], - ['line_to', values.FN_NATIVE(([x, y]) => { ctx.lineTo(x.value, y.value); })], - ['arc', values.FN_NATIVE(([x, y, radius, startAngle, endAngle]) => { ctx.arc(x.value, y.value, radius.value, startAngle.value, endAngle.value); })], - ['rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.rect(x.value, y.value, width.value, height.value); })], - ['fill', values.FN_NATIVE(() => { ctx.fill(); })], - ['stroke', values.FN_NATIVE(() => { ctx.stroke(); })], - ])); - }), - 'MkPages:chart': values.FN_NATIVE(([id, opts]) => { - /* TODO - utils.assertString(id); - utils.assertObject(opts); - const canvas = hpml.canvases[id.value]; - const color = getComputedStyle(document.documentElement).getPropertyValue('--accent'); - Chart.defaults.color = '#555'; - const chart = new Chart(canvas, { - type: opts.value.get('type').value, - data: { - labels: opts.value.get('labels').value.map(x => x.value), - datasets: opts.value.get('datasets').value.map(x => ({ - label: x.value.has('label') ? x.value.get('label').value : '', - data: x.value.get('data').value.map(x => x.value), - pointRadius: 0, - lineTension: 0, - borderWidth: 2, - borderColor: x.value.has('color') ? x.value.get('color') : color, - backgroundColor: tinycolor(x.value.has('color') ? x.value.get('color') : color).setAlpha(0.1).toRgbString(), - })) - }, - options: { - responsive: false, - devicePixelRatio: 1.5, - title: { - display: opts.value.has('title'), - text: opts.value.has('title') ? opts.value.get('title').value : '', - fontSize: 14, - }, - layout: { - padding: { - left: 32, - right: 32, - top: opts.value.has('title') ? 16 : 32, - bottom: 16 - } - }, - legend: { - display: opts.value.get('datasets').value.filter(x => x.value.has('label') && x.value.get('label').value).length === 0 ? false : true, - position: 'bottom', - labels: { - boxWidth: 16, - } - }, - tooltips: { - enabled: false, - }, - chartArea: { - backgroundColor: '#fff' - }, - ...(opts.value.get('type').value === 'radar' ? { - scale: { - ticks: { - display: opts.value.has('show_tick_label') ? opts.value.get('show_tick_label').value : false, - min: opts.value.has('min') ? opts.value.get('min').value : undefined, - max: opts.value.has('max') ? opts.value.get('max').value : undefined, - maxTicksLimit: 8, - }, - pointLabels: { - fontSize: 12 - } - } - } : { - scales: { - yAxes: [{ - ticks: { - display: opts.value.has('show_tick_label') ? opts.value.get('show_tick_label').value : true, - min: opts.value.has('min') ? opts.value.get('min').value : undefined, - max: opts.value.has('max') ? opts.value.get('max').value : undefined, - } - }] - } - }) - } - }); - */ - }), - }; -} - -export const funcDefs: Record<string, { in: any[]; out: any; category: string; icon: any; }> = { - if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: 'ti ti-share' }, - for: { in: ['number', 'function'], out: null, category: 'flow', icon: 'ti ti-recycle' }, - not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: 'ti ti-flag' }, - or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'ti ti-flag' }, - and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'ti ti-flag' }, - add: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-plus' }, - subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-minus' }, - multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-x' }, - divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-divide' }, - mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-divide' }, - round: { in: ['number'], out: 'number', category: 'operation', icon: 'ti ti-calculator' }, - eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'ti ti-equal' }, - notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'ti ti-equal-not' }, - gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'ti ti-math-greater' }, - lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'ti ti-math-lower' }, - gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'ti ti-math-equal-greater' }, - ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'ti ti-math-equal-lower' }, - strLen: { in: ['string'], out: 'number', category: 'text', icon: 'ti ti-quote' }, - strPick: { in: ['string', 'number'], out: 'string', category: 'text', icon: 'ti ti-quote' }, - strReplace: { in: ['string', 'string', 'string'], out: 'string', category: 'text', icon: 'ti ti-quote' }, - strReverse: { in: ['string'], out: 'string', category: 'text', icon: 'ti ti-quote' }, - join: { in: ['stringArray', 'string'], out: 'string', category: 'text', icon: 'ti ti-quote' }, - stringToNumber: { in: ['string'], out: 'number', category: 'convert', icon: 'ti ti-arrows-right-left' }, - numberToString: { in: ['number'], out: 'string', category: 'convert', icon: 'ti ti-arrows-right-left' }, - splitStrByLine: { in: ['string'], out: 'stringArray', category: 'convert', icon: 'ti ti-arrows-right-left' }, - pick: { in: [null, 'number'], out: null, category: 'list', icon: 'ti ti-indent-increase' }, - listLen: { in: [null], out: 'number', category: 'list', icon: 'ti ti-indent-increase' }, - rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'ti ti-dice' }, - dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'ti ti-dice' }, - seedRannum: { in: [null, 'number', 'number'], out: 'number', category: 'random', icon: 'ti ti-dice' }, - random: { in: ['number'], out: 'boolean', category: 'random', icon: 'ti ti-dice' }, - dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: 'ti ti-dice' }, - seedRandom: { in: [null, 'number'], out: 'boolean', category: 'random', icon: 'ti ti-dice' }, - randomPick: { in: [0], out: 0, category: 'random', icon: 'ti ti-dice' }, - dailyRandomPick: { in: [0], out: 0, category: 'random', icon: 'ti ti-dice' }, - seedRandomPick: { in: [null, 0], out: 0, category: 'random', icon: 'ti ti-dice' }, - DRPWPM: { in: ['stringArray'], out: 'string', category: 'random', icon: 'ti ti-dice' }, // dailyRandomPickWithProbabilityMapping -}; - -export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, visitor?: any) { - const date = new Date(); - const day = `${visitor ? visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; - - // SHOULD be fine to ignore since it's intended + function shape isn't defined - // eslint-disable-next-line @typescript-eslint/ban-types - const funcs: Record<string, Function> = { - not: (a: boolean) => !a, - or: (a: boolean, b: boolean) => a || b, - and: (a: boolean, b: boolean) => a && b, - eq: (a: any, b: any) => a === b, - notEq: (a: any, b: any) => a !== b, - gt: (a: number, b: number) => a > b, - lt: (a: number, b: number) => a < b, - gtEq: (a: number, b: number) => a >= b, - ltEq: (a: number, b: number) => a <= b, - if: (bool: boolean, a: any, b: any) => bool ? a : b, - for: (times: number, fn: Fn) => { - const result: any[] = []; - for (let i = 0; i < times; i++) { - result.push(fn.exec({ - [fn.slots[0]]: i + 1, - })); - } - return result; - }, - add: (a: number, b: number) => a + b, - subtract: (a: number, b: number) => a - b, - multiply: (a: number, b: number) => a * b, - divide: (a: number, b: number) => a / b, - mod: (a: number, b: number) => a % b, - round: (a: number) => Math.round(a), - strLen: (a: string) => a.length, - strPick: (a: string, b: number) => a[b - 1], - strReplace: (a: string, b: string, c: string) => a.split(b).join(c), - strReverse: (a: string) => a.split('').reverse().join(''), - join: (texts: string[], separator: string) => texts.join(separator || ''), - stringToNumber: (a: string) => parseInt(a), - numberToString: (a: number) => a.toString(), - splitStrByLine: (a: string) => a.split('\n'), - pick: (list: any[], i: number) => list[i - 1], - listLen: (list: any[]) => list.length, - random: (probability: number) => Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * 100) < probability, - rannum: (min: number, max: number) => min + Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * (max - min + 1)), - randomPick: (list: any[]) => list[Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * list.length)], - dailyRandom: (probability: number) => Math.floor(seedrandom(`${day}:${expr.id}`)() * 100) < probability, - dailyRannum: (min: number, max: number) => min + Math.floor(seedrandom(`${day}:${expr.id}`)() * (max - min + 1)), - dailyRandomPick: (list: any[]) => list[Math.floor(seedrandom(`${day}:${expr.id}`)() * list.length)], - seedRandom: (seed: any, probability: number) => Math.floor(seedrandom(seed)() * 100) < probability, - seedRannum: (seed: any, min: number, max: number) => min + Math.floor(seedrandom(seed)() * (max - min + 1)), - seedRandomPick: (seed: any, list: any[]) => list[Math.floor(seedrandom(seed)() * list.length)], - DRPWPM: (list: string[]) => { - const xs: any[] = []; - let totalFactor = 0; - for (const x of list) { - const parts = x.split(' '); - const factor = parseInt(parts.pop()!, 10); - const text = parts.join(' '); - totalFactor += factor; - xs.push({ factor, text }); - } - const r = seedrandom(`${day}:${expr.id}`)() * totalFactor; - let stackedFactor = 0; - for (const x of xs) { - if (r >= stackedFactor && r <= stackedFactor + x.factor) { - return x.text; - } else { - stackedFactor += x.factor; - } - } - return xs[0].text; - }, - }; - - return funcs; -} diff --git a/packages/frontend/src/scripts/hpml/type-checker.ts b/packages/frontend/src/scripts/hpml/type-checker.ts deleted file mode 100644 index ea8133f29..000000000 --- a/packages/frontend/src/scripts/hpml/type-checker.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { isLiteralValue } from './expr'; -import { funcDefs } from './lib'; -import { envVarsDef } from '.'; -import type { Type, PageVar } from '.'; -import type { Expr, Variable } from './expr'; - -type TypeError = { - arg: number; - expect: Type; - actual: Type; -}; - -/** - * Hpml type checker - */ -export class HpmlTypeChecker { - public variables: Variable[]; - public pageVars: PageVar[]; - - constructor(variables: HpmlTypeChecker['variables'] = [], pageVars: HpmlTypeChecker['pageVars'] = []) { - this.variables = variables; - this.pageVars = pageVars; - } - - public typeCheck(v: Expr): TypeError | null { - if (isLiteralValue(v)) return null; - - const def = funcDefs[v.type || '']; - if (def == null) { - throw new Error('Unknown type: ' + v.type); - } - - const generic: Type[] = []; - - for (let i = 0; i < def.in.length; i++) { - const arg = def.in[i]; - const type = this.infer(v.args[i]); - if (type === null) continue; - - if (typeof arg === 'number') { - if (generic[arg] === undefined) { - generic[arg] = type; - } else if (type !== generic[arg]) { - return { - arg: i, - expect: generic[arg], - actual: type, - }; - } - } else if (type !== arg) { - return { - arg: i, - expect: arg, - actual: type, - }; - } - } - - return null; - } - - public getExpectedType(v: Expr, slot: number): Type { - const def = funcDefs[v.type ?? '']; - if (def == null) { - throw new Error('Unknown type: ' + v.type); - } - - const generic: Type[] = []; - - for (let i = 0; i < def.in.length; i++) { - const arg = def.in[i]; - const type = this.infer(v.args[i]); - if (type === null) continue; - - if (typeof arg === 'number') { - if (generic[arg] === undefined) { - generic[arg] = type; - } - } - } - - if (typeof def.in[slot] === 'number') { - return generic[def.in[slot]] ?? null; - } else { - return def.in[slot]; - } - } - - public infer(v: Expr): Type { - if (v.type === null) return null; - if (v.type === 'text') return 'string'; - if (v.type === 'multiLineText') return 'string'; - if (v.type === 'textList') return 'stringArray'; - if (v.type === 'number') return 'number'; - if (v.type === 'ref') { - const variable = this.variables.find(va => va.name === v.value); - if (variable) { - return this.infer(variable); - } - - const pageVar = this.pageVars.find(va => va.name === v.value); - if (pageVar) { - return pageVar.type; - } - - const envVar = envVarsDef[v.value ?? '']; - if (envVar !== undefined) { - return envVar; - } - - return null; - } - if (v.type === 'aiScriptVar') return null; - if (v.type === 'fn') return null; // todo - if (v.type.startsWith('fn:')) return null; // todo - - const generic: Type[] = []; - - const def = funcDefs[v.type]; - - for (let i = 0; i < def.in.length; i++) { - const arg = def.in[i]; - if (typeof arg === 'number') { - const type = this.infer(v.args[i]); - - if (generic[arg] === undefined) { - generic[arg] = type; - } else { - if (type !== generic[arg]) { - generic[arg] = null; - } - } - } - } - - if (typeof def.out === 'number') { - return generic[def.out]; - } else { - return def.out; - } - } - - public getVarByName(name: string): Variable { - const v = this.variables.find(x => x.name === name); - if (v !== undefined) { - return v; - } else { - throw new Error(`No such variable '${name}'`); - } - } - - public getVarsByType(type: Type): Variable[] { - if (type == null) return this.variables; - return this.variables.filter(x => (this.infer(x) === null) || (this.infer(x) === type)); - } - - public getEnvVarsByType(type: Type): string[] { - if (type == null) return Object.keys(envVarsDef); - return Object.entries(envVarsDef).filter(([k, v]) => v === null || type === v).map(([k, v]) => k); - } - - public getPageVarsByType(type: Type): string[] { - if (type == null) return this.pageVars.map(v => v.name); - return this.pageVars.filter(v => type === v.type).map(v => v.name); - } - - public isUsedName(name: string) { - if (this.variables.some(v => v.name === name)) { - return true; - } - - if (this.pageVars.some(v => v.name === name)) { - return true; - } - - if (envVarsDef[name]) { - return true; - } - - return false; - } -} diff --git a/packages/frontend/src/scripts/idle-render.ts b/packages/frontend/src/scripts/idle-render.ts new file mode 100644 index 000000000..ccce8b02b --- /dev/null +++ b/packages/frontend/src/scripts/idle-render.ts @@ -0,0 +1,38 @@ +class IdlingRenderScheduler { + #renderers: Set<FrameRequestCallback>; + #rafId: number; + #ricId: number; + + constructor() { + this.#renderers = new Set(); + this.#rafId = 0; + this.#ricId = requestIdleCallback((deadline) => this.#schedule(deadline)); + } + + #schedule(deadline: IdleDeadline): void { + if (deadline.timeRemaining()) { + this.#rafId = requestAnimationFrame((time) => { + for (const renderer of this.#renderers) { + renderer(time); + } + }); + } + this.#ricId = requestIdleCallback((arg) => this.#schedule(arg)); + } + + add(renderer: FrameRequestCallback): void { + this.#renderers.add(renderer); + } + + delete(renderer: FrameRequestCallback): void { + this.#renderers.delete(renderer); + } + + dispose(): void { + this.#renderers.clear(); + cancelAnimationFrame(this.#rafId); + cancelIdleCallback(this.#ricId); + } +} + +export const defaultIdlingRenderScheduler = new IdlingRenderScheduler(); diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts index fe9f0a244..44a58d6c7 100644 --- a/packages/frontend/src/scripts/select-file.ts +++ b/packages/frontend/src/scripts/select-file.ts @@ -1,7 +1,7 @@ import { ref } from 'vue'; import { DriveFile } from 'misskey-js/built/entities'; import * as os from '@/os'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; import { uploadFile } from '@/scripts/upload'; @@ -51,7 +51,7 @@ export function chooseFileFromUrl(): Promise<DriveFile> { const marker = Math.random().toString(); // TODO: UUIDとか使う - const connection = stream.useChannel('main'); + const connection = useStream().useChannel('main'); connection.on('urlUploadFinished', urlResponse => { if (urlResponse.marker === marker) { res(urlResponse.file); diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index 28284c7bc..f2e825356 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -60,7 +60,7 @@ export function applyTheme(theme: Theme, persist = true) { document.documentElement.classList.remove('_themeChanging_'); }, 1000); - const colorSchema = theme.base === 'dark' ? 'dark' : 'light'; + const colorScheme = theme.base === 'dark' ? 'dark' : 'light'; // Deep copy const _theme = deepClone(theme); @@ -83,11 +83,11 @@ export function applyTheme(theme: Theme, persist = true) { document.documentElement.style.setProperty(`--${k}`, v.toString()); } - document.documentElement.style.setProperty('color-schema', colorSchema); + document.documentElement.style.setProperty('color-scheme', colorScheme); if (persist) { miLocalStorage.setItem('theme', JSON.stringify(props)); - miLocalStorage.setItem('colorSchema', colorSchema); + miLocalStorage.setItem('colorScheme', colorScheme); } // 色計算など再度行えるようにクライアント全体に通知 diff --git a/packages/frontend/src/scripts/time.ts b/packages/frontend/src/scripts/time.ts index 34e8b6b17..b21978b18 100644 --- a/packages/frontend/src/scripts/time.ts +++ b/packages/frontend/src/scripts/time.ts @@ -5,15 +5,16 @@ const dateTimeIntervals = { }; export function dateUTC(time: number[]): Date { - const d = time.length === 2 ? Date.UTC(time[0], time[1]) - : time.length === 3 ? Date.UTC(time[0], time[1], time[2]) - : time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3]) - : time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4]) - : time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5]) - : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6]) - : null; + const d = + time.length === 2 ? Date.UTC(time[0], time[1]) + : time.length === 3 ? Date.UTC(time[0], time[1], time[2]) + : time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3]) + : time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4]) + : time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5]) + : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6]) + : null; - if (!d) throw 'wrong number of arguments'; + if (!d) throw new Error('wrong number of arguments'); return new Date(d); } diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts index ffe33cccc..22a01e066 100644 --- a/packages/frontend/src/scripts/use-note-capture.ts +++ b/packages/frontend/src/scripts/use-note-capture.ts @@ -1,6 +1,6 @@ import { onUnmounted, Ref } from 'vue'; import * as misskey from 'misskey-js'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { $i } from '@/account'; export function useNoteCapture(props: { @@ -9,7 +9,7 @@ export function useNoteCapture(props: { isDeletedRef: Ref<boolean>; }) { const note = props.note; - const connection = $i ? stream : null; + const connection = $i ? useStream() : null; function onStreamNoteUpdated(noteData): void { const { type, id, body } = noteData; diff --git a/packages/frontend/src/scripts/worker-multi-dispatch.ts b/packages/frontend/src/scripts/worker-multi-dispatch.ts new file mode 100644 index 000000000..1847a8ccf --- /dev/null +++ b/packages/frontend/src/scripts/worker-multi-dispatch.ts @@ -0,0 +1,75 @@ +function defaultUseWorkerNumber(prev: number, totalWorkers: number) { + return prev + 1; +} + +export class WorkerMultiDispatch<POST = any, RETURN = any> { + private symbol = Symbol('WorkerMultiDispatch'); + private workers: Worker[] = []; + private terminated = false; + private prevWorkerNumber = 0; + private getUseWorkerNumber = defaultUseWorkerNumber; + private finalizationRegistry: FinalizationRegistry<symbol>; + + constructor(workerConstructor: () => Worker, concurrency: number, getUseWorkerNumber = defaultUseWorkerNumber) { + this.getUseWorkerNumber = getUseWorkerNumber; + for (let i = 0; i < concurrency; i++) { + this.workers.push(workerConstructor()); + } + + this.finalizationRegistry = new FinalizationRegistry(() => { + this.terminate(); + }); + this.finalizationRegistry.register(this, this.symbol); + + if (_DEV_) console.log('WorkerMultiDispatch: Created', this); + } + + public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: typeof defaultUseWorkerNumber = this.getUseWorkerNumber) { + let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length); + workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length; + if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber); + this.prevWorkerNumber = workerNumber; + + // 不毛だがunionをoverloadに突っ込めない + // https://stackoverflow.com/questions/66507585/overload-signatures-union-types-and-no-overload-matches-this-call-error + // https://github.com/microsoft/TypeScript/issues/14107 + if (Array.isArray(options)) { + this.workers[workerNumber].postMessage(message, options); + } else { + this.workers[workerNumber].postMessage(message, options); + } + return workerNumber; + } + + public addListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) { + this.workers.forEach(worker => { + worker.addEventListener('message', callback, options); + }); + } + + public removeListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) { + this.workers.forEach(worker => { + worker.removeEventListener('message', callback, options); + }); + } + + public terminate() { + this.terminated = true; + if (_DEV_) console.log('WorkerMultiDispatch: Terminating', this); + this.workers.forEach(worker => { + worker.terminate(); + }); + this.workers = []; + this.finalizationRegistry.unregister(this); + } + + public isTerminated() { + return this.terminated; + } + public getWorkers() { + return this.workers; + } + public getSymbol() { + return this.symbol; + } +} diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 245bcbefe..6ba05c36a 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -92,7 +92,7 @@ export const defaultStore = markRaw(new Storage('base', { }, reactionAcceptance: { where: 'account', - default: null as 'likeOnly' | 'likeOnlyForRemote' | null, + default: 'nonSensitiveOnly' as 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null, }, mutedWords: { where: 'account', @@ -102,6 +102,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: [] as string[], }, + showTimelineReplies: { + where: 'account', + default: false, + }, menu: { where: 'deviceAccount', @@ -314,6 +318,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, + devMode: { + where: 'device', + default: false, + }, mediaListWithOneImageAppearance: { where: 'device', default: 'expand' as 'expand' | '16_9' | '1_1' | '2_3', @@ -328,7 +336,11 @@ export const defaultStore = markRaw(new Storage('base', { }, enableCondensedLineForAcct: { where: 'device', - default: true, + default: false, + }, + additionalUnicodeEmojiIndexes: { + where: 'device', + default: {} as Record<string, Record<string, string[]>>, }, })); diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts index dea3459b8..9cae58a26 100644 --- a/packages/frontend/src/stream.ts +++ b/packages/frontend/src/stream.ts @@ -3,6 +3,14 @@ import { markRaw } from 'vue'; import { $i } from '@/account'; import { url } from '@/config'; -export const stream = markRaw(new Misskey.Stream(url, $i ? { - token: $i.token, -} : null)); +let stream: Misskey.Stream | null = null; + +export function useStream(): Misskey.Stream { + if (stream) return stream; + + stream = markRaw(new Misskey.Stream(url, $i ? { + token: $i.token, + } : null)); + + return stream; +} diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index 20254d335..b376e4c42 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -22,11 +22,7 @@ } html { - touch-action: manipulation; background-color: var(--bg); - background-attachment: fixed; - background-size: cover; - background-position: center; color: var(--fg); accent-color: var(--accent); overflow: auto; @@ -38,7 +34,7 @@ html { tab-size: 2; &, * { - scrollbar-color: var(--scrollbarHandle) inherit; + scrollbar-color: var(--scrollbarHandle) transparent; scrollbar-width: thin; &::-webkit-scrollbar { @@ -87,6 +83,7 @@ html._themeChanging_ { } html, body { + touch-action: manipulation; margin: 0; padding: 0; scroll-behavior: smooth; @@ -483,3 +480,140 @@ hr { transform: scaleX(1.00) scaleY(1.00) ; } } + +// MFM ----------------------------- + +._mfm_blur_ { + filter: blur(6px); + transition: filter 0.3s; + + &:hover { + filter: blur(0px); + } +} + +.mfm-x2 { + --mfm-zoom-size: 200%; +} + +.mfm-x3 { + --mfm-zoom-size: 400%; +} + +.mfm-x4 { + --mfm-zoom-size: 600%; +} + +.mfm-x2, .mfm-x3, .mfm-x4 { + font-size: var(--mfm-zoom-size); + + .mfm-x2, .mfm-x3, .mfm-x4 { + /* only half effective */ + font-size: calc(var(--mfm-zoom-size) / 2 + 50%); + + .mfm-x2, .mfm-x3, .mfm-x4 { + /* disabled */ + font-size: 100%; + } + } +} + +@keyframes mfm-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@keyframes mfm-spinX { + 0% { transform: perspective(128px) rotateX(0deg); } + 100% { transform: perspective(128px) rotateX(360deg); } +} + +@keyframes mfm-spinY { + 0% { transform: perspective(128px) rotateY(0deg); } + 100% { transform: perspective(128px) rotateY(360deg); } +} + +@keyframes mfm-jump { + 0% { transform: translateY(0); } + 25% { transform: translateY(-16px); } + 50% { transform: translateY(0); } + 75% { transform: translateY(-8px); } + 100% { transform: translateY(0); } +} + +@keyframes mfm-bounce { + 0% { transform: translateY(0) scale(1, 1); } + 25% { transform: translateY(-16px) scale(1, 1); } + 50% { transform: translateY(0) scale(1, 1); } + 75% { transform: translateY(0) scale(1.5, 0.75); } + 100% { transform: translateY(0) scale(1, 1); } +} + +// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`; +// let css = ''; +// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; } +@keyframes mfm-twitch { + 0% { transform: translate(7px, -2px) } + 5% { transform: translate(-3px, 1px) } + 10% { transform: translate(-7px, -1px) } + 15% { transform: translate(0px, -1px) } + 20% { transform: translate(-8px, 6px) } + 25% { transform: translate(-4px, -3px) } + 30% { transform: translate(-4px, -6px) } + 35% { transform: translate(-8px, -8px) } + 40% { transform: translate(4px, 6px) } + 45% { transform: translate(-3px, 1px) } + 50% { transform: translate(2px, -10px) } + 55% { transform: translate(-7px, 0px) } + 60% { transform: translate(-2px, 4px) } + 65% { transform: translate(3px, -8px) } + 70% { transform: translate(6px, 7px) } + 75% { transform: translate(-7px, -2px) } + 80% { transform: translate(-7px, -8px) } + 85% { transform: translate(9px, 3px) } + 90% { transform: translate(-3px, -2px) } + 95% { transform: translate(-10px, 2px) } + 100% { transform: translate(-2px, -6px) } +} + +// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`; +// let css = ''; +// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; } +@keyframes mfm-shake { + 0% { transform: translate(-3px, -1px) rotate(-8deg) } + 5% { transform: translate(0px, -1px) rotate(-10deg) } + 10% { transform: translate(1px, -3px) rotate(0deg) } + 15% { transform: translate(1px, 1px) rotate(11deg) } + 20% { transform: translate(-2px, 1px) rotate(1deg) } + 25% { transform: translate(-1px, -2px) rotate(-2deg) } + 30% { transform: translate(-1px, 2px) rotate(-3deg) } + 35% { transform: translate(2px, 1px) rotate(6deg) } + 40% { transform: translate(-2px, -3px) rotate(-9deg) } + 45% { transform: translate(0px, -1px) rotate(-12deg) } + 50% { transform: translate(1px, 2px) rotate(10deg) } + 55% { transform: translate(0px, -3px) rotate(8deg) } + 60% { transform: translate(1px, -1px) rotate(8deg) } + 65% { transform: translate(0px, -1px) rotate(-7deg) } + 70% { transform: translate(-1px, -3px) rotate(6deg) } + 75% { transform: translate(0px, -2px) rotate(4deg) } + 80% { transform: translate(-2px, -1px) rotate(3deg) } + 85% { transform: translate(1px, -3px) rotate(-10deg) } + 90% { transform: translate(1px, 0px) rotate(3deg) } + 95% { transform: translate(-2px, 0px) rotate(-3deg) } + 100% { transform: translate(2px, 1px) rotate(2deg) } +} + +@keyframes mfm-rubberBand { + from { transform: scale3d(1, 1, 1); } + 30% { transform: scale3d(1.25, 0.75, 1); } + 40% { transform: scale3d(0.75, 1.25, 1); } + 50% { transform: scale3d(1.15, 0.85, 1); } + 65% { transform: scale3d(0.95, 1.05, 1); } + 75% { transform: scale3d(1.05, 0.95, 1); } + to { transform: scale3d(1, 1, 1); } +} + +@keyframes mfm-rainbow { + 0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); } + 100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); } +} diff --git a/packages/frontend/src/themes/_dark.json5 b/packages/frontend/src/themes/_dark.json5 index a23d25e86..5ef6adb08 100644 --- a/packages/frontend/src/themes/_dark.json5 +++ b/packages/frontend/src/themes/_dark.json5 @@ -21,6 +21,7 @@ fgTransparent: ':alpha<0.5<@fg', fgHighlighted: ':lighten<3<@fg', fgOnAccent: '#fff', + fgOnWhite: '#333', divider: 'rgba(255, 255, 255, 0.1)', indicator: '@accent', panel: ':lighten<3<@bg', @@ -77,7 +78,7 @@ codeString: '#ffb675', codeNumber: '#cfff9e', codeBoolean: '#c59eff', - deckDivider: '#000', + deckBg: '#000', htmlThemeColor: '@bg', X2: ':darken<2<@panel', X3: 'rgba(255, 255, 255, 0.05)', diff --git a/packages/frontend/src/themes/_light.json5 b/packages/frontend/src/themes/_light.json5 index 713756221..32f3c7490 100644 --- a/packages/frontend/src/themes/_light.json5 +++ b/packages/frontend/src/themes/_light.json5 @@ -21,6 +21,7 @@ fgTransparent: ':alpha<0.5<@fg', fgHighlighted: ':darken<3<@fg', fgOnAccent: '#fff', + fgOnWhite: '#333', divider: 'rgba(0, 0, 0, 0.1)', indicator: '@accent', panel: ':lighten<3<@bg', @@ -77,7 +78,7 @@ codeString: '#b98710', codeNumber: '#0fbbbb', codeBoolean: '#62b70c', - deckDivider: ':darken<3<@bg', + deckBg: ':darken<3<@bg', htmlThemeColor: '@bg', X2: ':darken<2<@panel', X3: 'rgba(0, 0, 0, 0.05)', diff --git a/packages/frontend/src/themes/d-astro.json5 b/packages/frontend/src/themes/d-astro.json5 index c6a927ec3..09a9ead1a 100644 --- a/packages/frontend/src/themes/d-astro.json5 +++ b/packages/frontend/src/themes/d-astro.json5 @@ -53,6 +53,7 @@ panelHeaderBg: ':lighten<3<@panel', panelHeaderFg: '@fg', htmlThemeColor: '@bg', + fgOnWhite: '@accent', panelHighlight: ':lighten<3<@panel', listItemHoverBg: 'rgba(255, 255, 255, 0.03)', scrollbarHandle: 'rgba(255, 255, 255, 0.2)', diff --git a/packages/frontend/src/themes/d-botanical.json5 b/packages/frontend/src/themes/d-botanical.json5 index 33cf7aa81..62208d237 100644 --- a/packages/frontend/src/themes/d-botanical.json5 +++ b/packages/frontend/src/themes/d-botanical.json5 @@ -11,6 +11,7 @@ bg: 'rgb(37, 38, 36)', fg: 'rgb(216, 212, 199)', fgHighlighted: '#fff', + fgOnWhite: '@accent', divider: 'rgba(255, 255, 255, 0.14)', panel: 'rgb(47, 47, 44)', panelHeaderDivider: 'rgba(0, 0, 0, 0)', diff --git a/packages/frontend/src/themes/d-cherry.json5 b/packages/frontend/src/themes/d-cherry.json5 index a7e1ad1c8..f9638124c 100644 --- a/packages/frontend/src/themes/d-cherry.json5 +++ b/packages/frontend/src/themes/d-cherry.json5 @@ -10,6 +10,7 @@ accent: 'rgb(255, 89, 117)', bg: 'rgb(28, 28, 37)', fg: 'rgb(236, 239, 244)', + fgOnWhite: '@accent', panel: 'rgb(35, 35, 47)', renote: '@accent', link: '@accent', diff --git a/packages/frontend/src/themes/d-dark.json5 b/packages/frontend/src/themes/d-dark.json5 index 63144e88e..ae4f7d53f 100644 --- a/packages/frontend/src/themes/d-dark.json5 +++ b/packages/frontend/src/themes/d-dark.json5 @@ -11,6 +11,7 @@ bg: '#232323', fg: 'rgb(199, 209, 216)', fgHighlighted: '#fff', + fgOnWhite: '@accent', divider: 'rgba(255, 255, 255, 0.14)', panel: '#2d2d2d', panelHeaderDivider: 'rgba(0, 0, 0, 0)', diff --git a/packages/frontend/src/themes/d-future.json5 b/packages/frontend/src/themes/d-future.json5 index 0962a1241..f2c1f3eb8 100644 --- a/packages/frontend/src/themes/d-future.json5 +++ b/packages/frontend/src/themes/d-future.json5 @@ -12,6 +12,7 @@ fg: '#D5D5D6', fgHighlighted: '#fff', fgOnAccent: '#000', + fgOnWhite: '@accent', divider: 'rgba(255, 255, 255, 0.1)', panel: '#18181c', panelHeaderDivider: 'rgba(0, 0, 0, 0)', diff --git a/packages/frontend/src/themes/d-green-lime.json5 b/packages/frontend/src/themes/d-green-lime.json5 index 9522f534a..ca4e688fd 100644 --- a/packages/frontend/src/themes/d-green-lime.json5 +++ b/packages/frontend/src/themes/d-green-lime.json5 @@ -12,6 +12,7 @@ fg: '#dee7e4', fgHighlighted: '#fff', fgOnAccent: '#192320', + fgOnWhite: '@accent', divider: '#e7fffb24', panel: '#192320', panelHeaderDivider: 'rgba(0, 0, 0, 0)', diff --git a/packages/frontend/src/themes/d-green-orange.json5 b/packages/frontend/src/themes/d-green-orange.json5 index e542782c6..c2539816e 100644 --- a/packages/frontend/src/themes/d-green-orange.json5 +++ b/packages/frontend/src/themes/d-green-orange.json5 @@ -12,6 +12,7 @@ fg: '#dee7e4', fgHighlighted: '#fff', fgOnAccent: '#192320', + fgOnWhite: '@accent', divider: '#e7fffb24', panel: '#192320', panelHeaderDivider: 'rgba(0, 0, 0, 0)', diff --git a/packages/frontend/src/themes/d-ice.json5 b/packages/frontend/src/themes/d-ice.json5 index 179b060dc..b4abc0cac 100644 --- a/packages/frontend/src/themes/d-ice.json5 +++ b/packages/frontend/src/themes/d-ice.json5 @@ -8,6 +8,7 @@ props: { accent: '#47BFE8', + fgOnWhite: '@accent', bg: '#212526', }, } diff --git a/packages/frontend/src/themes/d-persimmon.json5 b/packages/frontend/src/themes/d-persimmon.json5 index e36265ff1..0ab6523dd 100644 --- a/packages/frontend/src/themes/d-persimmon.json5 +++ b/packages/frontend/src/themes/d-persimmon.json5 @@ -11,6 +11,7 @@ bg: 'rgb(31, 33, 31)', fg: '#cdd8c7', fgHighlighted: '#fff', + fgOnWhite: '@accent', divider: 'rgba(255, 255, 255, 0.14)', panel: 'rgb(41, 43, 41)', infoFg: '@fg', diff --git a/packages/frontend/src/themes/d-u0.json5 b/packages/frontend/src/themes/d-u0.json5 index b270f809a..ed776746a 100644 --- a/packages/frontend/src/themes/d-u0.json5 +++ b/packages/frontend/src/themes/d-u0.json5 @@ -55,6 +55,7 @@ codeNumber: '#cfff9e', codeString: '#ffb675', fgOnAccent: '#fff', + fgOnWhite: '@accent', infoWarnBg: '#42321c', infoWarnFg: '#ffbd3e', navHoverFg: ':lighten<17<@fg', @@ -83,6 +84,6 @@ fgTransparentWeak: ':alpha<0.75<@fg', panelHeaderDivider: 'rgba(0, 0, 0, 0)', scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', - deckDivider: '#142022', + deckBg: '#142022', }, } diff --git a/packages/frontend/src/themes/l-apricot.json5 b/packages/frontend/src/themes/l-apricot.json5 index 1ed552557..fe1f9f892 100644 --- a/packages/frontend/src/themes/l-apricot.json5 +++ b/packages/frontend/src/themes/l-apricot.json5 @@ -10,6 +10,7 @@ accent: 'rgb(234, 154, 82)', bg: '#e6e5e2', fg: 'rgb(149, 143, 139)', + fgOnWhite: '@accent', panel: '#EEECE8', renote: '@accent', link: '@accent', diff --git a/packages/frontend/src/themes/l-botanical.json5 b/packages/frontend/src/themes/l-botanical.json5 index 2ea9a7d6c..5c9892789 100644 --- a/packages/frontend/src/themes/l-botanical.json5 +++ b/packages/frontend/src/themes/l-botanical.json5 @@ -11,6 +11,7 @@ bg: 'e2deda', fg: '#3d3d3d', fgHighlighted: '#6bc9a0', + fgOnWhite: '@accent', divider: '#cfcfcf', panel: '@X14', panelHeaderBg: '@panel', diff --git a/packages/frontend/src/themes/l-cherry.json5 b/packages/frontend/src/themes/l-cherry.json5 index 5ad240241..1189a28fe 100644 --- a/packages/frontend/src/themes/l-cherry.json5 +++ b/packages/frontend/src/themes/l-cherry.json5 @@ -10,6 +10,7 @@ accent: 'rgb(219, 96, 114)', bg: 'rgb(254, 248, 249)', fg: 'rgb(152, 13, 26)', + fgOnWhite: '@accent', panel: 'rgb(255, 255, 255)', renote: '@accent', link: 'rgb(156, 187, 5)', diff --git a/packages/frontend/src/themes/l-coffee.json5 b/packages/frontend/src/themes/l-coffee.json5 index fbcd4fa9e..b64cc7358 100644 --- a/packages/frontend/src/themes/l-coffee.json5 +++ b/packages/frontend/src/themes/l-coffee.json5 @@ -10,6 +10,7 @@ accent: '#9f8989', bg: '#f5f3f3', fg: '#7f6666', + fgOnWhite: '@accent', panel: '#fff', divider: 'rgba(87, 68, 68, 0.1)', renote: 'rgb(160, 172, 125)', diff --git a/packages/frontend/src/themes/l-light.json5 b/packages/frontend/src/themes/l-light.json5 index 248355c94..63c2e6d27 100644 --- a/packages/frontend/src/themes/l-light.json5 +++ b/packages/frontend/src/themes/l-light.json5 @@ -10,6 +10,7 @@ props: { bg: '#f9f9f9', fg: '#676767', + fgOnWhite: '@accent', divider: '#e8e8e8', header: ':alpha<0.7<@panel', navBg: '#fff', diff --git a/packages/frontend/src/themes/l-rainy.json5 b/packages/frontend/src/themes/l-rainy.json5 index 283dd74c6..e7d1d5af0 100644 --- a/packages/frontend/src/themes/l-rainy.json5 +++ b/packages/frontend/src/themes/l-rainy.json5 @@ -10,6 +10,7 @@ accent: '#5db0da', bg: 'rgb(246 248 249)', fg: '#636b71', + fgOnWhite: '@accent', panel: '#fff', divider: 'rgb(230 233 234)', panelHeaderDivider: '@divider', diff --git a/packages/frontend/src/themes/l-sushi.json5 b/packages/frontend/src/themes/l-sushi.json5 index 5846927d6..e787d6373 100644 --- a/packages/frontend/src/themes/l-sushi.json5 +++ b/packages/frontend/src/themes/l-sushi.json5 @@ -10,6 +10,7 @@ accent: '#e36749', bg: '#f0eee9', fg: '#5f5f5f', + fgOnWhite: '@accent', renote: '@accent', link: '@accent', mention: '@accent', diff --git a/packages/frontend/src/themes/l-u0.json5 b/packages/frontend/src/themes/l-u0.json5 index 03b114ba3..b77b15e3f 100644 --- a/packages/frontend/src/themes/l-u0.json5 +++ b/packages/frontend/src/themes/l-u0.json5 @@ -55,6 +55,7 @@ codeNumber: '#cfff9e', codeString: '#ffb675', fgOnAccent: '#fff', + fgOnWhite: '@accent', infoWarnBg: '#42321c', infoWarnFg: '#ffbd3e', navHoverFg: ':lighten<17<@fg', diff --git a/packages/frontend/src/themes/l-vivid.json5 b/packages/frontend/src/themes/l-vivid.json5 index b3c08f38a..822ef948d 100644 --- a/packages/frontend/src/themes/l-vivid.json5 +++ b/packages/frontend/src/themes/l-vivid.json5 @@ -52,6 +52,7 @@ driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':darken<3<@fg', fgTransparent: ':alpha<0.5<@fg', + fgOnWhite: '@accent', panelHeaderBg: ':lighten<3<@panel', panelHeaderFg: '@fg', htmlThemeColor: '@bg', diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 71a4285e9..3b970eefb 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -10,12 +10,20 @@ <XUpload v-if="uploads.length > 0"/> <TransitionGroup - tag="div" :class="[$style.notifications, $style[`notificationsPosition-${defaultStore.state.notificationPosition}`], $style[`notificationsStackAxis-${defaultStore.state.notificationStackAxis}`]]" - :move-class="defaultStore.state.animation ? $style.transition_notification_move : ''" - :enter-active-class="defaultStore.state.animation ? $style.transition_notification_enterActive : ''" - :leave-active-class="defaultStore.state.animation ? $style.transition_notification_leaveActive : ''" - :enter-from-class="defaultStore.state.animation ? $style.transition_notification_enterFrom : ''" - :leave-to-class="defaultStore.state.animation ? $style.transition_notification_leaveTo : ''" + tag="div" + :class="[$style.notifications, { + [$style.notificationsPosition_leftTop]: defaultStore.state.notificationPosition === 'leftTop', + [$style.notificationsPosition_leftBottom]: defaultStore.state.notificationPosition === 'leftBottom', + [$style.notificationsPosition_rightTop]: defaultStore.state.notificationPosition === 'rightTop', + [$style.notificationsPosition_rightBottom]: defaultStore.state.notificationPosition === 'rightBottom', + [$style.notificationsStackAxis_vertical]: defaultStore.state.notificationStackAxis === 'vertical', + [$style.notificationsStackAxis_horizontal]: defaultStore.state.notificationStackAxis === 'horizontal', + }]" + :moveClass="defaultStore.state.animation ? $style.transition_notification_move : ''" + :enterActiveClass="defaultStore.state.animation ? $style.transition_notification_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_notification_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_notification_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_notification_leaveTo : ''" > <div v-for="notification in notifications" :key="notification.id" :class="$style.notification"> <XNotification :notification="notification"/> @@ -40,7 +48,7 @@ import { popups, pendingApiRequestsCount } from '@/os'; import { uploads } from '@/scripts/upload'; import * as sound from '@/scripts/sound'; import { $i } from '@/account'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; @@ -55,7 +63,7 @@ function onNotification(notification) { if ($i.mutingNotificationTypes.includes(notification.type)) return; if (document.visibilityState === 'visible') { - stream.send('readNotification'); + useStream().send('readNotification'); notifications.unshift(notification); window.setTimeout(() => { @@ -71,7 +79,7 @@ function onNotification(notification) { } if ($i) { - const connection = stream.useChannel('main', null, 'UI'); + const connection = useStream().useChannel('main', null, 'UI'); connection.on('notification', onNotification); //#region Listen message from SW @@ -103,31 +111,31 @@ if ($i) { pointer-events: none; display: flex; - &.notificationsPosition-leftTop { + &.notificationsPosition_leftTop { top: var(--margin); left: 0; } - &.notificationsPosition-rightTop { + &.notificationsPosition_rightTop { top: var(--margin); right: 0; } - &.notificationsPosition-leftBottom { + &.notificationsPosition_leftBottom { bottom: calc(var(--minBottomSpacing) + var(--margin)); left: 0; } - &.notificationsPosition-rightBottom { + &.notificationsPosition_rightBottom { bottom: calc(var(--minBottomSpacing) + var(--margin)); right: 0; } - &.notificationsStackAxis-vertical { + &.notificationsStackAxis_vertical { width: 250px; - &.notificationsPosition-leftTop, - &.notificationsPosition-rightTop { + &.notificationsPosition_leftTop, + &.notificationsPosition_rightTop { flex-direction: column; .notification { @@ -137,8 +145,8 @@ if ($i) { } } - &.notificationsPosition-leftBottom, - &.notificationsPosition-rightBottom { + &.notificationsPosition_leftBottom, + &.notificationsPosition_rightBottom { flex-direction: column-reverse; .notification { @@ -149,11 +157,11 @@ if ($i) { } } - &.notificationsStackAxis-horizontal { + &.notificationsStackAxis_horizontal { width: 100%; - &.notificationsPosition-leftTop, - &.notificationsPosition-leftBottom { + &.notificationsPosition_leftTop, + &.notificationsPosition_leftBottom { flex-direction: row; .notification { @@ -163,8 +171,8 @@ if ($i) { } } - &.notificationsPosition-rightTop, - &.notificationsPosition-rightBottom { + &.notificationsPosition_rightTop, + &.notificationsPosition_rightBottom { flex-direction: row-reverse; .notification { diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue index 7a94a0c3e..365486a0a 100644 --- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue @@ -1,43 +1,41 @@ <template> -<div class="kmwsukvl"> - <div class="body"> - <div class="top"> - <div class="banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div> - <button v-click-anime class="item _button instance" @click="openInstanceMenu"> - <img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/> - </button> - </div> - <div class="middle"> - <MkA v-click-anime class="item index" active-class="active" to="/" exact> - <i class="icon ti ti-home ti-fw"></i><span class="text">{{ i18n.ts.timeline }}</span> - </MkA> - <template v-for="item in menu"> - <div v-if="item === '-'" class="divider"></div> - <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: navbarItemDef[item].active }]" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> - <i class="icon ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span> - <span v-if="navbarItemDef[item].indicated" class="indicator"><i class="icon _indicatorCircle"></i></span> - </component> - </template> - <div class="divider"></div> - <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin"> - <i class="icon ti ti-dashboard ti-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span> - </MkA> - <button v-click-anime class="item _button" @click="more"> - <i class="icon ti ti-grid-dots ti-fw"></i><span class="text">{{ i18n.ts.more }}</span> - <span v-if="otherMenuItemIndicated" class="indicator"><i class="icon _indicatorCircle"></i></span> - </button> - <MkA v-click-anime class="item" active-class="active" to="/settings"> - <i class="icon ti ti-settings ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span> - </MkA> - </div> - <div class="bottom"> - <button class="item _button post" data-cy-open-post-form @click="os.post"> - <i class="icon ti ti-pencil ti-fw"></i><span class="text">{{ i18n.ts.note }}</span> - </button> - <button v-click-anime class="item _button account" @click="openAccountMenu"> - <MkAvatar :user="$i" class="avatar"/><MkAcct class="text _nowrap" :user="$i"/> - </button> - </div> +<div :class="$style.root"> + <div :class="$style.top"> + <div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div> + <button class="_button" :class="$style.instance" @click="openInstanceMenu"> + <img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/> + </button> + </div> + <div :class="$style.middle"> + <MkA :class="$style.item" :activeClass="$style.active" to="/" exact> + <i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span> + </MkA> + <template v-for="item in menu"> + <div v-if="item === '-'" :class="$style.divider"></div> + <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" class="_button" :class="[$style.item, { [$style.active]: navbarItemDef[item].active }]" :activeClass="$style.active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> + <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span> + <span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span> + </component> + </template> + <div :class="$style.divider"></div> + <MkA v-if="$i.isAdmin || $i.isModerator" :class="$style.item" :activeClass="$style.active" to="/admin"> + <i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span> + </MkA> + <button :class="$style.item" class="_button" @click="more"> + <i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span> + <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span> + </button> + <MkA :class="$style.item" :activeClass="$style.active" to="/settings"> + <i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span> + </MkA> + </div> + <div :class="$style.bottom"> + <button class="_button" :class="$style.post" data-cy-open-post-form @click="os.post"> + <i :class="$style.postIcon" class="ti ti-pencil ti-fw"></i><span style="position: relative;">{{ i18n.ts.note }}</span> + </button> + <button class="_button" :class="$style.account" @click="openAccountMenu"> + <MkAvatar :user="$i" :class="$style.avatar"/><MkAcct :class="$style.acct" class="_nowrap" :user="$i"/> + </button> </div> </div> </template> @@ -73,192 +71,186 @@ function more() { } </script> -<style lang="scss" scoped> -.kmwsukvl { - > .body { - display: flex; - flex-direction: column; +<style lang="scss" module> +.root { + display: flex; + flex-direction: column; +} - > .top { - position: sticky; - top: 0; - z-index: 1; - padding: 20px 0; - background: var(--X14); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); +.top { + position: sticky; + top: 0; + z-index: 1; + padding: 20px 0; + background: var(--X14); + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(8px)); +} - > .banner { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-size: cover; - background-position: center center; - -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); - mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); - } +.banner { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-size: cover; + background-position: center center; + -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); + mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); +} - > .instance { - position: relative; - display: block; - text-align: center; - width: 100%; +.instance { + position: relative; + display: block; + text-align: center; + width: 100%; +} - > .icon { - display: inline-block; - width: 38px; - aspect-ratio: 1; - } - } - } +.instanceIcon { + display: inline-block; + width: 38px; + aspect-ratio: 1; +} - > .bottom { - position: sticky; - bottom: 0; - padding: 20px 0; - background: var(--X14); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); +.bottom { + position: sticky; + bottom: 0; + padding: 20px 0; + background: var(--X14); + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(8px)); +} - > .post { - position: relative; - display: block; - width: 100%; - height: 40px; - color: var(--fgOnAccent); - font-weight: bold; - text-align: left; +.post { + position: relative; + display: block; + width: 100%; + height: 40px; + color: var(--fgOnAccent); + font-weight: bold; + text-align: left; - &:before { - content: ""; - display: block; - width: calc(100% - 38px); - height: 100%; - margin: auto; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 999px; - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); - } + &:before { + content: ""; + display: block; + width: calc(100% - 38px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + } - &:hover, &.active { - &:before { - background: var(--accentLighten); - } - } - - > .icon { - position: relative; - margin-left: 30px; - margin-right: 8px; - width: 32px; - } - - > .text { - position: relative; - } - } - - > .account { - position: relative; - display: flex; - align-items: center; - padding-left: 30px; - width: 100%; - text-align: left; - box-sizing: border-box; - margin-top: 16px; - - > .avatar { - display: block; - flex-shrink: 0; - position: relative; - width: 32px; - aspect-ratio: 1; - margin-right: 8px; - } - - > .text { - display: block; - flex-shrink: 1; - padding-right: 8px; - } - } - } - - > .middle { - flex: 1; - - > .divider { - margin: 16px 16px; - border-top: solid 0.5px var(--divider); - } - - > .item { - position: relative; - display: block; - padding-left: 24px; - line-height: 2.85rem; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - width: 100%; - text-align: left; - box-sizing: border-box; - color: var(--navFg); - - > .icon { - position: relative; - width: 32px; - margin-right: 8px; - } - - > .indicator { - position: absolute; - top: 0; - left: 20px; - color: var(--navIndicator); - font-size: 8px; - animation: blink 1s infinite; - } - - > .text { - position: relative; - font-size: 0.9em; - } - - &:hover { - text-decoration: none; - color: var(--navHoverFg); - } - - &.active { - color: var(--navActive); - } - - &:hover, &.active { - &:before { - content: ""; - display: block; - width: calc(100% - 24px); - height: 100%; - margin: auto; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 999px; - background: var(--accentedBg); - } - } - } + &:hover, &.active { + &:before { + background: var(--accentLighten); } } } + +.postIcon { + position: relative; + margin-left: 30px; + margin-right: 8px; + width: 32px; +} + +.account { + position: relative; + display: flex; + align-items: center; + padding-left: 30px; + width: 100%; + text-align: left; + box-sizing: border-box; + margin-top: 16px; +} + +.avatar { + display: block; + flex-shrink: 0; + position: relative; + width: 32px; + aspect-ratio: 1; + margin-right: 8px; +} + +.acct { + display: block; + flex-shrink: 1; + padding-right: 8px; +} + +.middle { + flex: 1; +} + +.divider { + margin: 16px 16px; + border-top: solid 0.5px var(--divider); +} + +.item { + position: relative; + display: block; + padding-left: 24px; + line-height: 2.85rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + text-align: left; + box-sizing: border-box; + color: var(--navFg); + + &:hover { + text-decoration: none; + color: var(--navHoverFg); + } + + &.active { + color: var(--navActive); + } + + &:hover, &.active { + &:before { + content: ""; + display: block; + width: calc(100% - 24px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: var(--accentedBg); + } + } +} + +.itemIcon { + position: relative; + width: 32px; + margin-right: 8px; +} + +.itemIndicator { + position: absolute; + top: 0; + left: 20px; + color: var(--navIndicator); + font-size: 8px; + animation: blink 1s infinite; +} + +.itemText { + position: relative; + font-size: 0.9em; +} </style> diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index 3b4b16142..a184f1d2f 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -1,51 +1,50 @@ <template> -<div class="mvcprjjd" :class="{ iconOnly }"> - <div class="body"> - <div class="top"> - <div class="banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div> - <button v-click-anime v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="item _button instance" @click="openInstanceMenu"> - <img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/> +<div :class="[$style.root, { [$style.iconOnly]: iconOnly }]"> + <div :class="$style.body"> + <div :class="$style.top"> + <div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div> + <button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu"> + <img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/> </button> </div> - <div class="middle"> - <MkA v-click-anime v-tooltip.noDelay.right="i18n.ts.timeline" class="item index" active-class="active" to="/" exact> - <i class="icon ti ti-home ti-fw"></i><span class="text">{{ i18n.ts.timeline }}</span> + <div :class="$style.middle"> + <MkA v-tooltip.noDelay.right="i18n.ts.timeline" :class="$style.item" :activeClass="$style.active" to="/" exact> + <i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span> </MkA> <template v-for="item in menu"> - <div v-if="item === '-'" class="divider"></div> + <div v-if="item === '-'" :class="$style.divider"></div> <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" - v-click-anime v-tooltip.noDelay.right="navbarItemDef[item].title" - class="item _button" - :class="[item, { active: navbarItemDef[item].active }]" - active-class="active" + class="_button" + :class="[$style.item, { [$style.active]: navbarItemDef[item].active }]" + :activeClass="$style.active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}" > - <i class="icon ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span> - <span v-if="navbarItemDef[item].indicated" class="indicator"><i class="icon _indicatorCircle"></i></span> + <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span> + <span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span> </component> </template> - <div class="divider"></div> - <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime v-tooltip.noDelay.right="i18n.ts.controlPanel" class="item" active-class="active" to="/admin"> - <i class="icon ti ti-dashboard ti-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span> + <div :class="$style.divider"></div> + <MkA v-if="$i.isAdmin || $i.isModerator" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin"> + <i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span> </MkA> - <button v-click-anime class="item _button" @click="more"> - <i class="icon ti ti-grid-dots ti-fw"></i><span class="text">{{ i18n.ts.more }}</span> - <span v-if="otherMenuItemIndicated" class="indicator"><i class="icon _indicatorCircle"></i></span> + <button class="_button" :class="$style.item" @click="more"> + <i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span> + <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span> </button> - <MkA v-click-anime v-tooltip.noDelay.right="i18n.ts.settings" class="item" active-class="active" to="/settings"> - <i class="icon ti ti-settings ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span> + <MkA v-tooltip.noDelay.right="i18n.ts.settings" :class="$style.item" :activeClass="$style.active" to="/settings"> + <i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span> </MkA> </div> - <div class="bottom"> - <button v-tooltip.noDelay.right="i18n.ts.note" class="item _button post" data-cy-open-post-form @click="os.post"> - <i class="icon ti ti-pencil ti-fw"></i><span class="text">{{ i18n.ts.note }}</span> + <div :class="$style.bottom"> + <button v-tooltip.noDelay.right="i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="os.post"> + <i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span> </button> - <button v-click-anime v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="item _button account" @click="openAccountMenu"> - <MkAvatar :user="$i" class="avatar"/><MkAcct class="text _nowrap" :user="$i"/> + <button v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu"> + <MkAvatar :user="$i" :class="$style.avatar"/><MkAcct class="_nowrap" :class="$style.acct" :user="$i"/> </button> </div> </div> @@ -99,374 +98,376 @@ function more(ev: MouseEvent) { } </script> -<style lang="scss" scoped> -.mvcprjjd { - $nav-width: 250px; - $nav-icon-only-width: 80px; +<style lang="scss" module> +.root { + --nav-width: 250px; + --nav-icon-only-width: 72px; - flex: 0 0 $nav-width; - width: $nav-width; + flex: 0 0 var(--nav-width); + width: var(--nav-width); box-sizing: border-box; +} - > .body { - position: fixed; +.body { + position: fixed; + top: 0; + left: 0; + z-index: 1001; + width: var(--nav-icon-only-width); + height: 100dvh; + box-sizing: border-box; + overflow: auto; + overflow-x: clip; + overscroll-behavior: contain; + background: var(--navBg); + contain: strict; + display: flex; + flex-direction: column; +} + +.root:not(.iconOnly) { + .body { + width: var(--nav-width); + } + + .top { + position: sticky; + top: 0; + z-index: 1; + padding: 20px 0; + background: var(--X14); + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(8px)); + } + + .banner { + position: absolute; top: 0; left: 0; - z-index: 1001; - width: $nav-icon-only-width; - height: 100dvh; - box-sizing: border-box; - overflow: auto; - overflow-x: clip; - background: var(--navBg); - contain: strict; + width: 100%; + height: 100%; + background-size: cover; + background-position: center center; + -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); + mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); + } + + .instance { + position: relative; + display: block; + text-align: center; + width: 100%; + } + + .instanceIcon { + display: inline-block; + width: 38px; + aspect-ratio: 1; + } + + .bottom { + position: sticky; + bottom: 0; + padding: 20px 0; + background: var(--X14); + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(8px)); + } + + .post { + position: relative; + display: block; + width: 100%; + height: 40px; + color: var(--fgOnAccent); + font-weight: bold; + text-align: left; + + &:before { + content: ""; + display: block; + width: calc(100% - 38px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + } + + &:hover, &.active { + &:before { + background: var(--accentLighten); + } + } + } + + .postIcon { + position: relative; + margin-left: 30px; + margin-right: 8px; + width: 32px; + } + + .postText { + position: relative; + } + + .account { + position: relative; display: flex; - flex-direction: column; + align-items: center; + padding-left: 30px; + width: 100%; + text-align: left; + box-sizing: border-box; + margin-top: 16px; } - &:not(.iconOnly) { - > .body { - width: $nav-width; + .avatar { + display: block; + flex-shrink: 0; + position: relative; + width: 32px; + aspect-ratio: 1; + margin-right: 8px; + } - > .top { - position: sticky; + .acct { + display: block; + flex-shrink: 1; + padding-right: 8px; + } + + .middle { + flex: 1; + } + + .divider { + margin: 16px 16px; + border-top: solid 0.5px var(--divider); + } + + .item { + position: relative; + display: block; + padding-left: 30px; + line-height: 2.85rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + text-align: left; + box-sizing: border-box; + color: var(--navFg); + + &:hover { + text-decoration: none; + color: var(--navHoverFg); + } + + &.active { + color: var(--navActive); + } + + &:hover, &.active { + color: var(--accent); + + &:before { + content: ""; + display: block; + width: calc(100% - 34px); + height: 100%; + margin: auto; + position: absolute; top: 0; - z-index: 1; - padding: 20px 0; - background: var(--X14); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); - - > .banner { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-size: cover; - background-position: center center; - -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); - mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%); - } - - > .instance { - position: relative; - display: block; - text-align: center; - width: 100%; - - > .icon { - display: inline-block; - width: 38px; - aspect-ratio: 1; - } - } - } - - > .bottom { - position: sticky; + left: 0; + right: 0; bottom: 0; - padding: 20px 0; - background: var(--X14); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); - - > .post { - position: relative; - display: block; - width: 100%; - height: 40px; - color: var(--fgOnAccent); - font-weight: bold; - text-align: left; - - &:before { - content: ""; - display: block; - width: calc(100% - 38px); - height: 100%; - margin: auto; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 999px; - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); - } - - &:hover, &.active { - &:before { - background: var(--accentLighten); - } - } - - > .icon { - position: relative; - margin-left: 30px; - margin-right: 8px; - width: 32px; - } - - > .text { - position: relative; - } - } - - > .account { - position: relative; - display: flex; - align-items: center; - padding-left: 30px; - width: 100%; - text-align: left; - box-sizing: border-box; - margin-top: 16px; - - > .avatar { - display: block; - flex-shrink: 0; - position: relative; - width: 32px; - aspect-ratio: 1; - margin-right: 8px; - } - - > .text { - display: block; - flex-shrink: 1; - padding-right: 8px; - } - } - } - - > .middle { - flex: 1; - - > .divider { - margin: 16px 16px; - border-top: solid 0.5px var(--divider); - } - - > .item { - position: relative; - display: block; - padding-left: 30px; - line-height: 2.85rem; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - width: 100%; - text-align: left; - box-sizing: border-box; - color: var(--navFg); - - > .icon { - position: relative; - width: 32px; - margin-right: 8px; - } - - > .indicator { - position: absolute; - top: 0; - left: 20px; - color: var(--navIndicator); - font-size: 8px; - animation: blink 1s infinite; - } - - > .text { - position: relative; - font-size: 0.9em; - } - - &:hover { - text-decoration: none; - color: var(--navHoverFg); - } - - &.active { - color: var(--navActive); - } - - &:hover, &.active { - color: var(--accent); - - &:before { - content: ""; - display: block; - width: calc(100% - 34px); - height: 100%; - margin: auto; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 999px; - background: var(--accentedBg); - } - } - } + border-radius: 999px; + background: var(--accentedBg); } } } - &.iconOnly { - flex: 0 0 $nav-icon-only-width; - width: $nav-icon-only-width; + .itemIcon { + position: relative; + width: 32px; + margin-right: 8px; + } - > .body { - width: $nav-icon-only-width; + .itemIndicator { + position: absolute; + top: 0; + left: 20px; + color: var(--navIndicator); + font-size: 8px; + animation: blink 1s infinite; + } - > .top { - position: sticky; - top: 0; - z-index: 1; - padding: 20px 0; - background: var(--X14); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); + .itemText { + position: relative; + font-size: 0.9em; + } +} - > .instance { - display: block; - text-align: center; - width: 100%; +.root.iconOnly { + flex: 0 0 var(--nav-icon-only-width); + width: var(--nav-icon-only-width); - > .icon { - display: inline-block; - width: 30px; - aspect-ratio: 1; - } - } - } + .body { + width: var(--nav-icon-only-width); + } - > .bottom { - position: sticky; - bottom: 0; - padding: 20px 0; - background: var(--X14); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); + .top { + position: sticky; + top: 0; + z-index: 1; + padding: 20px 0; + background: var(--X14); + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(8px)); + } - > .post { - display: block; - position: relative; - width: 100%; - height: 52px; - margin-bottom: 16px; - text-align: center; + .instance { + display: block; + text-align: center; + width: 100%; + } - &:before { - content: ""; - display: block; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - margin: auto; - width: 52px; - aspect-ratio: 1/1; - border-radius: 100%; - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); - } + .instanceIcon { + display: inline-block; + width: 30px; + aspect-ratio: 1; + } - &:hover, &.active { - &:before { - background: var(--accentLighten); - } - } + .bottom { + position: sticky; + bottom: 0; + padding: 20px 0; + background: var(--X14); + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(8px)); + } - > .icon { - position: relative; - color: var(--fgOnAccent); - } + .post { + display: block; + position: relative; + width: 100%; + height: 52px; + margin-bottom: 16px; + text-align: center; - > .text { - display: none; - } - } + &:before { + content: ""; + display: block; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + width: 52px; + aspect-ratio: 1/1; + border-radius: 100%; + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + } - > .account { - display: block; - text-align: center; - width: 100%; - - > .avatar { - display: inline-block; - width: 38px; - aspect-ratio: 1; - } - - > .text { - display: none; - } - } - } - - > .middle { - flex: 1; - - > .divider { - margin: 8px auto; - width: calc(100% - 32px); - border-top: solid 0.5px var(--divider); - } - - > .item { - display: block; - position: relative; - padding: 18px 0; - width: 100%; - text-align: center; - - > .icon { - display: block; - margin: 0 auto; - opacity: 0.7; - } - - > .text { - display: none; - } - - > .indicator { - position: absolute; - top: 6px; - left: 24px; - color: var(--navIndicator); - font-size: 8px; - animation: blink 1s infinite; - } - - &:hover, &.active { - text-decoration: none; - color: var(--accent); - - &:before { - content: ""; - display: block; - height: 100%; - aspect-ratio: 1; - margin: auto; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 999px; - background: var(--accentedBg); - } - - > .icon, > .text { - opacity: 1; - } - } - } + &:hover, &.active { + &:before { + background: var(--accentLighten); } } } + + .postIcon { + position: relative; + color: var(--fgOnAccent); + } + + .postText { + display: none; + } + + .account { + display: block; + text-align: center; + width: 100%; + } + + .avatar { + display: inline-block; + width: 38px; + aspect-ratio: 1; + } + + .acct { + display: none; + } + + .middle { + flex: 1; + } + + .divider { + margin: 8px auto; + width: calc(100% - 32px); + border-top: solid 0.5px var(--divider); + } + + .item { + display: block; + position: relative; + padding: 18px 0; + width: 100%; + text-align: center; + + &:hover, &.active { + text-decoration: none; + color: var(--accent); + + &:before { + content: ""; + display: block; + height: 100%; + aspect-ratio: 1; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: var(--accentedBg); + } + + > .icon, + > .text { + opacity: 1; + } + } + } + + .itemIcon { + display: block; + margin: 0 auto; + opacity: 0.7; + } + + .itemText { + display: none; + } + + .itemIndicator { + position: absolute; + top: 6px; + left: 24px; + color: var(--navIndicator); + font-size: 8px; + animation: blink 1s infinite; + } } </style> diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue index fe95460ba..6f2e4bc9a 100644 --- a/packages/frontend/src/ui/_common_/statusbar-federation.vue +++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue @@ -1,14 +1,20 @@ <template> -<span v-if="!fetching" class="nmidsaqw"> +<span v-if="!fetching" :class="$style.root"> <template v-if="display === 'marquee'"> - <Transition name="change" mode="default"> + <Transition + :enterActiveClass="$style.transition_change_enterActive" + :leaveActiveClass="$style.transition_change_leaveActive" + :enterFromClass="$style.transition_change_enterFrom" + :leaveToClass="$style.transition_change_leaveTo" + mode="default" + > <MarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse"> - <span v-for="instance in instances" :key="instance.id" class="item" :class="{ colored }" :style="{ background: colored ? instance.themeColor : null }"> - <img class="icon" :src="getInstanceIcon(instance)" alt=""/> - <MkA :to="`/instance-info/${instance.host}`" class="host _monospace"> + <span v-for="instance in instances" :key="instance.id" :class="[$style.item, { [$style.colored]: colored }]" :style="{ background: colored ? instance.themeColor : null }"> + <img :class="$style.icon" :src="getInstanceIcon(instance)" alt=""/> + <MkA :to="`/instance-info/${instance.host}`" :class="$style.host" class="_monospace"> {{ instance.host }} </MkA> - <span class="divider"></span> + <span></span> </span> </MarqueeText> </Transition> @@ -61,46 +67,47 @@ function getInstanceIcon(instance): string { } </script> -<style lang="scss" scoped> -.change-enter-active, .change-leave-active { +<style lang="scss" module> +.transition_change_enterActive, +.transition_change_leaveActive { position: absolute; top: 0; transition: all 1s ease; } -.change-enter-from { - opacity: 0; +.transition_change_enterFrom { + opacity: 0; transform: translateY(-100%); } -.change-leave-to { - opacity: 0; +.transition_change_leaveTo { + opacity: 0; transform: translateY(100%); } -.nmidsaqw { +.root { display: inline-block; position: relative; +} - ::v-deep(.item) { - display: inline-block; - vertical-align: bottom; - margin-right: 5em; +.item { + display: inline-block; + vertical-align: bottom; + margin-right: 5em; - > .icon { - display: inline-block; - height: var(--height); - aspect-ratio: 1; - vertical-align: bottom; - margin-right: 1em; - } - - > .host { - vertical-align: bottom; - } - - &.colored { - padding-right: 1em; - color: #fff; - } + &.colored { + padding-right: 1em; + color: #fff; } } + +.icon { + display: inline-block; + height: var(--height); + aspect-ratio: 1; + vertical-align: bottom; + margin-right: 1em; +} + +.host { + vertical-align: bottom; +} </style> diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue index 44b6b278e..82473b609 100644 --- a/packages/frontend/src/ui/_common_/statusbar-rss.vue +++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue @@ -1,10 +1,16 @@ <template> -<span v-if="!fetching" class="xbhtxfms"> +<span v-if="!fetching" :class="$style.root"> <template v-if="display === 'marquee'"> - <Transition name="change" mode="default"> + <Transition + :enterActiveClass="$style.transition_change_enterActive" + :leaveActiveClass="$style.transition_change_leaveActive" + :enterFromClass="$style.transition_change_enterFrom" + :leaveToClass="$style.transition_change_leaveTo" + mode="default" + > <MarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse"> - <span v-for="item in items" class="item"> - <a class="link" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span class="divider"></span> + <span v-for="item in items" :class="$style.item"> + <a :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span :class="$style.divider"></span> </span> </MarqueeText> </Transition> @@ -54,39 +60,40 @@ useInterval(tick, Math.max(5000, props.refreshIntervalSec * 1000), { }); </script> -<style lang="scss" scoped> -.change-enter-active, .change-leave-active { +<style lang="scss" module> +.transition_change_enterActive, +.transition_change_leaveActive { position: absolute; top: 0; transition: all 1s ease; } -.change-enter-from { - opacity: 0; +.transition_change_enterFrom { + opacity: 0; transform: translateY(-100%); } -.change-leave-to { - opacity: 0; +.transition_change_leaveTo { + opacity: 0; transform: translateY(100%); } -.xbhtxfms { +.root { display: inline-block; position: relative; +} - ::v-deep(.item) { - display: inline-flex; - align-items: center; - vertical-align: bottom; - margin: 0; +.item { + display: inline-flex; + align-items: center; + vertical-align: bottom; + margin: 0; +} - > .divider { - display: inline-block; - width: 0.5px; - height: var(--height); - margin: 0 3em; - background: currentColor; - opacity: 0.3; - } - } +.divider { + display: inline-block; + width: 0.5px; + height: var(--height); + margin: 0 3em; + background: currentColor; + opacity: 0.3; } </style> diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue index 16df69d96..9ac744943 100644 --- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue +++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue @@ -1,14 +1,20 @@ <template> -<span v-if="!fetching" class="osdsvwzy"> +<span v-if="!fetching" :class="$style.root"> <template v-if="display === 'marquee'"> - <Transition name="change" mode="default"> + <Transition + :enterActiveClass="$style.transition_change_enterActive" + :leaveActiveClass="$style.transition_change_leaveActive" + :enterFromClass="$style.transition_change_enterFrom" + :leaveToClass="$style.transition_change_leaveTo" + mode="default" + > <MarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse"> - <span v-for="note in notes" :key="note.id" class="item"> - <img class="avatar" :src="note.user.avatarUrl" decoding="async"/> - <MkA class="text" :to="notePage(note)"> - <Mfm class="text" :text="getNoteSummary(note)" :plain="true" :nowrap="true"/> + <span v-for="note in notes" :key="note.id" :class="$style.item"> + <img :class="$style.avatar" :src="note.user.avatarUrl" decoding="async"/> + <MkA :class="$style.text" :to="notePage(note)"> + <Mfm :text="getNoteSummary(note)" :plain="true" :nowrap="true"/> </MkA> - <span class="divider"></span> + <span :class="$style.divider"></span> </span> </MarqueeText> </Transition> @@ -60,54 +66,53 @@ useInterval(tick, Math.max(5000, props.refreshIntervalSec * 1000), { }); </script> -<style lang="scss" scoped> -.change-enter-active, .change-leave-active { +<style lang="scss" module> +.transition_change_enterActive, +.transition_change_leaveActive { position: absolute; top: 0; transition: all 1s ease; } -.change-enter-from { - opacity: 0; +.transition_change_enterFrom { + opacity: 0; transform: translateY(-100%); } -.change-leave-to { - opacity: 0; +.transition_change_leaveTo { + opacity: 0; transform: translateY(100%); } -.osdsvwzy { +.root { display: inline-block; position: relative; +} - ::v-deep(.item) { - display: inline-flex; - align-items: center; - vertical-align: bottom; - margin: 0; +.item { + display: inline-flex; + align-items: center; + vertical-align: bottom; + margin: 0; +} - > .avatar { - display: inline-block; - height: var(--height); - aspect-ratio: 1; - vertical-align: bottom; - margin-right: 8px; - } +.avatar { + display: inline-block; + height: var(--height); + aspect-ratio: 1; + vertical-align: bottom; + margin-right: 8px; +} - > .text { - > .text { - display: inline-block; - vertical-align: bottom; - } - } +.text { + display: inline-block; + vertical-align: bottom; +} - > .divider { - display: inline-block; - width: 0.5px; - height: 16px; - margin: 0 3em; - background: currentColor; - opacity: 0; - } - } +.divider { + display: inline-block; + width: 0.5px; + height: 16px; + margin: 0 3em; + background: currentColor; + opacity: 0; } </style> diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue index f84695c15..3533972cd 100644 --- a/packages/frontend/src/ui/_common_/statusbars.vue +++ b/packages/frontend/src/ui/_common_/statusbars.vue @@ -1,18 +1,17 @@ <template> -<div class="dlrsnxqu"> +<div :class="$style.root"> <div - v-for="x in defaultStore.reactiveState.statusbars.value" :key="x.id" class="item" :class="[{ black: x.black }, { - verySmall: x.size === 'verySmall', - small: x.size === 'small', - medium: x.size === 'medium', - large: x.size === 'large', - veryLarge: x.size === 'veryLarge', + v-for="x in defaultStore.reactiveState.statusbars.value" :key="x.id" :class="[$style.item, { [$style.black]: x.black, + [$style.verySmall]: x.size === 'verySmall', + [$style.small]: x.size === 'small', + [$style.large]: x.size === 'large', + [$style.veryLarge]: x.size === 'veryLarge', }]" > - <span class="name">{{ x.name }}</span> - <XRss v-if="x.type === 'rss'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :url="x.props.url" :shuffle="x.props.shuffle"/> - <XFederation v-else-if="x.type === 'federation'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :colored="x.props.colored"/> - <XUserList v-else-if="x.type === 'userList'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :user-list-id="x.props.userListId"/> + <span :class="$style.name">{{ x.name }}</span> + <XRss v-if="x.type === 'rss'" :class="$style.body" :refreshIntervalSec="x.props.refreshIntervalSec" :marqueeDuration="x.props.marqueeDuration" :marqueeReverse="x.props.marqueeReverse" :display="x.props.display" :url="x.props.url" :shuffle="x.props.shuffle"/> + <XFederation v-else-if="x.type === 'federation'" :class="$style.body" :refreshIntervalSec="x.props.refreshIntervalSec" :marqueeDuration="x.props.marqueeDuration" :marqueeReverse="x.props.marqueeReverse" :display="x.props.display" :colored="x.props.colored"/> + <XUserList v-else-if="x.type === 'userList'" :class="$style.body" :refreshIntervalSec="x.props.refreshIntervalSec" :marqueeDuration="x.props.marqueeDuration" :marqueeReverse="x.props.marqueeReverse" :display="x.props.display" :userListId="x.props.userListId"/> </div> </div> </template> @@ -25,67 +24,67 @@ const XFederation = defineAsyncComponent(() => import('./statusbar-federation.vu const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')); </script> -<style lang="scss" scoped> -.dlrsnxqu { +<style lang="scss" module> +.root { font-size: 15px; background: var(--panel); +} - > .item { - --height: 24px; - --nameMargin: 10px; - font-size: 0.85em; +.item { + --height: 24px; + --nameMargin: 10px; + font-size: 0.85em; - &.verySmall { - --nameMargin: 7px; - --height: 16px; - font-size: 0.75em; - } + &.verySmall { + --nameMargin: 7px; + --height: 16px; + font-size: 0.75em; + } - &.small { - --nameMargin: 8px; - --height: 20px; - font-size: 0.8em; - } + &.small { + --nameMargin: 8px; + --height: 20px; + font-size: 0.8em; + } - &.large { - --nameMargin: 12px; - --height: 26px; - font-size: 0.875em; - } + &.large { + --nameMargin: 12px; + --height: 26px; + font-size: 0.875em; + } - &.veryLarge { - --nameMargin: 14px; - --height: 30px; - font-size: 0.9em; - } + &.veryLarge { + --nameMargin: 14px; + --height: 30px; + font-size: 0.9em; + } - display: flex; - vertical-align: bottom; - width: 100%; - line-height: var(--height); - height: var(--height); - overflow: clip; - contain: strict; + display: flex; + vertical-align: bottom; + width: 100%; + line-height: var(--height); + height: var(--height); + overflow: clip; + contain: strict; - > .name { - padding: 0 var(--nameMargin); - font-weight: bold; - color: var(--accent); - - &:empty { - display: none; - } - } - - > .body { - min-width: 0; - flex: 1; - } - - &.black { - background: #000; - color: #fff; - } + &.black { + background: #000; + color: #fff; } } + +.name { + padding: 0 var(--nameMargin); + font-weight: bold; + color: var(--accent); + + &:empty { + display: none; + } +} + +.body { + min-width: 0; + flex: 1; +} </style> diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue index 2a856e2a4..74c475fc7 100644 --- a/packages/frontend/src/ui/_common_/stream-indicator.vue +++ b/packages/frontend/src/ui/_common_/stream-indicator.vue @@ -2,15 +2,15 @@ <div v-if="hasDisconnected && defaultStore.state.serverDisconnectedBehavior === 'quiet'" :class="$style.root" class="_panel _shadow" @click="resetDisconnected"> <div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.disconnectedFromServer }}</div> <div :class="$style.command" class="_buttons"> - <MkButton :class="$style.commandButton" small primary @click="reload">{{ i18n.ts.reload }}</MkButton> - <MkButton :class="$style.commandButton" small>{{ i18n.ts.doNothing }}</MkButton> + <MkButton small primary @click="reload">{{ i18n.ts.reload }}</MkButton> + <MkButton small>{{ i18n.ts.doNothing }}</MkButton> </div> </div> </template> <script lang="ts" setup> import { onUnmounted } from 'vue'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { i18n } from '@/i18n'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os'; @@ -32,10 +32,10 @@ function reload() { location.reload(); } -stream.on('_disconnected_', onDisconnected); +useStream().on('_disconnected_', onDisconnected); onUnmounted(() => { - stream.off('_disconnected_', onDisconnected); + useStream().off('_disconnected_', onDisconnected); }); </script> @@ -54,7 +54,4 @@ onUnmounted(() => { .command { margin-top: 8px; } - -.commandButton { -} </style> diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue index daea77555..747d4edcb 100644 --- a/packages/frontend/src/ui/classic.header.vue +++ b/packages/frontend/src/ui/classic.header.vue @@ -5,18 +5,18 @@ <button v-click-anime class="item _button instance" @click="openInstanceMenu"> <img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" class="_ghost"/> </button> - <MkA v-click-anime v-tooltip="i18n.ts.timeline" class="item index" active-class="active" to="/" exact> + <MkA v-click-anime v-tooltip="i18n.ts.timeline" class="item index" activeClass="active" to="/" exact> <i class="ti ti-home ti-fw"></i> </MkA> <template v-for="item in menu"> <div v-if="item === '-'" class="divider"></div> - <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="navbarItemDef[item].title" class="item _button" :class="item" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> + <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="navbarItemDef[item].title" class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> <i class="ti-fw" :class="navbarItemDef[item].icon"></i> <span v-if="navbarItemDef[item].indicated" class="indicator"><i class="_indicatorCircle"></i></span> </component> </template> <div class="divider"></div> - <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime v-tooltip="i18n.ts.controlPanel" class="item" active-class="active" to="/admin" :behavior="settingsWindowed ? 'modalWindow' : null"> + <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime v-tooltip="i18n.ts.controlPanel" class="item" activeClass="active" to="/admin" :behavior="settingsWindowed ? 'window' : null"> <i class="ti ti-dashboard ti-fw"></i> </MkA> <button v-click-anime class="item _button" @click="more"> @@ -25,7 +25,7 @@ </button> </div> <div class="right"> - <MkA v-click-anime v-tooltip="i18n.ts.settings" class="item" active-class="active" to="/settings" :behavior="settingsWindowed ? 'modalWindow' : null"> + <MkA v-click-anime v-tooltip="i18n.ts.settings" class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null"> <i class="ti ti-settings ti-fw"></i> </MkA> <button v-click-anime class="item _button account" @click="openAccountMenu"> diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue index 73db14c65..cb264fc3b 100644 --- a/packages/frontend/src/ui/classic.sidebar.vue +++ b/packages/frontend/src/ui/classic.sidebar.vue @@ -9,25 +9,25 @@ </MkButton> </div> <div class="divider"></div> - <MkA v-click-anime class="item index" active-class="active" to="/" exact> + <MkA v-click-anime class="item index" activeClass="active" to="/" exact> <i class="ti ti-home ti-fw"></i><span class="text">{{ i18n.ts.timeline }}</span> </MkA> <template v-for="item in menu"> <div v-if="item === '-'" class="divider"></div> - <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="item" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> + <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> <i class="ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span> <span v-if="navbarItemDef[item].indicated" class="indicator"><i class="_indicatorCircle"></i></span> </component> </template> <div class="divider"></div> - <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin" :behavior="settingsWindowed ? 'modalWindow' : null"> + <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" activeClass="active" to="/admin" :behavior="settingsWindowed ? 'window' : null"> <i class="ti ti-dashboard ti-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span> </MkA> <button v-click-anime class="item _button" @click="more"> <i class="ti ti-dots ti-fw"></i><span class="text">{{ i18n.ts.more }}</span> <span v-if="otherNavItemIndicated" class="indicator"><i class="_indicatorCircle"></i></span> </button> - <MkA v-click-anime class="item" active-class="active" to="/settings" :behavior="settingsWindowed ? 'modalWindow' : null"> + <MkA v-click-anime class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null"> <i class="ti ti-settings ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span> </MkA> <div class="divider"></div> diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue index 792c1ccc5..d50f2b045 100644 --- a/packages/frontend/src/ui/classic.vue +++ b/packages/frontend/src/ui/classic.vue @@ -7,17 +7,17 @@ <XSidebar/> </div> <div v-else ref="widgetsLeft" class="widgets left"> - <XWidgets place="left" :margin-top="'var(--margin)'" @mounted="attachSticky(widgetsLeft)"/> + <XWidgets place="left" :marginTop="'var(--margin)'" @mounted="attachSticky(widgetsLeft)"/> </div> - <main class="main" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu"> + <main class="main" @contextmenu.stop="onContextmenu"> <div class="content" style="container-type: inline-size;"> <RouterView/> </div> </main> <div v-if="isDesktop" ref="widgetsRight" class="widgets right"> - <XWidgets :place="showMenuOnTop ? 'right' : null" :margin-top="showMenuOnTop ? '0' : 'var(--margin)'" @mounted="attachSticky(widgetsRight)"/> + <XWidgets :place="showMenuOnTop ? 'right' : null" :marginTop="showMenuOnTop ? '0' : 'var(--margin)'" @mounted="attachSticky(widgetsRight)"/> </div> </div> diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 33e752513..c82873177 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -4,27 +4,23 @@ <div :class="$style.main"> <XStatusBars/> - <div ref="columnsEl" :class="[$style.columns, deckStore.reactiveState.columnAlign.value, { [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu"> - <template v-for="ids in layout"> - <!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため --> - <section - v-if="ids.length > 1" - :class="$style.folder" - :style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }" - > - <DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/> - </section> - <DeckColumnCore - v-else - :ref="ids[0]" - :key="ids[0]" + <div ref="columnsEl" :class="[$style.sections, { [$style.center]: deckStore.reactiveState.columnAlign.value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu"> + <!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため --> + <section + v-for="ids in layout" + :class="$style.section" + :style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }" + > + <component + :is="columnComponents[columns.find(c => c.id === id)!.type] ?? XTlColumn" + v-for="id in ids" + :ref="id" + :key="id" :class="$style.column" - :column="columns.find(c => c.id === ids[0])" - :is-stacked="false" - :style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }" - @parent-focus="moveFocus(ids[0], $event)" + :column="columns.find(c => c.id === id)" + :isStacked="ids.length > 1" /> - </template> + </section> <div v-if="layout.length === 0" class="_panel" :class="$style.onboarding"> <div>{{ i18n.ts._deck.introduction }}</div> <MkButton primary style="margin: 1em auto;" @click="addColumn">{{ i18n.ts._deck.addColumn }}</MkButton> @@ -53,10 +49,10 @@ </div> <Transition - :enter-active-class="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterActive : ''" - :leave-active-class="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveActive : ''" - :enter-from-class="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterFrom : ''" - :leave-to-class="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveTo : ''" + :enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveTo : ''" > <div v-if="drawerMenuShowing" @@ -68,10 +64,10 @@ </Transition> <Transition - :enter-active-class="defaultStore.state.animation ? $style.transition_menuDrawer_enterActive : ''" - :leave-active-class="defaultStore.state.animation ? $style.transition_menuDrawer_leaveActive : ''" - :enter-from-class="defaultStore.state.animation ? $style.transition_menuDrawer_enterFrom : ''" - :leave-to-class="defaultStore.state.animation ? $style.transition_menuDrawer_leaveTo : ''" + :enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveTo : ''" > <div v-if="drawerMenuShowing" :class="$style.menu"> <XDrawerMenu/> @@ -87,7 +83,6 @@ import { computed, defineAsyncComponent, ref, watch } from 'vue'; import { v4 as uuid } from 'uuid'; import XCommon from './_common_/common.vue'; import { deckStore, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store'; -import DeckColumnCore from '@/ui/deck/column-core.vue'; import XSidebar from '@/ui/_common_/navbar.vue'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import MkButton from '@/components/MkButton.vue'; @@ -100,8 +95,31 @@ import { mainRouter } from '@/router'; import { unisonReload } from '@/scripts/unison-reload'; import { deviceKind } from '@/scripts/device-kind'; import { defaultStore } from '@/store'; +import XMainColumn from '@/ui/deck/main-column.vue'; +import XTlColumn from '@/ui/deck/tl-column.vue'; +import XAntennaColumn from '@/ui/deck/antenna-column.vue'; +import XListColumn from '@/ui/deck/list-column.vue'; +import XChannelColumn from '@/ui/deck/channel-column.vue'; +import XNotificationsColumn from '@/ui/deck/notifications-column.vue'; +import XWidgetsColumn from '@/ui/deck/widgets-column.vue'; +import XMentionsColumn from '@/ui/deck/mentions-column.vue'; +import XDirectColumn from '@/ui/deck/direct-column.vue'; +import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue'; const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); +const columnComponents = { + main: XMainColumn, + widgets: XWidgetsColumn, + notifications: XNotificationsColumn, + tl: XTlColumn, + list: XListColumn, + channel: XChannelColumn, + antenna: XAntennaColumn, + mentions: XMentionsColumn, + direct: XDirectColumn, + roleTimeline: XRoleTimelineColumn, +}; + mainRouter.navHook = (path, flag): boolean => { if (flag === 'forcePage') return false; const noMainColumn = !deckStore.state.columns.some(x => x.type === 'main'); @@ -187,11 +205,8 @@ window.addEventListener('wheel', (ev) => { columnsEl.scrollLeft += ev.deltaY; } }); -loadDeck(); -function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') { - // TODO?? -} +loadDeck(); function changeProfile(ev: MouseEvent) { const items = ref([{ @@ -267,7 +282,7 @@ async function deleteProfile() { --margin: var(--marginHalf); - --deckDividerThickness: 5px; + --columnGap: 6px; display: flex; height: 100dvh; @@ -286,19 +301,21 @@ async function deleteProfile() { flex-direction: column; } -.columns { +.sections { flex: 1; display: flex; overflow-x: auto; overflow-y: clip; + overscroll-behavior: contain; + background: var(--deckBg); &.center { - > .column:first-of-type { - margin-left: auto; + > .section:first-of-type { + margin-left: auto !important; } - > .column:last-of-type { - margin-right: auto; + > .section:last-of-type { + margin-right: auto !important; } } @@ -307,23 +324,17 @@ async function deleteProfile() { } } -.column { - scroll-snap-align: start; - flex-shrink: 0; - border-right: solid var(--deckDividerThickness) var(--deckDivider); - - &:first-of-type { - border-left: solid var(--deckDividerThickness) var(--deckDivider); - } -} - -.folder { - composes: column; +.section { display: flex; flex-direction: column; + scroll-snap-align: start; + flex-shrink: 0; + padding-top: var(--columnGap); + padding-bottom: var(--columnGap); + padding-left: var(--columnGap); - > *:not(:last-of-type) { - border-bottom: solid var(--deckDividerThickness) var(--deckDivider); + > .column:not(:last-of-type) { + margin-bottom: var(--columnGap); } } diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue index 76a8b6e76..d21a9cc58 100644 --- a/packages/frontend/src/ui/deck/antenna-column.vue +++ b/packages/frontend/src/ui/deck/antenna-column.vue @@ -1,10 +1,10 @@ <template> -<XColumn :menu="menu" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked"> <template #header> <i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> - <MkTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => emit('loaded')"/> + <MkTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId"/> </XColumn> </template> @@ -21,11 +21,6 @@ const props = defineProps<{ isStacked: boolean; }>(); -const emit = defineEmits<{ - (ev: 'loaded'): void; - (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; -}>(); - let timeline = $shallowRef<InstanceType<typeof MkTimeline>>(); onMounted(() => { diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue index 9605d1b22..8b05ecc0b 100644 --- a/packages/frontend/src/ui/deck/channel-column.vue +++ b/packages/frontend/src/ui/deck/channel-column.vue @@ -1,5 +1,5 @@ <template> -<XColumn :menu="menu" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked"> <template #header> <i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> @@ -8,30 +8,25 @@ <div style="padding: 8px; text-align: center;"> <MkButton primary gradate rounded inline @click="post"><i class="ti ti-pencil"></i></MkButton> </div> - <MkTimeline ref="timeline" src="channel" :channel="column.channelId" @after="() => emit('loaded')"/> + <MkTimeline ref="timeline" src="channel" :channel="column.channelId"/> </template> </XColumn> </template> <script lang="ts" setup> +import * as misskey from 'misskey-js'; import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store'; import MkTimeline from '@/components/MkTimeline.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os'; import { i18n } from '@/i18n'; -import * as misskey from 'misskey-js'; const props = defineProps<{ column: Column; isStacked: boolean; }>(); -const emit = defineEmits<{ - (ev: 'loaded'): void; - (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; -}>(); - let timeline = $shallowRef<InstanceType<typeof MkTimeline>>(); let channel = $shallowRef<misskey.entities.Channel>(); diff --git a/packages/frontend/src/ui/deck/column-core.vue b/packages/frontend/src/ui/deck/column-core.vue deleted file mode 100644 index 8e7addf35..000000000 --- a/packages/frontend/src/ui/deck/column-core.vue +++ /dev/null @@ -1,38 +0,0 @@ -<template> -<!-- TODO: リファクタの余地がありそう --> -<div v-if="!column">たぶん見えちゃいけないやつ</div> -<XMainColumn v-else-if="column.type === 'main'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/> -<XWidgetsColumn v-else-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/> -<XNotificationsColumn v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/> -<XTlColumn v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/> -<XListColumn v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/> -<XChannelColumn v-else-if="column.type === 'channel'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/> -<XAntennaColumn v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/> -<XMentionsColumn v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/> -<XDirectColumn v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/> -<XRoleTimelineColumn v-else-if="column.type === 'roleTimeline'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/> -</template> - -<script lang="ts" setup> -import { } from 'vue'; -import XMainColumn from './main-column.vue'; -import XTlColumn from './tl-column.vue'; -import XAntennaColumn from './antenna-column.vue'; -import XListColumn from './list-column.vue'; -import XChannelColumn from './channel-column.vue'; -import XNotificationsColumn from './notifications-column.vue'; -import XWidgetsColumn from './widgets-column.vue'; -import XMentionsColumn from './mentions-column.vue'; -import XDirectColumn from './direct-column.vue'; -import XRoleTimelineColumn from './role-timeline-column.vue'; -import { Column } from './deck-store'; - -defineProps<{ - column?: Column; - isStacked: boolean; -}>(); - -const emit = defineEmits<{ - (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; -}>(); -</script> diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index 402bbe035..c376eb2b4 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -1,8 +1,6 @@ <template> -<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため --> -<section - v-hotkey="keymap" - :class="[$style.root, { [$style.paged]: isMainColumn, [$style.naked]: naked, [$style.active]: active, [$style.isStacked]: isStacked, [$style.draghover]: draghover, [$style.dragging]: dragging, [$style.dropready]: dropready }]" +<div + :class="[$style.root, { [$style.paged]: isMainColumn, [$style.naked]: naked, [$style.active]: active, [$style.draghover]: draghover, [$style.dragging]: dragging, [$style.dropready]: dropready }]" @dragover.prevent.stop="onDragover" @dragleave="onDragleave" @drop.prevent.stop="onDrop" @@ -15,17 +13,26 @@ @dragend="onDragend" @contextmenu.prevent.stop="onContextmenu" > + <svg viewBox="0 0 256 128" :class="$style.tabShape"> + <g transform="matrix(6.2431,0,0,6.2431,-677.417,-29.3839)"> + <path d="M149.512,4.707L108.507,4.707C116.252,4.719 118.758,14.958 118.758,14.958C118.758,14.958 121.381,25.283 129.009,25.209L149.512,25.209L149.512,4.707Z" style="fill:var(--deckBg);"/> + </g> + </svg> + <div :class="$style.color"></div> <button v-if="isStacked && !isMainColumn" :class="$style.toggleActive" class="_button" @click="toggleActive"> <template v-if="active"><i class="ti ti-chevron-up"></i></template> <template v-else><i class="ti ti-chevron-down"></i></template> </button> <span :class="$style.title"><slot name="header"></slot></span> + <svg viewBox="0 0 16 16" version="1.1" :class="$style.grabber"> + <path fill="currentColor" d="M10 13a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm0-4a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm-4 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm5-9a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path> + </svg> <button v-tooltip="i18n.ts.settings" :class="$style.menu" class="_button" @click.stop="showSettingsMenu"><i class="ti ti-dots"></i></button> </header> - <div v-show="active" ref="body" :class="$style.body"> + <div v-if="active" ref="body" :class="$style.body"> <slot></slot> </div> -</section> +</div> </template> <script lang="ts" setup> @@ -49,12 +56,7 @@ const props = withDefaults(defineProps<{ naked: false, }); -const emit = defineEmits<{ - (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; - (ev: 'change-active-state', v: boolean): void; -}>(); - -let body = $shallowRef<HTMLDivElement>(); +let body = $shallowRef<HTMLDivElement | null>(); let dragging = $ref(false); watch($$(dragging), v => os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd')); @@ -64,14 +66,6 @@ let dropready = $ref(false); const isMainColumn = $computed(() => props.column.type === 'main'); const active = $computed(() => props.column.active !== false); -watch($$(active), v => emit('change-active-state', v)); - -const keymap = $computed(() => ({ - 'shift+up': () => emit('parent-focus', 'up'), - 'shift+down': () => emit('parent-focus', 'down'), - 'shift+left': () => emit('parent-focus', 'left'), - 'shift+right': () => emit('parent-focus', 'right'), -})); onMounted(() => { os.deckGlobalEvents.on('column.dragStart', onOtherDragStart); @@ -190,10 +184,12 @@ function onContextmenu(ev: MouseEvent) { } function goTop() { - body.scrollTo({ - top: 0, - behavior: 'smooth', - }); + if (body) { + body.scrollTo({ + top: 0, + behavior: 'smooth', + }); + } } function onDragstart(ev) { @@ -248,6 +244,7 @@ function onDrop(ev) { height: 100%; overflow: clip; contain: strict; + border-radius: 10px; &.draghover { &:after { @@ -287,6 +284,7 @@ function onDrop(ev) { &:not(.active) { flex-basis: var(--deckColumnHeaderHeight); min-height: var(--deckColumnHeaderHeight); + border-bottom-right-radius: 0; } &.naked { @@ -299,10 +297,28 @@ function onDrop(ev) { box-shadow: none; color: var(--fg); } + + > .body { + background: transparent !important; + + &::-webkit-scrollbar-track { + background: transparent; + } + scrollbar-color: var(--scrollbarHandle) transparent; + } } &.paged { background: var(--bg) !important; + + > .body { + background: var(--bg) !important; + + &::-webkit-scrollbar-track { + background: inherit; + } + scrollbar-color: var(--scrollbarHandle) transparent; + } } } @@ -312,7 +328,7 @@ function onDrop(ev) { z-index: 2; line-height: var(--deckColumnHeaderHeight); height: var(--deckColumnHeaderHeight); - padding: 0 16px; + padding: 0 16px 0 30px; font-size: 0.9em; color: var(--panelHeaderFg); background: var(--panelHeaderBg); @@ -321,6 +337,24 @@ function onDrop(ev) { user-select: none; } +.color { + position: absolute; + top: 12px; + left: 12px; + width: 3px; + height: calc(100% - 24px); + background: var(--accent); + border-radius: 999px; +} + +.tabShape { + position: absolute; + top: 0; + right: -8px; + width: auto; + height: calc(100% - 6px); +} + .title { display: inline-block; align-items: center; @@ -335,34 +369,39 @@ function onDrop(ev) { z-index: 1; width: var(--deckColumnHeaderHeight); line-height: var(--deckColumnHeaderHeight); - color: var(--faceTextButton); - - &:hover { - color: var(--faceTextButtonHover); - } - - &:active { - color: var(--faceTextButtonActive); - } } .toggleActive { margin-left: -16px; } -.menu { +.grabber { margin-left: auto; + margin-right: 10px; + padding: 8px 8px; + box-sizing: border-box; + height: var(--deckColumnHeaderHeight); + cursor: move; + user-select: none; + opacity: 0.5; +} + +.menu { margin-right: -16px; } .body { height: calc(100% - var(--deckColumnHeaderHeight)); overflow-y: auto; - overflow-x: hidden; // Safari does not supports clip overflow-x: clip; - -webkit-overflow-scrolling: touch; + overscroll-behavior-y: contain; box-sizing: border-box; container-type: size; background-color: var(--bg); + + &::-webkit-scrollbar-track { + background: var(--panel); + } + scrollbar-color: var(--scrollbarHandle) var(--panel); } </style> diff --git a/packages/frontend/src/ui/deck/direct-column.vue b/packages/frontend/src/ui/deck/direct-column.vue index 15b76c4d9..dc3f58e6a 100644 --- a/packages/frontend/src/ui/deck/direct-column.vue +++ b/packages/frontend/src/ui/deck/direct-column.vue @@ -1,5 +1,5 @@ <template> -<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)"> +<XColumn :column="column" :isStacked="isStacked"> <template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name }}</template> <MkNotes :pagination="pagination"/> @@ -17,10 +17,6 @@ defineProps<{ isStacked: boolean; }>(); -const emit = defineEmits<{ - (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; -}>(); - const pagination = { endpoint: 'notes/mentions' as const, limit: 10, diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index 352c1d246..f36dc6151 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -1,10 +1,10 @@ <template> -<XColumn :menu="menu" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked"> <template #header> <i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> - <MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => emit('loaded')"/> + <MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId"/> </XColumn> </template> @@ -21,11 +21,6 @@ const props = defineProps<{ isStacked: boolean; }>(); -const emit = defineEmits<{ - (ev: 'loaded'): void; - (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; -}>(); - let timeline = $shallowRef<InstanceType<typeof MkTimeline>>(); if (props.column.listId == null) { diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue index f3826a8d3..169fac70a 100644 --- a/packages/frontend/src/ui/deck/main-column.vue +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -1,5 +1,5 @@ <template> -<XColumn v-if="deckStore.state.alwaysShowMainColumn || mainRouter.currentRoute.value.name !== 'index'" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)"> +<XColumn v-if="deckStore.state.alwaysShowMainColumn || mainRouter.currentRoute.value.name !== 'index'" :column="column" :isStacked="isStacked"> <template #header> <template v-if="pageMetadata?.value"> <i :class="pageMetadata?.value.icon"></i> @@ -25,10 +25,6 @@ defineProps<{ isStacked: boolean; }>(); -const emit = defineEmits<{ - (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; -}>(); - let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); provide('router', mainRouter); diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue index 852d7a8f7..98cf89874 100644 --- a/packages/frontend/src/ui/deck/mentions-column.vue +++ b/packages/frontend/src/ui/deck/mentions-column.vue @@ -1,5 +1,5 @@ <template> -<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)"> +<XColumn :column="column" :isStacked="isStacked"> <template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name }}</template> <MkNotes :pagination="pagination"/> @@ -17,10 +17,6 @@ defineProps<{ isStacked: boolean; }>(); -const emit = defineEmits<{ - (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; -}>(); - const pagination = { endpoint: 'notes/mentions' as const, limit: 10, diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue index 9d133035f..8cf6ec1f6 100644 --- a/packages/frontend/src/ui/deck/notifications-column.vue +++ b/packages/frontend/src/ui/deck/notifications-column.vue @@ -1,8 +1,8 @@ <template> -<XColumn :column="column" :is-stacked="isStacked" :menu="menu" @parent-focus="$event => emit('parent-focus', $event)"> +<XColumn :column="column" :isStacked="isStacked" :menu="menu"> <template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template> - <XNotifications :include-types="column.includingTypes"/> + <XNotifications :includeTypes="column.includingTypes"/> </XColumn> </template> @@ -19,10 +19,6 @@ const props = defineProps<{ isStacked: boolean; }>(); -const emit = defineEmits<{ - (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; -}>(); - function func() { os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSettingWindow.vue')), { includingTypes: props.column.includingTypes, diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue index 5783b3f07..a0b7f1c67 100644 --- a/packages/frontend/src/ui/deck/role-timeline-column.vue +++ b/packages/frontend/src/ui/deck/role-timeline-column.vue @@ -1,10 +1,10 @@ <template> -<XColumn :menu="menu" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked"> <template #header> <i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> - <MkTimeline v-if="column.roleId" ref="timeline" src="role" :role="column.roleId" @after="() => emit('loaded')"/> + <MkTimeline v-if="column.roleId" ref="timeline" src="role" :role="column.roleId"/> </XColumn> </template> @@ -21,11 +21,6 @@ const props = defineProps<{ isStacked: boolean; }>(); -const emit = defineEmits<{ - (ev: 'loaded'): void; - (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; -}>(); - let timeline = $shallowRef<InstanceType<typeof MkTimeline>>(); onMounted(() => { @@ -35,7 +30,7 @@ onMounted(() => { }); async function setRole() { - const roles = await os.api('roles/list'); + const roles = (await os.api('roles/list')).filter(x => x.isExplorable); const { canceled, result: role } = await os.select({ title: i18n.ts.role, items: roles.map(x => ({ diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index c23943d4d..4844ad11f 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -1,5 +1,5 @@ <template> -<XColumn :menu="menu" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked"> <template #header> <i v-if="column.tl === 'home'" class="ti ti-home"></i> <i v-else-if="column.tl === 'local'" class="ti ti-planet"></i> @@ -15,7 +15,7 @@ </p> <p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p> </div> - <MkTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')"/> + <MkTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl"/> </XColumn> </template> @@ -34,11 +34,6 @@ const props = defineProps<{ isStacked: boolean; }>(); -const emit = defineEmits<{ - (ev: 'loaded'): void; - (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; -}>(); - let disabled = $ref(false); const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable)); diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue index 3b5b72799..da14e54f7 100644 --- a/packages/frontend/src/ui/deck/widgets-column.vue +++ b/packages/frontend/src/ui/deck/widgets-column.vue @@ -1,10 +1,10 @@ <template> -<XColumn :menu="menu" :naked="true" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)"> +<XColumn :menu="menu" :naked="true" :column="column" :isStacked="isStacked"> <template #header><i class="ti ti-apps" style="margin-right: 8px;"></i>{{ column.name }}</template> <div :class="$style.root"> <div v-if="!(column.widgets && column.widgets.length > 0) && !edit" :class="$style.intro">{{ i18n.ts._deck.widgetsIntroduction }}</div> - <XWidgets :edit="edit" :widgets="column.widgets ?? []" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/> + <XWidgets :edit="edit" :widgets="column.widgets ?? []" @addWidget="addWidget" @removeWidget="removeWidget" @updateWidget="updateWidget" @updateWidgets="updateWidgets" @exit="edit = false"/> </div> </XColumn> </template> @@ -21,10 +21,6 @@ const props = defineProps<{ isStacked: boolean; }>(); -const emit = defineEmits<{ - (ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void; -}>(); - let edit = $ref(false); function addWidget(widget) { diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue new file mode 100644 index 000000000..e656f00bb --- /dev/null +++ b/packages/frontend/src/ui/minimum.vue @@ -0,0 +1,34 @@ +<template> +<div :class="$style.root" style="container-type: inline-size;"> + <RouterView/> + + <XCommon/> +</div> +</template> + +<script lang="ts" setup> +import { provide, ComputedRef } from 'vue'; +import XCommon from './_common_/common.vue'; +import { mainRouter } from '@/router'; +import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata'; +import { instanceName } from '@/config'; + +let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); + +provide('router', mainRouter); +provideMetadataReceiver((info) => { + pageMetadata = info; + if (pageMetadata.value) { + document.title = `${pageMetadata.value.title} | ${instanceName}`; + } +}); + +document.documentElement.style.overflowY = 'scroll'; +</script> + +<style lang="scss" module> +.root { + min-height: 100dvh; + box-sizing: border-box; +} +</style> diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index 27d0c26ac..c0da59a57 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -1,19 +1,15 @@ <template> -<div :class="[$style.root, { [$style.withWallpaper]: wallpaper }]"> +<div :class="$style.root"> <XSidebar v-if="!isMobile" :class="$style.sidebar"/> - <MkStickyContainer :class="$style.contents"> + <MkStickyContainer ref="contents" :class="$style.contents" style="container-type: inline-size;" @contextmenu.stop="onContextmenu"> <template #header><XStatusBars :class="$style.statusbars"/></template> - <main style="min-width: 0;" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu"> - <div :class="$style.content" style="container-type: inline-size;"> - <RouterView/> - </div> - <div :class="$style.spacer"></div> - </main> + <RouterView/> + <div :class="$style.spacer"></div> </MkStickyContainer> - <div v-if="isDesktop" ref="widgetsEl" :class="$style.widgets"> - <XWidgets :margin-top="'var(--margin)'" @mounted="attachSticky"/> + <div v-if="isDesktop" :class="$style.widgets"> + <XWidgets/> </div> <button v-if="!isDesktop && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button> @@ -27,10 +23,10 @@ </div> <Transition - :enter-active-class="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterActive : ''" - :leave-active-class="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveActive : ''" - :enter-from-class="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterFrom : ''" - :leave-to-class="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveTo : ''" + :enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveTo : ''" > <div v-if="drawerMenuShowing" @@ -42,10 +38,10 @@ </Transition> <Transition - :enter-active-class="defaultStore.state.animation ? $style.transition_menuDrawer_enterActive : ''" - :leave-active-class="defaultStore.state.animation ? $style.transition_menuDrawer_leaveActive : ''" - :enter-from-class="defaultStore.state.animation ? $style.transition_menuDrawer_enterFrom : ''" - :leave-to-class="defaultStore.state.animation ? $style.transition_menuDrawer_leaveTo : ''" + :enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveTo : ''" > <div v-if="drawerMenuShowing" :class="$style.menuDrawer"> <XDrawerMenu/> @@ -53,10 +49,10 @@ </Transition> <Transition - :enter-active-class="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_enterActive : ''" - :leave-active-class="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_leaveActive : ''" - :enter-from-class="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_enterFrom : ''" - :leave-to-class="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_leaveTo : ''" + :enterActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_leaveTo : ''" > <div v-if="widgetsShowing" @@ -68,10 +64,10 @@ </Transition> <Transition - :enter-active-class="defaultStore.state.animation ? $style.transition_widgetsDrawer_enterActive : ''" - :leave-active-class="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveActive : ''" - :enter-from-class="defaultStore.state.animation ? $style.transition_widgetsDrawer_enterFrom : ''" - :leave-to-class="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveTo : ''" + :enterActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_enterActive : ''" + :leaveActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveActive : ''" + :enterFromClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_enterFrom : ''" + :leaveToClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveTo : ''" > <div v-if="widgetsShowing" :class="$style.widgetsDrawer"> <button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i class="ti ti-x"></i></button> @@ -84,10 +80,10 @@ </template> <script lang="ts" setup> -import { defineAsyncComponent, provide, onMounted, computed, ref, ComputedRef, watch, inject, Ref } from 'vue'; +import { defineAsyncComponent, provide, onMounted, computed, ref, ComputedRef, watch, shallowRef, Ref } from 'vue'; import XCommon from './_common_/common.vue'; +import type MkStickyContainer from '@/components/global/MkStickyContainer.vue'; import { instanceName } from '@/config'; -import { StickySidebar } from '@/scripts/sticky-sidebar'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import * as os from '@/os'; import { defaultStore } from '@/store'; @@ -99,6 +95,7 @@ import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata'; import { deviceKind } from '@/scripts/device-kind'; import { miLocalStorage } from '@/local-storage'; import { CURRENT_STICKY_BOTTOM } from '@/const'; + const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue')); const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); @@ -114,9 +111,9 @@ window.addEventListener('resize', () => { }); let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); -const widgetsEl = $shallowRef<HTMLElement>(); const widgetsShowing = $ref(false); const navFooter = $shallowRef<HTMLElement>(); +const contents = shallowRef<InstanceType<typeof MkStickyContainer>>(); provide('router', mainRouter); provideMetadataReceiver((info) => { @@ -140,8 +137,6 @@ mainRouter.on('change', () => { drawerMenuShowing.value = false; }); -document.documentElement.style.overflowY = 'scroll'; - if (window.innerWidth > 1024) { const tempUI = miLocalStorage.getItem('ui_temp'); if (tempUI) { @@ -197,19 +192,13 @@ const onContextmenu = (ev) => { }], ev); }; -const attachSticky = (el) => { - const sticky = new StickySidebar(widgetsEl); - window.addEventListener('scroll', () => { - sticky.calc(window.scrollY); - }, { passive: true }); -}; - function top() { - window.scroll({ top: 0, behavior: 'smooth' }); + contents.value.rootEl.scrollTo({ + top: 0, + behavior: 'smooth', + }); } -const wallpaper = miLocalStorage.getItem('wallpaper') != null; - let navFooterHeight = $ref(0); provide<Ref<number>>(CURRENT_STICKY_BOTTOM, $$(navFooterHeight)); @@ -275,28 +264,33 @@ $widgets-hide-threshold: 1090px; } .root { - min-height: 100dvh; + height: 100dvh; + overflow: clip; + contain: strict; box-sizing: border-box; display: flex; } -.withWallpaper { - background: var(--wallpaperOverlay); - //backdrop-filter: var(--blur, blur(4px)); -} - .sidebar { border-right: solid 0.5px var(--divider); } .contents { - width: 100%; + flex: 1; + height: 100%; min-width: 0; + overflow: auto; + overflow-y: scroll; + overscroll-behavior: contain; background: var(--bg); } .widgets { - padding: 0 var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)); + width: 350px; + height: 100%; + box-sizing: border-box; + overflow: auto; + padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)); border-left: solid 0.5px var(--divider); background: var(--bg); @@ -328,6 +322,7 @@ $widgets-hide-threshold: 1090px; top: 0; right: 0; z-index: 1001; + width: 310px; height: 100dvh; padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)) !important; box-sizing: border-box; diff --git a/packages/frontend/src/ui/universal.widgets.vue b/packages/frontend/src/ui/universal.widgets.vue index 3e0c38bb8..ec5e8bb03 100644 --- a/packages/frontend/src/ui/universal.widgets.vue +++ b/packages/frontend/src/ui/universal.widgets.vue @@ -1,6 +1,6 @@ <template> -<div :class="$style.root" :style="{ paddingTop: marginTop }"> - <XWidgets :class="$style.widgets" :edit="editMode" :widgets="widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/> +<div> + <XWidgets :edit="editMode" :widgets="widgets" @addWidget="addWidget" @removeWidget="removeWidget" @updateWidget="updateWidget" @updateWidgets="updateWidgets" @exit="editMode = false"/> <button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button> <button v-else class="_textButton" data-cy-widget-edit :class="$style.edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button> @@ -11,7 +11,7 @@ let editMode = $ref(false); </script> <script lang="ts" setup> -import { onMounted } from 'vue'; +import { } from 'vue'; import XWidgets from '@/components/MkWidgets.vue'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; @@ -21,28 +21,16 @@ const props = withDefaults(defineProps<{ // left = place: leftだけを表示 // right = rightとnullを表示 place?: 'left' | null | 'right'; - marginTop?: string; }>(), { place: null, - marginTop: '0', }); -const emit = defineEmits<{ - (ev: 'mounted', el?: Element): void; -}>(); - -let rootEl = $shallowRef<HTMLDivElement>(); - const widgets = $computed(() => { if (props.place === null) return defaultStore.reactiveState.widgets.value; if (props.place === 'left') return defaultStore.reactiveState.widgets.value.filter(w => w.place === 'left'); return defaultStore.reactiveState.widgets.value.filter(w => w.place !== 'left'); }); -onMounted(() => { - emit('mounted', rootEl); -}); - function addWidget(widget) { defaultStore.set('widgets', [{ ...widget, @@ -82,16 +70,6 @@ function updateWidgets(thisWidgets) { </script> <style lang="scss" module> -.root { - position: sticky; - height: min-content; - box-sizing: border-box; -} - -.widgets { - width: 300px; -} - .edit { width: 100%; } diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue index 623abbda3..d6de145ed 100644 --- a/packages/frontend/src/ui/visitor.vue +++ b/packages/frontend/src/ui/visitor.vue @@ -12,10 +12,10 @@ <div class="main"> <div v-if="!root" class="header"> <div v-if="narrow === false" class="wide"> - <MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i> {{ i18n.ts.home }}</MkA> - <MkA v-if="isTimelineAvailable" to="/timeline" class="link" active-class="active"><i class="ti ti-message icon"></i> {{ i18n.ts.timeline }}</MkA> - <MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i> {{ i18n.ts.explore }}</MkA> - <MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i> {{ i18n.ts.channel }}</MkA> + <MkA to="/" class="link" activeClass="active"><i class="ti ti-home icon"></i> {{ i18n.ts.home }}</MkA> + <MkA v-if="isTimelineAvailable" to="/timeline" class="link" activeClass="active"><i class="ti ti-message icon"></i> {{ i18n.ts.timeline }}</MkA> + <MkA to="/explore" class="link" activeClass="active"><i class="ti ti-hash icon"></i> {{ i18n.ts.explore }}</MkA> + <MkA to="/channels" class="link" activeClass="active"><i class="ti ti-device-tv icon"></i> {{ i18n.ts.channel }}</MkA> </div> <div v-else-if="narrow === true" class="narrow"> <button class="menu _button" @click="showMenu = true"> @@ -44,15 +44,15 @@ <Transition :name="'tray'"> <div v-if="showMenu" class="menu"> - <MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ i18n.ts.home }}</MkA> - <MkA v-if="isTimelineAvailable" to="/timeline" class="link" active-class="active"><i class="ti ti-message icon"></i>{{ i18n.ts.timeline }}</MkA> - <MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ i18n.ts.explore }}</MkA> - <MkA to="/announcements" class="link" active-class="active"><i class="ti ti-speakerphone icon"></i>{{ i18n.ts.announcements }}</MkA> - <MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ i18n.ts.channel }}</MkA> + <MkA to="/" class="link" activeClass="active"><i class="ti ti-home icon"></i>{{ i18n.ts.home }}</MkA> + <MkA v-if="isTimelineAvailable" to="/timeline" class="link" activeClass="active"><i class="ti ti-message icon"></i>{{ i18n.ts.timeline }}</MkA> + <MkA to="/explore" class="link" activeClass="active"><i class="ti ti-hash icon"></i>{{ i18n.ts.explore }}</MkA> + <MkA to="/announcements" class="link" activeClass="active"><i class="ti ti-speakerphone icon"></i>{{ i18n.ts.announcements }}</MkA> + <MkA to="/channels" class="link" activeClass="active"><i class="ti ti-device-tv icon"></i>{{ i18n.ts.channel }}</MkA> <div class="divider"></div> - <MkA to="/pages" class="link" active-class="active"><i class="ti ti-news icon"></i>{{ i18n.ts.pages }}</MkA> - <MkA to="/play" class="link" active-class="active"><i class="ti ti-player-play icon"></i>Play</MkA> - <MkA to="/gallery" class="link" active-class="active"><i class="ti ti-icons icon"></i>{{ i18n.ts.gallery }}</MkA> + <MkA to="/pages" class="link" activeClass="active"><i class="ti ti-news icon"></i>{{ i18n.ts.pages }}</MkA> + <MkA to="/play" class="link" activeClass="active"><i class="ti ti-player-play icon"></i>Play</MkA> + <MkA to="/gallery" class="link" activeClass="active"><i class="ti ti-icons icon"></i>{{ i18n.ts.gallery }}</MkA> <div class="action"> <button class="_buttonPrimary" @click="signup()">{{ i18n.ts.signup }}</button> <button class="_button" @click="signin()">{{ i18n.ts.login }}</button> diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue index 628390e3f..d516a5df7 100644 --- a/packages/frontend/src/ui/zen.vue +++ b/packages/frontend/src/ui/zen.vue @@ -1,9 +1,17 @@ <template> -<div class="mk-app" style="container-type: inline-size;"> +<div :class="showBottom ? $style.rootWithBottom : $style.root" style="container-type: inline-size;"> <RouterView/> <XCommon/> </div> + +<!-- + デッキUIが設定されている場合はデッキUIに戻れるようにする (ただし?zenが明示された場合は表示しない) + See https://github.com/misskey-dev/misskey/issues/10905 +--> +<div v-if="showBottom" :class="$style.bottom"> + <button v-tooltip="i18n.ts.goToMisskey" :class="['_button', '_shadow', $style.button]" @click="goToMisskey"><i class="ti ti-home"></i></button> +</div> </template> <script lang="ts" setup> @@ -11,10 +19,13 @@ import { provide, ComputedRef } from 'vue'; import XCommon from './_common_/common.vue'; import { mainRouter } from '@/router'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata'; -import { instanceName } from '@/config'; +import { instanceName, ui } from '@/config'; +import { i18n } from '@/i18n'; let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); +const showBottom = !(new URLSearchParams(location.search)).has('zen') && ui === 'deck'; + provide('router', mainRouter); provideMetadataReceiver((info) => { pageMetadata = info; @@ -23,12 +34,41 @@ provideMetadataReceiver((info) => { } }); +function goToMisskey() { + window.location.href = '/'; +} + document.documentElement.style.overflowY = 'scroll'; </script> -<style lang="scss" scoped> -.mk-app { +<style lang="scss" module> +.root { min-height: 100dvh; box-sizing: border-box; } + +.rootWithBottom { + min-height: calc(100dvh - (60px + (var(--margin) * 2) + env(safe-area-inset-bottom, 0px))); + box-sizing: border-box; +} + +.bottom { + height: calc(60px + (var(--margin) * 2) + env(safe-area-inset-bottom, 0px)); + width: 100%; + margin-top: auto; +} + +.button { + position: fixed !important; + padding: 0; + aspect-ratio: 1; + width: 100%; + max-width: 60px; + margin: auto; + border-radius: 100%; + background: var(--panel); + color: var(--fg); + right: var(--margin); + bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px)); +} </style> diff --git a/packages/frontend/src/unicode-emoji-indexes/en-US.json b/packages/frontend/src/unicode-emoji-indexes/en-US.json new file mode 100644 index 000000000..c5544418d --- /dev/null +++ b/packages/frontend/src/unicode-emoji-indexes/en-US.json @@ -0,0 +1,1784 @@ +{ + "😀": ["face", "smile", "happy", "joy", ": D", "grin"], + "😬": ["face", "grimace", "teeth"], + "😁": ["face", "happy", "smile", "joy", "kawaii"], + "😂": ["face", "cry", "tears", "weep", "happy", "happytears", "haha"], + "🤣": ["face", "rolling", "floor", "laughing", "lol", "haha"], + "🥳": ["face", "celebration", "woohoo"], + "😃": ["face", "happy", "joy", "haha", ": D", ": )", "smile", "funny"], + "😄": ["face", "happy", "joy", "funny", "haha", "laugh", "like", ": D", ": )"], + "😅": ["face", "hot", "happy", "laugh", "sweat", "smile", "relief"], + "🥲": ["face"], + "😆": ["happy", "joy", "lol", "satisfied", "haha", "face", "glad", "XD", "laugh"], + "😇": ["face", "angel", "heaven", "halo"], + "😉": ["face", "happy", "mischievous", "secret", ";)", "smile", "eye"], + "😊": ["face", "smile", "happy", "flushed", "crush", "embarrassed", "shy", "joy"], + "🙂": ["face", "smile"], + "🙃": ["face", "flipped", "silly", "smile"], + "☺️": ["face", "blush", "massage", "happiness"], + "😋": ["happy", "joy", "tongue", "smile", "face", "silly", "yummy", "nom", "delicious", "savouring"], + "😌": ["face", "relaxed", "phew", "massage", "happiness"], + "😍": ["face", "love", "like", "affection", "valentines", "infatuation", "crush", "heart"], + "🥰": ["face", "love", "like", "affection", "valentines", "infatuation", "crush", "hearts", "adore"], + "😘": ["face", "love", "like", "affection", "valentines", "infatuation", "kiss"], + "😗": ["love", "like", "face", "3", "valentines", "infatuation", "kiss"], + "😙": ["face", "affection", "valentines", "infatuation", "kiss"], + "😚": ["face", "love", "like", "affection", "valentines", "infatuation", "kiss"], + "😜": ["face", "prank", "childish", "playful", "mischievous", "smile", "wink", "tongue"], + "🤪": ["face", "goofy", "crazy"], + "🤨": ["face", "distrust", "scepticism", "disapproval", "disbelief", "surprise"], + "🧐": ["face", "stuffy", "wealthy"], + "😝": ["face", "prank", "playful", "mischievous", "smile", "tongue"], + "😛": ["face", "prank", "childish", "playful", "mischievous", "smile", "tongue"], + "🤑": ["face", "rich", "dollar", "money"], + "🤓": ["face", "nerdy", "geek", "dork"], + "🥸": ["face", "nose", "glasses", "incognito"], + "😎": ["face", "cool", "smile", "summer", "beach", "sunglass"], + "🤩": ["face", "smile", "starry", "eyes", "grinning"], + "🤡": ["face"], + "🤠": ["face", "cowgirl", "hat"], + "🤗": ["face", "smile", "hug"], + "😏": ["face", "smile", "mean", "prank", "smug", "sarcasm"], + "😶": ["face", "hellokitty"], + "😐": ["indifference", "meh", ": |", "neutral"], + "😑": ["face", "indifferent", "-_-", "meh", "deadpan"], + "😒": ["indifference", "bored", "straight face", "serious", "sarcasm", "unimpressed", "skeptical", "dubious", "side_eye"], + "🙄": ["face", "eyeroll", "frustrated"], + "🤔": ["face", "hmmm", "think", "consider"], + "🤥": ["face", "lie", "pinocchio"], + "🤭": ["face", "whoops", "shock", "surprise"], + "🤫": ["face", "quiet", "shhh"], + "🤬": ["face", "swearing", "cursing", "cussing", "profanity", "expletive"], + "🤯": ["face", "shocked", "mind", "blown"], + "😳": ["face", "blush", "shy", "flattered"], + "😞": ["face", "sad", "upset", "depressed", ": ("], + "😟": ["face", "concern", "nervous", ": ("], + "😠": ["mad", "face", "annoyed", "frustrated"], + "😡": ["angry", "mad", "hate", "despise"], + "😔": ["face", "sad", "depressed", "upset"], + "😕": ["face", "indifference", "huh", "weird", "hmmm", ": /"], + "🙁": ["face", "frowning", "disappointed", "sad", "upset"], + "☹": ["face", "sad", "upset", "frown"], + "😣": ["face", "sick", "no", "upset", "oops"], + "😖": ["face", "confused", "sick", "unwell", "oops", ": S"], + "😫": ["sick", "whine", "upset", "frustrated"], + "😩": ["face", "tired", "sleepy", "sad", "frustrated", "upset"], + "🥺": ["face", "begging", "mercy"], + "😤": ["face", "gas", "phew", "proud", "pride"], + "😮": ["face", "surprise", "impressed", "wow", "whoa", ": O"], + "😱": ["face", "munch", "scared", "omg"], + "😨": ["face", "scared", "terrified", "nervous", "oops", "huh"], + "😰": ["face", "nervous", "sweat"], + "😯": ["face", "woo", "shh"], + "😦": ["face", "aw", "what"], + "😧": ["face", "stunned", "nervous"], + "😢": ["face", "tears", "sad", "depressed", "upset", ": '("], + "😥": ["face", "phew", "sweat", "nervous"], + "🤤": ["face"], + "😪": ["face", "tired", "rest", "nap"], + "😓": ["face", "hot", "sad", "tired", "exercise"], + "🥵": ["face", "feverish", "heat", "red", "sweating"], + "🥶": ["face", "blue", "freezing", "frozen", "frostbite", "icicles"], + "😭": ["face", "cry", "tears", "sad", "upset", "depressed"], + "😵": ["spent", "unconscious", "xox", "dizzy"], + "😲": ["face", "xox", "surprised", "poisoned"], + "🤐": ["face", "sealed", "zipper", "secret"], + "🤢": ["face", "vomit", "gross", "green", "sick", "throw up", "ill"], + "🤧": ["face", "gesundheit", "sneeze", "sick", "allergy"], + "🤮": ["face", "sick"], + "😷": ["face", "sick", "ill", "disease"], + "🤒": ["sick", "temperature", "thermometer", "cold", "fever"], + "🤕": ["injured", "clumsy", "bandage", "hurt"], + "🥴": ["face", "dizzy", "intoxicated", "tipsy", "wavy"], + "🥱": ["face", "tired", "yawning"], + "😴": ["face", "tired", "sleepy", "night", "zzz"], + "💤": ["sleepy", "tired", "dream"], + "😶🌫️": [], + "😮💨": [], + "😵💫": [], + "🫠": ["disappear", "dissolve", "liquid", "melt", "toketa"], + "🫢": ["amazement", "awe", "disbelief", "embarrass", "scared", "surprise", "ohoho"], + "🫣": ["captivated", "peep", "stare", "chunibyo"], + "🫡": ["ok", "salute", "sunny", "troops", "yes", "raja"], + "🫥": ["depressed", "disappear", "hide", "introvert", "invisible", "tensen"], + "🫤": ["disappointed", "meh", "skeptical", "unsure"], + "🥹": ["angry", "cry", "proud", "resist", "sad"], + "💩": ["hankey", "shitface", "fail", "turd", "shit"], + "😈": ["devil", "horns"], + "👿": ["devil", "angry", "horns"], + "👹": ["monster", "red", "mask", "halloween", "scary", "creepy", "devil", "demon", "japanese", "ogre"], + "👺": ["red", "evil", "mask", "monster", "scary", "creepy", "japanese", "goblin"], + "💀": ["dead", "skeleton", "creepy", "death"], + "👻": ["halloween", "spooky", "scary"], + "👽": ["UFO", "paul", "weird", "outer_space"], + "🤖": ["computer", "machine", "bot"], + "😺": ["animal", "cats", "happy", "smile"], + "😸": ["animal", "cats", "smile"], + "😹": ["animal", "cats", "haha", "happy", "tears"], + "😻": ["animal", "love", "like", "affection", "cats", "valentines", "heart"], + "😼": ["animal", "cats", "smirk"], + "😽": ["animal", "cats", "kiss"], + "🙀": ["animal", "cats", "munch", "scared", "scream"], + "😿": ["animal", "tears", "weep", "sad", "cats", "upset", "cry"], + "😾": ["animal", "cats"], + "🤲": ["hands", "gesture", "cupped", "prayer"], + "🙌": ["gesture", "hooray", "yea", "celebration", "hands"], + "👏": ["hands", "praise", "applause", "congrats", "yay"], + "👋": ["hands", "gesture", "goodbye", "solong", "farewell", "hello", "hi", "palm"], + "🤙": ["hands", "gesture"], + "👍": ["thumbsup", "yes", "awesome", "good", "agree", "accept", "cool", "hand", "like"], + "👎": ["thumbsdown", "no", "dislike", "hand"], + "👊": ["angry", "violence", "fist", "hit", "attack", "hand"], + "✊": ["fingers", "hand", "grasp"], + "🤛": ["hand", "fistbump"], + "🤜": ["hand", "fistbump"], + "✌": ["fingers", "ohyeah", "hand", "peace", "victory", "two"], + "👌": ["fingers", "limbs", "perfect", "ok", "okay"], + "✋": ["fingers", "stop", "highfive", "palm", "ban"], + "🤚": ["fingers", "raised", "backhand"], + "👐": ["fingers", "butterfly", "hands", "open"], + "💪": ["arm", "flex", "hand", "summer", "strong", "biceps"], + "🦾": ["flex", "hand", "strong", "biceps"], + "🙏": ["please", "hope", "wish", "namaste", "highfive"], + "🦶": ["kick", "stomp"], + "🦵": ["kick", "limb"], + "🦿": ["kick", "limb"], + "🤝": ["agreement", "shake"], + "☝": ["hand", "fingers", "direction", "up"], + "👆": ["fingers", "hand", "direction", "up"], + "👇": ["fingers", "hand", "direction", "down"], + "👈": ["direction", "fingers", "hand", "left"], + "👉": ["fingers", "hand", "direction", "right"], + "🖕": ["hand", "fingers", "rude", "middle", "flipping"], + "🖐": ["hand", "fingers", "palm"], + "🤟": ["hand", "fingers", "gesture"], + "🤘": ["hand", "fingers", "evil_eye", "sign_of_horns", "rock_on"], + "🤞": ["good", "lucky"], + "🖖": ["hand", "fingers", "spock", "star trek"], + "✍": ["lower_left_ballpoint_pen", "stationery", "write", "compose"], + "🫰": [], + "🫱": [], + "🫲": [], + "🫳": [], + "🫴": [], + "🫵": [], + "🫶": ["moemoekyun"], + "🤏": ["hand", "fingers"], + "🤌": ["hand", "fingers"], + "🤳": ["camera", "phone"], + "💅": ["beauty", "manicure", "finger", "fashion", "nail"], + "👄": ["mouth", "kiss"], + "🫦": [], + "🦷": ["teeth", "dentist"], + "👅": ["mouth", "playful"], + "👂": ["face", "hear", "sound", "listen"], + "🦻": ["face", "hear", "sound", "listen"], + "👃": ["smell", "sniff"], + "👁": ["face", "look", "see", "watch", "stare"], + "👀": ["look", "watch", "stalk", "peek", "see"], + "🧠": ["smart", "intelligent"], + "🫀": [], + "🫁": [], + "👤": ["user", "person", "human"], + "👥": ["user", "person", "human", "group", "team"], + "🗣": ["user", "person", "human", "sing", "say", "talk"], + "👶": ["child", "boy", "girl", "toddler"], + "🧒": ["gender-neutral", "young"], + "👦": ["man", "male", "guy", "teenager"], + "👧": ["female", "woman", "teenager"], + "🧑": ["gender-neutral", "person"], + "👨": ["mustache", "father", "dad", "guy", "classy", "sir", "moustache"], + "👩": ["female", "girls", "lady"], + "🧑🦱": ["curly", "afro", "braids", "ringlets"], + "👩🦱": ["woman", "female", "girl", "curly", "afro", "braids", "ringlets"], + "👨🦱": ["man", "male", "boy", "guy", "curly", "afro", "braids", "ringlets"], + "🧑🦰": ["redhead"], + "👩🦰": ["woman", "female", "girl", "ginger", "redhead"], + "👨🦰": ["man", "male", "boy", "guy", "ginger", "redhead"], + "👱♀️": ["woman", "female", "girl", "blonde", "person"], + "👱": ["man", "male", "boy", "blonde", "guy", "person"], + "🧑🦳": ["gray", "old", "white"], + "👩🦳": ["woman", "female", "girl", "gray", "old", "white"], + "👨🦳": ["man", "male", "boy", "guy", "gray", "old", "white"], + "🧑🦲": ["bald", "chemotherapy", "hairless", "shaven"], + "👩🦲": ["woman", "female", "girl", "bald", "chemotherapy", "hairless", "shaven"], + "👨🦲": ["man", "male", "boy", "guy", "bald", "chemotherapy", "hairless", "shaven"], + "🧔": ["person", "bewhiskered"], + "🧓": ["human", "elder", "senior", "gender-neutral"], + "👴": ["human", "male", "men", "old", "elder", "senior"], + "👵": ["human", "female", "women", "lady", "old", "elder", "senior"], + "👲": ["male", "boy", "chinese"], + "🧕": ["female", "hijab", "mantilla", "tichel"], + "👳♀️": ["female", "indian", "hinduism", "arabs", "woman"], + "👳": ["male", "indian", "hinduism", "arabs"], + "👮♀️": ["woman", "police", "law", "legal", "enforcement", "arrest", "911", "female"], + "👮": ["man", "police", "law", "legal", "enforcement", "arrest", "911"], + "👷♀️": ["female", "human", "wip", "build", "construction", "worker", "labor", "woman"], + "👷": ["male", "human", "wip", "guy", "build", "construction", "worker", "labor"], + "💂♀️": ["uk", "gb", "british", "female", "royal", "woman"], + "💂": ["uk", "gb", "british", "male", "guy", "royal"], + "🕵️♀️": ["human", "spy", "detective", "female", "woman"], + "🕵": ["human", "spy", "detective"], + "🧑⚕️": ["doctor", "nurse", "therapist", "healthcare", "human"], + "👩⚕️": ["doctor", "nurse", "therapist", "healthcare", "woman", "human"], + "👨⚕️": ["doctor", "nurse", "therapist", "healthcare", "man", "human"], + "🧑🌾": ["rancher", "gardener", "human"], + "👩🌾": ["rancher", "gardener", "woman", "human"], + "👨🌾": ["rancher", "gardener", "man", "human"], + "🧑🍳": ["chef", "human"], + "👩🍳": ["chef", "woman", "human"], + "👨🍳": ["chef", "man", "human"], + "🧑🎓": ["graduate", "human"], + "👩🎓": ["graduate", "woman", "human"], + "👨🎓": ["graduate", "man", "human"], + "🧑🎤": ["rockstar", "entertainer", "human"], + "👩🎤": ["rockstar", "entertainer", "woman", "human"], + "👨🎤": ["rockstar", "entertainer", "man", "human"], + "🧑🏫": ["instructor", "professor", "human"], + "👩🏫": ["instructor", "professor", "woman", "human"], + "👨🏫": ["instructor", "professor", "man", "human"], + "🧑🏭": ["assembly", "industrial", "human"], + "👩🏭": ["assembly", "industrial", "woman", "human"], + "👨🏭": ["assembly", "industrial", "man", "human"], + "🧑💻": ["coder", "developer", "engineer", "programmer", "software", "human", "laptop", "computer"], + "👩💻": ["coder", "developer", "engineer", "programmer", "software", "woman", "human", "laptop", "computer"], + "👨💻": ["coder", "developer", "engineer", "programmer", "software", "man", "human", "laptop", "computer"], + "🧑💼": ["business", "manager", "human"], + "👩💼": ["business", "manager", "woman", "human"], + "👨💼": ["business", "manager", "man", "human"], + "🧑🔧": ["plumber", "human", "wrench"], + "👩🔧": ["plumber", "woman", "human", "wrench"], + "👨🔧": ["plumber", "man", "human", "wrench"], + "🧑🔬": ["biologist", "chemist", "engineer", "physicist", "human"], + "👩🔬": ["biologist", "chemist", "engineer", "physicist", "woman", "human"], + "👨🔬": ["biologist", "chemist", "engineer", "physicist", "man", "human"], + "🧑🎨": ["painter", "human"], + "👩🎨": ["painter", "woman", "human"], + "👨🎨": ["painter", "man", "human"], + "🧑🚒": ["fireman", "human"], + "👩🚒": ["fireman", "woman", "human"], + "👨🚒": ["fireman", "man", "human"], + "🧑✈️": ["aviator", "plane", "human"], + "👩✈️": ["aviator", "plane", "woman", "human"], + "👨✈️": ["aviator", "plane", "man", "human"], + "🧑🚀": ["space", "rocket", "human"], + "👩🚀": ["space", "rocket", "woman", "human"], + "👨🚀": ["space", "rocket", "man", "human"], + "🧑⚖️": ["justice", "court", "human"], + "👩⚖️": ["justice", "court", "woman", "human"], + "👨⚖️": ["justice", "court", "man", "human"], + "🦸♀️": ["woman", "female", "good", "heroine", "superpowers"], + "🦸♂️": ["man", "male", "good", "hero", "superpowers"], + "🦹♀️": ["woman", "female", "evil", "bad", "criminal", "heroine", "superpowers"], + "🦹♂️": ["man", "male", "evil", "bad", "criminal", "hero", "superpowers"], + "🤶": ["woman", "female", "xmas", "mother christmas"], + "🧑🎄": ["xmas", "christmas"], + "🎅": ["festival", "man", "male", "xmas", "father christmas"], + "🥷": [], + "🧙♀️": ["woman", "female", "mage", "witch"], + "🧙♂️": ["man", "male", "mage", "sorcerer"], + "🧝♀️": ["woman", "female"], + "🧝♂️": ["man", "male"], + "🧛♀️": ["woman", "female"], + "🧛♂️": ["man", "male", "dracula"], + "🧟♀️": ["woman", "female", "undead", "walking dead"], + "🧟♂️": ["man", "male", "dracula", "undead", "walking dead"], + "🧞♀️": ["woman", "female"], + "🧞♂️": ["man", "male"], + "🧜♀️": ["woman", "female", "merwoman", "ariel"], + "🧜♂️": ["man", "male", "triton"], + "🧚♀️": ["woman", "female"], + "🧚♂️": ["man", "male"], + "👼": ["heaven", "wings", "halo"], + "🧌": [], + "🤰": ["baby"], + "🫃": [], + "🫄": [], + "🫅": [], + "🤱": ["nursing", "baby"], + "👩🍼": [], + "👨🍼": [], + "🧑🍼": [], + "👸": ["girl", "woman", "female", "blond", "crown", "royal", "queen"], + "🤴": ["boy", "man", "male", "crown", "royal", "king"], + "👰": ["couple", "marriage", "wedding", "woman", "bride"], + "👰": ["couple", "marriage", "wedding", "woman", "bride"], + "🤵": ["couple", "marriage", "wedding", "groom"], + "🤵": ["couple", "marriage", "wedding", "groom"], + "🏃♀️": ["woman", "walking", "exercise", "race", "running", "female"], + "🏃": ["man", "walking", "exercise", "race", "running"], + "🚶♀️": ["human", "feet", "steps", "woman", "female"], + "🚶": ["human", "feet", "steps"], + "💃": ["female", "girl", "woman", "fun"], + "🕺": ["male", "boy", "fun", "dancer"], + "👯": ["female", "bunny", "women", "girls"], + "👯♂️": ["male", "bunny", "men", "boys"], + "👫": ["pair", "people", "human", "love", "date", "dating", "like", "affection", "valentines", "marriage"], + "🧑🤝🧑": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "human"], + "👬": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "man", "human"], + "👭": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "female", "human"], + "🫂": [], + "🙇♀️": ["woman", "female", "girl"], + "🙇": ["man", "male", "boy"], + "🤦♂️": ["man", "male", "boy", "disbelief"], + "🤦♀️": ["woman", "female", "girl", "disbelief"], + "🤷": ["woman", "female", "girl", "confused", "indifferent", "doubt"], + "🤷♂️": ["man", "male", "boy", "confused", "indifferent", "doubt"], + "💁": ["female", "girl", "woman", "human", "information"], + "💁♂️": ["male", "boy", "man", "human", "information"], + "🙅": ["female", "girl", "woman", "nope"], + "🙅♂️": ["male", "boy", "man", "nope"], + "🙆": ["women", "girl", "female", "pink", "human", "woman"], + "🙆♂️": ["men", "boy", "male", "blue", "human", "man"], + "🙋": ["female", "girl", "woman"], + "🙋♂️": ["male", "boy", "man"], + "🙎": ["female", "girl", "woman"], + "🙎♂️": ["male", "boy", "man"], + "🙍": ["female", "girl", "woman", "sad", "depressed", "discouraged", "unhappy"], + "🙍♂️": ["male", "boy", "man", "sad", "depressed", "discouraged", "unhappy"], + "💇": ["female", "girl", "woman"], + "💇♂️": ["male", "boy", "man"], + "💆": ["female", "girl", "woman", "head"], + "💆♂️": ["male", "boy", "man", "head"], + "🧖♀️": ["female", "woman", "spa", "steamroom", "sauna"], + "🧖♂️": ["male", "man", "spa", "steamroom", "sauna"], + "🧏♀️": ["woman", "female"], + "🧏♂️": ["man", "male"], + "🧍♀️": ["woman", "female"], + "🧍♂️": ["man", "male"], + "🧎♀️": ["woman", "female"], + "🧎♂️": ["man", "male"], + "🧑🦯": ["accessibility", "blind"], + "👩🦯": ["woman", "female", "accessibility", "blind"], + "👨🦯": ["man", "male", "accessibility", "blind"], + "🧑🦼": ["accessibility"], + "👩🦼": ["woman", "female", "accessibility"], + "👨🦼": ["man", "male", "accessibility"], + "🧑🦽": ["accessibility"], + "👩🦽": ["woman", "female", "accessibility"], + "👨🦽": ["man", "male", "accessibility"], + "💑": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"], + "👩❤️👩": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"], + "👨❤️👨": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"], + "💏": ["pair", "valentines", "love", "like", "dating", "marriage"], + "👩❤️💋👩": ["pair", "valentines", "love", "like", "dating", "marriage"], + "👨❤️💋👨": ["pair", "valentines", "love", "like", "dating", "marriage"], + "👪": ["home", "parents", "child", "mom", "dad", "father", "mother", "people", "human"], + "👨👩👧": ["home", "parents", "people", "human", "child"], + "👨👩👧👦": ["home", "parents", "people", "human", "children"], + "👨👩👦👦": ["home", "parents", "people", "human", "children"], + "👨👩👧👧": ["home", "parents", "people", "human", "children"], + "👩👩👦": ["home", "parents", "people", "human", "children"], + "👩👩👧": ["home", "parents", "people", "human", "children"], + "👩👩👧👦": ["home", "parents", "people", "human", "children"], + "👩👩👦👦": ["home", "parents", "people", "human", "children"], + "👩👩👧👧": ["home", "parents", "people", "human", "children"], + "👨👨👦": ["home", "parents", "people", "human", "children"], + "👨👨👧": ["home", "parents", "people", "human", "children"], + "👨👨👧👦": ["home", "parents", "people", "human", "children"], + "👨👨👦👦": ["home", "parents", "people", "human", "children"], + "👨👨👧👧": ["home", "parents", "people", "human", "children"], + "👩👦": ["home", "parent", "people", "human", "child"], + "👩👧": ["home", "parent", "people", "human", "child"], + "👩👧👦": ["home", "parent", "people", "human", "children"], + "👩👦👦": ["home", "parent", "people", "human", "children"], + "👩👧👧": ["home", "parent", "people", "human", "children"], + "👨👦": ["home", "parent", "people", "human", "child"], + "👨👧": ["home", "parent", "people", "human", "child"], + "👨👧👦": ["home", "parent", "people", "human", "children"], + "👨👦👦": ["home", "parent", "people", "human", "children"], + "👨👧👧": ["home", "parent", "people", "human", "children"], + "🧶": ["ball", "crochet", "knit"], + "🧵": ["needle", "sewing", "spool", "string"], + "🧥": ["jacket"], + "🥼": ["doctor", "experiment", "scientist", "chemist"], + "👚": ["fashion", "shopping_bags", "female"], + "👕": ["fashion", "cloth", "casual", "shirt", "tee"], + "👖": ["fashion", "shopping"], + "👔": ["shirt", "suitup", "formal", "fashion", "cloth", "business"], + "👗": ["clothes", "fashion", "shopping"], + "👙": ["swimming", "female", "woman", "girl", "fashion", "beach", "summer"], + "🩱": ["swimming", "female", "woman", "girl", "fashion", "beach", "summer"], + "👘": ["dress", "fashion", "women", "female", "japanese"], + "🥻": ["dress", "fashion", "women", "female"], + "🩲": ["dress", "fashion"], + "🩳": ["dress", "fashion"], + "💄": ["female", "girl", "fashion", "woman"], + "💋": ["face", "lips", "love", "like", "affection", "valentines"], + "👣": ["feet", "tracking", "walking", "beach"], + "🥿": ["ballet", "slip-on", "slipper"], + "👠": ["fashion", "shoes", "female", "pumps", "stiletto"], + "👡": ["shoes", "fashion", "flip flops"], + "👢": ["shoes", "fashion"], + "👞": ["fashion", "male"], + "👟": ["shoes", "sports", "sneakers"], + "🩴": [], + "🩰": ["shoes", "sports"], + "🧦": ["stockings", "clothes"], + "🧤": ["hands", "winter", "clothes"], + "🧣": ["neck", "winter", "clothes"], + "👒": ["fashion", "accessories", "female", "lady", "spring"], + "🎩": ["magic", "gentleman", "classy", "circus"], + "🧢": ["cap", "baseball"], + "⛑": ["construction", "build"], + "🪖": [], + "🎓": ["school", "college", "degree", "university", "graduation", "cap", "hat", "legal", "learn", "education"], + "👑": ["king", "kod", "leader", "royalty", "lord"], + "🎒": ["student", "education", "bag", "backpack"], + "🧳": ["packing", "travel"], + "👝": ["bag", "accessories", "shopping"], + "👛": ["fashion", "accessories", "money", "sales", "shopping"], + "👜": ["fashion", "accessory", "accessories", "shopping"], + "💼": ["business", "documents", "work", "law", "legal", "job", "career"], + "👓": ["fashion", "accessories", "eyesight", "nerdy", "dork", "geek"], + "🕶": ["face", "cool", "accessories"], + "🥽": ["eyes", "protection", "safety"], + "💍": ["wedding", "propose", "marriage", "valentines", "diamond", "fashion", "jewelry", "gem", "engagement"], + "🌂": ["weather", "rain", "drizzle"], + "🐶": ["animal", "friend", "nature", "woof", "puppy", "pet", "faithful"], + "🐱": ["animal", "meow", "nature", "pet", "kitten"], + "🐈⬛": ["animal", "meow", "nature", "pet", "kitten"], + "🐭": ["animal", "nature", "cheese_wedge", "rodent"], + "🐹": ["animal", "nature"], + "🐰": ["animal", "nature", "pet", "spring", "magic", "bunny"], + "🦊": ["animal", "nature", "face"], + "🐻": ["animal", "nature", "wild"], + "🐼": ["animal", "nature", "panda"], + "🐨": ["animal", "nature"], + "🐯": ["animal", "cat", "danger", "wild", "nature", "roar"], + "🦁": ["animal", "nature"], + "🐮": ["beef", "ox", "animal", "nature", "moo", "milk"], + "🐷": ["animal", "oink", "nature"], + "🐽": ["animal", "oink"], + "🐸": ["animal", "nature", "croak", "toad"], + "🦑": ["animal", "nature", "ocean", "sea"], + "🐙": ["animal", "creature", "ocean", "sea", "nature", "beach"], + "🦐": ["animal", "ocean", "nature", "seafood"], + "🐵": ["animal", "nature", "circus"], + "🦍": ["animal", "nature", "circus"], + "🙈": ["monkey", "animal", "nature", "haha"], + "🙉": ["animal", "monkey", "nature"], + "🙊": ["monkey", "animal", "nature", "omg"], + "🐒": ["animal", "nature", "banana", "circus"], + "🐔": ["animal", "cluck", "nature", "bird"], + "🐧": ["animal", "nature"], + "🐦": ["animal", "nature", "fly", "tweet", "spring"], + "🐤": ["animal", "chicken", "bird"], + "🐣": ["animal", "chicken", "egg", "born", "baby", "bird"], + "🐥": ["animal", "chicken", "baby", "bird"], + "🦆": ["animal", "nature", "bird", "mallard"], + "🦅": ["animal", "nature", "bird"], + "🦉": ["animal", "nature", "bird", "hoot"], + "🦇": ["animal", "nature", "blind", "vampire"], + "🐺": ["animal", "nature", "wild"], + "🐗": ["animal", "nature"], + "🐴": ["animal", "brown", "nature"], + "🦄": ["animal", "nature", "mystical"], + "🐝": ["animal", "insect", "nature", "bug", "spring", "honey"], + "🐛": ["animal", "insect", "nature", "worm"], + "🦋": ["animal", "insect", "nature", "caterpillar"], + "🐌": ["slow", "animal", "shell"], + "🐞": ["animal", "insect", "nature", "ladybug"], + "🐜": ["animal", "insect", "nature", "bug"], + "🦗": ["animal", "cricket", "chirp"], + "🕷": ["animal", "arachnid"], + "🪲": ["animal"], + "🪳": ["animal"], + "🪰": ["animal"], + "🪱": ["animal"], + "🦂": ["animal", "arachnid"], + "🦀": ["animal", "crustacean"], + "🐍": ["animal", "evil", "nature", "hiss", "python"], + "🦎": ["animal", "nature", "reptile"], + "🦖": ["animal", "nature", "dinosaur", "tyrannosaurus", "extinct"], + "🦕": ["animal", "nature", "dinosaur", "brachiosaurus", "brontosaurus", "diplodocus", "extinct"], + "🐢": ["animal", "slow", "nature", "tortoise"], + "🐠": ["animal", "swim", "ocean", "beach", "nemo"], + "🐟": ["animal", "food", "nature"], + "🐡": ["animal", "nature", "food", "sea", "ocean"], + "🐬": ["animal", "nature", "fish", "sea", "ocean", "flipper", "fins", "beach"], + "🦈": ["animal", "nature", "fish", "sea", "ocean", "jaws", "fins", "beach"], + "🐳": ["animal", "nature", "sea", "ocean"], + "🐋": ["animal", "nature", "sea", "ocean"], + "🐊": ["animal", "nature", "reptile", "lizard", "alligator"], + "🐆": ["animal", "nature"], + "🦓": ["animal", "nature", "stripes", "safari"], + "🐅": ["animal", "nature", "roar"], + "🐃": ["animal", "nature", "ox", "cow"], + "🐂": ["animal", "cow", "beef"], + "🐄": ["beef", "ox", "animal", "nature", "moo", "milk"], + "🦌": ["animal", "nature", "horns", "venison"], + "🐪": ["animal", "hot", "desert", "hump"], + "🐫": ["animal", "nature", "hot", "desert", "hump"], + "🦒": ["animal", "nature", "spots", "safari"], + "🐘": ["animal", "nature", "nose", "th", "circus"], + "🦏": ["animal", "nature", "horn"], + "🐐": ["animal", "nature"], + "🐏": ["animal", "sheep", "nature"], + "🐑": ["animal", "nature", "wool", "shipit"], + "🐎": ["animal", "gamble", "luck"], + "🐖": ["animal", "nature"], + "🐀": ["animal", "mouse", "rodent"], + "🐁": ["animal", "nature", "rodent"], + "🐓": ["animal", "nature", "chicken"], + "🦃": ["animal", "bird"], + "🕊": ["animal", "bird"], + "🐕": ["animal", "nature", "friend", "doge", "pet", "faithful"], + "🐩": ["dog", "animal", "101", "nature", "pet"], + "🐈": ["animal", "meow", "pet", "cats"], + "🐇": ["animal", "nature", "pet", "magic", "spring"], + "🐿": ["animal", "nature", "rodent", "squirrel"], + "🦔": ["animal", "nature", "spiny"], + "🦝": ["animal", "nature"], + "🦙": ["animal", "nature", "alpaca"], + "🦛": ["animal", "nature"], + "🦘": ["animal", "nature", "australia", "joey", "hop", "marsupial"], + "🦡": ["animal", "nature", "honey"], + "🦢": ["animal", "nature", "bird"], + "🦚": ["animal", "nature", "peahen", "bird"], + "🦜": ["animal", "nature", "bird", "pirate", "talk"], + "🦞": ["animal", "nature", "bisque", "claws", "seafood"], + "🦠": ["amoeba", "bacteria", "germs"], + "🦟": ["animal", "nature", "insect", "malaria"], + "🦬": ["animal", "nature"], + "🦣": ["animal", "nature"], + "🦫": ["animal", "nature"], + "🐻❄️": ["animal", "nature"], + "🦤": ["animal", "nature"], + "🪶": ["animal", "nature"], + "🦭": ["animal", "nature"], + "🐾": ["animal", "tracking", "footprints", "dog", "cat", "pet", "feet"], + "🐉": ["animal", "myth", "nature", "chinese", "green"], + "🐲": ["animal", "myth", "nature", "chinese", "green"], + "🦧": ["animal", "nature"], + "🦮": ["animal", "nature"], + "🐕🦺": ["animal", "nature"], + "🦥": ["animal", "nature"], + "🦦": ["animal", "nature"], + "🦨": ["animal", "nature"], + "🦩": ["animal", "nature"], + "🌵": ["vegetable", "plant", "nature"], + "🎄": ["festival", "vacation", "december", "xmas", "celebration"], + "🌲": ["plant", "nature"], + "🌳": ["plant", "nature"], + "🌴": ["plant", "vegetable", "nature", "summer", "beach", "mojito", "tropical"], + "🌱": ["plant", "nature", "grass", "lawn", "spring"], + "🌿": ["vegetable", "plant", "medicine", "weed", "grass", "lawn"], + "☘": ["vegetable", "plant", "nature", "irish", "clover"], + "🍀": ["vegetable", "plant", "nature", "lucky", "irish"], + "🎍": ["plant", "nature", "vegetable", "panda", "pine_decoration"], + "🎋": ["plant", "nature", "branch", "summer"], + "🍃": ["nature", "plant", "tree", "vegetable", "grass", "lawn", "spring"], + "🍂": ["nature", "plant", "vegetable", "leaves"], + "🍁": ["nature", "plant", "vegetable", "ca", "fall"], + "🌾": ["nature", "plant"], + "🌺": ["plant", "vegetable", "flowers", "beach"], + "🌻": ["nature", "plant", "fall"], + "🌹": ["flowers", "valentines", "love", "spring"], + "🥀": ["plant", "nature", "flower"], + "🌷": ["flowers", "plant", "nature", "summer", "spring"], + "🌼": ["nature", "flowers", "yellow"], + "🌸": ["nature", "plant", "spring", "flower"], + "💐": ["flowers", "nature", "spring"], + "🍄": ["plant", "vegetable"], + "🪴": ["plant"], + "🌰": ["food", "squirrel"], + "🎃": ["halloween", "light", "pumpkin", "creepy", "fall"], + "🐚": ["nature", "sea", "beach"], + "🕸": ["animal", "insect", "arachnid", "silk"], + "🌎": ["globe", "world", "USA", "international"], + "🌍": ["globe", "world", "international"], + "🌏": ["globe", "world", "east", "international"], + "🪐": ["saturn"], + "🌕": ["nature", "yellow", "twilight", "planet", "space", "night", "evening", "sleep"], + "🌖": ["nature", "twilight", "planet", "space", "night", "evening", "sleep", "waxing_gibbous_moon"], + "🌗": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "🌘": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "🌑": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "🌒": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "🌓": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "🌔": ["nature", "night", "sky", "gray", "twilight", "planet", "space", "evening", "sleep"], + "🌚": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "🌝": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "🌛": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "🌜": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"], + "🌞": ["nature", "morning", "sky"], + "🌙": ["night", "sleep", "sky", "evening", "magic"], + "⭐": ["night", "yellow"], + "🌟": ["night", "sparkle", "awesome", "good", "magic"], + "💫": ["star", "sparkle", "shoot", "magic"], + "✨": ["stars", "shine", "shiny", "cool", "awesome", "good", "magic"], + "☄": ["space"], + "☀️": ["weather", "nature", "brightness", "summer", "beach", "spring"], + "🌤": ["weather"], + "⛅": ["weather", "nature", "cloudy", "morning", "fall", "spring"], + "🌥": ["weather"], + "🌦": ["weather"], + "☁️": ["weather", "sky"], + "🌧": ["weather"], + "⛈": ["weather", "lightning"], + "🌩": ["weather", "thunder"], + "⚡": ["thunder", "weather", "lightning bolt", "fast"], + "🔥": ["hot", "cook", "flame"], + "💥": ["bomb", "explode", "explosion", "collision", "blown"], + "❄️": ["winter", "season", "cold", "weather", "christmas", "xmas"], + "🌨": ["weather"], + "⛄": ["winter", "season", "cold", "weather", "christmas", "xmas", "frozen", "without_snow"], + "☃": ["winter", "season", "cold", "weather", "christmas", "xmas", "frozen"], + "🌬": ["gust", "air"], + "💨": ["wind", "air", "fast", "shoo", "fart", "smoke", "puff"], + "🌪": ["weather", "cyclone", "twister"], + "🌫": ["weather"], + "☂": ["weather", "spring"], + "☔": ["rainy", "weather", "spring"], + "💧": ["water", "drip", "faucet", "spring"], + "💦": ["water", "drip", "oops"], + "🌊": ["sea", "water", "wave", "nature", "tsunami", "disaster"], + "🪷": [], + "🪸": [], + "🪹": [], + "🪺": [], + "🍏": ["fruit", "nature"], + "🍎": ["fruit", "mac", "school"], + "🍐": ["fruit", "nature", "food"], + "🍊": ["food", "fruit", "nature", "orange"], + "🍋": ["fruit", "nature"], + "🍌": ["fruit", "food", "monkey"], + "🍉": ["fruit", "food", "picnic", "summer"], + "🍇": ["fruit", "food", "wine"], + "🍓": ["fruit", "food", "nature"], + "🍈": ["fruit", "nature", "food"], + "🍒": ["food", "fruit"], + "🍑": ["fruit", "nature", "food"], + "🍍": ["fruit", "nature", "food"], + "🥥": ["fruit", "nature", "food", "palm"], + "🥝": ["fruit", "food"], + "🥭": ["fruit", "food", "tropical"], + "🥑": ["fruit", "food"], + "🥦": ["fruit", "food", "vegetable"], + "🍅": ["fruit", "vegetable", "nature", "food"], + "🍆": ["vegetable", "nature", "food", "aubergine"], + "🥒": ["fruit", "food", "pickle"], + "🫐": ["fruit", "food"], + "🫒": ["fruit", "food"], + "🫑": ["fruit", "food"], + "🥕": ["vegetable", "food", "orange"], + "🌶": ["food", "spicy", "chilli", "chili"], + "🥔": ["food", "tuber", "vegatable", "starch"], + "🌽": ["food", "vegetable", "plant"], + "🥬": ["food", "vegetable", "plant", "bok choy", "cabbage", "kale", "lettuce"], + "🍠": ["food", "nature"], + "🥜": ["food", "nut"], + "🧄": ["food"], + "🧅": ["food"], + "🍯": ["bees", "sweet", "kitchen"], + "🥐": ["food", "bread", "french"], + "🍞": ["food", "wheat", "breakfast", "toast"], + "🥖": ["food", "bread", "french"], + "🥯": ["food", "bread", "bakery", "schmear"], + "🥨": ["food", "bread", "twisted"], + "🧀": ["food", "chadder"], + "🥚": ["food", "chicken", "breakfast"], + "🥓": ["food", "breakfast", "pork", "pig", "meat"], + "🥩": ["food", "cow", "meat", "cut", "chop", "lambchop", "porkchop"], + "🥞": ["food", "breakfast", "flapjacks", "hotcakes"], + "🍗": ["food", "meat", "drumstick", "bird", "chicken", "turkey"], + "🍖": ["good", "food", "drumstick"], + "🦴": ["skeleton"], + "🍤": ["food", "animal", "appetizer", "summer"], + "🍳": ["food", "breakfast", "kitchen", "egg"], + "🍔": ["meat", "fast food", "beef", "cheeseburger", "mcdonalds", "burger king"], + "🍟": ["chips", "snack", "fast food"], + "🥙": ["food", "flatbread", "stuffed", "gyro"], + "🌭": ["food", "frankfurter"], + "🍕": ["food", "party"], + "🥪": ["food", "lunch", "bread"], + "🥫": ["food", "soup"], + "🍝": ["food", "italian", "noodle"], + "🌮": ["food", "mexican"], + "🌯": ["food", "mexican"], + "🥗": ["food", "healthy", "lettuce"], + "🥘": ["food", "cooking", "casserole", "paella"], + "🍜": ["food", "japanese", "noodle", "chopsticks"], + "🍲": ["food", "meat", "soup"], + "🍥": ["food", "japan", "sea", "beach", "narutomaki", "pink", "swirl", "kamaboko", "surimi", "ramen"], + "🥠": ["food", "prophecy"], + "🍣": ["food", "fish", "japanese", "rice"], + "🍱": ["food", "japanese", "box"], + "🍛": ["food", "spicy", "hot", "indian"], + "🍙": ["food", "japanese"], + "🍚": ["food", "china", "asian"], + "🍘": ["food", "japanese"], + "🍢": ["food", "japanese"], + "🍡": ["food", "dessert", "sweet", "japanese", "barbecue", "meat"], + "🍧": ["hot", "dessert", "summer"], + "🍨": ["food", "hot", "dessert"], + "🍦": ["food", "hot", "dessert", "summer"], + "🥧": ["food", "dessert", "pastry"], + "🍰": ["food", "dessert"], + "🧁": ["food", "dessert", "bakery", "sweet"], + "🥮": ["food", "autumn"], + "🎂": ["food", "dessert", "cake"], + "🍮": ["dessert", "food"], + "🍬": ["snack", "dessert", "sweet", "lolly"], + "🍭": ["food", "snack", "candy", "sweet"], + "🍫": ["food", "snack", "dessert", "sweet"], + "🍿": ["food", "movie theater", "films", "snack"], + "🥟": ["food", "empanada", "pierogi", "potsticker"], + "🍩": ["food", "dessert", "snack", "sweet", "donut"], + "🍪": ["food", "snack", "oreo", "chocolate", "sweet", "dessert"], + "🧇": ["food"], + "🧆": ["food"], + "🧈": ["food"], + "🦪": ["food"], + "🫓": ["food"], + "🫔": ["food"], + "🫕": ["food"], + "🥛": ["beverage", "drink", "cow"], + "🍺": ["relax", "beverage", "drink", "drunk", "party", "pub", "summer", "alcohol", "booze"], + "🍻": ["relax", "beverage", "drink", "drunk", "party", "pub", "summer", "alcohol", "booze"], + "🥂": ["beverage", "drink", "party", "alcohol", "celebrate", "cheers", "wine", "champagne", "toast"], + "🍷": ["drink", "beverage", "drunk", "alcohol", "booze"], + "🥃": ["drink", "beverage", "drunk", "alcohol", "liquor", "booze", "bourbon", "scotch", "whisky", "glass", "shot"], + "🍸": ["drink", "drunk", "alcohol", "beverage", "booze", "mojito"], + "🍹": ["beverage", "cocktail", "summer", "beach", "alcohol", "booze", "mojito"], + "🍾": ["drink", "wine", "bottle", "celebration"], + "🍶": ["wine", "drink", "drunk", "beverage", "japanese", "alcohol", "booze"], + "🍵": ["drink", "bowl", "breakfast", "green", "british"], + "🥤": ["drink", "soda"], + "☕": ["beverage", "caffeine", "latte", "espresso"], + "🫖": [], + "🧋": ["tapioca"], + "🍼": ["food", "container", "milk"], + "🧃": ["food", "drink"], + "🧉": ["food", "drink"], + "🧊": ["food"], + "🧂": ["condiment", "shaker"], + "🥄": ["cutlery", "kitchen", "tableware"], + "🍴": ["cutlery", "kitchen"], + "🍽": ["food", "eat", "meal", "lunch", "dinner", "restaurant"], + "🥣": ["food", "breakfast", "cereal", "oatmeal", "porridge"], + "🥡": ["food", "leftovers"], + "🥢": ["food"], + "🫗": [], + "🫘": [], + "🫙": [], + "⚽": ["sports", "football"], + "🏀": ["sports", "balls", "NBA"], + "🏈": ["sports", "balls", "NFL"], + "⚾": ["sports", "balls"], + "🥎": ["sports", "balls"], + "🎾": ["sports", "balls", "green"], + "🏐": ["sports", "balls"], + "🏉": ["sports", "team"], + "🥏": ["sports", "frisbee", "ultimate"], + "🎱": ["pool", "hobby", "game", "luck", "magic"], + "⛳": ["sports", "business", "flag", "hole", "summer"], + "🏌️♀️": ["sports", "business", "woman", "female"], + "🏌": ["sports", "business"], + "🏓": ["sports", "pingpong"], + "🏸": ["sports"], + "🥅": ["sports"], + "🏒": ["sports"], + "🏑": ["sports"], + "🥍": ["sports", "ball", "stick"], + "🏏": ["sports"], + "🎿": ["sports", "winter", "cold", "snow"], + "⛷": ["sports", "winter", "snow"], + "🏂": ["sports", "winter"], + "🤺": ["sports", "fencing", "sword"], + "🤼♀️": ["sports", "wrestlers"], + "🤼♂️": ["sports", "wrestlers"], + "🤸♀️": ["gymnastics"], + "🤸♂️": ["gymnastics"], + "🤾♀️": ["sports"], + "🤾♂️": ["sports"], + "⛸": ["sports"], + "🥌": ["sports"], + "🛹": ["board"], + "🛷": ["sleigh", "luge", "toboggan"], + "🏹": ["sports"], + "🎣": ["food", "hobby", "summer"], + "🥊": ["sports", "fighting"], + "🥋": ["judo", "karate", "taekwondo"], + "🚣♀️": ["sports", "hobby", "water", "ship", "woman", "female"], + "🚣": ["sports", "hobby", "water", "ship"], + "🧗♀️": ["sports", "hobby", "woman", "female", "rock"], + "🧗♂️": ["sports", "hobby", "man", "male", "rock"], + "🏊♀️": ["sports", "exercise", "human", "athlete", "water", "summer", "woman", "female"], + "🏊": ["sports", "exercise", "human", "athlete", "water", "summer"], + "🤽♀️": ["sports", "pool"], + "🤽♂️": ["sports", "pool"], + "🧘♀️": ["woman", "female", "meditation", "yoga", "serenity", "zen", "mindfulness"], + "🧘♂️": ["man", "male", "meditation", "yoga", "serenity", "zen", "mindfulness"], + "🏄♀️": ["sports", "ocean", "sea", "summer", "beach", "woman", "female"], + "🏄": ["sports", "ocean", "sea", "summer", "beach"], + "🛀": ["clean", "shower", "bathroom"], + "⛹️♀️": ["sports", "human", "woman", "female"], + "⛹": ["sports", "human"], + "🏋️♀️": ["sports", "training", "exercise", "woman", "female"], + "🏋": ["sports", "training", "exercise"], + "🚴♀️": ["sports", "bike", "exercise", "hipster", "woman", "female"], + "🚴": ["sports", "bike", "exercise", "hipster"], + "🚵♀️": ["transportation", "sports", "human", "race", "bike", "woman", "female"], + "🚵": ["transportation", "sports", "human", "race", "bike"], + "🏇": ["animal", "betting", "competition", "gambling", "luck"], + "🤿": ["sports"], + "🪀": ["sports"], + "🪁": ["sports"], + "🦺": ["sports"], + "🪡": [], + "🪢": [], + "🕴": ["suit", "business", "levitate", "hover", "jump"], + "🏆": ["win", "award", "contest", "place", "ftw", "ceremony"], + "🎽": ["play", "pageant"], + "🏅": ["award", "winning"], + "🎖": ["award", "winning", "army"], + "🥇": ["award", "winning", "first"], + "🥈": ["award", "second"], + "🥉": ["award", "third"], + "🎗": ["sports", "cause", "support", "awareness"], + "🏵": ["flower", "decoration", "military"], + "🎫": ["event", "concert", "pass"], + "🎟": ["sports", "concert", "entrance"], + "🎭": ["acting", "theater", "drama"], + "🎨": ["design", "paint", "draw", "colors"], + "🎪": ["festival", "carnival", "party"], + "🤹♀️": ["juggle", "balance", "skill", "multitask"], + "🤹♂️": ["juggle", "balance", "skill", "multitask"], + "🎤": ["sound", "music", "PA", "sing", "talkshow"], + "🎧": ["music", "score", "gadgets"], + "🎼": ["treble", "clef", "compose"], + "🎹": ["piano", "instrument", "compose"], + "🥁": ["music", "instrument", "drumsticks", "snare"], + "🎷": ["music", "instrument", "jazz", "blues"], + "🎺": ["music", "brass"], + "🎸": ["music", "instrument"], + "🎻": ["music", "instrument", "orchestra", "symphony"], + "🪕": ["music", "instrument"], + "🪗": ["music", "instrument"], + "🪘": ["music", "instrument"], + "🎬": ["movie", "film", "record"], + "🎮": ["play", "console", "PS4", "controller"], + "👾": ["game", "arcade", "play"], + "🎯": ["game", "play", "bar", "target", "bullseye"], + "🎲": ["dice", "random", "tabletop", "play", "luck"], + "♟️": ["expendable"], + "🎰": ["bet", "gamble", "vegas", "fruit machine", "luck", "casino"], + "🧩": ["interlocking", "puzzle", "piece"], + "🎳": ["sports", "fun", "play"], + "🪄": [], + "🪅": [], + "🪆": [], + "🪬": [], + "🪩": [], + "🚗": ["red", "transportation", "vehicle"], + "🚕": ["uber", "vehicle", "cars", "transportation"], + "🚙": ["transportation", "vehicle"], + "🚌": ["car", "vehicle", "transportation"], + "🚎": ["bart", "transportation", "vehicle"], + "🏎": ["sports", "race", "fast", "formula", "f1"], + "🚓": ["vehicle", "cars", "transportation", "law", "legal", "enforcement"], + "🚑": ["health", "911", "hospital"], + "🚒": ["transportation", "cars", "vehicle"], + "🚐": ["vehicle", "car", "transportation"], + "🚚": ["cars", "transportation"], + "🚛": ["vehicle", "cars", "transportation", "express"], + "🚜": ["vehicle", "car", "farming", "agriculture"], + "🛴": ["vehicle", "kick", "razor"], + "🏍": ["race", "sports", "fast"], + "🚲": ["sports", "bicycle", "exercise", "hipster"], + "🛵": ["vehicle", "vespa", "sasha"], + "🦽": ["vehicle"], + "🦼": ["vehicle"], + "🛺": ["vehicle"], + "🪂": ["vehicle"], + "🚨": ["police", "ambulance", "911", "emergency", "alert", "error", "pinged", "law", "legal"], + "🚔": ["vehicle", "law", "legal", "enforcement", "911"], + "🚍": ["vehicle", "transportation"], + "🚘": ["car", "vehicle", "transportation"], + "🚖": ["vehicle", "cars", "uber"], + "🚡": ["transportation", "vehicle", "ski"], + "🚠": ["transportation", "vehicle", "ski"], + "🚟": ["vehicle", "transportation"], + "🚃": ["transportation", "vehicle", "train"], + "🚋": ["transportation", "vehicle", "carriage", "public", "travel"], + "🚝": ["transportation", "vehicle"], + "🚄": ["transportation", "vehicle"], + "🚅": ["transportation", "vehicle", "speed", "fast", "public", "travel"], + "🚈": ["transportation", "vehicle"], + "🚞": ["transportation", "vehicle"], + "🚂": ["transportation", "vehicle", "train"], + "🚆": ["transportation", "vehicle"], + "🚇": ["transportation", "blue-square", "mrt", "underground", "tube"], + "🚊": ["transportation", "vehicle"], + "🚉": ["transportation", "vehicle", "public"], + "🛸": ["transportation", "vehicle", "ufo"], + "🚁": ["transportation", "vehicle", "fly"], + "🛩": ["flight", "transportation", "fly", "vehicle"], + "✈️": ["vehicle", "transportation", "flight", "fly"], + "🛫": ["airport", "flight", "landing"], + "🛬": ["airport", "flight", "boarding"], + "⛵": ["ship", "summer", "transportation", "water", "sailing"], + "🛥": ["ship"], + "🚤": ["ship", "transportation", "vehicle", "summer"], + "⛴": ["boat", "ship", "yacht"], + "🛳": ["yacht", "cruise", "ferry"], + "🚀": ["launch", "ship", "staffmode", "NASA", "outer space", "outer_space", "fly"], + "🛰": ["communication", "gps", "orbit", "spaceflight", "NASA", "ISS"], + "🛻": ["car"], + "🛼": [], + "💺": ["sit", "airplane", "transport", "bus", "flight", "fly"], + "🛶": ["boat", "paddle", "water", "ship"], + "⚓": ["ship", "ferry", "sea", "boat"], + "🚧": ["wip", "progress", "caution", "warning"], + "⛽": ["gas station", "petroleum"], + "🚏": ["transportation", "wait"], + "🚦": ["transportation", "driving"], + "🚥": ["transportation", "signal"], + "🏁": ["contest", "finishline", "race", "gokart"], + "🚢": ["transportation", "titanic", "deploy"], + "🎡": ["photo", "carnival", "londoneye"], + "🎢": ["carnival", "playground", "photo", "fun"], + "🎠": ["photo", "carnival"], + "🏗": ["wip", "working", "progress"], + "🌁": ["photo", "mountain"], + "🏭": ["building", "industry", "pollution", "smoke"], + "⛲": ["photo", "summer", "water", "fresh"], + "🎑": ["photo", "japan", "asia", "tsukimi"], + "⛰": ["photo", "nature", "environment"], + "🏔": ["photo", "nature", "environment", "winter", "cold"], + "🗻": ["photo", "mountain", "nature", "japanese"], + "🌋": ["photo", "nature", "disaster"], + "🗾": ["nation", "country", "japanese", "asia"], + "🏕": ["photo", "outdoors", "tent"], + "⛺": ["photo", "camping", "outdoors"], + "🏞": ["photo", "environment", "nature"], + "🛣": ["road", "cupertino", "interstate", "highway"], + "🛤": ["train", "transportation"], + "🌅": ["morning", "view", "vacation", "photo"], + "🌄": ["view", "vacation", "photo"], + "🏜": ["photo", "warm", "saharah"], + "🏖": ["weather", "summer", "sunny", "sand", "mojito"], + "🏝": ["photo", "tropical", "mojito"], + "🌇": ["photo", "good morning", "dawn"], + "🌆": ["photo", "evening", "sky", "buildings"], + "🏙": ["photo", "night life", "urban"], + "🌃": ["evening", "city", "downtown"], + "🌉": ["photo", "sanfrancisco"], + "🌌": ["photo", "space", "stars"], + "🌠": ["night", "photo"], + "🎇": ["stars", "night", "shine"], + "🎆": ["photo", "festival", "carnival", "congratulations"], + "🌈": ["nature", "happy", "unicorn_face", "photo", "sky", "spring"], + "🏘": ["buildings", "photo"], + "🏰": ["building", "royalty", "history"], + "🏯": ["photo", "building"], + "🗼": ["photo", "japanese"], + "": ["photo", "japanese"], + "🏟": ["photo", "place", "sports", "concert", "venue"], + "🗽": ["american", "newyork"], + "🏠": ["building", "home"], + "🏡": ["home", "plant", "nature"], + "🏚": ["abandon", "evict", "broken", "building"], + "🏢": ["building", "bureau", "work"], + "🏬": ["building", "shopping", "mall"], + "🏣": ["building", "envelope", "communication"], + "🏤": ["building", "email"], + "🏥": ["building", "health", "surgery", "doctor"], + "🏦": ["building", "money", "sales", "cash", "business", "enterprise"], + "🏨": ["building", "accomodation", "checkin"], + "🏪": ["building", "shopping", "groceries"], + "🏫": ["building", "student", "education", "learn", "teach"], + "🏩": ["like", "affection", "dating"], + "💒": ["love", "like", "affection", "couple", "marriage", "bride", "groom"], + "🏛": ["art", "culture", "history"], + "⛪": ["building", "religion", "christ"], + "🕌": ["islam", "worship", "minaret"], + "🕍": ["judaism", "worship", "temple", "jewish"], + "🕋": ["mecca", "mosque", "islam"], + "⛩": ["temple", "japan", "kyoto"], + "🛕": ["temple"], + "🪨": [], + "🪵": [], + "🛖": [], + "🛝": [], + "🛞": [], + "🛟": [], + "⌚": ["time", "accessories"], + "📱": ["technology", "apple", "gadgets", "dial"], + "📲": ["iphone", "incoming"], + "💻": ["technology", "laptop", "screen", "display", "monitor"], + "⌨": ["technology", "computer", "type", "input", "text"], + "🖥": ["technology", "computing", "screen"], + "🖨": ["paper", "ink"], + "🖱": ["click"], + "🖲": ["technology", "trackpad"], + "🕹": ["game", "play"], + "🗜": ["tool"], + "💽": ["technology", "record", "data", "disk", "90s"], + "💾": ["oldschool", "technology", "save", "90s", "80s"], + "💿": ["technology", "dvd", "disk", "disc", "90s"], + "📀": ["cd", "disk", "disc"], + "📼": ["record", "video", "oldschool", "90s", "80s"], + "📷": ["gadgets", "photography"], + "📸": ["photography", "gadgets"], + "📹": ["film", "record"], + "🎥": ["film", "record"], + "📽": ["video", "tape", "record", "movie"], + "🎞": ["movie"], + "📞": ["technology", "communication", "dial"], + "☎️": ["technology", "communication", "dial", "telephone"], + "📟": ["bbcall", "oldschool", "90s"], + "📠": ["communication", "technology"], + "📺": ["technology", "program", "oldschool", "show", "television"], + "📻": ["communication", "music", "podcast", "program"], + "🎙": ["sing", "recording", "artist", "talkshow"], + "🎚": ["scale"], + "🎛": ["dial"], + "🧭": ["magnetic", "navigation", "orienteering"], + "⏱": ["time", "deadline"], + "⏲": ["alarm"], + "⏰": ["time", "wake"], + "🕰": ["time"], + "⏳": ["oldschool", "time", "countdown"], + "⌛": ["time", "clock", "oldschool", "limit", "exam", "quiz", "test"], + "📡": ["communication", "future", "radio", "space"], + "🔋": ["power", "energy", "sustain"], + "🪫": [], + "🔌": ["charger", "power"], + "💡": ["light", "electricity", "idea"], + "🔦": ["dark", "camping", "sight", "night"], + "🕯": ["fire", "wax"], + "🧯": ["quench"], + "🗑": ["bin", "trash", "rubbish", "garbage", "toss"], + "🛢": ["barrell"], + "💸": ["dollar", "bills", "payment", "sale"], + "💵": ["money", "sales", "bill", "currency"], + "💴": ["money", "sales", "japanese", "dollar", "currency"], + "💶": ["money", "sales", "dollar", "currency"], + "💷": ["british", "sterling", "money", "sales", "bills", "uk", "england", "currency"], + "💰": ["dollar", "payment", "coins", "sale"], + "🪙": ["dollar", "payment", "coins", "sale"], + "💳": ["money", "sales", "dollar", "bill", "payment", "shopping"], + "🪫": [], + "💎": ["blue", "ruby", "diamond", "jewelry"], + "⚖": ["law", "fairness", "weight"], + "🧰": ["tools", "diy", "fix", "maintainer", "mechanic"], + "🔧": ["tools", "diy", "ikea", "fix", "maintainer"], + "🔨": ["tools", "build", "create"], + "⚒": ["tools", "build", "create"], + "🛠": ["tools", "build", "create"], + "⛏": ["tools", "dig"], + "🪓": ["tools"], + "🦯": ["tools"], + "🔩": ["handy", "tools", "fix"], + "⚙": ["cog"], + "🪃": ["tool"], + "🪚": ["tool"], + "🪛": ["tool"], + "🪝": ["tool"], + "🪜": ["tool"], + "🧱": ["bricks"], + "⛓": ["lock", "arrest"], + "🧲": ["attraction", "magnetic"], + "🔫": ["violence", "weapon", "pistol", "revolver"], + "💣": ["boom", "explode", "explosion", "terrorism"], + "🧨": ["dynamite", "boom", "explode", "explosion", "explosive"], + "🔪": ["knife", "blade", "cutlery", "kitchen", "weapon"], + "🗡": ["weapon"], + "⚔": ["weapon"], + "🛡": ["protection", "security"], + "🚬": ["kills", "tobacco", "cigarette", "joint", "smoke"], + "☠": ["poison", "danger", "deadly", "scary", "death", "pirate", "evil"], + "⚰": ["vampire", "dead", "die", "death", "rip", "graveyard", "cemetery", "casket", "funeral", "box"], + "⚱": ["dead", "die", "death", "rip", "ashes"], + "🏺": ["vase", "jar"], + "🔮": ["disco", "party", "magic", "circus", "fortune_teller"], + "📿": ["dhikr", "religious"], + "🧿": ["bead", "charm"], + "💈": ["hair", "salon", "style"], + "⚗": ["distilling", "science", "experiment", "chemistry"], + "🔭": ["stars", "space", "zoom", "science", "astronomy"], + "🔬": ["laboratory", "experiment", "zoomin", "science", "study"], + "🕳": ["embarrassing"], + "💊": ["health", "medicine", "doctor", "pharmacy", "drug"], + "💉": ["health", "hospital", "drugs", "blood", "medicine", "needle", "doctor", "nurse"], + "🩸": ["health", "hospital", "medicine", "needle", "doctor", "nurse"], + "🩹": ["health", "hospital", "medicine", "needle", "doctor", "nurse"], + "🩺": ["health", "hospital", "medicine", "needle", "doctor", "nurse"], + "🪒": ["health"], + "🩻": [], + "🩼": [], + "🧬": ["biologist", "genetics", "life"], + "🧫": ["bacteria", "biology", "culture", "lab"], + "🧪": ["chemistry", "experiment", "lab", "science"], + "🌡": ["weather", "temperature", "hot", "cold"], + "🧹": ["cleaning", "sweeping", "witch"], + "🧺": ["laundry"], + "🧻": ["roll"], + "🏷": ["sale", "tag"], + "🔖": ["favorite", "label", "save"], + "🚽": ["restroom", "wc", "washroom", "bathroom", "potty"], + "🚿": ["clean", "water", "bathroom"], + "🛁": ["clean", "shower", "bathroom"], + "🧼": ["bar", "bathing", "cleaning", "lather"], + "🧽": ["absorbing", "cleaning", "porous"], + "🧴": ["moisturizer", "sunscreen"], + "🔑": ["lock", "door", "password"], + "🗝": ["lock", "door", "password"], + "🛋": ["read", "chill"], + "🪔": ["light", "oil"], + "🛌": ["bed", "rest"], + "🛏": ["sleep", "rest"], + "🚪": ["house", "entry", "exit"], + "🪑": ["house", "desk"], + "🛎": ["service"], + "🧸": ["plush", "stuffed"], + "🖼": ["photography"], + "🗺": ["location", "direction"], + "🛗": ["household"], + "🪞": ["household"], + "🪟": ["household"], + "🪠": ["household"], + "🪤": ["household"], + "🪣": ["household"], + "🪥": ["household"], + "🫧": [], + "⛱": ["weather", "summer"], + "🗿": ["rock", "easter island", "moai"], + "🛍": ["mall", "buy", "purchase"], + "🛒": ["trolley"], + "🎈": ["party", "celebration", "birthday", "circus"], + "🎏": ["fish", "japanese", "koinobori", "carp", "banner"], + "🎀": ["decoration", "pink", "girl", "bowtie"], + "🎁": ["present", "birthday", "christmas", "xmas"], + "🎊": ["festival", "party", "birthday", "circus"], + "🎉": ["party", "congratulations", "birthday", "magic", "circus", "celebration"], + "🎎": ["japanese", "toy", "kimono"], + "🎐": ["nature", "ding", "spring", "bell"], + "🎌": ["japanese", "nation", "country", "border"], + "🏮": ["light", "paper", "halloween", "spooky"], + "🧧": ["gift"], + "✉️": ["letter", "postal", "inbox", "communication"], + "📩": ["email", "communication"], + "📨": ["email", "inbox"], + "📧": ["communication", "inbox"], + "💌": ["email", "like", "affection", "envelope", "valentines"], + "📮": ["email", "letter", "envelope"], + "📪": ["email", "communication", "inbox"], + "📫": ["email", "inbox", "communication"], + "📬": ["email", "inbox", "communication"], + "📭": ["email", "inbox"], + "📦": ["mail", "gift", "cardboard", "box", "moving"], + "📯": ["instrument", "music"], + "📥": ["email", "documents"], + "📤": ["inbox", "email"], + "📜": ["documents", "ancient", "history", "paper"], + "📃": ["documents", "office", "paper"], + "📑": ["favorite", "save", "order", "tidy"], + "🧾": ["accounting", "expenses"], + "📊": ["graph", "presentation", "stats"], + "📈": ["graph", "presentation", "stats", "recovery", "business", "economics", "money", "sales", "good", "success"], + "📉": ["graph", "presentation", "stats", "recession", "business", "economics", "money", "sales", "bad", "failure"], + "📄": ["documents", "office", "paper", "information"], + "📅": ["calendar", "schedule"], + "📆": ["schedule", "date", "planning"], + "🗓": ["date", "schedule", "planning"], + "📇": ["business", "stationery"], + "🗃": ["business", "stationery"], + "🗳": ["election", "vote"], + "🗄": ["filing", "organizing"], + "📋": ["stationery", "documents"], + "🗒": ["memo", "stationery"], + "📁": ["documents", "business", "office"], + "📂": ["documents", "load"], + "🗂": ["organizing", "business", "stationery"], + "🗞": ["press", "headline"], + "📰": ["press", "headline"], + "📓": ["stationery", "record", "notes", "paper", "study"], + "📕": ["read", "library", "knowledge", "textbook", "learn"], + "📗": ["read", "library", "knowledge", "study"], + "📘": ["read", "library", "knowledge", "learn", "study"], + "📙": ["read", "library", "knowledge", "textbook", "study"], + "📔": ["classroom", "notes", "record", "paper", "study"], + "📒": ["notes", "paper"], + "📚": ["literature", "library", "study"], + "📖": ["book", "read", "library", "knowledge", "literature", "learn", "study"], + "🧷": ["diaper"], + "🔗": ["rings", "url"], + "📎": ["documents", "stationery"], + "🖇": ["documents", "stationery"], + "✂️": ["stationery", "cut"], + "📐": ["stationery", "math", "architect", "sketch"], + "📏": ["stationery", "calculate", "length", "math", "school", "drawing", "architect", "sketch"], + "🧮": ["calculation"], + "📌": ["stationery", "mark", "here"], + "📍": ["stationery", "location", "map", "here"], + "🚩": ["mark", "milestone", "place"], + "🏳": ["losing", "loser", "lost", "surrender", "give up", "fail"], + "🏴": ["pirate"], + "🏳️🌈": ["flag", "rainbow", "pride", "gay", "lgbt", "glbt", "queer", "homosexual", "lesbian", "bisexual", "transgender"], + "🏳️⚧️": ["flag", "transgender"], + "🔐": ["security", "privacy"], + "🔒": ["security", "password", "padlock"], + "🔓": ["privacy", "security"], + "🔏": ["security", "secret"], + "🖊": ["stationery", "writing", "write"], + "🖋": ["stationery", "writing", "write"], + "✒️": ["pen", "stationery", "writing", "write"], + "📝": ["write", "documents", "stationery", "pencil", "paper", "writing", "legal", "exam", "quiz", "test", "study", "compose"], + "✏️": ["stationery", "write", "paper", "writing", "school", "study"], + "🖍": ["drawing", "creativity"], + "🖌": ["drawing", "creativity", "art"], + "🔍": ["search", "zoom", "find", "detective"], + "🔎": ["search", "zoom", "find", "detective"], + "🪦": [], + "🪧": [], + "💯": ["score", "perfect", "numbers", "century", "exam", "quiz", "test", "pass", "hundred"], + "🔢": ["numbers", "blue-square"], + "❤️": ["love", "like", "affection", "valentines"], + "🧡": ["love", "like", "affection", "valentines"], + "💛": ["love", "like", "affection", "valentines"], + "💚": ["love", "like", "affection", "valentines"], + "💙": ["love", "like", "affection", "valentines"], + "💜": ["love", "like", "affection", "valentines"], + "🤎": ["love", "like", "affection", "valentines"], + "🖤": ["love", "like", "affection", "valentines"], + "🤍": ["love", "like", "affection", "valentines"], + "💔": ["sad", "sorry", "break", "heart", "heartbreak"], + "❣": ["decoration", "love"], + "💕": ["love", "like", "affection", "valentines", "heart"], + "💞": ["love", "like", "affection", "valentines"], + "💓": ["love", "like", "affection", "valentines", "pink", "heart"], + "💗": ["like", "love", "affection", "valentines", "pink"], + "💖": ["love", "like", "affection", "valentines"], + "💘": ["love", "like", "heart", "affection", "valentines"], + "💝": ["love", "valentines"], + "💟": ["purple-square", "love", "like"], + "❤️🔥": [], + "❤️🩹": [], + "☮": ["hippie"], + "✝": ["christianity"], + "☪": ["islam"], + "🕉": ["hinduism", "buddhism", "sikhism", "jainism"], + "☸": ["hinduism", "buddhism", "sikhism", "jainism"], + "✡": ["judaism"], + "🔯": ["purple-square", "religion", "jewish", "hexagram"], + "🕎": ["hanukkah", "candles", "jewish"], + "☯": ["balance"], + "☦": ["suppedaneum", "religion"], + "🛐": ["religion", "church", "temple", "prayer"], + "⛎": ["sign", "purple-square", "constellation", "astrology"], + "♈": ["sign", "purple-square", "zodiac", "astrology"], + "♉": ["purple-square", "sign", "zodiac", "astrology"], + "♊": ["sign", "zodiac", "purple-square", "astrology"], + "♋": ["sign", "zodiac", "purple-square", "astrology"], + "♌": ["sign", "purple-square", "zodiac", "astrology"], + "♍": ["sign", "zodiac", "purple-square", "astrology"], + "♎": ["sign", "purple-square", "zodiac", "astrology"], + "♏": ["sign", "zodiac", "purple-square", "astrology", "scorpio"], + "♐": ["sign", "zodiac", "purple-square", "astrology"], + "♑": ["sign", "zodiac", "purple-square", "astrology"], + "♒": ["sign", "purple-square", "zodiac", "astrology"], + "♓": ["purple-square", "sign", "zodiac", "astrology"], + "🆔": ["purple-square", "words"], + "⚛": ["science", "physics", "chemistry"], + "⚧️": ["purple-square", "woman", "female", "toilet", "loo", "restroom", "gender"], + "🈳": ["kanji", "japanese", "chinese", "empty", "sky", "blue-square", "aki"], + "🈹": ["cut", "divide", "chinese", "kanji", "pink-square", "waribiki"], + "☢": ["nuclear", "danger"], + "☣": ["danger"], + "📴": ["mute", "orange-square", "silence", "quiet"], + "📳": ["orange-square", "phone"], + "🈶": ["orange-square", "chinese", "have", "kanji", "ari"], + "🈚": ["nothing", "chinese", "kanji", "japanese", "orange-square", "nashi"], + "🈸": ["chinese", "japanese", "kanji", "orange-square", "moushikomi"], + "🈺": ["japanese", "opening hours", "orange-square", "eigyo"], + "🈷️": ["chinese", "month", "moon", "japanese", "orange-square", "kanji", "tsuki", "tsukigime", "getsugaku"], + "✴️": ["orange-square", "shape", "polygon"], + "🆚": ["words", "orange-square"], + "🉑": ["ok", "good", "chinese", "kanji", "agree", "yes", "orange-circle"], + "💮": ["japanese", "spring"], + "🉐": ["chinese", "kanji", "obtain", "get", "circle"], + "㊙️": ["privacy", "chinese", "sshh", "kanji", "red-circle"], + "㊗️": ["chinese", "kanji", "japanese", "red-circle"], + "🈴": ["japanese", "chinese", "join", "kanji", "red-square", "goukaku", "pass"], + "🈵": ["full", "chinese", "japanese", "red-square", "kanji", "man"], + "🈲": ["kanji", "japanese", "chinese", "forbidden", "limit", "restricted", "red-square", "kinshi"], + "🅰️": ["red-square", "alphabet", "letter"], + "🅱️": ["red-square", "alphabet", "letter"], + "🆎": ["red-square", "alphabet"], + "🆑": ["alphabet", "words", "red-square"], + "🅾️": ["alphabet", "red-square", "letter"], + "🆘": ["help", "red-square", "words", "emergency", "911"], + "⛔": ["limit", "security", "privacy", "bad", "denied", "stop", "circle"], + "📛": ["fire", "forbid"], + "🚫": ["forbid", "stop", "limit", "denied", "disallow", "circle"], + "❌": ["no", "delete", "remove", "cancel", "red"], + "⭕": ["circle", "round"], + "🛑": ["stop"], + "💢": ["angry", "mad"], + "♨️": ["bath", "warm", "relax"], + "🚷": ["rules", "crossing", "walking", "circle"], + "🚯": ["trash", "bin", "garbage", "circle"], + "🚳": ["cyclist", "prohibited", "circle"], + "🚱": ["drink", "faucet", "tap", "circle"], + "🔞": ["18", "drink", "pub", "night", "minor", "circle"], + "📵": ["iphone", "mute", "circle"], + "❗": ["heavy_exclamation_mark", "danger", "surprise", "punctuation", "wow", "warning"], + "❕": ["surprise", "punctuation", "gray", "wow", "warning"], + "❓": ["doubt", "confused"], + "❔": ["doubts", "gray", "huh", "confused"], + "‼️": ["exclamation", "surprise"], + "⁉️": ["wat", "punctuation", "surprise"], + "🔅": ["sun", "afternoon", "warm", "summer"], + "🔆": ["sun", "light"], + "🔱": ["weapon", "spear"], + "⚜": ["decorative", "scout"], + "〽️": ["graph", "presentation", "stats", "business", "economics", "bad"], + "⚠️": ["exclamation", "wip", "alert", "error", "problem", "issue"], + "🚸": ["school", "warning", "danger", "sign", "driving", "yellow-diamond"], + "🔰": ["badge", "shield"], + "♻️": ["arrow", "environment", "garbage", "trash"], + "🈯": ["chinese", "point", "green-square", "kanji", "reserved", "shiteiseki"], + "💹": ["green-square", "graph", "presentation", "stats"], + "❇️": ["stars", "green-square", "awesome", "good", "fireworks"], + "✳️": ["star", "sparkle", "green-square"], + "❎": ["x", "green-square", "no", "deny"], + "✅": ["green-square", "ok", "agree", "vote", "election", "answer", "tick"], + "💠": ["jewel", "blue", "gem", "crystal", "fancy"], + "🌀": ["weather", "swirl", "blue", "cloud", "vortex", "spiral", "whirlpool", "spin", "tornado", "hurricane", "typhoon"], + "➿": ["tape", "cassette"], + "🌐": ["earth", "international", "world", "internet", "interweb", "i18n"], + "Ⓜ️": ["alphabet", "blue-circle", "letter"], + "🏧": ["money", "sales", "cash", "blue-square", "payment", "bank"], + "🈂️": ["japanese", "blue-square", "katakana"], + "🛂": ["custom", "blue-square"], + "🛃": ["passport", "border", "blue-square"], + "🛄": ["blue-square", "airport", "transport"], + "🛅": ["blue-square", "travel"], + "♿": ["blue-square", "disabled", "a11y", "accessibility"], + "🚭": ["cigarette", "blue-square", "smell", "smoke"], + "🚾": ["toilet", "restroom", "blue-square"], + "🅿️": ["cars", "blue-square", "alphabet", "letter"], + "🚰": ["blue-square", "liquid", "restroom", "cleaning", "faucet"], + "🚹": ["toilet", "restroom", "wc", "blue-square", "gender", "male"], + "🚺": ["purple-square", "woman", "female", "toilet", "loo", "restroom", "gender"], + "🚼": ["orange-square", "child"], + "🚻": ["blue-square", "toilet", "refresh", "wc", "gender"], + "🚮": ["blue-square", "sign", "human", "info"], + "🎦": ["blue-square", "record", "film", "movie", "curtain", "stage", "theater"], + "📶": ["blue-square", "reception", "phone", "internet", "connection", "wifi", "bluetooth", "bars"], + "🈁": ["blue-square", "here", "katakana", "japanese", "destination"], + "🆖": ["blue-square", "words", "shape", "icon"], + "🆗": ["good", "agree", "yes", "blue-square"], + "🆙": ["blue-square", "above", "high"], + "🆒": ["words", "blue-square"], + "🆕": ["blue-square", "words", "start"], + "🆓": ["blue-square", "words"], + "0️⃣": ["0", "numbers", "blue-square", "null"], + "1️⃣": ["blue-square", "numbers", "1"], + "2️⃣": ["numbers", "2", "prime", "blue-square"], + "3️⃣": ["3", "numbers", "prime", "blue-square"], + "4️⃣": ["4", "numbers", "blue-square"], + "5️⃣": ["5", "numbers", "blue-square", "prime"], + "6️⃣": ["6", "numbers", "blue-square"], + "7️⃣": ["7", "numbers", "blue-square", "prime"], + "8️⃣": ["8", "blue-square", "numbers"], + "9️⃣": ["blue-square", "numbers", "9"], + "🔟": ["numbers", "10", "blue-square"], + "*⃣": ["star", "keycap"], + "⏏️": ["blue-square"], + "▶️": ["blue-square", "right", "direction", "play"], + "⏸": ["pause", "blue-square"], + "⏭": ["forward", "next", "blue-square"], + "⏹": ["blue-square"], + "⏺": ["blue-square"], + "⏯": ["blue-square", "play", "pause"], + "⏮": ["backward"], + "⏩": ["blue-square", "play", "speed", "continue"], + "⏪": ["play", "blue-square"], + "🔀": ["blue-square", "shuffle", "music", "random"], + "🔁": ["loop", "record"], + "🔂": ["blue-square", "loop"], + "◀️": ["blue-square", "left", "direction"], + "🔼": ["blue-square", "triangle", "direction", "point", "forward", "top"], + "🔽": ["blue-square", "direction", "bottom"], + "⏫": ["blue-square", "direction", "top"], + "⏬": ["blue-square", "direction", "bottom"], + "➡️": ["blue-square", "next"], + "⬅️": ["blue-square", "previous", "back"], + "⬆️": ["blue-square", "continue", "top", "direction"], + "⬇️": ["blue-square", "direction", "bottom"], + "↗️": ["blue-square", "point", "direction", "diagonal", "northeast"], + "↘️": ["blue-square", "direction", "diagonal", "southeast"], + "↙️": ["blue-square", "direction", "diagonal", "southwest"], + "↖️": ["blue-square", "point", "direction", "diagonal", "northwest"], + "↕️": ["blue-square", "direction", "way", "vertical"], + "↔️": ["shape", "direction", "horizontal", "sideways"], + "🔄": ["blue-square", "sync", "cycle"], + "↪️": ["blue-square", "return", "rotate", "direction"], + "↩️": ["back", "return", "blue-square", "undo", "enter"], + "⤴️": ["blue-square", "direction", "top"], + "⤵️": ["blue-square", "direction", "bottom"], + "#️⃣": ["symbol", "blue-square", "twitter"], + "ℹ️": ["blue-square", "alphabet", "letter"], + "🔤": ["blue-square", "alphabet"], + "🔡": ["blue-square", "alphabet"], + "🔠": ["alphabet", "words", "blue-square"], + "🔣": ["blue-square", "music", "note", "ampersand", "percent", "glyphs", "characters"], + "🎵": ["score", "tone", "sound"], + "🎶": ["music", "score"], + "〰️": ["draw", "line", "moustache", "mustache", "squiggle", "scribble"], + "➰": ["scribble", "draw", "shape", "squiggle"], + "✔️": ["ok", "nike", "answer", "yes", "tick"], + "🔃": ["sync", "cycle", "round", "repeat"], + "➕": ["math", "calculation", "addition", "more", "increase"], + "➖": ["math", "calculation", "subtract", "less"], + "➗": ["divide", "math", "calculation"], + "✖️": ["math", "calculation"], + "🟰": [], + "♾": ["forever"], + "💲": ["money", "sales", "payment", "currency", "buck"], + "💱": ["money", "sales", "dollar", "travel"], + "©️": ["ip", "license", "circle", "law", "legal"], + "®️": ["alphabet", "circle"], + "™️": ["trademark", "brand", "law", "legal"], + "🔚": ["words", "arrow"], + "🔙": ["arrow", "words", "return"], + "🔛": ["arrow", "words"], + "🔝": ["words", "blue-square"], + "🔜": ["arrow", "words"], + "☑️": ["ok", "agree", "confirm", "black-square", "vote", "election", "yes", "tick"], + "🔘": ["input", "old", "music", "circle"], + "⚫": ["shape", "button", "round"], + "⚪": ["shape", "round"], + "🔴": ["shape", "error", "danger"], + "🟠": ["shape"], + "🟡": ["shape"], + "🟢": ["shape"], + "🔵": ["shape", "icon", "button"], + "🟣": ["shape"], + "🟤": ["shape"], + "🔸": ["shape", "jewel", "gem"], + "🔹": ["shape", "jewel", "gem"], + "🔶": ["shape", "jewel", "gem"], + "🔷": ["shape", "jewel", "gem"], + "🔺": ["shape", "direction", "up", "top"], + "▪️": ["shape", "icon"], + "▫️": ["shape", "icon"], + "⬛": ["shape", "icon", "button"], + "⬜": ["shape", "icon", "stone", "button"], + "🟥": ["shape"], + "🟧": ["shape"], + "🟨": ["shape"], + "🟩": ["shape"], + "🟦": ["shape"], + "🟪": ["shape"], + "🟫": ["shape"], + "🔻": ["shape", "direction", "bottom"], + "◼️": ["shape", "button", "icon"], + "◻️": ["shape", "stone", "icon"], + "◾": ["icon", "shape", "button"], + "◽": ["shape", "stone", "icon", "button"], + "🔲": ["shape", "input", "frame"], + "🔳": ["shape", "input"], + "🔈": ["sound", "volume", "silence", "broadcast"], + "🔉": ["volume", "speaker", "broadcast"], + "🔊": ["volume", "noise", "noisy", "speaker", "broadcast"], + "🔇": ["sound", "volume", "silence", "quiet"], + "📣": ["sound", "speaker", "volume"], + "📢": ["volume", "sound"], + "🔔": ["sound", "notification", "christmas", "xmas", "chime"], + "🔕": ["sound", "volume", "mute", "quiet", "silent"], + "🃏": ["poker", "cards", "game", "play", "magic"], + "🀄": ["game", "play", "chinese", "kanji"], + "♠️": ["poker", "cards", "suits", "magic"], + "♣️": ["poker", "cards", "magic", "suits"], + "♥️": ["poker", "cards", "magic", "suits"], + "♦️": ["poker", "cards", "magic", "suits"], + "🎴": ["game", "sunset", "red"], + "💭": ["bubble", "cloud", "speech", "thinking", "dream"], + "🗯": ["caption", "speech", "thinking", "mad"], + "💬": ["bubble", "words", "message", "talk", "chatting"], + "🗨": ["words", "message", "talk", "chatting"], + "🕐": ["time", "late", "early", "schedule"], + "🕑": ["time", "late", "early", "schedule"], + "🕒": ["time", "late", "early", "schedule"], + "🕓": ["time", "late", "early", "schedule"], + "🕔": ["time", "late", "early", "schedule"], + "🕕": ["time", "late", "early", "schedule", "dawn", "dusk"], + "🕖": ["time", "late", "early", "schedule"], + "🕗": ["time", "late", "early", "schedule"], + "🕘": ["time", "late", "early", "schedule"], + "🕙": ["time", "late", "early", "schedule"], + "🕚": ["time", "late", "early", "schedule"], + "🕛": ["time", "noon", "midnight", "midday", "late", "early", "schedule"], + "🕜": ["time", "late", "early", "schedule"], + "🕝": ["time", "late", "early", "schedule"], + "🕞": ["time", "late", "early", "schedule"], + "🕟": ["time", "late", "early", "schedule"], + "🕠": ["time", "late", "early", "schedule"], + "🕡": ["time", "late", "early", "schedule"], + "🕢": ["time", "late", "early", "schedule"], + "🕣": ["time", "late", "early", "schedule"], + "🕤": ["time", "late", "early", "schedule"], + "🕥": ["time", "late", "early", "schedule"], + "🕦": ["time", "late", "early", "schedule"], + "🕧": ["time", "late", "early", "schedule"], + "🇦🇫": ["af", "flag", "nation", "country", "banner"], + "🇦🇽": ["Åland", "islands", "flag", "nation", "country", "banner"], + "🇦🇱": ["al", "flag", "nation", "country", "banner"], + "🇩🇿": ["dz", "flag", "nation", "country", "banner"], + "🇦🇸": ["american", "ws", "flag", "nation", "country", "banner"], + "🇦🇩": ["ad", "flag", "nation", "country", "banner"], + "🇦🇴": ["ao", "flag", "nation", "country", "banner"], + "🇦🇮": ["ai", "flag", "nation", "country", "banner"], + "🇦🇶": ["aq", "flag", "nation", "country", "banner"], + "🇦🇬": ["antigua", "barbuda", "flag", "nation", "country", "banner"], + "🇦🇷": ["ar", "flag", "nation", "country", "banner"], + "🇦🇲": ["am", "flag", "nation", "country", "banner"], + "🇦🇼": ["aw", "flag", "nation", "country", "banner"], + "🇦🇨": ["flag", "nation", "country", "banner"], + "🇦🇺": ["au", "flag", "nation", "country", "banner"], + "🇦🇹": ["at", "flag", "nation", "country", "banner"], + "🇦🇿": ["az", "flag", "nation", "country", "banner"], + "🇧🇸": ["bs", "flag", "nation", "country", "banner"], + "🇧🇭": ["bh", "flag", "nation", "country", "banner"], + "🇧🇩": ["bd", "flag", "nation", "country", "banner"], + "🇧🇧": ["bb", "flag", "nation", "country", "banner"], + "🇧🇾": ["by", "flag", "nation", "country", "banner"], + "🇧🇪": ["be", "flag", "nation", "country", "banner"], + "🇧🇿": ["bz", "flag", "nation", "country", "banner"], + "🇧🇯": ["bj", "flag", "nation", "country", "banner"], + "🇧🇲": ["bm", "flag", "nation", "country", "banner"], + "🇧🇹": ["bt", "flag", "nation", "country", "banner"], + "🇧🇴": ["bo", "flag", "nation", "country", "banner"], + "🇧🇶": ["bonaire", "flag", "nation", "country", "banner"], + "🇧🇦": ["bosnia", "herzegovina", "flag", "nation", "country", "banner"], + "🇧🇼": ["bw", "flag", "nation", "country", "banner"], + "🇧🇷": ["br", "flag", "nation", "country", "banner"], + "🇮🇴": ["british", "indian", "ocean", "territory", "flag", "nation", "country", "banner"], + "🇻🇬": ["british", "virgin", "islands", "bvi", "flag", "nation", "country", "banner"], + "🇧🇳": ["bn", "darussalam", "flag", "nation", "country", "banner"], + "🇧🇬": ["bg", "flag", "nation", "country", "banner"], + "🇧🇫": ["burkina", "faso", "flag", "nation", "country", "banner"], + "🇧🇮": ["bi", "flag", "nation", "country", "banner"], + "🇨🇻": ["cabo", "verde", "flag", "nation", "country", "banner"], + "🇰🇭": ["kh", "flag", "nation", "country", "banner"], + "🇨🇲": ["cm", "flag", "nation", "country", "banner"], + "🇨🇦": ["ca", "flag", "nation", "country", "banner"], + "🇮🇨": ["canary", "islands", "flag", "nation", "country", "banner"], + "🇰🇾": ["cayman", "islands", "flag", "nation", "country", "banner"], + "🇨🇫": ["central", "african", "republic", "flag", "nation", "country", "banner"], + "🇹🇩": ["td", "flag", "nation", "country", "banner"], + "🇨🇱": ["flag", "nation", "country", "banner"], + "🇨🇳": ["china", "chinese", "prc", "flag", "country", "nation", "banner"], + "🇨🇽": ["christmas", "island", "flag", "nation", "country", "banner"], + "🇨🇨": ["cocos", "keeling", "islands", "flag", "nation", "country", "banner"], + "🇨🇴": ["co", "flag", "nation", "country", "banner"], + "🇰🇲": ["km", "flag", "nation", "country", "banner"], + "🇨🇬": ["congo", "flag", "nation", "country", "banner"], + "🇨🇩": ["congo", "democratic", "republic", "flag", "nation", "country", "banner"], + "🇨🇰": ["cook", "islands", "flag", "nation", "country", "banner"], + "🇨🇷": ["costa", "rica", "flag", "nation", "country", "banner"], + "🇭🇷": ["hr", "flag", "nation", "country", "banner"], + "🇨🇺": ["cu", "flag", "nation", "country", "banner"], + "🇨🇼": ["curaçao", "flag", "nation", "country", "banner"], + "🇨🇾": ["cy", "flag", "nation", "country", "banner"], + "🇨🇿": ["cz", "flag", "nation", "country", "banner"], + "🇩🇰": ["dk", "flag", "nation", "country", "banner"], + "🇩🇯": ["dj", "flag", "nation", "country", "banner"], + "🇩🇲": ["dm", "flag", "nation", "country", "banner"], + "🇩🇴": ["dominican", "republic", "flag", "nation", "country", "banner"], + "🇪🇨": ["ec", "flag", "nation", "country", "banner"], + "🇪🇬": ["eg", "flag", "nation", "country", "banner"], + "🇸🇻": ["el", "salvador", "flag", "nation", "country", "banner"], + "🇬🇶": ["equatorial", "gn", "flag", "nation", "country", "banner"], + "🇪🇷": ["er", "flag", "nation", "country", "banner"], + "🇪🇪": ["ee", "flag", "nation", "country", "banner"], + "🇪🇹": ["et", "flag", "nation", "country", "banner"], + "🇪🇺": ["european", "union", "flag", "banner"], + "🇫🇰": ["falkland", "islands", "malvinas", "flag", "nation", "country", "banner"], + "🇫🇴": ["faroe", "islands", "flag", "nation", "country", "banner"], + "🇫🇯": ["fj", "flag", "nation", "country", "banner"], + "🇫🇮": ["fi", "flag", "nation", "country", "banner"], + "🇫🇷": ["banner", "flag", "nation", "france", "french", "country"], + "🇬🇫": ["french", "guiana", "flag", "nation", "country", "banner"], + "🇵🇫": ["french", "polynesia", "flag", "nation", "country", "banner"], + "🇹🇫": ["french", "southern", "territories", "flag", "nation", "country", "banner"], + "🇬🇦": ["ga", "flag", "nation", "country", "banner"], + "🇬🇲": ["gm", "flag", "nation", "country", "banner"], + "🇬🇪": ["ge", "flag", "nation", "country", "banner"], + "🇩🇪": ["german", "nation", "flag", "country", "banner"], + "🇬🇭": ["gh", "flag", "nation", "country", "banner"], + "🇬🇮": ["gi", "flag", "nation", "country", "banner"], + "🇬🇷": ["gr", "flag", "nation", "country", "banner"], + "🇬🇱": ["gl", "flag", "nation", "country", "banner"], + "🇬🇩": ["gd", "flag", "nation", "country", "banner"], + "🇬🇵": ["gp", "flag", "nation", "country", "banner"], + "🇬🇺": ["gu", "flag", "nation", "country", "banner"], + "🇬🇹": ["gt", "flag", "nation", "country", "banner"], + "🇬🇬": ["gg", "flag", "nation", "country", "banner"], + "🇬🇳": ["gn", "flag", "nation", "country", "banner"], + "🇬🇼": ["gw", "bissau", "flag", "nation", "country", "banner"], + "🇬🇾": ["gy", "flag", "nation", "country", "banner"], + "🇭🇹": ["ht", "flag", "nation", "country", "banner"], + "🇭🇳": ["hn", "flag", "nation", "country", "banner"], + "🇭🇰": ["hong", "kong", "flag", "nation", "country", "banner"], + "🇭🇺": ["hu", "flag", "nation", "country", "banner"], + "🇮🇸": ["is", "flag", "nation", "country", "banner"], + "🇮🇳": ["in", "flag", "nation", "country", "banner"], + "🇮🇩": ["flag", "nation", "country", "banner"], + "🇮🇷": ["iran, ", "islamic", "republic", "flag", "nation", "country", "banner"], + "🇮🇶": ["iq", "flag", "nation", "country", "banner"], + "🇮🇪": ["ie", "flag", "nation", "country", "banner"], + "🇮🇲": ["isle", "man", "flag", "nation", "country", "banner"], + "🇮🇱": ["il", "flag", "nation", "country", "banner"], + "🇮🇹": ["italy", "flag", "nation", "country", "banner"], + "🇨🇮": ["ivory", "coast", "flag", "nation", "country", "banner"], + "🇯🇲": ["jm", "flag", "nation", "country", "banner"], + "🇯🇵": ["japanese", "nation", "flag", "country", "banner"], + "🇯🇪": ["je", "flag", "nation", "country", "banner"], + "🇯🇴": ["jo", "flag", "nation", "country", "banner"], + "🇰🇿": ["kz", "flag", "nation", "country", "banner"], + "🇰🇪": ["ke", "flag", "nation", "country", "banner"], + "🇰🇮": ["ki", "flag", "nation", "country", "banner"], + "🇽🇰": ["xk", "flag", "nation", "country", "banner"], + "🇰🇼": ["kw", "flag", "nation", "country", "banner"], + "🇰🇬": ["kg", "flag", "nation", "country", "banner"], + "🇱🇦": ["lao", "democratic", "republic", "flag", "nation", "country", "banner"], + "🇱🇻": ["lv", "flag", "nation", "country", "banner"], + "🇱🇧": ["lb", "flag", "nation", "country", "banner"], + "🇱🇸": ["ls", "flag", "nation", "country", "banner"], + "🇱🇷": ["lr", "flag", "nation", "country", "banner"], + "🇱🇾": ["ly", "flag", "nation", "country", "banner"], + "🇱🇮": ["li", "flag", "nation", "country", "banner"], + "🇱🇹": ["lt", "flag", "nation", "country", "banner"], + "🇱🇺": ["lu", "flag", "nation", "country", "banner"], + "🇲🇴": ["macao", "flag", "nation", "country", "banner"], + "🇲🇰": ["macedonia, ", "flag", "nation", "country", "banner"], + "🇲🇬": ["mg", "flag", "nation", "country", "banner"], + "🇲🇼": ["mw", "flag", "nation", "country", "banner"], + "🇲🇾": ["my", "flag", "nation", "country", "banner"], + "🇲🇻": ["mv", "flag", "nation", "country", "banner"], + "🇲🇱": ["ml", "flag", "nation", "country", "banner"], + "🇲🇹": ["mt", "flag", "nation", "country", "banner"], + "🇲🇭": ["marshall", "islands", "flag", "nation", "country", "banner"], + "🇲🇶": ["mq", "flag", "nation", "country", "banner"], + "🇲🇷": ["mr", "flag", "nation", "country", "banner"], + "🇲🇺": ["mu", "flag", "nation", "country", "banner"], + "🇾🇹": ["yt", "flag", "nation", "country", "banner"], + "🇲🇽": ["mx", "flag", "nation", "country", "banner"], + "🇫🇲": ["micronesia, ", "federated", "states", "flag", "nation", "country", "banner"], + "🇲🇩": ["moldova, ", "republic", "flag", "nation", "country", "banner"], + "🇲🇨": ["mc", "flag", "nation", "country", "banner"], + "🇲🇳": ["mn", "flag", "nation", "country", "banner"], + "🇲🇪": ["me", "flag", "nation", "country", "banner"], + "🇲🇸": ["ms", "flag", "nation", "country", "banner"], + "🇲🇦": ["ma", "flag", "nation", "country", "banner"], + "🇲🇿": ["mz", "flag", "nation", "country", "banner"], + "🇲🇲": ["mm", "flag", "nation", "country", "banner"], + "🇳🇦": ["na", "flag", "nation", "country", "banner"], + "🇳🇷": ["nr", "flag", "nation", "country", "banner"], + "🇳🇵": ["np", "flag", "nation", "country", "banner"], + "🇳🇱": ["nl", "flag", "nation", "country", "banner"], + "🇳🇨": ["new", "caledonia", "flag", "nation", "country", "banner"], + "🇳🇿": ["new", "zealand", "flag", "nation", "country", "banner"], + "🇳🇮": ["ni", "flag", "nation", "country", "banner"], + "🇳🇪": ["ne", "flag", "nation", "country", "banner"], + "🇳🇬": ["flag", "nation", "country", "banner"], + "🇳🇺": ["nu", "flag", "nation", "country", "banner"], + "🇳🇫": ["norfolk", "island", "flag", "nation", "country", "banner"], + "🇲🇵": ["northern", "mariana", "islands", "flag", "nation", "country", "banner"], + "🇰🇵": ["north", "korea", "nation", "flag", "country", "banner"], + "🇳🇴": ["no", "flag", "nation", "country", "banner"], + "🇴🇲": ["om_symbol", "flag", "nation", "country", "banner"], + "🇵🇰": ["pk", "flag", "nation", "country", "banner"], + "🇵🇼": ["pw", "flag", "nation", "country", "banner"], + "🇵🇸": ["palestine", "palestinian", "territories", "flag", "nation", "country", "banner"], + "🇵🇦": ["pa", "flag", "nation", "country", "banner"], + "🇵🇬": ["papua", "new", "guinea", "flag", "nation", "country", "banner"], + "🇵🇾": ["py", "flag", "nation", "country", "banner"], + "🇵🇪": ["pe", "flag", "nation", "country", "banner"], + "🇵🇭": ["ph", "flag", "nation", "country", "banner"], + "🇵🇳": ["pitcairn", "flag", "nation", "country", "banner"], + "🇵🇱": ["pl", "flag", "nation", "country", "banner"], + "🇵🇹": ["pt", "flag", "nation", "country", "banner"], + "🇵🇷": ["puerto", "rico", "flag", "nation", "country", "banner"], + "🇶🇦": ["qa", "flag", "nation", "country", "banner"], + "🇷🇪": ["réunion", "flag", "nation", "country", "banner"], + "🇷🇴": ["ro", "flag", "nation", "country", "banner"], + "🇷🇺": ["russian", "federation", "flag", "nation", "country", "banner"], + "🇷🇼": ["rw", "flag", "nation", "country", "banner"], + "🇧🇱": ["saint", "barthélemy", "flag", "nation", "country", "banner"], + "🇸🇭": ["saint", "helena", "ascension", "tristan", "cunha", "flag", "nation", "country", "banner"], + "🇰🇳": ["saint", "kitts", "nevis", "flag", "nation", "country", "banner"], + "🇱🇨": ["saint", "lucia", "flag", "nation", "country", "banner"], + "🇵🇲": ["saint", "pierre", "miquelon", "flag", "nation", "country", "banner"], + "🇻🇨": ["saint", "vincent", "grenadines", "flag", "nation", "country", "banner"], + "🇼🇸": ["ws", "flag", "nation", "country", "banner"], + "🇸🇲": ["san", "marino", "flag", "nation", "country", "banner"], + "🇸🇹": ["sao", "tome", "principe", "flag", "nation", "country", "banner"], + "🇸🇦": ["flag", "nation", "country", "banner"], + "🇸🇳": ["sn", "flag", "nation", "country", "banner"], + "🇷🇸": ["rs", "flag", "nation", "country", "banner"], + "🇸🇨": ["sc", "flag", "nation", "country", "banner"], + "🇸🇱": ["sierra", "leone", "flag", "nation", "country", "banner"], + "🇸🇬": ["sg", "flag", "nation", "country", "banner"], + "🇸🇽": ["sint", "maarten", "dutch", "flag", "nation", "country", "banner"], + "🇸🇰": ["sk", "flag", "nation", "country", "banner"], + "🇸🇮": ["si", "flag", "nation", "country", "banner"], + "🇸🇧": ["solomon", "islands", "flag", "nation", "country", "banner"], + "🇸🇴": ["so", "flag", "nation", "country", "banner"], + "🇿🇦": ["south", "africa", "flag", "nation", "country", "banner"], + "🇬🇸": ["south", "georgia", "sandwich", "islands", "flag", "nation", "country", "banner"], + "🇰🇷": ["south", "korea", "nation", "flag", "country", "banner"], + "🇸🇸": ["south", "sd", "flag", "nation", "country", "banner"], + "🇪🇸": ["spain", "flag", "nation", "country", "banner"], + "🇱🇰": ["sri", "lanka", "flag", "nation", "country", "banner"], + "🇸🇩": ["sd", "flag", "nation", "country", "banner"], + "🇸🇷": ["sr", "flag", "nation", "country", "banner"], + "🇸🇿": ["sz", "flag", "nation", "country", "banner"], + "🇸🇪": ["se", "flag", "nation", "country", "banner"], + "🇨🇭": ["ch", "flag", "nation", "country", "banner"], + "🇸🇾": ["syrian", "arab", "republic", "flag", "nation", "country", "banner"], + "🇹🇼": ["tw", "flag", "nation", "country", "banner"], + "🇹🇯": ["tj", "flag", "nation", "country", "banner"], + "🇹🇿": ["tanzania, ", "united", "republic", "flag", "nation", "country", "banner"], + "🇹🇭": ["th", "flag", "nation", "country", "banner"], + "🇹🇱": ["timor", "leste", "flag", "nation", "country", "banner"], + "🇹🇬": ["tg", "flag", "nation", "country", "banner"], + "🇹🇰": ["tk", "flag", "nation", "country", "banner"], + "🇹🇴": ["to", "flag", "nation", "country", "banner"], + "🇹🇹": ["trinidad", "tobago", "flag", "nation", "country", "banner"], + "🇹🇦": ["flag", "nation", "country", "banner"], + "🇹🇳": ["tn", "flag", "nation", "country", "banner"], + "🇹🇷": ["turkey", "flag", "nation", "country", "banner"], + "🇹🇲": ["flag", "nation", "country", "banner"], + "🇹🇨": ["turks", "caicos", "islands", "flag", "nation", "country", "banner"], + "🇹🇻": ["flag", "nation", "country", "banner"], + "🇺🇬": ["ug", "flag", "nation", "country", "banner"], + "🇺🇦": ["ua", "flag", "nation", "country", "banner"], + "🇦🇪": ["united", "arab", "emirates", "flag", "nation", "country", "banner"], + "🇬🇧": ["united", "kingdom", "great", "britain", "northern", "ireland", "flag", "nation", "country", "banner", "british", "UK", "english", "england", "union jack"], + "🏴": ["flag", "english"], + "🏴": ["flag", "scottish"], + "🏴": ["flag", "welsh"], + "🇺🇸": ["united", "states", "america", "flag", "nation", "country", "banner"], + "🇻🇮": ["virgin", "islands", "us", "flag", "nation", "country", "banner"], + "🇺🇾": ["uy", "flag", "nation", "country", "banner"], + "🇺🇿": ["uz", "flag", "nation", "country", "banner"], + "🇻🇺": ["vu", "flag", "nation", "country", "banner"], + "🇻🇦": ["vatican", "city", "flag", "nation", "country", "banner"], + "🇻🇪": ["ve", "bolivarian", "republic", "flag", "nation", "country", "banner"], + "🇻🇳": ["viet", "nam", "flag", "nation", "country", "banner"], + "🇼🇫": ["wallis", "futuna", "flag", "nation", "country", "banner"], + "🇪🇭": ["western", "sahara", "flag", "nation", "country", "banner"], + "🇾🇪": ["ye", "flag", "nation", "country", "banner"], + "🇿🇲": ["zm", "flag", "nation", "country", "banner"], + "🇿🇼": ["zw", "flag", "nation", "country", "banner"], + "🇺🇳": ["un", "flag", "banner"], + "🏴☠️": ["skull", "crossbones", "flag", "banner"] +} diff --git a/packages/frontend/src/widgets/WidgetActivity.calendar.vue b/packages/frontend/src/widgets/WidgetActivity.calendar.vue index 84f6af1c1..110f1d32e 100644 --- a/packages/frontend/src/widgets/WidgetActivity.calendar.vue +++ b/packages/frontend/src/widgets/WidgetActivity.calendar.vue @@ -1,25 +1,31 @@ <template> <svg viewBox="0 0 21 7"> - <rect v-for="record in activity" class="day" + <rect + v-for="record in activity" class="day" width="1" height="1" :x="record.x" :y="record.date.weekday" rx="1" ry="1" - fill="transparent"> + fill="transparent" + > <title>{{ record.date.year }}/{{ record.date.month + 1 }}/{{ record.date.day }}</title> </rect> - <rect v-for="record in activity" class="day" + <rect + v-for="record in activity" class="day" :width="record.v" :height="record.v" :x="record.x + ((1 - record.v) / 2)" :y="record.date.weekday + ((1 - record.v) / 2)" rx="1" ry="1" :fill="record.color" - style="pointer-events: none;"/> - <rect class="today" + style="pointer-events: none;" + /> + <rect + class="today" width="1" height="1" :x="activity[0].x" :y="activity[0].date.weekday" rx="1" ry="1" fill="none" stroke-width="0.1" - stroke="#f73520"/> + stroke="#f73520" + /> </svg> </template> diff --git a/packages/frontend/src/widgets/WidgetActivity.chart.vue b/packages/frontend/src/widgets/WidgetActivity.chart.vue index b61e419f9..cc4df65dd 100644 --- a/packages/frontend/src/widgets/WidgetActivity.chart.vue +++ b/packages/frontend/src/widgets/WidgetActivity.chart.vue @@ -1,26 +1,30 @@ <template> -<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" @mousedown.prevent="onMousedown"> +<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" :class="$style.root" @mousedown.prevent="onMousedown"> <polyline :points="pointsNote" fill="none" stroke-width="1" - stroke="#41ddde"/> + stroke="#41ddde" + /> <polyline :points="pointsReply" fill="none" stroke-width="1" - stroke="#f7796c"/> + stroke="#f7796c" + /> <polyline :points="pointsRenote" fill="none" stroke-width="1" - stroke="#a1de41"/> + stroke="#a1de41" + /> <polyline :points="pointsTotal" fill="none" stroke-width="1" stroke="#555" - stroke-dasharray="2 2"/> + stroke-dasharray="2 2" + /> </svg> </template> @@ -81,8 +85,8 @@ function render() { } </script> -<style lang="scss" scoped> -svg { +<style lang="scss" module> +.root { display: block; padding: 16px; width: 100%; diff --git a/packages/frontend/src/widgets/WidgetActivity.vue b/packages/frontend/src/widgets/WidgetActivity.vue index e7f8819ab..892b24f69 100644 --- a/packages/frontend/src/widgets/WidgetActivity.vue +++ b/packages/frontend/src/widgets/WidgetActivity.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" data-cy-mkw-activity class="mkw-activity"> +<MkContainer :showHeader="widgetProps.showHeader" :naked="widgetProps.transparent" data-cy-mkw-activity class="mkw-activity"> <template #icon><i class="ti ti-chart-line"></i></template> <template #header>{{ i18n.ts._widgets.activity }}</template> <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="toggleView()"><i class="ti ti-selector"></i></button></template> @@ -16,7 +16,7 @@ <script lang="ts" setup> import { ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import XCalendar from './WidgetActivity.calendar.vue'; import XChart from './WidgetActivity.chart.vue'; import { GetFormResultType } from '@/scripts/form'; @@ -45,11 +45,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure, save } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/WidgetAichan.vue b/packages/frontend/src/widgets/WidgetAichan.vue index 37326ee98..797dd9c09 100644 --- a/packages/frontend/src/widgets/WidgetAichan.vue +++ b/packages/frontend/src/widgets/WidgetAichan.vue @@ -1,12 +1,12 @@ <template> -<MkContainer :naked="widgetProps.transparent" :show-header="false" data-cy-mkw-aichan class="mkw-aichan"> - <iframe ref="live2d" class="dedjhjmo" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100" @click="touched"></iframe> +<MkContainer :naked="widgetProps.transparent" :showHeader="false" data-cy-mkw-aichan class="mkw-aichan"> + <iframe ref="live2d" :class="$style.root" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100" @click="touched"></iframe> </MkContainer> </template> <script lang="ts" setup> import { onMounted, onUnmounted, shallowRef } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; const name = 'ai'; @@ -20,11 +20,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, @@ -64,8 +61,8 @@ defineExpose<WidgetComponentExpose>({ }); </script> -<style lang="scss" scoped> -.dedjhjmo { +<style lang="scss" module> +.root { width: 100%; height: 350px; border: none; diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue index 947dbe5e7..d6c94cd56 100644 --- a/packages/frontend/src/widgets/WidgetAiscript.vue +++ b/packages/frontend/src/widgets/WidgetAiscript.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" data-cy-mkw-aiscript class="mkw-aiscript"> +<MkContainer :showHeader="widgetProps.showHeader" data-cy-mkw-aiscript class="mkw-aiscript"> <template #icon><i class="ti ti-terminal-2"></i></template> <template #header>{{ i18n.ts._widgets.aiscript }}</template> @@ -16,7 +16,7 @@ <script lang="ts" setup> import { ref } from 'vue'; import { Interpreter, Parser, utils } from '@syuilo/aiscript'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import * as os from '@/os'; import MkContainer from '@/components/MkContainer.vue'; @@ -41,11 +41,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/WidgetAiscriptApp.vue b/packages/frontend/src/widgets/WidgetAiscriptApp.vue index 455a6e6ea..3b67972e4 100644 --- a/packages/frontend/src/widgets/WidgetAiscriptApp.vue +++ b/packages/frontend/src/widgets/WidgetAiscriptApp.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" class="mkw-aiscriptApp"> +<MkContainer :showHeader="widgetProps.showHeader" class="mkw-aiscriptApp"> <template #header>App</template> <div :class="$style.root"> <MkAsUi v-if="root" :component="root" :components="components" size="small"/> @@ -10,7 +10,7 @@ <script lang="ts" setup> import { onMounted, Ref, ref, watch } from 'vue'; import { Interpreter, Parser } from '@syuilo/aiscript'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import * as os from '@/os'; import { createAiScriptEnv } from '@/scripts/aiscript/api'; @@ -35,12 +35,9 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); - +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); + const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, props, diff --git a/packages/frontend/src/widgets/WidgetButton.vue b/packages/frontend/src/widgets/WidgetButton.vue index 9eee9680d..bcb380f84 100644 --- a/packages/frontend/src/widgets/WidgetButton.vue +++ b/packages/frontend/src/widgets/WidgetButton.vue @@ -8,7 +8,7 @@ <script lang="ts" setup> import { Interpreter, Parser } from '@syuilo/aiscript'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import * as os from '@/os'; import { createAiScriptEnv } from '@/scripts/aiscript/api'; @@ -35,11 +35,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, @@ -101,8 +98,3 @@ defineExpose<WidgetComponentExpose>({ id: props.widget ? props.widget.id : null, }); </script> - -<style lang="scss" scoped> -.mkw-button { -} -</style> diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue index 58d073226..447525837 100644 --- a/packages/frontend/src/widgets/WidgetCalendar.vue +++ b/packages/frontend/src/widgets/WidgetCalendar.vue @@ -34,7 +34,7 @@ <script lang="ts" setup> import { ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import { i18n } from '@/i18n'; import { useInterval } from '@/scripts/use-interval'; @@ -50,11 +50,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/WidgetClicker.vue b/packages/frontend/src/widgets/WidgetClicker.vue index 981788a3c..b7be2e8c8 100644 --- a/packages/frontend/src/widgets/WidgetClicker.vue +++ b/packages/frontend/src/widgets/WidgetClicker.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" class="mkw-clicker"> +<MkContainer :showHeader="widgetProps.showHeader" class="mkw-clicker"> <template #icon><i class="ti ti-cookie"></i></template> <template #header>Clicker</template> <MkClickerGame/> @@ -7,7 +7,7 @@ </template> <script lang="ts" setup> -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import MkContainer from '@/components/MkContainer.vue'; import MkClickerGame from '@/components/MkClickerGame.vue'; @@ -23,12 +23,9 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); - +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); + const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, props, diff --git a/packages/frontend/src/widgets/WidgetClock.vue b/packages/frontend/src/widgets/WidgetClock.vue index ebd73cb9f..aee5026db 100644 --- a/packages/frontend/src/widgets/WidgetClock.vue +++ b/packages/frontend/src/widgets/WidgetClock.vue @@ -1,25 +1,31 @@ <template> -<MkContainer :naked="widgetProps.transparent" :show-header="false" data-cy-mkw-clock class="mkw-clock"> - <div class="vubelbmv" :class="widgetProps.size"> - <div v-if="widgetProps.label === 'tz' || widgetProps.label === 'timeAndTz'" class="_monospace label a abbrev">{{ tzAbbrev }}</div> +<MkContainer :naked="widgetProps.transparent" :showHeader="false" data-cy-mkw-clock> + <div + :class="[$style.root, { + [$style.small]: widgetProps.size === 'small', + [$style.medium]: widgetProps.size === 'medium', + [$style.large]: widgetProps.size === 'large', + }]" + > + <div v-if="widgetProps.label === 'tz' || widgetProps.label === 'timeAndTz'" class="_monospace" :class="[$style.label, $style.a]">{{ tzAbbrev }}</div> <MkAnalogClock - class="clock" + :class="$style.clock" :thickness="widgetProps.thickness" :offset="tzOffset" :graduations="widgetProps.graduations" - :fade-graduations="widgetProps.fadeGraduations" + :fadeGraduations="widgetProps.fadeGraduations" :twentyfour="widgetProps.twentyFour" - :s-animation="widgetProps.sAnimation" + :sAnimation="widgetProps.sAnimation" /> - <MkDigitalClock v-if="widgetProps.label === 'time' || widgetProps.label === 'timeAndTz'" class="_monospace label c time" :show-s="false" :offset="tzOffset"/> - <div v-if="widgetProps.label === 'tz' || widgetProps.label === 'timeAndTz'" class="_monospace label d offset">{{ tzOffsetLabel }}</div> + <MkDigitalClock v-if="widgetProps.label === 'time' || widgetProps.label === 'timeAndTz'" :class="[$style.label, $style.c]" class="_monospace" :showS="false" :offset="tzOffset"/> + <div v-if="widgetProps.label === 'tz' || widgetProps.label === 'timeAndTz'" class="_monospace" :class="[$style.label, $style.d]">{{ tzOffsetLabel }}</div> </div> </MkContainer> </template> <script lang="ts" setup> import { } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import MkContainer from '@/components/MkContainer.vue'; import MkAnalogClock from '@/components/MkAnalogClock.vue'; @@ -114,11 +120,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, @@ -143,39 +146,10 @@ defineExpose<WidgetComponentExpose>({ }); </script> -<style lang="scss" scoped> -.vubelbmv { +<style lang="scss" module> +.root { position: relative; - > .label { - position: absolute; - opacity: 0.7; - - &.a { - top: 14px; - left: 14px; - } - - &.b { - top: 14px; - right: 14px; - } - - &.c { - bottom: 14px; - left: 14px; - } - - &.d { - bottom: 14px; - right: 14px; - } - } - - > .clock { - margin: auto; - } - &.small { padding: 12px; @@ -200,4 +174,33 @@ defineExpose<WidgetComponentExpose>({ } } } + +.label { + position: absolute; + opacity: 0.7; + + &.a { + top: 14px; + left: 14px; + } + + &.b { + top: 14px; + right: 14px; + } + + &.c { + bottom: 14px; + left: 14px; + } + + &.d { + bottom: 14px; + right: 14px; + } +} + +.clock { + margin: auto; +} </style> diff --git a/packages/frontend/src/widgets/WidgetDigitalClock.vue b/packages/frontend/src/widgets/WidgetDigitalClock.vue index cdd9c3a40..6148177d9 100644 --- a/packages/frontend/src/widgets/WidgetDigitalClock.vue +++ b/packages/frontend/src/widgets/WidgetDigitalClock.vue @@ -2,14 +2,14 @@ <div data-cy-mkw-digitalClock class="_monospace" :class="[$style.root, { _panel: !widgetProps.transparent }]" :style="{ fontSize: `${widgetProps.fontSize}em` }"> <div v-if="widgetProps.showLabel" :class="$style.label">{{ tzAbbrev }}</div> <div> - <MkDigitalClock :show-ms="widgetProps.showMs" :offset="tzOffset"/> + <MkDigitalClock :showMs="widgetProps.showMs" :offset="tzOffset"/> </div> <div v-if="widgetProps.showLabel" :class="$style.label">{{ tzOffsetLabel }}</div> </div> </template> <script lang="ts" setup> -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import { timezones } from '@/scripts/timezones'; import MkDigitalClock from '@/components/MkDigitalClock.vue'; @@ -49,11 +49,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue index 2033b074e..951c4aaa6 100644 --- a/packages/frontend/src/widgets/WidgetFederation.vue +++ b/packages/frontend/src/widgets/WidgetFederation.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable" data-cy-mkw-federation class="mkw-federation"> +<MkContainer :showHeader="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable" data-cy-mkw-federation class="mkw-federation"> <template #icon><i class="ti ti-whirl"></i></template> <template #header>{{ i18n.ts._widgets.federation }}</template> @@ -21,7 +21,7 @@ <script lang="ts" setup> import { ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import MkContainer from '@/components/MkContainer.vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; @@ -42,11 +42,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps> & { foldable?: boolean; scrollable?: boolean; }>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; foldable?: boolean; scrollable?: boolean; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/WidgetInstanceCloud.vue b/packages/frontend/src/widgets/WidgetInstanceCloud.vue index b15780765..f8b811e6b 100644 --- a/packages/frontend/src/widgets/WidgetInstanceCloud.vue +++ b/packages/frontend/src/widgets/WidgetInstanceCloud.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-instance-cloud"> +<MkContainer :naked="widgetProps.transparent" :showHeader="false" class="mkw-instance-cloud"> <div class=""> <MkTagCloud v-if="activeInstances"> <li v-for="instance in activeInstances" :key="instance.id"> @@ -14,7 +14,7 @@ <script lang="ts" setup> import { } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import MkContainer from '@/components/MkContainer.vue'; import MkTagCloud from '@/components/MkTagCloud.vue'; @@ -33,11 +33,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, @@ -75,7 +72,3 @@ defineExpose<WidgetComponentExpose>({ id: props.widget ? props.widget.id : null, }); </script> - -<style lang="scss" scoped> - -</style> diff --git a/packages/frontend/src/widgets/WidgetInstanceInfo.vue b/packages/frontend/src/widgets/WidgetInstanceInfo.vue index d702fd2cb..c77b98f8f 100644 --- a/packages/frontend/src/widgets/WidgetInstanceInfo.vue +++ b/packages/frontend/src/widgets/WidgetInstanceInfo.vue @@ -15,7 +15,7 @@ </template> <script lang="ts" setup> -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import { host } from '@/config'; import { instance } from '@/instance'; @@ -27,11 +27,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue index 84043cf13..3c8ffdb55 100644 --- a/packages/frontend/src/widgets/WidgetJobQueue.vue +++ b/packages/frontend/src/widgets/WidgetJobQueue.vue @@ -47,9 +47,9 @@ <script lang="ts" setup> import { onUnmounted, reactive } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import number from '@/filters/number'; import * as sound from '@/scripts/sound'; import { deepClone } from '@/scripts/clone'; @@ -69,11 +69,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, @@ -81,7 +78,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name, emit, ); -const connection = stream.useChannel('queueStats'); +const connection = useStream().useChannel('queueStats'); const current = reactive({ inbox: { activeSincePrevTick: 0, diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue index 959cf776a..78d27a31b 100644 --- a/packages/frontend/src/widgets/WidgetMemo.vue +++ b/packages/frontend/src/widgets/WidgetMemo.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" data-cy-mkw-memo class="mkw-memo"> +<MkContainer :showHeader="widgetProps.showHeader" data-cy-mkw-memo class="mkw-memo"> <template #icon><i class="ti ti-note"></i></template> <template #header>{{ i18n.ts._widgets.memo }}</template> @@ -12,7 +12,7 @@ <script lang="ts" setup> import { ref, watch } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import MkContainer from '@/components/MkContainer.vue'; import { defaultStore } from '@/store'; @@ -33,11 +33,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/WidgetNotifications.vue b/packages/frontend/src/widgets/WidgetNotifications.vue index 661f68b27..a24aa9b2e 100644 --- a/packages/frontend/src/widgets/WidgetNotifications.vue +++ b/packages/frontend/src/widgets/WidgetNotifications.vue @@ -1,18 +1,18 @@ <template> -<MkContainer :style="`height: ${widgetProps.height}px;`" :show-header="widgetProps.showHeader" :scrollable="true" data-cy-mkw-notifications class="mkw-notifications"> +<MkContainer :style="`height: ${widgetProps.height}px;`" :showHeader="widgetProps.showHeader" :scrollable="true" data-cy-mkw-notifications class="mkw-notifications"> <template #icon><i class="ti ti-bell"></i></template> <template #header>{{ i18n.ts.notifications }}</template> <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configureNotification()"><i class="ti ti-settings"></i></button></template> <div> - <XNotifications :include-types="widgetProps.includingTypes"/> + <XNotifications :includeTypes="widgetProps.includingTypes"/> </div> </MkContainer> </template> <script lang="ts" setup> import { defineAsyncComponent } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import MkContainer from '@/components/MkContainer.vue'; import XNotifications from '@/components/MkNotifications.vue'; @@ -39,12 +39,9 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); - +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); + const { widgetProps, configure, save } = useWidgetPropsManager(name, widgetPropsDef, props, diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue index 44e073545..c920c3ca5 100644 --- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue +++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue @@ -1,14 +1,16 @@ <template> -<div data-cy-mkw-onlineUsers class="mkw-onlineUsers" :class="{ _panel: !widgetProps.transparent, pad: !widgetProps.transparent }"> - <I18n v-if="onlineUsersCount" :src="i18n.ts.onlineUsersCount" text-tag="span" class="text"> - <template #n><b>{{ number(onlineUsersCount) }}</b></template> - </I18n> +<div data-cy-mkw-onlineUsers :class="[$style.root, { _panel: !widgetProps.transparent, [$style.pad]: !widgetProps.transparent }]"> + <span :class="$style.text"> + <I18n v-if="onlineUsersCount" :src="i18n.ts.onlineUsersCount" textTag="span"> + <template #n><b style="color: #41b781;">{{ number(onlineUsersCount) }}</b></template> + </I18n> + </span> </div> </template> <script lang="ts" setup> import { ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import * as os from '@/os'; import { useInterval } from '@/scripts/use-interval'; @@ -26,11 +28,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, @@ -58,22 +57,16 @@ defineExpose<WidgetComponentExpose>({ }); </script> -<style lang="scss" scoped> -.mkw-onlineUsers { +<style lang="scss" module> +.root { text-align: center; &.pad { padding: 16px 0; } +} - > .text { - ::v-deep(b) { - color: #41b781; - } - - ::v-deep(span) { - opacity: 0.7; - } - } +.text { + color: var(--fgTransparentWeak); } </style> diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue index 716bbb427..5c6a8cbf8 100644 --- a/packages/frontend/src/widgets/WidgetPhotos.vue +++ b/packages/frontend/src/widgets/WidgetPhotos.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" :class="$style.root" :data-transparent="widgetProps.transparent ? true : null" data-cy-mkw-photos class="mkw-photos"> +<MkContainer :showHeader="widgetProps.showHeader" :naked="widgetProps.transparent" :class="$style.root" :data-transparent="widgetProps.transparent ? true : null" data-cy-mkw-photos class="mkw-photos"> <template #icon><i class="ti ti-camera"></i></template> <template #header>{{ i18n.ts._widgets.photos }}</template> @@ -18,9 +18,9 @@ <script lang="ts" setup> import { onUnmounted, ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { getStaticImageUrl } from '@/scripts/media-proxy'; import * as os from '@/os'; import MkContainer from '@/components/MkContainer.vue'; @@ -42,11 +42,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, @@ -54,7 +51,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name, emit, ); -const connection = stream.useChannel('main'); +const connection = useStream().useChannel('main'); const images = ref([]); const fetching = ref(true); diff --git a/packages/frontend/src/widgets/WidgetPostForm.vue b/packages/frontend/src/widgets/WidgetPostForm.vue index 7a96b0021..bc63f0282 100644 --- a/packages/frontend/src/widgets/WidgetPostForm.vue +++ b/packages/frontend/src/widgets/WidgetPostForm.vue @@ -4,7 +4,7 @@ <script lang="ts" setup> import { } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import MkPostForm from '@/components/MkPostForm.vue'; @@ -15,11 +15,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/WidgetProfile.vue b/packages/frontend/src/widgets/WidgetProfile.vue index 819663a36..72e229ef8 100644 --- a/packages/frontend/src/widgets/WidgetProfile.vue +++ b/packages/frontend/src/widgets/WidgetProfile.vue @@ -17,7 +17,7 @@ </template> <script lang="ts" setup> -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import { $i } from '@/account'; import { userPage } from '@/filters/user'; @@ -29,11 +29,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue index 18fa2e2c2..1be882c66 100644 --- a/packages/frontend/src/widgets/WidgetRss.vue +++ b/packages/frontend/src/widgets/WidgetRss.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" data-cy-mkw-rss class="mkw-rss"> +<MkContainer :showHeader="widgetProps.showHeader" data-cy-mkw-rss class="mkw-rss"> <template #icon><i class="ti ti-rss"></i></template> <template #header>RSS</template> <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure"><i class="ti ti-settings"></i></button></template> @@ -19,7 +19,7 @@ <script lang="ts" setup> import { ref, watch, computed } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import MkContainer from '@/components/MkContainer.vue'; import { url as base } from '@/config'; @@ -49,11 +49,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue index b0408f0d7..6b346c059 100644 --- a/packages/frontend/src/widgets/WidgetRssTicker.vue +++ b/packages/frontend/src/widgets/WidgetRssTicker.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :naked="widgetProps.transparent" :show-header="widgetProps.showHeader" class="mkw-rss-ticker"> +<MkContainer :naked="widgetProps.transparent" :showHeader="widgetProps.showHeader" class="mkw-rss-ticker"> <template #icon><i class="ti ti-rss"></i></template> <template #header>RSS</template> <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure"><i class="ti ti-settings"></i></button></template> @@ -12,7 +12,7 @@ <Transition :name="$style.change" mode="default" appear> <MarqueeText :key="key" :duration="widgetProps.duration" :reverse="widgetProps.reverse"> <span v-for="item in items" :key="item.link" :class="$style.item"> - <a :class="$style.link" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span :class="$style.divider"></span> + <a :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span :class="$style.divider"></span> </span> </MarqueeText> </Transition> @@ -23,7 +23,7 @@ <script lang="ts" setup> import { ref, watch, computed } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import MarqueeText from '@/components/MkMarquee.vue'; import { GetFormResultType } from '@/scripts/form'; import MkContainer from '@/components/MkContainer.vue'; @@ -73,11 +73,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue index 915e7aaaf..d4ede5792 100644 --- a/packages/frontend/src/widgets/WidgetSlideshow.vue +++ b/packages/frontend/src/widgets/WidgetSlideshow.vue @@ -13,7 +13,7 @@ <script lang="ts" setup> import { onMounted, ref, shallowRef } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import * as os from '@/os'; import { useInterval } from '@/scripts/use-interval'; @@ -35,11 +35,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure, save } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue index 71ee75f6c..3d497c2e2 100644 --- a/packages/frontend/src/widgets/WidgetTimeline.vue +++ b/packages/frontend/src/widgets/WidgetTimeline.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" :style="`height: ${widgetProps.height}px;`" :scrollable="true" data-cy-mkw-timeline class="mkw-timeline"> +<MkContainer :showHeader="widgetProps.showHeader" :style="`height: ${widgetProps.height}px;`" :scrollable="true" data-cy-mkw-timeline class="mkw-timeline"> <template #icon> <i v-if="widgetProps.src === 'home'" class="ti ti-home"></i> <i v-else-if="widgetProps.src === 'local'" class="ti ti-planet"></i> @@ -30,7 +30,7 @@ <script lang="ts" setup> import { ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import * as os from '@/os'; import MkContainer from '@/components/MkContainer.vue'; @@ -71,11 +71,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure, save } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue index 01450a7ab..36f908d5e 100644 --- a/packages/frontend/src/widgets/WidgetTrends.vue +++ b/packages/frontend/src/widgets/WidgetTrends.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" data-cy-mkw-trends class="mkw-trends"> +<MkContainer :showHeader="widgetProps.showHeader" data-cy-mkw-trends class="mkw-trends"> <template #icon><i class="ti ti-hash"></i></template> <template #header>{{ i18n.ts._widgets.trends }}</template> @@ -20,7 +20,7 @@ <script lang="ts" setup> import { ref } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import MkContainer from '@/components/MkContainer.vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; @@ -40,11 +40,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/WidgetUnixClock.vue b/packages/frontend/src/widgets/WidgetUnixClock.vue index 22162d2b2..f1af71add 100644 --- a/packages/frontend/src/widgets/WidgetUnixClock.vue +++ b/packages/frontend/src/widgets/WidgetUnixClock.vue @@ -12,7 +12,7 @@ <script lang="ts" setup> import { onUnmounted, ref, watch } from 'vue'; -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; const name = 'unixClock'; @@ -39,11 +39,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/WidgetUserList.vue b/packages/frontend/src/widgets/WidgetUserList.vue index b8811d2fe..4380fdb62 100644 --- a/packages/frontend/src/widgets/WidgetUserList.vue +++ b/packages/frontend/src/widgets/WidgetUserList.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" class="mkw-userList"> +<MkContainer :showHeader="widgetProps.showHeader" class="mkw-userList"> <template #icon><i class="ti ti-users"></i></template> <template #header>{{ list ? list.name : i18n.ts._widgets.userList }}</template> <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure()"><i class="ti ti-settings"></i></button></template> @@ -19,7 +19,7 @@ </template> <script lang="ts" setup> -import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; import { GetFormResultType } from '@/scripts/form'; import MkContainer from '@/components/MkContainer.vue'; import * as os from '@/os'; @@ -43,11 +43,8 @@ const widgetPropsDef = { type WidgetProps = GetFormResultType<typeof widgetPropsDef>; -// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない -//const props = defineProps<WidgetComponentProps<WidgetProps>>(); -//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); -const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); -const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); +const props = defineProps<WidgetComponentProps<WidgetProps>>(); +const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); const { widgetProps, configure, save } = useWidgetPropsManager(name, widgetPropsDef, diff --git a/packages/frontend/src/widgets/server-metric/index.vue b/packages/frontend/src/widgets/server-metric/index.vue index 357d0ab78..e019ff540 100644 --- a/packages/frontend/src/widgets/server-metric/index.vue +++ b/packages/frontend/src/widgets/server-metric/index.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent"> +<MkContainer :showHeader="widgetProps.showHeader" :naked="widgetProps.transparent"> <template #icon><i class="ti ti-server"></i></template> <template #header>{{ i18n.ts._widgets.serverMetric }}</template> <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="toggleView()"><i class="ti ti-selector"></i></button></template> @@ -25,7 +25,7 @@ import XDisk from './disk.vue'; import MkContainer from '@/components/MkContainer.vue'; import { GetFormResultType } from '@/scripts/form'; import * as os from '@/os'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { i18n } from '@/i18n'; const name = 'serverMetric'; @@ -75,7 +75,7 @@ const toggleView = () => { save(); }; -const connection = stream.useChannel('serverStats'); +const connection = useStream().useChannel('serverStats'); onUnmounted(() => { connection.dispose(); }); diff --git a/packages/frontend/src/widgets/server-metric/pie.vue b/packages/frontend/src/widgets/server-metric/pie.vue index 868dbc048..398815a6a 100644 --- a/packages/frontend/src/widgets/server-metric/pie.vue +++ b/packages/frontend/src/widgets/server-metric/pie.vue @@ -1,11 +1,12 @@ <template> -<svg class="hsalcinq" viewBox="0 0 1 1" preserveAspectRatio="none"> +<svg :class="$style.root" viewBox="0 0 1 1" preserveAspectRatio="none"> <circle :r="r" cx="50%" cy="50%" fill="none" stroke-width="0.1" stroke="rgba(0, 0, 0, 0.05)" + :class="$style.circle" /> <circle :r="r" @@ -16,7 +17,7 @@ stroke-width="0.1" :stroke="color" /> - <text x="50%" y="50%" dy="0.05" text-anchor="middle">{{ (value * 100).toFixed(0) }}%</text> + <text x="50%" y="50%" dy="0.05" text-anchor="middle" :class="$style.text">{{ (value * 100).toFixed(0) }}%</text> </svg> </template> @@ -33,20 +34,20 @@ const color = $computed(() => `hsl(${180 - (props.value * 180)}, 80%, 70%)`); const strokeDashoffset = $computed(() => (1 - props.value) * (Math.PI * (r * 2))); </script> -<style lang="scss" scoped> -.hsalcinq { +<style lang="scss" module> +.root { display: block; height: 100%; +} - > circle { - transform-origin: center; - transform: rotate(-90deg); - transition: stroke-dashoffset 0.5s ease; - } +.circle { + transform-origin: center; + transform: rotate(-90deg); + transition: stroke-dashoffset 0.5s ease; +} - > text { - font-size: 0.15px; - fill: currentColor; - } +.text { + font-size: 0.15px; + fill: currentColor; } </style> diff --git a/packages/frontend/src/workers/draw-blurhash.ts b/packages/frontend/src/workers/draw-blurhash.ts new file mode 100644 index 000000000..5f2168a44 --- /dev/null +++ b/packages/frontend/src/workers/draw-blurhash.ts @@ -0,0 +1,15 @@ +import { render } from 'buraha'; + +onmessage = (event) => { + // console.log(event.data); + if (!('id' in event.data && typeof event.data.id === 'string')) { + return; + } + if (!('hash' in event.data && typeof event.data.hash === 'string')) { + return; + } + const work = new OffscreenCanvas(event.data.width ?? 64, event.data.height ?? 64); + render(event.data.hash, work); + const bitmap = work.transferToImageBitmap(); + postMessage({ id: event.data.id, bitmap }); +}; diff --git a/packages/frontend/src/workers/test-webgl2.ts b/packages/frontend/src/workers/test-webgl2.ts new file mode 100644 index 000000000..4769524d9 --- /dev/null +++ b/packages/frontend/src/workers/test-webgl2.ts @@ -0,0 +1,7 @@ +const canvas = new OffscreenCanvas(1, 1); +const gl = canvas.getContext('webgl2'); +if (gl) { + postMessage({ result: true }); +} else { + postMessage({ result: false }); +} diff --git a/packages/frontend/src/workers/tsconfig.json b/packages/frontend/src/workers/tsconfig.json new file mode 100644 index 000000000..8ee893046 --- /dev/null +++ b/packages/frontend/src/workers/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "lib": ["esnext", "webworker"], + } +} diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index fad0dd017..531dd0b48 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -6,7 +6,9 @@ import { type UserConfig, defineConfig } from 'vite'; import ReactivityTransform from '@vue-macros/reactivity-transform/vite'; import locales from '../../locales'; +import generateDTS from '../../locales/generateDTS'; import meta from '../../package.json'; +import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name'; import pluginJson5 from './vite.json5'; const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue']; @@ -53,6 +55,7 @@ export function getConfig(): UserConfig { reactivityTransform: true, }), ReactivityTransform(), + pluginUnwindCssModuleClassName(), pluginJson5(), ...process.env.NODE_ENV === 'production' ? [ @@ -64,6 +67,10 @@ export function getConfig(): UserConfig { }), ] : [], + { + name: 'locale:generateDTS', + buildStart: generateDTS, + }, ], resolve: { @@ -117,13 +124,15 @@ export function getConfig(): UserConfig { manifest: 'manifest.json', rollupOptions: { input: { - app: './src/init.ts', + app: './src/_boot_.ts', }, output: { manualChunks: { vue: ['vue'], photoswipe: ['photoswipe', 'photoswipe/lightbox', 'photoswipe/style.css'], }, + chunkFileNames: process.env.NODE_ENV === 'production' ? '[hash:8].js' : '[name]-[hash:8].js', + assetFileNames: process.env.NODE_ENV === 'production' ? '[hash:8][extname]' : '[name]-[hash:8][extname]', }, }, cssCodeSplit: true, @@ -139,6 +148,10 @@ export function getConfig(): UserConfig { }, }, + worker: { + format: 'es', + }, + test: { environment: 'happy-dom', deps: { diff --git a/packages/shared/.eslintrc.js b/packages/shared/.eslintrc.js index 7c979a93d..a53ad1789 100644 --- a/packages/shared/.eslintrc.js +++ b/packages/shared/.eslintrc.js @@ -49,7 +49,7 @@ module.exports = { 'no-multi-spaces': ['error'], 'no-var': ['error'], 'prefer-arrow-callback': ['error'], - 'no-throw-literal': ['warn'], + 'no-throw-literal': ['error'], 'no-param-reassign': ['warn'], 'no-constant-condition': ['warn'], 'no-empty-pattern': ['warn'], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74a7e8985..f48fac3f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,8 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false overrides: chokidar: 3.5.3 @@ -30,8 +34,8 @@ importers: specifier: 4.1.0 version: 4.1.0 typescript: - specifier: 5.0.4 - version: 5.0.4 + specifier: 5.1.3 + version: 5.1.3 optionalDependencies: '@tensorflow/tfjs-core': specifier: 4.4.0 @@ -44,20 +48,20 @@ importers: specifier: 2.0.1 version: 2.0.1 '@typescript-eslint/eslint-plugin': - specifier: 5.59.5 - version: 5.59.5(@typescript-eslint/parser@5.59.5)(eslint@8.40.0)(typescript@5.0.4) + specifier: 5.59.8 + version: 5.59.8(@typescript-eslint/parser@5.59.8)(eslint@8.41.0)(typescript@5.1.3) '@typescript-eslint/parser': - specifier: 5.59.5 - version: 5.59.5(eslint@8.40.0)(typescript@5.0.4) + specifier: 5.59.8 + version: 5.59.8(eslint@8.41.0)(typescript@5.1.3) cross-env: specifier: 7.0.3 version: 7.0.3 cypress: - specifier: 12.12.0 - version: 12.12.0 + specifier: 12.13.0 + version: 12.13.0 eslint: - specifier: 8.40.0 - version: 8.40.0 + specifier: 8.41.0 + version: 8.41.0 start-server-and-test: specifier: 2.0.0 version: 2.0.0 @@ -74,14 +78,14 @@ importers: specifier: 3.321.1 version: 3.321.1 '@bull-board/api': - specifier: 5.1.2 - version: 5.1.2(@bull-board/ui@5.1.2) + specifier: 5.2.0 + version: 5.2.0(@bull-board/ui@5.2.0) '@bull-board/fastify': - specifier: 5.1.2 - version: 5.1.2 + specifier: 5.2.0 + version: 5.2.0 '@bull-board/ui': - specifier: 5.1.2 - version: 5.1.2 + specifier: 5.2.0 + version: 5.2.0 '@discordapp/twemoji': specifier: 14.1.2 version: 14.1.2 @@ -92,41 +96,41 @@ importers: specifier: 8.3.0 version: 8.3.0 '@fastify/cors': - specifier: 8.2.1 - version: 8.2.1 + specifier: 8.3.0 + version: 8.3.0 '@fastify/http-proxy': specifier: 9.1.0 - version: 9.1.0 + version: 9.1.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) '@fastify/multipart': specifier: 7.6.0 version: 7.6.0 '@fastify/static': - specifier: 6.10.1 - version: 6.10.1 + specifier: 6.10.2 + version: 6.10.2 '@fastify/view': specifier: 7.4.1 version: 7.4.1 '@nestjs/common': - specifier: 9.4.0 - version: 9.4.0(reflect-metadata@0.1.13)(rxjs@7.8.1) + specifier: 9.4.2 + version: 9.4.2(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/core': - specifier: 9.4.0 - version: 9.4.0(@nestjs/common@9.4.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + specifier: 9.4.2 + version: 9.4.2(@nestjs/common@9.4.2)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/testing': - specifier: 9.4.0 - version: 9.4.0(@nestjs/common@9.4.0)(@nestjs/core@9.4.0) + specifier: 9.4.2 + version: 9.4.2(@nestjs/common@9.4.2)(@nestjs/core@9.4.2) '@peertube/http-signature': specifier: 1.7.0 version: 1.7.0 '@sinonjs/fake-timers': - specifier: 10.0.2 - version: 10.0.2 + specifier: 10.2.0 + version: 10.2.0 '@swc/cli': specifier: 0.1.62 - version: 0.1.62(@swc/core@1.3.56)(chokidar@3.5.3) + version: 0.1.62(@swc/core@1.3.61)(chokidar@3.5.3) '@swc/core': - specifier: 1.3.56 - version: 1.3.56 + specifier: 1.3.61 + version: 1.3.61 accepts: specifier: 1.3.8 version: 1.3.8 @@ -145,15 +149,15 @@ importers: blurhash: specifier: 2.0.5 version: 2.0.5 - bull: - specifier: 4.10.4 - version: 4.10.4 + bullmq: + specifier: 3.15.0 + version: 3.15.0 cacheable-lookup: specifier: 6.1.0 version: 6.1.0 cbor: - specifier: 8.1.0 - version: 8.1.0 + specifier: 9.0.0 + version: 9.0.0 chalk: specifier: 5.2.0 version: 5.2.0 @@ -200,8 +204,8 @@ importers: specifier: 12.6.0 version: 12.6.0 happy-dom: - specifier: 9.16.0 - version: 9.16.0 + specifier: 9.20.3 + version: 9.20.3 hpagent: specifier: 1.2.0 version: 1.2.0 @@ -218,20 +222,20 @@ importers: specifier: 4.1.0 version: 4.1.0 jsdom: - specifier: 21.1.1 - version: 21.1.1 + specifier: 22.1.0 + version: 22.1.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) json5: specifier: 2.2.3 version: 2.2.3 jsonld: - specifier: 8.1.1 - version: 8.1.1 + specifier: 8.2.0 + version: 8.2.0 jsrsasign: specifier: 10.8.6 version: 10.8.6 meilisearch: - specifier: 0.32.3 - version: 0.32.3 + specifier: 0.32.5 + version: 0.32.5 mfm-js: specifier: 0.23.3 version: 0.23.3 @@ -251,8 +255,8 @@ importers: specifier: 3.3.1 version: 3.3.1 nodemailer: - specifier: 6.9.2 - version: 6.9.2 + specifier: 6.9.3 + version: 6.9.3 nsfwjs: specifier: 2.4.2 version: 2.4.2(@tensorflow/tfjs@4.4.0) @@ -269,8 +273,8 @@ importers: specifier: 7.1.2 version: 7.1.2 pg: - specifier: 8.10.0 - version: 8.10.0 + specifier: 8.11.0 + version: 8.11.0 private-ip: specifier: 3.0.0 version: 3.0.0 @@ -299,8 +303,8 @@ importers: specifier: 3.4.1 version: 3.4.1 re2: - specifier: 1.18.0 - version: 1.18.0 + specifier: 1.19.0 + version: 1.19.0 redis-lock: specifier: 0.1.4 version: 0.1.4 @@ -329,8 +333,8 @@ importers: specifier: 3.0.5 version: 3.0.5 semver: - specifier: 7.5.0 - version: 7.5.0 + specifier: 7.5.1 + version: 7.5.1 sharp: specifier: 0.32.1 version: 0.32.1 @@ -338,8 +342,8 @@ importers: specifier: github:misskey-dev/sharp-read-bmp version: github.com/misskey-dev/sharp-read-bmp/02d9dc189fa7df0c4bea09330be26741772dac01 slacc: - specifier: 0.0.7 - version: 0.0.7 + specifier: 0.0.9 + version: 0.0.9 strict-event-emitter-types: specifier: 2.0.0 version: 2.0.0 @@ -350,8 +354,8 @@ importers: specifier: github:misskey-dev/summaly version: github.com/misskey-dev/summaly/77dd5654bb82280b38c1f50e51a771c33f3df503 systeminformation: - specifier: 5.17.12 - version: 5.17.12 + specifier: 5.17.16 + version: 5.17.16 tinycolor2: specifier: 1.6.0 version: 1.6.0 @@ -369,16 +373,16 @@ importers: version: 14.0.0 typeorm: specifier: 0.3.16 - version: 0.3.16(ioredis@5.3.2)(pg@8.10.0) + version: 0.3.16(ioredis@5.3.2)(pg@8.11.0) typescript: - specifier: 5.0.4 - version: 5.0.4 + specifier: 5.1.3 + version: 5.1.3 ulid: specifier: 2.3.0 version: 2.3.0 unzipper: - specifier: 0.10.11 - version: 0.10.11 + specifier: 0.10.14 + version: 0.10.14 uuid: specifier: 9.0.0 version: 9.0.0 @@ -388,12 +392,9 @@ importers: web-push: specifier: 3.6.1 version: 3.6.1 - websocket: - specifier: 1.0.34 - version: 1.0.34 ws: specifier: 8.13.0 - version: 8.13.0 + version: 8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) xev: specifier: 3.0.2 version: 3.0.2 @@ -437,46 +438,55 @@ importers: '@tensorflow/tfjs-node': specifier: 4.4.0 version: 4.4.0(seedrandom@3.0.5) + bufferutil: + specifier: ^4.0.7 + version: 4.0.7 slacc-android-arm-eabi: - specifier: 0.0.7 - version: 0.0.7 + specifier: 0.0.9 + version: 0.0.9 slacc-android-arm64: - specifier: 0.0.7 - version: 0.0.7 + specifier: 0.0.9 + version: 0.0.9 slacc-darwin-arm64: - specifier: 0.0.7 - version: 0.0.7 + specifier: 0.0.9 + version: 0.0.9 slacc-darwin-universal: - specifier: 0.0.7 - version: 0.0.7 + specifier: 0.0.9 + version: 0.0.9 slacc-darwin-x64: - specifier: 0.0.7 - version: 0.0.7 + specifier: 0.0.9 + version: 0.0.9 + slacc-freebsd-x64: + specifier: 0.0.9 + version: 0.0.9 slacc-linux-arm-gnueabihf: - specifier: 0.0.7 - version: 0.0.7 + specifier: 0.0.9 + version: 0.0.9 slacc-linux-arm64-gnu: - specifier: 0.0.7 - version: 0.0.7 + specifier: 0.0.9 + version: 0.0.9 slacc-linux-arm64-musl: - specifier: 0.0.7 - version: 0.0.7 + specifier: 0.0.9 + version: 0.0.9 slacc-linux-x64-gnu: - specifier: 0.0.7 - version: 0.0.7 + specifier: 0.0.9 + version: 0.0.9 slacc-win32-arm64-msvc: - specifier: 0.0.7 - version: 0.0.7 + specifier: 0.0.9 + version: 0.0.9 slacc-win32-x64-msvc: - specifier: 0.0.7 - version: 0.0.7 + specifier: 0.0.9 + version: 0.0.9 + utf-8-validate: + specifier: ^6.0.3 + version: 6.0.3 devDependencies: '@jest/globals': specifier: 29.5.0 version: 29.5.0 '@swc/jest': specifier: 0.2.26 - version: 0.2.26(@swc/core@1.3.56) + version: 0.2.26(@swc/core@1.3.61) '@types/accepts': specifier: 1.3.5 version: 1.3.5 @@ -486,9 +496,6 @@ importers: '@types/bcryptjs': specifier: 2.4.2 version: 2.4.2 - '@types/bull': - specifier: 4.10.0 - version: 4.10.0 '@types/cbor': specifier: 6.0.0 version: 6.0.0 @@ -505,8 +512,8 @@ importers: specifier: 2.1.21 version: 2.1.21 '@types/jest': - specifier: 29.5.1 - version: 29.5.1 + specifier: 29.5.2 + version: 29.5.2 '@types/js-yaml': specifier: 4.0.5 version: 4.0.5 @@ -523,20 +530,20 @@ importers: specifier: 2.1.1 version: 2.1.1 '@types/node': - specifier: 20.1.3 - version: 20.1.3 + specifier: 20.2.5 + version: 20.2.5 '@types/node-fetch': specifier: 3.0.3 version: 3.0.3 '@types/nodemailer': - specifier: 6.4.7 - version: 6.4.7 + specifier: 6.4.8 + version: 6.4.8 '@types/oauth': specifier: 0.9.1 version: 0.9.1 '@types/pg': - specifier: 8.6.6 - version: 8.6.6 + specifier: 8.10.1 + version: 8.10.1 '@types/pug': specifier: 2.0.6 version: 2.0.6 @@ -577,8 +584,8 @@ importers: specifier: 0.2.3 version: 0.2.3 '@types/unzipper': - specifier: 0.10.5 - version: 0.10.5 + specifier: 0.10.6 + version: 0.10.6 '@types/uuid': specifier: 9.0.1 version: 9.0.1 @@ -595,11 +602,11 @@ importers: specifier: 8.5.4 version: 8.5.4 '@typescript-eslint/eslint-plugin': - specifier: 5.59.5 - version: 5.59.5(@typescript-eslint/parser@5.59.5)(eslint@8.40.0)(typescript@5.0.4) + specifier: 5.59.8 + version: 5.59.8(@typescript-eslint/parser@5.59.8)(eslint@8.41.0)(typescript@5.1.3) '@typescript-eslint/parser': - specifier: 5.59.5 - version: 5.59.5(eslint@8.40.0)(typescript@5.0.4) + specifier: 5.59.8 + version: 5.59.8(eslint@8.41.0)(typescript@5.1.3) aws-sdk-client-mock: specifier: 2.1.1 version: 2.1.1 @@ -607,17 +614,17 @@ importers: specifier: 7.0.3 version: 7.0.3 eslint: - specifier: 8.40.0 - version: 8.40.0 + specifier: 8.41.0 + version: 8.41.0 eslint-plugin-import: specifier: 2.27.5 - version: 2.27.5(@typescript-eslint/parser@5.59.5)(eslint@8.40.0) + version: 2.27.5(@typescript-eslint/parser@5.59.8)(eslint@8.41.0) execa: specifier: 6.1.0 version: 6.1.0 jest: specifier: 29.5.0 - version: 29.5.0(@types/node@20.1.3) + version: 29.5.0(@types/node@20.2.5) jest-mock: specifier: 29.5.0 version: 29.5.0 @@ -629,43 +636,46 @@ importers: version: 14.1.2 '@rollup/plugin-alias': specifier: 5.0.0 - version: 5.0.0(rollup@3.21.6) + version: 5.0.0(rollup@3.23.0) '@rollup/plugin-json': specifier: 6.0.0 - version: 6.0.0(rollup@3.21.6) + version: 6.0.0(rollup@3.23.0) '@rollup/plugin-replace': specifier: 5.0.2 - version: 5.0.2(rollup@3.21.6) + version: 5.0.2(rollup@3.23.0) '@rollup/pluginutils': specifier: 5.0.2 - version: 5.0.2(rollup@3.21.6) + version: 5.0.2(rollup@3.23.0) '@syuilo/aiscript': - specifier: 0.13.2 - version: 0.13.2 + specifier: 0.13.3 + version: 0.13.3 '@tabler/icons-webfont': - specifier: 2.17.0 - version: 2.17.0 + specifier: 2.21.0 + version: 2.21.0 '@vitejs/plugin-vue': - specifier: 4.2.2 - version: 4.2.2(vite@4.3.5)(vue@3.3.1) + specifier: 4.2.3 + version: 4.2.3(vite@4.3.9)(vue@3.3.4) '@vue-macros/reactivity-transform': - specifier: 0.3.6 - version: 0.3.6(rollup@3.21.6)(vue@3.3.1) + specifier: 0.3.9 + version: 0.3.9(rollup@3.23.0)(vue@3.3.4) '@vue/compiler-sfc': - specifier: 3.3.1 - version: 3.3.1 + specifier: 3.3.4 + version: 3.3.4 + astring: + specifier: 1.8.6 + version: 1.8.6 autosize: - specifier: 5.0.2 - version: 5.0.2 - blurhash: - specifier: 2.0.5 - version: 2.0.5 + specifier: 6.0.1 + version: 6.0.1 broadcast-channel: - specifier: 4.20.2 - version: 4.20.2 + specifier: 5.1.0 + version: 5.1.0 browser-image-resizer: specifier: github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3 version: github.com/misskey-dev/browser-image-resizer/0227e860621e55cbed0aabe6dc601096a7748c4a + buraha: + specifier: github:misskey-dev/buraha + version: github.com/misskey-dev/buraha/92b20c1ab15c5cb5a224cf3b1ecd4f6baca12b7c canvas-confetti: specifier: 1.6.0 version: 1.6.0 @@ -685,8 +695,8 @@ importers: specifier: 2.0.1 version: 2.0.1(chart.js@4.3.0) chromatic: - specifier: 6.17.4 - version: 6.17.4 + specifier: 6.18.0 + version: 6.18.0 compare-versions: specifier: 5.0.3 version: 5.0.3 @@ -699,6 +709,9 @@ importers: escape-regexp: specifier: 0.0.1 version: 0.0.1 + estree-walker: + specifier: ^3.0.3 + version: 3.0.3 eventemitter3: specifier: 5.0.1 version: 5.0.1 @@ -742,8 +755,8 @@ importers: specifier: 1.0.0 version: 1.0.0 rollup: - specifier: 3.21.6 - version: 3.21.6 + specifier: 3.23.0 + version: 3.23.0 s-age: specifier: 1.1.2 version: 1.1.2 @@ -766,8 +779,8 @@ importers: specifier: 3.1.0 version: 3.1.0 three: - specifier: 0.151.3 - version: 0.151.3 + specifier: 0.153.0 + version: 0.153.0 throttle-debounce: specifier: 5.0.0 version: 5.0.0 @@ -784,8 +797,8 @@ importers: specifier: 14.0.0 version: 14.0.0 typescript: - specifier: 5.0.4 - version: 5.0.4 + specifier: 5.1.3 + version: 5.1.3 uuid: specifier: 9.0.0 version: 9.0.0 @@ -793,81 +806,78 @@ importers: specifier: 1.8.0 version: 1.8.0 vite: - specifier: 4.3.5 - version: 4.3.5(@types/node@20.1.3)(sass@1.62.1) + specifier: 4.3.9 + version: 4.3.9(@types/node@20.2.5)(sass@1.62.1) vue: - specifier: 3.3.1 - version: 3.3.1 - vue-plyr: - specifier: 7.0.0 - version: 7.0.0 + specifier: 3.3.4 + version: 3.3.4 vue-prism-editor: specifier: 2.0.0-alpha.2 - version: 2.0.0-alpha.2(vue@3.3.1) + version: 2.0.0-alpha.2(vue@3.3.4) vuedraggable: specifier: next - version: 4.1.0(vue@3.3.1) + version: 4.1.0(vue@3.3.4) devDependencies: '@storybook/addon-actions': - specifier: 7.0.10 - version: 7.0.10(react-dom@18.2.0)(react@18.2.0) + specifier: 7.0.18 + version: 7.0.18(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-essentials': - specifier: 7.0.10 - version: 7.0.10(react-dom@18.2.0)(react@18.2.0) + specifier: 7.0.18 + version: 7.0.18(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-interactions': - specifier: 7.0.10 - version: 7.0.10(react-dom@18.2.0)(react@18.2.0) + specifier: 7.0.18 + version: 7.0.18(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-links': - specifier: 7.0.10 - version: 7.0.10(react-dom@18.2.0)(react@18.2.0) + specifier: 7.0.18 + version: 7.0.18(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-storysource': - specifier: 7.0.10 - version: 7.0.10(react-dom@18.2.0)(react@18.2.0) + specifier: 7.0.18 + version: 7.0.18(react-dom@18.2.0)(react@18.2.0) '@storybook/addons': - specifier: 7.0.10 - version: 7.0.10(react-dom@18.2.0)(react@18.2.0) + specifier: 7.0.18 + version: 7.0.18(react-dom@18.2.0)(react@18.2.0) '@storybook/blocks': - specifier: 7.0.10 - version: 7.0.10(react-dom@18.2.0)(react@18.2.0) + specifier: 7.0.18 + version: 7.0.18(react-dom@18.2.0)(react@18.2.0) '@storybook/core-events': - specifier: 7.0.10 - version: 7.0.10 + specifier: 7.0.18 + version: 7.0.18 '@storybook/jest': specifier: 0.1.0 version: 0.1.0 '@storybook/manager-api': - specifier: 7.0.10 - version: 7.0.10(react-dom@18.2.0)(react@18.2.0) + specifier: 7.0.18 + version: 7.0.18(react-dom@18.2.0)(react@18.2.0) '@storybook/preview-api': - specifier: 7.0.10 - version: 7.0.10 + specifier: 7.0.18 + version: 7.0.18 '@storybook/react': - specifier: 7.0.10 - version: 7.0.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4) + specifier: 7.0.18 + version: 7.0.18(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3) '@storybook/react-vite': - specifier: 7.0.10 - version: 7.0.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4)(vite@4.3.5) + specifier: 7.0.18 + version: 7.0.18(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(vite@4.3.9) '@storybook/testing-library': specifier: 0.1.0 version: 0.1.0 '@storybook/theming': - specifier: 7.0.10 - version: 7.0.10(react-dom@18.2.0)(react@18.2.0) + specifier: 7.0.18 + version: 7.0.18(react-dom@18.2.0)(react@18.2.0) '@storybook/types': - specifier: 7.0.10 - version: 7.0.10 + specifier: 7.0.18 + version: 7.0.18 '@storybook/vue3': - specifier: 7.0.10 - version: 7.0.10(vue@3.3.1) + specifier: 7.0.18 + version: 7.0.18(vue@3.3.4) '@storybook/vue3-vite': - specifier: 7.0.10 - version: 7.0.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4)(vite@4.3.5)(vue@3.3.1) + specifier: 7.0.18 + version: 7.0.18(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(vite@4.3.9)(vue@3.3.4) '@testing-library/jest-dom': specifier: 5.16.5 version: 5.16.5 '@testing-library/vue': specifier: 7.0.0 - version: 7.0.0(@vue/compiler-sfc@3.3.1)(vue@3.3.1) + version: 7.0.0(@vue/compiler-sfc@3.3.4)(vue@3.3.4) '@types/escape-regexp': specifier: 0.0.1 version: 0.0.1 @@ -881,14 +891,14 @@ importers: specifier: 2.0.2 version: 2.0.2 '@types/matter-js': - specifier: 0.18.3 - version: 0.18.3 + specifier: 0.18.5 + version: 0.18.5 '@types/micromatch': specifier: 4.0.2 version: 4.0.2 '@types/node': - specifier: 20.1.3 - version: 20.1.3 + specifier: 20.2.5 + version: 20.2.5 '@types/punycode': specifier: 2.1.0 version: 2.1.0 @@ -899,8 +909,8 @@ importers: specifier: 3.0.5 version: 3.0.5 '@types/testing-library__jest-dom': - specifier: ^5.14.5 - version: 5.14.5 + specifier: ^5.14.6 + version: 5.14.6 '@types/throttle-debounce': specifier: 5.0.0 version: 5.0.0 @@ -917,20 +927,20 @@ importers: specifier: 8.5.4 version: 8.5.4 '@typescript-eslint/eslint-plugin': - specifier: 5.59.5 - version: 5.59.5(@typescript-eslint/parser@5.59.5)(eslint@8.40.0)(typescript@5.0.4) + specifier: 5.59.8 + version: 5.59.8(@typescript-eslint/parser@5.59.8)(eslint@8.41.0)(typescript@5.1.3) '@typescript-eslint/parser': - specifier: 5.59.5 - version: 5.59.5(eslint@8.40.0)(typescript@5.0.4) + specifier: 5.59.8 + version: 5.59.8(eslint@8.41.0)(typescript@5.1.3) '@vitest/coverage-c8': - specifier: 0.31.0 - version: 0.31.0(vitest@0.31.0) + specifier: 0.31.4 + version: 0.31.4(vitest@0.31.4) '@vue/runtime-core': - specifier: 3.3.1 - version: 3.3.1 - astring: - specifier: 1.8.4 - version: 1.8.4 + specifier: 3.3.4 + version: 3.3.4 + acorn: + specifier: ^8.8.2 + version: 8.8.2 chokidar-cli: specifier: 3.0.0 version: 3.0.0 @@ -938,29 +948,29 @@ importers: specifier: 7.0.3 version: 7.0.3 cypress: - specifier: 12.12.0 - version: 12.12.0 + specifier: 12.13.0 + version: 12.13.0 eslint: - specifier: 8.40.0 - version: 8.40.0 + specifier: 8.41.0 + version: 8.41.0 eslint-plugin-import: specifier: 2.27.5 - version: 2.27.5(@typescript-eslint/parser@5.59.5)(eslint@8.40.0) + version: 2.27.5(@typescript-eslint/parser@5.59.8)(eslint@8.41.0) eslint-plugin-vue: - specifier: 9.12.0 - version: 9.12.0(eslint@8.40.0) + specifier: 9.14.1 + version: 9.14.1(eslint@8.41.0) fast-glob: specifier: 3.2.12 version: 3.2.12 happy-dom: - specifier: 9.16.0 - version: 9.16.0 + specifier: 9.20.3 + version: 9.20.3 micromatch: specifier: 3.1.10 version: 3.1.10 msw: specifier: 1.2.1 - version: 1.2.1(typescript@5.0.4) + version: 1.2.1(typescript@5.1.3) msw-storybook-addon: specifier: 1.8.0 version: 1.8.0(msw@1.2.1) @@ -977,11 +987,11 @@ importers: specifier: 2.0.0 version: 2.0.0 storybook: - specifier: 7.0.10 - version: 7.0.10 + specifier: 7.0.18 + version: 7.0.18 storybook-addon-misskey-theme: specifier: github:misskey-dev/storybook-addon-misskey-theme - version: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.0.10)(@storybook/components@7.0.10)(@storybook/core-events@7.0.10)(@storybook/manager-api@7.0.10)(@storybook/preview-api@7.0.10)(@storybook/theming@7.0.10)(@storybook/types@7.0.10)(react-dom@18.2.0)(react@18.2.0) + version: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.0.18)(@storybook/components@7.0.18)(@storybook/core-events@7.0.18)(@storybook/manager-api@7.0.18)(@storybook/preview-api@7.0.18)(@storybook/theming@7.0.18)(@storybook/types@7.0.18)(react-dom@18.2.0)(react@18.2.0) summaly: specifier: github:misskey-dev/summaly version: github.com/misskey-dev/summaly/77dd5654bb82280b38c1f50e51a771c33f3df503 @@ -989,23 +999,23 @@ importers: specifier: 1.0.2 version: 1.0.2 vitest: - specifier: 0.31.0 - version: 0.31.0(happy-dom@9.16.0)(sass@1.62.1) + specifier: 0.31.4 + version: 0.31.4(happy-dom@9.20.3)(sass@1.62.1) vitest-fetch-mock: specifier: 0.2.2 - version: 0.2.2(vitest@0.31.0) + version: 0.2.2(vitest@0.31.4) vue-eslint-parser: - specifier: 9.2.1 - version: 9.2.1(eslint@8.40.0) + specifier: 9.3.0 + version: 9.3.0(eslint@8.41.0) vue-tsc: - specifier: 1.6.4 - version: 1.6.4(typescript@5.0.4) + specifier: 1.6.5 + version: 1.6.5(typescript@5.1.3) packages/misskey-js: dependencies: '@swc/cli': specifier: 0.1.62 - version: 0.1.62(@swc/core@1.3.56)(chokidar@3.5.3) + version: 0.1.62(@swc/core@1.3.61)(chokidar@3.5.3) '@swc/core': specifier: 1.3.56 version: 1.3.56 @@ -1090,11 +1100,11 @@ packages: resolution: {integrity: sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==} dev: true - /@ampproject/remapping@2.2.0: - resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} dependencies: - '@jridgewell/gen-mapping': 0.1.1 + '@jridgewell/gen-mapping': 0.3.2 '@jridgewell/trace-mapping': 0.3.17 dev: true @@ -1185,13 +1195,13 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/chunked-blob-reader@3.310.0: resolution: {integrity: sha512-CrJS3exo4mWaLnWxfCH+w88Ou0IcAZSIkk4QbmxiHl/5Dq705OLoxf4385MVyExpqpeVJYOYQ2WaD8i/pQZ2fg==} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/client-s3@3.321.1: @@ -1292,7 +1302,7 @@ packages: '@aws-sdk/util-user-agent-browser': 3.310.0 '@aws-sdk/util-user-agent-node': 3.310.0 '@aws-sdk/util-utf8': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 transitivePeerDependencies: - aws-crt dev: false @@ -1332,7 +1342,7 @@ packages: '@aws-sdk/util-user-agent-browser': 3.310.0 '@aws-sdk/util-user-agent-node': 3.310.0 '@aws-sdk/util-utf8': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 transitivePeerDependencies: - aws-crt dev: false @@ -1376,7 +1386,7 @@ packages: '@aws-sdk/util-user-agent-node': 3.310.0 '@aws-sdk/util-utf8': 3.310.0 fast-xml-parser: 4.1.2 - tslib: 2.5.0 + tslib: 2.5.2 transitivePeerDependencies: - aws-crt dev: false @@ -1388,7 +1398,7 @@ packages: '@aws-sdk/types': 3.310.0 '@aws-sdk/util-config-provider': 3.310.0 '@aws-sdk/util-middleware': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/credential-provider-env@3.310.0: @@ -1397,7 +1407,7 @@ packages: dependencies: '@aws-sdk/property-provider': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/credential-provider-imds@3.310.0: @@ -1408,7 +1418,7 @@ packages: '@aws-sdk/property-provider': 3.310.0 '@aws-sdk/types': 3.310.0 '@aws-sdk/url-parser': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/credential-provider-ini@3.321.1: @@ -1423,7 +1433,7 @@ packages: '@aws-sdk/property-provider': 3.310.0 '@aws-sdk/shared-ini-file-loader': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 transitivePeerDependencies: - aws-crt dev: false @@ -1441,7 +1451,7 @@ packages: '@aws-sdk/property-provider': 3.310.0 '@aws-sdk/shared-ini-file-loader': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 transitivePeerDependencies: - aws-crt dev: false @@ -1453,7 +1463,7 @@ packages: '@aws-sdk/property-provider': 3.310.0 '@aws-sdk/shared-ini-file-loader': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/credential-provider-sso@3.321.1: @@ -1465,7 +1475,7 @@ packages: '@aws-sdk/shared-ini-file-loader': 3.310.0 '@aws-sdk/token-providers': 3.321.1 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 transitivePeerDependencies: - aws-crt dev: false @@ -1476,7 +1486,7 @@ packages: dependencies: '@aws-sdk/property-provider': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/eventstream-codec@3.310.0: @@ -1485,7 +1495,7 @@ packages: '@aws-crypto/crc32': 3.0.0 '@aws-sdk/types': 3.310.0 '@aws-sdk/util-hex-encoding': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/eventstream-serde-browser@3.310.0: @@ -1494,7 +1504,7 @@ packages: dependencies: '@aws-sdk/eventstream-serde-universal': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/eventstream-serde-config-resolver@3.310.0: @@ -1502,7 +1512,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/eventstream-serde-node@3.310.0: @@ -1511,7 +1521,7 @@ packages: dependencies: '@aws-sdk/eventstream-serde-universal': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/eventstream-serde-universal@3.310.0: @@ -1520,7 +1530,7 @@ packages: dependencies: '@aws-sdk/eventstream-codec': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/fetch-http-handler@3.310.0: @@ -1530,7 +1540,7 @@ packages: '@aws-sdk/querystring-builder': 3.310.0 '@aws-sdk/types': 3.310.0 '@aws-sdk/util-base64': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/hash-blob-browser@3.310.0: @@ -1538,7 +1548,7 @@ packages: dependencies: '@aws-sdk/chunked-blob-reader': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/hash-node@3.310.0: @@ -1548,7 +1558,7 @@ packages: '@aws-sdk/types': 3.310.0 '@aws-sdk/util-buffer-from': 3.310.0 '@aws-sdk/util-utf8': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/hash-stream-node@3.310.0: @@ -1557,21 +1567,21 @@ packages: dependencies: '@aws-sdk/types': 3.310.0 '@aws-sdk/util-utf8': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/invalid-dependency@3.310.0: resolution: {integrity: sha512-1s5RG5rSPXoa/aZ/Kqr5U/7lqpx+Ry81GprQ2bxWqJvWQIJ0IRUwo5pk8XFxbKVr/2a+4lZT/c3OGoBOM1yRRA==} dependencies: '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/is-array-buffer@3.310.0: resolution: {integrity: sha512-urnbcCR+h9NWUnmOtet/s4ghvzsidFmspfhYaHAmSRdy9yDjdjBJMFjjsn85A1ODUktztm+cVncXjQ38WCMjMQ==} engines: {node: '>=14.0.0'} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/lib-storage@3.321.1(@aws-sdk/abort-controller@3.310.0)(@aws-sdk/client-s3@3.321.1): @@ -1596,7 +1606,7 @@ packages: dependencies: '@aws-sdk/types': 3.310.0 '@aws-sdk/util-utf8': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/middleware-bucket-endpoint@3.310.0: @@ -1607,7 +1617,7 @@ packages: '@aws-sdk/types': 3.310.0 '@aws-sdk/util-arn-parser': 3.310.0 '@aws-sdk/util-config-provider': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/middleware-content-length@3.310.0: @@ -1616,7 +1626,7 @@ packages: dependencies: '@aws-sdk/protocol-http': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/middleware-endpoint@3.310.0: @@ -1627,7 +1637,7 @@ packages: '@aws-sdk/types': 3.310.0 '@aws-sdk/url-parser': 3.310.0 '@aws-sdk/util-middleware': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/middleware-expect-continue@3.310.0: @@ -1636,7 +1646,7 @@ packages: dependencies: '@aws-sdk/protocol-http': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/middleware-flexible-checksums@3.310.0: @@ -1649,7 +1659,7 @@ packages: '@aws-sdk/protocol-http': 3.310.0 '@aws-sdk/types': 3.310.0 '@aws-sdk/util-utf8': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/middleware-host-header@3.310.0: @@ -1658,7 +1668,7 @@ packages: dependencies: '@aws-sdk/protocol-http': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/middleware-location-constraint@3.310.0: @@ -1666,7 +1676,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/middleware-logger@3.310.0: @@ -1674,7 +1684,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/middleware-recursion-detection@3.310.0: @@ -1683,7 +1693,7 @@ packages: dependencies: '@aws-sdk/protocol-http': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/middleware-retry@3.310.0: @@ -1695,7 +1705,7 @@ packages: '@aws-sdk/types': 3.310.0 '@aws-sdk/util-middleware': 3.310.0 '@aws-sdk/util-retry': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 uuid: 8.3.2 dev: false @@ -1706,7 +1716,7 @@ packages: '@aws-sdk/protocol-http': 3.310.0 '@aws-sdk/types': 3.310.0 '@aws-sdk/util-arn-parser': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/middleware-sdk-sts@3.310.0: @@ -1715,7 +1725,7 @@ packages: dependencies: '@aws-sdk/middleware-signing': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/middleware-serde@3.310.0: @@ -1723,7 +1733,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/middleware-signing@3.310.0: @@ -1735,7 +1745,7 @@ packages: '@aws-sdk/signature-v4': 3.310.0 '@aws-sdk/types': 3.310.0 '@aws-sdk/util-middleware': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/middleware-ssec@3.310.0: @@ -1743,14 +1753,14 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/middleware-stack@3.310.0: resolution: {integrity: sha512-010O1PD+UAcZVKRvqEusE1KJqN96wwrf6QsqbRM0ywsKQ21NDweaHvEDlds2VHpgmofxkRLRu/IDrlPkKRQrRg==} engines: {node: '>=14.0.0'} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/middleware-user-agent@3.319.0: @@ -1760,7 +1770,7 @@ packages: '@aws-sdk/protocol-http': 3.310.0 '@aws-sdk/types': 3.310.0 '@aws-sdk/util-endpoints': 3.319.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/node-config-provider@3.310.0: @@ -1770,7 +1780,7 @@ packages: '@aws-sdk/property-provider': 3.310.0 '@aws-sdk/shared-ini-file-loader': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/node-http-handler@3.321.1: @@ -1789,7 +1799,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/protocol-http@3.310.0: @@ -1797,7 +1807,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/querystring-builder@3.310.0: @@ -1806,7 +1816,7 @@ packages: dependencies: '@aws-sdk/types': 3.310.0 '@aws-sdk/util-uri-escape': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/querystring-parser@3.310.0: @@ -1814,7 +1824,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/service-error-classification@3.310.0: @@ -1827,7 +1837,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/signature-v4-multi-region@3.310.0: @@ -1842,7 +1852,7 @@ packages: '@aws-sdk/protocol-http': 3.310.0 '@aws-sdk/signature-v4': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/signature-v4@3.310.0: @@ -1855,7 +1865,7 @@ packages: '@aws-sdk/util-middleware': 3.310.0 '@aws-sdk/util-uri-escape': 3.310.0 '@aws-sdk/util-utf8': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/smithy-client@3.316.0: @@ -1864,7 +1874,7 @@ packages: dependencies: '@aws-sdk/middleware-stack': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/token-providers@3.321.1: @@ -1875,7 +1885,7 @@ packages: '@aws-sdk/property-provider': 3.310.0 '@aws-sdk/shared-ini-file-loader': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 transitivePeerDependencies: - aws-crt dev: false @@ -1884,7 +1894,7 @@ packages: resolution: {integrity: sha512-j8eamQJ7YcIhw7fneUfs8LYl3t01k4uHi4ZDmNRgtbmbmTTG3FZc2MotStZnp3nZB6vLiPF1o5aoJxWVvkzS6A==} engines: {node: '>=14.0.0'} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/url-parser@3.310.0: @@ -1892,14 +1902,14 @@ packages: dependencies: '@aws-sdk/querystring-parser': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-arn-parser@3.310.0: resolution: {integrity: sha512-jL8509owp/xB9+Or0pvn3Fe+b94qfklc2yPowZZIFAkFcCSIdkIglz18cPDWnYAcy9JGewpMS1COXKIUhZkJsA==} engines: {node: '>=14.0.0'} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-base64@3.310.0: @@ -1907,20 +1917,20 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@aws-sdk/util-buffer-from': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-body-length-browser@3.310.0: resolution: {integrity: sha512-sxsC3lPBGfpHtNTUoGXMQXLwjmR0zVpx0rSvzTPAuoVILVsp5AU/w5FphNPxD5OVIjNbZv9KsKTuvNTiZjDp9g==} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-body-length-node@3.310.0: resolution: {integrity: sha512-2tqGXdyKhyA6w4zz7UPoS8Ip+7sayOg9BwHNidiGm2ikbDxm1YrCfYXvCBdwaJxa4hJfRVz+aL9e+d3GqPI9pQ==} engines: {node: '>=14.0.0'} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-buffer-from@3.310.0: @@ -1928,14 +1938,14 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@aws-sdk/is-array-buffer': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-config-provider@3.310.0: resolution: {integrity: sha512-xIBaYo8dwiojCw8vnUcIL4Z5tyfb1v3yjqyJKJWV/dqKUFOOS0U591plmXbM+M/QkXyML3ypon1f8+BoaDExrg==} engines: {node: '>=14.0.0'} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-defaults-mode-browser@3.316.0: @@ -1945,7 +1955,7 @@ packages: '@aws-sdk/property-provider': 3.310.0 '@aws-sdk/types': 3.310.0 bowser: 2.11.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-defaults-mode-node@3.316.0: @@ -1957,7 +1967,7 @@ packages: '@aws-sdk/node-config-provider': 3.310.0 '@aws-sdk/property-provider': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-endpoints@3.319.0: @@ -1965,28 +1975,28 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-hex-encoding@3.310.0: resolution: {integrity: sha512-sVN7mcCCDSJ67pI1ZMtk84SKGqyix6/0A1Ab163YKn+lFBQRMKexleZzpYzNGxYzmQS6VanP/cfU7NiLQOaSfA==} engines: {node: '>=14.0.0'} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-locate-window@3.208.0: resolution: {integrity: sha512-iua1A2+P7JJEDHVgvXrRJSvsnzG7stYSGQnBVphIUlemwl6nN5D+QrgbjECtrbxRz8asYFHSzhdhECqN+tFiBg==} engines: {node: '>=14.0.0'} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-middleware@3.310.0: resolution: {integrity: sha512-FTSUKL/eRb9X6uEZClrTe27QFXUNNp7fxYrPndZwk1hlaOP5ix+MIHBcI7pIiiY/JPfOUmPyZOu+HetlFXjWog==} engines: {node: '>=14.0.0'} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-retry@3.310.0: @@ -1994,7 +2004,7 @@ packages: engines: {node: '>= 14.0.0'} dependencies: '@aws-sdk/service-error-classification': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-stream-browser@3.310.0: @@ -2005,7 +2015,7 @@ packages: '@aws-sdk/util-base64': 3.310.0 '@aws-sdk/util-hex-encoding': 3.310.0 '@aws-sdk/util-utf8': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-stream-node@3.321.1: @@ -2015,14 +2025,14 @@ packages: '@aws-sdk/node-http-handler': 3.321.1 '@aws-sdk/types': 3.310.0 '@aws-sdk/util-buffer-from': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-uri-escape@3.310.0: resolution: {integrity: sha512-drzt+aB2qo2LgtDoiy/3sVG8w63cgLkqFIa2NFlGpUgHFWTXkqtbgf4L5QdjRGKWhmZsnqkbtL7vkSWEcYDJ4Q==} engines: {node: '>=14.0.0'} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-user-agent-browser@3.310.0: @@ -2030,7 +2040,7 @@ packages: dependencies: '@aws-sdk/types': 3.310.0 bowser: 2.11.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-user-agent-node@3.310.0: @@ -2044,13 +2054,13 @@ packages: dependencies: '@aws-sdk/node-config-provider': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-utf8-browser@3.259.0: resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-utf8@3.310.0: @@ -2058,7 +2068,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@aws-sdk/util-buffer-from': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/util-waiter@3.310.0: @@ -2067,25 +2077,24 @@ packages: dependencies: '@aws-sdk/abort-controller': 3.310.0 '@aws-sdk/types': 3.310.0 - tslib: 2.5.0 + tslib: 2.5.2 dev: false /@aws-sdk/xml-builder@3.310.0: resolution: {integrity: sha512-TqELu4mOuSIKQCqj63fGVs86Yh+vBx5nHRpWKNUNhB2nPTpfbziTs5c1X358be3peVWA4wPxW7Nt53KIg1tnNw==} engines: {node: '>=14.0.0'} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: false - /@babel/code-frame@7.18.6: - resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} + /@babel/code-frame@7.21.4: + resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==} engines: {node: '>=6.9.0'} dependencies: '@babel/highlight': 7.18.6 - dev: true - /@babel/compat-data@7.21.4: - resolution: {integrity: sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g==} + /@babel/compat-data@7.22.3: + resolution: {integrity: sha512-aNtko9OPOwVESUFp3MZfD8Uzxl7JzSeJpd7npIoxCasU37PFbAQRpKglkaKwlHOyeJdrREpo8TW8ldrkYWwvIQ==} engines: {node: '>=6.9.0'} dev: true @@ -2093,16 +2102,39 @@ packages: resolution: {integrity: sha512-qIJONzoa/qiHghnm0l1n4i/6IIziDpzqc36FBs4pzMhDUraHqponwJLiAKm1hGLP3OSB/TVNz6rMwVGpwxxySw==} engines: {node: '>=6.9.0'} dependencies: - '@ampproject/remapping': 2.2.0 - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.21.3 - '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.21.3) - '@babel/helper-module-transforms': 7.21.2 - '@babel/helpers': 7.21.0 - '@babel/parser': 7.21.8 - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.3 - '@babel/types': 7.21.5 + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.21.4 + '@babel/generator': 7.22.3 + '@babel/helper-compilation-targets': 7.22.1(@babel/core@7.21.3) + '@babel/helper-module-transforms': 7.22.1 + '@babel/helpers': 7.22.3 + '@babel/parser': 7.22.4 + '@babel/template': 7.21.9 + '@babel/traverse': 7.22.4 + '@babel/types': 7.22.4 + convert-source-map: 1.9.0 + debug: 4.3.4(supports-color@8.1.1) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/core@7.22.1: + resolution: {integrity: sha512-Hkqu7J4ynysSXxmAahpN1jjRwVJ+NdpraFLIWflgjpVob3KNyK3/tIUc7Q7szed8WMp0JNa7Qtd1E9Oo22F9gA==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.21.4 + '@babel/generator': 7.22.3 + '@babel/helper-compilation-targets': 7.22.1(@babel/core@7.22.1) + '@babel/helper-module-transforms': 7.22.1 + '@babel/helpers': 7.22.3 + '@babel/parser': 7.22.4 + '@babel/template': 7.21.9 + '@babel/traverse': 7.22.4 + '@babel/types': 7.22.4 convert-source-map: 1.9.0 debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 @@ -2116,17 +2148,25 @@ packages: resolution: {integrity: sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.5 + '@babel/types': 7.22.4 + '@jridgewell/gen-mapping': 0.3.2 + '@jridgewell/trace-mapping': 0.3.17 + jsesc: 2.5.2 + + /@babel/generator@7.22.3: + resolution: {integrity: sha512-C17MW4wlk//ES/CJDL51kPNwl+qiBQyN7b9SKyVp11BLGFeSPoVaHrv+MNt8jwQFhQWowW88z1eeBx3pFz9v8A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.4 '@jridgewell/gen-mapping': 0.3.2 '@jridgewell/trace-mapping': 0.3.17 jsesc: 2.5.2 - dev: true /@babel/helper-annotate-as-pure@7.18.6: resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.5 + '@babel/types': 7.22.4 dev: true /@babel/helper-builder-binary-assignment-operator-visitor@7.18.9: @@ -2134,16 +2174,16 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/helper-explode-assignable-expression': 7.18.6 - '@babel/types': 7.21.5 + '@babel/types': 7.22.4 dev: true - /@babel/helper-compilation-targets@7.21.4(@babel/core@7.21.3): - resolution: {integrity: sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg==} + /@babel/helper-compilation-targets@7.22.1(@babel/core@7.21.3): + resolution: {integrity: sha512-Rqx13UM3yVB5q0D/KwQ8+SPfX/+Rnsy1Lw1k/UwOC4KC6qrzIQoY3lYnBu5EHKBlEHHcj0M0W8ltPSkD8rqfsQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/compat-data': 7.21.4 + '@babel/compat-data': 7.22.3 '@babel/core': 7.21.3 '@babel/helper-validator-option': 7.21.0 browserslist: 4.21.5 @@ -2151,6 +2191,20 @@ packages: semver: 6.3.0 dev: true + /@babel/helper-compilation-targets@7.22.1(@babel/core@7.22.1): + resolution: {integrity: sha512-Rqx13UM3yVB5q0D/KwQ8+SPfX/+Rnsy1Lw1k/UwOC4KC6qrzIQoY3lYnBu5EHKBlEHHcj0M0W8ltPSkD8rqfsQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/compat-data': 7.22.3 + '@babel/core': 7.22.1 + '@babel/helper-validator-option': 7.21.0 + browserslist: 4.21.5 + lru-cache: 5.1.1 + semver: 6.3.0 + dev: true + /@babel/helper-create-class-features-plugin@7.21.0(@babel/core@7.21.3): resolution: {integrity: sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ==} engines: {node: '>=6.9.0'} @@ -2159,7 +2213,26 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-environment-visitor': 7.22.1 + '@babel/helper-function-name': 7.21.0 + '@babel/helper-member-expression-to-functions': 7.21.0 + '@babel/helper-optimise-call-expression': 7.18.6 + '@babel/helper-replace-supers': 7.20.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/helper-split-export-declaration': 7.18.6 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-create-class-features-plugin@7.21.0(@babel/core@7.22.1): + resolution: {integrity: sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-environment-visitor': 7.22.1 '@babel/helper-function-name': 7.21.0 '@babel/helper-member-expression-to-functions': 7.21.0 '@babel/helper-optimise-call-expression': 7.18.6 @@ -2181,13 +2254,24 @@ packages: regexpu-core: 5.3.2 dev: true + /@babel/helper-create-regexp-features-plugin@7.21.0(@babel/core@7.22.1): + resolution: {integrity: sha512-N+LaFW/auRSWdx7SHD/HiARwXQju1vXTW4fKr4u5SgBUTm51OKEjKgj+cs00ggW3kEvNqwErnlwuq7Y3xBe4eg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-annotate-as-pure': 7.18.6 + regexpu-core: 5.3.2 + dev: true + /@babel/helper-define-polyfill-provider@0.3.3(@babel/core@7.21.3): resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==} peerDependencies: '@babel/core': ^7.4.0-0 dependencies: '@babel/core': 7.21.3 - '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.21.3) + '@babel/helper-compilation-targets': 7.22.1(@babel/core@7.21.3) '@babel/helper-plugin-utils': 7.20.2 debug: 4.3.4(supports-color@8.1.1) lodash.debounce: 4.0.8 @@ -2197,59 +2281,72 @@ packages: - supports-color dev: true - /@babel/helper-environment-visitor@7.18.9: - resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} - engines: {node: '>=6.9.0'} + /@babel/helper-define-polyfill-provider@0.3.3(@babel/core@7.22.1): + resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==} + peerDependencies: + '@babel/core': ^7.4.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-compilation-targets': 7.22.1(@babel/core@7.22.1) + '@babel/helper-plugin-utils': 7.20.2 + debug: 4.3.4(supports-color@8.1.1) + lodash.debounce: 4.0.8 + resolve: 1.22.1 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color dev: true + /@babel/helper-environment-visitor@7.22.1: + resolution: {integrity: sha512-Z2tgopurB/kTbidvzeBrc2To3PUP/9i5MUe+fU6QJCQDyPwSH2oRapkLw3KGECDYSjhQZCNxEvNvZlLw8JjGwA==} + engines: {node: '>=6.9.0'} + /@babel/helper-explode-assignable-expression@7.18.6: resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.5 + '@babel/types': 7.22.4 dev: true /@babel/helper-function-name@7.21.0: resolution: {integrity: sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/template': 7.20.7 - '@babel/types': 7.21.5 - dev: true + '@babel/template': 7.21.9 + '@babel/types': 7.22.4 /@babel/helper-hoist-variables@7.18.6: resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.5 - dev: true + '@babel/types': 7.22.4 /@babel/helper-member-expression-to-functions@7.21.0: resolution: {integrity: sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.5 + '@babel/types': 7.22.4 dev: true - /@babel/helper-module-imports@7.18.6: - resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + /@babel/helper-module-imports@7.21.4: + resolution: {integrity: sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.5 + '@babel/types': 7.22.4 dev: true - /@babel/helper-module-transforms@7.21.2: - resolution: {integrity: sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==} + /@babel/helper-module-transforms@7.22.1: + resolution: {integrity: sha512-dxAe9E7ySDGbQdCVOY/4+UcD8M9ZFqZcZhSPsPacvCG4M+9lwtDDQfI2EoaSvmf7W/8yCBkGU0m7Pvt1ru3UZw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-simple-access': 7.20.2 + '@babel/helper-environment-visitor': 7.22.1 + '@babel/helper-module-imports': 7.21.4 + '@babel/helper-simple-access': 7.21.5 '@babel/helper-split-export-declaration': 7.18.6 '@babel/helper-validator-identifier': 7.19.1 - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.3 - '@babel/types': 7.21.5 + '@babel/template': 7.21.9 + '@babel/traverse': 7.22.4 + '@babel/types': 7.22.4 transitivePeerDependencies: - supports-color dev: true @@ -2258,7 +2355,7 @@ packages: resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.5 + '@babel/types': 7.22.4 dev: true /@babel/helper-plugin-utils@7.20.2: @@ -2274,9 +2371,24 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-environment-visitor': 7.22.1 '@babel/helper-wrap-function': 7.20.5 - '@babel/types': 7.21.5 + '@babel/types': 7.22.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-remap-async-to-generator@7.18.9(@babel/core@7.22.1): + resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-environment-visitor': 7.22.1 + '@babel/helper-wrap-function': 7.20.5 + '@babel/types': 7.22.4 transitivePeerDependencies: - supports-color dev: true @@ -2285,40 +2397,35 @@ packages: resolution: {integrity: sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-environment-visitor': 7.22.1 '@babel/helper-member-expression-to-functions': 7.21.0 '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.3 - '@babel/types': 7.21.5 + '@babel/template': 7.21.9 + '@babel/traverse': 7.22.4 + '@babel/types': 7.22.4 transitivePeerDependencies: - supports-color dev: true - /@babel/helper-simple-access@7.20.2: - resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==} + /@babel/helper-simple-access@7.21.5: + resolution: {integrity: sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.5 + '@babel/types': 7.22.4 dev: true /@babel/helper-skip-transparent-expression-wrappers@7.20.0: resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.5 + '@babel/types': 7.22.4 dev: true /@babel/helper-split-export-declaration@7.18.6: resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.5 - dev: true - - /@babel/helper-string-parser@7.19.4: - resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} - engines: {node: '>=6.9.0'} + '@babel/types': 7.22.4 /@babel/helper-string-parser@7.21.5: resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==} @@ -2338,20 +2445,20 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/helper-function-name': 7.21.0 - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.3 - '@babel/types': 7.21.5 + '@babel/template': 7.21.9 + '@babel/traverse': 7.22.4 + '@babel/types': 7.22.4 transitivePeerDependencies: - supports-color dev: true - /@babel/helpers@7.21.0: - resolution: {integrity: sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==} + /@babel/helpers@7.22.3: + resolution: {integrity: sha512-jBJ7jWblbgr7r6wYZHMdIqKc73ycaTcCaWRq4/2LpuPHcx7xMlZvpGQkOYc9HeSjn6rcx15CPlgVcBtZ4WZJ2w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.3 - '@babel/types': 7.21.5 + '@babel/template': 7.21.9 + '@babel/traverse': 7.22.4 + '@babel/types': 7.22.4 transitivePeerDependencies: - supports-color dev: true @@ -2363,21 +2470,27 @@ packages: '@babel/helper-validator-identifier': 7.19.1 chalk: 2.4.2 js-tokens: 4.0.0 - dev: true - - /@babel/parser@7.21.4: - resolution: {integrity: sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==} - engines: {node: '>=6.0.0'} - hasBin: true - dependencies: - '@babel/types': 7.21.4 /@babel/parser@7.21.8: resolution: {integrity: sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.21.4 + '@babel/types': 7.22.4 + + /@babel/parser@7.21.9: + resolution: {integrity: sha512-q5PNg/Bi1OpGgx5jYlvWZwAorZepEudDMCLtj967aeS7WMont7dUZI46M2XwcIQqvUlMxWfdLFu4S/qSxeUu5g==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.22.4 + + /@babel/parser@7.22.4: + resolution: {integrity: sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.22.4 /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} @@ -2389,6 +2502,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.20.7(@babel/core@7.21.3): resolution: {integrity: sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==} engines: {node: '>=6.9.0'} @@ -2401,6 +2524,18 @@ packages: '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.21.3) dev: true + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.20.7(@babel/core@7.22.1): + resolution: {integrity: sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.22.1) + dev: true + /@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.21.3): resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} engines: {node: '>=6.9.0'} @@ -2408,7 +2543,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.3 - '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-environment-visitor': 7.22.1 '@babel/helper-plugin-utils': 7.20.2 '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.21.3) '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.3) @@ -2416,6 +2551,21 @@ packages: - supports-color dev: true + /@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.22.1): + resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-environment-visitor': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.22.1) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.1) + transitivePeerDependencies: + - supports-color + dev: true + /@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} engines: {node: '>=6.9.0'} @@ -2429,6 +2579,19 @@ packages: - supports-color dev: true + /@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.22.1) + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/plugin-proposal-class-static-block@7.21.0(@babel/core@7.21.3): resolution: {integrity: sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==} engines: {node: '>=6.9.0'} @@ -2443,6 +2606,20 @@ packages: - supports-color dev: true + /@babel/plugin-proposal-class-static-block@7.21.0(@babel/core@7.22.1): + resolution: {integrity: sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.22.1) + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.22.1) + transitivePeerDependencies: + - supports-color + dev: true + /@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} engines: {node: '>=6.9.0'} @@ -2454,6 +2631,17 @@ packages: '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.21.3) dev: true + /@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.22.1) + dev: true + /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.21.3): resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} engines: {node: '>=6.9.0'} @@ -2465,6 +2653,17 @@ packages: '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.21.3) dev: true + /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.22.1): + resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.22.1) + dev: true + /@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} engines: {node: '>=6.9.0'} @@ -2476,6 +2675,17 @@ packages: '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.21.3) dev: true + /@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.22.1) + dev: true + /@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.21.3): resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} engines: {node: '>=6.9.0'} @@ -2487,6 +2697,17 @@ packages: '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.21.3) dev: true + /@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.22.1): + resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.22.1) + dev: true + /@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} engines: {node: '>=6.9.0'} @@ -2498,6 +2719,17 @@ packages: '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.21.3) dev: true + /@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.22.1) + dev: true + /@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} engines: {node: '>=6.9.0'} @@ -2509,20 +2741,45 @@ packages: '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.21.3) dev: true + /@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.22.1) + dev: true + /@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.21.3): resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.21.4 + '@babel/compat-data': 7.22.3 '@babel/core': 7.21.3 - '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.21.3) + '@babel/helper-compilation-targets': 7.22.1(@babel/core@7.21.3) '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.21.3) '@babel/plugin-transform-parameters': 7.21.3(@babel/core@7.21.3) dev: true + /@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.22.1): + resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.22.3 + '@babel/core': 7.22.1 + '@babel/helper-compilation-targets': 7.22.1(@babel/core@7.22.1) + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.22.1) + '@babel/plugin-transform-parameters': 7.21.3(@babel/core@7.22.1) + dev: true + /@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} engines: {node: '>=6.9.0'} @@ -2534,6 +2791,17 @@ packages: '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.21.3) dev: true + /@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.22.1) + dev: true + /@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.21.3): resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} engines: {node: '>=6.9.0'} @@ -2546,6 +2814,18 @@ packages: '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.3) dev: true + /@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.22.1): + resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.22.1) + dev: true + /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} engines: {node: '>=6.9.0'} @@ -2559,6 +2839,19 @@ packages: - supports-color dev: true + /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.22.1) + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/plugin-proposal-private-property-in-object@7.21.0(@babel/core@7.21.3): resolution: {integrity: sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==} engines: {node: '>=6.9.0'} @@ -2574,6 +2867,21 @@ packages: - supports-color dev: true + /@babel/plugin-proposal-private-property-in-object@7.21.0(@babel/core@7.22.1): + resolution: {integrity: sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.22.1) + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.22.1) + transitivePeerDependencies: + - supports-color + dev: true + /@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} engines: {node: '>=4'} @@ -2585,6 +2893,17 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} + engines: {node: '>=4'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-create-regexp-features-plugin': 7.21.0(@babel/core@7.22.1) + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.21.3): resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: @@ -2594,6 +2913,15 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.22.1): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.21.3): resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: @@ -2603,6 +2931,15 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.22.1): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.21.3): resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: @@ -2612,6 +2949,15 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.22.1): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.21.3): resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} @@ -2622,6 +2968,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.22.1): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.21.3): resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} peerDependencies: @@ -2631,6 +2987,15 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.22.1): + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.21.3): resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} peerDependencies: @@ -2640,13 +3005,22 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/plugin-syntax-flow@7.18.6(@babel/core@7.21.3): + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.22.1): + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-flow@7.18.6(@babel/core@7.22.1): resolution: {integrity: sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.3 + '@babel/core': 7.22.1 '@babel/helper-plugin-utils': 7.20.2 dev: true @@ -2660,6 +3034,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-syntax-import-assertions@7.20.0(@babel/core@7.22.1): + resolution: {integrity: sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.21.3): resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: @@ -2669,6 +3053,15 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.22.1): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.21.3): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: @@ -2678,13 +3071,22 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/plugin-syntax-jsx@7.18.6(@babel/core@7.21.3): + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.22.1): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-jsx@7.18.6(@babel/core@7.22.1): resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.3 + '@babel/core': 7.22.1 '@babel/helper-plugin-utils': 7.20.2 dev: true @@ -2697,6 +3099,15 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.22.1): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.21.3): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: @@ -2706,6 +3117,15 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.22.1): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.21.3): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: @@ -2715,6 +3135,15 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.22.1): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.21.3): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: @@ -2724,6 +3153,15 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.22.1): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.21.3): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: @@ -2733,6 +3171,15 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.22.1): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.21.3): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: @@ -2742,6 +3189,15 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.22.1): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.21.3): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} engines: {node: '>=6.9.0'} @@ -2752,6 +3208,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.22.1): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.21.3): resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} @@ -2762,13 +3228,23 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.21.3): + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.22.1): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.22.1): resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.3 + '@babel/core': 7.22.1 '@babel/helper-plugin-utils': 7.20.2 dev: true @@ -2782,6 +3258,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-arrow-functions@7.20.7(@babel/core@7.22.1): + resolution: {integrity: sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-async-to-generator@7.20.7(@babel/core@7.21.3): resolution: {integrity: sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==} engines: {node: '>=6.9.0'} @@ -2789,13 +3275,27 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.3 - '@babel/helper-module-imports': 7.18.6 + '@babel/helper-module-imports': 7.21.4 '@babel/helper-plugin-utils': 7.20.2 '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.21.3) transitivePeerDependencies: - supports-color dev: true + /@babel/plugin-transform-async-to-generator@7.20.7(@babel/core@7.22.1): + resolution: {integrity: sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-module-imports': 7.21.4 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.22.1) + transitivePeerDependencies: + - supports-color + dev: true + /@babel/plugin-transform-block-scoped-functions@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} engines: {node: '>=6.9.0'} @@ -2806,6 +3306,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-block-scoped-functions@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-block-scoping@7.21.0(@babel/core@7.21.3): resolution: {integrity: sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==} engines: {node: '>=6.9.0'} @@ -2816,6 +3326,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-block-scoping@7.21.0(@babel/core@7.22.1): + resolution: {integrity: sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-classes@7.21.0(@babel/core@7.21.3): resolution: {integrity: sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==} engines: {node: '>=6.9.0'} @@ -2824,8 +3344,28 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.21.3) - '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-compilation-targets': 7.22.1(@babel/core@7.21.3) + '@babel/helper-environment-visitor': 7.22.1 + '@babel/helper-function-name': 7.21.0 + '@babel/helper-optimise-call-expression': 7.18.6 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-replace-supers': 7.20.7 + '@babel/helper-split-export-declaration': 7.18.6 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-classes@7.21.0(@babel/core@7.22.1): + resolution: {integrity: sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-compilation-targets': 7.22.1(@babel/core@7.22.1) + '@babel/helper-environment-visitor': 7.22.1 '@babel/helper-function-name': 7.21.0 '@babel/helper-optimise-call-expression': 7.18.6 '@babel/helper-plugin-utils': 7.20.2 @@ -2844,7 +3384,18 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-plugin-utils': 7.20.2 - '@babel/template': 7.20.7 + '@babel/template': 7.21.9 + dev: true + + /@babel/plugin-transform-computed-properties@7.20.7(@babel/core@7.22.1): + resolution: {integrity: sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/template': 7.21.9 dev: true /@babel/plugin-transform-destructuring@7.21.3(@babel/core@7.21.3): @@ -2857,6 +3408,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-destructuring@7.21.3(@babel/core@7.22.1): + resolution: {integrity: sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-dotall-regex@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} engines: {node: '>=6.9.0'} @@ -2868,6 +3429,17 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-dotall-regex@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-create-regexp-features-plugin': 7.21.0(@babel/core@7.22.1) + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-duplicate-keys@7.18.9(@babel/core@7.21.3): resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} engines: {node: '>=6.9.0'} @@ -2878,6 +3450,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-duplicate-keys@7.18.9(@babel/core@7.22.1): + resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-exponentiation-operator@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} engines: {node: '>=6.9.0'} @@ -2889,15 +3471,26 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/plugin-transform-flow-strip-types@7.21.0(@babel/core@7.21.3): + /@babel/plugin-transform-exponentiation-operator@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-flow-strip-types@7.21.0(@babel/core@7.22.1): resolution: {integrity: sha512-FlFA2Mj87a6sDkW4gfGrQQqwY/dLlBAyJa2dJEZ+FHXUVHBflO2wyKvg+OOEzXfrKYIa4HWl0mgmbCzt0cMb7w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.3 + '@babel/core': 7.22.1 '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-flow': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-syntax-flow': 7.18.6(@babel/core@7.22.1) dev: true /@babel/plugin-transform-for-of@7.21.0(@babel/core@7.21.3): @@ -2910,6 +3503,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-for-of@7.21.0(@babel/core@7.22.1): + resolution: {integrity: sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-function-name@7.18.9(@babel/core@7.21.3): resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} engines: {node: '>=6.9.0'} @@ -2917,7 +3520,19 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.3 - '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.21.3) + '@babel/helper-compilation-targets': 7.22.1(@babel/core@7.21.3) + '@babel/helper-function-name': 7.21.0 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-function-name@7.18.9(@babel/core@7.22.1): + resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-compilation-targets': 7.22.1(@babel/core@7.22.1) '@babel/helper-function-name': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 dev: true @@ -2932,6 +3547,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-literals@7.18.9(@babel/core@7.22.1): + resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-member-expression-literals@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} engines: {node: '>=6.9.0'} @@ -2942,6 +3567,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-member-expression-literals@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-modules-amd@7.20.11(@babel/core@7.21.3): resolution: {integrity: sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==} engines: {node: '>=6.9.0'} @@ -2949,7 +3584,20 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.3 - '@babel/helper-module-transforms': 7.21.2 + '@babel/helper-module-transforms': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-amd@7.20.11(@babel/core@7.22.1): + resolution: {integrity: sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-module-transforms': 7.22.1 '@babel/helper-plugin-utils': 7.20.2 transitivePeerDependencies: - supports-color @@ -2962,9 +3610,23 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.3 - '@babel/helper-module-transforms': 7.21.2 + '@babel/helper-module-transforms': 7.22.1 '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-simple-access': 7.20.2 + '@babel/helper-simple-access': 7.21.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-commonjs@7.21.2(@babel/core@7.22.1): + resolution: {integrity: sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-module-transforms': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-simple-access': 7.21.5 transitivePeerDependencies: - supports-color dev: true @@ -2977,7 +3639,22 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-module-transforms': 7.21.2 + '@babel/helper-module-transforms': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-validator-identifier': 7.19.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-systemjs@7.20.11(@babel/core@7.22.1): + resolution: {integrity: sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-module-transforms': 7.22.1 '@babel/helper-plugin-utils': 7.20.2 '@babel/helper-validator-identifier': 7.19.1 transitivePeerDependencies: @@ -2991,7 +3668,20 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.3 - '@babel/helper-module-transforms': 7.21.2 + '@babel/helper-module-transforms': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-umd@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-module-transforms': 7.22.1 '@babel/helper-plugin-utils': 7.20.2 transitivePeerDependencies: - supports-color @@ -3008,6 +3698,17 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-named-capturing-groups-regex@7.20.5(@babel/core@7.22.1): + resolution: {integrity: sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-create-regexp-features-plugin': 7.21.0(@babel/core@7.22.1) + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-new-target@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} engines: {node: '>=6.9.0'} @@ -3018,6 +3719,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-new-target@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-object-super@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} engines: {node: '>=6.9.0'} @@ -3031,6 +3742,19 @@ packages: - supports-color dev: true + /@babel/plugin-transform-object-super@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-replace-supers': 7.20.7 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/plugin-transform-parameters@7.21.3(@babel/core@7.21.3): resolution: {integrity: sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==} engines: {node: '>=6.9.0'} @@ -3041,6 +3765,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-parameters@7.21.3(@babel/core@7.22.1): + resolution: {integrity: sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-property-literals@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} engines: {node: '>=6.9.0'} @@ -3051,38 +3785,48 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/plugin-transform-react-jsx-self@7.21.0(@babel/core@7.21.3): + /@babel/plugin-transform-property-literals@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-react-jsx-self@7.21.0(@babel/core@7.22.1): resolution: {integrity: sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.3 + '@babel/core': 7.22.1 '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/plugin-transform-react-jsx-source@7.19.6(@babel/core@7.21.3): + /@babel/plugin-transform-react-jsx-source@7.19.6(@babel/core@7.22.1): resolution: {integrity: sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.3 + '@babel/core': 7.22.1 '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/plugin-transform-react-jsx@7.21.0(@babel/core@7.21.3): + /@babel/plugin-transform-react-jsx@7.21.0(@babel/core@7.22.1): resolution: {integrity: sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.3 + '@babel/core': 7.22.1 '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-module-imports': 7.18.6 + '@babel/helper-module-imports': 7.21.4 '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.3) - '@babel/types': 7.21.5 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.22.1) + '@babel/types': 7.22.4 dev: true /@babel/plugin-transform-regenerator@7.20.5(@babel/core@7.21.3): @@ -3096,6 +3840,17 @@ packages: regenerator-transform: 0.15.1 dev: true + /@babel/plugin-transform-regenerator@7.20.5(@babel/core@7.22.1): + resolution: {integrity: sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + regenerator-transform: 0.15.1 + dev: true + /@babel/plugin-transform-reserved-words@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} engines: {node: '>=6.9.0'} @@ -3106,6 +3861,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-reserved-words@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-shorthand-properties@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} engines: {node: '>=6.9.0'} @@ -3116,6 +3881,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-shorthand-properties@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-spread@7.20.7(@babel/core@7.21.3): resolution: {integrity: sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==} engines: {node: '>=6.9.0'} @@ -3127,6 +3902,17 @@ packages: '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 dev: true + /@babel/plugin-transform-spread@7.20.7(@babel/core@7.22.1): + resolution: {integrity: sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + dev: true + /@babel/plugin-transform-sticky-regex@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} engines: {node: '>=6.9.0'} @@ -3137,6 +3923,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-sticky-regex@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-template-literals@7.18.9(@babel/core@7.21.3): resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} engines: {node: '>=6.9.0'} @@ -3147,6 +3943,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-template-literals@7.18.9(@babel/core@7.22.1): + resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-typeof-symbol@7.18.9(@babel/core@7.21.3): resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} engines: {node: '>=6.9.0'} @@ -3157,17 +3963,27 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/plugin-transform-typescript@7.21.3(@babel/core@7.21.3): + /@babel/plugin-transform-typeof-symbol@7.18.9(@babel/core@7.22.1): + resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-typescript@7.21.3(@babel/core@7.22.1): resolution: {integrity: sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.3 + '@babel/core': 7.22.1 '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.21.3) + '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.22.1) '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.21.3) + '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.22.1) transitivePeerDependencies: - supports-color dev: true @@ -3182,6 +3998,16 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-unicode-escapes@7.18.10(@babel/core@7.22.1): + resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-unicode-regex@7.18.6(@babel/core@7.21.3): resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} engines: {node: '>=6.9.0'} @@ -3193,15 +4019,26 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-unicode-regex@7.18.6(@babel/core@7.22.1): + resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-create-regexp-features-plugin': 7.21.0(@babel/core@7.22.1) + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/preset-env@7.21.4(@babel/core@7.21.3): resolution: {integrity: sha512-2W57zHs2yDLm6GD5ZpvNn71lZ0B/iypSdIeq25OurDKji6AdzV07qp4s3n1/x5BqtiGaTrPN3nerlSCaC5qNTw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.21.4 + '@babel/compat-data': 7.22.3 '@babel/core': 7.21.3 - '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.21.3) + '@babel/helper-compilation-targets': 7.22.1(@babel/core@7.21.3) '@babel/helper-plugin-utils': 7.20.2 '@babel/helper-validator-option': 7.21.0 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6(@babel/core@7.21.3) @@ -3269,7 +4106,7 @@ packages: '@babel/plugin-transform-unicode-escapes': 7.18.10(@babel/core@7.21.3) '@babel/plugin-transform-unicode-regex': 7.18.6(@babel/core@7.21.3) '@babel/preset-modules': 0.1.5(@babel/core@7.21.3) - '@babel/types': 7.21.5 + '@babel/types': 7.22.4 babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.21.3) babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.21.3) babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.21.3) @@ -3279,16 +4116,102 @@ packages: - supports-color dev: true - /@babel/preset-flow@7.18.6(@babel/core@7.21.3): + /@babel/preset-env@7.21.4(@babel/core@7.22.1): + resolution: {integrity: sha512-2W57zHs2yDLm6GD5ZpvNn71lZ0B/iypSdIeq25OurDKji6AdzV07qp4s3n1/x5BqtiGaTrPN3nerlSCaC5qNTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.22.3 + '@babel/core': 7.22.1 + '@babel/helper-compilation-targets': 7.22.1(@babel/core@7.22.1) + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-validator-option': 7.21.0 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.20.7(@babel/core@7.22.1) + '@babel/plugin-proposal-async-generator-functions': 7.20.7(@babel/core@7.22.1) + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-proposal-class-static-block': 7.21.0(@babel/core@7.22.1) + '@babel/plugin-proposal-dynamic-import': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.22.1) + '@babel/plugin-proposal-json-strings': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-proposal-logical-assignment-operators': 7.20.7(@babel/core@7.22.1) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-proposal-object-rest-spread': 7.20.7(@babel/core@7.22.1) + '@babel/plugin-proposal-optional-catch-binding': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.22.1) + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-proposal-private-property-in-object': 7.21.0(@babel/core@7.22.1) + '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.1) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.22.1) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.22.1) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.22.1) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.22.1) + '@babel/plugin-syntax-import-assertions': 7.20.0(@babel/core@7.22.1) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.22.1) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.22.1) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.22.1) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.22.1) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.22.1) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.22.1) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.22.1) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.22.1) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.22.1) + '@babel/plugin-transform-arrow-functions': 7.20.7(@babel/core@7.22.1) + '@babel/plugin-transform-async-to-generator': 7.20.7(@babel/core@7.22.1) + '@babel/plugin-transform-block-scoped-functions': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-transform-block-scoping': 7.21.0(@babel/core@7.22.1) + '@babel/plugin-transform-classes': 7.21.0(@babel/core@7.22.1) + '@babel/plugin-transform-computed-properties': 7.20.7(@babel/core@7.22.1) + '@babel/plugin-transform-destructuring': 7.21.3(@babel/core@7.22.1) + '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-transform-duplicate-keys': 7.18.9(@babel/core@7.22.1) + '@babel/plugin-transform-exponentiation-operator': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-transform-for-of': 7.21.0(@babel/core@7.22.1) + '@babel/plugin-transform-function-name': 7.18.9(@babel/core@7.22.1) + '@babel/plugin-transform-literals': 7.18.9(@babel/core@7.22.1) + '@babel/plugin-transform-member-expression-literals': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-transform-modules-amd': 7.20.11(@babel/core@7.22.1) + '@babel/plugin-transform-modules-commonjs': 7.21.2(@babel/core@7.22.1) + '@babel/plugin-transform-modules-systemjs': 7.20.11(@babel/core@7.22.1) + '@babel/plugin-transform-modules-umd': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-transform-named-capturing-groups-regex': 7.20.5(@babel/core@7.22.1) + '@babel/plugin-transform-new-target': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-transform-object-super': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-transform-parameters': 7.21.3(@babel/core@7.22.1) + '@babel/plugin-transform-property-literals': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-transform-regenerator': 7.20.5(@babel/core@7.22.1) + '@babel/plugin-transform-reserved-words': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-transform-shorthand-properties': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-transform-spread': 7.20.7(@babel/core@7.22.1) + '@babel/plugin-transform-sticky-regex': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-transform-template-literals': 7.18.9(@babel/core@7.22.1) + '@babel/plugin-transform-typeof-symbol': 7.18.9(@babel/core@7.22.1) + '@babel/plugin-transform-unicode-escapes': 7.18.10(@babel/core@7.22.1) + '@babel/plugin-transform-unicode-regex': 7.18.6(@babel/core@7.22.1) + '@babel/preset-modules': 0.1.5(@babel/core@7.22.1) + '@babel/types': 7.22.4 + babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.22.1) + babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.22.1) + babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.22.1) + core-js-compat: 3.29.1 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/preset-flow@7.18.6(@babel/core@7.22.1): resolution: {integrity: sha512-E7BDhL64W6OUqpuyHnSroLnqyRTcG6ZdOBl1OKI/QK/HJfplqK/S3sq1Cckx7oTodJ5yOXyfw7rEADJ6UjoQDQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.3 + '@babel/core': 7.22.1 '@babel/helper-plugin-utils': 7.20.2 '@babel/helper-validator-option': 7.21.0 - '@babel/plugin-transform-flow-strip-types': 7.21.0(@babel/core@7.21.3) + '@babel/plugin-transform-flow-strip-types': 7.21.0(@babel/core@7.22.1) dev: true /@babel/preset-modules@0.1.5(@babel/core@7.21.3): @@ -3300,31 +4223,44 @@ packages: '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.21.3) '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.21.3) - '@babel/types': 7.21.5 + '@babel/types': 7.22.4 esutils: 2.0.3 dev: true - /@babel/preset-typescript@7.21.0(@babel/core@7.21.3): + /@babel/preset-modules@0.1.5(@babel/core@7.22.1): + resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.22.1) + '@babel/types': 7.22.4 + esutils: 2.0.3 + dev: true + + /@babel/preset-typescript@7.21.0(@babel/core@7.22.1): resolution: {integrity: sha512-myc9mpoVA5m1rF8K8DgLEatOYFDpwC+RkMkjZ0Du6uI62YvDe8uxIEYVs/VCdSJ097nlALiU/yBC7//3nI+hNg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.3 + '@babel/core': 7.22.1 '@babel/helper-plugin-utils': 7.20.2 '@babel/helper-validator-option': 7.21.0 - '@babel/plugin-transform-typescript': 7.21.3(@babel/core@7.21.3) + '@babel/plugin-transform-typescript': 7.21.3(@babel/core@7.22.1) transitivePeerDependencies: - supports-color dev: true - /@babel/register@7.21.0(@babel/core@7.21.3): + /@babel/register@7.21.0(@babel/core@7.22.1): resolution: {integrity: sha512-9nKsPmYDi5DidAqJaQooxIhsLJiNMkGr8ypQ8Uic7cIox7UCDsM7HuUGxdGT7mSDTYbqzIdsOWzfBton/YJrMw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.3 + '@babel/core': 7.22.1 clone-deep: 4.0.1 find-cache-dir: 2.1.0 make-dir: 2.1.0 @@ -3341,6 +4277,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.13.11 + dev: true /@babel/runtime@7.21.0: resolution: {integrity: sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==} @@ -3348,43 +4285,59 @@ packages: dependencies: regenerator-runtime: 0.13.11 - /@babel/template@7.20.7: - resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} + /@babel/template@7.21.9: + resolution: {integrity: sha512-MK0X5k8NKOuWRamiEfc3KEJiHMTkGZNUjzMipqCGDDc6ijRl/B7RGSKVGncu4Ro/HdyzzY6cmoXuKI2Gffk7vQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.18.6 - '@babel/parser': 7.21.8 - '@babel/types': 7.21.5 - dev: true + '@babel/code-frame': 7.21.4 + '@babel/parser': 7.22.4 + '@babel/types': 7.22.4 /@babel/traverse@7.21.3: resolution: {integrity: sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.21.3 - '@babel/helper-environment-visitor': 7.18.9 + '@babel/code-frame': 7.21.4 + '@babel/generator': 7.22.3 + '@babel/helper-environment-visitor': 7.22.1 '@babel/helper-function-name': 7.21.0 '@babel/helper-hoist-variables': 7.18.6 '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.21.8 - '@babel/types': 7.21.5 + '@babel/parser': 7.22.4 + '@babel/types': 7.22.4 + debug: 4.3.4(supports-color@8.1.1) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + /@babel/traverse@7.22.4: + resolution: {integrity: sha512-Tn1pDsjIcI+JcLKq1AVlZEr4226gpuAQTsLMorsYg9tuS/kG7nuwwJ4AB8jfQuEgb/COBwR/DqJxmoiYFu5/rQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.21.4 + '@babel/generator': 7.22.3 + '@babel/helper-environment-visitor': 7.22.1 + '@babel/helper-function-name': 7.21.0 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/parser': 7.22.4 + '@babel/types': 7.22.4 debug: 4.3.4(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color dev: true - /@babel/types@7.21.4: - resolution: {integrity: sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==} + /@babel/types@7.21.5: + resolution: {integrity: sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-string-parser': 7.19.4 + '@babel/helper-string-parser': 7.21.5 '@babel/helper-validator-identifier': 7.19.1 to-fast-properties: 2.0.0 - /@babel/types@7.21.5: - resolution: {integrity: sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==} + /@babel/types@7.22.4: + resolution: {integrity: sha512-Tx9x3UBHTTsMSW85WB2kphxYQVvrZ/t1FxD88IpSgIjiUJlCm9z+xWIDwyo1vffTwSqteqyznB8ZE9vYYk16zA==} engines: {node: '>=6.9.0'} dependencies: '@babel/helper-string-parser': 7.21.5 @@ -3399,29 +4352,29 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true - /@bull-board/api@5.1.2(@bull-board/ui@5.1.2): - resolution: {integrity: sha512-NLV88eDnOMd0XuYUNBcaYL6UdxbGYY91kP+P5Jled6CGcYmXLXv/mzAopxmgctqmP07cjC2EXN/4Oc7dyalDqA==} + /@bull-board/api@5.2.0(@bull-board/ui@5.2.0): + resolution: {integrity: sha512-1HGF2EF/4zI3+Cj414nQzwFprLXOJTlVdqXUf5UEBS4HtYafWv93mGIwkrD8S4Bpz4VSvM87adF6tQPJ7Ewt+w==} peerDependencies: - '@bull-board/ui': 5.1.2 + '@bull-board/ui': 5.2.0 dependencies: - '@bull-board/ui': 5.1.2 + '@bull-board/ui': 5.2.0 redis-info: 3.1.0 dev: false - /@bull-board/fastify@5.1.2: - resolution: {integrity: sha512-qiURhcqMfER5hp4RtgepMDbPj5H4ZKNOgK+7RIG3bd3d0tBoLjXzooXFryxzd6w130pXU9/crUMtcMP+Ulaj6g==} + /@bull-board/fastify@5.2.0: + resolution: {integrity: sha512-tvvgCAxFoiogqmAhxUiAOV/rXBVXlmg7JO3jkePA778O/YSiE7nrwwjiiLbLNuIYLZfdoYnRK4bIDmLeg1nK2A==} dependencies: - '@bull-board/api': 5.1.2(@bull-board/ui@5.1.2) - '@bull-board/ui': 5.1.2 - '@fastify/static': 6.10.1 + '@bull-board/api': 5.2.0(@bull-board/ui@5.2.0) + '@bull-board/ui': 5.2.0 + '@fastify/static': 6.10.2 '@fastify/view': 7.4.1 ejs: 3.1.8 dev: false - /@bull-board/ui@5.1.2: - resolution: {integrity: sha512-DXXbKA4NLo5D19Vssrg4pPFaFjXVzjFN0ht4GVuoJQejy7t/RVrWzZCjdyVuSiOFTlG3SyB39zW5a95Q5EXUTg==} + /@bull-board/ui@5.2.0: + resolution: {integrity: sha512-f2sgs7AjOVch7tFhbmlVCkhZjJWboxwNxWEfAsIUd1WidUC+Ef5J02tpQvu7apzRtu5zcn8IiJtI5HFO6oKaCA==} dependencies: - '@bull-board/api': 5.1.2(@bull-board/ui@5.1.2) + '@bull-board/api': 5.2.0(@bull-board/ui@5.2.0) dev: false /@canvas/image-data@1.0.0: @@ -3560,13 +4513,13 @@ packages: - supports-color dev: true - /@digitalbazaar/http-client@3.2.0: - resolution: {integrity: sha512-NhYXcWE/JDE7AnJikNX7q0S6zNuUPA2NuIoRdUpmvHlarjmRqyr6hIO3Awu2FxlUzbdiI1uzuWrZyB9mD1tTvw==} + /@digitalbazaar/http-client@3.4.1: + resolution: {integrity: sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g==} engines: {node: '>=14.0'} dependencies: - ky: 0.30.0 - ky-universal: 0.10.1(ky@0.30.0) - undici: 5.16.0 + ky: 0.33.3 + ky-universal: 0.11.0(ky@0.33.3) + undici: 5.22.1 transitivePeerDependencies: - web-streams-polyfill dev: false @@ -3778,6 +4731,16 @@ packages: eslint-visitor-keys: 3.4.1 dev: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.41.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.41.0 + eslint-visitor-keys: 3.4.1 + dev: true + /@eslint-community/regexpp@4.5.0: resolution: {integrity: sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -3805,6 +4768,11 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@eslint/js@8.41.0: + resolution: {integrity: sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /@fal-works/esbuild-plugin-global-externals@2.1.2: resolution: {integrity: sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==} dev: true @@ -3843,8 +4811,8 @@ packages: fastify-plugin: 4.5.0 dev: false - /@fastify/cors@8.2.1: - resolution: {integrity: sha512-2H2MrDD3ea7g707g1CNNLWb9/tYbmw7HS+MK2SDcgjxwzbOFR93JortelTIO8DBFsZqFtEpKNxiZfSyrGgYcbw==} + /@fastify/cors@8.3.0: + resolution: {integrity: sha512-oj9xkka2Tg0MrwuKhsSUumcAkfp2YCnKxmFEusi01pjk1YrdDsuSYTHXEelWNW+ilSy/ApZq0c2SvhKrLX0H1g==} dependencies: fastify-plugin: 4.5.0 mnemonist: 0.39.5 @@ -3864,12 +4832,12 @@ packages: fast-json-stringify: 5.7.0 dev: false - /@fastify/http-proxy@9.1.0: + /@fastify/http-proxy@9.1.0(bufferutil@4.0.7)(utf-8-validate@6.0.3): resolution: {integrity: sha512-vgHCTDKOqLB437zQJiLWFFnsrYfFZ6Lfwu/xXQoKqRUKIPDt+xG6LBRtf8s5MNqfFVoTE7kw1U/0qdRGDsMp4Q==} dependencies: '@fastify/reply-from': 9.0.1 fastify-plugin: 4.5.0 - ws: 8.13.0 + ws: 8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -3910,8 +4878,8 @@ packages: mime: 3.0.0 dev: false - /@fastify/static@6.10.1: - resolution: {integrity: sha512-DNnG+5QenQcTQw37qk0/191STThnN6SbU+2XMpWtpYR3gQUfUvMax14jTT/jqNINNbCkQJaKMnPtpFPKo4/68g==} + /@fastify/static@6.10.2: + resolution: {integrity: sha512-UoaMvIHSBLCZBYOVZwFRYqX2ufUhd7FFMYGDeSf0Z+D8jhYtwljjmuQGuanUP8kS4y/ZEV1a8mfLha3zNwsnnQ==} dependencies: '@fastify/accept-negotiator': 1.0.0 '@fastify/send': 2.0.1 @@ -3965,6 +4933,7 @@ packages: /@ioredis/commands@1.2.0: resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + dev: false /@istanbuljs/load-nyc-config@1.1.0: resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} @@ -3987,7 +4956,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.5.0 - '@types/node': 20.1.3 + '@types/node': 20.2.5 chalk: 4.1.2 jest-message-util: 29.5.0 jest-util: 29.5.0 @@ -4008,14 +4977,14 @@ packages: '@jest/test-result': 29.5.0 '@jest/transform': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 20.1.3 + '@types/node': 20.2.5 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.7.1 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.5.0 - jest-config: 29.5.0(@types/node@20.1.3) + jest-config: 29.5.0(@types/node@20.2.5) jest-haste-map: 29.5.0 jest-message-util: 29.5.0 jest-regex-util: 29.4.3 @@ -4049,7 +5018,7 @@ packages: dependencies: '@jest/fake-timers': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 20.1.3 + '@types/node': 20.2.5 jest-mock: 29.5.0 dev: true @@ -4075,8 +5044,8 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.5.0 - '@sinonjs/fake-timers': 10.0.2 - '@types/node': 20.1.3 + '@sinonjs/fake-timers': 10.2.0 + '@types/node': 20.2.5 jest-message-util: 29.5.0 jest-mock: 29.5.0 jest-util: 29.5.0 @@ -4109,7 +5078,7 @@ packages: '@jest/transform': 29.5.0 '@jest/types': 29.5.0 '@jridgewell/trace-mapping': 0.3.17 - '@types/node': 20.1.3 + '@types/node': 20.2.5 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -4178,7 +5147,7 @@ packages: resolution: {integrity: sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/core': 7.21.3 + '@babel/core': 7.22.1 '@jest/types': 29.5.0 '@jridgewell/trace-mapping': 0.3.17 babel-plugin-istanbul: 6.1.1 @@ -4203,7 +5172,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.1.3 + '@types/node': 20.2.5 '@types/yargs': 16.0.5 chalk: 4.1.2 dev: true @@ -4215,12 +5184,12 @@ packages: '@jest/schemas': 29.4.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.1.3 + '@types/node': 20.2.5 '@types/yargs': 17.0.19 chalk: 4.1.2 dev: true - /@joshwooding/vite-plugin-react-docgen-typescript@0.2.1(typescript@5.0.4)(vite@4.3.5): + /@joshwooding/vite-plugin-react-docgen-typescript@0.2.1(typescript@5.1.3)(vite@4.3.9): resolution: {integrity: sha512-ou4ZJSXMMWHqGS4g8uNRbC5TiTWxAgQZiVucoUrOCWuPrTbkpJbmVyIi9jU72SBry7gQtuMEDp4YR8EEXAg7VQ==} peerDependencies: typescript: '>= 4.3.x' @@ -4232,17 +5201,9 @@ packages: glob: 7.2.3 glob-promise: 4.2.2(glob@7.2.3) magic-string: 0.27.0 - react-docgen-typescript: 2.2.2(typescript@5.0.4) - typescript: 5.0.4 - vite: 4.3.5(@types/node@20.1.3)(sass@1.62.1) - dev: true - - /@jridgewell/gen-mapping@0.1.1: - resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 + react-docgen-typescript: 2.2.2(typescript@5.1.3) + typescript: 5.1.3 + vite: 4.3.9(@types/node@20.2.5)(sass@1.62.1) dev: true /@jridgewell/gen-mapping@0.3.2: @@ -4306,7 +5267,7 @@ packages: nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 - semver: 7.5.0 + semver: 7.5.1 tar: 6.1.13 transitivePeerDependencies: - encoding @@ -4381,46 +5342,52 @@ packages: os-filter-obj: 2.0.0 dev: false - /@msgpackr-extract/msgpackr-extract-darwin-arm64@2.2.0: - resolution: {integrity: sha512-Z9LFPzfoJi4mflGWV+rv7o7ZbMU5oAU9VmzCgL240KnqDW65Y2HFCT3MW06/ITJSnbVLacmcEJA8phywK7JinQ==} + /@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2: + resolution: {integrity: sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==} cpu: [arm64] os: [darwin] requiresBuild: true + dev: false optional: true - /@msgpackr-extract/msgpackr-extract-darwin-x64@2.2.0: - resolution: {integrity: sha512-vq0tT8sjZsy4JdSqmadWVw6f66UXqUCabLmUVHZwUFzMgtgoIIQjT4VVRHKvlof3P/dMCkbMJ5hB1oJ9OWHaaw==} + /@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.2: + resolution: {integrity: sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==} cpu: [x64] os: [darwin] requiresBuild: true + dev: false optional: true - /@msgpackr-extract/msgpackr-extract-linux-arm64@2.2.0: - resolution: {integrity: sha512-hlxxLdRmPyq16QCutUtP8Tm6RDWcyaLsRssaHROatgnkOxdleMTgetf9JsdncL8vLh7FVy/RN9i3XR5dnb9cRA==} + /@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.2: + resolution: {integrity: sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==} cpu: [arm64] os: [linux] requiresBuild: true + dev: false optional: true - /@msgpackr-extract/msgpackr-extract-linux-arm@2.2.0: - resolution: {integrity: sha512-SaJ3Qq4lX9Syd2xEo9u3qPxi/OB+5JO/ngJKK97XDpa1C587H9EWYO6KD8995DAjSinWvdHKRrCOXVUC5fvGOg==} + /@msgpackr-extract/msgpackr-extract-linux-arm@3.0.2: + resolution: {integrity: sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==} cpu: [arm] os: [linux] requiresBuild: true + dev: false optional: true - /@msgpackr-extract/msgpackr-extract-linux-x64@2.2.0: - resolution: {integrity: sha512-94y5PJrSOqUNcFKmOl7z319FelCLAE0rz/jPCWS+UtdMZvpa4jrQd+cJPQCLp2Fes1yAW/YUQj/Di6YVT3c3Iw==} + /@msgpackr-extract/msgpackr-extract-linux-x64@3.0.2: + resolution: {integrity: sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==} cpu: [x64] os: [linux] requiresBuild: true + dev: false optional: true - /@msgpackr-extract/msgpackr-extract-win32-x64@2.2.0: - resolution: {integrity: sha512-XrC0JzsqQSvOyM3t04FMLO6z5gCuhPE6k4FXuLK5xf52ZbdvcFe1yBmo7meCew9B8G2f0T9iu9t3kfTYRYROgA==} + /@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2: + resolution: {integrity: sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==} cpu: [x64] os: [win32] requiresBuild: true + dev: false optional: true /@mswjs/cookies@0.2.2: @@ -4455,8 +5422,8 @@ packages: tar-fs: 2.1.1 dev: true - /@nestjs/common@9.4.0(reflect-metadata@0.1.13)(rxjs@7.8.1): - resolution: {integrity: sha512-RUcVAQsEF4WPrmzFXEOUfZnPwrLTe1UVlzXTlSyfqfqbdWDPKDGlIPVelBLfc5/+RRUQ0I5iE4+CQvpCmkqldw==} + /@nestjs/common@9.4.2(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-sea+qZnbD5x3YWZDVQT/wbVJ2NiABaM1tyZTLuW9hpkcM2KFA96xKtK3VaCxyz49zoXIgSOefsyK7HuUMCe27Q==} peerDependencies: cache-manager: <=5 class-transformer: '*' @@ -4474,12 +5441,12 @@ packages: iterare: 1.2.1 reflect-metadata: 0.1.13 rxjs: 7.8.1 - tslib: 2.5.0 + tslib: 2.5.2 uid: 2.0.2 dev: false - /@nestjs/core@9.4.0(@nestjs/common@9.4.0)(reflect-metadata@0.1.13)(rxjs@7.8.1): - resolution: {integrity: sha512-yTLryCgFD0462wPe4HIzhyTcDgibt8Stfwb5YzcX7Ma0NM4m8uBIpcPG109KBubp8ZmV85e5mw4rl20qLQQVsQ==} + /@nestjs/core@9.4.2(@nestjs/common@9.4.2)(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-S5K9GTpjBqEJtu5VxRsVaaGEBZ1bkY+Ht4+2hqZSKsI+rzcEB5hcvR+5KiMsMY1VGYvlZ99lxYz72p4h8B0mKw==} requiresBuild: true peerDependencies: '@nestjs/common': ^9.0.0 @@ -4496,21 +5463,21 @@ packages: '@nestjs/websockets': optional: true dependencies: - '@nestjs/common': 9.4.0(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 9.4.2(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 iterare: 1.2.1 path-to-regexp: 3.2.0 reflect-metadata: 0.1.13 rxjs: 7.8.1 - tslib: 2.5.0 + tslib: 2.5.2 uid: 2.0.2 transitivePeerDependencies: - encoding dev: false - /@nestjs/testing@9.4.0(@nestjs/common@9.4.0)(@nestjs/core@9.4.0): - resolution: {integrity: sha512-xZWp363P4otcebg++gSjUcdCfTK0RorORzyFq3aLaSAQOlq8kxfFDRIKzEATR4aOUfqTMMsAA8lhnMJWf35N6A==} + /@nestjs/testing@9.4.2(@nestjs/common@9.4.2)(@nestjs/core@9.4.2): + resolution: {integrity: sha512-4WZPJz85zLVZkhmWYq+Unr43MixISelg/TyuX1YFZYOeukIN+O6fRtAAPIKLqRQsiY0rE/h8FAEbYGWhNrRfSA==} peerDependencies: '@nestjs/common': ^9.0.0 '@nestjs/core': ^9.0.0 @@ -4522,9 +5489,9 @@ packages: '@nestjs/platform-express': optional: true dependencies: - '@nestjs/common': 9.4.0(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 9.4.0(@nestjs/common@9.4.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - tslib: 2.5.0 + '@nestjs/common': 9.4.2(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 9.4.2(@nestjs/common@9.4.2)(reflect-metadata@0.1.13)(rxjs@7.8.1) + tslib: 2.5.2 dev: false /@nodelib/fs.scandir@2.1.5: @@ -4550,12 +5517,13 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} dependencies: '@gar/promisify': 1.1.3 - semver: 7.5.0 + semver: 7.5.1 dev: false /@npmcli/move-file@2.0.1: resolution: {integrity: sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This functionality has been moved to @npmcli/fs dependencies: mkdirp: 1.0.4 rimraf: 3.0.2 @@ -4653,7 +5621,7 @@ packages: '@redis/client': 1.4.2 dev: true - /@rollup/plugin-alias@5.0.0(rollup@3.21.6): + /@rollup/plugin-alias@5.0.0(rollup@3.23.0): resolution: {integrity: sha512-l9hY5chSCjuFRPsnRm16twWBiSApl2uYFLsepQYwtBuAxNMQ/1dJqADld40P0Jkqm65GRTLy/AC6hnpVebtLsA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -4662,11 +5630,11 @@ packages: rollup: optional: true dependencies: - rollup: 3.21.6 + rollup: 3.23.0 slash: 4.0.0 dev: false - /@rollup/plugin-json@6.0.0(rollup@3.21.6): + /@rollup/plugin-json@6.0.0(rollup@3.23.0): resolution: {integrity: sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==} engines: {node: '>=14.0.0'} peerDependencies: @@ -4675,11 +5643,11 @@ packages: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.2(rollup@3.21.6) - rollup: 3.21.6 + '@rollup/pluginutils': 5.0.2(rollup@3.23.0) + rollup: 3.23.0 dev: false - /@rollup/plugin-replace@5.0.2(rollup@3.21.6): + /@rollup/plugin-replace@5.0.2(rollup@3.23.0): resolution: {integrity: sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -4688,9 +5656,9 @@ packages: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.2(rollup@3.21.6) + '@rollup/pluginutils': 5.0.2(rollup@3.23.0) magic-string: 0.27.0 - rollup: 3.21.6 + rollup: 3.23.0 dev: false /@rollup/pluginutils@4.2.1: @@ -4701,7 +5669,7 @@ packages: picomatch: 2.3.1 dev: true - /@rollup/pluginutils@5.0.2(rollup@3.21.6): + /@rollup/pluginutils@5.0.2(rollup@3.23.0): resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -4713,7 +5681,7 @@ packages: '@types/estree': 1.0.1 estree-walker: 2.0.2 picomatch: 2.3.1 - rollup: 3.21.6 + rollup: 3.23.0 dev: false /@rushstack/node-core-library@3.58.0(@types/node@18.16.3): @@ -4791,11 +5759,17 @@ packages: resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} dependencies: type-detect: 4.0.8 + dev: true - /@sinonjs/fake-timers@10.0.2: - resolution: {integrity: sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==} + /@sinonjs/commons@3.0.0: + resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} dependencies: - '@sinonjs/commons': 2.0.0 + type-detect: 4.0.8 + + /@sinonjs/fake-timers@10.2.0: + resolution: {integrity: sha512-OPwQlEdg40HAj5KNF8WW6q2KG4Z+cBCZb3m4ninfTZKaBmbIJodviQsDBoYMPHkOyJJMHnOJo5j2+LKDOhOACg==} + dependencies: + '@sinonjs/commons': 3.0.0 /@sinonjs/fake-timers@9.1.2: resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==} @@ -4836,8 +5810,8 @@ packages: lodash.values: 4.3.0 object-hash: 3.0.0 packageurl-js: 1.0.1 - semver: 7.5.0 - tslib: 2.5.0 + semver: 7.5.1 + tslib: 2.5.2 dev: false /@snyk/graphlib@2.1.9-patch.3: @@ -4864,8 +5838,8 @@ packages: resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} dev: false - /@storybook/addon-actions@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-U8c7n918/mOjXnc1Iu/sglbK+ryC4xoyjWE5SG/68h0+sHb1rioNq7leAi24mCP6jNwNI5Q7TWtuvflOGxQDKQ==} + /@storybook/addon-actions@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-3M5AU/ZD79YP88vKlFezIJbIoG/II7wCixUBTmwiC3BeQZDuVsqPNl8eiP6MGT70xwyx7a993lSM5f5N5W93vg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -4875,14 +5849,14 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.0.10 - '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.0.10 + '@storybook/client-logger': 7.0.18 + '@storybook/components': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.0.18 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.0.10 - '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.0.10 + '@storybook/manager-api': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.0.18 + '@storybook/theming': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.0.18 dequal: 2.0.3 lodash: 4.17.21 polished: 4.2.2 @@ -4895,8 +5869,8 @@ packages: uuid: 9.0.0 dev: true - /@storybook/addon-backgrounds@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-QtOxXO9hKtwBjjdLXWYKp4HpcpNOrLfc71dn78XbMKyCkQRlYtVe8GNk/++70UQtFfKCEJIB0hTHrPmSjDJE5A==} + /@storybook/addon-backgrounds@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-cPQy1Ot7Urf4hQz+xnF1YKrqSyR0DRwozBmF+sGzceACWmueFl0CifYZC8RSmaiIyVh0RyWPxZ9F/eT67NX2lA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -4906,22 +5880,22 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.0.10 - '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.0.10 + '@storybook/client-logger': 7.0.18 + '@storybook/components': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.0.18 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.0.10 - '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.0.10 + '@storybook/manager-api': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.0.18 + '@storybook/theming': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.0.18 memoizerific: 1.11.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) ts-dedent: 2.2.0 dev: true - /@storybook/addon-controls@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-j5UiPH8ZJx0ieUoIeV3iENlsIRDuQCeg3gTlLD668sebx8KHOCSJygh0Zvg1sTUUGSIbenhWaPlqfaW6ShKFWQ==} + /@storybook/addon-controls@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-mD6DE52CCMKugXk2Uab0QxwgfE76kFJroxASmnePnXUNWfP9EZJpJXYE3cyyBbmZuxa46VHDGGEGXQWRl4+Eog==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -4931,15 +5905,15 @@ packages: react-dom: optional: true dependencies: - '@storybook/blocks': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/client-logger': 7.0.10 - '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-common': 7.0.10 - '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/node-logger': 7.0.10 - '@storybook/preview-api': 7.0.10 - '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.0.10 + '@storybook/blocks': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/client-logger': 7.0.18 + '@storybook/components': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-common': 7.0.18 + '@storybook/manager-api': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/node-logger': 7.0.18 + '@storybook/preview-api': 7.0.18 + '@storybook/theming': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.0.18 lodash: 4.17.21 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -4948,29 +5922,29 @@ packages: - supports-color dev: true - /@storybook/addon-docs@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-1tUsJ+fuBqk4oTOBLabyPQeQYiRKs9I6+soY7dG8jN15Bxe/Ey2giNpqUkA3xAIuqS75ydRVKmsfQvILu2nLjg==} + /@storybook/addon-docs@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-oq+ZN5809gIRdTZQIpeK1F8BJtL1/VWo9rWvl6ymVOL/Xzdgd7AOfKf9Y99X35RcxAGysRIHLGJjF4bgLoY1Aw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/core': 7.21.3 - '@babel/plugin-transform-react-jsx': 7.21.0(@babel/core@7.21.3) + '@babel/core': 7.22.1 + '@babel/plugin-transform-react-jsx': 7.21.0(@babel/core@7.22.1) '@jest/transform': 29.5.0 '@mdx-js/react': 2.3.0(react@18.2.0) - '@storybook/blocks': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/client-logger': 7.0.10 - '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/csf-plugin': 7.0.10 - '@storybook/csf-tools': 7.0.10 + '@storybook/blocks': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/client-logger': 7.0.18 + '@storybook/components': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/csf-plugin': 7.0.18 + '@storybook/csf-tools': 7.0.18 '@storybook/global': 5.0.0 '@storybook/mdx2-csf': 1.0.0 - '@storybook/node-logger': 7.0.10 - '@storybook/postinstall': 7.0.10 - '@storybook/preview-api': 7.0.10 - '@storybook/react-dom-shim': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.0.10 + '@storybook/node-logger': 7.0.18 + '@storybook/postinstall': 7.0.18 + '@storybook/preview-api': 7.0.18 + '@storybook/react-dom-shim': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/theming': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.0.18 fs-extra: 11.1.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -4981,25 +5955,25 @@ packages: - supports-color dev: true - /@storybook/addon-essentials@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-nOeUtNbfLXOlgGqqqlsYC9gcYSrAxABBo8jHYiZg3xaEB9+cnKjCKK8VxrqJiR002AG5JZvi+uHeAauM94fkkQ==} + /@storybook/addon-essentials@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-0XXu7xhtRefA1WxxorKk6BWeeB+7gQ+r2+bG1zQEfBgDYPR06YbPw4H79IZ8JiR97aJRsZBK5UUhOZMDrc5zcQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/addon-actions': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-backgrounds': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-controls': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-docs': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-highlight': 7.0.10 - '@storybook/addon-measure': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-outline': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-toolbars': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-viewport': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-common': 7.0.10 - '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/node-logger': 7.0.10 - '@storybook/preview-api': 7.0.10 + '@storybook/addon-actions': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-backgrounds': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-controls': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-docs': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-highlight': 7.0.18 + '@storybook/addon-measure': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-outline': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-toolbars': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-viewport': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-common': 7.0.18 + '@storybook/manager-api': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/node-logger': 7.0.18 + '@storybook/preview-api': 7.0.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) ts-dedent: 2.2.0 @@ -5007,16 +5981,16 @@ packages: - supports-color dev: true - /@storybook/addon-highlight@7.0.10: - resolution: {integrity: sha512-TohDxElSu7JrSvhLRZAwtNk/7Ot626wvlODwklocE4kbtn1fulFoUlRta7NImBGX554LITDFRy0m4R1rRQ9OfQ==} + /@storybook/addon-highlight@7.0.18: + resolution: {integrity: sha512-a3nfUhbu6whoDclIZSV/fzLj132tNNjV05ENTpuN3JpLoMd3+obDUWzeQUs9TetK4RBRN3ewM7sIMEI4oBpgmg==} dependencies: - '@storybook/core-events': 7.0.10 + '@storybook/core-events': 7.0.18 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.0.10 + '@storybook/preview-api': 7.0.18 dev: true - /@storybook/addon-interactions@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-7hdFgoetQblwysYwRlmC5fbMVDb6lIM6le1pVEmRci6X44Gr2Xe5w2s6h5bTp4tMpNS1CFKjru9kF/TqfK46wA==} + /@storybook/addon-interactions@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-V3OD5lSj6Te6Kzc//2k2S79dLPk6Zu1pAbqWAN4RrdXyKj6YCiZ666GmVdiaG+24Qp5UuMeAkd1D05osJlOteA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5026,16 +6000,16 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.0.10 - '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-common': 7.0.10 - '@storybook/core-events': 7.0.10 + '@storybook/client-logger': 7.0.18 + '@storybook/components': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-common': 7.0.18 + '@storybook/core-events': 7.0.18 '@storybook/global': 5.0.0 - '@storybook/instrumenter': 7.0.10 - '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.0.10 - '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.0.10 + '@storybook/instrumenter': 7.0.18 + '@storybook/manager-api': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.0.18 + '@storybook/theming': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.0.18 jest-mock: 27.5.1 polished: 4.2.2 react: 18.2.0 @@ -5045,8 +6019,8 @@ packages: - supports-color dev: true - /@storybook/addon-links@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Odhe0eICqW9X2yyIjtOVb23cKXJ2WRxPHBm5oYf6hBBoXXK7EJicwyQSJLxJyHK7r1PeAnFxSGlNrO3w7JULjg==} + /@storybook/addon-links@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-xEwflt7bp9FRoZVeqPGb6d3s2Gh+/jaSmnyIxMxrBy2oovKIqu9ptolqz1AhjFOXfaLs9c2RAmJUuFZJtETLxA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5056,22 +6030,22 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.0.10 - '@storybook/core-events': 7.0.10 + '@storybook/client-logger': 7.0.18 + '@storybook/core-events': 7.0.18 '@storybook/csf': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.0.10 - '@storybook/router': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.0.10 + '@storybook/manager-api': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.0.18 + '@storybook/router': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.0.18 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) ts-dedent: 2.2.0 dev: true - /@storybook/addon-measure@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-70BQT8PM6r3qjXDgXuN5mx9CBq9dYTdEgR1tlZ8FbMi8B8tB1oZJD0o6tfGM3r8WjdI0sTwX70ic5pv9Ma/MiA==} + /@storybook/addon-measure@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-iu8vQpGOA+CFYbWR6QNshj20o33OQ/xcTbp5P4U6xGYDUliUBbwJ2KLxcKlmIeBanBrBdz0jPFtHwY4dM1ZdKw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5081,19 +6055,19 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.0.10 - '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.0.10 + '@storybook/client-logger': 7.0.18 + '@storybook/components': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.0.18 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.0.10 - '@storybook/types': 7.0.10 + '@storybook/manager-api': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.0.18 + '@storybook/types': 7.0.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/addon-outline@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Aakoc+II7orfgUDmjgMbnSp5HZS/47z0NeRAfh+FP4fxL0lFd9vmaeIXWYo1DjJqdEFfvlSLd8aS9Ltb+souMw==} + /@storybook/addon-outline@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-3vNWO7ezo6GIvidbz8JxFrKtfVEoTQN7tnZx+wpqmCF8ihBORewkpeMUnvgb9ZKjD0X7gE8eQvvG8KKWcyHDBQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5103,20 +6077,20 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.0.10 - '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.0.10 + '@storybook/client-logger': 7.0.18 + '@storybook/components': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.0.18 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.0.10 - '@storybook/types': 7.0.10 + '@storybook/manager-api': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.0.18 + '@storybook/types': 7.0.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) ts-dedent: 2.2.0 dev: true - /@storybook/addon-storysource@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-5anwqnBOcHDI/EB3F2q3Vs/JN+vCBRr8UVqnKS8NqN3BrpJ4q7jUeQ2cA0Q2/aAmdHJn9FLh/Cgx7aTO+6iC2w==} + /@storybook/addon-storysource@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ejOO9d9Aa63DCXCoXtCsOJLefdbrsvSAEV9wU2HfT+EOIS1dq/SV+ZtIMAvdAf4whB42K+pEzB5hLE2+zCK9PQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5126,13 +6100,13 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.0.10 - '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.0.10 - '@storybook/router': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/source-loader': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0) + '@storybook/client-logger': 7.0.18 + '@storybook/components': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/manager-api': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.0.18 + '@storybook/router': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/source-loader': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/theming': 7.0.18(react-dom@18.2.0)(react@18.2.0) estraverse: 5.3.0 prop-types: 15.8.1 react: 18.2.0 @@ -5140,8 +6114,8 @@ packages: react-syntax-highlighter: 15.5.0(react@18.2.0) dev: true - /@storybook/addon-toolbars@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-U4a45CDw4SZzrgboYVMgxyiD7Ejud1kSz2lyS+J3fGTZGXq2+tmJS/2oNrLJlSH7v8629lVUbKnFxsP0HbfShg==} + /@storybook/addon-toolbars@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-mwhq962o0WloHAeFjJ6BXO2nzdTo5KE2fqawPpqcB2lwXP6tvaA2tDWwgntjPCHejqWTS+ZTdO4/1xrMhWYt/g==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5151,17 +6125,17 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.0.10 - '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.0.10 - '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0) + '@storybook/client-logger': 7.0.18 + '@storybook/components': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/manager-api': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.0.18 + '@storybook/theming': 7.0.18(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/addon-viewport@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Ck9sdCg3T8ChXoxYL5IEi+ZUOwdH6Je5XeK4kRVq+Ar+Ytm5CFTGJCCZjI6biroTnuJCUefaV2K5NUZoHkZI+A==} + /@storybook/addon-viewport@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-aVVLBsWXfGDX3z1pc93LWWdG5RUoJbGL/JJPMZGwXdwWpP8V3OBl8D8bgPymyg+MgwhSRZZDDGgnJaVGGwZ6bQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5171,49 +6145,49 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.0.10 - '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.0.10 + '@storybook/client-logger': 7.0.18 + '@storybook/components': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.0.18 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.0.10 - '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0) + '@storybook/manager-api': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.0.18 + '@storybook/theming': 7.0.18(react-dom@18.2.0)(react@18.2.0) memoizerific: 1.11.3 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/addons@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-RRtozbB0JovZdLgTgC03kOjNh/5sAN77VHZFC5aK/Y9Hz2A0C6V4w/SqTt0382skSllcGMcrHjB1k06BlxlZ8A==} + /@storybook/addons@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-+j9ItxWoVzarbllaV4WRaJpDM3P2aC5O6F3cPn4YkG/unb6HOs11WLAqFbzZnLYZNAFvWS8PYEAtqs1BxG66YQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.0.10 - '@storybook/types': 7.0.10 + '@storybook/manager-api': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.0.18 + '@storybook/types': 7.0.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/blocks@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-OqXuN1x2TjXgrOqGSaD0Vz8iCqmLjiPkrQpWMD7bToFpHH0dpmcrzzRhLhxgJLN2CAzyr98IYIkUgXX9Da1neA==} + /@storybook/blocks@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HLsuzmUdVIeFXEP5v5vyjnEePRNYjzltwTjCKQhHAlt8/aQZmREiIMOfoMoAa1Rd+On8Ib2DUd2cN10VS18H8A==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/channels': 7.0.10 - '@storybook/client-logger': 7.0.10 - '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.0.10 + '@storybook/channels': 7.0.18 + '@storybook/client-logger': 7.0.18 + '@storybook/components': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.0.18 '@storybook/csf': 0.1.0 - '@storybook/docs-tools': 7.0.10 + '@storybook/docs-tools': 7.0.18 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.0.10 - '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.0.10 + '@storybook/manager-api': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.0.18 + '@storybook/theming': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.0.18 '@types/lodash': 4.14.191 color-convert: 2.0.1 dequal: 2.0.3 @@ -5231,13 +6205,13 @@ packages: - supports-color dev: true - /@storybook/builder-manager@7.0.10: - resolution: {integrity: sha512-izCVE4JEbDVN5DPkX/Ym1PifAJKlheBvXKmGXGklnJQ2l+TEuvesPbOmVFNuu7ptJAFw4JO5n2KAo9+a5FRwiw==} + /@storybook/builder-manager@7.0.18: + resolution: {integrity: sha512-yFMm3xuYkyg2hS1uz3CkvyvLzK7qJsDPVEh7lew8GiJK1Xx8cc+FnAOlRTjWNxvhfiT296wAMCTPWv7LeoSgqQ==} dependencies: '@fal-works/esbuild-plugin-global-externals': 2.1.2 - '@storybook/core-common': 7.0.10 - '@storybook/manager': 7.0.10 - '@storybook/node-logger': 7.0.10 + '@storybook/core-common': 7.0.18 + '@storybook/manager': 7.0.18 + '@storybook/node-logger': 7.0.18 '@types/ejs': 3.1.2 '@types/find-cache-dir': 3.2.1 '@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.17.18) @@ -5254,8 +6228,8 @@ packages: - supports-color dev: true - /@storybook/builder-vite@7.0.10(typescript@5.0.4)(vite@4.3.5): - resolution: {integrity: sha512-tKY2QnHni10TE3+Sy2wfR7h4FuribR849VBpDI/LcwtRkCgoOBfMCdEnAKMWyU6qAlY+9KDSOQq9SDTu3WZGOg==} + /@storybook/builder-vite@7.0.18(typescript@5.1.3)(vite@4.3.9): + resolution: {integrity: sha512-Qze6/PwUJq+z776dBoG5uinAEVZyPalZIaU/VOWpTrN8L9FQbL0+HDrZU2E/BMNW+ZfnSjF3V2USLyiutsC1Tw==} peerDependencies: '@preact/preset-vite': '*' typescript: '>= 4.3.x' @@ -5269,16 +6243,16 @@ packages: vite-plugin-glimmerx: optional: true dependencies: - '@storybook/channel-postmessage': 7.0.10 - '@storybook/channel-websocket': 7.0.10 - '@storybook/client-logger': 7.0.10 - '@storybook/core-common': 7.0.10 - '@storybook/csf-plugin': 7.0.10 + '@storybook/channel-postmessage': 7.0.18 + '@storybook/channel-websocket': 7.0.18 + '@storybook/client-logger': 7.0.18 + '@storybook/core-common': 7.0.18 + '@storybook/csf-plugin': 7.0.18 '@storybook/mdx2-csf': 1.0.0 - '@storybook/node-logger': 7.0.10 - '@storybook/preview': 7.0.10 - '@storybook/preview-api': 7.0.10 - '@storybook/types': 7.0.10 + '@storybook/node-logger': 7.0.18 + '@storybook/preview': 7.0.18 + '@storybook/preview-api': 7.0.18 + '@storybook/types': 7.0.18 browser-assert: 1.2.1 es-module-lexer: 0.9.3 express: 4.18.2 @@ -5288,19 +6262,19 @@ packages: magic-string: 0.27.0 remark-external-links: 8.0.0 remark-slug: 6.1.0 - rollup: 3.21.6 - typescript: 5.0.4 - vite: 4.3.5(@types/node@20.1.3)(sass@1.62.1) + rollup: 3.23.0 + typescript: 5.1.3 + vite: 4.3.9(@types/node@20.2.5)(sass@1.62.1) transitivePeerDependencies: - supports-color dev: true - /@storybook/channel-postmessage@7.0.10: - resolution: {integrity: sha512-Y5ZSp9WYH3HznQ2rrGN78Y5fYM16Bfvyn3iKy5QD38gsQk1gTrra1osIZ0X+lk3sep14D4oW4QMW3DCSrn0Big==} + /@storybook/channel-postmessage@7.0.18: + resolution: {integrity: sha512-rpwBH5ANdPnugS6+7xG9qHSoS+aPSEnBxDKsONWFubfMTTXQuFkf/793rBbxGkoINdqh8kSdKOM2rIty6e9cmQ==} dependencies: - '@storybook/channels': 7.0.10 - '@storybook/client-logger': 7.0.10 - '@storybook/core-events': 7.0.10 + '@storybook/channels': 7.0.18 + '@storybook/client-logger': 7.0.18 + '@storybook/core-events': 7.0.18 '@storybook/global': 5.0.0 qs: 6.11.1 telejson: 7.0.4 @@ -5328,18 +6302,17 @@ packages: telejson: 7.0.4 dev: true - /@storybook/channel-websocket@7.0.10: - resolution: {integrity: sha512-WXueykS71YxEqKlsIbbmmA6QSChEePffzqs7QASUpHhi21IDqmtq2HhAqYWlQptyqSWYdv6wxrOqwe6z4zakLA==} + /@storybook/channel-websocket@7.0.18: + resolution: {integrity: sha512-QYsZIfe23NN4i+oIdPKHaYBehk3a/HYk57a+M2oR3Frmv8IOqc/e31uH+xx5NxnjHrTJj7Y80ZJw6EKB682S6w==} dependencies: - '@storybook/channels': 7.0.10 - '@storybook/client-logger': 7.0.10 + '@storybook/channels': 7.0.18 + '@storybook/client-logger': 7.0.18 '@storybook/global': 5.0.0 telejson: 7.0.4 dev: true - /@storybook/channels@7.0.10: - resolution: {integrity: sha512-hdPaGV3W7s6MkVcg33S5hmkVgqXC16AA7H0ID9MROiU1lQzolKhSqCs2iVfGPQfmWzEJeqWQoEXU7dmCclRM0w==} - dev: true + /@storybook/channels@7.0.18: + resolution: {integrity: sha512-rkA7ea0M3+dWS+71iHJdiZ5R2QuIdiVg0CgyLJHDagc1qej7pEVNhMWtppeq+X5Pwp9nkz8ZTQ7aCjTf6th0/A==} /@storybook/channels@7.0.2: resolution: {integrity: sha512-qkI8mFy9c8mxN2f01etayKhCaauL6RAsxRzbX1/pKj6UqhHWqqUbtHwymrv4hG5qDYjV1e9pd7ae5eNF8Kui0g==} @@ -5349,20 +6322,20 @@ packages: resolution: {integrity: sha512-+34cVmrXZ3lb1s5tDK+OWd5HLtEPSUMas0VKFJ0k9LBpFlVl9aiCZBJRvSYmWL7beauUfa+HSmJgjlD6228ChQ==} dev: true - /@storybook/cli@7.0.10: - resolution: {integrity: sha512-FhtE6Yrk7MMa9AgLb3MTmqiQ3IlWHjjrj7Vcj2QM6BcP342xSe7C1d+V6+tYX/oPOEB3Upz+PKNrju1iHxurQQ==} + /@storybook/cli@7.0.18: + resolution: {integrity: sha512-9n4J4thiCUsGSXiRc6ZysqYUaCMCrpu0/qgC+5ngfFRuMmZgUV0y5+0fmaOhT2XjsonTTgucizO82i7+ottCVg==} hasBin: true dependencies: - '@babel/core': 7.21.3 - '@babel/preset-env': 7.21.4(@babel/core@7.21.3) + '@babel/core': 7.22.1 + '@babel/preset-env': 7.21.4(@babel/core@7.22.1) '@ndelangen/get-tarball': 3.0.7 - '@storybook/codemod': 7.0.10 - '@storybook/core-common': 7.0.10 - '@storybook/core-server': 7.0.10 - '@storybook/csf-tools': 7.0.10 - '@storybook/node-logger': 7.0.10 - '@storybook/telemetry': 7.0.10 - '@storybook/types': 7.0.10 + '@storybook/codemod': 7.0.18 + '@storybook/core-common': 7.0.18 + '@storybook/core-server': 7.0.18 + '@storybook/csf-tools': 7.0.18 + '@storybook/node-logger': 7.0.18 + '@storybook/telemetry': 7.0.18 + '@storybook/types': 7.0.18 '@types/semver': 7.5.0 boxen: 5.1.2 chalk: 4.1.2 @@ -5380,11 +6353,12 @@ packages: globby: 11.1.0 jscodeshift: 0.14.0(@babel/preset-env@7.21.4) leven: 3.1.0 + ora: 5.4.1 prettier: 2.8.8 prompts: 2.4.2 puppeteer-core: 2.1.1 read-pkg-up: 7.0.1 - semver: 7.5.0 + semver: 7.5.1 shelljs: 0.8.5 simple-update-notifier: 1.1.0 strip-json-comments: 3.1.1 @@ -5398,8 +6372,8 @@ packages: - utf-8-validate dev: true - /@storybook/client-logger@7.0.10: - resolution: {integrity: sha512-hb8tO+w28ErzjEw69ERMtZT81Xyg835FQjH6Y42ejoGcBA9Z0W6RZmx4RgkcIUOlYXkU6lSnNVne9gXodV4/Hw==} + /@storybook/client-logger@7.0.18: + resolution: {integrity: sha512-uKgFdVedYoRDZBVrE1IBdWNHDFln1IxWEeI+7ZiNSQwREG9swHpU5Fa8DceclM/oLjJRuzG1jFzv+XZY8894+Q==} dependencies: '@storybook/global': 5.0.0 dev: true @@ -5416,16 +6390,16 @@ packages: '@storybook/global': 5.0.0 dev: true - /@storybook/codemod@7.0.10: - resolution: {integrity: sha512-BnPknLV3wnaSk0azjFBAWLVfwgUHtFvVk9I6y1idIaQhc0nnegKoa0jTxWigthftZK/Pv9yG3gxG7o7O4KcChQ==} + /@storybook/codemod@7.0.18: + resolution: {integrity: sha512-+9XFns29e8FpPLsqA8ZCQ3mNnIIKD3QnqGYkbkCVKi/G1fomvVQsIfsnkrYv5SobTbz29B4aNWxAaeSnO7/OGg==} dependencies: '@babel/core': 7.21.3 '@babel/preset-env': 7.21.4(@babel/core@7.21.3) '@babel/types': 7.21.5 '@storybook/csf': 0.1.0 - '@storybook/csf-tools': 7.0.10 - '@storybook/node-logger': 7.0.10 - '@storybook/types': 7.0.10 + '@storybook/csf-tools': 7.0.18 + '@storybook/node-logger': 7.0.18 + '@storybook/types': 7.0.18 cross-spawn: 7.0.3 globby: 11.1.0 jscodeshift: 0.14.0(@babel/preset-env@7.21.4) @@ -5436,17 +6410,17 @@ packages: - supports-color dev: true - /@storybook/components@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-jdGiVP+a3XqoGpKkDFGt4g2cgb23aLfMS/RhnuhT7FK6hGh7WFfuuqx4QqQHx4VZCdXIWVIzszaCdGCs7AsW2w==} + /@storybook/components@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Jn1CbF9UAKt8BVaZtuhmthpcZ02VMaCFXR0ISfDXCpiMKnylmpP0+WfXcoKLzz6yS+EW8EW5S9+Qq8xgQY8H7A==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/client-logger': 7.0.10 + '@storybook/client-logger': 7.0.18 '@storybook/csf': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.0.10 + '@storybook/theming': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.0.18 memoizerific: 1.11.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -5454,18 +6428,18 @@ packages: util-deprecate: 1.0.2 dev: true - /@storybook/core-client@7.0.10: - resolution: {integrity: sha512-sN/TKB7QHWP6josdjyNtoqDXihROPtgvzo5+akfW6+S7hhfsQ4BJd09nkBqEX9E7z81blmFFDUOU3a8bQbPdKQ==} + /@storybook/core-client@7.0.18: + resolution: {integrity: sha512-ueExRZx6fd9LRssgdhDJ0bL4Ir2RrbXzJz/kjIT2KgYY3l7jkhe0dpT3bOgGKjQt0f7XMFU24t/r7aDLGMB+2Q==} dependencies: - '@storybook/client-logger': 7.0.10 - '@storybook/preview-api': 7.0.10 + '@storybook/client-logger': 7.0.18 + '@storybook/preview-api': 7.0.18 dev: true - /@storybook/core-common@7.0.10: - resolution: {integrity: sha512-AAYXixukGlpMy8XoSM8cTfcyQ6ijBq5q50xNTj/ssTbGnGSk6POgtoJZf6g8XtS0OxsFXBSxuBuMBBBbKtoztw==} + /@storybook/core-common@7.0.18: + resolution: {integrity: sha512-HZAB1NIK/Yv0x9poyzqYcue2tx39+MAF1mbHgGy+JJZRerO2fRShgo8f8VPH9ChbFCoJ7isL5wNhgGdg9kp2kA==} dependencies: - '@storybook/node-logger': 7.0.10 - '@storybook/types': 7.0.10 + '@storybook/node-logger': 7.0.18 + '@storybook/types': 7.0.18 '@types/node': 16.18.16 '@types/pretty-hrtime': 1.0.1 chalk: 4.1.2 @@ -5487,8 +6461,8 @@ packages: - supports-color dev: true - /@storybook/core-events@7.0.10: - resolution: {integrity: sha512-OyBqhxVQOdI78Vgv6nKwXOdIVNChyfktpdxQZP1rz9MpO6MrqMaGAUL7k8xQMQAVx0VY+dAMYZB3bnyN2IC8FA==} + /@storybook/core-events@7.0.18: + resolution: {integrity: sha512-7gxHBQDezdKOeq/u1LL80Bwjfcwsv7XOS3yWQElcgqp+gLaYB6OwwgtkCB2yV6a6l4nep9IdPWE8G3TxIzn9xw==} dev: true /@storybook/core-events@7.0.2: @@ -5499,23 +6473,23 @@ packages: resolution: {integrity: sha512-kGrtjlYtjd4iTVk+Phb4CymZaVkB+MGscKAgcO8gfgJ/Q/gq8HQLVZSIzeoCDcDSHOGlBzbg2WVtdHIHhCKlOQ==} dev: true - /@storybook/core-server@7.0.10: - resolution: {integrity: sha512-KFCc3turPed8tiC5IUKTV7oObVmFckMP1XqO7zec2g2NlGQsN83DRso+BA1wpV/bb8AD1NJDU6LJnyN3KKdi1Q==} + /@storybook/core-server@7.0.18: + resolution: {integrity: sha512-zGSGYSoCaSXM28OYKW7zsmpo8VU1icubXLRgdF21fbMhFN1WVS+bPA5+gSkAMf8acq5RNM8uSKskh7E2YDVEqA==} dependencies: '@aw-web-design/x-default-browser': 1.4.88 '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-manager': 7.0.10 - '@storybook/core-common': 7.0.10 - '@storybook/core-events': 7.0.10 + '@storybook/builder-manager': 7.0.18 + '@storybook/core-common': 7.0.18 + '@storybook/core-events': 7.0.18 '@storybook/csf': 0.1.0 - '@storybook/csf-tools': 7.0.10 + '@storybook/csf-tools': 7.0.18 '@storybook/docs-mdx': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/manager': 7.0.10 - '@storybook/node-logger': 7.0.10 - '@storybook/preview-api': 7.0.10 - '@storybook/telemetry': 7.0.10 - '@storybook/types': 7.0.10 + '@storybook/manager': 7.0.18 + '@storybook/node-logger': 7.0.18 + '@storybook/preview-api': 7.0.18 + '@storybook/telemetry': 7.0.18 + '@storybook/types': 7.0.18 '@types/detect-port': 1.3.2 '@types/node': 16.18.16 '@types/node-fetch': 2.6.2 @@ -5532,18 +6506,18 @@ packages: globby: 11.1.0 ip: 2.0.0 lodash: 4.17.21 - node-fetch: 2.6.7 + node-fetch: 2.6.11 open: 8.4.2 pretty-hrtime: 1.0.3 prompts: 2.4.2 read-pkg-up: 7.0.1 - semver: 7.5.0 + semver: 7.5.1 serve-favicon: 2.5.0 telejson: 7.0.4 ts-dedent: 2.2.0 util-deprecate: 1.0.2 watchpack: 2.4.0 - ws: 8.13.0 + ws: 8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) transitivePeerDependencies: - bufferutil - encoding @@ -5551,48 +6525,46 @@ packages: - utf-8-validate dev: true - /@storybook/csf-plugin@7.0.10: - resolution: {integrity: sha512-uUty5rLs6O32tJaXIne2/42UxFL3eaRCDgtAoAkGxbUPa/FLYpO0rYtqF2OG9MagwXU7+As5RlLkDLeYAvUzlQ==} + /@storybook/csf-plugin@7.0.18: + resolution: {integrity: sha512-Cr/Qr4/H4JIYgbbmDjQIYuqjp6nOaZga73R3KZcuClk27B90sI2ADegMYvORgbFgSkwweNQjgak6hLoOyogAhw==} dependencies: - '@storybook/csf-tools': 7.0.10 + '@storybook/csf-tools': 7.0.18 unplugin: 0.10.2 transitivePeerDependencies: - supports-color dev: true - /@storybook/csf-tools@7.0.10: - resolution: {integrity: sha512-sl/995jq03HD7/Q9cb54h0glgt7JLGTkfikSlB35NGMEkgEXEswDmpQHA/TbzUYylIxuAwTKghwMqL3IwSSHwA==} + /@storybook/csf-tools@7.0.18: + resolution: {integrity: sha512-0IJ2qdrxleTl67FUzsEvGcy96CY0OKyERE33tAsLNbvWcabdJKpLHP+rJwbsCw4z6IlS+kkmEffeFf5qRPTwkQ==} dependencies: '@babel/generator': 7.21.3 - '@babel/parser': 7.21.8 + '@babel/parser': 7.21.9 '@babel/traverse': 7.21.3 '@babel/types': 7.21.5 '@storybook/csf': 0.1.0 - '@storybook/types': 7.0.10 + '@storybook/types': 7.0.18 fs-extra: 11.1.0 recast: 0.23.1 ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color - dev: true /@storybook/csf@0.1.0: resolution: {integrity: sha512-uk+jMXCZ8t38jSTHk2o5btI+aV2Ksbvl6DoOv3r6VaCM1KZqeuMwtwywIQdflkA8/6q/dKT8z8L+g8hC4GC3VQ==} dependencies: type-fest: 2.19.0 - dev: true /@storybook/docs-mdx@0.1.0: resolution: {integrity: sha512-JDaBR9lwVY4eSH5W8EGHrhODjygPd6QImRbwjAuJNEnY0Vw4ie3bPkeGfnacB3OBW6u/agqPv2aRlR46JcAQLg==} dev: true - /@storybook/docs-tools@7.0.10: - resolution: {integrity: sha512-w3m7+LlQGI50i07XjiOzIfoap8rnmsrs8hXGUTodbs9vvLt8HBdUaapOGnYr/1BzA0YQJ7Nz2z1nTirQEphmsQ==} + /@storybook/docs-tools@7.0.18: + resolution: {integrity: sha512-H95dW2DquGQ75ZVrFjvznPdCxT0eW6esDnemzLJB61KitcYZrWRavfrZzFtUcpzIa84OgY5pllFYt636v11LHQ==} dependencies: - '@babel/core': 7.21.3 - '@storybook/core-common': 7.0.10 - '@storybook/preview-api': 7.0.10 - '@storybook/types': 7.0.10 + '@babel/core': 7.22.1 + '@storybook/core-common': 7.0.18 + '@storybook/preview-api': 7.0.18 + '@storybook/types': 7.0.18 '@types/doctrine': 0.0.3 doctrine: 3.0.0 lodash: 4.17.21 @@ -5603,21 +6575,21 @@ packages: /@storybook/expect@27.5.2-0: resolution: {integrity: sha512-cP99mhWN/JeCp7VSIiymvj5tmuMY050iFohvp8Zq+kewKsBSZ6/qpTJAGCCZk6pneTcp4S0Fm5BSqyxzbyJ3gw==} dependencies: - '@types/jest': 29.5.1 + '@types/jest': 29.5.2 dev: true /@storybook/global@5.0.0: resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} dev: true - /@storybook/instrumenter@7.0.10: - resolution: {integrity: sha512-Z+kIidnxaq3tneUnIKB2d0DCqb4lwUdOS/AC43LNvd9C6BWYgj89cIPdLDTNhOWa0ZiEju7wTS+K/3uMvcHZ4w==} + /@storybook/instrumenter@7.0.18: + resolution: {integrity: sha512-fyQxeuVC0H+w3oyTuByE95xnAQ+l/WhUBVkHV2X+PWjg9vg9Y9JmrbNWynlvz5HLFlsY3qAWJh+ciVRVSvY5Jw==} dependencies: - '@storybook/channels': 7.0.10 - '@storybook/client-logger': 7.0.10 - '@storybook/core-events': 7.0.10 + '@storybook/channels': 7.0.18 + '@storybook/client-logger': 7.0.18 + '@storybook/core-events': 7.0.18 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.0.10 + '@storybook/preview-api': 7.0.18 dev: true /@storybook/instrumenter@7.0.2: @@ -5649,41 +6621,41 @@ packages: jest-mock: 27.5.1 dev: true - /@storybook/manager-api@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Dik73GKUX9QCFOvukTXjZoZX0G6n/LrRMkwLggb28E9m8iFt2ivWvF9MVvyRoDffR9VP5t53+nV5fqxqpXWoQw==} + /@storybook/manager-api@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-anQkm09twL96YkKGXHa+LI0+yMaY6Jxs1lRaetHdMlIqN4VHBHhizHaMgtGfH6xCTuO3WdrKTN7cZii5RH7PBQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/channels': 7.0.10 - '@storybook/client-logger': 7.0.10 - '@storybook/core-events': 7.0.10 + '@storybook/channels': 7.0.18 + '@storybook/client-logger': 7.0.18 + '@storybook/core-events': 7.0.18 '@storybook/csf': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/router': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.0.10 + '@storybook/router': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/theming': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.0.18 dequal: 2.0.3 lodash: 4.17.21 memoizerific: 1.11.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - semver: 7.5.0 + semver: 7.5.1 store2: 2.14.2 telejson: 7.0.4 ts-dedent: 2.2.0 dev: true - /@storybook/manager@7.0.10: - resolution: {integrity: sha512-cFMOOXmcRx1tN50TqC2huOsF91fAvNM82wTDnAbT2FtA+ZHFHNyE1PgWgiKDDepzOpKaG+FfT4bJcQAaAfYOBg==} + /@storybook/manager@7.0.18: + resolution: {integrity: sha512-hasb8XDmkT9lyX2cwb3Xg0ngcNQ1QCNHKurl2YJtXowb1CvawGKokhnVUTso15NCnurolDyw/Wqka1sagfm+Mg==} dev: true /@storybook/mdx2-csf@1.0.0: resolution: {integrity: sha512-dBAnEL4HfxxJmv7LdEYUoZlQbWj9APZNIbOaq0tgF8XkxiIbzqvgB0jhL/9UOrysSDbQWBiCRTu2wOVxedGfmw==} dev: true - /@storybook/node-logger@7.0.10: - resolution: {integrity: sha512-btCCreucTApi7EP84jbfqlFQZDD4Kz9lFLftalZA7nskDZW6i8reNNykTU2Y22TQvlbpqs5kL1N1cEsbG3vepw==} + /@storybook/node-logger@7.0.18: + resolution: {integrity: sha512-cIeKEBvELtoVP/5UeQ01GJWZ7wM69/9Q+R5uOtNQBlwWFcCD6AVFWMRqq7ObMvdJG/okhXSF+sDetb+BF3zvdw==} dependencies: '@types/npmlog': 4.1.4 chalk: 4.1.2 @@ -5691,20 +6663,20 @@ packages: pretty-hrtime: 1.0.3 dev: true - /@storybook/postinstall@7.0.10: - resolution: {integrity: sha512-SVPKGuuvfn1MceLWzYHGbpP77+waLKXglAH4Gkdoa2mKdk3XO45Zn8OhwwNzHuP698boMNaGaB/utBLBpkXMMg==} + /@storybook/postinstall@7.0.18: + resolution: {integrity: sha512-ObIwAK2UiYhXN/7UifISQgBoH5jnyxh6T8kvCw83YhC78SDOPNgIGjToJECizJ7iubtqAWtCfCT5TrGEpyLGbg==} dev: true - /@storybook/preview-api@7.0.10: - resolution: {integrity: sha512-URj2YJKbs8hc6JZQ3aA+MmjB4hTSzGZAVFVs3kLUEuaQPDbU1RT5GKxedwF5zlMnkZQPNoaUtopN3z7aF+SKFQ==} + /@storybook/preview-api@7.0.18: + resolution: {integrity: sha512-xxtC0gPGMn/DbwvS4ZuJaBwfFNsjUCf0yLYHFrNe6fxncbvcLZ550RuyUwYuIRfsiKrlgfa3QmmCa4JM/JesHQ==} dependencies: - '@storybook/channel-postmessage': 7.0.10 - '@storybook/channels': 7.0.10 - '@storybook/client-logger': 7.0.10 - '@storybook/core-events': 7.0.10 + '@storybook/channel-postmessage': 7.0.18 + '@storybook/channels': 7.0.18 + '@storybook/client-logger': 7.0.18 + '@storybook/core-events': 7.0.18 '@storybook/csf': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/types': 7.0.10 + '@storybook/types': 7.0.18 '@types/qs': 6.9.7 dequal: 2.0.3 lodash: 4.17.21 @@ -5755,12 +6727,12 @@ packages: util-deprecate: 1.0.2 dev: true - /@storybook/preview@7.0.10: - resolution: {integrity: sha512-IQX8v7OpKeo2Oqeyxo6/uSRys+dJ7zms12jViJWGzx9fg6IchV/iNtf4TBrF3Z2JBNKovk03kICAMHTpZuz9Qg==} + /@storybook/preview@7.0.18: + resolution: {integrity: sha512-L53p2eo8G12U6tp7hD3mk5tdWFXLvdEyV9e7a1x9bw1LfH15K/bp8lO6U/W1kkpse7+rqWBqoTjJC1Ktm5Sxog==} dev: true - /@storybook/react-dom-shim@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-NLuE2Be/BGmXHufwLp1Gje+IsTb0HWvwzHlci2U430WgwGU8fsTPNgALMrwCpqN9o1KnrRGpysQEoyIYStQBdg==} + /@storybook/react-dom-shim@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-O1FRypR8q1katjbznnxI+NtALd2gaWa7KnTwbIDf+ddZltXHMZ8xMiEGEtAMrfXlIuqIr9UvmLRfKZC/ysuA+g==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5769,25 +6741,25 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/react-vite@7.0.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4)(vite@4.3.5): - resolution: {integrity: sha512-ZEwRpMEJAQtMruG/XGha52XHkb3extXudWT5SoXOcfiRy9eK7Y3oJwHR8KHNH3LE+LrRh7c+D53k7eMudRtsNA==} + /@storybook/react-vite@7.0.18(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(vite@4.3.9): + resolution: {integrity: sha512-rxJwp/b0dPazn15xLIeRgwrdZGWmoqoLhU7Mm+AXKToXvbe77i2bjHhkFbz34dpKFtD0i/ajcZSpmsxpxfB0HA==} engines: {node: '>=16'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 vite: ^3.0.0 || ^4.0.0 dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.2.1(typescript@5.0.4)(vite@4.3.5) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.2.1(typescript@5.1.3)(vite@4.3.9) '@rollup/pluginutils': 4.2.1 - '@storybook/builder-vite': 7.0.10(typescript@5.0.4)(vite@4.3.5) - '@storybook/react': 7.0.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4) - '@vitejs/plugin-react': 3.1.0(vite@4.3.5) + '@storybook/builder-vite': 7.0.18(typescript@5.1.3)(vite@4.3.9) + '@storybook/react': 7.0.18(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3) + '@vitejs/plugin-react': 3.1.0(vite@4.3.9) ast-types: 0.14.2 magic-string: 0.27.0 react: 18.2.0 react-docgen: 6.0.0-alpha.3 react-dom: 18.2.0(react@18.2.0) - vite: 4.3.5(@types/node@20.1.3)(sass@1.62.1) + vite: 4.3.9(@types/node@20.2.5)(sass@1.62.1) transitivePeerDependencies: - '@preact/preset-vite' - supports-color @@ -5795,8 +6767,8 @@ packages: - vite-plugin-glimmerx dev: true - /@storybook/react@7.0.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4): - resolution: {integrity: sha512-/DDUGFz0bk5c/HCfSr7fL74rQc+3s317TDDKY6ZrgUzdIkze4D/TlAbWV78XV/ceeFNi1fLAUzGjFzuDwmVkJw==} + /@storybook/react@7.0.18(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3): + resolution: {integrity: sha512-lumUbHYeuL3qa4SZR9K2YC4UIt1hwW19GuI/6f2HEV5gR9QHHSJHg9HD9pjcxv4fQaiG81ACZ0Sg6lyUkcJvuQ==} engines: {node: '>=16.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5806,13 +6778,13 @@ packages: typescript: optional: true dependencies: - '@storybook/client-logger': 7.0.10 - '@storybook/core-client': 7.0.10 - '@storybook/docs-tools': 7.0.10 + '@storybook/client-logger': 7.0.18 + '@storybook/core-client': 7.0.18 + '@storybook/docs-tools': 7.0.18 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.0.10 - '@storybook/react-dom-shim': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.0.10 + '@storybook/preview-api': 7.0.18 + '@storybook/react-dom-shim': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.0.18 '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 '@types/node': 16.18.16 @@ -5828,33 +6800,33 @@ packages: react-element-to-jsx-string: 15.0.0(react-dom@18.2.0)(react@18.2.0) ts-dedent: 2.2.0 type-fest: 2.19.0 - typescript: 5.0.4 + typescript: 5.1.3 util-deprecate: 1.0.2 transitivePeerDependencies: - supports-color dev: true - /@storybook/router@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Vq3nuyrGsvbPYhsaVu0TwtzX8Yb5TZYg7v5gY/uk1brSIk7Mvw64E8WF4TKNhPcWnlxNrfP9S96IZgT9iuuCpw==} + /@storybook/router@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Mue4s/BnKgdYcsiW9yuvW3qL9k3AgYn5HIhnkBExAteyiUGdAca4IJFhArmGgFktgeLc4ecBQ7sgaCljApnbgg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/client-logger': 7.0.10 + '@storybook/client-logger': 7.0.18 memoizerific: 1.11.3 qs: 6.11.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/source-loader@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-DtdYllq0piU6vgoVjsuPsWaGlhSOJgJr/kRovu5zltaZzdEOyQZ7e0zQmA4Py0h9jnGbg2fQG9zccofY3jUdJw==} + /@storybook/source-loader@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-n910+/rNJ3tCRUx3JJm/5ehjp5CK2WZg+KPRtG5a4AeVhQBdxsxw2D2pDYBWY1aFhJ+S4AZJOLIk9cdOMneA9g==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@storybook/csf': 0.1.0 - '@storybook/types': 7.0.10 + '@storybook/types': 7.0.18 estraverse: 5.3.0 lodash: 4.17.21 prettier: 2.8.8 @@ -5862,11 +6834,11 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/telemetry@7.0.10: - resolution: {integrity: sha512-0xlMECcSU2UnmpDTxKE/+pKpcW88fhxEZxh54yoA6NPpq6SGUN1r5ybUMffJCZ0JgaQ8HOc3Vxd13T3VXAMLXA==} + /@storybook/telemetry@7.0.18: + resolution: {integrity: sha512-JP5Z7lGU+oKjNmz2cZW5J7EerwyWBBPOU+NvvooZsymIx02ZvJ4ClmFtolJnBM7m4KoAy50JxV5NQWi+q8PicQ==} dependencies: - '@storybook/client-logger': 7.0.10 - '@storybook/core-common': 7.0.10 + '@storybook/client-logger': 7.0.18 + '@storybook/core-common': 7.0.18 chalk: 4.1.2 detect-package-manager: 2.0.1 fetch-retry: 5.0.4 @@ -5889,28 +6861,27 @@ packages: ts-dedent: 2.2.0 dev: true - /@storybook/theming@7.0.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-kKxIMElOUAyIAJOlhU6NS6/F6KpZLWvfGnUYC5V4f5Rsu+lKnbWI/TJ1rCIooz2wZBQ6dv+fjA3sOh5K+LRh2w==} + /@storybook/theming@7.0.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-P1gMKa/mKQHIMq0sxBIwTzAcF6v/6hrc62YmkuV62vXu+8zNV2YWbRwywqm3Q6faZEadmb/bL9+z8whaKhCL/g==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@emotion/use-insertion-effect-with-fallbacks': 1.0.0(react@18.2.0) - '@storybook/client-logger': 7.0.10 + '@storybook/client-logger': 7.0.18 '@storybook/global': 5.0.0 memoizerific: 1.11.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/types@7.0.10: - resolution: {integrity: sha512-mFktvN8PjjDFJSjck4spikmjtr0AwfOhcEtIf4UCmUD5JHgGppkQmvO6483nGcprSFcWOvD2uYGs8Wp32wG/MQ==} + /@storybook/types@7.0.18: + resolution: {integrity: sha512-qPop2CbvmX42/BX29YT9jIzW2TlMcMjAE+KCpcKLBiD1oT5DJ1fhMzpe6RW9HkMegkBxjWx54iamN4oHM/pwcQ==} dependencies: - '@storybook/channels': 7.0.10 + '@storybook/channels': 7.0.18 '@types/babel__core': 7.20.0 '@types/express': 4.17.17 file-system-cache: 2.0.2 - dev: true /@storybook/types@7.0.2: resolution: {integrity: sha512-0OCt/kAexa8MCcljxA+yZxGMn0n2U2Ync0KxotItqNbKBKVkaLQUls0+IXTWSCpC/QJvNZ049jxUHHanNi/96w==} @@ -5930,23 +6901,23 @@ packages: file-system-cache: 2.0.2 dev: true - /@storybook/vue3-vite@7.0.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4)(vite@4.3.5)(vue@3.3.1): - resolution: {integrity: sha512-BbA6uLlNFIpSBW9UAJ9e96yCGGoMho0pogEbkzoRLdw/0OoqDqnRMue78CwW5eiIWXYjNZb3UwAyh9VgYqKk5g==} + /@storybook/vue3-vite@7.0.18(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(vite@4.3.9)(vue@3.3.4): + resolution: {integrity: sha512-dwkwBQRDUSvf44Z4ZDftusP6obuczPkApxALxsTczkbpOxK/13SXArlrKgyUaFrcqto9i2e8HbAYb7y1ymO3ig==} engines: {node: ^14.18 || >=16} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 vite: ^3.0.0 || ^4.0.0 dependencies: - '@storybook/builder-vite': 7.0.10(typescript@5.0.4)(vite@4.3.5) - '@storybook/core-server': 7.0.10 - '@storybook/vue3': 7.0.10(vue@3.3.1) - '@vitejs/plugin-vue': 4.2.2(vite@4.3.5)(vue@3.3.1) + '@storybook/builder-vite': 7.0.18(typescript@5.1.3)(vite@4.3.9) + '@storybook/core-server': 7.0.18 + '@storybook/vue3': 7.0.18(vue@3.3.4) + '@vitejs/plugin-vue': 4.2.3(vite@4.3.9)(vue@3.3.4) magic-string: 0.27.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - vite: 4.3.5(@types/node@20.1.3)(sass@1.62.1) - vue-docgen-api: 4.64.1(vue@3.3.1) + vite: 4.3.9(@types/node@20.2.5)(sass@1.62.1) + vue-docgen-api: 4.64.1(vue@3.3.4) transitivePeerDependencies: - '@preact/preset-vite' - bufferutil @@ -5958,25 +6929,26 @@ packages: - vue dev: true - /@storybook/vue3@7.0.10(vue@3.3.1): - resolution: {integrity: sha512-B4DW/lR9Am06RJM3TGrIgIYzurG6tsgUX9EQ6rQRDFd4EWw1bskcG8MrNwFBBiDBnXe1frL4AdDidF47CFStNg==} + /@storybook/vue3@7.0.18(vue@3.3.4): + resolution: {integrity: sha512-++oC4Ee74ln9jPJSUnA6RWLxk5PNBGSP7lu71bA0b98MYsQ4GKliNEQf8lZmelSQy6nWoVHO0iyOhsKey7K3Ow==} engines: {node: '>=16.0.0'} peerDependencies: vue: ^3.0.0 dependencies: - '@storybook/core-client': 7.0.10 - '@storybook/docs-tools': 7.0.10 + '@storybook/core-client': 7.0.18 + '@storybook/docs-tools': 7.0.18 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.0.10 - '@storybook/types': 7.0.10 + '@storybook/preview-api': 7.0.18 + '@storybook/types': 7.0.18 ts-dedent: 2.2.0 type-fest: 2.19.0 - vue: 3.3.1 + vue: 3.3.4 + vue-component-type-helpers: 1.6.5 transitivePeerDependencies: - supports-color dev: true - /@swc/cli@0.1.62(@swc/core@1.3.56)(chokidar@3.5.3): + /@swc/cli@0.1.62(@swc/core@1.3.61)(chokidar@3.5.3): resolution: {integrity: sha512-kOFLjKY3XH1DWLfXL1/B5MizeNorHR8wHKEi92S/Zi9Md/AK17KSqR8MgyRJ6C1fhKHvbBCl8wboyKAFXStkYw==} engines: {node: '>= 12.13'} hasBin: true @@ -5988,11 +6960,11 @@ packages: optional: true dependencies: '@mole-inc/bin-wrapper': 8.0.1 - '@swc/core': 1.3.56 + '@swc/core': 1.3.61 chokidar: 3.5.3 commander: 7.2.0 fast-glob: 3.2.12 - semver: 7.5.0 + semver: 7.5.1 slash: 3.0.0 source-map: 0.7.4 dev: false @@ -6016,6 +6988,14 @@ packages: requiresBuild: true optional: true + /@swc/core-darwin-arm64@1.3.61: + resolution: {integrity: sha512-Ra1CZIYYyIp/Y64VcKyaLjIPUwT83JmGduvHu8vhUZOvWV4dWL4s5DrcxQVaQJjjb7Z2N/IUYYS55US1TGnxZw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + /@swc/core-darwin-x64@1.3.56: resolution: {integrity: sha512-VH5saqYFasdRXJy6RAT+MXm0+IjkMZvOkohJwUei+oA65cKJofQwrJ1jZro8yOJFYvUSI3jgNRGsdBkmo/4hMw==} engines: {node: '>=10'} @@ -6024,6 +7004,14 @@ packages: requiresBuild: true optional: true + /@swc/core-darwin-x64@1.3.61: + resolution: {integrity: sha512-LUia75UByUFkYH1Ddw7IE0X9usNVGJ7aL6+cgOTju7P0dsU0f8h/OGc/GDfp1E4qnKxDCJE+GwDRLoi4SjIxpg==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + /@swc/core-linux-arm-gnueabihf@1.3.56: resolution: {integrity: sha512-LWwPo6NnJkH01+ukqvkoNIOpMdw+Zundm4vBeicwyVrkP+mC3kwVfi03TUFpQUz3kRKdw/QEnxGTj+MouCPbtw==} engines: {node: '>=10'} @@ -6032,6 +7020,14 @@ packages: requiresBuild: true optional: true + /@swc/core-linux-arm-gnueabihf@1.3.61: + resolution: {integrity: sha512-aalPlicYxHAn2PxNlo3JFEZkMXzCtUwjP27AgMqnfV4cSz7Omo56OtC+413e/kGyCH86Er9gJRQQsxNKP8Qbsg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + /@swc/core-linux-arm64-gnu@1.3.56: resolution: {integrity: sha512-GzsUy/4egJ4cMlxbM+Ub7AMi5CKAc+pxBxrh8MUPQbyStW8jGgnQsJouTnGy0LHawtdEnsCOl6PcO6OgvktXuQ==} engines: {node: '>=10'} @@ -6040,6 +7036,14 @@ packages: requiresBuild: true optional: true + /@swc/core-linux-arm64-gnu@1.3.61: + resolution: {integrity: sha512-9hGdsbQrYNPo1c7YzWF57yl17bsIuuEQi3I1fOFSv3puL3l5M/C/oCD0Bz6IdKh6mEDC5UNJE4LWtV1gFA995A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + /@swc/core-linux-arm64-musl@1.3.56: resolution: {integrity: sha512-9gxL09BIiAv8zY0DjfnFf19bo8+P4T9tdhzPwcm+1yPJcY5yr1+YFWLNFzz01agtOj6VlZ2/wUJTaOfdjjtc+A==} engines: {node: '>=10'} @@ -6048,6 +7052,14 @@ packages: requiresBuild: true optional: true + /@swc/core-linux-arm64-musl@1.3.61: + resolution: {integrity: sha512-mVmcNfFQRP4SYbGC08IPB3B9Xox+VpGIQqA3Qg7LMCcejLAQLi4Lfe8CDvvBPlQzXHso0Cv+BicJnQVKs8JLOA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + /@swc/core-linux-x64-gnu@1.3.56: resolution: {integrity: sha512-n0ORNknl50vMRkll3BDO1E4WOqY6iISlPV1ZQCRLWQ6YQ2q8/WAryBxc2OAybcGHBUFkxyACpJukeU1QZ/9tNw==} engines: {node: '>=10'} @@ -6056,6 +7068,14 @@ packages: requiresBuild: true optional: true + /@swc/core-linux-x64-gnu@1.3.61: + resolution: {integrity: sha512-ZkRHs7GEikN6JiVL1/stvq9BVHKrSKoRn9ulVK2hMr+mAGNOKm3Y06NSzOO+BWwMaFOgnO2dWlszCUICsQ0kpg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + /@swc/core-linux-x64-musl@1.3.56: resolution: {integrity: sha512-r+D34WLAOAlJtfw1gaVWpHRwCncU9nzW9i7w9kSw4HpWYnHJOz54jLGSEmNsrhdTCz1VK2ar+V2ktFUsrlGlDA==} engines: {node: '>=10'} @@ -6064,6 +7084,14 @@ packages: requiresBuild: true optional: true + /@swc/core-linux-x64-musl@1.3.61: + resolution: {integrity: sha512-zK7VqQ5JlK20+7fxI4AgvIUckeZyX0XIbliGXNMR3i+39SJq1vs9scYEmq8VnAfvNdMU5BG+DewbFJlMfCtkxQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + /@swc/core-win32-arm64-msvc@1.3.56: resolution: {integrity: sha512-29Yt75Is6X24z3x8h/xZC1HnDPkPpyLH9mDQiM6Cuc0I9mVr1XSriPEUB2N/awf5IE4SA8c+3IVq1DtKWbkJIw==} engines: {node: '>=10'} @@ -6072,6 +7100,14 @@ packages: requiresBuild: true optional: true + /@swc/core-win32-arm64-msvc@1.3.61: + resolution: {integrity: sha512-e9kVVPk5iVNhO41TvLvcExDHn5iATQ5/M4U7/CdcC7s0fK19TKSEUqkdoTLIJvHBFhgR7w3JJSErfnauO0xXoA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + /@swc/core-win32-ia32-msvc@1.3.56: resolution: {integrity: sha512-mplp0zbYDrcHtfvkniXlXdB04e2qIjz2Gq/XHKr4Rnc6xVORJjjXF91IemXKpavx2oZYJws+LNJL7UFQ8jyCdQ==} engines: {node: '>=10'} @@ -6080,6 +7116,14 @@ packages: requiresBuild: true optional: true + /@swc/core-win32-ia32-msvc@1.3.61: + resolution: {integrity: sha512-7cJULfa6HvKqvFh6M/f7mKiNRhE2AjgFUCZfdOuy5r8vbtpk+qBK94TXwaDjJYDUGKzDVZw/tJ1eN4Y9n9Ls/Q==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true + /@swc/core-win32-x64-msvc@1.3.56: resolution: {integrity: sha512-zp8MBnrw/bjdLenO/ifYzHrImSjKunqL0C2IF4LXYNRfcbYFh2NwobsVQMZ20IT0474lKRdlP8Oxdt+bHuXrzA==} engines: {node: '>=10'} @@ -6088,6 +7132,14 @@ packages: requiresBuild: true optional: true + /@swc/core-win32-x64-msvc@1.3.61: + resolution: {integrity: sha512-Jx8S+21WcKF/wlhW+sYpystWUyymDTEsbBpOgBRpXZelakVcNBCIIYSZOKW/A9PwWTpu6S8yvbs9nUOzKiVPqA==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + /@swc/core@1.3.56: resolution: {integrity: sha512-yz/EeXT+PMZucUNrYceRUaTfuNS4IIu5EDZSOlvCEvm4jAmZi7CYH1B/kvzEzoAOzr7zkQiDPNJftcQXLkjbjA==} engines: {node: '>=10'} @@ -6109,6 +7161,27 @@ packages: '@swc/core-win32-ia32-msvc': 1.3.56 '@swc/core-win32-x64-msvc': 1.3.56 + /@swc/core@1.3.61: + resolution: {integrity: sha512-p58Ltdjo7Yy8CU3zK0cp4/eAgy5qkHs35znGedqVGPiA67cuYZM63DuTfmyrOntMRwQnaFkMLklDAPCizDdDng==} + engines: {node: '>=10'} + requiresBuild: true + peerDependencies: + '@swc/helpers': ^0.5.0 + peerDependenciesMeta: + '@swc/helpers': + optional: true + optionalDependencies: + '@swc/core-darwin-arm64': 1.3.61 + '@swc/core-darwin-x64': 1.3.61 + '@swc/core-linux-arm-gnueabihf': 1.3.61 + '@swc/core-linux-arm64-gnu': 1.3.61 + '@swc/core-linux-arm64-musl': 1.3.61 + '@swc/core-linux-x64-gnu': 1.3.61 + '@swc/core-linux-x64-musl': 1.3.61 + '@swc/core-win32-arm64-msvc': 1.3.61 + '@swc/core-win32-ia32-msvc': 1.3.61 + '@swc/core-win32-x64-msvc': 1.3.61 + /@swc/jest@0.2.26(@swc/core@1.3.56): resolution: {integrity: sha512-7lAi7q7ShTO3E5Gt1Xqf3pIhRbERxR1DUxvtVa9WKzIB+HGQ7wZP5sYx86zqnaEoKKGhmOoZ7gyW0IRu8Br5+A==} engines: {npm: '>= 7.0.0'} @@ -6120,14 +7193,25 @@ packages: jsonc-parser: 3.2.0 dev: true + /@swc/jest@0.2.26(@swc/core@1.3.61): + resolution: {integrity: sha512-7lAi7q7ShTO3E5Gt1Xqf3pIhRbERxR1DUxvtVa9WKzIB+HGQ7wZP5sYx86zqnaEoKKGhmOoZ7gyW0IRu8Br5+A==} + engines: {npm: '>= 7.0.0'} + peerDependencies: + '@swc/core': '*' + dependencies: + '@jest/create-cache-key-function': 27.5.1 + '@swc/core': 1.3.61 + jsonc-parser: 3.2.0 + dev: true + /@swc/wasm@1.2.130: resolution: {integrity: sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==} requiresBuild: true dev: false optional: true - /@syuilo/aiscript@0.13.2: - resolution: {integrity: sha512-1aqQSH6U+vV01UDUotXUEjIwJKcZZPASJyIJ9msxXRpSInPGJJ/q1kGkZMgSpVhzYFT7/QBxo0UC1ZVEOsrDTw==} + /@syuilo/aiscript@0.13.3: + resolution: {integrity: sha512-0YFlWA+7YhyRRsp+9Nl72SoSUg5ghskthjCdLvj4qdGyLedeyanKZWJlH2A9d47Nes03UYY8CRDsMHHv64IWcg==} dependencies: autobind-decorator: 2.4.0 seedrandom: 3.0.5 @@ -6148,14 +7232,14 @@ packages: dependencies: defer-to-connect: 2.0.1 - /@tabler/icons-webfont@2.17.0: - resolution: {integrity: sha512-jgRZWiWCaG++jFTIU/dbOT+JmSgoFlALwBUUS31mt1b5py7B0YWelnfxf5s3ctE+0dlnoIS+r7rDOeDSAWx8SA==} + /@tabler/icons-webfont@2.21.0: + resolution: {integrity: sha512-WCa57zYBjD9NF3/g96WKePgKUkKKD95Y+mo27/fzXOGxuoP9lGRjd01UCeLTGVxdEPErwlCjHXSi8HoDX2jevg==} dependencies: - '@tabler/icons': 2.17.0 + '@tabler/icons': 2.21.0 dev: false - /@tabler/icons@2.17.0: - resolution: {integrity: sha512-UeJaylOGNRhQKyDlgZfrQ3UPSGlfVQuXcmCsTYeXioKKepibW6VZ3H36Lo1jvBTBkQD2e9m+k2NxwkztOTXwrA==} + /@tabler/icons@2.21.0: + resolution: {integrity: sha512-XKrTEHMX6XzCOwcOU8ZNA+Xqm51sI+0abn2jk1fyQUpWeFnGsOEiC+fpQ4EISc+v+U9jqgTSbh8bZ6JBuKU5sw==} dev: false /@tensorflow/tfjs-backend-cpu@4.4.0(@tensorflow/tfjs-core@4.4.0): @@ -6215,7 +7299,7 @@ packages: dependencies: '@tensorflow/tfjs-core': 4.4.0 '@types/node-fetch': 2.6.2 - node-fetch: 2.6.7 + node-fetch: 2.6.11 seedrandom: 3.0.5 string_decoder: 1.3.0 transitivePeerDependencies: @@ -6274,7 +7358,7 @@ packages: resolution: {integrity: sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA==} engines: {node: '>=12'} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.21.4 '@babel/runtime': 7.21.0 '@types/aria-query': 5.0.1 aria-query: 5.1.3 @@ -6288,7 +7372,7 @@ packages: resolution: {integrity: sha512-xTEnpUKiV/bMyEsE5bT4oYA0x0Z/colMtxzUY8bKyPXBNLn/e0V4ZjBZkEhms0xE4pv9QsPfSRu9AWS4y5wGvA==} engines: {node: '>=14'} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.21.4 '@babel/runtime': 7.21.0 '@types/aria-query': 5.0.1 aria-query: 5.1.3 @@ -6304,7 +7388,7 @@ packages: dependencies: '@adobe/css-tools': 4.2.0 '@babel/runtime': 7.20.7 - '@types/testing-library__jest-dom': 5.14.5 + '@types/testing-library__jest-dom': 5.14.6 aria-query: 5.1.3 chalk: 3.0.0 css.escape: 1.5.1 @@ -6323,7 +7407,7 @@ packages: '@testing-library/dom': 8.20.0 dev: true - /@testing-library/vue@7.0.0(@vue/compiler-sfc@3.3.1)(vue@3.3.1): + /@testing-library/vue@7.0.0(@vue/compiler-sfc@3.3.4)(vue@3.3.4): resolution: {integrity: sha512-JU/q93HGo2qdm1dCgWymkeQlfpC0/0/DBZ2nAHgEAsVZxX11xVIxT7gbXdI7HACQpUbsUWt1zABGU075Fzt9XQ==} engines: {node: '>=14'} peerDependencies: @@ -6332,9 +7416,9 @@ packages: dependencies: '@babel/runtime': 7.21.0 '@testing-library/dom': 9.2.0 - '@vue/compiler-sfc': 3.3.1 - '@vue/test-utils': 2.3.2(vue@3.3.1) - vue: 3.3.1 + '@vue/compiler-sfc': 3.3.4 + '@vue/test-utils': 2.3.2(vue@3.3.4) + vue: 3.3.4 dev: true /@tokenizer/token@0.3.0: @@ -6353,7 +7437,7 @@ packages: /@types/accepts@1.3.5: resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /@types/archiver@5.3.2: @@ -6373,31 +7457,27 @@ packages: /@types/babel__core@7.20.0: resolution: {integrity: sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==} dependencies: - '@babel/parser': 7.21.4 - '@babel/types': 7.21.4 + '@babel/parser': 7.22.4 + '@babel/types': 7.22.4 '@types/babel__generator': 7.6.4 '@types/babel__template': 7.4.1 - '@types/babel__traverse': 7.18.3 - dev: true + '@types/babel__traverse': 7.20.0 /@types/babel__generator@7.6.4: resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} dependencies: - '@babel/types': 7.21.5 - dev: true + '@babel/types': 7.22.4 /@types/babel__template@7.4.1: resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} dependencies: - '@babel/parser': 7.21.8 - '@babel/types': 7.21.5 - dev: true + '@babel/parser': 7.22.4 + '@babel/types': 7.22.4 - /@types/babel__traverse@7.18.3: - resolution: {integrity: sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==} + /@types/babel__traverse@7.20.0: + resolution: {integrity: sha512-TBOjqAGf0hmaqRwpii5LLkJLg7c6OMm4nHLmpsUxwk9bBHtoTC6dAHdVWdGv4TBxj2CZOZY8Xfq8WmfoVi7n4Q==} dependencies: - '@babel/types': 7.21.5 - dev: true + '@babel/types': 7.22.4 /@types/bcryptjs@2.4.2: resolution: {integrity: sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==} @@ -6407,44 +7487,35 @@ packages: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 20.1.3 - dev: true + '@types/node': 20.2.5 /@types/braces@3.0.1: resolution: {integrity: sha512-+euflG6ygo4bn0JHtn4pYqcXwRtLvElQ7/nnjDu7iYG56H0+OhCd7d6Ug0IE3WcFpZozBKW2+80FUbv5QGk5AQ==} dev: true - /@types/bull@4.10.0: - resolution: {integrity: sha512-RkYW8K2H3J76HT6twmHYbzJ0GtLDDotpLP9ah9gtiA7zfF6peBH1l5fEiK0oeIZ3/642M7Jcb9sPmor8Vf4w6g==} - dependencies: - bull: 4.10.4 - transitivePeerDependencies: - - supports-color - dev: true - /@types/cacheable-request@6.0.3: resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} dependencies: '@types/http-cache-semantics': 4.0.1 '@types/keyv': 3.1.4 - '@types/node': 20.1.3 + '@types/node': 20.2.5 '@types/responselike': 1.0.0 dev: false /@types/cbor@6.0.0: resolution: {integrity: sha512-mGQ1lbYOwVti5Xlarn1bTeBZqgY0kstsdjnkoEovgohYKdBjGejHyNGXHdMBeqyQazIv32Jjp33+5pBEaSRy2w==} dependencies: - cbor: 8.1.0 + cbor: 9.0.0 dev: true /@types/chai-subset@1.3.3: resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} dependencies: - '@types/chai': 4.3.4 + '@types/chai': 4.3.5 dev: true - /@types/chai@4.3.4: - resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} + /@types/chai@4.3.5: + resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==} dev: true /@types/color-convert@2.0.0: @@ -6460,8 +7531,7 @@ packages: /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 20.1.3 - dev: true + '@types/node': 20.2.5 /@types/content-disposition@0.5.5: resolution: {integrity: sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==} @@ -6525,10 +7595,9 @@ packages: /@types/express-serve-static-core@4.17.33: resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 - dev: true /@types/express@4.17.17: resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} @@ -6537,7 +7606,6 @@ packages: '@types/express-serve-static-core': 4.17.33 '@types/qs': 6.9.7 '@types/serve-static': 1.15.1 - dev: true /@types/find-cache-dir@3.2.1: resolution: {integrity: sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==} @@ -6546,34 +7614,34 @@ packages: /@types/fluent-ffmpeg@2.1.21: resolution: {integrity: sha512-+n3dy/Tegt6n+YwGZUiGq6i8Jrnt8+MoyPiW1L6J5EWUl7GSt18a/VyReecfCsvTTNBXNMIKOMHDstiQM8nJLA==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /@types/glob-stream@6.1.1: resolution: {integrity: sha512-AGOUTsTdbPkRS0qDeyeS+6KypmfVpbT5j23SN8UPG63qjKXNKjXn6V9wZUr8Fin0m9l8oGYaPK8b2WUMF8xI1A==} dependencies: '@types/glob': 8.1.0 - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /@types/glob@7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /@types/glob@8.1.0: resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /@types/graceful-fs@4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /@types/gulp-rename@2.0.1: @@ -6586,7 +7654,7 @@ packages: /@types/gulp-rename@2.0.2: resolution: {integrity: sha512-CQsXqTVtAXqrPd4IbrrlJEEzRkUR3RXsyZbrVoOVqjlchDDmnyRDatAUisjpQjjCg/wjJrSiNg8T1uAbJ/7Qqg==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 '@types/vinyl': 2.0.7 dev: true @@ -6630,6 +7698,13 @@ packages: pretty-format: 29.5.0 dev: true + /@types/jest@29.5.2: + resolution: {integrity: sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==} + dependencies: + expect: 29.5.0 + pretty-format: 29.5.0 + dev: true + /@types/js-levenshtein@1.1.1: resolution: {integrity: sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g==} dev: true @@ -6641,7 +7716,7 @@ packages: /@types/jsdom@21.1.1: resolution: {integrity: sha512-cZFuoVLtzKP3gmq9eNosUL1R50U+USkbLtUQ1bYVgl/lKp0FZM7Cq4aIHAL8oIvQ17uSHi7jXPtfDOdjPwBE7A==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 '@types/tough-cookie': 4.0.2 parse5: 7.1.2 dev: true @@ -6665,7 +7740,7 @@ packages: /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: false /@types/lodash@4.14.191: @@ -6676,8 +7751,8 @@ packages: resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} dev: false - /@types/matter-js@0.18.3: - resolution: {integrity: sha512-7DYI52ebEl6AD9+RV2jO/XHdlFlpozYbkURtYKKJ2tO34q49Y15cfl+JSJpoMglQCAL/PxBSHKVv3wkvfZZD7g==} + /@types/matter-js@0.18.5: + resolution: {integrity: sha512-CV8m/FUmjmFNFcI7fUnsKcCLeqbf0kzWdKOTLGrpfKwWwrF6ggLaQlHNsg8267TkkiUAPoXY/7q6H9qwmR5TZg==} dev: true /@types/mdx@2.0.3: @@ -6696,7 +7771,6 @@ packages: /@types/mime@3.0.1: resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} - dev: true /@types/minimatch@5.1.2: resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} @@ -6713,7 +7787,7 @@ packages: /@types/node-fetch@2.6.2: resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 form-data: 3.0.1 /@types/node-fetch@3.0.3: @@ -6741,13 +7815,13 @@ packages: resolution: {integrity: sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==} dev: true - /@types/node@20.1.3: - resolution: {integrity: sha512-NP2yfZpgmf2eDRPmgGq+fjGjSwFgYbihA8/gK+ey23qT9RkxsgNTZvGOEpXgzIGqesTYkElELLgtKoMQTys5vA==} + /@types/node@20.2.5: + resolution: {integrity: sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==} - /@types/nodemailer@6.4.7: - resolution: {integrity: sha512-f5qCBGAn/f0qtRcd4SEn88c8Fp3Swct1731X4ryPKqS61/A3LmmzN8zaEz7hneJvpjFbUUgY7lru/B/7ODTazg==} + /@types/nodemailer@6.4.8: + resolution: {integrity: sha512-oVsJSCkqViCn8/pEu2hfjwVO+Gb3e+eTWjg3PcjeFKRItfKpKwHphQqbYmPQrlMk+op7pNNWPbsJIEthpFN/OQ==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /@types/normalize-package-data@2.4.1: @@ -6761,7 +7835,7 @@ packages: /@types/oauth@0.9.1: resolution: {integrity: sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /@types/offscreencanvas@2019.3.0: @@ -6772,12 +7846,12 @@ packages: resolution: {integrity: sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==} dev: false - /@types/pg@8.6.6: - resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==} + /@types/pg@8.10.1: + resolution: {integrity: sha512-AmEHA/XxMxemQom5iDwP62FYNkv+gDDnetRG7v2N2dPtju7UKI7FknUimcZo7SodKTHtckYPzaTqUEvUKbVJEA==} dependencies: - '@types/node': 20.1.3 - pg-protocol: 1.5.0 - pg-types: 2.2.0 + '@types/node': 20.2.5 + pg-protocol: 1.6.0 + pg-types: 4.0.1 dev: true /@types/prettier@2.7.2: @@ -6803,12 +7877,11 @@ packages: /@types/qrcode@1.5.0: resolution: {integrity: sha512-x5ilHXRxUPIMfjtM+1vf/GPTRWZ81nqscursm5gMznJeK9M0YnZ1c3bEvRLQ0zSSgedLx1J6MGL231ObQGGhaA==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /@types/qs@6.9.7: resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} - dev: true /@types/random-seed@0.3.3: resolution: {integrity: sha512-kHsCbIRHNXJo6EN5W8EA5b4i1hdT6jaZke5crBPLUcLqaLdZ0QBq8QVMbafHzhjFF83Cl9qlee2dChD18d/kPg==} @@ -6816,7 +7889,6 @@ packages: /@types/range-parser@1.2.4: resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} - dev: true /@types/ratelimiter@3.4.4: resolution: {integrity: sha512-GSMb93iSA8KKFDgVL2Wzs/kqrHMJcU8xhLdwI5omoACcj7K18SacklLtY1C4G02HC5drd6GygtsIaGbfxJSe0g==} @@ -6833,7 +7905,7 @@ packages: /@types/readdir-glob@1.1.1: resolution: {integrity: sha512-ImM6TmoF8bgOwvehGviEj3tRdRBbQujr1N+0ypaln/GWjaerOB26jb93vsRHmdMtvVQZQebOlqt2HROark87mQ==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /@types/redis@4.0.11: @@ -6849,7 +7921,7 @@ packages: /@types/responselike@1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: false /@types/sanitize-html@2.9.0: @@ -6877,8 +7949,7 @@ packages: resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} dependencies: '@types/mime': 3.0.1 - '@types/node': 20.1.3 - dev: true + '@types/node': 20.2.5 /@types/serviceworker@0.0.67: resolution: {integrity: sha512-7TCH7iNsCSNb+aUD9M/36TekrWFSLCjNK8zw/3n5kOtRjbLtDfGYMXTrDnGhSfqXNwpqmt9Vd90w5C/ad1tX6Q==} @@ -6887,7 +7958,7 @@ packages: /@types/set-cookie-parser@2.4.2: resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /@types/sharp@0.32.0: @@ -6919,10 +7990,10 @@ packages: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} dev: true - /@types/testing-library__jest-dom@5.14.5: - resolution: {integrity: sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==} + /@types/testing-library__jest-dom@5.14.6: + resolution: {integrity: sha512-FkHXCb+ikSoUP4Y4rOslzTdX5sqYwMxfefKh1GmZ8ce1GOkEHntSp6b5cGadmNfp5e4BMEWOMx+WSKd5/MqlDA==} dependencies: - '@types/jest': 29.5.1 + '@types/jest': 29.5.2 dev: true /@types/throttle-debounce@5.0.0: @@ -6952,7 +8023,7 @@ packages: /@types/undertaker@1.2.8: resolution: {integrity: sha512-gW3PRqCHYpo45XFQHJBhch7L6hytPsIe0QeLujlnFsjHPnXLhJcPdN6a9368d7aIQgH2I/dUTPFBlGeSNA3qOg==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 '@types/undertaker-registry': 1.0.1 async-done: 1.3.2 dev: true @@ -6961,10 +8032,10 @@ packages: resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} dev: true - /@types/unzipper@0.10.5: - resolution: {integrity: sha512-NrLJb29AdnBARpg9S/4ktfPEisbJ0AvaaAr3j7Q1tg8AgcEUsq2HqbNzvgLRoWyRtjzeLEv7vuL39u1mrNIyNA==} + /@types/unzipper@0.10.6: + resolution: {integrity: sha512-zcBj329AHgKLQyz209N/S9R0GZqXSkUQO4tJSYE3x02qg4JuDFpgKMj50r82Erk1natCWQDIvSccDddt7jPzjA==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /@types/uuid@9.0.1: @@ -6974,14 +8045,14 @@ packages: /@types/vary@1.1.0: resolution: {integrity: sha512-LQWqrIa0dvEOOH37lGksMEXbypRLUFqu6Gx0pmX7zIUisD2I/qaVgEX/vJ/PSVSW0Hk6yz1BNkFpqg6dZm3Wug==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /@types/vinyl-fs@2.4.12: resolution: {integrity: sha512-LgBpYIWuuGsihnlF+OOWWz4ovwCYlT03gd3DuLwex50cYZLmX3yrW+sFF9ndtmh7zcZpS6Ri47PrIu+fV+sbXw==} dependencies: '@types/glob-stream': 6.1.1 - '@types/node': 20.1.3 + '@types/node': 20.2.5 '@types/vinyl': 2.0.7 dev: true @@ -6989,12 +8060,12 @@ packages: resolution: {integrity: sha512-4UqPv+2567NhMQuMLdKAyK4yzrfCqwaTt6bLhHEs8PFcxbHILsrxaY63n4wgE/BRLDWDQeI+WcTmkXKExh9hQg==} dependencies: '@types/expect': 1.20.4 - '@types/node': 20.1.3 + '@types/node': 20.2.5 /@types/web-push@3.3.2: resolution: {integrity: sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /@types/webgl-ext@0.0.30: @@ -7008,13 +8079,13 @@ packages: /@types/websocket@1.0.5: resolution: {integrity: sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /@types/ws@8.5.4: resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /@types/yargs-parser@21.0.0: @@ -7037,7 +8108,7 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true optional: true @@ -7069,6 +8140,34 @@ packages: - supports-color dev: true + /@typescript-eslint/eslint-plugin@5.59.8(@typescript-eslint/parser@5.59.8)(eslint@8.41.0)(typescript@5.1.3): + resolution: {integrity: sha512-JDMOmhXteJ4WVKOiHXGCoB96ADWg9q7efPWHRViT/f09bA8XOMLAVHHju3l0MkZnG1izaWXYmgvQcUjTRcpShQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.5.0 + '@typescript-eslint/parser': 5.59.8(eslint@8.41.0)(typescript@5.1.3) + '@typescript-eslint/scope-manager': 5.59.8 + '@typescript-eslint/type-utils': 5.59.8(eslint@8.41.0)(typescript@5.1.3) + '@typescript-eslint/utils': 5.59.8(eslint@8.41.0)(typescript@5.1.3) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.41.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.4 + natural-compare-lite: 1.4.0 + semver: 7.5.1 + tsutils: 3.21.0(typescript@5.1.3) + typescript: 5.1.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser@5.59.5(eslint@8.40.0)(typescript@5.0.4): resolution: {integrity: sha512-NJXQC4MRnF9N9yWqQE2/KLRSOLvrrlZb48NGVfBa+RuPMN6B7ZcK5jZOvhuygv4D64fRKnZI4L4p8+M+rfeQuw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7089,6 +8188,26 @@ packages: - supports-color dev: true + /@typescript-eslint/parser@5.59.8(eslint@8.41.0)(typescript@5.1.3): + resolution: {integrity: sha512-AnR19RjJcpjoeGojmwZtCwBX/RidqDZtzcbG3xHrmz0aHHoOcbWnpDllenRDmDvsV0RQ6+tbb09/kyc+UT9Orw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.59.8 + '@typescript-eslint/types': 5.59.8 + '@typescript-eslint/typescript-estree': 5.59.8(typescript@5.1.3) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.41.0 + typescript: 5.1.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/scope-manager@5.59.5: resolution: {integrity: sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7097,6 +8216,14 @@ packages: '@typescript-eslint/visitor-keys': 5.59.5 dev: true + /@typescript-eslint/scope-manager@5.59.8: + resolution: {integrity: sha512-/w08ndCYI8gxGf+9zKf1vtx/16y8MHrZs5/tnjHhMLNSixuNcJavSX4wAiPf4aS5x41Es9YPCn44MIe4cxIlig==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.59.8 + '@typescript-eslint/visitor-keys': 5.59.8 + dev: true + /@typescript-eslint/type-utils@5.59.5(eslint@8.40.0)(typescript@5.0.4): resolution: {integrity: sha512-4eyhS7oGym67/pSxA2mmNq7X164oqDYNnZCUayBwJZIRVvKpBCMBzFnFxjeoDeShjtO6RQBHBuwybuX3POnDqg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7117,11 +8244,36 @@ packages: - supports-color dev: true + /@typescript-eslint/type-utils@5.59.8(eslint@8.41.0)(typescript@5.1.3): + resolution: {integrity: sha512-+5M518uEIHFBy3FnyqZUF3BMP+AXnYn4oyH8RF012+e7/msMY98FhGL5SrN29NQ9xDgvqCgYnsOiKp1VjZ/fpA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.59.8(typescript@5.1.3) + '@typescript-eslint/utils': 5.59.8(eslint@8.41.0)(typescript@5.1.3) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.41.0 + tsutils: 3.21.0(typescript@5.1.3) + typescript: 5.1.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/types@5.59.5: resolution: {integrity: sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@typescript-eslint/types@5.59.8: + resolution: {integrity: sha512-+uWuOhBTj/L6awoWIg0BlWy0u9TyFpCHrAuQ5bNfxDaZ1Ppb3mx6tUigc74LHcbHpOHuOTOJrBoAnhdHdaea1w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /@typescript-eslint/typescript-estree@5.59.5(typescript@5.0.4): resolution: {integrity: sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7136,13 +8288,34 @@ packages: debug: 4.3.4(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.0 + semver: 7.5.1 tsutils: 3.21.0(typescript@5.0.4) typescript: 5.0.4 transitivePeerDependencies: - supports-color dev: true + /@typescript-eslint/typescript-estree@5.59.8(typescript@5.1.3): + resolution: {integrity: sha512-Jy/lPSDJGNow14vYu6IrW790p7HIf/SOV1Bb6lZ7NUkLc2iB2Z9elESmsaUtLw8kVqogSbtLH9tut5GCX1RLDg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.59.8 + '@typescript-eslint/visitor-keys': 5.59.8 + debug: 4.3.4(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.1 + tsutils: 3.21.0(typescript@5.1.3) + typescript: 5.1.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/utils@5.59.5(eslint@8.40.0)(typescript@5.0.4): resolution: {integrity: sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7157,7 +8330,27 @@ packages: '@typescript-eslint/typescript-estree': 5.59.5(typescript@5.0.4) eslint: 8.40.0 eslint-scope: 5.1.1 - semver: 7.5.0 + semver: 7.5.1 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/utils@5.59.8(eslint@8.41.0)(typescript@5.1.3): + resolution: {integrity: sha512-Tr65630KysnNn9f9G7ROF3w1b5/7f6QVCJ+WK9nhIocWmx9F+TmCAcglF26Vm7z8KCTwoKcNEBZrhlklla3CKg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.41.0) + '@types/json-schema': 7.0.11 + '@types/semver': 7.5.0 + '@typescript-eslint/scope-manager': 5.59.8 + '@typescript-eslint/types': 5.59.8 + '@typescript-eslint/typescript-estree': 5.59.8(typescript@5.1.3) + eslint: 8.41.0 + eslint-scope: 5.1.1 + semver: 7.5.1 transitivePeerDependencies: - supports-color - typescript @@ -7171,78 +8364,86 @@ packages: eslint-visitor-keys: 3.4.1 dev: true - /@vitejs/plugin-react@3.1.0(vite@4.3.5): + /@typescript-eslint/visitor-keys@5.59.8: + resolution: {integrity: sha512-pJhi2ms0x0xgloT7xYabil3SGGlojNNKjK/q6dB3Ey0uJLMjK2UDGJvHieiyJVW/7C3KI+Z4Q3pEHkm4ejA+xQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.59.8 + eslint-visitor-keys: 3.4.1 + dev: true + + /@vitejs/plugin-react@3.1.0(vite@4.3.9): resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.1.0-beta.0 dependencies: - '@babel/core': 7.21.3 - '@babel/plugin-transform-react-jsx-self': 7.21.0(@babel/core@7.21.3) - '@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.21.3) + '@babel/core': 7.22.1 + '@babel/plugin-transform-react-jsx-self': 7.21.0(@babel/core@7.22.1) + '@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.22.1) magic-string: 0.27.0 react-refresh: 0.14.0 - vite: 4.3.5(@types/node@20.1.3)(sass@1.62.1) + vite: 4.3.9(@types/node@20.2.5)(sass@1.62.1) transitivePeerDependencies: - supports-color dev: true - /@vitejs/plugin-vue@4.2.2(vite@4.3.5)(vue@3.3.1): - resolution: {integrity: sha512-kNH4wMAqs13UiZe/2If1ioO0Mjz71rr2oALTl2c5ajBIox9Vz/UGW/wGkr7GA3SC6Eb29c1HtzAtxdGfbXAkfQ==} + /@vitejs/plugin-vue@4.2.3(vite@4.3.9)(vue@3.3.4): + resolution: {integrity: sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.0.0 vue: ^3.2.25 dependencies: - vite: 4.3.5(@types/node@20.1.3)(sass@1.62.1) - vue: 3.3.1 + vite: 4.3.9(@types/node@20.2.5)(sass@1.62.1) + vue: 3.3.4 - /@vitest/coverage-c8@0.31.0(vitest@0.31.0): - resolution: {integrity: sha512-h72qN1D962AO7UefQVulm9JFP5ACS7OfhCdBHioXU8f7ohH/+NTZCgAqmgcfRNHHO/8wLFxx+93YVxhodkEJVA==} + /@vitest/coverage-c8@0.31.4(vitest@0.31.4): + resolution: {integrity: sha512-VPx368m4DTcpA/P0v3YdVxl4QOSh1DbUcXURLRvDShrIB5KxOgfzw4Bn2R8AhAe/GyiWW/FIsJ/OJdYXCCiC1w==} peerDependencies: vitest: '>=0.30.0 <1' dependencies: - '@ampproject/remapping': 2.2.0 + '@ampproject/remapping': 2.2.1 c8: 7.13.0 magic-string: 0.30.0 picocolors: 1.0.0 std-env: 3.3.2 - vitest: 0.31.0(happy-dom@9.16.0)(sass@1.62.1) + vitest: 0.31.4(happy-dom@9.20.3)(sass@1.62.1) dev: true - /@vitest/expect@0.31.0: - resolution: {integrity: sha512-Jlm8ZTyp6vMY9iz9Ny9a0BHnCG4fqBa8neCF6Pk/c/6vkUk49Ls6UBlgGAU82QnzzoaUs9E/mUhq/eq9uMOv/g==} + /@vitest/expect@0.31.4: + resolution: {integrity: sha512-tibyx8o7GUyGHZGyPgzwiaPaLDQ9MMuCOrc03BYT0nryUuhLbL7NV2r/q98iv5STlwMgaKuFJkgBW/8iPKwlSg==} dependencies: - '@vitest/spy': 0.31.0 - '@vitest/utils': 0.31.0 + '@vitest/spy': 0.31.4 + '@vitest/utils': 0.31.4 chai: 4.3.7 dev: true - /@vitest/runner@0.31.0: - resolution: {integrity: sha512-H1OE+Ly7JFeBwnpHTrKyCNm/oZgr+16N4qIlzzqSG/YRQDATBYmJb/KUn3GrZaiQQyL7GwpNHVZxSQd6juLCgw==} + /@vitest/runner@0.31.4: + resolution: {integrity: sha512-Wgm6UER+gwq6zkyrm5/wbpXGF+g+UBB78asJlFkIOwyse0pz8lZoiC6SW5i4gPnls/zUcPLWS7Zog0LVepXnpg==} dependencies: - '@vitest/utils': 0.31.0 + '@vitest/utils': 0.31.4 concordance: 5.0.4 p-limit: 4.0.0 pathe: 1.1.0 dev: true - /@vitest/snapshot@0.31.0: - resolution: {integrity: sha512-5dTXhbHnyUMTMOujZPB0wjFjQ6q5x9c8TvAsSPUNKjp1tVU7i9pbqcKPqntyu2oXtmVxKbuHCqrOd+Ft60r4tg==} + /@vitest/snapshot@0.31.4: + resolution: {integrity: sha512-LemvNumL3NdWSmfVAMpXILGyaXPkZbG5tyl6+RQSdcHnTj6hvA49UAI8jzez9oQyE/FWLKRSNqTGzsHuk89LRA==} dependencies: magic-string: 0.30.0 pathe: 1.1.0 pretty-format: 27.5.1 dev: true - /@vitest/spy@0.31.0: - resolution: {integrity: sha512-IzCEQ85RN26GqjQNkYahgVLLkULOxOm5H/t364LG0JYb3Apg0PsYCHLBYGA006+SVRMWhQvHlBBCyuByAMFmkg==} + /@vitest/spy@0.31.4: + resolution: {integrity: sha512-3ei5ZH1s3aqbEyftPAzSuunGICRuhE+IXOmpURFdkm5ybUADk+viyQfejNk6q8M5QGX8/EVKw+QWMEP3DTJDag==} dependencies: tinyspy: 2.1.0 dev: true - /@vitest/utils@0.31.0: - resolution: {integrity: sha512-kahaRyLX7GS1urekRXN2752X4gIgOGVX4Wo8eDUGUkTWlGpXzf5ZS6N9RUUS+Re3XEE8nVGqNyxkSxF5HXlGhQ==} + /@vitest/utils@0.31.4: + resolution: {integrity: sha512-DobZbHacWznoGUfYU8XDPY78UubJxXfMNY1+SUdOp1NsI34eopSA6aZMeaGu10waSOeYwE8lxrd/pLfT0RMxjQ==} dependencies: concordance: 5.0.4 loupe: 2.3.6 @@ -7261,166 +8462,158 @@ packages: muggle-string: 0.2.2 dev: true - /@volar/typescript@1.4.1(typescript@5.0.4): - resolution: {integrity: sha512-phTy6p9yG6bgMIKQWEeDOi/aeT0njZsb1a/G1mrEuDsLmAn24Le4gDwSsGNhea6Uhu+3gdpUZn2PmZXa+WG2iQ==} + /@volar/typescript@1.4.1-patch.2(typescript@5.1.3): + resolution: {integrity: sha512-lPFYaGt8OdMEzNGJJChF40uYqMO4Z/7Q9fHPQC/NRVtht43KotSXLrkPandVVMf9aPbiJ059eAT+fwHGX16k4w==} peerDependencies: typescript: '*' dependencies: '@volar/language-core': 1.4.1 - typescript: 5.0.4 + typescript: 5.1.3 dev: true - /@volar/vue-language-core@1.6.4: - resolution: {integrity: sha512-1o+cAtN2DIDNAX/HS8rkjZc8wTMTK+zCab/qtYbvEVlmokhZiDrQeoD9/l0Ug7YCNg+mVuMNHKNBY7pX8U2/Jw==} + /@volar/vue-language-core@1.6.5: + resolution: {integrity: sha512-IF2b6hW4QAxfsLd5mePmLgtkXzNi+YnH6ltCd80gb7+cbdpFMjM1I+w+nSg2kfBTyfu+W8useCZvW89kPTBpzg==} dependencies: '@volar/language-core': 1.4.1 '@volar/source-map': 1.4.1 - '@vue/compiler-dom': 3.3.1 - '@vue/compiler-sfc': 3.3.1 - '@vue/reactivity': 3.3.1 - '@vue/shared': 3.3.1 + '@vue/compiler-dom': 3.3.4 + '@vue/compiler-sfc': 3.3.4 + '@vue/reactivity': 3.3.4 + '@vue/shared': 3.3.4 minimatch: 9.0.0 muggle-string: 0.2.2 vue-template-compiler: 2.7.14 dev: true - /@volar/vue-typescript@1.6.4(typescript@5.0.4): - resolution: {integrity: sha512-qKwgP0KVQR/aaH/SN3AP7RB8NnXPWDn3tjyXP6IT6etxkDeZLBLsXWUD9KMak/RvV1DgbXDuz4F9yuZlbt29rA==} + /@volar/vue-typescript@1.6.5(typescript@5.1.3): + resolution: {integrity: sha512-er9rVClS4PHztMUmtPMDTl+7c7JyrxweKSAEe/o/Noeq2bQx6v3/jZHVHBe8ZNUti5ubJL/+Tg8L3bzmlalV8A==} peerDependencies: typescript: '*' dependencies: - '@volar/typescript': 1.4.1(typescript@5.0.4) - '@volar/vue-language-core': 1.6.4 - typescript: 5.0.4 + '@volar/typescript': 1.4.1-patch.2(typescript@5.1.3) + '@volar/vue-language-core': 1.6.5 + typescript: 5.1.3 dev: true - /@vue-macros/common@1.3.1(rollup@3.21.6)(vue@3.3.1): - resolution: {integrity: sha512-Lc5aP/8HNJD1XrnvpeNuWcCf82bZdR3auN/chA1b/1rKZgSnmQkH9f33tKO9qLwXSy+u4hpCi8Rw+oUuF1KCeg==} - engines: {node: '>=14.19.0'} + /@vue-macros/common@1.3.3(rollup@3.23.0)(vue@3.3.4): + resolution: {integrity: sha512-bjHomaf3mu+ARMD4DX22C/lLVVocbmwgcLH7bg1rK4kB5ghesgShZTQIrNR6ZjifQmdGc/2jjZ/25kSb364uEA==} + engines: {node: '>=16.14.0'} peerDependencies: vue: ^2.7.0 || ^3.2.25 peerDependenciesMeta: vue: optional: true dependencies: - '@babel/types': 7.21.5 - '@rollup/pluginutils': 5.0.2(rollup@3.21.6) - '@vue/compiler-sfc': 3.3.1 + '@babel/types': 7.22.4 + '@rollup/pluginutils': 5.0.2(rollup@3.23.0) + '@vue/compiler-sfc': 3.3.4 local-pkg: 0.4.3 magic-string-ast: 0.1.2 - vue: 3.3.1 + vue: 3.3.4 transitivePeerDependencies: - rollup dev: false - /@vue-macros/reactivity-transform@0.3.6(rollup@3.21.6)(vue@3.3.1): - resolution: {integrity: sha512-PFJRXHEdIP03LeNnfcwjUk8pKWjvyeOZjCNwbLgfqunI9tknG4IQMfl86qswK83f+DoOTMCoeMFoUnmlbr+yUw==} - engines: {node: '>=14.19.0'} + /@vue-macros/reactivity-transform@0.3.9(rollup@3.23.0)(vue@3.3.4): + resolution: {integrity: sha512-lzzH2qzIxc1LWRrSR+ax0TVeBTgwTpG9qTZOo4Au+ODgJyXpIWHGCnc9rjcxGfu6LitjZ75NmyjbEnaEkomefw==} + engines: {node: '>=16.14.0'} peerDependencies: vue: ^2.7.0 || ^3.2.25 dependencies: - '@babel/parser': 7.21.8 - '@vue-macros/common': 1.3.1(rollup@3.21.6)(vue@3.3.1) - '@vue/compiler-core': 3.3.1 - '@vue/shared': 3.3.1 + '@babel/parser': 7.22.4 + '@vue-macros/common': 1.3.3(rollup@3.23.0)(vue@3.3.4) + '@vue/compiler-core': 3.3.4 + '@vue/shared': 3.3.4 magic-string: 0.30.0 unplugin: 1.3.1 - vue: 3.3.1 + vue: 3.3.4 transitivePeerDependencies: - rollup dev: false - /@vue/compiler-core@3.3.1: - resolution: {integrity: sha512-5le1qYSBgLWg2jdLrbydlhnPJkkzMw46UrRUvTnOKlfg6pThtm9ohhqBhNPHbr0RcM1MCbK5WZe/3Ghz0SZjpQ==} + /@vue/compiler-core@3.3.4: + resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==} dependencies: - '@babel/parser': 7.21.8 - '@vue/shared': 3.3.1 + '@babel/parser': 7.22.4 + '@vue/shared': 3.3.4 estree-walker: 2.0.2 source-map-js: 1.0.2 - /@vue/compiler-dom@3.3.1: - resolution: {integrity: sha512-VmgIsoLivCft3+oNc5KM7b9wd0nZxP/g2qilMwi1hJyGA624KWnNKHn4hzBQs4FpzydUVpNy+TWVT8KiRCh3MQ==} + /@vue/compiler-dom@3.3.4: + resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==} dependencies: - '@vue/compiler-core': 3.3.1 - '@vue/shared': 3.3.1 + '@vue/compiler-core': 3.3.4 + '@vue/shared': 3.3.4 - /@vue/compiler-sfc@2.7.14: - resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==} + /@vue/compiler-sfc@3.3.4: + resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==} dependencies: '@babel/parser': 7.21.8 - postcss: 8.4.23 - source-map: 0.6.1 - dev: false - - /@vue/compiler-sfc@3.3.1: - resolution: {integrity: sha512-G+FPwBbXSLaA4+Ry5/bdD9Oda+sRslQcE9o6JSZaougRiT4OjVL0vtkbQHPrGRTULZV28OcrAjRfSZOSB0OTXQ==} - dependencies: - '@babel/parser': 7.21.4 - '@vue/compiler-core': 3.3.1 - '@vue/compiler-dom': 3.3.1 - '@vue/compiler-ssr': 3.3.1 - '@vue/reactivity-transform': 3.3.1 - '@vue/shared': 3.3.1 + '@vue/compiler-core': 3.3.4 + '@vue/compiler-dom': 3.3.4 + '@vue/compiler-ssr': 3.3.4 + '@vue/reactivity-transform': 3.3.4 + '@vue/shared': 3.3.4 estree-walker: 2.0.2 magic-string: 0.30.0 postcss: 8.4.23 source-map-js: 1.0.2 - /@vue/compiler-ssr@3.3.1: - resolution: {integrity: sha512-QOQWGNCWuSeyKx4KvWSJlnIMGg+/2oCHgkFUYo7aJ+9Uaaz45yRgKQ+FNigy50NYBQIhpXn2e4OSR8GXh4knrQ==} + /@vue/compiler-ssr@3.3.4: + resolution: {integrity: sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==} dependencies: - '@vue/compiler-dom': 3.3.1 - '@vue/shared': 3.3.1 + '@vue/compiler-dom': 3.3.4 + '@vue/shared': 3.3.4 - /@vue/reactivity-transform@3.3.1: - resolution: {integrity: sha512-MkOrJauAGH4MNdxGW/PmrDegMyOGX0wGIdKUZJRBXOTpotDONg7/TPJe2QeGeBCow/5v9iOqZOWCfvmOWIaDMg==} + /@vue/reactivity-transform@3.3.4: + resolution: {integrity: sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==} dependencies: - '@babel/parser': 7.21.8 - '@vue/compiler-core': 3.3.1 - '@vue/shared': 3.3.1 + '@babel/parser': 7.22.4 + '@vue/compiler-core': 3.3.4 + '@vue/shared': 3.3.4 estree-walker: 2.0.2 magic-string: 0.30.0 - /@vue/reactivity@3.3.1: - resolution: {integrity: sha512-zCfmazOtyUdC1NS/EPiSYJ4RqojqmTAviJyBbyVvY8zAv5NhK44Yfw0E1tt+m5vz0ZO1ptI9jDKBr3MWIEkpgw==} + /@vue/reactivity@3.3.4: + resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==} dependencies: - '@vue/shared': 3.3.1 + '@vue/shared': 3.3.4 - /@vue/runtime-core@3.3.1: - resolution: {integrity: sha512-Ljb37LYafhQqKIasc0r32Cva8gIh6VeSMjlwO6V03tCjHd18gmjP0F4UD+8/a59sGTysAgA8Rb9lIC2DVxRz2Q==} + /@vue/runtime-core@3.3.4: + resolution: {integrity: sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==} dependencies: - '@vue/reactivity': 3.3.1 - '@vue/shared': 3.3.1 + '@vue/reactivity': 3.3.4 + '@vue/shared': 3.3.4 - /@vue/runtime-dom@3.3.1: - resolution: {integrity: sha512-NBjYbQPtMklb7lsJsM2Juv5Ygry6mvZP7PdH1GZqrzfLkvlplQT3qCtQMd/sib6yiy8t9m/Y4hVU7X9nzb9Oeg==} + /@vue/runtime-dom@3.3.4: + resolution: {integrity: sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==} dependencies: - '@vue/runtime-core': 3.3.1 - '@vue/shared': 3.3.1 + '@vue/runtime-core': 3.3.4 + '@vue/shared': 3.3.4 csstype: 3.1.1 - /@vue/server-renderer@3.3.1(vue@3.3.1): - resolution: {integrity: sha512-sod8ggOwbkQXw3lBjfzrbdxRS9lw/lNHoMaXghHawNYowf+4WoaLWD5ouz6fPZadUqNKAsqK95p8DYb1vcVfPA==} + /@vue/server-renderer@3.3.4(vue@3.3.4): + resolution: {integrity: sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==} peerDependencies: - vue: 3.3.1 + vue: 3.3.4 dependencies: - '@vue/compiler-ssr': 3.3.1 - '@vue/shared': 3.3.1 - vue: 3.3.1 + '@vue/compiler-ssr': 3.3.4 + '@vue/shared': 3.3.4 + vue: 3.3.4 - /@vue/shared@3.3.1: - resolution: {integrity: sha512-ybDBtQ+479HL/bkeIOIAwgpeAEACzztkvulJLbK3JMFuTOv4qDivmV3AIsR8RHYJ+RD9tQxcHWBsX4GqEcYrfw==} + /@vue/shared@3.3.4: + resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==} - /@vue/test-utils@2.3.2(vue@3.3.1): + /@vue/test-utils@2.3.2(vue@3.3.4): resolution: {integrity: sha512-hJnVaYhbrIm0yBS0+e1Y0Sj85cMyAi+PAbK4JHqMRUZ6S622Goa+G7QzkRSyvCteG8wop7tipuEbHoZo26wsSA==} peerDependencies: vue: ^3.0.1 dependencies: js-beautify: 1.14.6 - vue: 3.3.1 + vue: 3.3.4 optionalDependencies: - '@vue/compiler-dom': 3.3.1 - '@vue/server-renderer': 3.3.1(vue@3.3.1) + '@vue/compiler-dom': 3.3.4 + '@vue/server-renderer': 3.3.4(vue@3.3.4) dev: true /@webgpu/types@0.1.30: @@ -7461,7 +8654,7 @@ packages: p-limit: 2.3.0 pluralize: 7.0.0 pretty-bytes: 5.6.0 - semver: 7.5.0 + semver: 7.5.1 stream-to-promise: 2.2.0 tar-stream: 2.2.0 treeify: 1.1.0 @@ -7476,7 +8669,7 @@ packages: esbuild: '>=0.10.0' dependencies: esbuild: 0.17.18 - tslib: 2.5.0 + tslib: 2.5.2 dev: true /@yarnpkg/fslib@2.10.2: @@ -7570,13 +8763,6 @@ packages: mime-types: 2.1.35 negotiator: 0.6.3 - /acorn-globals@7.0.1: - resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} - dependencies: - acorn: 8.8.2 - acorn-walk: 8.2.0 - dev: false - /acorn-jsx@5.3.2(acorn@7.4.1): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -7601,6 +8787,7 @@ packages: /acorn-walk@8.2.0: resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} engines: {node: '>=0.4.0'} + dev: true /acorn@7.4.1: resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} @@ -7750,7 +8937,6 @@ packages: engines: {node: '>=4'} dependencies: color-convert: 1.9.3 - dev: true /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} @@ -8015,7 +9201,6 @@ packages: is-nan: 1.3.2 object-is: 1.1.5 util: 0.12.5 - dev: true /assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} @@ -8029,32 +9214,31 @@ packages: resolution: {integrity: sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==} engines: {node: '>=4'} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: true /ast-types@0.15.2: resolution: {integrity: sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==} engines: {node: '>=4'} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: true /ast-types@0.16.1: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} dependencies: - tslib: 2.5.0 - dev: true + tslib: 2.5.2 /astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} dev: true - /astring@1.8.4: - resolution: {integrity: sha512-97a+l2LBU3Op3bBQEff79i/E4jMD2ZLFD8rHx9B6mXyB2uQwhJQYfiDqUwtfjF4QA1F2qs//N6Cw8LetMbQjcw==} + /astring@1.8.6: + resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==} hasBin: true - dev: true + dev: false /async-done@1.3.2: resolution: {integrity: sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==} @@ -8113,8 +9297,8 @@ packages: postcss-value-parser: 3.3.1 dev: false - /autosize@5.0.2: - resolution: {integrity: sha512-FPVt5ynkqUAA9gcMZnJHka1XfQgr1WNd/yRfIjmj5WGmjua+u5Hl9hn8M2nU5CNy2bEIcj1ZUwXq7IOHsfZG9w==} + /autosize@6.0.1: + resolution: {integrity: sha512-f86EjiUKE6Xvczc4ioP1JBlWG7FKrE13qe/DxBCpe8GCipCq2nFw73aO8QEBKHfSbYGDN5eB9jXWKen7tspDqQ==} dev: false /autwh@0.1.0: @@ -8126,7 +9310,6 @@ packages: /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} - dev: true /avvio@8.2.0: resolution: {integrity: sha512-bbCQdg7bpEv6kGH41RO/3B2/GMMmJSo2iBK+X8AWN9mujtfUipMDfIjsgHCfpnKqoGEQrrmCDKSa5OQ19+fDmg==} @@ -8169,12 +9352,12 @@ packages: - debug dev: true - /babel-core@7.0.0-bridge.0(@babel/core@7.21.3): + /babel-core@7.0.0-bridge.0(@babel/core@7.22.1): resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.3 + '@babel/core': 7.22.1 dev: true /babel-jest@29.5.0(@babel/core@7.21.3): @@ -8212,10 +9395,10 @@ packages: resolution: {integrity: sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/template': 7.20.7 - '@babel/types': 7.21.5 + '@babel/template': 7.21.9 + '@babel/types': 7.22.4 '@types/babel__core': 7.20.0 - '@types/babel__traverse': 7.18.3 + '@types/babel__traverse': 7.20.0 dev: true /babel-plugin-polyfill-corejs2@0.3.3(@babel/core@7.21.3): @@ -8223,7 +9406,7 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.21.4 + '@babel/compat-data': 7.22.3 '@babel/core': 7.21.3 '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.21.3) semver: 6.3.0 @@ -8231,6 +9414,19 @@ packages: - supports-color dev: true + /babel-plugin-polyfill-corejs2@0.3.3(@babel/core@7.22.1): + resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.22.3 + '@babel/core': 7.22.1 + '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.22.1) + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + /babel-plugin-polyfill-corejs3@0.6.0(@babel/core@7.21.3): resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==} peerDependencies: @@ -8243,6 +9439,18 @@ packages: - supports-color dev: true + /babel-plugin-polyfill-corejs3@0.6.0(@babel/core@7.22.1): + resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.22.1) + core-js-compat: 3.29.1 + transitivePeerDependencies: + - supports-color + dev: true + /babel-plugin-polyfill-regenerator@0.4.1(@babel/core@7.21.3): resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==} peerDependencies: @@ -8254,6 +9462,17 @@ packages: - supports-color dev: true + /babel-plugin-polyfill-regenerator@0.4.1(@babel/core@7.22.1): + resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.1 + '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.22.1) + transitivePeerDependencies: + - supports-color + dev: true + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.21.3): resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} peerDependencies: @@ -8274,6 +9493,26 @@ packages: '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.21.3) dev: true + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.22.1): + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.22.1 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.1) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.22.1) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.22.1) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.22.1) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.22.1) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.22.1) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.22.1) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.22.1) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.22.1) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.22.1) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.22.1) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.22.1) + dev: true + /babel-preset-jest@29.5.0(@babel/core@7.21.3): resolution: {integrity: sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -8289,7 +9528,7 @@ packages: resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} engines: {node: '>= 10.0.0'} dependencies: - '@babel/types': 7.21.4 + '@babel/types': 7.22.4 /bach@1.2.0: resolution: {integrity: sha512-bZOOfCb3gXBXbTFXq3OZtGR88LwGeJvzu6szttaIzymOTS4ZttBNOWSv7aLZja2EMycKtRYV0Oa8SNKH/zkxvg==} @@ -8361,7 +9600,7 @@ packages: engines: {node: '>=12'} dependencies: bin-version: 6.0.0 - semver: 7.5.0 + semver: 7.5.1 semver-truncate: 2.0.0 dev: false @@ -8507,10 +9746,10 @@ packages: dependencies: fill-range: 7.0.1 - /broadcast-channel@4.20.2: - resolution: {integrity: sha512-v0lJgMzC+MX4e2KCFWYXChZ2mKTqm5mnJGId6tqJp3NfylggbNd8c2uKeP4MQxD2ucKOesY68aN98zwl9d6Tvg==} + /broadcast-channel@5.1.0: + resolution: {integrity: sha512-wAbP+mtQ28N+iX3scX6Q97UN39ER5jRWOtM3r1BNPLWFOMt3AGmwN9kS3fqwgaUW0tbWHRSfTpsT+pAvrzQz0Q==} dependencies: - '@babel/runtime': 7.20.7 + '@babel/runtime': 7.21.0 oblivious-set: 1.1.1 p-queue: 6.6.2 rimraf: 3.0.2 @@ -8609,22 +9848,21 @@ packages: requiresBuild: true dependencies: node-gyp-build: 4.6.0 - dev: false - /bull@4.10.4: - resolution: {integrity: sha512-o9m/7HjS/Or3vqRd59evBlWCXd9Lp+ALppKseoSKHaykK46SmRjAilX98PgmOz1yeVaurt8D5UtvEt4bUjM3eA==} - engines: {node: '>=12'} + /bullmq@3.15.0: + resolution: {integrity: sha512-U0LSRjuoyIBpnE62T4maCWMYEt3qdBCa1lnlPxYKQmRF/Y+FQ9W6iW5JvNNN+NA5Jet7k0uX71a93EX1zGnrhw==} dependencies: - cron-parser: 4.7.1 - debuglog: 1.0.1 - get-port: 5.1.1 + cron-parser: 4.8.1 + glob: 8.1.0 ioredis: 5.3.2 lodash: 4.17.21 - msgpackr: 1.8.1 - semver: 7.5.0 - uuid: 8.3.2 + msgpackr: 1.9.2 + semver: 7.5.1 + tslib: 2.5.2 + uuid: 9.0.0 transitivePeerDependencies: - supports-color + dev: false /busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} @@ -8813,9 +10051,9 @@ packages: /caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} - /cbor@8.1.0: - resolution: {integrity: sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==} - engines: {node: '>=12.19'} + /cbor@9.0.0: + resolution: {integrity: sha512-87cFgOKxjUOnGpNeQMBVER4Mc/rZAk9xC+Ygfx5FLCAUt/tpVHphuZC5fJmp/KSDsEsBEDIPtEt0YbD/GFQw8Q==} + engines: {node: '>=16'} dependencies: nofilter: 3.1.0 @@ -8863,7 +10101,6 @@ packages: ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 supports-color: 5.5.0 - dev: true /chalk@3.0.0: resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} @@ -9022,11 +10259,12 @@ packages: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} - /chromatic@6.17.4: - resolution: {integrity: sha512-vnlvsv2lkp8BVtTn1OumJzqkDk2qB3pcGxEDIfZtVboKtzIPjnIlGa+c1fVKQe8NvHDU8R39k8klqgKHIXUVJw==} + /chromatic@6.18.0: + resolution: {integrity: sha512-Sj7xMFGQ6jSTBrsdgMMjSQAP2OMNogg4GXV4djf4kAp6Dp+pY4FwByIagvbtQRjC33kQVi592FS52vMBOBMEzw==} hasBin: true dependencies: '@discoveryjs/json-ext': 0.5.7 + '@storybook/csf-tools': 7.0.18 '@types/webpack-env': 1.18.0 snyk-nodejs-lockfile-parser: 1.49.0 transitivePeerDependencies: @@ -9209,6 +10447,7 @@ packages: /cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} + dev: false /co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} @@ -9414,7 +10653,7 @@ packages: js-string-escape: 1.0.1 lodash: 4.17.21 md5-hex: 3.0.1 - semver: 7.5.0 + semver: 7.5.1 well-known-symbols: 2.0.0 dev: true @@ -9435,8 +10674,8 @@ packages: /constantinople@4.0.1: resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==} dependencies: - '@babel/parser': 7.21.4 - '@babel/types': 7.21.4 + '@babel/parser': 7.22.4 + '@babel/types': 7.22.4 /content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} @@ -9510,11 +10749,12 @@ packages: readable-stream: 3.6.0 dev: false - /cron-parser@4.7.1: - resolution: {integrity: sha512-WguFaoQ0hQ61SgsCZLHUcNbAvlK0lypKXu62ARguefYmjzaOXIVRNrAmyXzabTwUn4sQvQLkk6bjH+ipGfw8bA==} + /cron-parser@4.8.1: + resolution: {integrity: sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ==} engines: {node: '>=12.0.0'} dependencies: - luxon: 3.2.1 + luxon: 3.3.0 + dev: false /cropperjs@2.0.0-beta.2: resolution: {integrity: sha512-jDRSODDGKmi9vp3p/+WXkxMqV/AE+GpSld1U3cHZDRdLy9UykRzurSe8k1dR0TExn45ygCMrv31qkg+K3EeXXw==} @@ -9536,6 +10776,15 @@ packages: node-fetch: 2.6.7 transitivePeerDependencies: - encoding + dev: true + + /cross-fetch@3.1.6: + resolution: {integrity: sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==} + dependencies: + node-fetch: 2.6.11 + transitivePeerDependencies: + - encoding + dev: false /cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -9639,18 +10888,14 @@ packages: /csstype@3.1.1: resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} - /custom-event-polyfill@1.0.7: - resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==} - dev: false - /cwise-compiler@1.1.3: resolution: {integrity: sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==} dependencies: uniq: 1.0.1 dev: false - /cypress@12.12.0: - resolution: {integrity: sha512-UU5wFQ7SMVCR/hyKok/KmzG6fpZgBHHfrXcHzDmPHWrT+UUetxFzQgt7cxCszlwfozckzwkd22dxMwl/vNkWRw==} + /cypress@12.13.0: + resolution: {integrity: sha512-QJlSmdPk+53Zhy69woJMySZQJoWfEWun3X5OOenGsXjRPVfByVTHorxNehbzhZrEzH9RDUDqVcck0ahtlS+N/Q==} engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0} hasBin: true requiresBuild: true @@ -9692,7 +10937,7 @@ packages: pretty-bytes: 5.6.0 proxy-from-env: 1.0.0 request-progress: 3.0.0 - semver: 7.5.0 + semver: 7.5.1 supports-color: 8.1.1 tmp: 0.2.1 untildify: 4.0.0 @@ -9784,9 +11029,6 @@ packages: ms: 2.1.2 supports-color: 8.1.1 - /debuglog@1.0.1: - resolution: {integrity: sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==} - /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} @@ -9880,6 +11122,7 @@ packages: /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true /deepmerge@4.2.2: resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} @@ -9978,6 +11221,7 @@ packages: /denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} + dev: false /depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} @@ -10240,10 +11484,6 @@ packages: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} dev: false - /entities@4.4.0: - resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} - engines: {node: '>=0.12'} - /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -10351,7 +11591,6 @@ packages: /es6-object-assign@1.1.0: resolution: {integrity: sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==} - dev: true /es6-promise@4.2.8: resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} @@ -10464,6 +11703,7 @@ packages: optionator: 0.8.3 optionalDependencies: source-map: 0.6.1 + dev: true /eslint-formatter-pretty@4.1.0: resolution: {integrity: sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ==} @@ -10518,6 +11758,35 @@ packages: - supports-color dev: true + /eslint-module-utils@2.7.4(@typescript-eslint/parser@5.59.8)(eslint-import-resolver-node@0.3.7)(eslint@8.41.0): + resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 5.59.8(eslint@8.41.0)(typescript@5.1.3) + debug: 3.2.7(supports-color@8.1.1) + eslint: 8.41.0 + eslint-import-resolver-node: 0.3.7 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.5)(eslint@8.40.0): resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} @@ -10551,19 +11820,52 @@ packages: - supports-color dev: true - /eslint-plugin-vue@9.12.0(eslint@8.40.0): - resolution: {integrity: sha512-xH8PgpDW2WwmFSmRfs/3iWogef1CJzQqX264I65zz77jDuxF2yLy7+GA2diUM8ZNATuSl1+UehMQkb5YEyau5w==} + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.8)(eslint@8.41.0): + resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 5.59.8(eslint@8.41.0)(typescript@5.1.3) + array-includes: 3.1.6 + array.prototype.flat: 1.3.1 + array.prototype.flatmap: 1.3.1 + debug: 3.2.7(supports-color@8.1.1) + doctrine: 2.1.0 + eslint: 8.41.0 + eslint-import-resolver-node: 0.3.7 + eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.59.8)(eslint-import-resolver-node@0.3.7)(eslint@8.41.0) + has: 1.0.3 + is-core-module: 2.11.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.values: 1.1.6 + resolve: 1.22.1 + semver: 6.3.0 + tsconfig-paths: 3.14.1 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-plugin-vue@9.14.1(eslint@8.41.0): + resolution: {integrity: sha512-LQazDB1qkNEKejLe/b5a9VfEbtbczcOaui5lQ4Qw0tbRBbQYREyxxOV5BQgNDTqGPs9pxqiEpbMi9ywuIaF7vw==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.40.0) - eslint: 8.40.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.41.0) + eslint: 8.41.0 natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 6.0.11 - semver: 7.5.0 - vue-eslint-parser: 9.2.1(eslint@8.40.0) + semver: 7.5.1 + vue-eslint-parser: 9.3.0(eslint@8.41.0) xml-name-validator: 4.0.0 transitivePeerDependencies: - supports-color @@ -10643,6 +11945,54 @@ packages: - supports-color dev: true + /eslint@8.41.0: + resolution: {integrity: sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.41.0) + '@eslint-community/regexpp': 4.5.0 + '@eslint/eslintrc': 2.0.3 + '@eslint/js': 8.41.0 + '@humanwhocodes/config-array': 0.11.8 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4(supports-color@8.1.1) + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.0 + eslint-visitor-keys: 3.4.1 + espree: 9.5.2 + esquery: 1.4.2 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.19.0 + graphemer: 1.4.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.1 + strip-ansi: 6.0.1 + strip-json-comments: 3.1.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + /espree@9.5.2: resolution: {integrity: sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10685,13 +12035,14 @@ packages: /estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + dev: true /estree-to-babel@3.2.1: resolution: {integrity: sha512-YNF+mZ/Wu2FU/gvmzuWtYc8rloubL7wfXCTgouFrnjGVXPA/EeYYA7pupXWrb3Iv1cTBeSSxxJIbK23l4MRNqg==} engines: {node: '>=8.3.0'} dependencies: - '@babel/traverse': 7.21.3 - '@babel/types': 7.21.5 + '@babel/traverse': 7.22.4 + '@babel/types': 7.22.4 c8: 7.13.0 transitivePeerDependencies: - supports-color @@ -10700,9 +12051,16 @@ packages: /estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.1 + dev: false + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + dev: true /etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} @@ -10712,7 +12070,7 @@ packages: /event-loop-spinner@2.2.0: resolution: {integrity: sha512-KB44sV4Mv7uLIkJHJ5qhiZe5um6th2g57nHQL/uqnPHKP2IswoTRWUteEXTJQL4gW++1zqWUni+H2hGkP51c9w==} dependencies: - tslib: 2.5.0 + tslib: 2.5.2 dev: false /event-stream@3.3.4: @@ -11037,6 +12395,7 @@ packages: /fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true /fast-querystring@1.1.0: resolution: {integrity: sha512-LWkjBCZlxjnSanuPpZ6mHswjy8hQv3VcPJsQB3ltUF2zjvrycr0leP3TSTEEfvQ1WEMSRl5YNsGqaft9bjLqEw==} @@ -11091,7 +12450,7 @@ packages: proxy-addr: 2.0.7 rfdc: 1.3.0 secure-json-parse: 2.7.0 - semver: 7.5.0 + semver: 7.5.1 tiny-lru: 11.0.1 transitivePeerDependencies: - supports-color @@ -11157,7 +12516,6 @@ packages: dependencies: fs-extra: 11.1.0 ramda: 0.28.0 - dev: true /file-type@17.1.6: resolution: {integrity: sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==} @@ -11381,7 +12739,6 @@ packages: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: is-callable: 1.2.7 - dev: true /for-in@1.0.2: resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} @@ -11473,7 +12830,6 @@ packages: graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.0 - dev: true /fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} @@ -11647,6 +13003,7 @@ packages: /get-port@5.1.1: resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} engines: {node: '>=8'} + dev: true /get-stream@3.0.0: resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} @@ -11840,7 +13197,6 @@ packages: /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} - dev: true /globals@13.19.0: resolution: {integrity: sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==} @@ -11876,7 +13232,6 @@ packages: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: get-intrinsic: 1.2.0 - dev: true /got@11.8.5: resolution: {integrity: sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==} @@ -11911,16 +13266,16 @@ packages: p-cancelable: 3.0.0 responselike: 3.0.0 - /graceful-fs@4.2.10: - resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} - dev: false - /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} /grapheme-splitter@1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + /graphql@16.6.0: resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -12041,8 +13396,8 @@ packages: uglify-js: 3.17.4 dev: true - /happy-dom@9.16.0: - resolution: {integrity: sha512-goq7grRjIiV2Svb251LWQOo/xm04za2mJ9+assbZJx1KnaVOX1gZBBp4MHbiFNkR6JW7UL81iCtZxCVu+qU5ng==} + /happy-dom@9.20.3: + resolution: {integrity: sha512-eBsgauT435fXFvQDNcmm5QbGtYzxEzOaX35Ia+h6yP/wwa4xSWZh1CfP+mGby8Hk6Xu59mTkpyf72rUXHNxY7A==} dependencies: css.escape: 1.5.1 entities: 4.5.0 @@ -12088,7 +13443,6 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -12463,8 +13817,9 @@ packages: resolution: {integrity: sha512-/nPtyeX9xPUvxZf+r0518B7uqNKlP+LqNJqSiXFEaa2T71rWIwTVXGH7hB9xO/EVdwa5/pWlFCPwShOW81XIxQ==} dev: false - /install-artifact-from-github@1.3.2: - resolution: {integrity: sha512-yCFcLvqk0yQdxx0uJz4t9Z3adDMLAYrcGYv546uRXCSvxE+GqNYhhz/KmrGcUKGI/gVLR9n/e/zM9jX/+ASMJQ==} + /install-artifact-from-github@1.3.3: + resolution: {integrity: sha512-x79SL0d8WOi1ZjXSTUqqs0GPQZ92YArJAN9O46wgU9wdH2U9ecyyhB9YGDbPe2OLV4ptmt6AZYRQZ2GydQZosQ==} + hasBin: true dev: false /internal-slot@1.0.5: @@ -12500,6 +13855,7 @@ packages: standard-as-callback: 2.1.0 transitivePeerDependencies: - supports-color + dev: false /iota-array@1.0.0: resolution: {integrity: sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==} @@ -12593,7 +13949,6 @@ packages: dependencies: call-bind: 1.0.2 has-tostringtag: 1.0.0 - dev: true /is-array-buffer@3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} @@ -12747,7 +14102,6 @@ packages: engines: {node: '>= 0.4'} dependencies: has-tostringtag: 1.0.0 - dev: true /is-glob@3.1.0: resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==} @@ -12804,7 +14158,6 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - dev: true /is-negated-glob@1.0.0: resolution: {integrity: sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==} @@ -12952,7 +14305,6 @@ packages: for-each: 0.3.3 gopd: 1.0.1 has-tostringtag: 1.0.0 - dev: true /is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} @@ -13036,7 +14388,7 @@ packages: /isomorphic-unfetch@3.1.0: resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==} dependencies: - node-fetch: 2.6.7 + node-fetch: 2.6.11 unfetch: 4.2.0 transitivePeerDependencies: - encoding @@ -13054,8 +14406,8 @@ packages: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} engines: {node: '>=8'} dependencies: - '@babel/core': 7.21.3 - '@babel/parser': 7.21.8 + '@babel/core': 7.22.1 + '@babel/parser': 7.22.4 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 semver: 6.3.0 @@ -13130,7 +14482,7 @@ packages: '@jest/expect': 29.5.0 '@jest/test-result': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 20.1.3 + '@types/node': 20.2.5 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 @@ -13178,7 +14530,7 @@ packages: - ts-node dev: true - /jest-cli@29.5.0(@types/node@20.1.3): + /jest-cli@29.5.0(@types/node@20.2.5): resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -13195,7 +14547,7 @@ packages: exit: 0.1.2 graceful-fs: 4.2.11 import-local: 3.1.0 - jest-config: 29.5.0(@types/node@20.1.3) + jest-config: 29.5.0(@types/node@20.2.5) jest-util: 29.5.0 jest-validate: 29.5.0 prompts: 2.4.2 @@ -13245,7 +14597,7 @@ packages: - supports-color dev: true - /jest-config@29.5.0(@types/node@20.1.3): + /jest-config@29.5.0(@types/node@20.2.5): resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -13260,7 +14612,7 @@ packages: '@babel/core': 7.21.3 '@jest/test-sequencer': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 20.1.3 + '@types/node': 20.2.5 babel-jest: 29.5.0(@babel/core@7.21.3) chalk: 4.1.2 ci-info: 3.7.1 @@ -13329,7 +14681,7 @@ packages: '@jest/environment': 29.5.0 '@jest/fake-timers': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 20.1.3 + '@types/node': 20.2.5 jest-mock: 29.5.0 jest-util: 29.5.0 dev: true @@ -13359,7 +14711,7 @@ packages: dependencies: '@jest/types': 29.5.0 '@types/graceful-fs': 4.1.6 - '@types/node': 20.1.3 + '@types/node': 20.2.5 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -13394,7 +14746,7 @@ packages: resolution: {integrity: sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.21.4 '@jest/types': 29.5.0 '@types/stack-utils': 2.0.1 chalk: 4.1.2 @@ -13410,7 +14762,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 20.1.3 + '@types/node': 20.2.5 dev: true /jest-mock@29.5.0: @@ -13418,7 +14770,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.5.0 - '@types/node': 20.1.3 + '@types/node': 20.2.5 jest-util: 29.5.0 dev: true @@ -13473,7 +14825,7 @@ packages: '@jest/test-result': 29.5.0 '@jest/transform': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 20.1.3 + '@types/node': 20.2.5 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -13504,7 +14856,7 @@ packages: '@jest/test-result': 29.5.0 '@jest/transform': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 20.1.3 + '@types/node': 20.2.5 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 @@ -13527,18 +14879,18 @@ packages: resolution: {integrity: sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/core': 7.21.3 - '@babel/generator': 7.21.3 - '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.3) - '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.21.3) - '@babel/traverse': 7.21.3 - '@babel/types': 7.21.4 + '@babel/core': 7.22.1 + '@babel/generator': 7.22.3 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.22.1) + '@babel/traverse': 7.22.4 + '@babel/types': 7.22.4 '@jest/expect-utils': 29.5.0 '@jest/transform': 29.5.0 '@jest/types': 29.5.0 - '@types/babel__traverse': 7.18.3 + '@types/babel__traverse': 7.20.0 '@types/prettier': 2.7.2 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.21.3) + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.1) chalk: 4.1.2 expect: 29.5.0 graceful-fs: 4.2.11 @@ -13549,7 +14901,7 @@ packages: jest-util: 29.5.0 natural-compare: 1.4.0 pretty-format: 29.5.0 - semver: 7.5.0 + semver: 7.5.1 transitivePeerDependencies: - supports-color dev: true @@ -13559,7 +14911,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.5.0 - '@types/node': 20.1.3 + '@types/node': 20.2.5 chalk: 4.1.2 ci-info: 3.7.1 graceful-fs: 4.2.11 @@ -13584,7 +14936,7 @@ packages: dependencies: '@jest/test-result': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 20.1.3 + '@types/node': 20.2.5 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -13603,7 +14955,7 @@ packages: resolution: {integrity: sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 jest-util: 29.5.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -13629,7 +14981,7 @@ packages: - ts-node dev: true - /jest@29.5.0(@types/node@20.1.3): + /jest@29.5.0(@types/node@20.2.5): resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -13642,7 +14994,7 @@ packages: '@jest/core': 29.5.0 '@jest/types': 29.5.0 import-local: 3.1.0 - jest-cli: 29.5.0(@types/node@20.1.3) + jest-cli: 29.5.0(@types/node@20.2.5) transitivePeerDependencies: - '@types/node' - supports-color @@ -13705,7 +15057,6 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true /js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} @@ -13744,17 +15095,17 @@ packages: peerDependencies: '@babel/preset-env': ^7.1.6 dependencies: - '@babel/core': 7.21.3 - '@babel/parser': 7.21.8 - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.21.3) - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.21.3) - '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.21.3) - '@babel/plugin-transform-modules-commonjs': 7.21.2(@babel/core@7.21.3) - '@babel/preset-env': 7.21.4(@babel/core@7.21.3) - '@babel/preset-flow': 7.18.6(@babel/core@7.21.3) - '@babel/preset-typescript': 7.21.0(@babel/core@7.21.3) - '@babel/register': 7.21.0(@babel/core@7.21.3) - babel-core: 7.0.0-bridge.0(@babel/core@7.21.3) + '@babel/core': 7.22.1 + '@babel/parser': 7.22.4 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.22.1) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.22.1) + '@babel/plugin-transform-modules-commonjs': 7.21.2(@babel/core@7.22.1) + '@babel/preset-env': 7.21.4(@babel/core@7.22.1) + '@babel/preset-flow': 7.18.6(@babel/core@7.22.1) + '@babel/preset-typescript': 7.21.0(@babel/core@7.22.1) + '@babel/register': 7.21.0(@babel/core@7.22.1) + babel-core: 7.0.0-bridge.0(@babel/core@7.22.1) chalk: 4.1.2 flow-parser: 0.202.0 graceful-fs: 4.2.11 @@ -13768,9 +15119,9 @@ packages: - supports-color dev: true - /jsdom@21.1.1: - resolution: {integrity: sha512-Jjgdmw48RKcdAIQyUD1UdBh2ecH7VqwaXPN3ehoZN6MqgVbMn+lRm1aAT1AsdJRAJpwfa4IpwgzySn61h2qu3w==} - engines: {node: '>=14'} + /jsdom@22.1.0(bufferutil@4.0.7)(utf-8-validate@6.0.3): + resolution: {integrity: sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==} + engines: {node: '>=16'} peerDependencies: canvas: ^2.5.0 peerDependenciesMeta: @@ -13778,19 +15129,16 @@ packages: optional: true dependencies: abab: 2.0.6 - acorn: 8.8.2 - acorn-globals: 7.0.1 cssstyle: 3.0.0 data-urls: 4.0.0 decimal.js: 10.4.3 domexception: 4.0.0 - escodegen: 2.0.0 form-data: 4.0.0 html-encoding-sniffer: 3.0.0 http-proxy-agent: 5.0.0 https-proxy-agent: 5.0.1 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.2 + nwsapi: 2.2.5 parse5: 7.1.2 rrweb-cssom: 0.6.0 saxes: 6.0.0 @@ -13801,7 +15149,7 @@ packages: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 12.0.1 - ws: 8.13.0 + ws: 8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil @@ -13818,7 +15166,6 @@ packages: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} hasBin: true - dev: true /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -13888,16 +15235,15 @@ packages: universalify: 2.0.0 optionalDependencies: graceful-fs: 4.2.11 - dev: true - /jsonld@8.1.1: - resolution: {integrity: sha512-TbtV1hlnoDYxbscazbxcS7seDGV+pc0yktxpMySh0OBFvnLw/TIth0jiQtP/9r+ywuCbtj10XjDNBIkRgiyeUg==} + /jsonld@8.2.0: + resolution: {integrity: sha512-qHUa9pn3/cdAZw26HY1Jmy9+sHOxaLrveTRWUcrSDx5apTa20bBTe+X4nzI7dlqc+M5GkwQW6RgRdqO6LF5nkw==} engines: {node: '>=14'} dependencies: - '@digitalbazaar/http-client': 3.2.0 + '@digitalbazaar/http-client': 3.4.1 canonicalize: 1.0.8 lru-cache: 6.0.0 - rdf-canonize: 3.3.0 + rdf-canonize: 3.4.0 transitivePeerDependencies: - web-streams-polyfill dev: false @@ -13989,24 +15335,24 @@ packages: engines: {node: '>=6'} dev: true - /ky-universal@0.10.1(ky@0.30.0): - resolution: {integrity: sha512-r8909k+ELKZAxhVA5c440x22hqw5XcMRwLRbgpPQk4JHy3/ddJnvzcnSo5Ww3HdKdNeS3Y8dBgcIYyVahMa46g==} - engines: {node: '>=14'} + /ky-universal@0.11.0(ky@0.33.3): + resolution: {integrity: sha512-65KyweaWvk+uKKkCrfAf+xqN2/epw1IJDtlyCPxYffFCMR8u1sp2U65NtWpnozYfZxQ6IUzIlvUcw+hQ82U2Xw==} + engines: {node: '>=14.16'} peerDependencies: - ky: '>=0.26.0' - web-streams-polyfill: '>=3.0.1' + ky: '>=0.31.4' + web-streams-polyfill: '>=3.2.1' peerDependenciesMeta: web-streams-polyfill: optional: true dependencies: abort-controller: 3.0.0 - ky: 0.30.0 + ky: 0.33.3 node-fetch: 3.3.1 dev: false - /ky@0.30.0: - resolution: {integrity: sha512-X/u76z4JtDVq10u1JA5UQfatPxgPaVDMYTrgHyiTpGN2z4TMEJkIHsoSBBSg9SWZEIXTKsi9kHgiQ9o3Y/4yog==} - engines: {node: '>=12'} + /ky@0.33.3: + resolution: {integrity: sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==} + engines: {node: '>=14.16'} dev: false /last-run@1.1.1: @@ -14063,6 +15409,7 @@ packages: dependencies: prelude-ls: 1.1.2 type-check: 0.3.2 + dev: true /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} @@ -14135,10 +15482,6 @@ packages: strip-bom: 2.0.0 dev: false - /loadjs@4.2.0: - resolution: {integrity: sha512-AgQGZisAlTPbTEzrHPb6q+NYBMD+DP9uvGSIjSUM5uG+0jG15cb8axWpxuOIqrmQjn6scaaH8JwloiP27b2KXA==} - dev: false - /local-pkg@0.4.3: resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} engines: {node: '>=14'} @@ -14182,6 +15525,7 @@ packages: /lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + dev: false /lodash.difference@4.5.0: resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} @@ -14213,6 +15557,7 @@ packages: /lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + dev: false /lodash.isempty@4.4.0: resolution: {integrity: sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==} @@ -14366,9 +15711,10 @@ packages: engines: {node: '>=16.14'} dev: true - /luxon@3.2.1: - resolution: {integrity: sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==} + /luxon@3.3.0: + resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==} engines: {node: '>=12'} + dev: false /lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} @@ -14529,10 +15875,10 @@ packages: engines: {node: '>= 0.6'} dev: true - /meilisearch@0.32.3: - resolution: {integrity: sha512-EOgfBuRE5SiIPIpEDYe2HO0D7a4z5bexIgaAdJFma/dH5hx1kwO+u/qb2g3qKyjG+iA3l8MlmTj/Xd72uahaAw==} + /meilisearch@0.32.5: + resolution: {integrity: sha512-pVccjGAGP1IDSLg3lx9VhyQjUo7kN8x/HVjSurtb8U24V5/pALpf5H2hj6f60QhJd0Ea4tnGRv8fGr2YqWMo9A==} dependencies: - cross-fetch: 3.1.5 + cross-fetch: 3.1.6 transitivePeerDependencies: - encoding dev: false @@ -14844,24 +16190,27 @@ packages: engines: {node: '>=12.13'} dev: false - /msgpackr-extract@2.2.0: - resolution: {integrity: sha512-0YcvWSv7ZOGl9Od6Y5iJ3XnPww8O7WLcpYMDwX+PAA/uXLDtyw94PJv9GLQV/nnp3cWlDhMoyKZIQLrx33sWog==} + /msgpackr-extract@3.0.2: + resolution: {integrity: sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==} + hasBin: true requiresBuild: true dependencies: - node-gyp-build-optional-packages: 5.0.3 + node-gyp-build-optional-packages: 5.0.7 optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 2.2.0 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 2.2.0 - '@msgpackr-extract/msgpackr-extract-linux-arm': 2.2.0 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 2.2.0 - '@msgpackr-extract/msgpackr-extract-linux-x64': 2.2.0 - '@msgpackr-extract/msgpackr-extract-win32-x64': 2.2.0 + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.2 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.2 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.2 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.2 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.2 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.2 + dev: false optional: true - /msgpackr@1.8.1: - resolution: {integrity: sha512-05fT4J8ZqjYlR4QcRDIhLCYKUOHXk7C/xa62GzMKj74l3up9k2QZ3LgFc6qWdsPHl91QA2WLWqWc8b8t7GLNNw==} + /msgpackr@1.9.2: + resolution: {integrity: sha512-xtDgI3Xv0AAiZWLRGDchyzBwU6aq0rwJ+W+5Y4CZhEWtkl/hJtFFLc+3JtGTw7nz1yquxs7nL8q/yA2aqpflIQ==} optionalDependencies: - msgpackr-extract: 2.2.0 + msgpackr-extract: 3.0.2 + dev: false /msw-storybook-addon@1.8.0(msw@1.2.1): resolution: {integrity: sha512-dw3vZwqjixmiur0vouRSOax7wPSu9Og2Hspy9JZFHf49bZRjwDiLF0Pfn2NXEkGviYJOJiGxS1ejoTiUwoSg4A==} @@ -14869,10 +16218,10 @@ packages: msw: '>=0.35.0 <2.0.0' dependencies: is-node-process: 1.0.1 - msw: 1.2.1(typescript@5.0.4) + msw: 1.2.1(typescript@5.1.3) dev: true - /msw@1.2.1(typescript@5.0.4): + /msw@1.2.1(typescript@5.1.3): resolution: {integrity: sha512-bF7qWJQSmKn6bwGYVPXOxhexTCGD5oJSZg8yt8IBClxvo3Dx/1W0zqE1nX9BSWmzRsCKWfeGWcB/vpqV6aclpw==} engines: {node: '>=14'} hasBin: true @@ -14901,7 +16250,7 @@ packages: path-to-regexp: 6.2.1 strict-event-emitter: 0.4.6 type-fest: 2.19.0 - typescript: 5.0.4 + typescript: 5.1.3 yargs: 17.6.2 transitivePeerDependencies: - encoding @@ -15044,7 +16393,7 @@ packages: resolution: {integrity: sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==} dependencies: '@sinonjs/commons': 2.0.0 - '@sinonjs/fake-timers': 10.0.2 + '@sinonjs/fake-timers': 10.2.0 '@sinonjs/text-encoding': 0.7.2 just-extend: 4.2.1 path-to-regexp: 1.8.0 @@ -15054,7 +16403,7 @@ packages: resolution: {integrity: sha512-eSKV6s+APenqVh8ubJyiu/YhZgxQpGP66ntzUb3lY1xB9ukSRaGnx0AIxI+IM+1+IVYC1oWobgG5L3Lt9ARykQ==} engines: {node: '>=10'} dependencies: - semver: 7.5.0 + semver: 7.5.1 /node-addon-api@5.0.0: resolution: {integrity: sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==} @@ -15083,6 +16432,17 @@ packages: resolution: {integrity: sha512-KIkvH1jl6b3O7es/0ShyCgWLcfXxlBrLBbP3rOr23WArC66IMcU4DeZEeYEOwnopYhawLTn7/y+YtmASe8DFVQ==} dev: true + /node-fetch@2.6.11: + resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + /node-fetch@2.6.7: resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} engines: {node: 4.x || >=6.0.0} @@ -15102,17 +16462,20 @@ packages: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - /node-gyp-build-optional-packages@5.0.3: - resolution: {integrity: sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==} + /node-gyp-build-optional-packages@5.0.7: + resolution: {integrity: sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==} + hasBin: true + dev: false optional: true /node-gyp-build@4.6.0: resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} - dev: false + hasBin: true /node-gyp@9.3.1: resolution: {integrity: sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg==} engines: {node: ^12.13 || ^14.13 || >=16} + hasBin: true dependencies: env-paths: 2.2.1 glob: 7.2.3 @@ -15121,7 +16484,7 @@ packages: nopt: 6.0.0 npmlog: 6.0.2 rimraf: 3.0.2 - semver: 7.5.0 + semver: 7.5.1 tar: 6.1.13 which: 2.0.2 transitivePeerDependencies: @@ -15145,8 +16508,8 @@ packages: is: 3.3.0 dev: false - /nodemailer@6.9.2: - resolution: {integrity: sha512-4+TYaa/e1nIxQfyw/WzNPYTEZ5OvHIDEnmjs4LPmIfccPQN+2CYKmGHjWixn/chzD3bmUTu5FMfpltizMxqzdg==} + /nodemailer@6.9.3: + resolution: {integrity: sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==} engines: {node: '>=6.0.0'} dev: false @@ -15184,7 +16547,7 @@ packages: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.11.0 - semver: 7.5.0 + semver: 7.5.1 validate-npm-package-license: 3.0.4 dev: true @@ -15291,8 +16654,8 @@ packages: engines: {node: '>=0.10.0'} dev: false - /nwsapi@2.2.2: - resolution: {integrity: sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==} + /nwsapi@2.2.5: + resolution: {integrity: sha512-6xpotnECFy/og7tKSBVmUNft7J3jyXAka4XvG6AUhFWRz+Q/Ljus7znJAA3bxColfQLdS+XsjoodtJfCgeTEFQ==} dev: false /oauth-sign@0.9.0: @@ -15334,7 +16697,6 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - dev: true /object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} @@ -15404,6 +16766,10 @@ packages: resolution: {integrity: sha512-Oh+8fK09mgGmAshFdH6hSVco6KZmd1tTwNFWj35OvzdmJTMZtAkbn05zar2iG3v6sDs1JLEtOiBGNb6BHwkb2w==} dev: false + /obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + dev: true + /omggif@1.0.10: resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} dev: false @@ -15479,6 +16845,7 @@ packages: prelude-ls: 1.1.2 type-check: 0.3.2 word-wrap: 1.2.3 + dev: true /optionator@0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} @@ -15684,7 +17051,7 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.21.4 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -15727,7 +17094,7 @@ packages: /parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} dependencies: - entities: 4.4.0 + entities: 4.5.0 /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} @@ -15856,29 +17223,35 @@ packages: /performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} - /pg-connection-string@2.5.0: - resolution: {integrity: sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==} + /pg-cloudflare@1.1.0: + resolution: {integrity: sha512-tGM8/s6frwuAIyRcJ6nWcIvd3+3NmUKIs6OjviIm1HPPFEt5MzQDOTBQyhPWg/m0kCl95M6gA1JaIXtS8KovOA==} + requiresBuild: true + dev: false + optional: true + + /pg-connection-string@2.6.0: + resolution: {integrity: sha512-x14ibktcwlHKoHxx9X3uTVW9zIGR41ZB6QNhHb21OPNdCCO3NaRnpJuwKIQSR4u+Yqjx4HCvy7Hh7VSy1U4dGg==} dev: false /pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - /pg-pool@3.6.0(pg@8.10.0): + /pg-numeric@1.0.2: + resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} + engines: {node: '>=4'} + dev: true + + /pg-pool@3.6.0(pg@8.11.0): resolution: {integrity: sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==} peerDependencies: pg: '>=8.0' dependencies: - pg: 8.10.0 + pg: 8.11.0 dev: false - /pg-protocol@1.5.0: - resolution: {integrity: sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==} - dev: true - /pg-protocol@1.6.0: resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} - dev: false /pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} @@ -15889,9 +17262,23 @@ packages: postgres-bytea: 1.0.0 postgres-date: 1.0.7 postgres-interval: 1.2.0 + dev: false - /pg@8.10.0: - resolution: {integrity: sha512-ke7o7qSTMb47iwzOSaZMfeR7xToFdkE71ifIipOAAaLIM0DYzfOAXlgFFmYUIE2BcJtvnVlGCID84ZzCegE8CQ==} + /pg-types@4.0.1: + resolution: {integrity: sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==} + engines: {node: '>=10'} + dependencies: + pg-int8: 1.0.1 + pg-numeric: 1.0.2 + postgres-array: 3.0.2 + postgres-bytea: 3.0.0 + postgres-date: 2.0.1 + postgres-interval: 3.0.0 + postgres-range: 1.1.3 + dev: true + + /pg@8.11.0: + resolution: {integrity: sha512-meLUVPn2TWgJyLmy7el3fQQVwft4gU5NGyvV0XbD41iU9Jbg8lCH4zexhIkihDzVHJStlt6r088G6/fWeNjhXA==} engines: {node: '>= 8.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -15901,11 +17288,13 @@ packages: dependencies: buffer-writer: 2.0.0 packet-reader: 1.0.0 - pg-connection-string: 2.5.0 - pg-pool: 3.6.0(pg@8.10.0) + pg-connection-string: 2.6.0 + pg-pool: 3.6.0(pg@8.11.0) pg-protocol: 1.6.0 pg-types: 2.2.0 pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.0 dev: false /pgpass@1.0.5: @@ -16302,20 +17691,50 @@ packages: /postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} + dev: false + + /postgres-array@3.0.2: + resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==} + engines: {node: '>=12'} + dev: true /postgres-bytea@1.0.0: resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} engines: {node: '>=0.10.0'} + dev: false + + /postgres-bytea@3.0.0: + resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} + engines: {node: '>= 6'} + dependencies: + obuf: 1.1.2 + dev: true /postgres-date@1.0.7: resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} engines: {node: '>=0.10.0'} + dev: false + + /postgres-date@2.0.1: + resolution: {integrity: sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==} + engines: {node: '>=12'} + dev: true /postgres-interval@1.2.0: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} dependencies: xtend: 4.0.2 + dev: false + + /postgres-interval@3.0.0: + resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} + engines: {node: '>=12'} + dev: true + + /postgres-range@1.1.3: + resolution: {integrity: sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==} + dev: true /prebuild-install@7.1.1: resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} @@ -16338,6 +17757,7 @@ packages: /prelude-ls@1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} engines: {node: '>= 0.8.0'} + dev: true /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} @@ -16755,7 +18175,6 @@ packages: /ramda@0.28.0: resolution: {integrity: sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==} - dev: true /random-seed@0.3.0: resolution: {integrity: sha512-y13xtn3kcTlLub3HKWXxJNeC2qK4mB59evwZ5EkeRlolx+Bp2ztF7LbcZmyCnOqlHQrLnfuNbi1sVmm9lPDlDA==} @@ -16773,10 +18192,6 @@ packages: resolution: {integrity: sha512-9CRCUX/w4+fNMzlYgA8GeJz7BZwBPwaGm3FhAm9Hi50k8wNy2CyiJQa8awygWJay87uVVCV0/FwbLcD6+/A9KQ==} dev: false - /rangetouch@2.0.1: - resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==} - dev: false - /ratelimiter@3.4.1: resolution: {integrity: sha512-5FJbRW/Jkkdk29ksedAfWFkQkhbUrMx3QJGwMKAypeIiQf4yrLW+gtPKZiaWt4zPrtw1uGufOjGO7UGM6VllsQ==} dev: false @@ -16800,18 +18215,18 @@ packages: minimist: 1.2.8 strip-json-comments: 2.0.1 - /rdf-canonize@3.3.0: - resolution: {integrity: sha512-gfSNkMua/VWC1eYbSkVaL/9LQhFeOh0QULwv7Or0f+po8pMgQ1blYQFe1r9Mv2GJZXw88Cz/drnAnB9UlNnHfQ==} + /rdf-canonize@3.4.0: + resolution: {integrity: sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==} engines: {node: '>=12'} dependencies: setimmediate: 1.0.5 dev: false - /re2@1.18.0: - resolution: {integrity: sha512-MoCYZlJ9YUgksND9asyNF2/x532daXU/ARp1UeJbQ5flMY6ryKNEhrWt85aw3YluzOJlC3vXpGgK2a1jb0b4GA==} + /re2@1.19.0: + resolution: {integrity: sha512-y0LcLZgBF3L7mDtNfbghb7dCmChYQO2QsUGklNueAJUH+HAZO8UZUubgNsf6OxRTAQpeE4KMPR7vcpK3+Q+GiA==} requiresBuild: true dependencies: - install-artifact-from-github: 1.3.2 + install-artifact-from-github: 1.3.3 nan: 2.17.0 node-gyp: 9.3.1 transitivePeerDependencies: @@ -16829,12 +18244,12 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /react-docgen-typescript@2.2.2(typescript@5.0.4): + /react-docgen-typescript@2.2.2(typescript@5.1.3): resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} peerDependencies: typescript: '>= 4.3.x' dependencies: - typescript: 5.0.4 + typescript: 5.1.3 dev: true /react-docgen@6.0.0-alpha.3: @@ -16842,8 +18257,8 @@ packages: engines: {node: '>=12.0.0'} hasBin: true dependencies: - '@babel/core': 7.21.3 - '@babel/generator': 7.21.3 + '@babel/core': 7.22.1 + '@babel/generator': 7.22.3 ast-types: 0.14.2 commander: 2.20.3 doctrine: 3.0.0 @@ -17033,7 +18448,7 @@ packages: ast-types: 0.15.2 esprima: 4.0.1 source-map: 0.6.1 - tslib: 2.5.0 + tslib: 2.5.2 dev: true /recast@0.22.0: @@ -17044,7 +18459,7 @@ packages: ast-types: 0.15.2 esprima: 4.0.1 source-map: 0.6.1 - tslib: 2.5.0 + tslib: 2.5.2 dev: true /recast@0.23.1: @@ -17055,8 +18470,7 @@ packages: ast-types: 0.16.1 esprima: 4.0.1 source-map: 0.6.1 - tslib: 2.5.0 - dev: true + tslib: 2.5.2 /rechoir@0.6.2: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} @@ -17079,6 +18493,7 @@ packages: /redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} + dev: false /redis-info@3.1.0: resolution: {integrity: sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==} @@ -17096,6 +18511,7 @@ packages: engines: {node: '>=4'} dependencies: redis-errors: 1.2.0 + dev: false /redis@4.5.1: resolution: {integrity: sha512-oxXSoIqMJCQVBTfxP6BNTCtDMyh9G6Vi5wjdPdV/sRKkufyZslDqCScSGcOr6XGR/reAWZefz7E4leM31RgdBA==} @@ -17444,8 +18860,8 @@ packages: seedrandom: 2.4.2 dev: false - /rollup@3.21.6: - resolution: {integrity: sha512-SXIICxvxQxR3D4dp/3LDHZIJPC8a4anKMHd4E3Jiz2/JnY+2bEjqrOokAauc5ShGVNFHlEFjBXAXlaxkJqIqSg==} + /rollup@3.23.0: + resolution: {integrity: sha512-h31UlwEi7FHihLe1zbk+3Q7z1k/84rb9BSwmBSr/XjOCEaBJ2YyedQDuM0t/kfOS0IxM+vk1/zI9XxYj9V+NJQ==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: @@ -17617,6 +19033,14 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 + dev: true + + /semver@7.5.1: + resolution: {integrity: sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} @@ -17708,7 +19132,7 @@ packages: detect-libc: 2.0.1 node-addon-api: 5.0.0 prebuild-install: 7.1.1 - semver: 7.5.0 + semver: 7.5.1 simple-get: 4.0.1 tar-fs: 2.1.1 tunnel-agent: 0.6.0 @@ -17723,7 +19147,7 @@ packages: detect-libc: 2.0.1 node-addon-api: 6.1.0 prebuild-install: 7.1.1 - semver: 7.5.0 + semver: 7.5.1 simple-get: 4.0.1 tar-fs: 2.1.1 tunnel-agent: 0.6.0 @@ -17816,8 +19240,8 @@ packages: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true - /slacc-android-arm-eabi@0.0.7: - resolution: {integrity: sha512-6TikZlR1jsQscxwphhrf0U4xbsRy6zKJ0zmEULopTzbohgo5OLdZ7L3tQazkYlaaFe3YjGnVLW3FfGhhrajVog==} + /slacc-android-arm-eabi@0.0.9: + resolution: {integrity: sha512-T5P5kJ5UwW3UMoPXqqHh9TpCnuCJDCoivoiuONDXrYPYKF8sKDPMVVg1x/KU9/m7e562x9vAMBrIyqFFbEW0Jw==} engines: {node: '>= 10'} cpu: [arm] os: [android] @@ -17825,8 +19249,8 @@ packages: dev: false optional: true - /slacc-android-arm64@0.0.7: - resolution: {integrity: sha512-aol/9Rg0Hfqu81hpK+HXcx9sGYu4qqYU+djBCgLtb8I6ZMdWUdE0dp8ACBoTOmYn34hYGcUu4FlJUZ8r7Utucg==} + /slacc-android-arm64@0.0.9: + resolution: {integrity: sha512-bcKB3ukcI5wWJa2clK/5cy6a4TKp51DRkdRuFgKLG05gBj1jbH+7+8iBPojljeY28LC2frmwVHGj3vDmkFUeYg==} engines: {node: '>= 10'} cpu: [arm64] os: [android] @@ -17834,8 +19258,8 @@ packages: dev: false optional: true - /slacc-darwin-arm64@0.0.7: - resolution: {integrity: sha512-PkV7rO/c9AImNYDacP+kxtOjVuxjy06IIOAxbWerIWvoeqsCNRtiF/dh+OqIACRFBuHIDe0oAyUCEMGUTnzjyQ==} + /slacc-darwin-arm64@0.0.9: + resolution: {integrity: sha512-EspX0Hj6t0Afxbsyc6rY9mTOUQQrPVtWPwwNRaljGRorPyRDDefrU1OnJXRcwcIp0oCZrRrivRYlO7lai63EMw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -17843,16 +19267,16 @@ packages: dev: false optional: true - /slacc-darwin-universal@0.0.7: - resolution: {integrity: sha512-Y9zXpL40m4Yq3dE5vdnAgfmn0Fxc0Bf0ixC9TSl96gKeIZEd6drkjfpHFdsIDNImzOksIAUo0HHiDdbEfE7zdQ==} + /slacc-darwin-universal@0.0.9: + resolution: {integrity: sha512-oQySg+9MPyKI9rwwwhmSZQkPks2/rq3k1P5HKwUgnaFZDvDtS/hpDycB3BxSDqWdD5kVA8PLCVa8pt9T5KyKfg==} engines: {node: '>= 10'} os: [darwin] requiresBuild: true dev: false optional: true - /slacc-darwin-x64@0.0.7: - resolution: {integrity: sha512-yKaGjX2YJl1QHe4NgqQVsY83jees3hjFxEUPoKpuZEQzWbMNn0XSyceFRGXIk1oDqiKU40UcsdcCedjYjSEd0Q==} + /slacc-darwin-x64@0.0.9: + resolution: {integrity: sha512-9Xp7mVKKF2QvDiIZOBgwsDdL/+95KBiFTdbo+XtH6YKoh6zNw0aPpkA3JojsdSMYdGHUrxl8b7avhzI0USqeEg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -17860,8 +19284,17 @@ packages: dev: false optional: true - /slacc-linux-arm-gnueabihf@0.0.7: - resolution: {integrity: sha512-pdWMdQeX6uA9JfSoWo9EHH0yRiwXKMbaKoS9gflDSyt/hjeR3Qx/KK7Wihd7HeXx7njlNdpr9ycTRmm5NgapQQ==} + /slacc-freebsd-x64@0.0.9: + resolution: {integrity: sha512-jRd8WmXZLU2mcxV7SN8CzZzGiwbpxtaTjLwrYMTryQZ2TFr1xd1r5mQfTN5sBiwu3tnyK5dmHnRAPy+215mOkQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /slacc-linux-arm-gnueabihf@0.0.9: + resolution: {integrity: sha512-nhP6+jgd30sq+zFxFW7fGhnPwCfCCU0l1JKk3ORGFMl7wH7ippTDd1xGapKq7N+zgdgURbyj83P3wWb2gcRZ1w==} engines: {node: '>= 10'} cpu: [arm] os: [linux] @@ -17869,8 +19302,8 @@ packages: dev: false optional: true - /slacc-linux-arm64-gnu@0.0.7: - resolution: {integrity: sha512-hz9TK/w6fxeNZXyFzuLq5cJD/XRyJbo6BaIdW+VrKKnb9nkLnWlqDQtdtJk7Fw7zHjdY3Uqufjwm0iT6qBVpUQ==} + /slacc-linux-arm64-gnu@0.0.9: + resolution: {integrity: sha512-x7v0rDe0KNVe1Hl6/XCtkCpqdT283pyVaUmk+af0AnoesNRjYEK8DBc8i53N4nhotionHzPIZfu5gPAFkf6RhA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -17878,8 +19311,8 @@ packages: dev: false optional: true - /slacc-linux-arm64-musl@0.0.7: - resolution: {integrity: sha512-wCDAYL7e+lh3XL7g87Ui/Bb2Ap9GcBqeJuj2yHIx6MYC8ontwFSXhqRTmd2zmPLmZA5Nc11aKGN11YNu0Pnwlw==} + /slacc-linux-arm64-musl@0.0.9: + resolution: {integrity: sha512-jyq/ylITHIXTQX5ZqAbi7Mn5SdRgYJi+uEoUCi5UhoXb9LjpNzhkFuY29Je3IkVIIV7AEcAxIlvjdymXdzcF5w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -17887,8 +19320,8 @@ packages: dev: false optional: true - /slacc-linux-x64-gnu@0.0.7: - resolution: {integrity: sha512-E5+2cveizpfHXCk/Hu5VfslWFeDVw47nywODiJ8CsofT2l5ITfYPMFEBXm9ORY25mGBTgsO6lJYiF9Hz4FlS9Q==} + /slacc-linux-x64-gnu@0.0.9: + resolution: {integrity: sha512-Xs/F81H7cKhlIBigFID6CJlgjy0NeDUGV1CI1MI5mSVHsVI8dUO8zXWETjo6o8krJPgfjT5Jd4tAgvUFct5hng==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -17896,8 +19329,8 @@ packages: dev: false optional: true - /slacc-win32-arm64-msvc@0.0.7: - resolution: {integrity: sha512-3a+qnkZbP+Pr5RZuzd0Vi1uCal137QiJajRAWT4r7qwu+Zidd50x2oikQ4rAegqZVTm8qTwVmWA+WmH8WHI7iw==} + /slacc-win32-arm64-msvc@0.0.9: + resolution: {integrity: sha512-C+H0VkKbEEnRbcXRIG5rIaXlg7IZw3o1BbvqA71B8ouQRCu/dNRuH9EQsOYXWltndY42zZi8IupNIwydTUg+Mg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -17905,8 +19338,8 @@ packages: dev: false optional: true - /slacc-win32-x64-msvc@0.0.7: - resolution: {integrity: sha512-ydFdZ7wEXQPsw2Tg+yG9uJdCGTehyPtrWBVUMa7fojr3j1gbtThXS2l9Ad/6fYYi2VwdaYPLWbwV3GYElPGL8g==} + /slacc-win32-x64-msvc@0.0.9: + resolution: {integrity: sha512-bElMnBbeMatCtVp2/+hBS6Z+846nQImEul9nBEr4gfezHotOM6MqR6PI7UQQzGhozpgwiDg2l1ub1MdOIgYizg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -17914,21 +19347,22 @@ packages: dev: false optional: true - /slacc@0.0.7: - resolution: {integrity: sha512-rwi2F3oJaGPST9JdCoUd5fnSZaoZFgTL00GFKhKufT48uwtUEAHlOL0t8gEVmon71X+53f9nEdsGWhwtOutJTQ==} + /slacc@0.0.9: + resolution: {integrity: sha512-BwhjD3daQB3VIx7GxkComMYrnkWuMt4YmDAueMMchblfUUBbP8EcuonJ1Bz9nqtRn1mAH2YPrrRDP95akM+ZuQ==} engines: {node: '>= 10'} optionalDependencies: - slacc-android-arm-eabi: 0.0.7 - slacc-android-arm64: 0.0.7 - slacc-darwin-arm64: 0.0.7 - slacc-darwin-universal: 0.0.7 - slacc-darwin-x64: 0.0.7 - slacc-linux-arm-gnueabihf: 0.0.7 - slacc-linux-arm64-gnu: 0.0.7 - slacc-linux-arm64-musl: 0.0.7 - slacc-linux-x64-gnu: 0.0.7 - slacc-win32-arm64-msvc: 0.0.7 - slacc-win32-x64-msvc: 0.0.7 + slacc-android-arm-eabi: 0.0.9 + slacc-android-arm64: 0.0.9 + slacc-darwin-arm64: 0.0.9 + slacc-darwin-universal: 0.0.9 + slacc-darwin-x64: 0.0.9 + slacc-freebsd-x64: 0.0.9 + slacc-linux-arm-gnueabihf: 0.0.9 + slacc-linux-arm64-gnu: 0.0.9 + slacc-linux-arm64-musl: 0.0.9 + slacc-linux-x64-gnu: 0.0.9 + slacc-win32-arm64-msvc: 0.0.9 + slacc-win32-x64-msvc: 0.0.9 dev: false /slash@3.0.0: @@ -18020,7 +19454,7 @@ packages: lodash.topairs: 4.3.0 micromatch: 4.0.5 p-map: 4.0.0 - semver: 7.5.0 + semver: 7.5.1 snyk-config: 5.1.0 tslib: 1.14.1 uuid: 8.3.2 @@ -18204,6 +19638,7 @@ packages: /standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + dev: false /start-server-and-test@2.0.0: resolution: {integrity: sha512-UqKLw0mJbfrsG1jcRLTUlvuRi9sjNuUiDOLI42r7R5fA9dsFoywAy9DoLXNYys9B886E4RCKb+qM1Gzu96h7DQ==} @@ -18248,11 +19683,11 @@ packages: resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==} dev: true - /storybook@7.0.10: - resolution: {integrity: sha512-L36+Um+Ra8AKTvv84ODFJfuthmWnR1Lc6pjslcb8LYO+PVlqEOeqSknmTcKntDYwgvKx5lg62urtJxzGdwO0yw==} + /storybook@7.0.18: + resolution: {integrity: sha512-FXMmTiomSlLPTHty7vGLr0prPf6pCV07EwAmNOYYYTskitEYV0R7hlhawByd7HuobjIhHvSTKesa1Whl86zLNA==} hasBin: true dependencies: - '@storybook/cli': 7.0.10 + '@storybook/cli': 7.0.18 transitivePeerDependencies: - bufferutil - encoding @@ -18516,7 +19951,6 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -18570,8 +20004,8 @@ packages: resolution: {integrity: sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==} dev: true - /systeminformation@5.17.12: - resolution: {integrity: sha512-I3pfMW2vue53u+X08BNxaJieaHkRoMMKjWetY9lbYJeWFaeWPO6P4FkNc4XOCX8F9vbQ0HqQ25RJoz3U/B7liw==} + /systeminformation@5.17.16: + resolution: {integrity: sha512-dl2QLa7yp9QbBl9um+51CAr3p/40tbz+f34X1lUXkk1SnDcNeJR2iWu/8HD7GM2yRukmy3RCRXFYcPZs0lCs0Q==} engines: {node: '>=8.0.0'} os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] hasBin: true @@ -18708,8 +20142,8 @@ packages: real-require: 0.2.0 dev: false - /three@0.151.3: - resolution: {integrity: sha512-+vbuqxFy8kzLeO5MgpBHUvP/EAiecaDwDuOPPDe6SbrZr96kccF0ktLngXc7xA7bzyd3N0t2f6mw3Z9y6JCojQ==} + /three@0.153.0: + resolution: {integrity: sha512-OCP2/uQR6GcDpSLnJt/3a4mdS0kNWcbfUXIwLoEMgLzEUIVIYsSDwskpmOii/AkDM+BBwrl6+CKgrjX9+E2aWg==} dev: false /throttle-debounce@5.0.0: @@ -18767,8 +20201,8 @@ packages: engines: {node: '>=12'} dev: false - /tinybench@2.4.0: - resolution: {integrity: sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg==} + /tinybench@2.5.0: + resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} dev: true /tinycolor2@1.6.0: @@ -18922,7 +20356,6 @@ packages: /ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} - dev: true /ts-map@1.0.3: resolution: {integrity: sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==} @@ -18978,6 +20411,9 @@ packages: /tslib@2.5.0: resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} + /tslib@2.5.2: + resolution: {integrity: sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==} + /tsutils@3.21.0(typescript@5.0.4): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -18988,6 +20424,16 @@ packages: typescript: 5.0.4 dev: true + /tsutils@3.21.0(typescript@5.1.3): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 5.1.3 + dev: true + /tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} dependencies: @@ -19010,6 +20456,7 @@ packages: engines: {node: '>= 0.8.0'} dependencies: prelude-ls: 1.1.2 + dev: true /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} @@ -19055,7 +20502,6 @@ packages: /type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} - dev: true /type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} @@ -19073,16 +20519,10 @@ packages: resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==} dev: false - /typedarray-to-buffer@3.1.5: - resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} - dependencies: - is-typedarray: 1.0.0 - dev: false - /typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - /typeorm@0.3.16(ioredis@5.3.2)(pg@8.10.0): + /typeorm@0.3.16(ioredis@5.3.2)(pg@8.11.0): resolution: {integrity: sha512-wJ4Qy1oqRKNDdZiBTTaVMqwo/XxC52Q7uNPTjltPgLhvIW173bL6Iad0lhptMOsFlpixFPaUu3PNziaRBwX2Zw==} engines: {node: '>= 12.9.0'} hasBin: true @@ -19151,7 +20591,7 @@ packages: glob: 8.1.0 ioredis: 5.3.2 mkdirp: 2.1.6 - pg: 8.10.0 + pg: 8.11.0 reflect-metadata: 0.1.13 sha.js: 2.4.11 tslib: 2.5.0 @@ -19171,6 +20611,12 @@ packages: resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} engines: {node: '>=12.20'} hasBin: true + dev: true + + /typescript@5.1.3: + resolution: {integrity: sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==} + engines: {node: '>=14.17'} + hasBin: true /ufo@1.1.1: resolution: {integrity: sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==} @@ -19230,16 +20676,16 @@ packages: undertaker-registry: 1.0.1 dev: false - /undici@5.16.0: - resolution: {integrity: sha512-KWBOXNv6VX+oJQhchXieUznEmnJMqgXMbs0xxH2t8q/FUAWSJvOSr/rMaZKnX5RIVq7JDn0JbP4BOnKG2SGXLQ==} + /undici@5.21.0: + resolution: {integrity: sha512-HOjK8l6a57b2ZGXOcUsI5NLfoTrfmbOl90ixJDl0AEFG4wgHNDQxtZy15/ZQp7HhjkpaGlp/eneMgtsu1dIlUA==} engines: {node: '>=12.18'} dependencies: busboy: 1.6.0 dev: false - /undici@5.21.0: - resolution: {integrity: sha512-HOjK8l6a57b2ZGXOcUsI5NLfoTrfmbOl90ixJDl0AEFG4wgHNDQxtZy15/ZQp7HhjkpaGlp/eneMgtsu1dIlUA==} - engines: {node: '>=12.18'} + /undici@5.22.1: + resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==} + engines: {node: '>=14.0'} dependencies: busboy: 1.6.0 dev: false @@ -19347,7 +20793,6 @@ packages: /universalify@2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} - dev: true /unload@2.4.1: resolution: {integrity: sha512-IViSAm8Z3sRBYA+9wc0fLQmU9Nrxb16rcDmIiR6Y9LJSZzI7QY5QsDhqPpKOjAn0O9/kfK1TfNEMMAGPTIraPw==} @@ -19388,8 +20833,8 @@ packages: engines: {node: '>=8'} dev: true - /unzipper@0.10.11: - resolution: {integrity: sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==} + /unzipper@0.10.14: + resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} dependencies: big-integer: 1.6.51 binary: 0.3.0 @@ -19397,7 +20842,7 @@ packages: buffer-indexof-polyfill: 1.0.2 duplexer2: 0.1.4 fstream: 1.0.12 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 listenercount: 1.0.1 readable-stream: 2.3.7 setimmediate: 1.0.5 @@ -19430,10 +20875,6 @@ packages: requires-port: 1.0.0 dev: false - /url-polyfill@1.1.12: - resolution: {integrity: sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A==} - dev: false - /urlsafe-base64@1.0.0: resolution: {integrity: sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA==} dev: false @@ -19453,13 +20894,12 @@ packages: resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} engines: {node: '>=0.10.0'} - /utf-8-validate@5.0.10: - resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} + /utf-8-validate@6.0.3: + resolution: {integrity: sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==} engines: {node: '>=6.14.2'} requiresBuild: true dependencies: node-gyp-build: 4.6.0 - dev: false /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -19472,7 +20912,6 @@ packages: is-generator-function: 1.0.10 is-typed-array: 1.1.10 which-typed-array: 1.1.9 - dev: true /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} @@ -19598,8 +21037,8 @@ packages: replace-ext: 1.0.1 dev: false - /vite-node@0.31.0(@types/node@20.1.3)(sass@1.62.1): - resolution: {integrity: sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==} + /vite-node@0.31.4(@types/node@20.2.5)(sass@1.62.1): + resolution: {integrity: sha512-uzL377GjJtTbuc5KQxVbDu2xfU/x0wVjUtXQR2ihS21q/NK6ROr4oG0rsSkBBddZUVCwzfx22in76/0ZZHXgkQ==} engines: {node: '>=v14.18.0'} hasBin: true dependencies: @@ -19608,7 +21047,7 @@ packages: mlly: 1.2.0 pathe: 1.1.0 picocolors: 1.0.0 - vite: 4.3.5(@types/node@20.1.3)(sass@1.62.1) + vite: 4.3.9(@types/node@20.2.5)(sass@1.62.1) transitivePeerDependencies: - '@types/node' - less @@ -19623,8 +21062,8 @@ packages: resolution: {integrity: sha512-irjKcKXRn7v5bPAg4mAbsS6DgibpP1VUFL9tlgxU6lloK6V9yw9qCZkS+s2PtbkZpWNzr3TN3zVJAc6J7gJZmA==} dev: true - /vite@4.3.5(@types/node@20.1.3)(sass@1.62.1): - resolution: {integrity: sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==} + /vite@4.3.9(@types/node@20.2.5)(sass@1.62.1): + resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: @@ -19648,28 +21087,28 @@ packages: terser: optional: true dependencies: - '@types/node': 20.1.3 + '@types/node': 20.2.5 esbuild: 0.17.18 postcss: 8.4.23 - rollup: 3.21.6 + rollup: 3.23.0 sass: 1.62.1 optionalDependencies: fsevents: 2.3.2 - /vitest-fetch-mock@0.2.2(vitest@0.31.0): + /vitest-fetch-mock@0.2.2(vitest@0.31.4): resolution: {integrity: sha512-XmH6QgTSjCWrqXoPREIdbj40T7i1xnGmAsTAgfckoO75W1IEHKR8hcPCQ7SO16RsdW1t85oUm6pcQRLeBgjVYQ==} engines: {node: '>=14.14.0'} peerDependencies: vitest: '>=0.16.0' dependencies: cross-fetch: 3.1.5 - vitest: 0.31.0(happy-dom@9.16.0)(sass@1.62.1) + vitest: 0.31.4(happy-dom@9.20.3)(sass@1.62.1) transitivePeerDependencies: - encoding dev: true - /vitest@0.31.0(happy-dom@9.16.0)(sass@1.62.1): - resolution: {integrity: sha512-JwWJS9p3GU9GxkG7eBSmr4Q4x4bvVBSswaCFf1PBNHiPx00obfhHRJfgHcnI0ffn+NMlIh9QGvG75FlaIBdKGA==} + /vitest@0.31.4(happy-dom@9.20.3)(sass@1.62.1): + resolution: {integrity: sha512-GoV0VQPmWrUFOZSg3RpQAPN+LPmHg2/gxlMNJlyxJihkz6qReHDV6b0pPDcqFLNEPya4tWJ1pgwUNP9MLmUfvQ==} engines: {node: '>=v14.18.0'} hasBin: true peerDependencies: @@ -19699,31 +21138,31 @@ packages: webdriverio: optional: true dependencies: - '@types/chai': 4.3.4 + '@types/chai': 4.3.5 '@types/chai-subset': 1.3.3 - '@types/node': 20.1.3 - '@vitest/expect': 0.31.0 - '@vitest/runner': 0.31.0 - '@vitest/snapshot': 0.31.0 - '@vitest/spy': 0.31.0 - '@vitest/utils': 0.31.0 + '@types/node': 20.2.5 + '@vitest/expect': 0.31.4 + '@vitest/runner': 0.31.4 + '@vitest/snapshot': 0.31.4 + '@vitest/spy': 0.31.4 + '@vitest/utils': 0.31.4 acorn: 8.8.2 acorn-walk: 8.2.0 cac: 6.7.14 chai: 4.3.7 concordance: 5.0.4 debug: 4.3.4(supports-color@8.1.1) - happy-dom: 9.16.0 + happy-dom: 9.20.3 local-pkg: 0.4.3 magic-string: 0.30.0 pathe: 1.1.0 picocolors: 1.0.0 std-env: 3.3.2 strip-literal: 1.0.1 - tinybench: 2.4.0 + tinybench: 2.5.0 tinypool: 0.5.0 - vite: 4.3.5(@types/node@20.1.3)(sass@1.62.1) - vite-node: 0.31.0(@types/node@20.1.3)(sass@1.62.1) + vite: 4.3.9(@types/node@20.2.5)(sass@1.62.1) + vite-node: 0.31.4(@types/node@20.2.5)(sass@1.62.1) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -19738,64 +21177,61 @@ packages: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} - /vue-docgen-api@4.64.1(vue@3.3.1): + /vue-component-type-helpers@1.6.5: + resolution: {integrity: sha512-iGdlqtajmiqed8ptURKPJ/Olz0/mwripVZszg6tygfZSIL9kYFPJTNY6+Q6OjWGznl2L06vxG5HvNvAnWrnzbg==} + dev: true + + /vue-docgen-api@4.64.1(vue@3.3.4): resolution: {integrity: sha512-jbOf7ByE3Zvtuk+429Jorl+eIeh2aB2Fx1GUo3xJd1aByJWE8KDlSEa6b11PB1ze8f0sRUBraRDinICCk0KY7g==} dependencies: - '@babel/parser': 7.21.8 - '@babel/types': 7.21.4 - '@vue/compiler-dom': 3.3.1 - '@vue/compiler-sfc': 3.3.1 + '@babel/parser': 7.22.4 + '@babel/types': 7.22.4 + '@vue/compiler-dom': 3.3.4 + '@vue/compiler-sfc': 3.3.4 ast-types: 0.14.2 hash-sum: 2.0.0 lru-cache: 8.0.4 pug: 3.0.2 recast: 0.22.0 ts-map: 1.0.3 - vue-inbrowser-compiler-independent-utils: 4.64.1(vue@3.3.1) + vue-inbrowser-compiler-independent-utils: 4.64.1(vue@3.3.4) transitivePeerDependencies: - vue dev: true - /vue-eslint-parser@9.2.1(eslint@8.40.0): - resolution: {integrity: sha512-tPOex4n6jit4E7h68auOEbDMwE58XiP4dylfaVTCOVCouR45g+QFDBjgIdEU52EXJxKyjgh91dLfN2rxUcV0bQ==} + /vue-eslint-parser@9.3.0(eslint@8.41.0): + resolution: {integrity: sha512-48IxT9d0+wArT1+3wNIy0tascRoywqSUe2E1YalIC1L8jsUGe5aJQItWfRok7DVFGz3UYvzEI7n5wiTXsCMAcQ==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' dependencies: debug: 4.3.4(supports-color@8.1.1) - eslint: 8.40.0 + eslint: 8.41.0 eslint-scope: 7.2.0 eslint-visitor-keys: 3.4.1 espree: 9.5.2 esquery: 1.4.2 lodash: 4.17.21 - semver: 7.5.0 + semver: 7.5.1 transitivePeerDependencies: - supports-color dev: true - /vue-inbrowser-compiler-independent-utils@4.64.1(vue@3.3.1): + /vue-inbrowser-compiler-independent-utils@4.64.1(vue@3.3.4): resolution: {integrity: sha512-Hn32n07XZ8j9W8+fmOXPQL+i+W2e/8i6mkH4Ju3H6nR0+cfvmWM95GhczYi5B27+Y8JlCKgAo04IUiYce4mKAw==} peerDependencies: vue: '>=2' dependencies: - vue: 3.3.1 + vue: 3.3.4 dev: true - /vue-plyr@7.0.0: - resolution: {integrity: sha512-NvbO/ZzV1IxlBQQbQlon5Sk8hKuGAj3k4k0XVdi7gM4oSqu8mZMhJ3WM3FfAtNfV790jbLnb8P3dHYqaBqIv6g==} - dependencies: - plyr: github.com/sampotts/plyr/d434c9af16e641400aaee93188594208d88f2658 - vue: 2.7.14 - dev: false - - /vue-prism-editor@2.0.0-alpha.2(vue@3.3.1): + /vue-prism-editor@2.0.0-alpha.2(vue@3.3.4): resolution: {integrity: sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==} engines: {node: '>=10'} peerDependencies: vue: ^3.0.0 dependencies: - vue: 3.3.1 + vue: 3.3.4 dev: false /vue-template-compiler@2.7.14: @@ -19805,41 +21241,34 @@ packages: he: 1.2.0 dev: true - /vue-tsc@1.6.4(typescript@5.0.4): - resolution: {integrity: sha512-8rg8S1AhRJ6/WriENQEhyqH5wsxSxuD5iaD+QnkZn2ArZ6evlhqfBAIcVN8mfSyCV9DeLkQXkOSv/MaeJiJPAQ==} + /vue-tsc@1.6.5(typescript@5.1.3): + resolution: {integrity: sha512-Wtw3J7CC+JM2OR56huRd5iKlvFWpvDiU+fO1+rqyu4V2nMTotShz4zbOZpW5g9fUOcjnyZYfBo5q5q+D/q27JA==} hasBin: true peerDependencies: typescript: '*' dependencies: - '@volar/vue-language-core': 1.6.4 - '@volar/vue-typescript': 1.6.4(typescript@5.0.4) - semver: 7.5.0 - typescript: 5.0.4 + '@volar/vue-language-core': 1.6.5 + '@volar/vue-typescript': 1.6.5(typescript@5.1.3) + semver: 7.5.1 + typescript: 5.1.3 dev: true - /vue@2.7.14: - resolution: {integrity: sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==} + /vue@3.3.4: + resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==} dependencies: - '@vue/compiler-sfc': 2.7.14 - csstype: 3.1.1 - dev: false + '@vue/compiler-dom': 3.3.4 + '@vue/compiler-sfc': 3.3.4 + '@vue/runtime-dom': 3.3.4 + '@vue/server-renderer': 3.3.4(vue@3.3.4) + '@vue/shared': 3.3.4 - /vue@3.3.1: - resolution: {integrity: sha512-3Rwy4I5idbPVSDZu6I+fFh6tdDSZbauImCTqLxE7y0LpHtiDvPeY01OI7RkFPbva1nk4hoO0sv/NzosH2h60sg==} - dependencies: - '@vue/compiler-dom': 3.3.1 - '@vue/compiler-sfc': 3.3.1 - '@vue/runtime-dom': 3.3.1 - '@vue/server-renderer': 3.3.1(vue@3.3.1) - '@vue/shared': 3.3.1 - - /vuedraggable@4.1.0(vue@3.3.1): + /vuedraggable@4.1.0(vue@3.3.4): resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==} peerDependencies: vue: ^3.0.1 dependencies: sortablejs: 1.14.0 - vue: 3.3.1 + vue: 3.3.4 dev: false /w3c-xmlserializer@4.0.0: @@ -19929,20 +21358,6 @@ packages: resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} dev: false - /websocket@1.0.34: - resolution: {integrity: sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==} - engines: {node: '>=4.0.0'} - dependencies: - bufferutil: 4.0.7 - debug: 2.6.9 - es5-ext: 0.10.62 - typedarray-to-buffer: 3.1.5 - utf-8-validate: 5.0.10 - yaeti: 0.0.6 - transitivePeerDependencies: - - supports-color - dev: false - /well-known-symbols@2.0.0: resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==} engines: {node: '>=6'} @@ -20013,7 +21428,6 @@ packages: gopd: 1.0.1 has-tostringtag: 1.0.0 is-typed-array: 1.1.10 - dev: true /which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} @@ -20054,14 +21468,15 @@ packages: resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} engines: {node: '>= 10.0.0'} dependencies: - '@babel/parser': 7.21.4 - '@babel/types': 7.21.4 + '@babel/parser': 7.22.4 + '@babel/types': 7.22.4 assert-never: 1.2.1 babel-walk: 3.0.0-canary-5 /word-wrap@1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} engines: {node: '>=0.10.0'} + dev: true /wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -20133,7 +21548,7 @@ packages: async-limiter: 1.0.1 dev: true - /ws@8.13.0: + /ws@8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3): resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} engines: {node: '>=10.0.0'} peerDependencies: @@ -20144,6 +21559,9 @@ packages: optional: true utf-8-validate: optional: true + dependencies: + bufferutil: 4.0.7 + utf-8-validate: 6.0.3 /xev@3.0.2: resolution: {integrity: sha512-8kxuH95iMXzHZj+fwqfA4UrPcYOy6bGIgfWzo9Ji23JoEc30ge/Z++Ubkiuy8c0+M64nXmmxrmJ7C8wnuBhluw==} @@ -20191,11 +21609,6 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} - /yaeti@0.0.6: - resolution: {integrity: sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==} - engines: {node: '>=0.10.32'} - dev: false - /yallist@2.1.2: resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} @@ -20352,6 +21765,12 @@ packages: version: 2.2.1-misskey.3 dev: false + github.com/misskey-dev/buraha/92b20c1ab15c5cb5a224cf3b1ecd4f6baca12b7c: + resolution: {tarball: https://codeload.github.com/misskey-dev/buraha/tar.gz/92b20c1ab15c5cb5a224cf3b1ecd4f6baca12b7c} + name: buraha + version: 0.0.0 + dev: false + github.com/misskey-dev/sharp-read-bmp/02d9dc189fa7df0c4bea09330be26741772dac01: resolution: {tarball: https://codeload.github.com/misskey-dev/sharp-read-bmp/tar.gz/02d9dc189fa7df0c4bea09330be26741772dac01} name: sharp-read-bmp @@ -20362,7 +21781,7 @@ packages: sharp: 0.31.3 dev: false - github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.0.10)(@storybook/components@7.0.10)(@storybook/core-events@7.0.10)(@storybook/manager-api@7.0.10)(@storybook/preview-api@7.0.10)(@storybook/theming@7.0.10)(@storybook/types@7.0.10)(react-dom@18.2.0)(react@18.2.0): + github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.0.18)(@storybook/components@7.0.18)(@storybook/core-events@7.0.18)(@storybook/manager-api@7.0.18)(@storybook/preview-api@7.0.18)(@storybook/theming@7.0.18)(@storybook/types@7.0.18)(react-dom@18.2.0)(react@18.2.0): resolution: {tarball: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640} id: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640 name: storybook-addon-misskey-theme @@ -20383,13 +21802,13 @@ packages: react-dom: optional: true dependencies: - '@storybook/blocks': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.0.10 - '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.0.10 - '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.0.10 + '@storybook/blocks': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/components': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.0.18 + '@storybook/manager-api': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.0.18 + '@storybook/theming': 7.0.18(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.0.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true @@ -20407,15 +21826,3 @@ packages: jschardet: 3.0.0 private-ip: 2.3.3 trace-redirect: 1.0.6 - - github.com/sampotts/plyr/d434c9af16e641400aaee93188594208d88f2658: - resolution: {tarball: https://codeload.github.com/sampotts/plyr/tar.gz/d434c9af16e641400aaee93188594208d88f2658} - name: plyr - version: 3.7.0 - dependencies: - core-js: 3.29.1 - custom-event-polyfill: 1.0.7 - loadjs: 4.2.0 - rangetouch: 2.0.1 - url-polyfill: 1.1.12 - dev: false diff --git a/scripts/dev.js b/scripts/dev.js index db7bc11fe..2f20d8f07 100644 --- a/scripts/dev.js +++ b/scripts/dev.js @@ -44,11 +44,17 @@ const fs = require('fs'); if (!stat) throw new Error('not exist yet'); if (stat.size === 0) throw new Error('not built yet'); - await execa('pnpm', ['start'], { + const subprocess = await execa('pnpm', ['start'], { cwd: __dirname + '/../', stdout: process.stdout, stderr: process.stderr, }); + + // なぜかworkerだけが終了してmasterが残るのでその対策 + process.on('SIGINT', () => { + subprocess.kill('SIGINT'); + process.exit(0); + }); } catch (e) { await new Promise(resolve => setTimeout(resolve, 3000)); start();