Merge branch 'misskey-dev:develop' into develop

This commit is contained in:
老兄 2023-11-25 14:54:48 +08:00 committed by GitHub
commit 00799ca71d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 610 additions and 272 deletions

View File

@ -8,7 +8,7 @@
"version": "8.9.2" "version": "8.9.2"
}, },
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
"version": "20.5.1" "version": "20.10.0"
} }
}, },
"forwardPorts": [3000], "forwardPorts": [3000],

View File

@ -6,37 +6,33 @@ on:
branches: branches:
- master - master
- develop - develop
paths:
- packages/backend/**
- .github/workflows/get-api-diff.yml
jobs: jobs:
get-base: get-from-misskey:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
strategy: strategy:
matrix: matrix:
node-version: [20.5.1] node-version: [20.10.0]
api-json-name: [api-base.json, api-head.json]
services: include:
db: - api-json-name: api-base.json
image: postgres:13 repo-name: ${{ github.event.pull_request.base.repo.full_name }}
ports: ref: ${{ github.base_ref }}
- 5432:5432 - api-json-name: api-head.json
env: repo-name: ${{ github.event.pull_request.head.repo.full_name }}
POSTGRES_DB: misskey ref: ${{ github.head_ref }}
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_USER: example-misskey-user
POSTGRESS_PASS: example-misskey-pass
redis:
image: redis:7
ports:
- 6379:6379
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
with: with:
repository: ${{ github.event.pull_request.base.repo.full_name }} repository: ${{ matrix.repo-name }}
ref: ${{ github.base_ref }} ref: ${{ matrix.ref }}
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
@ -56,121 +52,15 @@ jobs:
run: cp .config/example.yml .config/default.yml run: cp .config/example.yml .config/default.yml
- name: Build - name: Build
run: pnpm build run: pnpm build
- name : Migrate - name: Generate API JSON
run: pnpm migrate run: pnpm --filter backend generate-api-json
- name: Launch misskey - name: Copy API.json
run: | run: cp packages/backend/built/api.json ${{ matrix.api-json-name }}
screen -S misskey -dm pnpm run dev
sleep 30s
- name: Wait for Misskey 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 Misskey after $MAX_RETRIES attempts."
exit 1
fi
- id: fetch
name: Get api.json from Misskey
run: |
RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json)
echo $RESULT > api-base.json
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: api-artifact name: api-artifact
path: api-base.json path: ${{ matrix.api-json-name }}
- name: Kill Misskey Job
run: screen -S misskey -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: misskey
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_USER: example-misskey-user
POSTGRESS_PASS: example-misskey-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 misskey
run: |
screen -S misskey -dm pnpm run dev
sleep 30s
- name: Wait for Misskey 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 Misskey after $MAX_RETRIES attempts."
exit 1
fi
- id: fetch
name: Get api.json from Misskey
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 Misskey Job
run: screen -S misskey -X quit
save-pr-number: save-pr-number:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [20.5.1] node-version: [20.10.0]
services: services:
postgres: postgres:

View File

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [20.5.1] node-version: [20.10.0]
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
@ -51,7 +51,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
node-version: [20.5.1] node-version: [20.10.0]
browser: [chrome] browser: [chrome]
services: services:

View File

@ -16,7 +16,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [20.5.1] node-version: [20.10.0]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps: steps:

View File

@ -16,7 +16,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [20.5.1] node-version: [20.10.0]
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1

View File

@ -1 +1 @@
20.5.1 20.10.0

View File

@ -17,13 +17,24 @@
### General ### General
- Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed) - Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed)
- Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83) - Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83)
- Feat: TL上からートが見えなくなるワードミュートであるハードミュートを追加
- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正
### Client ### Client
- Enhance: 絵文字のオートコンプリート機能強化 #12364
- Enhance: ユーザーのRawデータを表示するページが復活
- fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正 - fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
- Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
- Fix: コードエディタが正しく表示されない問題を修正
- Fix: プロフィールの「ファイル」にセンシティブな画像がある際のデザインを修正
### Server ### Server
- Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように
- Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303 - Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303
- Fix: ロールタイムラインが保存されない問題を修正 - Fix: ロールタイムラインが保存されない問題を修正
- Fix: api.jsonの生成ロジックを改善 #12402
- Fix: 招待コードが使い回せる問題を修正
- Fix: 特定の条件下でチャンネルやユーザーのノート一覧に最新のノートが表示されなくなる問題を修正
## 2023.11.1 ## 2023.11.1

View File

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.4 # syntax = docker/dockerfile:1.4
ARG NODE_VERSION=20.5.1-bullseye ARG NODE_VERSION=20.10.0-bullseye
# build assets & compile TypeScript # build assets & compile TypeScript

4
locales/index.d.ts vendored
View File

@ -642,6 +642,7 @@ export interface Locale {
"smtpSecureInfo": string; "smtpSecureInfo": string;
"testEmail": string; "testEmail": string;
"wordMute": string; "wordMute": string;
"hardWordMute": string;
"regexpError": string; "regexpError": string;
"regexpErrorDescription": string; "regexpErrorDescription": string;
"instanceMute": string; "instanceMute": string;
@ -1039,6 +1040,7 @@ export interface Locale {
"enableChartsForFederatedInstances": string; "enableChartsForFederatedInstances": string;
"showClipButtonInNoteFooter": string; "showClipButtonInNoteFooter": string;
"reactionsDisplaySize": string; "reactionsDisplaySize": string;
"limitWidthOfReaction": string;
"noteIdOrUrl": string; "noteIdOrUrl": string;
"video": string; "video": string;
"videos": string; "videos": string;
@ -1641,7 +1643,9 @@ export interface Locale {
"assignTarget": string; "assignTarget": string;
"descriptionOfAssignTarget": string; "descriptionOfAssignTarget": string;
"manual": string; "manual": string;
"manualRoles": string;
"conditional": string; "conditional": string;
"conditionalRoles": string;
"condition": string; "condition": string;
"isConditionalRole": string; "isConditionalRole": string;
"isPublic": string; "isPublic": string;

View File

@ -639,6 +639,7 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
smtpSecureInfo: "STARTTLS使用時はオフにします。" smtpSecureInfo: "STARTTLS使用時はオフにします。"
testEmail: "配信テスト" testEmail: "配信テスト"
wordMute: "ワードミュート" wordMute: "ワードミュート"
hardWordMute: "ハードワードミュート"
regexpError: "正規表現エラー" regexpError: "正規表現エラー"
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
instanceMute: "サーバーミュート" instanceMute: "サーバーミュート"
@ -1036,6 +1037,7 @@ enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成" enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
reactionsDisplaySize: "リアクションの表示サイズ" reactionsDisplaySize: "リアクションの表示サイズ"
limitWidthOfReaction: "リアクションの最大横幅を制限し、縮小して表示する"
noteIdOrUrl: "ートIDまたはURL" noteIdOrUrl: "ートIDまたはURL"
video: "動画" video: "動画"
videos: "動画" videos: "動画"
@ -1551,7 +1553,9 @@ _role:
assignTarget: "アサイン" assignTarget: "アサイン"
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。" descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。"
manual: "マニュアル" manual: "マニュアル"
manualRoles: "マニュアルロール"
conditional: "コンディショナル" conditional: "コンディショナル"
conditionalRoles: "コンディショナルロール"
condition: "条件" condition: "条件"
isConditionalRole: "これはコンディショナルロールです。" isConditionalRole: "これはコンディショナルロールです。"
isPublic: "公開ロール" isPublic: "公開ロール"
@ -1855,7 +1859,7 @@ _ago:
weeksAgo: "{n}週間前" weeksAgo: "{n}週間前"
monthsAgo: "{n}ヶ月前" monthsAgo: "{n}ヶ月前"
yearsAgo: "{n}年前" yearsAgo: "{n}年前"
invalid: "ありません" invalid: "日時の解析に失敗"
_timeIn: _timeIn:
seconds: "{n}秒後" seconds: "{n}秒後"

View File

@ -0,0 +1,8 @@
import { loadConfig } from './built/config.js'
import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js'
import { writeFileSync } from "node:fs";
const config = loadConfig();
const spec = genOpenapiSpec(config);
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');

View File

@ -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"`);
}
}

View File

@ -23,7 +23,8 @@
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit", "jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit",
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache", "jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
"test": "pnpm jest", "test": "pnpm jest",
"test-and-coverage": "pnpm jest-and-coverage" "test-and-coverage": "pnpm jest-and-coverage",
"generate-api-json": "node ./generate_api_json.js"
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core-android-arm64": "1.3.11", "@swc/core-android-arm64": "1.3.11",

View File

@ -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<K>) => any } = { const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
bold: (node) => { bold: (node) => {
const el = doc.createElement('b'); const el = doc.createElement('b');
@ -276,17 +282,68 @@ export class MfmService {
}, },
fn: (node) => { fn: (node) => {
if (node.props.name === 'unixtime') { switch (node.props.name) {
const text = node.children[0]!.type === 'text' ? node.children[0].props.text : ''; case 'unixtime': {
const date = new Date(parseInt(text, 10) * 1000); const text = node.children[0].type === 'text' ? node.children[0].props.text : '';
const el = doc.createElement('time'); try {
el.setAttribute('datetime', date.toISOString()); const date = new Date(parseInt(text, 10) * 1000);
el.textContent = date.toISOString(); const el = doc.createElement('time');
return el; el.setAttribute('datetime', date.toISOString());
} else { el.textContent = date.toISOString();
const el = doc.createElement('i'); return el;
appendChildren(node.children, el); } catch (err) {
return el; 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);
}
} }
}, },

View File

@ -473,6 +473,7 @@ export class UserEntityService implements OnModuleInit {
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
unreadNotificationsCount: notificationsInfo?.unreadCount, unreadNotificationsCount: notificationsInfo?.unreadCount,
mutedWords: profile!.mutedWords, mutedWords: profile!.mutedWords,
hardMutedWords: profile!.hardMutedWords,
mutedInstances: profile!.mutedInstances, mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: [], // 後方互換性のため mutingNotificationTypes: [], // 後方互換性のため
notificationRecieveConfig: profile!.notificationRecieveConfig, notificationRecieveConfig: profile!.notificationRecieveConfig,

View File

@ -215,7 +215,12 @@ export class MiUserProfile {
@Column('jsonb', { @Column('jsonb', {
default: [], default: [],
}) })
public mutedWords: string[][]; public mutedWords: (string[] | string)[];
@Column('jsonb', {
default: [],
})
public hardMutedWords: (string[] | string)[];
@Column('jsonb', { @Column('jsonb', {
default: [], default: [],

View File

@ -530,6 +530,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: { mutedInstances: {
type: 'array', type: 'array',
nullable: true, optional: false, nullable: true, optional: false,

View File

@ -126,7 +126,7 @@ export class SignupApiService {
code: invitationCode, code: invitationCode,
}); });
if (ticket == null) { if (ticket == null || ticket.usedById != null) {
reply.code(400); reply.code(400);
return; return;
} }

View File

@ -22,7 +22,7 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' }, sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },
publishing: { type: 'boolean', default: null, nullable: true}, publishing: { type: 'boolean', default: null, nullable: true },
}, },
required: [], required: [],
} as const; } as const;

View File

@ -267,6 +267,14 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
enableVerifymailApi: {
type: 'boolean',
optional: false, nullable: false,
},
verifymailAuthKey: {
type: 'string',
optional: false, nullable: true,
},
enableChartsForRemoteUser: { enableChartsForRemoteUser: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
@ -421,6 +429,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
deeplIsPro: instance.deeplIsPro, deeplIsPro: instance.deeplIsPro,
enableIpLogging: instance.enableIpLogging, enableIpLogging: instance.enableIpLogging,
enableActiveEmailValidation: instance.enableActiveEmailValidation, enableActiveEmailValidation: instance.enableActiveEmailValidation,
enableVerifymailApi: instance.enableVerifymailApi,
verifymailAuthKey: instance.verifymailAuthKey,
enableChartsForRemoteUser: instance.enableChartsForRemoteUser, enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances, enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
enableServerMachineStats: instance.enableServerMachineStats, enableServerMachineStats: instance.enableServerMachineStats,

View File

@ -15,6 +15,7 @@ import { IdService } from '@/core/IdService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { isUserRelated } from '@/misc/is-user-related.js'; import { isUserRelated } from '@/misc/is-user-related.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { MetaService } from '@/core/MetaService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -72,12 +73,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private funoutTimelineService: FunoutTimelineService, private funoutTimelineService: FunoutTimelineService,
private cacheService: CacheService, private cacheService: CacheService,
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
private metaService: MetaService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
const isRangeSpecified = untilId != null && sinceId != null; const isRangeSpecified = untilId != null && sinceId != null;
const serverSettings = await this.metaService.fetch();
const channel = await this.channelsRepository.findOneBy({ const channel = await this.channelsRepository.findOneBy({
id: ps.channelId, id: ps.channelId,
}); });
@ -88,7 +92,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (me) this.activeUsersChart.read(me); if (me) this.activeUsersChart.read(me);
if (isRangeSpecified || sinceId == null) { if (serverSettings.enableFanoutTimeline && (isRangeSpecified || sinceId == null)) {
const [ const [
userIdsWhoMeMuting, userIdsWhoMeMuting,
] = me ? await Promise.all([ ] = me ? await Promise.all([

View File

@ -16,12 +16,9 @@ export const meta = {
requireCredential: false, requireCredential: false,
res: { res: {
oneOf: [{ type: 'object',
type: 'object', optional: false, nullable: true,
ref: 'FederationInstance', ref: 'FederationInstance',
}, {
type: 'null',
}],
}, },
} as const; } as const;

View File

@ -123,6 +123,11 @@ export const meta = {
}, },
} as const; } as const;
const muteWords = { type: 'array', items: { oneOf: [
{ type: 'array', items: { type: 'string' } },
{ type: 'string' }
] } } as const;
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
@ -171,7 +176,8 @@ export const paramDef = {
autoSensitive: { type: 'boolean' }, autoSensitive: { type: 'boolean' },
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true }, pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
mutedWords: { type: 'array' }, mutedWords: muteWords,
hardMutedWords: muteWords,
mutedInstances: { type: 'array', items: { mutedInstances: { type: 'array', items: {
type: 'string', type: 'string',
} }, } },
@ -234,16 +240,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.location !== undefined) profileUpdates.location = ps.location;
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
if (ps.mutedWords !== undefined) {
function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
// TODO: ちゃんと数える // TODO: ちゃんと数える
const length = JSON.stringify(ps.mutedWords).length; const length = JSON.stringify(mutedWords).length;
if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) { if (length > limit) {
throw new ApiError(meta.errors.tooManyMutedWords); throw new ApiError(meta.errors.tooManyMutedWords);
} }
}
// validate regular expression syntax function validateMuteWordRegex(mutedWords: (string[] | string)[]) {
ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => { for (const mutedWord of mutedWords) {
const regexp = x.match(/^\/(.+)\/(.*)$/); if (typeof mutedWord !== "string") continue;
const regexp = mutedWord.match(/^\/(.+)\/(.*)$/);
if (!regexp) throw new ApiError(meta.errors.invalidRegexp); if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
try { try {
@ -251,11 +261,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} catch (err) { } catch (err) {
throw new ApiError(meta.errors.invalidRegexp); 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.mutedWords = ps.mutedWords;
profileUpdates.enableWordMute = ps.mutedWords.length > 0; 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.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig; if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;

View File

@ -262,7 +262,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (renote.channelId && renote.channelId !== ps.channelId) { if (renote.channelId && renote.channelId !== ps.channelId) {
// チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック
// リートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する // リートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する
const renoteChannel = await this.channelsRepository.findOneById(renote.channelId); const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId });
if (renoteChannel == null) { if (renoteChannel == null) {
// リノートしたいノートが書き込まれているチャンネルが無い // リノートしたいノートが書き込まれているチャンネルが無い
throw new ApiError(meta.errors.noSuchChannel); throw new ApiError(meta.errors.noSuchChannel);

View File

@ -15,6 +15,7 @@ import { IdService } from '@/core/IdService.js';
import { isUserRelated } from '@/misc/is-user-related.js'; import { isUserRelated } from '@/misc/is-user-related.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { MetaService } from '@/core/MetaService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -71,6 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private cacheService: CacheService, private cacheService: CacheService,
private idService: IdService, private idService: IdService,
private funoutTimelineService: FunoutTimelineService, private funoutTimelineService: FunoutTimelineService,
private metaService: MetaService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@ -78,7 +80,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const isRangeSpecified = untilId != null && sinceId != null; const isRangeSpecified = untilId != null && sinceId != null;
const isSelf = me && (me.id === ps.userId); const isSelf = me && (me.id === ps.userId);
if (isRangeSpecified || sinceId == null) { const serverSettings = await this.metaService.fetch();
if (serverSettings.enableFanoutTimeline && (isRangeSpecified || sinceId == null)) {
const [ const [
userIdsWhoMeMuting, userIdsWhoMeMuting,
] = me ? await Promise.all([ ] = me ? await Promise.all([

View File

@ -4,7 +4,7 @@
*/ */
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import endpoints from '../endpoints.js'; import endpoints, { IEndpoint } from '../endpoints.js';
import { errors as basicErrors } from './errors.js'; import { errors as basicErrors } from './errors.js';
import { schemas, convertSchemaToOpenApiSchema } from './schemas.js'; import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
@ -33,16 +33,17 @@ export function genOpenapiSpec(config: Config) {
schemas: schemas, schemas: schemas,
securitySchemes: { securitySchemes: {
ApiKeyAuth: { bearerAuth: {
type: 'apiKey', type: 'http',
in: 'body', scheme: 'bearer',
name: 'i',
}, },
}, },
}, },
}; };
for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) { // 書き換えたりするのでディープコピーしておく。そのまま編集するとメモリ上の値が汚れて次回以降の出力に影響する
const copiedEndpoints = JSON.parse(JSON.stringify(endpoints)) as IEndpoint[];
for (const endpoint of copiedEndpoints.filter(ep => !ep.meta.secure)) {
const errors = {} as any; const errors = {} as any;
if (endpoint.meta.errors) { if (endpoint.meta.errors) {
@ -79,6 +80,13 @@ export function genOpenapiSpec(config: Config) {
schema.required = [...schema.required ?? [], 'file']; schema.required = [...schema.required ?? [], 'file'];
} }
if (schema.required && schema.required.length <= 0) {
// 空配列は許可されない
schema.required = undefined;
}
const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1);
const info = { const info = {
operationId: endpoint.name, operationId: endpoint.name,
summary: endpoint.name, summary: endpoint.name,
@ -92,17 +100,19 @@ export function genOpenapiSpec(config: Config) {
} : {}), } : {}),
...(endpoint.meta.requireCredential ? { ...(endpoint.meta.requireCredential ? {
security: [{ security: [{
ApiKeyAuth: [], bearerAuth: [],
}], }],
} : {}), } : {}),
requestBody: { ...(hasBody ? {
required: true, requestBody: {
content: { required: true,
[requestType]: { content: {
schema, [requestType]: {
schema,
},
}, },
}, },
}, } : {}),
responses: { responses: {
...(endpoint.meta.res ? { ...(endpoint.meta.res ? {
'200': { '200': {
@ -118,6 +128,11 @@ export function genOpenapiSpec(config: Config) {
description: 'OK (without any results)', description: 'OK (without any results)',
}, },
}), }),
...(endpoint.meta.res?.optional === true || endpoint.meta.res?.nullable === true ? {
'204': {
description: 'OK (without any results)',
},
} : {}),
'400': { '400': {
description: 'Client error', description: 'Client error',
content: { content: {
@ -190,6 +205,7 @@ export function genOpenapiSpec(config: Config) {
}; };
spec.paths['/' + endpoint.name] = { spec.paths['/' + endpoint.name] = {
...(endpoint.meta.allowGet ? { get: info } : {}),
post: info, post: info,
}; };
} }

View File

@ -7,10 +7,16 @@ import type { Schema } from '@/misc/json-schema.js';
import { refs } from '@/misc/json-schema.js'; import { refs } from '@/misc/json-schema.js';
export function convertSchemaToOpenApiSchema(schema: Schema) { export function convertSchemaToOpenApiSchema(schema: Schema) {
const res: any = schema; // optional, refはスキーマ定義に含まれないので分離しておく
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { optional, ref, ...res }: any = schema;
if (schema.type === 'object' && schema.properties) { if (schema.type === 'object' && schema.properties) {
res.required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k); const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
if (required.length > 0) {
// 空配列は許可されない
res.required = required;
}
for (const k of Object.keys(schema.properties)) { for (const k of Object.keys(schema.properties)) {
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]); res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]);

View File

@ -168,6 +168,7 @@ describe('ユーザー', () => {
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest, hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
unreadAnnouncements: user.unreadAnnouncements, unreadAnnouncements: user.unreadAnnouncements,
mutedWords: user.mutedWords, mutedWords: user.mutedWords,
hardMutedWords: user.hardMutedWords,
mutedInstances: user.mutedInstances, mutedInstances: user.mutedInstances,
mutingNotificationTypes: user.mutingNotificationTypes, mutingNotificationTypes: user.mutingNotificationTypes,
notificationRecieveConfig: user.notificationRecieveConfig, notificationRecieveConfig: user.notificationRecieveConfig,

View File

@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="report.comment"/> <Mfm :text="report.comment"/>
</div> </div>
<hr/> <hr/>
<div>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></div> <div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link">@{{ report.reporter.username }}</MkA></div>
<div v-if="report.assignee"> <div v-if="report.assignee">
{{ i18n.ts.moderator }}: {{ i18n.ts.moderator }}:
<MkAcct :user="report.assignee"/> <MkAcct :user="report.assignee"/>

View File

@ -242,29 +242,7 @@ function exec() {
return; return;
} }
const matched: EmojiDef[] = []; emojis.value = emojiAutoComplete(props.q, emojiDb.value);
const max = 30;
emojiDb.value.some(x => {
if (x.name.startsWith(props.q ?? '') && !x.aliasOf && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
return matched.length === max;
});
if (matched.length < max) {
emojiDb.value.some(x => {
if (x.name.startsWith(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
return matched.length === max;
});
}
if (matched.length < max) {
emojiDb.value.some(x => {
if (x.name.includes(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
return matched.length === max;
});
}
emojis.value = matched;
} else if (props.type === 'mfmTag') { } else if (props.type === 'mfmTag') {
if (!props.q || props.q === '') { if (!props.q || props.q === '') {
mfmTags.value = MFM_TAGS; mfmTags.value = MFM_TAGS;
@ -275,6 +253,78 @@ function exec() {
} }
} }
type EmojiScore = { emoji: EmojiDef, score: number };
function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] {
if (!query) {
return [];
}
const matched = new Map<string, EmojiScore>();
//
emojiDb.some(x => {
if (x.name.startsWith(query) && !x.aliasOf) {
matched.set(x.name, { emoji: x, score: query.length + 1 });
}
return matched.size === max;
});
//
if (matched.size < max) {
emojiDb.some(x => {
if (x.name.startsWith(query) && !matched.has(x.aliasOf ?? x.name)) {
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length });
}
return matched.size === max;
});
}
//
if (matched.size < max) {
emojiDb.some(x => {
if (x.name.includes(query) && !matched.has(x.aliasOf ?? x.name)) {
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 });
}
return matched.size === max;
});
}
// 3
if (matched.size < max && query.length > 3) {
const queryChars = [...query];
const hitEmojis = new Map<string, EmojiScore>();
for (const x of emojiDb) {
//
let pos = 0;
let hit = 0;
for (const c of queryChars) {
pos = x.name.indexOf(c, pos);
if (pos <= -1) break;
hit++;
}
//
if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) {
hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 });
}
}
// 66
[...hitEmojis.values()]
.sort((x, y) => y.score - x.score)
.slice(0, 6)
.forEach(it => matched.set(it.emoji.name, it));
}
return [...matched.values()]
.sort((x, y) => y.score - x.score)
.slice(0, max)
.map(it => it.emoji);
}
function onMousedown(event: Event) { function onMousedown(event: Event) {
if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close(); if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close();
} }

View File

@ -139,6 +139,10 @@ watch(v, () => {
height: 100%; height: 100%;
} }
.textarea, .codeEditorHighlighter {
margin: 0;
}
.textarea { .textarea {
position: absolute; position: absolute;
top: 0; top: 0;
@ -154,6 +158,8 @@ watch(v, () => {
background-color: transparent; background-color: transparent;
border: 0; border: 0;
outline: 0; outline: 0;
min-width: calc(100% - 24px);
height: calc(100% - 24px);
padding: 12px; padding: 12px;
line-height: 1.5em; line-height: 1.5em;
font-size: 1em; font-size: 1em;

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div <div
v-if="!muted" v-if="!hardMuted && !muted"
v-show="!isDeleted" v-show="!isDeleted"
ref="el" ref="el"
v-hotkey="keymap" v-hotkey="keymap"
@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</article> </article>
</div> </div>
<div v-else :class="$style.muted" @click="muted = false"> <div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small"> <I18n :src="i18n.ts.userSaysSomething" tag="small">
<template #name> <template #name>
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)"> <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
@ -183,6 +183,7 @@ const props = withDefaults(defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
pinned?: boolean; pinned?: boolean;
mock?: boolean; mock?: boolean;
withHardMute?: boolean;
}>(), { }>(), {
mock: false, mock: false,
}); });
@ -239,13 +240,23 @@ const urls = $computed(() => parsed ? extractUrlFromMfm(parsed) : null);
const isLong = shouldCollapsed(appearNote, urls ?? []); const isLong = shouldCollapsed(appearNote, urls ?? []);
const collapsed = ref(appearNote.cw == null && isLong); const collapsed = ref(appearNote.cw == null && isLong);
const isDeleted = ref(false); const isDeleted = ref(false);
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false); const muted = ref(checkMute(appearNote, $i?.mutedWords));
const hardMuted = ref(props.withHardMute && checkMute(appearNote, $i?.hardMutedWords));
const translation = ref<any>(null); const translation = ref<any>(null);
const translating = ref(false); const translating = ref(false);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id)); const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id));
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null))); let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean {
if (mutedWords == null) return false;
if (checkWordMute(note, $i, mutedWords)) return true;
if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true;
if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true;
return false;
}
const keymap = { const keymap = {
'r': () => reply(true), 'r': () => reply(true),
'e|a|plus': () => react(true), 'e|a|plus': () => react(true),

View File

@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:ad="true" :ad="true"
:class="$style.notes" :class="$style.notes"
> >
<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/> <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
</MkDateSeparatedList> </MkDateSeparatedList>
</div> </div>
</template> </template>

View File

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{ items: notifications }"> <template #default="{ items: notifications }">
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="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"/> <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/>
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/> <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
</MkDateSeparatedList> </MkDateSeparatedList>
</template> </template>

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]" :class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]"
@click="toggleReaction()" @click="toggleReaction()"
> >
<MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/> <MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
<span :class="$style.count">{{ count }}</span> <span :class="$style.count">{{ count }}</span>
</button> </button>
</template> </template>
@ -188,7 +188,7 @@ if (!mock) {
} }
} }
.icon { .limitWidth {
max-width: 150px; max-width: 150px;
} }

