Merge branch 'develop' into pag-back
This commit is contained in:
commit
ac96aba0f5
16 changed files with 202 additions and 149 deletions
|
@ -30,12 +30,13 @@
|
|||
- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正
|
||||
- Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正
|
||||
- Fix: iOSで画面を回転させるとテキストサイズが変わる問題を修正
|
||||
- Fix: タイムラインを下にスクロールしてノート画面に移動して再び戻ったら以前のスクロール位置を失う問題を修正
|
||||
|
||||
### Server
|
||||
- cacheRemoteFilesの初期値はfalseになりました
|
||||
- 一部のfeatured noteを照会できない問題を修正
|
||||
- ファイルアップロード時等にファイル名の拡張子を修正する関数(correctFilename)の挙動を改善
|
||||
- fix: muteがapiからのuser list timeline取得で機能しない問題を修正
|
||||
- Fix: 一部のfeatured noteを照会できない問題を修正
|
||||
- Fix: muteがapiからのuser list timeline取得で機能しない問題を修正
|
||||
|
||||
## 13.14.2
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div class="bcekxzvu _margin _panel">
|
||||
<div class="target">
|
||||
<MkA v-user-preview="report.targetUserId" class="info" :to="`/user-info/${report.targetUserId}`">
|
||||
<MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`">
|
||||
<MkAvatar class="avatar" :user="report.targetUser" indicator/>
|
||||
<div class="names">
|
||||
<MkUserName class="name" :user="report.targetUser"/>
|
||||
|
|
|
@ -9,16 +9,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-show="loaded" :class="$style.root">
|
||||
<img :src="serverErrorImageUrl" class="_ghost" :class="$style.img"/>
|
||||
<div class="_gaps">
|
||||
<p><b><i class="ti ti-alert-triangle"></i> {{ i18n.ts.pageLoadError }}</b></p>
|
||||
<p v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</p>
|
||||
<p v-else-if="serverIsDead">{{ i18n.ts.serverIsDead }}</p>
|
||||
<div><b><i class="ti ti-alert-triangle"></i> {{ i18n.ts.pageLoadError }}</b></div>
|
||||
<div v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</div>
|
||||
<div v-else-if="serverIsDead">{{ i18n.ts.serverIsDead }}</div>
|
||||
<template v-else>
|
||||
<p>{{ i18n.ts.newVersionOfClientAvailable }}</p>
|
||||
<p>{{ i18n.ts.youShouldUpgradeClient }}</p>
|
||||
<div>{{ i18n.ts.newVersionOfClientAvailable }}</div>
|
||||
<div>{{ i18n.ts.youShouldUpgradeClient }}</div>
|
||||
<MkButton style="margin: 8px auto;" @click="reload">{{ i18n.ts.reload }}</MkButton>
|
||||
</template>
|
||||
<p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.ts.troubleshooting }}</MkA></p>
|
||||
<p v-if="error" style="opacity: 0.7;">ERROR: {{ error }}</p>
|
||||
<div><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.ts.troubleshooting }}</MkA></div>
|
||||
<div v-if="error" style="opacity: 0.7;">ERROR: {{ error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
|
|
@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #value><span class="_monospace"><MkTime :time="file.createdAt" mode="detail" style="display: block;"/></span></template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
<MkA v-if="file.user" class="user" :to="`/user-info/${file.user.id}`">
|
||||
<MkA v-if="file.user" class="user" :to="`/admin/user/${file.user.id}`">
|
||||
<MkUserCardMini :user="file.user"/>
|
||||
</MkA>
|
||||
<div>
|
||||
|
|
|
@ -24,12 +24,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<MkInfo v-if="user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo>
|
||||
|
||||
<div v-if="user.url" class="_formLinksGrid">
|
||||
<FormLink :to="userPage(user)">Profile</FormLink>
|
||||
<FormLink :to="user.url" :external="true">Profile (remote)</FormLink>
|
||||
</div>
|
||||
<FormLink v-else :to="userPage(user)">Profile</FormLink>
|
||||
|
||||
<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||
|
@ -57,6 +51,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkKeyValue>
|
||||
</div>
|
||||
|
||||
<MkTextarea v-model="moderationNote" manualSave>
|
||||
<template #label>Moderation note</template>
|
||||
</MkTextarea>
|
||||
|
||||
<!--
|
||||
<FormSection>
|
||||
<template #label>ActivityPub</template>
|
||||
|
||||
|
@ -90,95 +89,85 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkFolder>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div v-else-if="tab === 'moderation'" class="_gaps_m">
|
||||
<MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
|
||||
|
||||
<div>
|
||||
<MkButton v-if="user.host == null && iAmModerator" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
|
||||
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
|
||||
</div>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-license"></i></template>
|
||||
<template #label>{{ i18n.ts._role.policies }}</template>
|
||||
<FormSection>
|
||||
<div class="_gaps">
|
||||
<div v-for="policy in Object.keys(info.policies)" :key="policy">
|
||||
{{ policy }} ... {{ info.policies[policy] }}
|
||||
<MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
|
||||
|
||||
<div>
|
||||
<MkButton v-if="user.host == null" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-badges"></i></template>
|
||||
<template #label>{{ i18n.ts.roles }}</template>
|
||||
<div class="_gaps">
|
||||
<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">
|
||||
<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>
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-license"></i></template>
|
||||
<template #label>{{ i18n.ts._role.policies }}</template>
|
||||
<div class="_gaps">
|
||||
<div v-for="policy in Object.keys(info.policies)" :key="policy">
|
||||
{{ policy }} ... {{ info.policies[policy] }}
|
||||
</div>
|
||||
</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>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="user.host == null && iAmModerator">
|
||||
<template #icon><i class="ti ti-speakerphone"></i></template>
|
||||
<template #label>{{ i18n.ts.announcements }}</template>
|
||||
<div class="_gaps">
|
||||
<MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts.new }}</MkButton>
|
||||
|
||||
<MkPagination :pagination="announcementsPagination">
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<div v-for="announcement in items" :key="announcement.id" v-panel :class="$style.announcementItem" @click="editAnnouncement(announcement)">
|
||||
<span style="margin-right: 0.5em;">
|
||||
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
|
||||
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
|
||||
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
|
||||
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
|
||||
</span>
|
||||
<span>{{ announcement.title }}</span>
|
||||
<span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }}</span>
|
||||
</div>
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-password"></i></template>
|
||||
<template #label>IP</template>
|
||||
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
|
||||
<MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo>
|
||||
<template v-if="iAmAdmin && ips">
|
||||
<div v-for="record in ips" :key="record.ip" class="_monospace" :class="$style.ip" style="margin: 1em 0;">
|
||||
<span class="date">{{ record.createdAt }}</span>
|
||||
<span class="ip">{{ record.ip }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-password"></i></template>
|
||||
<template #label>IP</template>
|
||||
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
|
||||
<MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo>
|
||||
<template v-if="iAmAdmin && ips">
|
||||
<div v-for="record in ips" :key="record.ip" class="_monospace" :class="$style.ip" style="margin: 1em 0;">
|
||||
<span class="date">{{ record.createdAt }}</span>
|
||||
<span class="ip">{{ record.ip }}</span>
|
||||
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'roles'" class="_gaps">
|
||||
<MkButton v-if="user.host == null" 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">
|
||||
<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>
|
||||
|
||||
<div v-else-if="tab === 'announcements'" class="_gaps">
|
||||
<MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts.new }}</MkButton>
|
||||
|
||||
<MkPagination :pagination="announcementsPagination">
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<div v-for="announcement in items" :key="announcement.id" v-panel :class="$style.announcementItem" @click="editAnnouncement(announcement)">
|
||||
<span style="margin-right: 0.5em;">
|
||||
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
|
||||
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
|
||||
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
|
||||
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
|
||||
</span>
|
||||
<span>{{ announcement.title }}</span>
|
||||
<span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkFolder>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-cloud"></i></template>
|
||||
<template #label>{{ i18n.ts.files }}</template>
|
||||
<MkFileListForAdmin :pagination="filesPagination" viewMode="grid"/>
|
||||
</MkFolder>
|
||||
|
||||
<MkTextarea v-model="moderationNote" manualSave>
|
||||
<template #label>Moderation note</template>
|
||||
</MkTextarea>
|
||||
<div v-else-if="tab === 'drive'" class="_gaps">
|
||||
<MkFileListForAdmin :pagination="filesPagination" viewMode="grid"/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'chart'" class="_gaps_m">
|
||||
|
@ -230,7 +219,7 @@ import { url } from '@/config';
|
|||
import { userPage, acct } from '@/filters/user';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
import { iAmAdmin, iAmModerator, $i } from '@/account';
|
||||
import { iAmAdmin, $i } from '@/account';
|
||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
|
||||
|
@ -269,34 +258,26 @@ const announcementsPagination = {
|
|||
let expandedRoles = $ref([]);
|
||||
|
||||
function createFetcher() {
|
||||
if (iAmModerator) {
|
||||
return () => Promise.all([os.api('users/show', {
|
||||
userId: props.userId,
|
||||
}), os.api('admin/show-user', {
|
||||
userId: props.userId,
|
||||
}), iAmAdmin ? os.api('admin/get-user-ips', {
|
||||
userId: props.userId,
|
||||
}) : Promise.resolve(null)]).then(([_user, _info, _ips]) => {
|
||||
user = _user;
|
||||
info = _info;
|
||||
ips = _ips;
|
||||
moderator = info.isModerator;
|
||||
silenced = info.isSilenced;
|
||||
suspended = info.isSuspended;
|
||||
moderationNote = info.moderationNote;
|
||||
return () => Promise.all([os.api('users/show', {
|
||||
userId: props.userId,
|
||||
}), os.api('admin/show-user', {
|
||||
userId: props.userId,
|
||||
}), iAmAdmin ? os.api('admin/get-user-ips', {
|
||||
userId: props.userId,
|
||||
}) : Promise.resolve(null)]).then(([_user, _info, _ips]) => {
|
||||
user = _user;
|
||||
info = _info;
|
||||
ips = _ips;
|
||||
moderator = info.isModerator;
|
||||
silenced = info.isSilenced;
|
||||
suspended = info.isSuspended;
|
||||
moderationNote = info.moderationNote;
|
||||
|
||||
watch($$(moderationNote), async () => {
|
||||
await os.api('admin/update-user-note', { userId: user.id, text: moderationNote });
|
||||
await refreshUser();
|
||||
});
|
||||
watch($$(moderationNote), async () => {
|
||||
await os.api('admin/update-user-note', { userId: user.id, text: moderationNote });
|
||||
await refreshUser();
|
||||
});
|
||||
} else {
|
||||
return () => os.api('users/show', {
|
||||
userId: props.userId,
|
||||
}).then((res) => {
|
||||
user = res;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function refreshUser() {
|
||||
|
@ -472,11 +453,19 @@ const headerTabs = $computed(() => [{
|
|||
key: 'overview',
|
||||
title: i18n.ts.overview,
|
||||
icon: 'ti ti-info-circle',
|
||||
}, iAmModerator ? {
|
||||
key: 'moderation',
|
||||
title: i18n.ts.moderation,
|
||||
icon: 'ti ti-user-exclamation',
|
||||
} : null, {
|
||||
}, {
|
||||
key: 'roles',
|
||||
title: i18n.ts.roles,
|
||||
icon: 'ti ti-badges',
|
||||
}, {
|
||||
key: 'announcements',
|
||||
title: i18n.ts.announcements,
|
||||
icon: 'ti ti-speakerphone',
|
||||
}, {
|
||||
key: 'drive',
|
||||
title: i18n.ts.drive,
|
||||
icon: 'ti ti-cloud',
|
||||
}, {
|
||||
key: 'chart',
|
||||
title: i18n.ts.charts,
|
||||
icon: 'ti ti-chart-line',
|
||||
|
@ -484,11 +473,11 @@ const headerTabs = $computed(() => [{
|
|||
key: 'raw',
|
||||
title: 'Raw',
|
||||
icon: 'ti ti-code',
|
||||
}].filter(x => x != null));
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: user ? acct(user) : i18n.ts.userInfo,
|
||||
icon: 'ti ti-info-circle',
|
||||
icon: 'ti ti-user-exclamation',
|
||||
})));
|
||||
</script>
|
||||
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div v-else :class="$style.root" class="_panel">
|
||||
<MkA v-for="user in moderators" :key="user.id" class="user" :to="`/user-info/${user.id}`">
|
||||
<MkA v-for="user in moderators" :key="user.id" class="user" :to="`/admin/user/${user.id}`">
|
||||
<MkAvatar :user="user" class="avatar" indicator/>
|
||||
</MkA>
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div v-else class="users">
|
||||
<MkA v-for="(user, i) in newUsers" :key="user.id" :to="`/user-info/${user.id}`" class="user">
|
||||
<MkA v-for="(user, i) in newUsers" :key="user.id" :to="`/admin/user/${user.id}`" class="user">
|
||||
<MkUserCardMini :user="user"/>
|
||||
</MkA>
|
||||
</div>
|
||||
|
|
|
@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_s">
|
||||
<div v-for="item in items" :key="item.user.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedItems.includes(item.id) }]">
|
||||
<div :class="$style.userItemMain">
|
||||
<MkA :class="$style.userItemMainBody" :to="`/user-info/${item.user.id}`">
|
||||
<MkA :class="$style.userItemMainBody" :to="`/admin/user/${item.user.id}`">
|
||||
<MkUserCardMini :user="item.user"/>
|
||||
</MkA>
|
||||
<button class="_button" :class="$style.userToggle" @click="toggleItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
|
||||
|
|
|
@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination">
|
||||
<div :class="$style.users">
|
||||
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" :class="$style.user" :to="`/user-info/${user.id}`">
|
||||
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" :class="$style.user" :to="`/admin/user/${user.id}`">
|
||||
<MkUserCardMini :user="user"/>
|
||||
</MkA>
|
||||
</div>
|
||||
|
@ -116,7 +116,7 @@ async function addUser() {
|
|||
}
|
||||
|
||||
function show(user) {
|
||||
os.pageWindow(`/user-info/${user.id}`);
|
||||
os.pageWindow(`/admin/user/${user.id}`);
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
|
|
|
@ -102,7 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div v-else-if="tab === 'users'" class="_gaps_m">
|
||||
<MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;">
|
||||
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/user-info/${user.id}`">
|
||||
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/admin/user/${user.id}`">
|
||||
<MkUserCardMini :user="user"/>
|
||||
</MkA>
|
||||
</MkPagination>
|
||||
|
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_s">
|
||||
<div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedRenoteMuteItems.includes(item.id) }]">
|
||||
<div :class="$style.userItemMain">
|
||||
<MkA :class="$style.userItemMainBody" :to="`/user-info/${item.mutee.id}`">
|
||||
<MkA :class="$style.userItemMainBody" :to="userPage(item.mutee)">
|
||||
<MkUserCardMini :user="item.mutee"/>
|
||||
</MkA>
|
||||
<button class="_button" :class="$style.userToggle" @click="toggleRenoteMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
|
||||
|
@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_s">
|
||||
<div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedMuteItems.includes(item.id) }]">
|
||||
<div :class="$style.userItemMain">
|
||||
<MkA :class="$style.userItemMainBody" :to="`/user-info/${item.mutee.id}`">
|
||||
<MkA :class="$style.userItemMainBody" :to="userPage(item.mutee)">
|
||||
<MkUserCardMini :user="item.mutee"/>
|
||||
</MkA>
|
||||
<button class="_button" :class="$style.userToggle" @click="toggleMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
|
||||
|
@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_s">
|
||||
<div v-for="item in items" :key="item.blockee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedBlockItems.includes(item.id) }]">
|
||||
<div :class="$style.userItemMain">
|
||||
<MkA :class="$style.userItemMainBody" :to="`/user-info/${item.blockee.id}`">
|
||||
<MkA :class="$style.userItemMainBody" :to="userPage(item.blockee)">
|
||||
<MkUserCardMini :user="item.blockee"/>
|
||||
</MkA>
|
||||
<button class="_button" :class="$style.userToggle" @click="toggleBlockItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
|
||||
|
|
|
@ -42,10 +42,6 @@ export const routes = [{
|
|||
}, {
|
||||
path: '/clips/:clipId',
|
||||
component: page(() => import('./pages/clip.vue')),
|
||||
}, {
|
||||
path: '/user-info/:userId',
|
||||
component: page(() => import('./pages/user-info.vue')),
|
||||
hash: 'initialTab',
|
||||
}, {
|
||||
path: '/instance-info/:host',
|
||||
component: page(() => import('./pages/instance-info.vue')),
|
||||
|
@ -334,6 +330,9 @@ export const routes = [{
|
|||
}, {
|
||||
path: '/registry',
|
||||
component: page(() => import('./pages/registry.vue')),
|
||||
}, {
|
||||
path: '/admin/user/:userId',
|
||||
component: iAmModerator ? page(() => import('./pages/admin-user.vue')) : page(() => import('./pages/not-found.vue')),
|
||||
}, {
|
||||
path: '/admin/file/:fileId',
|
||||
component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')),
|
||||
|
|
|
@ -133,13 +133,13 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
|
|||
action: () => {
|
||||
copyToClipboard(`@${user.username}@${user.host ?? host}`);
|
||||
},
|
||||
}, {
|
||||
icon: 'ti ti-info-circle',
|
||||
text: i18n.ts.info,
|
||||
}, ...(iAmModerator ? [{
|
||||
icon: 'ti ti-user-exclamation',
|
||||
text: i18n.ts.moderation,
|
||||
action: () => {
|
||||
router.push(`/user-info/${user.id}`);
|
||||
router.push(`/admin/user/${user.id}`);
|
||||
},
|
||||
}, {
|
||||
}] : []), {
|
||||
icon: 'ti ti-rss',
|
||||
text: i18n.ts.copyRSS,
|
||||
action: () => {
|
||||
|
|
|
@ -14,7 +14,7 @@ export async function lookupUser() {
|
|||
if (canceled) return;
|
||||
|
||||
const show = (user) => {
|
||||
os.pageWindow(`/user-info/${user.id}`);
|
||||
os.pageWindow(`/admin/user/${user.id}`);
|
||||
};
|
||||
|
||||
const usernamePromise = os.api('users/show', Acct.parse(result));
|
||||
|
|
|
@ -30,7 +30,7 @@ export function getScrollPosition(el: HTMLElement | null): number {
|
|||
|
||||
export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) {
|
||||
// とりあえず評価してみる
|
||||
if (isTopVisible(el, tolerance)) {
|
||||
if (el.isConnected && isTopVisible(el, tolerance)) {
|
||||
cb();
|
||||
if (once) return null;
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
|
|||
const container = getScrollContainer(el);
|
||||
|
||||
// とりあえず評価してみる
|
||||
if (isBottomVisible(el, tolerance, container)) {
|
||||
if (el.isConnected && isBottomVisible(el, tolerance, container)) {
|
||||
cb();
|
||||
if (once) return null;
|
||||
}
|
||||
|
|
64
packages/frontend/test/scroll.test.ts
Normal file
64
packages/frontend/test/scroll.test.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { describe, test, assert, afterEach } from 'vitest';
|
||||
import { Window } from 'happy-dom';
|
||||
import { onScrollBottom, onScrollTop } from '@/scripts/scroll';
|
||||
|
||||
describe('Scroll', () => {
|
||||
describe('onScrollTop', () => {
|
||||
test('Initial onScrollTop callback for connected elements', () => {
|
||||
const { document } = new Window();
|
||||
const div = document.createElement('div');
|
||||
assert.strictEqual(div.scrollTop, 0);
|
||||
|
||||
document.body.append(div);
|
||||
|
||||
let called = false;
|
||||
onScrollTop(div as any as HTMLElement, () => called = true);
|
||||
|
||||
assert.ok(called);
|
||||
});
|
||||
|
||||
test('No onScrollTop callback for disconnected elements', () => {
|
||||
const { document } = new Window();
|
||||
const div = document.createElement('div');
|
||||
assert.strictEqual(div.scrollTop, 0);
|
||||
|
||||
let called = false;
|
||||
onScrollTop(div as any as HTMLElement, () => called = true);
|
||||
|
||||
assert.ok(!called);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onScrollBottom', () => {
|
||||
test('Initial onScrollBottom callback for connected elements', () => {
|
||||
const { document } = new Window();
|
||||
const div = document.createElement('div');
|
||||
assert.strictEqual(div.scrollTop, 0);
|
||||
(div as any).scrollHeight = 100; // happy-dom has no scrollHeight
|
||||
|
||||
document.body.append(div);
|
||||
|
||||
let called = false;
|
||||
onScrollBottom(div as any as HTMLElement, () => called = true);
|
||||
|
||||
assert.ok(called);
|
||||
});
|
||||
|
||||
test('No onScrollBottom callback for disconnected elements', () => {
|
||||
const { document } = new Window();
|
||||
const div = document.createElement('div');
|
||||
assert.strictEqual(div.scrollTop, 0);
|
||||
(div as any).scrollHeight = 100; // happy-dom has no scrollHeight
|
||||
|
||||
let called = false;
|
||||
onScrollBottom(div as any as HTMLElement, () => called = true);
|
||||
|
||||
assert.ok(!called);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue