spec(skeb/role): Skeb募集中のクリエイターに自動でロールが付与されるように・バッジから募集状態の確認ができるように (MisskeyIO#593)
This commit is contained in:
parent
31ebd77e8a
commit
95838a036e
@ -1677,6 +1677,8 @@ _role:
|
|||||||
iconUrl: "Icon URL"
|
iconUrl: "Icon URL"
|
||||||
asBadge: "Show as badge"
|
asBadge: "Show as badge"
|
||||||
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
|
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"
|
isExplorable: "Make role explorable"
|
||||||
descriptionOfIsExplorable: "This role's timeline and the list of users with this will be made public if enabled."
|
descriptionOfIsExplorable: "This role's timeline and the list of users with this will be made public if enabled."
|
||||||
displayOrder: "Position"
|
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;
|
"descriptionOfAsBadge": string;
|
||||||
|
/**
|
||||||
|
* バッジの挙動
|
||||||
|
*/
|
||||||
|
"badgeBehavior": string;
|
||||||
|
/**
|
||||||
|
* バッジの挙動を設定します。
|
||||||
|
*/
|
||||||
|
"descriptionOfBadgeBehavior": string;
|
||||||
/**
|
/**
|
||||||
* ユーザーを見つけやすくする
|
* ユーザーを見つけやすくする
|
||||||
*/
|
*/
|
||||||
|
@ -1695,6 +1695,8 @@ _role:
|
|||||||
iconUrl: "アイコン画像のURL"
|
iconUrl: "アイコン画像のURL"
|
||||||
asBadge: "バッジとして表示"
|
asBadge: "バッジとして表示"
|
||||||
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
|
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
|
||||||
|
badgeBehavior: "バッジの挙動"
|
||||||
|
descriptionOfBadgeBehavior: "バッジの挙動を設定します。"
|
||||||
isExplorable: "ユーザーを見つけやすくする"
|
isExplorable: "ユーザーを見つけやすくする"
|
||||||
descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。"
|
descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。"
|
||||||
displayOrder: "表示順"
|
displayOrder: "表示順"
|
||||||
|
@ -1661,6 +1661,8 @@ _role:
|
|||||||
iconUrl: "아이콘 URL"
|
iconUrl: "아이콘 URL"
|
||||||
asBadge: "뱃지로 표시"
|
asBadge: "뱃지로 표시"
|
||||||
descriptionOfAsBadge: "활성화하면 유저명 옆에 역할의 아이콘이 표시됩니다."
|
descriptionOfAsBadge: "활성화하면 유저명 옆에 역할의 아이콘이 표시됩니다."
|
||||||
|
badgeBehavior: "뱃지 동작"
|
||||||
|
descriptionOfBadgeBehavior: "뱃지의 동작 방식을 설정합니다."
|
||||||
isExplorable: "역할 타임라인 공개"
|
isExplorable: "역할 타임라인 공개"
|
||||||
descriptionOfIsExplorable: "활성화하면 역할 타임라인을 공개합니다. 비활성화 시 타임라인이 공개되지 않습니다."
|
descriptionOfIsExplorable: "활성화하면 역할 타임라인을 공개합니다. 비활성화 시 타임라인이 공개되지 않습니다."
|
||||||
displayOrder: "표시 순서"
|
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 };
|
headers: { [x: string]: string };
|
||||||
parameters: { [x: string]: string };
|
parameters: { [x: string]: string };
|
||||||
userIdParameterName: string;
|
userIdParameterName: string;
|
||||||
|
roleId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy?: string;
|
proxy?: string;
|
||||||
@ -154,6 +155,7 @@ export type Config = {
|
|||||||
headers: { [x: string]: string };
|
headers: { [x: string]: string };
|
||||||
parameters: { [x: string]: string };
|
parameters: { [x: string]: string };
|
||||||
userIdParameterName: string;
|
userIdParameterName: string;
|
||||||
|
roleId: string;
|
||||||
} | undefined;
|
} | undefined;
|
||||||
proxy: string | undefined;
|
proxy: string | undefined;
|
||||||
proxySmtp: string | undefined;
|
proxySmtp: string | undefined;
|
||||||
|
@ -577,6 +577,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||||||
isModerator: values.isModerator,
|
isModerator: values.isModerator,
|
||||||
isExplorable: values.isExplorable,
|
isExplorable: values.isExplorable,
|
||||||
asBadge: values.asBadge,
|
asBadge: values.asBadge,
|
||||||
|
badgeBehavior: values.badgeBehavior,
|
||||||
canEditMembersByModerator: values.canEditMembersByModerator,
|
canEditMembersByModerator: values.canEditMembersByModerator,
|
||||||
displayOrder: values.displayOrder,
|
displayOrder: values.displayOrder,
|
||||||
policies: values.policies,
|
policies: values.policies,
|
||||||
|
@ -68,6 +68,7 @@ export class RoleEntityService {
|
|||||||
isModerator: role.isModerator,
|
isModerator: role.isModerator,
|
||||||
isExplorable: role.isExplorable,
|
isExplorable: role.isExplorable,
|
||||||
asBadge: role.asBadge,
|
asBadge: role.asBadge,
|
||||||
|
badgeBehavior: role.badgeBehavior,
|
||||||
canEditMembersByModerator: role.canEditMembersByModerator,
|
canEditMembersByModerator: role.canEditMembersByModerator,
|
||||||
displayOrder: role.displayOrder,
|
displayOrder: role.displayOrder,
|
||||||
policies: policies,
|
policies: policies,
|
||||||
|
@ -484,6 +484,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
name: r.name,
|
name: r.name,
|
||||||
iconUrl: r.iconUrl,
|
iconUrl: r.iconUrl,
|
||||||
displayOrder: r.displayOrder,
|
displayOrder: r.displayOrder,
|
||||||
|
behavior: r.badgeBehavior ?? undefined,
|
||||||
}))) : undefined,
|
}))) : undefined,
|
||||||
|
|
||||||
...(isDetailed ? {
|
...(isDetailed ? {
|
||||||
|
@ -155,6 +155,11 @@ export class MiRole {
|
|||||||
})
|
})
|
||||||
public asBadge: boolean;
|
public asBadge: boolean;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 256, nullable: true,
|
||||||
|
})
|
||||||
|
public badgeBehavior: string | null;
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
|
@ -384,6 +384,10 @@ export const packedRoleSchema = {
|
|||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
example: false,
|
example: false,
|
||||||
},
|
},
|
||||||
|
badgeBehavior: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
canEditMembersByModerator: {
|
canEditMembersByModerator: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -176,6 +176,10 @@ export const packedUserLiteSchema = {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
|
behavior: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: false, optional: true,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -36,6 +36,7 @@ export const paramDef = {
|
|||||||
isAdministrator: { type: 'boolean' },
|
isAdministrator: { type: 'boolean' },
|
||||||
isExplorable: { type: 'boolean', default: false }, // optional for backward compatibility
|
isExplorable: { type: 'boolean', default: false }, // optional for backward compatibility
|
||||||
asBadge: { type: 'boolean' },
|
asBadge: { type: 'boolean' },
|
||||||
|
badgeBehavior: { type: 'string', nullable: true },
|
||||||
canEditMembersByModerator: { type: 'boolean' },
|
canEditMembersByModerator: { type: 'boolean' },
|
||||||
displayOrder: { type: 'number' },
|
displayOrder: { type: 'number' },
|
||||||
policies: {
|
policies: {
|
||||||
|
@ -42,6 +42,7 @@ export const paramDef = {
|
|||||||
isAdministrator: { type: 'boolean' },
|
isAdministrator: { type: 'boolean' },
|
||||||
isExplorable: { type: 'boolean' },
|
isExplorable: { type: 'boolean' },
|
||||||
asBadge: { type: 'boolean' },
|
asBadge: { type: 'boolean' },
|
||||||
|
badgeBehavior: { type: 'string', nullable: true },
|
||||||
canEditMembersByModerator: { type: 'boolean' },
|
canEditMembersByModerator: { type: 'boolean' },
|
||||||
displayOrder: { type: 'number' },
|
displayOrder: { type: 'number' },
|
||||||
policies: {
|
policies: {
|
||||||
@ -92,6 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
isAdministrator: ps.isAdministrator,
|
isAdministrator: ps.isAdministrator,
|
||||||
isExplorable: ps.isExplorable,
|
isExplorable: ps.isExplorable,
|
||||||
asBadge: ps.asBadge,
|
asBadge: ps.asBadge,
|
||||||
|
badgeBehavior: ps.badgeBehavior,
|
||||||
canEditMembersByModerator: ps.canEditMembersByModerator,
|
canEditMembersByModerator: ps.canEditMembersByModerator,
|
||||||
displayOrder: ps.displayOrder,
|
displayOrder: ps.displayOrder,
|
||||||
policies: ps.policies,
|
policies: ps.policies,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
@ -88,6 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
private roleService: RoleService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
) {
|
) {
|
||||||
@ -128,13 +130,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
error?: unknown,
|
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 });
|
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);
|
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 });
|
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 {
|
return {
|
||||||
screenName: json.screen_name,
|
screenName: json.screen_name,
|
||||||
isCreator: json.is_creator,
|
isCreator: json.is_creator,
|
||||||
|
@ -467,6 +467,7 @@ describe('Note', () => {
|
|||||||
isPublic: false,
|
isPublic: false,
|
||||||
isExplorable: false,
|
isExplorable: false,
|
||||||
asBadge: false,
|
asBadge: false,
|
||||||
|
badgeBehavior: null,
|
||||||
canEditMembersByModerator: false,
|
canEditMembersByModerator: false,
|
||||||
policies: {
|
policies: {
|
||||||
alwaysMarkNsfw: {
|
alwaysMarkNsfw: {
|
||||||
@ -780,6 +781,7 @@ describe('Note', () => {
|
|||||||
isPublic: false,
|
isPublic: false,
|
||||||
isExplorable: false,
|
isExplorable: false,
|
||||||
asBadge: false,
|
asBadge: false,
|
||||||
|
badgeBehavior: null,
|
||||||
canEditMembersByModerator: false,
|
canEditMembersByModerator: false,
|
||||||
policies: {
|
policies: {
|
||||||
mentionLimit: {
|
mentionLimit: {
|
||||||
@ -834,6 +836,7 @@ describe('Note', () => {
|
|||||||
isPublic: false,
|
isPublic: false,
|
||||||
isExplorable: false,
|
isExplorable: false,
|
||||||
asBadge: false,
|
asBadge: false,
|
||||||
|
badgeBehavior: null,
|
||||||
canEditMembersByModerator: false,
|
canEditMembersByModerator: false,
|
||||||
policies: {
|
policies: {
|
||||||
mentionLimit: {
|
mentionLimit: {
|
||||||
@ -890,6 +893,7 @@ describe('Note', () => {
|
|||||||
isPublic: false,
|
isPublic: false,
|
||||||
isExplorable: false,
|
isExplorable: false,
|
||||||
asBadge: false,
|
asBadge: false,
|
||||||
|
badgeBehavior: null,
|
||||||
canEditMembersByModerator: false,
|
canEditMembersByModerator: false,
|
||||||
policies: {
|
policies: {
|
||||||
mentionLimit: {
|
mentionLimit: {
|
||||||
|
@ -651,11 +651,20 @@ describe('ユーザー', () => {
|
|||||||
});
|
});
|
||||||
test('を取得することができ、バッヂロールがセットされていること', async () => {
|
test('を取得することができ、バッヂロールがセットされていること', async () => {
|
||||||
const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRoleBadge.id }, user: alice });
|
const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRoleBadge.id }, user: alice });
|
||||||
|
if (roleBadge.badgeBehavior) {
|
||||||
|
assert.deepStrictEqual(response.badgeRoles, [{
|
||||||
|
name: roleBadge.name,
|
||||||
|
iconUrl: roleBadge.iconUrl,
|
||||||
|
displayOrder: roleBadge.displayOrder,
|
||||||
|
behavior: roleBadge.badgeBehavior,
|
||||||
|
}]);
|
||||||
|
} else {
|
||||||
assert.deepStrictEqual(response.badgeRoles, [{
|
assert.deepStrictEqual(response.badgeRoles, [{
|
||||||
name: roleBadge.name,
|
name: roleBadge.name,
|
||||||
iconUrl: roleBadge.iconUrl,
|
iconUrl: roleBadge.iconUrl,
|
||||||
displayOrder: roleBadge.displayOrder,
|
displayOrder: roleBadge.displayOrder,
|
||||||
}]);
|
}]);
|
||||||
|
}
|
||||||
assert.deepStrictEqual(response.roles, []); // バッヂだからといってrolesが取れるとは限らない
|
assert.deepStrictEqual(response.roles, []); // バッヂだからといってrolesが取れるとは限らない
|
||||||
});
|
});
|
||||||
test('をID指定のリスト形式で取得することができる(空)', async () => {
|
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> => {
|
export const role = async (user: UserToken, role: Partial<misskey.entities.Role> = {}, policies: any = {}): Promise<misskey.entities.Role> => {
|
||||||
const res = await api('admin/roles/create', {
|
const res = await api('admin/roles/create', {
|
||||||
asBadge: false,
|
asBadge: false,
|
||||||
|
badgeBehavior: null,
|
||||||
canEditMembersByModerator: false,
|
canEditMembersByModerator: false,
|
||||||
color: null,
|
color: null,
|
||||||
condFormula: {
|
condFormula: {
|
||||||
|
@ -175,8 +175,8 @@ const align = () => {
|
|||||||
let left;
|
let left;
|
||||||
let top;
|
let top;
|
||||||
|
|
||||||
const x = srcRect.left + (fixed.value ? 0 : window.pageXOffset);
|
const x = srcRect.left + (fixed.value ? 0 : window.scrollX);
|
||||||
const y = srcRect.top + (fixed.value ? 0 : window.pageYOffset);
|
const y = srcRect.top + (fixed.value ? 0 : window.scrollY);
|
||||||
|
|
||||||
if (props.anchor.x === 'center') {
|
if (props.anchor.x === 'center') {
|
||||||
left = x + (props.src.offsetWidth / 2) - (width / 2);
|
left = x + (props.src.offsetWidth / 2) - (width / 2);
|
||||||
@ -220,24 +220,24 @@ const align = () => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 画面から横にはみ出る場合
|
// 画面から横にはみ出る場合
|
||||||
if (left + width - window.pageXOffset > (window.innerWidth - SCROLLBAR_THICKNESS)) {
|
if (left + width - window.scrollX > (window.innerWidth - SCROLLBAR_THICKNESS)) {
|
||||||
left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset - 1;
|
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);
|
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 (props.noOverlap && props.anchor.x === 'center') {
|
||||||
if (underSpace >= (upperSpace / 3)) {
|
if (underSpace >= (upperSpace / 3)) {
|
||||||
maxHeight.value = underSpace;
|
maxHeight.value = underSpace;
|
||||||
} else {
|
} else {
|
||||||
maxHeight.value = upperSpace;
|
maxHeight.value = upperSpace;
|
||||||
top = window.pageYOffset + ((upperSpace + MARGIN) - height);
|
top = window.scrollY + ((upperSpace + MARGIN) - height);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height + window.pageYOffset - 1;
|
top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height + window.scrollY - 1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
maxHeight.value = underSpace;
|
maxHeight.value = underSpace;
|
||||||
@ -255,15 +255,15 @@ const align = () => {
|
|||||||
let transformOriginX = 'center';
|
let transformOriginX = 'center';
|
||||||
let transformOriginY = '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';
|
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';
|
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';
|
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';
|
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 v-if="note.user.isBot" :class="$style.isBot">bot</div>
|
||||||
<div :class="$style.username"><MkAcct :user="note.user"/></div>
|
<div :class="$style.username"><MkAcct :user="note.user"/></div>
|
||||||
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
|
<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>
|
||||||
<div :class="$style.info">
|
<div :class="$style.info">
|
||||||
<div v-if="mock">
|
<div v-if="mock">
|
||||||
@ -40,6 +40,7 @@ import * as Misskey from 'misskey-js';
|
|||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { notePage } from '@/filters/note.js';
|
import { notePage } from '@/filters/note.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
|
import MkRoleBadgeIcon from '@/components/MkRoleBadgeIcon.vue';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
note: Misskey.entities.Note;
|
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(() => {
|
onMounted(() => {
|
||||||
try {
|
try {
|
||||||
const rect = props.source.getBoundingClientRect();
|
const rect = props.source.getBoundingClientRect();
|
||||||
const x = Math.max((rect.left + (props.source.offsetWidth / 2)) - (300 / 2), 6) + window.pageXOffset;
|
const x = Math.max((rect.left + (props.source.offsetWidth / 2)) - (300 / 2), 6) + window.scrollX;
|
||||||
const y = rect.top + props.source.offsetHeight + window.pageYOffset;
|
const y = rect.top + props.source.offsetHeight + window.scrollY;
|
||||||
|
|
||||||
top.value = y;
|
top.value = y;
|
||||||
left.value = x;
|
left.value = x;
|
||||||
|
@ -63,6 +63,7 @@ if (props.id) {
|
|||||||
isPublic: false,
|
isPublic: false,
|
||||||
isExplorable: false,
|
isExplorable: false,
|
||||||
asBadge: false,
|
asBadge: false,
|
||||||
|
badgeBehavior: null,
|
||||||
canEditMembersByModerator: false,
|
canEditMembersByModerator: false,
|
||||||
displayOrder: 0,
|
displayOrder: 0,
|
||||||
policies: {},
|
policies: {},
|
||||||
|
@ -67,6 +67,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #caption>{{ i18n.ts._role.descriptionOfAsBadge }}</template>
|
<template #caption>{{ i18n.ts._role.descriptionOfAsBadge }}</template>
|
||||||
</MkSwitch>
|
</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">
|
<MkSwitch v-model="role.isExplorable" :readonly="readonly">
|
||||||
<template #label>{{ i18n.ts._role.isExplorable }}</template>
|
<template #label>{{ i18n.ts._role.isExplorable }}</template>
|
||||||
<template #caption>{{ i18n.ts._role.descriptionOfIsExplorable }}</template>
|
<template #caption>{{ i18n.ts._role.descriptionOfIsExplorable }}</template>
|
||||||
@ -848,6 +853,7 @@ const save = throttle(100, () => {
|
|||||||
isPublic: role.value.isPublic,
|
isPublic: role.value.isPublic,
|
||||||
isExplorable: role.value.isExplorable,
|
isExplorable: role.value.isExplorable,
|
||||||
asBadge: role.value.asBadge,
|
asBadge: role.value.asBadge,
|
||||||
|
badgeBehavior: role.value.badgeBehavior,
|
||||||
canEditMembersByModerator: role.value.canEditMembersByModerator,
|
canEditMembersByModerator: role.value.canEditMembersByModerator,
|
||||||
policies: role.value.policies,
|
policies: role.value.policies,
|
||||||
};
|
};
|
||||||
|
@ -319,7 +319,6 @@ async function fetchSkebStatus() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('fetching skeb status');
|
|
||||||
userSkebStatus.value = await misskeyApiGet('users/get-skeb-status', { userId: props.user.id });
|
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;
|
let top: number;
|
||||||
|
|
||||||
if (props.anchorElement) {
|
if (props.anchorElement) {
|
||||||
left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2);
|
left = rect.left + window.scrollX + (props.anchorElement.offsetWidth / 2);
|
||||||
top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin;
|
top = (rect.top + window.scrollY - contentHeight) - props.innerMargin;
|
||||||
} else {
|
} else {
|
||||||
left = props.x;
|
left = props.x;
|
||||||
top = (props.y - contentHeight) - props.innerMargin;
|
top = (props.y - contentHeight) - props.innerMargin;
|
||||||
@ -35,8 +35,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
|||||||
|
|
||||||
left -= (el.offsetWidth / 2);
|
left -= (el.offsetWidth / 2);
|
||||||
|
|
||||||
if (left + contentWidth - window.pageXOffset > window.innerWidth) {
|
if (left + contentWidth - window.scrollX > window.innerWidth) {
|
||||||
left = window.innerWidth - contentWidth + window.pageXOffset - 1;
|
left = window.innerWidth - contentWidth + window.scrollX - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [left, top];
|
return [left, top];
|
||||||
@ -47,8 +47,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
|||||||
let top: number;
|
let top: number;
|
||||||
|
|
||||||
if (props.anchorElement) {
|
if (props.anchorElement) {
|
||||||
left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2);
|
left = rect.left + window.scrollX + (props.anchorElement.offsetWidth / 2);
|
||||||
top = (rect.top + window.pageYOffset + props.anchorElement.offsetHeight) + props.innerMargin;
|
top = (rect.top + window.scrollY + props.anchorElement.offsetHeight) + props.innerMargin;
|
||||||
} else {
|
} else {
|
||||||
left = props.x;
|
left = props.x;
|
||||||
top = (props.y) + props.innerMargin;
|
top = (props.y) + props.innerMargin;
|
||||||
@ -56,8 +56,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
|||||||
|
|
||||||
left -= (el.offsetWidth / 2);
|
left -= (el.offsetWidth / 2);
|
||||||
|
|
||||||
if (left + contentWidth - window.pageXOffset > window.innerWidth) {
|
if (left + contentWidth - window.scrollX > window.innerWidth) {
|
||||||
left = window.innerWidth - contentWidth + window.pageXOffset - 1;
|
left = window.innerWidth - contentWidth + window.scrollX - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [left, top];
|
return [left, top];
|
||||||
@ -68,8 +68,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
|||||||
let top: number;
|
let top: number;
|
||||||
|
|
||||||
if (props.anchorElement) {
|
if (props.anchorElement) {
|
||||||
left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin;
|
left = (rect.left + window.scrollX - contentWidth) - props.innerMargin;
|
||||||
top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2);
|
top = rect.top + window.scrollY + (props.anchorElement.offsetHeight / 2);
|
||||||
} else {
|
} else {
|
||||||
left = (props.x - contentWidth) - props.innerMargin;
|
left = (props.x - contentWidth) - props.innerMargin;
|
||||||
top = props.y;
|
top = props.y;
|
||||||
@ -77,8 +77,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
|||||||
|
|
||||||
top -= (el.offsetHeight / 2);
|
top -= (el.offsetHeight / 2);
|
||||||
|
|
||||||
if (top + contentHeight - window.pageYOffset > window.innerHeight) {
|
if (top + contentHeight - window.scrollY > window.innerHeight) {
|
||||||
top = window.innerHeight - contentHeight + window.pageYOffset - 1;
|
top = window.innerHeight - contentHeight + window.scrollY - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [left, top];
|
return [left, top];
|
||||||
@ -89,15 +89,15 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
|||||||
let top: number;
|
let top: number;
|
||||||
|
|
||||||
if (props.anchorElement) {
|
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') {
|
if (props.align === 'top') {
|
||||||
top = rect.top + window.pageYOffset;
|
top = rect.top + window.scrollY;
|
||||||
if (props.alignOffset != null) top += props.alignOffset;
|
if (props.alignOffset != null) top += props.alignOffset;
|
||||||
} else if (props.align === 'bottom') {
|
} else if (props.align === 'bottom') {
|
||||||
// TODO
|
// TODO
|
||||||
} else { // center
|
} else { // center
|
||||||
top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2);
|
top = rect.top + window.scrollY + (props.anchorElement.offsetHeight / 2);
|
||||||
top -= (el.offsetHeight / 2);
|
top -= (el.offsetHeight / 2);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -106,8 +106,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
|||||||
top -= (el.offsetHeight / 2);
|
top -= (el.offsetHeight / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (top + contentHeight - window.pageYOffset > window.innerHeight) {
|
if (top + contentHeight - window.scrollY > window.innerHeight) {
|
||||||
top = window.innerHeight - contentHeight + window.pageYOffset - 1;
|
top = window.innerHeight - contentHeight + window.scrollY - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [left, top];
|
return [left, top];
|
||||||
@ -123,7 +123,7 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
|||||||
const [left, top] = calcPosWhenTop();
|
const [left, top] = calcPosWhenTop();
|
||||||
|
|
||||||
// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
|
// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
|
||||||
if (top - window.pageYOffset < 0) {
|
if (top - window.scrollY < 0) {
|
||||||
const [left, top] = calcPosWhenBottom();
|
const [left, top] = calcPosWhenBottom();
|
||||||
return { left, top, transformOrigin: 'center top' };
|
return { left, top, transformOrigin: 'center top' };
|
||||||
}
|
}
|
||||||
@ -141,7 +141,7 @@ export function calcPopupPosition(el: HTMLElement, props: {
|
|||||||
const [left, top] = calcPosWhenLeft();
|
const [left, top] = calcPosWhenLeft();
|
||||||
|
|
||||||
// ツールチップを左に向かって表示するスペースがなければ右に向かって出す
|
// ツールチップを左に向かって表示するスペースがなければ右に向かって出す
|
||||||
if (left - window.pageXOffset < 0) {
|
if (left - window.scrollX < 0) {
|
||||||
const [left, top] = calcPosWhenRight();
|
const [left, top] = calcPosWhenRight();
|
||||||
return { left, top, transformOrigin: 'left center' };
|
return { left, top, transformOrigin: 'left center' };
|
||||||
}
|
}
|
||||||
|
@ -53,11 +53,11 @@ export function useChartTooltip(opts: { position: 'top' | 'middle' } = { positio
|
|||||||
const rect = context.chart.canvas.getBoundingClientRect();
|
const rect = context.chart.canvas.getBoundingClientRect();
|
||||||
|
|
||||||
tooltipShowing.value = true;
|
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') {
|
if (opts.position === 'top') {
|
||||||
tooltipY.value = rect.top + window.pageYOffset;
|
tooltipY.value = rect.top + window.scrollY;
|
||||||
} else if (opts.position === 'middle') {
|
} 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;
|
name: string;
|
||||||
iconUrl: string | null;
|
iconUrl: string | null;
|
||||||
displayOrder: number;
|
displayOrder: number;
|
||||||
|
behavior?: string;
|
||||||
})[];
|
})[];
|
||||||
};
|
};
|
||||||
UserDetailedNotMeOnly: {
|
UserDetailedNotMeOnly: {
|
||||||
@ -4843,6 +4844,7 @@ export type components = {
|
|||||||
isExplorable: boolean;
|
isExplorable: boolean;
|
||||||
/** @example false */
|
/** @example false */
|
||||||
asBadge: boolean;
|
asBadge: boolean;
|
||||||
|
badgeBehavior: string | null;
|
||||||
/** @example false */
|
/** @example false */
|
||||||
canEditMembersByModerator: boolean;
|
canEditMembersByModerator: boolean;
|
||||||
policies: {
|
policies: {
|
||||||
@ -9827,6 +9829,7 @@ export type operations = {
|
|||||||
/** @default false */
|
/** @default false */
|
||||||
isExplorable?: boolean;
|
isExplorable?: boolean;
|
||||||
asBadge: boolean;
|
asBadge: boolean;
|
||||||
|
badgeBehavior?: string | null;
|
||||||
canEditMembersByModerator: boolean;
|
canEditMembersByModerator: boolean;
|
||||||
displayOrder: number;
|
displayOrder: number;
|
||||||
policies: Record<string, never>;
|
policies: Record<string, never>;
|
||||||
@ -10048,6 +10051,7 @@ export type operations = {
|
|||||||
isAdministrator: boolean;
|
isAdministrator: boolean;
|
||||||
isExplorable?: boolean;
|
isExplorable?: boolean;
|
||||||
asBadge: boolean;
|
asBadge: boolean;
|
||||||
|
badgeBehavior?: string | null;
|
||||||
canEditMembersByModerator: boolean;
|
canEditMembersByModerator: boolean;
|
||||||
displayOrder: number;
|
displayOrder: number;
|
||||||
policies: Record<string, never>;
|
policies: Record<string, never>;
|
||||||
|
Loading…
Reference in New Issue
Block a user