diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-04-11 15:51:07 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-04-11 15:51:07 +0900 |
| commit | 75b28d6782d9e1a37dd40444fbccffdf4331737a (patch) | |
| tree | 1b6a1b33ca19a579d7d24d92fef4bfb05ae1e86e /packages/frontend | |
| parent | Merge pull request #10543 from misskey-dev/develop (diff) | |
| parent | fix(client): noPaging: true with gallery/featured (diff) | |
| download | misskey-75b28d6782d9e1a37dd40444fbccffdf4331737a.tar.gz misskey-75b28d6782d9e1a37dd40444fbccffdf4331737a.tar.bz2 misskey-75b28d6782d9e1a37dd40444fbccffdf4331737a.zip | |
Merge pull request #10578 from misskey-dev/develop
Release: 13.11.2
Diffstat (limited to 'packages/frontend')
19 files changed, 262 insertions, 57 deletions
diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index 23b82a8ac5..5fd21cdf0a 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -77,6 +77,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi createdAt: '2016-12-28T22:49:51.000Z', description: 'I am a cool user!', ffVisibility: 'public', + roles: [], fields: [ { name: 'Website', diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index b3bbeeb51c..dd40bac2cc 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -398,6 +398,7 @@ function toStories(component: string): string { Promise.all([ glob('src/components/global/*.vue'), glob('src/components/MkGalleryPostPreview.vue'), + glob('src/pages/user/home.vue'), ]) .then((globs) => globs.flat()) .then((components) => Promise.all(components.map((component) => { diff --git a/packages/frontend/src/components/MkChannelList.vue b/packages/frontend/src/components/MkChannelList.vue new file mode 100644 index 0000000000..408eab7399 --- /dev/null +++ b/packages/frontend/src/components/MkChannelList.vue @@ -0,0 +1,31 @@ +<template> +<MkPagination :pagination="pagination"> + <template #empty> + <div class="_fullinfo"> + <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <div>{{ i18n.ts.notFound }}</div> + </div> + </template> + + <template #default="{ items }"> + <MkChannelPreview v-for="item in items" :key="item.id" class="_margin" :channel="extractor(item)"/> + </template> +</MkPagination> +</template> + +<script lang="ts" setup> +import MkChannelPreview from '@/components/MkChannelPreview.vue'; +import MkPagination, { Paging } from '@/components/MkPagination.vue'; +import { i18n } from '@/i18n'; + +const props = withDefaults(defineProps<{ + pagination: Paging; + noGap?: boolean; + extractor?: (item: any) => any; +}>(), { + extractor: (item) => item, +}); +</script> + +<style lang="scss" scoped> +</style> diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index 1834224b8d..a6372b7b6f 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -82,6 +82,7 @@ export default defineComponent({ omitted: null, ignoreOmit: false, defaultStore, + i18n, }; }, mounted() { diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 0ae182ce32..9eaf16374b 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -439,7 +439,6 @@ defineExpose({ &.asDrawer { width: 100% !important; - padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; > .emojis { ::v-deep(section) { @@ -498,6 +497,10 @@ defineExpose({ background: transparent; color: var(--fg); + &:not(:focus):not(.filled) { + margin-bottom: env(safe-area-inset-bottom, 0px); + } + &:not(.filled) { order: 1; z-index: 2; diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 10cb7d96cc..42a3748d9a 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -1124,16 +1124,16 @@ defineExpose({ display: grid; grid-auto-flow: row; grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); - grid-auto-rows: 46px; + grid-auto-rows: 40px; } .footerRight { - flex: 0.3; + flex: 0; margin-left: auto; display: grid; grid-auto-flow: row; grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); - grid-auto-rows: 46px; + grid-auto-rows: 40px; direction: rtl; } @@ -1198,13 +1198,21 @@ defineExpose({ } } -@container (max-width: 330px) { - .headerRight { - gap: 0; +@container (max-width: 350px) { + .footer { + font-size: 0.9em; } - .footer { - font-size: 14px; + .footerLeft { + grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); + } + + .footerRight { + grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); + } + + .headerRight { + gap: 0; } } </style> diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index 5799f99d5f..aa975600f0 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -83,7 +83,7 @@ const choseAd = (): Ad | null => { }; const chosen = ref(choseAd()); -const shouldHide = $ref($i && $i.policies.canHideAds && (props.specify == null)); +const shouldHide = $ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null)); function reduceFrequency(): void { if (chosen.value == null) return; diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue index 3a5aa00c5b..bc6a6e4952 100644 --- a/packages/frontend/src/pages/channels.vue +++ b/packages/frontend/src/pages/channels.vue @@ -2,6 +2,23 @@ <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :content-max="700"> + <div v-if="tab === 'search'"> + <div class="_gaps"> + <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search"> + <template #prefix><i class="ti ti-search"></i></template> + </MkInput> + <MkRadios v-model="searchType" @update:model-value="search()"> + <option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option> + <option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option> + </MkRadios> + <MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton> + </div> + + <MkFoldableSection v-if="channelPagination"> + <template #header>{{ i18n.ts.searchResult }}</template> + <MkChannelList :key="key" :pagination="channelPagination"/> + </MkFoldableSection> + </div> <div v-if="tab === 'featured'"> <MkPagination v-slot="{items}" :pagination="featuredPagination"> <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> @@ -28,17 +45,35 @@ </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, onMounted } from 'vue'; import MkChannelPreview from '@/components/MkChannelPreview.vue'; +import MkChannelList from '@/components/MkChannelList.vue'; import MkPagination from '@/components/MkPagination.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkRadios from '@/components/MkRadios.vue'; import MkButton from '@/components/MkButton.vue'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; import { useRouter } from '@/router'; import { definePageMetadata } from '@/scripts/page-metadata'; import { i18n } from '@/i18n'; const router = useRouter(); -let tab = $ref('featured'); +const props = defineProps<{ + query: string; + type?: string; +}>(); + +let key = $ref(''); +let tab = $ref('search'); +let searchQuery = $ref(''); +let searchType = $ref('nameAndDescription'); +let channelPagination = $ref(); + +onMounted(() => { + searchQuery = props.query ?? ''; + searchType = props.type ?? 'nameAndDescription'; +}); const featuredPagination = { endpoint: 'channels/featured' as const, @@ -58,6 +93,25 @@ const ownedPagination = { limit: 10, }; +async function search() { + const query = searchQuery.toString().trim(); + + if (query == null || query === '') return; + + const type = searchType.toString().trim(); + + channelPagination = { + endpoint: 'channels/search', + limit: 10, + params: { + query: searchQuery, + type: type, + }, + }; + + key = query + type; +} + function create() { router.push('/channels/new'); } @@ -69,6 +123,10 @@ const headerActions = $computed(() => [{ }]); const headerTabs = $computed(() => [{ + key: 'search', + title: i18n.ts.search, + icon: 'ti ti-search', +}, { key: 'featured', title: i18n.ts._channel.featured, icon: 'ti ti-comet', diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue index de8f448da1..fc9cc7ae9e 100644 --- a/packages/frontend/src/pages/gallery/index.vue +++ b/packages/frontend/src/pages/gallery/index.vue @@ -66,7 +66,7 @@ const recentPostsPagination = { }; const popularPostsPagination = { endpoint: 'gallery/featured' as const, - limit: 5, + noPaging: true, }; const myPostsPagination = { endpoint: 'i/gallery/posts' as const, diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue index 955d812154..599d6329e2 100644 --- a/packages/frontend/src/pages/settings/apps.vue +++ b/packages/frontend/src/pages/settings/apps.vue @@ -8,27 +8,29 @@ </div> </template> <template #default="{items}"> - <div v-for="token in items" :key="token.id" class="_panel bfomjevm"> - <img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/> - <div class="body"> - <div class="name">{{ token.name }}</div> - <div class="description">{{ token.description }}</div> - <MkKeyValue oneline> - <template #key>{{ i18n.ts.installedDate }}</template> - <template #value><MkTime :time="token.createdAt"/></template> - </MkKeyValue> - <MkKeyValue oneline> - <template #key>{{ i18n.ts.lastUsedDate }}</template> - <template #value><MkTime :time="token.lastUsedAt"/></template> - </MkKeyValue> - <details> - <summary>{{ i18n.ts.details }}</summary> - <ul> - <li v-for="p in token.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li> - </ul> - </details> - <div class="actions"> - <MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton> + <div class="_gaps"> + <div v-for="token in items" :key="token.id" class="_panel bfomjevm"> + <img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/> + <div class="body"> + <div class="name">{{ token.name }}</div> + <div class="description">{{ token.description }}</div> + <MkKeyValue oneline> + <template #key>{{ i18n.ts.installedDate }}</template> + <template #value><MkTime :time="token.createdAt"/></template> + </MkKeyValue> + <MkKeyValue oneline> + <template #key>{{ i18n.ts.lastUsedDate }}</template> + <template #value><MkTime :time="token.lastUsedAt"/></template> + </MkKeyValue> + <details> + <summary>{{ i18n.ts.details }}</summary> + <ul> + <li v-for="p in token.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li> + </ul> + </details> + <div class="actions"> + <MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton> + </div> </div> </div> </div> @@ -51,6 +53,7 @@ const list = ref<any>(null); const pagination = { endpoint: 'i/apps' as const, limit: 100, + noPaging: true, params: { sort: '+lastUsedAt', }, diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index f88e934e1d..904fd3f952 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -61,6 +61,7 @@ <MkSwitch v-model="squareAvatars">{{ i18n.ts.squareAvatars }}</MkSwitch> <MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch> <MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch> + <MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch> </div> <div> <MkRadios v-model="emojiStyle"> @@ -157,6 +158,7 @@ const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm')); const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle')); const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer')); const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages')); +const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds')); const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages')); const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab')); const nsfw = computed(defaultStore.makeGetterSetter('nsfw')); diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index 28e39236f7..092b9a9cc8 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -399,7 +399,7 @@ function menu(ev: MouseEvent, profileId: string) { icon: 'ti ti-device-floppy', action: () => save(profileId), }, null, { - text: ts._preferencesBackups.delete, + text: ts.delete, icon: 'ti ti-trash', action: () => deleteProfile(profileId), danger: true, diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue index bc729ab871..4f702758f4 100644 --- a/packages/frontend/src/pages/settings/webhook.vue +++ b/packages/frontend/src/pages/settings/webhook.vue @@ -7,18 +7,20 @@ <FormSection> <MkPagination :pagination="pagination"> <template #default="{items}"> - <FormLink v-for="webhook in items" :key="webhook.id" :to="`/settings/webhook/edit/${webhook.id}`" class="_margin"> - <template #icon> - <i v-if="webhook.active === false" class="ti ti-player-pause"></i> - <i v-else-if="webhook.latestStatus === null" class="ti ti-circle"></i> - <i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--success)' }"></i> - <i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"></i> - </template> - {{ webhook.name || webhook.url }} - <template #suffix> - <MkTime v-if="webhook.latestSentAt" :time="webhook.latestSentAt"></MkTime> - </template> - </FormLink> + <div class="_gaps"> + <FormLink v-for="webhook in items" :key="webhook.id" :to="`/settings/webhook/edit/${webhook.id}`"> + <template #icon> + <i v-if="webhook.active === false" class="ti ti-player-pause"></i> + <i v-else-if="webhook.latestStatus === null" class="ti ti-circle"></i> + <i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--success)' }"></i> + <i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"></i> + </template> + {{ webhook.name || webhook.url }} + <template #suffix> + <MkTime v-if="webhook.latestSentAt" :time="webhook.latestSentAt"></MkTime> + </template> + </FormLink> + </div> </template> </MkPagination> </FormSection> @@ -35,7 +37,8 @@ import { i18n } from '@/i18n'; const pagination = { endpoint: 'i/webhooks/list' as const, - limit: 10, + limit: 100, + noPaging: true, }; const headerActions = $computed(() => []); diff --git a/packages/frontend/src/pages/user/home.stories.impl.ts b/packages/frontend/src/pages/user/home.stories.impl.ts new file mode 100644 index 0000000000..50da0c6991 --- /dev/null +++ b/packages/frontend/src/pages/user/home.stories.impl.ts @@ -0,0 +1,74 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { StoryObj } from '@storybook/vue3'; +import { rest } from 'msw'; +import { userDetailed } from '../../../.storybook/fakes'; +import { commonHandlers } from '../../../.storybook/mocks'; +import home_ from './home.vue'; +export const Default = { + render(args) { + return { + components: { + home_, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<home_ v-bind="props" />', + }; + }, + args: { + user: userDetailed(), + disableNotes: false, + }, + parameters: { + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + rest.post('/api/users/notes', (req, res, ctx) => { + return res(ctx.json([])); + }), + rest.get('/api/charts/user/notes', (req, res, ctx) => { + const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300); + return res(ctx.json({ + total: Array.from({ length }, () => 0), + inc: Array.from({ length }, () => 0), + dec: Array.from({ length }, () => 0), + diffs: { + normal: Array.from({ length }, () => 0), + reply: Array.from({ length }, () => 0), + renote: Array.from({ length }, () => 0), + withFile: Array.from({ length }, () => 0), + }, + })); + }), + rest.get('/api/charts/user/pv', (req, res, ctx) => { + const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300); + return res(ctx.json({ + upv: { + user: Array.from({ length }, () => 0), + visitor: Array.from({ length }, () => 0), + }, + pv: { + user: Array.from({ length }, () => 0), + visitor: Array.from({ length }, () => 0), + }, + })); + }), + ], + }, + chromatic: { + // `XActivity` is not compatible with Chromatic for now + disableSnapshot: true, + }, + }, +} satisfies StoryObj<typeof home_>; diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index e5558829d4..0728fc84e5 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -298,6 +298,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, + forceShowAds: { + where: 'device', + default: false, + }, aiChanMode: { where: 'device', default: false, diff --git a/packages/frontend/src/ui/_common_/sw-inject.ts b/packages/frontend/src/ui/_common_/sw-inject.ts index a92a06bd3e..c97e2b567b 100644 --- a/packages/frontend/src/ui/_common_/sw-inject.ts +++ b/packages/frontend/src/ui/_common_/sw-inject.ts @@ -1,17 +1,18 @@ -import { post } from '@/os'; +import { api, post } from '@/os'; import { $i, login } from '@/account'; import { getAccountFromId } from '@/scripts/get-account-from-id'; import { mainRouter } from '@/router'; +import { deepClone } from '@/scripts/clone'; export function swInject() { - navigator.serviceWorker.addEventListener('message', ev => { + navigator.serviceWorker.addEventListener('message', async ev => { if (_DEV_) { console.log('sw msg', ev.data); } if (ev.data.type !== 'order') return; - if (ev.data.loginId !== $i?.id) { + if (ev.data.loginId && ev.data.loginId !== $i?.id) { return getAccountFromId(ev.data.loginId).then(account => { if (!account) return; return login(account.token, ev.data.url); @@ -19,8 +20,18 @@ export function swInject() { } switch (ev.data.order) { - case 'post': - return post(ev.data.options); + case 'post': { + const props = deepClone(ev.data.options); + // プッシュ通知から来たreply,renoteはtruncateBodyが通されているため、 + // 完全なノートを取得しなおす + if (props.reply) { + props.reply = await api('notes/show', { noteId: props.reply.id }); + } + if (props.renote) { + props.renote = await api('notes/show', { noteId: props.renote.id }); + } + return post(props); + } case 'push': if (mainRouter.currentRoute.value.path === ev.data.url) { return window.scroll({ top: 0, behavior: 'smooth' }); diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue index 4838272a9e..792c1ccc5e 100644 --- a/packages/frontend/src/ui/classic.vue +++ b/packages/frontend/src/ui/classic.vue @@ -250,6 +250,7 @@ onMounted(() => { > .widgets { //--panelBorder: none; width: 300px; + padding-bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px)); @media (max-width: $widgets-hide-threshold) { display: none; @@ -304,7 +305,7 @@ onMounted(() => { right: 0; z-index: 1001; height: 100dvh; - padding: var(--margin); + padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)); box-sizing: border-box; overflow: auto; background: var(--bg); diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index ab3d01532b..2462967515 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -296,7 +296,7 @@ $widgets-hide-threshold: 1090px; } .widgets { - padding: 0 var(--margin); + padding: 0 var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)); border-left: solid 0.5px var(--divider); background: var(--bg); @@ -329,7 +329,7 @@ $widgets-hide-threshold: 1090px; right: 0; z-index: 1001; height: 100dvh; - padding: var(--margin) !important; + padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)) !important; box-sizing: border-box; overflow: auto; overscroll-behavior: contain; diff --git a/packages/frontend/src/ui/universal.widgets.vue b/packages/frontend/src/ui/universal.widgets.vue index 2e557d30ce..d11649c603 100644 --- a/packages/frontend/src/ui/universal.widgets.vue +++ b/packages/frontend/src/ui/universal.widgets.vue @@ -3,7 +3,7 @@ <XWidgets :class="$style.widgets" :edit="editMode" :widgets="widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/> <button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button> - <button v-else class="_textButton mk-widget-edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button> + <button v-else class="_textButton mk-widget-edit" :class="$style.edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button> </div> </template> @@ -91,4 +91,8 @@ function updateWidgets(thisWidgets) { .widgets { width: 300px; } + +.edit { + width: 100%; +} </style> |