diff options
| author | tamaina <tamaina@hotmail.co.jp> | 2022-01-16 18:01:23 +0900 |
|---|---|---|
| committer | tamaina <tamaina@hotmail.co.jp> | 2022-01-16 18:01:23 +0900 |
| commit | 04bafc5aeef5dc5db41679ee959ceb300ceb6187 (patch) | |
| tree | 1bfcc2cc8e0a6305fa98567db01d1216579082da | |
| parent | Merge branch 'develop' into pizzax-indexeddb (diff) | |
| parent | wip: refactor(client): migrate components to composition api (diff) | |
| download | misskey-04bafc5aeef5dc5db41679ee959ceb300ceb6187.tar.gz misskey-04bafc5aeef5dc5db41679ee959ceb300ceb6187.tar.bz2 misskey-04bafc5aeef5dc5db41679ee959ceb300ceb6187.zip | |
Merge branch 'develop' into pizzax-indexeddb
190 files changed, 3525 insertions, 4766 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index ed5bdd3f1c..88e8055daa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ - Chat UIが削除されました ### Improvements +- カスタム絵文字一括編集機能 +- カスタム絵文字一括インポート ### Bugfixes diff --git a/packages/backend/package.json b/packages/backend/package.json index 65da382e2d..c940e98301 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -180,6 +180,7 @@ "typeorm": "0.2.39", "typescript": "4.4.4", "ulid": "2.3.0", + "unzipper": "0.10.11", "uuid": "8.3.2", "web-push": "3.4.5", "websocket": "1.0.34", diff --git a/packages/backend/src/misc/gen-avatar.ts b/packages/backend/src/misc/gen-identicon.ts index 8838ec8d15..5cedd7afaf 100644 --- a/packages/backend/src/misc/gen-avatar.ts +++ b/packages/backend/src/misc/gen-identicon.ts @@ -1,5 +1,6 @@ /** - * Random avatar generator + * Identicon generator + * https://en.wikipedia.org/wiki/Identicon */ import * as p from 'pureimage'; @@ -34,9 +35,9 @@ const cellSize = actualSize / n; const sideN = Math.floor(n / 2); /** - * Generate buffer of random avatar by seed + * Generate buffer of an identicon by seed */ -export function genAvatar(seed: string, stream: WriteStream): Promise<void> { +export function genIdenticon(seed: string, stream: WriteStream): Promise<void> { const rand = gen.create(seed); const canvas = p.make(size, size); const ctx = canvas.getContext('2d'); diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 3dc7c67ec2..85141cdc41 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -159,7 +159,7 @@ export class UserRepository extends Repository<User> { if (user.avatarUrl) { return user.avatarUrl; } else { - return `${config.url}/random-avatar/${user.id}`; + return `${config.url}/identicon/${user.id}`; } } diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index 2fbc1b1c01..f9994c3b59 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -213,6 +213,16 @@ export function createImportUserListsJob(user: ThinUser, fileId: DriveFile['id'] }); } +export function createImportCustomEmojisJob(user: ThinUser, fileId: DriveFile['id']) { + return dbQueue.add('importCustomEmojis', { + user: user, + fileId: fileId, + }, { + removeOnComplete: true, + removeOnFail: true, + }); +} + export function createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) { return dbQueue.add('deleteAccount', { user: user, diff --git a/packages/backend/src/queue/processors/db/export-custom-emojis.ts b/packages/backend/src/queue/processors/db/export-custom-emojis.ts index 3930b9d6d4..a420866dcf 100644 --- a/packages/backend/src/queue/processors/db/export-custom-emojis.ts +++ b/packages/backend/src/queue/processors/db/export-custom-emojis.ts @@ -52,7 +52,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi }); }; - await writeMeta(`{"metaVersion":1,"emojis":[`); + await writeMeta(`{"metaVersion":2,"emojis":[`); const customEmojis = await Emojis.find({ where: { @@ -64,9 +64,9 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi }); for (const emoji of customEmojis) { - const exportId = ulid().toLowerCase(); const ext = mime.extension(emoji.type); - const emojiPath = path + '/' + exportId + (ext ? '.' + ext : ''); + const fileName = emoji.name + (ext ? '.' + ext : ''); + const emojiPath = path + '/' + fileName; fs.writeFileSync(emojiPath, '', 'binary'); let downloaded = false; @@ -77,8 +77,12 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi logger.error(e); } + if (!downloaded) { + fs.unlinkSync(emojiPath); + } + const content = JSON.stringify({ - id: exportId, + fileName: fileName, downloaded: downloaded, emoji: emoji, }); diff --git a/packages/backend/src/queue/processors/db/import-custom-emojis.ts b/packages/backend/src/queue/processors/db/import-custom-emojis.ts new file mode 100644 index 0000000000..eb386bbb42 --- /dev/null +++ b/packages/backend/src/queue/processors/db/import-custom-emojis.ts @@ -0,0 +1,84 @@ +import * as Bull from 'bull'; +import * as tmp from 'tmp'; +import * as fs from 'fs'; +const unzipper = require('unzipper'); +import { getConnection } from 'typeorm'; + +import { queueLogger } from '../../logger'; +import { downloadUrl } from '@/misc/download-url'; +import { DriveFiles, Emojis } from '@/models/index'; +import { DbUserImportJobData } from '@/queue/types'; +import addFile from '@/services/drive/add-file'; +import { genId } from '@/misc/gen-id'; + +const logger = queueLogger.createSubLogger('import-custom-emojis'); + +// TODO: 名前衝突時の動作を選べるようにする +export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> { + logger.info(`Importing custom emojis ...`); + + const file = await DriveFiles.findOne({ + id: job.data.fileId, + }); + if (file == null) { + done(); + return; + } + + // Create temp dir + const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => { + tmp.dir((e, path, cleanup) => { + if (e) return rej(e); + res([path, cleanup]); + }); + }); + + logger.info(`Temp dir is ${path}`); + + const destPath = path + '/emojis.zip'; + + try { + fs.writeFileSync(destPath, '', 'binary'); + await downloadUrl(file.url, destPath); + } catch (e) { // TODO: 何度か再試行 + logger.error(e); + throw e; + } + + const outputPath = path + '/emojis'; + const unzipStream = fs.createReadStream(destPath); + const extractor = unzipper.Extract({ path: outputPath }); + extractor.on('close', async () => { + const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8'); + const meta = JSON.parse(metaRaw); + + for (const record of meta.emojis) { + if (!record.downloaded) continue; + const emojiInfo = record.emoji; + const emojiPath = outputPath + '/' + record.fileName; + await Emojis.delete({ + name: emojiInfo.name, + }); + const driveFile = await addFile(null, emojiPath, record.fileName, null, null, true); + const emoji = await Emojis.insert({ + id: genId(), + updatedAt: new Date(), + name: emojiInfo.name, + category: emojiInfo.category, + host: null, + aliases: emojiInfo.aliases, + url: driveFile.url, + type: driveFile.type, + }).then(x => Emojis.findOneOrFail(x.identifiers[0])); + } + + await getConnection().queryResultCache!.remove(['meta_emojis']); + + cleanup(); + + logger.succ('Imported'); + done(); + }); + unzipStream.pipe(extractor); + logger.succ(`Unzipping to ${outputPath}`); +} diff --git a/packages/backend/src/queue/processors/db/index.ts b/packages/backend/src/queue/processors/db/index.ts index 1542f401ef..5fffa378f5 100644 --- a/packages/backend/src/queue/processors/db/index.ts +++ b/packages/backend/src/queue/processors/db/index.ts @@ -12,6 +12,7 @@ import { importUserLists } from './import-user-lists'; import { deleteAccount } from './delete-account'; import { importMuting } from './import-muting'; import { importBlocking } from './import-blocking'; +import { importCustomEmojis } from './import-custom-emojis'; const jobs = { deleteDriveFiles, @@ -25,6 +26,7 @@ const jobs = { importMuting, importBlocking, importUserLists, + importCustomEmojis, deleteAccount, } as Record<string, Bull.ProcessCallbackFunction<DbJobData> | Bull.ProcessPromiseFunction<DbJobData>>; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts new file mode 100644 index 0000000000..ef0f315022 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts @@ -0,0 +1,39 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { ID } from '@/misc/cafy-id'; +import { Emojis } from '@/models/index'; +import { getConnection, In } from 'typeorm'; +import { ApiError } from '../../../error'; + +export const meta = { + tags: ['admin'], + + requireCredential: true as const, + requireModerator: true, + + params: { + ids: { + validator: $.arr($.type(ID)), + }, + + aliases: { + validator: $.arr($.str), + }, + }, +}; + +// eslint-disable-next-line import/no-default-export +export default define(meta, async (ps) => { + const emojis = await Emojis.find({ + id: In(ps.ids), + }); + + for (const emoji of emojis) { + await Emojis.update(emoji.id, { + updatedAt: new Date(), + aliases: [...new Set(emoji.aliases.concat(ps.aliases))], + }); + } + + await getConnection().queryResultCache!.remove(['meta_emojis']); +}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts new file mode 100644 index 0000000000..a99cd3c978 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts @@ -0,0 +1,37 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { ID } from '@/misc/cafy-id'; +import { Emojis } from '@/models/index'; +import { getConnection, In } from 'typeorm'; +import { insertModerationLog } from '@/services/insert-moderation-log'; +import { ApiError } from '../../../error'; + +export const meta = { + tags: ['admin'], + + requireCredential: true as const, + requireModerator: true, + + params: { + ids: { + validator: $.arr($.type(ID)), + }, + }, +}; + +// eslint-disable-next-line import/no-default-export +export default define(meta, async (ps, me) => { + const emojis = await Emojis.find({ + id: In(ps.ids), + }); + + for (const emoji of emojis) { + await Emojis.delete(emoji.id); + + await getConnection().queryResultCache!.remove(['meta_emojis']); + + insertModerationLog(me, 'deleteEmoji', { + emoji: emoji, + }); + } +}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts index 440c1008c7..870245ac92 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/remove.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -37,7 +37,7 @@ export default define(meta, async (ps, me) => { await getConnection().queryResultCache!.remove(['meta_emojis']); - insertModerationLog(me, 'removeEmoji', { + insertModerationLog(me, 'deleteEmoji', { emoji: emoji, }); }); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts new file mode 100644 index 0000000000..04895b8f20 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts @@ -0,0 +1,21 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { createImportCustomEmojisJob } from '@/queue/index'; +import ms from 'ms'; +import { ID } from '@/misc/cafy-id'; + +export const meta = { + secure: true, + requireCredential: true as const, + requireModerator: true, + params: { + fileId: { + validator: $.type(ID), + }, + }, +}; + +// eslint-disable-next-line import/no-default-export +export default define(meta, async (ps, user) => { + createImportCustomEmojisJob(user, ps.fileId); +}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts new file mode 100644 index 0000000000..4c771b4e42 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts @@ -0,0 +1,39 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { ID } from '@/misc/cafy-id'; +import { Emojis } from '@/models/index'; +import { getConnection, In } from 'typeorm'; +import { ApiError } from '../../../error'; + +export const meta = { + tags: ['admin'], + + requireCredential: true as const, + requireModerator: true, + + params: { + ids: { + validator: $.arr($.type(ID)), + }, + + aliases: { + validator: $.arr($.str), + }, + }, +}; + +// eslint-disable-next-line import/no-default-export +export default define(meta, async (ps) => { + const emojis = await Emojis.find({ + id: In(ps.ids), + }); + + for (const emoji of emojis) { + await Emojis.update(emoji.id, { + updatedAt: new Date(), + aliases: emoji.aliases.filter(x => !ps.aliases.includes(x)), + }); + } + + await getConnection().queryResultCache!.remove(['meta_emojis']); +}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts new file mode 100644 index 0000000000..33dccbc642 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts @@ -0,0 +1,35 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { ID } from '@/misc/cafy-id'; +import { Emojis } from '@/models/index'; +import { getConnection, In } from 'typeorm'; +import { ApiError } from '../../../error'; + +export const meta = { + tags: ['admin'], + + requireCredential: true as const, + requireModerator: true, + + params: { + ids: { + validator: $.arr($.type(ID)), + }, + + aliases: { + validator: $.arr($.str), + }, + }, +}; + +// eslint-disable-next-line import/no-default-export +export default define(meta, async (ps) => { + await Emojis.update({ + id: In(ps.ids), + }, { + updatedAt: new Date(), + aliases: ps.aliases, + }); + + await getConnection().queryResultCache!.remove(['meta_emojis']); +}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts new file mode 100644 index 0000000000..d40ed52da7 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts @@ -0,0 +1,35 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { ID } from '@/misc/cafy-id'; +import { Emojis } from '@/models/index'; +import { getConnection, In } from 'typeorm'; +import { ApiError } from '../../../error'; + +export const meta = { + tags: ['admin'], + + requireCredential: true as const, + requireModerator: true, + + params: { + ids: { + validator: $.arr($.type(ID)), + }, + + category: { + validator: $.optional.nullable.str, + }, + }, +}; + +// eslint-disable-next-line import/no-default-export +export default define(meta, async (ps) => { + await Emojis.update({ + id: In(ps.ids), + }, { + updatedAt: new Date(), + category: ps.category, + }); + + await getConnection().queryResultCache!.remove(['meta_emojis']); +}); diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts index 85fe21accb..764306c7d8 100644 --- a/packages/backend/src/server/index.ts +++ b/packages/backend/src/server/index.ts @@ -23,7 +23,7 @@ import Logger from '@/services/logger'; import { envOption } from '../env'; import { UserProfiles, Users } from '@/models/index'; import { networkChart } from '@/services/chart/index'; -import { genAvatar } from '@/misc/gen-avatar'; +import { genIdenticon } from '@/misc/gen-identicon'; import { createTemp } from '@/misc/create-temp'; import { publishMainStream } from '@/services/stream'; import * as Acct from 'misskey-js/built/acct'; @@ -84,9 +84,9 @@ router.get('/avatar/@:acct', async ctx => { } }); -router.get('/random-avatar/:x', async ctx => { +router.get('/identicon/:x', async ctx => { const [temp] = await createTemp(); - await genAvatar(ctx.params.x, fs.createWriteStream(temp)); + await genIdenticon(ctx.params.x, fs.createWriteStream(temp)); ctx.set('Content-Type', 'image/png'); ctx.body = fs.createReadStream(temp); }); diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock index 16144b6d57..9e21fb29e3 100644 --- a/packages/backend/yarn.lock +++ b/packages/backend/yarn.lock @@ -1522,6 +1522,11 @@ big-integer@^1.6.16: resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== +big-integer@^1.6.17: + version "1.6.51" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" + integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== + big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -1532,6 +1537,14 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== +binary@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" + integrity sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk= + dependencies: + buffers "~0.1.1" + chainsaw "~0.1.0" + bl@^4.0.1, bl@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" @@ -1546,6 +1559,11 @@ bluebird@^3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +bluebird@~3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" + integrity sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM= + blurhash@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.4.tgz#a7010ceb3019cd2c9809b17c910ebf6175d29244" @@ -1677,6 +1695,11 @@ buffer-from@^1.0.0, buffer-from@^1.1.1: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer-indexof-polyfill@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c" + integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A== + buffer-writer@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" @@ -1707,6 +1730,11 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s= + bufferutil@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.1.tgz#3a177e8e5819a1243fe16b63a199951a7ad8d4a7" @@ -1875,6 +1903,13 @@ cbor@8.1.0: dependencies: nofilter "^3.1.0" +chainsaw@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" + integrity sha1-XqtQsor+WAdNDVgpE4iCi15fvJg= + dependencies: + traverse ">=0.3.0 <0.4" + chalk@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72" @@ -2789,6 +2824,13 @@ dotenv@^8.2.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== +duplexer2@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= + dependencies: + readable-stream "^2.0.2" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -3480,6 +3522,16 @@ fsevents@~2.1.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== +fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -3690,7 +3742,7 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.4: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== -graceful-fs@^4.2.0: +graceful-fs@^4.2.0, graceful-fs@^4.2.2: version "4.2.8" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== @@ -4007,7 +4059,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -4800,6 +4852,11 @@ lilconfig@^2.0.3: resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.3.tgz#68f3005e921dafbd2a2afb48379986aa6d2579fd" integrity sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg== +listenercount@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" + integrity sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc= + loader-runner@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" @@ -5204,7 +5261,7 @@ mkdirp-classic@^0.5.3: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@0.x, mkdirp@^0.5.4: +mkdirp@0.x, "mkdirp@>=0.5 0", mkdirp@^0.5.4: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -6598,7 +6655,7 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.2.2: +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -6780,6 +6837,13 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rimraf@2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -6914,7 +6978,7 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= -setimmediate@^1.0.5: +setimmediate@^1.0.5, setimmediate@~1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= @@ -7584,6 +7648,11 @@ trace-redirect@1.0.6: resolved "https://registry.yarnpkg.com/trace-redirect/-/trace-redirect-1.0.6.tgz#ac629b5bf8247d30dde5a35fe9811b811075b504" integrity sha512-UUfa1DjjU5flcjMdaFIiIEGDTyu2y/IiMjOX4uGXa7meKBS4vD4f2Uy/tken9Qkd4Jsm4sRsfZcIIPqrRVF3Mg== +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk= + ts-jest@^25.2.1: version "25.5.1" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.5.1.tgz#2913afd08f28385d54f2f4e828be4d261f4337c7" @@ -7827,6 +7896,22 @@ unpipe@1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= +unzipper@0.10.11: + version "0.10.11" + resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.11.tgz#0b4991446472cbdb92ee7403909f26c2419c782e" + integrity sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw== + dependencies: + big-integer "^1.6.17" + binary "~0.3.0" + bluebird "~3.4.1" + buffer-indexof-polyfill "~1.0.0" + duplexer2 "~0.1.4" + fstream "^1.0.12" + graceful-fs "^4.2.2" + listenercount "~1.0.1" + readable-stream "~2.3.6" + setimmediate "~1.0.4" + uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" diff --git a/packages/client/.eslintrc.js b/packages/client/.eslintrc.js index 8e4ff6e455..e0113019ac 100644 --- a/packages/client/.eslintrc.js +++ b/packages/client/.eslintrc.js @@ -14,6 +14,10 @@ module.exports = { "plugin:vue/vue3-recommended" ], rules: { + // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため + // data の禁止理由: 抽象的すぎるため + // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため + "id-denylist": ["error", "window", "data", "e"], "vue/attributes-order": ["error", { "alphabetical": false }], diff --git a/packages/client/package.json b/packages/client/package.json index 88f8077df3..c2dc821b39 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -21,7 +21,6 @@ "@types/katex": "0.11.1", "@types/matter-js": "0.17.6", "@types/mocha": "8.2.3", - "@types/node": "16.11.12", "@types/oauth": "0.9.1", "@types/parse5": "6.0.3", "@types/punycode": "2.1.0", diff --git a/packages/client/src/components/note.sub.vue b/packages/client/src/components/MkNoteSub.vue index de4218e535..30c27e6235 100644 --- a/packages/client/src/components/note.sub.vue +++ b/packages/client/src/components/MkNoteSub.vue @@ -10,13 +10,13 @@ <XCwButton v-model="showContent" :note="note"/> </p> <div v-show="note.cw == null || showContent" class="content"> - <XSubNote-content class="text" :note="note"/> + <MkNoteSubNoteContent class="text" :note="note"/> </div> </div> </div> </div> <template v-if="depth < 5"> - <XSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :depth="depth + 1"/> + <MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :depth="depth + 1"/> </template> <div v-else class="more"> <MkA class="text _link" :to="notePage(note)">{{ $ts.continueThread }} <i class="fas fa-angle-double-right"></i></MkA> @@ -24,63 +24,36 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; +import * as misskey from 'misskey-js'; import { notePage } from '@/filters/note'; import XNoteHeader from './note-header.vue'; -import XSubNoteContent from './sub-note-content.vue'; +import MkNoteSubNoteContent from './sub-note-content.vue'; import XCwButton from './cw-button.vue'; import * as os from '@/os'; -export default defineComponent({ - name: 'XSub', +const props = withDefaults(defineProps<{ + note: misskey.entities.Note; + detail?: boolean; - components: { - XNoteHeader, - XSubNoteContent, - XCwButton, - }, - - props: { - note: { - type: Object, - required: true - }, - detail: { - type: Boolean, - required: false, - default: false - }, - // how many notes are in between this one and the note being viewed in detail - depth: { - type: Number, - required: false, - default: 1 - }, - }, - - data() { - return { - showContent: false, - replies: [], - }; - }, + // how many notes are in between this one and the note being viewed in detail + depth?: number; +}>(), { + depth: 1, +}); - created() { - if (this.detail) { - os.api('notes/children', { - noteId: this.note.id, - limit: 5 - }).then(replies => { - this.replies = replies; - }); - } - }, +let showContent = $ref(false); +let replies: misskey.entities.Note[] = $ref([]); - methods: { - notePage, - } -}); +if (props.detail) { + os.api('notes/children', { + noteId: props.note.id, + limit: 5 + }).then(res => { + replies = res; + }); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/analog-clock.vue b/packages/client/src/components/analog-clock.vue index 9ca511b6e9..59b8e97304 100644 --- a/packages/client/src/components/analog-clock.vue +++ b/packages/client/src/components/analog-clock.vue @@ -90,7 +90,7 @@ onMounted(() => { const update = () => { if (enabled.value) { tick(); - setTimeout(update, 1000); + window.setTimeout(update, 1000); } }; update(); diff --git a/packages/client/src/components/captcha.vue b/packages/client/src/components/captcha.vue index 2a4181255f..7fe499dc86 100644 --- a/packages/client/src/components/captcha.vue +++ b/packages/client/src/components/captcha.vue @@ -90,7 +90,7 @@ function requestRender() { 'error-callback': callback, }); } else { - setTimeout(requestRender, 1); + window.setTimeout(requestRender, 1); } } diff --git a/packages/client/src/components/form/input.vue b/packages/client/src/components/form/input.vue index 3533f4f27b..7165671af3 100644 --- a/packages/client/src/components/form/input.vue +++ b/packages/client/src/components/form/input.vue @@ -167,7 +167,7 @@ export default defineComponent({ // このコンポーネントが作成された時、非表示状態である場合がある // 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する - const clock = setInterval(() => { + const clock = window.setInterval(() => { if (prefixEl.value) { if (prefixEl.value.offsetWidth) { inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px'; @@ -181,7 +181,7 @@ export default defineComponent({ }, 100); onUnmounted(() => { - clearInterval(clock); + window.clearInterval(clock); }); }); }); diff --git a/packages/client/src/components/form/select.vue b/packages/client/src/components/form/select.vue index afc53ca9c8..87196027a8 100644 --- a/packages/client/src/components/form/select.vue +++ b/packages/client/src/components/form/select.vue @@ -117,7 +117,7 @@ export default defineComponent({ // このコンポーネントが作成された時、非表示状態である場合がある // 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する - const clock = setInterval(() => { + const clock = window.setInterval(() => { if (prefixEl.value) { if (prefixEl.value.offsetWidth) { inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px'; @@ -131,7 +131,7 @@ export default defineComponent({ }, 100); onUnmounted(() => { - clearInterval(clock); + window.clearInterval(clock); }); }); }); diff --git a/packages/client/src/components/global/a.vue b/packages/client/src/components/global/a.vue index 77ee7525a4..cf7385ca22 100644 --- a/packages/client/src/components/global/a.vue +++ b/packages/client/src/components/global/a.vue @@ -4,130 +4,114 @@ </a> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { inject } from 'vue'; import * as os from '@/os'; import copyToClipboard from '@/scripts/copy-to-clipboard'; import { router } from '@/router'; import { url } from '@/config'; -import { popout } from '@/scripts/popout'; -import { ColdDeviceStorage } from '@/store'; +import { popout as popout_ } from '@/scripts/popout'; +import { i18n } from '@/i18n'; +import { defaultStore } from '@/store'; -export default defineComponent({ - inject: { - navHook: { - default: null - }, - sideViewHook: { - default: null - } - }, +const props = withDefaults(defineProps<{ + to: string; + activeClass?: null | string; + behavior?: null | 'window' | 'browser' | 'modalWindow'; +}>(), { + activeClass: null, + behavior: null, +}); - props: { - to: { - type: String, - required: true, - }, - activeClass: { - type: String, - required: false, - }, - behavior: { - type: String, - required: false, - }, - }, +const navHook = inject('navHook', null); +const sideViewHook = inject('sideViewHook', null); - computed: { - active() { - if (this.activeClass == null) return false; - const resolved = router.resolve(this.to); - if (resolved.path == this.$route.path) return true; - if (resolved.name == null) return false; - if (this.$route.name == null) return false; - return resolved.name == this.$route.name; - } - }, +const active = $computed(() => { + if (props.activeClass == null) return false; + const resolved = router.resolve(props.to); + if (resolved.path === router.currentRoute.value.path) return true; + if (resolved.name == null) return false; + if (router.currentRoute.value.name == null) return false; + return resolved.name === router.currentRoute.value.name; +}); - methods: { - onContextmenu(e) { - if (window.getSelection().toString() !== '') return; - os.contextMenu([{ - type: 'label', - text: this.to, - }, { - icon: 'fas fa-window-maximize', - text: this.$ts.openInWindow, - action: () => { - os.pageWindow(this.to); - } - }, this.sideViewHook ? { - icon: 'fas fa-columns', - text: this.$ts.openInSideView, - action: () => { - this.sideViewHook(this.to); - } - } : undefined, { - icon: 'fas fa-expand-alt', - text: this.$ts.showInPage, - action: () => { - this.$router.push(this.to); - } - }, null, { - icon: 'fas fa-external-link-alt', - text: this.$ts.openInNewTab, - action: () => { - window.open(this.to, '_blank'); - } - }, { - icon: 'fas fa-link', - text: this.$ts.copyLink, - action: () => { - copyToClipboard(`${url}${this.to}`); - } - }], e); - }, +function onContextmenu(ev) { + const selection = window.getSelection(); + if (selection && selection.toString() !== '') return; + os.contextMenu([{ + type: 'label', + text: props.to, + }, { + icon: 'fas fa-window-maximize', + text: i18n.locale.openInWindow, + action: () => { + os.pageWindow(props.to); + } + }, sideViewHook ? { + icon: 'fas fa-columns', + text: i18n.locale.openInSideView, + action: () => { + sideViewHook(props.to); + } + } : undefined, { + icon: 'fas fa-expand-alt', + text: i18n.locale.showInPage, + action: () => { + router.push(props.to); + } + }, null, { + icon: 'fas fa-external-link-alt', + text: i18n.locale.openInNewTab, + action: () => { + window.open(props.to, '_blank'); + } + }, { + icon: 'fas fa-link', + text: i18n.locale.copyLink, + action: () => { + copyToClipboard(`${url}${props.to}`); + } + }], ev); +} - window() { - os.pageWindow(this.to); - }, +function openWindow() { + os.pageWindow(props.to); +} - modalWindow() { - os.modalPageWindow(this.to); - }, +function modalWindow() { + os.modalPageWindow(props.to); +} - popout() { - popout(this.to); - }, +function popout() { + popout_(props.to); +} - nav() { - if (this.behavior === 'browser') { - location.href = this.to; - return; - } +function nav() { + if (props.behavior === 'browser') { + location.href = props.to; + return; + } - if (this.behavior) { - if (this.behavior === 'window') { - return this.window(); - } else if (this.behavior === 'modalWindow') { - return this.modalWindow(); - } - } + if (props.behavior) { + if (props.behavior === 'window') { + return openWindow(); + } else if (props.behavior === 'modalWindow') { + return modalWindow(); + } + } - if (this.navHook) { - this.navHook(this.to); - } else { - if (this.$store.state.defaultSideView && this.sideViewHook && this.to !== '/') { - return this.sideViewHook(this.to); - } + if (navHook) { + navHook(props.to); + } else { + if (defaultStore.state.defaultSideView && sideViewHook && props.to !== '/') { + return sideViewHook(props.to); + } - if (this.$router.currentRoute.value.path === this.to) { - window.scroll({ top: 0, behavior: 'smooth' }); - } else { - this.$router.push(this.to); - } - } + if (router.currentRoute.value.path === props.to) { + window.scroll({ top: 0, behavior: 'smooth' }); + } else { + router.push(props.to); } } -}); +} </script> diff --git a/packages/client/src/components/global/ad.vue b/packages/client/src/components/global/ad.vue index 49046b00a7..180dabb2a2 100644 --- a/packages/client/src/components/global/ad.vue +++ b/packages/client/src/components/global/ad.vue @@ -20,7 +20,7 @@ <script lang="ts"> import { defineComponent, ref } from 'vue'; -import { Instance, instance } from '@/instance'; +import { instance } from '@/instance'; import { host } from '@/config'; import MkButton from '@/components/ui/button.vue'; import { defaultStore } from '@/store'; @@ -48,9 +48,9 @@ export default defineComponent({ showMenu.value = !showMenu.value; }; - const choseAd = (): Instance['ads'][number] | null => { + const choseAd = (): (typeof instance)['ads'][number] | null => { if (props.specify) { - return props.specify as Instance['ads'][number]; + return props.specify as (typeof instance)['ads'][number]; } const allAds = instance.ads.map(ad => defaultStore.state.mutedAds.includes(ad.id) ? { diff --git a/packages/client/src/components/global/avatar.vue b/packages/client/src/components/global/avatar.vue index 300e5e079f..9e8979fe56 100644 --- a/packages/client/src/components/global/avatar.vue +++ b/packages/client/src/components/global/avatar.vue @@ -1,74 +1,54 @@ <template> -<span v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat, square: $store.state.squareAvatars }" :title="acct(user)" @click="onClick"> +<span v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: $store.state.squareAvatars }" :style="{ color }" :title="acct(user)" @click="onClick"> <img class="inner" :src="url" decoding="async"/> <MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/> </span> -<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat, square: $store.state.squareAvatars }" :to="userPage(user)" :title="acct(user)" :target="target"> +<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: $store.state.squareAvatars }" :style="{ color }" :to="userPage(user)" :title="acct(user)" :target="target"> <img class="inner" :src="url" decoding="async"/> <MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/> </MkA> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { onMounted, watch } from 'vue'; +import * as misskey from 'misskey-js'; import { getStaticImageUrl } from '@/scripts/get-static-image-url'; import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash'; import { acct, userPage } from '@/filters/user'; import MkUserOnlineIndicator from '@/components/user-online-indicator.vue'; +import { defaultStore } from '@/store'; -export default defineComponent({ - components: { - MkUserOnlineIndicator - }, - props: { - user: { - type: Object, - required: true - }, - target: { - required: false, - default: null - }, - disableLink: { - required: false, - default: false - }, - disablePreview: { - required: false, - default: false - }, - showIndicator: { - required: false, - default: false - } - }, - emits: ['click'], - computed: { - cat(): boolean { - return this.user.isCat; - }, - url(): string { - return this.$store.state.disableShowingAnimatedImages - ? getStaticImageUrl(this.user.avatarUrl) - : this.user.avatarUrl; - }, - }, - watch: { - 'user.avatarBlurhash'() { - if (this.$el == null) return; - this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash); - } - }, - mounted() { - this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash); - }, - methods: { - onClick(e) { - this.$emit('click', e); - }, - acct, - userPage - } +const props = withDefaults(defineProps<{ + user: misskey.entities.User; + target?: string | null; + disableLink?: boolean; + disablePreview?: boolean; + showIndicator?: boolean; +}>(), { + target: null, + disableLink: false, + disablePreview: false, + showIndicator: false, +}); + +const emit = defineEmits<{ + (e: 'click', ev: MouseEvent): void; +}>(); + +const url = defaultStore.state.disableShowingAnimatedImages + ? getStaticImageUrl(props.user.avatarUrl) + : props.user.avatarUrl; + +function onClick(ev: MouseEvent) { + emit('click', ev); +} + +let color = $ref(); + +watch(() => props.user.avatarBlurhash, () => { + color = extractAvgColorFromBlurhash(props.user.avatarBlurhash); +}, { + immediate: true, }); </script> diff --git a/packages/client/src/components/global/loading.vue b/packages/client/src/components/global/loading.vue index 7bde53c12e..43ea1395ed 100644 --- a/packages/client/src/components/global/loading.vue +++ b/packages/client/src/components/global/loading.vue @@ -4,27 +4,17 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; -export default defineComponent({ - props: { - inline: { - type: Boolean, - required: false, - default: false - }, - colored: { - type: Boolean, - required: false, - default: true - }, - mini: { - type: Boolean, - required: false, - default: false - }, - } +const props = withDefaults(defineProps<{ + inline?: boolean; + colored?: boolean; + mini?: boolean; +}>(), { + inline: false, + colored: true, + mini: false, }); </script> diff --git a/packages/client/src/components/global/misskey-flavored-markdown.vue b/packages/client/src/components/global/misskey-flavored-markdown.vue index ab20404909..243d8614ba 100644 --- a/packages/client/src/components/global/misskey-flavored-markdown.vue +++ b/packages/client/src/components/global/misskey-flavored-markdown.vue @@ -1,15 +1,23 @@ <template> -<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }"/> +<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :customEmojis="customEmojis" :isNote="isNote" class="havbbuyv" :class="{ nowrap }"/> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MfmCore from '@/components/mfm'; -export default defineComponent({ - components: { - MfmCore - } +const props = withDefaults(defineProps<{ + text: string; + plain?: boolean; + nowrap?: boolean; + author?: any; + customEmojis?: any; + isNote?: boolean; +}>(), { + plain: false, + nowrap: false, + author: null, + isNote: true, }); </script> diff --git a/packages/client/src/components/global/sticky-container.vue b/packages/client/src/components/global/sticky-container.vue index 859b2c1d73..89d397f082 100644 --- a/packages/client/src/components/global/sticky-container.vue +++ b/packages/client/src/components/global/sticky-container.vue @@ -45,7 +45,7 @@ export default defineComponent({ calc(); const observer = new MutationObserver(() => { - setTimeout(() => { + window.setTimeout(() => { calc(); }, 100); }); diff --git a/packages/client/src/components/global/time.vue b/packages/client/src/components/global/time.vue index 6a330a2307..d2788264c5 100644 --- a/packages/client/src/components/global/time.vue +++ b/packages/client/src/components/global/time.vue @@ -1,73 +1,57 @@ <template> <time :title="absolute"> - <template v-if="mode == 'relative'">{{ relative }}</template> - <template v-else-if="mode == 'absolute'">{{ absolute }}</template> - <template v-else-if="mode == 'detail'">{{ absolute }} ({{ relative }})</template> + <template v-if="mode === 'relative'">{{ relative }}</template> + <template v-else-if="mode === 'absolute'">{{ absolute }}</template> + <template v-else-if="mode === 'detail'">{{ absolute }} ({{ relative }})</template> </time> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { onUnmounted } from 'vue'; +import { i18n } from '@/i18n'; -export default defineComponent({ - props: { - time: { - type: [Date, String], - required: true - }, - mode: { - type: String, - default: 'relative' - } - }, - data() { - return { - tickId: null, - now: new Date() - }; - }, - computed: { - _time(): Date { - return typeof this.time == 'string' ? new Date(this.time) : this.time; - }, - absolute(): string { - return this._time.toLocaleString(); - }, - relative(): string { - const time = this._time; - const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/; - return ( - ago >= 31536000 ? this.$t('_ago.yearsAgo', { n: (~~(ago / 31536000)).toString() }) : - ago >= 2592000 ? this.$t('_ago.monthsAgo', { n: (~~(ago / 2592000)).toString() }) : - ago >= 604800 ? this.$t('_ago.weeksAgo', { n: (~~(ago / 604800)).toString() }) : - ago >= 86400 ? this.$t('_ago.daysAgo', { n: (~~(ago / 86400)).toString() }) : - ago >= 3600 ? this.$t('_ago.hoursAgo', { n: (~~(ago / 3600)).toString() }) : - ago >= 60 ? this.$t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) : - ago >= 10 ? this.$t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) : - ago >= -1 ? this.$ts._ago.justNow : - ago < -1 ? this.$ts._ago.future : - this.$ts._ago.unknown); - } - }, - created() { - if (this.mode == 'relative' || this.mode == 'detail') { - this.tickId = window.requestAnimationFrame(this.tick); - } - }, - unmounted() { - if (this.mode === 'relative' || this.mode === 'detail') { - window.clearTimeout(this.tickId); - } - }, - methods: { - tick() { - // TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する - this.now = new Date(); +const props = withDefaults(defineProps<{ + time: Date | string; + mode?: 'relative' | 'absolute' | 'detail'; +}>(), { + mode: 'relative', +}); + +const _time = typeof props.time == 'string' ? new Date(props.time) : props.time; +const absolute = _time.toLocaleString(); - this.tickId = setTimeout(() => { - window.requestAnimationFrame(this.tick); - }, 10000); - } - } +let now = $ref(new Date()); +const relative = $computed(() => { + const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/; + return ( + ago >= 31536000 ? i18n.t('_ago.yearsAgo', { n: (~~(ago / 31536000)).toString() }) : + ago >= 2592000 ? i18n.t('_ago.monthsAgo', { n: (~~(ago / 2592000)).toString() }) : + ago >= 604800 ? i18n.t('_ago.weeksAgo', { n: (~~(ago / 604800)).toString() }) : + ago >= 86400 ? i18n.t('_ago.daysAgo', { n: (~~(ago / 86400)).toString() }) : + ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: (~~(ago / 3600)).toString() }) : + ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) : + ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) : + ago >= -1 ? i18n.locale._ago.justNow : + ago < -1 ? i18n.locale._ago.future : + i18n.locale._ago.unknown); }); + +function tick() { + // TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する + now = new Date(); + + tickId = window.setTimeout(() => { + window.requestAnimationFrame(tick); + }, 10000); +} + +let tickId: number; + +if (props.mode === 'relative' || props.mode === 'detail') { + tickId = window.requestAnimationFrame(tick); + + onUnmounted(() => { + window.clearTimeout(tickId); + }); +} </script> diff --git a/packages/client/src/components/global/user-name.vue b/packages/client/src/components/global/user-name.vue index bc93a8ea30..090de3df30 100644 --- a/packages/client/src/components/global/user-name.vue +++ b/packages/client/src/components/global/user-name.vue @@ -2,19 +2,14 @@ <Mfm :text="user.name || user.username" :plain="true" :nowrap="nowrap" :custom-emojis="user.emojis"/> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; +import * as misskey from 'misskey-js'; -export default defineComponent({ - props: { - user: { - type: Object, - required: true - }, - nowrap: { - type: Boolean, - default: true - }, - } +const props = withDefaults(defineProps<{ + user: misskey.entities.User; + nowrap?: boolean; +}>(), { + nowrap: true, }); </script> diff --git a/packages/client/src/components/image-viewer.vue b/packages/client/src/components/image-viewer.vue index 8584b91a61..c39076df16 100644 --- a/packages/client/src/components/image-viewer.vue +++ b/packages/client/src/components/image-viewer.vue @@ -1,8 +1,8 @@ <template> -<MkModal ref="modal" :z-priority="'middle'" @click="$refs.modal.close()" @closed="$emit('closed')"> +<MkModal ref="modal" :z-priority="'middle'" @click="modal.close()" @closed="emit('closed')"> <div class="xubzgfga"> <header>{{ image.name }}</header> - <img :src="image.url" :alt="image.comment" :title="image.comment" @click="$refs.modal.close()"/> + <img :src="image.url" :alt="image.comment" :title="image.comment" @click="modal.close()"/> <footer> <span>{{ image.type }}</span> <span>{{ bytes(image.size) }}</span> @@ -12,31 +12,23 @@ </MkModal> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; +import * as misskey from 'misskey-js'; import bytes from '@/filters/bytes'; import number from '@/filters/number'; import MkModal from '@/components/ui/modal.vue'; -export default defineComponent({ - components: { - MkModal, - }, - - props: { - image: { - type: Object, - required: true - }, - }, +const props = withDefaults(defineProps<{ + image: misskey.entities.DriveFile; +}>(), { +}); - emits: ['closed'], +const emit = defineEmits<{ + (e: 'closed'): void; +}>(); - methods: { - bytes, - number, - } -}); +const modal = $ref<InstanceType<typeof MkModal>>(); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/img-with-blurhash.vue b/packages/client/src/components/img-with-blurhash.vue index a000c699b6..06ad764403 100644 --- a/packages/client/src/components/img-with-blurhash.vue +++ b/packages/client/src/components/img-with-blurhash.vue @@ -5,67 +5,43 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { onMounted } from 'vue'; import { decode } from 'blurhash'; -export default defineComponent({ - props: { - src: { - type: String, - required: false, - default: null - }, - hash: { - type: String, - required: true - }, - alt: { - type: String, - required: false, - default: '', - }, - title: { - type: String, - required: false, - default: null, - }, - size: { - type: Number, - required: false, - default: 64 - }, - cover: { - type: Boolean, - required: false, - default: true, - } - }, +const props = withDefaults(defineProps<{ + src?: string | null; + hash: string; + alt?: string; + title?: string | null; + size?: number; + cover?: boolean; +}>(), { + src: null, + alt: '', + title: null, + size: 64, + cover: true, +}); - data() { - return { - loaded: false, - }; - }, +const canvas = $ref<HTMLCanvasElement>(); +let loaded = $ref(false); - mounted() { - this.draw(); - }, +function draw() { + if (props.hash == null) return; + const pixels = decode(props.hash, props.size, props.size); + const ctx = canvas.getContext('2d'); + const imageData = ctx!.createImageData(props.size, props.size); + imageData.data.set(pixels); + ctx!.putImageData(imageData, 0, 0); +} - methods: { - draw() { - if (this.hash == null) return; - const pixels = decode(this.hash, this.size, this.size); - const ctx = (this.$refs.canvas as HTMLCanvasElement).getContext('2d'); - const imageData = ctx!.createImageData(this.size, this.size); - imageData.data.set(pixels); - ctx!.putImageData(imageData, 0, 0); - }, +function onLoad() { + loaded = true; +} - onLoad() { - this.loaded = true; - } - } +onMounted(() => { + draw(); }); </script> diff --git a/packages/client/src/components/instance-ticker.vue b/packages/client/src/components/instance-ticker.vue index 1ce5a1c2c1..77fd8bb344 100644 --- a/packages/client/src/components/instance-ticker.vue +++ b/packages/client/src/components/instance-ticker.vue @@ -1,41 +1,22 @@ <template> <div class="hpaizdrt" :style="bg"> - <img v-if="info.faviconUrl" class="icon" :src="info.faviconUrl"/> - <span class="name">{{ info.name }}</span> + <img v-if="instance.faviconUrl" class="icon" :src="instance.faviconUrl"/> + <span class="name">{{ instance.name }}</span> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import { instanceName } from '@/config'; +<script lang="ts" setup> +import { } from 'vue'; -export default defineComponent({ - props: { - instance: { - type: Object, - required: false - }, - }, +const props = defineProps<{ + instance: any; // TODO +}>(); - data() { - return { - info: this.instance || { - faviconUrl: '/favicon.ico', - name: instanceName, - themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content - } - } - }, +const themeColor = props.instance.themeColor || '#777777'; - computed: { - bg(): any { - const themeColor = this.info.themeColor || '#777777'; - return { - background: `linear-gradient(90deg, ${themeColor}, ${themeColor + '00'})` - }; - } - } -}); +const bg = { + background: `linear-gradient(90deg, ${themeColor}, ${themeColor + '00'})` +}; </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/link.vue b/packages/client/src/components/link.vue index 8b8cde6510..317c931cec 100644 --- a/packages/client/src/components/link.vue +++ b/packages/client/src/components/link.vue @@ -1,82 +1,36 @@ <template> -<component :is="self ? 'MkA' : 'a'" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target" +<component :is="self ? 'MkA' : 'a'" ref="el" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target" :title="url" - @mouseover="onMouseover" - @mouseleave="onMouseleave" > <slot></slot> <i v-if="target === '_blank'" class="fas fa-external-link-square-alt icon"></i> </component> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import { url as local } from '@/config'; -import { isTouchUsing } from '@/scripts/touch'; +import { useTooltip } from '@/scripts/use-tooltip'; import * as os from '@/os'; -export default defineComponent({ - props: { - url: { - type: String, - required: true, - }, - rel: { - type: String, - required: false, - } - }, - data() { - const self = this.url.startsWith(local); - return { - local, - self: self, - attr: self ? 'to' : 'href', - target: self ? null : '_blank', - showTimer: null, - hideTimer: null, - checkTimer: null, - close: null, - }; - }, - methods: { - async showPreview() { - if (!document.body.contains(this.$el)) return; - if (this.close) return; +const props = withDefaults(defineProps<{ + url: string; + rel?: null | string; +}>(), { +}); - const { dispose } = await os.popup(import('@/components/url-preview-popup.vue'), { - url: this.url, - source: this.$el - }); +const self = props.url.startsWith(local); +const attr = self ? 'to' : 'href'; +const target = self ? null : '_blank'; - this.close = () => { - dispose(); - }; +const el = $ref(); - this.checkTimer = setInterval(() => { - if (!document.body.contains(this.$el)) this.closePreview(); - }, 1000); - }, - closePreview() { - if (this.close) { - clearInterval(this.checkTimer); - this.close(); - this.close = null; - } - }, - onMouseover() { - if (isTouchUsing) return; - clearTimeout(this.showTimer); - clearTimeout(this.hideTimer); - this.showTimer = setTimeout(this.showPreview, 500); - }, - onMouseleave() { - if (isTouchUsing) return; - clearTimeout(this.showTimer); - clearTimeout(this.hideTimer); - this.hideTimer = setTimeout(this.closePreview, 500); - } - } +useTooltip($$(el), (showing) => { + os.popup(import('@/components/url-preview-popup.vue'), { + showing, + url: props.url, + source: el, + }, {}, 'closed'); }); </script> diff --git a/packages/client/src/components/media-banner.vue b/packages/client/src/components/media-banner.vue index 9dbfe3d0c6..5093f11e97 100644 --- a/packages/client/src/components/media-banner.vue +++ b/packages/client/src/components/media-banner.vue @@ -6,7 +6,7 @@ <span>{{ $ts.clickToShow }}</span> </div> <div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" class="audio"> - <audio ref="audio" + <audio ref="audioEl" class="audio" :src="media.url" :title="media.name" @@ -25,34 +25,26 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@/os'; +<script lang="ts" setup> +import { onMounted } from 'vue'; +import * as misskey from 'misskey-js'; import { ColdDeviceStorage } from '@/store'; -export default defineComponent({ - props: { - media: { - type: Object, - required: true - } - }, - data() { - return { - hide: true, - }; - }, - mounted() { - const audioTag = this.$refs.audio as HTMLAudioElement; - if (audioTag) audioTag.volume = ColdDeviceStorage.get('mediaVolume'); - }, - methods: { - volumechange() { - const audioTag = this.$refs.audio as HTMLAudioElement; - ColdDeviceStorage.set('mediaVolume', audioTag.volume); - }, - }, -}) +const props = withDefaults(defineProps<{ + media: misskey.entities.DriveFile; +}>(), { +}); + +const audioEl = $ref<HTMLAudioElement | null>(); +let hide = $ref(true); + +function volumechange() { + if (audioEl) ColdDeviceStorage.set('mediaVolume', audioEl.volume); +} + +onMounted(() => { + if (audioEl) audioEl.volume = ColdDeviceStorage.get('mediaVolume'); +}); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/mini-chart.vue b/packages/client/src/components/mini-chart.vue index 2eb9ae8cbe..8c74eae876 100644 --- a/packages/client/src/components/mini-chart.vue +++ b/packages/client/src/components/mini-chart.vue @@ -63,10 +63,10 @@ export default defineComponent({ this.draw(); // Vueが何故かWatchを発動させない場合があるので - this.clock = setInterval(this.draw, 1000); + this.clock = window.setInterval(this.draw, 1000); }, beforeUnmount() { - clearInterval(this.clock); + window.clearInterval(this.clock); }, methods: { draw() { diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue index a5cb2f0426..07e9920f65 100644 --- a/packages/client/src/components/note-detailed.vue +++ b/packages/client/src/components/note-detailed.vue @@ -8,8 +8,8 @@ :tabindex="!isDeleted ? '-1' : null" :class="{ renote: isRenote }" > - <XSub v-for="note in conversation" :key="note.id" class="reply-to-more" :note="note"/> - <XSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/> + <MkNoteSub v-for="note in conversation" :key="note.id" class="reply-to-more" :note="note"/> + <MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/> <div v-if="isRenote" class="renote"> <MkAvatar class="avatar" :user="note.user"/> <i class="fas fa-retweet"></i> @@ -107,7 +107,7 @@ </footer> </div> </article> - <XSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/> + <MkNoteSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/> </div> <div v-else class="_panel muted" @click="muted = false"> <I18n :src="$ts.userSaysSomething" tag="small"> @@ -120,765 +120,171 @@ </div> </template> -<script lang="ts"> -import { defineAsyncComponent, defineComponent, markRaw } from 'vue'; +<script lang="ts" setup> +import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue'; import * as mfm from 'mfm-js'; -import { sum } from '@/scripts/array'; -import XSub from './note.sub.vue'; -import XNoteHeader from './note-header.vue'; +import * as misskey from 'misskey-js'; +import MkNoteSub from './MkNoteSub.vue'; import XNoteSimple from './note-simple.vue'; import XReactionsViewer from './reactions-viewer.vue'; import XMediaList from './media-list.vue'; import XCwButton from './cw-button.vue'; import XPoll from './poll.vue'; import XRenoteButton from './renote-button.vue'; +import MkUrlPreview from '@/components/url-preview.vue'; +import MkInstanceTicker from '@/components/instance-ticker.vue'; import { pleaseLogin } from '@/scripts/please-login'; -import { focusPrev, focusNext } from '@/scripts/focus'; -import { url } from '@/config'; -import copyToClipboard from '@/scripts/copy-to-clipboard'; import { checkWordMute } from '@/scripts/check-word-mute'; import { userPage } from '@/filters/user'; import { notePage } from '@/filters/note'; import * as os from '@/os'; -import { stream } from '@/stream'; -import { noteActions, noteViewInterruptors } from '@/store'; +import { defaultStore, noteViewInterruptors } from '@/store'; import { reactionPicker } from '@/scripts/reaction-picker'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; +import { getNoteMenu } from '@/scripts/get-note-menu'; +import { useNoteCapture } from '@/scripts/use-note-capture'; -// TODO: note.vueとほぼ同じなので共通化したい -export default defineComponent({ - components: { - XSub, - XNoteHeader, - XNoteSimple, - XReactionsViewer, - XMediaList, - XCwButton, - XPoll, - XRenoteButton, - MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')), - MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')), - }, +const props = defineProps<{ + note: misskey.entities.Note; + pinned?: boolean; +}>(); - inject: { - inChannel: { - default: null - }, - }, +const inChannel = inject('inChannel', null); - props: { - note: { - type: Object, - required: true - }, - }, +const isRenote = ( + props.note.renote != null && + props.note.text == null && + props.note.fileIds.length === 0 && + props.note.poll == null +); - emits: ['update:note'], +const el = ref<HTMLElement>(); +const menuButton = ref<HTMLElement>(); +const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); +const renoteTime = ref<HTMLElement>(); +const reactButton = ref<HTMLElement>(); +let appearNote = $ref(isRenote ? props.note.renote as misskey.entities.Note : props.note); +const isMyRenote = $i && ($i.id === props.note.userId); +const showContent = ref(false); +const isDeleted = ref(false); +const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords)); +const translation = ref(null); +const translating = ref(false); +const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null; +const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); +const conversation = ref<misskey.entities.Note[]>([]); +const replies = ref<misskey.entities.Note[]>([]); - data() { - return { - connection: null, - conversation: [], - replies: [], - showContent: false, - isDeleted: false, - muted: false, - translation: null, - translating: false, - notePage, - }; - }, +const keymap = { + 'r': () => reply(true), + 'e|a|plus': () => react(true), + 'q': () => renoteButton.value.renote(true), + 'esc': blur, + 'm|o': () => menu(true), + 's': () => showContent.value != showContent.value, +}; - computed: { - rs() { - return this.$store.state.reactions; - }, - keymap(): any { - return { - 'r': () => this.reply(true), - 'e|a|plus': () => this.react(true), - 'q': () => this.$refs.renoteButton.renote(true), - 'f|b': this.favorite, - 'delete|ctrl+d': this.del, - 'ctrl+q': this.renoteDirectly, - 'up|k|shift+tab': this.focusBefore, - 'down|j|tab': this.focusAfter, - 'esc': this.blur, - 'm|o': () => this.menu(true), - 's': this.toggleShowContent, - '1': () => this.reactDirectly(this.rs[0]), - '2': () => this.reactDirectly(this.rs[1]), - '3': () => this.reactDirectly(this.rs[2]), - '4': () => this.reactDirectly(this.rs[3]), - '5': () => this.reactDirectly(this.rs[4]), - '6': () => this.reactDirectly(this.rs[5]), - '7': () => this.reactDirectly(this.rs[6]), - '8': () => this.reactDirectly(this.rs[7]), - '9': () => this.reactDirectly(this.rs[8]), - '0': () => this.reactDirectly(this.rs[9]), - }; - }, - - isRenote(): boolean { - return (this.note.renote && - this.note.text == null && - this.note.fileIds.length == 0 && - this.note.poll == null); - }, - - appearNote(): any { - return this.isRenote ? this.note.renote : this.note; - }, - - isMyNote(): boolean { - return this.$i && (this.$i.id === this.appearNote.userId); - }, - - isMyRenote(): boolean { - return this.$i && (this.$i.id === this.note.userId); - }, - - reactionsCount(): number { - return this.appearNote.reactions - ? sum(Object.values(this.appearNote.reactions)) - : 0; - }, - - urls(): string[] { - if (this.appearNote.text) { - return extractUrlFromMfm(mfm.parse(this.appearNote.text)); - } else { - return null; - } - }, - - showTicker() { - if (this.$store.state.instanceTicker === 'always') return true; - if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true; - return false; - } - }, - - async created() { - if (this.$i) { - this.connection = stream; - } - - this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords); +useNoteCapture({ + appearNote: $$(appearNote), + rootEl: el, +}); - // plugin - if (noteViewInterruptors.length > 0) { - let result = this.note; - for (const interruptor of noteViewInterruptors) { - result = await interruptor.handler(JSON.parse(JSON.stringify(result))); - } - this.$emit('update:note', Object.freeze(result)); - } +function reply(viaKeyboard = false): void { + pleaseLogin(); + os.post({ + reply: appearNote, + animation: !viaKeyboard, + }, () => { + focus(); + }); +} - os.api('notes/children', { - noteId: this.appearNote.id, - limit: 30 - }).then(replies => { - this.replies = replies; +function react(viaKeyboard = false): void { + pleaseLogin(); + blur(); + reactionPicker.show(reactButton.value, reaction => { + os.api('notes/reactions/create', { + noteId: appearNote.id, + reaction: reaction }); + }, () => { + focus(); + }); +} - if (this.appearNote.replyId) { - os.api('notes/conversation', { - noteId: this.appearNote.replyId - }).then(conversation => { - this.conversation = conversation.reverse(); - }); - } - }, - - mounted() { - this.capture(true); - - if (this.$i) { - this.connection.on('_connected_', this.onStreamConnected); - } - }, - - beforeUnmount() { - this.decapture(true); +function undoReact(note): void { + const oldReaction = note.myReaction; + if (!oldReaction) return; + os.api('notes/reactions/delete', { + noteId: note.id + }); +} - if (this.$i) { - this.connection.off('_connected_', this.onStreamConnected); +function onContextmenu(e): void { + const isLink = (el: HTMLElement) => { + if (el.tagName === 'A') return true; + if (el.parentElement) { + return isLink(el.parentElement); } - }, - - methods: { - updateAppearNote(v) { - this.$emit('update:note', Object.freeze(this.isRenote ? { - ...this.note, - renote: { - ...this.note.renote, - ...v - } - } : { - ...this.note, - ...v - })); - }, - - readPromo() { - os.api('promo/read', { - noteId: this.appearNote.id - }); - this.isDeleted = true; - }, - - capture(withHandler = false) { - if (this.$i) { - // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する - this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id }); - if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); - } - }, - - decapture(withHandler = false) { - if (this.$i) { - this.connection.send('un', { - id: this.appearNote.id - }); - if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated); - } - }, - - onStreamConnected() { - this.capture(); - }, - - onStreamNoteUpdated(data) { - const { type, id, body } = data; - - if (id !== this.appearNote.id) return; + }; + if (isLink(e.target)) return; + if (window.getSelection().toString() !== '') return; - switch (type) { - case 'reacted': { - const reaction = body.reaction; - - // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので) - let n = { - ...this.appearNote, - }; - - if (body.emoji) { - const emojis = this.appearNote.emojis || []; - if (!emojis.includes(body.emoji)) { - n.emojis = [...emojis, body.emoji]; - } - } - - // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる - const currentCount = (this.appearNote.reactions || {})[reaction] || 0; - - // Increment the count - n.reactions = { - ...this.appearNote.reactions, - [reaction]: currentCount + 1 - }; - - if (body.userId === this.$i.id) { - n.myReaction = reaction; - } - - this.updateAppearNote(n); - break; - } - - case 'unreacted': { - const reaction = body.reaction; - - // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので) - let n = { - ...this.appearNote, - }; - - // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる - const currentCount = (this.appearNote.reactions || {})[reaction] || 0; - - // Decrement the count - n.reactions = { - ...this.appearNote.reactions, - [reaction]: Math.max(0, currentCount - 1) - }; - - if (body.userId === this.$i.id) { - n.myReaction = null; - } - - this.updateAppearNote(n); - break; - } - - case 'pollVoted': { - const choice = body.choice; - - // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので) - let n = { - ...this.appearNote, - }; - - const choices = [...this.appearNote.poll.choices]; - choices[choice] = { - ...choices[choice], - votes: choices[choice].votes + 1, - ...(body.userId === this.$i.id ? { - isVoted: true - } : {}) - }; - - n.poll = { - ...this.appearNote.poll, - choices: choices - }; - - this.updateAppearNote(n); - break; - } - - case 'deleted': { - this.isDeleted = true; - break; - } - } - }, - - reply(viaKeyboard = false) { - pleaseLogin(); - os.post({ - reply: this.appearNote, - animation: !viaKeyboard, - }, () => { - this.focus(); - }); - }, - - renoteDirectly() { - os.apiWithDialog('notes/create', { - renoteId: this.appearNote.id - }, undefined, (res: any) => { - os.alert({ - type: 'success', - text: this.$ts.renoted, - }); - }, (e: Error) => { - if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') { - os.alert({ - type: 'error', - text: this.$ts.cantRenote, - }); - } else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') { - os.alert({ - type: 'error', - text: this.$ts.cantReRenote, - }); - } - }); - }, - - react(viaKeyboard = false) { - pleaseLogin(); - this.blur(); - reactionPicker.show(this.$refs.reactButton, reaction => { - os.api('notes/reactions/create', { - noteId: this.appearNote.id, - reaction: reaction - }); - }, () => { - this.focus(); - }); - }, - - reactDirectly(reaction) { - os.api('notes/reactions/create', { - noteId: this.appearNote.id, - reaction: reaction - }); - }, - - undoReact(note) { - const oldReaction = note.myReaction; - if (!oldReaction) return; - os.api('notes/reactions/delete', { - noteId: note.id - }); - }, - - favorite() { - pleaseLogin(); - os.apiWithDialog('notes/favorites/create', { - noteId: this.appearNote.id - }, undefined, (res: any) => { - os.alert({ - type: 'success', - text: this.$ts.favorited, - }); - }, (e: Error) => { - if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') { - os.alert({ - type: 'error', - text: this.$ts.alreadyFavorited, - }); - } else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') { - os.alert({ - type: 'error', - text: this.$ts.cantFavorite, - }); - } - }); - }, - - del() { - os.confirm({ - type: 'warning', - text: this.$ts.noteDeleteConfirm, - }).then(({ canceled }) => { - if (canceled) return; - - os.api('notes/delete', { - noteId: this.appearNote.id - }); - }); - }, - - delEdit() { - os.confirm({ - type: 'warning', - text: this.$ts.deleteAndEditConfirm, - }).then(({ canceled }) => { - if (canceled) return; - - os.api('notes/delete', { - noteId: this.appearNote.id - }); - - os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel }); - }); - }, - - toggleFavorite(favorite: boolean) { - os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { - noteId: this.appearNote.id - }); - }, - - toggleWatch(watch: boolean) { - os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', { - noteId: this.appearNote.id - }); - }, - - toggleThreadMute(mute: boolean) { - os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', { - noteId: this.appearNote.id - }); - }, - - getMenu() { - let menu; - if (this.$i) { - const statePromise = os.api('notes/state', { - noteId: this.appearNote.id - }); - - menu = [{ - icon: 'fas fa-copy', - text: this.$ts.copyContent, - action: this.copyContent - }, { - icon: 'fas fa-link', - text: this.$ts.copyLink, - action: this.copyLink - }, (this.appearNote.url || this.appearNote.uri) ? { - icon: 'fas fa-external-link-square-alt', - text: this.$ts.showOnRemote, - action: () => { - window.open(this.appearNote.url || this.appearNote.uri, '_blank'); - } - } : undefined, - { - icon: 'fas fa-share-alt', - text: this.$ts.share, - action: this.share - }, - this.$instance.translatorAvailable ? { - icon: 'fas fa-language', - text: this.$ts.translate, - action: this.translate - } : undefined, - null, - statePromise.then(state => state.isFavorited ? { - icon: 'fas fa-star', - text: this.$ts.unfavorite, - action: () => this.toggleFavorite(false) - } : { - icon: 'fas fa-star', - text: this.$ts.favorite, - action: () => this.toggleFavorite(true) - }), - { - icon: 'fas fa-paperclip', - text: this.$ts.clip, - action: () => this.clip() - }, - (this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? { - icon: 'fas fa-eye-slash', - text: this.$ts.unwatch, - action: () => this.toggleWatch(false) - } : { - icon: 'fas fa-eye', - text: this.$ts.watch, - action: () => this.toggleWatch(true) - }) : undefined, - statePromise.then(state => state.isMutedThread ? { - icon: 'fas fa-comment-slash', - text: this.$ts.unmuteThread, - action: () => this.toggleThreadMute(false) - } : { - icon: 'fas fa-comment-slash', - text: this.$ts.muteThread, - action: () => this.toggleThreadMute(true) - }), - this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? { - icon: 'fas fa-thumbtack', - text: this.$ts.unpin, - action: () => this.togglePin(false) - } : { - icon: 'fas fa-thumbtack', - text: this.$ts.pin, - action: () => this.togglePin(true) - } : undefined, - /*...(this.$i.isModerator || this.$i.isAdmin ? [ - null, - { - icon: 'fas fa-bullhorn', - text: this.$ts.promote, - action: this.promote - }] - : [] - ),*/ - ...(this.appearNote.userId != this.$i.id ? [ - null, - { - icon: 'fas fa-exclamation-circle', - text: this.$ts.reportAbuse, - action: () => { - const u = `${url}/notes/${this.appearNote.id}`; - os.popup(import('@/components/abuse-report-window.vue'), { - user: this.appearNote.user, - initialComment: `Note: ${u}\n-----\n` - }, {}, 'closed'); - } - }] - : [] - ), - ...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [ - null, - this.appearNote.userId == this.$i.id ? { - icon: 'fas fa-edit', - text: this.$ts.deleteAndEdit, - action: this.delEdit - } : undefined, - { - icon: 'fas fa-trash-alt', - text: this.$ts.delete, - danger: true, - action: this.del - }] - : [] - )] - .filter(x => x !== undefined); - } else { - menu = [{ - icon: 'fas fa-copy', - text: this.$ts.copyContent, - action: this.copyContent - }, { - icon: 'fas fa-link', - text: this.$ts.copyLink, - action: this.copyLink - }, (this.appearNote.url || this.appearNote.uri) ? { - icon: 'fas fa-external-link-square-alt', - text: this.$ts.showOnRemote, - action: () => { - window.open(this.appearNote.url || this.appearNote.uri, '_blank'); - } - } : undefined] - .filter(x => x !== undefined); - } - - if (noteActions.length > 0) { - menu = menu.concat([null, ...noteActions.map(action => ({ - icon: 'fas fa-plug', - text: action.title, - action: () => { - action.handler(this.appearNote); - } - }))]); - } - - return menu; - }, - - onContextmenu(e) { - const isLink = (el: HTMLElement) => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - }; - if (isLink(e.target)) return; - if (window.getSelection().toString() !== '') return; - - if (this.$store.state.useReactionPickerForContextMenu) { - e.preventDefault(); - this.react(); - } else { - os.contextMenu(this.getMenu(), e).then(this.focus); - } - }, - - menu(viaKeyboard = false) { - os.popupMenu(this.getMenu(), this.$refs.menuButton, { - viaKeyboard - }).then(this.focus); - }, - - showRenoteMenu(viaKeyboard = false) { - if (!this.isMyRenote) return; - os.popupMenu([{ - text: this.$ts.unrenote, - icon: 'fas fa-trash-alt', - danger: true, - action: () => { - os.api('notes/delete', { - noteId: this.note.id - }); - this.isDeleted = true; - } - }], this.$refs.renoteTime, { - viaKeyboard: viaKeyboard - }); - }, - - toggleShowContent() { - this.showContent = !this.showContent; - }, - - copyContent() { - copyToClipboard(this.appearNote.text); - os.success(); - }, - - copyLink() { - copyToClipboard(`${url}/notes/${this.appearNote.id}`); - os.success(); - }, - - togglePin(pin: boolean) { - os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', { - noteId: this.appearNote.id - }, undefined, null, e => { - if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { - os.alert({ - type: 'error', - text: this.$ts.pinLimitExceeded - }); - } - }); - }, - - async clip() { - const clips = await os.api('clips/list'); - os.popupMenu([{ - icon: 'fas fa-plus', - text: this.$ts.createNew, - action: async () => { - const { canceled, result } = await os.form(this.$ts.createNewClip, { - name: { - type: 'string', - label: this.$ts.name - }, - description: { - type: 'string', - required: false, - multiline: true, - label: this.$ts.description - }, - isPublic: { - type: 'boolean', - label: this.$ts.public, - default: false - } - }); - if (canceled) return; - - const clip = await os.apiWithDialog('clips/create', result); - - os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id }); - } - }, null, ...clips.map(clip => ({ - text: clip.name, - action: () => { - os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id }); - } - }))], this.$refs.menuButton, { - }).then(this.focus); - }, - - async promote() { - const { canceled, result: days } = await os.inputNumber({ - title: this.$ts.numberOfDays, - }); - - if (canceled) return; - - os.apiWithDialog('admin/promo/create', { - noteId: this.appearNote.id, - expiresAt: Date.now() + (86400000 * days) - }); - }, + if (defaultStore.state.useReactionPickerForContextMenu) { + e.preventDefault(); + react(); + } else { + os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), e).then(focus); + } +} - share() { - navigator.share({ - title: this.$t('noteOf', { user: this.appearNote.user.name }), - text: this.appearNote.text, - url: `${url}/notes/${this.appearNote.id}` - }); - }, +function menu(viaKeyboard = false): void { + os.popupMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), menuButton.value, { + viaKeyboard + }).then(focus); +} - async translate() { - if (this.translation != null) return; - this.translating = true; - const res = await os.api('notes/translate', { - noteId: this.appearNote.id, - targetLang: localStorage.getItem('lang') || navigator.language, +function showRenoteMenu(viaKeyboard = false): void { + if (!isMyRenote) return; + os.popupMenu([{ + text: i18n.locale.unrenote, + icon: 'fas fa-trash-alt', + danger: true, + action: () => { + os.api('notes/delete', { + noteId: props.note.id }); - this.translating = false; - this.translation = res; - }, - - focus() { - this.$el.focus(); - }, - - blur() { - this.$el.blur(); - }, + isDeleted.value = true; + } + }], renoteTime.value, { + viaKeyboard: viaKeyboard + }); +} - focusBefore() { - focusPrev(this.$el); - }, +function focus() { + el.value.focus(); +} - focusAfter() { - focusNext(this.$el); - }, +function blur() { + el.value.blur(); +} - userPage - } +os.api('notes/children', { + noteId: appearNote.id, + limit: 30 +}).then(res => { + replies.value = res; }); + +if (appearNote.replyId) { + os.api('notes/conversation', { + noteId: appearNote.replyId + }).then(res => { + conversation.value = res.reverse(); + }); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/note-header.vue b/packages/client/src/components/note-header.vue index 26e725c6b8..56a3a37e75 100644 --- a/packages/client/src/components/note-header.vue +++ b/packages/client/src/components/note-header.vue @@ -19,30 +19,16 @@ </header> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; +import * as misskey from 'misskey-js'; import { notePage } from '@/filters/note'; import { userPage } from '@/filters/user'; -import * as os from '@/os'; -export default defineComponent({ - props: { - note: { - type: Object, - required: true - }, - }, - - data() { - return { - }; - }, - - methods: { - notePage, - userPage - } -}); +defineProps<{ + note: misskey.entities.Note; + pinned?: boolean; +}>(); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/note-preview.vue b/packages/client/src/components/note-preview.vue index bdcb8d5eed..a78b499654 100644 --- a/packages/client/src/components/note-preview.vue +++ b/packages/client/src/components/note-preview.vue @@ -14,20 +14,12 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; -export default defineComponent({ - components: { - }, - - props: { - text: { - type: String, - required: true - } - }, -}); +const props = defineProps<{ + text: string; +}>(); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/note-simple.vue b/packages/client/src/components/note-simple.vue index 135f06602d..c6907787b5 100644 --- a/packages/client/src/components/note-simple.vue +++ b/packages/client/src/components/note-simple.vue @@ -9,40 +9,26 @@ <XCwButton v-model="showContent" :note="note"/> </p> <div v-show="note.cw == null || showContent" class="content"> - <XSubNote-content class="text" :note="note"/> + <MkNoteSubNoteContent class="text" :note="note"/> </div> </div> </div> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; +import * as misskey from 'misskey-js'; import XNoteHeader from './note-header.vue'; -import XSubNoteContent from './sub-note-content.vue'; +import MkNoteSubNoteContent from './sub-note-content.vue'; import XCwButton from './cw-button.vue'; -import * as os from '@/os'; -export default defineComponent({ - components: { - XNoteHeader, - XSubNoteContent, - XCwButton, - }, +const props = defineProps<{ + note: misskey.entities.Note; + pinned?: boolean; +}>(); - props: { - note: { - type: Object, - required: true - } - }, - - data() { - return { - showContent: false - }; - } -}); +const showContent = $ref(false); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue index 3cf924928a..b309afe051 100644 --- a/packages/client/src/components/note.vue +++ b/packages/client/src/components/note.vue @@ -2,20 +2,21 @@ <div v-if="!muted" v-show="!isDeleted" + ref="el" v-hotkey="keymap" v-size="{ max: [500, 450, 350, 300] }" class="tkcbzcuz" :tabindex="!isDeleted ? '-1' : null" :class="{ renote: isRenote }" > - <XSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/> - <div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedNote }}</div> - <div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ $ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ $ts.hideThisNote }} <i class="fas fa-times"></i></button></div> - <div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ $ts.featured }}</div> + <MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/> + <div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.locale.pinnedNote }}</div> + <div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.locale.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.locale.hideThisNote }} <i class="fas fa-times"></i></button></div> + <div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.locale.featured }}</div> <div v-if="isRenote" class="renote"> <MkAvatar class="avatar" :user="note.user"/> <i class="fas fa-retweet"></i> - <I18n :src="$ts.renotedBy" tag="span"> + <I18n :src="i18n.locale.renotedBy" tag="span"> <template #user> <MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)"> <MkUserName :user="note.user"/> @@ -47,7 +48,7 @@ </p> <div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }"> <div class="text"> - <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $ts.private }})</span> + <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.locale.private }})</span> <MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA> <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> <a v-if="appearNote.renote != null" class="rp">RN:</a> @@ -66,7 +67,7 @@ <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/> <div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div> <button v-if="collapsed" class="fade _button" @click="collapsed = false"> - <span>{{ $ts.showMore }}</span> + <span>{{ i18n.locale.showMore }}</span> </button> </div> <MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA> @@ -93,7 +94,7 @@ </article> </div> <div v-else class="muted" @click="muted = false"> - <I18n :src="$ts.userSaysSomething" tag="small"> + <I18n :src="i18n.locale.userSaysSomething" tag="small"> <template #name> <MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)"> <MkUserName :user="appearNote.user"/> @@ -103,11 +104,11 @@ </div> </template> -<script lang="ts"> -import { defineAsyncComponent, defineComponent, markRaw } from 'vue'; +<script lang="ts" setup> +import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue'; import * as mfm from 'mfm-js'; -import { sum } from '@/scripts/array'; -import XSub from './note.sub.vue'; +import * as misskey from 'misskey-js'; +import MkNoteSub from './MkNoteSub.vue'; import XNoteHeader from './note-header.vue'; import XNoteSimple from './note-simple.vue'; import XReactionsViewer from './reactions-viewer.vue'; @@ -115,745 +116,164 @@ import XMediaList from './media-list.vue'; import XCwButton from './cw-button.vue'; import XPoll from './poll.vue'; import XRenoteButton from './renote-button.vue'; +import MkUrlPreview from '@/components/url-preview.vue'; +import MkInstanceTicker from '@/components/instance-ticker.vue'; import { pleaseLogin } from '@/scripts/please-login'; import { focusPrev, focusNext } from '@/scripts/focus'; -import { url } from '@/config'; -import copyToClipboard from '@/scripts/copy-to-clipboard'; import { checkWordMute } from '@/scripts/check-word-mute'; import { userPage } from '@/filters/user'; import * as os from '@/os'; -import { stream } from '@/stream'; -import { noteActions, noteViewInterruptors } from '@/store'; +import { defaultStore, noteViewInterruptors } from '@/store'; import { reactionPicker } from '@/scripts/reaction-picker'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; +import { getNoteMenu } from '@/scripts/get-note-menu'; +import { useNoteCapture } from '@/scripts/use-note-capture'; -export default defineComponent({ - components: { - XSub, - XNoteHeader, - XNoteSimple, - XReactionsViewer, - XMediaList, - XCwButton, - XPoll, - XRenoteButton, - MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')), - MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')), - }, +const props = defineProps<{ + note: misskey.entities.Note; + pinned?: boolean; +}>(); - inject: { - inChannel: { - default: null - }, - }, +const inChannel = inject('inChannel', null); - props: { - note: { - type: Object, - required: true - }, - pinned: { - type: Boolean, - required: false, - default: false - }, - }, +const isRenote = ( + props.note.renote != null && + props.note.text == null && + props.note.fileIds.length === 0 && + props.note.poll == null +); - emits: ['update:note'], +const el = ref<HTMLElement>(); +const menuButton = ref<HTMLElement>(); +const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); +const renoteTime = ref<HTMLElement>(); +const reactButton = ref<HTMLElement>(); +let appearNote = $ref(isRenote ? props.note.renote as misskey.entities.Note : props.note); +const isMyRenote = $i && ($i.id === props.note.userId); +const showContent = ref(false); +const collapsed = ref(appearNote.cw == null && appearNote.text != null && ( + (appearNote.text.split('\n').length > 9) || + (appearNote.text.length > 500) +)); +const isDeleted = ref(false); +const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords)); +const translation = ref(null); +const translating = ref(false); +const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null; +const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); - data() { - return { - connection: null, - replies: [], - showContent: false, - collapsed: false, - isDeleted: false, - muted: false, - translation: null, - translating: false, - }; - }, +const keymap = { + 'r': () => reply(true), + 'e|a|plus': () => react(true), + 'q': () => renoteButton.value.renote(true), + 'up|k|shift+tab': focusBefore, + 'down|j|tab': focusAfter, + 'esc': blur, + 'm|o': () => menu(true), + 's': () => showContent.value != showContent.value, +}; - computed: { - rs() { - return this.$store.state.reactions; - }, - keymap(): any { - return { - 'r': () => this.reply(true), - 'e|a|plus': () => this.react(true), - 'q': () => this.$refs.renoteButton.renote(true), - 'f|b': this.favorite, - 'delete|ctrl+d': this.del, - 'ctrl+q': this.renoteDirectly, - 'up|k|shift+tab': this.focusBefore, - 'down|j|tab': this.focusAfter, - 'esc': this.blur, - 'm|o': () => this.menu(true), - 's': this.toggleShowContent, - '1': () => this.reactDirectly(this.rs[0]), - '2': () => this.reactDirectly(this.rs[1]), - '3': () => this.reactDirectly(this.rs[2]), - '4': () => this.reactDirectly(this.rs[3]), - '5': () => this.reactDirectly(this.rs[4]), - '6': () => this.reactDirectly(this.rs[5]), - '7': () => this.reactDirectly(this.rs[6]), - '8': () => this.reactDirectly(this.rs[7]), - '9': () => this.reactDirectly(this.rs[8]), - '0': () => this.reactDirectly(this.rs[9]), - }; - }, - - isRenote(): boolean { - return (this.note.renote && - this.note.text == null && - this.note.fileIds.length == 0 && - this.note.poll == null); - }, - - appearNote(): any { - return this.isRenote ? this.note.renote : this.note; - }, - - isMyNote(): boolean { - return this.$i && (this.$i.id === this.appearNote.userId); - }, - - isMyRenote(): boolean { - return this.$i && (this.$i.id === this.note.userId); - }, - - reactionsCount(): number { - return this.appearNote.reactions - ? sum(Object.values(this.appearNote.reactions)) - : 0; - }, - - urls(): string[] { - if (this.appearNote.text) { - return extractUrlFromMfm(mfm.parse(this.appearNote.text)); - } else { - return null; - } - }, - - showTicker() { - if (this.$store.state.instanceTicker === 'always') return true; - if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true; - return false; - } - }, - - async created() { - if (this.$i) { - this.connection = stream; - } - - this.collapsed = this.appearNote.cw == null && this.appearNote.text && ( - (this.appearNote.text.split('\n').length > 9) || - (this.appearNote.text.length > 500) - ); - this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords); - - // plugin - if (noteViewInterruptors.length > 0) { - let result = this.note; - for (const interruptor of noteViewInterruptors) { - result = await interruptor.handler(JSON.parse(JSON.stringify(result))); - } - this.$emit('update:note', Object.freeze(result)); - } - }, +useNoteCapture({ + appearNote: $$(appearNote), + rootEl: el, +}); - mounted() { - this.capture(true); +function reply(viaKeyboard = false): void { + pleaseLogin(); + os.post({ + reply: appearNote, + animation: !viaKeyboard, + }, () => { + focus(); + }); +} - if (this.$i) { - this.connection.on('_connected_', this.onStreamConnected); - } - }, +function react(viaKeyboard = false): void { + pleaseLogin(); + blur(); + reactionPicker.show(reactButton.value, reaction => { + os.api('notes/reactions/create', { + noteId: appearNote.id, + reaction: reaction + }); + }, () => { + focus(); + }); +} - beforeUnmount() { - this.decapture(true); +function undoReact(note): void { + const oldReaction = note.myReaction; + if (!oldReaction) return; + os.api('notes/reactions/delete', { + noteId: note.id + }); +} - if (this.$i) { - this.connection.off('_connected_', this.onStreamConnected); +function onContextmenu(e): void { + const isLink = (el: HTMLElement) => { + if (el.tagName === 'A') return true; + if (el.parentElement) { + return isLink(el.parentElement); } - }, - - methods: { - updateAppearNote(v) { - this.$emit('update:note', Object.freeze(this.isRenote ? { - ...this.note, - renote: { - ...this.note.renote, - ...v - } - } : { - ...this.note, - ...v - })); - }, - - readPromo() { - os.api('promo/read', { - noteId: this.appearNote.id - }); - this.isDeleted = true; - }, - - capture(withHandler = false) { - if (this.$i) { - // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する - this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id }); - if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); - } - }, + }; + if (isLink(e.target)) return; + if (window.getSelection().toString() !== '') return; - decapture(withHandler = false) { - if (this.$i) { - this.connection.send('un', { - id: this.appearNote.id - }); - if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated); - } - }, - - onStreamConnected() { - this.capture(); - }, - - onStreamNoteUpdated(data) { - const { type, id, body } = data; - - if (id !== this.appearNote.id) return; - - switch (type) { - case 'reacted': { - const reaction = body.reaction; - - // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので) - let n = { - ...this.appearNote, - }; - - if (body.emoji) { - const emojis = this.appearNote.emojis || []; - if (!emojis.includes(body.emoji)) { - n.emojis = [...emojis, body.emoji]; - } - } - - // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる - const currentCount = (this.appearNote.reactions || {})[reaction] || 0; - - // Increment the count - n.reactions = { - ...this.appearNote.reactions, - [reaction]: currentCount + 1 - }; - - if (body.userId === this.$i.id) { - n.myReaction = reaction; - } - - this.updateAppearNote(n); - break; - } - - case 'unreacted': { - const reaction = body.reaction; - - // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので) - let n = { - ...this.appearNote, - }; - - // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる - const currentCount = (this.appearNote.reactions || {})[reaction] || 0; - - // Decrement the count - n.reactions = { - ...this.appearNote.reactions, - [reaction]: Math.max(0, currentCount - 1) - }; - - if (body.userId === this.$i.id) { - n.myReaction = null; - } - - this.updateAppearNote(n); - break; - } - - case 'pollVoted': { - const choice = body.choice; - - // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので) - let n = { - ...this.appearNote, - }; - - const choices = [...this.appearNote.poll.choices]; - choices[choice] = { - ...choices[choice], - votes: choices[choice].votes + 1, - ...(body.userId === this.$i.id ? { - isVoted: true - } : {}) - }; - - n.poll = { - ...this.appearNote.poll, - choices: choices - }; - - this.updateAppearNote(n); - break; - } - - case 'deleted': { - this.isDeleted = true; - break; - } - } - }, - - reply(viaKeyboard = false) { - pleaseLogin(); - os.post({ - reply: this.appearNote, - animation: !viaKeyboard, - }, () => { - this.focus(); - }); - }, - - renoteDirectly() { - os.apiWithDialog('notes/create', { - renoteId: this.appearNote.id - }, undefined, (res: any) => { - os.alert({ - type: 'success', - text: this.$ts.renoted, - }); - }, (e: Error) => { - if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') { - os.alert({ - type: 'error', - text: this.$ts.cantRenote, - }); - } else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') { - os.alert({ - type: 'error', - text: this.$ts.cantReRenote, - }); - } - }); - }, - - react(viaKeyboard = false) { - pleaseLogin(); - this.blur(); - reactionPicker.show(this.$refs.reactButton, reaction => { - os.api('notes/reactions/create', { - noteId: this.appearNote.id, - reaction: reaction - }); - }, () => { - this.focus(); - }); - }, - - reactDirectly(reaction) { - os.api('notes/reactions/create', { - noteId: this.appearNote.id, - reaction: reaction - }); - }, - - undoReact(note) { - const oldReaction = note.myReaction; - if (!oldReaction) return; - os.api('notes/reactions/delete', { - noteId: note.id - }); - }, - - favorite() { - pleaseLogin(); - os.apiWithDialog('notes/favorites/create', { - noteId: this.appearNote.id - }, undefined, (res: any) => { - os.alert({ - type: 'success', - text: this.$ts.favorited, - }); - }, (e: Error) => { - if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') { - os.alert({ - type: 'error', - text: this.$ts.alreadyFavorited, - }); - } else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') { - os.alert({ - type: 'error', - text: this.$ts.cantFavorite, - }); - } - }); - }, - - del() { - os.confirm({ - type: 'warning', - text: this.$ts.noteDeleteConfirm, - }).then(({ canceled }) => { - if (canceled) return; - - os.api('notes/delete', { - noteId: this.appearNote.id - }); - }); - }, - - delEdit() { - os.confirm({ - type: 'warning', - text: this.$ts.deleteAndEditConfirm, - }).then(({ canceled }) => { - if (canceled) return; - - os.api('notes/delete', { - noteId: this.appearNote.id - }); - - os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel }); - }); - }, - - toggleFavorite(favorite: boolean) { - os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { - noteId: this.appearNote.id - }); - }, - - toggleWatch(watch: boolean) { - os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', { - noteId: this.appearNote.id - }); - }, - - toggleThreadMute(mute: boolean) { - os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', { - noteId: this.appearNote.id - }); - }, - - getMenu() { - let menu; - if (this.$i) { - const statePromise = os.api('notes/state', { - noteId: this.appearNote.id - }); - - menu = [{ - icon: 'fas fa-copy', - text: this.$ts.copyContent, - action: this.copyContent - }, { - icon: 'fas fa-link', - text: this.$ts.copyLink, - action: this.copyLink - }, (this.appearNote.url || this.appearNote.uri) ? { - icon: 'fas fa-external-link-square-alt', - text: this.$ts.showOnRemote, - action: () => { - window.open(this.appearNote.url || this.appearNote.uri, '_blank'); - } - } : undefined, - { - icon: 'fas fa-share-alt', - text: this.$ts.share, - action: this.share - }, - this.$instance.translatorAvailable ? { - icon: 'fas fa-language', - text: this.$ts.translate, - action: this.translate - } : undefined, - null, - statePromise.then(state => state.isFavorited ? { - icon: 'fas fa-star', - text: this.$ts.unfavorite, - action: () => this.toggleFavorite(false) - } : { - icon: 'fas fa-star', - text: this.$ts.favorite, - action: () => this.toggleFavorite(true) - }), - { - icon: 'fas fa-paperclip', - text: this.$ts.clip, - action: () => this.clip() - }, - (this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? { - icon: 'fas fa-eye-slash', - text: this.$ts.unwatch, - action: () => this.toggleWatch(false) - } : { - icon: 'fas fa-eye', - text: this.$ts.watch, - action: () => this.toggleWatch(true) - }) : undefined, - statePromise.then(state => state.isMutedThread ? { - icon: 'fas fa-comment-slash', - text: this.$ts.unmuteThread, - action: () => this.toggleThreadMute(false) - } : { - icon: 'fas fa-comment-slash', - text: this.$ts.muteThread, - action: () => this.toggleThreadMute(true) - }), - this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? { - icon: 'fas fa-thumbtack', - text: this.$ts.unpin, - action: () => this.togglePin(false) - } : { - icon: 'fas fa-thumbtack', - text: this.$ts.pin, - action: () => this.togglePin(true) - } : undefined, - /* - ...(this.$i.isModerator || this.$i.isAdmin ? [ - null, - { - icon: 'fas fa-bullhorn', - text: this.$ts.promote, - action: this.promote - }] - : [] - ),*/ - ...(this.appearNote.userId != this.$i.id ? [ - null, - { - icon: 'fas fa-exclamation-circle', - text: this.$ts.reportAbuse, - action: () => { - const u = `${url}/notes/${this.appearNote.id}`; - os.popup(import('@/components/abuse-report-window.vue'), { - user: this.appearNote.user, - initialComment: `Note: ${u}\n-----\n` - }, {}, 'closed'); - } - }] - : [] - ), - ...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [ - null, - this.appearNote.userId == this.$i.id ? { - icon: 'fas fa-edit', - text: this.$ts.deleteAndEdit, - action: this.delEdit - } : undefined, - { - icon: 'fas fa-trash-alt', - text: this.$ts.delete, - danger: true, - action: this.del - }] - : [] - )] - .filter(x => x !== undefined); - } else { - menu = [{ - icon: 'fas fa-copy', - text: this.$ts.copyContent, - action: this.copyContent - }, { - icon: 'fas fa-link', - text: this.$ts.copyLink, - action: this.copyLink - }, (this.appearNote.url || this.appearNote.uri) ? { - icon: 'fas fa-external-link-square-alt', - text: this.$ts.showOnRemote, - action: () => { - window.open(this.appearNote.url || this.appearNote.uri, '_blank'); - } - } : undefined] - .filter(x => x !== undefined); - } - - if (noteActions.length > 0) { - menu = menu.concat([null, ...noteActions.map(action => ({ - icon: 'fas fa-plug', - text: action.title, - action: () => { - action.handler(this.appearNote); - } - }))]); - } - - return menu; - }, - - onContextmenu(e) { - const isLink = (el: HTMLElement) => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - }; - if (isLink(e.target)) return; - if (window.getSelection().toString() !== '') return; - - if (this.$store.state.useReactionPickerForContextMenu) { - e.preventDefault(); - this.react(); - } else { - os.contextMenu(this.getMenu(), e).then(this.focus); - } - }, - - menu(viaKeyboard = false) { - os.popupMenu(this.getMenu(), this.$refs.menuButton, { - viaKeyboard - }).then(this.focus); - }, - - showRenoteMenu(viaKeyboard = false) { - if (!this.isMyRenote) return; - os.popupMenu([{ - text: this.$ts.unrenote, - icon: 'fas fa-trash-alt', - danger: true, - action: () => { - os.api('notes/delete', { - noteId: this.note.id - }); - this.isDeleted = true; - } - }], this.$refs.renoteTime, { - viaKeyboard: viaKeyboard - }); - }, - - toggleShowContent() { - this.showContent = !this.showContent; - }, - - copyContent() { - copyToClipboard(this.appearNote.text); - os.success(); - }, - - copyLink() { - copyToClipboard(`${url}/notes/${this.appearNote.id}`); - os.success(); - }, - - togglePin(pin: boolean) { - os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', { - noteId: this.appearNote.id - }, undefined, null, e => { - if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { - os.alert({ - type: 'error', - text: this.$ts.pinLimitExceeded - }); - } - }); - }, - - async clip() { - const clips = await os.api('clips/list'); - os.popupMenu([{ - icon: 'fas fa-plus', - text: this.$ts.createNew, - action: async () => { - const { canceled, result } = await os.form(this.$ts.createNewClip, { - name: { - type: 'string', - label: this.$ts.name - }, - description: { - type: 'string', - required: false, - multiline: true, - label: this.$ts.description - }, - isPublic: { - type: 'boolean', - label: this.$ts.public, - default: false - } - }); - if (canceled) return; - - const clip = await os.apiWithDialog('clips/create', result); - - os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id }); - } - }, null, ...clips.map(clip => ({ - text: clip.name, - action: () => { - os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id }); - } - }))], this.$refs.menuButton, { - }).then(this.focus); - }, - - async promote() { - const { canceled, result: days } = await os.inputNumber({ - title: this.$ts.numberOfDays, - }); - - if (canceled) return; - - os.apiWithDialog('admin/promo/create', { - noteId: this.appearNote.id, - expiresAt: Date.now() + (86400000 * days) - }); - }, + if (defaultStore.state.useReactionPickerForContextMenu) { + e.preventDefault(); + react(); + } else { + os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), e).then(focus); + } +} - share() { - navigator.share({ - title: this.$t('noteOf', { user: this.appearNote.user.name }), - text: this.appearNote.text, - url: `${url}/notes/${this.appearNote.id}` - }); - }, +function menu(viaKeyboard = false): void { + os.popupMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), menuButton.value, { + viaKeyboard + }).then(focus); +} - async translate() { - if (this.translation != null) return; - this.translating = true; - const res = await os.api('notes/translate', { - noteId: this.appearNote.id, - targetLang: localStorage.getItem('lang') || navigator.language, +function showRenoteMenu(viaKeyboard = false): void { + if (!isMyRenote) return; + os.popupMenu([{ + text: i18n.locale.unrenote, + icon: 'fas fa-trash-alt', + danger: true, + action: () => { + os.api('notes/delete', { + noteId: props.note.id }); - this.translating = false; - this.translation = res; - }, + isDeleted.value = true; + } + }], renoteTime.value, { + viaKeyboard: viaKeyboard + }); +} - focus() { - this.$el.focus(); - }, +function focus() { + el.value.focus(); +} - blur() { - this.$el.blur(); - }, +function blur() { + el.value.blur(); +} - focusBefore() { - focusPrev(this.$el); - }, +function focusBefore() { + focusPrev(el.value); +} - focusAfter() { - focusNext(this.$el); - }, +function focusAfter() { + focusNext(el.value); +} - userPage - } -}); +function readPromo() { + os.api('promo/read', { + noteId: appearNote.id + }); + isDeleted.value = true; +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/notes.vue b/packages/client/src/components/notes.vue index d6107216e2..aec478ac95 100644 --- a/packages/client/src/components/notes.vue +++ b/packages/client/src/components/notes.vue @@ -10,7 +10,7 @@ <template #default="{ items: notes }"> <div class="giivymft" :class="{ noGap }"> <XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" :ad="true" class="notes"> - <XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note" @update:note="updated(note, $event)"/> + <XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note"/> </XList> </div> </template> @@ -31,10 +31,6 @@ const props = defineProps<{ const pagingComponent = ref<InstanceType<typeof MkPagination>>(); -const updated = (oldValue, newValue) => { - pagingComponent.value?.updateItem(oldValue.id, () => newValue); -}; - defineExpose({ prepend: (note) => { pagingComponent.value?.prepend(note); diff --git a/packages/client/src/components/notification-toast.vue b/packages/client/src/components/notification-toast.vue index 5449409ccc..fbd8467a6e 100644 --- a/packages/client/src/components/notification-toast.vue +++ b/packages/client/src/components/notification-toast.vue @@ -29,7 +29,7 @@ export default defineComponent({ }; }, mounted() { - setTimeout(() => { + window.setTimeout(() => { this.showing = false; }, 6000); } diff --git a/packages/client/src/components/notifications.vue b/packages/client/src/components/notifications.vue index 31511fb515..5a77b5487e 100644 --- a/packages/client/src/components/notifications.vue +++ b/packages/client/src/components/notifications.vue @@ -9,7 +9,7 @@ <template #default="{ items: notifications }"> <XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true"> - <XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" @update:note="noteUpdated(notification, $event)"/> + <XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/> <XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/> </XList> </template> @@ -62,13 +62,6 @@ const onNotification = (notification) => { } }; -const noteUpdated = (item, note) => { - pagingComponent.value?.updateItem(item.id, old => ({ - ...old, - note: note, - })); -}; - onMounted(() => { const connection = stream.useChannel('main'); connection.on('notification', onNotification); diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue index 24f35da2e9..3fcb1d906b 100644 --- a/packages/client/src/components/post-form.vue +++ b/packages/client/src/components/post-form.vue @@ -9,7 +9,7 @@ <header> <button v-if="!fixed" class="cancel _button" @click="cancel"><i class="fas fa-times"></i></button> <div> - <span class="text-count" :class="{ over: textLength > max }">{{ max - textLength }}</span> + <span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span> <span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span> <button ref="visibilityButton" v-tooltip="$ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility"> <span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span> @@ -36,9 +36,9 @@ </div> </div> <MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ $ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ $ts.add }}</button></MkInfo> - <input v-show="useCw" ref="cw" v-model="cw" class="cw" :placeholder="$ts.annotation" @keydown="onKeydown"> - <textarea ref="text" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> - <input v-show="withHashtags" ref="hashtags" v-model="hashtags" class="hashtags" :placeholder="$ts.hashtags" list="hashtags"> + <input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="$ts.annotation" @keydown="onKeydown"> + <textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> + <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="$ts.hashtags" list="hashtags"> <XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/> <XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/> <XNotePreview v-if="showPreview" class="preview" :text="text"/> @@ -58,667 +58,603 @@ </div> </template> -<script lang="ts"> -import { defineComponent, defineAsyncComponent } from 'vue'; +<script lang="ts" setup> +import { inject, watch, nextTick, onMounted } from 'vue'; +import * as mfm from 'mfm-js'; +import * as misskey from 'misskey-js'; import insertTextAtCursor from 'insert-text-at-cursor'; import { length } from 'stringz'; import { toASCII } from 'punycode/'; import XNoteSimple from './note-simple.vue'; import XNotePreview from './note-preview.vue'; -import * as mfm from 'mfm-js'; +import XPostFormAttaches from './post-form-attaches.vue'; +import XPollEditor from './poll-editor.vue'; import { host, url } from '@/config'; import { erase, unique } from '@/scripts/array'; import { extractMentions } from '@/scripts/extract-mentions'; import * as Acct from 'misskey-js/built/acct'; import { formatTimeString } from '@/scripts/format-time-string'; import { Autocomplete } from '@/scripts/autocomplete'; -import { noteVisibilities } from 'misskey-js'; import * as os from '@/os'; import { stream } from '@/stream'; import { selectFiles } from '@/scripts/select-file'; import { defaultStore, notePostInterruptors, postFormActions } from '@/store'; import { throttle } from 'throttle-debounce'; import MkInfo from '@/components/ui/info.vue'; +import { i18n } from '@/i18n'; +import { instance } from '@/instance'; +import { $i } from '@/account'; -export default defineComponent({ - components: { - XNoteSimple, - XNotePreview, - XPostFormAttaches: defineAsyncComponent(() => import('./post-form-attaches.vue')), - XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue')), - MkInfo, - }, +const modal = inject('modal'); - inject: ['modal'], +const props = withDefaults(defineProps<{ + reply?: misskey.entities.Note; + renote?: misskey.entities.Note; + channel?: any; // TODO + mention?: misskey.entities.User; + specified?: misskey.entities.User; + initialText?: string; + initialVisibility?: typeof misskey.noteVisibilities; + initialFiles?: misskey.entities.DriveFile[]; + initialLocalOnly?: boolean; + initialVisibleUsers?: misskey.entities.User[]; + initialNote?: misskey.entities.Note; + share?: boolean; + fixed?: boolean; + autofocus?: boolean; +}>(), { + initialVisibleUsers: [], + autofocus: true, +}); - props: { - reply: { - type: Object, - required: false - }, - renote: { - type: Object, - required: false - }, - channel: { - type: Object, - required: false - }, - mention: { - type: Object, - required: false - }, - specified: { - type: Object, - required: false - }, - initialText: { - type: String, - required: false - }, - initialVisibility: { - type: String, - required: false - }, - initialFiles: { - type: Array, - required: false - }, - initialLocalOnly: { - type: Boolean, - required: false - }, - initialVisibleUsers: { - type: Array, - required: false, - default: () => [] - }, - initialNote: { - type: Object, - required: false - }, - share: { - type: Boolean, - required: false, - default: false - }, - fixed: { - type: Boolean, - required: false, - default: false - }, - autofocus: { - type: Boolean, - required: false, - default: true - }, - }, +const emit = defineEmits<{ + (e: 'posted'): void; + (e: 'cancel'): void; + (e: 'esc'): void; +}>(); - emits: ['posted', 'cancel', 'esc'], +const textareaEl = $ref<HTMLTextAreaElement | null>(null); +const cwInputEl = $ref<HTMLInputElement | null>(null); +const hashtagsInputEl = $ref<HTMLInputElement | null>(null); +const visibilityButton = $ref<HTMLElement | null>(null); - data() { - return { - posting: false, - text: '', - files: [], - poll: null, - useCw: false, - showPreview: false, - cw: null, - localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly, - visibility: (this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility) as typeof noteVisibilities[number], - visibleUsers: [], - autocomplete: null, - draghover: false, - quoteId: null, - hasNotSpecifiedMentions: false, - recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'), - imeText: '', - typing: throttle(3000, () => { - if (this.channel) { - stream.send('typingOnChannel', { channel: this.channel.id }); - } - }), - postFormActions, - }; - }, +let posting = $ref(false); +let text = $ref(props.initialText ?? ''); +let files = $ref(props.initialFiles ?? []); +let poll = $ref<{ + choices: string[]; + multiple: boolean; + expiresAt: string; + expiredAfter: string; +} | null>(null); +let useCw = $ref(false); +let showPreview = $ref(false); +let cw = $ref<string | null>(null); +let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly); +let visibility = $ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof misskey.noteVisibilities[number]); +let visibleUsers = $ref(props.initialVisibleUsers ?? []); +let autocomplete = $ref(null); +let draghover = $ref(false); +let quoteId = $ref(null); +let hasNotSpecifiedMentions = $ref(false); +let recentHashtags = $ref(JSON.parse(localStorage.getItem('hashtags') || '[]')); +let imeText = $ref(''); - computed: { - draftKey(): string { - let key = this.channel ? `channel:${this.channel.id}` : ''; +const typing = throttle(3000, () => { + if (props.channel) { + stream.send('typingOnChannel', { channel: props.channel.id }); + } +}); - if (this.renote) { - key += `renote:${this.renote.id}`; - } else if (this.reply) { - key += `reply:${this.reply.id}`; - } else { - key += 'note'; - } +const draftKey = $computed((): string => { + let key = props.channel ? `channel:${props.channel.id}` : ''; - return key; - }, + if (props.renote) { + key += `renote:${props.renote.id}`; + } else if (props.reply) { + key += `reply:${props.reply.id}`; + } else { + key += 'note'; + } - placeholder(): string { - if (this.renote) { - return this.$ts._postForm.quotePlaceholder; - } else if (this.reply) { - return this.$ts._postForm.replyPlaceholder; - } else if (this.channel) { - return this.$ts._postForm.channelPlaceholder; - } else { - const xs = [ - this.$ts._postForm._placeholders.a, - this.$ts._postForm._placeholders.b, - this.$ts._postForm._placeholders.c, - this.$ts._postForm._placeholders.d, - this.$ts._postForm._placeholders.e, - this.$ts._postForm._placeholders.f - ]; - return xs[Math.floor(Math.random() * xs.length)]; - } - }, + return key; +}); - submitText(): string { - return this.renote - ? this.$ts.quote - : this.reply - ? this.$ts.reply - : this.$ts.note; - }, +const placeholder = $computed((): string => { + if (props.renote) { + return i18n.locale._postForm.quotePlaceholder; + } else if (props.reply) { + return i18n.locale._postForm.replyPlaceholder; + } else if (props.channel) { + return i18n.locale._postForm.channelPlaceholder; + } else { + const xs = [ + i18n.locale._postForm._placeholders.a, + i18n.locale._postForm._placeholders.b, + i18n.locale._postForm._placeholders.c, + i18n.locale._postForm._placeholders.d, + i18n.locale._postForm._placeholders.e, + i18n.locale._postForm._placeholders.f + ]; + return xs[Math.floor(Math.random() * xs.length)]; + } +}); - textLength(): number { - return length((this.text + this.imeText).trim()); - }, +const submitText = $computed((): string => { + return props.renote + ? i18n.locale.quote + : props.reply + ? i18n.locale.reply + : i18n.locale.note; +}); - canPost(): boolean { - return !this.posting && - (1 <= this.textLength || 1 <= this.files.length || !!this.poll || !!this.renote) && - (this.textLength <= this.max) && - (!this.poll || this.poll.choices.length >= 2); - }, +const textLength = $computed((): number => { + return length((text + imeText).trim()); +}); - max(): number { - return this.$instance ? this.$instance.maxNoteTextLength : 1000; - }, +const maxTextLength = $computed((): number => { + return instance ? instance.maxNoteTextLength : 1000; +}); - withHashtags: defaultStore.makeGetterSetter('postFormWithHashtags'), - hashtags: defaultStore.makeGetterSetter('postFormHashtags'), - }, +const canPost = $computed((): boolean => { + return !posting && + (1 <= textLength || 1 <= files.length || !!poll || !!props.renote) && + (textLength <= maxTextLength) && + (!poll || poll.choices.length >= 2); +}); - watch: { - text() { - this.checkMissingMention(); - }, - visibleUsers: { - handler() { - this.checkMissingMention(); - }, - deep: true - } - }, +const withHashtags = $computed(defaultStore.makeGetterSetter('postFormWithHashtags')); +const hashtags = $computed(defaultStore.makeGetterSetter('postFormHashtags')); - mounted() { - if (this.initialText) { - this.text = this.initialText; - } +watch($$(text), () => { + checkMissingMention(); +}); - if (this.initialVisibility) { - this.visibility = this.initialVisibility; - } +watch($$(visibleUsers), () => { + checkMissingMention(); +}, { + deep: true, +}); - if (this.initialFiles) { - this.files = this.initialFiles; - } +if (props.mention) { + text = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`; + text += ' '; +} - if (typeof this.initialLocalOnly === 'boolean') { - this.localOnly = this.initialLocalOnly; - } +if (props.reply && (props.reply.user.username != $i.username || (props.reply.user.host != null && props.reply.user.host != host))) { + text = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `; +} - if (this.initialVisibleUsers) { - this.visibleUsers = this.initialVisibleUsers; - } +if (props.reply && props.reply.text != null) { + const ast = mfm.parse(props.reply.text); + const otherHost = props.reply.user.host; - if (this.mention) { - this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`; - this.text += ' '; - } + for (const x of extractMentions(ast)) { + const mention = x.host ? + `@${x.username}@${toASCII(x.host)}` : + (otherHost == null || otherHost == host) ? + `@${x.username}` : + `@${x.username}@${toASCII(otherHost)}`; - if (this.reply && (this.reply.user.username != this.$i.username || (this.reply.user.host != null && this.reply.user.host != host))) { - this.text = `@${this.reply.user.username}${this.reply.user.host != null ? '@' + toASCII(this.reply.user.host) : ''} `; - } + // 自分は除外 + if ($i.username == x.username && x.host == null) continue; + if ($i.username == x.username && x.host == host) continue; - if (this.reply && this.reply.text != null) { - const ast = mfm.parse(this.reply.text); - const otherHost = this.reply.user.host; + // 重複は除外 + if (text.indexOf(`${mention} `) != -1) continue; - for (const x of extractMentions(ast)) { - const mention = x.host ? - `@${x.username}@${toASCII(x.host)}` : - (otherHost == null || otherHost == host) ? - `@${x.username}` : - `@${x.username}@${toASCII(otherHost)}`; + text += `${mention} `; + } +} - // 自分は除外 - if (this.$i.username == x.username && x.host == null) continue; - if (this.$i.username == x.username && x.host == host) continue; +if (props.channel) { + visibility = 'public'; + localOnly = true; // TODO: チャンネルが連合するようになった折には消す +} - // 重複は除外 - if (this.text.indexOf(`${mention} `) != -1) continue; +// 公開以外へのリプライ時は元の公開範囲を引き継ぐ +if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) { + visibility = props.reply.visibility; + if (props.reply.visibility === 'specified') { + os.api('users/show', { + userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId) + }).then(users => { + visibleUsers.push(...users); + }); - this.text += `${mention} `; - } + if (props.reply.userId !== $i.id) { + os.api('users/show', { userId: props.reply.userId }).then(user => { + visibleUsers.push(user); + }); } + } +} - if (this.channel) { - this.visibility = 'public'; - this.localOnly = true; // TODO: チャンネルが連合するようになった折には消す - } +if (props.specified) { + visibility = 'specified'; + visibleUsers.push(props.specified); +} - // 公開以外へのリプライ時は元の公開範囲を引き継ぐ - if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) { - this.visibility = this.reply.visibility; - if (this.reply.visibility === 'specified') { - os.api('users/show', { - userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$i.id && uid !== this.reply.userId) - }).then(users => { - this.visibleUsers.push(...users); - }); +// keep cw when reply +if (defaultStore.state.keepCw && props.reply && props.reply.cw) { + useCw = true; + cw = props.reply.cw; +} - if (this.reply.userId !== this.$i.id) { - os.api('users/show', { userId: this.reply.userId }).then(user => { - this.visibleUsers.push(user); - }); - } - } - } +function watchForDraft() { + watch($$(text), () => saveDraft()); + watch($$(useCw), () => saveDraft()); + watch($$(cw), () => saveDraft()); + watch($$(poll), () => saveDraft()); + watch($$(files), () => saveDraft(), { deep: true }); + watch($$(visibility), () => saveDraft()); + watch($$(localOnly), () => saveDraft()); +} - if (this.specified) { - this.visibility = 'specified'; - this.visibleUsers.push(this.specified); - } +function checkMissingMention() { + if (visibility === 'specified') { + const ast = mfm.parse(text); - // keep cw when reply - if (this.$store.state.keepCw && this.reply && this.reply.cw) { - this.useCw = true; - this.cw = this.reply.cw; + for (const x of extractMentions(ast)) { + if (!visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) { + hasNotSpecifiedMentions = true; + return; + } } + hasNotSpecifiedMentions = false; + } +} - if (this.autofocus) { - this.focus(); +function addMissingMention() { + const ast = mfm.parse(text); - this.$nextTick(() => { - this.focus(); + for (const x of extractMentions(ast)) { + if (!visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) { + os.api('users/show', { username: x.username, host: x.host }).then(user => { + visibleUsers.push(user); }); } + } +} - // TODO: detach when unmount - new Autocomplete(this.$refs.text, this, { model: 'text' }); - new Autocomplete(this.$refs.cw, this, { model: 'cw' }); - new Autocomplete(this.$refs.hashtags, this, { model: 'hashtags' }); +function togglePoll() { + if (poll) { + poll = null; + } else { + poll = { + choices: ['', ''], + multiple: false, + expiresAt: null, + expiredAfter: null, + }; + } +} - this.$nextTick(() => { - // 書きかけの投稿を復元 - if (!this.share && !this.mention && !this.specified) { - const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey]; - if (draft) { - this.text = draft.data.text; - this.useCw = draft.data.useCw; - this.cw = draft.data.cw; - this.visibility = draft.data.visibility; - this.localOnly = draft.data.localOnly; - this.files = (draft.data.files || []).filter(e => e); - if (draft.data.poll) { - this.poll = draft.data.poll; - } - } - } +function addTag(tag: string) { + insertTextAtCursor(textareaEl, ` #${tag} `); +} - // 削除して編集 - if (this.initialNote) { - const init = this.initialNote; - this.text = init.text ? init.text : ''; - this.files = init.files; - this.cw = init.cw; - this.useCw = init.cw != null; - if (init.poll) { - this.poll = { - choices: init.poll.choices.map(x => x.text), - multiple: init.poll.multiple, - expiresAt: init.poll.expiresAt, - expiredAfter: init.poll.expiredAfter, - }; - } - this.visibility = init.visibility; - this.localOnly = init.localOnly; - this.quoteId = init.renote ? init.renote.id : null; - } +function focus() { + textareaEl.focus(); +} - this.$nextTick(() => this.watch()); - }); - }, +function chooseFileFrom(ev) { + selectFiles(ev.currentTarget || ev.target, i18n.locale.attachFile).then(files => { + for (const file of files) { + files.push(file); + } + }); +} - methods: { - watch() { - this.$watch('text', () => this.saveDraft()); - this.$watch('useCw', () => this.saveDraft()); - this.$watch('cw', () => this.saveDraft()); - this.$watch('poll', () => this.saveDraft()); - this.$watch('files', () => this.saveDraft(), { deep: true }); - this.$watch('visibility', () => this.saveDraft()); - this.$watch('localOnly', () => this.saveDraft()); - }, +function detachFile(id) { + files = files.filter(x => x.id != id); +} - checkMissingMention() { - if (this.visibility === 'specified') { - const ast = mfm.parse(this.text); +function updateFiles(files) { + files = files; +} - for (const x of extractMentions(ast)) { - if (!this.visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) { - this.hasNotSpecifiedMentions = true; - return; - } - } - this.hasNotSpecifiedMentions = false; - } - }, +function updateFileSensitive(file, sensitive) { + files[files.findIndex(x => x.id === file.id)].isSensitive = sensitive; +} - addMissingMention() { - const ast = mfm.parse(this.text); +function updateFileName(file, name) { + files[files.findIndex(x => x.id === file.id)].name = name; +} - for (const x of extractMentions(ast)) { - if (!this.visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) { - os.api('users/show', { username: x.username, host: x.host }).then(user => { - this.visibleUsers.push(user); - }); - } - } - }, +function upload(file: File, name?: string) { + os.upload(file, defaultStore.state.uploadFolder, name).then(res => { + files.push(res); + }); +} - togglePoll() { - if (this.poll) { - this.poll = null; - } else { - this.poll = { - choices: ['', ''], - multiple: false, - expiresAt: null, - expiredAfter: null, - }; +function onPollUpdate(poll) { + poll = poll; + saveDraft(); +} + +function setVisibility() { + if (props.channel) { + // TODO: information dialog + return; + } + + os.popup(import('./visibility-picker.vue'), { + currentVisibility: visibility, + currentLocalOnly: localOnly, + src: visibilityButton, + }, { + changeVisibility: v => { + visibility = v; + if (defaultStore.state.rememberNoteVisibility) { + defaultStore.set('visibility', visibility); } }, + changeLocalOnly: v => { + localOnly = v; + if (defaultStore.state.rememberNoteVisibility) { + defaultStore.set('localOnly', localOnly); + } + } + }, 'closed'); +} - addTag(tag: string) { - insertTextAtCursor(this.$refs.text, ` #${tag} `); - }, +function addVisibleUser() { + os.selectUser().then(user => { + visibleUsers.push(user); + }); +} - focus() { - (this.$refs.text as any).focus(); - }, +function removeVisibleUser(user) { + visibleUsers = erase(user, visibleUsers); +} - chooseFileFrom(ev) { - selectFiles(ev.currentTarget || ev.target, this.$ts.attachFile).then(files => { - for (const file of files) { - this.files.push(file); - } - }); - }, +function clear() { + text = ''; + files = []; + poll = null; + quoteId = null; +} - detachFile(id) { - this.files = this.files.filter(x => x.id != id); - }, +function onKeydown(e: KeyboardEvent) { + if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && canPost) post(); + if (e.which === 27) emit('esc'); + typing(); +} - updateFiles(files) { - this.files = files; - }, +function onCompositionUpdate(e: CompositionEvent) { + imeText = e.data; + typing(); +} - updateFileSensitive(file, sensitive) { - this.files[this.files.findIndex(x => x.id === file.id)].isSensitive = sensitive; - }, +function onCompositionEnd(e: CompositionEvent) { + imeText = ''; +} - updateFileName(file, name) { - this.files[this.files.findIndex(x => x.id === file.id)].name = name; - }, +async function onPaste(e: ClipboardEvent) { + for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) { + if (item.kind == 'file') { + const file = item.getAsFile(); + const lio = file.name.lastIndexOf('.'); + const ext = lio >= 0 ? file.name.slice(lio) : ''; + const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`; + upload(file, formatted); + } + } - upload(file: File, name?: string) { - os.upload(file, this.$store.state.uploadFolder, name).then(res => { - this.files.push(res); - }); - }, + const paste = e.clipboardData.getData('text'); - onPollUpdate(poll) { - this.poll = poll; - this.saveDraft(); - }, + if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) { + e.preventDefault(); - setVisibility() { - if (this.channel) { - // TODO: information dialog + os.confirm({ + type: 'info', + text: i18n.locale.quoteQuestion, + }).then(({ canceled }) => { + if (canceled) { + insertTextAtCursor(textareaEl, paste); return; } - os.popup(import('./visibility-picker.vue'), { - currentVisibility: this.visibility, - currentLocalOnly: this.localOnly, - src: this.$refs.visibilityButton - }, { - changeVisibility: visibility => { - this.visibility = visibility; - if (this.$store.state.rememberNoteVisibility) { - this.$store.set('visibility', visibility); - } - }, - changeLocalOnly: localOnly => { - this.localOnly = localOnly; - if (this.$store.state.rememberNoteVisibility) { - this.$store.set('localOnly', localOnly); - } - } - }, 'closed'); - }, - - addVisibleUser() { - os.selectUser().then(user => { - this.visibleUsers.push(user); - }); - }, + quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1]; + }); + } +} - removeVisibleUser(user) { - this.visibleUsers = erase(user, this.visibleUsers); - }, +function onDragover(e) { + if (!e.dataTransfer.items[0]) return; + const isFile = e.dataTransfer.items[0].kind == 'file'; + const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; + if (isFile || isDriveFile) { + e.preventDefault(); + draghover = true; + e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; + } +} - clear() { - this.text = ''; - this.files = []; - this.poll = null; - this.quoteId = null; - }, +function onDragenter(e) { + draghover = true; +} - onKeydown(e: KeyboardEvent) { - if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post(); - if (e.which === 27) this.$emit('esc'); - this.typing(); - }, +function onDragleave(e) { + draghover = false; +} - onCompositionUpdate(e: CompositionEvent) { - this.imeText = e.data; - this.typing(); - }, +function onDrop(e): void { + draghover = false; - onCompositionEnd(e: CompositionEvent) { - this.imeText = ''; - }, + // ファイルだったら + if (e.dataTransfer.files.length > 0) { + e.preventDefault(); + for (const x of Array.from(e.dataTransfer.files)) upload(x); + return; + } - async onPaste(e: ClipboardEvent) { - for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) { - if (item.kind == 'file') { - const file = item.getAsFile(); - const lio = file.name.lastIndexOf('.'); - const ext = lio >= 0 ? file.name.slice(lio) : ''; - const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`; - this.upload(file, formatted); - } - } + //#region ドライブのファイル + const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); + if (driveFile != null && driveFile != '') { + const file = JSON.parse(driveFile); + files.push(file); + e.preventDefault(); + } + //#endregion +} - const paste = e.clipboardData.getData('text'); +function saveDraft() { + const data = JSON.parse(localStorage.getItem('drafts') || '{}'); - if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) { - e.preventDefault(); + data[draftKey] = { + updatedAt: new Date(), + data: { + text: text, + useCw: useCw, + cw: cw, + visibility: visibility, + localOnly: localOnly, + files: files, + poll: poll + } + }; - os.confirm({ - type: 'info', - text: this.$ts.quoteQuestion, - }).then(({ canceled }) => { - if (canceled) { - insertTextAtCursor(this.$refs.text, paste); - return; - } + localStorage.setItem('drafts', JSON.stringify(data)); +} - this.quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1]; - }); - } - }, +function deleteDraft() { + const data = JSON.parse(localStorage.getItem('drafts') || '{}'); - onDragover(e) { - if (!e.dataTransfer.items[0]) return; - const isFile = e.dataTransfer.items[0].kind == 'file'; - const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; - if (isFile || isDriveFile) { - e.preventDefault(); - this.draghover = true; - e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; - } - }, + delete data[draftKey]; - onDragenter(e) { - this.draghover = true; - }, + localStorage.setItem('drafts', JSON.stringify(data)); +} - onDragleave(e) { - this.draghover = false; - }, +async function post() { + let data = { + text: text == '' ? undefined : text, + fileIds: files.length > 0 ? files.map(f => f.id) : undefined, + replyId: props.reply ? props.reply.id : undefined, + renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined, + channelId: props.channel ? props.channel.id : undefined, + poll: poll, + cw: useCw ? cw || '' : undefined, + localOnly: localOnly, + visibility: visibility, + visibleUserIds: visibility == 'specified' ? visibleUsers.map(u => u.id) : undefined, + }; - onDrop(e): void { - this.draghover = false; + if (withHashtags && hashtags && hashtags.trim() !== '') { + const hashtags = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); + data.text = data.text ? `${data.text} ${hashtags}` : hashtags; + } - // ファイルだったら - if (e.dataTransfer.files.length > 0) { - e.preventDefault(); - for (const x of Array.from(e.dataTransfer.files)) this.upload(x); - return; - } + // plugin + if (notePostInterruptors.length > 0) { + for (const interruptor of notePostInterruptors) { + data = await interruptor.handler(JSON.parse(JSON.stringify(data))); + } + } - //#region ドライブのファイル - const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); - if (driveFile != null && driveFile != '') { - const file = JSON.parse(driveFile); - this.files.push(file); - e.preventDefault(); + posting = true; + os.api('notes/create', data).then(() => { + clear(); + nextTick(() => { + deleteDraft(); + emit('posted'); + if (data.text && data.text != '') { + const hashtags = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag); + const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; + localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history)))); } - //#endregion - }, - - saveDraft() { - const data = JSON.parse(localStorage.getItem('drafts') || '{}'); + posting = false; + }); + }).catch(err => { + posting = false; + os.alert({ + type: 'error', + text: err.message + '\n' + (err as any).id, + }); + }); +} - data[this.draftKey] = { - updatedAt: new Date(), - data: { - text: this.text, - useCw: this.useCw, - cw: this.cw, - visibility: this.visibility, - localOnly: this.localOnly, - files: this.files, - poll: this.poll - } - }; +function cancel() { + emit('cancel'); +} - localStorage.setItem('drafts', JSON.stringify(data)); - }, +function insertMention() { + os.selectUser().then(user => { + insertTextAtCursor(textareaEl, '@' + Acct.toString(user) + ' '); + }); +} - deleteDraft() { - const data = JSON.parse(localStorage.getItem('drafts') || '{}'); +async function insertEmoji(ev) { + os.openEmojiPicker(ev.currentTarget || ev.target, {}, textareaEl); +} - delete data[this.draftKey]; +function showActions(ev) { + os.popupMenu(postFormActions.map(action => ({ + text: action.title, + action: () => { + action.handler({ + text: text + }, (key, value) => { + if (key === 'text') { text = value; } + }); + } + })), ev.currentTarget || ev.target); +} - localStorage.setItem('drafts', JSON.stringify(data)); - }, +onMounted(() => { + if (props.autofocus) { + focus(); - async post() { - let data = { - text: this.text == '' ? undefined : this.text, - fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, - replyId: this.reply ? this.reply.id : undefined, - renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined, - channelId: this.channel ? this.channel.id : undefined, - poll: this.poll, - cw: this.useCw ? this.cw || '' : undefined, - localOnly: this.localOnly, - visibility: this.visibility, - visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined, - }; + nextTick(() => { + focus(); + }); + } - if (this.withHashtags && this.hashtags && this.hashtags.trim() !== '') { - const hashtags = this.hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); - data.text = data.text ? `${data.text} ${hashtags}` : hashtags; - } + // TODO: detach when unmount + new Autocomplete(textareaEl, $$(text)); + new Autocomplete(cwInputEl, $$(cw)); + new Autocomplete(hashtagsInputEl, $$(hashtags)); - // plugin - if (notePostInterruptors.length > 0) { - for (const interruptor of notePostInterruptors) { - data = await interruptor.handler(JSON.parse(JSON.stringify(data))); + nextTick(() => { + // 書きかけの投稿を復元 + if (!props.share && !props.mention && !props.specified) { + const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[draftKey]; + if (draft) { + text = draft.data.text; + useCw = draft.data.useCw; + cw = draft.data.cw; + visibility = draft.data.visibility; + localOnly = draft.data.localOnly; + files = (draft.data.files || []).filter(e => e); + if (draft.data.poll) { + poll = draft.data.poll; } } + } - this.posting = true; - os.api('notes/create', data).then(() => { - this.clear(); - this.$nextTick(() => { - this.deleteDraft(); - this.$emit('posted'); - if (data.text && data.text != '') { - const hashtags = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag); - const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; - localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history)))); - } - this.posting = false; - }); - }).catch(err => { - this.posting = false; - os.alert({ - type: 'error', - text: err.message + '\n' + (err as any).id, - }); - }); - }, - - cancel() { - this.$emit('cancel'); - }, - - insertMention() { - os.selectUser().then(user => { - insertTextAtCursor(this.$refs.text, '@' + Acct.toString(user) + ' '); - }); - }, - - async insertEmoji(ev) { - os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text); - }, - - showActions(ev) { - os.popupMenu(postFormActions.map(action => ({ - text: action.title, - action: () => { - action.handler({ - text: this.text - }, (key, value) => { - if (key === 'text') { this.text = value; } - }); - } - })), ev.currentTarget || ev.target); + // 削除して編集 + if (props.initialNote) { + const init = props.initialNote; + text = init.text ? init.text : ''; + files = init.files; + cw = init.cw; + useCw = init.cw != null; + if (init.poll) { + poll = { + choices: init.poll.choices.map(x => x.text), + multiple: init.poll.multiple, + expiresAt: init.poll.expiresAt, + expiredAfter: init.poll.expiredAfter, + }; + } + visibility = init.visibility; + localOnly = init.localOnly; + quoteId = init.renote ? init.renote.id : null; } - } + + nextTick(() => watchForDraft()); + }); }); </script> diff --git a/packages/client/src/components/reaction-icon.vue b/packages/client/src/components/reaction-icon.vue index c0ec955e32..5638c9a816 100644 --- a/packages/client/src/components/reaction-icon.vue +++ b/packages/client/src/components/reaction-icon.vue @@ -1,25 +1,13 @@ <template> -<MkEmoji :emoji="reaction" :custom-emojis="customEmojis" :is-reaction="true" :normal="true" :no-style="noStyle"/> +<MkEmoji :emoji="reaction" :custom-emojis="customEmojis || []" :is-reaction="true" :normal="true" :no-style="noStyle"/> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; -export default defineComponent({ - props: { - reaction: { - type: String, - required: true - }, - customEmojis: { - required: false, - default: () => [] - }, - noStyle: { - type: Boolean, - required: false, - default: false - }, - }, -}); +const props = defineProps<{ + reaction: string; + customEmojis?: any[]; // TODO + noStyle?: boolean; +}>(); </script> diff --git a/packages/client/src/components/reaction-tooltip.vue b/packages/client/src/components/reaction-tooltip.vue index dda8e7c6d7..1b2a024e21 100644 --- a/packages/client/src/components/reaction-tooltip.vue +++ b/packages/client/src/components/reaction-tooltip.vue @@ -1,5 +1,5 @@ <template> -<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="$emit('closed')"> +<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')"> <div class="beeadbfb"> <XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> <div class="name">{{ reaction.replace('@.', '') }}</div> @@ -7,31 +7,20 @@ </MkTooltip> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkTooltip from './ui/tooltip.vue'; import XReactionIcon from './reaction-icon.vue'; -export default defineComponent({ - components: { - MkTooltip, - XReactionIcon, - }, - props: { - reaction: { - type: String, - required: true, - }, - emojis: { - type: Array, - required: true, - }, - source: { - required: true, - } - }, - emits: ['closed'], -}) +const props = defineProps<{ + reaction: string; + emojis: any[]; // TODO + source: any; // TODO +}>(); + +const emit = defineEmits<{ + (e: 'closed'): void; +}>(); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/reactions-viewer.details.vue b/packages/client/src/components/reactions-viewer.details.vue index d6374517a2..8cec8dfa2f 100644 --- a/packages/client/src/components/reactions-viewer.details.vue +++ b/packages/client/src/components/reactions-viewer.details.vue @@ -1,5 +1,5 @@ <template> -<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="$emit('closed')"> +<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')"> <div class="bqxuuuey"> <div class="reaction"> <XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> @@ -16,39 +16,22 @@ </MkTooltip> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkTooltip from './ui/tooltip.vue'; import XReactionIcon from './reaction-icon.vue'; -export default defineComponent({ - components: { - MkTooltip, - XReactionIcon - }, - props: { - reaction: { - type: String, - required: true, - }, - users: { - type: Array, - required: true, - }, - count: { - type: Number, - required: true, - }, - emojis: { - type: Array, - required: true, - }, - source: { - required: true, - } - }, - emits: ['closed'], -}) +const props = defineProps<{ + reaction: string; + users: any[]; // TODO + count: number; + emojis: any[]; // TODO + source: any; // TODO +}>(); + +const emit = defineEmits<{ + (e: 'closed'): void; +}>(); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/reactions-viewer.vue b/packages/client/src/components/reactions-viewer.vue index 59fcbb7129..a9bf51f65f 100644 --- a/packages/client/src/components/reactions-viewer.vue +++ b/packages/client/src/components/reactions-viewer.vue @@ -4,31 +4,19 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; +import * as misskey from 'misskey-js'; +import { $i } from '@/account'; import XReaction from './reactions-viewer.reaction.vue'; -export default defineComponent({ - components: { - XReaction - }, - props: { - note: { - type: Object, - required: true - }, - }, - data() { - return { - initialReactions: new Set(Object.keys(this.note.reactions)) - }; - }, - computed: { - isMe(): boolean { - return this.$i && this.$i.id === this.note.userId; - }, - }, -}); +const props = defineProps<{ + note: misskey.entities.Note; +}>(); + +const initialReactions = new Set(Object.keys(props.note.reactions)); + +const isMe = computed(() => $i && $i.id === props.note.userId); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/renote.details.vue b/packages/client/src/components/renote.details.vue index e3ef15c753..cdbc71bdce 100644 --- a/packages/client/src/components/renote.details.vue +++ b/packages/client/src/components/renote.details.vue @@ -1,5 +1,5 @@ <template> -<MkTooltip ref="tooltip" :source="source" :max-width="250" @closed="$emit('closed')"> +<MkTooltip ref="tooltip" :source="source" :max-width="250" @closed="emit('closed')"> <div class="beaffaef"> <div v-for="u in users" :key="u.id" class="user"> <MkAvatar class="avatar" :user="u"/> @@ -10,29 +10,19 @@ </MkTooltip> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkTooltip from './ui/tooltip.vue'; -export default defineComponent({ - components: { - MkTooltip, - }, - props: { - users: { - type: Array, - required: true, - }, - count: { - type: Number, - required: true, - }, - source: { - required: true, - } - }, - emits: ['closed'], -}) +const props = defineProps<{ + users: any[]; // TODO + count: number; + source: any; // TODO +}>(); + +const emit = defineEmits<{ + (e: 'closed'): void; +}>(); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/ripple.vue b/packages/client/src/components/ripple.vue index 272eacbc6e..401e78e304 100644 --- a/packages/client/src/components/ripple.vue +++ b/packages/client/src/components/ripple.vue @@ -94,7 +94,7 @@ export default defineComponent({ } onMounted(() => { - setTimeout(() => { + window.setTimeout(() => { context.emit('end'); }, 1100); }); diff --git a/packages/client/src/components/signin-dialog.vue b/packages/client/src/components/signin-dialog.vue index 2edd10f539..5c2048e7b0 100644 --- a/packages/client/src/components/signin-dialog.vue +++ b/packages/client/src/components/signin-dialog.vue @@ -2,8 +2,8 @@ <XModalWindow ref="dialog" :width="370" :height="400" - @close="$refs.dialog.close()" - @closed="$emit('closed')" + @close="dialog.close()" + @closed="emit('closed')" > <template #header>{{ $ts.login }}</template> @@ -11,32 +11,26 @@ </XModalWindow> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import XModalWindow from '@/components/ui/modal-window.vue'; import MkSignin from './signin.vue'; -export default defineComponent({ - components: { - MkSignin, - XModalWindow, - }, +const props = withDefaults(defineProps<{ + autoSet?: boolean; +}>(), { + autoSet: false, +}); - props: { - autoSet: { - type: Boolean, - required: false, - default: false, - } - }, +const emit = defineEmits<{ + (e: 'done'): void; + (e: 'closed'): void; +}>(); - emits: ['done', 'closed'], +const dialog = $ref<InstanceType<typeof XModalWindow>>(); - methods: { - onLogin(res) { - this.$emit('done', res); - this.$refs.dialog.close(); - } - } -}); +function onLogin(res) { + emit('done', res); + dialog.close(); +} </script> diff --git a/packages/client/src/components/signup-dialog.vue b/packages/client/src/components/signup-dialog.vue index 30fe3bf7d3..bda2495ba7 100644 --- a/packages/client/src/components/signup-dialog.vue +++ b/packages/client/src/components/signup-dialog.vue @@ -2,7 +2,7 @@ <XModalWindow ref="dialog" :width="366" :height="500" - @close="$refs.dialog.close()" + @close="dialog.close()" @closed="$emit('closed')" > <template #header>{{ $ts.signup }}</template> @@ -15,36 +15,30 @@ </XModalWindow> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import XModalWindow from '@/components/ui/modal-window.vue'; import XSignup from './signup.vue'; -export default defineComponent({ - components: { - XSignup, - XModalWindow, - }, +const props = withDefaults(defineProps<{ + autoSet?: boolean; +}>(), { + autoSet: false, +}); - props: { - autoSet: { - type: Boolean, - required: false, - default: false, - } - }, +const emit = defineEmits<{ + (e: 'done'): void; + (e: 'closed'): void; +}>(); - emits: ['done', 'closed'], +const dialog = $ref<InstanceType<typeof XModalWindow>>(); - methods: { - onSignup(res) { - this.$emit('done', res); - this.$refs.dialog.close(); - }, +function onSignup(res) { + emit('done', res); + dialog.close(); +} - onSignupEmailPending() { - this.$refs.dialog.close(); - } - } -}); +function onSignupEmailPending() { + dialog.close(); +} </script> diff --git a/packages/client/src/components/sub-note-content.vue b/packages/client/src/components/sub-note-content.vue index efa202ce2f..d6a37d07be 100644 --- a/packages/client/src/components/sub-note-content.vue +++ b/packages/client/src/components/sub-note-content.vue @@ -21,35 +21,21 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import XPoll from './poll.vue'; import XMediaList from './media-list.vue'; -import * as os from '@/os'; +import * as misskey from 'misskey-js'; -export default defineComponent({ - components: { - XPoll, - XMediaList, - }, - props: { - note: { - type: Object, - required: true - } - }, - data() { - return { - collapsed: false, - }; - }, - created() { - this.collapsed = this.note.cw == null && this.note.text && ( - (this.note.text.split('\n').length > 9) || - (this.note.text.length > 500) - ); - } -}); +const props = defineProps<{ + note: misskey.entities.Note; +}>(); + +const collapsed = $ref( + props.note.cw == null && props.note.text != null && ( + (props.note.text.split('\n').length > 9) || + (props.note.text.length > 500) + )); </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/toast.vue b/packages/client/src/components/toast.vue index 869182d8e1..031aa45633 100644 --- a/packages/client/src/components/toast.vue +++ b/packages/client/src/components/toast.vue @@ -26,7 +26,7 @@ const showing = ref(true); const zIndex = os.claimZIndex('high'); onMounted(() => { - setTimeout(() => { + window.setTimeout(() => { showing.value = false; }, 4000); }); diff --git a/packages/client/src/components/ui/button.vue b/packages/client/src/components/ui/button.vue index 804a2e2720..c7b6c8ba96 100644 --- a/packages/client/src/components/ui/button.vue +++ b/packages/client/src/components/ui/button.vue @@ -117,14 +117,14 @@ export default defineComponent({ const scale = calcCircleScale(e.target.clientWidth, e.target.clientHeight, circleCenterX, circleCenterY); - setTimeout(() => { + window.setTimeout(() => { ripple.style.transform = 'scale(' + (scale / 2) + ')'; }, 1); - setTimeout(() => { + window.setTimeout(() => { ripple.style.transition = 'all 1s ease'; ripple.style.opacity = '0'; }, 1000); - setTimeout(() => { + window.setTimeout(() => { if (this.$refs.ripples) this.$refs.ripples.removeChild(ripple); }, 2000); } diff --git a/packages/client/src/components/ui/modal.vue b/packages/client/src/components/ui/modal.vue index 3e2e59b27c..c691c8c6d0 100644 --- a/packages/client/src/components/ui/modal.vue +++ b/packages/client/src/components/ui/modal.vue @@ -211,7 +211,7 @@ export default defineComponent({ contentClicking = true; window.addEventListener('mouseup', e => { // click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ - setTimeout(() => { + window.setTimeout(() => { contentClicking = false; }, 100); }, { passive: true, once: true }); diff --git a/packages/client/src/components/ui/pagination.vue b/packages/client/src/components/ui/pagination.vue index d4451e27cb..571ef71eab 100644 --- a/packages/client/src/components/ui/pagination.vue +++ b/packages/client/src/components/ui/pagination.vue @@ -90,7 +90,6 @@ const init = async (): Promise<void> => { }).then(res => { for (let i = 0; i < res.length; i++) { const item = res[i]; - markRaw(item); if (props.pagination.reversed) { if (i === res.length - 2) item._shouldInsertAd_ = true; } else { @@ -134,7 +133,6 @@ const fetchMore = async (): Promise<void> => { }).then(res => { for (let i = 0; i < res.length; i++) { const item = res[i]; - markRaw(item); if (props.pagination.reversed) { if (i === res.length - 9) item._shouldInsertAd_ = true; } else { @@ -169,9 +167,6 @@ const fetchMoreAhead = async (): Promise<void> => { sinceId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id, }), }).then(res => { - for (const item of res) { - markRaw(item); - } if (res.length > SECOND_FETCH_LIMIT) { res.pop(); items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res); diff --git a/packages/client/src/components/url-preview.vue b/packages/client/src/components/url-preview.vue index dff74800ed..bf3b358797 100644 --- a/packages/client/src/components/url-preview.vue +++ b/packages/client/src/components/url-preview.vue @@ -4,7 +4,7 @@ <iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen /> </div> <div v-else-if="tweetId && tweetExpanded" ref="twitter" class="twitter"> - <iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', left: `${tweetLeft}px`, width: `${tweetLeft < 0 ? 'auto' : '100%'}`, height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${$store.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe> + <iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${$store.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe> </div> <div v-else v-size="{ max: [400, 350] }" class="mk-url-preview"> <transition name="zoom" mode="out-in"> @@ -32,110 +32,80 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { onMounted, onUnmounted } from 'vue'; import { url as local, lang } from '@/config'; -import * as os from '@/os'; -export default defineComponent({ - props: { - url: { - type: String, - require: true - }, - - detail: { - type: Boolean, - required: false, - default: false - }, - - compact: { - type: Boolean, - required: false, - default: false - }, - }, - - data() { - const self = this.url.startsWith(local); - return { - local, - fetching: true, - title: null, - description: null, - thumbnail: null, - icon: null, - sitename: null, - player: { - url: null, - width: null, - height: null - }, - tweetId: null, - tweetExpanded: this.detail, - embedId: `embed${Math.random().toString().replace(/\D/,'')}`, - tweetHeight: 150, - tweetLeft: 0, - playerEnabled: false, - self: self, - attr: self ? 'to' : 'href', - target: self ? null : '_blank', - }; - }, +const props = withDefaults(defineProps<{ + url: string; + detail?: boolean; + compact?: boolean; +}>(), { + detail: false, + compact: false, +}); - created() { - const requestUrl = new URL(this.url); +const self = props.url.startsWith(local); +const attr = self ? 'to' : 'href'; +const target = self ? null : '_blank'; +let fetching = $ref(true); +let title = $ref<string | null>(null); +let description = $ref<string | null>(null); +let thumbnail = $ref<string | null>(null); +let icon = $ref<string | null>(null); +let sitename = $ref<string | null>(null); +let player = $ref({ + url: null, + width: null, + height: null +}); +let playerEnabled = $ref(false); +let tweetId = $ref<string | null>(null); +let tweetExpanded = $ref(props.detail); +const embedId = `embed${Math.random().toString().replace(/\D/,'')}`; +let tweetHeight = $ref(150); - if (requestUrl.hostname == 'twitter.com') { - const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/); - if (m) this.tweetId = m[1]; - } +const requestUrl = new URL(props.url); - if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/(?:watch|channel)')) { - requestUrl.hostname = 'www.youtube.com'; - } +if (requestUrl.hostname == 'twitter.com') { + const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/); + if (m) tweetId = m[1]; +} - const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP'); +if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/(?:watch|channel)')) { + requestUrl.hostname = 'www.youtube.com'; +} - requestUrl.hash = ''; +const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP'); - fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => { - res.json().then(info => { - if (info.url == null) return; - this.title = info.title; - this.description = info.description; - this.thumbnail = info.thumbnail; - this.icon = info.icon; - this.sitename = info.sitename; - this.fetching = false; - this.player = info.player; - }) - }); +requestUrl.hash = ''; - (window as any).addEventListener('message', this.adjustTweetHeight); - }, +fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => { + res.json().then(info => { + if (info.url == null) return; + title = info.title; + description = info.description; + thumbnail = info.thumbnail; + icon = info.icon; + sitename = info.sitename; + fetching = false; + player = info.player; + }) +}); - mounted() { - // 300pxないと絶対右にはみ出るので左に移動してしまう - const areaWidth = (this.$el as any)?.clientWidth; - if (areaWidth && areaWidth < 300) this.tweetLeft = areaWidth - 241; - }, +function adjustTweetHeight(message: any) { + if (message.origin !== 'https://platform.twitter.com') return; + const embed = message.data?.['twttr.embed']; + if (embed?.method !== 'twttr.private.resize') return; + if (embed?.id !== embedId) return; + const height = embed?.params[0]?.height; + if (height) tweetHeight = height; +} - beforeUnmount() { - (window as any).removeEventListener('message', this.adjustTweetHeight); - }, +(window as any).addEventListener('message', adjustTweetHeight); - methods: { - adjustTweetHeight(message: any) { - if (message.origin !== 'https://platform.twitter.com') return; - const embed = message.data?.['twttr.embed']; - if (embed?.method !== 'twttr.private.resize') return; - if (embed?.id !== this.embedId) return; - const height = embed?.params[0]?.height; - if (height) this.tweetHeight = height; - }, - }, +onUnmounted(() => { + (window as any).removeEventListener('message', adjustTweetHeight); }); </script> diff --git a/packages/client/src/components/user-online-indicator.vue b/packages/client/src/components/user-online-indicator.vue index 93e9dea57b..a87b0aeff5 100644 --- a/packages/client/src/components/user-online-indicator.vue +++ b/packages/client/src/components/user-online-indicator.vue @@ -2,26 +2,21 @@ <div v-tooltip="text" class="fzgwjkgc" :class="user.onlineStatus"></div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; +import * as misskey from 'misskey-js'; +import { i18n } from '@/i18n'; -export default defineComponent({ - props: { - user: { - type: Object, - required: true - }, - }, +const props = defineProps<{ + user: misskey.entities.User; +}>(); - computed: { - text(): string { - switch (this.user.onlineStatus) { - case 'online': return this.$ts.online; - case 'active': return this.$ts.active; - case 'offline': return this.$ts.offline; - case 'unknown': return this.$ts.unknown; - } - } +const text = $computed(() => { + switch (props.user.onlineStatus) { + case 'online': return i18n.locale.online; + case 'active': return i18n.locale.active; + case 'offline': return i18n.locale.offline; + case 'unknown': return i18n.locale.unknown; } }); </script> diff --git a/packages/client/src/components/visibility-picker.vue b/packages/client/src/components/visibility-picker.vue index 4200f4354e..4b20063a51 100644 --- a/packages/client/src/components/visibility-picker.vue +++ b/packages/client/src/components/visibility-picker.vue @@ -1,28 +1,28 @@ <template> -<MkModal ref="modal" :z-priority="'high'" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')"> +<MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')"> <div class="gqyayizv _popup"> - <button key="public" class="_button" :class="{ active: v == 'public' }" data-index="1" @click="choose('public')"> + <button key="public" class="_button" :class="{ active: v === 'public' }" data-index="1" @click="choose('public')"> <div><i class="fas fa-globe"></i></div> <div> <span>{{ $ts._visibility.public }}</span> <span>{{ $ts._visibility.publicDescription }}</span> </div> </button> - <button key="home" class="_button" :class="{ active: v == 'home' }" data-index="2" @click="choose('home')"> + <button key="home" class="_button" :class="{ active: v === 'home' }" data-index="2" @click="choose('home')"> <div><i class="fas fa-home"></i></div> <div> <span>{{ $ts._visibility.home }}</span> <span>{{ $ts._visibility.homeDescription }}</span> </div> </button> - <button key="followers" class="_button" :class="{ active: v == 'followers' }" data-index="3" @click="choose('followers')"> + <button key="followers" class="_button" :class="{ active: v === 'followers' }" data-index="3" @click="choose('followers')"> <div><i class="fas fa-unlock"></i></div> <div> <span>{{ $ts._visibility.followers }}</span> <span>{{ $ts._visibility.followersDescription }}</span> </div> </button> - <button key="specified" :disabled="localOnly" class="_button" :class="{ active: v == 'specified' }" data-index="4" @click="choose('specified')"> + <button key="specified" :disabled="localOnly" class="_button" :class="{ active: v === 'specified' }" data-index="4" @click="choose('specified')"> <div><i class="fas fa-envelope"></i></div> <div> <span>{{ $ts._visibility.specified }}</span> @@ -42,49 +42,40 @@ </MkModal> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { nextTick, watch } from 'vue'; +import * as misskey from 'misskey-js'; import MkModal from '@/components/ui/modal.vue'; -export default defineComponent({ - components: { - MkModal, - }, - props: { - currentVisibility: { - type: String, - required: true - }, - currentLocalOnly: { - type: Boolean, - required: true - }, - src: { - required: false - }, - }, - emits: ['change-visibility', 'change-local-only', 'closed'], - data() { - return { - v: this.currentVisibility, - localOnly: this.currentLocalOnly, - } - }, - watch: { - localOnly() { - this.$emit('change-local-only', this.localOnly); - } - }, - methods: { - choose(visibility) { - this.v = visibility; - this.$emit('change-visibility', visibility); - this.$nextTick(() => { - this.$refs.modal.close(); - }); - }, - } +const modal = $ref<InstanceType<typeof MkModal>>(); + +const props = withDefaults(defineProps<{ + currentVisibility: typeof misskey.noteVisibilities[number]; + currentLocalOnly: boolean; + src?: HTMLElement; +}>(), { +}); + +const emit = defineEmits<{ + (e: 'changeVisibility', v: typeof misskey.noteVisibilities[number]): void; + (e: 'changeLocalOnly', v: boolean): void; + (e: 'closed'): void; +}>(); + +let v = $ref(props.currentVisibility); +let localOnly = $ref(props.currentLocalOnly); + +watch($$(localOnly), () => { + emit('changeLocalOnly', localOnly); }); + +function choose(visibility: typeof misskey.noteVisibilities[number]): void { + v = visibility; + emit('changeVisibility', visibility); + nextTick(() => { + modal.close(); + }); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/components/waiting-dialog.vue b/packages/client/src/components/waiting-dialog.vue index 10aedbd8f6..7dfcc55695 100644 --- a/packages/client/src/components/waiting-dialog.vue +++ b/packages/client/src/components/waiting-dialog.vue @@ -1,5 +1,5 @@ <template> -<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="success ? done() : () => {}" @closed="$emit('closed')"> +<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')"> <div class="iuyakobc" :class="{ iconOnly: (text == null) || success }"> <i v-if="success" class="fas fa-check icon success"></i> <i v-else class="fas fa-spinner fa-pulse icon waiting"></i> @@ -8,49 +8,30 @@ </MkModal> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { watch, ref } from 'vue'; import MkModal from '@/components/ui/modal.vue'; -export default defineComponent({ - components: { - MkModal, - }, +const modal = ref<InstanceType<typeof MkModal>>(); - props: { - success: { - type: Boolean, - required: true, - }, - showing: { - type: Boolean, - required: true, - }, - text: { - type: String, - required: false, - }, - }, +const props = defineProps<{ + success: boolean; + showing: boolean; + text?: string; +}>(); - emits: ['done', 'closed'], +const emit = defineEmits<{ + (e: 'done'); + (e: 'closed'); +}>(); - data() { - return { - }; - }, - - watch: { - showing() { - if (!this.showing) this.done(); - } - }, +function done() { + emit('done'); + modal.value.close(); +} - methods: { - done() { - this.$emit('done'); - this.$refs.modal.close(); - }, - } +watch(() => props.showing, () => { + if (!props.showing) done(); }); </script> diff --git a/packages/client/src/directives/anim.ts b/packages/client/src/directives/anim.ts index 1ceef984d8..04e1c6a404 100644 --- a/packages/client/src/directives/anim.ts +++ b/packages/client/src/directives/anim.ts @@ -10,7 +10,7 @@ export default { }, mounted(src, binding, vn) { - setTimeout(() => { + window.setTimeout(() => { src.style.opacity = '1'; src.style.transform = 'none'; }, 1); diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts index e14ee81dff..fffde14874 100644 --- a/packages/client/src/directives/tooltip.ts +++ b/packages/client/src/directives/tooltip.ts @@ -21,7 +21,7 @@ export default { self.close = () => { if (self._close) { - clearInterval(self.checkTimer); + window.clearInterval(self.checkTimer); self._close(); self._close = null; } @@ -61,19 +61,19 @@ export default { }); el.addEventListener(start, () => { - clearTimeout(self.showTimer); - clearTimeout(self.hideTimer); - self.showTimer = setTimeout(self.show, delay); + window.clearTimeout(self.showTimer); + window.clearTimeout(self.hideTimer); + self.showTimer = window.setTimeout(self.show, delay); }, { passive: true }); el.addEventListener(end, () => { - clearTimeout(self.showTimer); - clearTimeout(self.hideTimer); - self.hideTimer = setTimeout(self.close, delay); + window.clearTimeout(self.showTimer); + window.clearTimeout(self.hideTimer); + self.hideTimer = window.setTimeout(self.close, delay); }, { passive: true }); el.addEventListener('click', () => { - clearTimeout(self.showTimer); + window.clearTimeout(self.showTimer); self.close(); }); }, @@ -85,6 +85,6 @@ export default { unmounted(el, binding, vn) { const self = el._tooltipDirective_; - clearInterval(self.checkTimer); + window.clearInterval(self.checkTimer); }, } as Directive; diff --git a/packages/client/src/directives/user-preview.ts b/packages/client/src/directives/user-preview.ts index 68d9e2816c..cdd2afa194 100644 --- a/packages/client/src/directives/user-preview.ts +++ b/packages/client/src/directives/user-preview.ts @@ -30,11 +30,11 @@ export class UserPreview { source: this.el }, { mouseover: () => { - clearTimeout(this.hideTimer); + window.clearTimeout(this.hideTimer); }, mouseleave: () => { - clearTimeout(this.showTimer); - this.hideTimer = setTimeout(this.close, 500); + window.clearTimeout(this.showTimer); + this.hideTimer = window.setTimeout(this.close, 500); }, }, 'closed'); @@ -44,10 +44,10 @@ export class UserPreview { } }; - this.checkTimer = setInterval(() => { + this.checkTimer = window.setInterval(() => { if (!document.body.contains(this.el)) { - clearTimeout(this.showTimer); - clearTimeout(this.hideTimer); + window.clearTimeout(this.showTimer); + window.clearTimeout(this.hideTimer); this.close(); } }, 1000); @@ -56,7 +56,7 @@ export class UserPreview { @autobind private close() { if (this.promise) { - clearInterval(this.checkTimer); + window.clearInterval(this.checkTimer); this.promise.cancel(); this.promise = null; } @@ -64,21 +64,21 @@ export class UserPreview { @autobind private onMouseover() { - clearTimeout(this.showTimer); - clearTimeout(this.hideTimer); - this.showTimer = setTimeout(this.show, 500); + window.clearTimeout(this.showTimer); + window.clearTimeout(this.hideTimer); + this.showTimer = window.setTimeout(this.show, 500); } @autobind private onMouseleave() { - clearTimeout(this.showTimer); - clearTimeout(this.hideTimer); - this.hideTimer = setTimeout(this.close, 500); + window.clearTimeout(this.showTimer); + window.clearTimeout(this.hideTimer); + this.hideTimer = window.setTimeout(this.close, 500); } @autobind private onClick() { - clearTimeout(this.showTimer); + window.clearTimeout(this.showTimer); this.close(); } @@ -94,7 +94,7 @@ export class UserPreview { this.el.removeEventListener('mouseover', this.onMouseover); this.el.removeEventListener('mouseleave', this.onMouseleave); this.el.removeEventListener('click', this.onClick); - clearInterval(this.checkTimer); + window.clearInterval(this.checkTimer); } } diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index e6dd4567f7..dd7fdea4bd 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -83,7 +83,7 @@ export function promiseDialog<T extends Promise<any>>( onSuccess(res); } else { success.value = true; - setTimeout(() => { + window.setTimeout(() => { showing.value = false; }, 1000); } @@ -139,7 +139,7 @@ export async function popup(component: Component | typeof import('*.vue') | Prom const id = ++popupIdCount; const dispose = () => { // このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ? - setTimeout(() => { + window.setTimeout(() => { popups.value = popups.value.filter(popup => popup.id !== id); }, 0); }; @@ -329,7 +329,7 @@ export function select(props: { export function success() { return new Promise((resolve, reject) => { const showing = ref(true); - setTimeout(() => { + window.setTimeout(() => { showing.value = false; }, 1000); popup(import('@/components/waiting-dialog.vue'), { diff --git a/packages/client/src/pages/_error_.vue b/packages/client/src/pages/_error_.vue index 2f8f08b5cf..7540995707 100644 --- a/packages/client/src/pages/_error_.vue +++ b/packages/client/src/pages/_error_.vue @@ -1,68 +1,61 @@ <template> -<MkLoading v-if="!loaded" /> +<MkLoading v-if="!loaded"/> <transition :name="$store.state.animation ? 'zoom' : ''" appear> <div v-show="loaded" class="mjndxjch"> <img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> - <p><b><i class="fas fa-exclamation-triangle"></i> {{ $ts.pageLoadError }}</b></p> - <p v-if="version === meta.version">{{ $ts.pageLoadErrorDescription }}</p> - <p v-else-if="serverIsDead">{{ $ts.serverIsDead }}</p> + <p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.locale.pageLoadError }}</b></p> + <p v-if="meta && (version === meta.version)">{{ i18n.locale.pageLoadErrorDescription }}</p> + <p v-else-if="serverIsDead">{{ i18n.locale.serverIsDead }}</p> <template v-else> - <p>{{ $ts.newVersionOfClientAvailable }}</p> - <p>{{ $ts.youShouldUpgradeClient }}</p> - <MkButton class="button primary" @click="reload">{{ $ts.reload }}</MkButton> + <p>{{ i18n.locale.newVersionOfClientAvailable }}</p> + <p>{{ i18n.locale.youShouldUpgradeClient }}</p> + <MkButton class="button primary" @click="reload">{{ i18n.locale.reload }}</MkButton> </template> - <p><MkA to="/docs/general/troubleshooting" class="_link">{{ $ts.troubleshooting }}</MkA></p> + <p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.locale.troubleshooting }}</MkA></p> <p v-if="error" class="error">ERROR: {{ error }}</p> </div> </transition> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; +import * as misskey from 'misskey-js'; import MkButton from '@/components/ui/button.vue'; import * as symbols from '@/symbols'; import { version } from '@/config'; import * as os from '@/os'; import { unisonReload } from '@/scripts/unison-reload'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, - }, - props: { - error: { - required: false, - } - }, - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.error, - icon: 'fas fa-exclamation-triangle' - }, - loaded: false, - serverIsDead: false, - meta: {} as any, - version, - }; - }, - created() { - os.api('meta', { - detail: false - }).then(meta => { - this.loaded = true; - this.serverIsDead = false; - this.meta = meta; - localStorage.setItem('v', meta.version); - }, () => { - this.loaded = true; - this.serverIsDead = true; - }); - }, - methods: { - reload() { - unisonReload(); - }, +const props = withDefaults(defineProps<{ + error?: Error; +}>(), { +}); + +let loaded = $ref(false); +let serverIsDead = $ref(false); +let meta = $ref<misskey.entities.LiteInstanceMetadata | null>(null); + +os.api('meta', { + detail: false, +}).then(res => { + loaded = true; + serverIsDead = false; + meta = res; + localStorage.setItem('v', res.version); +}, () => { + loaded = true; + serverIsDead = true; +}); + +function reload() { + unisonReload(); +} + +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.error, + icon: 'fas fa-exclamation-triangle', }, }); </script> diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue index 04257f86ce..31cdef492a 100644 --- a/packages/client/src/pages/admin/abuses.vue +++ b/packages/client/src/pages/admin/abuses.vue @@ -95,7 +95,7 @@ export default defineComponent({ reporterOrigin: 'combined', targetUserOrigin: 'combined', pagination: { - endpoint: 'admin/abuse-user-reports', + endpoint: 'admin/abuse-user-reports' as const, limit: 10, params: computed(() => ({ state: this.state, @@ -106,10 +106,6 @@ export default defineComponent({ } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { acct, diff --git a/packages/client/src/pages/admin/ads.vue b/packages/client/src/pages/admin/ads.vue index 0396dae10c..8f164caa99 100644 --- a/packages/client/src/pages/admin/ads.vue +++ b/packages/client/src/pages/admin/ads.vue @@ -87,10 +87,6 @@ export default defineComponent({ }); }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { add() { this.ads.unshift({ diff --git a/packages/client/src/pages/admin/announcements.vue b/packages/client/src/pages/admin/announcements.vue index 3614cb1441..a0d720bb29 100644 --- a/packages/client/src/pages/admin/announcements.vue +++ b/packages/client/src/pages/admin/announcements.vue @@ -61,10 +61,6 @@ export default defineComponent({ }); }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { add() { this.announcements.unshift({ diff --git a/packages/client/src/pages/admin/bot-protection.vue b/packages/client/src/pages/admin/bot-protection.vue index 81b09fb4d9..82ab155317 100644 --- a/packages/client/src/pages/admin/bot-protection.vue +++ b/packages/client/src/pages/admin/bot-protection.vue @@ -82,10 +82,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/database.vue b/packages/client/src/pages/admin/database.vue index c1088afd77..3a835eeafa 100644 --- a/packages/client/src/pages/admin/database.vue +++ b/packages/client/src/pages/admin/database.vue @@ -37,10 +37,6 @@ export default defineComponent({ } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { bytes, number, } diff --git a/packages/client/src/pages/admin/email-settings.vue b/packages/client/src/pages/admin/email-settings.vue index 0799755a4d..6491a453ab 100644 --- a/packages/client/src/pages/admin/email-settings.vue +++ b/packages/client/src/pages/admin/email-settings.vue @@ -93,10 +93,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/emoji-edit-dialog.vue b/packages/client/src/pages/admin/emoji-edit-dialog.vue index a45d92fa16..2e3903426e 100644 --- a/packages/client/src/pages/admin/emoji-edit-dialog.vue +++ b/packages/client/src/pages/admin/emoji-edit-dialog.vue @@ -95,7 +95,7 @@ export default defineComponent({ }); if (canceled) return; - os.api('admin/emoji/remove', { + os.api('admin/emoji/delete', { id: this.emoji.id }).then(() => { this.$emit('done', { diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue index df5d234d6f..5b1dfe565a 100644 --- a/packages/client/src/pages/admin/emojis.vue +++ b/packages/client/src/pages/admin/emojis.vue @@ -6,11 +6,22 @@ <template #prefix><i class="fas fa-search"></i></template> <template #label>{{ $ts.search }}</template> </MkInput> - <MkPagination ref="emojis" :pagination="pagination"> + <MkSwitch v-model="selectMode" style="margin: 8px 0;"> + <template #label>Select mode</template> + </MkSwitch> + <div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> + <MkButton inline @click="selectAll">Select all</MkButton> + <MkButton inline @click="setCategoryBulk">Set category</MkButton> + <MkButton inline @click="addTagBulk">Add tag</MkButton> + <MkButton inline @click="removeTagBulk">Remove tag</MkButton> + <MkButton inline @click="setTagBulk">Set tag</MkButton> + <MkButton inline danger @click="delBulk">Delete</MkButton> + </div> + <MkPagination ref="emojisPaginationComponent" :pagination="pagination"> <template #empty><span>{{ $ts.noCustomEmojis }}</span></template> <template v-slot="{items}"> <div class="ldhfsamy"> - <button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="edit(emoji)"> + <button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)"> <img :src="emoji.url" class="img" :alt="emoji.name"/> <div class="body"> <div class="name _monospace">{{ emoji.name }}</div> @@ -32,7 +43,7 @@ <template #label>{{ $ts.host }}</template> </MkInput> </FormSplit> - <MkPagination ref="remoteEmojis" :pagination="remotePagination"> + <MkPagination :pagination="remotePagination"> <template #empty><span>{{ $ts.noCustomEmojis }}</span></template> <template v-slot="{items}"> <div class="ldhfsamy"> @@ -51,148 +62,233 @@ </MkSpacer> </template> -<script lang="ts"> -import { computed, defineComponent, toRef } from 'vue'; +<script lang="ts" setup> +import { computed, defineComponent, ref, toRef } from 'vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkPagination from '@/components/ui/pagination.vue'; import MkTab from '@/components/tab.vue'; +import MkSwitch from '@/components/form/switch.vue'; import FormSplit from '@/components/form/split.vue'; -import { selectFiles } from '@/scripts/select-file'; +import { selectFile, selectFiles } from '@/scripts/select-file'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkTab, - MkButton, - MkInput, - MkPagination, - FormSplit, - }, +const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>(); - emits: ['info'], +const tab = ref('local'); +const query = ref(null); +const queryRemote = ref(null); +const host = ref(null); +const selectMode = ref(false); +const selectedEmojis = ref<string[]>([]); - data() { - return { - [symbols.PAGE_INFO]: computed(() => ({ - title: this.$ts.customEmojis, - icon: 'fas fa-laugh', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-plus', - text: this.$ts.addEmoji, - handler: this.add, - }, { - icon: 'fas fa-ellipsis-h', - handler: this.menu, - }], - tabs: [{ - active: this.tab === 'local', - title: this.$ts.local, - onClick: () => { this.tab = 'local'; }, - }, { - active: this.tab === 'remote', - title: this.$ts.remote, - onClick: () => { this.tab = 'remote'; }, - },] - })), - tab: 'local', - query: null, - queryRemote: null, - host: '', - pagination: { - endpoint: 'admin/emoji/list', - limit: 30, - params: computed(() => ({ - query: (this.query && this.query !== '') ? this.query : null - })) - }, - remotePagination: { - endpoint: 'admin/emoji/list-remote', - limit: 30, - params: computed(() => ({ - query: (this.queryRemote && this.queryRemote !== '') ? this.queryRemote : null, - host: (this.host && this.host !== '') ? this.host : null - })) - }, - } - }, +const pagination = { + endpoint: 'admin/emoji/list' as const, + limit: 30, + params: computed(() => ({ + query: (query.value && query.value !== '') ? query.value : null, + })), +}; - async mounted() { - this.$emit('info', toRef(this, symbols.PAGE_INFO)); - }, +const remotePagination = { + endpoint: 'admin/emoji/list-remote' as const, + limit: 30, + params: computed(() => ({ + query: (queryRemote.value && queryRemote.value !== '') ? queryRemote.value : null, + host: (host.value && host.value !== '') ? host.value : null, + })), +}; - methods: { - async add(e) { - const files = await selectFiles(e.currentTarget || e.target, null); +const selectAll = () => { + if (selectedEmojis.value.length > 0) { + selectedEmojis.value = []; + } else { + selectedEmojis.value = emojisPaginationComponent.value.items.map(item => item.id); + } +}; - const promise = Promise.all(files.map(file => os.api('admin/emoji/add', { - fileId: file.id, - }))); - promise.then(() => { - this.$refs.emojis.reload(); - }); - os.promiseDialog(promise); - }, +const toggleSelect = (emoji) => { + if (selectedEmojis.value.includes(emoji.id)) { + selectedEmojis.value = selectedEmojis.value.filter(x => x !== emoji.id); + } else { + selectedEmojis.value.push(emoji.id); + } +}; - edit(emoji) { - os.popup(import('./emoji-edit-dialog.vue'), { - emoji: emoji - }, { - done: result => { - if (result.updated) { - this.$refs.emojis.replaceItem(item => item.id === emoji.id, { - ...emoji, - ...result.updated - }); - } else if (result.deleted) { - this.$refs.emojis.removeItem(item => item.id === emoji.id); - } - }, - }, 'closed'); - }, +const add = async (ev: MouseEvent) => { + const files = await selectFiles(ev.currentTarget || ev.target, null); - im(emoji) { - os.apiWithDialog('admin/emoji/copy', { - emojiId: emoji.id, - }); - }, + const promise = Promise.all(files.map(file => os.api('admin/emoji/add', { + fileId: file.id, + }))); + promise.then(() => { + emojisPaginationComponent.value.reload(); + }); + os.promiseDialog(promise); +}; - remoteMenu(emoji, ev) { - os.popupMenu([{ - type: 'label', - text: ':' + emoji.name + ':', - }, { - text: this.$ts.import, - icon: 'fas fa-plus', - action: () => { this.im(emoji) } - }], ev.currentTarget || ev.target); +const edit = (emoji) => { + os.popup(import('./emoji-edit-dialog.vue'), { + emoji: emoji + }, { + done: result => { + if (result.updated) { + emojisPaginationComponent.value.replaceItem(item => item.id === emoji.id, { + ...emoji, + ...result.updated + }); + } else if (result.deleted) { + emojisPaginationComponent.value.removeItem(item => item.id === emoji.id); + } }, + }, 'closed'); +}; - menu(ev) { - os.popupMenu([{ - icon: 'fas fa-download', - text: this.$ts.export, - action: async () => { - os.api('export-custom-emojis', { - }) - .then(() => { - os.alert({ - type: 'info', - text: this.$ts.exportRequested, - }); - }).catch((e) => { - os.alert({ - type: 'error', - text: e.message, - }); - }); - } - }], ev.currentTarget || ev.target); +const im = (emoji) => { + os.apiWithDialog('admin/emoji/copy', { + emojiId: emoji.id, + }); +}; + +const remoteMenu = (emoji, ev: MouseEvent) => { + os.popupMenu([{ + type: 'label', + text: ':' + emoji.name + ':', + }, { + text: i18n.locale.import, + icon: 'fas fa-plus', + action: () => { im(emoji) } + }], ev.currentTarget || ev.target); +}; + +const menu = (ev: MouseEvent) => { + os.popupMenu([{ + icon: 'fas fa-download', + text: i18n.locale.export, + action: async () => { + os.api('export-custom-emojis', { + }) + .then(() => { + os.alert({ + type: 'info', + text: i18n.locale.exportRequested, + }); + }).catch((e) => { + os.alert({ + type: 'error', + text: e.message, + }); + }); } - } + }, { + icon: 'fas fa-upload', + text: i18n.locale.import, + action: async () => { + const file = await selectFile(ev.currentTarget || ev.target); + os.api('admin/emoji/import-zip', { + fileId: file.id, + }) + .then(() => { + os.alert({ + type: 'info', + text: i18n.locale.importRequested, + }); + }).catch((e) => { + os.alert({ + type: 'error', + text: e.message, + }); + }); + } + }], ev.currentTarget || ev.target); +}; + +const setCategoryBulk = async () => { + const { canceled, result } = await os.inputText({ + title: 'Category', + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/set-category-bulk', { + ids: selectedEmojis.value, + category: result, + }); + emojisPaginationComponent.value.reload(); +}; + +const addTagBulk = async () => { + const { canceled, result } = await os.inputText({ + title: 'Tag', + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/add-aliases-bulk', { + ids: selectedEmojis.value, + aliases: result.split(' '), + }); + emojisPaginationComponent.value.reload(); +}; + +const removeTagBulk = async () => { + const { canceled, result } = await os.inputText({ + title: 'Tag', + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/remove-aliases-bulk', { + ids: selectedEmojis.value, + aliases: result.split(' '), + }); + emojisPaginationComponent.value.reload(); +}; + +const setTagBulk = async () => { + const { canceled, result } = await os.inputText({ + title: 'Tag', + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/set-aliases-bulk', { + ids: selectedEmojis.value, + aliases: result.split(' '), + }); + emojisPaginationComponent.value.reload(); +}; + +const delBulk = async () => { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.locale.deleteConfirm, + }); + if (canceled) return; + await os.apiWithDialog('admin/emoji/delete-bulk', { + ids: selectedEmojis.value, + }); + emojisPaginationComponent.value.reload(); +}; + +defineExpose({ + [symbols.PAGE_INFO]: computed(() => ({ + title: i18n.locale.customEmojis, + icon: 'fas fa-laugh', + bg: 'var(--bg)', + actions: [{ + asFullButton: true, + icon: 'fas fa-plus', + text: i18n.locale.addEmoji, + handler: add, + }, { + icon: 'fas fa-ellipsis-h', + handler: menu, + }], + tabs: [{ + active: tab.value === 'local', + title: i18n.locale.local, + onClick: () => { tab.value = 'local'; }, + }, { + active: tab.value === 'remote', + title: i18n.locale.remote, + onClick: () => { tab.value = 'remote'; }, + },] + })), }); </script> @@ -212,11 +308,16 @@ export default defineComponent({ > .emoji { display: flex; align-items: center; - padding: 12px; + padding: 11px; text-align: left; + border: solid 1px var(--panel); &:hover { - color: var(--accent); + border-color: var(--inputBorderHover); + } + + &.selected { + border-color: var(--accent); } > .img { diff --git a/packages/client/src/pages/admin/files.vue b/packages/client/src/pages/admin/files.vue index 6baa49f825..87dd12f489 100644 --- a/packages/client/src/pages/admin/files.vue +++ b/packages/client/src/pages/admin/files.vue @@ -95,7 +95,7 @@ export default defineComponent({ type: null, searchHost: '', pagination: { - endpoint: 'admin/drive/files', + endpoint: 'admin/drive/files' as const, limit: 10, params: computed(() => ({ type: (this.type && this.type !== '') ? this.type : null, @@ -106,10 +106,6 @@ export default defineComponent({ } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { clear() { os.confirm({ diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue index b7160de11d..350e7defc6 100644 --- a/packages/client/src/pages/admin/index.vue +++ b/packages/client/src/pages/admin/index.vue @@ -19,7 +19,7 @@ <div class="main"> <MkStickyContainer> <template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template> - <component :is="component" :key="page" v-bind="pageProps" @info="onInfo"/> + <component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/> </MkStickyContainer> </div> </div> @@ -66,7 +66,9 @@ export default defineComponent({ const narrow = ref(false); const view = ref(null); const el = ref(null); - const onInfo = (viewInfo) => { + const pageChanged = (page) => { + if (page == null) return; + const viewInfo = page[symbols.PAGE_INFO]; if (isRef(viewInfo)) { watch(viewInfo, () => { childInfo.value = viewInfo.value; @@ -311,7 +313,7 @@ export default defineComponent({ narrow, view, el, - onInfo, + pageChanged, childInfo, pageProps, component, diff --git a/packages/client/src/pages/admin/instance-block.vue b/packages/client/src/pages/admin/instance-block.vue index d1f7914ee4..6cadc7df39 100644 --- a/packages/client/src/pages/admin/instance-block.vue +++ b/packages/client/src/pages/admin/instance-block.vue @@ -40,10 +40,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/integrations.discord.vue b/packages/client/src/pages/admin/integrations.discord.vue index 8303afa3b0..8fc340150a 100644 --- a/packages/client/src/pages/admin/integrations.discord.vue +++ b/packages/client/src/pages/admin/integrations.discord.vue @@ -58,10 +58,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/integrations.github.vue b/packages/client/src/pages/admin/integrations.github.vue index c0316c317a..d9db9c00f1 100644 --- a/packages/client/src/pages/admin/integrations.github.vue +++ b/packages/client/src/pages/admin/integrations.github.vue @@ -58,10 +58,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/integrations.twitter.vue b/packages/client/src/pages/admin/integrations.twitter.vue index 5feabcc39d..1f8074535a 100644 --- a/packages/client/src/pages/admin/integrations.twitter.vue +++ b/packages/client/src/pages/admin/integrations.twitter.vue @@ -58,10 +58,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/integrations.vue b/packages/client/src/pages/admin/integrations.vue index 455fb6f4d6..91d03fef31 100644 --- a/packages/client/src/pages/admin/integrations.vue +++ b/packages/client/src/pages/admin/integrations.vue @@ -60,10 +60,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/object-storage.vue b/packages/client/src/pages/admin/object-storage.vue index 0f74865b10..6c5be220f8 100644 --- a/packages/client/src/pages/admin/object-storage.vue +++ b/packages/client/src/pages/admin/object-storage.vue @@ -118,10 +118,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/other-settings.vue b/packages/client/src/pages/admin/other-settings.vue index d21d0c5992..6b588e88aa 100644 --- a/packages/client/src/pages/admin/other-settings.vue +++ b/packages/client/src/pages/admin/other-settings.vue @@ -42,10 +42,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue index 564a63fda0..b8ae8ad9e1 100644 --- a/packages/client/src/pages/admin/overview.vue +++ b/packages/client/src/pages/admin/overview.vue @@ -116,8 +116,6 @@ export default defineComponent({ }, async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - os.api('meta', { detail: true }).then(meta => { this.meta = meta; }); diff --git a/packages/client/src/pages/admin/proxy-account.vue b/packages/client/src/pages/admin/proxy-account.vue index 9878df9122..5c4fbffa0c 100644 --- a/packages/client/src/pages/admin/proxy-account.vue +++ b/packages/client/src/pages/admin/proxy-account.vue @@ -44,10 +44,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/queue.vue b/packages/client/src/pages/admin/queue.vue index 719a3c2651..522210d933 100644 --- a/packages/client/src/pages/admin/queue.vue +++ b/packages/client/src/pages/admin/queue.vue @@ -38,8 +38,6 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - this.$nextTick(() => { this.connection.send('requestLog', { id: Math.random().toString().substr(2, 8), diff --git a/packages/client/src/pages/admin/relays.vue b/packages/client/src/pages/admin/relays.vue index 5ab02002b4..bb840db0a2 100644 --- a/packages/client/src/pages/admin/relays.vue +++ b/packages/client/src/pages/admin/relays.vue @@ -48,10 +48,6 @@ export default defineComponent({ this.refresh(); }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async addRelay() { const { canceled, result: inbox } = await os.inputText({ diff --git a/packages/client/src/pages/admin/security.vue b/packages/client/src/pages/admin/security.vue index 276c514f16..d069891647 100644 --- a/packages/client/src/pages/admin/security.vue +++ b/packages/client/src/pages/admin/security.vue @@ -70,10 +70,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue index 802d7463ec..a4bac93834 100644 --- a/packages/client/src/pages/admin/settings.vue +++ b/packages/client/src/pages/admin/settings.vue @@ -197,10 +197,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async init() { const meta = await os.api('meta', { detail: true }); diff --git a/packages/client/src/pages/admin/users.vue b/packages/client/src/pages/admin/users.vue index a094227ae9..03e155ddcf 100644 --- a/packages/client/src/pages/admin/users.vue +++ b/packages/client/src/pages/admin/users.vue @@ -110,7 +110,7 @@ export default defineComponent({ searchUsername: '', searchHost: '', pagination: { - endpoint: 'admin/show-users', + endpoint: 'admin/show-users' as const, limit: 10, params: computed(() => ({ sort: this.sort, @@ -124,10 +124,6 @@ export default defineComponent({ } }, - async mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { lookupUser, diff --git a/packages/client/src/pages/announcements.vue b/packages/client/src/pages/announcements.vue index ca94640dda..53727823a4 100644 --- a/packages/client/src/pages/announcements.vue +++ b/packages/client/src/pages/announcements.vue @@ -36,7 +36,7 @@ export default defineComponent({ bg: 'var(--bg)', }, pagination: { - endpoint: 'announcements', + endpoint: 'announcements' as const, limit: 10, }, }; diff --git a/packages/client/src/pages/channel.vue b/packages/client/src/pages/channel.vue index 598e173d81..c9a8f36844 100644 --- a/packages/client/src/pages/channel.vue +++ b/packages/client/src/pages/channel.vue @@ -67,7 +67,7 @@ export default defineComponent({ channel: null, showBanner: true, pagination: { - endpoint: 'channels/timeline', + endpoint: 'channels/timeline' as const, limit: 10, params: computed(() => ({ channelId: this.channelId, diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue index 48877ab3ec..4e538a6da3 100644 --- a/packages/client/src/pages/channels.vue +++ b/packages/client/src/pages/channels.vue @@ -60,15 +60,15 @@ export default defineComponent({ })), tab: 'featured', featuredPagination: { - endpoint: 'channels/featured', + endpoint: 'channels/featured' as const, noPaging: true, }, followingPagination: { - endpoint: 'channels/followed', + endpoint: 'channels/followed' as const, limit: 5, }, ownedPagination: { - endpoint: 'channels/owned', + endpoint: 'channels/owned' as const, limit: 5, }, }; diff --git a/packages/client/src/pages/clip.vue b/packages/client/src/pages/clip.vue index b375856803..6b49221d32 100644 --- a/packages/client/src/pages/clip.vue +++ b/packages/client/src/pages/clip.vue @@ -50,7 +50,7 @@ export default defineComponent({ } : null), clip: null, pagination: { - endpoint: 'clips/notes', + endpoint: 'clips/notes' as const, limit: 10, params: computed(() => ({ clipId: this.clipId, diff --git a/packages/client/src/pages/drive.vue b/packages/client/src/pages/drive.vue index f30000367f..1e17bea0cc 100644 --- a/packages/client/src/pages/drive.vue +++ b/packages/client/src/pages/drive.vue @@ -4,27 +4,21 @@ </div> </template> -<script lang="ts"> -import { computed, defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; import XDrive from '@/components/drive.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XDrive - }, +let folder = $ref(null); - data() { - return { - [symbols.PAGE_INFO]: { - title: computed(() => this.folder ? this.folder.name : this.$ts.drive), - icon: 'fas fa-cloud', - bg: 'var(--bg)', - hideHeader: true, - }, - folder: null, - }; - }, +defineExpose({ + [symbols.PAGE_INFO]: computed(() => ({ + title: folder ? folder.name : i18n.locale.drive, + icon: 'fas fa-cloud', + bg: 'var(--bg)', + hideHeader: true, + })), }); </script> diff --git a/packages/client/src/pages/emojis.emoji.vue b/packages/client/src/pages/emojis.emoji.vue index 5dab72daea..83539ce7a3 100644 --- a/packages/client/src/pages/emojis.emoji.vue +++ b/packages/client/src/pages/emojis.emoji.vue @@ -8,35 +8,29 @@ </button> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import * as os from '@/os'; import copyToClipboard from '@/scripts/copy-to-clipboard'; +import { i18n } from '@/i18n'; -export default defineComponent({ - props: { - emoji: { - type: Object, - required: true, - } - }, +const props = defineProps<{ + emoji: Record<string, unknown>; // TODO +}>(); - methods: { - menu(ev) { - os.popupMenu([{ - type: 'label', - text: ':' + this.emoji.name + ':', - }, { - text: this.$ts.copy, - icon: 'fas fa-copy', - action: () => { - copyToClipboard(`:${this.emoji.name}:`); - os.success(); - } - }], ev.currentTarget || ev.target); +function menu(ev) { + os.popupMenu([{ + type: 'label', + text: ':' + props.emoji.name + ':', + }, { + text: i18n.locale.copy, + icon: 'fas fa-copy', + action: () => { + copyToClipboard(`:${props.emoji.name}:`); + os.success(); } - } -}); + }], ev.currentTarget || ev.target); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/pages/emojis.vue b/packages/client/src/pages/emojis.vue index 2adb5345e2..6577f5abd9 100644 --- a/packages/client/src/pages/emojis.vue +++ b/packages/client/src/pages/emojis.vue @@ -4,55 +4,47 @@ </div> </template> -<script lang="ts"> -import { defineComponent, computed } from 'vue'; +<script lang="ts" setup> +import { ref, computed } from 'vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import XCategory from './emojis.category.vue'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XCategory, - }, +const tab = ref('category'); - data() { - return { - [symbols.PAGE_INFO]: computed(() => ({ - title: this.$ts.customEmojis, - icon: 'fas fa-laugh', - bg: 'var(--bg)', - actions: [{ - icon: 'fas fa-ellipsis-h', - handler: this.menu - }], - })), - tab: 'category', +function menu(ev) { + os.popupMenu([{ + icon: 'fas fa-download', + text: i18n.locale.export, + action: async () => { + os.api('export-custom-emojis', { + }) + .then(() => { + os.alert({ + type: 'info', + text: i18n.locale.exportRequested, + }); + }).catch((e) => { + os.alert({ + type: 'error', + text: e.message, + }); + }); } - }, + }], ev.currentTarget || ev.target); +} - methods: { - menu(ev) { - os.popupMenu([{ - icon: 'fas fa-download', - text: this.$ts.export, - action: async () => { - os.api('export-custom-emojis', { - }) - .then(() => { - os.alert({ - type: 'info', - text: this.$ts.exportRequested, - }); - }).catch((e) => { - os.alert({ - type: 'error', - text: e.message, - }); - }); - } - }], ev.currentTarget || ev.target); - } - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.customEmojis, + icon: 'fas fa-laugh', + bg: 'var(--bg)', + actions: [{ + icon: 'fas fa-ellipsis-h', + handler: menu, + }], + }, }); </script> diff --git a/packages/client/src/pages/explore.vue b/packages/client/src/pages/explore.vue index a3c3b771f2..04cc3662a7 100644 --- a/packages/client/src/pages/explore.vue +++ b/packages/client/src/pages/explore.vue @@ -156,7 +156,7 @@ export default defineComponent({ sort: '+createdAt', } }, searchPagination: { - endpoint: 'users/search', + endpoint: 'users/search' as const, limit: 10, params: computed(() => (this.searchQuery && this.searchQuery !== '') ? { query: this.searchQuery, @@ -178,7 +178,7 @@ export default defineComponent({ }, tagUsers(): any { return { - endpoint: 'hashtags/users', + endpoint: 'hashtags/users' as const, limit: 30, params: { tag: this.tag, diff --git a/packages/client/src/pages/favorites.vue b/packages/client/src/pages/favorites.vue index f530da2a19..8965b30d60 100644 --- a/packages/client/src/pages/favorites.vue +++ b/packages/client/src/pages/favorites.vue @@ -10,7 +10,7 @@ <template #default="{ items }"> <XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false"> - <XNote :key="item.id" :note="item.note" :class="$style.note" @update:note="noteUpdated(item, $event)"/> + <XNote :key="item.id" :note="item.note" :class="$style.note"/> </XList> </template> </MkPagination> @@ -32,13 +32,6 @@ const pagination = { const pagingComponent = ref<InstanceType<typeof MkPagination>>(); -const noteUpdated = (item, note) => { - pagingComponent.value?.updateItem(item.id, old => ({ - ...old, - note: note, - })); -}; - defineExpose({ [symbols.PAGE_INFO]: { title: i18n.locale.favorites, @@ -53,4 +46,4 @@ defineExpose({ background: var(--panel); border-radius: var(--radius); } -</style>
\ No newline at end of file +</style> diff --git a/packages/client/src/pages/featured.vue b/packages/client/src/pages/featured.vue index efa74ca599..725c70f0f7 100644 --- a/packages/client/src/pages/featured.vue +++ b/packages/client/src/pages/featured.vue @@ -10,7 +10,7 @@ import * as symbols from '@/symbols'; import { i18n } from '@/i18n'; const pagination = { - endpoint: 'notes/featured', + endpoint: 'notes/featured' as const, limit: 10, offsetMode: true, }; diff --git a/packages/client/src/pages/federation.vue b/packages/client/src/pages/federation.vue index 9815e68986..6a4a28b6b4 100644 --- a/packages/client/src/pages/federation.vue +++ b/packages/client/src/pages/federation.vue @@ -95,8 +95,8 @@ </MkSpacer> </template> -<script lang="ts"> -import { computed, defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkSelect from '@/components/form/select.vue'; @@ -104,68 +104,41 @@ import MkPagination from '@/components/ui/pagination.vue'; import FormSplit from '@/components/form/split.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, - MkInput, - MkSelect, - MkPagination, - FormSplit, - }, +let host = $ref(''); +let state = $ref('federating'); +let sort = $ref('+pubSub'); +const pagination = { + endpoint: 'federation/instances' as const, + limit: 10, + offsetMode: true, + params: computed(() => ({ + sort: sort, + host: host != '' ? host : null, + ...( + state === 'federating' ? { federating: true } : + state === 'subscribing' ? { subscribing: true } : + state === 'publishing' ? { publishing: true } : + state === 'suspended' ? { suspended: true } : + state === 'blocked' ? { blocked: true } : + state === 'notResponding' ? { notResponding: true } : + {}) + })) +}; - emits: ['info'], +function getStatus(instance) { + if (instance.isSuspended) return 'suspended'; + if (instance.isNotResponding) return 'error'; + return 'alive'; +}; - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.federation, - icon: 'fas fa-globe', - bg: 'var(--bg)', - }, - host: '', - state: 'federating', - sort: '+pubSub', - pagination: { - endpoint: 'federation/instances', - limit: 10, - offsetMode: true, - params: computed(() => ({ - sort: this.sort, - host: this.host != '' ? this.host : null, - ...( - this.state === 'federating' ? { federating: true } : - this.state === 'subscribing' ? { subscribing: true } : - this.state === 'publishing' ? { publishing: true } : - this.state === 'suspended' ? { suspended: true } : - this.state === 'blocked' ? { blocked: true } : - this.state === 'notResponding' ? { notResponding: true } : - {}) - })) - }, - } - }, - - watch: { - host() { - this.$refs.instances.reload(); - }, - state() { - this.$refs.instances.reload(); - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.federation, + icon: 'fas fa-globe', + bg: 'var(--bg)', }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - methods: { - getStatus(instance) { - if (instance.isSuspended) return 'suspended'; - if (instance.isNotResponding) return 'error'; - return 'alive'; - }, - } }); </script> diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue index 54d695091d..764daa0d3e 100644 --- a/packages/client/src/pages/follow-requests.vue +++ b/packages/client/src/pages/follow-requests.vue @@ -1,6 +1,6 @@ <template> <div> - <MkPagination ref="list" :pagination="pagination" class="mk-follow-requests"> + <MkPagination ref="paginationComponent" :pagination="pagination"> <template #empty> <div class="_fullinfo"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> @@ -8,19 +8,21 @@ </div> </template> <template v-slot="{items}"> - <div v-for="req in items" :key="req.id" class="user _panel"> - <MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/> - <div class="body"> - <div class="name"> - <MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA> - <p class="acct">@{{ acct(req.follower) }}</p> - </div> - <div v-if="req.follower.description" class="description" :title="req.follower.description"> - <Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/> - </div> - <div class="actions"> - <button class="_button" @click="accept(req.follower)"><i class="fas fa-check"></i></button> - <button class="_button" @click="reject(req.follower)"><i class="fas fa-times"></i></button> + <div class="mk-follow-requests"> + <div v-for="req in items" :key="req.id" class="user _panel"> + <MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/> + <div class="body"> + <div class="name"> + <MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA> + <p class="acct">@{{ acct(req.follower) }}</p> + </div> + <div v-if="req.follower.description" class="description" :title="req.follower.description"> + <Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/> + </div> + <div class="actions"> + <button class="_button" @click="accept(req.follower)"><i class="fas fa-check"></i></button> + <button class="_button" @click="reject(req.follower)"><i class="fas fa-times"></i></button> + </div> </div> </div> </div> @@ -29,45 +31,39 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { ref, computed } from 'vue'; import MkPagination from '@/components/ui/pagination.vue'; import { userPage, acct } from '@/filters/user'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkPagination - }, +const paginationComponent = ref<InstanceType<typeof MkPagination>>(); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.followRequests, - icon: 'fas fa-user-clock', - }, - pagination: { - endpoint: 'following/requests/list', - limit: 10, - }, - }; - }, +const pagination = { + endpoint: 'following/requests/list' as const, + limit: 10, +}; - methods: { - accept(user) { - os.api('following/requests/accept', { userId: user.id }).then(() => { - this.$refs.list.reload(); - }); - }, - reject(user) { - os.api('following/requests/reject', { userId: user.id }).then(() => { - this.$refs.list.reload(); - }); - }, - userPage, - acct - } +function accept(user) { + os.api('following/requests/accept', { userId: user.id }).then(() => { + paginationComponent.value.reload(); + }); +} + +function reject(user) { + os.api('following/requests/reject', { userId: user.id }).then(() => { + paginationComponent.value.reload(); + }); +} + +defineExpose({ + [symbols.PAGE_INFO]: computed(() => ({ + title: i18n.locale.followRequests, + icon: 'fas fa-user-clock', + bg: 'var(--bg)', + })), }); </script> diff --git a/packages/client/src/pages/gallery/index.vue b/packages/client/src/pages/gallery/index.vue index cd0d2a40e4..a19d69d5c2 100644 --- a/packages/client/src/pages/gallery/index.vue +++ b/packages/client/src/pages/gallery/index.vue @@ -81,19 +81,19 @@ export default defineComponent({ }, tab: 'explore', recentPostsPagination: { - endpoint: 'gallery/posts', + endpoint: 'gallery/posts' as const, limit: 6, }, popularPostsPagination: { - endpoint: 'gallery/featured', + endpoint: 'gallery/featured' as const, limit: 5, }, myPostsPagination: { - endpoint: 'i/gallery/posts', + endpoint: 'i/gallery/posts' as const, limit: 5, }, likedPostsPagination: { - endpoint: 'i/gallery/likes', + endpoint: 'i/gallery/likes' as const, limit: 5, }, tags: [], @@ -106,7 +106,7 @@ export default defineComponent({ }, tagUsers(): any { return { - endpoint: 'hashtags/users', + endpoint: 'hashtags/users' as const, limit: 30, params: { tag: this.tag, diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue index 9d769deca0..fff2b6a74e 100644 --- a/packages/client/src/pages/gallery/post.vue +++ b/packages/client/src/pages/gallery/post.vue @@ -93,7 +93,7 @@ export default defineComponent({ }] } : null), otherPostsPagination: { - endpoint: 'users/gallery/posts', + endpoint: 'users/gallery/posts' as const, limit: 6, params: computed(() => ({ userId: this.post.user.id diff --git a/packages/client/src/pages/mentions.vue b/packages/client/src/pages/mentions.vue index ea23c6a2f6..bda56fc729 100644 --- a/packages/client/src/pages/mentions.vue +++ b/packages/client/src/pages/mentions.vue @@ -10,7 +10,7 @@ import * as symbols from '@/symbols'; import { i18n } from '@/i18n'; const pagination = { - endpoint: 'notes/mentions', + endpoint: 'notes/mentions' as const, limit: 10, }; diff --git a/packages/client/src/pages/messages.vue b/packages/client/src/pages/messages.vue index 448aa0241f..8efdc55586 100644 --- a/packages/client/src/pages/messages.vue +++ b/packages/client/src/pages/messages.vue @@ -10,7 +10,7 @@ import * as symbols from '@/symbols'; import { i18n } from '@/i18n'; const pagination = { - endpoint: 'notes/mentions', + endpoint: 'notes/mentions' as const, limit: 10, params: () => ({ visibility: 'specified' diff --git a/packages/client/src/pages/messaging/messaging-room.vue b/packages/client/src/pages/messaging/messaging-room.vue index 1bcee01d29..9a34551ddd 100644 --- a/packages/client/src/pages/messaging/messaging-room.vue +++ b/packages/client/src/pages/messaging/messaging-room.vue @@ -162,7 +162,7 @@ const Component = defineComponent({ // もっと見るの交差検知を発火させないためにfetchは // スクロールが終わるまでfalseにしておく // scrollendのようなイベントはないのでsetTimeoutで - setTimeout(() => this.fetching = false, 300); + window.setTimeout(() => this.fetching = false, 300); }); }, @@ -300,9 +300,9 @@ const Component = defineComponent({ this.showIndicator = false; }); - if (this.timer) clearTimeout(this.timer); + if (this.timer) window.clearTimeout(this.timer); - this.timer = setTimeout(() => { + this.timer = window.setTimeout(() => { this.showIndicator = false; }, 4000); }, diff --git a/packages/client/src/pages/my-antennas/create.vue b/packages/client/src/pages/my-antennas/create.vue index 173807475a..427c9935c3 100644 --- a/packages/client/src/pages/my-antennas/create.vue +++ b/packages/client/src/pages/my-antennas/create.vue @@ -4,45 +4,37 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@/components/ui/button.vue'; +<script lang="ts" setup> +import { } from 'vue'; import XAntenna from './editor.vue'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; +import { router } from '@/router'; -export default defineComponent({ - components: { - MkButton, - XAntenna, - }, +let draft = $ref({ + name: '', + src: 'all', + userListId: null, + userGroupId: null, + users: [], + keywords: [], + excludeKeywords: [], + withReplies: false, + caseSensitive: false, + withFile: false, + notify: false +}); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.manageAntennas, - icon: 'fas fa-satellite', - }, - draft: { - name: '', - src: 'all', - userListId: null, - userGroupId: null, - users: [], - keywords: [], - excludeKeywords: [], - withReplies: false, - caseSensitive: false, - withFile: false, - notify: false - }, - }; - }, +function onAntennaCreated() { + router.push('/my/antennas'); +} - methods: { - onAntennaCreated() { - this.$router.push('/my/antennas'); - }, - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.manageAntennas, + icon: 'fas fa-satellite', + bg: 'var(--bg)', + }, }); </script> diff --git a/packages/client/src/pages/my-antennas/index.vue b/packages/client/src/pages/my-antennas/index.vue index d185e796c3..7138d269a9 100644 --- a/packages/client/src/pages/my-antennas/index.vue +++ b/packages/client/src/pages/my-antennas/index.vue @@ -38,7 +38,7 @@ export default defineComponent({ } }, pagination: { - endpoint: 'antennas/list', + endpoint: 'antennas/list' as const, limit: 10, }, }; diff --git a/packages/client/src/pages/my-clips/index.vue b/packages/client/src/pages/my-clips/index.vue index a5bbc3fd2d..97b563f6f8 100644 --- a/packages/client/src/pages/my-clips/index.vue +++ b/packages/client/src/pages/my-clips/index.vue @@ -3,7 +3,7 @@ <div class="qtcaoidl"> <MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> - <MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="list"> + <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="list"> <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap"> <b>{{ item.name }}</b> <div v-if="item.description" class="description">{{ item.description }}</div> @@ -13,71 +13,64 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkPagination from '@/components/ui/pagination.vue'; import MkButton from '@/components/ui/button.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import i18n from '@/components/global/i18n'; -export default defineComponent({ - components: { - MkPagination, - MkButton, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.clip, - icon: 'fas fa-paperclip', - bg: 'var(--bg)', - action: { - icon: 'fas fa-plus', - handler: this.create - } - }, - pagination: { - endpoint: 'clips/list', - limit: 10, - }, - draft: null, - }; - }, +const pagination = { + endpoint: 'clips/list' as const, + limit: 10, +}; - methods: { - async create() { - const { canceled, result } = await os.form(this.$ts.createNewClip, { - name: { - type: 'string', - label: this.$ts.name - }, - description: { - type: 'string', - required: false, - multiline: true, - label: this.$ts.description - }, - isPublic: { - type: 'boolean', - label: this.$ts.public, - default: false - } - }); - if (canceled) return; +const pagingComponent = $ref<InstanceType<typeof MkPagination>>(); - os.apiWithDialog('clips/create', result); +async function create() { + const { canceled, result } = await os.form(i18n.locale.createNewClip, { + name: { + type: 'string', + label: i18n.locale.name, }, - - onClipCreated() { - this.$refs.list.reload(); - this.draft = null; + description: { + type: 'string', + required: false, + multiline: true, + label: i18n.locale.description, }, + isPublic: { + type: 'boolean', + label: i18n.locale.public, + default: false, + }, + }); + if (canceled) return; + + os.apiWithDialog('clips/create', result); + + pagingComponent.reload(); +} + +function onClipCreated() { + pagingComponent.reload(); +} - onClipDeleted() { - this.$refs.list.reload(); +function onClipDeleted() { + pagingComponent.reload(); +} + +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.clip, + icon: 'fas fa-paperclip', + bg: 'var(--bg)', + action: { + icon: 'fas fa-plus', + handler: create }, - } + }, }); </script> diff --git a/packages/client/src/pages/my-groups/index.vue b/packages/client/src/pages/my-groups/index.vue index db5ccde466..4b2b2963a8 100644 --- a/packages/client/src/pages/my-groups/index.vue +++ b/packages/client/src/pages/my-groups/index.vue @@ -87,15 +87,15 @@ export default defineComponent({ })), tab: 'owned', ownedPagination: { - endpoint: 'users/groups/owned', + endpoint: 'users/groups/owned' as const, limit: 10, }, joinedPagination: { - endpoint: 'users/groups/joined', + endpoint: 'users/groups/joined' as const, limit: 10, }, invitationPagination: { - endpoint: 'i/user-group-invites', + endpoint: 'i/user-group-invites' as const, limit: 10, }, }; diff --git a/packages/client/src/pages/my-lists/index.vue b/packages/client/src/pages/my-lists/index.vue index 94a869b9ff..e6fcba1b34 100644 --- a/packages/client/src/pages/my-lists/index.vue +++ b/packages/client/src/pages/my-lists/index.vue @@ -3,7 +3,7 @@ <div class="qkcjvfiv"> <MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton> - <MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="lists _content"> + <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="lists _content"> <MkA v-for="list in items" :key="list.id" class="list _panel" :to="`/my/lists/${ list.id }`"> <div class="name">{{ list.name }}</div> <MkAvatars :user-ids="list.userIds"/> @@ -13,50 +13,41 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkPagination from '@/components/ui/pagination.vue'; import MkButton from '@/components/ui/button.vue'; import MkAvatars from '@/components/avatars.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkPagination, - MkButton, - MkAvatars, - }, +const pagingComponent = $ref<InstanceType<typeof MkPagination>>(); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.manageLists, - icon: 'fas fa-list-ul', - bg: 'var(--bg)', - action: { - icon: 'fas fa-plus', - handler: this.create - }, - }, - pagination: { - endpoint: 'users/lists/list', - limit: 10, - }, - }; - }, +const pagination = { + endpoint: 'users/lists/list' as const, + limit: 10, +}; + +async function create() { + const { canceled, result: name } = await os.inputText({ + title: i18n.locale.enterListName, + }); + if (canceled) return; + await os.apiWithDialog('users/lists/create', { name: name }); + pagingComponent.reload(); +} - methods: { - async create() { - const { canceled, result: name } = await os.inputText({ - title: this.$ts.enterListName, - }); - if (canceled) return; - await os.api('users/lists/create', { name: name }); - this.$refs.list.reload(); - os.success(); +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.manageLists, + icon: 'fas fa-list-ul', + bg: 'var(--bg)', + action: { + icon: 'fas fa-plus', + handler: create, }, - } + }, }); </script> diff --git a/packages/client/src/pages/note.vue b/packages/client/src/pages/note.vue index d40082381c..72ac85ee90 100644 --- a/packages/client/src/pages/note.vue +++ b/packages/client/src/pages/note.vue @@ -82,21 +82,21 @@ export default defineComponent({ showNext: false, error: null, prev: { - endpoint: 'users/notes', + endpoint: 'users/notes' as const, limit: 10, - params: init => ({ + params: computed(() => ({ userId: this.note.userId, untilId: this.note.id, - }) + })), }, next: { reversed: true, - endpoint: 'users/notes', + endpoint: 'users/notes' as const, limit: 10, - params: init => ({ + params: computed(() => ({ userId: this.note.userId, sinceId: this.note.id, - }) + })), }, }; }, diff --git a/packages/client/src/pages/notifications.vue b/packages/client/src/pages/notifications.vue index 695c54a535..090e80f99a 100644 --- a/packages/client/src/pages/notifications.vue +++ b/packages/client/src/pages/notifications.vue @@ -6,70 +6,62 @@ </MkSpacer> </template> -<script lang="ts"> -import { computed, defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; import XNotifications from '@/components/notifications.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { notificationTypes } from 'misskey-js'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XNotifications - }, +let tab = $ref('all'); +let includeTypes = $ref<string[] | null>(null); - data() { - return { - [symbols.PAGE_INFO]: computed(() => ({ - title: this.$ts.notifications, - icon: 'fas fa-bell', - bg: 'var(--bg)', - actions: [{ - text: this.$ts.filter, - icon: 'fas fa-filter', - highlighted: this.includeTypes != null, - handler: this.setFilter, - }, { - text: this.$ts.markAllAsRead, - icon: 'fas fa-check', - handler: () => { - os.apiWithDialog('notifications/mark-all-as-read'); - }, - }], - tabs: [{ - active: this.tab === 'all', - title: this.$ts.all, - onClick: () => { this.tab = 'all'; }, - }, { - active: this.tab === 'unread', - title: this.$ts.unread, - onClick: () => { this.tab = 'unread'; }, - },] - })), - tab: 'all', - includeTypes: null, - }; - }, - - methods: { - setFilter(ev) { - const typeItems = notificationTypes.map(t => ({ - text: this.$t(`_notification._types.${t}`), - active: this.includeTypes && this.includeTypes.includes(t), - action: () => { - this.includeTypes = [t]; - } - })); - const items = this.includeTypes != null ? [{ - icon: 'fas fa-times', - text: this.$ts.clear, - action: () => { - this.includeTypes = null; - } - }, null, ...typeItems] : typeItems; - os.popupMenu(items, ev.currentTarget || ev.target); +function setFilter(ev) { + const typeItems = notificationTypes.map(t => ({ + text: i18n.t(`_notification._types.${t}`), + active: includeTypes && includeTypes.includes(t), + action: () => { + includeTypes = [t]; + } + })); + const items = includeTypes != null ? [{ + icon: 'fas fa-times', + text: i18n.locale.clear, + action: () => { + includeTypes = null; } - } + }, null, ...typeItems] : typeItems; + os.popupMenu(items, ev.currentTarget || ev.target); +} + +defineExpose({ + [symbols.PAGE_INFO]: computed(() => ({ + title: i18n.locale.notifications, + icon: 'fas fa-bell', + bg: 'var(--bg)', + actions: [{ + text: i18n.locale.filter, + icon: 'fas fa-filter', + highlighted: includeTypes != null, + handler: setFilter, + }, { + text: i18n.locale.markAllAsRead, + icon: 'fas fa-check', + handler: () => { + os.apiWithDialog('notifications/mark-all-as-read'); + }, + }], + tabs: [{ + active: tab === 'all', + title: i18n.locale.all, + onClick: () => { tab = 'all'; }, + }, { + active: tab === 'unread', + title: i18n.locale.unread, + onClick: () => { tab = 'unread'; }, + },] + })), }); </script> diff --git a/packages/client/src/pages/page.vue b/packages/client/src/pages/page.vue index 5cb3948f1c..429d1ddea2 100644 --- a/packages/client/src/pages/page.vue +++ b/packages/client/src/pages/page.vue @@ -106,7 +106,7 @@ export default defineComponent({ page: null, error: null, otherPostsPagination: { - endpoint: 'users/pages', + endpoint: 'users/pages' as const, limit: 6, params: computed(() => ({ userId: this.page.user.id diff --git a/packages/client/src/pages/pages.vue b/packages/client/src/pages/pages.vue index f1dd64f119..dcccf7f7c4 100644 --- a/packages/client/src/pages/pages.vue +++ b/packages/client/src/pages/pages.vue @@ -62,15 +62,15 @@ export default defineComponent({ })), tab: 'featured', featuredPagesPagination: { - endpoint: 'pages/featured', + endpoint: 'pages/featured' as const, noPaging: true, }, myPagesPagination: { - endpoint: 'i/pages', + endpoint: 'i/pages' as const, limit: 5, }, likedPagesPagination: { - endpoint: 'i/page-likes', + endpoint: 'i/page-likes' as const, limit: 5, }, }; diff --git a/packages/client/src/pages/preview.vue b/packages/client/src/pages/preview.vue index 9d1ebb74ed..8eb4549516 100644 --- a/packages/client/src/pages/preview.vue +++ b/packages/client/src/pages/preview.vue @@ -4,24 +4,18 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; import MkSample from '@/components/sample.vue'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkSample, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.preview, - icon: 'fas fa-eye', - }, - } - }, +defineExpose({ + [symbols.PAGE_INFO]: computed(() => ({ + title: i18n.locale.preview, + icon: 'fas fa-eye', + bg: 'var(--bg)', + })), }); </script> diff --git a/packages/client/src/pages/reset-password.vue b/packages/client/src/pages/reset-password.vue index e0608654c7..8ef73858f6 100644 --- a/packages/client/src/pages/reset-password.vue +++ b/packages/client/src/pages/reset-password.vue @@ -3,62 +3,51 @@ <div class="_formRoot"> <FormInput v-model="password" type="password" class="_formBlock"> <template #prefix><i class="fas fa-lock"></i></template> - <template #label>{{ $ts.newPassword }}</template> + <template #label>{{ i18n.locale.newPassword }}</template> </FormInput> - <FormButton primary class="_formBlock" @click="save">{{ $ts.save }}</FormButton> + <FormButton primary class="_formBlock" @click="save">{{ i18n.locale.save }}</FormButton> </div> </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { onMounted } from 'vue'; import FormInput from '@/components/form/input.vue'; import FormButton from '@/components/ui/button.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; +import { router } from '@/router'; -export default defineComponent({ - components: { - FormInput, - FormButton, - }, - - props: { - token: { - type: String, - required: false - } - }, +const props = defineProps<{ + token?: string; +}>(); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.resetPassword, - icon: 'fas fa-lock', - bg: 'var(--bg)', - }, - password: '', - } - }, +let password = $ref(''); - mounted() { - if (this.token == null) { - os.popup(import('@/components/forgot-password.vue'), {}, {}, 'closed'); - this.$router.push('/'); - } - }, +async function save() { + await os.apiWithDialog('reset-password', { + token: props.token, + password: password, + }); + router.push('/'); +} - methods: { - async save() { - await os.apiWithDialog('reset-password', { - token: this.token, - password: this.password, - }); - this.$router.push('/'); - } +onMounted(() => { + if (props.token == null) { + os.popup(import('@/components/forgot-password.vue'), {}, {}, 'closed'); + router.push('/'); } }); + +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.resetPassword, + icon: 'fas fa-lock', + bg: 'var(--bg)', + }, +}); </script> <style lang="scss" scoped> diff --git a/packages/client/src/pages/search.vue b/packages/client/src/pages/search.vue index 771d0b557e..ce2b7035da 100644 --- a/packages/client/src/pages/search.vue +++ b/packages/client/src/pages/search.vue @@ -6,31 +6,31 @@ </div> </template> -<script lang="ts"> -import { computed, defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; import XNotes from '@/components/notes.vue'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XNotes - }, +const props = defineProps<{ + query: string; + channel?: string; +}>(); - data() { - return { - [symbols.PAGE_INFO]: { - title: computed(() => this.$t('searchWith', { q: this.$route.query.q })), - icon: 'fas fa-search', - }, - pagination: { - endpoint: 'notes/search', - limit: 10, - params: computed(() => ({ - query: this.$route.query.q, - channelId: this.$route.query.channel, - })) - }, - }; - }, +const pagination = { + endpoint: 'notes/search' as const, + limit: 10, + params: computed(() => ({ + query: props.query, + channelId: props.channel, + })) +}; + +defineExpose({ + [symbols.PAGE_INFO]: computed(() => ({ + title: i18n.t('searchWith', { q: props.query }), + icon: 'fas fa-search', + bg: 'var(--bg)', + })), }); </script> diff --git a/packages/client/src/pages/settings/account-info.vue b/packages/client/src/pages/settings/account-info.vue index ceff587302..c98ad056f6 100644 --- a/packages/client/src/pages/settings/account-info.vue +++ b/packages/client/src/pages/settings/account-info.vue @@ -154,8 +154,6 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - os.api('users/stats', { userId: this.$i.id }).then(stats => { diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue index 9ff11adda3..c795ede8ac 100644 --- a/packages/client/src/pages/settings/accounts.vue +++ b/packages/client/src/pages/settings/accounts.vue @@ -53,10 +53,6 @@ export default defineComponent({ }; }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { menu(account, ev) { os.popupMenu([{ diff --git a/packages/client/src/pages/settings/api.vue b/packages/client/src/pages/settings/api.vue index 1a51b526f2..20ff2a8d96 100644 --- a/packages/client/src/pages/settings/api.vue +++ b/packages/client/src/pages/settings/api.vue @@ -32,10 +32,6 @@ export default defineComponent({ }; }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { generateToken() { os.popup(import('@/components/token-generate-window.vue'), {}, { diff --git a/packages/client/src/pages/settings/apps.vue b/packages/client/src/pages/settings/apps.vue index 68952bbbdb..9c0fa8a54d 100644 --- a/packages/client/src/pages/settings/apps.vue +++ b/packages/client/src/pages/settings/apps.vue @@ -58,7 +58,7 @@ export default defineComponent({ bg: 'var(--bg)', }, pagination: { - endpoint: 'i/apps', + endpoint: 'i/apps' as const, limit: 100, params: { sort: '+lastUsedAt' @@ -67,10 +67,6 @@ export default defineComponent({ }; }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { revoke(token) { os.api('i/revoke-token', { tokenId: token.id }).then(() => { diff --git a/packages/client/src/pages/settings/custom-css.vue b/packages/client/src/pages/settings/custom-css.vue index 6dbb8c2ae9..556ee30c1d 100644 --- a/packages/client/src/pages/settings/custom-css.vue +++ b/packages/client/src/pages/settings/custom-css.vue @@ -37,8 +37,6 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - this.$watch('localCustomCss', this.apply); }, diff --git a/packages/client/src/pages/settings/deck.vue b/packages/client/src/pages/settings/deck.vue index e290b095ac..46b90d3d1a 100644 --- a/packages/client/src/pages/settings/deck.vue +++ b/packages/client/src/pages/settings/deck.vue @@ -83,10 +83,6 @@ export default defineComponent({ } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async setProfile() { const { canceled, result: name } = await os.inputText({ diff --git a/packages/client/src/pages/settings/delete-account.vue b/packages/client/src/pages/settings/delete-account.vue index 17501d9510..7edc81a309 100644 --- a/packages/client/src/pages/settings/delete-account.vue +++ b/packages/client/src/pages/settings/delete-account.vue @@ -33,10 +33,6 @@ export default defineComponent({ } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async deleteAccount() { { diff --git a/packages/client/src/pages/settings/drive.vue b/packages/client/src/pages/settings/drive.vue index c123159b61..f1016ebd84 100644 --- a/packages/client/src/pages/settings/drive.vue +++ b/packages/client/src/pages/settings/drive.vue @@ -99,10 +99,6 @@ export default defineComponent({ } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { chooseUploadFolder() { os.selectDriveFolder(false).then(async folder => { diff --git a/packages/client/src/pages/settings/email.vue b/packages/client/src/pages/settings/email.vue index e9010fbe42..54557f8773 100644 --- a/packages/client/src/pages/settings/email.vue +++ b/packages/client/src/pages/settings/email.vue @@ -111,8 +111,6 @@ export default defineComponent({ }); onMounted(() => { - context.emit('info', INFO); - watch(emailAddress, () => { saveEmailAddress(); }); diff --git a/packages/client/src/pages/settings/general.vue b/packages/client/src/pages/settings/general.vue index 734bc78442..2e159e56a9 100644 --- a/packages/client/src/pages/settings/general.vue +++ b/packages/client/src/pages/settings/general.vue @@ -195,10 +195,6 @@ export default defineComponent({ }, }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async reloadAsk() { const { canceled } = await os.confirm({ diff --git a/packages/client/src/pages/settings/import-export.vue b/packages/client/src/pages/settings/import-export.vue index a1dd6a1539..21031c559e 100644 --- a/packages/client/src/pages/settings/import-export.vue +++ b/packages/client/src/pages/settings/import-export.vue @@ -133,10 +133,6 @@ export default defineComponent({ os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); }; - onMounted(() => { - context.emit('info', INFO); - }); - return { [symbols.PAGE_INFO]: INFO, excludeMutingUsers, diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue index c9acf2c63c..66c8b147bb 100644 --- a/packages/client/src/pages/settings/index.vue +++ b/packages/client/src/pages/settings/index.vue @@ -14,7 +14,7 @@ </div> <div class="main"> <div class="bkzroven"> - <component :is="component" :key="page" v-bind="pageProps" @info="onInfo"/> + <component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/> </div> </div> </div> @@ -250,8 +250,9 @@ export default defineComponent({ const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified)); - const onInfo = (info) => { - childInfo.value = info; + const pageChanged = (page) => { + if (page == null) return; + childInfo.value = page[symbols.PAGE_INFO]; }; return { @@ -264,7 +265,7 @@ export default defineComponent({ pageProps, component, emailNotConfigured, - onInfo, + pageChanged, childInfo, }; }, diff --git a/packages/client/src/pages/settings/instance-mute.vue b/packages/client/src/pages/settings/instance-mute.vue index 584a21e4bd..f84a209b60 100644 --- a/packages/client/src/pages/settings/instance-mute.vue +++ b/packages/client/src/pages/settings/instance-mute.vue @@ -47,11 +47,6 @@ export default defineComponent({ }, }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - - async created() { this.instanceMutes = this.$i.mutedInstances.join('\n'); }, diff --git a/packages/client/src/pages/settings/integration.vue b/packages/client/src/pages/settings/integration.vue index e3dbc6fde9..ca36c91665 100644 --- a/packages/client/src/pages/settings/integration.vue +++ b/packages/client/src/pages/settings/integration.vue @@ -73,8 +73,6 @@ export default defineComponent({ }, mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - document.cookie = `igi=${this.$i.token}; path=/;` + ` max-age=31536000;` + (document.location.protocol.startsWith('https') ? ' secure' : ''); diff --git a/packages/client/src/pages/settings/menu.vue b/packages/client/src/pages/settings/menu.vue index 26404f3adf..6e38cd5dfe 100644 --- a/packages/client/src/pages/settings/menu.vue +++ b/packages/client/src/pages/settings/menu.vue @@ -67,10 +67,6 @@ export default defineComponent({ }, }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async addItem() { const menu = Object.keys(this.menuDef).filter(k => !this.$store.state.menu.includes(k)); diff --git a/packages/client/src/pages/settings/mute-block.vue b/packages/client/src/pages/settings/mute-block.vue index 6a63c9eb21..f4f9ebf8dd 100644 --- a/packages/client/src/pages/settings/mute-block.vue +++ b/packages/client/src/pages/settings/mute-block.vue @@ -27,8 +27,8 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import MkPagination from '@/components/ui/pagination.vue'; import MkTab from '@/components/tab.vue'; import FormInfo from '@/components/ui/info.vue'; @@ -36,42 +36,25 @@ import FormLink from '@/components/form/link.vue'; import { userPage } from '@/filters/user'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkPagination, - MkTab, - FormInfo, - FormLink, - }, +let tab = $ref('mute'); - emits: ['info'], +const mutingPagination = { + endpoint: 'mute/list' as const, + limit: 10, +}; - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.muteAndBlock, - icon: 'fas fa-ban', - bg: 'var(--bg)', - }, - tab: 'mute', - mutingPagination: { - endpoint: 'mute/list', - limit: 10, - }, - blockingPagination: { - endpoint: 'blocking/list', - limit: 10, - }, - } - }, +const blockingPagination = { + endpoint: 'blocking/list' as const, + limit: 10, +}; - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.muteAndBlock, + icon: 'fas fa-ban', + bg: 'var(--bg)', }, - - methods: { - userPage - } }); </script> diff --git a/packages/client/src/pages/settings/notifications.vue b/packages/client/src/pages/settings/notifications.vue index ab26d7d558..12171530bb 100644 --- a/packages/client/src/pages/settings/notifications.vue +++ b/packages/client/src/pages/settings/notifications.vue @@ -37,10 +37,6 @@ export default defineComponent({ } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { readAllUnreadNotes() { os.api('i/read-all-unread-notes'); diff --git a/packages/client/src/pages/settings/other.vue b/packages/client/src/pages/settings/other.vue index 7547013832..6e48cb58a6 100644 --- a/packages/client/src/pages/settings/other.vue +++ b/packages/client/src/pages/settings/other.vue @@ -47,10 +47,6 @@ export default defineComponent({ reportError: defaultStore.makeGetterSetter('reportError'), }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { changeDebug(v) { console.log(v); diff --git a/packages/client/src/pages/settings/plugin.install.vue b/packages/client/src/pages/settings/plugin.install.vue index bf494fa719..d35d20d17a 100644 --- a/packages/client/src/pages/settings/plugin.install.vue +++ b/packages/client/src/pages/settings/plugin.install.vue @@ -45,10 +45,6 @@ export default defineComponent({ } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { installPlugin({ id, meta, ast, token }) { ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({ diff --git a/packages/client/src/pages/settings/plugin.vue b/packages/client/src/pages/settings/plugin.vue index d411ad2961..7a3ab9d152 100644 --- a/packages/client/src/pages/settings/plugin.vue +++ b/packages/client/src/pages/settings/plugin.vue @@ -64,10 +64,6 @@ export default defineComponent({ } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { uninstall(plugin) { ColdDeviceStorage.set('plugins', this.plugins.filter(x => x.id !== plugin.id)); diff --git a/packages/client/src/pages/settings/privacy.vue b/packages/client/src/pages/settings/privacy.vue index 78a0ea8b8d..dd13ba4bd0 100644 --- a/packages/client/src/pages/settings/privacy.vue +++ b/packages/client/src/pages/settings/privacy.vue @@ -47,8 +47,8 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import FormSwitch from '@/components/form/switch.vue'; import FormSelect from '@/components/form/select.vue'; import FormSection from '@/components/form/section.vue'; @@ -56,67 +56,39 @@ import FormGroup from '@/components/form/group.vue'; import * as os from '@/os'; import { defaultStore } from '@/store'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; +import { $i } from '@/account'; -export default defineComponent({ - components: { - FormSelect, - FormSection, - FormGroup, - FormSwitch, - }, - - emits: ['info'], - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.privacy, - icon: 'fas fa-lock-open', - bg: 'var(--bg)', - }, - isLocked: false, - autoAcceptFollowed: false, - noCrawle: false, - isExplorable: false, - hideOnlineStatus: false, - publicReactions: false, - ffVisibility: 'public', - } - }, +let isLocked = $ref($i.isLocked); +let autoAcceptFollowed = $ref($i.autoAcceptFollowed); +let noCrawle = $ref($i.noCrawle); +let isExplorable = $ref($i.isExplorable); +let hideOnlineStatus = $ref($i.hideOnlineStatus); +let publicReactions = $ref($i.publicReactions); +let ffVisibility = $ref($i.ffVisibility); - computed: { - defaultNoteVisibility: defaultStore.makeGetterSetter('defaultNoteVisibility'), - defaultNoteLocalOnly: defaultStore.makeGetterSetter('defaultNoteLocalOnly'), - rememberNoteVisibility: defaultStore.makeGetterSetter('rememberNoteVisibility'), - keepCw: defaultStore.makeGetterSetter('keepCw'), - }, +let defaultNoteVisibility = $computed(defaultStore.makeGetterSetter('defaultNoteVisibility')); +let defaultNoteLocalOnly = $computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly')); +let rememberNoteVisibility = $computed(defaultStore.makeGetterSetter('rememberNoteVisibility')); +let keepCw = $computed(defaultStore.makeGetterSetter('keepCw')); - created() { - this.isLocked = this.$i.isLocked; - this.autoAcceptFollowed = this.$i.autoAcceptFollowed; - this.noCrawle = this.$i.noCrawle; - this.isExplorable = this.$i.isExplorable; - this.hideOnlineStatus = this.$i.hideOnlineStatus; - this.publicReactions = this.$i.publicReactions; - this.ffVisibility = this.$i.ffVisibility; - }, +function save() { + os.api('i/update', { + isLocked: !!isLocked, + autoAcceptFollowed: !!autoAcceptFollowed, + noCrawle: !!noCrawle, + isExplorable: !!isExplorable, + hideOnlineStatus: !!hideOnlineStatus, + publicReactions: !!publicReactions, + ffVisibility: ffVisibility, + }); +} - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.privacy, + icon: 'fas fa-lock-open', + bg: 'var(--bg)', }, - - methods: { - save() { - os.api('i/update', { - isLocked: !!this.isLocked, - autoAcceptFollowed: !!this.autoAcceptFollowed, - noCrawle: !!this.noCrawle, - isExplorable: !!this.isExplorable, - hideOnlineStatus: !!this.hideOnlineStatus, - publicReactions: !!this.publicReactions, - ffVisibility: this.ffVisibility, - }); - } - } }); </script> diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue index 2eaf9a9f83..9a2395872e 100644 --- a/packages/client/src/pages/settings/profile.vue +++ b/packages/client/src/pages/settings/profile.vue @@ -132,10 +132,6 @@ export default defineComponent({ this.$watch('alwaysMarkNsfw', this.save); }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { changeAvatar(e) { selectFile(e.currentTarget || e.target, this.$ts.avatar).then(file => { diff --git a/packages/client/src/pages/settings/reaction.vue b/packages/client/src/pages/settings/reaction.vue index 0d4db46936..e5b1189947 100644 --- a/packages/client/src/pages/settings/reaction.vue +++ b/packages/client/src/pages/settings/reaction.vue @@ -100,10 +100,6 @@ export default defineComponent({ } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { save() { this.$store.set('reactions', this.reactions); diff --git a/packages/client/src/pages/settings/security.vue b/packages/client/src/pages/settings/security.vue index 03f2d6300b..6fb3f1c413 100644 --- a/packages/client/src/pages/settings/security.vue +++ b/packages/client/src/pages/settings/security.vue @@ -66,16 +66,12 @@ export default defineComponent({ bg: 'var(--bg)', }, pagination: { - endpoint: 'i/signin-history', + endpoint: 'i/signin-history' as const, limit: 5, }, } }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async change() { const { canceled: canceled1, result: currentPassword } = await os.inputText({ diff --git a/packages/client/src/pages/settings/sounds.vue b/packages/client/src/pages/settings/sounds.vue index b538bf7cf5..490a1b5514 100644 --- a/packages/client/src/pages/settings/sounds.vue +++ b/packages/client/src/pages/settings/sounds.vue @@ -96,10 +96,6 @@ export default defineComponent({ this.sounds.channel = ColdDeviceStorage.get('sound_channel'); }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async edit(type) { const { canceled, result } = await os.form(this.$t('_sfx.' + type), { diff --git a/packages/client/src/pages/settings/theme.install.vue b/packages/client/src/pages/settings/theme.install.vue index 52935c75dc..e2a3f042b9 100644 --- a/packages/client/src/pages/settings/theme.install.vue +++ b/packages/client/src/pages/settings/theme.install.vue @@ -1,18 +1,18 @@ <template> <div class="_formRoot"> <FormTextarea v-model="installThemeCode" class="_formBlock"> - <template #label>{{ $ts._theme.code }}</template> + <template #label>{{ i18n.locale._theme.code }}</template> </FormTextarea> <div class="_formBlock" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> - <FormButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton> - <FormButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="fas fa-check"></i> {{ $ts.install }}</FormButton> + <FormButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="fas fa-eye"></i> {{ i18n.locale.preview }}</FormButton> + <FormButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="fas fa-check"></i> {{ i18n.locale.install }}</FormButton> </div> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import * as JSON5 from 'json5'; import FormTextarea from '@/components/form/textarea.vue'; import FormButton from '@/components/ui/button.vue'; @@ -20,75 +20,60 @@ import { applyTheme, validateTheme } from '@/scripts/theme'; import * as os from '@/os'; import { addTheme, getThemes } from '@/theme-store'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormTextarea, - FormButton, - }, - - emits: ['info'], +let installThemeCode = $ref(null); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts._theme.install, - icon: 'fas fa-download', - bg: 'var(--bg)', - }, - installThemeCode: null, - } - }, - - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, +function parseThemeCode(code: string) { + let theme; - methods: { - parseThemeCode(code) { - let theme; + try { + theme = JSON5.parse(code); + } catch (e) { + os.alert({ + type: 'error', + text: i18n.locale._theme.invalid + }); + return false; + } + if (!validateTheme(theme)) { + os.alert({ + type: 'error', + text: i18n.locale._theme.invalid + }); + return false; + } + if (getThemes().some(t => t.id === theme.id)) { + os.alert({ + type: 'info', + text: i18n.locale._theme.alreadyInstalled + }); + return false; + } - try { - theme = JSON5.parse(code); - } catch (e) { - os.alert({ - type: 'error', - text: this.$ts._theme.invalid - }); - return false; - } - if (!validateTheme(theme)) { - os.alert({ - type: 'error', - text: this.$ts._theme.invalid - }); - return false; - } - if (getThemes().some(t => t.id === theme.id)) { - os.alert({ - type: 'info', - text: this.$ts._theme.alreadyInstalled - }); - return false; - } + return theme; +} - return theme; - }, +function preview(code: string): void { + const theme = parseThemeCode(code); + if (theme) applyTheme(theme, false); +} - preview(code) { - const theme = this.parseThemeCode(code); - if (theme) applyTheme(theme, false); - }, +async function install(code: string): Promise<void> { + const theme = parseThemeCode(code); + if (!theme) return; + await addTheme(theme); + os.alert({ + type: 'success', + text: i18n.t('_theme.installed', { name: theme.name }) + }); +} - async install(code) { - const theme = this.parseThemeCode(code); - if (!theme) return; - await addTheme(theme); - os.alert({ - type: 'success', - text: this.$t('_theme.installed', { name: theme.name }) - }); - }, - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale._theme.install, + icon: 'fas fa-download', + bg: 'var(--bg)', + }, }); </script> diff --git a/packages/client/src/pages/settings/theme.manage.vue b/packages/client/src/pages/settings/theme.manage.vue index a913ba4748..a1e849b540 100644 --- a/packages/client/src/pages/settings/theme.manage.vue +++ b/packages/client/src/pages/settings/theme.manage.vue @@ -78,10 +78,6 @@ export default defineComponent({ }, }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { copyThemeCode() { copyToClipboard(this.selectedThemeCode); diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue index 6c88b65699..658e36ec05 100644 --- a/packages/client/src/pages/settings/theme.vue +++ b/packages/client/src/pages/settings/theme.vue @@ -163,10 +163,6 @@ export default defineComponent({ location.reload(); }); - onMounted(() => { - emit('info', INFO); - }); - onActivated(() => { fetchThemes().then(() => { installedThemes.value = getThemes(); diff --git a/packages/client/src/pages/settings/word-mute.vue b/packages/client/src/pages/settings/word-mute.vue index 34edd0492c..19980dea14 100644 --- a/packages/client/src/pages/settings/word-mute.vue +++ b/packages/client/src/pages/settings/word-mute.vue @@ -87,10 +87,6 @@ export default defineComponent({ this.hardWordMutedNotesCount = (await os.api('i/get-word-muted-notes-count', {})).count; }, - mounted() { - this.$emit('info', this[symbols.PAGE_INFO]); - }, - methods: { async save() { this.$store.set('mutedWords', this.softMutedWords.trim().split('\n').map(x => x.trim().split(' '))); diff --git a/packages/client/src/pages/share.vue b/packages/client/src/pages/share.vue index bdd8500ee4..5df6256fb2 100644 --- a/packages/client/src/pages/share.vue +++ b/packages/client/src/pages/share.vue @@ -169,7 +169,7 @@ export default defineComponent({ window.close(); // 閉じなければ100ms後タイムラインに - setTimeout(() => { + window.setTimeout(() => { this.$router.push('/'); }, 100); } diff --git a/packages/client/src/pages/signup-complete.vue b/packages/client/src/pages/signup-complete.vue index 89375e05d2..a10af1a4cc 100644 --- a/packages/client/src/pages/signup-complete.vue +++ b/packages/client/src/pages/signup-complete.vue @@ -1,50 +1,36 @@ <template> <div> - {{ $ts.processing }} + {{ i18n.locale.processing }} </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { onMounted } from 'vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; import { login } from '@/account'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { +const props = defineProps<{ + code: string; +}>(); - }, - - props: { - code: { - type: String, - required: true - } - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.signup, - icon: 'fas fa-user' - }, - } - }, +onMounted(async () => { + await os.alert({ + type: 'info', + text: i18n.t('clickToFinishEmailVerification', { ok: i18n.locale.gotIt }), + }); + const res = await os.apiWithDialog('signup-pending', { + code: props.code, + }); + login(res.i, '/'); +}); - async mounted() { - await os.alert({ - type: 'info', - text: this.$t('clickToFinishEmailVerification', { ok: this.$ts.gotIt }), - }); - const res = await os.apiWithDialog('signup-pending', { - code: this.code, - }); - login(res.i, '/'); +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.signup, + icon: 'fas fa-user', }, - - methods: { - - } }); </script> diff --git a/packages/client/src/pages/tag.vue b/packages/client/src/pages/tag.vue index a9497ae801..8d8dc0a65c 100644 --- a/packages/client/src/pages/tag.vue +++ b/packages/client/src/pages/tag.vue @@ -4,37 +4,28 @@ </div> </template> -<script lang="ts"> -import { computed, defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; import XNotes from '@/components/notes.vue'; import * as symbols from '@/symbols'; -export default defineComponent({ - components: { - XNotes - }, +const props = defineProps<{ + tag: string; +}>(); - props: { - tag: { - type: String, - required: true - } - }, +const pagination = { + endpoint: 'notes/search-by-tag' as const, + limit: 10, + params: computed(() => ({ + tag: props.tag, + })), +}; - data() { - return { - [symbols.PAGE_INFO]: { - title: this.tag, - icon: 'fas fa-hashtag' - }, - pagination: { - endpoint: 'notes/search-by-tag', - limit: 10, - params: computed(() => ({ - tag: this.tag, - })) - }, - }; - }, +defineExpose({ + [symbols.PAGE_INFO]: computed(() => ({ + title: props.tag, + icon: 'fas fa-hashtag', + bg: 'var(--bg)', + })), }); </script> diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue index c4917e2270..80b8c7806c 100644 --- a/packages/client/src/pages/theme-editor.vue +++ b/packages/client/src/pages/theme-editor.vue @@ -2,7 +2,7 @@ <MkSpacer :content-max="800" :margin-min="16" :margin-max="32"> <div class="cwepdizn _formRoot"> <FormFolder :default-open="true" class="_formBlock"> - <template #label>{{ $ts.backgroundColor }}</template> + <template #label>{{ i18n.locale.backgroundColor }}</template> <div class="cwepdizn-colors"> <div class="row"> <button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)"> @@ -18,7 +18,7 @@ </FormFolder> <FormFolder :default-open="true" class="_formBlock"> - <template #label>{{ $ts.accentColor }}</template> + <template #label>{{ i18n.locale.accentColor }}</template> <div class="cwepdizn-colors"> <div class="row"> <button v-for="color in accentColors" :key="color" class="color rounded _button" :class="{ active: theme.props.accent === color }" @click="setAccentColor(color)"> @@ -29,7 +29,7 @@ </FormFolder> <FormFolder :default-open="true" class="_formBlock"> - <template #label>{{ $ts.textColor }}</template> + <template #label>{{ i18n.locale.textColor }}</template> <div class="cwepdizn-colors"> <div class="row"> <button v-for="color in fgColors" :key="color" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }" @click="setFgColor(color)"> @@ -41,22 +41,22 @@ <FormFolder :default-open="false" class="_formBlock"> <template #icon><i class="fas fa-code"></i></template> - <template #label>{{ $ts.editCode }}</template> + <template #label>{{ i18n.locale.editCode }}</template> <div class="_formRoot"> <FormTextarea v-model="themeCode" tall class="_formBlock"> - <template #label>{{ $ts._theme.code }}</template> + <template #label>{{ i18n.locale._theme.code }}</template> </FormTextarea> - <FormButton primary class="_formBlock" @click="applyThemeCode">{{ $ts.apply }}</FormButton> + <FormButton primary class="_formBlock" @click="applyThemeCode">{{ i18n.locale.apply }}</FormButton> </div> </FormFolder> <FormFolder :default-open="false" class="_formBlock"> - <template #label>{{ $ts.addDescription }}</template> + <template #label>{{ i18n.locale.addDescription }}</template> <div class="_formRoot"> <FormTextarea v-model="description"> - <template #label>{{ $ts._theme.description }}</template> + <template #label>{{ i18n.locale._theme.description }}</template> </FormTextarea> </div> </FormFolder> @@ -64,8 +64,8 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { watch } from 'vue'; import { toUnicode } from 'punycode/'; import * as tinycolor from 'tinycolor2'; import { v4 as uuid} from 'uuid'; @@ -78,181 +78,147 @@ import FormFolder from '@/components/form/folder.vue'; import { Theme, applyTheme, darkTheme, lightTheme } from '@/scripts/theme'; import { host } from '@/config'; import * as os from '@/os'; -import { ColdDeviceStorage } from '@/store'; +import { ColdDeviceStorage, defaultStore } from '@/store'; import { addTheme } from '@/theme-store'; import * as symbols from '@/symbols'; +import { i18n } from '@/i18n'; +import { useLeaveGuard } from '@/scripts/use-leave-guard'; -export default defineComponent({ - components: { - FormButton, - FormTextarea, - FormFolder, - }, - - async beforeRouteLeave(to, from) { - if (this.changed && !(await this.leaveConfirm())) { - return false; - } - }, +const bgColors = [ + { color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' }, + { color: '#f0eee9', kind: 'light', forPreview: '#f3e2b9' }, + { color: '#e9eff0', kind: 'light', forPreview: '#bfe3e8' }, + { color: '#f0e9ee', kind: 'light', forPreview: '#f1d1e8' }, + { color: '#dce2e0', kind: 'light', forPreview: '#a4dccc' }, + { color: '#e2e0dc', kind: 'light', forPreview: '#d8c7a5' }, + { color: '#d5dbe0', kind: 'light', forPreview: '#b0cae0' }, + { color: '#dad5d5', kind: 'light', forPreview: '#d6afaf' }, + { color: '#2b2b2b', kind: 'dark', forPreview: '#444444' }, + { color: '#362e29', kind: 'dark', forPreview: '#735c4d' }, + { color: '#303629', kind: 'dark', forPreview: '#506d2f' }, + { color: '#293436', kind: 'dark', forPreview: '#258192' }, + { color: '#2e2936', kind: 'dark', forPreview: '#504069' }, + { color: '#252722', kind: 'dark', forPreview: '#3c462f' }, + { color: '#212525', kind: 'dark', forPreview: '#303e3e' }, + { color: '#191919', kind: 'dark', forPreview: '#272727' }, +] as const; +const accentColors = ['#e36749', '#f29924', '#98c934', '#34c9a9', '#34a1c9', '#606df7', '#8d34c9', '#e84d83']; +const fgColors = [ + { color: 'none', forLight: '#5f5f5f', forDark: '#dadada', forPreview: null }, + { color: 'red', forLight: '#7f6666', forDark: '#e4d1d1', forPreview: '#ca4343' }, + { color: 'yellow', forLight: '#736955', forDark: '#e0d5c0', forPreview: '#d49923' }, + { color: 'green', forLight: '#586d5b', forDark: '#d1e4d4', forPreview: '#4cbd5c' }, + { color: 'cyan', forLight: '#5d7475', forDark: '#d1e3e4', forPreview: '#2abdc3' }, + { color: 'blue', forLight: '#676880', forDark: '#d1d2e4', forPreview: '#7275d8' }, + { color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' }, +]; - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.themeEditor, - icon: 'fas fa-palette', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-eye', - text: this.$ts.preview, - handler: this.showPreview, - }, { - asFullButton: true, - icon: 'fas fa-check', - text: this.$ts.saveAs, - handler: this.saveAs, - }], - }, - theme: { - base: 'light', - props: lightTheme.props - } as Theme, - description: null, - themeCode: null, - bgColors: [ - { color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' }, - { color: '#f0eee9', kind: 'light', forPreview: '#f3e2b9' }, - { color: '#e9eff0', kind: 'light', forPreview: '#bfe3e8' }, - { color: '#f0e9ee', kind: 'light', forPreview: '#f1d1e8' }, - { color: '#dce2e0', kind: 'light', forPreview: '#a4dccc' }, - { color: '#e2e0dc', kind: 'light', forPreview: '#d8c7a5' }, - { color: '#d5dbe0', kind: 'light', forPreview: '#b0cae0' }, - { color: '#dad5d5', kind: 'light', forPreview: '#d6afaf' }, - { color: '#2b2b2b', kind: 'dark', forPreview: '#444444' }, - { color: '#362e29', kind: 'dark', forPreview: '#735c4d' }, - { color: '#303629', kind: 'dark', forPreview: '#506d2f' }, - { color: '#293436', kind: 'dark', forPreview: '#258192' }, - { color: '#2e2936', kind: 'dark', forPreview: '#504069' }, - { color: '#252722', kind: 'dark', forPreview: '#3c462f' }, - { color: '#212525', kind: 'dark', forPreview: '#303e3e' }, - { color: '#191919', kind: 'dark', forPreview: '#272727' }, - ], - accentColors: ['#e36749', '#f29924', '#98c934', '#34c9a9', '#34a1c9', '#606df7', '#8d34c9', '#e84d83'], - fgColors: [ - { color: 'none', forLight: '#5f5f5f', forDark: '#dadada', forPreview: null }, - { color: 'red', forLight: '#7f6666', forDark: '#e4d1d1', forPreview: '#ca4343' }, - { color: 'yellow', forLight: '#736955', forDark: '#e0d5c0', forPreview: '#d49923' }, - { color: 'green', forLight: '#586d5b', forDark: '#d1e4d4', forPreview: '#4cbd5c' }, - { color: 'cyan', forLight: '#5d7475', forDark: '#d1e3e4', forPreview: '#2abdc3' }, - { color: 'blue', forLight: '#676880', forDark: '#d1d2e4', forPreview: '#7275d8' }, - { color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' }, - ], - changed: false, - } - }, - - created() { - this.$watch('theme', this.apply, { deep: true }); - window.addEventListener('beforeunload', this.beforeunload); - }, +const theme = $ref<Partial<Theme>>({ + base: 'light', + props: lightTheme.props, +}); +let description = $ref<string | null>(null); +let themeCode = $ref<string | null>(null); +let changed = $ref(false); - beforeUnmount() { - window.removeEventListener('beforeunload', this.beforeunload); - }, +useLeaveGuard($$(changed)); - methods: { - beforeunload(e: BeforeUnloadEvent) { - if (this.changed) { - e.preventDefault(); - e.returnValue = ''; - } - }, +function showPreview() { + os.pageWindow('preview'); +} - async leaveConfirm(): Promise<boolean> { - const { canceled } = await os.confirm({ - type: 'warning', - text: this.$ts.leaveConfirm, - }); - return !canceled; - }, +function setBgColor(color: typeof bgColors[number]) { + if (theme.base != color.kind) { + const base = color.kind === 'dark' ? darkTheme : lightTheme; + for (const prop of Object.keys(base.props)) { + if (prop === 'accent') continue; + if (prop === 'fg') continue; + theme.props[prop] = base.props[prop]; + } + } + theme.base = color.kind; + theme.props.bg = color.color; - showPreview() { - os.pageWindow('preview'); - }, + if (theme.props.fg) { + const matchedFgColor = fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(theme.props.fg).toRgbString())); + if (matchedFgColor) setFgColor(matchedFgColor); + } +} - setBgColor(color) { - if (this.theme.base != color.kind) { - const base = color.kind === 'dark' ? darkTheme : lightTheme; - for (const prop of Object.keys(base.props)) { - if (prop === 'accent') continue; - if (prop === 'fg') continue; - this.theme.props[prop] = base.props[prop]; - } - } - this.theme.base = color.kind; - this.theme.props.bg = color.color; +function setAccentColor(color) { + theme.props.accent = color; +} - if (this.theme.props.fg) { - const matchedFgColor = this.fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(this.theme.props.fg).toRgbString())); - if (matchedFgColor) this.setFgColor(matchedFgColor); - } - }, +function setFgColor(color) { + theme.props.fg = theme.base === 'light' ? color.forLight : color.forDark; +} - setAccentColor(color) { - this.theme.props.accent = color; - }, +function apply() { + themeCode = JSON5.stringify(theme, null, '\t'); + applyTheme(theme, false); + changed = true; +} - setFgColor(color) { - this.theme.props.fg = this.theme.base === 'light' ? color.forLight : color.forDark; - }, +function applyThemeCode() { + let parsed; - apply() { - this.themeCode = JSON5.stringify(this.theme, null, '\t'); - applyTheme(this.theme, false); - this.changed = true; - }, + try { + parsed = JSON5.parse(themeCode); + } catch (err) { + os.alert({ + type: 'error', + text: i18n.locale._theme.invalid, + }); + return; + } - applyThemeCode() { - let parsed; + theme = parsed; +} - try { - parsed = JSON5.parse(this.themeCode); - } catch (e) { - os.alert({ - type: 'error', - text: this.$ts._theme.invalid - }); - return; - } +async function saveAs() { + const { canceled, result: name } = await os.inputText({ + title: i18n.locale.name, + allowEmpty: false, + }); + if (canceled) return; - this.theme = parsed; - }, + theme.id = uuid(); + theme.name = name; + theme.author = `@${$i.username}@${toUnicode(host)}`; + if (description) theme.desc = description; + addTheme(theme); + applyTheme(theme); + if (defaultStore.state.darkMode) { + ColdDeviceStorage.set('darkTheme', theme); + } else { + ColdDeviceStorage.set('lightTheme', theme); + } + changed = false; + os.alert({ + type: 'success', + text: i18n.t('_theme.installed', { name: theme.name }), + }); +} - async saveAs() { - const { canceled, result: name } = await os.inputText({ - title: this.$ts.name, - allowEmpty: false - }); - if (canceled) return; +watch($$(theme), apply, { deep: true }); - this.theme.id = uuid(); - this.theme.name = name; - this.theme.author = `@${this.$i.username}@${toUnicode(host)}`; - if (this.description) this.theme.desc = this.description; - addTheme(this.theme); - applyTheme(this.theme); - if (this.$store.state.darkMode) { - ColdDeviceStorage.set('darkTheme', this.theme); - } else { - ColdDeviceStorage.set('lightTheme', this.theme); - } - this.changed = false; - os.alert({ - type: 'success', - text: this.$t('_theme.installed', { name: this.theme.name }) - }); - } - } +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.locale.themeEditor, + icon: 'fas fa-palette', + bg: 'var(--bg)', + actions: [{ + asFullButton: true, + icon: 'fas fa-eye', + text: i18n.locale.preview, + handler: showPreview, + }, { + asFullButton: true, + icon: 'fas fa-check', + text: i18n.locale.saveAs, + handler: saveAs, + }], + }, }); </script> diff --git a/packages/client/src/pages/timeline.tutorial.vue b/packages/client/src/pages/timeline.tutorial.vue index 3775796940..432d28c60b 100644 --- a/packages/client/src/pages/timeline.tutorial.vue +++ b/packages/client/src/pages/timeline.tutorial.vue @@ -65,26 +65,14 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; import MkButton from '@/components/ui/button.vue'; +import { defaultStore } from '@/store'; -export default defineComponent({ - components: { - MkButton, - }, - - data() { - return { - } - }, - - computed: { - tutorial: { - get() { return this.$store.reactiveState.tutorial.value || 0; }, - set(value) { this.$store.set('tutorial', value); } - }, - }, +const tutorial = computed({ + get() { return defaultStore.reactiveState.tutorial.value || 0; }, + set(value) { defaultStore.set('tutorial', value); } }); </script> diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue index 216b3c34ea..ecd1ae6257 100644 --- a/packages/client/src/pages/timeline.vue +++ b/packages/client/src/pages/timeline.vue @@ -1,6 +1,6 @@ <template> <MkSpacer :content-max="800"> - <div v-hotkey.global="keymap" class="cmuxhskf"> + <div ref="rootEl" v-hotkey.global="keymap" class="cmuxhskf"> <XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/> <XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/> @@ -17,163 +17,139 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent, defineAsyncComponent, computed } from 'vue'; +<script lang="ts" setup> +import { defineAsyncComponent, computed, watch } from 'vue'; import XTimeline from '@/components/timeline.vue'; import XPostForm from '@/components/post-form.vue'; import { scroll } from '@/scripts/scroll'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { defaultStore } from '@/store'; +import { i18n } from '@/i18n'; +import { instance } from '@/instance'; +import { $i } from '@/account'; -export default defineComponent({ - name: 'timeline', +const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue')); - components: { - XTimeline, - XTutorial: defineAsyncComponent(() => import('./timeline.tutorial.vue')), - XPostForm, - }, +const isLocalTimelineAvailable = !instance.disableLocalTimeline || ($i != null && ($i.isModerator || $i.isAdmin)); +const isGlobalTimelineAvailable = !instance.disableGlobalTimeline || ($i != null && ($i.isModerator || $i.isAdmin)); +const keymap = { + 't': focus, +}; - data() { - return { - src: 'home', - queue: 0, - [symbols.PAGE_INFO]: computed(() => ({ - title: this.$ts.timeline, - icon: this.src === 'local' ? 'fas fa-comments' : this.src === 'social' ? 'fas fa-share-alt' : this.src === 'global' ? 'fas fa-globe' : 'fas fa-home', - bg: 'var(--bg)', - actions: [{ - icon: 'fas fa-list-ul', - text: this.$ts.lists, - handler: this.chooseList - }, { - icon: 'fas fa-satellite', - text: this.$ts.antennas, - handler: this.chooseAntenna - }, { - icon: 'fas fa-satellite-dish', - text: this.$ts.channel, - handler: this.chooseChannel - }, { - icon: 'fas fa-calendar-alt', - text: this.$ts.jumpToSpecifiedDate, - handler: this.timetravel - }], - tabs: [{ - active: this.src === 'home', - title: this.$ts._timelines.home, - icon: 'fas fa-home', - iconOnly: true, - onClick: () => { this.src = 'home'; this.saveSrc(); }, - }, ...(this.isLocalTimelineAvailable ? [{ - active: this.src === 'local', - title: this.$ts._timelines.local, - icon: 'fas fa-comments', - iconOnly: true, - onClick: () => { this.src = 'local'; this.saveSrc(); }, - }, { - active: this.src === 'social', - title: this.$ts._timelines.social, - icon: 'fas fa-share-alt', - iconOnly: true, - onClick: () => { this.src = 'social'; this.saveSrc(); }, - }] : []), ...(this.isGlobalTimelineAvailable ? [{ - active: this.src === 'global', - title: this.$ts._timelines.global, - icon: 'fas fa-globe', - iconOnly: true, - onClick: () => { this.src = 'global'; this.saveSrc(); }, - }] : [])], - })), - }; - }, +const tlComponent = $ref<InstanceType<typeof XTimeline>>(); +const rootEl = $ref<HTMLElement>(); - computed: { - keymap(): any { - return { - 't': this.focus - }; - }, +let src = $ref<'home' | 'local' | 'social' | 'global'>(defaultStore.state.tl.src); +let queue = $ref(0); - isLocalTimelineAvailable(): boolean { - return !this.$instance.disableLocalTimeline || this.$i.isModerator || this.$i.isAdmin; - }, - - isGlobalTimelineAvailable(): boolean { - return !this.$instance.disableGlobalTimeline || this.$i.isModerator || this.$i.isAdmin; - }, - }, - - watch: { - src() { - this.showNav = false; - }, - }, - - created() { - this.src = this.$store.state.tl.src; - }, +function queueUpdated(q: number): void { + queue = q; +} - methods: { - queueUpdated(q) { - this.queue = q; - }, +function top(): void { + scroll(rootEl, { top: 0 }); +} - top() { - scroll(this.$el, { top: 0 }); - }, +async function chooseList(ev: MouseEvent): Promise<void> { + const lists = await os.api('users/lists/list'); + const items = lists.map(list => ({ + type: 'link', + text: list.name, + to: `/timeline/list/${list.id}`, + })); + os.popupMenu(items, ev.currentTarget || ev.target); +} - async chooseList(ev) { - const lists = await os.api('users/lists/list'); - const items = lists.map(list => ({ - type: 'link', - text: list.name, - to: `/timeline/list/${list.id}` - })); - os.popupMenu(items, ev.currentTarget || ev.target); - }, +async function chooseAntenna(ev: MouseEvent): Promise<void> { + const antennas = await os.api('antennas/list'); + const items = antennas.map(antenna => ({ + type: 'link', + text: antenna.name, + indicate: antenna.hasUnreadNote, + to: `/timeline/antenna/${antenna.id}`, + })); + os.popupMenu(items, ev.currentTarget || ev.target); +} - async chooseAntenna(ev) { - const antennas = await os.api('antennas/list'); - const items = antennas.map(antenna => ({ - type: 'link', - text: antenna.name, - indicate: antenna.hasUnreadNote, - to: `/timeline/antenna/${antenna.id}` - })); - os.popupMenu(items, ev.currentTarget || ev.target); - }, +async function chooseChannel(ev: MouseEvent): Promise<void> { + const channels = await os.api('channels/followed'); + const items = channels.map(channel => ({ + type: 'link', + text: channel.name, + indicate: channel.hasUnreadNote, + to: `/channels/${channel.id}`, + })); + os.popupMenu(items, ev.currentTarget || ev.target); +} - async chooseChannel(ev) { - const channels = await os.api('channels/followed'); - const items = channels.map(channel => ({ - type: 'link', - text: channel.name, - indicate: channel.hasUnreadNote, - to: `/channels/${channel.id}` - })); - os.popupMenu(items, ev.currentTarget || ev.target); - }, +function saveSrc(): void { + defaultStore.set('tl', { + src: src, + }); +} - saveSrc() { - this.$store.set('tl', { - src: this.src, - }); - }, +async function timetravel(): Promise<void> { + const { canceled, result: date } = await os.inputDate({ + title: i18n.locale.date, + }); + if (canceled) return; - async timetravel() { - const { canceled, result: date } = await os.inputDate({ - title: this.$ts.date, - }); - if (canceled) return; + tlComponent.timetravel(date); +} - this.$refs.tl.timetravel(date); - }, +function focus(): void { + tlComponent.focus(); +} - focus() { - (this.$refs.tl as any).focus(); - } - } +defineExpose({ + [symbols.PAGE_INFO]: computed(() => ({ + title: i18n.locale.timeline, + icon: src === 'local' ? 'fas fa-comments' : src === 'social' ? 'fas fa-share-alt' : src === 'global' ? 'fas fa-globe' : 'fas fa-home', + bg: 'var(--bg)', + actions: [{ + icon: 'fas fa-list-ul', + text: i18n.locale.lists, + handler: chooseList, + }, { + icon: 'fas fa-satellite', + text: i18n.locale.antennas, + handler: chooseAntenna, + }, { + icon: 'fas fa-satellite-dish', + text: i18n.locale.channel, + handler: chooseChannel, + }, { + icon: 'fas fa-calendar-alt', + text: i18n.locale.jumpToSpecifiedDate, + handler: timetravel, + }], + tabs: [{ + active: src === 'home', + title: i18n.locale._timelines.home, + icon: 'fas fa-home', + iconOnly: true, + onClick: () => { src = 'home'; saveSrc(); }, + }, ...(isLocalTimelineAvailable ? [{ + active: src === 'local', + title: i18n.locale._timelines.local, + icon: 'fas fa-comments', + iconOnly: true, + onClick: () => { src = 'local'; saveSrc(); }, + }, { + active: src === 'social', + title: i18n.locale._timelines.social, + icon: 'fas fa-share-alt', + iconOnly: true, + onClick: () => { src = 'social'; saveSrc(); }, + }] : []), ...(isGlobalTimelineAvailable ? [{ + active: src === 'global', + title: i18n.locale._timelines.global, + icon: 'fas fa-globe', + iconOnly: true, + onClick: () => { src = 'global'; saveSrc(); }, + }] : [])], + })), }); </script> diff --git a/packages/client/src/pages/user/clips.vue b/packages/client/src/pages/user/clips.vue index aad5317ce0..870e6f7174 100644 --- a/packages/client/src/pages/user/clips.vue +++ b/packages/client/src/pages/user/clips.vue @@ -28,7 +28,7 @@ export default defineComponent({ data() { return { pagination: { - endpoint: 'users/clips', + endpoint: 'users/clips' as const, limit: 20, params: { userId: this.user.id, diff --git a/packages/client/src/pages/user/follow-list.vue b/packages/client/src/pages/user/follow-list.vue index e12ea477ca..98a1fc0f86 100644 --- a/packages/client/src/pages/user/follow-list.vue +++ b/packages/client/src/pages/user/follow-list.vue @@ -8,47 +8,32 @@ </div> </template> -<script lang="ts"> -import { computed, defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; +import * as misskey from 'misskey-js'; import MkUserInfo from '@/components/user-info.vue'; import MkPagination from '@/components/ui/pagination.vue'; -export default defineComponent({ - components: { - MkPagination, - MkUserInfo, - }, +const props = defineProps<{ + user: misskey.entities.User; + type: 'following' | 'followers'; +}>(); - props: { - user: { - type: Object, - required: true - }, - type: { - type: String, - required: true - }, - }, +const followingPagination = { + endpoint: 'users/following' as const, + limit: 20, + params: computed(() => ({ + userId: props.user.id, + })), +}; - data() { - return { - followingPagination: { - endpoint: 'users/following', - limit: 20, - params: computed(() => ({ - userId: this.user.id, - })), - }, - followersPagination: { - endpoint: 'users/followers', - limit: 20, - params: computed(() => ({ - userId: this.user.id, - })), - }, - }; - }, -}); +const followersPagination = { + endpoint: 'users/followers' as const, + limit: 20, + params: computed(() => ({ + userId: props.user.id, + })), +}; </script> <style lang="scss" scoped> diff --git a/packages/client/src/pages/user/gallery.vue b/packages/client/src/pages/user/gallery.vue index 88f0604f1f..07dda4a292 100644 --- a/packages/client/src/pages/user/gallery.vue +++ b/packages/client/src/pages/user/gallery.vue @@ -29,7 +29,7 @@ export default defineComponent({ data() { return { pagination: { - endpoint: 'users/gallery/posts', + endpoint: 'users/gallery/posts' as const, limit: 6, params: computed(() => ({ userId: this.user.id diff --git a/packages/client/src/pages/user/index.activity.vue b/packages/client/src/pages/user/index.activity.vue index e51d6c6090..43a4f476f1 100644 --- a/packages/client/src/pages/user/index.activity.vue +++ b/packages/client/src/pages/user/index.activity.vue @@ -8,27 +8,16 @@ </MkContainer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@/os'; +<script lang="ts" setup> +import { } from 'vue'; +import * as misskey from 'misskey-js'; import MkContainer from '@/components/ui/container.vue'; import MkChart from '@/components/chart.vue'; -export default defineComponent({ - components: { - MkContainer, - MkChart, - }, - props: { - user: { - type: Object, - required: true - }, - limit: { - type: Number, - required: false, - default: 40 - } - }, +const props = withDefaults(defineProps<{ + user: misskey.entities.User; + limit?: number; +}>(), { + limit: 40, }); </script> diff --git a/packages/client/src/pages/user/pages.vue b/packages/client/src/pages/user/pages.vue index 3075dd5729..ad101158e0 100644 --- a/packages/client/src/pages/user/pages.vue +++ b/packages/client/src/pages/user/pages.vue @@ -6,36 +6,23 @@ </div> </template> -<script lang="ts"> -import { computed, defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; +import * as misskey from 'misskey-js'; import MkPagePreview from '@/components/page-preview.vue'; import MkPagination from '@/components/ui/pagination.vue'; -export default defineComponent({ - components: { - MkPagination, - MkPagePreview, - }, +const props = defineProps<{ + user: misskey.entities.User; +}>(); - props: { - user: { - type: Object, - required: true - }, - }, - - data() { - return { - pagination: { - endpoint: 'users/pages', - limit: 20, - params: computed(() => ({ - userId: this.user.id, - })), - }, - }; - }, -}); +const pagination = { + endpoint: 'users/pages' as const, + limit: 20, + params: computed(() => ({ + userId: props.user.id, + })), +}; </script> <style lang="scss" scoped> diff --git a/packages/client/src/pages/user/reactions.vue b/packages/client/src/pages/user/reactions.vue index f51f6669c3..d2c1f92ebb 100644 --- a/packages/client/src/pages/user/reactions.vue +++ b/packages/client/src/pages/user/reactions.vue @@ -7,44 +7,30 @@ <MkReactionIcon class="reaction" :reaction="item.type" :custom-emojis="item.note.emojis" :no-style="true"/> <MkTime :time="item.createdAt" class="createdAt"/> </div> - <MkNote :key="item.id" :note="item.note" @update:note="updated(note, $event)"/> + <MkNote :key="item.id" :note="item.note"/> </div> </MkPagination> </div> </template> -<script lang="ts"> -import { computed, defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; +import * as misskey from 'misskey-js'; import MkPagination from '@/components/ui/pagination.vue'; import MkNote from '@/components/note.vue'; import MkReactionIcon from '@/components/reaction-icon.vue'; -export default defineComponent({ - components: { - MkPagination, - MkNote, - MkReactionIcon, - }, +const props = defineProps<{ + user: misskey.entities.User; +}>(); - props: { - user: { - type: Object, - required: true - }, - }, - - data() { - return { - pagination: { - endpoint: 'users/reactions', - limit: 20, - params: computed(() => ({ - userId: this.user.id, - })), - }, - }; - }, -}); +const pagination = { + endpoint: 'users/reactions' as const, + limit: 20, + params: computed(() => ({ + userId: props.user.id, + })), +}; </script> <style lang="scss" scoped> diff --git a/packages/client/src/pizzax.ts b/packages/client/src/pizzax.ts index 17a91af58b..11bf4f9c48 100644 --- a/packages/client/src/pizzax.ts +++ b/packages/client/src/pizzax.ts @@ -125,7 +125,7 @@ export class Storage<T extends StateDef> { return new Promise((resolve, reject) => { if ($i) { // api関数と循環参照なので一応setTimeoutしておく - setTimeout(async () => { + window.setTimeout(async () => { await defaultStore.ready; api('i/registry/get-all', { scope: ['client', this.key] }) diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts index 312374cdb9..ec48b76fdf 100644 --- a/packages/client/src/router.ts +++ b/packages/client/src/router.ts @@ -33,7 +33,7 @@ const defaultRoutes = [ { path: '/explore/tags/:tag', props: true, component: page('explore') }, { path: '/federation', component: page('federation') }, { path: '/emojis', component: page('emojis') }, - { path: '/search', component: page('search') }, + { path: '/search', component: page('search'), props: route => ({ query: route.query.q, channel: route.query.channel }) }, { path: '/pages', name: 'pages', component: page('pages') }, { path: '/pages/new', component: page('page-editor/page-editor') }, { path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) }, @@ -115,11 +115,11 @@ export const router = createRouter({ window._scroll = () => { // さらにHacky if (to.name === 'index') { window.scroll({ top: indexScrollPos, behavior: 'instant' }); - const i = setInterval(() => { + const i = window.setInterval(() => { window.scroll({ top: indexScrollPos, behavior: 'instant' }); }, 10); - setTimeout(() => { - clearInterval(i); + window.setTimeout(() => { + window.clearInterval(i); }, 500); } else { window.scroll({ top: 0, behavior: 'instant' }); diff --git a/packages/client/src/scripts/autocomplete.ts b/packages/client/src/scripts/autocomplete.ts index f2d5806484..f4a3a4c0fc 100644 --- a/packages/client/src/scripts/autocomplete.ts +++ b/packages/client/src/scripts/autocomplete.ts @@ -1,4 +1,4 @@ -import { Ref, ref } from 'vue'; +import { nextTick, Ref, ref } from 'vue'; import * as getCaretCoordinates from 'textarea-caret'; import { toASCII } from 'punycode/'; import { popup } from '@/os'; @@ -10,26 +10,23 @@ export class Autocomplete { q: Ref<string | null>; close: Function; } | null; - private textarea: any; - private vm: any; + private textarea: HTMLInputElement | HTMLTextAreaElement; private currentType: string; - private opts: { - model: string; - }; + private textRef: Ref<string>; private opening: boolean; private get text(): string { - return this.vm[this.opts.model]; + return this.textRef.value; } private set text(text: string) { - this.vm[this.opts.model] = text; + this.textRef.value = text; } /** * 対象のテキストエリアを与えてインスタンスを初期化します。 */ - constructor(textarea, vm, opts) { + constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) { //#region BIND this.onInput = this.onInput.bind(this); this.complete = this.complete.bind(this); @@ -38,8 +35,7 @@ export class Autocomplete { this.suggestion = null; this.textarea = textarea; - this.vm = vm; - this.opts = opts; + this.textRef = textRef; this.opening = false; this.attach(); @@ -218,7 +214,7 @@ export class Autocomplete { this.text = `${trimmedBefore}@${acct} ${after}`; // キャレットを戻す - this.vm.$nextTick(() => { + nextTick(() => { this.textarea.focus(); const pos = trimmedBefore.length + (acct.length + 2); this.textarea.setSelectionRange(pos, pos); @@ -234,7 +230,7 @@ export class Autocomplete { this.text = `${trimmedBefore}#${value} ${after}`; // キャレットを戻す - this.vm.$nextTick(() => { + nextTick(() => { this.textarea.focus(); const pos = trimmedBefore.length + (value.length + 2); this.textarea.setSelectionRange(pos, pos); @@ -250,7 +246,7 @@ export class Autocomplete { this.text = trimmedBefore + value + after; // キャレットを戻す - this.vm.$nextTick(() => { + nextTick(() => { this.textarea.focus(); const pos = trimmedBefore.length + value.length; this.textarea.setSelectionRange(pos, pos); @@ -266,7 +262,7 @@ export class Autocomplete { this.text = `${trimmedBefore}$[${value} ]${after}`; // キャレットを戻す - this.vm.$nextTick(() => { + nextTick(() => { this.textarea.focus(); const pos = trimmedBefore.length + (value.length + 3); this.textarea.setSelectionRange(pos, pos); diff --git a/packages/client/src/scripts/check-word-mute.ts b/packages/client/src/scripts/check-word-mute.ts index 3b1fa75b1e..55637bb3b3 100644 --- a/packages/client/src/scripts/check-word-mute.ts +++ b/packages/client/src/scripts/check-word-mute.ts @@ -1,4 +1,4 @@ -export async function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: string[][]): Promise<boolean> { +export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: string[][]): boolean { // 自分自身 if (me && (note.userId === me.id)) return false; diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts new file mode 100644 index 0000000000..61120d53ba --- /dev/null +++ b/packages/client/src/scripts/get-note-menu.ts @@ -0,0 +1,310 @@ +import { Ref } from 'vue'; +import * as misskey from 'misskey-js'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; +import { instance } from '@/instance'; +import * as os from '@/os'; +import copyToClipboard from '@/scripts/copy-to-clipboard'; +import { url } from '@/config'; +import { noteActions } from '@/store'; +import { pleaseLogin } from './please-login'; + +export function getNoteMenu(props: { + note: misskey.entities.Note; + menuButton: Ref<HTMLElement>; + translation: Ref<any>; + translating: Ref<boolean>; +}) { + const isRenote = ( + props.note.renote != null && + props.note.text == null && + props.note.fileIds.length === 0 && + props.note.poll == null + ); + + let appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note; + + function del(): void { + os.confirm({ + type: 'warning', + text: i18n.locale.noteDeleteConfirm, + }).then(({ canceled }) => { + if (canceled) return; + + os.api('notes/delete', { + noteId: appearNote.id + }); + }); + } + + function delEdit(): void { + os.confirm({ + type: 'warning', + text: i18n.locale.deleteAndEditConfirm, + }).then(({ canceled }) => { + if (canceled) return; + + os.api('notes/delete', { + noteId: appearNote.id + }); + + os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel }); + }); + } + + function toggleFavorite(favorite: boolean): void { + os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { + noteId: appearNote.id + }); + } + + function toggleWatch(watch: boolean): void { + os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', { + noteId: appearNote.id + }); + } + + function toggleThreadMute(mute: boolean): void { + os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', { + noteId: appearNote.id + }); + } + + function copyContent(): void { + copyToClipboard(appearNote.text); + os.success(); + } + + function copyLink(): void { + copyToClipboard(`${url}/notes/${appearNote.id}`); + os.success(); + } + + function togglePin(pin: boolean): void { + os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', { + noteId: appearNote.id + }, undefined, null, e => { + if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { + os.alert({ + type: 'error', + text: i18n.locale.pinLimitExceeded + }); + } + }); + } + + async function clip(): Promise<void> { + const clips = await os.api('clips/list'); + os.popupMenu([{ + icon: 'fas fa-plus', + text: i18n.locale.createNew, + action: async () => { + const { canceled, result } = await os.form(i18n.locale.createNewClip, { + name: { + type: 'string', + label: i18n.locale.name + }, + description: { + type: 'string', + required: false, + multiline: true, + label: i18n.locale.description + }, + isPublic: { + type: 'boolean', + label: i18n.locale.public, + default: false + } + }); + if (canceled) return; + + const clip = await os.apiWithDialog('clips/create', result); + + os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id }); + } + }, null, ...clips.map(clip => ({ + text: clip.name, + action: () => { + os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id }); + } + }))], props.menuButton.value, { + }).then(focus); + } + + async function promote(): Promise<void> { + const { canceled, result: days } = await os.inputNumber({ + title: i18n.locale.numberOfDays, + }); + + if (canceled) return; + + os.apiWithDialog('admin/promo/create', { + noteId: appearNote.id, + expiresAt: Date.now() + (86400000 * days), + }); + } + + function share(): void { + navigator.share({ + title: i18n.t('noteOf', { user: appearNote.user.name }), + text: appearNote.text, + url: `${url}/notes/${appearNote.id}`, + }); + } + + async function translate(): Promise<void> { + if (props.translation.value != null) return; + props.translating.value = true; + const res = await os.api('notes/translate', { + noteId: appearNote.id, + targetLang: localStorage.getItem('lang') || navigator.language, + }); + props.translating.value = false; + props.translation.value = res; + } + + let menu; + if ($i) { + const statePromise = os.api('notes/state', { + noteId: appearNote.id + }); + + menu = [{ + icon: 'fas fa-copy', + text: i18n.locale.copyContent, + action: copyContent + }, { + icon: 'fas fa-link', + text: i18n.locale.copyLink, + action: copyLink + }, (appearNote.url || appearNote.uri) ? { + icon: 'fas fa-external-link-square-alt', + text: i18n.locale.showOnRemote, + action: () => { + window.open(appearNote.url || appearNote.uri, '_blank'); + } + } : undefined, + { + icon: 'fas fa-share-alt', + text: i18n.locale.share, + action: share + }, + instance.translatorAvailable ? { + icon: 'fas fa-language', + text: i18n.locale.translate, + action: translate + } : undefined, + null, + statePromise.then(state => state.isFavorited ? { + icon: 'fas fa-star', + text: i18n.locale.unfavorite, + action: () => toggleFavorite(false) + } : { + icon: 'fas fa-star', + text: i18n.locale.favorite, + action: () => toggleFavorite(true) + }), + { + icon: 'fas fa-paperclip', + text: i18n.locale.clip, + action: () => clip() + }, + (appearNote.userId != $i.id) ? statePromise.then(state => state.isWatching ? { + icon: 'fas fa-eye-slash', + text: i18n.locale.unwatch, + action: () => toggleWatch(false) + } : { + icon: 'fas fa-eye', + text: i18n.locale.watch, + action: () => toggleWatch(true) + }) : undefined, + statePromise.then(state => state.isMutedThread ? { + icon: 'fas fa-comment-slash', + text: i18n.locale.unmuteThread, + action: () => toggleThreadMute(false) + } : { + icon: 'fas fa-comment-slash', + text: i18n.locale.muteThread, + action: () => toggleThreadMute(true) + }), + appearNote.userId == $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? { + icon: 'fas fa-thumbtack', + text: i18n.locale.unpin, + action: () => togglePin(false) + } : { + icon: 'fas fa-thumbtack', + text: i18n.locale.pin, + action: () => togglePin(true) + } : undefined, + /* + ...($i.isModerator || $i.isAdmin ? [ + null, + { + icon: 'fas fa-bullhorn', + text: i18n.locale.promote, + action: promote + }] + : [] + ),*/ + ...(appearNote.userId != $i.id ? [ + null, + { + icon: 'fas fa-exclamation-circle', + text: i18n.locale.reportAbuse, + action: () => { + const u = `${url}/notes/${appearNote.id}`; + os.popup(import('@/components/abuse-report-window.vue'), { + user: appearNote.user, + initialComment: `Note: ${u}\n-----\n` + }, {}, 'closed'); + } + }] + : [] + ), + ...(appearNote.userId == $i.id || $i.isModerator || $i.isAdmin ? [ + null, + appearNote.userId == $i.id ? { + icon: 'fas fa-edit', + text: i18n.locale.deleteAndEdit, + action: delEdit + } : undefined, + { + icon: 'fas fa-trash-alt', + text: i18n.locale.delete, + danger: true, + action: del + }] + : [] + )] + .filter(x => x !== undefined); + } else { + menu = [{ + icon: 'fas fa-copy', + text: i18n.locale.copyContent, + action: copyContent + }, { + icon: 'fas fa-link', + text: i18n.locale.copyLink, + action: copyLink + }, (appearNote.url || appearNote.uri) ? { + icon: 'fas fa-external-link-square-alt', + text: i18n.locale.showOnRemote, + action: () => { + window.open(appearNote.url || appearNote.uri, '_blank'); + } + } : undefined] + .filter(x => x !== undefined); + } + + if (noteActions.length > 0) { + menu = menu.concat([null, ...noteActions.map(action => ({ + icon: 'fas fa-plug', + text: action.title, + action: () => { + action.handler(appearNote); + } + }))]); + } + + return menu; +} diff --git a/packages/client/src/scripts/physics.ts b/packages/client/src/scripts/physics.ts index 445b6296eb..36e476b6f9 100644 --- a/packages/client/src/scripts/physics.ts +++ b/packages/client/src/scripts/physics.ts @@ -136,7 +136,7 @@ export function physics(container: HTMLElement) { } // 奈落に落ちたオブジェクトは消す - const intervalId = setInterval(() => { + const intervalId = window.setInterval(() => { for (const obj of objs) { if (obj.position.y > (containerHeight + 1024)) Matter.World.remove(world, obj); } @@ -146,7 +146,7 @@ export function physics(container: HTMLElement) { stop: () => { stop = true; Matter.Runner.stop(runner); - clearInterval(intervalId); + window.clearInterval(intervalId); } }; } diff --git a/packages/client/src/scripts/theme.ts b/packages/client/src/scripts/theme.ts index 3b7f003d0f..85c087331b 100644 --- a/packages/client/src/scripts/theme.ts +++ b/packages/client/src/scripts/theme.ts @@ -34,11 +34,11 @@ export const builtinThemes = [ let timeout = null; export function applyTheme(theme: Theme, persist = true) { - if (timeout) clearTimeout(timeout); + if (timeout) window.clearTimeout(timeout); document.documentElement.classList.add('_themeChanging_'); - timeout = setTimeout(() => { + timeout = window.setTimeout(() => { document.documentElement.classList.remove('_themeChanging_'); }, 1000); diff --git a/packages/client/src/scripts/use-leave-guard.ts b/packages/client/src/scripts/use-leave-guard.ts new file mode 100644 index 0000000000..21899af59a --- /dev/null +++ b/packages/client/src/scripts/use-leave-guard.ts @@ -0,0 +1,34 @@ +import { inject, onUnmounted, Ref } from 'vue'; +import { i18n } from '@/i18n'; +import * as os from '@/os'; + +export function useLeaveGuard(enabled: Ref<boolean>) { + const setLeaveGuard = inject('setLeaveGuard'); + + if (setLeaveGuard) { + setLeaveGuard(async () => { + if (!enabled.value) return false; + + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.locale.leaveConfirm, + }); + + return canceled; + }); + } + + /* + function onBeforeLeave(ev: BeforeUnloadEvent) { + if (enabled.value) { + ev.preventDefault(); + ev.returnValue = ''; + } + } + + window.addEventListener('beforeunload', onBeforeLeave); + onUnmounted(() => { + window.removeEventListener('beforeunload', onBeforeLeave); + }); + */ +} diff --git a/packages/client/src/scripts/use-note-capture.ts b/packages/client/src/scripts/use-note-capture.ts new file mode 100644 index 0000000000..bb00e464e3 --- /dev/null +++ b/packages/client/src/scripts/use-note-capture.ts @@ -0,0 +1,123 @@ +import { onUnmounted, Ref } from 'vue'; +import * as misskey from 'misskey-js'; +import { stream } from '@/stream'; +import { $i } from '@/account'; + +export function useNoteCapture(props: { + rootEl: Ref<HTMLElement>; + appearNote: Ref<misskey.entities.Note>; +}) { + const appearNote = props.appearNote; + const connection = $i ? stream : null; + + function onStreamNoteUpdated(data): void { + const { type, id, body } = data; + + if (id !== appearNote.value.id) return; + + switch (type) { + case 'reacted': { + const reaction = body.reaction; + + const updated = JSON.parse(JSON.stringify(appearNote.value)); + + if (body.emoji) { + const emojis = appearNote.value.emojis || []; + if (!emojis.includes(body.emoji)) { + updated.emojis = [...emojis, body.emoji]; + } + } + + // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる + const currentCount = (appearNote.value.reactions || {})[reaction] || 0; + + updated.reactions[reaction] = currentCount + 1; + + if ($i && (body.userId === $i.id)) { + updated.myReaction = reaction; + } + + appearNote.value = updated; + break; + } + + case 'unreacted': { + const reaction = body.reaction; + + const updated = JSON.parse(JSON.stringify(appearNote.value)); + + // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる + const currentCount = (appearNote.value.reactions || {})[reaction] || 0; + + updated.reactions[reaction] = Math.max(0, currentCount - 1); + + if ($i && (body.userId === $i.id)) { + updated.myReaction = null; + } + + appearNote.value = updated; + break; + } + + case 'pollVoted': { + const choice = body.choice; + + const updated = JSON.parse(JSON.stringify(appearNote.value)); + + const choices = [...appearNote.value.poll.choices]; + choices[choice] = { + ...choices[choice], + votes: choices[choice].votes + 1, + ...($i && (body.userId === $i.id) ? { + isVoted: true + } : {}) + }; + + updated.poll.choices = choices; + + appearNote.value = updated; + break; + } + + case 'deleted': { + const updated = JSON.parse(JSON.stringify(appearNote.value)); + updated.value = true; + appearNote.value = updated; + break; + } + } + } + + function capture(withHandler = false): void { + if (connection) { + // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する + connection.send(document.body.contains(props.rootEl.value) ? 'sr' : 's', { id: appearNote.value.id }); + if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated); + } + } + + function decapture(withHandler = false): void { + if (connection) { + connection.send('un', { + id: appearNote.value.id, + }); + if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated); + } + } + + function onStreamConnected() { + capture(false); + } + + capture(true); + if (connection) { + connection.on('_connected_', onStreamConnected); + } + + onUnmounted(() => { + decapture(true); + if (connection) { + connection.off('_connected_', onStreamConnected); + } + }); +} diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index 745d323100..cd358d29d0 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -97,7 +97,7 @@ export const defaultStore = markRaw(new Storage('base', { tl: { where: 'deviceAccount', default: { - src: 'home', + src: 'home' as 'home' | 'local' | 'social' | 'global', arg: null } }, @@ -160,7 +160,7 @@ export const defaultStore = markRaw(new Storage('base', { }, useReactionPickerForContextMenu: { where: 'device', - default: true + default: false }, showGapBetweenNotesInTimeline: { where: 'device', diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss index b95a5c3950..c1d47ffd08 100644 --- a/packages/client/src/style.scss +++ b/packages/client/src/style.scss @@ -26,6 +26,7 @@ html { background-size: cover; background-position: center; color: var(--fg); + accent-color: var(--accent); overflow: auto; overflow-wrap: break-word; font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; diff --git a/packages/client/src/ui/_common_/stream-indicator.vue b/packages/client/src/ui/_common_/stream-indicator.vue index c75c6d1c0a..5e811e1b88 100644 --- a/packages/client/src/ui/_common_/stream-indicator.vue +++ b/packages/client/src/ui/_common_/stream-indicator.vue @@ -8,39 +8,28 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import * as os from '@/os'; +<script lang="ts" setup> +import { onUnmounted } from 'vue'; import { stream } from '@/stream'; -export default defineComponent({ - data() { - return { - hasDisconnected: false, - } - }, - computed: { - stream() { - return stream; - }, - }, - created() { - stream.on('_disconnected_', this.onDisconnected); - }, - beforeUnmount() { - stream.off('_disconnected_', this.onDisconnected); - }, - methods: { - onDisconnected() { - this.hasDisconnected = true; - }, - resetDisconnected() { - this.hasDisconnected = false; - }, - reload() { - location.reload(); - }, - } +let hasDisconnected = $ref(false); + +function onDisconnected() { + hasDisconnected = true; +} + +function resetDisconnected() { + hasDisconnected = false; +} + +function reload() { + location.reload(); +} + +stream.on('_disconnected_', onDisconnected); + +onUnmounted(() => { + stream.off('_disconnected_', onDisconnected); }); </script> diff --git a/packages/client/src/ui/_common_/upload.vue b/packages/client/src/ui/_common_/upload.vue index a1c5dcdecc..ab7678a505 100644 --- a/packages/client/src/ui/_common_/upload.vue +++ b/packages/client/src/ui/_common_/upload.vue @@ -17,18 +17,12 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import * as os from '@/os'; -export default defineComponent({ - data() { - return { - uploads: os.uploads, - zIndex: os.claimZIndex('high'), - }; - }, -}); +const uploads = os.uploads; +const zIndex = os.claimZIndex('high'); </script> <style lang="scss" scoped> diff --git a/packages/client/src/ui/deck/column.vue b/packages/client/src/ui/deck/column.vue index d3c7cf8213..1982d92ad3 100644 --- a/packages/client/src/ui/deck/column.vue +++ b/packages/client/src/ui/deck/column.vue @@ -224,7 +224,7 @@ export default defineComponent({ // Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう // SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately - setTimeout(() => { + window.setTimeout(() => { this.dragging = true; }, 10); }, diff --git a/packages/client/src/ui/deck/direct-column.vue b/packages/client/src/ui/deck/direct-column.vue index 4206b09b97..ca70f693c3 100644 --- a/packages/client/src/ui/deck/direct-column.vue +++ b/packages/client/src/ui/deck/direct-column.vue @@ -2,43 +2,26 @@ <XColumn :column="column" :is-stacked="isStacked"> <template #header><i class="fas fa-envelope" style="margin-right: 8px;"></i>{{ column.name }}</template> - <XNotes :pagination="pagination" @before="before()" @after="after()"/> + <XNotes :pagination="pagination"/> </XColumn> </template> -<script lang="ts"> -import { computed, defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed } from 'vue'; import XColumn from './column.vue'; import XNotes from '@/components/notes.vue'; import * as os from '@/os'; -export default defineComponent({ - components: { - XColumn, - XNotes - }, +const props = defineProps<{ + column: Record<string, unknown>; // TODO + isStacked: boolean; +}>(); - props: { - column: { - type: Object, - required: true - }, - isStacked: { - type: Boolean, - required: true - } - }, - - data() { - return { - pagination: { - endpoint: 'notes/mentions', - limit: 10, - params: computed(() => ({ - visibility: 'specified' - })), - }, - } - }, -}); +const pagination = { + point: 'notes/mentions' as const, + limit: 10, + params: computed(() => ({ + visibility: 'specified' as const, + })), +}; </script> diff --git a/packages/client/src/ui/deck/mentions-column.vue b/packages/client/src/ui/deck/mentions-column.vue index 4b8dc0c4ee..6822e7ef06 100644 --- a/packages/client/src/ui/deck/mentions-column.vue +++ b/packages/client/src/ui/deck/mentions-column.vue @@ -2,40 +2,23 @@ <XColumn :column="column" :is-stacked="isStacked"> <template #header><i class="fas fa-at" style="margin-right: 8px;"></i>{{ column.name }}</template> - <XNotes :pagination="pagination" @before="before()" @after="after()"/> + <XNotes :pagination="pagination"/> </XColumn> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import XColumn from './column.vue'; import XNotes from '@/components/notes.vue'; import * as os from '@/os'; -export default defineComponent({ - components: { - XColumn, - XNotes - }, +const props = defineProps<{ + column: Record<string, unknown>; // TODO + isStacked: boolean; +}>(); - props: { - column: { - type: Object, - required: true - }, - isStacked: { - type: Boolean, - required: true - } - }, - - data() { - return { - pagination: { - endpoint: 'notes/mentions', - limit: 10, - }, - } - }, -}); +const pagination = { + endpoint: 'notes/mentions' as const, + limit: 10, +}; </script> diff --git a/packages/client/src/widgets/calendar.vue b/packages/client/src/widgets/calendar.vue index d16d3424b6..b0e3edcb12 100644 --- a/packages/client/src/widgets/calendar.vue +++ b/packages/client/src/widgets/calendar.vue @@ -104,9 +104,9 @@ const tick = () => { tick(); -const intervalId = setInterval(tick, 1000); +const intervalId = window.setInterval(tick, 1000); onUnmounted(() => { - clearInterval(intervalId); + window.clearInterval(intervalId); }); defineExpose<WidgetComponentExpose>({ diff --git a/packages/client/src/widgets/digital-clock.vue b/packages/client/src/widgets/digital-clock.vue index 637b0368be..62f052a692 100644 --- a/packages/client/src/widgets/digital-clock.vue +++ b/packages/client/src/widgets/digital-clock.vue @@ -67,12 +67,12 @@ const tick = () => { tick(); watch(() => widgetProps.showMs, () => { - if (intervalId) clearInterval(intervalId); - intervalId = setInterval(tick, widgetProps.showMs ? 10 : 1000); + if (intervalId) window.clearInterval(intervalId); + intervalId = window.setInterval(tick, widgetProps.showMs ? 10 : 1000); }, { immediate: true }); onUnmounted(() => { - clearInterval(intervalId); + window.clearInterval(intervalId); }); defineExpose<WidgetComponentExpose>({ diff --git a/packages/client/src/widgets/federation.vue b/packages/client/src/widgets/federation.vue index 5d53b683b4..ed7350188e 100644 --- a/packages/client/src/widgets/federation.vue +++ b/packages/client/src/widgets/federation.vue @@ -66,9 +66,9 @@ const fetch = async () => { onMounted(() => { fetch(); - const intervalId = setInterval(fetch, 1000 * 60); + const intervalId = window.setInterval(fetch, 1000 * 60); onUnmounted(() => { - clearInterval(intervalId); + window.clearInterval(intervalId); }); }); diff --git a/packages/client/src/widgets/memo.vue b/packages/client/src/widgets/memo.vue index 3dfc6eb5fa..450598f65a 100644 --- a/packages/client/src/widgets/memo.vue +++ b/packages/client/src/widgets/memo.vue @@ -51,8 +51,8 @@ const saveMemo = () => { const onChange = () => { changed.value = true; - clearTimeout(timeoutId); - timeoutId = setTimeout(saveMemo, 1000); + window.clearTimeout(timeoutId); + timeoutId = window.setTimeout(saveMemo, 1000); }; watch(() => defaultStore.reactiveState.memo, newText => { diff --git a/packages/client/src/widgets/online-users.vue b/packages/client/src/widgets/online-users.vue index 2d47688697..1746a8314e 100644 --- a/packages/client/src/widgets/online-users.vue +++ b/packages/client/src/widgets/online-users.vue @@ -45,9 +45,9 @@ const tick = () => { onMounted(() => { tick(); - const intervalId = setInterval(tick, 1000 * 15); + const intervalId = window.setInterval(tick, 1000 * 15); onUnmounted(() => { - clearInterval(intervalId); + window.clearInterval(intervalId); }); }); diff --git a/packages/client/src/widgets/rss.vue b/packages/client/src/widgets/rss.vue index 7a2272d744..9e2e503602 100644 --- a/packages/client/src/widgets/rss.vue +++ b/packages/client/src/widgets/rss.vue @@ -62,9 +62,9 @@ watch(() => widgetProps.url, tick); onMounted(() => { tick(); - const intervalId = setInterval(tick, 60000); + const intervalId = window.setInterval(tick, 60000); onUnmounted(() => { - clearInterval(intervalId); + window.clearInterval(intervalId); }); }); diff --git a/packages/client/src/widgets/server-metric/disk.vue b/packages/client/src/widgets/server-metric/disk.vue index 650101b0ee..052991b554 100644 --- a/packages/client/src/widgets/server-metric/disk.vue +++ b/packages/client/src/widgets/server-metric/disk.vue @@ -10,32 +10,19 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; import XPie from './pie.vue'; import bytes from '@/filters/bytes'; -export default defineComponent({ - components: { - XPie - }, - props: { - meta: { - required: true, - } - }, - data() { - return { - usage: this.meta.fs.used / this.meta.fs.total, - total: this.meta.fs.total, - used: this.meta.fs.used, - available: this.meta.fs.total - this.meta.fs.used, - }; - }, - methods: { - bytes - } -}); +const props = defineProps<{ + meta: any; // TODO +}>(); + +const usage = $computed(() => props.meta.fs.used / props.meta.fs.total); +const total = $computed(() => props.meta.fs.total); +const used = $computed(() => props.meta.fs.used); +const available = $computed(() => props.meta.fs.total - props.meta.fs.used); </script> <style lang="scss" scoped> diff --git a/packages/client/src/widgets/server-metric/pie.vue b/packages/client/src/widgets/server-metric/pie.vue index 38dcf6fcd9..868dbc0484 100644 --- a/packages/client/src/widgets/server-metric/pie.vue +++ b/packages/client/src/widgets/server-metric/pie.vue @@ -20,30 +20,17 @@ </svg> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { } from 'vue'; -export default defineComponent({ - props: { - value: { - type: Number, - required: true - } - }, - data() { - return { - r: 0.45 - }; - }, - computed: { - color(): string { - return `hsl(${180 - (this.value * 180)}, 80%, 70%)`; - }, - strokeDashoffset(): number { - return (1 - this.value) * (Math.PI * (this.r * 2)); - } - } -}); +const props = defineProps<{ + value: number; +}>(); + +const r = 0.45; + +const color = $computed(() => `hsl(${180 - (props.value * 180)}, 80%, 70%)`); +const strokeDashoffset = $computed(() => (1 - props.value) * (Math.PI * (r * 2))); </script> <style lang="scss" scoped> diff --git a/packages/client/src/widgets/slideshow.vue b/packages/client/src/widgets/slideshow.vue index ac0c6c9e07..7b2e539685 100644 --- a/packages/client/src/widgets/slideshow.vue +++ b/packages/client/src/widgets/slideshow.vue @@ -59,7 +59,7 @@ const change = () => { slideB.value.style.backgroundImage = img; slideB.value.classList.add('anime'); - setTimeout(() => { + window.setTimeout(() => { // 既にこのウィジェットがunmountされていたら要素がない if (slideA.value == null) return; @@ -101,9 +101,9 @@ onMounted(() => { fetch(); } - const intervalId = setInterval(change, 10000); + const intervalId = window.setInterval(change, 10000); onUnmounted(() => { - clearInterval(intervalId); + window.clearInterval(intervalId); }); }); diff --git a/packages/client/src/widgets/trends.vue b/packages/client/src/widgets/trends.vue index 3905daa673..5768a8d5d1 100644 --- a/packages/client/src/widgets/trends.vue +++ b/packages/client/src/widgets/trends.vue @@ -60,9 +60,9 @@ const fetch = () => { onMounted(() => { fetch(); - const intervalId = setInterval(fetch, 1000 * 60); + const intervalId = window.setInterval(fetch, 1000 * 60); onUnmounted(() => { - clearInterval(intervalId); + window.clearInterval(intervalId); }); }); diff --git a/packages/client/yarn.lock b/packages/client/yarn.lock index 1d1f5dcca3..b972800dca 100644 --- a/packages/client/yarn.lock +++ b/packages/client/yarn.lock @@ -415,11 +415,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50" integrity sha512-LSw8TZt12ZudbpHc6EkIyDM3nHVWKYrAvGy6EAJfNfjusbwnThqjqxUKKRwuV3iWYeW/LYMzNgaq3MaLffQ2xA== -"@types/node@16.11.12": - version "16.11.12" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.12.tgz#ac7fb693ac587ee182c3780c26eb65546a1a3c10" - integrity sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw== - "@types/node@^14.11.8", "@types/node@^14.14.31": version "14.17.9" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.9.tgz#b97c057e6138adb7b720df2bd0264b03c9f504fd" |