diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index eda28c5f77..9659757c49 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -5,7 +5,9 @@ on: branches: - master - develop - pull_request_target: + pull_request: + branches-ignore: + - l10n_develop jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index c3add38674..d031d4f85e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ ### Client - 検索ページでURLを入力した際に照会したときと同等の挙動をするように - ノートのリアクションを大きく表示するオプションを追加 +- ギャラリー一覧にメディア表示と同じように NSFW 設定を反映するように(ホバーで表示) - オブジェクトストレージの設定画面を分かりやすく - 「にゃああああああああああああああ!!!!!!!!!!!!」 (`isCat`) 有効時にアバターに表示される猫耳について挙動を変更 - 「UIにぼかし効果を使用」 (`useBlurEffect`) で次の挙動が有効になります diff --git a/README.md b/README.md index c12882ca32..fdc8c1d8d3 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,17 @@ With Misskey's built in drive, you get cloud storage right in your social media, Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/), some of the links and graphics above also lead to specific portions of it. ## Sponsors +
RSS3
+ +## Thanks + +Chromatic + +Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions. + +Docker + +Thanks to [Docker](https://hub.docker.com/) for providing the container platform that helps us run Misskey in production. diff --git a/package.json b/package.json index 8506d8aa29..52f0c50a52 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.10.3", + "version": "13.11.0.beta-1", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index 366dc08c02..c44dddea41 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -43,6 +43,7 @@ export class NotificationService implements OnApplicationShutdown { @bindThis public async readAllNotification( userId: User['id'], + force = false, ) { const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`); @@ -57,7 +58,7 @@ export class NotificationService implements OnApplicationShutdown { this.redisClient.set(`latestReadNotification:${userId}`, latestNotificationId); - if (latestReadNotificationId == null || (latestReadNotificationId < latestNotificationId)) { + if (force || latestReadNotificationId == null || (latestReadNotificationId < latestNotificationId)) { return this.postReadAllNotifications(userId); } } @@ -95,7 +96,7 @@ export class NotificationService implements OnApplicationShutdown { ...data, } as Notification; - this.redisClient.xadd( + const redisIdPromise = this.redisClient.xadd( `notificationTimeline:${notifieeId}`, 'MAXLEN', '~', '300', `${this.idService.parse(notification.id).date.getTime()}-*`, @@ -109,7 +110,7 @@ export class NotificationService implements OnApplicationShutdown { // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => { const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`); - if (latestReadNotificationId && (latestReadNotificationId >= notification.id)) return; + if (latestReadNotificationId && (latestReadNotificationId >= await redisIdPromise)) return; this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed); this.pushNotificationService.pushNotification(notifieeId, 'notification', packed); diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index ed96e9a525..cf78d8330c 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -86,6 +86,10 @@ export class ImportCustomEmojisProcessorService { continue; } const emojiInfo = record.emoji; + if (!/^[a-zA-Z0-9_]+$/.test(emojiInfo.name)) { + this.logger.error(`invalid emojiname: ${emojiInfo.name}`); + continue; + } const emojiPath = outputPath + '/' + record.fileName; await this.emojisRepository.delete({ name: emojiInfo.name, diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts index 9ba6079189..e601bf9d5b 100644 --- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts @@ -24,7 +24,7 @@ export default class extends Endpoint { private notificationService: NotificationService, ) { super(meta, paramDef, async (ps, me) => { - this.notificationService.readAllNotification(me.id); + this.notificationService.readAllNotification(me.id, true); }); } } diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index b620cf68a3..23b82a8ac5 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -1,54 +1,116 @@ import type { entities } from 'misskey-js' -export const userDetailed = { - id: 'someuserid', - username: 'miskist', - host: 'misskey-hub.net', - name: 'Misskey User', - onlineStatus: 'unknown', - avatarUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true', - avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay', - emojis: [], - bannerBlurhash: 'eQA^IW^-MH8w9tE8I=S^o{$*R4RikXtSxutRozjEnNR.RQadoyozog', - bannerColor: '#000000', - bannerUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true', - birthday: '2014-06-20', - createdAt: '2016-12-28T22:49:51.000Z', - description: 'I am a cool user!', - ffVisibility: 'public', - fields: [ - { - name: 'Website', - value: 'https://misskey-hub.net', +export function abuseUserReport() { + return { + id: 'someabusereportid', + createdAt: '2016-12-28T22:49:51.000Z', + comment: 'This user is a spammer!', + resolved: false, + reporterId: 'reporterid', + targetUserId: 'targetuserid', + assigneeId: 'assigneeid', + reporter: userDetailed('reporterid', 'reporter', 'misskey-hub.net', 'Reporter'), + targetUser: userDetailed('targetuserid', 'target', 'misskey-hub.net', 'Target'), + assignee: userDetailed('assigneeid', 'assignee', 'misskey-hub.net', 'Assignee'), + me: null, + forwarded: false, + }; +} + +export function galleryPost(isSensitive = false) { + return { + id: 'somepostid', + createdAt: '2016-12-28T22:49:51.000Z', + updatedAt: '2016-12-28T22:49:51.000Z', + userid: 'someuserid', + user: userDetailed(), + title: 'Some post title', + description: 'Some post description', + fileIds: ['somefileid'], + files: [ + file(isSensitive), + ], + isSensitive, + likedCount: 0, + isLiked: false, + } +} + +export function file(isSensitive = false) { + return { + id: 'somefileid', + createdAt: '2016-12-28T22:49:51.000Z', + name: 'somefile.jpg', + type: 'image/jpeg', + md5: 'f6fc51c73dc21b1fb85ead2cdf57530a', + size: 77752, + isSensitive, + blurhash: 'eQAmoa^-MH8w9ZIvNLSvo^$*MwRPbwtSxutRozjEiwR.RjWBoeozog', + properties: { + width: 1024, + height: 270 }, - ], - followersCount: 1024, - followingCount: 16, - hasPendingFollowRequestFromYou: false, - hasPendingFollowRequestToYou: false, - isAdmin: false, - isBlocked: false, - isBlocking: false, - isBot: false, - isCat: false, - isFollowed: false, - isFollowing: false, - isLocked: false, - isModerator: false, - isMuted: false, - isSilenced: false, - isSuspended: false, - lang: 'en', - location: 'Fediverse', - notesCount: 65536, - pinnedNoteIds: [], - pinnedNotes: [], - pinnedPage: null, - pinnedPageId: null, - publicReactions: false, - securityKeys: false, - twoFactorEnabled: false, - updatedAt: null, - uri: null, - url: null, -} satisfies entities.UserDetailed + url: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true', + thumbnailUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true', + comment: null, + folderId: null, + folder: null, + userId: null, + user: null, + }; +} + +export function userDetailed(id = 'someuserid', username = 'miskist', host = 'misskey-hub.net', name = 'Misskey User'): entities.UserDetailed { + return { + id, + username, + host, + name, + onlineStatus: 'unknown', + avatarUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true', + avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay', + emojis: [], + bannerBlurhash: 'eQA^IW^-MH8w9tE8I=S^o{$*R4RikXtSxutRozjEnNR.RQadoyozog', + bannerColor: '#000000', + bannerUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true', + birthday: '2014-06-20', + createdAt: '2016-12-28T22:49:51.000Z', + description: 'I am a cool user!', + ffVisibility: 'public', + fields: [ + { + name: 'Website', + value: 'https://misskey-hub.net', + }, + ], + followersCount: 1024, + followingCount: 16, + hasPendingFollowRequestFromYou: false, + hasPendingFollowRequestToYou: false, + isAdmin: false, + isBlocked: false, + isBlocking: false, + isBot: false, + isCat: false, + isFollowed: false, + isFollowing: false, + isLocked: false, + isModerator: false, + isMuted: false, + isSilenced: false, + isSuspended: false, + lang: 'en', + location: 'Fediverse', + notesCount: 65536, + pinnedNoteIds: [], + pinnedNotes: [], + pinnedPage: null, + pinnedPageId: null, + publicReactions: false, + securityKeys: false, + twoFactorEnabled: false, + updatedAt: null, + uri: null, + url: null, + }; +} diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index f0865fcc24..b3bbeeb51c 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -394,13 +394,13 @@ function toStories(component: string): string { ); } -// glob('src/{components,pages,ui,widgets}/**/*.vue').then( -glob('src/components/global/**/*.vue').then( - (components) => - Promise.all( - components.map((component) => { - const stories = component.replace(/\.vue$/, '.stories.ts'); - return writeFile(stories, toStories(component)); - }) - ) -); +// glob('src/{components,pages,ui,widgets}/**/*.vue') +Promise.all([ + glob('src/components/global/*.vue'), + glob('src/components/MkGalleryPostPreview.vue'), +]) + .then((globs) => globs.flat()) + .then((components) => Promise.all(components.map((component) => { + const stories = component.replace(/\.vue$/, '.stories.ts'); + return writeFile(stories, toStories(component)); + }))); diff --git a/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts b/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts new file mode 100644 index 0000000000..e46a708192 --- /dev/null +++ b/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts @@ -0,0 +1,85 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { expect } from '@storybook/jest'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; +import { StoryObj } from '@storybook/vue3'; +import { galleryPost } from '../../.storybook/fakes'; +import MkGalleryPostPreview from './MkGalleryPostPreview.vue'; +export const Default = { + render(args) { + return { + components: { + MkGalleryPostPreview, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '', + }; + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const links = canvas.getAllByRole('link'); + await expect(links).toHaveLength(2); + await expect(links[0]).toHaveAttribute('href', `/gallery/${galleryPost().id}`); + await expect(links[1]).toHaveAttribute('href', `/@${galleryPost().user.username}@${galleryPost().user.host}`); + }, + args: { + post: galleryPost(), + }, + decorators: [ + () => ({ + template: '
', + }), + ], + parameters: { + layout: 'centered', + }, +} satisfies StoryObj; +export const Hover = { + ...Default, + async play(context) { + await Default.play(context); + const canvas = within(context.canvasElement); + const links = canvas.getAllByRole('link'); + await waitFor(() => userEvent.hover(links[0])); + }, +} satisfies StoryObj; +export const HoverThenUnhover = { + ...Default, + async play(context) { + await Hover.play(context); + const canvas = within(context.canvasElement); + const links = canvas.getAllByRole('link'); + await waitFor(() => userEvent.unhover(links[0])); + }, +} satisfies StoryObj; +export const Sensitive = { + ...Default, + args: { + ...Default.args, + post: galleryPost(true), + }, +} satisfies StoryObj; +export const SensitiveHover = { + ...Hover, + args: { + ...Hover.args, + post: galleryPost(true), + }, +} satisfies StoryObj; +export const SensitiveHoverThenUnhover = { + ...HoverThenUnhover, + args: { + ...HoverThenUnhover.args, + post: galleryPost(true), + }, +} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue index 2c5032119f..944f5ad97b 100644 --- a/packages/frontend/src/components/MkGalleryPostPreview.vue +++ b/packages/frontend/src/components/MkGalleryPostPreview.vue @@ -1,7 +1,10 @@