This commit is contained in:
samunohito 2024-01-27 12:02:50 +09:00
parent 8d1a5734cd
commit aacee3c970
22 changed files with 878 additions and 280 deletions

View File

@ -16,7 +16,6 @@ import type { EmojisRepository, MiRole, MiUser } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js';
import { query } from '@/misc/prelude/url.js';
import type { Serialized } from '@/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@ -30,10 +29,8 @@ export class CustomEmojiService implements OnApplicationShutdown {
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private utilityService: UtilityService,
private idService: IdService,
private emojiEntityService: EmojiEntityService,
@ -102,6 +99,64 @@ export class CustomEmojiService implements OnApplicationShutdown {
return emoji;
}
@bindThis
public async addBulk(
params: {
driveFile: MiDriveFile;
name: string;
category: string | null;
aliases: string[];
host: string | null;
license: string | null;
isSensitive: boolean;
localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
}[],
moderator?: MiUser,
): Promise<MiEmoji[]> {
const emojis = await this.emojisRepository
.insert(
params.map(it => ({
id: this.idService.gen(),
updatedAt: new Date(),
name: it.name,
category: it.category,
host: it.host,
aliases: it.aliases,
originalUrl: it.driveFile.url,
publicUrl: it.driveFile.webpublicUrl ?? it.driveFile.url,
type: it.driveFile.webpublicType ?? it.driveFile.type,
license: it.license,
isSensitive: it.isSensitive,
localOnly: it.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: it.roleIdsThatCanBeUsedThisEmojiAsReaction,
})),
)
.then(x => this.emojisRepository.createQueryBuilder('emoji').whereInIds(x.identifiers).getMany());
const localEmojis = emojis.filter(it => it.host == null);
if (localEmojis.length > 0) {
this.localEmojisCache.refresh();
this.emojiEntityService.packDetailedMany(localEmojis).then(it => {
for (const emoji of it) {
this.globalEventService.publishBroadcastStream('emojiAdded', { emoji });
}
});
if (moderator) {
for (const emoji of localEmojis) {
this.moderationLogService.log(moderator, 'addCustomEmoji', {
emojiId: emoji.id,
emoji: emoji,
});
}
}
}
return emojis;
}
@bindThis
public async update(id: MiEmoji['id'], data: {
driveFile?: MiDriveFile;
@ -159,6 +214,103 @@ export class CustomEmojiService implements OnApplicationShutdown {
}
}
@bindThis
public async updateBulk(
params: {
id: MiEmoji['id'];
driveFile?: MiDriveFile;
name?: string;
category?: string | null;
aliases?: string[];
license?: string | null;
isSensitive?: boolean;
localOnly?: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
}[],
moderator?: MiUser,
): Promise<void> {
const ids = params.map(it => it.id);
// IDに対応するものと、新しく設定しようとしている名前と同じ名前を持つレコードをそれぞれ取得する
const [storedEmojis, sameNameEmojis] = await Promise.all([
this.emojisRepository.createQueryBuilder('emoji')
.whereInIds(ids)
.getMany()
.then(emojis => new Map(emojis.map(it => [it.id, it]))),
this.emojisRepository.createQueryBuilder('emoji')
.where('emoji.name IN (:...names) AND emoji.host IS NULL', { names: params.map(it => it.name) })
.getMany(),
]);
// 新しく設定しようとしている名前と同じ名前を持つ別レコードがある場合、重複とみなしてエラーとする
const alreadyExists = Array.of<string>();
for (const sameNameEmoji of sameNameEmojis) {
const emoji = storedEmojis.get(sameNameEmoji.id);
if (emoji != null && emoji.id !== sameNameEmoji.id) {
alreadyExists.push(sameNameEmoji.name);
}
}
if (alreadyExists.length > 0) {
throw new Error(`name already exists: ${alreadyExists.join(', ')}`);
}
for (const emoji of params) {
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
name: emoji.name,
category: emoji.category,
aliases: emoji.aliases,
license: emoji.license,
isSensitive: emoji.isSensitive,
localOnly: emoji.localOnly,
originalUrl: emoji.driveFile != null ? emoji.driveFile.url : undefined,
publicUrl: emoji.driveFile != null ? (emoji.driveFile.webpublicUrl ?? emoji.driveFile.url) : undefined,
type: emoji.driveFile != null ? (emoji.driveFile.webpublicType ?? emoji.driveFile.type) : undefined,
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined,
});
}
this.localEmojisCache.refresh();
// 名前が変わっていないものはそのまま更新としてイベント発信
const updateEmojis = params.filter(it => storedEmojis.get(it.id)?.name === it.name);
if (updateEmojis.length > 0) {
const packedList = await this.emojiEntityService.packDetailedMany(updateEmojis);
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: packedList,
});
}
// 名前が変わったものは削除・追加としてイベント発信
const nameChangeEmojis = params.filter(it => storedEmojis.get(it.id)?.name !== it.name);
if (nameChangeEmojis.length > 0) {
const packedList = await this.emojiEntityService.packDetailedMany(nameChangeEmojis);
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: packedList,
});
for (const packed of packedList) {
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: packed,
});
}
}
if (moderator) {
const updatedEmojis = await this.emojisRepository.createQueryBuilder('emoji')
.whereInIds(storedEmojis.keys())
.getMany()
.then(it => new Map(it.map(it => [it.id, it])));
for (const emoji of storedEmojis.values()) {
this.moderationLogService.log(moderator, 'updateCustomEmoji', {
emojiId: emoji.id,
before: emoji,
after: updatedEmojis.get(emoji.id),
});
}
}
}
@bindThis
public async addAliasesBulk(ids: MiEmoji['id'][], aliases: string[]) {
const emojis = await this.emojisRepository.findBy({

View File

@ -67,7 +67,7 @@ export class EmojiEntityService {
@bindThis
public packDetailedMany(
emojis: any[],
) {
): Promise<Packed<'EmojiDetailed'>[]> {
return Promise.all(emojis.map(x => this.packDetailed(x)));
}
}

View File

@ -44,15 +44,21 @@ export const paramDef = {
nullable: true,
description: 'Use `null` to reset the category.',
},
aliases: { type: 'array', items: {
aliases: {
type: 'array',
items: {
type: 'string',
} },
},
},
license: { type: 'string', nullable: true },
isSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
roleIdsThatCanBeUsedThisEmojiAsReaction: {
type: 'array',
items: {
type: 'string',
} },
},
},
},
required: ['name', 'fileId'],
} as const;
@ -64,9 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private customEmojiService: CustomEmojiService,
private emojiEntityService: EmojiEntityService,
) {
super(meta, paramDef, async (ps, me) => {

View File

@ -21,42 +21,9 @@ export const meta = {
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
aliases: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
name: {
type: 'string',
optional: false, nullable: false,
},
category: {
type: 'string',
optional: false, nullable: true,
},
host: {
type: 'string',
optional: false, nullable: true,
description: 'The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files.',
},
url: {
type: 'string',
optional: false, nullable: false,
},
},
ref: 'EmojiDetailed',
},
},
} as const;
@ -88,15 +55,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let emojis: MiEmoji[];
if (ps.query) {
//q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
//const emojis = await q.limit(ps.limit).getMany();
emojis = await q.getMany();
const queryarry = ps.query.match(/\:([a-z0-9_]*)\:/g);
if (queryarry) {
const queries = ps.query.match(/:([a-z0-9_]*):/g);
if (queries) {
emojis = emojis.filter(emoji =>
queryarry.includes(`:${emoji.name}:`),
queries.includes(`:${emoji.name}:`),
);
} else {
emojis = emojis.filter(emoji =>

View File

@ -25,19 +25,10 @@ export const meta = {
type: 'array',
optional: false, nullable: false,
items: {
anyOf: [
{
type: 'object',
optional: false, nullable: false,
ref: 'EmojiSimple',
},
{
type: 'object',
optional: false, nullable: false,
ref: 'EmojiDetailed',
},
],
},
},
},
},
@ -46,10 +37,6 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
detail: {
type: 'boolean',
nullable: true,
},
},
required: [],
} as const;
@ -74,9 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
return {
emojis: ps.detail
? await this.emojiEntityService.packDetailedMany(emojis)
: await this.emojiEntityService.packSimpleMany(emojis),
emojis: await this.emojiEntityService.packSimpleMany(emojis),
};
});
}

