diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 824bdd0f3f..6ea82e6d0c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,7 @@ "version": "8.9.2" }, "ghcr.io/devcontainers/features/node:1": { - "version": "20.5.1" + "version": "20.10.0" } }, "forwardPorts": [3000], diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml index 1aefda730c..d24a04a5a8 100644 --- a/.github/workflows/get-api-diff.yml +++ b/.github/workflows/get-api-diff.yml @@ -6,37 +6,33 @@ on: branches: - master - develop + paths: + - packages/backend/** + - .github/workflows/get-api-diff.yml jobs: - get-base: + get-from-cherrypick: runs-on: ubuntu-latest permissions: contents: read strategy: matrix: - node-version: [20.5.1] - - services: - db: - image: postgres:13 - ports: - - 5432:5432 - env: - POSTGRES_DB: cherrypick - POSTGRES_HOST_AUTH_METHOD: trust - POSTGRES_USER: example-cherrypick-user - POSTGRESS_PASS: example-cherrypick-pass - redis: - image: redis:7 - ports: - - 6379:6379 + node-version: [20.10.0] + 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 }} steps: - uses: actions/checkout@v4.1.1 with: - repository: ${{ github.event.pull_request.base.repo.full_name }} - ref: ${{ github.base_ref }} + repository: ${{ matrix.repo-name }} + ref: ${{ matrix.ref }} submodules: true - name: Install pnpm uses: pnpm/action-setup@v2 @@ -56,121 +52,15 @@ jobs: run: cp .config/example.yml .config/default.yml - name: Build run: pnpm build - - name : Migrate - run: pnpm migrate - - name: Launch cherrypick - run: | - screen -S cherrypick -dm pnpm run dev - sleep 30s - - name: Wait for CherryPick to be ready - run: | - MAX_RETRIES=12 - RETRY_DELAY=5 - count=0 - until $(curl --output /dev/null --silent --head --fail http://localhost:3000) || [[ $count -eq $MAX_RETRIES ]]; do - printf '.' - sleep $RETRY_DELAY - count=$((count + 1)) - done - - if [[ $count -eq $MAX_RETRIES ]]; then - echo "Failed to connect to CherryPick after $MAX_RETRIES attempts." - exit 1 - fi - - id: fetch - name: Get api.json from CherryPick - run: | - RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json) - echo $RESULT > api-base.json + - name: Generate API JSON + run: pnpm --filter backend generate-api-json + - name: Copy API.json + run: cp packages/backend/built/api.json ${{ matrix.api-json-name }} - name: Upload Artifact uses: actions/upload-artifact@v3 with: name: api-artifact - path: api-base.json - - name: Kill CherryPick Job - run: screen -S cherrypick -X quit - - get-head: - runs-on: ubuntu-latest - permissions: - contents: read - - strategy: - matrix: - node-version: [20.5.1] - - services: - db: - image: postgres:13 - ports: - - 5432:5432 - env: - POSTGRES_DB: cherrypick - POSTGRES_HOST_AUTH_METHOD: trust - POSTGRES_USER: example-cherrypick-user - POSTGRESS_PASS: example-cherrypick-pass - redis: - image: redis:7 - ports: - - 6379:6379 - - steps: - - uses: actions/checkout@v4.1.1 - with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.head_ref }} - submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 - run_install: false - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.0 - with: - node-version: ${{ matrix.node-version }} - cache: 'pnpm' - - run: corepack enable - - run: pnpm i --frozen-lockfile - - name: Check pnpm-lock.yaml - run: git diff --exit-code pnpm-lock.yaml - - name: Copy Configure - run: cp .config/example.yml .config/default.yml - - name: Build - run: pnpm build - - name : Migrate - run: pnpm migrate - - name: Launch cherrypick - run: | - screen -S cherrypick -dm pnpm run dev - sleep 30s - - name: Wait for CherryPick to be ready - run: | - MAX_RETRIES=12 - RETRY_DELAY=5 - count=0 - until $(curl --output /dev/null --silent --head --fail http://localhost:3000) || [[ $count -eq $MAX_RETRIES ]]; do - printf '.' - sleep $RETRY_DELAY - count=$((count + 1)) - done - - if [[ $count -eq $MAX_RETRIES ]]; then - echo "Failed to connect to CherryPick after $MAX_RETRIES attempts." - exit 1 - fi - - id: fetch - name: Get api.json from CherryPick - run: | - RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json) - echo $RESULT > api-head.json - - name: Upload Artifact - uses: actions/upload-artifact@v3 - with: - name: api-artifact - path: api-head.json - - name: Kill CherryPick Job - run: screen -S cherrypick -X quit + path: ${{ matrix.api-json-name }} save-pr-number: runs-on: ubuntu-latest diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index b161b4f877..5760a5620d 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [20.5.1] + node-version: [20.10.0] services: postgres: diff --git a/.github/workflows/test-cherrypick-js.yml b/.github/workflows/test-cherrypick-js.yml index e740fc70e8..792358d943 100644 --- a/.github/workflows/test-cherrypick-js.yml +++ b/.github/workflows/test-cherrypick-js.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [20.5.1] + node-version: [20.10.0] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index 44f721ad2d..7ba92eb714 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [20.5.1] + node-version: [20.10.0] steps: - uses: actions/checkout@v4.1.1 @@ -51,7 +51,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20.5.1] + node-version: [20.10.0] browser: [chrome] services: diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml index 34289c9797..ccf851f557 100644 --- a/.github/workflows/test-production.yml +++ b/.github/workflows/test-production.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [20.5.1] + node-version: [20.10.0] steps: - uses: actions/checkout@v4.1.1 diff --git a/.node-version b/.node-version index 7cc2069986..d5a159609d 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20.5.1 +20.10.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0261eb248c..f2ba735708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,16 +17,21 @@ ### General - Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed) - Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83) +- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 ### Client - Enhance: 絵文字のオートコンプリート機能強化 #12364 - fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正 - Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367 +- Fix: コードエディタが正しく表示されない問題を修正 +- Fix: プロフィールの「ファイル」にセンシティブな画像がある際のデザインを修正 ### Server +- Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように - Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303 - Fix: ロールタイムラインが保存されない問題を修正 - Fix: api.jsonの生成ロジックを改善 #12402 +- Fix: 招待コードが使い回せる問題を修正 ## 2023.11.1 diff --git a/Dockerfile b/Dockerfile index 673855072e..c89c197a46 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.4 -ARG NODE_VERSION=20.5.1-bullseye +ARG NODE_VERSION=20.10.0-bullseye # build assets & compile TypeScript diff --git a/locales/index.d.ts b/locales/index.d.ts index f5ead95a8c..c7ba5c6332 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -729,6 +729,7 @@ export interface Locale { "smtpSecureInfo": string; "testEmail": string; "wordMute": string; + "hardWordMute": string; "regexpError": string; "regexpErrorDescription": string; "instanceMute": string; @@ -1132,6 +1133,7 @@ export interface Locale { "enableChartsForFederatedInstances": string; "showClipButtonInNoteFooter": string; "reactionsDisplaySize": string; + "limitWidthOfReaction": string; "noteIdOrUrl": string; "video": string; "videos": string; @@ -1846,7 +1848,9 @@ export interface Locale { "assignTarget": string; "descriptionOfAssignTarget": string; "manual": string; + "manualRoles": string; "conditional": string; + "conditionalRoles": string; "condition": string; "isConditionalRole": string; "isPublic": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0c9125d03d..e19586d53c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -726,6 +726,7 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する" smtpSecureInfo: "STARTTLS使用時はオフにします。" testEmail: "配信テスト" wordMute: "ワードミュート" +hardWordMute: "ハードワードミュート" regexpError: "正規表現エラー" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:" instanceMute: "サーバーミュート" @@ -1129,6 +1130,7 @@ enableChartsForRemoteUser: "リモートユーザーのチャートを生成" enableChartsForFederatedInstances: "リモートサーバーのチャートを生成" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" reactionsDisplaySize: "リアクションの表示サイズ" +limitWidthOfReaction: "リアクションの最大横幅を制限し、縮小して表示する" noteIdOrUrl: "ノートIDまたはURL" video: "動画" videos: "動画" @@ -1755,7 +1757,9 @@ _role: assignTarget: "アサイン" descriptionOfAssignTarget: "マニュアルは誰がこのロールに含まれるかを手動で管理します。\nコンディショナルは条件を設定し、それに合致するユーザーが自動で含まれるようになります。" manual: "マニュアル" + manualRoles: "マニュアルロール" conditional: "コンディショナル" + conditionalRoles: "コンディショナルロール" condition: "条件" isConditionalRole: "これはコンディショナルロールです。" isPublic: "公開ロール" @@ -2157,7 +2161,7 @@ _ago: weeksAgo: "{n}週間前" monthsAgo: "{n}ヶ月前" yearsAgo: "{n}年前" - invalid: "ありません" + invalid: "日時の解析に失敗" _timeIn: seconds: "{n}秒後" diff --git a/packages/backend/migration/1700383825690-hard-mute.js b/packages/backend/migration/1700383825690-hard-mute.js new file mode 100644 index 0000000000..afd3247f5c --- /dev/null +++ b/packages/backend/migration/1700383825690-hard-mute.js @@ -0,0 +1,11 @@ +export class HardMute1700383825690 { + name = 'HardMute1700383825690' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "hardMutedWords" jsonb NOT NULL DEFAULT '[]'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hardMutedWords"`); + } +} diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 3a01f6741c..722dbdfc99 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -250,6 +250,12 @@ export class MfmService { } } + function fnDefault(node: mfm.MfmFn) { + const el = doc.createElement('i'); + appendChildren(node.children, el); + return el; + } + const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => any } = { bold: (node) => { const el = doc.createElement('b'); @@ -276,17 +282,68 @@ export class MfmService { }, fn: (node) => { - if (node.props.name === 'unixtime') { - const text = node.children[0]!.type === 'text' ? node.children[0].props.text : ''; - const date = new Date(parseInt(text, 10) * 1000); - const el = doc.createElement('time'); - el.setAttribute('datetime', date.toISOString()); - el.textContent = date.toISOString(); - return el; - } else { - const el = doc.createElement('i'); - appendChildren(node.children, el); - return el; + switch (node.props.name) { + case 'unixtime': { + const text = node.children[0].type === 'text' ? node.children[0].props.text : ''; + try { + const date = new Date(parseInt(text, 10) * 1000); + const el = doc.createElement('time'); + el.setAttribute('datetime', date.toISOString()); + el.textContent = date.toISOString(); + return el; + } catch (err) { + return fnDefault(node); + } + } + + case 'ruby': { + if (node.children.length === 1) { + const child = node.children[0]; + const text = child.type === 'text' ? child.props.text : ''; + const rubyEl = doc.createElement('ruby'); + const rtEl = doc.createElement('rt'); + + // ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする + const rpStartEl = doc.createElement('rp'); + rpStartEl.appendChild(doc.createTextNode('(')); + const rpEndEl = doc.createElement('rp'); + rpEndEl.appendChild(doc.createTextNode(')')); + + rubyEl.appendChild(doc.createTextNode(text.split(' ')[0])); + rtEl.appendChild(doc.createTextNode(text.split(' ')[1])); + rubyEl.appendChild(rpStartEl); + rubyEl.appendChild(rtEl); + rubyEl.appendChild(rpEndEl); + return rubyEl; + } else { + const rt = node.children.at(-1); + + if (!rt) { + return fnDefault(node); + } + + const text = rt.type === 'text' ? rt.props.text : ''; + const rubyEl = doc.createElement('ruby'); + const rtEl = doc.createElement('rt'); + + // ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする + const rpStartEl = doc.createElement('rp'); + rpStartEl.appendChild(doc.createTextNode('(')); + const rpEndEl = doc.createElement('rp'); + rpEndEl.appendChild(doc.createTextNode(')')); + + appendChildren(node.children.slice(0, node.children.length - 1), rubyEl); + rtEl.appendChild(doc.createTextNode(text.trim())); + rubyEl.appendChild(rpStartEl); + rubyEl.appendChild(rtEl); + rubyEl.appendChild(rpEndEl); + return rubyEl; + } + } + + default: { + return fnDefault(node); + } } }, diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 72e4caf7ab..32386f6625 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -510,6 +510,7 @@ export class UserEntityService implements OnModuleInit { hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), unreadNotificationsCount: notificationsInfo?.unreadCount, mutedWords: profile!.mutedWords, + hardMutedWords: profile!.hardMutedWords, mutedInstances: profile!.mutedInstances, mutingNotificationTypes: [], // 後方互換性のため notificationRecieveConfig: profile!.notificationRecieveConfig, diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 35eb17ea2c..9535c57450 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -215,7 +215,12 @@ export class MiUserProfile { @Column('jsonb', { default: [], }) - public mutedWords: string[][]; + public mutedWords: (string[] | string)[]; + + @Column('jsonb', { + default: [], + }) + public hardMutedWords: (string[] | string)[]; @Column('jsonb', { default: [], diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 298557982d..bb512b8a30 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -534,6 +534,18 @@ export const packedMeDetailedOnlySchema = { }, }, }, + hardMutedWords: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'string', + nullable: false, optional: false, + }, + }, + }, mutedInstances: { type: 'array', nullable: true, optional: false, diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index f1296da57e..30612062b5 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -126,7 +126,7 @@ export class SignupApiService { code: invitationCode, }); - if (ticket == null) { + if (ticket == null || ticket.usedById != null) { reply.code(400); return; } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 8140f72476..88334b53c4 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -123,6 +123,11 @@ export const meta = { }, } as const; +const muteWords = { type: 'array', items: { oneOf: [ + { type: 'array', items: { type: 'string' } }, + { type: 'string' } +] } } as const; + export const paramDef = { type: 'object', properties: { @@ -171,7 +176,8 @@ export const paramDef = { autoSensitive: { type: 'boolean' }, ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true }, - mutedWords: { type: 'array' }, + mutedWords: muteWords, + hardMutedWords: muteWords, mutedInstances: { type: 'array', items: { type: 'string', } }, @@ -234,16 +240,20 @@ export default class extends Endpoint { // eslint- if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; - if (ps.mutedWords !== undefined) { + + function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) { // TODO: ちゃんと数える const length = JSON.stringify(ps.mutedWords).length; - if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) { + if (length > limit) { throw new ApiError(meta.errors.tooManyMutedWords); } + } - // validate regular expression syntax - ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => { - const regexp = x.match(/^\/(.+)\/(.*)$/); + function validateMuteWordRegex(mutedWords: (string[] | string)[]) { + for (const mutedWord of mutedWords) { + if (typeof mutedWord !== "string") continue; + + const regexp = mutedWord.match(/^\/(.+)\/(.*)$/); if (!regexp) throw new ApiError(meta.errors.invalidRegexp); try { @@ -251,11 +261,21 @@ export default class extends Endpoint { // eslint- } catch (err) { throw new ApiError(meta.errors.invalidRegexp); } - }); + } + } + + if (ps.mutedWords !== undefined) { + checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); + validateMuteWordRegex(ps.mutedWords); profileUpdates.mutedWords = ps.mutedWords; profileUpdates.enableWordMute = ps.mutedWords.length > 0; } + if (ps.hardMutedWords !== undefined) { + checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); + validateMuteWordRegex(ps.hardMutedWords); + profileUpdates.hardMutedWords = ps.hardMutedWords; + } if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig; if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 8886e25ebe..d1ab8c1468 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -273,7 +273,7 @@ export default class extends Endpoint { // eslint- if (renote.channelId && renote.channelId !== ps.channelId) { // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する - const renoteChannel = await this.channelsRepository.findOneById(renote.channelId); + const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId }); if (renoteChannel == null) { // リノートしたいノートが書き込まれているチャンネルが無い throw new ApiError(meta.errors.noSuchChannel); diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 89bacc7bd3..93cee0a8dd 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -169,6 +169,7 @@ describe('ユーザー', () => { hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest, unreadAnnouncements: user.unreadAnnouncements, mutedWords: user.mutedWords, + hardMutedWords: user.hardMutedWords, mutedInstances: user.mutedInstances, mutingNotificationTypes: user.mutingNotificationTypes, notificationRecieveConfig: user.notificationRecieveConfig, diff --git a/packages/cherrypick-js/etc/cherrypick-js.api.md b/packages/cherrypick-js/etc/cherrypick-js.api.md index cf5b90e2bf..1180c90ed4 100644 --- a/packages/cherrypick-js/etc/cherrypick-js.api.md +++ b/packages/cherrypick-js/etc/cherrypick-js.api.md @@ -1593,7 +1593,8 @@ export type Endpoints = { injectFeaturedNote?: boolean; receiveAnnouncementEmail?: boolean; alwaysMarkNsfw?: boolean; - mutedWords?: string[][]; + mutedWords?: (string[] | string)[]; + hardMutedWords?: (string[] | string)[]; notificationRecieveConfig?: any; emailNotificationTypes?: string[]; alsoKnownAs?: string[]; @@ -2581,7 +2582,8 @@ type MeDetailed = UserDetailed & { integrations: Record; isDeleted: boolean; isExplorable: boolean; - mutedWords: string[][]; + mutedWords: (string[] | string)[]; + hardMutedWords: (string[] | string)[]; notificationRecieveConfig: { [notificationType in typeof notificationTypes_2[number]]?: { type: 'all'; @@ -3127,9 +3129,9 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u // // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts // src/api.types.ts:20:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts -// src/api.types.ts:663:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts -// src/entities.ts:116:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts -// src/entities.ts:637:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts +// src/api.types.ts:664:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts +// src/entities.ts:117:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts +// src/entities.ts:638:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/cherrypick-js/src/api.types.ts b/packages/cherrypick-js/src/api.types.ts index de5ddeafdb..f3ec816b85 100644 --- a/packages/cherrypick-js/src/api.types.ts +++ b/packages/cherrypick-js/src/api.types.ts @@ -439,7 +439,8 @@ export type Endpoints = { injectFeaturedNote?: boolean; receiveAnnouncementEmail?: boolean; alwaysMarkNsfw?: boolean; - mutedWords?: string[][]; + mutedWords?: (string[] | string)[]; + hardMutedWords?: (string[] | string)[]; notificationRecieveConfig?: any; emailNotificationTypes?: string[]; alsoKnownAs?: string[]; diff --git a/packages/cherrypick-js/src/entities.ts b/packages/cherrypick-js/src/entities.ts index 15589f8946..87a10fd1ac 100644 --- a/packages/cherrypick-js/src/entities.ts +++ b/packages/cherrypick-js/src/entities.ts @@ -112,7 +112,8 @@ export type MeDetailed = UserDetailed & { integrations: Record; isDeleted: boolean; isExplorable: boolean; - mutedWords: string[][]; + mutedWords: (string[] | string)[]; + hardMutedWords: (string[] | string)[]; notificationRecieveConfig: { [notificationType in typeof notificationTypes[number]]?: { type: 'all'; diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue index 46acbc9f25..e2e3b19139 100644 --- a/packages/frontend/src/components/MkCodeEditor.vue +++ b/packages/frontend/src/components/MkCodeEditor.vue @@ -139,6 +139,10 @@ watch(v, () => { height: 100%; } +.textarea, .codeEditorHighlighter { + margin: 0; +} + .textarea { position: absolute; top: 0; @@ -154,6 +158,8 @@ watch(v, () => { background-color: transparent; border: 0; outline: 0; + min-width: calc(100% - 24px); + height: calc(100% - 24px); padding: 12px; line-height: 1.5em; font-size: 1em; diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 07bf3f83ac..06ce751cda 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -211,7 +211,7 @@ if (!mock) { } } -.icon { +.limitWidth { max-width: 150px; } diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index bbafd5ee40..8f4d76f9dd 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -29,12 +29,25 @@ const props = withDefaults(defineProps<{ mode: 'relative', }); -const _time = props.time == null ? NaN : - typeof props.time === 'number' ? props.time : - (props.time instanceof Date ? props.time : new Date(props.time)).getTime(); +function getDateSafe(n: Date | string | number) { + try { + if (n instanceof Date) { + return n; + } + return new Date(n); + } catch (err) { + return { + getTime: () => NaN, + }; + } +} + +// eslint-disable-next-line vue/no-setup-props-destructure +const _time = props.time == null ? NaN : getDateSafe(props.time).getTime(); const invalid = Number.isNaN(_time); const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid; +// eslint-disable-next-line vue/no-setup-props-destructure let now = $ref((props.origin ?? new Date()).getTime()); const ago = $computed(() => (now - _time) / 1000/*ms*/); diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index 702fdf783d..ecc9a7a463 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -205,13 +205,13 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
- +
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 78f60936e5..adb6c3fe5f 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -66,6 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only + {{ i18n.ts.limitWidthOfReaction }} {{ i18n.ts.hideAvatarsInNote }} CherryPick {{ i18n.ts.enableAbsoluteTime }} CherryPick {{ i18n.ts.enableMarkByDate }} CherryPick @@ -338,6 +339,7 @@ const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serve const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover')); const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter')); const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize')); +const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthOfReaction')); const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes')); const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v)); const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal')); @@ -433,6 +435,7 @@ watch([ overridedDeviceKind, mediaListWithOneImageAppearance, reactionsDisplaySize, + limitWidthOfReaction, highlightSensitiveMedia, keepScreenOn, disableStreamingTimeline, diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index 5601bfd22f..c2dafe5a62 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -9,7 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only - + + + + + + + + @@ -129,6 +136,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import * as os from '@/os.js'; import { infoImageUrl } from '@/instance.js'; +import { $i } from '@/account.js'; import MkFolder from '@/components/MkFolder.vue'; const renoteMutingPagination = { @@ -207,6 +215,14 @@ async function toggleBlockItem(item) { } } +async function saveMutedWords(mutedWords: (string | string[])[]) { + await os.api('i/update', { mutedWords }); +} + +async function saveHardMutedWords(hardMutedWords: (string | string[])[]) { + await os.api('i/update', { hardMutedWords }); +} + const headerActions = $computed(() => []); const headerTabs = $computed(() => []); diff --git a/packages/frontend/src/pages/settings/mute-block.word-mute.vue b/packages/frontend/src/pages/settings/mute-block.word-mute.vue index 6d5857accd..1f097cc24d 100644 --- a/packages/frontend/src/pages/settings/mute-block.word-mute.vue +++ b/packages/frontend/src/pages/settings/mute-block.word-mute.vue @@ -18,16 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only