feat(#8149): respect nsfw settings on gallery list (#10481)

* feat(#8149): respect nsfw settings on gallery list

* ci(#10336): use pull_request

* test(#8149): add interaction tests

* test(#10336): use `waitFor`

* chore: transition
This commit is contained in:
Acid Chicken (硫酸鶏) 2023-04-06 08:19:49 +09:00 committed by GitHub
parent 516a791bf4
commit 3b3f683f8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 254 additions and 76 deletions

View file

@ -1,54 +1,116 @@
import type { entities } from 'misskey-js' import type { entities } from 'misskey-js'
export const userDetailed = { export function abuseUserReport() {
id: 'someuserid', return {
username: 'miskist', id: 'someabusereportid',
host: 'misskey-hub.net', createdAt: '2016-12-28T22:49:51.000Z',
name: 'Misskey User', comment: 'This user is a spammer!',
onlineStatus: 'unknown', resolved: false,
avatarUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true', reporterId: 'reporterid',
avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay', targetUserId: 'targetuserid',
emojis: [], assigneeId: 'assigneeid',
bannerBlurhash: 'eQA^IW^-MH8w9tE8I=S^o{$*R4RikXtSxutRozjEnNR.RQadoyozog', reporter: userDetailed('reporterid', 'reporter', 'misskey-hub.net', 'Reporter'),
bannerColor: '#000000', targetUser: userDetailed('targetuserid', 'target', 'misskey-hub.net', 'Target'),
bannerUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true', assignee: userDetailed('assigneeid', 'assignee', 'misskey-hub.net', 'Assignee'),
birthday: '2014-06-20', me: null,
createdAt: '2016-12-28T22:49:51.000Z', forwarded: false,
description: 'I am a cool user!', };
ffVisibility: 'public', }
fields: [
{ export function galleryPost(isSensitive = false) {
name: 'Website', return {
value: 'https://misskey-hub.net', 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
}, },
], url: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true',
followersCount: 1024, thumbnailUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true',
followingCount: 16, comment: null,
hasPendingFollowRequestFromYou: false, folderId: null,
hasPendingFollowRequestToYou: false, folder: null,
isAdmin: false, userId: null,
isBlocked: false, user: null,
isBlocking: false, };
isBot: false, }
isCat: false,
isFollowed: false, export function userDetailed(id = 'someuserid', username = 'miskist', host = 'misskey-hub.net', name = 'Misskey User'): entities.UserDetailed {
isFollowing: false, return {
isLocked: false, id,
isModerator: false, username,
isMuted: false, host,
isSilenced: false, name,
isSuspended: false, onlineStatus: 'unknown',
lang: 'en', avatarUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true',
location: 'Fediverse', avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay',
notesCount: 65536, emojis: [],
pinnedNoteIds: [], bannerBlurhash: 'eQA^IW^-MH8w9tE8I=S^o{$*R4RikXtSxutRozjEnNR.RQadoyozog',
pinnedNotes: [], bannerColor: '#000000',
pinnedPage: null, bannerUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true',
pinnedPageId: null, birthday: '2014-06-20',
publicReactions: false, createdAt: '2016-12-28T22:49:51.000Z',
securityKeys: false, description: 'I am a cool user!',
twoFactorEnabled: false, ffVisibility: 'public',
updatedAt: null, fields: [
uri: null, {
url: null, name: 'Website',
} satisfies entities.UserDetailed 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,
};
}

View file

@ -394,13 +394,13 @@ function toStories(component: string): string {
); );
} }
// glob('src/{components,pages,ui,widgets}/**/*.vue').then( // glob('src/{components,pages,ui,widgets}/**/*.vue')
glob('src/components/global/**/*.vue').then( Promise.all([
(components) => glob('src/components/global/*.vue'),
Promise.all( glob('src/components/MkGalleryPostPreview.vue'),
components.map((component) => { ])
const stories = component.replace(/\.vue$/, '.stories.ts'); .then((globs) => globs.flat())
return writeFile(stories, toStories(component)); .then((components) => Promise.all(components.map((component) => {
}) const stories = component.replace(/\.vue$/, '.stories.ts');
) return writeFile(stories, toStories(component));
); })));

View file

@ -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: '<MkGalleryPostPreview v-bind="props" />',
};
},
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: '<div style="width:260px"><story /></div>',
}),
],
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkGalleryPostPreview>;
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<typeof MkGalleryPostPreview>;
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<typeof MkGalleryPostPreview>;
export const Sensitive = {
...Default,
args: {
...Default.args,
post: galleryPost(true),
},
} satisfies StoryObj<typeof MkGalleryPostPreview>;
export const SensitiveHover = {
...Hover,
args: {
...Hover.args,
post: galleryPost(true),
},
} satisfies StoryObj<typeof MkGalleryPostPreview>;
export const SensitiveHoverThenUnhover = {
...HoverThenUnhover,
args: {
...HoverThenUnhover.args,
post: galleryPost(true),
},
} satisfies StoryObj<typeof MkGalleryPostPreview>;

View file

