Merge remote-branch 'misskey/develop'

This commit is contained in:
NoriDev 2023-08-02 16:08:31 +09:00
commit 69ef7a3bf8
29 changed files with 311 additions and 182 deletions

View file

@ -18,6 +18,9 @@
- OAuth 2.0のサポート
### Client
- メニューのスイッチの動作を改善
- Enhance: ユーザーメニューでスイッチでユーザーリストに追加・削除できるように
- Enhance: 自分が押したリアクションのデザインを改善
- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正
- Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正

View file

@ -44,7 +44,7 @@
"lodash": "4.17.21"
},
"dependencies": {
"execa": "7.1.1",
"execa": "7.2.0",
"gulp": "4.0.2",
"gulp-cssnano": "2.1.3",
"gulp-rename": "2.0.0",
@ -54,13 +54,13 @@
"typescript": "5.1.6"
},
"devDependencies": {
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.61.0",
"@types/gulp": "4.0.13",
"@types/gulp-rename": "2.0.2",
"@typescript-eslint/eslint-plugin": "6.2.0",
"@typescript-eslint/parser": "6.2.0",
"cross-env": "7.0.3",
"cypress": "12.17.1",
"eslint": "8.45.0",
"cypress": "12.17.2",
"eslint": "8.46.0",
"start-server-and-test": "2.0.0"
},
"optionalDependencies": {

View file

@ -59,28 +59,28 @@
"@aws-sdk/client-s3": "3.367.0",
"@aws-sdk/lib-storage": "3.367.0",
"@aws-sdk/node-http-handler": "3.360.0",
"@bull-board/api": "5.6.1",
"@bull-board/fastify": "5.6.1",
"@bull-board/ui": "5.6.1",
"@bull-board/api": "5.7.1",
"@bull-board/fastify": "5.7.1",
"@bull-board/ui": "5.7.1",
"@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.2.0",
"@fastify/cookie": "8.3.0",
"@fastify/cors": "8.3.0",
"@fastify/express": "2.3.0",
"@fastify/http-proxy": "9.2.1",
"@fastify/multipart": "7.7.1",
"@fastify/multipart": "7.7.3",
"@fastify/static": "6.10.2",
"@fastify/view": "8.0.0",
"@google-cloud/logging": "^10.5.0",
"@google-cloud/translate": "^7.2.1",
"@nestjs/common": "10.1.0",
"@nestjs/core": "10.1.0",
"@nestjs/testing": "10.1.0",
"@google-cloud/logging": "^10.5.0",
"@google-cloud/translate": "^7.2.1",
"@nestjs/common": "10.1.2",
"@nestjs/core": "10.1.2",
"@nestjs/testing": "10.1.2",
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.3.0",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.70",
"@vitalets/google-translate-api": "9.2.0",
"@swc/core": "1.3.72",
"@vitalets/google-translate-api": "9.2.0",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "5.3.1",
@ -88,9 +88,9 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"bullmq": "4.4.0",
"bullmq": "4.6.3",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.0",
"cbor": "9.0.1",
"chalk": "5.3.0",
"chalk-template": "1.1.0",
"cherrypick-js": "workspace:*",
@ -101,7 +101,7 @@
"content-disposition": "0.5.4",
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
"fastify": "4.20.0",
"fastify": "4.21.0",
"feed": "4.2.2",
"file-type": "18.5.0",
"fluent-ffmpeg": "2.1.2",
@ -109,7 +109,7 @@
"got": "13.0.0",
"happy-dom": "10.0.3",
"hpagent": "1.2.0",
"http-link-header": "1.1.0",
"http-link-header": "1.1.1",
"ioredis": "5.3.2",
"ip-cidr": "3.1.0",
"ipaddr.js": "2.1.0",
@ -124,14 +124,14 @@
"mime-types": "2.1.35",
"ms": "3.0.0-canary.1",
"nested-property": "4.0.0",
"node-fetch": "3.3.1",
"nodemailer": "6.9.3",
"node-fetch": "3.3.2",
"nodemailer": "6.9.4",
"nsfwjs": "2.4.2",
"oauth": "0.10.0",
"oauth2orize": "1.11.1",
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
"otpauth": "9.1.3",
"otpauth": "9.1.4",
"parse5": "7.1.2",
"pg": "8.11.1",
"pkce-challenge": "4.0.1",
@ -143,21 +143,21 @@
"qrcode": "1.5.3",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "1.19.1",
"re2": "1.20.1",
"redis-lock": "0.1.4",
"reflect-metadata": "0.1.13",
"rename": "1.0.4",
"rss-parser": "3.13.0",
"rxjs": "7.8.1",
"sanitize-html": "2.11.0",
"sharp": "0.32.3",
"sharp": "0.32.4",
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"strip-ansi": "^7.1.0",
"summaly": "github:misskey-dev/summaly",
"systeminformation": "5.18.7",
"systeminformation": "5.18.9",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.7",
@ -167,13 +167,13 @@
"typescript": "5.1.6",
"ulid": "2.3.0",
"vary": "1.1.2",
"web-push": "3.6.3",
"web-push": "3.6.4",
"ws": "8.13.0",
"xev": "3.0.2"
},
"devDependencies": {
"@jest/globals": "29.6.1",
"@swc/jest": "0.2.26",
"@jest/globals": "29.6.2",
"@swc/jest": "0.2.27",
"@types/accepts": "1.3.5",
"@types/archiver": "5.3.2",
"@types/bcryptjs": "2.4.2",
@ -190,9 +190,9 @@
"@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1",
"@types/ms": "0.7.31",
"@types/node": "20.4.2",
"@types/node": "20.4.5",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.8",
"@types/nodemailer": "6.4.9",
"@types/oauth": "0.9.1",
"@types/oauth2orize": "1.11.0",
"@types/oauth2orize-pkce": "0.1.0",
@ -213,15 +213,15 @@
"@types/vary": "1.1.0",
"@types/web-push": "3.3.2",
"@types/ws": "8.5.5",
"@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.61.0",
"@typescript-eslint/eslint-plugin": "6.2.0",
"@typescript-eslint/parser": "6.2.0",
"aws-sdk-client-mock": "3.0.0",
"cross-env": "7.0.3",
"eslint": "8.45.0",
"eslint-plugin-import": "2.27.5",
"execa": "7.1.1",
"jest": "29.6.1",
"jest-mock": "29.6.1",
"eslint": "8.46.0",
"eslint-plugin-import": "2.28.0",
"execa": "7.2.0",
"jest": "29.6.2",
"jest-mock": "29.6.2",
"simple-oauth2": "5.0.0"
}
}

