From 6a73f7c10802734fe59a76307c312b69810979fa Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Wed, 1 Nov 2023 20:29:58 +0900 Subject: [PATCH 01/60] =?UTF-8?q?i/update=E3=81=AE=E3=83=AC=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=83=AA=E3=83=9F=E3=83=83=E3=83=88=E3=82=92=E7=B7=A9?= =?UTF-8?q?=E5=92=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/server/api/endpoints/i/update.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index b03381a3f3..0e6a4d2e36 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -45,7 +45,7 @@ export const meta = { limit: { duration: ms('1hour'), - max: 10, + max: 20, }, errors: { From c7129d519095c23159553abe3b089e77de56a667 Mon Sep 17 00:00:00 2001 From: Camilla Ett <camilla.ett@gmail.com> Date: Thu, 2 Nov 2023 09:12:09 +0900 Subject: [PATCH 02/60] =?UTF-8?q?fix(frontend):=20/about=20=E3=81=AE?= =?UTF-8?q?=E9=80=A3=E5=90=88=E3=82=BF=E3=83=96=E3=81=AE=E3=83=AC=E3=82=A4?= =?UTF-8?q?=E3=82=A2=E3=82=A6=E3=83=88=E3=81=8C=E4=B8=80=E9=83=A8=E5=B4=A9?= =?UTF-8?q?=E3=82=8C=E3=81=A6=E3=81=84=E3=82=8B=E3=81=AE=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20(#12215)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/pages/about.federation.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue index 333af93ef8..47fe9c4279 100644 --- a/packages/frontend/src/pages/about.federation.vue +++ b/packages/frontend/src/pages/about.federation.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div> +<div class="_gaps"> <div> <MkInput v-model="host" :debounce="true" class=""> <template #prefix><i class="ti ti-search"></i></template> From f62ad3ed3eccfd242b2d1f1e25f00276f2bfff77 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Thu, 2 Nov 2023 15:57:55 +0900 Subject: [PATCH 03/60] feat: notification grouping Resolve #12211 --- CHANGELOG.md | 3 +- locales/index.d.ts | 4 + locales/ja-JP.yml | 4 + .../backend/src/core/NoteCreateService.ts | 19 +- .../backend/src/core/NotificationService.ts | 13 +- .../backend/src/core/UserFollowingService.ts | 1 - .../entities/NotificationEntityService.ts | 162 ++++++++++++++-- packages/backend/src/models/Notification.ts | 110 ++++++++--- .../src/models/json-schema/notification.ts | 31 ++- .../backend/src/server/api/EndpointsModule.ts | 4 + packages/backend/src/server/api/endpoints.ts | 2 + .../api/endpoints/i/notifications-grouped.ts | 178 ++++++++++++++++++ .../server/api/endpoints/i/notifications.ts | 6 +- .../api/endpoints/notifications/create.ts | 4 +- packages/backend/src/types.ts | 6 + .../src/components/MkNotification.vue | 82 +++++++- .../src/components/MkNotifications.vue | 11 +- .../frontend/src/pages/settings/general.vue | 3 + packages/frontend/src/store.ts | 4 + 19 files changed, 581 insertions(+), 66 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/i/notifications-grouped.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d00c960c95..38ef92e953 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,8 @@ - Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました - 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください https://misskey-hub.net/docs/advanced/publish-on-your-website.html -- Enhance: スワイプしてタイムラインを再読込できるように +- Feat: 通知をグルーピングして表示するオプション(オプトアウト) +- Feat: スワイプしてタイムラインを再読込できるように - PCの場合は右上のボタンからでも再読込できます - Enhance: タイムラインの自動更新を無効にできるように - Enhance: コードのシンタックスハイライトエンジンをShikiに変更 diff --git a/locales/index.d.ts b/locales/index.d.ts index 8bc073d1e5..eb27983087 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1157,6 +1157,7 @@ export interface Locale { "refreshing": string; "pullDownToRefresh": string; "disableStreamingTimeline": string; + "useGroupedNotifications": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; @@ -2200,6 +2201,9 @@ export interface Locale { "checkNotificationBehavior": string; "sendTestNotification": string; "notificationWillBeDisplayedLikeThis": string; + "reactedBySomeUsers": string; + "renotedBySomeUsers": string; + "followedBySomeUsers": string; "_types": { "all": string; "note": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 035cecd25a..5e711fcdf4 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1154,6 +1154,7 @@ releaseToRefresh: "離してリロード" refreshing: "リロード中" pullDownToRefresh: "引っ張ってリロード" disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする" +useGroupedNotifications: "通知をグルーピングして表示する" _announcement: forExistingUsers: "既存ユーザーのみ" @@ -2114,6 +2115,9 @@ _notification: checkNotificationBehavior: "通知の表示を確かめる" sendTestNotification: "テスト通知を送信する" notificationWillBeDisplayedLikeThis: "通知はこのように表示されます" + reactedBySomeUsers: "{n}人がリアクションしました" + renotedBySomeUsers: "{n}人がリノートしました" + followedBySomeUsers: "{n}人にフォローされました" _types: all: "すべて" diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 6caa3d463c..acd11a9fa7 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -100,17 +100,14 @@ class NotificationManager { } @bindThis - public async deliver() { + public async notify() { for (const x of this.queue) { - // ミュート情報を取得 - const mentioneeMutes = await this.mutingsRepository.findBy({ - muterId: x.target, - }); - - const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId); - - // 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する - if (!mentioneesMutedUserIds.includes(this.notifier.id)) { + if (x.reason === 'renote') { + this.notificationService.createNotification(x.target, 'renote', { + noteId: this.note.id, + targetNoteId: this.note.renoteId!, + }, this.notifier.id); + } else { this.notificationService.createNotification(x.target, x.reason, { noteId: this.note.id, }, this.notifier.id); @@ -642,7 +639,7 @@ export class NoteCreateService implements OnApplicationShutdown { } } - nm.deliver(); + nm.notify(); //#region AP deliver if (this.userEntityService.isLocalUser(user)) { diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index 7c3672c67a..ad7be83e5b 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -19,6 +19,7 @@ import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; import type { Config } from '@/config.js'; import { UserListService } from '@/core/UserListService.js'; +import type { FilterUnionByProperty } from '@/types.js'; @Injectable() export class NotificationService implements OnApplicationShutdown { @@ -73,10 +74,10 @@ export class NotificationService implements OnApplicationShutdown { } @bindThis - public async createNotification( + public async createNotification<T extends MiNotification['type']>( notifieeId: MiUser['id'], - type: MiNotification['type'], - data: Omit<Partial<MiNotification>, 'notifierId'>, + type: T, + data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>, notifierId?: MiUser['id'] | null, ): Promise<MiNotification | null> { const profile = await this.cacheService.userProfileCache.fetch(notifieeId); @@ -128,9 +129,11 @@ export class NotificationService implements OnApplicationShutdown { id: this.idService.gen(), createdAt: new Date(), type: type, - notifierId: notifierId, + ...(notifierId ? { + notifierId, + } : {}), ...data, - } as MiNotification; + } as any as FilterUnionByProperty<MiNotification, 'type', T>; const redisIdPromise = this.redisClient.xadd( `notificationTimeline:${notifieeId}`, diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 4d7e14f683..bd7f298021 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -509,7 +509,6 @@ export class UserFollowingService implements OnModuleInit { // 通知を作成 this.notificationService.createNotification(followee.id, 'receiveFollowRequest', { - followRequestId: followRequest.id, }, follower.id); } diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 9542815bd7..f74594ff0c 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -9,18 +9,19 @@ import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { FollowRequestsRepository, NotesRepository, MiUser, UsersRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { MiNotification } from '@/models/Notification.js'; +import type { MiGroupedNotification, MiNotification } from '@/models/Notification.js'; import type { MiNote } from '@/models/Note.js'; import type { Packed } from '@/misc/json-schema.js'; import { bindThis } from '@/decorators.js'; import { isNotNull } from '@/misc/is-not-null.js'; -import { notificationTypes } from '@/types.js'; +import { FilterUnionByProperty, notificationTypes } from '@/types.js'; import type { OnModuleInit } from '@nestjs/common'; import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]); +const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded']); @Injectable() export class NotificationEntityService implements OnModuleInit { @@ -66,17 +67,17 @@ export class NotificationEntityService implements OnModuleInit { }, ): Promise<Packed<'Notification'>> { const notification = src; - const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && notification.noteId != null ? ( + const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification ? ( hint?.packedNotes != null ? hint.packedNotes.get(notification.noteId) - : this.noteEntityService.pack(notification.noteId!, { id: meId }, { + : this.noteEntityService.pack(notification.noteId, { id: meId }, { detail: true, }) ) : undefined; - const userIfNeed = notification.notifierId != null ? ( + const userIfNeed = 'notifierId' in notification ? ( hint?.packedUsers != null ? hint.packedUsers.get(notification.notifierId) - : this.userEntityService.pack(notification.notifierId!, { id: meId }, { + : this.userEntityService.pack(notification.notifierId, { id: meId }, { detail: false, }) ) : undefined; @@ -85,7 +86,7 @@ export class NotificationEntityService implements OnModuleInit { id: notification.id, createdAt: new Date(notification.createdAt).toISOString(), type: notification.type, - userId: notification.notifierId, + userId: 'notifierId' in notification ? notification.notifierId : undefined, ...(userIfNeed != null ? { user: userIfNeed } : {}), ...(noteIfNeed != null ? { note: noteIfNeed } : {}), ...(notification.type === 'reaction' ? { @@ -111,7 +112,7 @@ export class NotificationEntityService implements OnModuleInit { let validNotifications = notifications; - const noteIds = validNotifications.map(x => x.noteId).filter(isNotNull); + const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull); const notes = noteIds.length > 0 ? await this.notesRepository.find({ where: { id: In(noteIds) }, relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'], @@ -121,9 +122,9 @@ export class NotificationEntityService implements OnModuleInit { }); const packedNotes = new Map(packedNotesArray.map(p => [p.id, p])); - validNotifications = validNotifications.filter(x => x.noteId == null || packedNotes.has(x.noteId)); + validNotifications = validNotifications.filter(x => !('noteId' in x) || packedNotes.has(x.noteId)); - const userIds = validNotifications.map(x => x.notifierId).filter(isNotNull); + const userIds = validNotifications.map(x => 'notifierId' in x ? x.notifierId : null).filter(isNotNull); const users = userIds.length > 0 ? await this.usersRepository.find({ where: { id: In(userIds) }, }) : []; @@ -133,10 +134,10 @@ export class NotificationEntityService implements OnModuleInit { const packedUsers = new Map(packedUsersArray.map(p => [p.id, p])); // 既に解決されたフォローリクエストの通知を除外 - const followRequestNotifications = validNotifications.filter(x => x.type === 'receiveFollowRequest'); + const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty<MiGroupedNotification, 'type', 'receiveFollowRequest'> => x.type === 'receiveFollowRequest'); if (followRequestNotifications.length > 0) { const reqs = await this.followRequestsRepository.find({ - where: { followerId: In(followRequestNotifications.map(x => x.notifierId!)) }, + where: { followerId: In(followRequestNotifications.map(x => x.notifierId)) }, }); validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId)); } @@ -146,4 +147,141 @@ export class NotificationEntityService implements OnModuleInit { packedUsers, }))); } + + @bindThis + public async packGrouped( + src: MiGroupedNotification, + meId: MiUser['id'], + // eslint-disable-next-line @typescript-eslint/ban-types + options: { + + }, + hint?: { + packedNotes: Map<MiNote['id'], Packed<'Note'>>; + packedUsers: Map<MiUser['id'], Packed<'User'>>; + }, + ): Promise<Packed<'Notification'>> { + const notification = src; + const noteIfNeed = NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification ? ( + hint?.packedNotes != null + ? hint.packedNotes.get(notification.noteId) + : this.noteEntityService.pack(notification.noteId, { id: meId }, { + detail: true, + }) + ) : undefined; + const userIfNeed = 'notifierId' in notification ? ( + hint?.packedUsers != null + ? hint.packedUsers.get(notification.notifierId) + : this.userEntityService.pack(notification.notifierId, { id: meId }, { + detail: false, + }) + ) : undefined; + + if (notification.type === 'reaction:grouped') { + const reactions = await Promise.all(notification.reactions.map(async reaction => { + const user = hint?.packedUsers != null + ? hint.packedUsers.get(reaction.userId)! + : await this.userEntityService.pack(reaction.userId, { id: meId }, { + detail: false, + }); + return { + user, + reaction: reaction.reaction, + }; + })); + return await awaitAll({ + id: notification.id, + createdAt: new Date(notification.createdAt).toISOString(), + type: notification.type, + note: noteIfNeed, + reactions, + }); + } else if (notification.type === 'renote:grouped') { + const users = await Promise.all(notification.userIds.map(userId => { + const user = hint?.packedUsers != null + ? hint.packedUsers.get(userId) + : this.userEntityService.pack(userId!, { id: meId }, { + detail: false, + }); + return user; + })); + return await awaitAll({ + id: notification.id, + createdAt: new Date(notification.createdAt).toISOString(), + type: notification.type, + note: noteIfNeed, + users, + }); + } + + return await awaitAll({ + id: notification.id, + createdAt: new Date(notification.createdAt).toISOString(), + type: notification.type, + userId: 'notifierId' in notification ? notification.notifierId : undefined, + ...(userIfNeed != null ? { user: userIfNeed } : {}), + ...(noteIfNeed != null ? { note: noteIfNeed } : {}), + ...(notification.type === 'reaction' ? { + reaction: notification.reaction, + } : {}), + ...(notification.type === 'achievementEarned' ? { + achievement: notification.achievement, + } : {}), + ...(notification.type === 'app' ? { + body: notification.customBody, + header: notification.customHeader, + icon: notification.customIcon, + } : {}), + }); + } + + @bindThis + public async packGroupedMany( + notifications: MiGroupedNotification[], + meId: MiUser['id'], + ) { + if (notifications.length === 0) return []; + + let validNotifications = notifications; + + const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull); + const notes = noteIds.length > 0 ? await this.notesRepository.find({ + where: { id: In(noteIds) }, + relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'], + }) : []; + const packedNotesArray = await this.noteEntityService.packMany(notes, { id: meId }, { + detail: true, + }); + const packedNotes = new Map(packedNotesArray.map(p => [p.id, p])); + + validNotifications = validNotifications.filter(x => !('noteId' in x) || packedNotes.has(x.noteId)); + + const userIds = []; + for (const notification of validNotifications) { + if ('notifierId' in notification) userIds.push(notification.notifierId); + if (notification.type === 'reaction:grouped') userIds.push(...notification.reactions.map(x => x.userId)); + if (notification.type === 'renote:grouped') userIds.push(...notification.userIds); + } + const users = userIds.length > 0 ? await this.usersRepository.find({ + where: { id: In(userIds) }, + }) : []; + const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }, { + detail: false, + }); + const packedUsers = new Map(packedUsersArray.map(p => [p.id, p])); + + // 既に解決されたフォローリクエストの通知を除外 + const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty<MiGroupedNotification, 'type', 'receiveFollowRequest'> => x.type === 'receiveFollowRequest'); + if (followRequestNotifications.length > 0) { + const reqs = await this.followRequestsRepository.find({ + where: { followerId: In(followRequestNotifications.map(x => x.notifierId)) }, + }); + validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId)); + } + + return await Promise.all(validNotifications.map(x => this.packGrouped(x, meId, {}, { + packedNotes, + packedUsers, + }))); + } } diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index c0a9df2e23..1d5fc124e2 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -10,30 +10,73 @@ import { MiFollowRequest } from './FollowRequest.js'; import { MiAccessToken } from './AccessToken.js'; export type MiNotification = { + type: 'note'; + id: string; + createdAt: string; + notifierId: MiUser['id']; + noteId: MiNote['id']; +} | { + type: 'follow'; + id: string; + createdAt: string; + notifierId: MiUser['id']; +} | { + type: 'mention'; + id: string; + createdAt: string; + notifierId: MiUser['id']; + noteId: MiNote['id']; +} | { + type: 'reply'; + id: string; + createdAt: string; + notifierId: MiUser['id']; + noteId: MiNote['id']; +} | { + type: 'renote'; + id: string; + createdAt: string; + notifierId: MiUser['id']; + noteId: MiNote['id']; + targetNoteId: MiNote['id']; +} | { + type: 'quote'; + id: string; + createdAt: string; + notifierId: MiUser['id']; + noteId: MiNote['id']; +} | { + type: 'reaction'; + id: string; + createdAt: string; + notifierId: MiUser['id']; + noteId: MiNote['id']; + reaction: string; +} | { + type: 'pollEnded'; + id: string; + createdAt: string; + notifierId: MiUser['id']; + noteId: MiNote['id']; +} | { + type: 'receiveFollowRequest'; + id: string; + createdAt: string; + notifierId: MiUser['id']; +} | { + type: 'followRequestAccepted'; + id: string; + createdAt: string; + notifierId: MiUser['id']; +} | { + type: 'achievementEarned'; + id: string; + createdAt: string; + achievement: string; +} | { + type: 'app'; id: string; - - // RedisのためDateではなくstring createdAt: string; - - /** - * 通知の送信者(initiator) - */ - notifierId: MiUser['id'] | null; - - /** - * 通知の種類。 - */ - type: typeof notificationTypes[number]; - - noteId: MiNote['id'] | null; - - followRequestId: MiFollowRequest['id'] | null; - - reaction: string | null; - - choice: number | null; - - achievement: string | null; /** * アプリ通知のbody @@ -56,4 +99,25 @@ export type MiNotification = { * アプリ通知のアプリ(のトークン) */ appAccessTokenId: MiAccessToken['id'] | null; -} +} | { + type: 'test'; + id: string; + createdAt: string; +}; + +export type MiGroupedNotification = MiNotification | { + type: 'reaction:grouped'; + id: string; + createdAt: string; + noteId: MiNote['id']; + reactions: { + userId: string; + reaction: string; + }[]; +} | { + type: 'renote:grouped'; + id: string; + createdAt: string; + noteId: MiNote['id']; + userIds: string[]; +}; diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index 2c434913da..27db3bb62c 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -12,7 +12,6 @@ export const packedNotificationSchema = { type: 'string', optional: false, nullable: false, format: 'id', - example: 'xxxxxxxxxx', }, createdAt: { type: 'string', @@ -22,7 +21,7 @@ export const packedNotificationSchema = { type: { type: 'string', optional: false, nullable: false, - enum: [...notificationTypes], + enum: [...notificationTypes, 'reaction:grouped', 'renote:grouped'], }, user: { type: 'object', @@ -63,5 +62,33 @@ export const packedNotificationSchema = { type: 'string', optional: true, nullable: true, }, + reactions: { + type: 'array', + optional: true, nullable: true, + items: { + type: 'object', + properties: { + user: { + type: 'object', + ref: 'UserLite', + optional: false, nullable: false, + }, + reaction: { + type: 'string', + optional: false, nullable: false, + }, + }, + required: ['user', 'reaction'], + }, + }, + }, + users: { + type: 'array', + optional: true, nullable: true, + items: { + type: 'object', + ref: 'UserLite', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 376226be69..3f8a46d855 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -217,6 +217,7 @@ import * as ep___i_importMuting from './endpoints/i/import-muting.js'; import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js'; import * as ep___i_importAntennas from './endpoints/i/import-antennas.js'; import * as ep___i_notifications from './endpoints/i/notifications.js'; +import * as ep___i_notificationsGrouped from './endpoints/i/notifications-grouped.js'; import * as ep___i_pageLikes from './endpoints/i/page-likes.js'; import * as ep___i_pages from './endpoints/i/pages.js'; import * as ep___i_pin from './endpoints/i/pin.js'; @@ -574,6 +575,7 @@ const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep_ const $i_importUserLists: Provider = { provide: 'ep:i/import-user-lists', useClass: ep___i_importUserLists.default }; const $i_importAntennas: Provider = { provide: 'ep:i/import-antennas', useClass: ep___i_importAntennas.default }; const $i_notifications: Provider = { provide: 'ep:i/notifications', useClass: ep___i_notifications.default }; +const $i_notificationsGrouped: Provider = { provide: 'ep:i/notifications-grouped', useClass: ep___i_notificationsGrouped.default }; const $i_pageLikes: Provider = { provide: 'ep:i/page-likes', useClass: ep___i_pageLikes.default }; const $i_pages: Provider = { provide: 'ep:i/pages', useClass: ep___i_pages.default }; const $i_pin: Provider = { provide: 'ep:i/pin', useClass: ep___i_pin.default }; @@ -935,6 +937,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_importUserLists, $i_importAntennas, $i_notifications, + $i_notificationsGrouped, $i_pageLikes, $i_pages, $i_pin, @@ -1290,6 +1293,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_importUserLists, $i_importAntennas, $i_notifications, + $i_notificationsGrouped, $i_pageLikes, $i_pages, $i_pin, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 8be91469be..e87e1df591 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -217,6 +217,7 @@ import * as ep___i_importMuting from './endpoints/i/import-muting.js'; import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js'; import * as ep___i_importAntennas from './endpoints/i/import-antennas.js'; import * as ep___i_notifications from './endpoints/i/notifications.js'; +import * as ep___i_notificationsGrouped from './endpoints/i/notifications-grouped.js'; import * as ep___i_pageLikes from './endpoints/i/page-likes.js'; import * as ep___i_pages from './endpoints/i/pages.js'; import * as ep___i_pin from './endpoints/i/pin.js'; @@ -572,6 +573,7 @@ const eps = [ ['i/import-user-lists', ep___i_importUserLists], ['i/import-antennas', ep___i_importAntennas], ['i/notifications', ep___i_notifications], + ['i/notifications-grouped', ep___i_notificationsGrouped], ['i/page-likes', ep___i_pageLikes], ['i/pages', ep___i_pages], ['i/pin', ep___i_pin], diff --git a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts new file mode 100644 index 0000000000..4ea94b07f6 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts @@ -0,0 +1,178 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Brackets, In } from 'typeorm'; +import * as Redis from 'ioredis'; +import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository } from '@/models/_.js'; +import { obsoleteNotificationTypes, notificationTypes, FilterUnionByProperty } from '@/types.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteReadService } from '@/core/NoteReadService.js'; +import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; +import { NotificationService } from '@/core/NotificationService.js'; +import { DI } from '@/di-symbols.js'; +import { IdService } from '@/core/IdService.js'; +import { MiGroupedNotification, MiNotification } from '@/models/Notification.js'; + +export const meta = { + tags: ['account', 'notifications'], + + requireCredential: true, + + limit: { + duration: 30000, + max: 30, + }, + + kind: 'read:notifications', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Notification', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + markAsRead: { type: 'boolean', default: true }, + // 後方互換のため、廃止された通知タイプも受け付ける + includeTypes: { type: 'array', items: { + type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes], + } }, + excludeTypes: { type: 'array', items: { + type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes], + } }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private idService: IdService, + private notificationEntityService: NotificationEntityService, + private notificationService: NotificationService, + private noteReadService: NoteReadService, + ) { + super(meta, paramDef, async (ps, me) => { + const EXTRA_LIMIT = 100; + + // includeTypes が空の場合はクエリしない + if (ps.includeTypes && ps.includeTypes.length === 0) { + return []; + } + // excludeTypes に全指定されている場合はクエリしない + if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) { + return []; + } + + const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; + const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; + + const limit = (ps.limit + EXTRA_LIMIT) + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + const notificationsRes = await this.redisClient.xrevrange( + `notificationTimeline:${me.id}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-', + 'COUNT', limit); + + if (notificationsRes.length === 0) { + return []; + } + + let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as MiNotification[]; + + if (includeTypes && includeTypes.length > 0) { + notifications = notifications.filter(notification => includeTypes.includes(notification.type)); + } else if (excludeTypes && excludeTypes.length > 0) { + notifications = notifications.filter(notification => !excludeTypes.includes(notification.type)); + } + + if (notifications.length === 0) { + return []; + } + + // Mark all as read + if (ps.markAsRead) { + this.notificationService.readAllNotification(me.id); + } + + // grouping + let groupedNotifications = [notifications[0]] as MiGroupedNotification[]; + for (let i = 1; i < notifications.length; i++) { + const notification = notifications[i]; + const prev = notifications[i - 1]; + let prevGroupedNotification = groupedNotifications.at(-1)!; + + if (prev.type === 'reaction' && notification.type === 'reaction' && prev.noteId === notification.noteId) { + if (prevGroupedNotification.type !== 'reaction:grouped') { + groupedNotifications[groupedNotifications.length - 1] = { + type: 'reaction:grouped', + id: '', + createdAt: prev.createdAt, + noteId: prev.noteId!, + reactions: [{ + userId: prev.notifierId!, + reaction: prev.reaction!, + }], + }; + prevGroupedNotification = groupedNotifications.at(-1)!; + } + (prevGroupedNotification as FilterUnionByProperty<MiGroupedNotification, 'type', 'reaction:grouped'>).reactions.push({ + userId: notification.notifierId!, + reaction: notification.reaction!, + }); + prevGroupedNotification.id = notification.id; + continue; + } + if (prev.type === 'renote' && notification.type === 'renote' && prev.targetNoteId === notification.targetNoteId) { + if (prevGroupedNotification.type !== 'renote:grouped') { + groupedNotifications[groupedNotifications.length - 1] = { + type: 'renote:grouped', + id: '', + createdAt: notification.createdAt, + noteId: prev.noteId!, + userIds: [prev.notifierId!], + }; + prevGroupedNotification = groupedNotifications.at(-1)!; + } + (prevGroupedNotification as FilterUnionByProperty<MiGroupedNotification, 'type', 'renote:grouped'>).userIds.push(notification.notifierId!); + prevGroupedNotification.id = notification.id; + continue; + } + + groupedNotifications.push(notification); + } + + groupedNotifications = groupedNotifications.slice(0, ps.limit); + + const noteIds = groupedNotifications + .filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote'> => ['mention', 'reply', 'quote'].includes(notification.type)) + .map(notification => notification.noteId!); + + if (noteIds.length > 0) { + const notes = await this.notesRepository.findBy({ id: In(noteIds) }); + this.noteReadService.read(me.id, notes); + } + + return await this.notificationEntityService.packGroupedMany(groupedNotifications, me.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index 91dd72e805..039fd9454c 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -7,7 +7,7 @@ import { Brackets, In } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable } from '@nestjs/common'; import type { NotesRepository } from '@/models/_.js'; -import { obsoleteNotificationTypes, notificationTypes } from '@/types.js'; +import { obsoleteNotificationTypes, notificationTypes, FilterUnionByProperty } from '@/types.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteReadService } from '@/core/NoteReadService.js'; import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; @@ -113,8 +113,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } const noteIds = notifications - .filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)) - .map(notification => notification.noteId!); + .filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote'> => ['mention', 'reply', 'quote'].includes(notification.type)) + .map(notification => notification.noteId); if (noteIds.length > 0) { const notes = await this.notesRepository.findBy({ id: In(noteIds) }); diff --git a/packages/backend/src/server/api/endpoints/notifications/create.ts b/packages/backend/src/server/api/endpoints/notifications/create.ts index 19bc6fa8d7..7c6a979160 100644 --- a/packages/backend/src/server/api/endpoints/notifications/create.ts +++ b/packages/backend/src/server/api/endpoints/notifications/create.ts @@ -42,8 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.notificationService.createNotification(user.id, 'app', { appAccessTokenId: token ? token.id : null, customBody: ps.body, - customHeader: ps.header ?? token?.name, - customIcon: ps.icon ?? token?.iconUrl, + customHeader: ps.header ?? token?.name ?? null, + customIcon: ps.icon ?? token?.iconUrl ?? null, }); }); } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 69224360b3..e6dfeb6f8c 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -249,3 +249,9 @@ export type Serialized<T> = { ? Serialized<T[K]> : T[K]; }; + +export type FilterUnionByProperty< + Union, + Property extends string | number | symbol, + Condition +> = Union extends Record<Property, Condition> ? Union : never; diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index c507236216..ff20bc591f 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -9,9 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/> + <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div> + <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div> <img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/> <MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/> - <img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/> + <img v-else-if="notification.icon" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/> <div :class="[$style.subIcon, { [$style.t_follow]: notification.type === 'follow', @@ -39,7 +41,6 @@ SPDX-License-Identifier: AGPL-3.0-only v-else-if="notification.type === 'reaction'" ref="reactionRef" :reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction" - :customEmojis="notification.note.emojis" :noStyle="true" style="width: 100%; height: 100%;" /> @@ -52,16 +53,18 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> <span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span> <MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> + <span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.t('_notification.reactedBySomeUsers', { n: notification.reactions.length }) }}</span> + <span v-else-if="notification.type === 'renote:grouped'">{{ i18n.t('_notification.renotedBySomeUsers', { n: notification.users.length }) }}</span> <span v-else>{{ notification.header }}</span> <MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/> </header> <div> - <MkA v-if="notification.type === 'reaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> + <MkA v-if="notification.type === 'reaction' || notification.type === 'reaction:grouped'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> <i class="ti ti-quote" :class="$style.quote"></i> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/> <i class="ti ti-quote" :class="$style.quote"></i> </MkA> - <MkA v-else-if="notification.type === 'renote'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)"> + <MkA v-else-if="notification.type === 'renote' || notification.type === 'renote:grouped'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)"> <i class="ti ti-quote" :class="$style.quote"></i> <Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :author="notification.note.renote.user"/> <i class="ti ti-quote" :class="$style.quote"></i> @@ -102,6 +105,24 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="notification.type === 'app'" :class="$style.text"> <Mfm :text="notification.body" :nowrap="false"/> </span> + + <div v-if="notification.type === 'reaction:grouped'"> + <div v-for="reaction of notification.reactions" :class="$style.reactionsItem"> + <MkAvatar :class="$style.reactionsItemAvatar" :user="reaction.user" link preview/> + <div :class="$style.reactionsItemReaction"> + <MkReactionIcon + :reaction="reaction.reaction ? reaction.reaction.replace(/^:(\w+):$/, ':$1@.:') : reaction.reaction" + :noStyle="true" + style="width: 100%; height: 100%;" + /> + </div> + </div> + </div> + <div v-else-if="notification.type === 'renote:grouped'"> + <div v-for="user of notification.users" :class="$style.reactionsItem"> + <MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/> + </div> + </div> </div> </div> </div> @@ -181,6 +202,29 @@ useTooltip(reactionRef, (showing) => { display: block; width: 100%; height: 100%; +} + +.icon_reactionGroup, +.icon_renoteGroup { + display: grid; + align-items: center; + justify-items: center; + width: 80%; + height: 80%; + font-size: 15px; + border-radius: 100%; + color: #fff; +} + +.icon_reactionGroup { + background: #e99a0b; +} + +.icon_renoteGroup { + background: #36d298; +} + +.icon_app { border-radius: 6px; } @@ -305,6 +349,36 @@ useTooltip(reactionRef, (showing) => { flex: 1; } +.reactionsItem { + display: inline-block; + position: relative; + width: 38px; + height: 38px; + margin-top: 8px; + margin-right: 8px; +} + +.reactionsItemAvatar { + width: 100%; + height: 100%; +} + +.reactionsItemReaction { + position: absolute; + z-index: 1; + bottom: -2px; + right: -2px; + width: 20px; + height: 20px; + box-sizing: border-box; + border-radius: 100%; + background: var(--panel); + box-shadow: 0 0 0 3px var(--panel); + font-size: 11px; + text-align: center; + color: #fff; +} + @container (max-width: 600px) { .root { padding: 16px; diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 896f97a48d..8d99e440e1 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #default="{ items: notifications }"> <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/> - <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel notification"/> + <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/> </MkDateSeparatedList> </template> </MkPagination> @@ -32,6 +32,7 @@ import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { notificationTypes } from '@/const.js'; import { infoImageUrl } from '@/instance.js'; +import { defaultStore } from '@/store.js'; const props = defineProps<{ excludeTypes?: typeof notificationTypes[number][]; @@ -39,7 +40,13 @@ const props = defineProps<{ const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>(); -const pagination: Paging = { +const pagination: Paging = defaultStore.state.useGroupedNotifications ? { + endpoint: 'i/notifications-grouped' as const, + limit: 20, + params: computed(() => ({ + excludeTypes: props.excludeTypes ?? undefined, + })), +} : { endpoint: 'i/notifications' as const, limit: 20, params: computed(() => ({ diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 85d038e3d1..d96c984688 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -88,6 +88,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.notificationDisplay }}</template> <div class="_gaps_m"> + <MkSwitch v-model="useGroupedNotifications">{{ i18n.ts.useGroupedNotifications }}</MkSwitch> + <MkRadios v-model="notificationPosition"> <template #label>{{ i18n.ts.position }}</template> <option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option> @@ -255,6 +257,7 @@ const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificati const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn')); const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies')); const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline')); +const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications')); watch(lang, () => { miLocalStorage.setItem('lang', lang.value as string); diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 803f2f648d..0f2e642b7b 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -373,6 +373,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, + useGroupedNotifications: { + where: 'device', + default: true, + }, })); // TODO: 他のタブと永続化されたstateを同期 From 3b272b43ec48b1c1a537646292f759ea9db60e5e Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Thu, 2 Nov 2023 15:58:36 +0900 Subject: [PATCH 04/60] Update locales/ja-JP.yml Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com> --- locales/ja-JP.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 5e711fcdf4..4af21ab529 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1135,8 +1135,8 @@ showRepliesToOthersInTimeline: "TLに他の人への返信を含める" hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない" showRepliesToOthersInTimelineAll: "TLに現在フォロー中の人全員の返信を含めるようにする" hideRepliesToOthersInTimelineAll: "TLに現在フォロー中の人全員の返信を含めないようにする" -confirmShowRepliesAll: "この操作は元の戻せません。本当にTLに現在フォロー中の人全員の返信を含めるようにしますか" -confirmHideRepliesAll: "この操作は元の戻せません。本当にTLに現在フォロー中の人全員の返信を含めないようにしますか" +confirmShowRepliesAll: "この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めるようにしますか?" +confirmHideRepliesAll: "この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めないようにしますか?" externalServices: "外部サービス" impressum: "運営者情報" impressumUrl: "運営者情報URL" From cd0b6c1729b863347557a825b7deeefb947a03a2 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Thu, 2 Nov 2023 15:59:38 +0900 Subject: [PATCH 05/60] 2023.11.0-beta.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d4de09c9a6..4915d64da1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2023.11.0-beta.7", + "version": "2023.11.0-beta.8", "codename": "nasubi", "repository": { "type": "git", From d0d32e88466cef53a0d4429cce1ce4b9cb97d5b1 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Thu, 2 Nov 2023 18:07:42 +0900 Subject: [PATCH 06/60] enhance(frontend): improve pull to refresh --- .../src/components/MkNotifications.vue | 51 ++++++++++++------- .../src/components/MkPullToRefresh.vue | 16 ++++-- .../frontend/src/components/MkTimeline.vue | 25 ++++----- 3 files changed, 56 insertions(+), 36 deletions(-) diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 8d99e440e1..77e66f0165 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -4,25 +4,27 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkPagination ref="pagingComponent" :pagination="pagination"> - <template #empty> - <div class="_fullinfo"> - <img :src="infoImageUrl" class="_ghost"/> - <div>{{ i18n.ts.noNotifications }}</div> - </div> - </template> +<MkPullToRefresh :refresher="() => reload()"> + <MkPagination ref="pagingComponent" :pagination="pagination"> + <template #empty> + <div class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.noNotifications }}</div> + </div> + </template> - <template #default="{ items: notifications }"> - <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> - <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/> - <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/> - </MkDateSeparatedList> - </template> -</MkPagination> + <template #default="{ items: notifications }"> + <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> + <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/> + <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/> + </MkDateSeparatedList> + </template> + </MkPagination> +</MkPullToRefresh> </template> <script lang="ts" setup> -import { onUnmounted, onDeactivated, onMounted, computed, shallowRef } from 'vue'; +import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue'; import XNotification from '@/components/MkNotification.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; @@ -33,6 +35,7 @@ import { i18n } from '@/i18n.js'; import { notificationTypes } from '@/const.js'; import { infoImageUrl } from '@/instance.js'; import { defaultStore } from '@/store.js'; +import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; const props = defineProps<{ excludeTypes?: typeof notificationTypes[number][]; @@ -54,7 +57,7 @@ const pagination: Paging = defaultStore.state.useGroupedNotifications ? { })), }; -const onNotification = (notification) => { +function onNotification(notification) { const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false; if (isMuted || document.visibilityState === 'visible') { useStream().send('readNotification'); @@ -63,7 +66,15 @@ const onNotification = (notification) => { if (!isMuted) { pagingComponent.value.prepend(notification); } -}; +} + +function reload() { + return new Promise<void>((res) => { + pagingComponent.value?.reload().then(() => { + res(); + }); + }); +} let connection; @@ -72,6 +83,12 @@ onMounted(() => { connection.on('notification', onNotification); }); +onActivated(() => { + pagingComponent.value?.reload(); + connection = useStream().useChannel('main'); + connection.on('notification', onNotification); +}); + onUnmounted(() => { if (connection) connection.dispose(); }); diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue index c38d0ff6a1..f3f5660143 100644 --- a/packages/frontend/src/components/MkPullToRefresh.vue +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -47,7 +47,13 @@ let scrollEl: HTMLElement | null = null; let disabled = false; -const emits = defineEmits<{ +const props = withDefaults(defineProps<{ + refresher: () => Promise<void>; +}>(), { + refresher: () => Promise.resolve(), +}); + +const emit = defineEmits<{ (ev: 'refresh'): void; }>(); @@ -120,7 +126,12 @@ function moveEnd() { if (isPullEnd) { isPullEnd = false; isRefreshing = true; - fixOverContent().then(() => emits('refresh')); + fixOverContent().then(() => { + emit('refresh'); + props.refresher().then(() => { + refreshFinished(); + }); + }); } else { closeContent().then(() => isPullStart = false); } @@ -188,7 +199,6 @@ onUnmounted(() => { }); defineExpose({ - refreshFinished, setDisabled, }); </script> diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index a2ada35f91..845c7a414c 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkPullToRefresh ref="prComponent" @refresh="() => reloadTimeline(true)"> +<MkPullToRefresh ref="prComponent" :refresher="() => reloadTimeline()"> <MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)" @status="prComponent.setDisabled($event)"/> </MkPullToRefresh> </template> @@ -196,25 +196,18 @@ const pagination = { params: query, }; -const reloadTimeline = (fromPR = false) => { - tlNotesCount = 0; +function reloadTimeline() { + return new Promise<void>((res) => { + tlNotesCount = 0; - tlComponent.pagingComponent?.reload().then(() => { - reloadStream(); - if (fromPR) prComponent.refreshFinished(); + tlComponent.pagingComponent?.reload().then(() => { + reloadStream(); + res(); + }); }); -}; - -//const pullRefresh = () => reloadTimeline(true); +} defineExpose({ reloadTimeline, }); - -/* TODO -const timetravel = (date?: Date) => { - this.date = date; - this.$refs.tl.reload(); -}; -*/ </script> From ed699b4aedc8bec8df13e2a6777298992b0f0f95 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Thu, 2 Nov 2023 18:12:01 +0900 Subject: [PATCH 07/60] =?UTF-8?q?Revert=20"enhance(frontend):=20=E3=80=8C?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E3=82=92=E9=9A=A0=E3=81=99=E3=80=8D=E3=81=A7?= =?UTF-8?q?=E3=83=AA=E3=82=A2=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=82?= =?UTF-8?q?=E9=9A=A0=E3=82=8C=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit e85b8217c0eda4b0cb2ebf5642cabd2af7212140. --- packages/frontend/src/components/MkNote.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 7b8223dfea..b31ee78532 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> </div> - <MkReactionsViewer v-show="appearNote.cw == null || showContent" :note="appearNote" :maxNumber="16"> + <MkReactionsViewer :note="appearNote" :maxNumber="16"> <template #more> <div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div> </template> From e333e7ced8f319be704c5d871a6dd727691e99df Mon Sep 17 00:00:00 2001 From: Tom Anderson <twocs@users.noreply.github.com> Date: Thu, 2 Nov 2023 21:27:43 +1030 Subject: [PATCH 08/60] docs: Remove forum references and use Github Discussions (#12158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: Replace forum with Github Discussions * Remove outdated forum link from CONTRIBUTING.md * Remove outdated forum link from misskey-js/CONTRIBUTING.md * Remove outdated forum link from misskey-js/docs/CONTRIBUTING.en.md --------- Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- packages/misskey-js/CONTRIBUTING.md | 2 +- packages/misskey-js/docs/CONTRIBUTING.en.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 484fd99413..13e0656041 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ Before creating an issue, please check the following: - To avoid duplication, please search for similar issues before creating a new issue. - Do not use Issues to ask questions or troubleshooting. - Issues should only be used to feature requests, suggestions, and bug tracking. - - Please ask questions or troubleshooting in ~~the [Misskey Forum](https://forum.misskey.io/)~~ [GitHub Discussions](https://github.com/misskey-dev/misskey/discussions) or [Discord](https://discord.gg/Wp8gVStHW3). + - Please ask questions or troubleshooting in [GitHub Discussions](https://github.com/misskey-dev/misskey/discussions) or [Discord](https://discord.gg/Wp8gVStHW3). > **Warning** > Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged. diff --git a/packages/misskey-js/CONTRIBUTING.md b/packages/misskey-js/CONTRIBUTING.md index aa759345b0..4ed0c70a67 100644 --- a/packages/misskey-js/CONTRIBUTING.md +++ b/packages/misskey-js/CONTRIBUTING.md @@ -15,7 +15,7 @@ Issueを作成する前に、以下をご確認ください: - 重複を防ぐため、既に同様の内容のIssueが作成されていないか検索してから新しいIssueを作ってください。 - Issueを質問に使わないでください。 - Issueは、要望、提案、問題の報告にのみ使用してください。 - - 質問は、[Misskey Forum](https://forum.misskey.io/)や[Discord](https://discord.gg/Wp8gVStHW3)でお願いします。 + - 質問は、[GitHub Discussions](https://github.com/misskey-dev/misskey/discussions)や[Discord](https://discord.gg/Wp8gVStHW3)でお願いします。 ## PRの作成 PRを作成する前に、以下をご確認ください: diff --git a/packages/misskey-js/docs/CONTRIBUTING.en.md b/packages/misskey-js/docs/CONTRIBUTING.en.md index 1db282e356..22fea4c79d 100644 --- a/packages/misskey-js/docs/CONTRIBUTING.en.md +++ b/packages/misskey-js/docs/CONTRIBUTING.en.md @@ -11,7 +11,7 @@ Before creating an issue, please check the following: - To avoid duplication, please search for similar issues before creating a new issue. - Do not use Issues as a question. - Issues should only be used to feature requests, suggestions, and report problems. - - Please ask questions in the [Misskey Forum](https://forum.misskey.io/) or [Discord](https://discord.gg/Wp8gVStHW3). + - Please ask questions in [GitHub Discussions](https://github.com/misskey-dev/misskey/discussions) or [Discord](https://discord.gg/Wp8gVStHW3). ## Creating a PR Thank you for your PR! Before creating a PR, please check the following: From d20f778bd0a92fa9d3a489964f8e75981e4aadeb Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Thu, 2 Nov 2023 19:59:18 +0900 Subject: [PATCH 09/60] enhance(frontend): tweak MkNotification --- .../src/components/MkNotification.vue | 19 +++-------------- .../src/components/MkReactionIcon.vue | 21 ++++++++++++++++--- packages/frontend/src/scripts/use-tooltip.ts | 2 +- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index ff20bc591f..fcf4791240 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div ref="elRef" :class="$style.root"> +<div :class="$style.root"> <div :class="$style.head"> <MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/> @@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only <!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> <MkReactionIcon v-else-if="notification.type === 'reaction'" - ref="reactionRef" + :withTooltip="true" :reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction" :noStyle="true" style="width: 100%; height: 100%;" @@ -111,6 +111,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAvatar :class="$style.reactionsItemAvatar" :user="reaction.user" link preview/> <div :class="$style.reactionsItemReaction"> <MkReactionIcon + :withTooltip="true" :reaction="reaction.reaction ? reaction.reaction.replace(/^:(\w+):$/, ':$1@.:') : reaction.reaction" :noStyle="true" style="width: 100%; height: 100%;" @@ -133,14 +134,12 @@ import { ref, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; -import XReactionTooltip from '@/components/MkReactionTooltip.vue'; import MkButton from '@/components/MkButton.vue'; import { getNoteSummary } from '@/scripts/get-note-summary.js'; import { notePage } from '@/filters/note.js'; import { userPage } from '@/filters/user.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; -import { useTooltip } from '@/scripts/use-tooltip.js'; import { $i } from '@/account.js'; import { infoImageUrl } from '@/instance.js'; @@ -153,9 +152,6 @@ const props = withDefaults(defineProps<{ full: false, }); -const elRef = shallowRef<HTMLElement>(null); -const reactionRef = ref(null); - const followRequestDone = ref(false); const acceptFollowRequest = () => { @@ -167,15 +163,6 @@ const rejectFollowRequest = () => { followRequestDone.value = true; os.api('following/requests/reject', { userId: props.notification.user.id }); }; - -useTooltip(reactionRef, (showing) => { - os.popup(XReactionTooltip, { - showing, - reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction, - emojis: props.notification.note.emojis, - targetElement: reactionRef.value.$el, - }, {}, 'closed'); -}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkReactionIcon.vue b/packages/frontend/src/components/MkReactionIcon.vue index 55c812cbc1..fdc3bfd23c 100644 --- a/packages/frontend/src/components/MkReactionIcon.vue +++ b/packages/frontend/src/components/MkReactionIcon.vue @@ -4,16 +4,31 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkCustomEmoji v-if="reaction[0] === ':'" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl"/> -<MkEmoji v-else :emoji="reaction" :normal="true" :noStyle="noStyle"/> +<MkCustomEmoji v-if="reaction[0] === ':'" ref="elRef" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl"/> +<MkEmoji v-else ref="elRef" :emoji="reaction" :normal="true" :noStyle="noStyle"/> </template> <script lang="ts" setup> -import { } from 'vue'; +import { defineAsyncComponent, shallowRef } from 'vue'; +import { useTooltip } from '@/scripts/use-tooltip.js'; +import * as os from '@/os.js'; const props = defineProps<{ reaction: string; noStyle?: boolean; emojiUrl?: string; + withTooltip?: boolean; }>(); + +const elRef = shallowRef(); + +if (props.withTooltip) { + useTooltip(elRef, (showing) => { + os.popup(defineAsyncComponent(() => import('@/components/MkReactionTooltip.vue')), { + showing, + reaction: props.reaction.replace(/^:(\w+):$/, ':$1@.:'), + targetElement: elRef.value.$el, + }, {}, 'closed'); + }); +} </script> diff --git a/packages/frontend/src/scripts/use-tooltip.ts b/packages/frontend/src/scripts/use-tooltip.ts index 17ea380db0..aaf0a0285a 100644 --- a/packages/frontend/src/scripts/use-tooltip.ts +++ b/packages/frontend/src/scripts/use-tooltip.ts @@ -37,7 +37,7 @@ export function useTooltip( }; autoHidingTimer = window.setInterval(() => { - if (!document.body.contains(elRef.value)) { + if (elRef.value == null || !document.body.contains(elRef.value instanceof Element ? elRef.value : elRef.value.$el)) { if (!isHovering) return; isHovering = false; window.clearTimeout(timeoutId); From f1903b26a57cb30c4ec59e17751a09e19c3c5c8d Mon Sep 17 00:00:00 2001 From: Srgr0 <66754887+Srgr0@users.noreply.github.com> Date: Thu, 2 Nov 2023 20:02:00 +0900 Subject: [PATCH 10/60] =?UTF-8?q?fix=20=E7=B5=B5=E6=96=87=E5=AD=97?= =?UTF-8?q?=E3=83=94=E3=83=83=E3=82=AB=E3=83=BC=E3=81=A7=E3=83=90=E3=83=83?= =?UTF-8?q?=E3=83=86=E3=83=AA=E3=83=BC=E3=81=AE=E7=B5=B5=E6=96=87=E5=AD=97?= =?UTF-8?q?=E3=81=8C=E8=A4=87=E6=95=B0=E8=A1=A8=E7=A4=BA=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(#1221?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update emojilist.json * Update CHANGELOG.md --- CHANGELOG.md | 1 + packages/frontend/src/emojilist.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38ef92e953..42b1320897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ - Fix: チャンネルの作成・更新時に失敗した場合何も表示されない問題を修正 #11983 - Fix: 個人カードのemojiがバッテリーになっている問題を修正 - Fix: 標準テーマと同じIDを使用してインストールできてしまう問題を修正 +- Fix: 絵文字ピッカーでバッテリーの絵文字が複数表示される問題を修正 #12197 ### Server - Enhance: RedisへのTLのキャッシュ(FTT)をオフにできるように diff --git a/packages/frontend/src/emojilist.json b/packages/frontend/src/emojilist.json index eae822e652..fe1d884ebe 100644 --- a/packages/frontend/src/emojilist.json +++ b/packages/frontend/src/emojilist.json @@ -1045,7 +1045,7 @@ ["⌛", "hourglass", 6], ["📡", "satellite", 6], ["🔋", "battery", 6], - ["🪫", "battery", 6], + ["🪫", "low_battery", 6], ["🔌", "electric_plug", 6], ["💡", "bulb", 6], ["🔦", "flashlight", 6], From 5f888809e9427ed3483abaebe2d903ab46137166 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Fri, 3 Nov 2023 08:00:26 +0900 Subject: [PATCH 11/60] clean up --- packages/frontend/src/components/global/MkAcct.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue index 42d29db488..594494f3c8 100644 --- a/packages/frontend/src/components/global/MkAcct.vue +++ b/packages/frontend/src/components/global/MkAcct.vue @@ -17,7 +17,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; import { toUnicode } from 'punycode/'; -import MkCondensedLine from './MkCondensedLine.vue'; import { host as hostRaw } from '@/config.js'; import { defaultStore } from '@/store.js'; From 7f5ad5badbf9574ca7183d7381333fb06cd31a15 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Fri, 3 Nov 2023 08:01:22 +0900 Subject: [PATCH 12/60] enhance(frontend): tweak drive file component Resolve #12220 --- packages/frontend/src/components/MkDrive.file.vue | 7 ++++++- packages/frontend/src/pages/drive.file.info.vue | 4 ++++ packages/frontend/src/scripts/get-drive-file-menu.ts | 10 +++++----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index 96704996f9..b46b25eba2 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -47,6 +47,7 @@ import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; import { useRouter } from '@/router.js'; import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js'; +import { deviceKind } from '@/scripts/device-kind.js'; const router = useRouter(); @@ -74,7 +75,11 @@ function onClick(ev: MouseEvent) { if (props.selectMode) { emit('chosen', props.file); } else { - router.push(`/my/drive/file/${props.file.id}`); + if (deviceKind === 'desktop') { + router.push(`/my/drive/file/${props.file.id}`); + } else { + os.popupMenu(getDriveFileMenu(props.file, props.folder), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); + } } } diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue index ae9256b8e3..1a2fc197f9 100644 --- a/packages/frontend/src/pages/drive.file.info.vue +++ b/packages/frontend/src/pages/drive.file.info.vue @@ -56,6 +56,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template #key>{{ i18n.ts._fileViewer.size }}</template> <template #value>{{ bytes(file.size) }}</template> </MkKeyValue> + <MkKeyValue :class="$style.fileMetaDataChildren" :copy="file.url"> + <template #key>URL</template> + <template #value>{{ file.url }}</template> + </MkKeyValue> </div> </div> <div v-else class="_fullinfo"> diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index d1cafdf27b..23a1a77bfb 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -78,6 +78,11 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss const isImage = file.type.startsWith('image/'); let menu; menu = [{ + type: 'link', + to: `/my/drive/file/${file.id}`, + text: i18n.ts._fileViewer.title, + icon: 'ti ti-info-circle', + }, null, { text: i18n.ts.rename, icon: 'ti ti-forms', action: () => rename(file), @@ -113,11 +118,6 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss text: i18n.ts.download, icon: 'ti ti-download', download: file.name, - }, null, { - type: 'link', - to: `/my/drive/file/${file.id}`, - text: i18n.ts._fileViewer.title, - icon: 'ti ti-file', }, null, { text: i18n.ts.delete, icon: 'ti ti-trash', From 82526ad4f39ce5864feef2dabf9ec2feb810d063 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Fri, 3 Nov 2023 08:17:35 +0900 Subject: [PATCH 13/60] =?UTF-8?q?CW=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=99?= =?UTF-8?q?=E3=82=8B=E5=A0=B4=E5=90=88=E3=80=81=E6=B3=A8=E9=87=88=E3=82=92?= =?UTF-8?q?=E7=A9=BA=E3=81=AB=E3=81=99=E3=82=8B=E3=81=93=E3=81=A8=E3=82=92?= =?UTF-8?q?=E8=A8=B1=E5=8F=AF=E3=81=97=E3=81=AA=E3=81=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve #12217 --- CHANGELOG.md | 1 + locales/index.d.ts | 1 + locales/ja-JP.yml | 1 + packages/backend/src/server/api/endpoints/notes/create.ts | 4 ++-- packages/frontend/src/components/MkPostForm.vue | 8 ++++++++ 5 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42b1320897..82822c903d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Enhance: 未読の通知数を表示できるように - Enhance: ローカリゼーションの更新 - Enhance: 依存関係の更新 +- Change: CWを使用する場合、注釈を空にすることは許可されなくなりました ### Client - Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました diff --git a/locales/index.d.ts b/locales/index.d.ts index eb27983087..b8dc3a68bc 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1158,6 +1158,7 @@ export interface Locale { "pullDownToRefresh": string; "disableStreamingTimeline": string; "useGroupedNotifications": string; + "cwNotationRequired": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 4af21ab529..76b5386b39 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1155,6 +1155,7 @@ refreshing: "リロード中" pullDownToRefresh: "引っ張ってリロード" disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする" useGroupedNotifications: "通知をグルーピングして表示する" +cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。" _announcement: forExistingUsers: "既存ユーザーのみ" diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 649068fb20..fb650f69ff 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -16,8 +16,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { isPureRenote } from '@/misc/is-pure-renote.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['notes'], @@ -109,7 +109,7 @@ export const paramDef = { visibleUserIds: { type: 'array', uniqueItems: true, items: { type: 'string', format: 'misskey:id', } }, - cw: { type: 'string', nullable: true, maxLength: 100 }, + cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 }, localOnly: { type: 'boolean', default: false }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, noExtractMentions: { type: 'boolean', default: false }, diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 598846b166..1fa5685861 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -658,6 +658,14 @@ function deleteDraft() { } async function post(ev?: MouseEvent) { + if (useCw && (cw == null || cw.trim() === '')) { + os.alert({ + type: 'error', + text: i18n.ts.cwNotationRequired, + }); + return; + } + if (ev) { const el = ev.currentTarget ?? ev.target; const rect = el.getBoundingClientRect(); From 79346272f8792d35955efd3aaaa1e42e0cd2a6e3 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Fri, 3 Nov 2023 13:23:03 +0900 Subject: [PATCH 14/60] =?UTF-8?q?feat:=20=E3=83=AC=E3=82=B8=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=83=AAAPI=E3=82=92=E3=82=B5=E3=83=BC=E3=83=89?= =?UTF-8?q?=E3=83=91=E3=83=BC=E3=83=86=E3=82=A3=E3=81=8B=E3=82=89=E5=88=A9?= =?UTF-8?q?=E7=94=A8=E5=8F=AF=E8=83=BD=E3=81=AB=20(#12229)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * wip * Update remove.ts * refactor --- CHANGELOG.md | 1 + packages/backend/src/core/CoreModule.ts | 6 + .../backend/src/core/RegistryApiService.ts | 147 ++++++++++++++++++ .../backend/src/server/api/EndpointsModule.ts | 8 +- packages/backend/src/server/api/endpoints.ts | 4 +- .../api/endpoints/i/registry/get-all.ts | 20 +-- .../api/endpoints/i/registry/get-detail.ts | 21 +-- .../server/api/endpoints/i/registry/get.ts | 21 +-- .../endpoints/i/registry/keys-with-type.ts | 34 ++-- .../server/api/endpoints/i/registry/keys.ts | 23 +-- .../server/api/endpoints/i/registry/remove.ts | 25 +-- .../i/registry/scopes-with-domain.ts | 30 ++++ .../server/api/endpoints/i/registry/scopes.ts | 47 ------ .../server/api/endpoints/i/registry/set.ts | 50 +----- packages/frontend/src/pages/about-misskey.vue | 2 +- packages/frontend/src/pages/about.vue | 4 +- packages/frontend/src/pages/registry.keys.vue | 10 +- .../frontend/src/pages/registry.value.vue | 6 +- packages/frontend/src/pages/registry.vue | 20 +-- packages/frontend/src/router.ts | 6 +- packages/frontend/src/style.scss | 6 - packages/misskey-js/etc/misskey-js.api.md | 6 +- packages/misskey-js/src/api.types.ts | 1 - 23 files changed, 268 insertions(+), 230 deletions(-) create mode 100644 packages/backend/src/core/RegistryApiService.ts create mode 100644 packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/registry/scopes.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 82822c903d..bbf99bee95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ - Fix: 絵文字ピッカーでバッテリーの絵文字が複数表示される問題を修正 #12197 ### Server +- Feat: Registry APIがサードパーティから利用可能になりました - Enhance: RedisへのTLのキャッシュ(FTT)をオフにできるように - Enhance: フォローしているチャンネルをフォロー解除した時(またはその逆)、タイムラインに反映される間隔を改善 - Enhance: プロフィールの自己紹介欄のMFMが連合するようになりました diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index c17ea9999a..9fb29e0e68 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -64,6 +64,7 @@ import { ClipService } from './ClipService.js'; import { FeaturedService } from './FeaturedService.js'; import { FunoutTimelineService } from './FunoutTimelineService.js'; import { ChannelFollowingService } from './ChannelFollowingService.js'; +import { RegistryApiService } from './RegistryApiService.js'; import { ChartLoggerService } from './chart/ChartLoggerService.js'; import FederationChart from './chart/charts/federation.js'; import NotesChart from './chart/charts/notes.js'; @@ -195,6 +196,7 @@ const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipServic const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService }; const $FunoutTimelineService: Provider = { provide: 'FunoutTimelineService', useExisting: FunoutTimelineService }; const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService }; +const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService }; const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService }; const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart }; @@ -330,6 +332,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FeaturedService, FunoutTimelineService, ChannelFollowingService, + RegistryApiService, ChartLoggerService, FederationChart, NotesChart, @@ -458,6 +461,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FeaturedService, $FunoutTimelineService, $ChannelFollowingService, + $RegistryApiService, $ChartLoggerService, $FederationChart, $NotesChart, @@ -587,6 +591,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FeaturedService, FunoutTimelineService, ChannelFollowingService, + RegistryApiService, FederationChart, NotesChart, UsersChart, @@ -714,6 +719,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FeaturedService, $FunoutTimelineService, $ChannelFollowingService, + $RegistryApiService, $FederationChart, $NotesChart, $UsersChart, diff --git a/packages/backend/src/core/RegistryApiService.ts b/packages/backend/src/core/RegistryApiService.ts new file mode 100644 index 0000000000..d340c5e480 --- /dev/null +++ b/packages/backend/src/core/RegistryApiService.ts @@ -0,0 +1,147 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { MiRegistryItem, RegistryItemsRepository } from '@/models/_.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import type { MiUser } from '@/models/User.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { bindThis } from '@/decorators.js'; + +@Injectable() +export class RegistryApiService { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + + private idService: IdService, + private globalEventService: GlobalEventService, + ) { + } + + @bindThis + public async set(userId: MiUser['id'], domain: string | null, scope: string[], key: string, value: any) { + // TODO: 作成できるキーの数を制限する + + const query = this.registryItemsRepository.createQueryBuilder('item'); + if (domain) { + query.where('item.domain = :domain', { domain: domain }); + } else { + query.where('item.domain IS NULL'); + } + query.andWhere('item.userId = :userId', { userId: userId }); + query.andWhere('item.key = :key', { key: key }); + query.andWhere('item.scope = :scope', { scope: scope }); + + const existingItem = await query.getOne(); + + if (existingItem) { + await this.registryItemsRepository.update(existingItem.id, { + updatedAt: new Date(), + value: value, + }); + } else { + await this.registryItemsRepository.insert({ + id: this.idService.gen(), + updatedAt: new Date(), + userId: userId, + domain: domain, + scope: scope, + key: key, + value: value, + }); + } + + if (domain == null) { + // TODO: サードパーティアプリが傍受出来てしまうのでどうにかする + this.globalEventService.publishMainStream(userId, 'registryUpdated', { + scope: scope, + key: key, + value: value, + }); + } + } + + @bindThis + public async getItem(userId: MiUser['id'], domain: string | null, scope: string[], key: string): Promise<MiRegistryItem | null> { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where(domain == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: domain }) + .andWhere('item.userId = :userId', { userId: userId }) + .andWhere('item.key = :key', { key: key }) + .andWhere('item.scope = :scope', { scope: scope }); + + const item = await query.getOne(); + + return item; + } + + @bindThis + public async getAllItemsOfScope(userId: MiUser['id'], domain: string | null, scope: string[]): Promise<MiRegistryItem[]> { + const query = this.registryItemsRepository.createQueryBuilder('item'); + query.where(domain == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: domain }); + query.andWhere('item.userId = :userId', { userId: userId }); + query.andWhere('item.scope = :scope', { scope: scope }); + + const items = await query.getMany(); + + return items; + } + + @bindThis + public async getAllKeysOfScope(userId: MiUser['id'], domain: string | null, scope: string[]): Promise<string[]> { + const query = this.registryItemsRepository.createQueryBuilder('item'); + query.select('item.key'); + query.where(domain == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: domain }); + query.andWhere('item.userId = :userId', { userId: userId }); + query.andWhere('item.scope = :scope', { scope: scope }); + + const items = await query.getMany(); + + return items.map(x => x.key); + } + + @bindThis + public async getAllScopeAndDomains(userId: MiUser['id']): Promise<{ domain: string | null; scopes: string[][] }[]> { + const query = this.registryItemsRepository.createQueryBuilder('item') + .select(['item.scope', 'item.domain']) + .where('item.userId = :userId', { userId: userId }); + + const items = await query.getMany(); + + const res = [] as { domain: string | null; scopes: string[][] }[]; + + for (const item of items) { + const target = res.find(x => x.domain === item.domain); + if (target) { + if (target.scopes.some(scope => scope.join('.') === item.scope.join('.'))) continue; + target.scopes.push(item.scope); + } else { + res.push({ + domain: item.domain, + scopes: [item.scope], + }); + } + } + + return res; + } + + @bindThis + public async remove(userId: MiUser['id'], domain: string | null, scope: string[], key: string) { + const query = this.registryItemsRepository.createQueryBuilder().delete(); + if (domain) { + query.where('domain = :domain', { domain: domain }); + } else { + query.where('domain IS NULL'); + } + query.andWhere('userId = :userId', { userId: userId }); + query.andWhere('key = :key', { key: key }); + query.andWhere('scope = :scope', { scope: scope }); + + await query.execute(); + } +} diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 3f8a46d855..23067a9b26 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -230,7 +230,7 @@ import * as ep___i_registry_get from './endpoints/i/registry/get.js'; import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js'; import * as ep___i_registry_keys from './endpoints/i/registry/keys.js'; import * as ep___i_registry_remove from './endpoints/i/registry/remove.js'; -import * as ep___i_registry_scopes from './endpoints/i/registry/scopes.js'; +import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js'; import * as ep___i_registry_set from './endpoints/i/registry/set.js'; import * as ep___i_revokeToken from './endpoints/i/revoke-token.js'; import * as ep___i_signinHistory from './endpoints/i/signin-history.js'; @@ -588,7 +588,7 @@ const $i_registry_get: Provider = { provide: 'ep:i/registry/get', useClass: ep__ const $i_registry_keysWithType: Provider = { provide: 'ep:i/registry/keys-with-type', useClass: ep___i_registry_keysWithType.default }; const $i_registry_keys: Provider = { provide: 'ep:i/registry/keys', useClass: ep___i_registry_keys.default }; const $i_registry_remove: Provider = { provide: 'ep:i/registry/remove', useClass: ep___i_registry_remove.default }; -const $i_registry_scopes: Provider = { provide: 'ep:i/registry/scopes', useClass: ep___i_registry_scopes.default }; +const $i_registry_scopesWithDomain: Provider = { provide: 'ep:i/registry/scopes-with-domain', useClass: ep___i_registry_scopesWithDomain.default }; const $i_registry_set: Provider = { provide: 'ep:i/registry/set', useClass: ep___i_registry_set.default }; const $i_revokeToken: Provider = { provide: 'ep:i/revoke-token', useClass: ep___i_revokeToken.default }; const $i_signinHistory: Provider = { provide: 'ep:i/signin-history', useClass: ep___i_signinHistory.default }; @@ -950,7 +950,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_registry_keysWithType, $i_registry_keys, $i_registry_remove, - $i_registry_scopes, + $i_registry_scopesWithDomain, $i_registry_set, $i_revokeToken, $i_signinHistory, @@ -1306,7 +1306,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_registry_keysWithType, $i_registry_keys, $i_registry_remove, - $i_registry_scopes, + $i_registry_scopesWithDomain, $i_registry_set, $i_revokeToken, $i_signinHistory, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index e87e1df591..af798fd166 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -230,7 +230,7 @@ import * as ep___i_registry_get from './endpoints/i/registry/get.js'; import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js'; import * as ep___i_registry_keys from './endpoints/i/registry/keys.js'; import * as ep___i_registry_remove from './endpoints/i/registry/remove.js'; -import * as ep___i_registry_scopes from './endpoints/i/registry/scopes.js'; +import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js'; import * as ep___i_registry_set from './endpoints/i/registry/set.js'; import * as ep___i_revokeToken from './endpoints/i/revoke-token.js'; import * as ep___i_signinHistory from './endpoints/i/signin-history.js'; @@ -586,7 +586,7 @@ const eps = [ ['i/registry/keys-with-type', ep___i_registry_keysWithType], ['i/registry/keys', ep___i_registry_keys], ['i/registry/remove', ep___i_registry_remove], - ['i/registry/scopes', ep___i_registry_scopes], + ['i/registry/scopes-with-domain', ep___i_registry_scopesWithDomain], ['i/registry/set', ep___i_registry_set], ['i/revoke-token', ep___i_revokeToken], ['i/signin-history', ep___i_signinHistory], diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts index 211e6637dc..29fa0a29cc 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts @@ -5,13 +5,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, - - secure: true, } as const; export const paramDef = { @@ -20,23 +17,18 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, + domain: { type: 'string', nullable: true }, }, - required: [], + required: ['scope'], } as const; @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, + private registryApiService: RegistryApiService, ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const items = await query.getMany(); + super(meta, paramDef, async (ps, me, accessToken) => { + const items = await this.registryApiService.getAllItemsOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope); const res = {} as Record<string, any>; diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts index 9c6f2d6781..5b460b45d6 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts @@ -5,15 +5,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, - secure: true, - errors: { noSuchKey: { message: 'No such key.', @@ -30,24 +27,18 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, + domain: { type: 'string', nullable: true }, }, - required: ['key'], + required: ['key', 'scope'], } as const; @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, + private registryApiService: RegistryApiService, ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }) - .andWhere('item.key = :key', { key: ps.key }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const item = await query.getOne(); + super(meta, paramDef, async (ps, me, accessToken) => { + const item = await this.registryApiService.getItem(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key); if (item == null) { throw new ApiError(meta.errors.noSuchKey); diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts index 729e729b8c..e8c28298ef 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -5,15 +5,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, - secure: true, - errors: { noSuchKey: { message: 'No such key.', @@ -30,24 +27,18 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, + domain: { type: 'string', nullable: true }, }, - required: ['key'], + required: ['key', 'scope'], } as const; @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, + private registryApiService: RegistryApiService, ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }) - .andWhere('item.key = :key', { key: ps.key }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const item = await query.getOne(); + super(meta, paramDef, async (ps, me, accessToken) => { + const item = await this.registryApiService.getItem(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key); if (item == null) { throw new ApiError(meta.errors.noSuchKey); diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts index ffd2860fde..8953ee5d3d 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts @@ -5,13 +5,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, - - secure: true, } as const; export const paramDef = { @@ -20,36 +17,31 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, + domain: { type: 'string', nullable: true }, }, - required: [], + required: ['scope'], } as const; @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, + private registryApiService: RegistryApiService, ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const items = await query.getMany(); + super(meta, paramDef, async (ps, me, accessToken) => { + const items = await this.registryApiService.getAllItemsOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope); const res = {} as Record<string, string>; for (const item of items) { const type = typeof item.value; res[item.key] = - item.value === null ? 'null' : - Array.isArray(item.value) ? 'array' : - type === 'number' ? 'number' : - type === 'string' ? 'string' : - type === 'boolean' ? 'boolean' : - type === 'object' ? 'object' : - null as never; + item.value === null ? 'null' : + Array.isArray(item.value) ? 'array' : + type === 'number' ? 'number' : + type === 'string' ? 'string' : + type === 'boolean' ? 'boolean' : + type === 'object' ? 'object' : + null as never; } return res; diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys.ts b/packages/backend/src/server/api/endpoints/i/registry/keys.ts index 7239bb66e1..04e120d752 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts @@ -5,13 +5,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, - - secure: true, } as const; export const paramDef = { @@ -20,26 +17,18 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, + domain: { type: 'string', nullable: true }, }, - required: [], + required: ['scope'], } as const; @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, + private registryApiService: RegistryApiService, ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .select('item.key') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const items = await query.getMany(); - - return items.map(x => x.key); + super(meta, paramDef, async (ps, me, accessToken) => { + return await this.registryApiService.getAllKeysOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/registry/remove.ts b/packages/backend/src/server/api/endpoints/i/registry/remove.ts index ae687fefe9..ba8100b547 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts @@ -7,13 +7,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { RegistryItemsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, - secure: true, - errors: { noSuchKey: { message: 'No such key.', @@ -30,30 +29,18 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, + domain: { type: 'string', nullable: true }, }, - required: ['key'], + required: ['key', 'scope'], } as const; @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, + private registryApiService: RegistryApiService, ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }) - .andWhere('item.key = :key', { key: ps.key }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const item = await query.getOne(); - - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); - } - - await this.registryItemsRepository.remove(item); + super(meta, paramDef, async (ps, me, accessToken) => { + await this.registryApiService.remove(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts new file mode 100644 index 0000000000..1ff994b82c --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; + +export const meta = { + requireCredential: true, + secure: true, +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private registryApiService: RegistryApiService, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.registryApiService.getAllScopeAndDomains(me.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts deleted file mode 100644 index 7637cdcf73..0000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, - ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .select('item.scope') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }); - - const items = await query.getMany(); - - const res = [] as string[][]; - - for (const item of items) { - if (res.some(scope => scope.join('.') === item.scope.join('.'))) continue; - res.push(item.scope); - } - - return res; - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts index 6203e7aa8b..58bb450bce 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts @@ -5,15 +5,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, - - secure: true, } as const; export const paramDef = { @@ -24,51 +19,18 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, + domain: { type: 'string', nullable: true }, }, - required: ['key', 'value'], + required: ['key', 'value', 'scope'], } as const; @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, - - private idService: IdService, - private globalEventService: GlobalEventService, + private registryApiService: RegistryApiService, ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }) - .andWhere('item.key = :key', { key: ps.key }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const existingItem = await query.getOne(); - - if (existingItem) { - await this.registryItemsRepository.update(existingItem.id, { - updatedAt: new Date(), - value: ps.value, - }); - } else { - await this.registryItemsRepository.insert({ - id: this.idService.gen(), - updatedAt: new Date(), - userId: me.id, - domain: null, - scope: ps.scope, - key: ps.key, - value: ps.value, - }); - } - - // TODO: サードパーティアプリが傍受出来てしまうのでどうにかする - this.globalEventService.publishMainStream(me.id, 'registryUpdated', { - scope: ps.scope, - key: ps.key, - value: ps.value, - }); + super(meta, paramDef, async (ps, me, accessToken) => { + await this.registryApiService.set(me.id, accessToken ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key, ps.value); }); } } diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index 7a2c698d11..b446a4d554 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton> </div> <FormSection> - <div class="_formLinks"> + <div class="_gaps_s"> <FormLink to="https://github.com/misskey-dev/misskey" external> <template #icon><i class="ti ti-code"></i></template> {{ i18n.ts._aboutMisskey.source }} diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index ee4043f9a5..4fa409ff4b 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkKeyValue> </FormSplit> <FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>{{ i18n.ts.impressum }}</FormLink> - <div class="_formLinks"> + <div class="_gaps_s"> <MkFolder v-if="instance.serverRules.length > 0"> <template #label>{{ i18n.ts.serverRules }}</template> @@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection> <template #label>Well-known resources</template> - <div class="_formLinks"> + <div class="_gaps_s"> <FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink> <FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink> <FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink> diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue index a1a5fd0cf3..387cb2f1f7 100644 --- a/packages/frontend/src/pages/registry.keys.vue +++ b/packages/frontend/src/pages/registry.keys.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSplit> <MkKeyValue> <template #key>{{ i18n.ts._registry.domain }}</template> - <template #value>{{ i18n.ts.system }}</template> + <template #value>{{ props.domain === '@' ? i18n.ts.system : props.domain.toUpperCase() }}</template> </MkKeyValue> <MkKeyValue> <template #key>{{ i18n.ts._registry.scope }}</template> @@ -23,8 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection v-if="keys"> <template #label>{{ i18n.ts.keys }}</template> - <div class="_formLinks"> - <FormLink v-for="key in keys" :to="`/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink> + <div class="_gaps_s"> + <FormLink v-for="key in keys" :to="`/registry/value/${props.domain}/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink> </div> </FormSection> </div> @@ -46,15 +46,17 @@ import FormSplit from '@/components/form/split.vue'; const props = defineProps<{ path: string; + domain: string; }>(); -const scope = $computed(() => props.path.split('/')); +const scope = $computed(() => props.path ? props.path.split('/') : []); let keys = $ref(null); function fetchKeys() { os.api('i/registry/keys-with-type', { scope: scope, + domain: props.domain === '@' ? null : props.domain, }).then(res => { keys = Object.entries(res).sort((a, b) => a[0].localeCompare(b[0])); }); diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue index ebcb04e9f5..68d6c8c1a0 100644 --- a/packages/frontend/src/pages/registry.value.vue +++ b/packages/frontend/src/pages/registry.value.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSplit> <MkKeyValue> <template #key>{{ i18n.ts._registry.domain }}</template> - <template #value>{{ i18n.ts.system }}</template> + <template #value>{{ props.domain === '@' ? i18n.ts.system : props.domain.toUpperCase() }}</template> </MkKeyValue> <MkKeyValue> <template #key>{{ i18n.ts._registry.scope }}</template> @@ -58,6 +58,7 @@ import FormInfo from '@/components/MkInfo.vue'; const props = defineProps<{ path: string; + domain: string; }>(); const scope = $computed(() => props.path.split('/').slice(0, -1)); @@ -70,6 +71,7 @@ function fetchValue() { os.api('i/registry/get-detail', { scope, key, + domain: props.domain === '@' ? null : props.domain, }).then(res => { value = res; valueForEditor = JSON5.stringify(res.value, null, '\t'); @@ -95,6 +97,7 @@ async function save() { scope, key, value: JSON5.parse(valueForEditor), + domain: props.domain === '@' ? null : props.domain, }); }); } @@ -108,6 +111,7 @@ function del() { os.apiWithDialog('i/registry/remove', { scope, key, + domain: props.domain === '@' ? null : props.domain, }); }); } diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue index 37a0b52511..d0a3df5deb 100644 --- a/packages/frontend/src/pages/registry.vue +++ b/packages/frontend/src/pages/registry.vue @@ -9,12 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="600" :marginMin="16"> <MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton> - <FormSection v-if="scopes"> - <template #label>{{ i18n.ts.system }}</template> - <div class="_formLinks"> - <FormLink v-for="scope in scopes" :to="`/registry/keys/system/${scope.join('/')}`" class="_monospace">{{ scope.join('/') }}</FormLink> - </div> - </FormSection> + <div v-if="scopesWithDomain" class="_gaps_m"> + <FormSection v-for="domain in scopesWithDomain" :key="domain.domain"> + <template #label>{{ domain.domain ? domain.domain.toUpperCase() : i18n.ts.system }}</template> + <div class="_gaps_s"> + <FormLink v-for="scope in domain.scopes" :to="`/registry/keys/${domain.domain ?? '@'}/${scope.join('/')}`" class="_monospace">{{ scope.length === 0 ? '(root)' : scope.join('/') }}</FormLink> + </div> + </FormSection> + </div> </MkSpacer> </MkStickyContainer> </template> @@ -28,11 +30,11 @@ import FormLink from '@/components/form/link.vue'; import FormSection from '@/components/form/section.vue'; import MkButton from '@/components/MkButton.vue'; -let scopes = $ref(null); +let scopesWithDomain = $ref(null); function fetchScopes() { - os.api('i/registry/scopes').then(res => { - scopes = res.slice().sort((a, b) => a.join('/').localeCompare(b.join('/'))); + os.api('i/registry/scopes-with-domain').then(res => { + scopesWithDomain = res; }); } diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index ef0f5343bb..b81811d2e7 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -4,7 +4,7 @@ */ import { AsyncComponentLoader, defineAsyncComponent, inject } from 'vue'; -import { Router } from '@/nirax'; +import { Router } from '@/nirax.js'; import { $i, iAmModerator } from '@/account.js'; import MkLoading from '@/pages/_loading_.vue'; import MkError from '@/pages/_error_.vue'; @@ -318,10 +318,10 @@ export const routes = [{ name: 'avatarDecorations', component: page(() => import('./pages/avatar-decorations.vue')), }, { - path: '/registry/keys/system/:path(*)?', + path: '/registry/keys/:domain/:path(*)?', component: page(() => import('./pages/registry.keys.vue')), }, { - path: '/registry/value/system/:path(*)?', + path: '/registry/value/:domain/:path(*)?', component: page(() => import('./pages/registry.value.vue')), }, { path: '/registry', diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index 28fb5ba2a5..7bb443cece 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -344,12 +344,6 @@ hr { grid-gap: 12px; } -._formLinks { - > *:not(:last-child) { - margin-bottom: 8px; - } -} - ._beta { margin-left: 0.7em; font-size: 65%; diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 8f389086c9..a15e5888e8 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1482,10 +1482,6 @@ export type Endpoints = { }; res: null; }; - 'i/registry/scopes': { - req: NoParams; - res: string[][]; - }; 'i/registry/set': { req: { key: string; @@ -3023,7 +3019,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u // // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts -// src/api.types.ts:633:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts +// src/api.types.ts:632:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts // src/entities.ts:116:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts // src/entities.ts:612:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts index e1c2aaf51d..54b175fcf1 100644 --- a/packages/misskey-js/src/api.types.ts +++ b/packages/misskey-js/src/api.types.ts @@ -399,7 +399,6 @@ export type Endpoints = { 'i/registry/keys-with-type': { req: { scope?: string[]; }; res: Record<string, 'null' | 'array' | 'number' | 'string' | 'boolean' | 'object'>; }; 'i/registry/keys': { req: { scope?: string[]; }; res: string[]; }; 'i/registry/remove': { req: { key: string; scope?: string[]; }; res: null; }; - 'i/registry/scopes': { req: NoParams; res: string[][]; }; 'i/registry/set': { req: { key: string; value: any; scope?: string[]; }; res: null; }; 'i/revoke-token': { req: TODO; res: TODO; }; 'i/signin-history': { req: { limit?: number; sinceId?: Signin['id']; untilId?: Signin['id']; }; res: Signin[]; }; From 0efacdfcf0d94ccaa5499af66c9bed5a13875184 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Fri, 3 Nov 2023 13:26:48 +0900 Subject: [PATCH 15/60] fix cw test --- packages/backend/src/server/api/endpoints/notes/create.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts index bfb024bcf2..6086f99c92 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.test.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts @@ -64,7 +64,7 @@ describe('api:notes/create', () => { test('0 characters cw', () => { expect(v({ text: 'Body', cw: '' })) - .toBe(VALID); + .toBe(INVALID); }); test('reject only cw', () => { From 3e00b32faeb416c15ee75346ccd4c135bdfe576a Mon Sep 17 00:00:00 2001 From: anatawa12 <anatawa12@icloud.com> Date: Fri, 3 Nov 2023 13:34:57 +0900 Subject: [PATCH 16/60] build: port vite port configuration (#12223) --- packages/backend/src/server/web/ClientServerService.ts | 3 ++- scripts/dev.mjs | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index cf621f4579..7a2a52a982 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -253,8 +253,9 @@ export class ClientServerService { decorateReply: false, }); } else { + const port = (process.env.VITE_PORT ?? '5173'); fastify.register(fastifyProxy, { - upstream: 'http://localhost:5173', // TODO: port configuration + upstream: 'http://localhost:' + port, prefix: '/vite', rewritePrefix: '/vite', }); diff --git a/scripts/dev.mjs b/scripts/dev.mjs index 26f29fc491..1d06aa541f 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -11,6 +11,8 @@ import { execa } from 'execa'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); +const vitePort = process.env.VITE_PORT ? ["--strictPort", "--port", process.env.VITE_PORT] : ["--strictPort"]; + await execa('pnpm', ['clean'], { cwd: _dirname + '/../', stdout: process.stdout, @@ -41,7 +43,7 @@ execa('pnpm', ['--filter', 'backend', 'watch'], { stderr: process.stderr, }); -execa('pnpm', ['--filter', 'frontend', 'watch'], { +execa('pnpm', ['--filter', 'frontend', 'watch', ...vitePort], { cwd: _dirname + '/../', stdout: process.stdout, stderr: process.stderr, From 1729307fcfb0b50cdcca785865a4b40a87450132 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Fri, 3 Nov 2023 14:41:21 +0900 Subject: [PATCH 17/60] update deps --- packages/backend/package.json | 10 +-- packages/frontend/package.json | 2 +- packages/misskey-js/package.json | 2 +- pnpm-lock.yaml | 145 ++++++++----------------------- 4 files changed, 45 insertions(+), 114 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 0c4879a297..01bb30b2b7 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -72,9 +72,9 @@ "@fastify/multipart": "8.0.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", - "@nestjs/common": "10.2.7", - "@nestjs/core": "10.2.7", - "@nestjs/testing": "10.2.7", + "@nestjs/common": "10.2.8", + "@nestjs/core": "10.2.8", + "@nestjs/testing": "10.2.8", "@peertube/http-signature": "1.7.0", "@simplewebauthn/server": "8.3.5", "@sinonjs/fake-timers": "11.2.2", @@ -87,7 +87,7 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.2", - "bullmq": "4.12.7", + "bullmq": "4.12.8", "cacheable-lookup": "7.0.0", "cbor": "9.0.1", "chalk": "5.3.0", @@ -100,7 +100,7 @@ "deep-email-validator": "0.1.21", "fastify": "4.24.3", "feed": "4.2.2", - "file-type": "18.5.0", + "file-type": "18.6.0", "fluent-ffmpeg": "2.1.2", "form-data": "4.0.0", "got": "13.0.0", diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 97b1ac5a9a..de74922644 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -71,7 +71,7 @@ "twemoji-parser": "14.0.0", "typescript": "5.2.2", "uuid": "9.0.1", - "v-code-diff": "1.7.1", + "v-code-diff": "1.7.2", "vanilla-tilt": "1.8.1", "vite": "4.5.0", "vue": "3.3.7", diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 8c28810cba..7a165912c9 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -20,7 +20,7 @@ "url": "git+https://github.com/misskey-dev/misskey.js.git" }, "devDependencies": { - "@microsoft/api-extractor": "7.38.1", + "@microsoft/api-extractor": "7.38.2", "@swc/jest": "0.2.29", "@types/jest": "29.5.7", "@types/node": "20.8.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9bc37d5312..a731705fa2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,14 +99,14 @@ importers: specifier: 8.2.0 version: 8.2.0 '@nestjs/common': - specifier: 10.2.7 - version: 10.2.7(reflect-metadata@0.1.13)(rxjs@7.8.1) + specifier: 10.2.8 + version: 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/core': - specifier: 10.2.7 - version: 10.2.7(@nestjs/common@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) + specifier: 10.2.8 + version: 10.2.8(@nestjs/common@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/testing': - specifier: 10.2.7 - version: 10.2.7(@nestjs/common@10.2.7)(@nestjs/core@10.2.7) + specifier: 10.2.8 + version: 10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8) '@peertube/http-signature': specifier: 1.7.0 version: 1.7.0 @@ -147,8 +147,8 @@ importers: specifier: 1.20.2 version: 1.20.2 bullmq: - specifier: 4.12.7 - version: 4.12.7 + specifier: 4.12.8 + version: 4.12.8 cacheable-lookup: specifier: 7.0.0 version: 7.0.0 @@ -186,8 +186,8 @@ importers: specifier: 4.2.2 version: 4.2.2 file-type: - specifier: 18.5.0 - version: 18.5.0 + specifier: 18.6.0 + version: 18.6.0 fluent-ffmpeg: specifier: 2.1.2 version: 2.1.2 @@ -806,8 +806,8 @@ importers: specifier: 9.0.1 version: 9.0.1 v-code-diff: - specifier: 1.7.1 - version: 1.7.1(vue@3.3.7) + specifier: 1.7.2 + version: 1.7.2(vue@3.3.7) vanilla-tilt: specifier: 1.8.1 version: 1.8.1 @@ -979,7 +979,7 @@ importers: version: 7.5.2 storybook-addon-misskey-theme: specifier: github:misskey-dev/storybook-addon-misskey-theme - version: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.5.2)(@storybook/components@7.5.1)(@storybook/core-events@7.5.2)(@storybook/manager-api@7.5.2)(@storybook/preview-api@7.5.2)(@storybook/theming@7.5.2)(@storybook/types@7.5.2)(react-dom@18.2.0)(react@18.2.0) + version: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.5.2)(@storybook/components@7.5.2)(@storybook/core-events@7.5.2)(@storybook/manager-api@7.5.2)(@storybook/preview-api@7.5.2)(@storybook/theming@7.5.2)(@storybook/types@7.5.2)(react-dom@18.2.0)(react@18.2.0) summaly: specifier: github:misskey-dev/summaly version: github.com/misskey-dev/summaly/d2d8db49943ccb201c1b1b283e9d0a630519fac7 @@ -1015,8 +1015,8 @@ importers: version: 4.4.0 devDependencies: '@microsoft/api-extractor': - specifier: 7.38.1 - version: 7.38.1(@types/node@20.8.10) + specifier: 7.38.2 + version: 7.38.2(@types/node@20.8.10) '@swc/jest': specifier: 0.2.29 version: 0.2.29(@swc/core@1.3.95) @@ -4357,8 +4357,8 @@ packages: - '@types/node' dev: true - /@microsoft/api-extractor@7.38.1(@types/node@20.8.10): - resolution: {integrity: sha512-Hxu/RrVpItQ4dzeMyfwlk4lGQFsXMoMS7bYU9YUrpW16hH04PXLRiTXJz77WhBiSGNtTuufz2xh6hWyXhC9JuQ==} + /@microsoft/api-extractor@7.38.2(@types/node@20.8.10): + resolution: {integrity: sha512-JOARuhTwOcOMIU0O2czscoJy3ddVzIRhSA9/7T1ALuZSNphgWsPk+Bv4E7AnBDmTV4pP4lBNLtCxEHjjpWaytQ==} hasBin: true dependencies: '@microsoft/api-extractor-model': 7.28.2(@types/node@20.8.10) @@ -4366,7 +4366,7 @@ packages: '@microsoft/tsdoc-config': 0.16.2 '@rushstack/node-core-library': 3.61.0(@types/node@20.8.10) '@rushstack/rig-package': 0.5.1 - '@rushstack/ts-command-line': 4.17.0 + '@rushstack/ts-command-line': 4.17.1 colors: 1.2.5 lodash: 4.17.21 resolve: 1.22.8 @@ -4484,8 +4484,8 @@ packages: tar-fs: 2.1.1 dev: true - /@nestjs/common@10.2.7(reflect-metadata@0.1.13)(rxjs@7.8.1): - resolution: {integrity: sha512-cUtCRXiUstDmh4bSBhVbq4cI439Gngp4LgLGLBmd5dqFQodfXKnSD441ldYfFiLz4rbUsnoMJz/8ZjuIEI+B7A==} + /@nestjs/common@10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-rmpwcdvq2IWMmsUVP8rsdKub6uDWk7dwCYo0aif50JTwcvcxzaP3iKVFKoSgvp0RKYu8h15+/AEOfaInmPpl0Q==} peerDependencies: class-transformer: '*' class-validator: '*' @@ -4504,8 +4504,8 @@ packages: uid: 2.0.2 dev: false - /@nestjs/core@10.2.7(@nestjs/common@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1): - resolution: {integrity: sha512-5GSu53QUUcwX17sNmlJPa1I0wIeAZOKbedyVuQx0ZAwWVa9g0wJBbsNP+R4EJ+j5Dkdzt/8xkiZvnKt8RFRR8g==} + /@nestjs/core@10.2.8(@nestjs/common@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-9+MZ2s8ixfY9Bl/M9ofChiyYymcwdK9ZWNH4GDMF7Am7XRAQ1oqde6MYGG05rhQwiVXuTwaYLlXciJKfsrg5qg==} requiresBuild: true peerDependencies: '@nestjs/common': ^10.0.0 @@ -4522,7 +4522,7 @@ packages: '@nestjs/websockets': optional: true dependencies: - '@nestjs/common': 10.2.7(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -4535,8 +4535,8 @@ packages: - encoding dev: false - /@nestjs/testing@10.2.7(@nestjs/common@10.2.7)(@nestjs/core@10.2.7): - resolution: {integrity: sha512-d2SIqiJIf/7NSILeNNWSdRvTTpHSouGgisGHwf5PVDC7z4/yXZw/wPO9eJhegnxFlqk6n2LW4QBTmMzbqjAfHA==} + /@nestjs/testing@10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8): + resolution: {integrity: sha512-9Kj5IQhM67/nj/MT6Wi2OmWr5YQnCMptwKVFrX1TDaikpY12196v7frk0jVjdT7wms7rV07GZle9I2z0aSjqtQ==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 @@ -4548,8 +4548,8 @@ packages: '@nestjs/platform-express': optional: true dependencies: - '@nestjs/common': 10.2.7(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.2.7(@nestjs/common@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.8(@nestjs/common@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) tslib: 2.6.2 dev: false @@ -5362,8 +5362,8 @@ packages: strip-json-comments: 3.1.1 dev: true - /@rushstack/ts-command-line@4.17.0: - resolution: {integrity: sha512-1S0sXuEpZlzKTfvUqNs7Rg4leVkeLJc4Dn9cm+pSIn35a0Ztp5GxPN2gabD2G4RrQoQcJLLyVu+twzrJl1C0eA==} + /@rushstack/ts-command-line@4.17.1: + resolution: {integrity: sha512-2jweO1O57BYP5qdBGl6apJLB+aRIn5ccIRTPDyULh0KMwVzFqWtw6IZWt1qtUoZD/pD2RNkIOosH6Cq45rIYeg==} dependencies: '@types/argparse': 1.0.38 argparse: 1.0.10 @@ -6367,17 +6367,6 @@ packages: - supports-color dev: true - /@storybook/channels@7.5.1: - resolution: {integrity: sha512-7hTGHqvtdFTqRx8LuCznOpqPBYfUeMUt/0IIp7SFuZT585yMPxrYoaK//QmLEWnPb80B8HVTSQi7caUkJb32LA==} - dependencies: - '@storybook/client-logger': 7.5.1 - '@storybook/core-events': 7.5.1 - '@storybook/global': 5.0.0 - qs: 6.11.1 - telejson: 7.2.0 - tiny-invariant: 1.3.1 - dev: true - /@storybook/channels@7.5.2: resolution: {integrity: sha512-3SgqWq9NS0XX1QxK3riuaOLrReHWwVhI63u6q1ryDD3SttpmAezZETibOAtzDuk2FKgsyHTmAlmcGQf4ZxhOJA==} dependencies: @@ -6441,12 +6430,6 @@ packages: - utf-8-validate dev: true - /@storybook/client-logger@7.5.1: - resolution: {integrity: sha512-XxbLvg0aQRoBrzxYLcVYCbjDkGbkU8Rfb74XbV2CLiO2bIbFPmA1l1Nwbp+wkCGA+O6Z1zwzSl6wcKKqZ6XZCg==} - dependencies: - '@storybook/global': 5.0.0 - dev: true - /@storybook/client-logger@7.5.2: resolution: {integrity: sha512-7YgLItlmiYDzWYexTaRNuHhtFarh9krsI+8l7Yjn9ryoHSTJUcTWx+yPJm1II+PQR8v/x5UgsxzultjgEurfRQ==} dependencies: @@ -6474,29 +6457,6 @@ packages: - supports-color dev: true - /@storybook/components@7.5.1(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-fdzzxGBV/Fj9pYwfYL3RZsVUHeBqlfLMBP/L6mPmjaZSwHFqkaRZZUajZc57lCtI+TOy2gY6WH3cPavEtqtgLw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - '@radix-ui/react-select': 1.2.2(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-toolbar': 1.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/client-logger': 7.5.1 - '@storybook/csf': 0.1.0 - '@storybook/global': 5.0.0 - '@storybook/theming': 7.5.1(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.5.1 - memoizerific: 1.11.3 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - use-resize-observer: 9.1.0(react-dom@18.2.0)(react@18.2.0) - util-deprecate: 1.0.2 - transitivePeerDependencies: - - '@types/react' - - '@types/react-dom' - dev: true - /@storybook/components@7.5.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-OP+o6AoxoQDbqjk/jdQ1arlc1T8601eCL+rS1dJY9EtAFq7Z0LEFtafhEW/Lx8FotfVGjfCNptH9ODhHU6e5Jw==} peerDependencies: @@ -6558,12 +6518,6 @@ packages: - supports-color dev: true - /@storybook/core-events@7.5.1: - resolution: {integrity: sha512-2eyaUhTfmEEqOEZVoCXVITCBn6N7QuZCG2UNxv0l//ED+7MuMiFhVw7kS7H3WOVk65R7gb8qbKFTNX8HFTgBHg==} - dependencies: - ts-dedent: 2.2.0 - dev: true - /@storybook/core-events@7.5.2: resolution: {integrity: sha512-DV8bFEFVKDEvaH87KYPXDE0YEV+Y9yjFv2xxmC9pF8l+MWCtVW72RBLhB+gU5NM1bkHrRDNb0lOJfVGKlhxOog==} dependencies: @@ -6896,20 +6850,6 @@ packages: ts-dedent: 2.2.0 dev: true - /@storybook/theming@7.5.1(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-ETLAOn10hI4Mkmjsr0HGcM6HbzaURrrPBYmfXOrdbrzEVN+AHW4FlvP9d8fYyP1gdjPE1F39XvF0jYgt1zXiHQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - '@emotion/use-insertion-effect-with-fallbacks': 1.0.0(react@18.2.0) - '@storybook/client-logger': 7.5.1 - '@storybook/global': 5.0.0 - memoizerific: 1.11.3 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: true - /@storybook/theming@7.5.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-DZBTcYErSYvmTYsGz7lKtiIcBe8flBw5Ojp52r3O4GcRYG4AbuUwwVvehz+O1cWaS+UW3HavrcgapERH7ZHd1A==} peerDependencies: @@ -6924,15 +6864,6 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/types@7.5.1: - resolution: {integrity: sha512-ZcMSaqFNx1E+G00nRDUi8kKL7gxJVlnCvbKLNj3V85guy4DkIYAZr31yDqze07gDWbjvKoHIp3tKpgE+2i8upQ==} - dependencies: - '@storybook/channels': 7.5.1 - '@types/babel__core': 7.20.0 - '@types/express': 4.17.17 - file-system-cache: 2.3.0 - dev: true - /@storybook/types@7.5.2: resolution: {integrity: sha512-RDKHo6WUES+4nt7uZMfankjxdpYX2EI2GpJ2n2RPcnhzmb/ub1huNTjbzDEYMqY24SppljZeIN57m3Ar6L6f9A==} dependencies: @@ -9463,8 +9394,8 @@ packages: dependencies: node-gyp-build: 4.6.0 - /bullmq@4.12.7: - resolution: {integrity: sha512-wigDuI8dyzY1jaUZLrwMp0L7t2glp0eErnRCYlVwi56DUWYSrzrOB3Vz8SaAmpc3Ro5dS4mBwt7RDJG3jiuJKA==} + /bullmq@4.12.8: + resolution: {integrity: sha512-aG9o2/y6P+SvsIlIfjTP4Cn2wOsD6r7IplWBovi1wCmTMDBhtKsPVCC2ZKezaagtTCGtV6IN5Bx5g6WrtMUz0Q==} dependencies: cron-parser: 4.8.1 glob: 8.1.0 @@ -11763,8 +11694,8 @@ packages: token-types: 5.0.1 dev: false - /file-type@18.5.0: - resolution: {integrity: sha512-yvpl5U868+V6PqXHMmsESpg6unQ5GfnPssl4dxdJudBrr9qy7Fddt7EVX1VLlddFfe8Gj9N7goCZH22FXuSQXQ==} + /file-type@18.6.0: + resolution: {integrity: sha512-uLqXnIAIyy8K9rnvdU9IYi3WIL+6qVBWn24kThYOPlnyU+6yrr2oarn+j7seMLh1wOEG4hEjRP6a30IiKR9OaA==} engines: {node: '>=14.16'} dependencies: readable-web-to-node-stream: 3.0.2 @@ -15154,7 +15085,7 @@ packages: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.1 + resolve: 1.22.8 semver: 5.7.1 validate-npm-package-license: 3.0.4 dev: true @@ -19117,8 +19048,8 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true - /v-code-diff@1.7.1(vue@3.3.7): - resolution: {integrity: sha512-2O34z6DcVw3LygR9Xl07A28115nsps56dCH6zxFMLoW1jyEnWFPN7Kwh0GAYAeWzDiltbqsMWgvfqJYjBEZPgw==} + /v-code-diff@1.7.2(vue@3.3.7): + resolution: {integrity: sha512-y+q8ZHf8GfphYLhcZbjAKcId/h6vZujS71Ryq5u+dI6Jg4ZLTdLrBNVSzYpHywHSSFFfBMdilm6XvVryEaH4+A==} requiresBuild: true peerDependencies: '@vue/composition-api': ^1.4.9 @@ -19876,7 +19807,7 @@ packages: sharp: 0.31.3 dev: false - github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.5.2)(@storybook/components@7.5.1)(@storybook/core-events@7.5.2)(@storybook/manager-api@7.5.2)(@storybook/preview-api@7.5.2)(@storybook/theming@7.5.2)(@storybook/types@7.5.2)(react-dom@18.2.0)(react@18.2.0): + github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.5.2)(@storybook/components@7.5.2)(@storybook/core-events@7.5.2)(@storybook/manager-api@7.5.2)(@storybook/preview-api@7.5.2)(@storybook/theming@7.5.2)(@storybook/types@7.5.2)(react-dom@18.2.0)(react@18.2.0): resolution: {tarball: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640} id: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640 name: storybook-addon-misskey-theme @@ -19898,7 +19829,7 @@ packages: optional: true dependencies: '@storybook/blocks': 7.5.2(react-dom@18.2.0)(react@18.2.0) - '@storybook/components': 7.5.1(react-dom@18.2.0)(react@18.2.0) + '@storybook/components': 7.5.2(react-dom@18.2.0)(react@18.2.0) '@storybook/core-events': 7.5.2 '@storybook/manager-api': 7.5.2(react-dom@18.2.0)(react@18.2.0) '@storybook/preview-api': 7.5.2 From 025ae436b5fcb0a019962807f0f79068f135038f Mon Sep 17 00:00:00 2001 From: yukineko <27853966+hideki0403@users.noreply.github.com> Date: Fri, 3 Nov 2023 14:54:28 +0900 Subject: [PATCH 18/60] =?UTF-8?q?enhance:=20=E3=82=A2=E3=82=AB=E3=82=A6?= =?UTF-8?q?=E3=83=B3=E3=83=88=E7=99=BB=E9=8C=B2=E6=99=82=E3=81=AE=E3=83=A1?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E3=82=A2=E3=83=89=E3=83=AC=E3=82=B9=E8=AA=8D?= =?UTF-8?q?=E8=A8=BC=E3=81=AB30=E5=88=86=E3=81=AE=E6=9C=89=E5=8A=B9?= =?UTF-8?q?=E6=9C=9F=E9=99=90=E3=82=92=E8=A8=AD=E5=AE=9A=20(#12221)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add: metaにemailVerificationExpiresInを追加 * enhance: 招待コード使用時, メアド認証時に認証期限を確認するように * add: クライアント側に実装 * update: CHANGELOG.md * add: コメントを追加 * Revert "add: metaにemailVerificationExpiresInを追加" This reverts commit ceb6ccff51a406bfd87b4da6c59401ce5551dd95. * Revert "add: コメントを追加" This reverts commit 7ee301c3eed4ded295490a6614650a3720317772. * change(client): メール認証の有効期限を30分で固定するように変更 * change(backend): メール認証の有効期限を30分で固定するように変更 * update: CHANGELOG.md --- CHANGELOG.md | 3 +++ locales/index.d.ts | 1 + locales/ja-JP.yml | 3 ++- .../src/server/api/SignupApiService.ts | 19 ++++++++++++++++++- .../frontend/src/pages/signup-complete.vue | 3 ++- 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbf99bee95..6837cf6e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,9 @@ - Enhance: 未読の通知数を表示できるように - Enhance: ローカリゼーションの更新 - Enhance: 依存関係の更新 +- Enhance: アカウント登録時のメールアドレス認証に30分の有効期限を設定 + - 有効期限が切れた後であれば、登録時に使用した招待コードを再度利用できるように変更しました。 + - ユーザーが誤ったメールアドレスを入力した場合に招待コードが失効してしまう問題が解消されます。 - Change: CWを使用する場合、注釈を空にすることは許可されなくなりました ### Client diff --git a/locales/index.d.ts b/locales/index.d.ts index b8dc3a68bc..f6db40e944 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1158,6 +1158,7 @@ export interface Locale { "pullDownToRefresh": string; "disableStreamingTimeline": string; "useGroupedNotifications": string; + "signupPendingError": string; "cwNotationRequired": string; "_announcement": { "forExistingUsers": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 76b5386b39..1b79c399e7 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1155,6 +1155,7 @@ refreshing: "リロード中" pullDownToRefresh: "引っ張ってリロード" disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする" useGroupedNotifications: "通知をグルーピングして表示する" +signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。" cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。" _announcement: @@ -1554,7 +1555,7 @@ _ffVisibility: _signup: almostThere: "ほとんど完了です" emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。" - emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。" + emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。メールに記載されているリンクの有効期限は30分です。" _accountDelete: accountDelete: "アカウントの削除" diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index d2c4440116..d6f4df7f13 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -136,7 +136,20 @@ export class SignupApiService { return; } - if (ticket.usedAt) { + // メアド認証が有効の場合 + if (instance.emailRequiredForSignup) { + // メアド認証済みならエラー + if (ticket.usedBy) { + reply.code(400); + return; + } + + // 認証しておらず、メール送信から30分以内ならエラー + if (ticket.usedAt && ticket.usedAt.getTime() + (1000 * 60 * 30) > Date.now()) { + reply.code(400); + return; + } + } else if (ticket.usedAt) { reply.code(400); return; } @@ -224,6 +237,10 @@ export class SignupApiService { try { const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code }); + if (this.idService.parse(pendingUser.id).date.getTime() + (1000 * 60 * 30) < Date.now()) { + throw new FastifyReplyError(400, 'EXPIRED'); + } + const { account, secret } = await this.signupService.signup({ username: pendingUser.username, passwordHash: pendingUser.password, diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue index e9c89fa3bb..d9a730851d 100644 --- a/packages/frontend/src/pages/signup-complete.vue +++ b/packages/frontend/src/pages/signup-complete.vue @@ -51,7 +51,8 @@ function submit() { os.alert({ type: 'error', - text: i18n.ts.somethingHappened, + title: i18n.ts.somethingHappened, + text: i18n.ts.signupPendingError, }); }); } From 24e629ca5c50789ff0aba31532ae66b51148d70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:35:07 +0900 Subject: [PATCH 19/60] =?UTF-8?q?enhance:=20=E5=88=9D=E6=9C=9F=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E3=81=A8=E3=83=81=E3=83=A5=E3=83=BC=E3=83=88=E3=83=AA?= =?UTF-8?q?=E3=82=A2=E3=83=AB=E3=82=92=E7=B5=B1=E5=90=88=20(#12141)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * better onboarding experience * enhance: iroiro * (add) title * (enhance) 戻る・次へボタンを全ページでstickyに * fix merging * (add) iroiro * remove unnecessary file * Update CHANGELOG.md * tweak texts * (fix) reactionViewer mock * change strings * Update MkTutorialDialog.Note.vue * Update ja-JP.yml * (fix) reactionViewer error * (fix) path * refactor * fix * Update MkPostForm.vue * Update ja-JP.yml * Update ja-JP.yml * tweak text * Update ja-JP.yml * Update ja-JP.yml * Update ja-JP.yml * (add) achivement * (add) もう一度見れますよメッセージを追加 * Revert "feat: レジストリAPIをサードパーティから利用可能に (#12229)" This reverts commit 79346272f8792d35955efd3aaaa1e42e0cd2a6e3. * Revert "(add) もう一度見れますよメッセージを追加" This reverts commit 6123b35215133f0d5e5db356bb43f4acbafab8fa. * Revert "Revert "feat: レジストリAPIをサードパーティから利用可能に (#12229)"" This reverts commit bae684e484ef99308d7ac816a822047117efe1c6. * tweak --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> --- CHANGELOG.md | 1 + locales/index.d.ts | 98 ++++++- locales/ja-JP.yml | 88 +++++- .../backend/src/core/AchievementService.ts | 1 + packages/frontend/assets/tutorial/ai.webp | Bin 0 -> 12238 bytes .../assets/tutorial/natto_failed.webp | Bin 0 -> 13196 bytes .../frontend/assets/tutorial/timeline_tab.png | Bin 0 -> 2860 bytes packages/frontend/src/components/MkInfo.vue | 20 +- packages/frontend/src/components/MkNote.vue | 150 +++++++--- .../frontend/src/components/MkNoteHeader.vue | 14 +- .../frontend/src/components/MkPostForm.vue | 28 +- .../src/components/MkPostFormAttaches.vue | 17 +- .../components/MkReactionsViewer.reaction.vue | 52 ++-- .../src/components/MkReactionsViewer.vue | 19 +- .../src/components/MkTutorialDialog.Note.vue | 117 ++++++++ .../components/MkTutorialDialog.PostNote.vue | 135 +++++++++ .../components/MkTutorialDialog.Sensitive.vue | 144 ++++++++++ .../components/MkTutorialDialog.Timeline.vue | 87 ++++++ .../src/components/MkTutorialDialog.vue | 260 ++++++++++++++++++ .../src/components/MkUserSetupDialog.vue | 77 ++++-- .../frontend/src/pages/timeline.tutorial.vue | 123 --------- packages/frontend/src/pages/timeline.vue | 16 +- packages/frontend/src/scripts/achievements.ts | 6 + packages/frontend/src/store.ts | 9 +- packages/frontend/src/ui/_common_/common.ts | 9 +- 25 files changed, 1223 insertions(+), 248 deletions(-) create mode 100644 packages/frontend/assets/tutorial/ai.webp create mode 100644 packages/frontend/assets/tutorial/natto_failed.webp create mode 100644 packages/frontend/assets/tutorial/timeline_tab.png create mode 100644 packages/frontend/src/components/MkTutorialDialog.Note.vue create mode 100644 packages/frontend/src/components/MkTutorialDialog.PostNote.vue create mode 100644 packages/frontend/src/components/MkTutorialDialog.Sensitive.vue create mode 100644 packages/frontend/src/components/MkTutorialDialog.Timeline.vue create mode 100644 packages/frontend/src/components/MkTutorialDialog.vue delete mode 100644 packages/frontend/src/pages/timeline.tutorial.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index 6837cf6e1b..4fdf9687ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください https://misskey-hub.net/docs/advanced/publish-on-your-website.html - Feat: 通知をグルーピングして表示するオプション(オプトアウト) +- Feat: Misskeyの基本的なチュートリアルを実装 - Feat: スワイプしてタイムラインを再読込できるように - PCの場合は右上のボタンからでも再読込できます - Enhance: タイムラインの自動更新を無効にできるように diff --git a/locales/index.d.ts b/locales/index.d.ts index f6db40e944..b45559eea2 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1182,10 +1182,91 @@ export interface Locale { "pushNotificationDescription": string; "initialAccountSettingCompleted": string; "haveFun": string; - "ifYouNeedLearnMore": string; + "youCanContinueTutorial": string; + "startTutorial": string; "skipAreYouSure": string; "laterAreYouSure": string; }; + "_initialTutorial": { + "launchTutorial": string; + "title": string; + "wellDone": string; + "skipAreYouSure": string; + "_landing": { + "title": string; + "description": string; + }; + "_note": { + "title": string; + "description": string; + "reply": string; + "renote": string; + "reaction": string; + "menu": string; + }; + "_reaction": { + "title": string; + "description": string; + "letsTryReacting": string; + "reactToContinue": string; + "reactNotification": string; + "reactDone": string; + }; + "_timeline": { + "title": string; + "description1": string; + "home": string; + "local": string; + "social": string; + "global": string; + "description2": string; + "description3": string; + }; + "_postNote": { + "title": string; + "description1": string; + "_visibility": { + "description": string; + "public": string; + "home": string; + "followers": string; + "direct": string; + "doNotSendConfidencialOnDirect1": string; + "doNotSendConfidencialOnDirect2": string; + "localOnly": string; + }; + "_cw": { + "title": string; + "description": string; + "_exampleNote": { + "cw": string; + "note": string; + }; + "useCases": string; + }; + }; + "_howToMakeAttachmentsSensitive": { + "title": string; + "description": string; + "tryThisFile": string; + "_exampleNote": { + "note": string; + }; + "method": string; + "sensitiveSucceeded": string; + "doItToContinue": string; + }; + "_done": { + "title": string; + "description": string; + }; + }; + "_timelineDescription": { + "home": string; + "local": string; + "social": string; + "global": string; + }; "_serverRules": { "description": string; }; @@ -1533,6 +1614,10 @@ export interface Locale { "title": string; "description": string; }; + "_tutorialCompleted": { + "title": string; + "description": string; + }; }; }; "_role": { @@ -1861,17 +1946,6 @@ export interface Locale { "hour": string; "day": string; }; - "_timelineTutorial": { - "title": string; - "step1_1": string; - "step1_2": string; - "step2_1": string; - "step2_2": string; - "step3_1": string; - "step3_2": string; - "step4_1": string; - "step4_2": string; - }; "_2fa": { "alreadyRegistered": string; "registerTOTP": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 1b79c399e7..8fd77afd92 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1170,7 +1170,7 @@ _announcement: _initialAccountSetting: accountCreated: "アカウントの作成が完了しました!" - letsStartAccountSetup: "アカウントの初期設定を行いましょう。" + letsStartAccountSetup: "さっそくアカウントの初期設定を行いましょう。" letsFillYourProfile: "まずはあなたのプロフィールを設定しましょう。" profileSetting: "プロフィール設定" privacySetting: "プライバシー設定" @@ -1180,10 +1180,80 @@ _initialAccountSetting: pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をお使いのデバイスで受け取ることができます。" initialAccountSettingCompleted: "初期設定が完了しました!" haveFun: "{name}をお楽しみください!" - ifYouNeedLearnMore: "{name}(Misskey)の使い方などを詳しく知るには{link}をご覧ください。" + youCanContinueTutorial: "このまま{name}(Misskey)の使い方についてのチュートリアルに進むこともできますが、ここで中断してすぐに使い始めることもできます。" + startTutorial: "チュートリアルを開始" skipAreYouSure: "初期設定をスキップしますか?" laterAreYouSure: "初期設定をあとでやり直しますか?" +_initialTutorial: + launchTutorial: "チュートリアルを見る" + title: "チュートリアル" + wellDone: "よくできました" + skipAreYouSure: "チュートリアルを終了しますか?" + _landing: + title: "チュートリアルへようこそ" + description: "ここでは、Misskeyの基本的な使い方や機能を確認できます。" + _note: + title: "ノートって何?" + description: "Misskeyでの投稿は「ノート」と呼びます。ノートはタイムラインに時系列で並んでいて、リアルタイムで更新されていきます。" + reply: "返信することができます。返信に対しての返信も可能で、スレッドのように会話を続けることもできます。" + renote: "そのノートを自分のタイムラインに流して共有することができます。テキストを追加して引用することも可能です。" + reaction: "リアクションをつけることができます。詳しくは次のページで解説します。" + menu: "ノートの詳細を表示したり、リンクをコピーしたりなどの様々な操作が行えます。" + _reaction: + title: "リアクションって何?" + description: "ノートには「リアクション」をつけることができます。「いいね」では伝わらないニュアンスも、リアクションで簡単・気軽に表現できます。" + letsTryReacting: "リアクションは、ノートの「+」ボタンをクリックするとつけられます。試しにこのサンプルのノートにリアクションをつけてみてください!" + reactToContinue: "リアクションをつけると先に進めるようになります。" + reactNotification: "あなたのノートが誰かにリアクションされると、リアルタイムで通知を受け取ります。" + reactDone: "「ー」ボタンを押すとリアクションを取り消すことができます。" + _timeline: + title: "タイムラインのしくみ" + description1: "Misskeyには、使い方に応じて複数のタイムラインが用意されています(サーバーによってはいずれかが無効になっていることがあります)。" + home: "あなたがフォローしているアカウントの投稿を見られます。" + local: "このサーバーにいるユーザー全員の投稿を見られます。" + social: "ホームタイムラインとローカルタイムラインの投稿が両方表示されます。" + global: "接続している他のすべてのサーバーからの投稿を見られます。" + description2: "それぞれのタイムラインは、画面上部でいつでも切り替えられます。" + description3: "その他にも、リストタイムラインやチャンネルタイムラインなどがあります。詳しくは{link}をご覧ください。" + _postNote: + title: "ノートの投稿設定" + description1: "Misskeyにノートを投稿する際には、様々なオプションの設定が可能です。投稿フォームはこのようになっています。" + _visibility: + description: "ノートを表示できる相手を制限できます。" + public: "すべてのユーザーに公開。" + home: "ホームタイムラインのみに公開。フォロワー・プロフィールを見に来た人・リノートから、他のユーザーも見ることができます。" + followers: "フォロワーにのみ公開。本人以外がリノートすることはできず、またフォロワー以外は閲覧できません。" + direct: "指定したユーザーにのみ公開され、また相手に通知が入ります。ダイレクトメッセージのかわりにお使いいただけます。" + doNotSendConfidencialOnDirect1: "機密情報は送信する際は注意してください。" + doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーにダイレクト投稿を送信する場合は、機密情報の扱いに注意が必要です。" + localOnly: "他のサーバーに投稿を連合しません。上記の公開範囲に関わらず、他のサーバーのユーザーは、この設定がついたノートを直接閲覧することができなくなります。" + _cw: + title: "内容を隠す(CW)" + description: "本文のかわりに「注釈」に書いた内容が表示されます。「もっと見る」を押すと本文が表示されます。" + _exampleNote: + cw: "飯テロ注意" + note: "チョコのかかったドーナツを食べました🍩😋" + useCases: "サーバーのガイドラインにより必要とされるノートに指定したり、ネタバレ投稿やセンシティブな文章を自主規制したりするときに使います。" + _howToMakeAttachmentsSensitive: + title: "添付ファイルをセンシティブにするには?" + description: "サーバーのガイドラインにより必要とされる際や、そのまま見れる状態にしておくべきではない添付ファイルには、「センシティブ」設定を付けます。" + tryThisFile: "試しに、このフォームに添付された画像をセンシティブにしてみてください!" + _exampleNote: + note: "納豆のフタ開けるのミスったわね…" + method: "添付ファイルをセンシティブにする際は、そのファイルをクリックしてメニューを開き、「センシティブとして設定」をクリックします。" + sensitiveSucceeded: "ファイルを添付する際は、サーバーのガイドラインに従ってセンシティブを適切に設定してください。" + doItToContinue: "画像をセンシティブに設定すると先に進めるようになります。" + _done: + title: "チュートリアルは終了です🎉" + description: "ここで紹介した機能はほんの一部にすぎません。Misskeyの使い方をより詳しく知るには、{link}をご覧ください。" + +_timelineDescription: + home: "ホームタイムラインでは、あなたがフォローしているアカウントの投稿を見られます。" + local: "ローカルタイムラインでは、このサーバーにいるユーザー全員の投稿を見られます。" + social: "ソーシャルタイムラインには、ホームタイムラインとローカルタイムラインの投稿が両方表示されます。" + global: "グローバルタイムラインでは、接続している他のすべてのサーバーからの投稿を見られます。" + _serverRules: description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。" @@ -1456,6 +1526,9 @@ _achievements: _smashTestNotificationButton: title: "テスト過剰" description: "通知のテストをごく短時間のうちに連続して行った" + _tutorialCompleted: + title: "Misskey初心者講座 修了証" + description: "チュートリアルを完了した" _role: new: "ロールの作成" @@ -1778,17 +1851,6 @@ _time: hour: "時間" day: "日" -_timelineTutorial: - title: "Misskeyの使い方" - step1_1: "この画面は「タイムライン」です。{name}に投稿された「ノート」が時系列で表示されます。" - step1_2: "タイムラインにはいくつか種類があり、例えば「ホームタイムライン」にはあなたがフォローしている人のノートが流れ、「ローカルタイムライン」には{name}全体のノートが流れます。" - step2_1: "試しに、何かノートを投稿してみましょう。画面上にある鉛筆マークのボタンを押すとフォームが開きます。" - step2_2: "初めてのノートの内容は、あなたの自己紹介や「{name}始めました」などがおすすめです。" - step3_1: "投稿できましたか?" - step3_2: "あなたのノートがタイムラインに表示されていれば成功です。" - step4_1: "ノートには、「リアクション」を付けることができます。" - step4_2: "リアクションを付けるには、ノートの「+」マークをクリックして、好きな絵文字を選択します。" - _2fa: alreadyRegistered: "既に設定は完了しています。" registerTOTP: "認証アプリの設定を開始" diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts index 1b8718335b..88fc033859 100644 --- a/packages/backend/src/core/AchievementService.ts +++ b/packages/backend/src/core/AchievementService.ts @@ -86,6 +86,7 @@ export const ACHIEVEMENT_TYPES = [ 'cookieClicked', 'brainDiver', 'smashTestNotificationButton', + 'tutorialCompleted', ] as const; @Injectable() diff --git a/packages/frontend/assets/tutorial/ai.webp b/packages/frontend/assets/tutorial/ai.webp new file mode 100644 index 0000000000000000000000000000000000000000..d9d456494272913452dba78c1b13192db0e7d49d GIT binary patch literal 12238 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 literal 0 HcmV?d00001 diff --git a/packages/frontend/assets/tutorial/natto_failed.webp b/packages/frontend/assets/tutorial/natto_failed.webp new file mode 100644 index 0000000000000000000000000000000000000000..87db5f7732de56cf1f9074e38de6dd80c6cdfcba GIT binary patch literal 13196 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 literal 0 HcmV?d00001 diff --git a/packages/frontend/assets/tutorial/timeline_tab.png b/packages/frontend/assets/tutorial/timeline_tab.png new file mode 100644 index 0000000000000000000000000000000000000000..b52ad5fb519119efa418f47204c1131db0a9d1c5 GIT binary patch literal 2860 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 literal 0 HcmV?d00001 diff --git a/packages/frontend/src/components/MkInfo.vue b/packages/frontend/src/components/MkInfo.vue index 37490887e1..19402a44ce 100644 --- a/packages/frontend/src/components/MkInfo.vue +++ b/packages/frontend/src/components/MkInfo.vue @@ -7,7 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="[$style.root, { [$style.warn]: warn }]"> <i v-if="warn" class="ti ti-alert-triangle" :class="$style.i"></i> <i v-else class="ti ti-info-circle" :class="$style.i"></i> - <slot></slot> + <div><slot></slot></div> + <button v-if="closable" :class="$style.button" class="_button" @click="close()"><i class="ti ti-x"></i></button> </div> </template> @@ -16,11 +17,23 @@ import { } from 'vue'; const props = defineProps<{ warn?: boolean; + closable?: boolean; }>(); + +const emit = defineEmits<{ + (ev: 'close'): void; +}>(); + +function close() { + // こいつの中では非表示動作は行わない + emit('close'); +} </script> <style lang="scss" module> .root { + display: flex; + align-items: center; padding: 12px 14px; font-size: 90%; background: var(--infoBg); @@ -37,4 +50,9 @@ const props = defineProps<{ .i { margin-right: 4px; } + +.button { + margin-left: auto; + padding: 4px; +} </style> diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index b31ee78532..d71b07c51b 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <article v-else :class="$style.article" @contextmenu.stop="onContextmenu"> <div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div> - <MkAvatar :class="$style.avatar" :user="appearNote.user" link preview/> + <MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/> <div :class="$style.main"> <MkNoteHeader :note="appearNote" :mini="true"/> <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> @@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> </div> - <MkReactionsViewer :note="appearNote" :maxNumber="16"> + <MkReactionsViewer :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction"> <template #more> <div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div> </template> @@ -136,7 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent } from 'vue'; +import { computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent, watch, provide } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import MkNoteSub from '@/components/MkNoteSub.vue'; @@ -170,9 +170,19 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { shouldCollapsed } from '@/scripts/collapsed.js'; -const props = defineProps<{ +const props = withDefaults(defineProps<{ note: Misskey.entities.Note; pinned?: boolean; + mock?: boolean; +}>(), { + mock: false, +}); + +provide('mock', props.mock); + +const emit = defineEmits<{ + (ev: 'reaction', emoji: string): void; + (ev: 'removeReaction', emoji: string): void; }>(); const inChannel = inject('inChannel', null); @@ -232,30 +242,38 @@ const keymap = { 's': () => showContent.value !== showContent.value, }; -useNoteCapture({ - rootEl: el, - note: $$(appearNote), - pureNote: $$(note), - isDeletedRef: isDeleted, -}); - -useTooltip(renoteButton, async (showing) => { - const renotes = await os.api('notes/renotes', { - noteId: appearNote.id, - limit: 11, +if (props.mock) { + watch(() => props.note, (to) => { + note = deepClone(to); + }, { deep: true }); +} else { + useNoteCapture({ + rootEl: el, + note: $$(appearNote), + pureNote: $$(note), + isDeletedRef: isDeleted, }); +} - const users = renotes.map(x => x.user); +if (!props.mock) { + useTooltip(renoteButton, async (showing) => { + const renotes = await os.api('notes/renotes', { + noteId: appearNote.id, + limit: 11, + }); - if (users.length < 1) return; + const users = renotes.map(x => x.user); - os.popup(MkUsersTooltip, { - showing, - users, - count: appearNote.renoteCount, - targetElement: renoteButton.value, - }, {}, 'closed'); -}); + if (users.length < 1) return; + + os.popup(MkUsersTooltip, { + showing, + users, + count: appearNote.renoteCount, + targetElement: renoteButton.value, + }, {}, 'closed'); + }); +} type Visibility = 'public' | 'home' | 'followers' | 'specified'; @@ -287,21 +305,25 @@ function renote(viaKeyboard = false) { os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - os.api('notes/create', { - renoteId: appearNote.id, - channelId: appearNote.channelId, - }).then(() => { - os.toast(i18n.ts.renoted); - }); + if (!props.mock) { + os.api('notes/create', { + renoteId: appearNote.id, + channelId: appearNote.channelId, + }).then(() => { + os.toast(i18n.ts.renoted); + }); + } }, }, { text: i18n.ts.inChannelQuote, icon: 'ti ti-quote', action: () => { - os.post({ - renote: appearNote, - channel: appearNote.channel, - }); + if (!props.mock) { + os.post({ + renote: appearNote, + channel: appearNote.channel, + }); + } }, }, null]); } @@ -327,15 +349,17 @@ function renote(viaKeyboard = false) { visibility = smallerVisibility(visibility, 'home'); } - os.api('notes/create', { - localOnly, - visibility, - renoteId: appearNote.id, - }).then(() => { - os.toast(i18n.ts.renoted); - }); + if (!props.mock) { + os.api('notes/create', { + localOnly, + visibility, + renoteId: appearNote.id, + }).then(() => { + os.toast(i18n.ts.renoted); + }); + } }, - }, { + }, (props.mock) ? undefined : { text: i18n.ts.quote, icon: 'ti ti-quote', action: () => { @@ -352,6 +376,9 @@ function renote(viaKeyboard = false) { function reply(viaKeyboard = false): void { pleaseLogin(); + if (props.mock) { + return; + } os.post({ reply: appearNote, channel: appearNote.channel, @@ -365,6 +392,10 @@ function react(viaKeyboard = false): void { pleaseLogin(); showMovedDialog(); if (appearNote.reactionAcceptance === 'likeOnly') { + if (props.mock) { + return; + } + os.api('notes/reactions/create', { noteId: appearNote.id, reaction: '❤️', @@ -379,6 +410,11 @@ function react(viaKeyboard = false): void { } else { blur(); reactionPicker.show(reactButton.value, reaction => { + if (props.mock) { + emit('reaction', reaction); + return; + } + os.api('notes/reactions/create', { noteId: appearNote.id, reaction: reaction, @@ -395,12 +431,22 @@ function react(viaKeyboard = false): void { function undoReact(note): void { const oldReaction = note.myReaction; if (!oldReaction) return; + + if (props.mock) { + emit('removeReaction', oldReaction); + return; + } + os.api('notes/reactions/delete', { noteId: note.id, }); } function onContextmenu(ev: MouseEvent): void { + if (props.mock) { + return; + } + const isLink = (el: HTMLElement) => { if (el.tagName === 'A') return true; // 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。 @@ -422,6 +468,10 @@ function onContextmenu(ev: MouseEvent): void { } function menu(viaKeyboard = false): void { + if (props.mock) { + return; + } + const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }); os.popupMenu(menu, menuButton.value, { viaKeyboard, @@ -429,10 +479,18 @@ function menu(viaKeyboard = false): void { } async function clip() { + if (props.mock) { + return; + } + os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); } function showRenoteMenu(viaKeyboard = false): void { + if (props.mock) { + return; + } + function getUnrenote(): MenuItem { return { text: i18n.ts.unrenote, @@ -490,6 +548,14 @@ function readPromo() { }); isDeleted.value = true; } + +function emitUpdReaction(emoji: string, delta: number) { + if (delta < 0) { + emit('removeReaction', emoji); + } else if (delta > 0) { + emit('reaction', emoji); + } +} </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index 52d5b03685..b2236b99c2 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -5,7 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <header :class="$style.root"> - <MkA v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> + <div v-if="mock" :class="$style.name"> + <MkUserName :user="note.user"/> + </div> + <MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> <MkUserName :user="note.user"/> </MkA> <div v-if="note.user.isBot" :class="$style.isBot">bot</div> @@ -14,7 +17,10 @@ SPDX-License-Identifier: AGPL-3.0-only <img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/> </div> <div :class="$style.info"> - <MkA :to="notePage(note)"> + <div v-if="mock"> + <MkTime :time="note.createdAt" colored/> + </div> + <MkA v-else :to="notePage(note)"> <MkTime :time="note.createdAt" colored/> </MkA> <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> @@ -29,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { inject } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import { notePage } from '@/filters/note.js'; @@ -38,6 +44,8 @@ import { userPage } from '@/filters/user.js'; defineProps<{ note: Misskey.entities.Note; }>(); + +const mock = inject<boolean>('mock', false); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 1fa5685861..46faae9523 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -98,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { inject, watch, nextTick, onMounted, defineAsyncComponent } from 'vue'; +import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import insertTextAtCursor from 'insert-text-at-cursor'; @@ -143,15 +143,22 @@ const props = withDefaults(defineProps<{ fixed?: boolean; autofocus?: boolean; freezeAfterPosted?: boolean; + mock?: boolean; }>(), { initialVisibleUsers: () => [], autofocus: true, + mock: false, }); +provide('mock', props.mock); + const emit = defineEmits<{ (ev: 'posted'): void; (ev: 'cancel'): void; (ev: 'esc'): void; + + // Mock用 + (ev: 'fileChangeSensitive', fileId: string, to: boolean): void; }>(); const textareaEl = $shallowRef<HTMLTextAreaElement | null>(null); @@ -239,7 +246,7 @@ const maxTextLength = $computed((): number => { }); const canPost = $computed((): boolean => { - return !posting && !posted && + return !props.mock && !posting && !posted && (1 <= textLength || 1 <= files.length || !!poll || !!props.renote) && (textLength <= maxTextLength) && (!poll || poll.choices.length >= 2); @@ -396,6 +403,8 @@ function focus() { } function chooseFileFrom(ev) { + if (props.mock) return; + selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => { for (const file of files_) { files.push(file); @@ -408,6 +417,9 @@ function detachFile(id) { } function updateFileSensitive(file, sensitive) { + if (props.mock) { + emit('fileChangeSensitive', file.id, sensitive); + } files[files.findIndex(x => x.id === file.id)].isSensitive = sensitive; } @@ -420,6 +432,8 @@ function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities } function upload(file: File, name?: string): void { + if (props.mock) return; + uploadFile(file, defaultStore.state.uploadFolder, name).then(res => { files.push(res); }); @@ -545,6 +559,8 @@ function onCompositionEnd(ev: CompositionEvent) { } async function onPaste(ev: ClipboardEvent) { + if (props.mock) return; + for (const { item, i } of Array.from(ev.clipboardData.items, (item, i) => ({ item, i }))) { if (item.kind === 'file') { const file = item.getAsFile(); @@ -629,7 +645,7 @@ function onDrop(ev): void { } function saveDraft() { - if (props.instant) return; + if (props.instant || props.mock) return; const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}'); @@ -674,6 +690,8 @@ async function post(ev?: MouseEvent) { os.popup(MkRippleEffect, { x, y }, {}, 'end'); } + if (props.mock) return; + const annoying = text.includes('$[x2') || text.includes('$[x3') || @@ -839,6 +857,8 @@ function showActions(ev) { let postAccount = $ref<Misskey.entities.UserDetailed | null>(null); function openAccountMenu(ev: MouseEvent) { + if (props.mock) return; + openAccountMenu_({ withExtraOperation: false, includeCurrentAccount: true, @@ -869,7 +889,7 @@ onMounted(() => { nextTick(() => { // 書きかけの投稿を復元 - if (!props.instant && !props.mention && !props.specified) { + if (!props.instant && !props.mention && !props.specified && !props.mock) { const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey]; if (draft) { text = draft.data.text; diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index d499a22ed6..28a09c571f 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent } from 'vue'; +import { defineAsyncComponent, inject } from 'vue'; import * as Misskey from 'misskey-js'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import * as os from '@/os.js'; @@ -33,6 +33,8 @@ const props = defineProps<{ detachMediaFn?: (id: string) => void; }>(); +const mock = inject<boolean>('mock', false); + const emit = defineEmits<{ (ev: 'update:modelValue', value: any[]): void; (ev: 'detach', id: string): void; @@ -44,6 +46,8 @@ const emit = defineEmits<{ let menuShowing = false; function detachMedia(id: string) { + if (mock) return; + if (props.detachMediaFn) { props.detachMediaFn(id); } else { @@ -52,6 +56,11 @@ function detachMedia(id: string) { } function toggleSensitive(file) { + if (mock) { + emit('changeSensitive', file, !file.isSensitive); + return; + } + os.api('drive/files/update', { fileId: file.id, isSensitive: !file.isSensitive, @@ -61,6 +70,8 @@ function toggleSensitive(file) { } async function rename(file) { + if (mock) return; + const { canceled, result } = await os.inputText({ title: i18n.ts.enterFileName, default: file.name, @@ -77,6 +88,8 @@ async function rename(file) { } async function describe(file) { + if (mock) return; + os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { default: file.comment !== null ? file.comment : '', file: file, @@ -94,6 +107,8 @@ async function describe(file) { } async function crop(file: Misskey.entities.DriveFile): Promise<void> { + if (mock) return; + const newFile = await os.cropImage(file, { aspectRatio: NaN }); emit('replaceFile', file, newFile); } diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index d0db515219..d532ef9b66 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onMounted, shallowRef, watch } from 'vue'; +import { computed, inject, onMounted, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import XDetails from '@/components/MkReactionsViewer.details.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; @@ -36,6 +36,12 @@ const props = defineProps<{ note: Misskey.entities.Note; }>(); +const mock = inject<boolean>('mock', false); + +const emit = defineEmits<{ + (ev: 'reactionToggled', emoji: string, newCount: number): void; +}>(); + const buttonEl = shallowRef<HTMLElement>(); const canToggle = computed(() => !props.reaction.match(/@\w/) && $i); @@ -53,6 +59,11 @@ async function toggleReaction() { }); if (confirm.canceled) return; + if (mock) { + emit('reactionToggled', props.reaction, (props.count - 1)); + return; + } + os.api('notes/reactions/delete', { noteId: props.note.id, }).then(() => { @@ -64,6 +75,11 @@ async function toggleReaction() { } }); } else { + if (mock) { + emit('reactionToggled', props.reaction, (props.count + 1)); + return; + } + os.api('notes/reactions/create', { noteId: props.note.id, reaction: props.reaction, @@ -92,24 +108,26 @@ onMounted(() => { if (!props.isInitial) anime(); }); -useTooltip(buttonEl, async (showing) => { - const reactions = await os.apiGet('notes/reactions', { - noteId: props.note.id, - type: props.reaction, - limit: 11, - _cacheKey_: props.count, - }); +if (!mock) { + useTooltip(buttonEl, async (showing) => { + const reactions = await os.apiGet('notes/reactions', { + noteId: props.note.id, + type: props.reaction, + limit: 11, + _cacheKey_: props.count, + }); - const users = reactions.map(x => x.user); + const users = reactions.map(x => x.user); - os.popup(XDetails, { - showing, - reaction: props.reaction, - users, - count: props.count, - targetElement: buttonEl.value, - }, {}, 'closed'); -}, 100); + os.popup(XDetails, { + showing, + reaction: props.reaction, + users, + count: props.count, + targetElement: buttonEl.value, + }, {}, 'closed'); + }, 100); +} </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue index 52ead19a4b..eaa7faa4f9 100644 --- a/packages/frontend/src/components/MkReactionsViewer.vue +++ b/packages/frontend/src/components/MkReactionsViewer.vue @@ -12,14 +12,14 @@ SPDX-License-Identifier: AGPL-3.0-only :moveClass="defaultStore.state.animation ? $style.transition_x_move : ''" tag="div" :class="$style.root" > - <XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note"/> + <XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/> <slot v-if="hasMoreReactions" name="more"/> </TransitionGroup> </template> <script lang="ts" setup> import * as Misskey from 'misskey-js'; -import { watch } from 'vue'; +import { inject, watch } from 'vue'; import XReaction from '@/components/MkReactionsViewer.reaction.vue'; import { defaultStore } from '@/store.js'; @@ -30,6 +30,12 @@ const props = withDefaults(defineProps<{ maxNumber: Infinity, }); +const mock = inject<boolean>('mock', false); + +const emit = defineEmits<{ + (ev: 'mockUpdateMyReaction', emoji: string, delta: number): void; +}>(); + const initialReactions = new Set(Object.keys(props.note.reactions)); let reactions = $ref<[string, number][]>([]); @@ -39,6 +45,15 @@ if (props.note.myReaction && !Object.keys(reactions).includes(props.note.myReact reactions[props.note.myReaction] = props.note.reactions[props.note.myReaction]; } +function onMockToggleReaction(emoji: string, count: number) { + if (!mock) return; + + const i = reactions.findIndex((item) => item[0] === emoji); + if (i < 0) return; + + emit('mockUpdateMyReaction', emoji, (count - reactions[i][1])); +} + watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => { let newReactions: [string, number][] = []; hasMoreReactions = Object.keys(newSource).length > maxNumber; diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue new file mode 100644 index 0000000000..c7df1a576e --- /dev/null +++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue @@ -0,0 +1,117 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div v-if="phase === 'aboutNote'" class="_gaps"> + <div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._note.description }}</div> + <MkNote :class="$style.exampleNoteRoot" style="pointer-events: none;" :note="exampleNote" :mock="true"/> + <div class="_gaps_s"> + <div><i class="ti ti-arrow-back-up"></i> <b>{{ i18n.ts.reply }}</b> … {{ i18n.ts._initialTutorial._note.reply }}</div> + <div><i class="ti ti-repeat"></i> <b>{{ i18n.ts.renote }}</b> … {{ i18n.ts._initialTutorial._note.renote }}</div> + <div><i class="ti ti-plus"></i> <b>{{ i18n.ts.reaction }}</b> … {{ i18n.ts._initialTutorial._note.reaction }}</div> + <div><i class="ti ti-dots"></i> <b>{{ i18n.ts.menu }}</b> … {{ i18n.ts._initialTutorial._note.menu }}</div> + </div> +</div> +<div v-else-if="phase === 'howToReact'" class="_gaps"> + <div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div> + <div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div> + <MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction" @updateReaction="updateReaction"/> + <div v-if="onceReacted"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div> +</div> +</template> + +<script setup lang="ts"> +import * as Misskey from 'misskey-js'; +import { ref, reactive } from 'vue'; +import { i18n } from '@/i18n.js'; +import { globalEvents } from '@/events.js'; +import { $i } from '@/account.js'; +import MkNote from '@/components/MkNote.vue'; + +const props = defineProps<{ + phase: 'aboutNote' | 'howToReact'; +}>(); + +const emit = defineEmits<{ + (ev: 'reacted'): void; +}>(); + +const exampleNote = reactive<Misskey.entities.Note>({ + id: '0000000000', + createdAt: '2019-04-14T17:30:49.181Z', + userId: '0000000001', + user: { + id: '0000000001', + name: '藍', + username: 'ai', + host: null, + avatarDecorations: [], + avatarUrl: '/client-assets/tutorial/ai.webp', + avatarBlurhash: 'eiKmhHIByXxZ~qWXs:-pR*NbR*s:xuRjoL-oR*WCt6WWf6WVf6oeWB', + isBot: false, + isCat: true, + emojis: {}, + onlineStatus: null, + badgeRoles: [], + }, + text: 'just setting up my msky', + cw: null, + visibility: 'public', + localOnly: false, + reactionAcceptance: null, + renoteCount: 0, + repliesCount: 1, + reactions: {}, + reactionEmojis: {}, + fileIds: [], + files: [], + replyId: null, + renoteId: null, +}); +const onceReacted = ref<boolean>(false); + +function addReaction(emoji) { + onceReacted.value = true; + emit('reacted'); + exampleNote.reactions[emoji] = 1; + exampleNote.myReaction = emoji; + doNotification(emoji); +} + +function doNotification(emoji: string): void { + if (!$i || !emoji) return; + + const notification: Misskey.entities.Notification = { + id: Math.random().toString(), + createdAt: new Date().toUTCString(), + isRead: false, + type: 'reaction', + reaction: emoji, + user: $i, + userId: $i.id, + note: exampleNote, + }; + + globalEvents.emit('clientNotification', notification); +} + +function removeReaction(emoji) { + delete exampleNote.reactions[emoji]; + exampleNote.myReaction = undefined; +} +</script> + +<style lang="scss" module> +.exampleNoteRoot { + border-radius: var(--radius); + border: var(--panelBorder); + background: var(--panel); +} + +.divider { + height: 1px; + background: var(--divider); +} +</style> diff --git a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue new file mode 100644 index 0000000000..9b55a1dca7 --- /dev/null +++ b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue @@ -0,0 +1,135 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div class="_gaps"> + <div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._postNote.description1 }}</div> + <MkPostForm :class="$style.exampleRoot" :mock="true"/> + <MkFormSection> + <template #label>{{ i18n.ts.visibility }}</template> + <div class="_gaps"> + <div>{{ i18n.ts._initialTutorial._postNote._visibility.description }}</div> + <div><i class="ti ti-world"></i> <b>{{ i18n.ts._visibility.public }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.public }}</div> + <div><i class="ti ti-home"></i> <b>{{ i18n.ts._visibility.home }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.home }}</div> + <div><i class="ti ti-lock"></i> <b>{{ i18n.ts._visibility.followers }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.followers }}</div> + <div class="_gaps_s"> + <div><i class="ti ti-mail"></i> <b>{{ i18n.ts._visibility.specified }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.direct }}</div> + <MkInfo :warn="true"> + <b>{{ i18n.ts._initialTutorial._postNote._visibility.doNotSendConfidencialOnDirect1 }}</b> {{ i18n.ts._initialTutorial._postNote._visibility.doNotSendConfidencialOnDirect2 }} + </MkInfo> + </div> + <div><i class="ti ti-rocket-off"></i> <b>{{ i18n.ts._visibility.disableFederation }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.localOnly }}</div> + </div> + </MkFormSection> + <MkFormSection> + <template #label>{{ i18n.ts._initialTutorial._postNote._cw.title }}</template> + <div class="_gaps"> + <div>{{ i18n.ts._initialTutorial._postNote._cw.description }}</div> + <MkNote :class="$style.exampleRoot" :note="exampleCWNote" :mock="true"/> + <div>{{ i18n.ts._initialTutorial._postNote._cw.useCases }}</div> + </div> + </MkFormSection> +</div> +</template> + +<script setup lang="ts"> +import * as Misskey from 'misskey-js'; +import { reactive } from 'vue'; +import { i18n } from '@/i18n.js'; +import MkNote from '@/components/MkNote.vue'; +import MkPostForm from '@/components/MkPostForm.vue'; +import MkFormSection from '@/components/form/section.vue'; +import MkInfo from '@/components/MkInfo.vue'; + +const exampleCWNote = reactive<Misskey.entities.Note>({ + id: '0000000000', + createdAt: '2019-04-14T17:30:49.181Z', + userId: '0000000001', + user: { + id: '0000000001', + name: '藍', + username: 'ai', + host: null, + avatarDecorations: [], + avatarUrl: '/client-assets/tutorial/ai.webp', + avatarBlurhash: 'eiKmhHIByXxZ~qWXs:-pR*NbR*s:xuRjoL-oR*WCt6WWf6WVf6oeWB', + isBot: false, + isCat: true, + emojis: {}, + onlineStatus: null, + badgeRoles: [], + }, + text: i18n.ts._initialTutorial._postNote._cw._exampleNote.note, + cw: i18n.ts._initialTutorial._postNote._cw._exampleNote.cw, + visibility: 'public', + localOnly: false, + reactionAcceptance: null, + renoteCount: 0, + repliesCount: 1, + reactions: {}, + reactionEmojis: {}, + fileIds: [], + files: [], + replyId: null, + renoteId: null, +}); +</script> + +<style lang="scss" module> +.exampleRoot { + max-width: none!important; + border-radius: var(--radius); + border: var(--panelBorder); + background: var(--panel); +} + +.divider { + height: 1px; + background: var(--divider); +} + +.image { + max-width: 300px; + margin: 0 auto; +} + +.post { + position: relative; + display: block; + width: 100%; + height: 40px; + color: var(--fgOnAccent); + font-weight: bold; + text-align: left; + + &:before { + content: ""; + display: block; + width: calc(100% - 38px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + } + +} + +.postIcon { + position: relative; + margin-left: 30px; + margin-right: 8px; + width: 32px; +} + +.postText { + position: relative; + line-height: 40px; +} +</style> diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue new file mode 100644 index 0000000000..768d00bb07 --- /dev/null +++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue @@ -0,0 +1,144 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div class="_gaps"> + <div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.description }}</div> + <div>{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.tryThisFile }}</div> + <MkInfo>{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.method }}</MkInfo> + <MkPostForm + :class="$style.exampleRoot" + :mock="true" + :initialNote="exampleNote" + @fileChangeSensitive="doSucceeded" + ></MkPostForm> + <div v-if="onceSucceeded"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div> + <MkFolder> + <template #label>{{ i18n.ts.previewNoteText }}</template> + <MkNote :mock="true" :note="exampleNote" :class="$style.exampleRoot"></MkNote> + </MkFolder> +</div> +</template> + +<script setup lang="ts"> +import * as Misskey from 'misskey-js'; +import { ref, reactive } from 'vue'; +import { i18n } from '@/i18n.js'; +import MkPostForm from '@/components/MkPostForm.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import MkInfo from '@/components/MkInfo.vue'; +import MkNote from '@/components/MkNote.vue'; +import { $i } from '@/account.js'; + +const emit = defineEmits<{ + (ev: 'succeeded'): void; +}>(); + +const onceSucceeded = ref<boolean>(false); + +function doSucceeded(fileId: string, to: boolean) { + if (fileId === exampleNote.fileIds[0] && to) { + onceSucceeded.value = true; + emit('succeeded'); + } +} + +const exampleNote = reactive<Misskey.entities.Note>({ + id: '0000000000', + createdAt: '2019-04-14T17:30:49.181Z', + userId: '0000000001', + user: $i!, + text: i18n.ts._initialTutorial._howToMakeAttachmentsSensitive._exampleNote.note, + cw: null, + visibility: 'public', + localOnly: false, + reactionAcceptance: null, + renoteCount: 0, + repliesCount: 1, + reactions: {}, + reactionEmojis: {}, + fileIds: ['0000000002'], + files: [{ + id: '0000000002', + createdAt: '2019-04-14T17:30:49.181Z', + name: 'natto_failed.webp', + type: 'image/webp', + md5: 'c44286cf152d0740be0ce5ad45ea85c3', + size: 827532, + isSensitive: false, + blurhash: 'LXNA3TD*XAIA%1%M%gt7.TofRioz', + properties: { + width: 256, + height: 256, + }, + url: '/client-assets/tutorial/natto_failed.webp', + thumbnailUrl: '/client-assets/tutorial/natto_failed.webp', + comment: null, + folderId: null, + folder: null, + userId: null, + user: null, + }], + replyId: null, + renoteId: null, +}); + +</script> + +<style lang="scss" module> +.exampleRoot { + border-radius: var(--radius); + border: var(--panelBorder); + background: var(--panel); +} + +.divider { + height: 1px; + background: var(--divider); +} + +.image { + max-width: 300px; + margin: 0 auto; +} + +.post { + position: relative; + display: block; + width: 100%; + height: 40px; + color: var(--fgOnAccent); + font-weight: bold; + text-align: left; + + &:before { + content: ""; + display: block; + width: calc(100% - 38px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + } + +} + +.postIcon { + position: relative; + margin-left: 30px; + margin-right: 8px; + width: 32px; +} + +.postText { + position: relative; + line-height: 40px; +} +</style> diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue new file mode 100644 index 0000000000..75b917f33c --- /dev/null +++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue @@ -0,0 +1,87 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div class="_gaps"> + <div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._timeline.description1 }}</div> + <div class="_gaps_s"> + <div><i class="ti ti-home"></i> <b>{{ i18n.ts._timelines.home }}</b> … {{ i18n.ts._initialTutorial._timeline.home }}</div> + <div><i class="ti ti-planet"></i> <b>{{ i18n.ts._timelines.local }}</b> … {{ i18n.ts._initialTutorial._timeline.local }}</div> + <div><i class="ti ti-universe"></i> <b>{{ i18n.ts._timelines.social }}</b> … {{ i18n.ts._initialTutorial._timeline.social }}</div> + <div><i class="ti ti-whirl"></i> <b>{{ i18n.ts._timelines.global }}</b> … {{ i18n.ts._initialTutorial._timeline.global }}</div> + </div> + <div class="_gaps_s"> + <div>{{ i18n.ts._initialTutorial._timeline.description2 }}</div> + <img :class="$style.image" src="/client-assets/tutorial/timeline_tab.png"/> + </div> + <div :class="$style.divider"></div> + <I18n :src="i18n.ts._initialTutorial._timeline.description3" tag="div" style="padding: 0 16px;"> + <template #link> + <a href="https://misskey-hub.net/docs/features/timeline.html" target="_blank" class="_link">{{ i18n.ts.help }}</a> + </template> + </I18n> + +</div> +</template> + +<script setup lang="ts"> +import { i18n } from '@/i18n.js'; +</script> + +<style lang="scss" module> +.exampleNoteRoot { + border-radius: var(--radius); + border: var(--panelBorder); + background: var(--panel); +} + +.divider { + height: 1px; + background: var(--divider); +} + +.image { + max-width: 300px; + margin: 0 auto; +} + +.post { + position: relative; + display: block; + width: 100%; + height: 40px; + color: var(--fgOnAccent); + font-weight: bold; + text-align: left; + + &:before { + content: ""; + display: block; + width: calc(100% - 38px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + } + +} + +.postIcon { + position: relative; + margin-left: 30px; + margin-right: 8px; + width: 32px; +} + +.postText { + position: relative; + line-height: 40px; +} +</style> diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue new file mode 100644 index 0000000000..e28838425f --- /dev/null +++ b/packages/frontend/src/components/MkTutorialDialog.vue @@ -0,0 +1,260 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModalWindow + ref="dialog" + :width="600" + :height="650" + @close="close(true)" + @closed="emit('closed')" +> + <template v-if="page === 1" #header><i class="ti ti-pencil"></i> {{ i18n.ts._initialTutorial._note.title }}</template> + <template v-else-if="page === 2" #header><i class="ti ti-mood-smile"></i> {{ i18n.ts._initialTutorial._reaction.title }}</template> + <template v-else-if="page === 3" #header><i class="ti ti-home"></i> {{ i18n.ts._initialTutorial._timeline.title }}</template> + <template v-else-if="page === 4" #header><i class="ti ti-pencil-plus"></i> {{ i18n.ts._initialTutorial._postNote.title }}</template> + <template v-else-if="page === 5" #header><i class="ti ti-eye-exclamation"></i> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.title }}</template> + <template v-else #header>{{ i18n.ts._initialTutorial.title }}</template> + + <div style="overflow-x: clip;"> + <Transition + mode="out-in" + :enterActiveClass="$style.transition_x_enterActive" + :leaveActiveClass="$style.transition_x_leaveActive" + :enterFromClass="$style.transition_x_enterFrom" + :leaveToClass="$style.transition_x_leaveTo" + > + <template v-if="page === 0"> + <div :class="$style.centerPage"> + <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/> + <MkSpacer :marginMin="20" :marginMax="28"> + <div class="_gaps" style="text-align: center;"> + <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <div style="font-size: 120%;">{{ i18n.ts._initialTutorial._landing.title }}</div> + <div>{{ i18n.ts._initialTutorial._landing.description }}</div> + <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts._initialTutorial.launchTutorial }} <i class="ti ti-arrow-right"></i></MkButton> + <MkButton style="margin: 0 auto;" transparent rounded @click="close(true)">{{ i18n.ts.close }}</MkButton> + </div> + </MkSpacer> + </div> + </template> + <template v-else-if="page === 1"> + <div style="height: 100cqh; overflow: auto;"> + <div :class="$style.pageRoot"> + <MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain"> + <XNote phase="aboutNote"/> + </MkSpacer> + <div :class="$style.pageFooter"> + <div class="_buttonsCenter"> + <MkButton v-if="initialPage !== 1" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> + </div> + </div> + </div> + </template> + <template v-else-if="page === 2"> + <div style="height: 100cqh; overflow: auto;"> + <div :class="$style.pageRoot"> + <MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain"> + <div class="_gaps"> + <XNote phase="howToReact" @reacted="isReactionTutorialPushed = true"/> + <div v-if="!isReactionTutorialPushed">{{ i18n.ts._initialTutorial._reaction.reactToContinue }}</div> + </div> + </MkSpacer> + <div :class="$style.pageFooter"> + <div class="_buttonsCenter"> + <MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate :disabled="!isReactionTutorialPushed" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> + </div> + </div> + </div> + </template> + <template v-else-if="page === 3"> + <div style="height: 100cqh; overflow: auto;"> + <div :class="$style.pageRoot"> + <MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain"> + <XTimeline/> + </MkSpacer> + <div :class="$style.pageFooter"> + <div class="_buttonsCenter"> + <MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> + </div> + </div> + </div> + </template> + <template v-else-if="page === 4"> + <div style="height: 100cqh; overflow: auto;"> + <div :class="$style.pageRoot"> + <MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain"> + <XPostNote/> + </MkSpacer> + <div :class="$style.pageFooter"> + <div class="_buttonsCenter"> + <MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> + </div> + </div> + </div> + </template> + <template v-else-if="page === 5"> + <div style="height: 100cqh; overflow: auto;"> + <div :class="$style.pageRoot"> + <MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain"> + <div class="_gaps"> + <XSensitive @succeeded="isSensitiveTutorialSucceeded = true"/> + <div v-if="!isSensitiveTutorialSucceeded">{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.doItToContinue }}</div> + </div> + </MkSpacer> + <div :class="$style.pageFooter"> + <div class="_buttonsCenter"> + <MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate :disabled="!isSensitiveTutorialSucceeded" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> + </div> + </div> + </div> + </template> + <template v-else-if="page === 6"> + <div :class="$style.centerPage"> + <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/> + <MkSpacer :marginMin="20" :marginMax="28"> + <div class="_gaps" style="text-align: center;"> + <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div> + <I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;"> + <template #link> + <a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a> + </template> + </I18n> + <div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div> + <div class="_buttonsCenter" style="margin-top: 16px;"> + <MkButton v-if="initialPage !== 4" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton rounded primary gradate @click="close(false)">{{ i18n.ts.close }}</MkButton> + </div> + </div> + </MkSpacer> + </div> + </template> + </Transition> + </div> +</MkModalWindow> +</template> + +<script lang="ts" setup> +import { ref, shallowRef, watch } from 'vue'; +import MkModalWindow from '@/components/MkModalWindow.vue'; +import MkButton from '@/components/MkButton.vue'; +import XNote from '@/components/MkTutorialDialog.Note.vue'; +import XTimeline from '@/components/MkTutorialDialog.Timeline.vue'; +import XPostNote from '@/components/MkTutorialDialog.PostNote.vue'; +import XSensitive from '@/components/MkTutorialDialog.Sensitive.vue'; +import MkAnimBg from '@/components/MkAnimBg.vue'; +import { i18n } from '@/i18n.js'; +import { instance } from '@/instance.js'; +import { host } from '@/config.js'; +import { claimAchievement } from '@/scripts/achievements.js'; +import * as os from '@/os.js'; + +const props = defineProps<{ + initialPage?: number; +}>(); + +const emit = defineEmits<{ + (ev: 'closed'): void; +}>(); + +const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); + +// eslint-disable-next-line vue/no-setup-props-destructure +const page = ref(props.initialPage ?? 0); + +watch(page, (to) => { + // チュートリアルの枚数を増やしたら必ず変更すること!! + if (to === 6) { + claimAchievement('tutorialCompleted'); + } +}); + +const isReactionTutorialPushed = ref<boolean>(false); +const isSensitiveTutorialSucceeded = ref<boolean>(false); + +async function close(skip: boolean) { + if (skip) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts._initialTutorial.skipAreYouSure, + }); + if (canceled) return; + } + + dialog.value?.close(); +} +</script> + +<style lang="scss" module> +.transition_x_enterActive, +.transition_x_leaveActive { + transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1); +} +.transition_x_enterFrom { + opacity: 0; + transform: translateX(50px); +} +.transition_x_leaveTo { + opacity: 0; + transform: translateX(-50px); +} + +.progressBar { + position: absolute; + top: 0; + left: 0; + z-index: 10; + width: 100%; + height: 4px; +} + +.progressBarValue { + height: 100%; + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + transition: all 0.5s cubic-bezier(0,.5,.5,1); +} + +.centerPage { + display: flex; + justify-content: center; + align-items: center; + height: 100cqh; + padding-bottom: 30px; + box-sizing: border-box; +} + +.pageRoot { + display: flex; + flex-direction: column; + min-height: 100%; +} + +.pageMain { + flex-grow: 1; + line-height: 1.5; +} + +.pageFooter { + position: sticky; + bottom: 0; + left: 0; + flex-shrink: 0; + padding: 12px; + border-top: solid 0.5px var(--divider); + -webkit-backdrop-filter: blur(15px); + backdrop-filter: blur(15px); +} +</style> diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue index d60e01c44d..05b55f77a7 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.vue @@ -46,24 +46,32 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template v-else-if="page === 1"> <div style="height: 100cqh; overflow: auto;"> - <MkSpacer :marginMin="20" :marginMax="28"> - <XProfile/> - <div class="_buttonsCenter" style="margin-top: 16px;"> - <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> - <MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + <div :class="$style.pageRoot"> + <MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain"> + <XProfile/> + </MkSpacer> + <div :class="$style.pageFooter"> + <div class="_buttonsCenter"> + <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> </div> - </MkSpacer> + </div> </div> </template> <template v-else-if="page === 2"> <div style="height: 100cqh; overflow: auto;"> - <MkSpacer :marginMin="20" :marginMax="28"> - <XPrivacy/> - <div class="_buttonsCenter" style="margin-top: 16px;"> - <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> - <MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + <div :class="$style.pageRoot"> + <MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain"> + <XPrivacy/> + </MkSpacer> + <div :class="$style.pageFooter"> + <div class="_buttonsCenter"> + <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> </div> - </MkSpacer> + </div> </div> </template> <template v-else-if="page === 3"> @@ -102,16 +110,13 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps" style="text-align: center;"> <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> <div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div> - <I18n :src="i18n.ts._initialAccountSetting.ifYouNeedLearnMore" tag="div" style="padding: 0 16px;"> - <template #name>{{ instance.name ?? host }}</template> - <template #link> - <a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a> - </template> - </I18n> - <div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div> + <div>{{ i18n.t('_initialAccountSetting.youCanContinueTutorial', { name: instance.name ?? host }) }}</div> <div class="_buttonsCenter" style="margin-top: 16px;"> + <MkButton rounded primary gradate data-cy-user-setup-continue @click="launchTutorial()">{{ i18n.ts._initialAccountSetting.startTutorial }} <i class="ti ti-arrow-right"></i></MkButton> + </div> + <div class="_buttonsCenter"> <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> - <MkButton primary rounded gradate data-cy-user-setup-continue @click="close(false)">{{ i18n.ts.close }}</MkButton> + <MkButton rounded primary data-cy-user-setup-continue @click="setupComplete()">{{ i18n.ts.close }}</MkButton> </div> </div> </MkSpacer> @@ -123,7 +128,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, shallowRef, watch } from 'vue'; +import { ref, shallowRef, watch, nextTick, defineAsyncComponent } from 'vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import MkButton from '@/components/MkButton.vue'; import XProfile from '@/components/MkUserSetupDialog.Profile.vue'; @@ -143,6 +148,7 @@ const emit = defineEmits<{ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +// eslint-disable-next-line vue/no-setup-props-destructure const page = ref(defaultStore.state.accountSetupWizard); watch(page, () => { @@ -158,10 +164,24 @@ async function close(skip: boolean) { if (canceled) return; } - dialog.value.close(); + dialog.value?.close(); defaultStore.set('accountSetupWizard', -1); } +function setupComplete() { + defaultStore.set('accountSetupWizard', -1); + dialog.value?.close(); +} + +function launchTutorial() { + setupComplete(); + nextTick(() => { + os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), { + initialPage: 1, + }, {}, 'closed'); + }); +} + async function later(later: boolean) { if (later) { const { canceled } = await os.confirm({ @@ -171,7 +191,7 @@ async function later(later: boolean) { if (canceled) return; } - dialog.value.close(); + dialog.value?.close(); defaultStore.set('accountSetupWizard', 0); } </script> @@ -214,10 +234,21 @@ async function later(later: boolean) { box-sizing: border-box; } +.pageRoot { + display: flex; + flex-direction: column; + min-height: 100%; +} + +.pageMain { + flex-grow: 1; +} + .pageFooter { position: sticky; bottom: 0; left: 0; + flex-shrink: 0; padding: 12px; border-top: solid 0.5px var(--divider); -webkit-backdrop-filter: blur(15px); diff --git a/packages/frontend/src/pages/timeline.tutorial.vue b/packages/frontend/src/pages/timeline.tutorial.vue deleted file mode 100644 index 66b8e796e5..0000000000 --- a/packages/frontend/src/pages/timeline.tutorial.vue +++ /dev/null @@ -1,123 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and other misskey contributors -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<div :class="$style.container"> - <div :class="$style.title"> - <div :class="$style.titleText"><i class="ti ti-info-circle"></i> {{ i18n.ts._timelineTutorial.title }}</div> - <div :class="$style.step"> - <button class="_button" :class="$style.stepArrow" :disabled="tutorial === 0" @click="tutorial--"> - <i class="ti ti-chevron-left"></i> - </button> - <span :class="$style.stepNumber">{{ tutorial + 1 }} / {{ tutorialsNumber }}</span> - <button class="_button" :class="$style.stepArrow" :disabled="tutorial === tutorialsNumber - 1" @click="tutorial++"> - <i class="ti ti-chevron-right"></i> - </button> - </div> - </div> - - <div v-if="tutorial === 0" :class="$style.body"> - <div>{{ i18n.t('_timelineTutorial.step1_1', { name: instance.name ?? host }) }}</div> - <div>{{ i18n.t('_timelineTutorial.step1_2', { name: instance.name ?? host }) }}</div> - </div> - <div v-else-if="tutorial === 1" :class="$style.body"> - <div>{{ i18n.ts._timelineTutorial.step2_1 }}</div> - <div>{{ i18n.t('_timelineTutorial.step2_2', { name: instance.name ?? host }) }}</div> - </div> - <div v-else-if="tutorial === 2" :class="$style.body"> - <div>{{ i18n.ts._timelineTutorial.step3_1 }}</div> - <div>{{ i18n.ts._timelineTutorial.step3_2 }}</div> - </div> - <div v-else-if="tutorial === 3" :class="$style.body"> - <div>{{ i18n.ts._timelineTutorial.step4_1 }}</div> - <div>{{ i18n.ts._timelineTutorial.step4_2 }}</div> - </div> - - <div :class="$style.footer"> - <template v-if="tutorial === tutorialsNumber - 1"> - <MkButton :class="$style.footerItem" primary rounded gradate @click="tutorial = -1">{{ i18n.ts.done }} <i class="ti ti-check"></i></MkButton> - </template> - <template v-else> - <MkButton :class="$style.footerItem" primary rounded gradate @click="tutorial++">{{ i18n.ts.next }} <i class="ti ti-arrow-right"></i></MkButton> - </template> - </div> -</div> -</template> - -<script lang="ts" setup> -import { computed } from 'vue'; -import MkButton from '@/components/MkButton.vue'; -import { defaultStore } from '@/store.js'; -import { i18n } from '@/i18n.js'; -import { instance } from '@/instance.js'; -import { host } from '@/config.js'; - -const tutorialsNumber = 4; - -const tutorial = computed({ - get() { return defaultStore.reactiveState.timelineTutorial.value || 0; }, - set(value) { defaultStore.set('timelineTutorial', value); }, -}); -</script> - -<style lang="scss" module> -.small { - opacity: 0.7; -} - -.container { - border: solid 2px var(--accent); -} - -.title { - display: flex; - flex-wrap: wrap; - padding: 22px 32px; - font-weight: bold; - - &Text { - margin: 4px 0; - padding-right: 4px; - } -} - -.step { - margin-left: auto; - - &Arrow { - padding: 4px; - &:disabled { - opacity: 0.5; - } - &:first-child { - padding-right: 8px; - } - &:last-child { - padding-left: 8px; - } - } - - &Number { - font-weight: normal; - margin: 4px; - } -} - -.body { - padding: 0 32px; -} - -.footer { - display: flex; - flex-wrap: wrap; - flex-direction: row; - justify-content: right; - padding: 22px 32px; - - &Item { - margin: 4px; - } -} -</style> diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 5b97385ead..cfe270aefb 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -8,7 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template> <MkSpacer :contentMax="800"> <div ref="rootEl" v-hotkey.global="keymap"> - <XTutorial v-if="$i && defaultStore.reactiveState.timelineTutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/> + <MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()"> + {{ i18n.ts._timelineDescription[src] }} + </MkInfo> <MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/> <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> @@ -31,9 +33,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, computed, watch, provide } from 'vue'; +import { computed, watch, provide } from 'vue'; import type { Tab } from '@/components/global/MkPageHeader.tabs.vue'; import MkTimeline from '@/components/MkTimeline.vue'; +import MkInfo from '@/components/MkInfo.vue'; import MkPostForm from '@/components/MkPostForm.vue'; import { scroll } from '@/scripts/scroll.js'; import * as os from '@/os.js'; @@ -48,8 +51,6 @@ import { deviceKind } from '@/scripts/device-kind.js'; provide('shouldOmitHeaderTitle', true); -const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue')); - const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable); const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable); const keymap = { @@ -140,6 +141,13 @@ function focus(): void { tlComponent.focus(); } +function closeTutorial(): void { + if (!['home', 'local', 'social', 'global'].includes(src)) return; + const before = defaultStore.state.timelineTutorials; + before[src] = true; + defaultStore.set('timelineTutorials', before); +} + const headerActions = $computed(() => { const tmp = [ { diff --git a/packages/frontend/src/scripts/achievements.ts b/packages/frontend/src/scripts/achievements.ts index af7c6b060c..e7585fcf81 100644 --- a/packages/frontend/src/scripts/achievements.ts +++ b/packages/frontend/src/scripts/achievements.ts @@ -82,6 +82,7 @@ export const ACHIEVEMENT_TYPES = [ 'cookieClicked', 'brainDiver', 'smashTestNotificationButton', + 'tutorialCompleted', ] as const; export const ACHIEVEMENT_BADGES = { @@ -460,6 +461,11 @@ export const ACHIEVEMENT_BADGES = { bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))', frame: 'bronze', }, + 'tutorialCompleted': { + img: '/fluent-emoji/1f393.png', + bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', + frame: 'bronze', + }, /* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107> } as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], { img: string; diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 0f2e642b7b..7f916656de 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -49,9 +49,14 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: 0, }, - timelineTutorial: { + timelineTutorials: { where: 'account', - default: 0, + default: { + home: false, + local: false, + social: false, + global: false, + }, }, keepCw: { where: 'account', diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index ff6157f5f8..64008c5748 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { defineAsyncComponent } from 'vue'; import type { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; import { instance } from '@/instance.js'; @@ -102,7 +103,13 @@ export function openInstanceMenu(ev: MouseEvent) { action: () => { window.open('https://misskey-hub.net/help.html', '_blank'); }, - }, { + }, ($i) ? { + text: i18n.ts._initialTutorial.launchTutorial, + icon: 'ti ti-presentation', + action: () => { + os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {}, 'closed'); + }, + } : undefined, { type: 'link', text: i18n.ts.aboutMisskey, to: '/about-misskey', From ee191169f5ed90b9a343d21eaf2166ffd71efebc Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Fri, 3 Nov 2023 17:14:35 +0900 Subject: [PATCH 20/60] enhance(frontend): tweak announcement manage ui --- locales/index.d.ts | 2 ++ locales/ja-JP.yml | 2 ++ packages/frontend/src/pages/admin/announcements.vue | 2 ++ 3 files changed, 6 insertions(+) diff --git a/locales/index.d.ts b/locales/index.d.ts index b45559eea2..2437775648 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1169,6 +1169,8 @@ export interface Locale { "tooManyActiveAnnouncementDescription": string; "readConfirmTitle": string; "readConfirmText": string; + "shouldNotBeUsedToPresentPermanentInfo": string; + "dialogAnnouncementUxWarn": string; }; "_initialAccountSetting": { "accountCreated": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 8fd77afd92..58ba8e04de 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1167,6 +1167,8 @@ _announcement: tooManyActiveAnnouncementDescription: "アクティブなお知らせが多いため、UXが低下する可能性があります。終了したお知らせはアーカイブすることを検討してください。" readConfirmTitle: "既読にしますか?" readConfirmText: "「{title}」の内容を読み、既読にします。" + shouldNotBeUsedToPresentPermanentInfo: "特に新規ユーザーのUXを損ねる可能性が高いため、ストック情報ではなくフロー情報の掲示にお知らせを使用することを推奨します。" + dialogAnnouncementUxWarn: "ダイアログ形式のお知らせが同時に2つ以上ある場合、UXに悪影響を及ぼす可能性が非常に高いため、使用は慎重に行うことを推奨します。" _initialAccountSetting: accountCreated: "アカウントの作成が完了しました!" diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index d29a07b8cd..36a67eba31 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="900"> <div class="_gaps"> + <MkInfo>{{ i18n.ts._announcement.shouldNotBeUsedToPresentPermanentInfo }}</MkInfo> <MkInfo v-if="announcements.length > 5" warn>{{ i18n.ts._announcement.tooManyActiveAnnouncementDescription }}</MkInfo> <MkFolder v-for="announcement in announcements" :key="announcement.id ?? announcement._id" :defaultOpen="announcement.id == null"> @@ -43,6 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="banner">{{ i18n.ts.banner }}</option> <option value="dialog">{{ i18n.ts.dialog }}</option> </MkRadios> + <MkInfo v-if="announcement.display === 'dialog'" warn>{{ i18n.ts._announcement.dialogAnnouncementUxWarn }}</MkInfo> <MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription"> {{ i18n.ts._announcement.forExistingUsers }} </MkSwitch> From 4631e6cd4a4b8bf2d8dcbb729058a3b47300f016 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Fri, 3 Nov 2023 17:18:30 +0900 Subject: [PATCH 21/60] fix(frontend): In deck layout, replies option is not saved after refresh Fix #12228 --- CHANGELOG.md | 1 + packages/frontend/src/ui/deck/tl-column.vue | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fdf9687ec..c99410696d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ - Fix: 個人カードのemojiがバッテリーになっている問題を修正 - Fix: 標準テーマと同じIDを使用してインストールできてしまう問題を修正 - Fix: 絵文字ピッカーでバッテリーの絵文字が複数表示される問題を修正 #12197 +- Fix: In deck layout, replies option is not saved after refresh ### Server - Feat: Registry APIがサードパーティから利用可能になりました diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index bab93622f0..c5629f69a4 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -61,6 +61,12 @@ watch($$(withRenotes), v => { }); }); +watch($$(withReplies), v => { + updateColumn(props.column.id, { + withReplies: v, + }); +}); + watch($$(onlyFiles), v => { updateColumn(props.column.id, { onlyFiles: v, From 39a3f4ae98ebe436ed023fab737a823717da5e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Fri, 3 Nov 2023 17:34:23 +0900 Subject: [PATCH 22/60] =?UTF-8?q?feat:=20=E3=83=81=E3=83=A3=E3=83=B3?= =?UTF-8?q?=E3=83=8D=E3=83=AB=E5=86=85=E2=86=92=E3=83=81=E3=83=A3=E3=83=B3?= =?UTF-8?q?=E3=83=8D=E3=83=AB=E5=A4=96=E3=81=B8=E3=81=AE=E3=83=AA=E3=83=8E?= =?UTF-8?q?=E3=83=BC=E3=83=88=E5=88=B6=E9=99=90=E6=A9=9F=E8=83=BD=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=20(#12230)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * チャンネル内→チャンネル外へのリノート制限機能追加 * fix CHANGELOG.md * コメント対応(canRenoteSwitch→allowRenoteToExternal) * コメント対応(別チャンネルへのリノート対策) * コメント対応(canRenote->allowRenoteToExternal) * fix comment * Update misskey-js.api.md * :v: --------- Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com> Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> --- CHANGELOG.md | 1 + locales/index.d.ts | 1 + locales/ja-JP.yml | 1 + ...8840138000-add-allow-renote-to-external.js | 16 +++ .../src/core/entities/ChannelEntityService.ts | 1 + .../src/core/entities/NoteEntityService.ts | 1 + packages/backend/src/models/Channel.ts | 5 + .../backend/src/models/json-schema/channel.ts | 4 + .../server/api/endpoints/channels/create.ts | 2 + .../server/api/endpoints/channels/update.ts | 2 + .../src/server/api/endpoints/notes/create.ts | 19 +++ packages/frontend/src/components/MkNote.vue | 97 +------------- .../src/components/MkNoteDetailed.vue | 69 +--------- .../frontend/src/pages/channel-editor.vue | 9 +- .../frontend/src/scripts/get-note-menu.ts | 120 ++++++++++++++++++ packages/misskey-js/etc/misskey-js.api.md | 18 ++- packages/misskey-js/src/entities.ts | 17 ++- 17 files changed, 222 insertions(+), 161 deletions(-) create mode 100644 packages/backend/migration/1698840138000-add-allow-renote-to-external.js diff --git a/CHANGELOG.md b/CHANGELOG.md index c99410696d..d096d02e62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - 画像のテンプレートはこちらです: https://misskey-hub.net/avatar-decoration-template.png - 最大でも黄色いエリア内にデコレーションを収めることを推奨します。 - 画像は512x512pxを推奨します。 +- Feat: チャンネル設定にリノート/引用リノートの可否を設定できる項目を追加 - Enhance: すでにフォローしたすべての人の返信をTLに追加できるように - Enhance: 未読の通知数を表示できるように - Enhance: ローカリゼーションの更新 diff --git a/locales/index.d.ts b/locales/index.d.ts index 2437775648..50b11acc06 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1832,6 +1832,7 @@ export interface Locale { "notesCount": string; "nameAndDescription": string; "nameOnly": string; + "allowRenoteToExternal": string; }; "_menuDisplay": { "sideFull": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 58ba8e04de..de4e8ce2b3 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1737,6 +1737,7 @@ _channel: notesCount: "{n}投稿があります" nameAndDescription: "名前と説明" nameOnly: "名前のみ" + allowRenoteToExternal: "チャンネル外へのリノートと引用リノートを許可する" _menuDisplay: sideFull: "横" diff --git a/packages/backend/migration/1698840138000-add-allow-renote-to-external.js b/packages/backend/migration/1698840138000-add-allow-renote-to-external.js new file mode 100644 index 0000000000..0edf298841 --- /dev/null +++ b/packages/backend/migration/1698840138000-add-allow-renote-to-external.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddAllowRenoteToExternal1698840138000 { + name = 'AddAllowRenoteToExternal1698840138000' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "channel" ADD "allowRenoteToExternal" boolean NOT NULL DEFAULT true`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "allowRenoteToExternal"`); + } +} diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts index 9e66834dfa..305946b8a6 100644 --- a/packages/backend/src/core/entities/ChannelEntityService.ts +++ b/packages/backend/src/core/entities/ChannelEntityService.ts @@ -85,6 +85,7 @@ export class ChannelEntityService { usersCount: channel.usersCount, notesCount: channel.notesCount, isSensitive: channel.isSensitive, + allowRenoteToExternal: channel.allowRenoteToExternal, ...(me ? { isFollowing, diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 6fde1c3830..c49dad8e79 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -350,6 +350,7 @@ export class NoteEntityService implements OnModuleInit { name: channel.name, color: channel.color, isSensitive: channel.isSensitive, + allowRenoteToExternal: channel.allowRenoteToExternal, } : undefined, mentions: note.mentions.length > 0 ? note.mentions : undefined, uri: note.uri ?? undefined, diff --git a/packages/backend/src/models/Channel.ts b/packages/backend/src/models/Channel.ts index f90f8c03d8..a7f9e262b1 100644 --- a/packages/backend/src/models/Channel.ts +++ b/packages/backend/src/models/Channel.ts @@ -93,4 +93,9 @@ export class MiChannel { default: false, }) public isSensitive: boolean; + + @Column('boolean', { + default: true, + }) + public allowRenoteToExternal: boolean; } diff --git a/packages/backend/src/models/json-schema/channel.ts b/packages/backend/src/models/json-schema/channel.ts index f1019d1461..8f9770cdc5 100644 --- a/packages/backend/src/models/json-schema/channel.ts +++ b/packages/backend/src/models/json-schema/channel.ts @@ -76,5 +76,9 @@ export const packedChannelSchema = { type: 'boolean', optional: false, nullable: false, }, + allowRenoteToExternal: { + type: 'boolean', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts index 3ba411d28c..3dd1eddd01 100644 --- a/packages/backend/src/server/api/endpoints/channels/create.ts +++ b/packages/backend/src/server/api/endpoints/channels/create.ts @@ -50,6 +50,7 @@ export const paramDef = { bannerId: { type: 'string', format: 'misskey:id', nullable: true }, color: { type: 'string', minLength: 1, maxLength: 16 }, isSensitive: { type: 'boolean', nullable: true }, + allowRenoteToExternal: { type: 'boolean', nullable: true }, }, required: ['name'], } as const; @@ -87,6 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- bannerId: banner ? banner.id : null, isSensitive: ps.isSensitive ?? false, ...(ps.color !== undefined ? { color: ps.color } : {}), + allowRenoteToExternal: ps.allowRenoteToExternal ?? true, } as MiChannel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0])); return await this.channelEntityService.pack(channel, me); diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts index ab69f62a7b..93d02e4a12 100644 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ b/packages/backend/src/server/api/endpoints/channels/update.ts @@ -61,6 +61,7 @@ export const paramDef = { }, color: { type: 'string', minLength: 1, maxLength: 16 }, isSensitive: { type: 'boolean', nullable: true }, + allowRenoteToExternal: { type: 'boolean', nullable: true }, }, required: ['channelId'], } as const; @@ -115,6 +116,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ...(typeof ps.isArchived === 'boolean' ? { isArchived: ps.isArchived } : {}), ...(banner ? { bannerId: banner.id } : {}), ...(typeof ps.isSensitive === 'boolean' ? { isSensitive: ps.isSensitive } : {}), + ...(typeof ps.allowRenoteToExternal === 'boolean' ? { allowRenoteToExternal: ps.allowRenoteToExternal } : {}), }); return await this.channelEntityService.pack(channel.id, me); diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index fb650f69ff..df02d3acb7 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -99,6 +99,12 @@ export const meta = { code: 'NO_SUCH_FILE', id: 'b6992544-63e7-67f0-fa7f-32444b1b5306', }, + + cannotRenoteOutsideOfChannel: { + message: 'Cannot renote outside of channel.', + code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL', + id: '33510210-8452-094c-6227-4a6c05d99f00', + }, }, } as const; @@ -246,6 +252,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- // specified / direct noteはreject throw new ApiError(meta.errors.cannotRenoteDueToVisibility); } + + if (renote.channelId && renote.channelId !== ps.channelId) { + // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック + // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する + const renoteChannel = await this.channelsRepository.findOneById(renote.channelId); + if (renoteChannel == null) { + // リノートしたいノートが書き込まれているチャンネルが無い + throw new ApiError(meta.errors.noSuchChannel); + } else if (!renoteChannel.allowRenoteToExternal) { + // リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合 + throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel); + } + } } let reply: MiNote | null = null; diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index d71b07c51b..30a68f38f2 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -159,7 +159,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; -import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js'; +import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js'; import { useNoteCapture } from '@/scripts/use-note-capture.js'; import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; @@ -275,103 +275,14 @@ if (!props.mock) { }); } -type Visibility = 'public' | 'home' | 'followers' | 'specified'; - -// defaultStore.state.visibilityがstringなためstringも受け付けている -function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility { - if (a === 'specified' || b === 'specified') return 'specified'; - if (a === 'followers' || b === 'followers') return 'followers'; - if (a === 'home' || b === 'home') return 'home'; - // if (a === 'public' || b === 'public') - return 'public'; -} - function renote(viaKeyboard = false) { pleaseLogin(); showMovedDialog(); - let items = [] as MenuItem[]; - - if (appearNote.channel) { - items = items.concat([{ - text: i18n.ts.inChannelRenote, - icon: 'ti ti-repeat', - action: () => { - const el = renoteButton.value as HTMLElement | null | undefined; - if (el) { - const rect = el.getBoundingClientRect(); - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); - } - - if (!props.mock) { - os.api('notes/create', { - renoteId: appearNote.id, - channelId: appearNote.channelId, - }).then(() => { - os.toast(i18n.ts.renoted); - }); - } - }, - }, { - text: i18n.ts.inChannelQuote, - icon: 'ti ti-quote', - action: () => { - if (!props.mock) { - os.post({ - renote: appearNote, - channel: appearNote.channel, - }); - } - }, - }, null]); - } - - items = items.concat([{ - text: i18n.ts.renote, - icon: 'ti ti-repeat', - action: () => { - const el = renoteButton.value as HTMLElement | null | undefined; - if (el) { - const rect = el.getBoundingClientRect(); - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); - } - - const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility; - const localOnly = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly; - - let visibility = appearNote.visibility; - visibility = smallerVisibility(visibility, configuredVisibility); - if (appearNote.channel?.isSensitive) { - visibility = smallerVisibility(visibility, 'home'); - } - - if (!props.mock) { - os.api('notes/create', { - localOnly, - visibility, - renoteId: appearNote.id, - }).then(() => { - os.toast(i18n.ts.renoted); - }); - } - }, - }, (props.mock) ? undefined : { - text: i18n.ts.quote, - icon: 'ti ti-quote', - action: () => { - os.post({ - renote: appearNote, - }); - }, - }]); - - os.popupMenu(items, renoteButton.value, { + const { menu } = getRenoteMenu({ note: note, renoteButton, mock: props.mock }); + os.popupMenu(menu, renoteButton.value, { viaKeyboard, - }); + }).then(focus); } function reply(viaKeyboard = false): void { diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index d34d35a0c3..9e9b1035d7 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -206,7 +206,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; -import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js'; +import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js'; import { useNoteCapture } from '@/scripts/use-note-capture.js'; import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; @@ -325,71 +325,10 @@ function renote(viaKeyboard = false) { pleaseLogin(); showMovedDialog(); - let items = [] as MenuItem[]; - - if (appearNote.channel) { - items = items.concat([{ - text: i18n.ts.inChannelRenote, - icon: 'ti ti-repeat', - action: () => { - const el = renoteButton.value as HTMLElement | null | undefined; - if (el) { - const rect = el.getBoundingClientRect(); - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); - } - - os.api('notes/create', { - renoteId: appearNote.id, - channelId: appearNote.channelId, - }).then(() => { - os.toast(i18n.ts.renoted); - }); - }, - }, { - text: i18n.ts.inChannelQuote, - icon: 'ti ti-quote', - action: () => { - os.post({ - renote: appearNote, - channel: appearNote.channel, - }); - }, - }, null]); - } - - items = items.concat([{ - text: i18n.ts.renote, - icon: 'ti ti-repeat', - action: () => { - const el = renoteButton.value as HTMLElement | null | undefined; - if (el) { - const rect = el.getBoundingClientRect(); - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); - } - - os.api('notes/create', { - renoteId: appearNote.id, - }).then(() => { - os.toast(i18n.ts.renoted); - }); - }, - }, { - text: i18n.ts.quote, - icon: 'ti ti-quote', - action: () => { - os.post({ - renote: appearNote, - }); - }, - }]); - - os.popupMenu(items, renoteButton.value, { + const { menu } = getRenoteMenu({ note: note, renoteButton }); + os.popupMenu(menu, renoteButton.value, { viaKeyboard, - }); + }).then(focus); } function reply(viaKeyboard = false): void { diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index faef8fdb1f..5256ea4f11 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -24,6 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.sensitive }}</template> </MkSwitch> + <MkSwitch v-model="allowRenoteToExternal"> + <template #label>{{ i18n.ts._channel.allowRenoteToExternal }}</template> + </MkSwitch> + <div> <MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ti ti-plus"></i> {{ i18n.ts._channel.setBanner }}</MkButton> <div v-else-if="bannerUrl"> @@ -76,7 +80,7 @@ import { useRouter } from '@/router.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import MkFolder from '@/components/MkFolder.vue'; -import MkSwitch from "@/components/MkSwitch.vue"; +import MkSwitch from '@/components/MkSwitch.vue'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); @@ -93,6 +97,7 @@ let bannerUrl = $ref<string | null>(null); let bannerId = $ref<string | null>(null); let color = $ref('#000'); let isSensitive = $ref(false); +let allowRenoteToExternal = $ref(true); const pinnedNotes = ref([]); watch(() => bannerId, async () => { @@ -121,6 +126,7 @@ async function fetchChannel() { id, })); color = channel.color; + allowRenoteToExternal = channel.allowRenoteToExternal; } fetchChannel(); @@ -150,6 +156,7 @@ function save() { pinnedNoteIds: pinnedNotes.value.map(x => x.id), color: color, isSensitive: isSensitive, + allowRenoteToExternal: allowRenoteToExternal, }; if (props.channelId) { diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index e399145fc9..d0753872ff 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -17,6 +17,7 @@ import { miLocalStorage } from '@/local-storage.js'; import { getUserMenu } from '@/scripts/get-user-menu.js'; import { clipsCache } from '@/cache.js'; import { MenuItem } from '@/types/menu.js'; +import MkRippleEffect from '@/components/MkRippleEffect.vue'; export async function getNoteClipMenu(props: { note: Misskey.entities.Note; @@ -418,3 +419,122 @@ export function getNoteMenu(props: { cleanup, }; } + +type Visibility = 'public' | 'home' | 'followers' | 'specified'; + +// defaultStore.state.visibilityがstringなためstringも受け付けている +function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility { + if (a === 'specified' || b === 'specified') return 'specified'; + if (a === 'followers' || b === 'followers') return 'followers'; + if (a === 'home' || b === 'home') return 'home'; + // if (a === 'public' || b === 'public') + return 'public'; +} + +export function getRenoteMenu(props: { + note: Misskey.entities.Note; + renoteButton: Ref<HTMLElement>; + mock?: boolean; +}) { + const isRenote = ( + props.note.renote != null && + props.note.text == null && + props.note.fileIds.length === 0 && + props.note.poll == null + ); + + const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note; + + const channelRenoteItems: MenuItem[] = []; + const normalRenoteItems: MenuItem[] = []; + + if (appearNote.channel) { + channelRenoteItems.push(...[{ + text: i18n.ts.inChannelRenote, + icon: 'ti ti-repeat', + action: () => { + const el = props.renoteButton.value as HTMLElement | null | undefined; + if (el) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); + } + + if (!props.mock) { + os.api('notes/create', { + renoteId: appearNote.id, + channelId: appearNote.channelId, + }).then(() => { + os.toast(i18n.ts.renoted); + }); + } + }, + }, { + text: i18n.ts.inChannelQuote, + icon: 'ti ti-quote', + action: () => { + if (!props.mock) { + os.post({ + renote: appearNote, + channel: appearNote.channel, + }); + } + }, + }]); + } + + if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) { + normalRenoteItems.push(...[{ + text: i18n.ts.renote, + icon: 'ti ti-repeat', + action: () => { + const el = props.renoteButton.value as HTMLElement | null | undefined; + if (el) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); + } + + const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility; + const localOnly = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly; + + let visibility = appearNote.visibility; + visibility = smallerVisibility(visibility, configuredVisibility); + if (appearNote.channel?.isSensitive) { + visibility = smallerVisibility(visibility, 'home'); + } + + if (!props.mock) { + os.api('notes/create', { + localOnly, + visibility, + renoteId: appearNote.id, + }).then(() => { + os.toast(i18n.ts.renoted); + }); + } + }, + }, (props.mock) ? undefined : { + text: i18n.ts.quote, + icon: 'ti ti-quote', + action: () => { + os.post({ + renote: appearNote, + }); + }, + }]); + } + + // nullを挟むことで区切り線を出せる + const renoteItems = [ + ...normalRenoteItems, + ...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [null] : [], + ...channelRenoteItems, + ]; + + return { + menu: renoteItems, + }; +} diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index a15e5888e8..87922ba791 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -134,6 +134,20 @@ type Blocking = { // @public (undocumented) type Channel = { id: ID; + lastNotedAt: Date | null; + userId: User['id'] | null; + user: User | null; + name: string; + description: string | null; + bannerId: DriveFile['id'] | null; + banner: DriveFile | null; + pinnedNoteIds: string[]; + color: string; + isArchived: boolean; + notesCount: number; + usersCount: number; + isSensitive: boolean; + allowRenoteToExternal: boolean; }; // Warning: (ae-forgotten-export) The symbol "AnyOf" needs to be exported by the entry point index.d.ts @@ -2683,6 +2697,8 @@ type Note = { fileIds: DriveFile['id'][]; visibility: 'public' | 'home' | 'followers' | 'specified'; visibleUserIds?: User['id'][]; + channel?: Channel; + channelId?: Channel['id']; localOnly?: boolean; myReaction?: string; reactions: Record<string, number>; @@ -3021,7 +3037,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts // src/api.types.ts:632:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts // src/entities.ts:116:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts -// src/entities.ts:612:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts +// src/entities.ts:627:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 029bf48c84..a0d0b7528d 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -198,6 +198,8 @@ export type Note = { fileIds: DriveFile['id'][]; visibility: 'public' | 'home' | 'followers' | 'specified'; visibleUserIds?: User['id'][]; + channel?: Channel; + channelId?: Channel['id']; localOnly?: boolean; myReaction?: string; reactions: Record<string, number>; @@ -514,7 +516,20 @@ export type FollowRequest = { export type Channel = { id: ID; - // TODO + lastNotedAt: Date | null; + userId: User['id'] | null; + user: User | null; + name: string; + description: string | null; + bannerId: DriveFile['id'] | null; + banner: DriveFile | null; + pinnedNoteIds: string[]; + color: string; + isArchived: boolean; + notesCount: number; + usersCount: number; + isSensitive: boolean; + allowRenoteToExternal: boolean; }; export type Following = { From 57d72c0db5465d0a6d8b997f1471e3862401b3c9 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Fri, 3 Nov 2023 17:35:55 +0900 Subject: [PATCH 23/60] New Crowdin updates (#12180) * New translations ja-jp.yml (German) * New translations ja-jp.yml (English) * New translations ja-jp.yml (Japanese, Kansai) * New translations ja-jp.yml (Chinese Traditional) * New translations ja-jp.yml (German) * New translations ja-jp.yml (English) * New translations ja-jp.yml (German) * New translations ja-jp.yml (English) * New translations ja-jp.yml (Italian) * New translations ja-jp.yml (French) * New translations ja-jp.yml (French) * New translations ja-jp.yml (French) * New translations ja-jp.yml (French) * New translations ja-jp.yml (Italian) * New translations ja-jp.yml (Chinese Traditional) * New translations ja-jp.yml (Dutch) * New translations ja-jp.yml (Italian) * New translations ja-jp.yml (Italian) * New translations ja-jp.yml (Italian) * New translations ja-jp.yml (German) * New translations ja-jp.yml (Korean) * New translations ja-jp.yml (Chinese Traditional) * New translations ja-jp.yml (English) * New translations ja-jp.yml (Italian) * New translations ja-jp.yml (French) * New translations ja-jp.yml (Chinese Traditional) * New translations ja-jp.yml (German) * New translations ja-jp.yml (English) * New translations ja-jp.yml (Indonesian) * New translations ja-jp.yml (French) * New translations ja-jp.yml (Spanish) * New translations ja-jp.yml (Arabic) * New translations ja-jp.yml (Czech) * New translations ja-jp.yml (German) * New translations ja-jp.yml (Italian) * New translations ja-jp.yml (Korean) * New translations ja-jp.yml (Norwegian) * New translations ja-jp.yml (Portuguese) * New translations ja-jp.yml (Russian) * New translations ja-jp.yml (Chinese Simplified) * New translations ja-jp.yml (Chinese Traditional) * New translations ja-jp.yml (English) * New translations ja-jp.yml (Vietnamese) * New translations ja-jp.yml (Thai) * New translations ja-jp.yml (Japanese, Kansai) --- locales/ar-SA.yml | 3 --- locales/cs-CZ.yml | 11 ----------- locales/de-DE.yml | 23 ++++++++++++----------- locales/en-US.yml | 23 ++++++++++++----------- locales/es-ES.yml | 11 ----------- locales/fr-FR.yml | 48 ++++++++++++++++++++++++++++++++++------------- locales/id-ID.yml | 11 ----------- locales/it-IT.yml | 40 +++++++++++++++++++-------------------- locales/ja-KS.yml | 34 ++++++++++++++++++++++----------- locales/ko-KR.yml | 15 ++++----------- locales/nl-NL.yml | 1 + locales/no-NO.yml | 3 --- locales/pt-PT.yml | 2 -- locales/ru-RU.yml | 10 ---------- locales/th-TH.yml | 11 ----------- locales/vi-VN.yml | 4 ---- locales/zh-CN.yml | 11 ----------- locales/zh-TW.yml | 29 ++++++++++++++-------------- 18 files changed, 122 insertions(+), 168 deletions(-) diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 27f69ad5af..d62990b7b7 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -1262,9 +1262,6 @@ _time: minute: "د" hour: "سا" day: "ي" -_timelineTutorial: - title: "كيف تستخدم Misskey" - step3_1: "هل نشرت ملاحظتك الأولى؟" _2fa: alreadyRegistered: "سجلت سلفًا جهازًا للاستيثاق بعاملين." step1: "أولًا ثبّت تطبيق استيثاق على جهازك (مثل {a} و{b})." diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index f3694af2c5..5d6487d6df 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -1109,7 +1109,6 @@ _initialAccountSetting: pushNotificationDescription: "Povolení push oznámení vám umožní přijímat oznámení od {name} přímo ve vašem zařízení." initialAccountSettingCompleted: "Nastavení profilu dokončeno!" haveFun: "Užívejte {name}!" - ifYouNeedLearnMore: "Pokud se chcete dozvědět více o tom, jak používat {name} (Misskey), navštivte {link}." skipAreYouSure: "Opravdu chcete přeskočit nastavení profilu?" laterAreYouSure: "Opravdu chcete provést nastavení profilu později?" _serverRules: @@ -1658,16 +1657,6 @@ _time: minute: "Minut" hour: "Hodin" day: "Dnů" -_timelineTutorial: - title: "Jak používat Misskey" - step1_1: "Toto je \"časová osa\". Zde se chronologicky zobrazují všechny \"poznámky\" odeslané na {name}." - step1_2: "Existuje několik různých časových plánů. Například \"Domácí časová osa\" bude obsahovat poznámky uživatelů, které sledujete, a \"Místní časová osa\" bude obsahovat poznámky všech uživatelů {name}." - step2_1: "Zkusme zveřejnit poznámku. Můžete tak učinit stisknutím tlačítka s ikonou tužky." - step2_2: "Co takhle napsat sebepředstavení, nebo jen \"Ahoj {name}!\", pokud se vám nechce?" - step3_1: "Dokončil jsi svou první poznámku?" - step3_2: "Na časové ose by se nyní měla zobrazit vaše první poznámka." - step4_1: "K poznámkám můžete také připojit \"Reakce\"." - step4_2: "Chcete-li připojit reakci, stiskněte na poznámce znaménko \"+\" a vyberte emoji, kterým chcete reagovat." _2fa: alreadyRegistered: "Již jste zaregistrovali dvoufaktorové ověřovací zařízení." registerTOTP: "Registrovat aplikaci autentizátoru" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 7dce2332a6..af927586d0 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -979,6 +979,7 @@ assign: "Zuweisen" unassign: "Entfernen" color: "Farbe" manageCustomEmojis: "Kann benutzerdefinierte Emojis verwalten" +manageAvatarDecorations: "Profilbilddekorationen verwalten" youCannotCreateAnymore: "Du hast das Erstellungslimit erreicht." cannotPerformTemporary: "Vorübergehend nicht verfügbar" cannotPerformTemporaryDescription: "Diese Aktion ist wegen des Überschreitenes des Ausführungslimits temporär nicht verfügbar. Bitte versuche es nach einiger Zeit erneut." @@ -1149,6 +1150,12 @@ detach: "Entfernen" angle: "Winkel" flip: "Umdrehen" showAvatarDecorations: "Profilbilddekoration anzeigen" +releaseToRefresh: "Zum Aktualisieren loslassen" +refreshing: "Wird aktualisiert..." +pullDownToRefresh: "Zum Aktualisieren ziehen" +disableStreamingTimeline: "Echtzeitaktualisierung der Chronik deaktivieren" +useGroupedNotifications: "Benachrichtigungen gruppieren" +cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden." _announcement: forExistingUsers: "Nur für existierende Nutzer" forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt." @@ -1170,7 +1177,6 @@ _initialAccountSetting: pushNotificationDescription: "Durch die Aktivierung von Push-Benachrichtigungen kannst du von {name} Benachrichtigungen direkt auf dein Gerät erhalten." initialAccountSettingCompleted: "Kontoeinrichtung abgeschlossen!" haveFun: "Viel Spaß mit {name}!" - ifYouNeedLearnMore: "Besuche {link}, falls du mehr über {name} (Misskey) lernen möchtest." skipAreYouSure: "Die Kontoeinrichtung wirklich überspringen?" laterAreYouSure: "Die Kontoeinrichtung wirklich später erledigen?" _serverRules: @@ -1485,6 +1491,7 @@ _role: inviteLimitCycle: "Zyklus des Einladungslimits" inviteExpirationTime: "Gültigkeitsdauer von Einladungen" canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten" + canManageAvatarDecorations: "Profilbilddekorationen verwalten" driveCapacity: "Drive-Kapazität" alwaysMarkNsfw: "Dateien immer als NSFW markieren" pinMax: "Maximale Anzahl an angehefteten Notizen" @@ -1604,6 +1611,7 @@ _aboutMisskey: donate: "An Misskey spenden" morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter Personen sehr. Danke! 🥰" patrons: "UnterstützerInnen" + projectMembers: "Projektmitglieder" _displayOfSensitiveMedia: respect: "Sensible Medien verbergen" ignore: "Sensible Medien anzeigen" @@ -1735,16 +1743,6 @@ _time: minute: "Minute(n)" hour: "Stunde(n)" day: "Tag(en)" -_timelineTutorial: - title: "Wie du Misskey verwendest" - step1_1: "Dieser Bildschirm ist die \"Chronik\". Hier werden alle \"Notizen\" von {name} angezeigt." - step1_2: "Es gibt einige verschiedene Chroniken. Beispielsweise werden in der \"Startseite\" alle Notizen von Nutzern, denen du folgst, angezeigt, und in der \"Lokalen Chronik\" werden Notizen aller Nutzer auf {name} angezeigt." - step2_1: "Lass uns als nächstes versuchen, eine Notiz zu schreiben. Dies kannst du tun, indem du auf den Knopf mit dem Stift-Icon drückst." - step2_2: "Stell dich den anderen vor oder schreibe einfach \"Hallo {name}!\", wenn du darauf keine Lust hast oder dir nichts einfällt." - step3_1: "Fertig mit dem Senden deiner ersten Notiz?" - step3_2: "Falls deine Notiz nun in deiner Chronik auftaucht, hast du alles richtig gemacht." - step4_1: "Notizen können zusätzlich mit \"Reaktionen\" ausgestattet werden." - step4_2: "Um eine Reaktion anzufügen, klicke auf das „+“-Symbol einer Notiz und wähle ein Emoji aus, mit dem du reagieren möchtest." _2fa: alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung registriert." registerTOTP: "Authentifizierungs-App registrieren" @@ -2054,6 +2052,9 @@ _notification: checkNotificationBehavior: "Aussehen von Benachrichtigungen überprüfen" sendTestNotification: "Testbenachrichtigung senden" notificationWillBeDisplayedLikeThis: "Benachrichtigungen sehen so aus" + reactedBySomeUsers: "{n} Benutzer haben eine Reaktion geschickt" + renotedBySomeUsers: "Renote von {n} Benutzern" + followedBySomeUsers: "Von {n} Benutzern gefolgt" _types: all: "Alle" note: "Neue Notizen" diff --git a/locales/en-US.yml b/locales/en-US.yml index 95e0766058..25c9c38aac 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -979,6 +979,7 @@ assign: "Assign" unassign: "Unassign" color: "Color" manageCustomEmojis: "Manage Custom Emojis" +manageAvatarDecorations: "Manage avatar decorations" youCannotCreateAnymore: "You've hit the creation limit." cannotPerformTemporary: "Temporarily unavailable" cannotPerformTemporaryDescription: "This action cannot be performed temporarily due to exceeding the execution limit. Please wait for a while and then try again." @@ -1149,6 +1150,12 @@ detach: "Remove" angle: "Angle" flip: "Flip" showAvatarDecorations: "Show avatar decorations" +releaseToRefresh: "Release to refresh" +refreshing: "Refreshing..." +pullDownToRefresh: "Pull down to refresh" +disableStreamingTimeline: "Disable real-time timeline updates" +useGroupedNotifications: "Display grouped notifications" +cwNotationRequired: "If \"Hide content\" is enabled, a description must be provided." _announcement: forExistingUsers: "Existing users only" forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it." @@ -1170,7 +1177,6 @@ _initialAccountSetting: pushNotificationDescription: "Enabling push notifications will allow you to receive notifications from {name} directly on your device." initialAccountSettingCompleted: "Profile setup complete!" haveFun: "Enjoy {name}!" - ifYouNeedLearnMore: "If you'd like to learn more about how to use {name} (Misskey), please visit {link}." skipAreYouSure: "Really skip profile setup?" laterAreYouSure: "Really do profile setup later?" _serverRules: @@ -1485,6 +1491,7 @@ _role: inviteLimitCycle: "Invite limit cooldown" inviteExpirationTime: "Invite expiration interval" canManageCustomEmojis: "Can manage custom emojis" + canManageAvatarDecorations: "Manage avatar decorations" driveCapacity: "Drive capacity" alwaysMarkNsfw: "Always mark files as NSFW" pinMax: "Maximum number of pinned notes" @@ -1604,6 +1611,7 @@ _aboutMisskey: donate: "Donate to Misskey" morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰" patrons: "Patrons" + projectMembers: "Project members" _displayOfSensitiveMedia: respect: "Hide media marked as sensitive" ignore: "Display media marked as sensitive" @@ -1735,16 +1743,6 @@ _time: minute: "Minute(s)" hour: "Hour(s)" day: "Day(s)" -_timelineTutorial: - title: "How to use Misskey" - step1_1: "This is the \"timeline\". All \"notes\" submitted on {name} will be chronologically displayed here." - step1_2: "There are a few different timelines. For example, the \"Home timeline\" will contain notes of users you follow, and the \"Local timeline\" will contain notes from all users of {name}." - step2_1: "Let's try posting a note next. You can do so by pressing the button with a pencil icon." - step2_2: "How about writing a self-introduction, or just \"Hello {name}!\" if you don't feel like it?" - step3_1: "Finished posting your first note?" - step3_2: "Your first note should now be displayed on your timeline." - step4_1: "You can also attach \"Reactions\" to notes." - step4_2: "To attach a reaction, press the \"+\" mark on a note and choose an emoji you'd like to react with." _2fa: alreadyRegistered: "You have already registered a 2-factor authentication device." registerTOTP: "Register authenticator app" @@ -2054,6 +2052,9 @@ _notification: checkNotificationBehavior: "Check notification appearance" sendTestNotification: "Send test notification" notificationWillBeDisplayedLikeThis: "Notifications look like this" + reactedBySomeUsers: "{n} users reacted" + renotedBySomeUsers: "Renote from {n} users" + followedBySomeUsers: "Followed by {n} users" _types: all: "All" note: "New notes" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index a32b02e3e6..df611c2350 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -1161,7 +1161,6 @@ _initialAccountSetting: pushNotificationDescription: "Habilitar las notificaciones push te permitirá recibir notificaciones de {name} directamente en tu dispositivo." initialAccountSettingCompleted: "¡Configuración del perfil completada!" haveFun: "¡Disfruta de {name}!" - ifYouNeedLearnMore: "Si quieres aprender cómo usar {name} (Misskey), por favor, visita {link}." skipAreYouSure: "¿Realmente quieres saltarte la configuración del perfil?" laterAreYouSure: "¿Realmente quieres configurar tu perfil después?" _serverRules: @@ -1725,16 +1724,6 @@ _time: minute: "Minutos" hour: "Horas" day: "Días" -_timelineTutorial: - title: "Cómo usar Misskey" - step1_1: "Ésta es la \"línea de tiempo\". Todas las \"notas\" que sean publicadas en {name} serán mostradas cronológicamente aquí." - step1_2: "Hay varias líneas de tiempo. Por ejemplo, la línea temporal \"Inicio\" contiene las notas de otros usuarios que sigues, y la línea \"Local\" contandrá las notas de todos los usuarios de {name}." - step2_1: "Ahora probemos publicar una nota. Puedes hacerlo presionando el botón que tiene un ícono de lápiz." - step2_2: "¿Qué tal si escribimos una introducción? o sólo un \"¡Hola {name}!\" ¿No te apetece?" - step3_1: "¿Terminaste de publicar tu primera nota?" - step3_2: "Tu primera nota ahora se mostrará en tu línea de tiempo." - step4_1: "También puedes añadir \"Reacciones\" a notas." - step4_2: "Para añadir una reacción selecciona el botón \"+\" en la nota y escoge el emoji que quieras para reaccionar." _2fa: alreadyRegistered: "Ya has completado la configuración." registerTOTP: "Registrar aplicación autenticadora" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 02fd7c1e6b..bef2628636 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -693,7 +693,7 @@ repliesCount: "Nombre de réponses envoyées" renotesCount: "Nombre de notes que vous avez renotées" repliedCount: "Nombre de réponses reçues" renotedCount: "Nombre de vos notes renotées" -followingCount: "Nombre de comptes suivis" +followingCount: "Nombre d'abonnements" followersCount: "Nombre d'abonnés" sentReactionsCount: "Nombre de réactions envoyées" receivedReactionsCount: "Nombre de réactions reçues" @@ -781,7 +781,7 @@ addDescription: "Ajouter une description" userPagePinTip: "Vous pouvez afficher des notes ici en sélectionnant l'option « Épingler au profil » dans le menu de chaque note." notSpecifiedMentionWarning: "Vous avez mentionné des utilisateur·rice·s qui ne font pas partie de la liste des destinataires" info: "Informations" -userInfo: "Informations sur l'utilisateur" +userInfo: "Informations sur l'utilisateur·rice" unknown: "Inconnu" onlineStatus: "Statut" hideOnlineStatus: "Se rendre invisible" @@ -970,6 +970,7 @@ assign: "Attribuer" unassign: "Retirer" color: "Couleur" manageCustomEmojis: "Gestion des émojis personnalisés" +manageAvatarDecorations: "Gérer les décorations d'avatar" youCannotCreateAnymore: "Vous avez atteint la limite de création." cannotPerformTemporary: "Temporairement indisponible" invalidParamError: "Paramètres invalides" @@ -1022,12 +1023,12 @@ continue: "Continuer" preservedUsernames: "Noms d'utilisateur·rice réservés" archive: "Archive" displayOfNote: "Affichage de la note" -initialAccountSetting: "Réglage initial du profil" +initialAccountSetting: "Configuration initiale du profil" youFollowing: "Abonné·e" preventAiLearning: "Refuser l'usage dans l'apprentissage automatique d'IA générative" preventAiLearningDescription: "Demander aux robots d'indexation de ne pas utiliser le contenu publié, tel que les notes et les images, dans l'apprentissage automatique d'IA générative. Cela est réalisé en incluant le drapeau « noai » dans la réponse HTML. Une prévention complète n'est toutefois pas possible, car il est au robot d'indexation de respecter cette demande." options: "Options" -specifyUser: "Spécifier l'utilisateur" +specifyUser: "Spécifier l'utilisateur·rice" failedToPreviewUrl: "Aperçu d'URL échoué" update: "Mettre à jour" later: "Plus tard" @@ -1052,7 +1053,11 @@ pinnedList: "Liste épinglée" notifyNotes: "Notifier à propos des nouvelles notes" authentication: "Authentification" authenticationRequiredToContinue: "Veuillez vous authentifier pour continuer" +dateAndTime: "Date et heure" showRenotes: "Afficher les renotes" +edited: "Modifié" +notificationRecieveConfig: "Paramètres des notifications" +mutualFollow: "Abonnement mutuel" showRepliesToOthersInTimeline: "Afficher les réponses aux autres dans le fil" hideRepliesToOthersInTimeline: "Masquer les réponses aux autres dans le fil" showRepliesToOthersInTimelineAll: "Afficher les réponses de toutes les personnes que vous suivez dans le fil" @@ -1072,16 +1077,21 @@ detach: "Enlever" angle: "Angle" flip: "Inverser" showAvatarDecorations: "Afficher les décorations d'avatar" +releaseToRefresh: "Relâcher pour rafraîchir" +refreshing: "Rafraîchissement..." +pullDownToRefresh: "Tirer vers le bas pour rafraîchir" +disableStreamingTimeline: "Désactiver les mises à jour en temps réel de la ligne du temps" +useGroupedNotifications: "Grouper les notifications" _announcement: readConfirmTitle: "Marquer comme lu ?" _initialAccountSetting: profileSetting: "Paramètres du profil" privacySetting: "Paramètres de confidentialité" initialAccountSettingCompleted: "Configuration du profil terminée avec succès !" - ifYouNeedLearnMore: "Si vous voulez en savoir plus comment utiliser {name}(Misskey), veuillez visiter {link}." - skipAreYouSure: "Désirez-vous ignorer la configuration du profile ?" + skipAreYouSure: "Désirez-vous ignorer la configuration du profil ?" _serverSettings: iconUrl: "URL de l’icône" + fanoutTimelineDescription: "Si activée, la performance de la récupération de la chronologie augmentera considérablement et la charge sur la base de données sera réduite. En revanche, l'utilisation de la mémoire de Redis augmentera. Considérez désactiver cette option si le serveur est bas en mémoire ou instable." _accountMigration: moveFrom: "Migrer un autre compte vers le présent compte" moveFromSub: "Créer un alias vers un autre compte" @@ -1148,9 +1158,16 @@ _achievements: description: "Rendre votre compte comme un chat" flavor: "Je n'ai pas encore de nom" _following1: - title: "Vous suivez votre premier utilisateur·rice" + title: "Vous suivez votre premier·ère utilisateur·rice" + _following10: + description: "S'abonner à plus de 10 utilisateur·rice·s" _following50: title: "Beaucoup d'amis" + description: "S'abonner à plus de 50 utilisateur·rice·s" + _following100: + description: "S'abonner à plus de 100 utilisateur·rice·s" + _following300: + description: "S'abonner à plus de 300 utilisateur·rice·s" _followers10: title: "Abonnez-moi !" description: "Obtenir plus de 10 abonné·e·s" @@ -1230,6 +1247,7 @@ _role: high: "Haute" _options: canManageCustomEmojis: "Gestion des émojis personnalisés" + canManageAvatarDecorations: "Gestion des décorations d'avatar" wordMuteMax: "Nombre maximal de caractères dans le filtre de mots" _sensitiveMediaDetection: description: "L'apprentissage automatique peut être utilisé pour détecter automatiquement les médias sensibles à modérer. La sollicitation des serveurs augmente légèrement." @@ -1264,8 +1282,10 @@ _ad: back: "Retour" reduceFrequencyOfThisAd: "Voir cette publicité moins souvent" hide: "Cacher " - adsSettings: "Réglages des publicités" + adsSettings: "Paramètres des publicités" notesPerOneAd: "Intervalle de diffusion de publicités lors de la mise à jour en temps réel (nombre de notes par publicité)" + setZeroToDisable: "Mettre cette valeur à 0 pour désactiver la diffusion de publicités lors de la mise à jour en temps réel" + adsTooClose: "L'expérience de l'utilisateur peut être gravement compromise par un intervalle de diffusion de publicités extrêmement court." _forgotPassword: enterEmail: "Entrez ici l'adresse e-mail que vous avez enregistrée pour votre compte. Un lien vous permettant de réinitialiser votre mot de passe sera envoyé à cette adresse." ifNoEmail: "Si vous n'avez pas enregistré d'adresse e-mail, merci de contacter l'administrateur·rice de votre instance." @@ -1318,6 +1338,7 @@ _aboutMisskey: donate: "Soutenir Misskey" morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes non mentionnées ici. Merci à toutes et à tous ! 🥰" patrons: "Contributeurs" + projectMembers: "Membres du projet" _displayOfSensitiveMedia: force: "Masquer tous les médias" _instanceTicker: @@ -1447,9 +1468,6 @@ _time: minute: "min" hour: "h" day: "j" -_timelineTutorial: - title: "Comment utiliser Misskey" - step3_1: "Avez-vous publié votre première note ?" _2fa: alreadyRegistered: "Configuration déjà achevée." step1: "Tout d'abord, installez une application d'authentification, telle que {a} ou {b}, sur votre appareil." @@ -1613,6 +1631,7 @@ _exportOrImport: userLists: "Listes" excludeMutingUsers: "Exclure les utilisateur·rice·s mis en sourdine" excludeInactiveUsers: "Exclure les utilisateur·rice·s inactifs" + withReplies: "Inclure les réponses des utilisateur·rice·s importé·e·s dans le fil" _charts: federation: "Fédération" apRequest: "Requêtes" @@ -1709,13 +1728,16 @@ _notification: youGotReply: "Réponse de {name}" youGotQuote: "Cité·e par {name}" youRenoted: "{name} vous a Renoté" - youWereFollowed: "Vous suit" + youWereFollowed: "s'est abonné·e à vous" youReceivedFollowRequest: "Vous avez reçu une demande d’abonnement" yourFollowRequestAccepted: "Votre demande d’abonnement a été accepté" pollEnded: "Les résultats du sondage sont disponibles" unreadAntennaNote: "Antenne {name}" emptyPushNotificationMessage: "Les notifications push ont été mises à jour" achievementEarned: "Accomplissement" + reactedBySomeUsers: "{n} utilisateur·rice·s ont réagi" + renotedBySomeUsers: "{n} utilisateur·rice·s ont renoté" + followedBySomeUsers: "{n} utilisateur·rice·s se sont abonné·e·s à vous" _types: all: "Toutes" follow: "Nouvel·le abonné·e" @@ -1774,7 +1796,7 @@ _moderationLogTypes: addCustomEmoji: "Émoji personnalisé ajouté" updateCustomEmoji: "Émoji personnalisé mis à jour" deleteCustomEmoji: "Émoji personnalisé supprimé" - updateServerSettings: "Réglages du serveur mis à jour" + updateServerSettings: "Paramètres du serveur mis à jour" updateUserNote: "Note de modération mise à jour" deleteDriveFile: "Fichier supprimé" deleteNote: "Note supprimée" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index d984ad4c3a..041c55cc2d 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -1161,7 +1161,6 @@ _initialAccountSetting: pushNotificationDescription: "Menyalakan notifikasi dorong akan membuatmu menerima notifikasi dari {name} secara langsung ke perangkatmu." initialAccountSettingCompleted: "Pengaturan profil selesai!" haveFun: "Selamat menikmati, {name}!" - ifYouNeedLearnMore: "Kalau kamu ingin mempelajari lebih lanjut bagaimana cara menggunakan {name} (Misskey), silahkan kunjungi {link}." skipAreYouSure: "Yakin melewati atur profil?" laterAreYouSure: "Yakin banget untuk atur profil nanti?" _serverRules: @@ -1725,16 +1724,6 @@ _time: minute: "menit" hour: "jam" day: "hari" -_timelineTutorial: - title: "Bagaimana cara menggunakan Misskey" - step1_1: "Ini adalah \"lini masa\". Semua \"catatan\" yang dikirimkan oleh {name} akan dimunculkan secara kronologis di sini." - step1_2: "Ada beberapa lini masa yang berbeda. Seperti contoh, \"Lini masa Beranda\" berisi catatan dari pengguna yang kamu ikuti, dan \"Lini masa lokal\" berisi catatan dari semua pengguna dari {name}." - step2_1: "Selanjutnya, mari kita coba memposting sebuah catatan. Kamu dapat melakukanya dengan menekan tombol dengan ikon pensil." - step2_2: "Bagaimana dengan menuliskan sedikit perkenalan diri, atau hanya \"Hello {name}\" kalau kamu lagi ngga feeling?" - step3_1: "Udah selesai memposting catatan pertamamu?" - step3_2: "Catatan pertamamu seharusnya sekarang sudah tampil di lini masa kamu." - step4_1: "Kamu dapat menyisipkan \"Reaksi\" ke dalam catatan." - step4_2: "Untuk menyisipkan reaksi, tekan tanda \"+\" dalam catatan dan pilih emoji yang kamu suka untuk mereaksi catatan tersebut." _2fa: alreadyRegistered: "Kamu telah mendaftarkan perangkat autentikasi 2-faktor." registerTOTP: "Daftarkan aplikasi autentikator" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index be456f7a05..9118bccb20 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -162,8 +162,8 @@ cacheRemoteSensitiveFiles: "Copia nella cache locale i file espliciti remoti" cacheRemoteSensitiveFilesDescription: "Disattivando questa opzione, i file espliciti verranno richiesti direttamente all'istanza remota senza essere salvati nel server locale." flagAsBot: "Io sono un robot" flagAsBotDescription: "Attiva questo campo se il profilo esegue principalmente operazioni automatiche. L'attivazione segnala agli altri sviluppatori come comportarsi per evitare catene d’interazione infinite con altri bot. I sistemi interni di Misskey si adegueranno al fine di trattare questo profilo come bot." -flagAsCat: "Sono un gatto" -flagAsCatDescription: "La modalità \"sono un gatto\" aggiunge le orecchie al tuo profilo" +flagAsCat: "MIIaaaoo!!! (Io sono un gatto è un romanzo del 1905, il primo dello scrittore giapponese Natsume Sōseki)" +flagAsCatDescription: "Miaoo mia miao mi miao?" flagShowTimelineReplies: "Mostra le risposte alle note sulla timeline." flagShowTimelineRepliesDescription: "Attivando, la timeline mostra le Note del profilo ed anche le risposte ad altre Note" autoAcceptFollowed: "Accetta automaticamente le richieste di follow da profili che già segui" @@ -326,9 +326,9 @@ avatar: "Foto del profilo" banner: "Intestazione" displayOfSensitiveMedia: "Visibilità dei media espliciti" whenServerDisconnected: "Quando la connessione col server è persa" -disconnectedFromServer: "Il server si è disconnesso" +disconnectedFromServer: "Connessione persa" reload: "Ricarica" -doNothing: "Nessun'azione" +doNothing: "Ignora" reloadConfirm: "Vuoi ricaricare?" watch: "Osserva" unwatch: "Smetti di Osserva" @@ -653,7 +653,7 @@ notificationSetting: "Impostazioni notifiche" notificationSettingDesc: "Seleziona il tipo di notifiche da visualizzare." useGlobalSetting: "Usa impostazioni generali" useGlobalSettingDesc: "Quando attiva, verranno utilizzate le impostazioni notifiche del profilo. Altrimenti si possono segliere impostazioni personalizzate." -other: "Avanzate" +other: "Ulteriori" regenerateLoginToken: "Genera di nuovo un token di connessione" regenerateLoginTokenDescription: "Genera un nuovo token di autenticazione. Solitamente questa operazione non è necessaria: quando si genera un nuovo token, tutti i dispositivi vanno disconnessi." setMultipleBySeparatingWithSpace: "È possibile creare multiple voci separate da spazi." @@ -979,6 +979,7 @@ assign: "Assegna" unassign: "Disassegna" color: "Colore" manageCustomEmojis: "Gestisci le emoji personalizzate" +manageAvatarDecorations: "Gestire le decorazioni di foto del profilo" youCannotCreateAnymore: "Non puoi creare, hai raggiunto il limite." cannotPerformTemporary: "Indisponibilità temporanea" cannotPerformTemporaryDescription: "L'attività non può essere svolta, poiché si è raggiunto il limite di esecuzioni possibili. Per favore, riprova più tardi." @@ -1149,6 +1150,11 @@ detach: "Rimuovi" angle: "Angolo" flip: "Inverti" showAvatarDecorations: "Mostra decorazione della foto profilo" +releaseToRefresh: "Rilascia per aggiornare" +refreshing: "Aggiornamento..." +pullDownToRefresh: "Trascina per aggiornare" +disableStreamingTimeline: "Disabilitare gli aggiornamenti della TL in tempo reale" +useGroupedNotifications: "Mostra le notifiche raggruppate" _announcement: forExistingUsers: "Solo ai profili attuali" forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio." @@ -1170,7 +1176,6 @@ _initialAccountSetting: pushNotificationDescription: "Attivare le notifiche push ti permettera di ricevere informazioni sulla attività di {name} direttamente sul tuo dispositivo." initialAccountSettingCompleted: "Hai completato la configurazione iniziale!" haveFun: "Divertiti con {name}!" - ifYouNeedLearnMore: "Per saperne di più su come usare {name} (Misskey), visita la pagina {link}" skipAreYouSure: "Vuoi davvero saltare la configurazione iniziale?" laterAreYouSure: "Vuoi davvero rimandare la configurazione iniziale?" _serverRules: @@ -1485,6 +1490,7 @@ _role: inviteLimitCycle: "Intervallo di emissione del codice di invito" inviteExpirationTime: "Scadenza del codice di invito" canManageCustomEmojis: "Gestire le emoji personalizzate" + canManageAvatarDecorations: "Gestisce le decorazioni di immagini del profilo" driveCapacity: "Capienza del Drive" alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)" pinMax: "Quantità massima di Note in primo piano" @@ -1604,6 +1610,7 @@ _aboutMisskey: donate: "Sostieni Misskey" morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰" patrons: "Sostenitori" + projectMembers: "Partecipanti al progetto" _displayOfSensitiveMedia: respect: "Nascondere i media espliciti" ignore: "Non nascondere i media espliciti" @@ -1735,16 +1742,6 @@ _time: minute: "min" hour: "ore" day: "giorni" -_timelineTutorial: - title: "Come usare Misskey" - step1_1: "Questa è la \"Timeline\". tutte le \"Note\" pubblicate su {name} vengono elencate in ordine cronologico." - step1_2: "Le Timeline sono diverse, ad esempio, la \"Home\" elenca le Note dei profili che segui. Quella \"Locale\" elenca quelle di tutti i profili attivi su {name}." - step2_1: "Prova a pubblicare una Nota. Semplicemente premendo il bottone con l'icona di una matita." - step2_2: "Potresti scrivere la tua presentazione, oppure semplicemente \"Ciao da {name}!\"" - step3_1: "Hai pubblicato qualcosa?" - step3_2: "In tal caso, dovrebbe comparire subito nella tua \"Home\"" - step4_1: "Puoi reagire con un emoji alle Note." - step4_2: "To attach a reaction, press the \"+\" mark on a note and choose an emoji you'd like to react with.\nPer reagire con una emoji, premi il bottone \"+\" (più) visibile vicino ad ogni Nota e scegli dall'elenco la emoji che rappresenta la tua reazione." _2fa: alreadyRegistered: "La configurazione è stata già completata." registerTOTP: "Registra un'app di autenticazione" @@ -1844,14 +1841,14 @@ _widgets: calendar: "Calendario" trends: "Di tendenza" clock: "Orologio" - rss: "Aggregatore rss" - rssTicker: "Ticker RSS" + rss: "Lettura RSS" + rssTicker: "Nastro RSS" activity: "Attività" photos: "Foto" digitalClock: "Orologio digitale" unixClock: "Orologio UNIX" federation: "Federazione" - instanceCloud: "Istanza Cloud" + instanceCloud: "Nuvola di federazione" postForm: "Finestra di pubblicazione" slideshow: "Diapositive" button: "Pulsante" @@ -1867,7 +1864,7 @@ _widgets: clicker: "Cliccaggio" _cw: hide: "Nascondere" - show: "Apri..." + show: "Attenzione: continua la lettura" chars: "{count} caratteri" files: "{count} file" _poll: @@ -2054,6 +2051,9 @@ _notification: checkNotificationBehavior: "Prova il comportamento della notifica" sendTestNotification: "Spedisci una notifica di prova" notificationWillBeDisplayedLikeThis: "La notifica apparirà così" + reactedBySomeUsers: "{n} reazioni" + renotedBySomeUsers: "{n} Rinota" + followedBySomeUsers: "{n} nuovi follower" _types: all: "Tutto" note: "Nuove Note" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 9579c07eb7..e93c4b2e34 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -1133,6 +1133,9 @@ fileAttachedOnly: "ファイル付きのみ" showRepliesToOthersInTimeline: "タイムラインに他の人への返信とかも含めんで" hideRepliesToOthersInTimeline: "タイムラインに他の人への返信とかは見ーへんで" showRepliesToOthersInTimelineAll: "" +hideRepliesToOthersInTimelineAll: "" +confirmShowRepliesAll: "" +confirmHideRepliesAll: "" externalServices: "他のサイトのサービス" impressum: "運営者の情報" impressumUrl: "運営者の情報URL" @@ -1141,7 +1144,11 @@ privacyPolicy: "プライバシーポリシー" privacyPolicyUrl: "プライバシーポリシーURL" tosAndPrivacyPolicy: "利用規約・プライバシーポリシー" avatarDecorations: "アイコンデコレーション" +attach: "" +detach: "" +angle: "" flip: "反転" +showAvatarDecorations: "" _announcement: forExistingUsers: "もうおるユーザーのみ" forExistingUsersDescription: "有効にすると、このお知らせ作成時点でおるユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。" @@ -1163,7 +1170,6 @@ _initialAccountSetting: pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をあんたのデバイスで受け取れるで。" initialAccountSettingCompleted: "初期設定が終わったで。" haveFun: "{name}、楽しんでな~" - ifYouNeedLearnMore: "{name}(Misskey)の使い方とかをよー知りたいんやったら{link}をみてな。" skipAreYouSure: "初期設定飛ばすか?" laterAreYouSure: "初期設定あとでやり直すん?" _serverRules: @@ -1177,6 +1183,7 @@ _serverSettings: manifestJsonOverride: "manifest.jsonのオーバーライド" shortName: "略称" shortNameDescription: "サーバーの名前が長い時に、代わりに表示することのできるあだ名。" + fanoutTimelineDescription: "" _accountMigration: moveFrom: "別のアカウントからこのアカウントに引っ越す" moveFromSub: "別のアカウントへエイリアスを作る" @@ -1596,6 +1603,7 @@ _aboutMisskey: donate: "Misskeyに寄付" morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰" patrons: "支援者" + projectMembers: "" _displayOfSensitiveMedia: respect: "きわどいのは見とうない" ignore: "きわどいのも見たい" @@ -1727,16 +1735,6 @@ _time: minute: "分" hour: "時間" day: "日" -_timelineTutorial: - title: "Misskeyってなんや?" - step1_1: "これは「タイムライン」や。{name}に投稿された「ノート」が順番に表示されるで。" - step1_2: "タイムラインには何個か種類があってな、例えば「ホームタイムライン」だったらあんたのフォローしてる人のノート、「ローカルタイムライン」には{name}全部のノートが流れてくるで。" - step2_1: "試しに、何かノートを投稿してみ。画面の鉛筆マークのボタンでフォームが開くで。" - step2_2: "最初のノートは、自己紹介とか「{name}始めてみたんや」とかがええと思うで。" - step3_1: "投稿できた?" - step3_2: "あんたのノートがタイムラインに出てきたら成功や。" - step4_1: "ノートには、「ツッコミ」を付けれるで。" - step4_2: "ツッコむんやったら、ノートの「+」マークを押して、好きな絵文字を選ぶんやで。" _2fa: alreadyRegistered: "もう設定終わっとるわ。" registerTOTP: "認証アプリの設定はじめる" @@ -2169,7 +2167,21 @@ _externalResourceInstaller: _theme: title: "このテーマインストールする?" metaTitle: "テーマ情報" + _meta: + base: "" + _vendorInfo: + title: "" + endpoint: "" + hashVerify: "" _errors: + _invalidParams: + title: "" + description: "" + _resourceTypeNotSupported: + title: "" + description: "" + _failedToFetch: + title: "" _pluginParseFailed: title: "AiScriptエラー起こしてもうたねん" description: "データは取得できたものの、AiScript解析時にエラーがあったから読み込めへんかってん。すまんが、プラグインを作った人に問い合わせてくれへん?ごめんな。エラーの詳細はJavaScriptコンソール読んでな。" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 30481ffc3e..8609bad2e5 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1123,6 +1123,7 @@ edited: "수정됨" notificationRecieveConfig: "알림 설정" mutualFollow: "맞팔로우" flip: "플립" +useGroupedNotifications: "알림을 그룹화하고 표시" _announcement: forExistingUsers: "기존 유저에게만 알림" forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다." @@ -1144,7 +1145,6 @@ _initialAccountSetting: pushNotificationDescription: "푸시 알림을 활성화하면 {name}의 알림을 나의 기기에서 받아볼 수 있게 됩니다." initialAccountSettingCompleted: "초기 설정을 모두 마쳤습니다!" haveFun: "{name}와 함께 즐거운 시간 보내세요!" - ifYouNeedLearnMore: "{name}(Misskey)의 사용 방법에 대해 자세히 알아보려면 {link}를 참고해 주세요." skipAreYouSure: "초기 설정을 중단하시겠습니까?" laterAreYouSure: "초기 설정을 나중에 진행하시겠습니까?" _serverRules: @@ -1699,16 +1699,6 @@ _time: minute: "분" hour: "시간" day: "일" -_timelineTutorial: - title: "Misskey의 사용 방법" - step1_1: "이것은 '타임라인'입니다. {name}에 게시된 '노트'가 시간 순서대로 표시됩니다." - step1_2: "타임라인은 몇 가지 종류로 나뉩니다. 그 중에 '홈 타임라인'은 내가 팔로우한 사람의 노트가 표시되며, '로컬 타임라인'에는 {name} 의 모든 노트가 표시됩니다." - step2_1: "그럼 시험삼아 노트를 작성해 봅시다. 화면에 있는 연필 버튼을 눌러 보세요." - step2_2: "첫 노트이니까 자기소개, 혹은 가볍게 \"안녕 {name}\"라고 올려 보는 건 어떨까요?" - step3_1: "노트 작성을 끝내셨나요?" - step3_2: "당신의 노트가 타임라인에 표시되어 있다면 성공입니다." - step4_1: "노트에는 '리액션'을 붙일 수 있습니다." - step4_2: "리액션을 붙이려면, 노트의 \"+\" 버튼을 클릭하고 원하는 이모지를 선택합니다." _2fa: alreadyRegistered: "이미 설정이 완료되었습니다." registerTOTP: "인증 앱 설정 시작" @@ -2014,6 +2004,9 @@ _notification: checkNotificationBehavior: "알림 표시를 체크하기" sendTestNotification: "테스트 알림 보내기" notificationWillBeDisplayedLikeThis: "알림이 이렇게 표시됩니다" + reactedBySomeUsers: "{n}명이 반응했습니다" + renotedBySomeUsers: "{n}명이 리노트했습니다" + followedBySomeUsers: "{n}명에게 팔로우됨" _types: all: "전부" follow: "팔로잉" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 6f789dff10..1c207d89c8 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -45,6 +45,7 @@ pin: "Vastmaken aan profielpagina" unpin: "Losmaken van profielpagina" copyContent: "Kopiëren inhoud" copyLink: "Kopiëren link" +copyLinkRenote: "" delete: "Verwijderen" deleteAndEdit: "Verwijderen en bewerken" deleteAndEditConfirm: "Weet je zeker dat je deze notitie wilt verwijderen en dan bewerken? Je verliest alle reacties, herdelingen en antwoorden erop." diff --git a/locales/no-NO.yml b/locales/no-NO.yml index d99c61c1dd..44944f8465 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -601,9 +601,6 @@ _time: minute: "Minutter" hour: "Timer" day: "Dager" -_timelineTutorial: - title: "Hvordan bruke Misskey" - step2_2: "Hva med å skrive en selvpresentasjon, eller bare \"Hei {name}!\" hvis du ikke har lyst?" _2fa: renewTOTPCancel: "Avbryt" _weekday: diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index b0604e0425..32740175e3 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -1323,8 +1323,6 @@ _sfx: notification: "Notificações" _ago: invalid: "Não há nada aqui" -_timelineTutorial: - step1_2: "Existem vários tipos de linhas do tempo, por exemplo, na 'Linha do Tempo Principal', você verá as notas das pessoas que está seguindo, e na 'Linha do Tempo Local', verá todas as notas de {name}." _2fa: securityKeyInfo: "Além da autenticação por impressão digital ou PIN, você também pode configurar a autenticação por chaves de segurança de hardware compatível com FIDO2 para proteger ainda mais a sua conta." removeKeyConfirm: "Deseja excluir {name}?" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 606986203f..c0de0bdf59 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -1587,16 +1587,6 @@ _time: minute: "мин" hour: "ч" day: "сут" -_timelineTutorial: - title: "Как пользоваться Misskey" - step1_1: "Это лицо Misskey, так называемая лента. Ваш инстанс, {name}, покажет тут все опубликованные на нём заметки в хронологическом порядке." - step1_2: "Здесь есть несколько лент. К примеру «персональная» лента отображает заметки тех, на кого вы подписаны. А «местная» — заметки тех, кого приютил {name}." - step2_1: "Что ж, теперь самое время опубликовать заметку. Если нажать вверху страницы на изображение карандаша, появится форма для текста." - step2_2: "Почему бы не написать немного о себе? Ну, или хотя бы «Привет, {name}»?" - step3_1: "Справились с первой заметкой?" - step3_2: "Отлично, теперь она должна появиться в вашей ленте." - step4_1: "А ещё здесь можно делиться своими реакциями на заметки." - step4_2: "Отмечайте реакции, нажимая на символ «+» под заметкой и выбирая значок по душе." _2fa: alreadyRegistered: "Двухфакторная аутентификация уже настроена." registerTOTP: "Начните настраивать приложение-аутентификатор" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 1313bb76cb..8df36a6829 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -1154,7 +1154,6 @@ _initialAccountSetting: pushNotificationDescription: "กำลังเปิดใช้งานการแจ้งเตือนแบบพุชจะช่วยให้คุณได้รับการแจ้งเตือนจาก {name} โดยตรงบนอุปกรณ์ของคุณนะ" initialAccountSettingCompleted: "ตั้งค่าโปรไฟล์เสร็จสมบูรณ์แล้ว!" haveFun: "ขอให้สนุก {name}!" - ifYouNeedLearnMore: "ถ้าหากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับวิธีใช้ {ชื่อ} (Misskey) กรุณาไปที่ {link}" skipAreYouSure: "ต้องการข้ามการตั้งค่าโปรไฟล์จริงๆแบบนั้นหรอ?" laterAreYouSure: "ต้องการตั้งค่าโปรไฟล์ในภายหลังจริงๆอย่างงั้นหรอ?" _serverRules: @@ -1713,16 +1712,6 @@ _time: minute: "นาที" hour: "ชั่วโมง" day: "วัน" -_timelineTutorial: - title: "วิธีใช้งาน Misskey" - step1_1: "นี่คือ \"ไทม์ไลน์\" \"โน้ต\" ทั้งหมดที่ส่งใน {name} จะแสดงรายการตามลำดับเวลาที่นี่นะ" - step1_2: "อาจจะมีไทม์ไลน์ที่แตกต่างกันเล็กน้อยยกตัวอย่างเช่น \"ไทม์ไลน์หน้าแรก\" จะมีโน้ตของผู้ใช้ที่คุณติดตามและ \"ไทม์ไลน์ท้องถิ่น\" จะมีโน้ตจากผู้ใช้ทั้งหมดของ {name}" - step2_1: "มาลองโพสต์โน้ตต่อไปกัน คุณสามารถทำได้โดยการกดปุ่มที่มีไอคอนดินสอ" - step2_2: "ยังไงไหนลองเขียนแนะนำตัวเองหรือแค่ \"สวัสดี {name}!\" ถ้าคุณไม่รู้สึกเหมือนมัน?" - step3_1: "เสร็จสิ้นการโพสต์โน้ตย่อแรกของคุณแล้วอย่างงั้นหรอ?" - step3_2: "ไชโย! ตอนนี้โน้ตย่อแรกของคุณได้ปรากฏบนไทม์ไลน์ของคุณแล้วนะ" - step4_1: "คุณสามารถเพิ่ม \"การตอบสนอง\" ในโน้ตได้" - step4_2: "หากต้องการแนบการแสดงความรู้สึก ให้กดเครื่องหมาย \"+\" บนโน้ตแล้วเลือกอิโมจิที่คุณต้องการแสดงความรู้สึกที่ตนเองชอบได้เลย" _2fa: alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว" registerTOTP: "ลงทะเบียนแอพตัวตรวจสอบสิทธิ์" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 7d650e016a..c816fc314b 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1067,7 +1067,6 @@ _initialAccountSetting: pushNotificationDescription: "Bật thông báo đẩy sẽ cho phép bạn nhận thông báo từ {name} trực tiếp từ thiết bị của bạn." initialAccountSettingCompleted: "Thiết lập tài khoản thành công!" haveFun: "Hãy tận hưởng {name} nhé!" - ifYouNeedLearnMore: "Nếu bạn muốn tìm hiểu thêm về cách sử dụng {name} (Misskey), hãy vào {link}." skipAreYouSure: "Bạn thực sự muốn bỏ qua mục thiết lập tài khoản?" laterAreYouSure: "Bạn thực sự muốn thiết lập tài khoản vào lúc khác?" _serverSettings: @@ -1503,9 +1502,6 @@ _time: minute: "phút" hour: "giờ" day: "ngày" -_timelineTutorial: - step4_1: "Bạn có thể thêm \"Reaction\" vào ghi chú" - step4_2: "Khi thêm biểu cảm hãy nhấn dấu \"+\"" _2fa: alreadyRegistered: "Bạn đã đăng ký thiết bị xác minh 2 bước." registerTOTP: "Đăng ký ứng dụng xác thực" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 646fd47f1f..76bc5c3271 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1153,7 +1153,6 @@ _initialAccountSetting: pushNotificationDescription: "启用推送通知的话,就可以在设备上接收来自 {name} 的通知了。" initialAccountSettingCompleted: "初始设定已经完成了!" haveFun: "希望 {name} 在这里玩得开心!" - ifYouNeedLearnMore: "关于 {name}(Misskey) 的使用方法,详见 {link}。" skipAreYouSure: "要跳过初始设置吗?" laterAreYouSure: "要稍后再进行初始设定吗?" _serverRules: @@ -1712,16 +1711,6 @@ _time: minute: "分" hour: "小时" day: "日" -_timelineTutorial: - title: "Misskey 的使用方法" - step1_1: "这个画面是「时间线」。{name}的投稿会按照帖子的发布时间顺序来显示。" - step1_2: "时间线有许多种类,比如在「首页时间线」中展现的是你关注的人的贴文;而在「本地时间线」中展现的是{name}里全部用户的贴文。" - step2_1: "那么接下来,试着写一些什么东西来发布吧!你可以通过点击屏幕上的铅笔图标来打开投稿页面。" - step2_2: "第一次发布的帖子内容,建议包含自我介绍,以及「开始使用{name}了」。" - step3_1: "将想说的话发出去了吗?" - step3_2: "太棒了!现在你可以在你的时间线中看到刚刚发布的帖子了。" - step4_1: "试着对帖子使用「回应」吧!" - step4_2: "在他人的帖子上按下「+」图标,即可选择想要的表情来进行「回应」。" _2fa: alreadyRegistered: "此设备已被注册" registerTOTP: "开始设置认证应用" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index fbbed30a79..11d894900d 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -161,7 +161,7 @@ youCanCleanRemoteFilesCache: "按檔案管理的🗑️按鈕,可將快取全 cacheRemoteSensitiveFiles: "快取遠端的敏感檔案" cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。" flagAsBot: "此使用者是機器人" -flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整Misskey內部系統將本帳戶識別為機器人" +flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整 Misskey 內部系統將本帳戶識別為機器人。" flagAsCat: "此帳戶是一隻貓,喵~~~!!!" flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示" flagShowTimelineReplies: "在時間軸上顯示貼文的回覆" @@ -979,6 +979,7 @@ assign: "指派" unassign: "取消指派" color: "顏色" manageCustomEmojis: "管理自訂表情符號" +manageAvatarDecorations: "管理頭像裝飾" youCannotCreateAnymore: "您無法再建立更多了。" cannotPerformTemporary: "暫時無法進行" cannotPerformTemporaryDescription: "由於超過操作次數限制,因此暫時無法進行。請稍後再嘗試。" @@ -1030,7 +1031,7 @@ retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。" enableChartsForRemoteUser: "生成遠端使用者的圖表" enableChartsForFederatedInstances: "生成遠端伺服器的圖表" showClipButtonInNoteFooter: "新增摘錄至貼文" -reactionsDisplaySize: "表情回應的顯示尺寸" +reactionsDisplaySize: "反應的顯示尺寸" noteIdOrUrl: "貼文ID或URL" video: "影片" videos: "影片" @@ -1125,7 +1126,7 @@ unnotifyNotes: "關閉貼文通知" authentication: "驗證" authenticationRequiredToContinue: "請於繼續前完成驗證" dateAndTime: "日期與時間" -showRenotes: "顯示轉發貼文" +showRenotes: "顯示其他人的轉發貼文" edited: "已編輯" notificationRecieveConfig: "接受通知的設定" mutualFollow: "互相追隨" @@ -1149,6 +1150,12 @@ detach: "取下" angle: "角度" flip: "翻轉" showAvatarDecorations: "顯示頭像裝飾" +releaseToRefresh: "放開以更新內容" +refreshing: "載入更新中" +pullDownToRefresh: "往下拉來更新內容" +disableStreamingTimeline: "停用時間軸的即時更新" +useGroupedNotifications: "分組顯示通知訊息" +cwNotationRequired: "如果開啟「隱藏內容」,則需要註解說明。" _announcement: forExistingUsers: "僅限既有的使用者" forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。" @@ -1170,7 +1177,6 @@ _initialAccountSetting: pushNotificationDescription: "啟用推送通知,就可以在設備上接收{name}的通知。" initialAccountSettingCompleted: "初始設定完成了!" haveFun: "盡情享受{name}吧!" - ifYouNeedLearnMore: "請瀏覽{link}以更瞭解{name}(Misskey)的使用方法。" skipAreYouSure: "要略過初始設定嗎?" laterAreYouSure: "稍後再重新進行初始設定嗎?" _serverRules: @@ -1485,6 +1491,7 @@ _role: inviteLimitCycle: "邀請碼的發放間隔" inviteExpirationTime: "邀請碼的有效日期" canManageCustomEmojis: "管理自訂表情符號" + canManageAvatarDecorations: "管理頭像裝飾" driveCapacity: "雲端硬碟容量" alwaysMarkNsfw: "總是將檔案標記為NSFW" pinMax: "置頂貼文的最大數量" @@ -1604,6 +1611,7 @@ _aboutMisskey: donate: "贊助 Misskey" morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰" patrons: "贊助者" + projectMembers: "專案成員" _displayOfSensitiveMedia: respect: "隱藏敏感檔案" ignore: "顯示敏感檔案" @@ -1735,16 +1743,6 @@ _time: minute: "分鐘" hour: "小時" day: "日" -_timelineTutorial: - title: "Misskey 的使用方法" - step1_1: "這個畫面是「時間軸」。發佈到{name}的「貼文」會按照時間順序顯示。" - step1_2: "時間軸有多種類型,例如「首頁時間軸」是您追蹤帳戶的貼文、「本地時間軸」是{name}內所有帳戶的貼文。" - step2_1: "不如現在就嘗試發文吧!按鉛筆圖示的按鈕開啟發文頁面。" - step2_2: "您可以在第一篇貼文裡寫自我介紹,或是「我來到 {name} 了」之類的話。" - step3_1: "貼文發出去了嗎?" - step3_2: "如果您的貼文出現在時間軸上,就代表發文成功。" - step4_1: "可以對貼文標記「反應」。" - step4_2: "點擊貼文的「+」圖示,即可選擇表情符號來反應。" _2fa: alreadyRegistered: "此裝置已被註冊過了" registerTOTP: "開始設定驗證應用程式" @@ -2054,6 +2052,9 @@ _notification: checkNotificationBehavior: "確認通知的顯示行為" sendTestNotification: "發送測試通知" notificationWillBeDisplayedLikeThis: "通知會以這樣的方式顯示" + reactedBySomeUsers: "{n}人做出了反應" + renotedBySomeUsers: "{n}人做了轉發" + followedBySomeUsers: "被{n}人追隨了" _types: all: "全部 " note: "使用者的最新貼文" From afd3b5d47231f10e400fffca93066edb4c720ab5 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Fri, 3 Nov 2023 17:36:20 +0900 Subject: [PATCH 24/60] 2023.11.0-beta.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4915d64da1..2abd33f6d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2023.11.0-beta.8", + "version": "2023.11.0-beta.9", "codename": "nasubi", "repository": { "type": "git", From 470a1c30e8d30d299a1f48da8a5cdf15c908bc8b Mon Sep 17 00:00:00 2001 From: Caipira <caipira@libnare.net> Date: Fri, 3 Nov 2023 17:38:33 +0900 Subject: [PATCH 25/60] enhance(frontend): federated instance icon with proxy (welcome entrance) (#12213) --- packages/frontend/src/pages/welcome.entrance.a.vue | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue index 998d942488..e1f2a0cbda 100644 --- a/packages/frontend/src/pages/welcome.entrance.a.vue +++ b/packages/frontend/src/pages/welcome.entrance.a.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MarqueeText :duration="40"> <MkA v-for="instance in instances" :key="instance.id" :class="$style.federationInstance" :to="`/instance-info/${instance.host}`" behavior="window"> <!--<MkInstanceCardMini :instance="instance"/>--> - <img v-if="instance.iconUrl" class="icon" :src="instance.iconUrl" alt=""/> + <img v-if="instance.iconUrl" class="icon" :src="getInstanceIcon(instance)" alt=""/> <span class="name _monospace">{{ instance.host }}</span> </MkA> </MarqueeText> @@ -46,10 +46,15 @@ import { instance } from '@/instance.js'; import number from '@/filters/number.js'; import MkNumber from '@/components/MkNumber.vue'; import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue'; +import { getProxiedImageUrl } from '@/scripts/media-proxy.js'; let meta = $ref<Misskey.entities.Instance>(); let instances = $ref<any[]>(); +function getInstanceIcon(instance): string { + return getProxiedImageUrl(instance.iconUrl, 'preview'); +} + os.api('meta', { detail: true }).then(_meta => { meta = _meta; }); From c31d2e256318df04505e4cf22994001d2d4eaf0a Mon Sep 17 00:00:00 2001 From: ozelot <contact@ozelot.dev> Date: Fri, 3 Nov 2023 17:52:31 +0900 Subject: [PATCH 26/60] =?UTF-8?q?=20fix(frontend):=20=E3=82=B5=E3=82=A4?= =?UTF-8?q?=E3=83=AC=E3=83=B3=E3=82=B9=E7=8A=B6=E6=85=8B=E3=81=A7=E5=85=AC?= =?UTF-8?q?=E9=96=8B=E7=AF=84=E5=9B=B2=E3=81=AE=E3=83=91=E3=83=96=E3=83=AA?= =?UTF-8?q?=E3=83=83=E3=82=AF=E3=82=92=E9=81=B8=E6=8A=9E=E3=81=A7=E3=81=8D?= =?UTF-8?q?=E3=81=A6=E3=81=97=E3=81=BE=E3=81=86=E5=95=8F=E9=A1=8C=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#12224)=20(#12225)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): サイレンス状態で公開範囲のパブリックを選択できてしまう問題を修正 (#12224) * docs: update changelog --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> --- CHANGELOG.md | 1 + packages/frontend/src/components/MkPostForm.vue | 5 +++++ packages/frontend/src/components/MkVisibilityPicker.vue | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d096d02e62..c6844ea2f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ - Fix: 個人カードのemojiがバッテリーになっている問題を修正 - Fix: 標準テーマと同じIDを使用してインストールできてしまう問題を修正 - Fix: 絵文字ピッカーでバッテリーの絵文字が複数表示される問題を修正 #12197 +- Fix: サイレンス状態で公開範囲のパブリックを選択できてしまう問題を修正 #12224 - Fix: In deck layout, replies option is not saved after refresh ### Server diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 46faae9523..c0fd1c14d7 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -299,6 +299,10 @@ if (props.reply && props.reply.text != null) { } } +if ($i?.isSilenced && visibility === 'public') { + visibility = 'home'; +} + if (props.channel) { visibility = 'public'; localOnly = true; // TODO: チャンネルが連合するようになった折には消す @@ -448,6 +452,7 @@ function setVisibility() { os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), { currentVisibility: visibility, + isSilenced: $i?.isSilenced, localOnly: localOnly, src: visibilityButton, }, { diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue index 982a69925b..bbb3d3dbf5 100644 --- a/packages/frontend/src/components/MkVisibilityPicker.vue +++ b/packages/frontend/src/components/MkVisibilityPicker.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="[$style.label, $style.item]"> {{ i18n.ts.visibility }} </div> - <button key="public" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')"> + <button key="public" :disabled="isSilenced" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')"> <div :class="$style.icon"><i class="ti ti-world"></i></div> <div :class="$style.body"> <span :class="$style.itemTitle">{{ i18n.ts._visibility.public }}</span> @@ -51,6 +51,7 @@ const modal = $shallowRef<InstanceType<typeof MkModal>>(); const props = withDefaults(defineProps<{ currentVisibility: typeof Misskey.noteVisibilities[number]; + isSilenced: boolean; localOnly: boolean; src?: HTMLElement; }>(), { From 8ddbe914620fc9d28311f9276dcd569c8674294f Mon Sep 17 00:00:00 2001 From: Srgr0 <66754887+Srgr0@users.noreply.github.com> Date: Fri, 3 Nov 2023 17:55:39 +0900 Subject: [PATCH 27/60] =?UTF-8?q?11=E4=BB=A5=E4=B8=8A=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=81=A6=E3=81=84=E3=82=8B=E3=83=AA=E3=82=A2=E3=82=AF=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=81=AB=E3=81=8A=E3=81=84=E3=81=A6=E3=83=84?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E3=83=81=E3=83=83=E3=83=97=E3=81=A7=E7=A4=BA?= =?UTF-8?q?=E3=81=95=E3=82=8C=E3=82=8B=E3=83=AA=E3=82=A2=E3=82=AF=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E6=95=B0=E3=81=8C=E6=9C=AC=E6=9D=A5=E3=82=88?= =?UTF-8?q?=E3=82=8A=E3=82=821=E5=A4=9A=E3=81=84=E5=95=8F=E9=A1=8C?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3=20(#12219)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update MkReactionsViewer.reaction.vue * Update CHANGELOG.md * Update MkReactionsViewer.details.vue --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> --- CHANGELOG.md | 1 + packages/frontend/src/components/MkReactionsViewer.details.vue | 2 +- packages/frontend/src/components/MkReactionsViewer.reaction.vue | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6844ea2f2..29d9ed11e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ - Fix: 個人カードのemojiがバッテリーになっている問題を修正 - Fix: 標準テーマと同じIDを使用してインストールできてしまう問題を修正 - Fix: 絵文字ピッカーでバッテリーの絵文字が複数表示される問題を修正 #12197 +- Fix: 11以上されているリアクションにおいてツールチップで示されるリアクション数が本来よりも1多い問題を修正 #12174 - Fix: サイレンス状態で公開範囲のパブリックを選択できてしまう問題を修正 #12224 - Fix: In deck layout, replies option is not saved after refresh diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue index fdd96d05ae..17cd083561 100644 --- a/packages/frontend/src/components/MkReactionsViewer.details.vue +++ b/packages/frontend/src/components/MkReactionsViewer.details.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAvatar :class="$style.avatar" :user="u"/> <MkUserName :user="u" :nowrap="true"/> </div> - <div v-if="users.length > 10" :class="$style.more">+{{ count - 10 }}</div> + <div v-if="count > 10" :class="$style.more">+{{ count - 10 }}</div> </div> </div> </MkTooltip> diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index d532ef9b66..2b850016c5 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -113,7 +113,7 @@ if (!mock) { const reactions = await os.apiGet('notes/reactions', { noteId: props.note.id, type: props.reaction, - limit: 11, + limit: 10, _cacheKey_: props.count, }); From 8366984b2b81d0f13fb2c705ccf5cc5062bc7d4e Mon Sep 17 00:00:00 2001 From: ikasoba <57828948+ikasoba@users.noreply.github.com> Date: Fri, 3 Nov 2023 19:44:17 +0900 Subject: [PATCH 28/60] =?UTF-8?q?fix:=20URL=E3=83=97=E3=83=AC=E3=83=93?= =?UTF-8?q?=E3=83=A5=E3=83=BC=E3=81=8C=E8=A1=A8=E7=A4=BA=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=81=AA=E3=81=84=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=20(#1222?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * プレビューが表示されないのを修正 * 修正 * Update packages/frontend/src/components/MkUrlPreview.vue --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> --- packages/frontend/src/components/MkUrlPreview.vue | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index e2844f8fa1..e4a6a87c26 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only :style="player.width ? `padding: ${(player.height || 0) / player.width * 100}% 0 0` : `padding: ${(player.height || 0)}px 0 0`" > <iframe - v-if="player.url.startsWith('http://') || player.url.startsWith('https://')" + v-if="player.url?.startsWith('http://') || player.url?.startsWith('https://')" sandbox="allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin" scrolling="no" :allow="player.allow.join(';')" @@ -118,11 +118,12 @@ 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({ +let player = $ref<SummalyResult['player']>({ url: null, width: null, height: null, -} as SummalyResult['player']); + allow: [], +}); let playerEnabled = $ref(false); let tweetId = $ref<string | null>(null); let tweetExpanded = $ref(props.detail); From 4226657aa23ab7c23c7c1f043a943b8123d2008d Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Fri, 3 Nov 2023 19:45:15 +0900 Subject: [PATCH 29/60] Update CHANGELOG.md --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29d9ed11e6..2149a9cca5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,13 +21,13 @@ - 最大でも黄色いエリア内にデコレーションを収めることを推奨します。 - 画像は512x512pxを推奨します。 - Feat: チャンネル設定にリノート/引用リノートの可否を設定できる項目を追加 +- Enhance: アカウント登録時のメールアドレス認証に30分の有効期限を設定 + - 有効期限が切れた後であれば、登録時に使用した招待コードを再度利用できるように変更しました。 + - ユーザーが誤ったメールアドレスを入力した場合に招待コードが失効してしまう問題が解消されます。 - Enhance: すでにフォローしたすべての人の返信をTLに追加できるように - Enhance: 未読の通知数を表示できるように - Enhance: ローカリゼーションの更新 - Enhance: 依存関係の更新 -- Enhance: アカウント登録時のメールアドレス認証に30分の有効期限を設定 - - 有効期限が切れた後であれば、登録時に使用した招待コードを再度利用できるように変更しました。 - - ユーザーが誤ったメールアドレスを入力した場合に招待コードが失効してしまう問題が解消されます。 - Change: CWを使用する場合、注釈を空にすることは許可されなくなりました ### Client @@ -58,6 +58,7 @@ - Fix: 絵文字ピッカーでバッテリーの絵文字が複数表示される問題を修正 #12197 - Fix: 11以上されているリアクションにおいてツールチップで示されるリアクション数が本来よりも1多い問題を修正 #12174 - Fix: サイレンス状態で公開範囲のパブリックを選択できてしまう問題を修正 #12224 +- Fix: URLプレビューが表示されないことがある問題を修正 - Fix: In deck layout, replies option is not saved after refresh ### Server From e893494b48669f9bac309f9824fe28cd6df194fd Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Fri, 3 Nov 2023 19:50:27 +0900 Subject: [PATCH 30/60] =?UTF-8?q?Revert=20"fix:=20URL=E3=83=97=E3=83=AC?= =?UTF-8?q?=E3=83=93=E3=83=A5=E3=83=BC=E3=81=8C=E8=A1=A8=E7=A4=BA=E3=81=95?= =?UTF-8?q?=E3=82=8C=E3=81=AA=E3=81=84=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=20(#12222)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 8366984b2b81d0f13fb2c705ccf5cc5062bc7d4e. --- packages/frontend/src/components/MkUrlPreview.vue | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index e4a6a87c26..e2844f8fa1 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only :style="player.width ? `padding: ${(player.height || 0) / player.width * 100}% 0 0` : `padding: ${(player.height || 0)}px 0 0`" > <iframe - v-if="player.url?.startsWith('http://') || player.url?.startsWith('https://')" + v-if="player.url.startsWith('http://') || player.url.startsWith('https://')" sandbox="allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin" scrolling="no" :allow="player.allow.join(';')" @@ -118,12 +118,11 @@ 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<SummalyResult['player']>({ +let player = $ref({ url: null, width: null, height: null, - allow: [], -}); +} as SummalyResult['player']); let playerEnabled = $ref(false); let tweetId = $ref<string | null>(null); let tweetExpanded = $ref(props.detail); From a8e976d72f08d325510d08c01f3e1317d31d08b9 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Fri, 3 Nov 2023 19:50:35 +0900 Subject: [PATCH 31/60] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2149a9cca5..6f29eaf8d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,7 +58,6 @@ - Fix: 絵文字ピッカーでバッテリーの絵文字が複数表示される問題を修正 #12197 - Fix: 11以上されているリアクションにおいてツールチップで示されるリアクション数が本来よりも1多い問題を修正 #12174 - Fix: サイレンス状態で公開範囲のパブリックを選択できてしまう問題を修正 #12224 -- Fix: URLプレビューが表示されないことがある問題を修正 - Fix: In deck layout, replies option is not saved after refresh ### Server From 5e166101e32b836d16abe8e6f03db3d3b97c4372 Mon Sep 17 00:00:00 2001 From: Mar0xy <marie@kaifa.ch> Date: Fri, 3 Nov 2023 15:43:52 +0100 Subject: [PATCH 32/60] fix: tooltips missing --- packages/frontend/src/components/MkNote.vue | 67 ++++++++++++++++----- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index e267560db1..32894533dd 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -288,23 +288,62 @@ if (props.mock) { }); } -if ($i && !props.mock) { - os.api("notes/renotes", { - noteId: appearNote.id, - userId: $i.id, - limit: 1, - }).then((res) => { - renoted.value = res.length > 0; +if (!props.mock) { + useTooltip(renoteButton, async (showing) => { + const renotes = await os.api('notes/renotes', { + noteId: appearNote.id, + limit: 11, + }); + + const users = renotes.map(x => x.user); + + if (users.length < 1) return; + + os.popup(MkUsersTooltip, { + showing, + users, + count: appearNote.renoteCount, + targetElement: renoteButton.value, + }, {}, 'closed'); }); - os.api("notes/renotes", { - noteId: appearNote.id, - userId: $i.id, - limit: 1, - quote: true, - }).then((res) => { - quoted.value = res.length > 0; + useTooltip(quoteButton, async (showing) => { + const renotes = await os.api('notes/renotes', { + noteId: appearNote.id, + limit: 11, + quote: true, + }); + + const users = renotes.map(x => x.user); + + if (users.length < 1) return; + + os.popup(MkUsersTooltip, { + showing, + users, + count: appearNote.renoteCount, + targetElement: quoteButton.value, + }, {}, 'closed'); }); + + if ($i) { + os.api("notes/renotes", { + noteId: appearNote.id, + userId: $i.id, + limit: 1, + }).then((res) => { + renoted.value = res.length > 0; + }); + + os.api("notes/renotes", { + noteId: appearNote.id, + userId: $i.id, + limit: 1, + quote: true, + }).then((res) => { + quoted.value = res.length > 0; + }); + } } type Visibility = 'public' | 'home' | 'followers' | 'specified'; From 4ec812b3b383df670f5eba47ce5347e6099a9fab Mon Sep 17 00:00:00 2001 From: Mar0xy <marie@kaifa.ch> Date: Fri, 3 Nov 2023 15:55:35 +0100 Subject: [PATCH 33/60] upd: change misskey to sharkey, msky to shonk --- locales/en-US.yml | 2 +- locales/fr-FR.yml | 2 +- locales/ja-JP.yml | 12 ++++++------ locales/th-TH.yml | 2 +- locales/vi-VN.yml | 2 +- .../src/components/MkTutorialDialog.Note.vue | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index a2a2efffde..24479a598f 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1240,7 +1240,7 @@ _achievements: earnedAt: "Unlocked at" _types: _notes1: - title: "just setting up my msky" + title: "just setting up my shonk" description: "Post your first note" flavor: "Have a good time with Sharkey!" _notes10: diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 785e178d70..9fea3df006 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -1101,7 +1101,7 @@ _accountMigration: _achievements: _types: _notes1: - title: "Je viens tout juste de configurer mon msky" + title: "Je viens tout juste de configurer mon shonk" description: "Publiez votre première note" flavor: "Passez un bon moment avec Misskey !" _notes10: diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b9748ce4d4..eba3dd17c2 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1224,10 +1224,10 @@ _initialTutorial: skipAreYouSure: "チュートリアルを終了しますか?" _landing: title: "チュートリアルへようこそ" - description: "ここでは、Misskeyの基本的な使い方や機能を確認できます。" + description: "ここでは、Sharkeyの基本的な使い方や機能を確認できます。" _note: title: "ノートって何?" - description: "Misskeyでの投稿は「ノート」と呼びます。ノートはタイムラインに時系列で並んでいて、リアルタイムで更新されていきます。" + description: "Sharkeyでの投稿は「ノート」と呼びます。ノートはタイムラインに時系列で並んでいて、リアルタイムで更新されていきます。" reply: "返信することができます。返信に対しての返信も可能で、スレッドのように会話を続けることもできます。" renote: "そのノートを自分のタイムラインに流して共有することができます。テキストを追加して引用することも可能です。" reaction: "リアクションをつけることができます。詳しくは次のページで解説します。" @@ -1241,7 +1241,7 @@ _initialTutorial: reactDone: "「ー」ボタンを押すとリアクションを取り消すことができます。" _timeline: title: "タイムラインのしくみ" - description1: "Misskeyには、使い方に応じて複数のタイムラインが用意されています(サーバーによってはいずれかが無効になっていることがあります)。" + description1: "Sharkeyには、使い方に応じて複数のタイムラインが用意されています(サーバーによってはいずれかが無効になっていることがあります)。" home: "あなたがフォローしているアカウントの投稿を見られます。" local: "このサーバーにいるユーザー全員の投稿を見られます。" social: "ホームタイムラインとローカルタイムラインの投稿が両方表示されます。" @@ -1250,7 +1250,7 @@ _initialTutorial: description3: "その他にも、リストタイムラインやチャンネルタイムラインなどがあります。詳しくは{link}をご覧ください。" _postNote: title: "ノートの投稿設定" - description1: "Misskeyにノートを投稿する際には、様々なオプションの設定が可能です。投稿フォームはこのようになっています。" + description1: "Sharkeyにノートを投稿する際には、様々なオプションの設定が可能です。投稿フォームはこのようになっています。" _visibility: description: "ノートを表示できる相手を制限できます。" public: "すべてのユーザーに公開。" @@ -1278,7 +1278,7 @@ _initialTutorial: doItToContinue: "画像をセンシティブに設定すると先に進めるようになります。" _done: title: "チュートリアルは終了です🎉" - description: "ここで紹介した機能はほんの一部にすぎません。Misskeyの使い方をより詳しく知るには、{link}をご覧ください。" + description: "ここで紹介した機能はほんの一部にすぎません。Sharkeyの使い方をより詳しく知るには、{link}をご覧ください。" _timelineDescription: home: "ホームタイムラインでは、あなたがフォローしているアカウントの投稿を見られます。" @@ -1320,7 +1320,7 @@ _achievements: earnedAt: "獲得日時" _types: _notes1: - title: "just setting up my msky" + title: "just setting up my shonk" description: "初めてノートを投稿した" flavor: "良いSharkeyライフを!" _notes10: diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 8df36a6829..efa400eced 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -1183,7 +1183,7 @@ _achievements: earnedAt: "ได้รับเมื่อ" _types: _notes1: - title: "just setting up my msky" + title: "just setting up my shonk" description: "โพสต์โน้ตแรกของคุณ" flavor: "ขอให้มีช่วงเวลาที่ดีกับ Misskey นะคะ!" _notes10: diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index c816fc314b..ac9f55c5de 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1086,7 +1086,7 @@ _achievements: earnedAt: "Ngày thu nhận" _types: _notes1: - title: "just setting up my msky" + title: "just setting up my shonk" description: "Lần đầu tiên đăng bài" flavor: "Chúc bạn trên Miskey vui vẻ nha!!" _notes10: diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue index e16624daad..3fca958055 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Note.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue @@ -56,7 +56,7 @@ const exampleNote = reactive<Misskey.entities.Note>({ onlineStatus: null, badgeRoles: [], }, - text: 'just setting up my msky', + text: 'just setting up my shonk', cw: null, visibility: 'public', localOnly: false, From a656447aa58440e9a41fa39e1c5d6ff3d859b64e Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sat, 4 Nov 2023 08:56:48 +0900 Subject: [PATCH 34/60] enhance(frontend): improve pull to refresh --- .../frontend/src/components/MkPageWindow.vue | 2 +- .../src/components/MkPullToRefresh.vue | 72 +++++++++++-------- packages/frontend/src/ui/universal.vue | 2 +- 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index 5edae1bc3c..1b1eb11444 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -166,7 +166,7 @@ defineExpose({ <style lang="scss" module> .root { - overscroll-behavior: none; + overscroll-behavior: contain; min-height: 100%; background: var(--bg); diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue index f3f5660143..64c4d6e91d 100644 --- a/packages/frontend/src/components/MkPullToRefresh.vue +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -23,9 +23,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onUnmounted } from 'vue'; +import { onMounted, onUnmounted, watch } from 'vue'; import { deviceKind } from '@/scripts/device-kind.js'; import { i18n } from '@/i18n.js'; +import { getScrollContainer } from '@/scripts/scroll.js'; const SCROLL_STOP = 10; const MAX_PULL_DISTANCE = Infinity; @@ -57,18 +58,6 @@ const emit = defineEmits<{ (ev: 'refresh'): void; }>(); -function getScrollableParentElement(node) { - if (node == null) { - return null; - } - - if (node.scrollHeight > node.clientHeight) { - return node; - } else { - return getScrollableParentElement(node.parentNode); - } -} - function getScreenY(event) { if (supportPointerDesktop) { return event.screenY; @@ -138,12 +127,9 @@ function moveEnd() { } } -function moving(event) { +function moving(event: TouchEvent | PointerEvent) { if (!isPullStart || isRefreshing || disabled) return; - if (!scrollEl) { - scrollEl = getScrollableParentElement(rootEl); - } if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance)) { pullDistance = 0; isPullEnd = false; @@ -159,6 +145,10 @@ function moving(event) { const moveHeight = moveScreenY - startScreenY!; pullDistance = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE); + if (pullDistance > 0) { + if (event.cancelable) event.preventDefault(); + } + isPullEnd = pullDistance >= FIRE_THRESHOLD; } @@ -178,24 +168,48 @@ function setDisabled(value) { disabled = value; } -onMounted(() => { - // マウス操作でpull to refreshするのは不便そう - //supportPointerDesktop = !!window.PointerEvent && deviceKind === 'desktop'; +function onScrollContainerScroll() { + const scrollPos = scrollEl!.scrollTop; - if (supportPointerDesktop) { - rootEl.addEventListener('pointerdown', moveStart); - // ポインターの場合、ポップアップ系の動作をするとdownだけ発火されてupが発火されないため - window.addEventListener('pointerup', moveEnd); - rootEl.addEventListener('pointermove', moving, { passive: true }); + // When at the top of the page, disable vertical overscroll so passive touch listeners can take over. + if (scrollPos === 0) { + scrollEl!.style.touchAction = 'pan-x pan-down pinch-zoom'; + registerEventListenersForReadyToPull(); } else { - rootEl.addEventListener('touchstart', moveStart); - rootEl.addEventListener('touchend', moveEnd); - rootEl.addEventListener('touchmove', moving, { passive: true }); + scrollEl!.style.touchAction = 'auto'; + unregisterEventListenersForReadyToPull(); } +} + +function registerEventListenersForReadyToPull() { + if (rootEl == null) return; + rootEl.addEventListener('touchstart', moveStart, { passive: true }); + rootEl.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない +} + +function unregisterEventListenersForReadyToPull() { + if (rootEl == null) return; + rootEl.removeEventListener('touchstart', moveStart); + rootEl.removeEventListener('touchmove', moving); +} + +onMounted(() => { + if (rootEl == null) return; + + scrollEl = getScrollContainer(rootEl); + if (scrollEl == null) return; + + scrollEl.addEventListener('scroll', onScrollContainerScroll, { passive: true }); + + rootEl.addEventListener('touchend', moveEnd, { passive: true }); + + registerEventListenersForReadyToPull(); }); onUnmounted(() => { - if (supportPointerDesktop) window.removeEventListener('pointerup', moveEnd); + if (scrollEl) scrollEl.removeEventListener('scroll', onScrollContainerScroll); + + unregisterEventListenersForReadyToPull(); }); defineExpose({ diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index 86ec8650f9..5ed1e76828 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -324,7 +324,7 @@ $widgets-hide-threshold: 1090px; min-width: 0; overflow: auto; overflow-y: scroll; - overscroll-behavior: none; + overscroll-behavior: contain; background: var(--bg); } From ef8a65e6ff99e86da38e75ef6f3215d7b6b65012 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sat, 4 Nov 2023 10:02:26 +0900 Subject: [PATCH 35/60] Update about-misskey.vue --- packages/frontend/src/pages/about-misskey.vue | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index b446a4d554..9fe7f7f79c 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -73,8 +73,36 @@ SPDX-License-Identifier: AGPL-3.0-only </FormSection> <FormSection> <template #label>{{ i18n.ts._aboutMisskey.contributors }}</template> + <div :class="$style.contributors" style="margin-bottom: 8px;"> + <a href="https://github.com/mei23" target="_blank" :class="$style.contributor"> + <img src="https://avatars.githubusercontent.com/u/30769358?v=4" :class="$style.contributorAvatar"> + <span :class="$style.contributorUsername">@mei23</span> + </a> + <a href="https://github.com/rinsuki" target="_blank" :class="$style.contributor"> + <img src="https://avatars.githubusercontent.com/u/6533808?v=4" :class="$style.contributorAvatar"> + <span :class="$style.contributorUsername">@rinsuki</span> + </a> + <a href="https://github.com/robflop" target="_blank" :class="$style.contributor"> + <img src="https://avatars.githubusercontent.com/u/8159402?v=4" :class="$style.contributorAvatar"> + <span :class="$style.contributorUsername">@robflop</span> + </a> + </div> <MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink> </FormSection> + <FormSection> + <template #label>Special thanks</template> + <div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(130px, 1fr));grid-gap:24px;align-items:center;"> + <div> + <a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img style="width: 100%;" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a> + </div> + <div> + <a style="display: inline-block;" class="xserver" title="XServer" href="https://www.xserver.ne.jp/" target="_blank"><img style="width: 100%;" src="https://misskey-hub.net/sponsors/xserver.png" alt="XServer"></a> + </div> + <div> + <a style="display: inline-block;" class="skeb" title="Skeb" href="https://skeb.jp/" target="_blank"><img style="width: 100%;" src="https://misskey-hub.net/sponsors/skeb.svg" alt="Skeb"></a> + </div> + </div> + </FormSection> <FormSection> <template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template> <div :class="$style.patronsWithIcon"> @@ -88,23 +116,6 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <p>{{ i18n.ts._aboutMisskey.morePatrons }}</p> </FormSection> - <FormSection> - <template #label>Special thanks</template> - <div class="_gaps" style="text-align: center;"> - <div> - <a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a> - </div> - <div> - <a style="display: inline-block;" class="xserver" title="XServer" href="https://www.xserver.ne.jp/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/xserver.png" alt="XServer"></a> - </div> - <div> - <a style="display: inline-block;" class="skeb" title="Skeb" href="https://skeb.jp/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/skeb.svg" alt="Skeb"></a> - </div> - <div> - <a style="display: inline-block;" class="dcadvirth" title="DC Advirth" href="https://www.dotchain.ltd/advirth" target="_blank"><img width="100" src="https://misskey-hub.net/sponsors/dcadvirth.png" alt="DC Advirth"></a> - </div> - </div> - </FormSection> </div> </MkSpacer> </div> From 67414e0181877730d598f76dcc54bc1d10b86bd3 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sat, 4 Nov 2023 10:09:21 +0900 Subject: [PATCH 36/60] =?UTF-8?q?perf(frontend):=20soundConfigStore=20?= =?UTF-8?q?=E3=82=92=20defaultStore=20=E3=81=AB=E7=B5=B1=E5=90=88=E3=81=97?= =?UTF-8?q?API=E3=83=AA=E3=82=AF=E3=82=A8=E3=82=B9=E3=83=88=E3=82=92?= =?UTF-8?q?=E5=89=8A=E6=B8=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/src/components/MkMediaBanner.vue | 1 - .../frontend/src/pages/settings/sounds.vue | 20 ++++---- packages/frontend/src/scripts/sound.ts | 46 ++----------------- packages/frontend/src/store.ts | 25 ++++++++++ 4 files changed, 38 insertions(+), 54 deletions(-) diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue index 69da1a7466..122f8ad794 100644 --- a/packages/frontend/src/components/MkMediaBanner.vue +++ b/packages/frontend/src/components/MkMediaBanner.vue @@ -34,7 +34,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; -import { soundConfigStore } from '@/scripts/sound.js'; import { i18n } from '@/i18n.js'; const props = withDefaults(defineProps<{ diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue index 819e7ffe53..cd1707a594 100644 --- a/packages/frontend/src/pages/settings/sounds.vue +++ b/packages/frontend/src/pages/settings/sounds.vue @@ -32,20 +32,20 @@ import MkRange from '@/components/MkRange.vue'; import MkButton from '@/components/MkButton.vue'; import FormSection from '@/components/form/section.vue'; import MkFolder from '@/components/MkFolder.vue'; -import { soundConfigStore } from '@/scripts/sound.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { defaultStore } from '@/store.js'; -const masterVolume = computed(soundConfigStore.makeGetterSetter('sound_masterVolume')); +const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume')); const soundsKeys = ['note', 'noteMy', 'notification', 'antenna', 'channel'] as const; const sounds = ref<Record<typeof soundsKeys[number], Ref<any>>>({ - note: soundConfigStore.reactiveState.sound_note, - noteMy: soundConfigStore.reactiveState.sound_noteMy, - notification: soundConfigStore.reactiveState.sound_notification, - antenna: soundConfigStore.reactiveState.sound_antenna, - channel: soundConfigStore.reactiveState.sound_channel, + note: defaultStore.reactiveState.sound_note, + noteMy: defaultStore.reactiveState.sound_noteMy, + notification: defaultStore.reactiveState.sound_notification, + antenna: defaultStore.reactiveState.sound_antenna, + channel: defaultStore.reactiveState.sound_channel, }); async function updated(type: keyof typeof sounds.value, sound) { @@ -54,14 +54,14 @@ async function updated(type: keyof typeof sounds.value, sound) { volume: sound.volume, }; - soundConfigStore.set(`sound_${type}`, v); + defaultStore.set(`sound_${type}`, v); sounds.value[type] = v; } function reset() { for (const sound of Object.keys(sounds.value) as Array<keyof typeof sounds.value>) { - const v = soundConfigStore.def[`sound_${sound}`].default; - soundConfigStore.set(`sound_${sound}`, v); + const v = defaultStore.def[`sound_${sound}`].default; + defaultStore.set(`sound_${sound}`, v); sounds.value[sound] = v; } } diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 1ef075818f..f995c122d1 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -3,47 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { markRaw } from 'vue'; -import { Storage } from '@/pizzax.js'; - -export const soundConfigStore = markRaw(new Storage('sound', { - sound_masterVolume: { - where: 'device', - default: 0.3, - }, - sound_note: { - where: 'account', - default: { type: 'syuilo/n-aec', volume: 1 }, - }, - sound_noteMy: { - where: 'account', - default: { type: 'syuilo/n-cea-4va', volume: 1 }, - }, - sound_notification: { - where: 'account', - default: { type: 'syuilo/n-ea', volume: 1 }, - }, - sound_antenna: { - where: 'account', - default: { type: 'syuilo/triple', volume: 1 }, - }, - sound_channel: { - where: 'account', - default: { type: 'syuilo/square-pico', volume: 1 }, - }, -})); - -await soundConfigStore.ready; - -//#region サウンドのColdDeviceStorage => indexedDBのマイグレーション -for (const target of Object.keys(soundConfigStore.state) as Array<keyof typeof soundConfigStore.state>) { - const value = localStorage.getItem(`miux:${target}`); - if (value) { - soundConfigStore.set(target, JSON.parse(value) as typeof soundConfigStore.def[typeof target]['default']); - localStorage.removeItem(`miux:${target}`); - } -} -//#endregion +import { defaultStore } from '@/store.js'; const cache = new Map<string, HTMLAudioElement>(); @@ -112,13 +72,13 @@ export function getAudio(file: string, useCache = true): HTMLAudioElement { } export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement { - const masterVolume = soundConfigStore.state.sound_masterVolume; + const masterVolume = defaultStore.state.sound_masterVolume; audio.volume = masterVolume - ((1 - volume) * masterVolume); return audio; } export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') { - const sound = soundConfigStore.state[`sound_${type}`]; + const sound = defaultStore.state[`sound_${type}`]; if (_DEV_) console.log('play', type, sound); if (sound.type == null) return; playFile(sound.type, sound.volume); diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 7f916656de..6d95ddba35 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -382,6 +382,31 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: true, }, + + sound_masterVolume: { + where: 'device', + default: 0.3, + }, + sound_note: { + where: 'device', + default: { type: 'syuilo/n-aec', volume: 1 }, + }, + sound_noteMy: { + where: 'device', + default: { type: 'syuilo/n-cea-4va', volume: 1 }, + }, + sound_notification: { + where: 'device', + default: { type: 'syuilo/n-ea', volume: 1 }, + }, + sound_antenna: { + where: 'device', + default: { type: 'syuilo/triple', volume: 1 }, + }, + sound_channel: { + where: 'device', + default: { type: 'syuilo/square-pico', volume: 1 }, + }, })); // TODO: 他のタブと永続化されたstateを同期 From 39f731804812d2e325c8b5c7c9077ff9c4835bc4 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sat, 4 Nov 2023 13:58:41 +0900 Subject: [PATCH 37/60] tweak MkPullToRefresh --- packages/frontend/src/components/MkPullToRefresh.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue index 64c4d6e91d..00c1d3808e 100644 --- a/packages/frontend/src/components/MkPullToRefresh.vue +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -32,8 +32,8 @@ const SCROLL_STOP = 10; const MAX_PULL_DISTANCE = Infinity; const FIRE_THRESHOLD = 230; const RELEASE_TRANSITION_DURATION = 200; -const PULL_BRAKE_BASE = 2; -const PULL_BRAKE_FACTOR = 200; +const PULL_BRAKE_BASE = 1.5; +const PULL_BRAKE_FACTOR = 170; let isPullStart = $ref(false); let isPullEnd = $ref(false); From b92b70459264b09ccf8b17fabc992e77e20df0fd Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sat, 4 Nov 2023 14:37:47 +0900 Subject: [PATCH 38/60] fix control panel navigation --- packages/frontend/src/pages/admin/index.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 2bb1e80c18..b304edbf57 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -118,7 +118,7 @@ const menuDef = $computed(() => [{ }, { icon: 'ti ti-sparkles', text: i18n.ts.avatarDecorations, - to: '/avatar-decorations', + to: '/admin/avatar-decorations', active: currentPage?.route.name === 'avatarDecorations', }, { icon: 'ti ti-whirl', From e88a9702d06ff0d3fe4d8fc3099136d5a1b4695d Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sat, 4 Nov 2023 14:41:01 +0900 Subject: [PATCH 39/60] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f29eaf8d5..0ad55752b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ - Fix: 11以上されているリアクションにおいてツールチップで示されるリアクション数が本来よりも1多い問題を修正 #12174 - Fix: サイレンス状態で公開範囲のパブリックを選択できてしまう問題を修正 #12224 - Fix: In deck layout, replies option is not saved after refresh +- Note: アップデート後、サウンドに関する設定が初期化されます ### Server - Feat: Registry APIがサードパーティから利用可能になりました From ca1cda0db0309804c282473939538e2a3d9e940f Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sat, 4 Nov 2023 16:44:14 +0900 Subject: [PATCH 40/60] enhance(frontend): tweak settings page --- packages/frontend/src/pages/settings/general.vue | 4 +--- packages/frontend/src/pages/settings/other.vue | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index d96c984688..06d3789829 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -29,7 +29,6 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_s"> <MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch> <MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch> - <MkSwitch v-model="defaultWithReplies">{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</MkSwitch> <MkFolder> <template #label>{{ i18n.ts.pinnedList }}</template> <!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ --> @@ -51,7 +50,6 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch> <MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch> <MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch> - <MkSwitch v-model="useReactionPickerForContextMenu">{{ i18n.ts.useReactionPickerForContextMenu }}</MkSwitch> <MkRadios v-model="reactionsDisplaySize"> <template #label>{{ i18n.ts.reactionsDisplaySize }}</template> <option value="small">{{ i18n.ts.small }}</option> @@ -151,6 +149,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <div class="_gaps_s"> <MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch> + <MkSwitch v-model="useReactionPickerForContextMenu">{{ i18n.ts.useReactionPickerForContextMenu }}</MkSwitch> <MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch> <MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch> <MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch> @@ -255,7 +254,6 @@ const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter(' const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition')); const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis')); const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn')); -const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies')); const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline')); const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications')); diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index 43a8632130..36666b9c20 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -76,6 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection> <div class="_gaps_s"> + <MkSwitch v-model="defaultWithReplies">{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</MkSwitch> <MkButton danger @click="updateRepliesAll(true)"><i class="ti ti-messages"></i> {{ i18n.ts.showRepliesToOthersInTimelineAll }}</MkButton> <MkButton danger @click="updateRepliesAll(false)"><i class="ti ti-messages-off"></i> {{ i18n.ts.hideRepliesToOthersInTimelineAll }}</MkButton> </div> @@ -102,6 +103,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')); +const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies')); function onChangeInjectFeaturedNote(v) { os.api('i/update', { From 5e9f6a90df9c999d36283d2ba9eb8e23ccfd7d7b Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sat, 4 Nov 2023 18:27:22 +0900 Subject: [PATCH 41/60] =?UTF-8?q?enhance(frontend):=20=E3=83=8E=E3=83=BC?= =?UTF-8?q?=E3=83=88=E5=86=85=E3=81=AE=E3=82=AB=E3=82=B9=E3=82=BF=E3=83=A0?= =?UTF-8?q?=E7=B5=B5=E6=96=87=E5=AD=97=E3=82=92=E3=82=AF=E3=83=AA=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=81=99=E3=82=8B=E3=81=93=E3=81=A8=E3=81=A7=E3=80=81?= =?UTF-8?q?=E3=82=B3=E3=83=94=E3=83=BC=E3=81=8A=E3=82=88=E3=81=B3=E3=83=AA?= =?UTF-8?q?=E3=82=A2=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=8C=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + locales/index.d.ts | 1 + locales/ja-JP.yml | 1 + packages/frontend/src/components/MkNote.vue | 22 ++++++++-- .../src/components/MkNoteDetailed.vue | 6 +-- .../frontend/src/components/MkNoteSimple.vue | 2 +- .../frontend/src/components/MkNoteSub.vue | 2 +- .../src/components/MkSubNoteContent.vue | 2 +- .../frontend/src/components/MkUserInfo.vue | 2 +- .../frontend/src/components/MkUserPopup.vue | 2 +- .../src/components/MkUserSetupDialog.User.vue | 2 +- .../src/components/global/MkCustomEmoji.vue | 43 ++++++++++++++++++- .../global/MkMisskeyFlavoredMarkdown.ts | 5 ++- .../src/components/page/page.text.vue | 2 +- packages/frontend/src/pages/channel.vue | 2 +- packages/frontend/src/pages/clip.vue | 2 +- packages/frontend/src/pages/user/home.vue | 4 +- .../frontend/src/pages/welcome.timeline.vue | 2 +- 18 files changed, 82 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ad55752b1..4038755124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ - Enhance: プラグインで`Plugin:register_note_view_interruptor`を用いてnoteの代わりにnullを返却することでノートを非表示にできるようになりました - Enhance: AiScript関数`Mk:nyaize()`が追加されました - Enhance: 情報→ツール はナビゲーションバーにツールとして独立した項目になりました +- Enhance: ノート内のカスタム絵文字をクリックすることで、コピーおよびリアクションができるように - Enhance: その他細かなブラッシュアップ - Fix: 投稿フォームでのユーザー変更がプレビューに反映されない問題を修正 - Fix: ユーザーページの ノート > ファイル付き タブにリプライが表示されてしまう diff --git a/locales/index.d.ts b/locales/index.d.ts index 50b11acc06..aedaaa9f7c 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1160,6 +1160,7 @@ export interface Locale { "useGroupedNotifications": string; "signupPendingError": string; "cwNotationRequired": string; + "doReaction": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index de4e8ce2b3..6ecebfc393 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1157,6 +1157,7 @@ disableStreamingTimeline: "タイムラインのリアルタイム更新を無 useGroupedNotifications: "通知をグルーピングして表示する" signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。" cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。" +doReaction: "リアクションする" _announcement: forExistingUsers: "既存ユーザーのみ" diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 30a68f38f2..0ae3423a21 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -53,19 +53,28 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> <div style="container-type: inline-size;"> <p v-if="appearNote.cw != null" :class="$style.cw"> - <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'" :i="$i"/> + <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/> <MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;"/> </p> <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"> <div :class="$style.text"> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="appearNote.text" :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/> + <Mfm + v-if="appearNote.text" + :parsedNodes="parsed" + :text="appearNote.text" + :author="appearNote.user" + :nyaize="'account'" + :emojiUrls="appearNote.emojis" + :enableEmojiMenu="true" + :enableEmojiMenuReaction="true" + /> <div v-if="translating || translation" :class="$style.translation"> <MkLoading v-if="translating" mini/> <div v-else> <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> - <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/> + <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/> </div> </div> </div> @@ -242,6 +251,13 @@ const keymap = { 's': () => showContent.value !== showContent.value, }; +provide('react', (reaction: string) => { + os.api('notes/reactions/create', { + noteId: appearNote.id, + reaction: reaction, + }); +}); + if (props.mock) { watch(() => props.note, (to) => { note = deepClone(to); diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 9e9b1035d7..ff2941344e 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -67,19 +67,19 @@ SPDX-License-Identifier: AGPL-3.0-only </header> <div :class="$style.noteContent"> <p v-if="appearNote.cw != null" :class="$style.cw"> - <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'" :i="$i"/> + <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/> <MkCwButton v-model="showContent" :note="appearNote"/> </p> <div v-show="appearNote.cw == null || showContent"> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="appearNote.text" :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/> + <Mfm v-if="appearNote.text" :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/> <a v-if="appearNote.renote != null" :class="$style.rn">RN:</a> <div v-if="translating || translation" :class="$style.translation"> <MkLoading v-if="translating" mini/> <div v-else> <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> - <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :i="$i" :emojiUrls="appearNote.emojis"/> + <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/> </div> </div> <div v-if="appearNote.files.length > 0"> diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index dc401a7ecb..28b00af246 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkNoteHeader :class="$style.header" :note="note" :mini="true"/> <div> <p v-if="note.cw != null" :class="$style.cw"> - <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'" :i="$i" :emojiUrls="note.emojis"/> + <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'" :emojiUrls="note.emojis"/> <MkCwButton v-model="showContent" :note="note"/> </p> <div v-show="note.cw == null || showContent"> diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index 3cc8767007..f61b963d9b 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkNoteHeader :class="$style.header" :note="note" :mini="true"/> <div> <p v-if="note.cw != null" :class="$style.cw"> - <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'" :i="$i"/> + <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'"/> <MkCwButton v-model="showContent" :note="note"/> </p> <div v-show="note.cw == null || showContent"> diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index 51dabee161..e9f2b838d2 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span> <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :emojiUrls="note.emojis"/> + <Mfm v-if="note.text" :text="note.text" :author="note.user" :emojiUrls="note.emojis"/> <MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> </div> <details v-if="note.files.length > 0"> diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue index c13ef60f3b..eaebbf03e7 100644 --- a/packages/frontend/src/components/MkUserInfo.vue +++ b/packages/frontend/src/components/MkUserInfo.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="$i && $i.id !== user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span> <div :class="$style.description"> <div v-if="user.description" :class="$style.mfm"> - <Mfm :text="user.description" :author="user" :i="$i"/> + <Mfm :text="user.description" :author="user"/> </div> <span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span> </div> diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index bcba4196b5..20eb9b3e93 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.username"><MkAcct :user="user"/></div> </div> <div :class="$style.description"> - <Mfm v-if="user.description" :class="$style.mfm" :text="user.description" :author="user" :i="$i"/> + <Mfm v-if="user.description" :class="$style.mfm" :text="user.description" :author="user"/> <div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div> </div> <div :class="$style.status"> diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue index 746781d71f..4fbaf75454 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.User.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div :class="$style.description"> <div v-if="user.description" :class="$style.mfm"> - <Mfm :text="user.description" :author="user" :i="$i"/> + <Mfm :text="user.description" :author="user"/> </div> <span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span> </div> diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index 063b122f8b..1e17bab849 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -5,14 +5,27 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <span v-if="errored">:{{ customEmojiName }}:</span> -<img v-else :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async" @error="errored = true" @load="errored = false"/> +<img + v-else + :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" + :src="url" + :alt="alt" + :title="alt" + decoding="async" + @error="errored = true" + @load="errored = false" + @click="onClick" +/> </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, inject } from 'vue'; import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js'; import { defaultStore } from '@/store.js'; import { customEmojisMap } from '@/custom-emojis.js'; +import * as os from '@/os.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { i18n } from '@/i18n.js'; const props = defineProps<{ name: string; @@ -21,8 +34,12 @@ const props = defineProps<{ host?: string | null; url?: string; useOriginalSize?: boolean; + menu?: boolean; + menuReaction?: boolean; }>(); +const react = inject<((name: string) => void) | null>('react', null); + const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', '')); const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@'))); @@ -55,6 +72,28 @@ const url = computed(() => { const alt = computed(() => `:${customEmojiName.value}:`); let errored = $ref(url.value == null); + +function onClick(ev: MouseEvent) { + if (props.menu) { + os.popupMenu([{ + type: 'label', + text: `:${props.name}:`, + }, { + text: i18n.ts.copy, + icon: 'ti ti-copy', + action: () => { + copyToClipboard(`:${props.name}:`); + os.success(); + }, + }, ...(props.menuReaction && react ? [{ + text: i18n.ts.doReaction, + icon: 'ti ti-plus', + action: () => { + react(`:${props.name}:`); + }, + }] : [])], ev.currentTarget ?? ev.target); + } +} </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index ab8a342691..d7e1490502 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -33,12 +33,13 @@ type MfmProps = { plain?: boolean; nowrap?: boolean; author?: Misskey.entities.UserLite; - i?: Misskey.entities.UserLite | null; isNote?: boolean; emojiUrls?: string[]; rootScale?: number; nyaize: boolean | 'account'; parsedNodes?: mfm.MfmNode[] | null; + enableEmojiMenu?: boolean; + enableEmojiMenuReaction?: boolean; }; // eslint-disable-next-line import/no-default-export @@ -328,6 +329,8 @@ export default function(props: MfmProps) { normal: props.plain, host: null, useOriginalSize: scale >= 2.5, + menu: props.enableEmojiMenu, + menuReaction: props.enableEmojiMenuReaction, })]; } else { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue index 35021be95e..e0f1a4af90 100644 --- a/packages/frontend/src/components/page/page.text.vue +++ b/packages/frontend/src/components/page/page.text.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps"> - <Mfm :text="block.text" :isNote="false" :i="$i"/> + <Mfm :text="block.text" :isNote="false"/> <MkUrlPreview v-for="url in urls" :key="url" :url="url"/> </div> </template> diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 911f4e95d2..1d41fe7529 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.bannerFade"></div> </div> <div v-if="channel.description" :class="$style.description"> - <Mfm :text="channel.description" :isNote="false" :i="$i"/> + <Mfm :text="channel.description" :isNote="false"/> </div> </div> diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 80b94acb6b..4573bbb81c 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="clip" class="_gaps"> <div class="_panel"> <div v-if="clip.description" :class="$style.description"> - <Mfm :text="clip.description" :isNote="false" :i="$i"/> + <Mfm :text="clip.description" :isNote="false"/> </div> <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton> <MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton> diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 4c425898d5..7ff490bf8b 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="description"> <MkOmit> - <Mfm v-if="user.description" :text="user.description" :isNote="false" :author="user" :i="$i"/> + <Mfm v-if="user.description" :text="user.description" :isNote="false" :author="user"/> <p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p> </MkOmit> </div> @@ -100,7 +100,7 @@ SPDX-License-Identifier: AGPL-3.0-only <Mfm :text="field.name" :plain="true" :colored="false"/> </dt> <dd class="value"> - <Mfm :text="field.value" :author="user" :i="$i" :colored="false"/> + <Mfm :text="field.value" :author="user" :colored="false"/> <i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ti ti-circle-check" :class="$style.verifiedLink"></i> </dd> </dl> diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue index f2e151468a..8e2192074d 100644 --- a/packages/frontend/src/pages/welcome.timeline.vue +++ b/packages/frontend/src/pages/welcome.timeline.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_panel" :class="$style.content"> <div> <MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i"/> + <Mfm v-if="note.text" :text="note.text" :author="note.user"/> <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> </div> <div v-if="note.files.length > 0" :class="$style.richcontent"> From 3642a2b6250a61e59ad5e0fa8e5d4e5284ac0647 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sat, 4 Nov 2023 18:45:34 +0900 Subject: [PATCH 42/60] New Crowdin updates (#12231) * New translations ja-jp.yml (Chinese Traditional) * New translations ja-jp.yml (Chinese Traditional) * New translations ja-jp.yml (Chinese Traditional) * New translations ja-jp.yml (Chinese Traditional) * New translations ja-jp.yml (Italian) * New translations ja-jp.yml (Italian) * New translations ja-jp.yml (Italian) * New translations ja-jp.yml (Italian) * New translations ja-jp.yml (Chinese Traditional) * New translations ja-jp.yml (Korean) * New translations ja-jp.yml (English) --- locales/en-US.yml | 24 +++++++++++++ locales/it-IT.yml | 85 ++++++++++++++++++++++++++++++++++++++++++++--- locales/ko-KR.yml | 34 +++++++++++++++++++ locales/zh-TW.yml | 78 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 216 insertions(+), 5 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index 25c9c38aac..40a5f7b14f 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1155,6 +1155,7 @@ refreshing: "Refreshing..." pullDownToRefresh: "Pull down to refresh" disableStreamingTimeline: "Disable real-time timeline updates" useGroupedNotifications: "Display grouped notifications" +signupPendingError: "There was a problem verifying the email address. The link may have expired." cwNotationRequired: "If \"Hide content\" is enabled, a description must be provided." _announcement: forExistingUsers: "Existing users only" @@ -1165,6 +1166,8 @@ _announcement: tooManyActiveAnnouncementDescription: "Having too many active announcements may worsen the user experience. Please consider archiving announcements that have become obsolete." readConfirmTitle: "Mark as read?" readConfirmText: "This will mark the contents of \"{title}\" as read." + shouldNotBeUsedToPresentPermanentInfo: "As it may significantly impact the user experience for new users, it is recommended to use notifications in the flow information rather than stock information." + dialogAnnouncementUxWarn: "Having two or more dialog-style notifications simultaneously can significantly impact the user experience, so please use them carefully." _initialAccountSetting: accountCreated: "Your account was successfully created!" letsStartAccountSetup: "For starters, let's set up your profile." @@ -1177,8 +1180,29 @@ _initialAccountSetting: pushNotificationDescription: "Enabling push notifications will allow you to receive notifications from {name} directly on your device." initialAccountSettingCompleted: "Profile setup complete!" haveFun: "Enjoy {name}!" + youCanContinueTutorial: "You can proceed to a tutorial on how to use {name} (Misskey) or you can exit the setup here and start using it immediately." + startTutorial: "Start Tutorial" skipAreYouSure: "Really skip profile setup?" laterAreYouSure: "Really do profile setup later?" +_initialTutorial: + launchTutorial: "Start Tutorial" + title: "Tutorial" + wellDone: "Well done!" + skipAreYouSure: "Quit Tutorial?" + _landing: + title: "Welcome to the Tutorial" + description: "Here, you can learn the basics of using Misskey and its features." + _note: + title: "What is a Note?" + description: "Posts on Misskey are called 'Notes.' Notes are arranged chronologically on the timeline and are updated in real-time." + reply: "Click on this button to reply to a message. It's also possible to reply to replies, continuing the conversation like a thread." + renote: "You can share that note to your own timeline. You can also quote them with your comments." + reaction: "You can add reactions to the Note. More details will be explained on the next page." + menu: "You can view Note details, copy links, and perform various other actions." + _reaction: + title: "What are Reactions?" + description: "Notes can be reacted to with various emojis. Reactions allow you to express nuances that may not be conveyed with just a 'like.'" + letsTryReacting: "Reactions can be added by clicking the '+' button on the note. Try reacting to this sample note!" _serverRules: description: "A set of rules to be displayed before registration. Setting a summary of the Terms of Service is recommended." _serverSettings: diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 9118bccb20..b474dbda2c 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -590,7 +590,7 @@ invisibleNote: "Nota invisibile" enableInfiniteScroll: "Abilita scorrimento infinito" visibility: "Visibilità" poll: "Sondaggio" -useCw: "Content Warning" +useCw: "Contenuto esplicito" enablePlayer: "Visualizza" disablePlayer: "Chiudi" expandTweet: "Espandi tweet" @@ -808,8 +808,8 @@ user: "Profilo" administration: "Gestione" accounts: "Profilo" switch: "Cambia" -noMaintainerInformationWarning: "Le informazioni amministratore non sono impostate." -noBotProtectionWarning: "Nessuna protezione impostata contro i bot." +noMaintainerInformationWarning: "Mancano le informazioni sull'amministratore." +noBotProtectionWarning: "Non è stata impostata alcuna protezione dai Bot" configure: "Imposta" postToGallery: "Pubblicare nella galleria" postToHashtag: "Pubblica a questo hashtag" @@ -847,7 +847,7 @@ accountDeletionInProgress: "È in corso l'eliminazione del profilo" usernameInfo: "Un nome per identificare univocamente il tuo profilo sull'istanza. Puoi utilizzare caratteri alfanumerici maiuscoli, minuscoli e il trattino basso (_). Non potrai cambiare nome utente in seguito." aiChanMode: "Modalità Ai" devMode: "Modalità sviluppatori" -keepCw: "Mantieni il Content Warning" +keepCw: "Mostra i contenuti espliciti" pubSub: "Publish/Subscribe del profilo" lastCommunication: "La comunicazione più recente" resolved: "Risolto" @@ -1155,6 +1155,8 @@ refreshing: "Aggiornamento..." pullDownToRefresh: "Trascina per aggiornare" disableStreamingTimeline: "Disabilitare gli aggiornamenti della TL in tempo reale" useGroupedNotifications: "Mostra le notifiche raggruppate" +signupPendingError: "Si è verificato un problema durante la verifica del tuo indirizzo email. Potrebbe essere scaduto il collegamento temporaneo." +cwNotationRequired: "Devi indicare perché il contenuto è indicato come esplicito." _announcement: forExistingUsers: "Solo ai profili attuali" forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio." @@ -1164,6 +1166,8 @@ _announcement: tooManyActiveAnnouncementDescription: "L'esperienza delle persone può peggiorare se ci sono troppi annunci attivi. Considera anche l'archiviazione degli annunci conclusi." readConfirmTitle: "Segnare come già letto?" readConfirmText: "Hai già letto \"{title}˝?" + shouldNotBeUsedToPresentPermanentInfo: "Ti consigliamo di utilizzare gli annunci per pubblicare informazioni tempestive e limitate nel tempo, anziché informazioni importanti a lungo andare nel tempo, poiché potrebbero risultare difficili da ritrovare e peggiorare la fruibilità del servizio, specialmente alle nuove persone iscritte." + dialogAnnouncementUxWarn: "Ti consigliamo di usarli con cautela, poiché è molto probabile che avere più di un annuncio in stile \"finestra di dialogo\" peggiori sensibilmente la fruibilità del servizio, specialmente alle nuove persone iscritte." _initialAccountSetting: accountCreated: "Il tuo profilo è stato creato!" letsStartAccountSetup: "Per iniziare, impostiamo il tuo profilo." @@ -1176,8 +1180,77 @@ _initialAccountSetting: pushNotificationDescription: "Attivare le notifiche push ti permettera di ricevere informazioni sulla attività di {name} direttamente sul tuo dispositivo." initialAccountSettingCompleted: "Hai completato la configurazione iniziale!" haveFun: "Divertiti con {name}!" + youCanContinueTutorial: "Puoi continuare con l'esercitazione su come usare {name} (Misskey), oppure interrompere, iniziando subito a usarlo." + startTutorial: "Avvia l'esercitazione" skipAreYouSure: "Vuoi davvero saltare la configurazione iniziale?" laterAreYouSure: "Vuoi davvero rimandare la configurazione iniziale?" +_initialTutorial: + launchTutorial: "Guarda il tutorial" + title: "Tutorial" + wellDone: "Ottimo lavoro!" + skipAreYouSure: "Vuoi davvero interrompere il tutorial?" + _landing: + title: "Eccoci nel tutorial" + description: "Qui puoi verificare l'uso delle funzionalità base di Misskey." + _note: + title: "Cosa sono le Note?" + description: "Gli status su Misskey sono chiamati \"Note\". Le Note sono elencate in ordine cronologico nelle timeline e vengono aggiornate in tempo reale." + reply: "Puoi rispondere alle Note. Puoi anche rispondere alle risposte e continuare i dialoghi come un conversazioni." + renote: "Puoi ri-condividere le Note, facendole rifluire sulla Timeline. Puoi anche aggiungere testo e citare altri profili." + reaction: "Puoi aggiungere una reazione. Nella pagina successiva spiegheremo i dettagli." + menu: "Puoi svolgere varie attività, come visualizzare i dettagli delle Note o copiare i collegamenti." + _reaction: + title: "Cosa sono le Reazioni?" + description: "Puoi reagire alle Note. Le sensazioni che non si riescono a trasmettere con i \"Mi piace\" si possono esprimere facilmente inviando una reazione." + letsTryReacting: "Puoi aggiungere una Reazione cliccando il bottone \"+\" (più) della relativa Nota. Prova ad aggiungerne una a questa Nota di esempio!" + reactToContinue: "Aggiungere la Reazione ti consentirà di procedere col tutorial." + reactNotification: "Quando qualcuno reagisce alle tue Note, ricevi una notifica in tempo reale." + reactDone: "Puoi annullare la tua Reazione premendo il bottone \"ー\" (meno)" + _timeline: + title: "Come funziona la Timeline" + description1: "Misskey fornisce alcune Timeline (sequenze cronologiche di Note). Una di queste potrebbe essere stata disattivata dagli amministratori." + home: "Puoi vedere le Note provenienti dai profili che segui (follow)." + local: "Puoi vedere tutte le Note pubblicate dai profili di questa istanza." + social: "Puoi vedere sia le Note della Timeline Home che quelle della Timeline Locale, insieme!" + global: "Puoi vedere le Note da pubblicate da tutte le altre istanze federate con la nostra." + description2: "Nella parte superiore dello schermo, puoi scegliere una Timeline o l'altra in qualsiasi momento." + description3: "Ci sono anche sequenze temporali di elenchi, sequenze temporali di canali, ecc. Per ulteriori dettagli, consultare il {link}.\nPuoi vedere anche Timeline delle liste di profili (se ne hai create), canali, ecc... Per i dettagli, visita {link}." + _postNote: + title: "La Nota e le sue impostazioni" + description1: "Quando scrivi una Nota su Misskey, hai a disposizione varie opzioni. Il modulo di invio è simile a questo." + _visibility: + description: "Puoi limitare chi può vedere la tua Nota." + public: "Visibile a tutti." + home: "Pubblicato solo sulla Timeline Home (personale). Visibile anche da profili remoti follower, visitatori del tuo profilo e tramite i Rinota dei follower." + followers: "Visibile solo ai profili tuoi follower (locali o remoti). Nessun altro oltre a te può \"Rinotare\"." + direct: "Visibile solo ai profili specificati, i quali riceveranno una notifica. Puoi usarlo come se fossero messaggi diretti." + doNotSendConfidencialOnDirect1: "Attenzione, quando si inviano informazioni confidenziali." + doNotSendConfidencialOnDirect2: "Poiché le Note non sono crittografate, l'amministratore del server di destinazione potrebbe leggere cosa è stato scritto, quindi se spedisci una Nota diretta a un profilo che risiede su un server non attendibile, evita di scrivere informazioni riservate." + localOnly: "Indipendentemente dalla visualizzazione sopra indicata, i profili su altri server non saranno in grado di visualizzare la Nota, se questa impostazione è attivata. Non non verrà comunicata ad altri server." + _cw: + title: "Nascondere il contenuto esplicito" + description: "Verrà visualizzato il testo scritto nel campo \"Annotazione preventiva\" al posto del testo principale della Nota. Premere il bottone \"Continua la lettura\" se si intende davvero leggere il testo." + _exampleNote: + cw: "Attenzione: contiene zuccheri" + note: "Ho appena mangiato una ciambella ricoperta di cioccolato 🍩😋" + useCases: "Utilizzalo per chiarire il contenuto della Nota, prima che sia letta. Come richiesto dal regolamento del server o per autoregolamentare spoiler e testi troppo espliciti." + _howToMakeAttachmentsSensitive: + title: "Come indicare che gli allegati sono espliciti?" + description: "Contrassegnare gli allegati come espliciti, va fatto quando è richiesto dal regolamento del server o quando gli allegati non devono essere immediatamente visibili." + tryThisFile: "Prova a rendere esplicite le immagini allegate a questo modulo!" + _exampleNote: + note: "Ho fatto un errore aprendo il coperchio del natto... (fagioli di soia fermentati, particolarmente appiccicosi)" + method: "Per indicare che un allegato è esplicito, tocca il file per aprirne il menu e scegliere la voce \"Segna come esplicito\"." + sensitiveSucceeded: "Quando alleghi file, assicurati di indicare se è materiale esplicito, in modo appropriato, in base al regolamento del tuo server." + doItToContinue: "Impostando l'immagine come esplicita, potrai procedere col tutorial." + _done: + title: "Il tutorial è finito! 🎉" + description: "Queste sono solamente alcune delle funzionalità principali di Misskey. Per ulteriori informazioni, {link}." +_timelineDescription: + home: "Nella Timeline Home, la tua cronologia principale, puoi vedere le Note provenienti dai profili che segui (follow)." + local: "La Timeline Locale, è una cronologia di Note pubblicate da tutti i profili iscritti su questo server." + social: "La Timeline Sociale, unisce in ordine cronologico l'elenco di Note presenti nella Timeline Home e quella Locale." + global: "La Timeline Federata ti consente di vedere le Note pubblicate dai profili di tutti gli altri server federati a questo." _serverRules: description: "In Europa è necessario mostrare l'informativa sul trattamento dei dati personali, prima della registrazione al servizio." _serverSettings: @@ -1447,6 +1520,9 @@ _achievements: _smashTestNotificationButton: title: "Prove eccessive" description: "Hai provato le notifiche consecutivamente in un periodo di tempo molto breve" + _tutorialCompleted: + title: "Attestato di partecipazione al corso per principianti di Misskey" + description: "Ha completato il tutorial" _role: new: "Nuovo ruolo" edit: "Modifica ruolo" @@ -1635,6 +1711,7 @@ _channel: notesCount: "{n} note" nameAndDescription: "Nome e descrizione" nameOnly: "Solo il nome" + allowRenoteToExternal: "Consenti i Rinota e le citazioni all'esterno del canale" _menuDisplay: sideFull: "Laterale" sideIcon: "Laterale (solo icone)" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 8609bad2e5..01c880852d 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -531,6 +531,7 @@ serverLogs: "서버 로그" deleteAll: "모두 삭제" showFixedPostForm: "타임라인 상단에 글 작성란을 표시" showFixedPostFormInChannel: "채널 타임라인 상단에 글 작성란을 표시" +withRepliesByDefaultForNewlyFollowed: "팔로우 할 때 기본적으로 답글을 타임라인에 나오게 하기" newNoteRecived: "새 노트가 있습니다" sounds: "소리" sound: "소리" @@ -711,6 +712,7 @@ lockedAccountInfo: "팔로우를 승인으로 승인받더라도 노트의 공 alwaysMarkSensitive: "미디어를 항상 열람 주의로 설정" loadRawImages: "첨부한 이미지의 썸네일을 원본화질로 표시" disableShowingAnimatedImages: "움직이는 이미지를 자동으로 재생하지 않음" +highlightSensitiveMedia: "미디어가 민감한 내용이라는 것을 알기 쉽게 표시" verificationEmailSent: "확인 메일을 발송하였습니다. 설정을 완료하려면 메일에 첨부된 링크를 확인해 주세요." notSet: "설정되지 않음" emailVerified: "메일 주소가 확인되었습니다." @@ -1122,8 +1124,26 @@ showRenotes: "리노트 표시" edited: "수정됨" notificationRecieveConfig: "알림 설정" mutualFollow: "맞팔로우" +showRepliesToOthersInTimeline: "타임라인에 다른 사람에게 보내는 답글을 포함" +hideRepliesToOthersInTimeline: "타임라인에 다른 사람에게 보내는 답글을 포함하지 않음" +showRepliesToOthersInTimelineAll: "타임라인에 현재 팔로우 중인 사람 전원의 답글을 포함하게 하기" +hideRepliesToOthersInTimelineAll: "타임라인에 현재 팔로우 중인 사람 전원의 답글이 나오지 않게 하기" +confirmShowRepliesAll: "이 조작은 되돌릴 수 없습니다. 정말로 타임라인에 현재 팔로우 중인 사람 전원의 답글이 나오지 않게 하시겠습니까?" +confirmHideRepliesAll: "이 조작은 되돌릴 수 없습니다. 정말로 타임라인에 현재 팔로우 중인 사람 전원의 답글이 나오지 않게 하시겠습니까?" +externalServices: "외부 서비스" +impressum: "운영자 정보" +impressumUrl: "운영자 정보 URL" +impressumDescription: "독일 등의 일부 나라와 지역에서는 꼭 표시해야 합니다(Impressum)." +avatarDecorations: "아이콘 장식" +attach: "붙이기" +detach: "떼기" +angle: "각도" flip: "플립" +showAvatarDecorations: "아이콘 장식을 표시" +disableStreamingTimeline: "타임라인의 실시간 갱신을 무효화하기" useGroupedNotifications: "알림을 그룹화하고 표시" +signupPendingError: "메일 주소 확인중에 문제가 발생했습니다. 링크의 유효기간이 지났을 가능성이 있습니다." +cwNotationRequired: "'내용을 숨기기'를 체크했을 경우 주석을 써야 합니다." _announcement: forExistingUsers: "기존 유저에게만 알림" forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다." @@ -1145,8 +1165,22 @@ _initialAccountSetting: pushNotificationDescription: "푸시 알림을 활성화하면 {name}의 알림을 나의 기기에서 받아볼 수 있게 됩니다." initialAccountSettingCompleted: "초기 설정을 모두 마쳤습니다!" haveFun: "{name}와 함께 즐거운 시간 보내세요!" + youCanContinueTutorial: "이대로 {name}(Misskey)의 사용법에 대해 튜토리얼을 진행할 수도 있지만, 여기서 중단하고 바로 시작할 수도 있습니다." + startTutorial: "튜토리얼 시작" skipAreYouSure: "초기 설정을 중단하시겠습니까?" laterAreYouSure: "초기 설정을 나중에 진행하시겠습니까?" +_initialTutorial: + launchTutorial: "튜토리얼 보기" + title: "튜토리얼" + wellDone: "잘 하셨습니다" + skipAreYouSure: "튜토리얼을 종료하시겠습니까?" + _landing: + description: "여기서는 미스키의 기본적인 사용법이나 기능을 확인할 수 있습니다." + _note: + description: "미스키에서는 게시물을 '노트'라고 합니다. 노트는 타임라인에 시간순으로 정렬되어 있고, 실시간으로 갱신됩니다." + reply: "답글을 다는 것이 가능합니다. 답글에 답글을 다는 것도 가능하며 스레드처럼 대화를 계속하는 것도 가능합니다." + renote: "그 노트를 자기 타임라인에 가져와서 공유하는 것이 가능합니다. 글을 추가해서 인용하는 것도 가능합니다." + reaction: "리액션을 다는 것이 가능합니다. 다음 페이지에서 자세한 설명을 볼 수 있습니다." _serverRules: description: "회원 가입 이전에 간단하게 표시할 서버 규칙입니다. 이용 약관의 요약으로 구성하는 것을 추천합니다." _serverSettings: diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 11d894900d..90cb5ac3ce 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1155,6 +1155,7 @@ refreshing: "載入更新中" pullDownToRefresh: "往下拉來更新內容" disableStreamingTimeline: "停用時間軸的即時更新" useGroupedNotifications: "分組顯示通知訊息" +signupPendingError: "驗證您的電子郵件地址時出現問題。連結可能已過期。" cwNotationRequired: "如果開啟「隱藏內容」,則需要註解說明。" _announcement: forExistingUsers: "僅限既有的使用者" @@ -1165,6 +1166,8 @@ _announcement: tooManyActiveAnnouncementDescription: "有過多公告可能會影響使用者體驗。請考慮歸檔已結束的公告。" readConfirmTitle: "標記為已讀嗎?" readConfirmText: "閱讀「{title}」的內容並標記為已讀。" + shouldNotBeUsedToPresentPermanentInfo: "由於可能會破壞使用者體驗,尤其是對於新使用者而言,我們建議使用公告來發布有時效性的資訊而不是固定不變的資訊。" + dialogAnnouncementUxWarn: "如果同時有 2 個以上對話方塊形式的公告存在,對於使用者體驗很可能會有不良的影響,因此建議謹慎使用。" _initialAccountSetting: accountCreated: "帳戶已建立完成!" letsStartAccountSetup: "來進行帳戶的初始設定吧。" @@ -1177,8 +1180,77 @@ _initialAccountSetting: pushNotificationDescription: "啟用推送通知,就可以在設備上接收{name}的通知。" initialAccountSettingCompleted: "初始設定完成了!" haveFun: "盡情享受{name}吧!" + youCanContinueTutorial: "您可以繼續學習如何使用{name}(Misskey),也可以就此打住,立即開始使用。" + startTutorial: "開始教學課程" skipAreYouSure: "要略過初始設定嗎?" laterAreYouSure: "稍後再重新進行初始設定嗎?" +_initialTutorial: + launchTutorial: "觀看教學課程" + title: "新手教學" + wellDone: "做得好" + skipAreYouSure: "結束教學模式?" + _landing: + title: "歡迎使用本教學課程" + description: "在這裡您可以查看Misskey的基本使用方法和功能。" + _note: + title: "什麼是貼文?" + description: "在Misskey上發布的內容稱為「貼文」。貼文在時間軸上按時間順序排列,並即時更新。" + reply: "您可以回覆貼文,並像討論串一樣繼續對話。" + renote: "您可以將此貼文分享到自己的時間軸。您也可以在引用時添加文字。" + reaction: "您可以添加反應。詳細資訊將在下一頁進行說明。" + menu: "可執行各種操作,如查看貼文詳細資訊和複製連結。" + _reaction: + title: "什麼是反應?" + description: "您可以在貼文中添加「反應」。您可以使用反應輕鬆隨意地表達「最愛/大心」所無法傳達的細微差別。" + letsTryReacting: "可以透過點擊貼文上的「+」按鈕來添加反應。請嘗試在此範例貼文添加反應!" + reactToContinue: "添加反應以繼續教學課程。" + reactNotification: "當有人對您的貼文做出反應時會即時接收到通知。" + reactDone: "按下「-」按鈕可以取消反應。" + _timeline: + title: "時間軸如何運作" + description1: "Misskey根據使用方式提供了多個時間軸(伺服器可能會將部份時間軸停用)。" + home: "您可以查看您追隨的使用者的貼文。" + local: "您可以看到此伺服器上所有使用者的貼文。" + social: "來自首頁時間軸和本地時間軸的貼文都會顯示。" + global: "可以看到其他已連接伺服器的貼文。" + description2: "您可以隨時在螢幕上方切換對應的時間軸。" + description3: "除此之外還有清單時間軸、頻道時間軸等。請參閱{link}以了解更多詳情。" + _postNote: + title: "貼文的發布設定" + description1: "在Misskey上發布貼文時,可以設定各種選項。發布表單如下所示。" + _visibility: + description: "可以限制誰可以看到您的貼文。" + public: "所有人都可以看見。" + home: "僅在首頁時間軸上發布。其他使用者只在下列情況可看見該貼文:追隨者、觀看使用者的個人資料頁面,以及貼文被轉發時。" + followers: "僅追隨者可見。只有發文者本人可轉發,未追隨發文者的使用者無法看見。" + direct: "僅指定的使用者可見,對方也會收到通知。可代替直接訊息使用。" + doNotSendConfidencialOnDirect1: "發送機密訊息時請務必注意。" + doNotSendConfidencialOnDirect2: "目標伺服器的管理員可以看到發布的內容,因此如果您向不受信任的伺服器上的使用者發送直接訊息,必須小心處理機密訊息。" + localOnly: "不將貼文發布到聯邦上的其他伺服器。不論上述發布範圍,使用此設定後,其他伺服器上的使用者將無法直接查看此貼文。" + _cw: + title: "隱藏內容(CW)" + description: "將顯示「註釋」中寫入的內容而不是本文。按一下「查看更多」以顯示本文。" + _exampleNote: + cw: "美食恐怖主義注意" + note: "我吃了一個巧克力甜甜圈🍩😋" + useCases: "伺服器的服務條款可能會規範特定的貼文需要使用隱藏內容,除此之外也會用在隱藏劇情洩漏與敏感內容的貼文。" + _howToMakeAttachmentsSensitive: + title: "如何標記上傳附件為敏感內容?" + description: "如果伺服器服務條款有規範,又或者不希望上傳附件直接被看見,可以設置為「敏感內容」" + tryThisFile: "試試看!把附加在發文表單的圖像檔案標記為敏感內容。" + _exampleNote: + note: "打開納豆的包裝失敗了…" + method: "若要使上傳附件標記為敏感內容,請按一下該檔案以開啟選單,然後點擊「標記為敏感內容」。" + sensitiveSucceeded: "上傳附件時,請務必根據伺服器的服務條款適當設定敏感內容。" + doItToContinue: "把圖像標記為敏感內容以繼續教學課程。" + _done: + title: "教學課程已結束" + description: "這裡介紹的功能只是其中的一小部分。要了解更多有關如何使用Misskey的資訊,請瀏覽{link}。" +_timelineDescription: + home: "在首頁時間線上,可以看到您追隨的使用者的貼文。" + local: "在本地時間軸上,可以看到此伺服器所有使用者的貼文。" + social: "在社交時間軸上,可以看到首頁與本地時間軸的貼文。" + global: "在公開時間軸上,可以看到其他已連接伺服器的貼文。\n" _serverRules: description: "設定在註冊頁面顯示的伺服器簡要規則。建議是服務條款的摘要。" _serverSettings: @@ -1448,6 +1520,9 @@ _achievements: _smashTestNotificationButton: title: "過度測試" description: "極短時間內連續測試通知" + _tutorialCompleted: + title: "Misskey新手講座 結業證書" + description: "已完成教學課程" _role: new: "建立角色" edit: "編輯角色" @@ -1636,6 +1711,7 @@ _channel: notesCount: "有 {n} 篇貼文" nameAndDescription: "名稱與說明" nameOnly: "僅名稱" + allowRenoteToExternal: "允許在頻道外轉發和引用" _menuDisplay: sideFull: "橫向" sideIcon: "橫向(圖示)" @@ -1865,7 +1941,7 @@ _widgets: clicker: "點擊器" _cw: hide: "隱藏" - show: "瀏覽更多" + show: "顯示內容" chars: "{count} 個字元" files: "{count} 個檔案" _poll: From 1e737dbb94ac856be7cc68e834f99e0bb759cf41 Mon Sep 17 00:00:00 2001 From: ozelot <contact@ozelot.dev> Date: Sat, 4 Nov 2023 18:45:59 +0900 Subject: [PATCH 43/60] =?UTF-8?q?fix(backend):=20GTL=E3=81=AE=E3=80=8C?= =?UTF-8?q?=E3=83=AA=E3=83=8E=E3=83=BC=E3=83=88=E3=82=92=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=80=8D=E3=82=AA=E3=83=97=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=8C?= =?UTF-8?q?=E6=A9=9F=E8=83=BD=E3=81=97=E3=81=AA=E3=81=84=E3=81=AE=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#12234)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend): GTLの「リノートを表示」オプションが機能しないのを修正 (#12233) * docs: Update changelog * Apply suggestions from code review Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> --- CHANGELOG.md | 1 + .../src/server/api/endpoints/notes/global-timeline.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4038755124..9a10167418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ - Fix: アクセストークンを削除すると、通知が取得できなくなる場合がある問題を修正 - Fix: 自身の宛先なしダイレクト投稿がストリーミングで流れてこない問題を修正 - Fix: サーバーサイドからのテスト通知を正しく行えるように修正 +- Fix: GTLの「リノートを表示」オプションが機能しないのを修正 #12233 ## 2023.10.2 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 be7557c213..68fefa5b58 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -87,6 +87,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); } + + if (ps.withRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.where('note.renoteId IS NULL'); + qb.orWhere(new Brackets(qb => { + qb.where('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + })); + })); + } //#endregion const timeline = await query.limit(ps.limit).getMany(); From 3733cbf81800dc6e4554694175d5478ddb3f1b62 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sat, 4 Nov 2023 18:47:22 +0900 Subject: [PATCH 44/60] 2023.11.0-beta.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2abd33f6d1..9d5da3009c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2023.11.0-beta.9", + "version": "2023.11.0-beta.10", "codename": "nasubi", "repository": { "type": "git", From fc0ea0ddacda082528f6aafe78297bf8e592bcdc Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sat, 4 Nov 2023 19:45:37 +0900 Subject: [PATCH 45/60] perf(frontend): improve nyaize performance --- packages/frontend/src/scripts/nyaize.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/frontend/src/scripts/nyaize.ts b/packages/frontend/src/scripts/nyaize.ts index 0ac77e1006..62833b4de3 100644 --- a/packages/frontend/src/scripts/nyaize.ts +++ b/packages/frontend/src/scripts/nyaize.ts @@ -3,18 +3,25 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +const enRegex1 = /(?<=n)a/gi; +const enRegex2 = /(?<=morn)ing/gi; +const enRegex3 = /(?<=every)one/gi; +const koRegex1 = /[나-낳]/g; +const koRegex2 = /(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm; +const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm; + export function nyaize(text: string): string { return text // ja-JP .replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ') // en-US - .replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya') - .replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan') - .replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan') + .replace(enRegex1, x => x === 'A' ? 'YA' : 'ya') + .replace(enRegex2, x => x === 'ING' ? 'YAN' : 'yan') + .replace(enRegex3, x => x === 'ONE' ? 'NYAN' : 'nyan') // ko-KR - .replace(/[나-낳]/g, match => String.fromCharCode( + .replace(koRegex1, match => String.fromCharCode( match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0), )) - .replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥') - .replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥'); + .replace(koRegex2, '다냥') + .replace(koRegex3, '냥'); } From 47851025a6ad4a5e4de15ff1caa6618e8d7f8efb Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sat, 4 Nov 2023 19:47:43 +0900 Subject: [PATCH 46/60] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a10167418..37f40667bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ ## 2023.11.0 (unreleased) +### Note +- iOS 16.4未満を使用している場合はiOS 16.4以上にアップデートをお願いします + ### General - Feat: アイコンデコレーション機能 - サーバーで用意された画像をアイコンに重ねることができます From b7d3c5f4f07454237fda86c3e00b94fb3b2aefd8 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sat, 4 Nov 2023 19:50:49 +0900 Subject: [PATCH 47/60] enhance of 5e9f6a90df --- .../src/components/MkNoteDetailed.vue | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index ff2941344e..1d8049934a 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -73,7 +73,16 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-show="appearNote.cw == null || showContent"> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="appearNote.text" :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/> + <Mfm + v-if="appearNote.text" + :parsedNodes="parsed" + :text="appearNote.text" + :author="appearNote.user" + :nyaize="'account'" + :emojiUrls="appearNote.emojis" + :enableEmojiMenu="true" + :enableEmojiMenuReaction="true" + /> <a v-if="appearNote.renote != null" :class="$style.rn">RN:</a> <div v-if="translating || translation" :class="$style.translation"> <MkLoading v-if="translating" mini/> @@ -184,7 +193,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, inject, onMounted, ref, shallowRef } from 'vue'; +import { computed, inject, onMounted, provide, ref, shallowRef } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import MkNoteSub from '@/components/MkNoteSub.vue'; @@ -276,6 +285,13 @@ const keymap = { 's': () => showContent.value !== showContent.value, }; +provide('react', (reaction: string) => { + os.api('notes/reactions/create', { + noteId: appearNote.id, + reaction: reaction, + }); +}); + let tab = $ref('replies'); let reactionTabType = $ref(null); From 94a20205eb28decaf58205a7d92d9bdde4ca3f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sat, 4 Nov 2023 20:21:42 +0900 Subject: [PATCH 48/60] =?UTF-8?q?(fix)=20=E3=83=81=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=83=AA=E3=82=A2=E3=83=AB=E4=B8=AD=E3=81=ABPostForm?= =?UTF-8?q?=E3=81=AB=E3=83=95=E3=82=A9=E3=83=BC=E3=82=AB=E3=82=B9=E3=81=8C?= =?UTF-8?q?=E5=BD=93=E3=81=9F=E3=82=89=E3=81=AA=E3=81=84=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B=20(#12242)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/components/MkTutorialDialog.PostNote.vue | 2 +- packages/frontend/src/components/MkTutorialDialog.Sensitive.vue | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue index 9b55a1dca7..b395f64853 100644 --- a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue +++ b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps"> <div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._postNote.description1 }}</div> - <MkPostForm :class="$style.exampleRoot" :mock="true"/> + <MkPostForm :class="$style.exampleRoot" :mock="true" :autofocus="false"/> <MkFormSection> <template #label>{{ i18n.ts.visibility }}</template> <div class="_gaps"> diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue index 768d00bb07..896db5eb3a 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue @@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkPostForm :class="$style.exampleRoot" :mock="true" + :autofocus="false" :initialNote="exampleNote" @fileChangeSensitive="doSucceeded" ></MkPostForm> From 8372e547ebd820447aa4c6c8bd1040c51411a2aa Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sat, 4 Nov 2023 20:40:34 +0900 Subject: [PATCH 49/60] New Crowdin updates (#12241) * New translations ja-jp.yml (English) * New translations ja-jp.yml (Chinese Traditional) * New translations ja-jp.yml (English) --- locales/en-US.yml | 53 +++++++++++++++++++++++++++++++++++++++++++++++ locales/zh-TW.yml | 3 ++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index 40a5f7b14f..5056b24128 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1157,6 +1157,7 @@ disableStreamingTimeline: "Disable real-time timeline updates" useGroupedNotifications: "Display grouped notifications" signupPendingError: "There was a problem verifying the email address. The link may have expired." cwNotationRequired: "If \"Hide content\" is enabled, a description must be provided." +doReaction: "Add reaction" _announcement: forExistingUsers: "Existing users only" forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it." @@ -1203,6 +1204,54 @@ _initialTutorial: title: "What are Reactions?" description: "Notes can be reacted to with various emojis. Reactions allow you to express nuances that may not be conveyed with just a 'like.'" letsTryReacting: "Reactions can be added by clicking the '+' button on the note. Try reacting to this sample note!" + reactToContinue: "Add a reaction to proceed." + reactNotification: "You'll receive real-time notifications when someone reacts to your note." + reactDone: "You can undo a reaction by pressing the '-' button." + _timeline: + title: "The Concept of Timelines" + description1: "Misskey provides multiple timelines based on usage (some may not be available depending on the server's policies)." + home: "You can view notes from accounts you follow." + local: "You can view notes from all users on this server." + social: "Notes from the Home and Local timelines will be displayed." + global: "You can view notes from all connected servers." + description2: "You can switch between timelines at the top of the screen at any time." + description3: "Additionally, there are list timelines and channel timelines. For more details, please refer to {link}." + _postNote: + title: "Note Posting Settings" + description1: "When posting a note on Misskey, various options are available. The posting form looks like this." + _visibility: + description: "You can limit who can view your note." + public: "Your note will be visible for all users." + home: "Public only on the Home timeline. People visiting your profile, via followers, and through renotes can see it." + followers: "Visible to followers only. Only followers can see it and no one else, and it cannot be renoted by others." + direct: "Visible only to specified users, and the recipient will be notified. It can be used as an alternative to direct messaging." + doNotSendConfidencialOnDirect1: "Be careful when sending sensitive information!" + doNotSendConfidencialOnDirect2: "Administrators of the server can see what you write. Be careful with sensitive information when sending direct notes to users on untrusted servers." + localOnly: "Posting with this flag will not federate the note to other servers. Users on other servers will not be able to view these notes directly, regardless of the display settings above." + _cw: + title: "Content Warning" + description: "Instead of the body, the content written in 'comments' field will be displayed. Pressing \"read more\" will reveal the body." + _exampleNote: + cw: "This will surely make you hungry!" + note: "Just had a chocolate-glazed donut 🍩😋" + useCases: "This is used when following the server guidelines for necessary notes or for self-restriction of spoiler or sensitive text." + _howToMakeAttachmentsSensitive: + title: "How to Mark Attachments as Sensitive?" + description: "For attachments that are required by server guidelines or that should not be left intact, add a \"sensitive\" flag." + tryThisFile: "Try marking the image attached in this form as sensitive!" + _exampleNote: + note: "Oops, messed up opening the natto lid..." + method: "To mark an attachment as sensitive, click the file thumbnail, open the menu, and click \"Mark as Sensitive.\"" + sensitiveSucceeded: "When attaching files, please set sensitivities in accordance with the server guidelines." + doItToContinue: "Mark the attachment file as sensitive to proceed." + _done: + title: "The tutorial is complete! 🎉" + description: "The functions introduced here are just a small part. For a more detailed understanding of using Misskey, please refer to {link}." +_timelineDescription: + home: "In the Home timeline, you can see notes from accounts you follow." + local: "In the Local timeline, you can see notes from all users on this server." + social: "The Social timeline displays notes from both the Home and Local timelines." + global: "In the Global timeline, you can see notes from all connected servers." _serverRules: description: "A set of rules to be displayed before registration. Setting a summary of the Terms of Service is recommended." _serverSettings: @@ -1472,6 +1521,9 @@ _achievements: _smashTestNotificationButton: title: "Test overflow" description: "Trigger the notification test repeatedly within an extremely short time" + _tutorialCompleted: + title: "Misskey Elementary Course Diploma" + description: "Tutorial completed" _role: new: "New role" edit: "Edit role" @@ -1660,6 +1712,7 @@ _channel: notesCount: "{n} Notes" nameAndDescription: "Name and description" nameOnly: "Name only" + allowRenoteToExternal: "Allow renote and quote outside the channel" _menuDisplay: sideFull: "Side" sideIcon: "Side (Icons)" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 90cb5ac3ce..4bad051a44 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1157,6 +1157,7 @@ disableStreamingTimeline: "停用時間軸的即時更新" useGroupedNotifications: "分組顯示通知訊息" signupPendingError: "驗證您的電子郵件地址時出現問題。連結可能已過期。" cwNotationRequired: "如果開啟「隱藏內容」,則需要註解說明。" +doReaction: "做出反應" _announcement: forExistingUsers: "僅限既有的使用者" forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。" @@ -1229,7 +1230,7 @@ _initialTutorial: localOnly: "不將貼文發布到聯邦上的其他伺服器。不論上述發布範圍,使用此設定後,其他伺服器上的使用者將無法直接查看此貼文。" _cw: title: "隱藏內容(CW)" - description: "將顯示「註釋」中寫入的內容而不是本文。按一下「查看更多」以顯示本文。" + description: "將顯示「註釋」中寫入的內容而不是本文。按一下「顯示內容」以顯示本文。" _exampleNote: cw: "美食恐怖主義注意" note: "我吃了一個巧克力甜甜圈🍩😋" From 2eebf3e33f42f433830be5372fca66fc209456bd Mon Sep 17 00:00:00 2001 From: Mar0xy <marie@kaifa.ch> Date: Sat, 4 Nov 2023 14:35:21 +0100 Subject: [PATCH 50/60] upd: locales --- locales/de-DE.yml | 2 +- locales/en-US.yml | 22 +++++++++++----------- locales/ja-JP.yml | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/locales/de-DE.yml b/locales/de-DE.yml index d6f965a432..4747ac86b6 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1444,7 +1444,7 @@ _achievements: _brainDiver: title: "Brain Diver" description: "Sende den Link zu Brain Diver" - flavor: "Misskey-Misskey La-Tu-Ma" + flavor: "Sharkey-Sharkey La-Tu-Ma" _smashTestNotificationButton: title: "Testüberfluss" description: "Betätige den Benachrichtigungstest mehrfach innerhalb einer extrem kurzen Zeitspanne" diff --git a/locales/en-US.yml b/locales/en-US.yml index 3c187eba55..d76c7e6ffc 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -169,7 +169,7 @@ youCanCleanRemoteFilesCache: "You can clear the cache by clicking the 🗑️ bu cacheRemoteSensitiveFiles: "Cache sensitive remote files" cacheRemoteSensitiveFilesDescription: "When this setting is disabled, sensitive remote files are loaded directly from the remote instance without caching." flagAsBot: "Mark this account as a bot" -flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Misskey's internal systems to treat this account as a bot." +flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Sharkey's internal systems to treat this account as a bot." flagAsCat: "Mark this account as a cat" flagAsCatDescription: "Enable this option to mark this account as a cat." flagSpeakAsCat: "Speak as a cat" @@ -573,7 +573,7 @@ sort: "Sorting order" ascendingOrder: "Ascending" descendingOrder: "Descending" scratchpad: "Scratchpad" -scratchpadDescription: "The Scratchpad provides an environment for AiScript experiments. You can write, execute, and check the results of it interacting with Misskey in it." +scratchpadDescription: "The Scratchpad provides an environment for AiScript experiments. You can write, execute, and check the results of it interacting with Sharkey in it." output: "Output" script: "Script" disablePagesScript: "Disable AiScript on Pages" @@ -705,7 +705,7 @@ unclip: "Unclip" confirmToUnclipAlreadyClippedNote: "This note is already part of the \"{name}\" clip. Do you want to remove it from this clip instead?" public: "Public" private: "Private" -i18nInfo: "Misskey is being translated into various languages by volunteers. You can help at {link}." +i18nInfo: "Sharkey is being translated into various languages by volunteers. You can help at {link}." manageAccessTokens: "Manage access tokens" accountInfo: "Account Info" notesCount: "Number of notes" @@ -1211,7 +1211,7 @@ _initialAccountSetting: pushNotificationDescription: "Enabling push notifications will allow you to receive notifications from {name} directly on your device." initialAccountSettingCompleted: "Profile setup complete!" haveFun: "Enjoy {name}!" - youCanContinueTutorial: "You can proceed to a tutorial on how to use {name} (Misskey) or you can exit the setup here and start using it immediately." + youCanContinueTutorial: "You can proceed to a tutorial on how to use {name} (Sharkey) or you can exit the setup here and start using it immediately." startTutorial: "Start Tutorial" skipAreYouSure: "Really skip profile setup?" laterAreYouSure: "Really do profile setup later?" @@ -1222,10 +1222,10 @@ _initialTutorial: skipAreYouSure: "Quit Tutorial?" _landing: title: "Welcome to the Tutorial" - description: "Here, you can learn the basics of using Misskey and its features." + description: "Here, you can learn the basics of using Sharkey and its features." _note: title: "What is a Note?" - description: "Posts on Misskey are called 'Notes.' Notes are arranged chronologically on the timeline and are updated in real-time." + description: "Posts on Sharkey are called 'Notes.' Notes are arranged chronologically on the timeline and are updated in real-time." reply: "Click on this button to reply to a message. It's also possible to reply to replies, continuing the conversation like a thread." renote: "You can share that note to your own timeline. You can also quote them with your comments." reaction: "You can add reactions to the Note. More details will be explained on the next page." @@ -1239,7 +1239,7 @@ _initialTutorial: reactDone: "You can undo a reaction by pressing the '-' button." _timeline: title: "The Concept of Timelines" - description1: "Misskey provides multiple timelines based on usage (some may not be available depending on the server's policies)." + description1: "Sharkey provides multiple timelines based on usage (some may not be available depending on the server's policies)." home: "You can view notes from accounts you follow." local: "You can view notes from all users on this server." social: "Notes from the Home and Local timelines will be displayed." @@ -1248,7 +1248,7 @@ _initialTutorial: description3: "Additionally, there are list timelines and channel timelines. For more details, please refer to {link}." _postNote: title: "Note Posting Settings" - description1: "When posting a note on Misskey, various options are available. The posting form looks like this." + description1: "When posting a note on Sharkey, various options are available. The posting form looks like this." _visibility: description: "You can limit who can view your note." public: "Your note will be visible for all users." @@ -1276,7 +1276,7 @@ _initialTutorial: doItToContinue: "Mark the attachment file as sensitive to proceed." _done: title: "The tutorial is complete! 🎉" - description: "The functions introduced here are just a small part. For a more detailed understanding of using Misskey, please refer to {link}." + description: "The functions introduced here are just a small part. For a more detailed understanding of using Sharkey, please refer to {link}." _timelineDescription: home: "In the Home timeline, you can see notes from accounts you follow." local: "In the Local timeline, you can see notes from all users on this server." @@ -1547,12 +1547,12 @@ _achievements: _brainDiver: title: "Brain Diver" description: "Post the link to Brain Diver" - flavor: "Misskey-Misskey La-Tu-Ma" + flavor: "Sharkey-Sharkey La-Tu-Ma" _smashTestNotificationButton: title: "Test overflow" description: "Trigger the notification test repeatedly within an extremely short time" _tutorialCompleted: - title: "Misskey Elementary Course Diploma" + title: "Sharkey Elementary Course Diploma" description: "Tutorial completed" _role: new: "New role" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 7c2a7061a0..8508f93575 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1560,7 +1560,7 @@ _achievements: title: "テスト過剰" description: "通知のテストをごく短時間のうちに連続して行った" _tutorialCompleted: - title: "Misskey初心者講座 修了証" + title: "Sharkey初心者講座 修了証" description: "チュートリアルを完了した" _role: From 83c64377fc8be569b5cdadcdb6a8bfdf923d4caf Mon Sep 17 00:00:00 2001 From: Mar0xy <marie@kaifa.ch> Date: Sat, 4 Nov 2023 14:51:43 +0100 Subject: [PATCH 51/60] chore: replace icons on new menu --- packages/frontend/src/components/global/MkCustomEmoji.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index 1e17bab849..65d45f2534 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -80,14 +80,14 @@ function onClick(ev: MouseEvent) { text: `:${props.name}:`, }, { text: i18n.ts.copy, - icon: 'ti ti-copy', + icon: 'ph-copy ph-bold ph-lg', action: () => { copyToClipboard(`:${props.name}:`); os.success(); }, }, ...(props.menuReaction && react ? [{ text: i18n.ts.doReaction, - icon: 'ti ti-plus', + icon: 'ph-plus ph-bold ph-lg', action: () => { react(`:${props.name}:`); }, From 56401ed91c34a265fa5edcf5554cff3684ec3d98 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 5 Nov 2023 08:25:08 +0900 Subject: [PATCH 52/60] :art: --- .../frontend/src/components/global/MkCondensedLine.vue | 8 ++++---- packages/frontend/src/pages/settings/profile.vue | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/frontend/src/components/global/MkCondensedLine.vue b/packages/frontend/src/components/global/MkCondensedLine.vue index ef1c931bc3..2ed615f5ff 100644 --- a/packages/frontend/src/components/global/MkCondensedLine.vue +++ b/packages/frontend/src/components/global/MkCondensedLine.vue @@ -18,10 +18,10 @@ interface Props { const contentSymbol = Symbol(); const observer = new ResizeObserver((entries) => { - const results: { - container: HTMLSpanElement; - transform: string; - }[] = []; + const results: { + container: HTMLSpanElement; + transform: string; + }[] = []; for (const entry of entries) { const content = (entry.target[contentSymbol] ? entry.target : entry.target.firstElementChild) as HTMLSpanElement; const props: Required<Props> = content[contentSymbol]; diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 2ac8d15545..f6e387da52 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -94,7 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only :class="[$style.avatarDecoration, { [$style.avatarDecorationActive]: $i.avatarDecorations.some(x => x.id === avatarDecoration.id) }]" @click="openDecoration(avatarDecoration)" > - <div :class="$style.avatarDecorationName"><MkCondensedLine :minScale="2 / 3">{{ avatarDecoration.name }}</MkCondensedLine></div> + <div :class="$style.avatarDecorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div> <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decoration="{ url: avatarDecoration.url }" forceShowDecoration/> <i v-if="avatarDecoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => avatarDecoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.avatarDecorationLock" class="ti ti-lock"></i> </div> From bdbb3266ae9a720c48ae7c12c47ba7852445e89b Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 5 Nov 2023 09:04:03 +0900 Subject: [PATCH 53/60] =?UTF-8?q?fix(backend):=20=E3=82=A2=E3=83=BC?= =?UTF-8?q?=E3=82=AB=E3=82=A4=E3=83=96=E3=81=97=E3=81=9F=E3=81=8A=E7=9F=A5?= =?UTF-8?q?=E3=82=89=E3=81=9B=E3=81=8C=E3=82=B3=E3=83=B3=E3=83=88=E3=83=AD?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E3=83=91=E3=83=8D=E3=83=AB=E3=81=AB=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA=E3=81=95=E3=82=8C=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + .../backend/src/server/api/endpoints/admin/announcements/list.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f40667bf..7890408cd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ - Fix: 11以上されているリアクションにおいてツールチップで示されるリアクション数が本来よりも1多い問題を修正 #12174 - Fix: サイレンス状態で公開範囲のパブリックを選択できてしまう問題を修正 #12224 - Fix: In deck layout, replies option is not saved after refresh +- Fix: アーカイブしたお知らせがコントロールパネルに表示される問題を修正 - Note: アップデート後、サウンドに関する設定が初期化されます ### Server diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index fefc379c00..0bda61a361 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -86,6 +86,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); + query.andWhere('announcement.isActive = true'); if (ps.userId) { query.andWhere('announcement.userId = :userId', { userId: ps.userId }); } else { From c2ddb649f841c48bca91db3fdee1e95f79e8bdf4 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 5 Nov 2023 09:04:38 +0900 Subject: [PATCH 54/60] =?UTF-8?q?enhance:=20=E9=9D=9E=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E3=81=AA=E3=81=8A=E7=9F=A5=E3=82=89=E3=81=9B=E3=82=92=E4=BD=9C?= =?UTF-8?q?=E6=88=90=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + locales/index.d.ts | 2 ++ locales/ja-JP.yml | 2 ++ .../1699141698112-announcement-silence.js | 18 ++++++++++++++++++ .../backend/src/core/AnnouncementService.ts | 4 ++++ packages/backend/src/models/Announcement.ts | 6 ++++++ .../endpoints/admin/announcements/create.ts | 2 ++ .../api/endpoints/admin/announcements/list.ts | 1 + .../endpoints/admin/announcements/update.ts | 2 ++ .../frontend/src/pages/admin/announcements.vue | 4 ++++ packages/frontend/src/pages/announcements.vue | 4 ++-- 11 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 packages/backend/migration/1699141698112-announcement-silence.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 7890408cd6..e8b845974c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - ユーザーが誤ったメールアドレスを入力した場合に招待コードが失効してしまう問題が解消されます。 - Enhance: すでにフォローしたすべての人の返信をTLに追加できるように - Enhance: 未読の通知数を表示できるように +- Enhance: 通知されず、確認の必要もないお知らせ(silence)を作成可能になりました - Enhance: ローカリゼーションの更新 - Enhance: 依存関係の更新 - Change: CWを使用する場合、注釈を空にすることは許可されなくなりました diff --git a/locales/index.d.ts b/locales/index.d.ts index aedaaa9f7c..fc6653b05b 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1172,6 +1172,8 @@ export interface Locale { "readConfirmText": string; "shouldNotBeUsedToPresentPermanentInfo": string; "dialogAnnouncementUxWarn": string; + "silence": string; + "silenceDescription": string; }; "_initialAccountSetting": { "accountCreated": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 6ecebfc393..67a57f994c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1170,6 +1170,8 @@ _announcement: readConfirmText: "「{title}」の内容を読み、既読にします。" shouldNotBeUsedToPresentPermanentInfo: "特に新規ユーザーのUXを損ねる可能性が高いため、ストック情報ではなくフロー情報の掲示にお知らせを使用することを推奨します。" dialogAnnouncementUxWarn: "ダイアログ形式のお知らせが同時に2つ以上ある場合、UXに悪影響を及ぼす可能性が非常に高いため、使用は慎重に行うことを推奨します。" + silence: "非通知" + silenceDescription: "オンにすると、このお知らせは通知されず、既読にする必要もなくなります。" _initialAccountSetting: accountCreated: "アカウントの作成が完了しました!" diff --git a/packages/backend/migration/1699141698112-announcement-silence.js b/packages/backend/migration/1699141698112-announcement-silence.js new file mode 100644 index 0000000000..eef9b076fc --- /dev/null +++ b/packages/backend/migration/1699141698112-announcement-silence.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AnnouncementSilence1699141698112 { + name = 'AnnouncementSilence1699141698112' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "announcement" ADD "silence" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`CREATE INDEX "IDX_7b8d9225168e962f94ea517e00" ON "announcement" ("silence") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_7b8d9225168e962f94ea517e00"`); + await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "silence"`); + } +} diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts index ec1a082d78..8c348e595d 100644 --- a/packages/backend/src/core/AnnouncementService.ts +++ b/packages/backend/src/core/AnnouncementService.ts @@ -47,6 +47,7 @@ export class AnnouncementService { const q = this.announcementsRepository.createQueryBuilder('announcement') .where('announcement.isActive = true') + .andWhere('announcement.silence = false') .andWhere(new Brackets(qb => { qb.orWhere('announcement.userId = :userId', { userId: user.id }); qb.orWhere('announcement.userId IS NULL'); @@ -73,6 +74,7 @@ export class AnnouncementService { icon: values.icon, display: values.display, forExistingUsers: values.forExistingUsers, + silence: values.silence, needConfirmationToRead: values.needConfirmationToRead, userId: values.userId, }).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0])); @@ -124,6 +126,7 @@ export class AnnouncementService { display: values.display, icon: values.icon, forExistingUsers: values.forExistingUsers, + silence: values.silence, needConfirmationToRead: values.needConfirmationToRead, isActive: values.isActive, }); @@ -210,6 +213,7 @@ export class AnnouncementService { icon: announcement.icon, display: announcement.display, needConfirmationToRead: announcement.needConfirmationToRead, + silence: announcement.silence, forYou: announcement.userId === me?.id, isRead: reads.some(read => read.announcementId === announcement.id), })); diff --git a/packages/backend/src/models/Announcement.ts b/packages/backend/src/models/Announcement.ts index 05d5a086f1..8f8be88fed 100644 --- a/packages/backend/src/models/Announcement.ts +++ b/packages/backend/src/models/Announcement.ts @@ -66,6 +66,12 @@ export class MiAnnouncement { }) public forExistingUsers: boolean; + @Index() + @Column('boolean', { + default: false, + }) + public silence: boolean; + @Index() @Column({ ...id(), diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts index 253a29cf5a..69c31a05eb 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts @@ -58,6 +58,7 @@ export const paramDef = { icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'], default: 'info' }, display: { type: 'string', enum: ['normal', 'banner', 'dialog'], default: 'normal' }, forExistingUsers: { type: 'boolean', default: false }, + silence: { type: 'boolean', default: false }, needConfirmationToRead: { type: 'boolean', default: false }, userId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, }, @@ -78,6 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- icon: ps.icon, display: ps.display, forExistingUsers: ps.forExistingUsers, + silence: ps.silence, needConfirmationToRead: ps.needConfirmationToRead, userId: ps.userId, }, me); diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index 0bda61a361..9630299a6e 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -114,6 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- display: announcement.display, isActive: announcement.isActive, forExistingUsers: announcement.forExistingUsers, + silence: announcement.silence, needConfirmationToRead: announcement.needConfirmationToRead, userId: announcement.userId, reads: reads.get(announcement)!, diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts index d36590c264..717866aead 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts @@ -35,6 +35,7 @@ export const paramDef = { icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'] }, display: { type: 'string', enum: ['normal', 'banner', 'dialog'] }, forExistingUsers: { type: 'boolean' }, + silence: { type: 'boolean' }, needConfirmationToRead: { type: 'boolean' }, isActive: { type: 'boolean' }, }, @@ -63,6 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- display: ps.display, icon: ps.icon, forExistingUsers: ps.forExistingUsers, + silence: ps.silence, needConfirmationToRead: ps.needConfirmationToRead, isActive: ps.isActive, }, me); diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index 36a67eba31..5785fb118c 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -48,6 +48,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription"> {{ i18n.ts._announcement.forExistingUsers }} </MkSwitch> + <MkSwitch v-model="announcement.silence" :helpText="i18n.ts._announcement.silenceDescription"> + {{ i18n.ts._announcement.silence }} + </MkSwitch> <MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription"> {{ i18n.ts._announcement.needConfirmationToRead }} </MkSwitch> @@ -97,6 +100,7 @@ function add() { icon: 'info', display: 'normal', forExistingUsers: false, + silence: false, needConfirmationToRead: false, }); } diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue index babac9d805..afc6a98281 100644 --- a/packages/frontend/src/pages/announcements.vue +++ b/packages/frontend/src/pages/announcements.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only <section v-for="announcement in items" :key="announcement.id" class="_panel" :class="$style.announcement"> <div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div> <div :class="$style.header"> - <span v-if="$i && !announcement.isRead" style="margin-right: 0.5em;">🆕</span> + <span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span> <span style="margin-right: 0.5em;"> <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkTime :time="announcement.updatedAt ?? announcement.createdAt" mode="detail"/> </div> </div> - <div v-if="tab !== 'past' && $i && !announcement.isRead" :class="$style.footer"> + <div v-if="tab !== 'past' && $i && !announcement.silence && !announcement.isRead" :class="$style.footer"> <MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton> </div> </section> From 2cce28533f3948e04c91e64379315dedc35ec71d Mon Sep 17 00:00:00 2001 From: Marie <robloxfilmcam@gmail.com> Date: Sun, 5 Nov 2023 02:22:10 +0100 Subject: [PATCH 55/60] fix(backend): isBot not being set on `Application` type (#12248) * fix: bot not being set on all relays * updatePerson missing the change * chore: replace wrong word with correct word --- .../backend/src/core/activitypub/models/ApPersonService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index d6a7de0601..bf38d5fd60 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -269,7 +269,7 @@ export class ApPersonService implements OnModuleInit { const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32); - const isBot = getApType(object) === 'Service'; + const isBot = getApType(object) === 'Service' || getApType(object) === 'Application'; const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); @@ -456,7 +456,7 @@ export class ApPersonService implements OnModuleInit { emojis: emojiNames, name: truncate(person.name, nameLength), tags, - isBot: getApType(object) === 'Service', + isBot: getApType(object) === 'Service' || getApType(object) === 'Application', isCat: (person as any).isCat === true, isLocked: person.manuallyApprovesFollowers, movedToUri: person.movedTo ?? null, From 66cecfaefda698e183b9320b957e8e7d41cbaf4a Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 5 Nov 2023 10:23:24 +0900 Subject: [PATCH 56/60] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8b845974c..14785c763b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ - Enhance: プロフィールの自己紹介欄のMFMが連合するようになりました - 相手がMisskey v2023.11.0以降である必要があります - Enhance: チャンネル取得時のパフォーマンスを向上 +- Enhance: AP: ApplicationタイプのアカウントをisBotとして扱うように - Fix: リストTLに自分のフォロワー限定投稿が含まれない問題を修正 - Fix: ローカルタイムラインに投稿者自身の投稿への返信が含まれない問題を修正 - Fix: 自分のフォローしているユーザーの自分のフォローしていないユーザーの visibility: followers な投稿への返信がストリーミングで流れてくる問題を修正 From 8f49c5cd4868fad1f0d13499b484496dccea86e0 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 5 Nov 2023 17:53:08 +0900 Subject: [PATCH 57/60] New Crowdin updates (#12244) * New translations ja-jp.yml (Italian) * New translations ja-jp.yml (French) * New translations ja-jp.yml (French) * New translations ja-jp.yml (French) --- locales/fr-FR.yml | 19 +++++++++++++++++++ locales/it-IT.yml | 1 + 2 files changed, 20 insertions(+) diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index bef2628636..e06c5c4e50 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -1088,7 +1088,26 @@ _initialAccountSetting: profileSetting: "Paramètres du profil" privacySetting: "Paramètres de confidentialité" initialAccountSettingCompleted: "Configuration du profil terminée avec succès !" + startTutorial: "Démarrer le tutoriel" skipAreYouSure: "Désirez-vous ignorer la configuration du profil ?" +_initialTutorial: + title: "Tutoriel" + wellDone: "Bien joué !" + skipAreYouSure: "Quitter le tutoriel ?" + _landing: + title: "Bienvenue dans le tutoriel" + description: "Ici, vous pouvez apprendre l'utilisation de base de Misskey et ses fonctionnalités." + _note: + title: "Qu'est-ce que les notes ?" + description: "Les messages sur Misskey sont appelés des « notes » . Les notes sont classées par ordre chronologique sur le fil et sont mises à jour en temps réel." + reply: "Vous pouvez répondre aux messages. Vous pouvez également répondre aux réponses et poursuivre la conversation comme un fil de discussion." + renote: "Vous pouvez partager cette note sur votre propre fil. Vous pouvez aussi ajouter du texte en citant." + reaction: "Vous pouvez ajouter des réactions. Les détails sont expliqués à la page suivante." + menu: "Vous pouvez afficher les détails de la note, copier le lien et effectuer d'autres actions." + _reaction: + title: "Qu'est-ce que les réactions ?" + description: "Vous pouvez ajouter des « réactions » aux notes. Les réactions vous permettent d'exprimer à l'aise des nuances qui ne peuvent pas être exprimées par des mentions j'aime." + letsTryReacting: "Des réactions peuvent être ajoutées en cliquant sur le bouton « + » de la note. Essayez d'ajouter une réaction à cet exemple de note !" _serverSettings: iconUrl: "URL de l’icône" fanoutTimelineDescription: "Si activée, la performance de la récupération de la chronologie augmentera considérablement et la charge sur la base de données sera réduite. En revanche, l'utilisation de la mémoire de Redis augmentera. Considérez désactiver cette option si le serveur est bas en mémoire ou instable." diff --git a/locales/it-IT.yml b/locales/it-IT.yml index b474dbda2c..1d323d6043 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -1157,6 +1157,7 @@ disableStreamingTimeline: "Disabilitare gli aggiornamenti della TL in tempo real useGroupedNotifications: "Mostra le notifiche raggruppate" signupPendingError: "Si è verificato un problema durante la verifica del tuo indirizzo email. Potrebbe essere scaduto il collegamento temporaneo." cwNotationRequired: "Devi indicare perché il contenuto è indicato come esplicito." +doReaction: "Reagisci" _announcement: forExistingUsers: "Solo ai profili attuali" forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio." From 2c836ba71fbc3e5cb4120439d74e31258b85ac23 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 5 Nov 2023 18:00:41 +0900 Subject: [PATCH 58/60] =?UTF-8?q?enhance(build):=20=E3=83=95=E3=82=A9?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E3=83=90=E3=83=83=E3=82=AF=E5=8A=B9=E3=81=8B?= =?UTF-8?q?=E3=81=99=E3=81=9F=E3=82=81=E3=81=ABlocale=E3=81=AE=E7=A9=BA?= =?UTF-8?q?=E6=96=87=E5=AD=97=E3=81=AF=E9=A0=85=E7=9B=AE=E3=81=94=E3=81=A8?= =?UTF-8?q?=E6=B6=88=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/locales/index.js b/locales/index.js index 7801f1275b..67a406d98d 100644 --- a/locales/index.js +++ b/locales/index.js @@ -53,6 +53,19 @@ const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g') const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {}); +// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す +const removeEmpty = (obj) => { + for (const [k, v] of Object.entries(obj)) { + if (v === '') { + delete obj[k]; + } else if (typeof v === 'object') { + removeEmpty(v); + } + } + return obj; +}; +removeEmpty(locales); + export default Object.entries(locales) .reduce((a, [k ,v]) => (a[k] = (() => { const [lang] = k.split('-'); @@ -63,7 +76,7 @@ export default Object.entries(locales) default: return merge( locales['ja-JP'], locales['en-US'], - locales[`${lang}-${primaries[lang]}`] || {}, + locales[`${lang}-${primaries[lang]}`] ?? {}, v ); } From bb76ee2c0ec20cc465ee2647cf9412db379db5fb Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 5 Nov 2023 18:01:51 +0900 Subject: [PATCH 59/60] =?UTF-8?q?enhance(frontend):=20=E6=8A=95=E7=A8=BF?= =?UTF-8?q?=E5=86=85=E3=81=AEunicode=E7=B5=B5=E6=96=87=E5=AD=97=E3=82=82?= =?UTF-8?q?=E3=83=A1=E3=83=8B=E3=83=A5=E3=83=BC=E3=82=92=E5=87=BA=E3=81=9B?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- .../src/components/global/MkEmoji.vue | 35 +++++++++++++++++-- .../global/MkMisskeyFlavoredMarkdown.ts | 2 ++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14785c763b..86bc272187 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,7 @@ - Enhance: プラグインで`Plugin:register_note_view_interruptor`を用いてnoteの代わりにnullを返却することでノートを非表示にできるようになりました - Enhance: AiScript関数`Mk:nyaize()`が追加されました - Enhance: 情報→ツール はナビゲーションバーにツールとして独立した項目になりました -- Enhance: ノート内のカスタム絵文字をクリックすることで、コピーおよびリアクションができるように +- Enhance: ノート内の絵文字をクリックすることで、コピーおよびリアクションができるように - Enhance: その他細かなブラッシュアップ - Fix: 投稿フォームでのユーザー変更がプレビューに反映されない問題を修正 - Fix: ユーザーページの ノート > ファイル付き タブにリプライが表示されてしまう diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index e06549a891..0855f20b8d 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -4,21 +4,28 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle"/> -<span v-else-if="useOsNativeEmojis" :alt="props.emoji" @pointerenter="computeTitle">{{ props.emoji }}</span> +<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/> +<span v-else-if="useOsNativeEmojis" :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ props.emoji }}</span> <span v-else>{{ emoji }}</span> </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, inject } from 'vue'; import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js'; import { defaultStore } from '@/store.js'; import { getEmojiName } from '@/scripts/emojilist.js'; +import * as os from '@/os.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { i18n } from '@/i18n.js'; const props = defineProps<{ emoji: string; + menu?: boolean; + menuReaction?: boolean; }>(); +const react = inject<((name: string) => void) | null>('react', null); + const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native'); @@ -31,6 +38,28 @@ function computeTitle(event: PointerEvent): void { const title = getEmojiName(props.emoji as string) ?? props.emoji as string; (event.target as HTMLElement).title = title; } + +function onClick(ev: MouseEvent) { + if (props.menu) { + os.popupMenu([{ + type: 'label', + text: props.emoji, + }, { + text: i18n.ts.copy, + icon: 'ti ti-copy', + action: () => { + copyToClipboard(props.emoji); + os.success(); + }, + }, ...(props.menuReaction && react ? [{ + text: i18n.ts.doReaction, + icon: 'ti ti-plus', + action: () => { + react(props.emoji); + }, + }] : [])], ev.currentTarget ?? ev.target); + } +} </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index d7e1490502..441731d7ca 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -354,6 +354,8 @@ export default function(props: MfmProps) { return [h(MkEmoji, { key: Math.random(), emoji: token.props.emoji, + menu: props.enableEmojiMenu, + menuReaction: props.enableEmojiMenuReaction, })]; } From f72228f428b62b886421d113379536dba1820ca7 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 5 Nov 2023 18:17:50 +0900 Subject: [PATCH 60/60] 2023.11.0 --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86bc272187..ee98f4ccb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ --> -## 2023.11.0 (unreleased) +## 2023.11.0 ### Note - iOS 16.4未満を使用している場合はiOS 16.4以上にアップデートをお願いします diff --git a/package.json b/package.json index 9d5da3009c..0ed73f56b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2023.11.0-beta.10", + "version": "2023.11.0", "codename": "nasubi", "repository": { "type": "git",