summaryrefslogtreecommitdiff
path: root/packages/frontend/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/pages')
-rw-r--r--packages/frontend/src/pages/about-misskey.vue34
-rw-r--r--packages/frontend/src/pages/about.emojis.vue4
-rw-r--r--packages/frontend/src/pages/admin-file.vue2
-rw-r--r--packages/frontend/src/pages/admin/abuses.vue2
-rw-r--r--packages/frontend/src/pages/admin/ads.vue19
-rw-r--r--packages/frontend/src/pages/admin/index.vue13
-rw-r--r--packages/frontend/src/pages/admin/invites.vue126
-rw-r--r--packages/frontend/src/pages/admin/moderation.vue2
-rw-r--r--packages/frontend/src/pages/admin/other-settings.vue40
-rw-r--r--packages/frontend/src/pages/admin/overview.ap-requests.vue2
-rw-r--r--packages/frontend/src/pages/admin/overview.federation.vue2
-rw-r--r--packages/frontend/src/pages/admin/overview.queue.vue4
-rw-r--r--packages/frontend/src/pages/admin/overview.stats.vue2
-rw-r--r--packages/frontend/src/pages/admin/overview.vue4
-rw-r--r--packages/frontend/src/pages/admin/queue.chart.vue2
-rw-r--r--packages/frontend/src/pages/admin/roles.editor.vue65
-rw-r--r--packages/frontend/src/pages/admin/roles.role.vue2
-rw-r--r--packages/frontend/src/pages/admin/roles.vue23
-rw-r--r--packages/frontend/src/pages/admin/settings.vue15
-rw-r--r--packages/frontend/src/pages/channel-editor.vue6
-rw-r--r--packages/frontend/src/pages/channel.vue5
-rw-r--r--packages/frontend/src/pages/clip.vue2
-rw-r--r--packages/frontend/src/pages/custom-emojis-manager.vue10
-rw-r--r--packages/frontend/src/pages/emoji-edit-dialog.vue9
-rw-r--r--packages/frontend/src/pages/flash/flash-edit.vue10
-rw-r--r--packages/frontend/src/pages/follow.vue2
-rw-r--r--packages/frontend/src/pages/gallery/post.vue1
-rw-r--r--packages/frontend/src/pages/instance-info.vue2
-rw-r--r--packages/frontend/src/pages/invite.vue114
-rw-r--r--packages/frontend/src/pages/list.vue2
-rw-r--r--packages/frontend/src/pages/my-antennas/create.vue6
-rw-r--r--packages/frontend/src/pages/my-antennas/edit.vue6
-rw-r--r--packages/frontend/src/pages/my-antennas/index.vue89
-rw-r--r--packages/frontend/src/pages/my-lists/index.vue63
-rw-r--r--packages/frontend/src/pages/my-lists/list.vue50
-rw-r--r--packages/frontend/src/pages/not-found.vue9
-rw-r--r--packages/frontend/src/pages/registry.keys.vue2
-rw-r--r--packages/frontend/src/pages/registry.value.vue2
-rw-r--r--packages/frontend/src/pages/reset-password.vue2
-rw-r--r--packages/frontend/src/pages/search.user.vue2
-rw-r--r--packages/frontend/src/pages/settings/deck.vue3
-rw-r--r--packages/frontend/src/pages/settings/drive-cleaner.vue2
-rw-r--r--packages/frontend/src/pages/settings/general.vue8
-rw-r--r--packages/frontend/src/pages/settings/index.vue2
-rw-r--r--packages/frontend/src/pages/settings/migration.vue3
-rw-r--r--packages/frontend/src/pages/settings/mute-block.vue4
-rw-r--r--packages/frontend/src/pages/settings/navbar.vue2
-rw-r--r--packages/frontend/src/pages/settings/preferences-backups.vue4
-rw-r--r--packages/frontend/src/pages/settings/privacy.vue4
-rw-r--r--packages/frontend/src/pages/settings/roles.vue2
-rw-r--r--packages/frontend/src/pages/settings/security.vue2
-rw-r--r--packages/frontend/src/pages/user-info.vue34
-rw-r--r--packages/frontend/src/pages/user/home.vue13
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'));