View File

@ -21,7 +21,7 @@
{{ cell.value }}
</div>
<div v-else-if="cellType === 'boolean'">
<span v-if="cell.value" class="ti ti-check"/>
<span v-if="cell.value === true" class="ti ti-check"/>
<span v-else class="ti ti-x"/>
</div>
<div v-else-if="cellType === 'image'">
@ -49,18 +49,17 @@
<script setup lang="ts">
import { computed, nextTick, ref, toRefs, watch } from 'vue';
import {
CellAddress,
CellValue,
equalCellAddress,
getCellAddress,
GridCell,
GridEventEmitter, Size,
GridEventEmitter,
Size,
} from '@/components/grid/types.js';
const emit = defineEmits<{
(ev: 'operation:beginEdit', sender: GridCell): void;
(ev: 'operation:endEdit', sender: GridCell): void;
(ev: 'operation:selectionMove', sender: GridCell, next: CellAddress): void;
(ev: 'change:value', sender: GridCell, newValue: CellValue): void;
(ev: 'change:contentSize', sender: GridCell, newSize: Size): void;
}>();

View File

@ -2,9 +2,7 @@
<tr :class="$style.row">
<MkNumberCell
:content="(row.index + 1).toString()"
:selectable="true"
:row="row"
@operation:selectionRow="(sender) => emit('operation:selectionRow', sender)"
/>
<MkDataCell
v-for="cell in cells"
@ -13,7 +11,6 @@
:bus="bus"
@operation:beginEdit="(sender) => emit('operation:beginEdit', sender)"
@operation:endEdit="(sender) => emit('operation:endEdit', sender)"
@operation:selectionMove="(sender, next) => emit('operation:selectionMove', sender, next)"
@change:value="(sender, newValue) => emit('change:value', sender, newValue)"
@change:contentSize="(sender, newSize) => emit('change:contentSize', sender, newSize)"
/>
@ -21,16 +18,14 @@
</template>
<script setup lang="ts">
import { computed, toRefs } from 'vue';
import { CellAddress, CellValue, GridCell, GridEventEmitter, GridRow, Size } from '@/components/grid/types.js';
import { toRefs } from 'vue';
import { CellValue, GridCell, GridEventEmitter, GridRow, Size } from '@/components/grid/types.js';
import MkDataCell from '@/components/grid/MkDataCell.vue';
import MkNumberCell from '@/components/grid/MkNumberCell.vue';
const emit = defineEmits<{
(ev: 'operation:beginEdit', sender: GridCell): void;
(ev: 'operation:endEdit', sender: GridCell): void;
(ev: 'operation:selectionRow', sender: GridRow): void;
(ev: 'operation:selectionMove', sender: GridCell, next: CellAddress): void;
(ev: 'change:value', sender: GridCell, newValue: CellValue): void;
(ev: 'change:contentSize', sender: GridCell, newSize: Size): void;
}>();

View File