View File

@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<iframe <iframe
ref="tweet" ref="tweet"
allow="fullscreen;web-share" allow="fullscreen;web-share"
sandbox="allow-popups allow-scripts allow-same-origin" sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin"
scrolling="no" scrolling="no"
:style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }"
:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"

View File

@ -28,12 +28,25 @@ const props = withDefaults(defineProps<{
mode: 'relative', mode: 'relative',
}); });
const _time = props.time == null ? NaN : function getDateSafe(n: Date | string | number) {
typeof props.time === 'number' ? props.time : try {
(props.time instanceof Date ? props.time : new Date(props.time)).getTime(); 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 invalid = Number.isNaN(_time);
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid; 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()); let now = $ref((props.origin ?? new Date()).getTime());
const ago = $computed(() => (now - _time) / 1000/*ms*/); const ago = $computed(() => (now - _time) / 1000/*ms*/);

View File

@ -65,9 +65,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<img src="https://avatars.githubusercontent.com/u/67428053?v=4" :class="$style.contributorAvatar"> <img src="https://avatars.githubusercontent.com/u/67428053?v=4" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@kakkokari-gtyih</span> <span :class="$style.contributorUsername">@kakkokari-gtyih</span>
</a> </a>
<a href="https://github.com/taichanNE30" target="_blank" :class="$style.contributor"> <a href="https://github.com/tai-cha" target="_blank" :class="$style.contributor">
<img src="https://avatars.githubusercontent.com/u/40626578?v=4" :class="$style.contributorAvatar"> <img src="https://avatars.githubusercontent.com/u/40626578?v=4" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@taichanNE30</span> <span :class="$style.contributorUsername">@tai-cha</span>
</a> </a>
</div> </div>
</FormSection> </FormSection>

View File

@ -9,11 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<XHeader :actions="headerActions" :tabs="headerTabs"/> <XHeader :actions="headerActions" :tabs="headerTabs"/>
</template> </template>
<MkSpacer :contentMax="900"> <MkSpacer :contentMax="900">
<MkSelect v-model="type" :class="$style.input" @update:modelValue="onChangePublishing"> <MkSelect v-model="filterType" :class="$style.input" @update:modelValue="filterItems">
<template #label>{{ i18n.ts.state }}</template> <template #label>{{ i18n.ts.state }}</template>
<option value="null">{{ i18n.ts.all }}</option> <option value="all">{{ i18n.ts.all }}</option>
<option value="true">{{ i18n.ts.publishing }}</option> <option value="publishing">{{ i18n.ts.publishing }}</option>
<option value="false">{{ i18n.ts.expired }}</option> <option value="expired">{{ i18n.ts.expired }}</option>
</MkSelect> </MkSelect>
<div> <div>
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad"> <div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
@ -104,8 +104,8 @@ let ads: any[] = $ref([]);
const localTime = new Date(); const localTime = new Date();
const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000; const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000;
const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday, i18n.ts._weekday.tuesday, i18n.ts._weekday.wednesday, i18n.ts._weekday.thursday, i18n.ts._weekday.friday, i18n.ts._weekday.saturday]; const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday, i18n.ts._weekday.tuesday, i18n.ts._weekday.wednesday, i18n.ts._weekday.thursday, i18n.ts._weekday.friday, i18n.ts._weekday.saturday];
const filterType = ref('all');
let publishing: boolean | null = null; let publishing: boolean | null = null;
let type = ref('null');
os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => { os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => {
if (adsResponse != null) { if (adsResponse != null) {
@ -123,9 +123,15 @@ os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => {
} }
}); });
const onChangePublishing = (v) => { const filterItems = (v) => {
console.log(v); if (v === 'publishing') {
publishing = v === 'true' ? true : v === 'false' ? false : null; publishing = true;
} else if (v === 'expired') {
publishing = false;
} else {
publishing = null;
}
refresh(); refresh();
}; };

