summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortamaina <tamaina@hotmail.co.jp>2022-01-16 18:01:23 +0900
committertamaina <tamaina@hotmail.co.jp>2022-01-16 18:01:23 +0900
commit04bafc5aeef5dc5db41679ee959ceb300ceb6187 (patch)
tree1bfcc2cc8e0a6305fa98567db01d1216579082da
parentMerge branch 'develop' into pizzax-indexeddb (diff)
parentwip: refactor(client): migrate components to composition api (diff)
downloadmisskey-04bafc5aeef5dc5db41679ee959ceb300ceb6187.tar.gz
misskey-04bafc5aeef5dc5db41679ee959ceb300ceb6187.tar.bz2
misskey-04bafc5aeef5dc5db41679ee959ceb300ceb6187.zip
Merge branch 'develop' into pizzax-indexeddb
-rw-r--r--CHANGELOG.md2
-rw-r--r--packages/backend/package.json1
-rw-r--r--packages/backend/src/misc/gen-identicon.ts (renamed from packages/backend/src/misc/gen-avatar.ts)7
-rw-r--r--packages/backend/src/models/repositories/user.ts2
-rw-r--r--packages/backend/src/queue/index.ts10
-rw-r--r--packages/backend/src/queue/processors/db/export-custom-emojis.ts12
-rw-r--r--packages/backend/src/queue/processors/db/import-custom-emojis.ts84
-rw-r--r--packages/backend/src/queue/processors/db/index.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts39
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts37
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/delete.ts (renamed from packages/backend/src/server/api/endpoints/admin/emoji/remove.ts)2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts21
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts39
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts35
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts35
-rw-r--r--packages/backend/src/server/index.ts6
-rw-r--r--packages/backend/yarn.lock95
-rw-r--r--packages/client/.eslintrc.js4
-rw-r--r--packages/client/package.json1
-rw-r--r--packages/client/src/components/MkNoteSub.vue (renamed from packages/client/src/components/note.sub.vue)75
-rw-r--r--packages/client/src/components/analog-clock.vue2
-rw-r--r--packages/client/src/components/captcha.vue2
-rw-r--r--packages/client/src/components/form/input.vue4
-rw-r--r--packages/client/src/components/form/select.vue4
-rw-r--r--packages/client/src/components/global/a.vue202
-rw-r--r--packages/client/src/components/global/ad.vue6
-rw-r--r--packages/client/src/components/global/avatar.vue94
-rw-r--r--packages/client/src/components/global/loading.vue30
-rw-r--r--packages/client/src/components/global/misskey-flavored-markdown.vue22
-rw-r--r--packages/client/src/components/global/sticky-container.vue2
-rw-r--r--packages/client/src/components/global/time.vue112
-rw-r--r--packages/client/src/components/global/user-name.vue21
-rw-r--r--packages/client/src/components/image-viewer.vue34
-rw-r--r--packages/client/src/components/img-with-blurhash.vue86
-rw-r--r--packages/client/src/components/instance-ticker.vue41
-rw-r--r--packages/client/src/components/link.vue84
-rw-r--r--packages/client/src/components/media-banner.vue46
-rw-r--r--packages/client/src/components/mini-chart.vue4
-rw-r--r--packages/client/src/components/note-detailed.vue862
-rw-r--r--packages/client/src/components/note-header.vue28
-rw-r--r--packages/client/src/components/note-preview.vue18
-rw-r--r--packages/client/src/components/note-simple.vue34
-rw-r--r--packages/client/src/components/note.vue868
-rw-r--r--packages/client/src/components/notes.vue6
-rw-r--r--packages/client/src/components/notification-toast.vue2
-rw-r--r--packages/client/src/components/notifications.vue9
-rw-r--r--packages/client/src/components/post-form.vue1048
-rw-r--r--packages/client/src/components/reaction-icon.vue28
-rw-r--r--packages/client/src/components/reaction-tooltip.vue35
-rw-r--r--packages/client/src/components/reactions-viewer.details.vue45
-rw-r--r--packages/client/src/components/reactions-viewer.vue34
-rw-r--r--packages/client/src/components/renote.details.vue34
-rw-r--r--packages/client/src/components/ripple.vue2
-rw-r--r--packages/client/src/components/signin-dialog.vue42
-rw-r--r--packages/client/src/components/signup-dialog.vue46
-rw-r--r--packages/client/src/components/sub-note-content.vue38
-rw-r--r--packages/client/src/components/toast.vue2
-rw-r--r--packages/client/src/components/ui/button.vue6
-rw-r--r--packages/client/src/components/ui/modal.vue2
-rw-r--r--packages/client/src/components/ui/pagination.vue5
-rw-r--r--packages/client/src/components/url-preview.vue156
-rw-r--r--packages/client/src/components/user-online-indicator.vue31
-rw-r--r--packages/client/src/components/visibility-picker.vue81
-rw-r--r--packages/client/src/components/waiting-dialog.vue57
-rw-r--r--packages/client/src/directives/anim.ts2
-rw-r--r--packages/client/src/directives/tooltip.ts18
-rw-r--r--packages/client/src/directives/user-preview.ts30
-rw-r--r--packages/client/src/os.ts6
-rw-r--r--packages/client/src/pages/_error_.vue89
-rw-r--r--packages/client/src/pages/admin/abuses.vue6
-rw-r--r--packages/client/src/pages/admin/ads.vue4
-rw-r--r--packages/client/src/pages/admin/announcements.vue4
-rw-r--r--packages/client/src/pages/admin/bot-protection.vue4
-rw-r--r--packages/client/src/pages/admin/database.vue4
-rw-r--r--packages/client/src/pages/admin/email-settings.vue4
-rw-r--r--packages/client/src/pages/admin/emoji-edit-dialog.vue2
-rw-r--r--packages/client/src/pages/admin/emojis.vue357
-rw-r--r--packages/client/src/pages/admin/files.vue6
-rw-r--r--packages/client/src/pages/admin/index.vue8
-rw-r--r--packages/client/src/pages/admin/instance-block.vue4
-rw-r--r--packages/client/src/pages/admin/integrations.discord.vue4
-rw-r--r--packages/client/src/pages/admin/integrations.github.vue4
-rw-r--r--packages/client/src/pages/admin/integrations.twitter.vue4
-rw-r--r--packages/client/src/pages/admin/integrations.vue4
-rw-r--r--packages/client/src/pages/admin/object-storage.vue4
-rw-r--r--packages/client/src/pages/admin/other-settings.vue4
-rw-r--r--packages/client/src/pages/admin/overview.vue2
-rw-r--r--packages/client/src/pages/admin/proxy-account.vue4
-rw-r--r--packages/client/src/pages/admin/queue.vue2
-rw-r--r--packages/client/src/pages/admin/relays.vue4
-rw-r--r--packages/client/src/pages/admin/security.vue4
-rw-r--r--packages/client/src/pages/admin/settings.vue4
-rw-r--r--packages/client/src/pages/admin/users.vue6
-rw-r--r--packages/client/src/pages/announcements.vue2
-rw-r--r--packages/client/src/pages/channel.vue2
-rw-r--r--packages/client/src/pages/channels.vue6
-rw-r--r--packages/client/src/pages/clip.vue2
-rw-r--r--packages/client/src/pages/drive.vue28
-rw-r--r--packages/client/src/pages/emojis.emoji.vue42
-rw-r--r--packages/client/src/pages/emojis.vue76
-rw-r--r--packages/client/src/pages/explore.vue4
-rw-r--r--packages/client/src/pages/favorites.vue11
-rw-r--r--packages/client/src/pages/featured.vue2
-rw-r--r--packages/client/src/pages/federation.vue93
-rw-r--r--packages/client/src/pages/follow-requests.vue88
-rw-r--r--packages/client/src/pages/gallery/index.vue10
-rw-r--r--packages/client/src/pages/gallery/post.vue2
-rw-r--r--packages/client/src/pages/mentions.vue2
-rw-r--r--packages/client/src/pages/messages.vue2
-rw-r--r--packages/client/src/pages/messaging/messaging-room.vue6
-rw-r--r--packages/client/src/pages/my-antennas/create.vue60
-rw-r--r--packages/client/src/pages/my-antennas/index.vue2
-rw-r--r--packages/client/src/pages/my-clips/index.vue103
-rw-r--r--packages/client/src/pages/my-groups/index.vue6
-rw-r--r--packages/client/src/pages/my-lists/index.vue63
-rw-r--r--packages/client/src/pages/note.vue12
-rw-r--r--packages/client/src/pages/notifications.vue106
-rw-r--r--packages/client/src/pages/page.vue2
-rw-r--r--packages/client/src/pages/pages.vue6
-rw-r--r--packages/client/src/pages/preview.vue24
-rw-r--r--packages/client/src/pages/reset-password.vue69
-rw-r--r--packages/client/src/pages/search.vue44
-rw-r--r--packages/client/src/pages/settings/account-info.vue2
-rw-r--r--packages/client/src/pages/settings/accounts.vue4
-rw-r--r--packages/client/src/pages/settings/api.vue4
-rw-r--r--packages/client/src/pages/settings/apps.vue6
-rw-r--r--packages/client/src/pages/settings/custom-css.vue2
-rw-r--r--packages/client/src/pages/settings/deck.vue4
-rw-r--r--packages/client/src/pages/settings/delete-account.vue4
-rw-r--r--packages/client/src/pages/settings/drive.vue4
-rw-r--r--packages/client/src/pages/settings/email.vue2
-rw-r--r--packages/client/src/pages/settings/general.vue4
-rw-r--r--packages/client/src/pages/settings/import-export.vue4
-rw-r--r--packages/client/src/pages/settings/index.vue9
-rw-r--r--packages/client/src/pages/settings/instance-mute.vue5
-rw-r--r--packages/client/src/pages/settings/integration.vue2
-rw-r--r--packages/client/src/pages/settings/menu.vue4
-rw-r--r--packages/client/src/pages/settings/mute-block.vue51
-rw-r--r--packages/client/src/pages/settings/notifications.vue4
-rw-r--r--packages/client/src/pages/settings/other.vue4
-rw-r--r--packages/client/src/pages/settings/plugin.install.vue4
-rw-r--r--packages/client/src/pages/settings/plugin.vue4
-rw-r--r--packages/client/src/pages/settings/privacy.vue90
-rw-r--r--packages/client/src/pages/settings/profile.vue4
-rw-r--r--packages/client/src/pages/settings/reaction.vue4
-rw-r--r--packages/client/src/pages/settings/security.vue6
-rw-r--r--packages/client/src/pages/settings/sounds.vue4
-rw-r--r--packages/client/src/pages/settings/theme.install.vue121
-rw-r--r--packages/client/src/pages/settings/theme.manage.vue4
-rw-r--r--packages/client/src/pages/settings/theme.vue4
-rw-r--r--packages/client/src/pages/settings/word-mute.vue4
-rw-r--r--packages/client/src/pages/share.vue2
-rw-r--r--packages/client/src/pages/signup-complete.vue56
-rw-r--r--packages/client/src/pages/tag.vue45
-rw-r--r--packages/client/src/pages/theme-editor.vue300
-rw-r--r--packages/client/src/pages/timeline.tutorial.vue24
-rw-r--r--packages/client/src/pages/timeline.vue254
-rw-r--r--packages/client/src/pages/user/clips.vue2
-rw-r--r--packages/client/src/pages/user/follow-list.vue57
-rw-r--r--packages/client/src/pages/user/gallery.vue2
-rw-r--r--packages/client/src/pages/user/index.activity.vue27
-rw-r--r--packages/client/src/pages/user/pages.vue39
-rw-r--r--packages/client/src/pages/user/reactions.vue42
-rw-r--r--packages/client/src/pizzax.ts2
-rw-r--r--packages/client/src/router.ts8
-rw-r--r--packages/client/src/scripts/autocomplete.ts26
-rw-r--r--packages/client/src/scripts/check-word-mute.ts2
-rw-r--r--packages/client/src/scripts/get-note-menu.ts310
-rw-r--r--packages/client/src/scripts/physics.ts4
-rw-r--r--packages/client/src/scripts/theme.ts4
-rw-r--r--packages/client/src/scripts/use-leave-guard.ts34
-rw-r--r--packages/client/src/scripts/use-note-capture.ts123
-rw-r--r--packages/client/src/store.ts4
-rw-r--r--packages/client/src/style.scss1
-rw-r--r--packages/client/src/ui/_common_/stream-indicator.vue51
-rw-r--r--packages/client/src/ui/_common_/upload.vue14
-rw-r--r--packages/client/src/ui/deck/column.vue2
-rw-r--r--packages/client/src/ui/deck/direct-column.vue45
-rw-r--r--packages/client/src/ui/deck/mentions-column.vue39
-rw-r--r--packages/client/src/widgets/calendar.vue4
-rw-r--r--packages/client/src/widgets/digital-clock.vue6
-rw-r--r--packages/client/src/widgets/federation.vue4
-rw-r--r--packages/client/src/widgets/memo.vue4
-rw-r--r--packages/client/src/widgets/online-users.vue4
-rw-r--r--packages/client/src/widgets/rss.vue4
-rw-r--r--packages/client/src/widgets/server-metric/disk.vue33
-rw-r--r--packages/client/src/widgets/server-metric/pie.vue33
-rw-r--r--packages/client/src/widgets/slideshow.vue6
-rw-r--r--packages/client/src/widgets/trends.vue4
-rw-r--r--packages/client/yarn.lock5
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}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;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}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;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"