@ -1,5 +1,7 @@
<template>
<table
ref="rootEl"
tabindex="-1"
:class="$style.grid"
@mousedown="onMouseDown"
@keydown="onKeyDown"
@ -11,7 +13,6 @@
@operation:beginWidthChange="onHeaderCellWidthBeginChange"
@operation:endWidthChange="onHeaderCellWidthEndChange"
@operation:widthLargest="onHeaderCellWidthLargest"
@operation:selectionColumn="onSelectionColumn"
@change:width="onHeaderCellChangeWidth"
@change:contentSize="onHeaderCellChangeContentSize"
/>
@ -25,8 +26,6 @@
:bus="bus"
@operation:beginEdit="onCellEditBegin"
@operation:endEdit="onCellEditEnd"
@operation:selectionMove="onSelectionMove"
@operation:selectionRow="onSelectionRow"
@change:value="onChangeCellValue"
@change:contentSize="onChangeCellContentSize"
/>
@ -40,7 +39,7 @@ import {
calcCellWidth,
CELL_ADDRESS_NONE,
CellAddress,
CellValue,
CellValue, CellValueChangedEvent,
ColumnSetting,
DataSource,
equalCellAddress,
@ -54,16 +53,22 @@ import {
} from '@/components/grid/types.js';
import MkDataRow from '@/components/grid/MkDataRow.vue';
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{
columnSettings: ColumnSetting[],
data: DataSource[]
}>();
const emit = defineEmits<{
(ev: 'change:cellValue', event: CellValueChangedEvent): void;
}>();
const bus = new GridEventEmitter();
const { columnSettings, data } = toRefs(props);
const rootEl = ref<InstanceType<typeof HTMLTableElement>>();
const columns = ref<GridColumn[]>([]);
const rows = ref<GridRow[]>([]);
const cells = ref<GridCell[][]>([]);
@ -78,9 +83,36 @@ const selectedCell = computed(() => {
return selected.length > 0 ? selected[0] : undefined;
});
const rangedCells = computed(() => cells.value.flat().filter(it => it.ranged));
const rangedBounds = computed(() => {
const _cells = rangedCells.value;
const leftTop = {
col: Math.min(..._cells.map(it => it.address.col)),
row: Math.min(..._cells.map(it => it.address.row)),
};
const rightBottom = {
col: Math.max(..._cells.map(it => it.address.col)),
row: Math.max(..._cells.map(it => it.address.row)),
};
return {
leftTop,
rightBottom,
};
});
const availableBounds = computed(() => {
const leftTop = {
col: 0,
row: 0,
};
const rightBottom = {
col: Math.max(...columns.value.map(it => it.index)),
row: Math.max(...rows.value.map(it => it.index)),
};
return { leftTop, rightBottom };
});
watch(columnSettings, refreshColumnsSetting);
watch(data, refreshData);
if (_DEV_) {
watch(state, (value) => {
console.log(`state: ${value}`);
@ -88,29 +120,50 @@ if (_DEV_) {
}
function onKeyDown(ev: KeyboardEvent) {
switch (state.value) {
case 'normal': {
const selectedCellAddress = selectedCell.value?.address;
if (!selectedCellAddress) {
return;
if (_DEV_) {
console.log('[Grid]', `ctrl: ${ev.ctrlKey}, shift: ${ev.shiftKey}, code: ${ev.code}`);
}
let next: CellAddress;
switch (state.value) {
case 'normal': {
// normal
ev.preventDefault();
if (ev.ctrlKey) {
if (ev.shiftKey) {
// ctrl + shift
const selectedCellAddress = requireSelectionCell();
const max = availableBounds.value;
const bounds = rangedBounds.value;
let newBounds: { leftTop: CellAddress, rightBottom: CellAddress };
switch (ev.code) {
case 'ArrowRight': {
next = { col: selectedCellAddress.col + 1, row: selectedCellAddress.row };
newBounds = {
leftTop: { col: selectedCellAddress.col, row: bounds.leftTop.row },
rightBottom: { col: max.rightBottom.col, row: bounds.rightBottom.row },
};
break;
}
case 'ArrowLeft': {
next = { col: selectedCellAddress.col - 1, row: selectedCellAddress.row };
newBounds = {
leftTop: { col: max.leftTop.col, row: bounds.leftTop.row },
rightBottom: { col: selectedCellAddress.col, row: bounds.rightBottom.row },
};
break;
}
case 'ArrowUp': {
next = { col: selectedCellAddress.col, row: selectedCellAddress.row - 1 };
newBounds = {
leftTop: { col: bounds.leftTop.col, row: max.leftTop.row },
rightBottom: { col: bounds.rightBottom.col, row: selectedCellAddress.row },
};
break;
}
case 'ArrowDown': {
next = { col: selectedCellAddress.col, row: selectedCellAddress.row + 1 };
newBounds = {
leftTop: { col: bounds.leftTop.col, row: selectedCellAddress.row },
rightBottom: { col: bounds.rightBottom.col, row: max.rightBottom.row },
};
break;
}
default: {
@ -118,7 +171,138 @@ function onKeyDown(ev: KeyboardEvent) {
}
}
selectionCell(next);
unSelectionOutOfRange(newBounds.leftTop, newBounds.rightBottom);
expandRange(newBounds.leftTop, newBounds.rightBottom);
} else {
switch (ev.code) {
case 'KeyC': {
rangeCopyToClipboard();
break;
}
case 'KeyV': {
pasteFromClipboard();
break;
}
}
}
} else {
if (ev.shiftKey) {
// shift
const selectedCellAddress = requireSelectionCell();
const bounds = rangedBounds.value;
let newBounds: { leftTop: CellAddress, rightBottom: CellAddress };
switch (ev.code) {
case 'ArrowRight': {
newBounds = {
leftTop: {
col: bounds.leftTop.col < selectedCellAddress.col
? bounds.leftTop.col + 1
: selectedCellAddress.col,
row: bounds.leftTop.row,
},
rightBottom: {
col: (bounds.rightBottom.col > selectedCellAddress.col || bounds.leftTop.col === selectedCellAddress.col)
? bounds.rightBottom.col + 1
: selectedCellAddress.col,
row: bounds.rightBottom.row,
},
};
break;
}
case 'ArrowLeft': {
newBounds = {
leftTop: {
col: (bounds.leftTop.col < selectedCellAddress.col || bounds.rightBottom.col === selectedCellAddress.col)
? bounds.leftTop.col - 1
: selectedCellAddress.col,
row: bounds.leftTop.row,
},
rightBottom: {
col: bounds.rightBottom.col > selectedCellAddress.col
? bounds.rightBottom.col - 1
: selectedCellAddress.col,
row: bounds.rightBottom.row,
},
};
break;
}
case 'ArrowUp': {
newBounds = {
leftTop: {
col: bounds.leftTop.col,
row: (bounds.leftTop.row < selectedCellAddress.row || bounds.rightBottom.row === selectedCellAddress.row)
? bounds.leftTop.row - 1
: selectedCellAddress.row,
},
rightBottom: {
col: bounds.rightBottom.col,
row: bounds.rightBottom.row > selectedCellAddress.row
? bounds.rightBottom.row - 1
: selectedCellAddress.row,
},
};
break;
}
case 'ArrowDown': {
newBounds = {
leftTop: {
col: bounds.leftTop.col,
row: bounds.leftTop.row < selectedCellAddress.row
? bounds.leftTop.row + 1
: selectedCellAddress.row,
},
rightBottom: {
col: bounds.rightBottom.col,
row: (bounds.rightBottom.row > selectedCellAddress.row || bounds.leftTop.row === selectedCellAddress.row)
? bounds.rightBottom.row + 1
: selectedCellAddress.row,
},
};
break;
}
default: {
return;
}
}
unSelectionOutOfRange(newBounds.leftTop, newBounds.rightBottom);
expandRange(newBounds.leftTop, newBounds.rightBottom);
} else {
// shiftctrl
switch (ev.code) {
case 'ArrowRight': {
const selectedCellAddress = requireSelectionCell();
selectionCell({ col: selectedCellAddress.col + 1, row: selectedCellAddress.row });
break;
}
case 'ArrowLeft': {
const selectedCellAddress = requireSelectionCell();
selectionCell({ col: selectedCellAddress.col - 1, row: selectedCellAddress.row });
break;
}
case 'ArrowUp': {
const selectedCellAddress = requireSelectionCell();
selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row - 1 });
break;
}
case 'ArrowDown': {
const selectedCellAddress = requireSelectionCell();
selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row + 1 });
break;
}
case 'Delete': {
const ranges = rangedCells.value;
for (const range of ranges) {
range.value = undefined;
}
break;
}
default: {
return;
}
}
}
}
break;
}
}
@ -134,7 +318,6 @@ function onMouseDown(ev: MouseEvent) {
break;
}
case 'normal': {
const cellAddress = getCellAddress(ev.target as HTMLElement);
if (availableCellAddress(cellAddress)) {
selectionCell(cellAddress);
@ -151,6 +334,8 @@ function onMouseDown(ev: MouseEvent) {
registerMouseMove();
firstSelectionColumnIdx.value = cellAddress.col;
state.value = 'colSelecting';
rootEl.value?.focus();
} else if (isRowNumberCellAddress(cellAddress)) {
unSelectionRange();
@ -161,6 +346,8 @@ function onMouseDown(ev: MouseEvent) {
registerMouseMove();
firstSelectionRowIdx.value = cellAddress.row;
state.value = 'rowSelecting';
rootEl.value?.focus();
}
break;
}
@ -168,6 +355,7 @@ function onMouseDown(ev: MouseEvent) {
}
function onMouseMove(ev: MouseEvent) {
ev.preventDefault();
switch (state.value) {
case 'cellSelecting': {
const selectedCellAddress = selectedCell.value?.address;
@ -240,6 +428,7 @@ function onMouseMove(ev: MouseEvent) {
}
function onMouseUp(ev: MouseEvent) {
ev.preventDefault();
switch (state.value) {
case 'rowSelecting':
case 'colSelecting':
@ -270,20 +459,14 @@ function onCellEditEnd() {
}
function onChangeCellValue(sender: GridCell, newValue: CellValue) {
cells.value[sender.address.row][sender.address.col].value = newValue;
setCellValue(sender, newValue);
}
function onChangeCellContentSize(sender: GridCell, contentSize: Size) {
cells.value[sender.address.row][sender.address.col].contentSize = contentSize;
}
function onSelectionMove(_: GridCell, next: CellAddress) {
if (availableCellAddress(next)) {
selectionCell(next);
}
}
function onHeaderCellWidthBeginChange(_: GridColumn) {
function onHeaderCellWidthBeginChange() {
switch (state.value) {
case 'normal': {
state.value = 'colResizing';
@ -292,7 +475,7 @@ function onHeaderCellWidthBeginChange(_: GridColumn) {
}
}
function onHeaderCellWidthEndChange(_: GridColumn) {
function onHeaderCellWidthEndChange() {
switch (state.value) {
case 'colResizing': {
state.value = 'normal';
@ -341,18 +524,14 @@ function onHeaderCellWidthLargest(sender: GridColumn) {
}
}
function onSelectionColumn(sender: GridColumn) {
unSelectionRange();
const targets = cells.value.map(row => row[sender.index].address);
selectionRange(...targets);
}
function onSelectionRow(sender: GridRow) {
unSelectionRange();
const targets = cells.value[sender.index].map(cell => cell.address);
selectionRange(...targets);
function setCellValue(sender: GridCell | CellAddress, newValue: CellValue) {
const cellAddress = 'address' in sender ? sender.address : sender;
cells.value[cellAddress.row][cellAddress.col].value = newValue;
emit('change:cellValue', {
column: columns.value[cellAddress.col],
row: rows.value[cellAddress.row],
value: newValue,
});
}
function selectionCell(target: CellAddress) {
@ -367,6 +546,15 @@ function selectionCell(target: CellAddress) {
_cells[target.row][target.col].ranged = true;
}
function requireSelectionCell(): CellAddress {
const selected = selectedCell.value;
if (!selected) {
throw new Error('No selected cell');
}
return selected.address;
}
function selectionRange(...targets: CellAddress[]) {
const _cells = cells.value;
for (const target of targets) {
@ -414,6 +602,75 @@ function isRowNumberCellAddress(cellAddress: CellAddress): boolean {
return cellAddress.row >= 0 && cellAddress.col === -1;
}
function rangeCopyToClipboard() {
const lines = Array.of<string>();
const bounds = rangedBounds.value;
for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
const items = Array.of<string>();
for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
const cell = cells.value[row][col];
items.push(cell.value?.toString() ?? '');
}
lines.push(items.join('\t'));
}
const text = lines.join('\n');
copyToClipboard(text);
}
async function pasteFromClipboard() {
function parseValue(value: string, type: ColumnSetting['type']): CellValue {
switch (type) {
case 'number': {
return Number(value);
}
case 'boolean': {
return value === 'true';
}
default: {
return value;
}
}
}
const clipBoardText = await navigator.clipboard.readText();
const bounds = rangedBounds.value;
const lines = clipBoardText.replace(/\r/g, '')
.split('\n')
.map(it => it.split('\t'));
if (lines.length === 1 && lines[0].length === 1) {
//
const ranges = rangedCells.value;
for (const cell of ranges) {
setCellValue(cell, parseValue(lines[0][0], cell.column.setting.type));
}
} else {
//
const offsetRow = bounds.leftTop.row;
const offsetCol = bounds.leftTop.col;
for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
const rowIdx = row - offsetRow;
if (lines.length <= rowIdx) {
//
break;
}
const items = lines[rowIdx];
for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
const colIdx = col - offsetCol;
if (items.length <= colIdx) {
//
break;
}
setCellValue(cells.value[row][col], parseValue(items[colIdx], cells.value[row][col].column.setting.type));
}
}
}
}
function refreshColumnsSetting() {
const bindToList = columnSettings.value.map(it => it.bindTo);
if (new Set(bindToList).size !== columnSettings.value.length) {
@ -427,6 +684,7 @@ function refreshData() {
const _data: DataSource[] = data.value;
const _rows: GridRow[] = _data.map((_, index) => ({
index,
}));
const _columns: GridColumn[] = columnSettings.value.map((setting, index) => ({
index,

View File

@ -2,7 +2,6 @@
<tr :class="$style.header">
<MkNumberCell
content="#"
:selectable="false"
:top="true"
/>
<MkHeaderCell

View File

@ -7,12 +7,9 @@
<script setup lang="ts">
import { GridRow } from '@/components/grid/types.js';
const emit = defineEmits<{}>();
defineProps<{
content: string,
row?: GridRow,
selectable: boolean,
top?: boolean,
}>();

View File

@ -56,6 +56,12 @@ export type GridRow = {
index: number;
}
export type CellValueChangedEvent = {
column: GridColumn;
row: GridRow;
value: CellValue;
}
export class GridEventEmitter extends EventEmitter<{}> {
}
@ -71,7 +77,7 @@ export function isRowElement(elem: any): elem is HTMLTableRowElement {
return elem instanceof HTMLTableRowElement;
}
export function getCellAddress(elem: HTMLElement, parentNodeCount = 5): CellAddress {
export function getCellAddress(elem: HTMLElement, parentNodeCount = 10): CellAddress {
let node = elem;
for (let i = 0; i < parentNodeCount; i++) {
if (isCellElement(node) && isRowElement(node.parentElement)) {

View File

@ -1,9 +1,23 @@
import * as Misskey from 'misskey-js';
export class GridItem {
export interface IGridItem {
readonly id?: string;
readonly url?: string;
readonly blob?: Blob;
readonly fileId?: string;
readonly url: string;
name: string;
category: string;
aliases: string;
license: string;
isSensitive: boolean;
localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: string;
}
export class GridItem implements IGridItem {
readonly id?: string;
readonly fileId?: string;
readonly url: string;
public name: string;
public category: string;
@ -17,8 +31,8 @@ export class GridItem {
constructor(
id: string | undefined,
url: string | undefined = undefined,
blob: Blob | undefined = undefined,
fileId: string | undefined,
url: string,
name: string,
category: string,
aliases: string,
@ -28,8 +42,8 @@ export class GridItem {
roleIdsThatCanBeUsedThisEmojiAsReaction: string,
) {
this.id = id;
this.fileId = fileId;
this.url = url;
this.blob = blob;
this.aliases = aliases;
this.name = name;
@ -42,11 +56,11 @@ export class GridItem {
this.origin = JSON.stringify(this);
}
static ofEmojiDetailed(it: Misskey.entities.EmojiDetailed): GridItem {
static fromEmojiDetailed(it: Misskey.entities.EmojiDetailed): GridItem {
return new GridItem(
it.id,
it.url,
undefined,
it.url,
it.name,
it.category ?? '',
it.aliases.join(', '),
@ -57,6 +71,21 @@ export class GridItem {
);
}
static fromDriveFile(it: Misskey.entities.DriveFile): GridItem {
return new GridItem(
undefined,
it.id,
it.url,
it.name.replace(/\.[a-zA-Z0-9]+$/, ''),
'',
'',
'',
it.isSensitive,
false,
'',
);
}
public get edited(): boolean {
const { origin, ..._this } = this;
return JSON.stringify(_this) !== origin;

View File

@ -0,0 +1,89 @@
<template>
<div class="_gaps">
<MkInput :modelValue="query" :debounce="true" type="search" autocapitalize="off" @change="(v) => query = v">
<template #prefix><i class="ti ti-search"></i></template>
<template #label>{{ i18n.ts.search }}</template>
</MkInput>
<div
style="overflow-y: scroll; padding-top: 8px; padding-bottom: 8px;"
>
<MkGrid :data="convertedGridItems" :columnSettings="columnSettings"/>
</div>
<div :class="$style.pages">
<button>&lt;&lt;</button>
<button>&lt;</button>
<button>1</button>
<button>2</button>
<button>3</button>
<button>4</button>
<button>5</button>
<span>...</span>
<button>10</button>
<button>&gt;</button>
<button>&gt;&gt;</button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onActivated, onMounted, ref, toRefs, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import MkGrid from '@/components/grid/MkGrid.vue';
import { ColumnSetting } from '@/components/grid/types.js';
import { i18n } from '@/i18n.js';
import MkInput from '@/components/MkInput.vue';
const columnSettings: ColumnSetting[] = [
{ bindTo: 'url', title: '🎨', type: 'image', editable: false, width: 50 },
{ bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140 },
{ bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 },
{ bindTo: 'aliases', title: 'aliases', type: 'text', editable: true, width: 140 },
{ bindTo: 'license', title: 'license', type: 'text', editable: true, width: 140 },
{ bindTo: 'isSensitive', title: 'sensitive', type: 'boolean', editable: true, width: 90 },
{ bindTo: 'localOnly', title: 'localOnly', type: 'boolean', editable: true, width: 90 },
{ bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 140 },
];
const props = defineProps<{
customEmojis: Misskey.entities.EmojiDetailed[];
}>();
const { customEmojis } = toRefs(props);
const query = ref('');
const gridItems = ref<GridItem[]>([]);
const convertedGridItems = computed(() => gridItems.value.map(it => it.asRecord()));
watch(customEmojis, refreshGridItems);
function refreshGridItems() {
gridItems.value = customEmojis.value.map(it => GridItem.fromEmojiDetailed(it));
}
refreshGridItems();
</script>
<style module lang="scss">
.pages {
display: flex;
justify-content: center;
align-items: center;
margin-top: 8px;
button {
background-color: var(--buttonBg);
border-radius: 9999px;
border: none;
margin: 0 4px;
padding: 8px;
}
}
</style>

View File

@ -0,0 +1,218 @@
<template>
<div class="_gaps">
<MkFolder>
<template #icon><i class="ti ti-settings"></i></template>
<template #label>アップロード設定</template>
<template #caption>この画面で絵文字アップロードを行う際の動作を設定できます</template>
<div class="_gaps">
<MkSelect v-model="selectedFolderId">
<template #label>{{ i18n.ts.uploadFolder }}</template>
<option v-for="folder in uploadFolders" :key="folder.id" :value="folder.id">
{{ folder.name }}
</option>
</MkSelect>
<MkSwitch v-model="keepOriginalUploading">
<template #label>{{ i18n.ts.keepOriginalUploading }}</template>
<template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template>
</MkSwitch>
</div>
</MkFolder>
<div
:class="$style.uploadBox"
@dragover.prevent
@drop.prevent.stop="onDrop"
>
ここに絵文字の画像ファイルをドラッグドロップするとドライブにアップロードされます
</div>
<div
v-if="gridItems.length > 0"
style="overflow-y: scroll;"
>
<MkGrid
:data="convertedGridItems"
:columnSettings="columnSettings"
@change:cellValue="onChangeCellValue"
/>
</div>
<div
v-if="gridItems.length > 0"
:class="$style.buttons"
>
<MkButton primary @click="onRegistryClicked">{{ i18n.ts.registration }}</MkButton>
<MkButton @click="onClearClicked">{{ i18n.ts.clear }}</MkButton>
</div>
</div>
</template>
<script setup lang="ts">
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { computed, onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { GridItem, IGridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import MkGrid from '@/components/grid/MkGrid.vue';
import { CellValueChangedEvent, ColumnSetting } from '@/components/grid/types.js';
import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue';
import { uploadFile } from '@/scripts/upload.js';
import MkSwitch from '@/components/MkSwitch.vue';
import { defaultStore } from '@/store.js';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
type FolderItem = {
id?: string;
name: string;
};
type UploadResult = { key: string, item: IGridItem, success: boolean, err: any };
const emit = defineEmits<{
(ev: 'operation:registered'): void;
}>();
const columnSettings: ColumnSetting[] = [
{ bindTo: 'url', title: '🎨', type: 'image', editable: false, width: 50 },
{ bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140 },
{ bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 },
{ bindTo: 'aliases', title: 'aliases', type: 'text', editable: true, width: 140 },
{ bindTo: 'license', title: 'license', type: 'text', editable: true, width: 140 },
{ bindTo: 'isSensitive', title: 'sensitive', type: 'boolean', editable: true, width: 90 },
{ bindTo: 'localOnly', title: 'localOnly', type: 'boolean', editable: true, width: 90 },
{ bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 100 },
];
const uploadFolders = ref<FolderItem[]>([]);
const gridItems = ref<IGridItem[]>([]);
const selectedFolderId = ref(defaultStore.state.uploadFolder);
const keepOriginalUploading = ref(defaultStore.state.keepOriginalUploading);
const convertedGridItems = computed(() => gridItems.value.map(it => it as Record<string, any>));
async function onRegistryClicked() {
const dialogSelection = await os.confirm({
type: 'info',
title: '確認',
text: 'リストに表示されている絵文字を新たなカスタム絵文字として登録します。よろしいですか?',
});
if (dialogSelection.canceled) {
return;
}
const items = new Map<string, IGridItem>(gridItems.value.map(it => [`${it.fileId}|${it.name}`, it]));
const upload = async (): Promise<UploadResult[]> => {
const result = Array.of<UploadResult>();
for (const [key, item] of items.entries()) {
try {
await misskeyApi('admin/emoji/add', {
name: item.name,
category: item.category,
aliases: item.aliases.split(',').map(it => it.trim()),
license: item.license,
isSensitive: item.isSensitive,
localOnly: item.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.split(',').map(it => it.trim()),
fileId: item.fileId!,
});
result.push({ key, item, success: true, err: undefined });
} catch (err: any) {
result.push({ key, item, success: false, err });
}
}
return result;
};
const result = await os.promiseDialog(upload());
const failedItems = result.filter(it => !it.success);
gridItems.value = failedItems.map(it => it.item);
emit('operation:registered');
}
async function onClearClicked() {
const result = await os.confirm({
type: 'warning',
title: '確認',
text: '編集内容を破棄し、リストに表示されている絵文字をクリアします。よろしいですか?',
});
if (!result.canceled) {
gridItems.value = [];
}
}
async function onDrop(ev: DragEvent) {
ev.preventDefault();
ev.stopPropagation();
const dropFiles = ev.dataTransfer?.files;
if (!dropFiles) {
return;
}
const uploadingPromises = Array.of<Promise<Misskey.entities.DriveFile>>();
for (let i = 0; i < dropFiles.length; i++) {
const file = dropFiles.item(i);
if (file) {
const name = file.name.replace(/\.[a-zA-Z0-9]+$/, '');
uploadingPromises.push(
uploadFile(
file,
selectedFolderId.value,
name,
keepOriginalUploading.value,
),
);
}
}
const uploadedFiles = await Promise.all(uploadingPromises);
for (const uploadedFile of uploadedFiles) {
const item = GridItem.fromDriveFile(uploadedFile);
gridItems.value.push(item);
}
}
function onChangeCellValue(event: CellValueChangedEvent) {
console.log(event);
const item = gridItems.value[event.row.index];
item[event.column.setting.bindTo] = event.value;
}
async function refreshUploadFolders() {
const result = await misskeyApi('drive/folders', {});
uploadFolders.value = Array.of<FolderItem>({ name: '-' }, ...result);
}
onMounted(async () => {
await refreshUploadFolders();
});
</script>
<style module lang="scss">
.uploadBox {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 120px;
border: 0.5px dotted var(--accentedBg);
border-radius: var(--border-radius);
background-color: var(--accentedBg);
}
.buttons {
margin-top: 16px;
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
</style>

View File

@ -2,38 +2,17 @@
<div>
<MkStickyContainer>
<template #header>
<MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/>
<MkPageHeader v-model:tab="headerTab" :actions="headerActions" :tabs="headerTabs"/>
</template>
<div class="_gaps" :class="$style.root">
<MkInput v-model="query" :debounce="true" type="search" autocapitalize="off">
<template #prefix><i class="ti ti-search"></i></template>
<template #label>{{ i18n.ts.search }}</template>
</MkInput>
<MkTab v-model="modeTab" style="margin-bottom: var(--margin);">
<option value="list">登録済み絵文字一覧</option>
<option value="register">新規登録</option>
</MkTab>
<div :class="$style.controller">
<MkSelect v-model="limit">
<option value="100">100</option>
</MkSelect>
</div>
<div style="overflow-y: scroll; padding-top: 8px; padding-bottom: 8px;">
<MkGrid :data="convertedGridItems" :columnSettings="columnSettings"/>
</div>
<div :class="$style.pages">
<button>&lt;&lt;</button>
<button>&lt;</button>
<button>1</button>
<button>2</button>
<button>3</button>
<button>4</button>
<button>5</button>
<span>...</span>
<button>10</button>
<button>&gt;</button>
<button>&gt;&gt;</button>
<div>
<XListComponent v-if="modeTab === 'list'" :customEmojis="customEmojis"/>
<XRegisterComponent v-else @operation:registered="onOperationRegistered"/>
</div>
</div>
</MkStickyContainer>
@ -41,49 +20,31 @@
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import { computed, onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import MkGrid from '@/components/grid/MkGrid.vue';
import { ColumnSetting } from '@/components/grid/types.js';
import { i18n } from '@/i18n.js';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkTab from '@/components/MkTab.vue';
import XListComponent from '@/pages/admin/custom-emojis-grid.list.vue';
import XRegisterComponent from '@/pages/admin/custom-emojis-grid.register.vue';
const columnSettings: ColumnSetting[] = [
{ bindTo: 'url', title: '🎨', type: 'image', editable: false, width: 50 },
{ bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140 },
{ bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 },
{ bindTo: 'aliases', title: 'aliases', type: 'text', editable: true, width: 140 },
{ bindTo: 'license', title: 'license', type: 'text', editable: true, width: 140 },
{ bindTo: 'isSensitive', title: 'sensitive', type: 'boolean', editable: true, width: 90 },
{ bindTo: 'localOnly', title: 'localOnly', type: 'boolean', editable: true, width: 90 },
{ bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 140 },
];
type PageMode = 'list' | 'register';
const customEmojis = ref<Misskey.entities.EmojiDetailed[]>([]);
const gridItems = ref<GridItem[]>([]);
const query = ref('');
const limit = ref(100);
const tab = ref('local');
const headerTab = ref('local');
const modeTab = ref<PageMode>('list');
const convertedGridItems = computed(() => gridItems.value.map(it => it.asRecord()));
async function refreshCustomEmojis() {
customEmojis.value = await misskeyApi('admin/emoji/list', { limit: 100 });
}
const refreshCustomEmojis = async () => {
customEmojis.value = await misskeyApi('emojis', { detail: true }).then(it => it.emojis);
};
const refreshGridItems = () => {
gridItems.value = customEmojis.value.map(it => GridItem.ofEmojiDetailed(it));
};
watch(customEmojis, refreshGridItems);
async function onOperationRegistered() {
await refreshCustomEmojis();
}
onMounted(async () => {
await refreshCustomEmojis();
refreshGridItems();
});
const headerTabs = computed(() => [{
@ -98,10 +59,13 @@ const headerActions = computed(() => [{
asFullButton: true,
icon: 'ti ti-plus',
text: i18n.ts.addEmoji,
handler: () => {},
handler: () => {
},
}, {
icon: 'ti ti-dots',
handler: () => {},
text: '',
handler: () => {
},
}]);
definePageMetadata(computed(() => ({

View File

@ -1005,9 +1005,6 @@ type EmojiResponse = operations['emoji']['responses']['200']['content']['applica
// @public (undocumented)
type EmojiSimple = components['schemas']['EmojiSimple'];
// @public (undocumented)
type EmojisRequest = operations['emojis']['requestBody']['content']['application/json'];
// @public (undocumented)
type EmojisResponse = operations['emojis']['responses']['200']['content']['application/json'];
@ -1049,26 +1046,6 @@ export type Endpoints = Overwrite<Endpoints_2, {
};
};
};
'emojis': {
req: EmojisRequest;
res: {
$switch: {
$cases: [
[
{
detail: true;
},
{
emojis: EmojiDetailed[];
}
]
];
$default: {
emojis: EmojiSimple[];
};
};
};
};
'signup': {
req: SignupRequest;
res: SignupResponse;
@ -1468,7 +1445,6 @@ declare namespace entities {
InviteLimitResponse,
MetaRequest,
MetaResponse,
EmojisRequest,
EmojisResponse,
EmojiRequest,
EmojiResponse,

View File

@ -1,6 +1,6 @@
import { Endpoints as Gen } from './autogen/endpoint.js';
import { EmojiDetailed, EmojiSimple, UserDetailed } from './autogen/models.js';
import { EmojisRequest, UsersShowRequest } from './autogen/entities.js';
import { UserDetailed } from './autogen/models.js';
import { UsersShowRequest } from './autogen/entities.js';
import {
SigninRequest,
SigninResponse,
@ -64,24 +64,6 @@ export type Endpoints = Overwrite<
};
};
},
'emojis': {
req: EmojisRequest;
res: {
$switch: {
$cases: [[
{
detail: true;
},
{
emojis: EmojiDetailed[]
}
]];
$default: {
emojis: EmojiSimple[]
}
};
}
},
// api.jsonには載せないものなのでここで定義
'signup': {
req: SignupRequest;

View File

@ -1,6 +1,6 @@
/*
* version: 2024.2.0-beta.4
* generatedAt: 2024-01-23T07:38:51.367Z
* generatedAt: 2024-01-27T12:44:54.092Z
*/
import type { SwitchCaseResponseType } from '../api.js';

View File

@ -1,6 +1,6 @@
/*
* version: 2024.2.0-beta.4
* generatedAt: 2024-01-23T07:38:51.365Z
* generatedAt: 2024-01-27T12:44:54.091Z
*/
import type {
@ -366,7 +366,6 @@ import type {
InviteLimitResponse,
MetaRequest,
MetaResponse,
EmojisRequest,
EmojisResponse,
EmojiRequest,
EmojiResponse,
@ -806,7 +805,7 @@ export type Endpoints = {
'invite/list': { req: InviteListRequest; res: InviteListResponse };
'invite/limit': { req: EmptyRequest; res: InviteLimitResponse };
'meta': { req: MetaRequest; res: MetaResponse };
'emojis': { req: EmojisRequest; res: EmojisResponse };
'emojis': { req: EmptyRequest; res: EmojisResponse };
'emoji': { req: EmojiRequest; res: EmojiResponse };
'miauth/gen-token': { req: MiauthGenTokenRequest; res: MiauthGenTokenResponse };
'mute/create': { req: MuteCreateRequest; res: EmptyResponse };

View File

@ -1,6 +1,6 @@
/*
* version: 2024.2.0-beta.4
* generatedAt: 2024-01-23T07:38:51.364Z
* generatedAt: 2024-01-27T12:44:54.089Z
*/
import { operations } from './types.js';
@ -368,7 +368,6 @@ export type InviteListResponse = operations['invite/list']['responses']['200']['
export type InviteLimitResponse = operations['invite/limit']['responses']['200']['content']['application/json'];
export type MetaRequest = operations['meta']['requestBody']['content']['application/json'];
export type MetaResponse = operations['meta']['responses']['200']['content']['application/json'];
export type EmojisRequest = operations['emojis']['requestBody']['content']['application/json'];
export type EmojisResponse = operations['emojis']['responses']['200']['content']['application/json'];
export type EmojiRequest = operations['emoji']['requestBody']['content']['application/json'];
export type EmojiResponse = operations['emoji']['responses']['200']['content']['application/json'];

View File

@ -1,6 +1,6 @@
/*
* version: 2024.2.0-beta.4
* generatedAt: 2024-01-23T07:38:51.363Z
* generatedAt: 2024-01-27T12:44:54.088Z
*/
import { components } from './types.js';

View File

@ -3,7 +3,7 @@
/*
* version: 2024.2.0-beta.4
* generatedAt: 2024-01-23T07:38:51.282Z
* generatedAt: 2024-01-27T12:44:54.008Z
*/
/**
@ -6539,16 +6539,7 @@ export type operations = {
/** @description OK (with results) */
200: {
content: {
'application/json': ({
/** Format: id */
id: string;
aliases: string[];
name: string;
category: string | null;
/** @description The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files. */
host: string | null;
url: string;
})[];
'application/json': components['schemas']['EmojiDetailed'][];
};
};
/** @description Client error */
@ -19027,19 +19018,12 @@ export type operations = {
* **Credential required**: *No*
*/
emojis: {
requestBody: {
content: {
'application/json': {
detail?: boolean | null;
};
};
};
responses: {
/** @description OK (with results) */
200: {
content: {
'application/json': {
emojis: (components['schemas']['EmojiSimple'] | components['schemas']['EmojiDetailed'])[];
emojis: components['schemas']['EmojiSimple'][];
};
};
};