View file

@ -203,7 +203,7 @@ function convertRedisOptions(options: RedisOptionsSource, host: string): RedisOp
...options,
password: options.pass,
prefix: options.prefix ?? host,
family: options.family == null ? 0 : options.family,
family: options.family ?? 0,
keyPrefix: `${options.prefix ?? host}:`,
db: options.db ?? 0,
};

View file

@ -587,9 +587,7 @@ export class DriveService {
file.maybePorn = info.porn;
file.isSensitive = user
? this.userEntityService.isLocalUser(user) && profile!.alwaysMarkNsfw ? true :
(sensitive !== null && sensitive !== undefined)
? sensitive
: false
sensitive ?? false
: false;
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;

View file

@ -108,7 +108,7 @@ export class FetchInstanceMetadataService {
if (name) updates.name = name;
if (description) updates.description = description;
if (icon || favicon) updates.iconUrl = (icon && !icon.includes('data:image/png;base64')) ? icon : favicon;
if (icon ?? favicon) updates.iconUrl = (icon && !icon.includes('data:image/png;base64')) ? icon : favicon;
if (favicon) updates.faviconUrl = favicon;
if (themeColor) updates.themeColor = themeColor;

View file

@ -93,7 +93,7 @@ export class HttpRequestService {
*/
@bindThis
public getAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent {
if (bypassProxy || (this.config.proxyBypassHosts || []).includes(url.hostname)) {
if (bypassProxy || (this.config.proxyBypassHosts ?? []).includes(url.hostname)) {
return url.protocol === 'http:' ? this.http : this.https;
} else {
return url.protocol === 'http:' ? this.httpAgent : this.httpsAgent;

View file

@ -372,7 +372,7 @@ export class NoteCreateService implements OnApplicationShutdown {
text: data.text,
hasPoll: data.poll != null,
hasEvent: data.event != null,
cw: data.cw == null ? null : data.cw,
cw: data.cw ?? null,
tags: tags.map(tag => normalizeForSearch(tag)),
emojis,
userId: user.id,
@ -408,7 +408,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const url = profile != null ? profile.url : null;
return {
uri: u.uri,
url: url == null ? undefined : url,
url: url ?? undefined,
username: u.username,
host: u.host,
} as IMentionedRemoteUsers[0];

View file

@ -220,7 +220,7 @@ export class ApNoteService {
// 引用
let quote: Note | undefined | null = null;
if (note._misskey_quote || note.quoteUrl) {
if (note._misskey_quote ?? note.quoteUrl) {
const tryResolveNote = async (uri: string): Promise<
| { status: 'ok'; res: Note }
| { status: 'permerror' | 'temperror' }

View file

@ -242,7 +242,7 @@ export function createPostgresDataSource(config: Config) {
options: {
host: config.redis.host,
port: config.redis.port,
family: config.redis.family == null ? 0 : config.redis.family,
family: config.redis.family ?? 0,
password: config.redis.pass,
keyPrefix: `${config.redis.prefix}:query:`,
db: config.redis.db ?? 0,

View file

@ -204,7 +204,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
let files: DriveFile[] = [];
const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null;
const fileIds = ps.fileIds ?? ps.mediaIds ?? null;
if (fileIds != null) {
files = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.userId = :userId AND file.id IN (:...fileIds)', {

View file

@ -117,13 +117,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
await this.pagesRepository.update(page.id, {
updatedAt: new Date(),
title: ps.title,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
name: ps.name === undefined ? page.name : ps.name,
summary: ps.summary === undefined ? page.summary : ps.summary,
content: ps.content,
variables: ps.variables,
script: ps.script,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
font: ps.font === undefined ? page.font : ps.font,
eyeCatchingImageId: ps.eyeCatchingImageId === null
? null

View file

@ -20,14 +20,14 @@
"url": "git+https://github.com/misskey-dev/misskey.js.git"
},
"devDependencies": {
"@microsoft/api-extractor": "7.36.2",
"@swc/jest": "0.2.26",
"@microsoft/api-extractor": "7.36.3",
"@swc/jest": "0.2.27",
"@types/jest": "29.5.3",
"@types/node": "20.4.2",
"@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.61.0",
"eslint": "8.45.0",
"jest": "29.6.1",
"@types/node": "20.4.5",
"@typescript-eslint/eslint-plugin": "6.2.0",
"@typescript-eslint/parser": "6.2.0",
"eslint": "8.46.0",
"jest": "29.6.2",
"jest-fetch-mock": "3.0.3",
"jest-websocket-mock": "2.4.0",
"mock-socket": "9.2.1",
@ -39,7 +39,7 @@
],
"dependencies": {
"@swc/cli": "0.1.62",
"@swc/core": "1.3.69",
"@swc/core": "1.3.71",
"eventemitter3": "5.0.1",
"reconnecting-websocket": "4.4.0"
}

View file

@ -22,9 +22,6 @@ module.exports = {
'allowSingleExtends': true,
},
],
'@typescript-eslint/prefer-nullish-coalescing': [
'error',
],
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
'id-denylist': ['error', 'window', 'e'],

View file

@ -23,7 +23,7 @@
"@syuilo/aiscript": "0.15.0",
"@tabler/icons-webfont": "2.30.0",
"@vitejs/plugin-vue": "4.2.3",
"@vue-macros/reactivity-transform": "0.3.15",
"@vue-macros/reactivity-transform": "0.3.16",
"@vue/compiler-sfc": "3.3.4",
"astring": "1.8.6",
"autosize": "6.0.1",
@ -31,7 +31,7 @@
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
"buraha": "0.0.1",
"canvas-confetti": "1.6.0",
"chart.js": "4.3.0",
"chart.js": "4.3.2",
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1",
@ -39,7 +39,7 @@
"cherrypick-js": "workspace:*",
"cherrypick-mfm-js": "github:kokonect-link/mfm.js",
"chromatic": "6.19.9",
"compare-versions": "5.0.3",
"compare-versions": "6.0.0",
"cropperjs": "2.0.0-beta.3",
"date-fns": "2.30.0",
"escape-regexp": "0.0.1",
@ -56,15 +56,15 @@
"prismjs": "1.29.0",
"punycode": "2.3.0",
"querystring": "0.2.1",
"rollup": "3.26.3",
"rollup": "3.27.0",
"s-age": "1.1.2",
"sanitize-html": "2.11.0",
"sass": "1.63.6",
"sass": "1.64.1",
"strict-event-emitter-types": "2.0.0",
"syuilo-password-strength": "0.0.1",
"temml": "0.10.13",
"textarea-caret": "3.1.0",
"three": "0.154.0",
"three": "0.155.0",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.7",
@ -73,7 +73,7 @@
"typescript": "5.1.6",
"uuid": "9.0.0",
"vanilla-tilt": "1.8.0",
"vite": "4.4.4",
"vite": "4.4.7",
"vue": "3.3.4",
"vue-prism-editor": "2.0.0-alpha.2",
"vuedraggable": "next"
@ -105,29 +105,29 @@
"@types/gulp-rename": "2.0.2",
"@types/matter-js": "0.18.5",
"@types/micromatch": "4.0.2",
"@types/node": "20.4.2",
"@types/node": "20.4.5",
"@types/punycode": "2.1.0",
"@types/sanitize-html": "2.9.0",
"@types/testing-library__jest-dom": "5.14.8",
"@types/testing-library__jest-dom": "5.14.9",
"@types/throttle-debounce": "5.0.0",
"@types/tinycolor2": "1.4.3",
"@types/uuid": "9.0.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.5",
"@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.61.0",
"@typescript-eslint/eslint-plugin": "6.2.0",
"@typescript-eslint/parser": "6.2.0",
"@vitest/coverage-v8": "0.33.0",
"@vue/runtime-core": "3.3.4",
"acorn": "8.10.0",
"cross-env": "7.0.3",
"cypress": "12.17.1",
"eslint": "8.45.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-vue": "9.15.1",
"fast-glob": "3.3.0",
"cypress": "12.17.2",
"eslint": "8.46.0",
"eslint-plugin-import": "2.28.0",
"eslint-plugin-vue": "9.16.1",
"fast-glob": "3.3.1",
"happy-dom": "10.0.3",
"micromatch": "4.0.5",
"msw": "1.2.2",
"msw": "1.2.3",
"msw-storybook-addon": "1.8.0",
"nodemon": "3.0.1",
"prettier": "3.0.0",
@ -141,6 +141,6 @@
"vitest": "0.33.0",
"vitest-fetch-mock": "0.2.2",
"vue-eslint-parser": "9.3.1",
"vue-tsc": "1.8.5"
"vue-tsc": "1.8.8"
}
}

View file

@ -155,13 +155,13 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
.then(async res => {
if (res.error) {
if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
// SUSPENDED
// SUSPENDED
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
await showSuspendedDialog();
}
} else if (res.error.id === 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a') {
// USER_IS_DELETED
// アカウントが削除されている
// USER_IS_DELETED
// アカウントが削除されている
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
await alert({
type: 'error',
@ -170,8 +170,8 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
});
}
} else if (res.error.id === 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14') {
// AUTHENTICATION_FAILED
// トークンが無効化されていたりアカウントが削除されたりしている
// AUTHENTICATION_FAILED
// トークンが無効化されていたりアカウントが削除されたりしている
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
await alert({
type: 'error',

View file

@ -35,9 +35,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
</button>
<span v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" :class="$style.item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</MkSwitch>
</span>
<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)" />
<span :class="$style.switchText">{{ item.text }}</span>
</button>
<button v-else-if="item.type === 'parent'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="showChildren(item, $event)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<span>{{ item.text }}</span>
@ -63,8 +64,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { focusPrev, focusNext } from '@/scripts/focus';
import MkSwitch from '@/components/MkSwitch.vue';
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
import MkSwitchButton from '@/components/MkSwitch.button.vue';
import { MenuItem, InnerMenuItem, OuterMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu';
import * as os from '@/os';
import { i18n } from '@/i18n';
@ -145,17 +146,17 @@ function onItemMouseLeave(item) {
if (childCloseTimer) window.clearTimeout(childCloseTimer);
}
let childrenCache = new WeakMap();
async function showChildren(item: MenuItem, ev: MouseEvent) {
const children = ref([]);
let childrenCache = new WeakMap<MenuParent, OuterMenuItem[]>();
async function showChildren(item: MenuParent, ev: MouseEvent) {
const children = ref<OuterMenuItem[]>([]);
if (childrenCache.has(item)) {
children.value = childrenCache.get(item);
children.value = childrenCache.get(item)!;
} else {
if (typeof item.children === 'function') {
children.value = [{
type: 'pending',
}];
item.children().then(x => {
Promise.resolve(item.children()).then(x => {
children.value = x;
childrenCache.set(item, x);
});
@ -191,6 +192,11 @@ function focusDown() {
focusNext(document.activeElement);
}
function switchItem(item: MenuSwitch & { ref: any }) {
if (item.disabled) return;
item.ref = !item.ref;
}
onMounted(() => {
if (props.viaKeyboard) {
nextTick(() => {
@ -357,6 +363,37 @@ onBeforeUnmount(() => {
}
}
.switch {
position: relative;
display: flex;
transition: all 0.2s ease;
user-select: none;
cursor: pointer;
}
.switchDisabled {
cursor: not-allowed;
}
.switchButton {
margin-left: -2px;
}
.switchText {
margin-left: 8px;
margin-top: 2px;
overflow: hidden;
text-overflow: ellipsis;
}
.switchInput {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
.icon {
margin-right: 8px;
}

View file

@ -457,14 +457,16 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }), ev).then(focus);
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }), menuButton.value, {
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
os.popupMenu(menu, menuButton.value, {
viaKeyboard,
}).then(focus);
}).then(focus).finally(cleanup);
}
async function clip() {

View file

@ -433,14 +433,16 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), ev).then(focus);
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), menuButton.value, {
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
os.popupMenu(menu, menuButton.value, {
viaKeyboard,
}).then(focus);
}).then(focus).finally(cleanup);
}
async function translate(): Promise<void> {

View file

@ -166,15 +166,13 @@ useTooltip(buttonEl, async (showing) => {
}
}
&.reacted {
background: var(--accent);
&:hover {
background: var(--accent);
}
&.reacted, &.reacted:hover {
background: var(--accentedBg);
color: var(--accent);
border: 1px solid var(--accent);
> .count {
color: var(--fgOnAccent);
color: var(--accent);
}
> .icon {

View file

@ -0,0 +1,88 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<span
v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff"
:class="{
[$style.button]: true,
[$style.buttonChecked]: checked,
[$style.buttonDisabled]: props.disabled
}"
data-cy-switch-toggle
@click.prevent.stop="toggle"
>
<div :class="{ [$style.knob]: true, [$style.knobChecked]: checked }"></div>
</span>
</template>
<script lang="ts" setup>
import { toRefs, Ref } from 'vue';
import { i18n } from '@/i18n';
const props = withDefaults(defineProps<{
checked: boolean | Ref<boolean>;
disabled?: boolean;
}>(), {
disabled: false,
});
const emit = defineEmits<{
(ev: 'toggle'): void;
}>();
const checked = toRefs(props).checked;
const toggle = () => {
emit('toggle');
};
</script>
<style lang="scss" module>
.button {
position: relative;
display: inline-flex;
flex-shrink: 0;
margin: 0;
box-sizing: border-box;
width: 32px;
height: 23px;
outline: none;
background: var(--switchOffBg);
background-clip: content-box;
border: solid 1px var(--switchOffBg);
border-radius: 999px;
cursor: pointer;
transition: inherit;
user-select: none;
}
.buttonChecked {
background-color: var(--switchOnBg) !important;
border-color: var(--switchOnBg) !important;
}
.buttonDisabled {
cursor: not-allowed;
}
.knob {
position: absolute;
top: 3px;
width: 15px;
height: 15px;
border-radius: 999px;
transition: all 0.2s ease;
&:not(.knobChecked) {
left: 3px;
background: var(--switchOffFg);
}
}
.knobChecked {
left: 12px;
background: var(--switchOnFg);
}
</style>

View file

@ -12,9 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.input"
@keydown.enter="toggle"
>
<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" :class="$style.button" data-cy-switch-toggle @click.prevent="toggle">
<div :class="$style.knob"></div>
</span>
<XButton :checked="checked" :disabled="disabled" @toggle="toggle" />
<span :class="$style.body">
<!-- TODO: 無名slotの方は廃止 -->
<span :class="$style.label" @click="toggle"><slot name="label"></slot><slot></slot></span>
@ -25,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { toRefs, Ref } from 'vue';
import { i18n } from '@/i18n';
import XButton from '@/components/MkSwitch.button.vue';
const props = defineProps<{
modelValue: boolean | Ref<boolean>;
@ -36,7 +34,6 @@ const emit = defineEmits<{
(ev: 'update:modelValue', v: boolean): void;
}>();
let button = $shallowRef<HTMLElement>();
const checked = toRefs(props).modelValue;
const toggle = () => {
if (props.disabled) return;
@ -66,17 +63,8 @@ const toggle = () => {
cursor: not-allowed;
}
&.checked {
> .button {
background-color: var(--switchOnBg) !important;
border-color: var(--switchOnBg) !important;
> .knob {
left: 12px;
background: var(--switchOnFg);
}
}
}
//&.checked {
//}
}
.input {
@ -86,36 +74,6 @@ const toggle = () => {
opacity: 0;
margin: 0;
}
.button {
position: relative;
display: inline-flex;
flex-shrink: 0;
margin: 0;
box-sizing: border-box;
width: 32px;
height: 23px;
outline: none;
background: var(--switchOffBg);
background-clip: content-box;
border: solid 1px var(--switchOffBg);
border-radius: 999px;
cursor: pointer;
transition: inherit;
user-select: none;
}
.knob {
position: absolute;
top: 3px;
left: 3px;
width: 15px;
height: 15px;
background: var(--switchOffFg);
border-radius: 999px;
transition: all 0.2s ease;
}
.body {
margin-left: 12px;
margin-top: 2px;

View file

@ -86,7 +86,8 @@ let top = $ref(0);
let left = $ref(0);
function showMenu(ev: MouseEvent) {
os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
const { menu, cleanup } = getUserMenu(user);
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
}
onMounted(() => {

View file

@ -237,7 +237,8 @@ const age = $computed(() => {
});
function menu(ev) {
os.popupMenu(getUserMenu(props.user, router), ev.currentTarget ?? ev.target);
const { menu, cleanup } = getUserMenu(props.user, router);
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
}
function parallaxLoop() {

View file

@ -16,6 +16,7 @@ import { defaultStore, noteActions } from '@/store';
import { miLocalStorage } from '@/local-storage';
import { getUserMenu } from '@/scripts/get-user-menu';
import { clipsCache } from '@/cache';
import { MenuItem } from '@/types/menu';
export async function getNoteClipMenu(props: {
note: misskey.entities.Note;
@ -108,6 +109,8 @@ export function getNoteMenu(props: {
const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note;
const cleanups = [] as (() => void)[];
function del(): void {
os.confirm({
type: 'warning',
@ -244,7 +247,7 @@ export function getNoteMenu(props: {
props.translation.value = res;
}
let menu;
let menu: MenuItem[];
if ($i) {
const statePromise = os.api('notes/state', {
noteId: appearNote.id,
@ -311,7 +314,7 @@ export function getNoteMenu(props: {
action: () => toggleFavorite(true),
}),
{
type: 'parent',
type: 'parent' as const,
icon: 'ti ti-paperclip',
text: i18n.ts.clip,
children: () => getNoteClipMenu(props),
@ -334,15 +337,17 @@ export function getNoteMenu(props: {
text: i18n.ts.pin,
action: () => togglePin(true),
} : undefined,
appearNote.userId !== $i.id ? {
type: 'parent',
{
type: 'parent' as const,
icon: 'ti ti-user',
text: i18n.ts.user,
children: async () => {
const user = await os.api('users/show', { userId: appearNote.userId });
return getUserMenu(user);
const user = appearNote.userId === $i?.id ? $i : await os.api('users/show', { userId: appearNote.userId });
const { menu, cleanup } = getUserMenu(user);
cleanups.push(cleanup);
return menu;
},
} : undefined,
},
/*
...($i.isModerator || $i.isAdmin ? [
null,
@ -429,5 +434,13 @@ export function getNoteMenu(props: {
}]);
}
return menu;
const cleanup = () => {
if (_DEV_) console.log('note menu cleanup', cleanups);
cleanups.forEach(cleanup => cleanup());
};
return {
menu,
cleanup,
};
}

View file

@ -4,7 +4,7 @@
*/
import { toUnicode } from 'punycode';
import { defineAsyncComponent } from 'vue';
import { defineAsyncComponent, ref, watch } from 'vue';
import * as misskey from 'cherrypick-js';
import { i18n } from '@/i18n';
import copyToClipboard from '@/scripts/copy-to-clipboard';
@ -20,6 +20,8 @@ import { editNickname } from '@/scripts/edit-nickname';
export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) {
const meId = $i ? $i.id : null;
const cleanups = [] as (() => void)[];
async function inviteGroup() {
const groups = await os.api('users/groups/owned');
if (groups.length === 0) {
@ -206,17 +208,32 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
text: i18n.ts.addToList,
children: async () => {
const lists = await userListsCache.fetch(() => os.api('users/lists/list'));
return lists.map(list => {
const isListed = ref(list.userIds.includes(user.id));
cleanups.push(watch(isListed, () => {
if (isListed.value) {
os.apiWithDialog('users/lists/push', {
listId: list.id,
userId: user.id,
}).then(() => {
list.userIds.push(user.id);
});
} else {
os.apiWithDialog('users/lists/pull', {
listId: list.id,
userId: user.id,
}).then(() => {
list.userIds.splice(list.userIds.indexOf(user.id), 1);
});
}
}));
return lists.map(list => ({
text: list.name,
action: async () => {
await os.apiWithDialog('users/lists/push', {
listId: list.id,
userId: user.id,
});
userListsCache.delete();
},
}));
return {
type: 'switch',
text: list.name,
ref: isListed,
};
});
},
}, {
type: 'parent',
@ -349,5 +366,13 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
}))]);
}
return menu;
const cleanup = () => {
if (_DEV_) console.log('user menu cleanup', cleanups);
cleanups.forEach(cleanup => cleanup());
};
return {
menu,
cleanup,
};
}

View file

@ -16,7 +16,7 @@ export type MenuA = { type: 'a', href: string, target?: string, download?: strin
export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean };
export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction };
export type MenuParent = { type: 'parent', text: string, icon?: string, children: OuterMenuItem[] };
export type MenuParent = { type: 'parent', text: string, icon?: string, children: OuterMenuItem[] | (() => Promise<OuterMenuItem[]> | OuterMenuItem[]) };
export type MenuPending = { type: 'pending' };

View file

@ -64,6 +64,8 @@ module.exports = {
'object-curly-spacing': ['error', 'always'],
'space-infix-ops': ['error'],
'space-before-blocks': ['error', 'always'],
'@typescript-eslint/no-explicit-any': ['warn'],
'@typescript-eslint/no-unused-vars': ['warn'],
'@typescript-eslint/no-unnecessary-condition': ['warn'],
'@typescript-eslint/no-var-requires': ['warn'],
'@typescript-eslint/no-inferrable-types': ['warn'],
@ -75,7 +77,7 @@ module.exports = {
}],
'@typescript-eslint/consistent-type-imports': 'off',
'@typescript-eslint/prefer-nullish-coalescing': [
'error',
'warn',
],
'@typescript-eslint/naming-convention': [
'error',

View file

@ -9,15 +9,15 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"dependencies": {
"esbuild": "0.18.11",
"esbuild": "0.18.17",
"idb-keyval": "6.2.1",
"cherrypick-js": "workspace:*"
},
"devDependencies": {
"@typescript-eslint/parser": "5.61.0",
"@typescript-eslint/parser": "6.2.0",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
"eslint": "8.45.0",
"eslint-plugin-import": "2.27.5",
"eslint": "8.46.0",
"eslint-plugin-import": "2.28.0",
"typescript": "5.1.6"
},
"type": "module"