View File

@ -198,13 +198,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton> <MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>
<div class="_gaps_s"> <div class="_gaps_s">
<MkFoldableSection> <MkFoldableSection>
<template #header>Manual roles</template> <template #header>{{ i18n.ts._role.manualRoles }}</template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkRolePreview v-for="role in roles.filter(x => x.target === 'manual')" :key="role.id" :role="role" :forModeration="true"/> <MkRolePreview v-for="role in roles.filter(x => x.target === 'manual')" :key="role.id" :role="role" :forModeration="true"/>
</div> </div>
</MkFoldableSection> </MkFoldableSection>
<MkFoldableSection> <MkFoldableSection>
<template #header>Conditional roles</template> <template #header>{{ i18n.ts._role.conditionalRoles }}</template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkRolePreview v-for="role in roles.filter(x => x.target === 'conditional')" :key="role.id" :role="role" :forModeration="true"/> <MkRolePreview v-for="role in roles.filter(x => x.target === 'conditional')" :key="role.id" :role="role" :forModeration="true"/>
</div> </div>

View File

@ -56,6 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="medium">{{ i18n.ts.medium }}</option> <option value="medium">{{ i18n.ts.medium }}</option>
<option value="large">{{ i18n.ts.large }}</option> <option value="large">{{ i18n.ts.large }}</option>
</MkRadios> </MkRadios>
<MkSwitch v-model="limitWidthOfReaction">{{ i18n.ts.limitWidthOfReaction }}</MkSwitch>
</div> </div>
<MkSelect v-model="instanceTicker"> <MkSelect v-model="instanceTicker">
@ -226,6 +227,7 @@ const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serve
const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover')); const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter')); const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter'));
const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize')); const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize'));
const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthOfReaction'));
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes')); const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v)); const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal')); const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
@ -290,6 +292,7 @@ watch([
overridedDeviceKind, overridedDeviceKind,
mediaListWithOneImageAppearance, mediaListWithOneImageAppearance,
reactionsDisplaySize, reactionsDisplaySize,
limitWidthOfReaction,
highlightSensitiveMedia, highlightSensitiveMedia,
keepScreenOn, keepScreenOn,
disableStreamingTimeline, disableStreamingTimeline,

View File

@ -9,7 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-message-off"></i></template> <template #icon><i class="ti ti-message-off"></i></template>
<template #label>{{ i18n.ts.wordMute }}</template> <template #label>{{ i18n.ts.wordMute }}</template>
<XWordMute/> <XWordMute :muted="$i!.mutedWords" @save="saveMutedWords"/>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-message-off"></i></template>
<template #label>{{ i18n.ts.hardWordMute }}</template>
<XWordMute :muted="$i!.hardMutedWords" @save="saveHardMutedWords"/>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
@ -129,6 +136,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { infoImageUrl } from '@/instance.js'; import { infoImageUrl } from '@/instance.js';
import { $i } from '@/account.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
const renoteMutingPagination = { 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 headerActions = $computed(() => []);
const headerTabs = $computed(() => []); const headerTabs = $computed(() => []);

View File

@ -18,16 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import MkTextarea from '@/components/MkTextarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkTab from '@/components/MkTab.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import number from '@/filters/number.js';
import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
const props = defineProps<{
muted: (string[] | string)[];
}>();
const emit = defineEmits<{
(ev: 'save', value: (string[] | string)[]): void;
}>();
const render = (mutedWords) => mutedWords.map(x => { const render = (mutedWords) => mutedWords.map(x => {
if (Array.isArray(x)) { if (Array.isArray(x)) {
@ -37,8 +38,7 @@ const render = (mutedWords) => mutedWords.map(x => {
} }
}).join('\n'); }).join('\n');
const tab = ref('soft'); const mutedWords = ref(render(props.muted));
const mutedWords = ref(render($i!.mutedWords));
const changed = ref(false); const changed = ref(false);
watch(mutedWords, () => { watch(mutedWords, () => {
@ -85,9 +85,7 @@ async function save() {
return; return;
} }
await os.api('i/update', { emit('save', parsed);
mutedWords: parsed,
});
changed.value = false; changed.value = false;
} }

View File

@ -11,10 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading v-if="fetching"/> <MkLoading v-if="fetching"/>
<div v-if="!fetching && files.length > 0" :class="$style.stream"> <div v-if="!fetching && files.length > 0" :class="$style.stream">
<template v-for="file in files" :key="file.note.id + file.file.id"> <template v-for="file in files" :key="file.note.id + file.file.id">
<div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.sensitive" @click="showingFiles.push(file.file.id)"> <div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.img" @click="showingFiles.push(file.file.id)">
<div> <!-- TODO: 画像以外のファイルに対応 -->
<div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div> <ImgWithBlurhash :class="$style.sensitiveImg" :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name" :forceBlurhash="true"/>
<div>{{ i18n.ts.clickToShow }}</div> <div :class="$style.sensitive">
<div>
<div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div>
<div>{{ i18n.ts.clickToShow }}</div>
</div>
</div> </div>
</div> </div>
<MkA v-else :class="$style.img" :to="notePage(file.note)"> <MkA v-else :class="$style.img" :to="notePage(file.note)">
@ -88,6 +92,7 @@ onMounted(() => {
} }
.img { .img {
position: relative;
height: 128px; height: 128px;
border-radius: 6px; border-radius: 6px;
overflow: clip; overflow: clip;
@ -99,8 +104,24 @@ onMounted(() => {
text-align: center; text-align: center;
} }
.sensitiveImg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
filter: brightness(0.7);
}
.sensitive { .sensitive {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: grid; display: grid;
place-items: center; place-items: center;
font-size: 0.8em;
color: #fff;
cursor: pointer;
} }
</style> </style>

View File

@ -18,6 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XPages v-else-if="tab === 'pages'" :user="user"/> <XPages v-else-if="tab === 'pages'" :user="user"/>
<XFlashs v-else-if="tab === 'flashs'" :user="user"/> <XFlashs v-else-if="tab === 'flashs'" :user="user"/>
<XGallery v-else-if="tab === 'gallery'" :user="user"/> <XGallery v-else-if="tab === 'gallery'" :user="user"/>
<XRaw v-else-if="tab === 'raw'" :user="user"/>
</div> </div>
<MkError v-else-if="error" @retry="fetchUser()"/> <MkError v-else-if="error" @retry="fetchUser()"/>
<MkLoading v-else/> <MkLoading v-else/>
@ -44,6 +45,7 @@ const XLists = defineAsyncComponent(() => import('./lists.vue'));
const XPages = defineAsyncComponent(() => import('./pages.vue')); const XPages = defineAsyncComponent(() => import('./pages.vue'));
const XFlashs = defineAsyncComponent(() => import('./flashs.vue')); const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
const XGallery = defineAsyncComponent(() => import('./gallery.vue')); const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
acct: string; acct: string;
@ -112,6 +114,10 @@ const headerTabs = $computed(() => user ? [{
key: 'gallery', key: 'gallery',
title: i18n.ts.gallery, title: i18n.ts.gallery,
icon: 'ti ti-icons', icon: 'ti ti-icons',
}, {
key: 'raw',
title: 'Raw',
icon: 'ti ti-code',
}] : []); }] : []);
definePageMetadata(computed(() => user ? { definePageMetadata(computed(() => user ? {

View File

@ -0,0 +1,130 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkSpacer :contentMax="600" :marginMin="16" :marginMax="32">
<div class="_gaps_m">
<div :class="$style.userMInfoRoot">
<MkAvatar :class="$style.userMInfoAvatar" :user="user" indicator link preview/>
<div :class="$style.userMInfoMetaRoot">
<span :class="$style.userMInfoMetaName"><MkUserName :class="$style.userMInfoMetaName" :user="user"/></span>
<span :class="$style.userMInfoMetaSub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
<span :class="$style.userMInfoMetaState">
<span v-if="suspended" :class="$style.suspended">Suspended</span>
<span v-if="silenced" :class="$style.silenced">Silenced</span>
<span v-if="moderator" :class="$style.moderator">Moderator</span>
</span>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: 1em;">
<MkKeyValue :copy="user.id" oneline>
<template #key>ID</template>
<template #value><span class="_monospace">{{ user.id }}</span></template>
</MkKeyValue>
<MkKeyValue oneline>
<template #key>{{ i18n.ts.createdAt }}</template>
<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
</MkKeyValue>
</div>
<FormSection>
<template #label>Raw</template>
<MkObjectView tall :value="user"></MkObjectView>
</FormSection>
</div>
</MkSpacer>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import * as Misskey from 'misskey-js';
import { acct } from '@/filters/user.js';
import { i18n } from '@/i18n.js';
import MkKeyValue from '@/components/MkKeyValue.vue';
import FormSection from '@/components/form/section.vue';
import MkObjectView from '@/components/MkObjectView.vue';
const props = defineProps<{
user: Misskey.entities.User;
}>();
const moderator = computed(() => props.user.isModerator ?? false);
const silenced = computed(() => props.user.isSilenced ?? false);
const suspended = computed(() => props.user.isSuspended ?? false);
</script>
<style lang="scss" module>
.userMInfoRoot {
display: flex;
align-items: center;
}
.userMInfoAvatar {
display: block;
width: 64px;
height: 64px;
margin-right: 16px;
}
.userMInfoMetaRoot {
flex: 1;
overflow: hidden;
}
.userMInfoMetaName {
display: block;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.userMInfoMetaSub {
display: block;
width: 100%;
font-size: 85%;
opacity: 0.7;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.userMInfoMetaState {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 4px;
&:empty {
display: none;
}
> .suspended,
> .silenced,
> .moderator {
display: inline-block;
border: solid 1px;
border-radius: 6px;
padding: 2px 6px;
font-size: 85%;
}
> .suspended {
color: var(--error);
border-color: var(--error);
}
> .silenced {
color: var(--warn);
border-color: var(--warn);
}
> .moderator {
color: var(--success);
border-color: var(--success);
}
}
</style>

View File

@ -61,7 +61,7 @@ export const soundsTypes = [
'noizenecio/kick_gaba7', 'noizenecio/kick_gaba7',
] as const; ] as const;
export async function getAudio(file: string, useCache = true) { export async function loadAudio(file: string, useCache = true) {
if (useCache && cache.has(file)) { if (useCache && cache.has(file)) {
return cache.get(file)!; return cache.get(file)!;
} }
@ -77,12 +77,6 @@ export async function getAudio(file: string, useCache = true) {
return audioBuffer; return audioBuffer;
} }
export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement {
const masterVolume = defaultStore.state.sound_masterVolume;
audio.volume = masterVolume - ((1 - volume) * masterVolume);
return audio;
}
export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') { export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') {
const sound = defaultStore.state[`sound_${type}`]; const sound = defaultStore.state[`sound_${type}`];
if (_DEV_) console.log('play', type, sound); if (_DEV_) console.log('play', type, sound);
@ -91,16 +85,22 @@ export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notifica
} }
export async function playFile(file: string, volume: number) { export async function playFile(file: string, volume: number) {
const buffer = await loadAudio(file);
createSourceNode(buffer, volume)?.start();
}
export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null {
const masterVolume = defaultStore.state.sound_masterVolume; const masterVolume = defaultStore.state.sound_masterVolume;
if (masterVolume === 0 || volume === 0) { if (masterVolume === 0 || volume === 0) {
return; return null;
} }
const gainNode = ctx.createGain(); const gainNode = ctx.createGain();
gainNode.gain.value = masterVolume * volume; gainNode.gain.value = masterVolume * volume;
const soundSource = ctx.createBufferSource(); const soundSource = ctx.createBufferSource();
soundSource.buffer = await getAudio(file); soundSource.buffer = buffer;
soundSource.connect(gainNode).connect(ctx.destination); soundSource.connect(gainNode).connect(ctx.destination);
soundSource.start();
return soundSource;
} }

View File

@ -330,6 +330,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device', where: 'device',
default: 'medium' as 'small' | 'medium' | 'large', default: 'medium' as 'small' | 'medium' | 'large',
}, },
limitWidthOfReaction: {
where: 'device',
default: true,
},
forceShowAds: { forceShowAds: {
where: 'device', where: 'device',
default: false, default: false,

View File

@ -99,7 +99,10 @@ const current = reactive({
}, },
}); });
const prev = reactive({} as typeof current); const prev = reactive({} as typeof current);
const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1); let jammedAudioBuffer: AudioBuffer | null = $ref(null);
let jammedSoundNodePlaying: boolean = $ref(false);
sound.loadAudio('syuilo/queue-jammed').then(buf => jammedAudioBuffer = buf);
for (const domain of ['inbox', 'deliver']) { for (const domain of ['inbox', 'deliver']) {
prev[domain] = deepClone(current[domain]); prev[domain] = deepClone(current[domain]);
@ -113,8 +116,13 @@ const onStats = (stats) => {
current[domain].waiting = stats[domain].waiting; current[domain].waiting = stats[domain].waiting;
current[domain].delayed = stats[domain].delayed; current[domain].delayed = stats[domain].delayed;
if (current[domain].waiting > 0 && widgetProps.sound && jammedSound.paused) { if (current[domain].waiting > 0 && widgetProps.sound && jammedAudioBuffer && !jammedSoundNodePlaying) {
jammedSound.play(); const soundNode = sound.createSourceNode(jammedAudioBuffer, 1);
if (soundNode) {
jammedSoundNodePlaying = true;
soundNode.onended = () => jammedSoundNodePlaying = false;
soundNode.start();
}
} }
} }
}; };

View File

@ -150,7 +150,7 @@ describe('MkUrlPreview', () => {
}); });
assert.exists(iframe, 'iframe should exist'); assert.exists(iframe, 'iframe should exist');
assert.strictEqual(iframe?.getAttribute('allow'), 'fullscreen;web-share'); assert.strictEqual(iframe?.getAttribute('allow'), 'fullscreen;web-share');
assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-scripts allow-same-origin'); assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin');
}); });
test('Loading a post in iframe', async () => { test('Loading a post in iframe', async () => {
@ -159,6 +159,6 @@ describe('MkUrlPreview', () => {
}); });
assert.exists(iframe, 'iframe should exist'); assert.exists(iframe, 'iframe should exist');
assert.strictEqual(iframe?.getAttribute('allow'), 'fullscreen;web-share'); assert.strictEqual(iframe?.getAttribute('allow'), 'fullscreen;web-share');
assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-scripts allow-same-origin'); assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin');
}); });
}); });

