diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-07-21 20:36:07 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-07-21 20:36:07 +0900 |
| commit | e64a81aa1d2801516e8eac8dc69aac540489f20b (patch) | |
| tree | 56accbc0f5f71db864e1e975920135fb0a957291 /packages/frontend/src/pages | |
| parent | Merge pull request #10990 from misskey-dev/develop (diff) | |
| parent | New Crowdin updates (#11336) (diff) | |
| download | misskey-e64a81aa1d2801516e8eac8dc69aac540489f20b.tar.gz misskey-e64a81aa1d2801516e8eac8dc69aac540489f20b.tar.bz2 misskey-e64a81aa1d2801516e8eac8dc69aac540489f20b.zip | |
Merge pull request #11301 from misskey-dev/develop
Release: 13.14.0
Diffstat (limited to 'packages/frontend/src/pages')
53 files changed, 686 insertions, 154 deletions
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index 0017145fa1..6d2f7e155e 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -88,10 +88,13 @@ <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="200" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a> + <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="dcadvirth" title="DC Advirth" href="https://www.dotchain.ltd/advirth" target="_blank"><img width="200" src="https://misskey-hub.net/sponsors/dcadvirth.png" alt="DC Advirth"></a> + <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> @@ -155,6 +158,30 @@ const patronsWithIcon = [{ }, { name: 'spinlock', icon: 'https://misskey-hub.net/patrons/6a1cebc819d540a78bf20e9e3115baa8.jpg', +}, { + name: 'じゅくま', + icon: 'https://misskey-hub.net/patrons/3e56bdac69dd42f7a06e0f12cf2fc895.jpg', +}, { + name: '清遊あみ', + icon: 'https://misskey-hub.net/patrons/de25195b88e940a388388bea2e7637d8.jpg', +}, { + name: 'Nagi8410', + icon: 'https://misskey-hub.net/patrons/31b102ab4fc540ed806b0461575d38be.jpg', +}, { + name: '山岡士郎', + icon: 'https://misskey-hub.net/patrons/84b9056341684266bb1eda3e680d094d.jpg', +}, { + name: 'よもやまたろう', + icon: 'https://misskey-hub.net/patrons/4273c9cce50d445f8f7d0f16113d6d7f.jpg', +}, { + name: '花咲ももか', + icon: 'https://misskey-hub.net/patrons/8c9b2b9128cb4fee99f04bb4f86f2efa.jpg', +}, { + name: 'カガミ', + icon: 'https://misskey-hub.net/patrons/226ea3a4617749548580ec2d9a263e24.jpg', +}, { + name: 'フランギ・シュウ', + icon: 'https://misskey-hub.net/patrons/3016d37e35f3430b90420176c912d304.jpg', }]; const patrons = [ @@ -250,6 +277,9 @@ const patrons = [ 'binvinyl', '渡志郎', 'ぷーざ', + '越貝鯛丸', + 'Nick / pprmint.', + 'kino3277', ]; let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure')); diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue index 3744bed10f..cc0bf2eed2 100644 --- a/packages/frontend/src/pages/about.emojis.vue +++ b/packages/frontend/src/pages/about.emojis.vue @@ -20,7 +20,7 @@ <XEmoji v-for="emoji in searchEmojis" :key="emoji.name" :emoji="emoji"/> </div> </MkFoldableSection> - + <MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category"> <template #header>{{ category || i18n.ts.other }}</template> <div :class="$style.emojis"> @@ -56,7 +56,7 @@ function search() { const queryarry = q.match(/\:([a-z0-9_]*)\:/g); if (queryarry) { - searchEmojis = customEmojis.value.filter(emoji => + searchEmojis = customEmojis.value.filter(emoji => queryarry.includes(`:${emoji.name}:`), ); } else { diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue index 24c863ba62..57e50b692c 100644 --- a/packages/frontend/src/pages/admin-file.vue +++ b/packages/frontend/src/pages/admin-file.vue @@ -32,7 +32,7 @@ <MkUserCardMini :user="file.user"/> </MkA> <div> - <MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">NSFW</MkSwitch> + <MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">{{ i18n.ts.sensitive }}</MkSwitch> </div> <div> diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue index 3bc5ee9723..9cf96d3d04 100644 --- a/packages/frontend/src/pages/admin/abuses.vue +++ b/packages/frontend/src/pages/admin/abuses.vue @@ -75,7 +75,7 @@ const pagination = { }; function resolved(reportId) { - reports.removeItem(item => item.id === reportId); + reports.removeItem(reportId); } const headerActions = $computed(() => []); diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue index 2c9e18b0bf..9a5bd88b2e 100644 --- a/packages/frontend/src/pages/admin/ads.vue +++ b/packages/frontend/src/pages/admin/ads.vue @@ -36,6 +36,16 @@ <template #label>{{ i18n.ts.expiration }}</template> </MkInput> </FormSplit> + <MkFolder> + <template #label>{{ i18n.ts.advancedSettings }}</template> + <span> + {{ i18n.ts._ad.timezoneinfo }} + <div v-for="(day, index) in daysOfWeek" :key="index"> + <input :id="`ad${ad.id}-${index}`" type="checkbox" :checked="(ad.dayOfWeek & (1 << index)) !== 0" @change="toggleDayOfWeek(ad, index)"> + <label :for="`ad${ad.id}-${index}`">{{ day }}</label> + </div> + </span> + </MkFolder> <MkTextarea v-model="ad.memo"> <template #label>{{ i18n.ts.memo }}</template> </MkTextarea> @@ -59,6 +69,7 @@ import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkRadios from '@/components/MkRadios.vue'; +import MkFolder from '@/components/MkFolder.vue'; import FormSplit from '@/components/form/split.vue'; import * as os from '@/os'; import { i18n } from '@/i18n'; @@ -69,6 +80,7 @@ let ads: any[] = $ref([]); // ISO形式はTZがUTCになってしまうので、TZ分ずらして時間を初期化 const localTime = new Date(); const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000; +const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday, i18n.ts._weekday.tuesday, i18n.ts._weekday.wednesday, i18n.ts._weekday.thursday, i18n.ts._weekday.friday, i18n.ts._weekday.saturday]; os.api('admin/ad/list').then(adsResponse => { ads = adsResponse.map(r => { @@ -84,6 +96,11 @@ os.api('admin/ad/list').then(adsResponse => { }); }); +// 選択された曜日(index)のビットフラグを操作する +function toggleDayOfWeek(ad, index) { + ad.dayOfWeek ^= 1 << index; +} + function add() { ads.unshift({ id: null, @@ -95,6 +112,7 @@ function add() { imageUrl: null, expiresAt: null, startsAt: null, + dayOfWeek: 0, }); } @@ -105,6 +123,7 @@ function remove(ad) { }).then(({ canceled }) => { if (canceled) return; ads = ads.filter(x => x !== ad); + if (ad.id == null) return; os.apiWithDialog('admin/ad/delete', { id: ad.id, }); diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 8b083bc896..e91f65b5d5 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -1,6 +1,6 @@ <template> <div ref="el" class="hiyeyicy" :class="{ wide: !narrow }"> - <div v-if="!narrow || currentPage?.route.name == null" class="nav"> + <div v-if="!narrow || currentPage?.route.name == null" class="nav"> <MkSpacer :contentMax="700" :marginMin="16"> <div class="lxpfedzu"> <div class="banner"> @@ -80,7 +80,7 @@ const menuDef = $computed(() => [{ }, ...(instance.disableRegistration ? [{ type: 'button', icon: 'ti ti-user-plus', - text: i18n.ts.invite, + text: i18n.ts.createInviteCode, action: invite, }] : [])], }, { @@ -96,6 +96,11 @@ const menuDef = $computed(() => [{ to: '/admin/users', active: currentPage?.route.name === 'users', }, { + icon: 'ti ti-user-plus', + text: i18n.ts.invite, + to: '/admin/invites', + active: currentPage?.route.name === 'invites', + }, { icon: 'ti ti-badges', text: i18n.ts.roles, to: '/admin/roles', @@ -240,10 +245,10 @@ provideMetadataReceiver((info) => { }); const invite = () => { - os.api('invite').then(x => { + os.api('admin/invite/create').then(x => { os.alert({ type: 'info', - text: x.code, + text: x?.[0].code, }); }).catch(err => { os.alert({ diff --git a/packages/frontend/src/pages/admin/invites.vue b/packages/frontend/src/pages/admin/invites.vue new file mode 100644 index 0000000000..70a9c93713 --- /dev/null +++ b/packages/frontend/src/pages/admin/invites.vue @@ -0,0 +1,126 @@ +<template> +<MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :contentMax="800"> + <div class="_gaps_m"> + <MkFolder :expanded="false"> + <template #icon><i class="ti ti-plus"></i></template> + <template #label>{{ i18n.ts.createInviteCode }}</template> + + <div class="_gaps_m"> + <MkSwitch v-model="noExpirationDate"> + <template #label>{{ i18n.ts.noExpirationDate }}</template> + </MkSwitch> + <MkInput v-if="!noExpirationDate" v-model="expiresAt" type="datetime-local"> + <template #label>{{ i18n.ts.expirationDate }}</template> + </MkInput> + <MkInput v-model="createCount" type="number"> + <template #label>{{ i18n.ts.createCount }}</template> + </MkInput> + <MkButton primary rounded @click="createWithOptions">{{ i18n.ts.create }}</MkButton> + </div> + </MkFolder> + + <div :class="$style.inputs"> + <MkSelect v-model="type" :class="$style.input"> + <template #label>{{ i18n.ts.state }}</template> + <option value="all">{{ i18n.ts.all }}</option> + <option value="unused">{{ i18n.ts.unused }}</option> + <option value="used">{{ i18n.ts.used }}</option> + <option value="expired">{{ i18n.ts.expired }}</option> + </MkSelect> + <MkSelect v-model="sort" :class="$style.input"> + <template #label>{{ i18n.ts.sort }}</template> + <option value="+createdAt">{{ i18n.ts.createdAt }} ({{ i18n.ts.ascendingOrder }})</option> + <option value="-createdAt">{{ i18n.ts.createdAt }} ({{ i18n.ts.descendingOrder }})</option> + <option value="+usedAt">{{ i18n.ts.usedAt }} ({{ i18n.ts.ascendingOrder }})</option> + <option value="-usedAt">{{ i18n.ts.usedAt }} ({{ i18n.ts.descendingOrder }})</option> + </MkSelect> + </div> + <MkPagination ref="pagingComponent" :pagination="pagination"> + <template #default="{ items }"> + <div class="_gaps_s"> + <MkInviteCode v-for="item in items" :key="item.id" :invite="(item as any)" :onDeleted="deleted" moderator/> + </div> + </template> + </MkPagination> + </div> + </MkSpacer> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import { computed, ref, shallowRef } from 'vue'; +import XHeader from './_header_.vue'; +import { i18n } from '@/i18n'; +import * as os from '@/os'; +import MkButton from '@/components/MkButton.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import MkSelect from '@/components/MkSelect.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import MkPagination, { Paging } from '@/components/MkPagination.vue'; +import MkInviteCode from '@/components/MkInviteCode.vue'; +import { definePageMetadata } from '@/scripts/page-metadata'; + +const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>(); + +let type = ref('all'); +let sort = ref('+createdAt'); + +const pagination: Paging = { + endpoint: 'admin/invite/list' as const, + limit: 10, + params: computed(() => ({ + type: type.value, + sort: sort.value, + })), + offsetMode: true, +}; + +const expiresAt = ref(''); +const noExpirationDate = ref(true); +const createCount = ref(1); + +async function createWithOptions() { + const options = { + expiresAt: noExpirationDate.value ? null : expiresAt.value, + count: createCount.value, + }; + + const tickets = await os.api('admin/invite/create', options); + os.alert({ + type: 'success', + title: i18n.ts.inviteCodeCreated, + text: tickets?.map(x => x.code).join('\n'), + }); + + tickets?.forEach(ticket => pagingComponent.value?.prepend(ticket)); +} + +function deleted(id: string) { + if (pagingComponent.value) { + pagingComponent.value.items.delete(id); + } +} + +const headerActions = $computed(() => []); +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.invite, + icon: 'ti ti-user-plus', +}); +</script> + +<style lang="scss" module> +.inputs { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.input { + flex: 1; +} +</style> diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index e36c9ac91d..13789820a0 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -24,7 +24,7 @@ <template #label>{{ i18n.ts.preservedUsernames }}</template> <template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template> </MkTextarea> - + <MkTextarea v-model="sensitiveWords"> <template #label>{{ i18n.ts.sensitiveWords }}</template> <template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template> diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/other-settings.vue index 15d720a070..13e3588740 100644 --- a/packages/frontend/src/pages/admin/other-settings.vue +++ b/packages/frontend/src/pages/admin/other-settings.vue @@ -3,14 +3,34 @@ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> - <div class="_gaps_s"> - <MkSwitch v-model="enableChartsForRemoteUser"> - <template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template> - </MkSwitch> + <div class="_gaps"> + <div class="_panel" style="padding: 16px;"> + <MkSwitch v-model="enableServerMachineStats"> + <template #label>{{ i18n.ts.enableServerMachineStats }}</template> + <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> + </MkSwitch> + </div> - <MkSwitch v-model="enableChartsForFederatedInstances"> - <template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template> - </MkSwitch> + <div class="_panel" style="padding: 16px;"> + <MkSwitch v-model="enableIdenticonGeneration"> + <template #label>{{ i18n.ts.enableIdenticonGeneration }}</template> + <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> + </MkSwitch> + </div> + + <div class="_panel" style="padding: 16px;"> + <MkSwitch v-model="enableChartsForRemoteUser"> + <template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template> + <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> + </MkSwitch> + </div> + + <div class="_panel" style="padding: 16px;"> + <MkSwitch v-model="enableChartsForFederatedInstances"> + <template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template> + <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> + </MkSwitch> + </div> </div> </FormSuspense> </MkSpacer> @@ -27,17 +47,23 @@ import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import MkSwitch from '@/components/MkSwitch.vue'; +let enableServerMachineStats: boolean = $ref(false); +let enableIdenticonGeneration: boolean = $ref(false); let enableChartsForRemoteUser: boolean = $ref(false); let enableChartsForFederatedInstances: boolean = $ref(false); async function init() { const meta = await os.api('admin/meta'); + enableServerMachineStats = meta.enableServerMachineStats; + enableIdenticonGeneration = meta.enableIdenticonGeneration; enableChartsForRemoteUser = meta.enableChartsForRemoteUser; enableChartsForFederatedInstances = meta.enableChartsForFederatedInstances; } function save() { os.apiWithDialog('admin/update-meta', { + enableServerMachineStats, + enableIdenticonGeneration, enableChartsForRemoteUser, enableChartsForFederatedInstances, }).then(() => { diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue index ad8e623415..bde5580366 100644 --- a/packages/frontend/src/pages/admin/overview.ap-requests.vue +++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue @@ -259,7 +259,7 @@ onMounted(async () => { }, plugins: [chartVLine(vLineColor)], }); - + fetching = false; }); </script> diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue index ab78c4c393..469d2e6927 100644 --- a/packages/frontend/src/pages/admin/overview.federation.vue +++ b/packages/frontend/src/pages/admin/overview.federation.vue @@ -58,7 +58,7 @@ let federationSubActiveDiff = $ref<number | null>(null); let fetching = $ref(true); const { handler: externalTooltipHandler } = useChartTooltip(); - + onMounted(async () => { const chart = await os.apiGet('charts/federation', { limit: 2, span: 'day' }); federationPubActive = chart.pubActive[0]; diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue index 69ca89e226..7d8d468512 100644 --- a/packages/frontend/src/pages/admin/overview.queue.vue +++ b/packages/frontend/src/pages/admin/overview.queue.vue @@ -85,7 +85,7 @@ onMounted(() => { connection.on('stats', onStats); connection.on('statsLog', onStatsLog); connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), + id: Math.random().toString().substring(2, 10), length: 100, }); }); @@ -122,4 +122,4 @@ onUnmounted(() => { } } } -</style> +</style> diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue index 142e70c698..f746ad14b9 100644 --- a/packages/frontend/src/pages/admin/overview.stats.vue +++ b/packages/frontend/src/pages/admin/overview.stats.vue @@ -73,7 +73,7 @@ let fetching = $ref(true); onMounted(async () => { const [_stats, _onlineUsersCount] = await Promise.all([ os.api('stats', {}), - os.api('get-online-users-count').then(res => res.count), + os.apiGet('get-online-users-count').then(res => res.count), ]); stats = _stats; onlineUsersCount = _onlineUsersCount; diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue index e8295c81b5..41a6d4f5b7 100644 --- a/packages/frontend/src/pages/admin/overview.vue +++ b/packages/frontend/src/pages/admin/overview.vue @@ -30,7 +30,7 @@ <template #header>Federation</template> <XFederation/> </MkFoldableSection> - + <MkFoldableSection class="item"> <template #header>Instances</template> <XInstances/> @@ -156,7 +156,7 @@ onMounted(async () => { nextTick(() => { queueStatsConnection.send('requestLog', { - id: Math.random().toString().substr(2, 8), + id: Math.random().toString().substring(2, 10), length: 100, }); }); diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue index 8e6856fddd..83ca9639e7 100644 --- a/packages/frontend/src/pages/admin/queue.chart.vue +++ b/packages/frontend/src/pages/admin/queue.chart.vue @@ -106,7 +106,7 @@ onMounted(() => { connection.on('stats', onStats); connection.on('statsLog', onStatsLog); connection.send('requestLog', { - id: Math.random().toString().substr(2, 8), + id: Math.random().toString().substring(2, 10), length: 200, }); }); diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index a1fa9d2932..1ba502ff86 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -1,5 +1,9 @@ <template> <div class="_gaps"> + <MkInput v-if="readonly" :modelValue="role.id" :readonly="true"> + <template #label>ID</template> + </MkInput> + <MkInput v-model="role.name" :readonly="readonly"> <template #label>{{ i18n.ts._role.name }}</template> </MkInput> @@ -171,6 +175,65 @@ </div> </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])"> + <template #label>{{ i18n.ts._role._options.inviteLimit }}</template> + <template #suffix> + <span v-if="role.policies.inviteLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.inviteLimit.value }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.inviteLimit)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.inviteLimit.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkInput v-model="role.policies.inviteLimit.value" :disabled="role.policies.inviteLimit.useDefault" type="number" :readonly="readonly"> + </MkInput> + <MkRange v-model="role.policies.inviteLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimitCycle, 'inviteLimitCycle'])"> + <template #label>{{ i18n.ts._role._options.inviteLimitCycle }}</template> + <template #suffix> + <span v-if="role.policies.inviteLimitCycle.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.inviteLimitCycle.value + i18n.ts._time.minute }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.inviteLimitCycle)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.inviteLimitCycle.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkInput v-model="role.policies.inviteLimitCycle.value" :disabled="role.policies.inviteLimitCycle.useDefault" type="number" :readonly="readonly"> + <template #suffix>{{ i18n.ts._time.minute }}</template> + </MkInput> + <MkRange v-model="role.policies.inviteLimitCycle.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.inviteExpirationTime, 'inviteExpirationTime'])"> + <template #label>{{ i18n.ts._role._options.inviteExpirationTime }}</template> + <template #suffix> + <span v-if="role.policies.inviteExpirationTime.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.inviteExpirationTime.value + i18n.ts._time.minute }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.inviteExpirationTime)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.inviteExpirationTime.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkInput v-model="role.policies.inviteExpirationTime.value" :disabled="role.policies.inviteExpirationTime.useDefault" type="number" :readonly="readonly"> + <template #suffix>{{ i18n.ts._time.minute }}</template> + </MkInput> + <MkRange v-model="role.policies.inviteExpirationTime.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])"> <template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template> <template #suffix> @@ -210,7 +273,7 @@ </MkRange> </div> </MkFolder> - + <MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])"> <template #label>{{ i18n.ts._role._options.driveCapacity }}</template> <template #suffix> diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index 6cbe7ae658..789c9da277 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -40,7 +40,7 @@ </div> <div v-if="expandedItems.includes(item.id)" :class="$style.userItemSub"> <div>Assigned: <MkTime :time="item.createdAt" mode="detail"/></div> - <div v-if="item.expiresAt">Period: {{ item.expiresAt.toLocaleString() }}</div> + <div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div> <div v-else>Period: {{ i18n.ts.indefinitely }}</div> </div> </div> diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index 6634d9cba9..cdb6e90505 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -51,6 +51,29 @@ </MkSwitch> </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])"> + <template #label>{{ i18n.ts._role._options.inviteLimit }}</template> + <template #suffix>{{ policies.inviteLimit }}</template> + <MkInput v-model="policies.inviteLimit" type="number"> + </MkInput> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimitCycle, 'inviteLimitCycle'])"> + <template #label>{{ i18n.ts._role._options.inviteLimitCycle }}</template> + <template #suffix>{{ policies.inviteLimitCycle + i18n.ts._time.minute }}</template> + <MkInput v-model="policies.inviteLimitCycle" type="number"> + <template #suffix>{{ i18n.ts._time.minute }}</template> + </MkInput> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.inviteExpirationTime, 'inviteExpirationTime'])"> + <template #label>{{ i18n.ts._role._options.inviteExpirationTime }}</template> + <template #suffix>{{ policies.inviteExpirationTime + i18n.ts._time.minute }}</template> + <MkInput v-model="policies.inviteExpirationTime" type="number"> + <template #suffix>{{ i18n.ts._time.minute }}</template> + </MkInput> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])"> <template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template> <template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template> diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 4c2fe46f28..bd57c06181 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -37,6 +37,13 @@ <template #label>{{ i18n.ts.cacheRemoteFiles }}</template> <template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template> </MkSwitch> + + <template v-if="cacheRemoteFiles"> + <MkSwitch v-model="cacheRemoteSensitiveFiles"> + <template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}</template> + <template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template> + </MkSwitch> + </template> </div> </FormSection> @@ -104,7 +111,6 @@ import { fetchInstance } from '@/instance'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import MkButton from '@/components/MkButton.vue'; -import MkColorInput from '@/components/MkColorInput.vue'; let name: string | null = $ref(null); let description: string | null = $ref(null); @@ -112,13 +118,14 @@ let maintainerName: string | null = $ref(null); let maintainerEmail: string | null = $ref(null); let pinnedUsers: string = $ref(''); let cacheRemoteFiles: boolean = $ref(false); +let cacheRemoteSensitiveFiles: boolean = $ref(false); let enableServiceWorker: boolean = $ref(false); let swPublicKey: any = $ref(null); let swPrivateKey: any = $ref(null); let deeplAuthKey: string = $ref(''); let deeplIsPro: boolean = $ref(false); -async function init() { +async function init(): Promise<void> { const meta = await os.api('admin/meta'); name = meta.name; description = meta.description; @@ -126,6 +133,7 @@ async function init() { maintainerEmail = meta.maintainerEmail; pinnedUsers = meta.pinnedUsers.join('\n'); cacheRemoteFiles = meta.cacheRemoteFiles; + cacheRemoteSensitiveFiles = meta.cacheRemoteSensitiveFiles; enableServiceWorker = meta.enableServiceWorker; swPublicKey = meta.swPublickey; swPrivateKey = meta.swPrivateKey; @@ -133,7 +141,7 @@ async function init() { deeplIsPro = meta.deeplIsPro; } -function save() { +function save(): void { os.apiWithDialog('admin/update-meta', { name, description, @@ -141,6 +149,7 @@ function save() { maintainerEmail, pinnedUsers: pinnedUsers.split('\n'), cacheRemoteFiles, + cacheRemoteSensitiveFiles, enableServiceWorker, swPublicKey, swPrivateKey, diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index 0a358a141b..cacdab040f 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -25,11 +25,11 @@ <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts.pinnedNotes }}</template> - + <div class="_gaps"> <MkButton primary rounded @click="addPinnedNote()"><i class="ti ti-plus"></i></MkButton> - <Sortable + <Sortable v-model="pinnedNotes" itemKey="id" :handle="'.' + $style.pinnedNoteHandle" @@ -160,7 +160,7 @@ async function archive() { }); if (canceled) return; - + os.api('channels/update', { channelId: props.channelId, isArchived: true, diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index bcc0fc6860..2a056f21d4 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -87,7 +87,7 @@ const props = defineProps<{ channelId: string; }>(); -let tab = $ref('timeline'); +let tab = $ref('overview'); let channel = $ref(null); let favorited = $ref(false); let searchQuery = $ref(''); @@ -107,6 +107,9 @@ watch(() => props.channelId, async () => { channelId: props.channelId, }); favorited = channel.isFavorited; + if (favorited || channel.isFollowing) { + tab = 'timeline'; + } }, { immediate: true }); function edit() { diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index d5313099da..b09f787b5b 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -55,7 +55,7 @@ watch(() => props.clipId, async () => { favorited = clip.isFavorited; }, { immediate: true, -}); +}); provide('currentClip', $$(clip)); diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index 3da6a0d9cb..359bbeadc3 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -18,7 +18,7 @@ <MkButton inline @click="setTagBulk">Set tag</MkButton> <MkButton inline @click="addTagBulk">Add tag</MkButton> <MkButton inline @click="removeTagBulk">Remove tag</MkButton> - <MkButton inline @click="setLisenceBulk">Set Lisence</MkButton> + <MkButton inline @click="setLicenseBulk">Set License</MkButton> <MkButton inline danger @click="delBulk">Delete</MkButton> </div> <MkPagination ref="emojisPaginationComponent" :pagination="pagination"> @@ -144,7 +144,7 @@ const edit = (emoji) => { ...result.updated, })); } else if (result.deleted) { - emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id); + emojisPaginationComponent.value.removeItem(emoji.id); } }, }, 'closed'); @@ -221,7 +221,7 @@ const setCategoryBulk = async () => { emojisPaginationComponent.value.reload(); }; -const setLisenceBulk = async () => { +const setLicenseBulk = async () => { const { canceled, result } = await os.inputText({ title: 'License', }); @@ -311,13 +311,13 @@ definePageMetadata(computed(() => ({ .empty { margin: var(--margin); } - + .ldhfsamy { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); grid-gap: 12px; margin: var(--margin) 0; - + > .emoji { display: flex; align-items: center; diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index 3208c92738..f49057930c 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -26,7 +26,7 @@ </div> </div> <MkButton rounded style="margin: 0 auto;" @click="changeImage">{{ i18n.ts.selectFile }}</MkButton> - <MkInput v-model="name"> + <MkInput v-model="name" pattern="[a-z0-9_]"> <template #label>{{ i18n.ts.name }}</template> </MkInput> <MkInput v-model="category" :datalist="customEmojiCategories"> @@ -70,6 +70,7 @@ <script lang="ts" setup> import { computed, watch } from 'vue'; +import * as misskey from 'misskey-js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; @@ -95,7 +96,7 @@ let isSensitive = $ref(props.emoji ? props.emoji.isSensitive : false); let localOnly = $ref(props.emoji ? props.emoji.localOnly : false); let roleIdsThatCanBeUsedThisEmojiAsReaction = $ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []); let rolesThatCanBeUsedThisEmojiAsReaction = $ref([]); -let file = $ref(); +let file = $ref<misskey.entities.DriveFile>(); watch($$(roleIdsThatCanBeUsedThisEmojiAsReaction), async () => { rolesThatCanBeUsedThisEmojiAsReaction = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); @@ -110,6 +111,10 @@ const emit = defineEmits<{ async function changeImage(ev) { file = await selectFile(ev.currentTarget ?? ev.target, null); + const candidate = file.name.replace(/\.(.+)$/, ''); + if (candidate.match(/^[a-z0-9_]+$/)) { + name = candidate; + } } async function addRole() { diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index 6a16cd1c4a..86aaad8f53 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -33,7 +33,7 @@ import MkTextarea from '@/components/MkTextarea.vue'; import MkInput from '@/components/MkInput.vue'; import { useRouter } from '@/router'; -const PRESET_DEFAULT = `/// @ 0.13.3 +const PRESET_DEFAULT = `/// @ 0.15.0 var name = "" @@ -51,7 +51,7 @@ Ui:render([ ]) `; -const PRESET_OMIKUJI = `/// @ 0.13.3 +const PRESET_OMIKUJI = `/// @ 0.15.0 // ユーザーごとに日替わりのおみくじのプリセット // 選択肢 @@ -94,7 +94,7 @@ Ui:render([ ]) `; -const PRESET_SHUFFLE = `/// @ 0.13.3 +const PRESET_SHUFFLE = `/// @ 0.15.0 // 巻き戻し可能な文字シャッフルのプリセット let string = "ペペロンチーノ" @@ -173,7 +173,7 @@ var cursor = 0 do() `; -const PRESET_QUIZ = `/// @ 0.13.3 +const PRESET_QUIZ = `/// @ 0.15.0 let title = '地理クイズ' let qas = [{ @@ -286,7 +286,7 @@ qaEls.push(Ui:C:container({ Ui:render(qaEls) `; -const PRESET_TIMELINE = `/// @ 0.13.3 +const PRESET_TIMELINE = `/// @ 0.15.0 // APIリクエストを行いローカルタイムラインを表示するプリセット @fetch() { diff --git a/packages/frontend/src/pages/follow.vue b/packages/frontend/src/pages/follow.vue index d14b663364..2d08b66868 100644 --- a/packages/frontend/src/pages/follow.vue +++ b/packages/frontend/src/pages/follow.vue @@ -20,7 +20,7 @@ async function follow(user): Promise<void> { window.close(); return; } - + os.apiWithDialog('following/create', { userId: user.id, }); diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index dfa6c0bac0..39b2c2c90b 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -236,6 +236,7 @@ definePageMetadata(computed(() => post ? { border-top: solid 0.5px var(--divider); display: flex; align-items: center; + flex-wrap: wrap; > .avatar { width: 52px; diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 83997b2555..ac765e88b7 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -49,7 +49,7 @@ <template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template> </MkKeyValue> </FormSection> - + <FormSection> <MkKeyValue oneline style="margin: 1em 0;"> <template #key>Following (Pub)</template> diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue new file mode 100644 index 0000000000..c893ad51e8 --- /dev/null +++ b/packages/frontend/src/pages/invite.vue @@ -0,0 +1,114 @@ +<template> +<MkStickyContainer> + <template #header> + <MkPageHeader/> + </template> + <MKSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200"> + <div :class="$style.root"> + <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> + <div :class="$style.text"> + <i class="ti ti-alert-triangle"></i> + {{ i18n.ts.nothing }} + </div> + </div> + </MKSpacer> + <MkSpacer v-else :contentMax="800"> + <div class="_gaps_m" style="text-align: center;"> + <div v-if="resetCycle && inviteLimit">{{ i18n.t('inviteLimitResetCycle', { time: resetCycle, limit: inviteLimit }) }}</div> + <MkButton inline primary rounded :disabled="currentInviteLimit !== null && currentInviteLimit <= 0" @click="create"><i class="ti ti-user-plus"></i> {{ i18n.ts.createInviteCode }}</MkButton> + <div v-if="currentInviteLimit !== null">{{ i18n.t('createLimitRemaining', { limit: currentInviteLimit }) }}</div> + + <MkPagination ref="pagingComponent" :pagination="pagination"> + <template #default="{ items }"> + <div class="_gaps_s"> + <MkInviteCode v-for="item in (items as Invite[])" :key="item.id" :invite="item" :onDeleted="deleted"/> + </div> + </template> + </MkPagination> + </div> + </MkSpacer> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import { computed, ref, shallowRef } from 'vue'; +import type { Invite } from 'misskey-js/built/entities'; +import { i18n } from '@/i18n'; +import * as os from '@/os'; +import MkButton from '@/components/MkButton.vue'; +import MkPagination, { Paging } from '@/components/MkPagination.vue'; +import MkInviteCode from '@/components/MkInviteCode.vue'; +import { definePageMetadata } from '@/scripts/page-metadata'; +import { serverErrorImageUrl, instance } from '@/instance'; +import { $i } from '@/account'; + +const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>(); +const currentInviteLimit = ref<null | number>(null); +const inviteLimit = (($i != null && $i.policies.inviteLimit) || (($i == null && instance.policies.inviteLimit))) as number; +const inviteLimitCycle = (($i != null && $i.policies.inviteLimitCycle) || ($i == null && instance.policies.inviteLimitCycle)) as number; + +const pagination: Paging = { + endpoint: 'invite/list' as const, + limit: 10, +}; + +const resetCycle = computed<null | string>(() => { + if (!inviteLimitCycle) return null; + + const minutes = inviteLimitCycle; + if (minutes < 60) return minutes + i18n.ts._time.minute; + const hours = Math.floor(minutes / 60); + if (hours < 24) return hours + i18n.ts._time.hour; + return Math.floor(hours / 24) + i18n.ts._time.day; +}); + +async function create() { + const ticket = await os.api('invite/create'); + os.alert({ + type: 'success', + title: i18n.ts.inviteCodeCreated, + text: ticket.code, + }); + + pagingComponent.value?.prepend(ticket); + update(); +} + +function deleted(id: string) { + if (pagingComponent.value) { + pagingComponent.value.items.delete(id); + } + update(); +} + +async function update() { + currentInviteLimit.value = (await os.api('invite/limit')).remaining; +} + +update(); + +definePageMetadata({ + title: i18n.ts.invite, + icon: 'ti ti-user-plus', +}); +</script> + +<style lang="scss" module> +.root { + padding: 32px; + text-align: center; + align-items: center; +} + +.text { + margin: 0 0 8px 0; +} + +.img { + vertical-align: bottom; + width: 128px; + height: 128px; + margin-bottom: 16px; + border-radius: 16px; +} +</style> diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue index 40934fb71d..3307eef359 100644 --- a/packages/frontend/src/pages/list.vue +++ b/packages/frontend/src/pages/list.vue @@ -112,7 +112,7 @@ definePageMetadata(computed(() => list ? { flex: 1; min-width: 0; margin-right: 8px; - + &:hover { text-decoration: none; } diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue index 355d18fdb5..632c36bbf8 100644 --- a/packages/frontend/src/pages/my-antennas/create.vue +++ b/packages/frontend/src/pages/my-antennas/create.vue @@ -9,6 +9,7 @@ import XAntenna from './editor.vue'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import { useRouter } from '@/router'; +import { antennasCache } from '@/cache'; const router = useRouter(); @@ -26,13 +27,10 @@ let draft = $ref({ }); function onAntennaCreated() { + antennasCache.delete(); router.push('/my/antennas'); } -const headerActions = $computed(() => []); - -const headerTabs = $computed(() => []); - definePageMetadata({ title: i18n.ts.manageAntennas, icon: 'ti ti-antenna', diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue index da9b2de48f..3fb9690ac1 100644 --- a/packages/frontend/src/pages/my-antennas/edit.vue +++ b/packages/frontend/src/pages/my-antennas/edit.vue @@ -10,6 +10,7 @@ import * as os from '@/os'; import { i18n } from '@/i18n'; import { useRouter } from '@/router'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { antennasCache } from '@/cache'; const router = useRouter(); @@ -20,6 +21,7 @@ const props = defineProps<{ }>(); function onAntennaUpdated() { + antennasCache.delete(); router.push('/my/antennas'); } @@ -27,10 +29,6 @@ os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) = antenna = antennaResponse; }); -const headerActions = $computed(() => []); - -const headerTabs = $computed(() => []); - definePageMetadata({ title: i18n.ts.manageAntennas, icon: 'ti ti-antenna', diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue index 2ca026b9a1..1e9136f1fa 100644 --- a/packages/frontend/src/pages/my-antennas/index.vue +++ b/packages/frontend/src/pages/my-antennas/index.vue @@ -2,15 +2,20 @@ <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="700"> - <div class="ieepwinx"> - <MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> + <div> + <div v-if="antennas.length === 0" class="empty"> + <div class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.nothing }}</div> + </div> + </div> + + <MkButton :link="true" to="/my/antennas/create" primary :class="$style.add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> - <div class=""> - <MkPagination v-slot="{items}" ref="list" :pagination="pagination"> - <MkA v-for="antenna in items" :key="antenna.id" class="ljoevbzj" :to="`/my/antennas/${antenna.id}`"> - <div class="name">{{ antenna.name }}</div> - </MkA> - </MkPagination> + <div v-if="antennas.length > 0" class="_gaps"> + <MkA v-for="antenna in antennas" :key="antenna.id" :class="$style.antenna" :to="`/my/antennas/${antenna.id}`"> + <div class="name">{{ antenna.name }}</div> + </MkA> </div> </div> </MkSpacer> @@ -18,19 +23,31 @@ </template> <script lang="ts" setup> -import { } from 'vue'; -import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { antennasCache } from '@/cache'; +import { api } from '@/os'; +import { onActivated } from 'vue'; +import { infoImageUrl } from '@/instance'; -const pagination = { - endpoint: 'antennas/list' as const, - noPaging: true, - limit: 10, -}; +const antennas = $computed(() => antennasCache.value.value ?? []); + +function fetch() { + antennasCache.fetch(() => api('antennas/list')); +} -const headerActions = $computed(() => []); +fetch(); + +const headerActions = $computed(() => [{ + asFullButton: true, + icon: 'ti ti-refresh', + text: i18n.ts.reload, + handler: () => { + antennasCache.delete(); + fetch(); + }, +}]); const headerTabs = $computed(() => []); @@ -38,30 +55,30 @@ definePageMetadata({ title: i18n.ts.manageAntennas, icon: 'ti ti-antenna', }); -</script> - -<style lang="scss" scoped> -.ieepwinx { - > .add { - margin: 0 auto 16px auto; - } +onActivated(() => { + antennasCache.fetch(() => api('antennas/list')); +}); +</script> - .ljoevbzj { - display: block; - padding: 16px; - margin-bottom: 8px; - border: solid 1px var(--divider); - border-radius: 6px; +<style lang="scss" module> +.add { + margin: 0 auto 16px auto; +} - &:hover { - border: solid 1px var(--accent); - text-decoration: none; - } +.antenna { + display: block; + padding: 16px; + border: solid 1px var(--divider); + border-radius: 6px; - > .name { - font-weight: bold; - } + &:hover { + border: solid 1px var(--accent); + text-decoration: none; } } + +.name { + font-weight: bold; +} </style> diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue index cee241c489..38cee91a51 100644 --- a/packages/frontend/src/pages/my-lists/index.vue +++ b/packages/frontend/src/pages/my-lists/index.vue @@ -3,38 +3,44 @@ <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="700"> <div class="_gaps"> + <div v-if="items.length === 0" class="empty"> + <div class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.nothing }}</div> + </div> + </div> + <MkButton primary rounded style="margin: 0 auto;" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.createList }}</MkButton> - <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination"> - <div class="_gaps"> - <MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`"> - <div style="margin-bottom: 4px;">{{ list.name }}</div> - <MkAvatars :userIds="list.userIds"/> - </MkA> - </div> - </MkPagination> + <div v-if="items.length > 0" class="_gaps"> + <MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`"> + <div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i?.policies['userEachUserListsLimit']}` }) }})</span></div> + <MkAvatars :userIds="list.userIds" :limit="10"/> + </MkA> + </div> </div> </MkSpacer> </MkStickyContainer> </template> <script lang="ts" setup> -import { } from 'vue'; -import MkPagination from '@/components/MkPagination.vue'; +import { onActivated } from 'vue'; import MkButton from '@/components/MkButton.vue'; import MkAvatars from '@/components/MkAvatars.vue'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import { userListsCache } from '@/cache'; +import { infoImageUrl } from '@/instance'; +import { $i } from '@/account'; -const pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>(); +const items = $computed(() => userListsCache.value.value ?? []); -const pagination = { - endpoint: 'users/lists/list' as const, - noPaging: true, - limit: 10, -}; +function fetch() { + userListsCache.fetch(() => os.api('users/lists/list')); +} + +fetch(); async function create() { const { canceled, result: name } = await os.inputText({ @@ -43,20 +49,28 @@ async function create() { if (canceled) return; await os.apiWithDialog('users/lists/create', { name: name }); userListsCache.delete(); - pagingComponent.reload(); + fetch(); } -const headerActions = $computed(() => []); +const headerActions = $computed(() => [{ + asFullButton: true, + icon: 'ti ti-refresh', + text: i18n.ts.reload, + handler: () => { + userListsCache.delete(); + fetch(); + }, +}]); const headerTabs = $computed(() => []); definePageMetadata({ title: i18n.ts.manageLists, icon: 'ti ti-list', - action: { - icon: 'ti ti-plus', - handler: create, - }, +}); + +onActivated(() => { + fetch(); }); </script> @@ -73,4 +87,9 @@ definePageMetadata({ text-decoration: none; } } + +.nUsers { + font-size: .9em; + opacity: .7; +} </style> diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index dd431e8dc0..36a3a123c5 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -20,6 +20,7 @@ <MkFolder defaultOpen> <template #label>{{ i18n.ts.members }}</template> + <template #caption>{{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i?.policies['userEachUserListsLimit']}` }) }}</template> <div class="_gaps_s"> <MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton> @@ -29,6 +30,10 @@ </MkA> <button class="_button" :class="$style.remove" @click="removeUser(user, $event)"><i class="ti ti-x"></i></button> </div> + <MkButton v-if="!fetching && queueUserIds.length !== 0" v-appear="enableInfiniteScroll ? fetchMoreUsers : null" :class="$style.more" :style="{ cursor: 'pointer' }" primary rounded @click="fetchMoreUsers"> + {{ i18n.ts.loadMore }} + </MkButton> + <MkLoading v-if="fetching" class="loading"/> </div> </MkFolder> </div> @@ -49,34 +54,57 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkInput from '@/components/MkInput.vue'; import { userListsCache } from '@/cache'; +import { UserList, UserLite } from 'misskey-js/built/entities'; +import { $i } from '@/account'; +import { defaultStore } from '@/store'; +const { + enableInfiniteScroll, +} = defaultStore.reactiveState; const props = defineProps<{ listId: string; }>(); -let list = $ref(null); -let users = $ref([]); +const FETCH_USERS_LIMIT = 20; + +let list = $ref<UserList | null>(null); +let users = $ref<UserLite[]>([]); +let queueUserIds = $ref<string[]>([]); +let fetching = $ref(true); const isPublic = ref(false); const name = ref(''); function fetchList() { + fetching = true; os.api('users/lists/show', { listId: props.listId, }).then(_list => { list = _list; name.value = list.name; isPublic.value = list.isPublic; + queueUserIds = list.userIds; - os.api('users/show', { - userIds: list.userIds, - }).then(_users => { - users = _users; - }); + return fetchMoreUsers(); + }); +} + +function fetchMoreUsers() { + if (!list) return; + if (fetching && users.length !== 0) return; // fetchingがtrueならやめるが、usersが空なら続行 + fetching = true; + os.api('users/show', { + userIds: queueUserIds.slice(0, FETCH_USERS_LIMIT), + }).then(_users => { + users = users.concat(_users); + queueUserIds = queueUserIds.slice(FETCH_USERS_LIMIT); + }).finally(() => { + fetching = false; }); } function addUser() { os.selectUser().then(user => { + if (!list) return; os.apiWithDialog('users/lists/push', { listId: list.id, userId: user.id, @@ -92,6 +120,7 @@ async function removeUser(user, ev) { icon: 'ti ti-x', danger: true, action: async () => { + if (!list) return; os.api('users/lists/pull', { listId: list.id, userId: user.id, @@ -103,6 +132,7 @@ async function removeUser(user, ev) { } async function deleteList() { + if (!list) return; const { canceled } = await os.confirm({ type: 'warning', text: i18n.t('removeAreYouSure', { x: list.name }), @@ -117,6 +147,7 @@ async function deleteList() { } async function updateSettings() { + if (!list) return; await os.apiWithDialog('users/lists/update', { listId: list.id, name: name.value, @@ -166,6 +197,11 @@ definePageMetadata(computed(() => list ? { align-self: center; } +.more { + margin-left: auto; + margin-right: auto; +} + .footer { -webkit-backdrop-filter: var(--blur, blur(15px)); backdrop-filter: var(--blur, blur(15px)); diff --git a/packages/frontend/src/pages/not-found.vue b/packages/frontend/src/pages/not-found.vue index 43dc41e7cc..d10f221b8c 100644 --- a/packages/frontend/src/pages/not-found.vue +++ b/packages/frontend/src/pages/not-found.vue @@ -10,8 +10,17 @@ <script lang="ts" setup> import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { pleaseLogin } from '@/scripts/please-login'; import { notFoundImageUrl } from '@/instance'; +const props = defineProps<{ + showLoginPopup?: boolean; +}>(); + +if (props.showLoginPopup) { + pleaseLogin('/'); +} + const headerActions = $computed(() => []); const headerTabs = $computed(() => []); diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue index b1d41fe2c7..59f1f7fdb5 100644 --- a/packages/frontend/src/pages/registry.keys.vue +++ b/packages/frontend/src/pages/registry.keys.vue @@ -13,7 +13,7 @@ <template #value>{{ scope.join('/') }}</template> </MkKeyValue> </FormSplit> - + <MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton> <FormSection v-if="keys"> diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue index 513a2f8feb..ed01381d57 100644 --- a/packages/frontend/src/pages/registry.value.vue +++ b/packages/frontend/src/pages/registry.value.vue @@ -20,7 +20,7 @@ <template #value>{{ key }}</template> </MkKeyValue> </FormSplit> - + <MkTextarea v-model="valueForEditor" tall class="_monospace"> <template #label>{{ i18n.ts.value }} (JSON)</template> </MkTextarea> diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue index 9d57307314..fbd109b3f0 100644 --- a/packages/frontend/src/pages/reset-password.vue +++ b/packages/frontend/src/pages/reset-password.vue @@ -7,7 +7,7 @@ <template #prefix><i class="ti ti-lock"></i></template> <template #label>{{ i18n.ts.newPassword }}</template> </MkInput> - + <MkButton primary @click="save">{{ i18n.ts.save }}</MkButton> </div> </MkSpacer> diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue index bd1389ffef..8e4a4a78c5 100644 --- a/packages/frontend/src/pages/search.user.vue +++ b/packages/frontend/src/pages/search.user.vue @@ -67,7 +67,7 @@ async function search() { endpoint: 'users/search', limit: 10, params: { - query: searchQuery, + query: query, origin: searchOrigin, }, }; diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue index bc0179b3aa..481959fd08 100644 --- a/packages/frontend/src/pages/settings/deck.vue +++ b/packages/frontend/src/pages/settings/deck.vue @@ -1,5 +1,7 @@ <template> <div class="_gaps_m"> + <MkSwitch v-model="useSimpleUiForNonRootPages">{{ i18n.ts._deck.useSimpleUiForNonRootPages }}</MkSwitch> + <MkSwitch v-model="navWindow">{{ i18n.ts.defaultNavigationBehaviour }}: {{ i18n.ts.openInWindow }}</MkSwitch> <MkSwitch v-model="alwaysShowMainColumn">{{ i18n.ts._deck.alwaysShowMainColumn }}</MkSwitch> @@ -21,6 +23,7 @@ import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; const navWindow = computed(deckStore.makeGetterSetter('navWindow')); +const useSimpleUiForNonRootPages = computed(deckStore.makeGetterSetter('useSimpleUiForNonRootPages')); const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn')); const columnAlign = computed(deckStore.makeGetterSetter('columnAlign')); diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue index 8178343bbb..98471e94db 100644 --- a/packages/frontend/src/pages/settings/drive-cleaner.vue +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -80,7 +80,7 @@ watch(sortModeSelect, () => { sortMode.value = '+size'; fetchDriveInfo(); break; - + case 'createdAtAsc': sortMode.value = '-createdAt'; fetchDriveInfo(); diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 20b36f0fcb..cfe5cd31e7 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -52,10 +52,10 @@ </MkSelect> <MkSelect v-model="nsfw"> - <template #label>{{ i18n.ts.nsfw }}</template> - <option value="respect">{{ i18n.ts._nsfw.respect }}</option> - <option value="ignore">{{ i18n.ts._nsfw.ignore }}</option> - <option value="force">{{ i18n.ts._nsfw.force }}</option> + <template #label>{{ i18n.ts.displayOfSensitiveMedia }}</template> + <option value="respect">{{ i18n.ts._displayOfSensitiveMedia.respect }}</option> + <option value="ignore">{{ i18n.ts._displayOfSensitiveMedia.ignore }}</option> + <option value="force">{{ i18n.ts._displayOfSensitiveMedia.force }}</option> </MkSelect> <MkRadios v-model="mediaListWithOneImageAppearance"> diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index b4f056d8a6..d53519e0d5 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -166,7 +166,7 @@ const menuDef = computed(() => [{ active: currentPage?.route.name === 'import-export', }, { icon: 'ti ti-plane', - text: `${i18n.ts.accountMigration} (${i18n.ts.experimental})`, + text: `${i18n.ts.accountMigration}`, to: '/settings/migration', active: currentPage?.route.name === 'migration', }, { diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue index 102bc68523..38e0d0abb2 100644 --- a/packages/frontend/src/pages/settings/migration.vue +++ b/packages/frontend/src/pages/settings/migration.vue @@ -1,8 +1,5 @@ <template> <div class="_gaps_m"> - <FormInfo warn> - {{ i18n.ts.thisIsExperimentalFeature }} - </FormInfo> <MkFolder :defaultOpen="true"> <template #icon><i class="ti ti-plane-arrival"></i></template> <template #label>{{ i18n.ts._accountMigration.moveFrom }}</template> diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index e0785ab9fe..e1f3c6bed9 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -55,7 +55,7 @@ </div> <div v-if="expandedMuteItems.includes(item.id)" :class="$style.userItemSub"> <div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div> - <div v-if="item.expiresAt">Period: {{ item.expiresAt.toLocaleString() }}</div> + <div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div> <div v-else>Period: {{ i18n.ts.indefinitely }}</div> </div> </div> @@ -85,7 +85,7 @@ </div> <div v-if="expandedBlockItems.includes(item.id)" :class="$style.userItemSub"> <div>Blocked at: <MkTime :time="item.createdAt" mode="detail"/></div> - <div v-if="item.expiresAt">Period: {{ item.expiresAt.toLocaleString() }}</div> + <div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div> <div v-else>Period: {{ i18n.ts.indefinitely }}</div> </div> </div> diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue index 8780bfbc1e..f0e9a1c3d9 100644 --- a/packages/frontend/src/pages/settings/navbar.vue +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -3,7 +3,7 @@ <FormSlot> <template #label>{{ i18n.ts.navbar }}</template> <MkContainer :showHeader="false"> - <Sortable + <Sortable v-model="items" itemKey="id" :animation="150" diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index e34901cd11..1aa1a5f81c 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -144,7 +144,7 @@ function validate(profile: unknown): void { if (!profile.name) throw new Error('Missing required prop: name'); if (!profile.misskeyVersion) throw new Error('Missing required prop: misskeyVersion'); - + // Check if createdAt and updatedAt is Date // https://zenn.dev/lollipop_onl/articles/eoz-judge-js-invalid-date if (!profile.createdAt || Number.isNaN(new Date(profile.createdAt).getTime())) throw new Error('createdAt is falsy or not Date'); @@ -273,7 +273,7 @@ async function applyProfile(id: string): Promise<void> { defaultStore.set(key, settings.hot[key]); } } - + // coldDeviceStorage for (const key of coldDeviceStorageSaveKeys) { if (settings.cold[key] !== undefined) { diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index 7fd4d6d34e..88d109c021 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -7,7 +7,7 @@ {{ i18n.ts.makeReactionsPublic }} <template #caption>{{ i18n.ts.makeReactionsPublicDescription }}</template> </MkSwitch> - + <MkSelect v-model="ffVisibility" @update:modelValue="save()"> <template #label>{{ i18n.ts.ffVisibility }}</template> <option value="public">{{ i18n.ts._ffVisibility.public }}</option> @@ -15,7 +15,7 @@ <option value="private">{{ i18n.ts._ffVisibility.private }}</option> <template #caption>{{ i18n.ts.ffVisibilityDescription }}</template> </MkSelect> - + <MkSwitch v-model="hideOnlineStatus" @update:modelValue="save()"> {{ i18n.ts.hideOnlineStatus }} <template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template> diff --git a/packages/frontend/src/pages/settings/roles.vue b/packages/frontend/src/pages/settings/roles.vue index 05753c9b60..4b842f56fd 100644 --- a/packages/frontend/src/pages/settings/roles.vue +++ b/packages/frontend/src/pages/settings/roles.vue @@ -37,7 +37,7 @@ import MkRolePreview from '@/components/MkRolePreview.vue'; function save() { os.apiWithDialog('i/update', { - + }); } diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue index 2da84763a3..bd5e2e350a 100644 --- a/packages/frontend/src/pages/settings/security.vue +++ b/packages/frontend/src/pages/settings/security.vue @@ -78,7 +78,7 @@ async function change() { }); return; } - + os.apiWithDialog('i/change-password', { currentPassword, newPassword, diff --git a/packages/frontend/src/pages/user-info.vue b/packages/frontend/src/pages/user-info.vue index 56e8737e1c..f7650285c7 100644 --- a/packages/frontend/src/pages/user-info.vue +++ b/packages/frontend/src/pages/user-info.vue @@ -112,9 +112,17 @@ <MkButton v-if="user.host == null && iAmModerator" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton> <div v-for="role in info.roles" :key="role.id" :class="$style.roleItem"> - <MkRolePreview :class="$style.role" :role="role" :forModeration="true"/> - <button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button> - <button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button> + <div :class="$style.roleItemMain"> + <MkRolePreview :class="$style.role" :role="role" :forModeration="true"/> + <button class="_button" :class="$style.roleToggle" @click="toggleRoleItem(role)"><i class="ti ti-chevron-down"></i></button> + <button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button> + <button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button> + </div> + <div v-if="expandedRoles.includes(role.id)" :class="$style.roleItemSub"> + <div>Assigned: <MkTime :time="info.roleAssigns.find(a => a.roleId === role.id).createdAt" mode="detail"/></div> + <div v-if="info.roleAssigns.find(a => a.roleId === role.id).expiresAt">Period: {{ new Date(info.roleAssigns.find(a => a.roleId === role.id).expiresAt).toLocaleString() }}</div> + <div v-else>Period: {{ i18n.ts.indefinitely }}</div> + </div> </div> </div> </MkFolder> @@ -220,6 +228,7 @@ const filesPagination = { userId: props.userId, })), }; +let expandedRoles = $ref([]); function createFetcher() { if (iAmModerator) { @@ -384,6 +393,14 @@ async function unassignRole(role, ev) { }], ev.currentTarget ?? ev.target); } +function toggleRoleItem(role) { + if (expandedRoles.includes(role.id)) { + expandedRoles = expandedRoles.filter(x => x !== role.id); + } else { + expandedRoles.push(role.id); + } +} + watch(() => props.userId, () => { init = createFetcher(); }, { @@ -523,11 +540,22 @@ definePageMetadata(computed(() => ({ } .roleItem { +} + +.roleItemMain { display: flex; } .role { flex: 1; + min-width: 0; + margin-right: 8px; +} + +.roleItemSub { + padding: 6px 12px; + font-size: 85%; + color: var(--fgTransparentWeak); } .roleUnassign { diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 2e69eb367b..b0d42463a0 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -44,8 +44,10 @@ </div> <div v-if="user.roles.length > 0" class="roles"> <span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }"> - <img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/> - {{ role.name }} + <MkA v-adaptive-bg :to="`/roles/${role.id}`"> + <img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/> + {{ role.name }} + </MkA> </span> </div> <div v-if="iAmModerator" class="moderationNote"> @@ -98,15 +100,15 @@ </dl> </div> <div class="status"> - <MkA v-click-anime :to="userPage(user)"> + <MkA :to="userPage(user)"> <b>{{ number(user.notesCount) }}</b> <span>{{ i18n.ts.notes }}</span> </MkA> - <MkA v-click-anime :to="userPage(user, 'following')"> + <MkA v-if="isFfVisibleForMe(user)" :to="userPage(user, 'following')"> <b>{{ number(user.followingCount) }}</b> <span>{{ i18n.ts.following }}</span> </MkA> - <MkA v-click-anime :to="userPage(user, 'followers')"> + <MkA v-if="isFfVisibleForMe(user)" :to="userPage(user, 'followers')"> <b>{{ number(user.followersCount) }}</b> <span>{{ i18n.ts.followers }}</span> </MkA> @@ -158,6 +160,7 @@ import { dateString } from '@/filters/date'; import { confetti } from '@/scripts/confetti'; import MkNotes from '@/components/MkNotes.vue'; import { api } from '@/os'; +import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe'; const XPhotos = defineAsyncComponent(() => import('./index.photos.vue')); const XActivity = defineAsyncComponent(() => import('./index.activity.vue')); |