1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-07-27 21:04:37 +02:00

feat(frontend): ロール設定ページを追加

This commit is contained in:
mattyatea
2026-05-23 20:24:16 +09:00
parent d490936bf6
commit e57e88ebc6
7 changed files with 114 additions and 114 deletions

View File

@@ -1094,6 +1094,7 @@ likeOnlyForRemote: "全て (リモートはいいねのみ)"
nonSensitiveOnly: "非センシティブのみ"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "非センシティブのみ (リモートはいいねのみ)"
rolesAssignedToMe: "自分に割り当てられたロール"
roleSettings: "ロール設定"
resetPasswordConfirm: "パスワードリセットしますか?"
sensitiveWords: "センシティブワード"
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"

View File

@@ -93,6 +93,11 @@ const menuDef = computed<SuperMenuDef[]>(() => [{
text: i18n.ts.privacy,
to: '/settings/privacy',
active: currentPage.value?.route.name === 'privacy',
}, {
icon: 'ti ti-badges',
text: i18n.ts.roleSettings,
to: '/settings/roles',
active: currentPage.value?.route.name === 'roles',
}, {
icon: 'ti ti-bell',
text: i18n.ts.notifications,

View File

@@ -49,17 +49,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['roles']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-badges"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.rolesAssignedToMe }}</SearchLabel></template>
<div class="_gaps_s">
<MkRolePreview v-for="role in $i.roles" :key="role.id" :role="role" :forModeration="false"/>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['account', 'move', 'migration']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-plane"></i></SearchIcon></template>
@@ -171,7 +160,6 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import FormSection from '@/components/form/section.vue';
import { prefer } from '@/preferences.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
import { signout } from '@/signout.js';
import { migrateOldSettings } from '@/pref-migrate.js';
import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js';

View File

@@ -51,41 +51,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['role', 'badge', 'visibility']">
<FormSection>
<template #label><SearchLabel>{{ i18n.ts._roleDisplay.title }}</SearchLabel></template>
<template #description><SearchText>{{ i18n.ts._roleDisplay.description }}</SearchText></template>
<div class="_gaps_s">
<MkInfo v-if="roleDisplayRoles.length === 0">{{ i18n.ts._roleDisplay.noRoles }}</MkInfo>
<MkSwitch
v-for="role in roleDisplayRoles"
:key="role.id"
:modelValue="isRoleDisplayShown(role)"
:disabled="role.isPublicDisplayRequired"
@update:modelValue="value => updateRoleDisplay(role, value)"
>
<template #label>
<SearchLabel>
<span :class="$style.roleLabel" :style="{ '--color': role.color ?? '' }">
<span :class="$style.roleIcon">
<img v-if="role.iconUrl" :class="$style.roleBadge" :src="role.iconUrl"/>
<i v-else-if="role.isAdministrator" class="ti ti-crown"></i>
<i v-else-if="role.isModerator" class="ti ti-shield"></i>
<i v-else class="ti ti-user"></i>
</span>
<span>{{ role.name }}</span>
</span>
</SearchLabel>
</template>
<template #caption>
<SearchText v-if="role.isPublicDisplayRequired">{{ i18n.ts._roleDisplay.alwaysShownByAdmin }}</SearchText>
</template>
</MkSwitch>
</div>
</FormSection>
</SearchMarker>
<SearchMarker :keywords="['crawle', 'index', 'search']">
<MkSwitch v-model="noCrawle" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.noCrawle }}</SearchLabel></template>
@@ -248,7 +213,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import * as Misskey from 'misskey-js';
import type { MkSelectItem } from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
@@ -266,23 +230,9 @@ import * as os from '@/os.js';
import MkDisableSection from '@/components/MkDisableSection.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
import { updateCurrentAccount } from '@/accounts.js';
const $i = ensureSignin();
type RoleDisplayRole = Misskey.entities.IResponse['roles'][number] & {
isPublicDisplayRequired?: boolean;
};
type MeDetailedWithRoleDisplay = Misskey.entities.MeDetailed & {
hiddenRoleIds?: string[];
roles: RoleDisplayRole[];
};
type IUpdateWithHiddenRoleIdsRequest = Misskey.Endpoints['i/update']['req'] & {
hiddenRoleIds: string[];
};
const me = $i as MeDetailedWithRoleDisplay;
const isLocked = ref($i.isLocked);
const autoAcceptFollowed = ref($i.autoAcceptFollowed);
const noCrawle = ref($i.noCrawle);
@@ -292,8 +242,6 @@ const requireSigninToViewContents = ref($i.requireSigninToViewContents ?? false)
const makeNotesFollowersOnlyBefore = ref($i.makeNotesFollowersOnlyBefore ?? null);
const makeNotesHiddenBefore = ref($i.makeNotesHiddenBefore ?? null);
const hideOnlineStatus = ref($i.hideOnlineStatus);
const hiddenRoleIds = ref([...getHiddenRoleIds(me)]);
const roleDisplayRoles = computed(() => me.roles);
const publicReactions = ref($i.publicReactions);
const {
model: followingVisibility,
@@ -452,35 +400,6 @@ async function update_requireSigninToViewContents(value: boolean) {
save();
}
function getHiddenRoleIds(user: { hiddenRoleIds?: string[] }): string[] {
return user.hiddenRoleIds ?? [];
}
function isRoleDisplayShown(role: RoleDisplayRole): boolean {
return role.isPublicDisplayRequired === true || !hiddenRoleIds.value.includes(role.id);
}
async function updateRoleDisplay(role: RoleDisplayRole, visible: boolean) {
if (role.isPublicDisplayRequired === true) return;
const nextHiddenRoleIds = new Set(hiddenRoleIds.value);
if (visible) {
nextHiddenRoleIds.delete(role.id);
} else {
nextHiddenRoleIds.add(role.id);
}
const nextIds = roleDisplayRoles.value
.filter(role => role.isPublicDisplayRequired !== true && nextHiddenRoleIds.has(role.id))
.map(role => role.id);
const updated = await misskeyApi<MeDetailedWithRoleDisplay, 'i/update', IUpdateWithHiddenRoleIdsRequest>('i/update', {
hiddenRoleIds: nextIds,
});
updateCurrentAccount(updated);
hiddenRoleIds.value = [...getHiddenRoleIds(updated)];
}
function save() {
misskeyApi('i/update', {
isLocked: !!isLocked.value,
@@ -508,24 +427,3 @@ definePage(() => ({
icon: 'ti ti-lock-open',
}));
</script>
<style lang="scss" module>
.roleLabel {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--color, inherit);
}
.roleIcon {
display: inline-flex;
align-items: center;
width: 1.3em;
justify-content: center;
}
.roleBadge {
height: 1.3em;
vertical-align: -20%;
}
</style>

View File

@@ -0,0 +1,100 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<SearchMarker path="/settings/roles" :label="i18n.ts.roleSettings" :keywords="['role', 'badge']" icon="ti ti-badges">
<div class="_gaps_m">
<SearchMarker :keywords="['roles']">
<div class="_gaps_s">
<MkInfo v-if="roleDisplayRoles.length === 0">{{ i18n.ts._roleDisplay.noRoles }}</MkInfo>
<div v-for="role in roleDisplayRoles" :key="role.id" :class="$style.roleItem">
<MkRolePreview :role="role" :forModeration="false"/>
<MkSwitch
:modelValue="isRoleDisplayShown(role)"
:disabled="role.isPublicDisplayRequired"
@update:modelValue="value => updateRoleDisplay(role, value)"
>
<template #label>{{ i18n.ts._roleDisplay.title }}</template>
<template #caption v-if="role.isPublicDisplayRequired">{{ i18n.ts._roleDisplay.alwaysShownByAdmin }}</template>
</MkSwitch>
</div>
</div>
</SearchMarker>
</div>
</SearchMarker>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import type * as Misskey from 'misskey-js';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkRolePreview from '@/components/MkRolePreview.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { ensureSignin } from '@/i.js';
import { definePage } from '@/page.js';
import { updateCurrentAccount } from '@/accounts.js';
const $i = ensureSignin();
type RoleDisplayRole = Misskey.entities.IResponse['roles'][number] & {
isPublicDisplayRequired?: boolean;
};
type MeDetailedWithRoleDisplay = Misskey.entities.MeDetailed & {
hiddenRoleIds?: string[];
roles: RoleDisplayRole[];
};
type IUpdateWithHiddenRoleIdsRequest = Misskey.Endpoints['i/update']['req'] & {
hiddenRoleIds: string[];
};
const me = $i as MeDetailedWithRoleDisplay;
const hiddenRoleIds = ref([...getHiddenRoleIds(me)]);
const roleDisplayRoles = computed(() => me.roles);
function getHiddenRoleIds(user: { hiddenRoleIds?: string[] }): string[] {
return user.hiddenRoleIds ?? [];
}
function isRoleDisplayShown(role: RoleDisplayRole): boolean {
return role.isPublicDisplayRequired === true || !hiddenRoleIds.value.includes(role.id);
}
async function updateRoleDisplay(role: RoleDisplayRole, visible: boolean) {
if (role.isPublicDisplayRequired === true) return;
const nextHiddenRoleIds = new Set(hiddenRoleIds.value);
if (visible) {
nextHiddenRoleIds.delete(role.id);
} else {
nextHiddenRoleIds.add(role.id);
}
const nextIds = roleDisplayRoles.value
.filter(role => role.isPublicDisplayRequired !== true && nextHiddenRoleIds.has(role.id))
.map(role => role.id);
const updated = await misskeyApi<MeDetailedWithRoleDisplay, 'i/update', IUpdateWithHiddenRoleIdsRequest>('i/update', {
hiddenRoleIds: nextIds,
});
updateCurrentAccount(updated);
hiddenRoleIds.value = [...getHiddenRoleIds(updated)];
}
definePage(() => ({
title: i18n.ts.roleSettings,
icon: 'ti ti-badges',
}));
</script>
<style lang="scss" module>
.roleItem {
display: flex;
flex-direction: column;
gap: 8px;
}
</style>

View File

@@ -88,6 +88,10 @@ export const ROUTE_DEF = [{
path: '/privacy',
name: 'privacy',
component: page(() => import('@/pages/settings/privacy.vue')),
}, {
path: '/roles',
name: 'roles',
component: page(() => import('@/pages/settings/roles.vue')),
}, {
path: '/emoji-palette',
name: 'emoji-palette',

View File

@@ -4388,6 +4388,10 @@ export interface Locale extends ILocale {
* 自分に割り当てられたロール
*/
"rolesAssignedToMe": string;
/**
* ロール設定
*/
"roleSettings": string;
/**
* パスワードリセットしますか?
*/