@ -1,7 +1,10 @@
<template> <template>
<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel" tabindex="-1"> <MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel" tabindex="-1" @pointerenter="enterHover" @pointerleave="leaveHover">
<div class="thumbnail"> <div class="thumbnail">
<ImgWithBlurhash class="img" :src="post.files[0].thumbnailUrl" :hash="post.files[0].blurhash"/> <ImgWithBlurhash class="img" :hash="post.files[0].blurhash"/>
<Transition>
<ImgWithBlurhash v-if="show" class="img layered" :src="post.files[0].thumbnailUrl" :hash="post.files[0].blurhash"/>
</Transition>
</div> </div>
<article> <article>
<header> <header>
@ -15,12 +18,25 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import * as misskey from 'misskey-js';
import { computed, ref } from 'vue';
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
import { defaultStore } from '@/store';
const props = defineProps<{ const props = defineProps<{
post: any; post: misskey.entities.GalleryPost;
}>(); }>();
const hover = ref(false);
const show = computed(() => defaultStore.state.nsfw === 'ignore' || defaultStore.state.nsfw === 'respect' && !props.post.isSensitive || hover.value);
function enterHover(): void {
hover.value = true;
}
function leaveHover(): void {
hover.value = false;
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -56,6 +72,21 @@ const props = defineProps<{
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
&.layered {
position: absolute;
top: 0;
&.v-enter-active,
&.v-leave-active {
transition: opacity 0.5s ease;
}
&.v-enter-from,
&.v-leave-to {
opacity: 0;
}
}
} }
} }

View file

@ -25,7 +25,7 @@ export const Default = {
}, },
args: { args: {
user: { user: {
...userDetailed, ...userDetailed(),
host: null, host: null,
}, },
}, },
@ -37,7 +37,7 @@ export const Detail = {
...Default, ...Default,
args: { args: {
...Default.args, ...Default.args,
user: userDetailed, user: userDetailed(),
detail: true, detail: true,
}, },
} satisfies StoryObj<typeof MkAcct>; } satisfies StoryObj<typeof MkAcct>;

View file

@ -24,7 +24,7 @@ const common = {
}; };
}, },
args: { args: {
user: userDetailed, user: userDetailed(),
}, },
decorators: [ decorators: [
(Story, context) => ({ (Story, context) => ({
@ -49,7 +49,7 @@ export const ProfilePageCat = {
args: { args: {
...ProfilePage.args, ...ProfilePage.args,
user: { user: {
...userDetailed, ...userDetailed(),
isCat: true, isCat: true,
}, },
}, },

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from '@storybook/jest'; import { expect } from '@storybook/jest';
import { userEvent, within } from '@storybook/testing-library'; import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw'; import { rest } from 'msw';
import { commonHandlers } from '../../../.storybook/mocks'; import { commonHandlers } from '../../../.storybook/mocks';
@ -30,7 +30,7 @@ export const Default = {
const canvas = within(canvasElement); const canvas = within(canvasElement);
const a = canvas.getByRole<HTMLAnchorElement>('link'); const a = canvas.getByRole<HTMLAnchorElement>('link');
await expect(a).toHaveAttribute('href', 'https://misskey-hub.net/'); await expect(a).toHaveAttribute('href', 'https://misskey-hub.net/');
await userEvent.hover(a); await waitFor(() => userEvent.hover(a));
/* /*
await tick(); // FIXME: wait for network request await tick(); // FIXME: wait for network request
const anchors = canvas.getAllByRole<HTMLAnchorElement>('link'); const anchors = canvas.getAllByRole<HTMLAnchorElement>('link');
@ -44,7 +44,7 @@ export const Default = {
await expect(icon).toBeInTheDocument(); await expect(icon).toBeInTheDocument();
await expect(icon).toHaveAttribute('src', 'https://misskey-hub.net/favicon.ico'); await expect(icon).toHaveAttribute('src', 'https://misskey-hub.net/favicon.ico');
*/ */
await userEvent.unhover(a); await waitFor(() => userEvent.unhover(a));
}, },
args: { args: {
url: 'https://misskey-hub.net/', url: 'https://misskey-hub.net/',

View file

@ -26,10 +26,10 @@ export const Default = {
}; };
}, },
async play({ canvasElement }) { async play({ canvasElement }) {
await expect(canvasElement).toHaveTextContent(userDetailed.name); await expect(canvasElement).toHaveTextContent(userDetailed().name);
}, },
args: { args: {
user: userDetailed, user: userDetailed(),
}, },
parameters: { parameters: {
layout: 'centered', layout: 'centered',
@ -38,12 +38,12 @@ export const Default = {
export const Anonymous = { export const Anonymous = {
...Default, ...Default,
async play({ canvasElement }) { async play({ canvasElement }) {
await expect(canvasElement).toHaveTextContent(userDetailed.username); await expect(canvasElement).toHaveTextContent(userDetailed().username);
}, },
args: { args: {
...Default.args, ...Default.args,
user: { user: {
...userDetailed, ...userDetailed(),
name: null, name: null,
}, },
}, },