diff --git a/CHANGELOG.md b/CHANGELOG.md index 47e13ba5a5..4def20d06c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,16 @@ --> +## 13.x.x (unreleased) + +### General +- タイムラインにフォロイーの行った他人へのリプライを含めるかどうかの設定をアカウントに保存するのをやめるように + - 今後はAPI呼び出し時およびストリーミング接続時に設定するようになります + +### Client +- 開発者モードを追加 +- AiScriptを0.13.3に更新 + ## 13.12.2 ## NOTE diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0b7108fe6d..340698a12b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -52,6 +52,8 @@ addToList: "リストに追加" sendMessage: "メッセージを送信" copyRSS: "RSSをコピー" copyUsername: "ユーザー名をコピー" +copyUserId: "ユーザーIDをコピー" +copyNoteId: "ノートIDをコピー" searchUser: "ユーザーを検索" reply: "返信" loadMore: "もっと見る" @@ -823,6 +825,7 @@ translatedFrom: "{x}から翻訳" accountDeletionInProgress: "アカウントの削除が進行中です" usernameInfo: "サーバー上であなたのアカウントを一意に識別するための名前。アルファベット(a~z, A~Z)、数字(0~9)、およびアンダーバー(_)が使用できます。ユーザー名は後から変更することは出来ません。" aiChanMode: "藍モード" +devMode: "開発者モード" keepCw: "CWを維持する" pubSub: "Pub/Subのアカウント" lastCommunication: "直近の通信" diff --git a/packages/backend/migration/1684206886988-remove-showTimelineReplies.js b/packages/backend/migration/1684206886988-remove-showTimelineReplies.js new file mode 100644 index 0000000000..690653bd7c --- /dev/null +++ b/packages/backend/migration/1684206886988-remove-showTimelineReplies.js @@ -0,0 +1,11 @@ +export class RemoveShowTimelineReplies1684206886988 { + name = 'RemoveShowTimelineReplies1684206886988' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "showTimelineReplies"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" ADD "showTimelineReplies" boolean NOT NULL DEFAULT false`); + } +} diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index c6e1075389..744f999596 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -144,7 +144,7 @@ export function loadConfig() { const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json'); const clientManifest = clientManifestExists ? JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8')) - : { 'src/init.ts': { file: 'src/init.ts' } }; + : { 'src/_boot_.ts': { file: 'src/_boot_.ts' } }; const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; const mixin = {} as Mixin; @@ -165,7 +165,7 @@ export function loadConfig() { mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`; mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`; mixin.userAgent = `Misskey/${meta.version} (${config.url})`; - mixin.clientEntry = clientManifest['src/init.ts']; + mixin.clientEntry = clientManifest['src/_boot_.ts']; mixin.clientManifestExists = clientManifestExists; const externalMediaProxy = config.mediaProxy ? diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index 0cee2076bf..bf50a1cded 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -208,7 +208,7 @@ export class QueryService { } @bindThis - public generateRepliesQuery(q: SelectQueryBuilder, me?: Pick | null): void { + public generateRepliesQuery(q: SelectQueryBuilder, withReplies: boolean, me?: Pick | null): void { if (me == null) { q.andWhere(new Brackets(qb => { qb .where('note.replyId IS NULL') // 返信ではない @@ -217,7 +217,7 @@ export class QueryService { .andWhere('note.replyUserId = note.userId'); })); })); - } else if (!me.showTimelineReplies) { + } else if (!withReplies) { q.andWhere(new Brackets(qb => { qb .where('note.replyId IS NULL') // 返信ではない .orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信 diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index eea1d1b848..f52ebed107 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -32,6 +32,8 @@ import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import type { AccountMoveService } from '@/core/AccountMoveService.js'; +import { checkHttps } from '@/misc/check-https.js'; import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { extractApHashtags } from './tag.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -42,8 +44,6 @@ import type { ApLoggerService } from '../ApLoggerService.js'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports import type { ApImageService } from './ApImageService.js'; import type { IActor, IObject } from '../type.js'; -import type { AccountMoveService } from '@/core/AccountMoveService.js'; -import { checkHttps } from '@/misc/check-https.js'; const nameLength = 128; const summaryLength = 2048; @@ -306,7 +306,6 @@ export class ApPersonService implements OnModuleInit { tags, isBot, isCat: (person as any).isCat === true, - showTimelineReplies: false, })) as RemoteUser; await transactionalEntityManager.save(new UserProfile({ @@ -696,7 +695,7 @@ export class ApPersonService implements OnModuleInit { if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) { return 'skip: dst.alsoKnownAs is empty'; } - if (!dst.alsoKnownAs?.includes(src.uri)) { + if (!dst.alsoKnownAs.includes(src.uri)) { return 'skip: alsoKnownAs does not include from.uri'; } diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 7f61e1d6f3..bfd506ea86 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -466,7 +466,6 @@ export class UserEntityService implements OnModuleInit { mutedInstances: profile!.mutedInstances, mutingNotificationTypes: profile!.mutingNotificationTypes, emailNotificationTypes: profile!.emailNotificationTypes, - showTimelineReplies: user.showTimelineReplies ?? falsy, achievements: profile!.achievements, loggedInDays: profile!.loggedInDates.length, policies: this.roleService.getUserPolicies(user.id), diff --git a/packages/backend/src/models/entities/User.ts b/packages/backend/src/models/entities/User.ts index 8e10f999b6..6669890cf6 100644 --- a/packages/backend/src/models/entities/User.ts +++ b/packages/backend/src/models/entities/User.ts @@ -232,12 +232,6 @@ export class User { }) public followersUri: string | null; - @Column('boolean', { - default: false, - comment: 'Whether to show users replying to other users in the timeline.', - }) - public showTimelineReplies: boolean; - @Index({ unique: true }) @Column('char', { length: 16, nullable: true, unique: true, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 74be00a8b8..d10f690a32 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -141,7 +141,6 @@ export const paramDef = { preventAiLearning: { type: 'boolean' }, isBot: { type: 'boolean' }, isCat: { type: 'boolean' }, - showTimelineReplies: { type: 'boolean' }, injectFeaturedNote: { type: 'boolean' }, receiveAnnouncementEmail: { type: 'boolean' }, alwaysMarkNsfw: { type: 'boolean' }, @@ -239,7 +238,6 @@ export default class extends Endpoint { if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus; if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions; if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot; - if (typeof ps.showTimelineReplies === 'boolean') updates.showTimelineReplies = ps.showTimelineReplies; if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index c11c1eac40..88c1ca7f58 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -34,11 +34,8 @@ export const meta = { export const paramDef = { type: 'object', properties: { - withFiles: { - type: 'boolean', - default: false, - description: 'Only show notes that have attached files.', - }, + withFiles: { type: 'boolean', default: false }, + withReplies: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -78,7 +75,7 @@ export default class extends Endpoint { .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - this.queryService.generateRepliesQuery(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); if (me) { this.queryService.generateMutedUserQuery(query, me); this.queryService.generateMutedNoteQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 89abd91c7e..7a3581e6e4 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -46,11 +46,8 @@ export const paramDef = { includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, - withFiles: { - type: 'boolean', - default: false, - description: 'Only show notes that have attached files.', - }, + withFiles: { type: 'boolean', default: false }, + withReplies: { type: 'boolean', default: false }, }, required: [], } as const; @@ -98,7 +95,7 @@ export default class extends Endpoint { .setParameters(followingQuery.getParameters()); this.queryService.generateChannelQuery(query, me); - this.queryService.generateRepliesQuery(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); this.queryService.generateVisibilityQuery(query, me); this.queryService.generateMutedUserQuery(query, me); this.queryService.generateMutedNoteQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index afdafc7c55..2ee549232c 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -36,11 +36,8 @@ export const meta = { export const paramDef = { type: 'object', properties: { - withFiles: { - type: 'boolean', - default: false, - description: 'Only show notes that have attached files.', - }, + withFiles: { type: 'boolean', default: false }, + withReplies: { type: 'boolean', default: false }, fileType: { type: 'array', items: { type: 'string', } }, @@ -86,7 +83,7 @@ export default class extends Endpoint { .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateChannelQuery(query, me); - this.queryService.generateRepliesQuery(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); this.queryService.generateVisibilityQuery(query, me); if (me) this.queryService.generateMutedUserQuery(query, me); if (me) this.queryService.generateMutedNoteQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index c6ee1e5c2b..e1f286439b 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -35,11 +35,8 @@ export const paramDef = { includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, - withFiles: { - type: 'boolean', - default: false, - description: 'Only show notes that have attached files.', - }, + withFiles: { type: 'boolean', default: false }, + withReplies: { type: 'boolean', default: false }, }, required: [], } as const; @@ -84,7 +81,7 @@ export default class extends Endpoint { } this.queryService.generateChannelQuery(query, me); - this.queryService.generateRepliesQuery(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); this.queryService.generateVisibilityQuery(query, me); this.queryService.generateMutedUserQuery(query, me); this.queryService.generateMutedNoteQuery(query, me); diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 5454836fe1..d3339072c1 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -13,6 +13,7 @@ class GlobalTimelineChannel extends Channel { public readonly chName = 'globalTimeline'; public static shouldShare = true; public static requireCredential = false; + private withReplies: boolean; constructor( private metaService: MetaService, @@ -31,6 +32,8 @@ class GlobalTimelineChannel extends Channel { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.gtlAvailable) return; + this.withReplies = params.withReplies as boolean; + // Subscribe events this.subscriber.on('notesStream', this.onNote); } @@ -54,7 +57,7 @@ class GlobalTimelineChannel extends Channel { } // 関係ない返信は除外 - if (note.reply && !this.user!.showTimelineReplies) { + if (note.reply && !this.withReplies) { const reply = note.reply; // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index ee874ad81e..1755aa94cf 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -11,6 +11,7 @@ class HomeTimelineChannel extends Channel { public readonly chName = 'homeTimeline'; public static shouldShare = true; public static requireCredential = true; + private withReplies: boolean; constructor( private noteEntityService: NoteEntityService, @@ -24,6 +25,8 @@ class HomeTimelineChannel extends Channel { @bindThis public async init(params: any) { + this.withReplies = params.withReplies as boolean; + this.subscriber.on('notesStream', this.onNote); } @@ -63,7 +66,7 @@ class HomeTimelineChannel extends Channel { } // 関係ない返信は除外 - if (note.reply && !this.user!.showTimelineReplies) { + if (note.reply && !this.withReplies) { const reply = note.reply; // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 4f7b4e78b6..5a33e13cf5 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -13,6 +13,7 @@ class HybridTimelineChannel extends Channel { public readonly chName = 'hybridTimeline'; public static shouldShare = true; public static requireCredential = true; + private withReplies: boolean; constructor( private metaService: MetaService, @@ -31,6 +32,8 @@ class HybridTimelineChannel extends Channel { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.ltlAvailable) return; + this.withReplies = params.withReplies as boolean; + // Subscribe events this.subscriber.on('notesStream', this.onNote); } @@ -75,7 +78,7 @@ class HybridTimelineChannel extends Channel { if (isInstanceMuted(note, new Set(this.userProfile!.mutedInstances ?? []))) return; // 関係ない返信は除外 - if (note.reply && !this.user!.showTimelineReplies) { + if (note.reply && !this.withReplies) { const reply = note.reply; // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 09b0005ac1..9ca4db8ced 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -12,6 +12,7 @@ class LocalTimelineChannel extends Channel { public readonly chName = 'localTimeline'; public static shouldShare = true; public static requireCredential = false; + private withReplies: boolean; constructor( private metaService: MetaService, @@ -30,6 +31,8 @@ class LocalTimelineChannel extends Channel { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.ltlAvailable) return; + this.withReplies = params.withReplies as boolean; + // Subscribe events this.subscriber.on('notesStream', this.onNote); } @@ -54,7 +57,7 @@ class LocalTimelineChannel extends Channel { } // 関係ない返信は除外 - if (note.reply && this.user && !this.user.showTimelineReplies) { + if (note.reply && this.user && !this.withReplies) { const reply = note.reply; // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return; diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index a6f9145952..fee56e3668 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -246,7 +246,7 @@ export default class Connection { const ch: Channel = channelService.create(id, this); this.channels.push(ch); - ch.init(params); + ch.init(params ?? {}); if (pong) { this.sendMessageToWs('connected', { diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index a7f8210c8e..02684c93b8 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -43,7 +43,6 @@ describe('ユーザー', () => { type MeDetailed = UserDetailedNotMe & misskey.entities.MeDetailed & { - showTimelineReplies: boolean, achievements: object[], loggedInDays: number, policies: object, @@ -160,7 +159,6 @@ describe('ユーザー', () => { mutedInstances: user.mutedInstances, mutingNotificationTypes: user.mutingNotificationTypes, emailNotificationTypes: user.emailNotificationTypes, - showTimelineReplies: user.showTimelineReplies, achievements: user.achievements, loggedInDays: user.loggedInDays, policies: user.policies, @@ -406,7 +404,6 @@ describe('ユーザー', () => { assert.deepStrictEqual(response.mutedInstances, []); assert.deepStrictEqual(response.mutingNotificationTypes, []); assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']); - assert.strictEqual(response.showTimelineReplies, false); assert.deepStrictEqual(response.achievements, []); assert.deepStrictEqual(response.loggedInDays, 0); assert.deepStrictEqual(response.policies, DEFAULT_POLICIES); @@ -470,8 +467,6 @@ describe('ユーザー', () => { { parameters: (): object => ({ isBot: false }) }, { parameters: (): object => ({ isCat: true }) }, { parameters: (): object => ({ isCat: false }) }, - { parameters: (): object => ({ showTimelineReplies: true }) }, - { parameters: (): object => ({ showTimelineReplies: false }) }, { parameters: (): object => ({ injectFeaturedNote: true }) }, { parameters: (): object => ({ injectFeaturedNote: false }) }, { parameters: (): object => ({ receiveAnnouncementEmail: true }) }, diff --git a/packages/frontend/.eslintrc.js b/packages/frontend/.eslintrc.js index e8e0e57d2a..a1e1f57e10 100644 --- a/packages/frontend/.eslintrc.js +++ b/packages/frontend/.eslintrc.js @@ -64,6 +64,7 @@ module.exports = { 'vue/singleline-html-element-content-newline': 'off', // (vue/vue3-recommended disabled the autofix for Vue 2 compatibility) 'vue/v-on-event-hyphenation': ['warn', 'always', { autofix: true }], + 'vue/attribute-hyphenation': ['warn', 'never'], }, globals: { // Node.js diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 2392e510a4..2f754f8aa2 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -19,12 +19,12 @@ "@rollup/plugin-json": "6.0.0", "@rollup/plugin-replace": "5.0.2", "@rollup/pluginutils": "5.0.2", - "@syuilo/aiscript": "0.13.2", + "@syuilo/aiscript": "0.13.3", "@tabler/icons-webfont": "2.17.0", - "@vitejs/plugin-vue": "4.2.2", - "@vue-macros/reactivity-transform": "0.3.6", - "@vue/compiler-sfc": "3.3.1", - "autosize": "5.0.2", + "@vitejs/plugin-vue": "4.2.3", + "@vue-macros/reactivity-transform": "0.3.7", + "@vue/compiler-sfc": "3.3.2", + "autosize": "6.0.1", "broadcast-channel": "4.20.2", "browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3", "buraha": "github:misskey-dev/buraha", @@ -53,7 +53,7 @@ "punycode": "2.3.0", "querystring": "0.2.1", "rndstr": "1.0.0", - "rollup": "3.21.6", + "rollup": "3.22.0", "s-age": "1.1.2", "sanitize-html": "2.10.0", "sass": "1.62.1", @@ -70,31 +70,31 @@ "typescript": "5.0.4", "uuid": "9.0.0", "vanilla-tilt": "1.8.0", - "vite": "4.3.5", - "vue": "3.3.1", + "vite": "4.3.7", + "vue": "3.3.2", "vue-plyr": "7.0.0", "vue-prism-editor": "2.0.0-alpha.2", "vuedraggable": "next" }, "devDependencies": { - "@storybook/addon-actions": "7.0.10", - "@storybook/addon-essentials": "7.0.10", - "@storybook/addon-interactions": "7.0.10", - "@storybook/addon-links": "7.0.10", - "@storybook/addon-storysource": "7.0.10", - "@storybook/addons": "7.0.10", - "@storybook/blocks": "7.0.10", - "@storybook/core-events": "7.0.10", + "@storybook/addon-actions": "7.0.12", + "@storybook/addon-essentials": "7.0.12", + "@storybook/addon-interactions": "7.0.12", + "@storybook/addon-links": "7.0.12", + "@storybook/addon-storysource": "7.0.12", + "@storybook/addons": "7.0.12", + "@storybook/blocks": "7.0.12", + "@storybook/core-events": "7.0.12", "@storybook/jest": "0.1.0", - "@storybook/manager-api": "7.0.10", - "@storybook/preview-api": "7.0.10", - "@storybook/react": "7.0.10", - "@storybook/react-vite": "7.0.10", + "@storybook/manager-api": "7.0.12", + "@storybook/preview-api": "7.0.12", + "@storybook/react": "7.0.12", + "@storybook/react-vite": "7.0.12", "@storybook/testing-library": "0.1.0", - "@storybook/theming": "7.0.10", - "@storybook/types": "7.0.10", - "@storybook/vue3": "7.0.10", - "@storybook/vue3-vite": "7.0.10", + "@storybook/theming": "7.0.12", + "@storybook/types": "7.0.12", + "@storybook/vue3": "7.0.12", + "@storybook/vue3-vite": "7.0.12", "@testing-library/jest-dom": "5.16.5", "@testing-library/vue": "7.0.0", "@types/escape-regexp": "0.0.1", @@ -103,7 +103,7 @@ "@types/gulp-rename": "2.0.2", "@types/matter-js": "0.18.3", "@types/micromatch": "4.0.2", - "@types/node": "20.1.3", + "@types/node": "20.1.7", "@types/punycode": "2.1.0", "@types/sanitize-html": "2.9.0", "@types/seedrandom": "3.0.5", @@ -116,16 +116,16 @@ "@typescript-eslint/eslint-plugin": "5.59.5", "@typescript-eslint/parser": "5.59.5", "@vitest/coverage-c8": "0.31.0", - "@vue/runtime-core": "3.3.1", + "@vue/runtime-core": "3.3.2", "astring": "1.8.4", "chokidar-cli": "3.0.0", "cross-env": "7.0.3", "cypress": "12.12.0", "eslint": "8.40.0", "eslint-plugin-import": "2.27.5", - "eslint-plugin-vue": "9.12.0", + "eslint-plugin-vue": "9.13.0", "fast-glob": "3.2.12", - "happy-dom": "9.16.0", + "happy-dom": "9.18.3", "micromatch": "3.1.10", "msw": "1.2.1", "msw-storybook-addon": "1.8.0", @@ -133,13 +133,13 @@ "react": "18.2.0", "react-dom": "18.2.0", "start-server-and-test": "2.0.0", - "storybook": "7.0.10", + "storybook": "7.0.12", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "summaly": "github:misskey-dev/summaly", "vite-plugin-turbosnap": "1.0.2", "vitest": "0.31.0", "vitest-fetch-mock": "0.2.2", - "vue-eslint-parser": "9.2.1", - "vue-tsc": "1.6.4" + "vue-eslint-parser": "9.3.0", + "vue-tsc": "1.6.5" } } diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts new file mode 100644 index 0000000000..a8efafca61 --- /dev/null +++ b/packages/frontend/src/_boot_.ts @@ -0,0 +1,12 @@ +// https://vitejs.dev/config/build-options.html#build-modulepreload +import 'vite/modulepreload-polyfill'; + +import '@/style.scss'; +import { mainBoot } from './boot/main-boot'; +import { subBoot } from './boot/sub-boot'; + +if (['/share', '/auth', '/miauth'].includes(location.pathname)) { + subBoot(); +} else { + mainBoot(); +} diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts new file mode 100644 index 0000000000..e9d6586f85 --- /dev/null +++ b/packages/frontend/src/boot/common.ts @@ -0,0 +1,263 @@ +import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent, App } from 'vue'; +import { compareVersions } from 'compare-versions'; +import JSON5 from 'json5'; +import widgets from '@/widgets'; +import directives from '@/directives'; +import components from '@/components'; +import { version, ui, lang, updateLocale } from '@/config'; +import { applyTheme } from '@/scripts/theme'; +import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; +import { i18n, updateI18n } from '@/i18n'; +import { confirm, alert, post, popup, toast } from '@/os'; +import { $i, refreshAccount, login, updateAccount, signout } from '@/account'; +import { defaultStore, ColdDeviceStorage } from '@/store'; +import { fetchInstance, instance } from '@/instance'; +import { deviceKind } from '@/scripts/device-kind'; +import { reloadChannel } from '@/scripts/unison-reload'; +import { reactionPicker } from '@/scripts/reaction-picker'; +import { getUrlWithoutLoginId } from '@/scripts/login-id'; +import { getAccountFromId } from '@/scripts/get-account-from-id'; +import { deckStore } from '@/ui/deck/deck-store'; +import { miLocalStorage } from '@/local-storage'; +import { fetchCustomEmojis } from '@/custom-emojis'; +import { mainRouter } from '@/router'; + +export async function common(createVue: () => App) { + console.info(`Misskey v${version}`); + + if (_DEV_) { + console.warn('Development mode!!!'); + + console.info(`vue ${vueVersion}`); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as any).$i = $i; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as any).$store = defaultStore; + + window.addEventListener('error', event => { + console.error(event); + /* + alert({ + type: 'error', + title: 'DEV: Unhandled error', + text: event.message + }); + */ + }); + + window.addEventListener('unhandledrejection', event => { + console.error(event); + /* + alert({ + type: 'error', + title: 'DEV: Unhandled promise rejection', + text: event.reason + }); + */ + }); + } + + const splash = document.getElementById('splash'); + // 念のためnullチェック(HTMLが古い場合があるため(そのうち消す)) + if (splash) splash.addEventListener('transitionend', () => { + splash.remove(); + }); + + let isClientUpdated = false; + + //#region クライアントが更新されたかチェック + const lastVersion = miLocalStorage.getItem('lastVersion'); + if (lastVersion !== version) { + miLocalStorage.setItem('lastVersion', version); + + // テーマリビルドするため + miLocalStorage.removeItem('theme'); + + try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため + if (lastVersion != null && compareVersions(version, lastVersion) === 1) { + isClientUpdated = true; + } + } catch (err) { /* empty */ } + } + //#endregion + + //#region Detect language & fetch translations + const localeVersion = miLocalStorage.getItem('localeVersion'); + const localeOutdated = (localeVersion == null || localeVersion !== version); + if (localeOutdated) { + const res = await window.fetch(`/assets/locales/${lang}.${version}.json`); + if (res.status === 200) { + const newLocale = await res.text(); + const parsedNewLocale = JSON.parse(newLocale); + miLocalStorage.setItem('locale', newLocale); + miLocalStorage.setItem('localeVersion', version); + updateLocale(parsedNewLocale); + updateI18n(parsedNewLocale); + } + } + //#endregion + + // タッチデバイスでCSSの:hoverを機能させる + document.addEventListener('touchend', () => {}, { passive: true }); + + // 一斉リロード + reloadChannel.addEventListener('message', path => { + if (path !== null) location.href = path; + else location.reload(); + }); + + // If mobile, insert the viewport meta tag + if (['smartphone', 'tablet'].includes(deviceKind)) { + const viewport = document.getElementsByName('viewport').item(0); + viewport.setAttribute('content', + `${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`); + } + + //#region Set lang attr + const html = document.documentElement; + html.setAttribute('lang', lang); + //#endregion + + await defaultStore.ready; + await deckStore.ready; + + const fetchInstanceMetaPromise = fetchInstance(); + + fetchInstanceMetaPromise.then(() => { + miLocalStorage.setItem('v', instance.version); + }); + + //#region loginId + const params = new URLSearchParams(location.search); + const loginId = params.get('loginId'); + + if (loginId) { + const target = getUrlWithoutLoginId(location.href); + + if (!$i || $i.id !== loginId) { + const account = await getAccountFromId(loginId); + if (account) { + await login(account.token, target); + } + } + + history.replaceState({ misskey: 'loginId' }, '', target); + } + //#endregion + + // NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため) + watch(defaultStore.reactiveState.darkMode, (darkMode) => { + applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme')); + }, { immediate: miLocalStorage.getItem('theme') == null }); + + const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme')); + const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme')); + + watch(darkTheme, (theme) => { + if (defaultStore.state.darkMode) { + applyTheme(theme); + } + }); + + watch(lightTheme, (theme) => { + if (!defaultStore.state.darkMode) { + applyTheme(theme); + } + }); + + //#region Sync dark mode + if (ColdDeviceStorage.get('syncDeviceDarkMode')) { + defaultStore.set('darkMode', isDeviceDarkmode()); + } + + window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => { + if (ColdDeviceStorage.get('syncDeviceDarkMode')) { + defaultStore.set('darkMode', mql.matches); + } + }); + //#endregion + + fetchInstanceMetaPromise.then(() => { + if (defaultStore.state.themeInitial) { + if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme)); + if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme)); + defaultStore.set('themeInitial', false); + } + }); + + watch(defaultStore.reactiveState.useBlurEffectForModal, v => { + document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none'); + }, { immediate: true }); + + watch(defaultStore.reactiveState.useBlurEffect, v => { + if (v) { + document.documentElement.style.removeProperty('--blur'); + } else { + document.documentElement.style.setProperty('--blur', 'none'); + } + }, { immediate: true }); + + //#region Fetch user + if ($i && $i.token) { + if (_DEV_) { + console.log('account cache found. refreshing...'); + } + + refreshAccount(); + } + //#endregion + + try { + await fetchCustomEmojis(); + } catch (err) { /* empty */ } + + const app = createVue(); + + if (_DEV_) { + app.config.performance = true; + } + + widgets(app); + directives(app); + components(app); + + // https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 + // なぜか2回実行されることがあるため、mountするdivを1つに制限する + const rootEl = ((): HTMLElement => { + const MISSKEY_MOUNT_DIV_ID = 'misskey_app'; + + const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID); + + if (currentRoot) { + console.warn('multiple import detected'); + return currentRoot; + } + + const root = document.createElement('div'); + root.id = MISSKEY_MOUNT_DIV_ID; + document.body.appendChild(root); + return root; + })(); + + app.mount(rootEl); + + // boot.jsのやつを解除 + window.onerror = null; + window.onunhandledrejection = null; + + removeSplash(); + + return { + isClientUpdated, + app, + }; +} + +function removeSplash() { + const splash = document.getElementById('splash'); + if (splash) { + splash.style.opacity = '0'; + splash.style.pointerEvents = 'none'; + } +} diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts new file mode 100644 index 0000000000..c0bfa4603e --- /dev/null +++ b/packages/frontend/src/boot/main-boot.ts @@ -0,0 +1,254 @@ +import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; +import { common } from './common'; +import { version, ui, lang, updateLocale } from '@/config'; +import { i18n, updateI18n } from '@/i18n'; +import { confirm, alert, post, popup, toast } from '@/os'; +import { useStream } from '@/stream'; +import * as sound from '@/scripts/sound'; +import { $i, refreshAccount, login, updateAccount, signout } from '@/account'; +import { defaultStore, ColdDeviceStorage } from '@/store'; +import { makeHotkey } from '@/scripts/hotkey'; +import { reactionPicker } from '@/scripts/reaction-picker'; +import { miLocalStorage } from '@/local-storage'; +import { claimAchievement, claimedAchievements } from '@/scripts/achievements'; +import { mainRouter } from '@/router'; +import { initializeSw } from '@/scripts/initialize-sw'; + +export async function mainBoot() { + const { isClientUpdated } = await common(() => createApp( + new URLSearchParams(window.location.search).has('zen') ? defineAsyncComponent(() => import('@/ui/zen.vue')) : + !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : + ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : + ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : + defineAsyncComponent(() => import('@/ui/universal.vue')), + )); + + reactionPicker.init(); + + if (isClientUpdated && $i) { + popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed'); + } + + const stream = useStream(); + + let reloadDialogShowing = false; + stream.on('_disconnected_', async () => { + if (defaultStore.state.serverDisconnectedBehavior === 'reload') { + location.reload(); + } else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') { + if (reloadDialogShowing) return; + reloadDialogShowing = true; + const { canceled } = await confirm({ + type: 'warning', + title: i18n.ts.disconnectedFromServer, + text: i18n.ts.reloadConfirm, + }); + reloadDialogShowing = false; + if (!canceled) { + location.reload(); + } + } + }); + + for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { + import('../plugin').then(async ({ install }) => { + // Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740 + await new Promise(r => setTimeout(r, 0)); + install(plugin); + }); + } + + const hotkeys = { + 'd': (): void => { + defaultStore.set('darkMode', !defaultStore.state.darkMode); + }, + 's': (): void => { + mainRouter.push('/search'); + }, + }; + + if ($i) { + // only add post shortcuts if logged in + hotkeys['p|n'] = post; + + defaultStore.loaded.then(() => { + if (defaultStore.state.accountSetupWizard !== -1) { + popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed'); + } + }); + + if ($i.isDeleted) { + alert({ + type: 'warning', + text: i18n.ts.accountDeletionInProgress, + }); + } + + const now = new Date(); + const m = now.getMonth() + 1; + const d = now.getDate(); + + if ($i.birthday) { + const bm = parseInt($i.birthday.split('-')[1]); + const bd = parseInt($i.birthday.split('-')[2]); + if (m === bm && d === bd) { + claimAchievement('loggedInOnBirthday'); + } + } + + if (m === 1 && d === 1) { + claimAchievement('loggedInOnNewYearsDay'); + } + + if ($i.loggedInDays >= 3) claimAchievement('login3'); + if ($i.loggedInDays >= 7) claimAchievement('login7'); + if ($i.loggedInDays >= 15) claimAchievement('login15'); + if ($i.loggedInDays >= 30) claimAchievement('login30'); + if ($i.loggedInDays >= 60) claimAchievement('login60'); + if ($i.loggedInDays >= 100) claimAchievement('login100'); + if ($i.loggedInDays >= 200) claimAchievement('login200'); + if ($i.loggedInDays >= 300) claimAchievement('login300'); + if ($i.loggedInDays >= 400) claimAchievement('login400'); + if ($i.loggedInDays >= 500) claimAchievement('login500'); + if ($i.loggedInDays >= 600) claimAchievement('login600'); + if ($i.loggedInDays >= 700) claimAchievement('login700'); + if ($i.loggedInDays >= 800) claimAchievement('login800'); + if ($i.loggedInDays >= 900) claimAchievement('login900'); + if ($i.loggedInDays >= 1000) claimAchievement('login1000'); + + if ($i.notesCount > 0) claimAchievement('notes1'); + if ($i.notesCount >= 10) claimAchievement('notes10'); + if ($i.notesCount >= 100) claimAchievement('notes100'); + if ($i.notesCount >= 500) claimAchievement('notes500'); + if ($i.notesCount >= 1000) claimAchievement('notes1000'); + if ($i.notesCount >= 5000) claimAchievement('notes5000'); + if ($i.notesCount >= 10000) claimAchievement('notes10000'); + if ($i.notesCount >= 20000) claimAchievement('notes20000'); + if ($i.notesCount >= 30000) claimAchievement('notes30000'); + if ($i.notesCount >= 40000) claimAchievement('notes40000'); + if ($i.notesCount >= 50000) claimAchievement('notes50000'); + if ($i.notesCount >= 60000) claimAchievement('notes60000'); + if ($i.notesCount >= 70000) claimAchievement('notes70000'); + if ($i.notesCount >= 80000) claimAchievement('notes80000'); + if ($i.notesCount >= 90000) claimAchievement('notes90000'); + if ($i.notesCount >= 100000) claimAchievement('notes100000'); + + if ($i.followersCount > 0) claimAchievement('followers1'); + if ($i.followersCount >= 10) claimAchievement('followers10'); + if ($i.followersCount >= 50) claimAchievement('followers50'); + if ($i.followersCount >= 100) claimAchievement('followers100'); + if ($i.followersCount >= 300) claimAchievement('followers300'); + if ($i.followersCount >= 500) claimAchievement('followers500'); + if ($i.followersCount >= 1000) claimAchievement('followers1000'); + + if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) { + claimAchievement('passedSinceAccountCreated1'); + } + if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) { + claimAchievement('passedSinceAccountCreated2'); + } + if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) { + claimAchievement('passedSinceAccountCreated3'); + } + + if (claimedAchievements.length >= 30) { + claimAchievement('collectAchievements30'); + } + + window.setInterval(() => { + if (Math.floor(Math.random() * 20000) === 0) { + claimAchievement('justPlainLucky'); + } + }, 1000 * 10); + + window.setTimeout(() => { + claimAchievement('client30min'); + }, 1000 * 60 * 30); + + window.setTimeout(() => { + claimAchievement('client60min'); + }, 1000 * 60 * 60); + + const lastUsed = miLocalStorage.getItem('lastUsed'); + if (lastUsed) { + const lastUsedDate = parseInt(lastUsed, 10); + // 二時間以上前なら + if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) { + toast(i18n.t('welcomeBackWithName', { + name: $i.name || $i.username, + })); + } + } + miLocalStorage.setItem('lastUsed', Date.now().toString()); + + const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt'); + const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo'); + if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) { + if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) { + popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed'); + } + } + + if ('Notification' in window) { + // 許可を得ていなかったらリクエスト + if (Notification.permission === 'default') { + Notification.requestPermission(); + } + } + + const main = markRaw(stream.useChannel('main', null, 'System')); + + // 自分の情報が更新されたとき + main.on('meUpdated', i => { + updateAccount(i); + }); + + main.on('readAllNotifications', () => { + updateAccount({ hasUnreadNotification: false }); + }); + + main.on('unreadNotification', () => { + updateAccount({ hasUnreadNotification: true }); + }); + + main.on('unreadMention', () => { + updateAccount({ hasUnreadMentions: true }); + }); + + main.on('readAllUnreadMentions', () => { + updateAccount({ hasUnreadMentions: false }); + }); + + main.on('unreadSpecifiedNote', () => { + updateAccount({ hasUnreadSpecifiedNotes: true }); + }); + + main.on('readAllUnreadSpecifiedNotes', () => { + updateAccount({ hasUnreadSpecifiedNotes: false }); + }); + + main.on('readAllAntennas', () => { + updateAccount({ hasUnreadAntenna: false }); + }); + + main.on('unreadAntenna', () => { + updateAccount({ hasUnreadAntenna: true }); + sound.play('antenna'); + }); + + main.on('readAllAnnouncements', () => { + updateAccount({ hasUnreadAnnouncement: false }); + }); + + // トークンが再生成されたとき + // このままではMisskeyが利用できないので強制的にサインアウトさせる + main.on('myTokenRegenerated', () => { + signout(); + }); + } + + // shortcut + document.addEventListener('keydown', makeHotkey(hotkeys)); + + initializeSw(); +} diff --git a/packages/frontend/src/boot/sub-boot.ts b/packages/frontend/src/boot/sub-boot.ts new file mode 100644 index 0000000000..c2664f6c1d --- /dev/null +++ b/packages/frontend/src/boot/sub-boot.ts @@ -0,0 +1,8 @@ +import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; +import { common } from './common'; + +export async function subBoot() { + const { isClientUpdated } = await common(() => createApp( + defineAsyncComponent(() => import('@/ui/minimum.vue')), + )); +} diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue index 9f2bf99338..7a1b7d532e 100644 --- a/packages/frontend/src/components/MkAbuseReportWindow.vue +++ b/packages/frontend/src/components/MkAbuseReportWindow.vue @@ -9,7 +9,7 @@ -
+
@@ -60,8 +60,8 @@ function send() { } - diff --git a/packages/frontend/src/components/MkAnimBg.vue b/packages/frontend/src/components/MkAnimBg.vue new file mode 100644 index 0000000000..575ea7c5e3 --- /dev/null +++ b/packages/frontend/src/components/MkAnimBg.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/packages/frontend/src/components/MkChannelList.vue b/packages/frontend/src/components/MkChannelList.vue index 408eab7399..4050520eb9 100644 --- a/packages/frontend/src/components/MkChannelList.vue +++ b/packages/frontend/src/components/MkChannelList.vue @@ -26,6 +26,3 @@ const props = withDefaults(defineProps<{ extractor: (item) => item, }); - - diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index bfec57d6a0..edeaabfba4 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -95,7 +95,7 @@ import XNavFolder from '@/components/MkDrive.navFolder.vue'; import XFolder from '@/components/MkDrive.folder.vue'; import XFile from '@/components/MkDrive.file.vue'; import * as os from '@/os'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { defaultStore } from '@/store'; import { i18n } from '@/i18n'; import { uploadFile, uploads } from '@/scripts/upload'; @@ -131,7 +131,7 @@ const hierarchyFolders = ref([]); const selectedFiles = ref([]); const selectedFolders = ref([]); const uploadings = uploads; -const connection = stream.useChannel('drive'); +const connection = useStream().useChannel('drive'); const keepOriginal = ref(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい // ドロップされようとしているか diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue index 475e01c8d4..f52c66a8be 100644 --- a/packages/frontend/src/components/MkFoldableSection.vue +++ b/packages/frontend/src/components/MkFoldableSection.vue @@ -1,5 +1,5 @@ - diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index 10eee6aab1..8800f31400 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -6,7 +6,7 @@
- +
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index beee21c647..7d066fd033 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -33,7 +33,7 @@ import { onBeforeUnmount, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { i18n } from '@/i18n'; import { claimAchievement } from '@/scripts/achievements'; import { $i } from '@/account'; @@ -50,7 +50,7 @@ const props = withDefaults(defineProps<{ let isFollowing = $ref(props.user.isFollowing); let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou); let wait = $ref(false); -const connection = stream.useChannel('main'); +const connection = useStream().useChannel('main'); if (props.user.isFollowing == null) { os.api('users/show', { diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue index 979df2e7c1..b4ef54aecd 100644 --- a/packages/frontend/src/components/MkFormDialog.vue +++ b/packages/frontend/src/components/MkFormDialog.vue @@ -54,8 +54,8 @@ - diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 1aea95fe0e..8cb1b064ba 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -22,7 +22,7 @@ import MkPagination, { Paging } from '@/components/MkPagination.vue'; import XNotification from '@/components/MkNotification.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkNote from '@/components/MkNote.vue'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { $i } from '@/account'; import { i18n } from '@/i18n'; import { notificationTypes } from '@/const'; @@ -45,7 +45,7 @@ const pagination: Paging = { const onNotification = (notification) => { const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type); if (isMuted || document.visibilityState === 'visible') { - stream.send('readNotification'); + useStream().send('readNotification'); } if (!isMuted) { @@ -56,7 +56,7 @@ const onNotification = (notification) => { let connection; onMounted(() => { - connection = stream.useChannel('main'); + connection = useStream().useChannel('main'); connection.on('notification', onNotification); }); diff --git a/packages/frontend/src/components/MkObjectView.value.vue b/packages/frontend/src/components/MkObjectView.value.vue index e7fc73bce3..d48e7886eb 100644 --- a/packages/frontend/src/components/MkObjectView.value.vue +++ b/packages/frontend/src/components/MkObjectView.value.vue @@ -28,54 +28,38 @@
- diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index 02ce58451d..bd842c486a 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -12,12 +12,12 @@ > -
+
diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue index e2240fb4e1..84be10078a 100644 --- a/packages/frontend/src/components/MkRadios.vue +++ b/packages/frontend/src/components/MkRadios.vue @@ -1,37 +1,27 @@ - - diff --git a/packages/frontend/src/components/MkRippleEffect.vue b/packages/frontend/src/components/MkRippleEffect.vue index 9d93211d5f..60c3a47385 100644 --- a/packages/frontend/src/components/MkRippleEffect.vue +++ b/packages/frontend/src/components/MkRippleEffect.vue @@ -1,7 +1,7 @@ - - diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index ffc5e82b56..6eae8ecf84 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -1,7 +1,7 @@ - - - diff --git a/packages/frontend/src/components/global/i18n.ts b/packages/frontend/src/components/global/i18n.ts index 1fd293ba10..2708b759aa 100644 --- a/packages/frontend/src/components/global/i18n.ts +++ b/packages/frontend/src/components/global/i18n.ts @@ -1,42 +1,24 @@ -import { h, defineComponent } from 'vue'; +import { h } from 'vue'; -export default defineComponent({ - props: { - src: { - type: String, - required: true, - }, - tag: { - type: String, - required: false, - default: 'span', - }, - textTag: { - type: String, - required: false, - default: null, - }, - }, - render() { - let str = this.src; - const parsed = [] as (string | { arg: string; })[]; - while (true) { - const nextBracketOpen = str.indexOf('{'); - const nextBracketClose = str.indexOf('}'); +export default function(props: { src: string; tag?: string; textTag?: string; }, { slots }) { + let str = props.src; + const parsed = [] as (string | { arg: string; })[]; + while (true) { + const nextBracketOpen = str.indexOf('{'); + const nextBracketClose = str.indexOf('}'); - if (nextBracketOpen === -1) { - parsed.push(str); - break; - } else { - if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen)); - parsed.push({ - arg: str.substring(nextBracketOpen + 1, nextBracketClose), - }); - } - - str = str.substr(nextBracketClose + 1); + if (nextBracketOpen === -1) { + parsed.push(str); + break; + } else { + if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen)); + parsed.push({ + arg: str.substring(nextBracketOpen + 1, nextBracketClose), + }); } - return h(this.tag, parsed.map(x => typeof x === 'string' ? (this.textTag ? h(this.textTag, x) : x) : this.$slots[x.arg]())); - }, -}); + str = str.substr(nextBracketClose + 1); + } + + return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]())); +} diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index 4ef8111da9..ee2a2bc7bd 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -1,6 +1,6 @@ import { App } from 'vue'; -import Mfm from './global/MkMisskeyFlavoredMarkdown.vue'; +import Mfm from './global/MkMisskeyFlavoredMarkdown.ts'; import MkA from './global/MkA.vue'; import MkAcct from './global/MkAcct.vue'; import MkAvatar from './global/MkAvatar.vue'; diff --git a/packages/frontend/src/components/mfm.ts b/packages/frontend/src/components/mfm.ts deleted file mode 100644 index c3c07b5834..0000000000 --- a/packages/frontend/src/components/mfm.ts +++ /dev/null @@ -1,390 +0,0 @@ -import { VNode, defineComponent, h } from 'vue'; -import * as mfm from 'mfm-js'; -import MkUrl from '@/components/global/MkUrl.vue'; -import MkLink from '@/components/MkLink.vue'; -import MkMention from '@/components/MkMention.vue'; -import MkEmoji from '@/components/global/MkEmoji.vue'; -import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue'; -import MkCode from '@/components/MkCode.vue'; -import MkGoogle from '@/components/MkGoogle.vue'; -import MkSparkle from '@/components/MkSparkle.vue'; -import MkA from '@/components/global/MkA.vue'; -import { host } from '@/config'; -import { defaultStore } from '@/store'; - -const QUOTE_STYLE = ` -display: block; -margin: 8px; -padding: 6px 0 6px 12px; -color: var(--fg); -border-left: solid 3px var(--fg); -opacity: 0.7; -`.split('\n').join(' '); - -export default defineComponent({ - props: { - text: { - type: String, - required: true, - }, - plain: { - type: Boolean, - default: false, - }, - nowrap: { - type: Boolean, - default: false, - }, - author: { - type: Object, - default: null, - }, - i: { - type: Object, - default: null, - }, - isNote: { - type: Boolean, - default: true, - }, - emojiUrls: { - type: Object, - default: null, - }, - rootScale: { - type: Number, - default: 1, - } - }, - - render() { - if (this.text == null || this.text === '') return; - - const ast = (this.plain ? mfm.parseSimple : mfm.parse)(this.text); - - const validTime = (t: string | null | undefined) => { - if (t == null) return null; - return t.match(/^[0-9.]+s$/) ? t : null; - }; - - const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm; - - /** - * Gen Vue Elements from MFM AST - * @param ast MFM AST - * @param scale How times large the text is - */ - const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => { - switch (token.type) { - case 'text': { - const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); - - if (!this.plain) { - const res: (VNode | string)[] = []; - for (const t of text.split('\n')) { - res.push(h('br')); - res.push(t); - } - res.shift(); - return res; - } else { - return [text.replace(/\n/g, ' ')]; - } - } - - case 'bold': { - return [h('b', genEl(token.children, scale))]; - } - - case 'strike': { - return [h('del', genEl(token.children, scale))]; - } - - case 'italic': { - return h('i', { - style: 'font-style: oblique;', - }, genEl(token.children, scale)); - } - - case 'fn': { - // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる - let style; - switch (token.props.name) { - case 'tada': { - const speed = validTime(token.props.args.speed) ?? '1s'; - style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : ''); - break; - } - case 'jelly': { - const speed = validTime(token.props.args.speed) ?? '1s'; - style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : ''); - break; - } - case 'twitch': { - const speed = validTime(token.props.args.speed) ?? '0.5s'; - style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : ''; - break; - } - case 'shake': { - const speed = validTime(token.props.args.speed) ?? '0.5s'; - style = useAnim ? `animation: mfm-shake ${speed} ease infinite;` : ''; - break; - } - case 'spin': { - const direction = - token.props.args.left ? 'reverse' : - token.props.args.alternate ? 'alternate' : - 'normal'; - const anime = - token.props.args.x ? 'mfm-spinX' : - token.props.args.y ? 'mfm-spinY' : - 'mfm-spin'; - const speed = validTime(token.props.args.speed) ?? '1.5s'; - style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : ''; - break; - } - case 'jump': { - const speed = validTime(token.props.args.speed) ?? '0.75s'; - style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : ''; - break; - } - case 'bounce': { - const speed = validTime(token.props.args.speed) ?? '0.75s'; - style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : ''; - break; - } - case 'flip': { - const transform = - (token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' : - token.props.args.v ? 'scaleY(-1)' : - 'scaleX(-1)'; - style = `transform: ${transform};`; - break; - } - case 'x2': { - return h('span', { - class: defaultStore.state.advancedMfm ? 'mfm-x2' : '', - }, genEl(token.children, scale * 2)); - } - case 'x3': { - return h('span', { - class: defaultStore.state.advancedMfm ? 'mfm-x3' : '', - }, genEl(token.children, scale * 3)); - } - case 'x4': { - return h('span', { - class: defaultStore.state.advancedMfm ? 'mfm-x4' : '', - }, genEl(token.children, scale * 4)); - } - case 'font': { - const family = - token.props.args.serif ? 'serif' : - token.props.args.monospace ? 'monospace' : - token.props.args.cursive ? 'cursive' : - token.props.args.fantasy ? 'fantasy' : - token.props.args.emoji ? 'emoji' : - token.props.args.math ? 'math' : - null; - if (family) style = `font-family: ${family};`; - break; - } - case 'blur': { - return h('span', { - class: '_mfm_blur_', - }, genEl(token.children, scale)); - } - case 'rainbow': { - const speed = validTime(token.props.args.speed) ?? '1s'; - style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite;` : ''; - break; - } - case 'sparkle': { - if (!useAnim) { - return genEl(token.children, scale); - } - return h(MkSparkle, {}, genEl(token.children, scale)); - } - case 'rotate': { - const degrees = parseFloat(token.props.args.deg ?? '90'); - style = `transform: rotate(${degrees}deg); transform-origin: center center;`; - break; - } - case 'position': { - if (!defaultStore.state.advancedMfm) break; - const x = parseFloat(token.props.args.x ?? '0'); - const y = parseFloat(token.props.args.y ?? '0'); - style = `transform: translateX(${x}em) translateY(${y}em);`; - break; - } - case 'scale': { - if (!defaultStore.state.advancedMfm) { - style = ''; - break; - } - const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5); - const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5); - style = `transform: scale(${x}, ${y});`; - scale = scale * Math.max(x, y); - break; - } - case 'fg': { - let color = token.props.args.color; - if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00'; - style = `color: #${color};`; - break; - } - case 'bg': { - let color = token.props.args.color; - if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00'; - style = `background-color: #${color};`; - break; - } - } - if (style == null) { - return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']); - } else { - return h('span', { - style: 'display: inline-block; ' + style, - }, genEl(token.children, scale)); - } - } - - case 'small': { - return [h('small', { - style: 'opacity: 0.7;', - }, genEl(token.children, scale))]; - } - - case 'center': { - return [h('div', { - style: 'text-align:center;', - }, genEl(token.children, scale))]; - } - - case 'url': { - return [h(MkUrl, { - key: Math.random(), - url: token.props.url, - rel: 'nofollow noopener', - })]; - } - - case 'link': { - return [h(MkLink, { - key: Math.random(), - url: token.props.url, - rel: 'nofollow noopener', - }, genEl(token.children, scale))]; - } - - case 'mention': { - return [h(MkMention, { - key: Math.random(), - host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host, - username: token.props.username, - })]; - } - - case 'hashtag': { - return [h(MkA, { - key: Math.random(), - to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`, - style: 'color:var(--hashtag);', - }, `#${token.props.hashtag}`)]; - } - - case 'blockCode': { - return [h(MkCode, { - key: Math.random(), - code: token.props.code, - lang: token.props.lang, - })]; - } - - case 'inlineCode': { - return [h(MkCode, { - key: Math.random(), - code: token.props.code, - inline: true, - })]; - } - - case 'quote': { - if (!this.nowrap) { - return [h('div', { - style: QUOTE_STYLE, - }, genEl(token.children, scale))]; - } else { - return [h('span', { - style: QUOTE_STYLE, - }, genEl(token.children, scale))]; - } - } - - case 'emojiCode': { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (this.author?.host == null) { - return [h(MkCustomEmoji, { - key: Math.random(), - name: token.props.name, - normal: this.plain, - host: null, - useOriginalSize: scale >= 2.5, - })]; - } else { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (this.emojiUrls && (this.emojiUrls[token.props.name] == null)) { - return [h('span', `:${token.props.name}:`)]; - } else { - return [h(MkCustomEmoji, { - key: Math.random(), - name: token.props.name, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - url: this.emojiUrls ? this.emojiUrls[token.props.name] : null, - normal: this.plain, - host: this.author.host, - useOriginalSize: scale >= 2.5, - })]; - } - } - } - - case 'unicodeEmoji': { - return [h(MkEmoji, { - key: Math.random(), - emoji: token.props.emoji, - })]; - } - - case 'mathInline': { - return [h('code', token.props.formula)]; - } - - case 'mathBlock': { - return [h('code', token.props.formula)]; - } - - case 'search': { - return [h(MkGoogle, { - key: Math.random(), - q: token.props.query, - })]; - } - - case 'plain': { - return [h('span', genEl(token.children, scale))]; - } - - default: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - console.error('unrecognized ast type:', (token as any).type); - - return []; - } - } - }).flat(Infinity) as (VNode | string)[]; - - // Parse ast to DOM - return h('span', genEl(ast, this.rootScale)); - }, -}); diff --git a/packages/frontend/src/components/page/block.type.ts b/packages/frontend/src/components/page/block.type.ts new file mode 100644 index 0000000000..71249a8aff --- /dev/null +++ b/packages/frontend/src/components/page/block.type.ts @@ -0,0 +1,29 @@ +export type BlockBase = { + id: string; + type: string; +}; + +export type TextBlock = BlockBase & { + type: 'text'; + text: string; +}; + +export type SectionBlock = BlockBase & { + type: 'section'; + title: string; + children: Block[]; +}; + +export type ImageBlock = BlockBase & { + type: 'image'; + fileId: string | null; +}; + +export type NoteBlock = BlockBase & { + type: 'note'; + detailed: boolean; + note: string | null; +}; + +export type Block = + TextBlock | SectionBlock | ImageBlock | NoteBlock; diff --git a/packages/frontend/src/components/page/page.block.vue b/packages/frontend/src/components/page/page.block.vue index f3e7764604..dddb9d76bc 100644 --- a/packages/frontend/src/components/page/page.block.vue +++ b/packages/frontend/src/components/page/page.block.vue @@ -1,44 +1,19 @@ - diff --git a/packages/frontend/src/components/page/page.button.vue b/packages/frontend/src/components/page/page.button.vue deleted file mode 100644 index 83931021d8..0000000000 --- a/packages/frontend/src/components/page/page.button.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - - - diff --git a/packages/frontend/src/components/page/page.canvas.vue b/packages/frontend/src/components/page/page.canvas.vue deleted file mode 100644 index 82ff36ec36..0000000000 --- a/packages/frontend/src/components/page/page.canvas.vue +++ /dev/null @@ -1,48 +0,0 @@ - - - - - diff --git a/packages/frontend/src/components/page/page.counter.vue b/packages/frontend/src/components/page/page.counter.vue deleted file mode 100644 index 63fde6a120..0000000000 --- a/packages/frontend/src/components/page/page.counter.vue +++ /dev/null @@ -1,51 +0,0 @@ - - - - - diff --git a/packages/frontend/src/components/page/page.if.vue b/packages/frontend/src/components/page/page.if.vue deleted file mode 100644 index 372a15f0c6..0000000000 --- a/packages/frontend/src/components/page/page.if.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue index 6ea81d257f..2edcfb8b1a 100644 --- a/packages/frontend/src/components/page/page.image.vue +++ b/packages/frontend/src/components/page/page.image.vue @@ -5,15 +5,15 @@ diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue index 8c65dabf08..7133a7f5a1 100644 --- a/packages/frontend/src/components/page/page.note.vue +++ b/packages/frontend/src/components/page/page.note.vue @@ -1,47 +1,29 @@ - - - diff --git a/packages/frontend/src/components/page/page.number-input.vue b/packages/frontend/src/components/page/page.number-input.vue deleted file mode 100644 index 72c1b6deb0..0000000000 --- a/packages/frontend/src/components/page/page.number-input.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - - - diff --git a/packages/frontend/src/components/page/page.post.vue b/packages/frontend/src/components/page/page.post.vue deleted file mode 100644 index 55da610cb6..0000000000 --- a/packages/frontend/src/components/page/page.post.vue +++ /dev/null @@ -1,111 +0,0 @@ - - - - - diff --git a/packages/frontend/src/components/page/page.radio-button.vue b/packages/frontend/src/components/page/page.radio-button.vue deleted file mode 100644 index ce8f252e44..0000000000 --- a/packages/frontend/src/components/page/page.radio-button.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - diff --git a/packages/frontend/src/components/page/page.section.vue b/packages/frontend/src/components/page/page.section.vue index 50181b3905..dc06a231f9 100644 --- a/packages/frontend/src/components/page/page.section.vue +++ b/packages/frontend/src/components/page/page.section.vue @@ -3,34 +3,23 @@ {{ block.title }}
- +
- diff --git a/packages/frontend/src/components/page/page.text-input.vue b/packages/frontend/src/components/page/page.text-input.vue deleted file mode 100644 index d020a99de8..0000000000 --- a/packages/frontend/src/components/page/page.text-input.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - - - diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue index e0e4959efa..308948b45c 100644 --- a/packages/frontend/src/components/page/page.text.vue +++ b/packages/frontend/src/components/page/page.text.vue @@ -1,56 +1,26 @@ - diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index b76e4b9114..638b193c11 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -2,7 +2,7 @@ -
+
@@ -113,9 +113,3 @@ definePageMetadata({ icon: 'ti ti-speakerphone', }); - - diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue index c189437246..2b13a7c80c 100644 --- a/packages/frontend/src/pages/admin/files.vue +++ b/packages/frontend/src/pages/admin/files.vue @@ -3,7 +3,7 @@ -
+
@@ -109,9 +109,3 @@ definePageMetadata(computed(() => ({ icon: 'ti ti-cloud', }))); - - diff --git a/packages/frontend/src/pages/admin/overview.pie.vue b/packages/frontend/src/pages/admin/overview.pie.vue index 08a29bf550..af7bc70551 100644 --- a/packages/frontend/src/pages/admin/overview.pie.vue +++ b/packages/frontend/src/pages/admin/overview.pie.vue @@ -67,7 +67,3 @@ onMounted(() => { }); }); - - diff --git a/packages/frontend/src/pages/admin/overview.queue.chart.vue b/packages/frontend/src/pages/admin/overview.queue.chart.vue index 6a11e8b768..a3c8659ce5 100644 --- a/packages/frontend/src/pages/admin/overview.queue.chart.vue +++ b/packages/frontend/src/pages/admin/overview.queue.chart.vue @@ -132,7 +132,3 @@ defineExpose({ pushData, }); - - diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue index 1f56a2826a..69ca89e226 100644 --- a/packages/frontend/src/pages/admin/overview.queue.vue +++ b/packages/frontend/src/pages/admin/overview.queue.vue @@ -33,9 +33,9 @@ import { markRaw, onMounted, onUnmounted, ref } from 'vue'; import XChart from './overview.queue.chart.vue'; import number from '@/filters/number'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; -const connection = markRaw(stream.useChannel('queueStats')); +const connection = markRaw(useStream().useChannel('queueStats')); const activeSincePrevTick = ref(0); const active = ref(0); diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue index 5c96c07bfb..46a93ac5d8 100644 --- a/packages/frontend/src/pages/admin/overview.vue +++ b/packages/frontend/src/pages/admin/overview.vue @@ -1,6 +1,6 @@ @@ -54,11 +54,3 @@ onMounted(async () => { } }); - - diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue index bf21ae3c67..3b15c17747 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue @@ -3,8 +3,8 @@ -
- +
+
@@ -33,23 +33,21 @@ watch($$(text), () => { }); - diff --git a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue index 97bdcfe80f..2c3d59256c 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue @@ -9,49 +9,41 @@ - diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue index c687b89eab..52b7c256e0 100644 --- a/packages/frontend/src/pages/registry.keys.vue +++ b/packages/frontend/src/pages/registry.keys.vue @@ -93,6 +93,3 @@ definePageMetadata({ icon: 'ti ti-adjustments', }); - - diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue index 00e2ca5e03..6ff07e2b77 100644 --- a/packages/frontend/src/pages/registry.value.vue +++ b/packages/frontend/src/pages/registry.value.vue @@ -118,6 +118,3 @@ definePageMetadata({ icon: 'ti ti-adjustments', }); - - diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue index 5a029cb0c7..016af22815 100644 --- a/packages/frontend/src/pages/registry.vue +++ b/packages/frontend/src/pages/registry.vue @@ -68,6 +68,3 @@ definePageMetadata({ icon: 'ti ti-adjustments', }); - - diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue index 38c88cc650..ab7a96a8d0 100644 --- a/packages/frontend/src/pages/reset-password.vue +++ b/packages/frontend/src/pages/reset-password.vue @@ -53,7 +53,3 @@ definePageMetadata({ icon: 'ti ti-lock', }); - - diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 7c1522e059..9d06d35e60 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -144,12 +144,14 @@ - {{ i18n.ts.aiChanMode }} + + +
+ {{ i18n.ts.flagShowTimelineReplies }} + {{ i18n.ts.deck }} + {{ i18n.ts.customCss }} +
- - {{ i18n.ts.deck }} - - {{ i18n.ts.customCss }}
@@ -211,10 +213,10 @@ const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker')) const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll')); const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu')); const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars')); -const aiChanMode = computed(defaultStore.makeGetterSetter('aiChanMode')); const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('mediaListWithOneImageAppearance')); const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition')); const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis')); +const showTimelineReplies = computed(defaultStore.makeGetterSetter('showTimelineReplies')); watch(lang, () => { miLocalStorage.setItem('lang', lang.value as string); @@ -243,7 +245,6 @@ watch([ useSystemFont, enableInfiniteScroll, squareAvatars, - aiChanMode, showNoteActionsOnlyHover, showGapBetweenNotesInTimeline, instanceTicker, diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index 776305d723..0b73780a8b 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -53,6 +53,17 @@
+ + + + + +
+ + + +
+
@@ -80,6 +91,7 @@ import FormSection from '@/components/form/section.vue'; const reportError = computed(defaultStore.makeGetterSetter('reportError')); const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct')); +const devMode = computed(defaultStore.makeGetterSetter('devMode')); function onChangeInjectFeaturedNote(v) { os.api('i/update', { diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue index 8b57dceefb..f90ca737e9 100644 --- a/packages/frontend/src/pages/settings/plugin.vue +++ b/packages/frontend/src/pages/settings/plugin.vue @@ -94,7 +94,3 @@ definePageMetadata({ icon: 'ti ti-plug', }); - - diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index 6613ce4c1d..86f633c445 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -40,7 +40,7 @@ import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os'; import { ColdDeviceStorage, defaultStore } from '@/store'; import { unisonReload } from '@/scripts/unison-reload'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { $i } from '@/account'; import { i18n } from '@/i18n'; import { version, host } from '@/config'; @@ -125,7 +125,7 @@ type Profile = { }; }; -const connection = $i && stream.useChannel('main'); +const connection = $i && useStream().useChannel('main'); let profiles = $ref | null>(null); diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 6ffd682610..35af43d789 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -1,11 +1,11 @@ @@ -248,36 +246,39 @@ definePageMetadata({ }); - - diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue index 78e0710162..5abb234893 100644 --- a/packages/frontend/src/pages/share.vue +++ b/packages/frontend/src/pages/share.vue @@ -16,7 +16,7 @@ class="_panel" @posted="state = 'posted'" /> - {{ i18n.ts.close }} + {{ i18n.ts.close }} @@ -162,7 +162,7 @@ definePageMetadata({ }); - diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 9c133346d5..a39f206863 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -69,7 +69,7 @@
- +

{{ i18n.ts.noAccountDescription }}

diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue index 7728d97a65..5a68292b1b 100644 --- a/packages/frontend/src/pages/welcome.setup.vue +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -1,27 +1,32 @@ diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue index 792c1ccc5e..c17450f6ed 100644 --- a/packages/frontend/src/ui/classic.vue +++ b/packages/frontend/src/ui/classic.vue @@ -10,7 +10,7 @@
-
+
diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue new file mode 100644 index 0000000000..628390e3f7 --- /dev/null +++ b/packages/frontend/src/ui/minimum.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index 27d0c26ac4..f755dda627 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -4,7 +4,7 @@ -
+
diff --git a/packages/frontend/src/widgets/WidgetActivity.chart.vue b/packages/frontend/src/widgets/WidgetActivity.chart.vue index b61e419f94..cc4df65dd2 100644 --- a/packages/frontend/src/widgets/WidgetActivity.chart.vue +++ b/packages/frontend/src/widgets/WidgetActivity.chart.vue @@ -1,26 +1,30 @@ @@ -81,8 +85,8 @@ function render() { } - diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue index 84043cf13f..73a4802595 100644 --- a/packages/frontend/src/widgets/WidgetJobQueue.vue +++ b/packages/frontend/src/widgets/WidgetJobQueue.vue @@ -49,7 +49,7 @@ import { onUnmounted, reactive } from 'vue'; import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; import { GetFormResultType } from '@/scripts/form'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import number from '@/filters/number'; import * as sound from '@/scripts/sound'; import { deepClone } from '@/scripts/clone'; @@ -81,7 +81,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name, emit, ); -const connection = stream.useChannel('queueStats'); +const connection = useStream().useChannel('queueStats'); const current = reactive({ inbox: { activeSincePrevTick: 0, diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue index 44e073545d..19195d6fde 100644 --- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue +++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue @@ -1,6 +1,6 @@