View File

@ -1565,7 +1565,8 @@ export type Endpoints = {
injectFeaturedNote?: boolean; injectFeaturedNote?: boolean;
receiveAnnouncementEmail?: boolean; receiveAnnouncementEmail?: boolean;
alwaysMarkNsfw?: boolean; alwaysMarkNsfw?: boolean;
mutedWords?: string[][]; mutedWords?: (string[] | string)[];
hardMutedWords?: (string[] | string)[];
notificationRecieveConfig?: any; notificationRecieveConfig?: any;
emailNotificationTypes?: string[]; emailNotificationTypes?: string[];
alsoKnownAs?: string[]; alsoKnownAs?: string[];
@ -2516,7 +2517,8 @@ type MeDetailed = UserDetailed & {
integrations: Record<string, any>; integrations: Record<string, any>;
isDeleted: boolean; isDeleted: boolean;
isExplorable: boolean; isExplorable: boolean;
mutedWords: string[][]; mutedWords: (string[] | string)[];
hardMutedWords: (string[] | string)[];
notificationRecieveConfig: { notificationRecieveConfig: {
[notificationType in typeof notificationTypes_2[number]]?: { [notificationType in typeof notificationTypes_2[number]]?: {
type: 'all'; type: 'all';
@ -3053,9 +3055,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: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:20:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
// src/api.types.ts:634:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts // src/api.types.ts:635: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:117:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
// src/entities.ts:627:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/entities.ts:628: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 // 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) // (No @packageDocumentation comment for this package)

View File

@ -432,7 +432,8 @@ export type Endpoints = {
injectFeaturedNote?: boolean; injectFeaturedNote?: boolean;
receiveAnnouncementEmail?: boolean; receiveAnnouncementEmail?: boolean;
alwaysMarkNsfw?: boolean; alwaysMarkNsfw?: boolean;
mutedWords?: string[][]; mutedWords?: (string[] | string)[];
hardMutedWords?: (string[] | string)[];
notificationRecieveConfig?: any; notificationRecieveConfig?: any;
emailNotificationTypes?: string[]; emailNotificationTypes?: string[];
alsoKnownAs?: string[]; alsoKnownAs?: string[];

View File

@ -112,7 +112,8 @@ export type MeDetailed = UserDetailed & {
integrations: Record<string, any>; integrations: Record<string, any>;
isDeleted: boolean; isDeleted: boolean;
isExplorable: boolean; isExplorable: boolean;
mutedWords: string[][]; mutedWords: (string[] | string)[];
hardMutedWords: (string[] | string)[];
notificationRecieveConfig: { notificationRecieveConfig: {
[notificationType in typeof notificationTypes[number]]?: { [notificationType in typeof notificationTypes[number]]?: {
type: 'all'; type: 'all';