diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-07-27 13:00:14 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-07-27 13:00:14 +0900 |
| commit | a8d45d4b0d24e0c422d4e6d8feab57035239db56 (patch) | |
| tree | 7fd6414990788c281997c75882961b8364e337ab /packages | |
| parent | Merge pull request #11338 from misskey-dev/develop (diff) | |
| parent | chore: update pnpm to 8.6.10 (diff) | |
| download | misskey-a8d45d4b0d24e0c422d4e6d8feab57035239db56.tar.gz misskey-a8d45d4b0d24e0c422d4e6d8feab57035239db56.tar.bz2 misskey-a8d45d4b0d24e0c422d4e6d8feab57035239db56.zip | |
Merge pull request #11384 from misskey-dev/develop
Release: 13.14.2
Diffstat (limited to 'packages')
34 files changed, 194 insertions, 137 deletions
diff --git a/packages/backend/package.json b/packages/backend/package.json index 7f64c2a9ac..f5298c056e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -28,6 +28,7 @@ "@swc/core-android-arm64": "1.3.11", "@swc/core-darwin-arm64": "1.3.56", "@swc/core-darwin-x64": "1.3.56", + "@swc/core-freebsd-x64": "1.3.11", "@swc/core-linux-arm-gnueabihf": "1.3.56", "@swc/core-linux-arm64-gnu": "1.3.56", "@swc/core-linux-arm64-musl": "1.3.56", @@ -39,18 +40,19 @@ "@tensorflow/tfjs": "4.4.0", "@tensorflow/tfjs-node": "4.4.0", "bufferutil": "^4.0.7", - "slacc-android-arm-eabi": "0.0.9", - "slacc-android-arm64": "0.0.9", - "slacc-darwin-arm64": "0.0.9", - "slacc-darwin-universal": "0.0.9", - "slacc-darwin-x64": "0.0.9", - "slacc-freebsd-x64": "0.0.9", - "slacc-linux-arm-gnueabihf": "0.0.9", - "slacc-linux-arm64-gnu": "0.0.9", - "slacc-linux-arm64-musl": "0.0.9", - "slacc-linux-x64-gnu": "0.0.9", - "slacc-win32-arm64-msvc": "0.0.9", - "slacc-win32-x64-msvc": "0.0.9", + "slacc-android-arm-eabi": "0.0.10", + "slacc-android-arm64": "0.0.10", + "slacc-darwin-arm64": "0.0.10", + "slacc-darwin-universal": "0.0.10", + "slacc-darwin-x64": "0.0.10", + "slacc-freebsd-x64": "0.0.10", + "slacc-linux-arm-gnueabihf": "0.0.10", + "slacc-linux-arm64-gnu": "0.0.10", + "slacc-linux-arm64-musl": "0.0.10", + "slacc-linux-x64-gnu": "0.0.10", + "slacc-linux-x64-musl": "0.0.10", + "slacc-win32-arm64-msvc": "0.0.10", + "slacc-win32-x64-msvc": "0.0.10", "utf-8-validate": "^6.0.3" }, "dependencies": { @@ -141,7 +143,7 @@ "semver": "7.5.4", "sharp": "0.32.3", "sharp-read-bmp": "github:misskey-dev/sharp-read-bmp", - "slacc": "0.0.9", + "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "summaly": "github:misskey-dev/summaly", diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index 09039a8b57..1ad7c0137b 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -1,6 +1,5 @@ import * as fs from 'node:fs'; -import * as stream from 'node:stream'; -import * as util from 'node:util'; +import * as stream from 'node:stream/promises'; import { Inject, Injectable } from '@nestjs/common'; import ipaddr from 'ipaddr.js'; import chalk from 'chalk'; @@ -14,7 +13,6 @@ import { StatusError } from '@/misc/status-error.js'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; -const pipeline = util.promisify(stream.pipeline); import { bindThis } from '@/decorators.js'; @Injectable() @@ -102,7 +100,7 @@ export class DownloadService { }); try { - await pipeline(req, fs.createWriteStream(path)); + await stream.pipeline(req, fs.createWriteStream(path)); } catch (e) { if (e instanceof Got.HTTPError) { throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage); @@ -129,7 +127,7 @@ export class DownloadService { // write content at URL to temp file await this.downloadUrl(url, path); - const text = await util.promisify(fs.readFile)(path, 'utf8'); + const text = await fs.promises.readFile(path, 'utf8'); return text; } finally { diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index d43575b336..1028d3760e 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -1,8 +1,7 @@ import * as fs from 'node:fs'; import * as crypto from 'node:crypto'; import { join } from 'node:path'; -import * as stream from 'node:stream'; -import * as util from 'node:util'; +import * as stream from 'node:stream/promises'; import { Injectable } from '@nestjs/common'; import { FSWatcher } from 'chokidar'; import * as fileType from 'file-type'; @@ -16,8 +15,6 @@ import { createTempDir } from '@/misc/create-temp.js'; import { AiService } from '@/core/AiService.js'; import { bindThis } from '@/decorators.js'; -const pipeline = util.promisify(stream.pipeline); - export type FileInfo = { size: number; md5: string; @@ -371,8 +368,7 @@ export class FileInfoService { */ @bindThis public async getFileSize(path: string): Promise<number> { - const getStat = util.promisify(fs.stat); - return (await getStat(path)).size; + return (await fs.promises.stat(path)).size; } /** @@ -381,7 +377,7 @@ export class FileInfoService { @bindThis private async calcHash(path: string): Promise<string> { const hash = crypto.createHash('md5').setEncoding('hex'); - await pipeline(fs.createReadStream(path), hash); + await stream.pipeline(fs.createReadStream(path), hash); return hash.read(); } diff --git a/packages/backend/src/core/activitypub/ApAudienceService.ts b/packages/backend/src/core/activitypub/ApAudienceService.ts index a4ab5eae20..f2d84341f4 100644 --- a/packages/backend/src/core/activitypub/ApAudienceService.ts +++ b/packages/backend/src/core/activitypub/ApAudienceService.ts @@ -95,7 +95,7 @@ export class ApAudienceService { private isPublic(id: string): boolean { return [ 'https://www.w3.org/ns/activitystreams#Public', - 'as#Public', + 'as:Public', 'Public', ].includes(id); } diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index c4c02e7afe..3e8b9fb727 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -1,7 +1,6 @@ import { randomUUID } from 'node:crypto'; -import { pipeline } from 'node:stream'; import * as fs from 'node:fs'; -import { promisify } from 'node:util'; +import * as stream from 'node:stream/promises'; import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; @@ -21,8 +20,6 @@ import type { FastifyRequest, FastifyReply } from 'fastify'; import type { OnApplicationShutdown } from '@nestjs/common'; import type { IEndpointMeta, IEndpoint } from './endpoints.js'; -const pump = promisify(pipeline); - const accessDenied = { message: 'Access denied.', code: 'ACCESS_DENIED', @@ -138,7 +135,7 @@ export class ApiCallService implements OnApplicationShutdown { } const [path] = await createTemp(); - await pump(multipartData.file, fs.createWriteStream(path)); + await stream.pipeline(multipartData.file, fs.createWriteStream(path)); const fields = {} as Record<string, unknown>; for (const [k, v] of Object.entries(multipartData.fields)) { diff --git a/packages/backend/src/server/api/endpoints/admin/invite/list.ts b/packages/backend/src/server/api/endpoints/admin/invite/list.ts index 5d7a7f632c..d8bf6e286f 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite/list.ts @@ -60,7 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } query.limit(ps.limit); - query.skip(ps.offset); + query.offset(ps.offset); const tickets = await query.getMany(); diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 0a150d1dfd..2ae5bc3de3 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -105,7 +105,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } query.limit(ps.limit); - query.skip(ps.offset); + query.offset(ps.offset); const users = await query.getMany(); diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index ddf1a178b1..b140321f44 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -126,7 +126,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { query.andWhere('instance.host like :host', { host: '%' + sqlLikeEscape(ps.host.toLowerCase()) + '%' }); } - const instances = await query.limit(ps.limit).skip(ps.offset).getMany(); + const instances = await query.limit(ps.limit).offset(ps.offset).getMany(); return await this.instanceEntityService.packMany(instances); }); diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts index e2e00def79..81a790316b 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts @@ -42,7 +42,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { .orderBy('tag.count', 'DESC') .groupBy('tag.id') .limit(ps.limit) - .skip(ps.offset) + .offset(ps.offset) .getMany(); return hashtags.map(tag => tag.name); diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts index 0b4ccdcf20..889f644643 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts @@ -83,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { const polls = await query .orderBy('poll.noteId', 'DESC') .limit(ps.limit) - .skip(ps.offset) + .offset(ps.offset) .getMany(); if (polls.length === 0) return []; diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index 2582932e3a..47d0a81552 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { if (me) this.queryService.generateBlockQueryForUsers(query, me); query.limit(ps.limit); - query.skip(ps.offset); + query.offset(ps.offset); const users = await query.getMany(); diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts index d39657059a..eebc5d14d5 100644 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts @@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { query.setParameters(followingQuery.getParameters()); - const users = await query.limit(ps.limit).skip(ps.offset).getMany(); + const users = await query.limit(ps.limit).offset(ps.offset).getMany(); return await this.userEntityService.packMany(users, me, { detail: true }); }); diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index 1180de3611..836218ccd9 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { users = await usernameQuery .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') .limit(ps.limit) - .skip(ps.offset) + .offset(ps.offset) .getMany(); } else { const nameQuery = this.usersRepository.createQueryBuilder('user') @@ -102,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { users = await nameQuery .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') .limit(ps.limit) - .skip(ps.offset) + .offset(ps.offset) .getMany(); if (users.length < ps.limit) { @@ -128,7 +128,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { users = users.concat(await query .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') .limit(ps.limit) - .skip(ps.offset) + .offset(ps.offset) .getMany(), ); } diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index b5eea07775..363cca8feb 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -3,7 +3,7 @@ import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import { createBullBoard } from '@bull-board/api'; -import { BullAdapter } from '@bull-board/api/bullAdapter.js'; +import { BullMQAdapter } from '@bull-board/api/bullMQAdapter.js'; import { FastifyAdapter } from '@bull-board/fastify'; import ms from 'ms'; import sharp from 'sharp'; @@ -168,7 +168,7 @@ export class ClientServerService { this.dbQueue, this.objectStorageQueue, this.webhookDeliverQueue, - ].map(q => new BullAdapter(q)), + ].map(q => new BullMQAdapter(q)), serverAdapter, }); diff --git a/packages/backend/src/server/web/views/gallery-post.pug b/packages/backend/src/server/web/views/gallery-post.pug index a458d7f8c7..9ae25d9ac8 100644 --- a/packages/backend/src/server/web/views/gallery-post.pug +++ b/packages/backend/src/server/web/views/gallery-post.pug @@ -16,8 +16,12 @@ block og meta(property='og:title' content= title) meta(property='og:description' content= post.description) meta(property='og:url' content= url) - meta(property='og:image' content= post.files[0].thumbnailUrl) - meta(property='twitter:card' content='summary_large_image') + if post.isSensitive + meta(property='og:image' content= avatarUrl) + meta(property='twitter:card' content='summary') + else + meta(property='og:image' content= post.files[0].thumbnailUrl) + meta(property='twitter:card' content='summary_large_image') block meta if user.host || profile.noCrawle diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index 33da811a26..39a296f1aa 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -5,6 +5,7 @@ import { Note } from '@/models/entities/Note.js'; import { signup, post, uploadUrl, startServer, initTestDb, api, uploadFile } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; +import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; describe('Note', () => { let app: INestApplicationContext; @@ -164,7 +165,7 @@ describe('Note', () => { test('文字数ぎりぎりで怒られない', async () => { const post = { - text: '!'.repeat(3000), + text: '!'.repeat(MAX_NOTE_TEXT_LENGTH), // 3000文字 }; const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 200); @@ -172,7 +173,7 @@ describe('Note', () => { test('文字数オーバーで怒られる', async () => { const post = { - text: '!'.repeat(3001), + text: '!'.repeat(MAX_NOTE_TEXT_LENGTH + 1), // 3001文字 }; const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index 4e36defb7c..6dcc890cd3 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -108,8 +108,7 @@ function waitForDecode() { .then(() => { loaded = true; }, error => { - console.error('Error occurred during decoding image', img.value, error); - throw Error(error); + console.log('Error occurred during decoding image', img.value, error); }); } else { loaded = false; diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index be0aed6524..0cdccfb169 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -1,5 +1,5 @@ <template> -<div> +<div ref="root"> <XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/> <div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container"> <div @@ -22,8 +22,43 @@ </div> </template> +<script lang="ts"> +/** + * アスペクト比算出のためにHTMLElement.clientWidthを使うが、 + * 大変重たいのでコンテナ要素とメディアリスト幅のペアをキャッシュする + * (タイムラインごとにスクロールコンテナが存在する前提だが……) + */ +const widthCache = new Map<Element, number>(); + +/** + * コンテナ要素がリサイズされたらキャッシュを削除する + */ +const ro = new ResizeObserver(entries => { + for (const entry of entries) { + widthCache.delete(entry.target); + } +}); + +async function getClientWidthWithCache(targetEl: HTMLElement, containerEl: HTMLElement, count = 0) { + if (_DEV_) console.log('getClientWidthWithCache', { targetEl, containerEl, count, cache: widthCache.get(containerEl) }); + if (widthCache.has(containerEl)) return widthCache.get(containerEl)!; + + const width = targetEl.clientWidth; + + if (count <= 10 && width < 64) { + // widthが64未満はおかしいのでリトライする + await new Promise(resolve => setTimeout(resolve, 50)); + return getClientWidthWithCache(targetEl, containerEl, count + 1); + } + + widthCache.set(containerEl, width); + ro.observe(containerEl); + return width; +} +</script> + <script lang="ts" setup> -import { onMounted, watch, shallowRef } from 'vue'; +import { onMounted, onUnmounted, shallowRef } from 'vue'; import * as misskey from 'misskey-js'; import PhotoSwipeLightbox from 'photoswipe/lightbox'; import PhotoSwipe from 'photoswipe'; @@ -34,19 +69,33 @@ import XVideo from '@/components/MkMediaVideo.vue'; import * as os from '@/os'; import { FILE_TYPE_BROWSERSAFE } from '@/const'; import { defaultStore } from '@/store'; +import { getScrollContainer, getBodyScrollHeight } from '@/scripts/scroll'; const props = defineProps<{ mediaList: misskey.entities.DriveFile[]; raw?: boolean; }>(); +const root = shallowRef<HTMLDivElement>(); +const container = shallowRef<HTMLElement | null | undefined>(undefined); const gallery = shallowRef<HTMLDivElement>(); const pswpZIndex = os.claimZIndex('middle'); document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString()); const count = $computed(() => props.mediaList.filter(media => previewable(media)).length); +let lightbox: PhotoSwipeLightbox | null; -function calcAspectRatio() { - if (!gallery.value) return; +const popstateHandler = (): void => { + if (lightbox.pswp && lightbox.pswp.isOpen === true) { + lightbox.pswp.close(); + } +}; + +/** + * アスペクト比をmediaListWithOneImageAppearanceに基づいていい感じに調整する + * aspect-ratioではなくheightを使う + */ +async function calcAspectRatio() { + if (!gallery.value || !root.value) return; let img = props.mediaList[0]; @@ -55,29 +104,47 @@ function calcAspectRatio() { return; } - // アスペクト比上限設定では、横長の場合は高さを縮小させる - const ratioMax = (ratio: number) => `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`; + if (!container.value) container.value = getScrollContainer(root.value); + const width = container.value ? await getClientWidthWithCache(root.value, container.value) : root.value.clientWidth; + + const heightMin = (ratio: number) => { + const imgResizeRatio = width / img.properties.width; + const imgDrawHeight = img.properties.height * imgResizeRatio; + const maxHeight = width * ratio; + const height = Math.min(imgDrawHeight, maxHeight); + if (_DEV_) console.log('Image height calculated:', { width, properties: img.properties, imgResizeRatio, imgDrawHeight, maxHeight, height }); + return `${height}px`; + }; switch (defaultStore.state.mediaListWithOneImageAppearance) { case '16_9': - gallery.value.style.aspectRatio = ratioMax(16 / 9); + gallery.value.style.height = heightMin(9 / 16); break; case '1_1': - gallery.value.style.aspectRatio = ratioMax(1); + gallery.value.style.height = heightMin(1); break; case '2_3': - gallery.value.style.aspectRatio = ratioMax(2 / 3); + gallery.value.style.height = heightMin(3 / 2); break; - default: - gallery.value.style.aspectRatio = ''; + default: { + const maxHeight = Math.max(64, (container.value ? container.value.clientHeight : getBodyScrollHeight()) * 0.5 || 360); + if (width === 0 || !maxHeight) return; + const imgResizeRatio = width / img.properties.width; + const imgDrawHeight = img.properties.height * imgResizeRatio; + gallery.value.style.height = `${Math.max(64, Math.min(imgDrawHeight, maxHeight))}px`; + gallery.value.style.minHeight = 'initial'; + gallery.value.style.maxHeight = 'initial'; break; + } } -} -watch([defaultStore.reactiveState.mediaListWithOneImageAppearance, gallery], () => calcAspectRatio()); + gallery.value.style.aspectRatio = 'initial'; +} onMounted(() => { - const lightbox = new PhotoSwipeLightbox({ + calcAspectRatio(); + + lightbox = new PhotoSwipeLightbox({ dataSource: props.mediaList .filter(media => { if (media.type === 'image/svg+xml') return true; // svgのwebpublicはpngなのでtrue @@ -161,12 +228,7 @@ onMounted(() => { lightbox.init(); - window.addEventListener('popstate', () => { - if (lightbox.pswp && lightbox.pswp.isOpen === true) { - lightbox.pswp.close(); - return; - } - }); + window.addEventListener('popstate', popstateHandler); lightbox.on('beforeOpen', () => { history.pushState(null, '', '#pswp'); @@ -179,6 +241,12 @@ onMounted(() => { }); }); +onUnmounted(() => { + window.removeEventListener('popstate', popstateHandler); + lightbox?.destroy(); + lightbox = null; +}); + const previewable = (file: misskey.entities.DriveFile): boolean => { if (file.type === 'image/svg+xml') return true; // svgのwebpublic/thumbnailはpngなのでtrue // FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切 @@ -203,7 +271,7 @@ const previewable = (file: misskey.entities.DriveFile): boolean => { &.n1 { grid-template-rows: 1fr; - // default (expand) + // default but fallback (expand) min-height: 64px; max-height: clamp( 64px, @@ -212,20 +280,20 @@ const previewable = (file: misskey.entities.DriveFile): boolean => { ); &.n116_9 { - min-height: none; - max-height: none; + min-height: initial; + max-height: initial; aspect-ratio: 16 / 9; // fallback } &.n11_1{ - min-height: none; - max-height: none; + min-height: initial; + max-height: initial; aspect-ratio: 1 / 1; // fallback } &.n12_3 { - min-height: none; - max-height: none; + min-height: initial; + max-height: initial; aspect-ratio: 2 / 3; // fallback } } diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index bb5c6c7aab..bc6e3e4171 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -431,6 +431,7 @@ defineExpose({ margin: auto; padding: 32px; display: flex; + overflow: auto; @media (max-width: 500px) { padding: 16px; diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index deeae6e940..02431a4557 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -341,6 +341,7 @@ function reply(viaKeyboard = false): void { pleaseLogin(); os.post({ reply: appearNote, + channel: appearNote.channel, animation: !viaKeyboard, }, () => { focus(); @@ -758,6 +759,7 @@ function showReactions(): void { padding: 16px; border: dashed 1px var(--renote); border-radius: 8px; + overflow: clip; } .channel { diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 1f8a36b8de..a40b9cd2bd 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -320,6 +320,7 @@ function reply(viaKeyboard = false): void { showMovedDialog(); os.post({ reply: appearNote, + channel: appearNote.channel, animation: !viaKeyboard, }, () => { focus(); @@ -595,6 +596,7 @@ if (appearNote.replyId) { padding: 16px; border: dashed 1px var(--renote); border-radius: 8px; + overflow: clip; } .channel { diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index 21be1454a7..98ea91d6be 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -37,7 +37,6 @@ const showContent = $ref(false); display: flex; margin: 0; padding: 0; - overflow: clip; font-size: 0.95em; } diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index f516ccbad8..0527811ab0 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -540,7 +540,7 @@ function onCompositionEnd(ev: CompositionEvent) { } async function onPaste(ev: ClipboardEvent) { - for (const { item, i } of Array.from(ev.clipboardData.items).map((item, i) => ({ item, i }))) { + for (const { item, i } of Array.from(ev.clipboardData.items, (item, i) => ({ item, i }))) { if (item.kind === 'file') { const file = item.getAsFile(); const lio = file.name.lastIndexOf('.'); @@ -907,7 +907,6 @@ defineExpose({ display: flex; flex-wrap: nowrap; gap: 4px; - margin-bottom: -10px; } .headerLeft { @@ -1025,7 +1024,7 @@ defineExpose({ } .targetNote { - padding: 10px 20px 16px 20px; + padding: 0 20px 16px 20px; } .withQuote { diff --git a/packages/frontend/src/components/MkTagCloud.vue b/packages/frontend/src/components/MkTagCloud.vue index 6e4e054aad..21e76b766b 100644 --- a/packages/frontend/src/components/MkTagCloud.vue +++ b/packages/frontend/src/components/MkTagCloud.vue @@ -16,8 +16,8 @@ import tinycolor from 'tinycolor2'; const loaded = !!window.TagCanvas; const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz'; const computedStyle = getComputedStyle(document.documentElement); -const idForCanvas = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join(''); -const idForTags = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join(''); +const idForCanvas = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join(''); +const idForTags = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join(''); let available = $ref(false); let rootEl = $shallowRef<HTMLElement | null>(null); let canvasEl = $shallowRef<HTMLCanvasElement | null>(null); diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 2595ebc45d..062d0bd87a 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -38,14 +38,6 @@ const prepend = note => { } }; -const onUserAdded = () => { - tlComponent.pagingComponent?.reload(); -}; - -const onUserRemoved = () => { - tlComponent.pagingComponent?.reload(); -}; - let endpoint; let query; let connection; @@ -125,8 +117,6 @@ if (props.src === 'antenna') { listId: props.list, }); connection.on('note', prepend); - connection.on('userAdded', onUserAdded); - connection.on('userRemoved', onUserRemoved); } else if (props.src === 'channel') { endpoint = 'channels/timeline'; query = { diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index 359bbeadc3..c6f089420e 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -110,7 +110,7 @@ const selectAll = () => { if (selectedEmojis.value.length > 0) { selectedEmojis.value = []; } else { - selectedEmojis.value = emojisPaginationComponent.value.items.map(item => item.id); + selectedEmojis.value = Array.from(emojisPaginationComponent.value.items.values(), item => item.id); } }; diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index 27a4cd0595..8b316c7182 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -166,7 +166,7 @@ const headerActions = $computed(() => []); const headerTabs = $computed(() => []); definePageMetadata(computed(() => page ? { - title: computed(() => page.title || page.name), + title: page.title || page.name, avatar: page.user, path: `/@${page.user.username}/pages/${page.name}`, share: { diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index cfe5cd31e7..31a288745e 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -258,6 +258,7 @@ watch([ showGapBetweenNotesInTimeline, instanceTicker, overridedDeviceKind, + mediaListWithOneImageAppearance, ], async () => { await reloadAsk(); }); diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index 1aa1a5f81c..535a8820d2 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -32,7 +32,7 @@ </template> <script lang="ts" setup> -import { computed, onMounted, onUnmounted } from 'vue'; +import { computed, onMounted, onUnmounted, ref } from 'vue'; import { v4 as uuid } from 'uuid'; import FormSection from '@/components/form/section.vue'; import MkButton from '@/components/MkButton.vue'; @@ -93,15 +93,6 @@ const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [ 'darkTheme', 'syncDeviceDarkMode', 'plugins', - 'mediaVolume', - 'sound_masterVolume', - 'sound_note', - 'sound_noteMy', - 'sound_notification', - 'sound_chat', - 'sound_chatBg', - 'sound_antenna', - 'sound_channel', ]; const scope = ['clientPreferencesProfiles']; @@ -125,18 +116,18 @@ type Profile = { const connection = $i && useStream().useChannel('main'); -let profiles = $ref<Record<string, Profile> | null>(null); +const profiles = ref<Record<string, Profile> | null>(null); os.api('i/registry/get-all', { scope }) .then(res => { - profiles = res || {}; + profiles.value = res || {}; }); function isObject(value: unknown): value is Record<string, unknown> { return value != null && typeof value === 'object' && !Array.isArray(value); } -function validate(profile: unknown): void { +function validate(profile: any): void { if (!isObject(profile)) throw new Error('not an object'); // Check if unnecessary properties exist @@ -147,9 +138,9 @@ function validate(profile: unknown): void { // Check if createdAt and updatedAt is Date // https://zenn.dev/lollipop_onl/articles/eoz-judge-js-invalid-date - if (!profile.createdAt || Number.isNaN(new Date(profile.createdAt).getTime())) throw new Error('createdAt is falsy or not Date'); + if (!profile.createdAt || Number.isNaN(new Date(profile.createdAt as any).getTime())) throw new Error('createdAt is falsy or not Date'); if (profile.updatedAt) { - if (Number.isNaN(new Date(profile.updatedAt).getTime())) { + if (Number.isNaN(new Date(profile.updatedAt as any).getTime())) { throw new Error('updatedAt is not Date'); } } else if (profile.updatedAt !== null) { @@ -181,14 +172,14 @@ function getSettings(): Profile['settings'] { } async function saveNew(): Promise<void> { - if (!profiles) return; + if (!profiles.value) return; const { canceled, result: name } = await os.inputText({ title: ts._preferencesBackups.inputName, }); if (canceled) return; - if (Object.values(profiles).some(x => x.name === name)) { + if (Object.values(profiles.value).some(x => x.name === name)) { return os.alert({ title: ts._preferencesBackups.cannotSave, text: t('_preferencesBackups.nameAlreadyExists', { name }), @@ -212,7 +203,7 @@ function loadFile(): void { input.type = 'file'; input.multiple = false; input.onchange = async () => { - if (!profiles) return; + if (!profiles.value) return; if (!input.files || input.files.length === 0) return; const file = input.files[0]; @@ -233,7 +224,7 @@ function loadFile(): void { return os.alert({ type: 'error', title: ts._preferencesBackups.cannotLoad, - text: err?.message, + text: (err as any)?.message ?? '', }); } @@ -252,9 +243,9 @@ function loadFile(): void { } async function applyProfile(id: string): Promise<void> { - if (!profiles) return; + if (!profiles.value) return; - const profile = profiles[id]; + const profile = profiles.value[id]; const { canceled: cancel1 } = await os.confirm({ type: 'warning', @@ -312,23 +303,23 @@ async function applyProfile(id: string): Promise<void> { } async function deleteProfile(id: string): Promise<void> { - if (!profiles) return; + if (!profiles.value) return; const { canceled } = await os.confirm({ type: 'info', title: ts.delete, - text: t('deleteAreYouSure', { x: profiles[id].name }), + text: t('deleteAreYouSure', { x: profiles.value[id].name }), }); if (canceled) return; await os.apiWithDialog('i/registry/remove', { scope, key: id }); - delete profiles[id]; + delete profiles.value[id]; } async function save(id: string): Promise<void> { - if (!profiles) return; + if (!profiles.value) return; - const { name, createdAt } = profiles[id]; + const { name, createdAt } = profiles.value[id]; const { canceled } = await os.confirm({ type: 'info', @@ -349,25 +340,25 @@ async function save(id: string): Promise<void> { } async function rename(id: string): Promise<void> { - if (!profiles) return; + if (!profiles.value) return; const { canceled: cancel1, result: name } = await os.inputText({ title: ts._preferencesBackups.inputName, }); - if (cancel1 || profiles[id].name === name) return; + if (cancel1 || profiles.value[id].name === name) return; - if (Object.values(profiles).some(x => x.name === name)) { + if (Object.values(profiles.value).some(x => x.name === name)) { return os.alert({ title: ts._preferencesBackups.cannotSave, text: t('_preferencesBackups.nameAlreadyExists', { name }), }); } - const registry = Object.assign({}, { ...profiles[id] }); + const registry = Object.assign({}, { ...profiles.value[id] }); const { canceled: cancel2 } = await os.confirm({ type: 'info', - title: ts._preferencesBackups.rename, + title: ts.rename, text: t('_preferencesBackups.renameConfirm', { old: registry.name, new: name }), }); if (cancel2) return; @@ -377,7 +368,7 @@ async function rename(id: string): Promise<void> { } function menu(ev: MouseEvent, profileId: string) { - if (!profiles) return; + if (!profiles.value) return; return os.popupMenu([{ text: ts._preferencesBackups.apply, @@ -387,8 +378,8 @@ function menu(ev: MouseEvent, profileId: string) { type: 'a', text: ts.download, icon: 'ti ti-download', - href: URL.createObjectURL(new Blob([JSON.stringify(profiles[profileId], null, 2)], { type: 'application/json' })), - download: `${profiles[profileId].name}.json`, + href: URL.createObjectURL(new Blob([JSON.stringify(profiles.value[profileId], null, 2)], { type: 'application/json' })), + download: `${profiles.value[profileId].name}.json`, }, null, { text: ts.rename, icon: 'ti ti-forms', @@ -402,16 +393,16 @@ function menu(ev: MouseEvent, profileId: string) { icon: 'ti ti-trash', action: () => deleteProfile(profileId), danger: true, - }], ev.currentTarget ?? ev.target); + }], (ev.currentTarget ?? ev.target ?? undefined) as unknown as HTMLElement | undefined); } onMounted(() => { // streamingのuser storage updateイベントを監視して更新 connection?.on('registryUpdated', ({ scope: recievedScope, key, value }) => { if (!recievedScope || recievedScope.length !== scope.length || recievedScope[0] !== scope[0]) return; - if (!profiles) return; + if (!profiles.value) return; - profiles[key] = value; + profiles.value[key] = value; }); }); diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index 5453fe827d..c8b90b4fd7 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -11,7 +11,6 @@ export function createAiScriptEnv(opts) { USER_NAME: $i ? values.STR($i.name) : values.NULL, USER_USERNAME: $i ? values.STR($i.username) : values.NULL, CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value), - CURRENT_URL: values.STR(window.location.href), 'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => { await os.alert({ type: type ? type.value : 'info', @@ -48,5 +47,8 @@ export function createAiScriptEnv(opts) { utils.assertString(key); return utils.jsToVal(JSON.parse(miLocalStorage.getItem(`aiscript:${opts.storageKey}:${key.value}`))); }), + 'Mk:url': values.FN_NATIVE(() => { + return values.STR(window.location.href); + }), }; } diff --git a/packages/frontend/src/scripts/emoji-base.ts b/packages/frontend/src/scripts/emoji-base.ts index 3f05642d57..e91f2fa22d 100644 --- a/packages/frontend/src/scripts/emoji-base.ts +++ b/packages/frontend/src/scripts/emoji-base.ts @@ -2,7 +2,7 @@ const twemojiSvgBase = '/twemoji'; const fluentEmojiPngBase = '/fluent-emoji'; export function char2twemojiFilePath(char: string): string { - let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16)); + let codes = Array.from(char, x => x.codePointAt(0)?.toString(16)); if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); codes = codes.filter(x => x && x.length); const fileName = codes.join('-'); @@ -10,7 +10,7 @@ export function char2twemojiFilePath(char: string): string { } export function char2fluentEmojiFilePath(char: string): string { - let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16)); + let codes = Array.from(char, x => x.codePointAt(0)?.toString(16)); // Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25 if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char); if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts index 44a58d6c7d..891da6eb78 100644 --- a/packages/frontend/src/scripts/select-file.ts +++ b/packages/frontend/src/scripts/select-file.ts @@ -12,7 +12,8 @@ export function chooseFileFromPc(multiple: boolean, keepOriginal = false): Promi input.type = 'file'; input.multiple = multiple; input.onchange = () => { - const promises = Array.from(input.files).map(file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal)); + if (!input.files) return res([]); + const promises = Array.from(input.files, file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal)); Promise.all(promises).then(driveFiles => { res(driveFiles); diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index bd74db7c85..85ad442ba4 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -6,11 +6,13 @@ --marginHalf: 10px; --margin: var(--marginFull); - --minBottomSpacing: 0px; + + // switch dynamically + --minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px))); + --minBottomSpacing: var(--minBottomSpacingMobile); @media (max-width: 500px) { --margin: var(--marginHalf); - --minBottomSpacing: calc(72px + max(12px, env(safe-area-inset-bottom, 0px))); } //--ad: rgb(255 169 0 / 10%); diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index 9ae43c39d3..50866b23ed 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -207,9 +207,11 @@ watch($$(navFooter), () => { if (navFooter) { navFooterHeight = navFooter.offsetHeight; document.body.style.setProperty('--stickyBottom', `${navFooterHeight}px`); + document.body.style.setProperty('--minBottomSpacing', 'var(--minBottomSpacingMobile)'); } else { navFooterHeight = 0; document.body.style.setProperty('--stickyBottom', '0px'); + document.body.style.setProperty('--minBottomSpacing', '0px'); } }, { immediate: true, |