diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml index edb18b04da..eee7a78fed 100644 --- a/.github/workflows/dockle.yml +++ b/.github/workflows/dockle.yml @@ -20,7 +20,7 @@ jobs: sudo dpkg -i dockle.deb - run: | cp .config/docker_example.env .config/docker.env - cp ./docker-compose.yml.example ./docker-compose.yml + cp ./docker-compose_example.yml ./docker-compose.yml - run: | docker compose up -d web docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml index 281e2058a6..75a458424e 100644 --- a/.github/workflows/get-api-diff.yml +++ b/.github/workflows/get-api-diff.yml @@ -22,16 +22,13 @@ jobs: api-json-name: [api-base.json, api-head.json] include: - api-json-name: api-base.json - repo-name: ${{ github.event.pull_request.base.repo.full_name }} ref: ${{ github.base_ref }} - api-json-name: api-head.json - repo-name: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.head_ref }} + ref: refs/pull/${{ github.event.number }}/merge steps: - uses: actions/checkout@v4.1.1 with: - repository: ${{ matrix.repo-name }} ref: ${{ matrix.ref }} submodules: true - name: Install pnpm diff --git a/CHANGELOG.md b/CHANGELOG.md index bdd082a1c2..4f7389c31c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,15 +21,24 @@ - Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 ### Client +- Feat: 今日誕生日のフォロー中のユーザーを一覧表示できるウィジェットを追加 - Enhance: 絵文字のオートコンプリート機能強化 #12364 - Enhance: ユーザーのRawデータを表示するページが復活 - Enhance: リアクション選択時に音を鳴らせるように - Enhance: サウンドにドライブのファイルを使用できるように +- Enhance: ナビゲーションバーに項目「キャッシュを削除」を追加 +- Enhance: Shareページで投稿を完了すると、親ウィンドウ(親フレーム)にpostMessageするように +- Enhance: チャンネル、クリップ、ページ、Play、ギャラリーにURLのコピーボタンを設置 #11305 +- Enhance: ノートプレビューに「内容を隠す」が反映されるように - fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正 - Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367 +- Enhance: 絵文字の詳細ページに記載される情報を追加 - Fix: コードエディタが正しく表示されない問題を修正 - Fix: プロフィールの「ファイル」にセンシティブな画像がある際のデザインを修正 - Fix: 一度に大量の通知が入った際に通知音が音割れする問題を修正 +- Fix: 共有機能をサポートしていないブラウザの場合は共有ボタンを非表示にする #11305 +- Fix: 通知のグルーピング設定を変更してもリロードされるまで表示が変わらない問題を修正 #12470 +- Fix: 長い名前のチャンネルにおける投稿フォームの表示が崩れる問題を修正 ### Server - Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように @@ -168,6 +177,7 @@ ### Client - Enhance: TLの返信表示オプションを記憶するように - Enhance: 投稿されてから時間が経過しているノートであることを視覚的に分かりやすく +- Feat: 絵文字ピッカーのカテゴリに「/」を入れることでフォルダ分け表示できるように ### Server - Enhance: タイムライン取得時のパフォーマンスを向上 diff --git a/Dockerfile b/Dockerfile index 028a3976d2..38aa5bc7b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -67,8 +67,8 @@ RUN apt-get update \ && corepack enable \ && groupadd -g "${GID}" misskey \ && useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \ - && find / -type d -path /proc -prune -o -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \ - && find / -type d -path /proc -prune -o -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \; \ + && find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \ + && find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \; \ && apt-get clean \ && rm -rf /var/lib/apt/lists diff --git a/docker-compose.yml.example b/docker-compose_example.yml similarity index 100% rename from docker-compose.yml.example rename to docker-compose_example.yml diff --git a/locales/generateDTS.js b/locales/generateDTS.js index 7af773f3b1..d3afdd6e15 100644 --- a/locales/generateDTS.js +++ b/locales/generateDTS.js @@ -56,6 +56,18 @@ export default function generateDTS() { ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags, ), ), + ts.factory.createFunctionDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + undefined, + ts.factory.createIdentifier('build'), + undefined, + [], + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier('Locale'), + undefined, + ), + undefined, + ), ts.factory.createExportDefault(ts.factory.createIdentifier('locales')), ]; const printed = ts.createPrinter({ diff --git a/locales/index.d.ts b/locales/index.d.ts index 64ee30410e..a8f54e2e18 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -314,6 +314,7 @@ export interface Locale { "createFolder": string; "renameFolder": string; "deleteFolder": string; + "folder": string; "addFile": string; "emptyDrive": string; "emptyFolder": string; @@ -440,7 +441,6 @@ export interface Locale { "notFound": string; "notFoundDescription": string; "uploadFolder": string; - "cacheClear": string; "markAsReadAllNotifications": string; "markAsReadAllUnreadNotes": string; "markAsReadAllTalkMessages": string; @@ -1030,6 +1030,8 @@ export interface Locale { "sensitiveWords": string; "sensitiveWordsDescription": string; "sensitiveWordsDescription2": string; + "hiddenTags": string; + "hiddenTagsDescription": string; "notesSearchNotAvailable": string; "license": string; "unfavoriteConfirm": string; @@ -2108,6 +2110,7 @@ export interface Locale { "chooseList": string; }; "clicker": string; + "birthdayFollowings": string; }; "_cw": { "hide": string; @@ -2503,4 +2506,5 @@ export interface Locale { declare const locales: { [lang: string]: Locale; }; +export function build(): Locale; export default locales; diff --git a/locales/index.js b/locales/index.js index 67a406d98d..650e552337 100644 --- a/locales/index.js +++ b/locales/index.js @@ -51,33 +51,37 @@ const primaries = { // 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); -const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {}); +export function build() { + const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {}); -// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す -const removeEmpty = (obj) => { - for (const [k, v] of Object.entries(obj)) { - if (v === '') { - delete obj[k]; - } else if (typeof v === 'object') { - removeEmpty(v); + // 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す + const removeEmpty = (obj) => { + for (const [k, v] of Object.entries(obj)) { + if (v === '') { + delete obj[k]; + } else if (typeof v === 'object') { + removeEmpty(v); + } } - } - return obj; -}; -removeEmpty(locales); + return obj; + }; + removeEmpty(locales); -export default Object.entries(locales) - .reduce((a, [k ,v]) => (a[k] = (() => { - const [lang] = k.split('-'); - switch (k) { - case 'ja-JP': return v; - case 'ja-KS': - case 'en-US': return merge(locales['ja-JP'], v); - default: return merge( - locales['ja-JP'], - locales['en-US'], - locales[`${lang}-${primaries[lang]}`] ?? {}, - v - ); - } - })(), a), {}); + return Object.entries(locales) + .reduce((a, [k, v]) => (a[k] = (() => { + const [lang] = k.split('-'); + switch (k) { + case 'ja-JP': return v; + case 'ja-KS': + case 'en-US': return merge(locales['ja-JP'], v); + default: return merge( + locales['ja-JP'], + locales['en-US'], + locales[`${lang}-${primaries[lang]}`] ?? {}, + v + ); + } + })(), a), {}); +} + +export default build(); diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index f4daefa978..04fb1f947d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -311,6 +311,7 @@ folderName: "フォルダー名" createFolder: "フォルダーを作成" renameFolder: "フォルダー名を変更" deleteFolder: "フォルダーを削除" +folder: "フォルダー" addFile: "ファイルを追加" emptyDrive: "ドライブは空です" emptyFolder: "フォルダーは空です" @@ -437,7 +438,6 @@ share: "共有" notFound: "見つかりません" notFoundDescription: "指定されたURLに該当するページはありませんでした。" uploadFolder: "既定アップロード先" -cacheClear: "キャッシュを削除" markAsReadAllNotifications: "すべての通知を既読にする" markAsReadAllUnreadNotes: "すべての投稿を既読にする" markAsReadAllTalkMessages: "すべてのチャットを既読にする" @@ -1027,6 +1027,8 @@ resetPasswordConfirm: "パスワードリセットしますか?" sensitiveWords: "センシティブワード" sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。" sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。" +hiddenTags: "非表示ハッシュタグ" +hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。" notesSearchNotAvailable: "ノート検索は利用できません。" license: "ライセンス" unfavoriteConfirm: "お気に入り解除しますか?" @@ -2012,6 +2014,7 @@ _widgets: _userList: chooseList: "リストを選択" clicker: "クリッカー" + birthdayFollowings: "今日誕生日のユーザー" _cw: hide: "隠す" diff --git a/packages/backend/migration/1700902349231-add-bday-index.js b/packages/backend/migration/1700902349231-add-bday-index.js new file mode 100644 index 0000000000..251526fc26 --- /dev/null +++ b/packages/backend/migration/1700902349231-add-bday-index.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddBdayIndex1700902349231 { + name = 'AddBdayIndex1700902349231' + + async up(queryRunner) { + await queryRunner.query(`CREATE INDEX "IDX_de22cd2b445eee31ae51cdbe99" ON "user_profile" (SUBSTR("birthday", 6, 5))`); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_de22cd2b445eee31ae51cdbe99"`); + } +} diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 3062999c08..d600ffb9d9 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -304,8 +304,6 @@ export class UserFollowingService implements OnModuleInit { }); } }); - - this.fanoutTimelineService.purge(`homeTimeline:${follower.id}`); } // Publish followed event @@ -373,8 +371,6 @@ export class UserFollowingService implements OnModuleInit { }); } }); - - this.fanoutTimelineService.purge(`homeTimeline:${follower.id}`); } if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 8a43b60039..6659a01412 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -29,6 +29,7 @@ export class MiUserProfile { }) public location: string | null; + @Index() @Column('char', { length: 10, nullable: true, comment: 'The birthday (YYYY-MM-DD) of the User.', diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index 392fa7e1cb..aa749943f0 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -186,6 +186,10 @@ export const packedNoteSchema = { optional: false, nullable: false, }, }, + clippedCount: { + type: 'number', + optional: true, nullable: false, + }, myReaction: { type: 'object', diff --git a/packages/backend/src/server/api/endpoints/admin/invite/create.ts b/packages/backend/src/server/api/endpoints/admin/invite/create.ts index 4a22fd4824..c6ee45735e 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite/create.ts @@ -33,13 +33,7 @@ export const meta = { items: { type: 'object', optional: false, nullable: false, - properties: { - code: { - type: 'string', - optional: false, nullable: false, - example: 'GR6S02ERUA5VR', - }, - }, + ref: 'InviteCode', }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/invite/list.ts b/packages/backend/src/server/api/endpoints/admin/invite/list.ts index f25d3fcb33..ff57940d48 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite/list.ts @@ -21,6 +21,7 @@ export const meta = { items: { type: 'object', optional: false, nullable: false, + ref: 'InviteCode', }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/invite/create.ts b/packages/backend/src/server/api/endpoints/invite/create.ts index 94836283fa..d82fa50e4f 100644 --- a/packages/backend/src/server/api/endpoints/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/invite/create.ts @@ -31,13 +31,7 @@ export const meta = { res: { type: 'object', optional: false, nullable: false, - properties: { - code: { - type: 'string', - optional: false, nullable: false, - example: 'GR6S02ERUA5VR', - }, - }, + ref: 'InviteCode', }, } as const; diff --git a/packages/backend/src/server/api/endpoints/invite/list.ts b/packages/backend/src/server/api/endpoints/invite/list.ts index 06139b6806..2107516ce4 100644 --- a/packages/backend/src/server/api/endpoints/invite/list.ts +++ b/packages/backend/src/server/api/endpoints/invite/list.ts @@ -9,7 +9,6 @@ import type { RegistrationTicketsRepository } from '@/models/_.js'; import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; export const meta = { tags: ['meta'], @@ -23,6 +22,7 @@ export const meta = { items: { type: 'object', optional: false, nullable: false, + ref: 'InviteCode', }, }, } as const; 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 42e2c75c47..a218c46bd3 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -48,7 +48,6 @@ export const paramDef = { withFiles: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false }, - excludeNsfw: { 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' }, diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 03487275a3..ead7ba8c40 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -42,6 +42,12 @@ export const meta = { code: 'FORBIDDEN', id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba', }, + + birthdayInvalid: { + message: 'Birthday date format is invalid.', + code: 'BIRTHDAY_DATE_FORMAT_INVALID', + id: 'a2b007b9-4782-4eba-abd3-93b05ed4130d', + }, }, } as const; @@ -59,6 +65,8 @@ export const paramDef = { nullable: true, description: 'The local host is represented with `null`.', }, + + birthday: { type: 'string', nullable: true }, }, anyOf: [ { required: ['userId'] }, @@ -117,6 +125,21 @@ export default class extends Endpoint { // eslint- .andWhere('following.followerId = :userId', { userId: user.id }) .innerJoinAndSelect('following.followee', 'followee'); + if (ps.birthday) { + try { + const d = new Date(ps.birthday); + d.setHours(0, 0, 0, 0); + const birthday = `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`; + const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile'); + birthdayUserQuery.select('user_profile.userId') + .where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`); + + query.andWhere(`following.followeeId IN (${ birthdayUserQuery.getQuery() })`); + } catch (err) { + throw new ApiError(meta.errors.birthdayInvalid); + } + } + const followings = await query .limit(ps.limit) .getMany(); diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 81fdc84adb..6958d28181 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -52,7 +52,6 @@ export const paramDef = { sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, withFiles: { type: 'boolean', default: false }, - excludeNsfw: { type: 'boolean', default: false }, }, required: ['userId'], } as const; 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 index a7b8cbb037..759f270393 100644 --- 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 @@ -89,7 +89,6 @@ const _sfc_main = /* @__PURE__ */ defineComponent({ api("users/notes", { userId: props.user.id, fileType: image, - excludeNsfw: defaultStore.state.nsfw !== "ignore", limit: 10 }).then((notes) => { for (const note of notes) { @@ -198,7 +197,6 @@ const _sfc_main = defineComponent({ api("users/notes", { userId: props.user.id, fileType: image, - excludeNsfw: defaultStore.state.nsfw !== "ignore", limit: 10 }).then(notes => { for (const note of notes) { diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index 594fe64230..6e216a78b4 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -202,20 +202,24 @@ export async function common(createVue: () => App) { } }, { immediate: true }); - if (defaultStore.state.keepScreenOn) { - if ('wakeLock' in navigator) { - navigator.wakeLock.request('screen') - .then(() => { - document.addEventListener('visibilitychange', async () => { - if (document.visibilityState === 'visible') { - navigator.wakeLock.request('screen'); - } - }); - }) - .catch(() => { - // If Permission fails on an AppleDevice such as Safari - }); + // Keep screen on + const onVisibilityChange = () => document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + navigator.wakeLock.request('screen'); } + }); + if (defaultStore.state.keepScreenOn && 'wakeLock' in navigator) { + navigator.wakeLock.request('screen') + .then(onVisibilityChange) + .catch(() => { + // On WebKit-based browsers, user activation is required to send wake lock request + // https://webkit.org/blog/13862/the-user-activation-api/ + document.addEventListener( + 'click', + () => navigator.wakeLock.request('screen').then(onVisibilityChange), + { once: true }, + ); + }); } //#region Fetch user diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue index 0cdaf7c9bd..70b7bc8295 100644 --- a/packages/frontend/src/components/MkCwButton.vue +++ b/packages/frontend/src/components/MkCwButton.vue @@ -16,7 +16,22 @@ import MkButton from '@/components/MkButton.vue'; const props = defineProps<{ modelValue: boolean; - note: Misskey.entities.Note; + text: string | null; + files: Misskey.entities.DriveFile[]; + poll?: { + expiresAt: string | null; + multiple: boolean; + choices: { + isVoted: boolean; + text: string; + votes: number; + }[]; + } | { + choices: string[]; + multiple: boolean; + expiresAt: string | null; + expiredAfter: string | null; + }; }>(); const emit = defineEmits<{ @@ -25,9 +40,9 @@ const emit = defineEmits<{ const label = computed(() => { return concat([ - props.note.text ? [i18n.t('_cw.chars', { count: props.note.text.length })] : [], - props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length })] : [], - props.note.poll != null ? [i18n.ts.poll] : [], + props.text ? [i18n.t('_cw.chars', { count: props.text.length })] : [], + props.files.length !== 0 ? [i18n.t('_cw.files', { count: props.files.length })] : [], + props.poll != null ? [i18n.ts.poll] : [], ] as string[][]).join(' / '); }); diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index 08297ea5ba..35de9bbb5e 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -5,9 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 5b420c499e..603f676d5d 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -73,18 +73,20 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.customEmojis }}
- {{ category || i18n.ts.other }} + {{ child.value || i18n.ts.other }}
{{ i18n.ts.emoji }}
- {{ category }} + {{ category }}
@@ -100,7 +102,14 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, shallowRef, computed, watch, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; import XSection from '@/components/MkEmojiPicker.section.vue'; -import { emojilist, emojiCharByCategory, UnicodeEmojiDef, unicodeEmojiCategories as categories, getEmojiName } from '@/scripts/emojilist.js'; +import { + emojilist, + emojiCharByCategory, + UnicodeEmojiDef, + unicodeEmojiCategories as categories, + getEmojiName, + CustomEmojiFolderTree +} from '@/scripts/emojilist.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import * as os from '@/os.js'; import { isTouchUsing } from '@/scripts/touch.js'; @@ -144,6 +153,35 @@ const searchResultCustom = ref([]); const searchResultUnicode = ref([]); const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index'); +const customEmojiFolderRoot: CustomEmojiFolderTree = { value: "", category: "", children: [] }; + +function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree { + const parts = input.split('/').map(p => p.trim()); + let currentNode: CustomEmojiFolderTree = root; + + for (const part of parts) { + let existingNode = currentNode.children.find((node) => node.value === part); + + if (!existingNode) { + const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] }; + currentNode.children.push(newNode); + existingNode = newNode; + } + + currentNode = existingNode; + } + + return currentNode; +} + +customEmojiCategories.value.forEach(ec => { + if (ec !== null) { + parseAndMergeCategories(ec, customEmojiFolderRoot); + } +}); + +parseAndMergeCategories('', customEmojiFolderRoot); + watch(q, () => { if (emojisEl.value) emojisEl.value.scrollTop = 0; @@ -572,8 +610,7 @@ defineExpose({ position: sticky; top: 0; left: 0; - height: 32px; - line-height: 32px; + line-height: 28px; z-index: 1; padding: 0 8px; font-size: 12px; diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue index 87f15a110f..b16c05f575 100644 --- a/packages/frontend/src/components/MkLaunchPad.vue +++ b/packages/frontend/src/components/MkLaunchPad.vue @@ -101,6 +101,8 @@ function close() { vertical-align: bottom; height: 100px; border-radius: 10px; + padding: 10px; + box-sizing: border-box; &:hover { color: var(--accent); diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 04fe8d52fb..596895efb9 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only

- +

diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index d8089ac36f..31e97b6aaa 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only

- +

({{ i18n.ts.private }}) diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue index 9b7a39b537..d664d88231 100644 --- a/packages/frontend/src/components/MkNotePreview.vue +++ b/packages/frontend/src/components/MkNotePreview.vue @@ -11,7 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+

+ + +

+
@@ -20,11 +24,23 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -53,6 +69,14 @@ const props = defineProps<{ min-width: 0; } +.cw { + cursor: default; + display: block; + margin: 0; + padding: 0; + overflow-wrap: break-word; +} + .header { margin-bottom: 2px; font-weight: bold; diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index a40dcaf003..f3ab6b2723 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only

- +

diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index 422e9094cc..1e901a1fd6 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only

- +

diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 7b072fa492..2a0082204a 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only + + diff --git a/packages/frontend/src/widgets/index.ts b/packages/frontend/src/widgets/index.ts index 405c49ab06..36925e1bd8 100644 --- a/packages/frontend/src/widgets/index.ts +++ b/packages/frontend/src/widgets/index.ts @@ -33,6 +33,7 @@ export default function(app: App) { app.component('WidgetAichan', defineAsyncComponent(() => import('./WidgetAichan.vue'))); app.component('WidgetUserList', defineAsyncComponent(() => import('./WidgetUserList.vue'))); app.component('WidgetClicker', defineAsyncComponent(() => import('./WidgetClicker.vue'))); + app.component('WidgetBirthdayFollowings', defineAsyncComponent(() => import('./WidgetBirthdayFollowings.vue'))); } export const widgets = [ @@ -63,4 +64,5 @@ export const widgets = [ 'aichan', 'userList', 'clicker', + 'birthdayFollowings', ]; diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index da976b7917..4d2bb00e33 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -150,10 +150,14 @@ export function getConfig(): UserConfig { test: { environment: 'happy-dom', deps: { - inline: [ - // XXX: misskey-dev/browser-image-resizer has no "type": "module" - 'browser-image-resizer', - ], + optimizer: { + web: { + include: [ + // XXX: misskey-dev/browser-image-resizer has no "type": "module" + 'browser-image-resizer', + ], + }, + }, }, }, }; diff --git a/scripts/build-assets.mjs b/scripts/build-assets.mjs index 1ffcec8aa3..f8f09ec2fb 100644 --- a/scripts/build-assets.mjs +++ b/scripts/build-assets.mjs @@ -9,10 +9,12 @@ import cssnano from 'cssnano'; import postcss from 'postcss'; import * as terser from 'terser'; -import locales from '../locales/index.js'; +import { build as buildLocales } from '../locales/index.js'; import generateDTS from '../locales/generateDTS.js'; import meta from '../package.json' assert { type: "json" }; +let locales = buildLocales(); + async function copyFrontendFonts() { await fs.cp('./packages/frontend/node_modules/three/examples/fonts', './built/_frontend_dist_/fonts', { dereference: true, recursive: true }); } @@ -89,10 +91,12 @@ async function build() { await build(); if (process.argv.includes("--watch")) { - const watcher = fs.watch('./packages', { recursive: true }); - for await (const event of watcher) { - if (/^[a-z]+\/src/.test(event.filename)) { - await build(); - } - } + const watcher = fs.watch('./locales'); + for await (const event of watcher) { + const filename = event.filename?.replaceAll('\\', '/'); + if (/^[a-z]+-[A-Z]+\.yml/.test(filename)) { + locales = buildLocales(); + await copyFrontendLocales() + } + } }