mirror of
https://github.com/MisskeyIO/misskey
synced 2024-11-27 06:18:40 +09:00
spec(skeb/role): Skeb募集中のクリエイターに自動でロールが付与されるように・バッジから募集状態の確認ができるように (MisskeyIO#593)
This commit is contained in:
parent
31ebd77e8a
commit
95838a036e
@ -1677,6 +1677,8 @@ _role:
|
||||
iconUrl: "Icon URL"
|
||||
asBadge: "Show as badge"
|
||||
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
|
||||
badgeBehavior: "Badge behavior"
|
||||
descriptionOfBadgeBehavior: "Set the behavior of the badge icon."
|
||||
isExplorable: "Make role explorable"
|
||||
descriptionOfIsExplorable: "This role's timeline and the list of users with this will be made public if enabled."
|
||||
displayOrder: "Position"
|
||||
|
8
locales/index.d.ts
vendored
8
locales/index.d.ts
vendored
@ -6558,6 +6558,14 @@ export interface Locale extends ILocale {
|
||||
* オンにすると、ユーザー名の横にロールのアイコンが表示されます。
|
||||
*/
|
||||
"descriptionOfAsBadge": string;
|
||||
/**
|
||||
* バッジの挙動
|
||||
*/
|
||||
"badgeBehavior": string;
|
||||
/**
|
||||
* バッジの挙動を設定します。
|
||||
*/
|
||||
"descriptionOfBadgeBehavior": string;
|
||||
/**
|
||||
* ユーザーを見つけやすくする
|
||||
*/
|
||||
|
@ -1695,6 +1695,8 @@ _role:
|
||||
iconUrl: "アイコン画像のURL"
|
||||
asBadge: "バッジとして表示"
|
||||
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
|
||||
badgeBehavior: "バッジの挙動"
|
||||
descriptionOfBadgeBehavior: "バッジの挙動を設定します。"
|
||||
isExplorable: "ユーザーを見つけやすくする"
|
||||
descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。"
|
||||
displayOrder: "表示順"
|
||||
|
@ -1661,6 +1661,8 @@ _role:
|
||||
iconUrl: "아이콘 URL"
|
||||
asBadge: "뱃지로 표시"
|
||||
descriptionOfAsBadge: "활성화하면 유저명 옆에 역할의 아이콘이 표시됩니다."
|
||||
badgeBehavior: "뱃지 동작"
|
||||
descriptionOfBadgeBehavior: "뱃지의 동작 방식을 설정합니다."
|
||||
isExplorable: "역할 타임라인 공개"
|
||||
descriptionOfIsExplorable: "활성화하면 역할 타임라인을 공개합니다. 비활성화 시 타임라인이 공개되지 않습니다."
|
||||
displayOrder: "표시 순서"
|
||||
|
@ -0,0 +1,11 @@
|
||||
export class RoleBadgeBehavior1711946753142 {
|
||||
name = 'RoleBadgeBehavior1711946753142'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "role" ADD "badgeBehavior" character varying(256)`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "badgeBehavior"`);
|
||||
}
|
||||
}
|
@ -72,6 +72,7 @@ type Source = {
|
||||
headers: { [x: string]: string };
|
||||
parameters: { [x: string]: string };
|
||||
userIdParameterName: string;
|
||||
roleId: string;
|
||||
}
|
||||
|
||||
proxy?: string;
|
||||
@ -154,6 +155,7 @@ export type Config = {
|
||||
headers: { [x: string]: string };
|
||||
parameters: { [x: string]: string };
|
||||
userIdParameterName: string;
|
||||
roleId: string;
|
||||
} | undefined;
|
||||
proxy: string | undefined;
|
||||
proxySmtp: string | undefined;
|
||||
|
@ -577,6 +577,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
isModerator: values.isModerator,
|
||||
isExplorable: values.isExplorable,
|
||||
asBadge: values.asBadge,
|
||||
badgeBehavior: values.badgeBehavior,
|
||||
canEditMembersByModerator: values.canEditMembersByModerator,
|
||||
displayOrder: values.displayOrder,
|
||||
policies: values.policies,
|
||||
|
@ -68,6 +68,7 @@ export class RoleEntityService {
|
||||
isModerator: role.isModerator,
|
||||
isExplorable: role.isExplorable,
|
||||
asBadge: role.asBadge,
|
||||
badgeBehavior: role.badgeBehavior,
|
||||
canEditMembersByModerator: role.canEditMembersByModerator,
|
||||
displayOrder: role.displayOrder,
|
||||
policies: policies,
|
||||
|
@ -484,6 +484,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
name: r.name,
|
||||
iconUrl: r.iconUrl,
|
||||
displayOrder: r.displayOrder,
|
||||
behavior: r.badgeBehavior ?? undefined,
|
||||
}))) : undefined,
|
||||
|
||||
...(isDetailed ? {
|
||||
|
@ -155,6 +155,11 @@ export class MiRole {
|
||||
})
|
||||
public asBadge: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256, nullable: true,
|
||||
})
|
||||
public badgeBehavior: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
@ -384,6 +384,10 @@ export const packedRoleSchema = {
|
||||
optional: false, nullable: false,
|
||||
example: false,
|
||||
},
|
||||
badgeBehavior: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
canEditMembersByModerator: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@ -176,6 +176,10 @@ export const packedUserLiteSchema = {
|
||||
type: 'number',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
behavior: {
|
||||
type: 'string',
|
||||
nullable: false, optional: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -36,6 +36,7 @@ export const paramDef = {
|
||||
isAdministrator: { type: 'boolean' },
|
||||
isExplorable: { type: 'boolean', default: false }, // optional for backward compatibility
|
||||
asBadge: { type: 'boolean' },
|
||||
badgeBehavior: { type: 'string', nullable: true },
|
||||
canEditMembersByModerator: { type: 'boolean' },
|
||||
displayOrder: { type: 'number' },
|
||||
policies: {
|
||||
|
@ -42,6 +42,7 @@ export const paramDef = {
|
||||
isAdministrator: { type: 'boolean' },
|
||||
isExplorable: { type: 'boolean' },
|
||||
asBadge: { type: 'boolean' },
|
||||
badgeBehavior: { type: 'string', nullable: true },
|
||||
canEditMembersByModerator: { type: 'boolean' },
|
||||
displayOrder: { type: 'number' },
|
||||
policies: {
|
||||
@ -92,6 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
isAdministrator: ps.isAdministrator,
|
||||
isExplorable: ps.isExplorable,
|
||||
asBadge: ps.asBadge,
|
||||
badgeBehavior: ps.badgeBehavior,
|
||||
canEditMembersByModerator: ps.canEditMembersByModerator,
|
||||
displayOrder: ps.displayOrder,
|
||||
policies: ps.policies,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@ -88,6 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
private roleService: RoleService,
|
||||
private loggerService: LoggerService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
) {
|
||||
@ -128,13 +130,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
error?: unknown,
|
||||
};
|
||||
|
||||
if (res.status > 399 || (json.error ?? json.ban_reason)) {
|
||||
const hasSkebRole = await this.roleService.getUserRoles(ps.userId).then(roles => roles.some(r => r.id === this.config.skebStatus?.roleId));
|
||||
|
||||
if (res.status > 299 || (json.error ?? json.ban_reason)) {
|
||||
logger.error('Skeb status response error', { url: url.href, userId: ps.userId, status: res.status, statusText: res.statusText, error: json.error ?? json.ban_reason });
|
||||
if (res.status === 404 && hasSkebRole) await this.roleService.unassign(ps.userId, this.config.skebStatus.roleId);
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
}
|
||||
|
||||
logger.info('Skeb status response', { url: url.href, userId: ps.userId, status: res.status, statusText: res.statusText, skebStatus: json });
|
||||
|
||||
if (json.is_acceptable) {
|
||||
if (!hasSkebRole) await this.roleService.assign(ps.userId, this.config.skebStatus.roleId);
|
||||
} else if (hasSkebRole) {
|
||||
await this.roleService.unassign(ps.userId, this.config.skebStatus.roleId);
|
||||
}
|
||||
|
||||
return {
|
||||
screenName: json.screen_name,
|
||||
isCreator: json.is_creator,
|
||||
|
@ -467,6 +467,7 @@ describe('Note', () => {
|
||||
isPublic: false,
|
||||
isExplorable: false,
|
||||
asBadge: false,
|
||||
badgeBehavior: null,
|
||||
canEditMembersByModerator: false,
|
||||
policies: {
|
||||
alwaysMarkNsfw: {
|
||||
@ -780,6 +781,7 @@ describe('Note', () => {
|
||||
isPublic: false,
|
||||
isExplorable: false,
|
||||
asBadge: false,
|
||||
badgeBehavior: null,
|
||||
canEditMembersByModerator: false,
|
||||
policies: {
|
||||
mentionLimit: {
|
||||
@ -834,6 +836,7 @@ describe('Note', () => {
|
||||
isPublic: false,
|
||||
isExplorable: false,
|
||||
asBadge: false,
|
||||
badgeBehavior: null,
|
||||
canEditMembersByModerator: false,
|
||||
policies: {
|
||||
mentionLimit: {
|
||||
@ -890,6 +893,7 @@ describe('Note', () => {
|
||||
isPublic: false,
|
||||
isExplorable: false,
|
||||
asBadge: false,
|
||||
badgeBehavior: null,
|
||||
canEditMembersByModerator: false,
|
||||
policies: {
|
||||
mentionLimit: {
|
||||
|
@ -651,11 +651,20 @@ describe('ユーザー', () => {
|
||||
});
|
||||
test('を取得することができ、バッヂロールがセットされていること', async () => {
|
||||
const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRoleBadge.id }, user: alice });
|
||||
assert.deepStrictEqual(response.badgeRoles, [{
|
||||
name: roleBadge.name,
|
||||
iconUrl: roleBadge.iconUrl,
|
||||
displayOrder: roleBadge.displayOrder,
|
||||
}]);
|
||||
if (roleBadge.badgeBehavior) {
|
||||
assert.deepStrictEqual(response.badgeRoles, [{
|
||||
name: roleBadge.name,
|
||||
iconUrl: roleBadge.iconUrl,
|
||||
displayOrder: roleBadge.displayOrder,
|
||||
behavior: roleBadge.badgeBehavior,
|
||||
}]);
|
||||
} else {
|
||||
assert.deepStrictEqual(response.badgeRoles, [{
|
||||
name: roleBadge.name,
|
||||
iconUrl: roleBadge.iconUrl,
|
||||
displayOrder: roleBadge.displayOrder,
|
||||
}]);
|
||||
}
|
||||
assert.deepStrictEqual(response.roles, []); // バッヂだからといってrolesが取れるとは限らない
|
||||
});
|
||||
test('をID指定のリスト形式で取得することができる(空)', async () => {
|
||||
|
@ -251,6 +251,7 @@ export const channel = async (user: UserToken, channel: Partial<misskey.entities
|
||||
export const role = async (user: UserToken, role: Partial<misskey.entities.Role> = {}, policies: any = {}): Promise<misskey.entities.Role> => {
|
||||
const res = await api('admin/roles/create', {
|
||||
asBadge: false,
|
||||
badgeBehavior: null,
|
||||
canEditMembersByModerator: false,
|
||||
color: null,
|
||||
condFormula: {
|
||||
|
@ -175,8 +175,8 @@ const align = () => {
|
||||
let left;
|
||||
let top;
|
||||
|
||||
const x = srcRect.left + (fixed.value ? 0 : window.pageXOffset);
|
||||
const y = srcRect.top + (fixed.value ? 0 : window.pageYOffset);
|
||||
const x = srcRect.left + (fixed.value ? 0 : window.scrollX);
|
||||
const y = srcRect.top + (fixed.value ? 0 : window.scrollY);
|
||||
|
||||
if (props.anchor.x === 'center') {
|
||||
left = x + (props.src.offsetWidth / 2) - (width / 2);
|
||||
@ -220,24 +220,24 @@ const align = () => {
|
||||
}
|
||||
} else {
|
||||
// 画面から横にはみ出る場合
|
||||
if (left + width - window.pageXOffset > (window.innerWidth - SCROLLBAR_THICKNESS)) {
|
||||
left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset - 1;
|
||||
if (left + width - window.scrollX > (window.innerWidth - SCROLLBAR_THICKNESS)) {
|
||||
left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.scrollX - 1;
|
||||
}
|
||||
|
||||
const underSpace = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - (top - window.pageYOffset);
|
||||
const underSpace = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - (top - window.scrollY);
|
||||
const upperSpace = (srcRect.top - MARGIN);
|
||||
|
||||
// 画面から縦にはみ出る場合
|
||||
if (top + height - window.pageYOffset > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) {
|
||||
if (top + height - window.scrollY > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) {
|
||||
if (props.noOverlap && props.anchor.x === 'center') {
|
||||
if (underSpace >= (upperSpace / 3)) {
|
||||
maxHeight.value = underSpace;
|
||||
} else {
|
||||
maxHeight.value = upperSpace;
|
||||
top = window.pageYOffset + ((upperSpace + MARGIN) - height);
|
||||
top = window.scrollY + ((upperSpace + MARGIN) - height);
|
||||
}
|
||||
} else {
|
||||
top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height + window.pageYOffset - 1;
|
||||
top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height + window.scrollY - 1;
|
||||
}
|
||||
} else {
|
||||
maxHeight.value = underSpace;
|
||||
@ -255,15 +255,15 @@ const align = () => {
|
||||
let transformOriginX = 'center';
|
||||
let transformOriginY = 'center';
|
||||
|
||||
if (top >= srcRect.top + props.src.offsetHeight + (fixed.value ? 0 : window.pageYOffset)) {
|
||||
if (top >= srcRect.top + props.src.offsetHeight + (fixed.value ? 0 : window.scrollY)) {
|
||||
transformOriginY = 'top';
|
||||
} else if ((top + height) <= srcRect.top + (fixed.value ? 0 : window.pageYOffset)) {
|
||||
} else if ((top + height) <= srcRect.top + (fixed.value ? 0 : window.scrollY)) {
|
||||
transformOriginY = 'bottom';
|
||||
}
|
||||
|
||||
if (left >= srcRect.left + props.src.offsetWidth + (fixed.value ? 0 : window.pageXOffset)) {
|
||||
if (left >= srcRect.left + props.src.offsetWidth + (fixed.value ? 0 : window.scrollX)) {
|
||||
transformOriginX = 'left';
|
||||
} else if ((left + width) <= srcRect.left + (fixed.value ? 0 : window.pageXOffset)) {
|
||||
} else if ((left + width) <= srcRect.left + (fixed.value ? 0 : window.scrollX)) {
|
||||
transformOriginX = 'right';
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
|
||||
<div :class="$style.username"><MkAcct :user="note.user"/></div>
|
||||
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
|
||||
<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
|
||||
<MkRoleBadgeIcon v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :userId="note.user.id" :role="role" :class="$style.badgeRole"/>
|
||||
</div>
|
||||
<div :class="$style.info">
|
||||
<div v-if="mock">
|
||||
@ -40,6 +40,7 @@ import * as Misskey from 'misskey-js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { notePage } from '@/filters/note.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import MkRoleBadgeIcon from '@/components/MkRoleBadgeIcon.vue';
|
||||
|
||||
defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
57
packages/frontend/src/components/MkRoleBadgeIcon.vue
Normal file
57
packages/frontend/src/components/MkRoleBadgeIcon.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<img ref="el" :src="role.iconUrl!" @click="onClick(role)"/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||
|
||||
const props = defineProps<{
|
||||
userId: string,
|
||||
role: { name: string, iconUrl: string | null, displayOrder: number, behavior?: string }
|
||||
}>();
|
||||
|
||||
const el = ref<HTMLElement | { $el: HTMLElement }>();
|
||||
const userSkebStatus = ref<Misskey.Endpoints['users/get-skeb-status']['res'] | null>(null);
|
||||
|
||||
async function fetchSkebStatus() {
|
||||
if (!instance.enableSkebStatus || props.role.behavior !== 'skeb') {
|
||||
userSkebStatus.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
userSkebStatus.value = await misskeyApiGet('users/get-skeb-status', { userId: props.userId });
|
||||
}
|
||||
|
||||
if (props.role.behavior === 'skeb') {
|
||||
useTooltip(el, async (showing) => {
|
||||
if (userSkebStatus.value == null) {
|
||||
await fetchSkebStatus();
|
||||
}
|
||||
|
||||
if (userSkebStatus.value === null) return;
|
||||
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkSkebStatusPopup.vue')), {
|
||||
showing,
|
||||
skebStatus: userSkebStatus.value,
|
||||
source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
}
|
||||
|
||||
async function onClick(role: { name: string, iconUrl: string | null, displayOrder: number, behavior?: string }) {
|
||||
if (role.behavior === 'skeb') {
|
||||
if (userSkebStatus.value == null) {
|
||||
await fetchSkebStatus();
|
||||
}
|
||||
|
||||
if (userSkebStatus.value != null) {
|
||||
window.open(`https://skeb.jp/@${userSkebStatus.value.screenName}`, '_blank', 'noopener');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
140
packages/frontend/src/components/MkSkebStatusPopup.vue
Normal file
140
packages/frontend/src/components/MkSkebStatusPopup.vue
Normal file
@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div ref="el" :class="$style.root" :style="{ zIndex }">
|
||||
<Transition
|
||||
:name="defaultStore.state.animation ? '_transition_zoom' : ''"
|
||||
@afterLeave="emit('closed')"
|
||||
>
|
||||
<div v-if="showing" class="_popup _shadow">
|
||||
<article :class="$style.body">
|
||||
<header :class="$style.header">
|
||||
<span v-if="skebStatus.isAcceptable" :class="$style.skebAcceptable">
|
||||
{{ i18n.ts._skebStatus.seeking }}
|
||||
</span>
|
||||
<span v-else-if="skebStatus.isCreator" :class="$style.skebStopped">
|
||||
{{ i18n.ts._skebStatus.stopped }}
|
||||
</span>
|
||||
<span v-else :class="$style.skebClient">
|
||||
{{ i18n.ts._skebStatus.client }}
|
||||
</span>
|
||||
<Mfm v-if="skebStatus.creatorRequestCount > 0" :text="i18n.tsx._skebStatus.nWorks({ n: skebStatus.creatorRequestCount.toLocaleString() })" :nyaize="false" :colored="false"/>
|
||||
<Mfm v-else-if="skebStatus.clientRequestCount > 0" :text="i18n.tsx._skebStatus.nRequests({ n: skebStatus.clientRequestCount.toLocaleString() })" :nyaize="false" :colored="false"/>
|
||||
</header>
|
||||
<div v-if="skebStatus.isAcceptable" :class="$style.divider"></div>
|
||||
<div v-if="skebStatus.isAcceptable" class="contents _gaps_s">
|
||||
<Mfm v-for="skill in skebStatus.skills" :key="skill.genre" :text="`${i18n.ts._skebStatus._genres[skill.genre]} ${i18n.tsx._skebStatus.yenX({ x: skill.amount.toLocaleString() })}`" :nyaize="false" :colored="false"/>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, onUnmounted, shallowRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { calcPopupPosition } from '@/scripts/popup-position.js';
|
||||
|
||||
const props = defineProps<{
|
||||
showing: boolean;
|
||||
skebStatus: Misskey.Endpoints['users/get-skeb-status']['res'];
|
||||
source: HTMLElement;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<(ev: 'closed') => void>();
|
||||
|
||||
// タイミングによっては最初から showing = false な場合があり、その場合に closed 扱いにしないと永久にDOMに残ることになる
|
||||
if (!props.showing) emit('closed');
|
||||
|
||||
const el = shallowRef<HTMLElement>();
|
||||
const zIndex = os.claimZIndex('high');
|
||||
|
||||
function setPosition() {
|
||||
if (el.value == null) return;
|
||||
|
||||
const data = calcPopupPosition(el.value, {
|
||||
anchorElement: props.source,
|
||||
direction: 'bottom',
|
||||
align: 'center',
|
||||
innerMargin: 0,
|
||||
});
|
||||
|
||||
el.value.style.transformOrigin = data.transformOrigin;
|
||||
el.value.style.left = data.left + 'px';
|
||||
el.value.style.top = data.top + 'px';
|
||||
}
|
||||
|
||||
let loopHandler;
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
setPosition();
|
||||
|
||||
const loop = () => {
|
||||
setPosition();
|
||||
loopHandler = window.requestAnimationFrame(loop);
|
||||
};
|
||||
|
||||
loop();
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.cancelAnimationFrame(loopHandler);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: absolute;
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 8px auto;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
}
|
||||
|
||||
.skebAcceptable,
|
||||
.skebStopped,
|
||||
.skebClient {
|
||||
display: inline-flex;
|
||||
border: solid 1px;
|
||||
border-radius: 6px;
|
||||
padding: 2px 6px;
|
||||
margin-right: 4px;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
.skebAcceptable {
|
||||
color: rgb(255, 255, 255);
|
||||
background-color: rgb(241, 70, 104);
|
||||
}
|
||||
|
||||
.skebStopped {
|
||||
color: rgb(255, 255, 255);
|
||||
background-color: rgb(54, 54, 54);
|
||||
}
|
||||
|
||||
.skebClient {
|
||||
color: rgb(255, 255, 255);
|
||||
background-color: rgb(54, 54, 54);
|
||||
}
|
||||
</style>
|
@ -34,8 +34,8 @@ const left = ref(0);
|
||||
onMounted(() => {
|
||||
try {
|
||||
const rect = props.source.getBoundingClientRect();
|
||||
const x = Math.max((rect.left + (props.source.offsetWidth / 2)) - (300 / 2), 6) + window.pageXOffset;
|
||||
const y = rect.top + props.source.offsetHeight + window.pageYOffset;
|
||||
const x = Math.max((rect.left + (props.source.offsetWidth / 2)) - (300 / 2), 6) + window.scrollX;
|
||||
const y = rect.top + props.source.offsetHeight + window.scrollY;
|
||||
|
||||
top.value = y;
|
||||
left.value = x;
|
||||
|
@ -63,6 +63,7 @@ if (props.id) {
|
||||
isPublic: false,
|
||||
isExplorable: false,
|
||||
asBadge: false,
|
||||
badgeBehavior: null,
|
||||
canEditMembersByModerator: false,
|
||||
displayOrder: 0,
|
||||
policies: {},
|
||||
|
@ -67,6 +67,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfAsBadge }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkInput v-if="role.asBadge" v-model="role.badgeBehavior" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.badgeBehavior }}</template>
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfBadgeBehavior }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkSwitch v-model="role.isExplorable" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.isExplorable }}</template>
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfIsExplorable }}</template>
|
||||
@ -848,6 +853,7 @@ const save = throttle(100, () => {
|
||||
isPublic: role.value.isPublic,
|
||||
isExplorable: role.value.isExplorable,
|
||||
asBadge: role.value.asBadge,
|
||||
badgeBehavior: role.value.badgeBehavior,
|
||||
canEditMembersByModerator: role.value.canEditMembersByModerator,
|
||||
policies: role.value.policies,
|
||||
};
|
||||
|
@ -319,7 +319,6 @@ async function fetchSkebStatus() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('fetching skeb status');
|
||||
userSkebStatus.value = await misskeyApiGet('users/get-skeb-status', { userId: props.user.id });
|
||||
}
|
||||
|
||||
|
@ -26,8 +26,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
||||
let top: number;
|
||||
|
||||
if (props.anchorElement) {
|
||||
left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2);
|
||||
top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin;
|
||||
left = rect.left + window.scrollX + (props.anchorElement.offsetWidth / 2);
|
||||
top = (rect.top + window.scrollY - contentHeight) - props.innerMargin;
|
||||
} else {
|
||||
left = props.x;
|
||||
top = (props.y - contentHeight) - props.innerMargin;
|
||||
@ -35,8 +35,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
||||
|
||||
left -= (el.offsetWidth / 2);
|
||||
|
||||
if (left + contentWidth - window.pageXOffset > window.innerWidth) {
|
||||
left = window.innerWidth - contentWidth + window.pageXOffset - 1;
|
||||
if (left + contentWidth - window.scrollX > window.innerWidth) {
|
||||
left = window.innerWidth - contentWidth + window.scrollX - 1;
|
||||
}
|
||||
|
||||
return [left, top];
|
||||
@ -47,8 +47,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
||||
let top: number;
|
||||
|
||||
if (props.anchorElement) {
|
||||
left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2);
|
||||
top = (rect.top + window.pageYOffset + props.anchorElement.offsetHeight) + props.innerMargin;
|
||||
left = rect.left + window.scrollX + (props.anchorElement.offsetWidth / 2);
|
||||
top = (rect.top + window.scrollY + props.anchorElement.offsetHeight) + props.innerMargin;
|
||||
} else {
|
||||
left = props.x;
|
||||
top = (props.y) + props.innerMargin;
|
||||
@ -56,8 +56,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
||||
|
||||
left -= (el.offsetWidth / 2);
|
||||
|
||||
if (left + contentWidth - window.pageXOffset > window.innerWidth) {
|
||||
left = window.innerWidth - contentWidth + window.pageXOffset - 1;
|
||||
if (left + contentWidth - window.scrollX > window.innerWidth) {
|
||||
left = window.innerWidth - contentWidth + window.scrollX - 1;
|
||||
}
|
||||
|
||||
return [left, top];
|
||||
@ -68,8 +68,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
||||
let top: number;
|
||||
|
||||
if (props.anchorElement) {
|
||||
left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin;
|
||||
top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2);
|
||||
left = (rect.left + window.scrollX - contentWidth) - props.innerMargin;
|
||||
top = rect.top + window.scrollY + (props.anchorElement.offsetHeight / 2);
|
||||
} else {
|
||||
left = (props.x - contentWidth) - props.innerMargin;
|
||||
top = props.y;
|
||||
@ -77,8 +77,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
||||
|
||||
top -= (el.offsetHeight / 2);
|
||||
|
||||
if (top + contentHeight - window.pageYOffset > window.innerHeight) {
|
||||
top = window.innerHeight - contentHeight + window.pageYOffset - 1;
|
||||
if (top + contentHeight - window.scrollY > window.innerHeight) {
|
||||
top = window.innerHeight - contentHeight + window.scrollY - 1;
|
||||
}
|
||||
|
||||
return [left, top];
|
||||
@ -89,15 +89,15 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
||||
let top: number;
|
||||
|
||||
if (props.anchorElement) {
|
||||
left = (rect.left + props.anchorElement.offsetWidth + window.pageXOffset) + props.innerMargin;
|
||||
left = (rect.left + props.anchorElement.offsetWidth + window.scrollX) + props.innerMargin;
|
||||
|
||||
if (props.align === 'top') {
|
||||
top = rect.top + window.pageYOffset;
|
||||
top = rect.top + window.scrollY;
|
||||
if (props.alignOffset != null) top += props.alignOffset;
|
||||
} else if (props.align === 'bottom') {
|
||||
// TODO
|
||||
} else { // center
|
||||
top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2);
|
||||
top = rect.top + window.scrollY + (props.anchorElement.offsetHeight / 2);
|
||||
top -= (el.offsetHeight / 2);
|
||||
}
|
||||
} else {
|
||||
@ -106,8 +106,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
||||
top -= (el.offsetHeight / 2);
|
||||
}
|
||||
|
||||
if (top + contentHeight - window.pageYOffset > window.innerHeight) {
|
||||
top = window.innerHeight - contentHeight + window.pageYOffset - 1;
|
||||
if (top + contentHeight - window.scrollY > window.innerHeight) {
|
||||
top = window.innerHeight - contentHeight + window.scrollY - 1;
|
||||
}
|
||||
|
||||
return [left, top];
|
||||
@ -123,7 +123,7 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
||||
const [left, top] = calcPosWhenTop();
|
||||
|
||||
// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
|
||||
if (top - window.pageYOffset < 0) {
|
||||
if (top - window.scrollY < 0) {
|
||||
const [left, top] = calcPosWhenBottom();
|
||||
return { left, top, transformOrigin: 'center top' };
|
||||
}
|
||||
@ -141,7 +141,7 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
||||
const [left, top] = calcPosWhenLeft();
|
||||
|
||||
// ツールチップを左に向かって表示するスペースがなければ右に向かって出す
|
||||
if (left - window.pageXOffset < 0) {
|
||||
if (left - window.scrollX < 0) {
|
||||
const [left, top] = calcPosWhenRight();
|
||||
return { left, top, transformOrigin: 'left center' };
|
||||
}
|
||||
|
@ -53,11 +53,11 @@ export function useChartTooltip(opts: { position: 'top' | 'middle' } = { positio
|
||||
const rect = context.chart.canvas.getBoundingClientRect();
|
||||
|
||||
tooltipShowing.value = true;
|
||||
tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX;
|
||||
tooltipX.value = rect.left + window.scrollX + context.tooltip.caretX;
|
||||
if (opts.position === 'top') {
|
||||
tooltipY.value = rect.top + window.pageYOffset;
|
||||
tooltipY.value = rect.top + window.scrollY;
|
||||
} else if (opts.position === 'middle') {
|
||||
tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY;
|
||||
tooltipY.value = rect.top + window.scrollY + context.tooltip.caretY;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3748,6 +3748,7 @@ export type components = {
|
||||
name: string;
|
||||
iconUrl: string | null;
|
||||
displayOrder: number;
|
||||
behavior?: string;
|
||||
})[];
|
||||
};
|
||||
UserDetailedNotMeOnly: {
|
||||
@ -4843,6 +4844,7 @@ export type components = {
|
||||
isExplorable: boolean;
|
||||
/** @example false */
|
||||
asBadge: boolean;
|
||||
badgeBehavior: string | null;
|
||||
/** @example false */
|
||||
canEditMembersByModerator: boolean;
|
||||
policies: {
|
||||
@ -9827,6 +9829,7 @@ export type operations = {
|
||||
/** @default false */
|
||||
isExplorable?: boolean;
|
||||
asBadge: boolean;
|
||||
badgeBehavior?: string | null;
|
||||
canEditMembersByModerator: boolean;
|
||||
displayOrder: number;
|
||||
policies: Record<string, never>;
|
||||
@ -10048,6 +10051,7 @@ export type operations = {
|
||||
isAdministrator: boolean;
|
||||
isExplorable?: boolean;
|
||||
asBadge: boolean;
|
||||
badgeBehavior?: string | null;
|
||||
canEditMembersByModerator: boolean;
|
||||
displayOrder: number;
|
||||
policies: Record<string, never>;
|
||||
|
Loading…
Reference in New Issue
Block a user