summaryrefslogtreecommitdiff
path: root/packages/frontend
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-05-18 18:45:49 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2023-05-18 18:45:49 +0900
commit7ce569424a67c786a4f44993ce74f06989eba9df (patch)
tree895e142f514452310382ce8c315feebcce27b1e9 /packages/frontend
parentperf(backend): pre-compile regexp (diff)
downloadmisskey-7ce569424a67c786a4f44993ce74f06989eba9df.tar.gz
misskey-7ce569424a67c786a4f44993ce74f06989eba9df.tar.bz2
misskey-7ce569424a67c786a4f44993ce74f06989eba9df.zip
feat: カスタム絵文字ごとにそれをリアクションとして使えるロールを設定できるように
Diffstat (limited to 'packages/frontend')
-rw-r--r--packages/frontend/src/components/MkRolePreview.vue13
-rw-r--r--packages/frontend/src/pages/custom-emojis-manager.vue19
-rw-r--r--packages/frontend/src/pages/emoji-edit-dialog.vue214
3 files changed, 186 insertions, 60 deletions
diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue
index 2f5866f340..9fbe1ec993 100644
--- a/packages/frontend/src/components/MkRolePreview.vue
+++ b/packages/frontend/src/components/MkRolePreview.vue
@@ -12,8 +12,10 @@
</template>
</span>
<span :class="$style.name">{{ role.name }}</span>
- <span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span>
- <span v-else-if="role.target === 'conditional'" :class="$style.users">({{ i18n.ts._role.conditional }})</span>
+ <template v-if="detailed">
+ <span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span>
+ <span v-else-if="role.target === 'conditional'" :class="$style.users">({{ i18n.ts._role.conditional }})</span>
+ </template>
</div>
<div :class="$style.description">{{ role.description }}</div>
</MkA>
@@ -23,10 +25,13 @@
import { } from 'vue';
import { i18n } from '@/i18n';
-const props = defineProps<{
+const props = withDefaults(defineProps<{
role: any;
forModeration: boolean;
-}>();
+ detailed: boolean;
+}>(), {
+ detailed: true,
+});
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 3f13f0787d..3da6a0d9cb 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -2,7 +2,7 @@
<div>
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="900">
+ <MkSpacer :contentMax="900">
<div class="ogwlenmc">
<div v-if="tab === 'local'" class="local">
<MkInput v-model="query" :debounce="true" type="search">
@@ -123,15 +123,14 @@ const toggleSelect = (emoji) => {
};
const add = async (ev: MouseEvent) => {
- const files = await selectFiles(ev.currentTarget ?? ev.target, null);
-
- const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
- fileId: file.id,
- })));
- promise.then(() => {
- emojisPaginationComponent.value.reload();
- });
- os.promiseDialog(promise);
+ os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
+ }, {
+ done: result => {
+ if (result.created) {
+ emojisPaginationComponent.value.prepend(result.created);
+ }
+ },
+ }, 'closed');
};
const edit = (emoji) => {
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index 3c829d6a8e..24b72b6f7f 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -1,84 +1,168 @@
<template>
<MkModalWindow
ref="dialog"
- :width="370"
- :with-ok-button="true"
- @close="$refs.dialog.close()"
+ :width="400"
+ @close="dialog.close()"
@closed="$emit('closed')"
- @ok="ok()"
>
- <template #header>:{{ emoji.name }}:</template>
+ <template v-if="emoji" #header>:{{ emoji.name }}:</template>
+ <template v-else #header>New emoji</template>
- <MkSpacer :margin-min="20" :margin-max="28">
- <div class="_gaps_m">
- <img :src="`/emoji/${emoji.name}.webp`" :class="$style.img"/>
- <MkInput v-model="name">
- <template #label>{{ i18n.ts.name }}</template>
- </MkInput>
- <MkInput v-model="category" :datalist="customEmojiCategories">
- <template #label>{{ i18n.ts.category }}</template>
- </MkInput>
- <MkInput v-model="aliases">
- <template #label>{{ i18n.ts.tags }}</template>
- <template #caption>{{ i18n.ts.setMultipleBySeparatingWithSpace }}</template>
- </MkInput>
- <MkInput v-model="license">
- <template #label>{{ i18n.ts.license }}</template>
- </MkInput>
- <MkButton danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+ <div>
+ <MkSpacer :marginMin="20" :marginMax="28">
+ <div class="_gaps_m">
+ <div v-if="imgUrl != null" :class="$style.imgs">
+ <div style="background: #000;" :class="$style.imgContainer">
+ <img :src="imgUrl" :class="$style.img"/>
+ </div>
+ <div style="background: #222;" :class="$style.imgContainer">
+ <img :src="imgUrl" :class="$style.img"/>
+ </div>
+ <div style="background: #ddd;" :class="$style.imgContainer">
+ <img :src="imgUrl" :class="$style.img"/>
+ </div>
+ <div style="background: #fff;" :class="$style.imgContainer">
+ <img :src="imgUrl" :class="$style.img"/>
+ </div>
+ </div>
+ <MkButton rounded style="margin: 0 auto;" @click="changeImage">{{ i18n.ts.selectFile }}</MkButton>
+ <MkInput v-model="name">
+ <template #label>{{ i18n.ts.name }}</template>
+ </MkInput>
+ <MkInput v-model="category" :datalist="customEmojiCategories">
+ <template #label>{{ i18n.ts.category }}</template>
+ </MkInput>
+ <MkInput v-model="aliases">
+ <template #label>{{ i18n.ts.tags }}</template>
+ <template #caption>{{ i18n.ts.setMultipleBySeparatingWithSpace }}</template>
+ </MkInput>
+ <MkInput v-model="license">
+ <template #label>{{ i18n.ts.license }}</template>
+ </MkInput>
+ <MkFolder>
+ <template #label>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReaction }}</template>
+ <div class="_gaps">
+ <MkInfo>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription }}</MkInfo>
+
+ <MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+
+ <div v-for="role in rolesThatCanBeUsedThisEmojiAsReaction" :key="role.id" :class="$style.roleItem">
+ <MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false"/>
+ <button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button>
+ <button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
+ </div>
+ </div>
+ </MkFolder>
+ <MkSwitch v-model="isSensitive">isSensitive</MkSwitch>
+ <MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
+ <MkButton danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+ </div>
+ </MkSpacer>
+ <div :class="$style.footer">
+ <MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton>
</div>
- </MkSpacer>
+ </div>
</MkModalWindow>
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { computed, watch } from 'vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { customEmojiCategories } from '@/custom-emojis';
+import MkSwitch from '@/components/MkSwitch.vue';
+import { selectFile, selectFiles } from '@/scripts/select-file';
+import MkRolePreview from '@/components/MkRolePreview.vue';
const props = defineProps<{
- emoji: any,
+ emoji?: any,
}>();
let dialog = $ref(null);
-let name: string = $ref(props.emoji.name);
-let category: string = $ref(props.emoji.category);
-let aliases: string = $ref(props.emoji.aliases.join(' '));
-let license: string = $ref(props.emoji.license ?? '');
+let name: string = $ref(props.emoji ? props.emoji.name : '');
+let category: string = $ref(props.emoji ? props.emoji.category : '');
+let aliases: string = $ref(props.emoji ? props.emoji.aliases.join(' ') : '');
+let license: string = $ref(props.emoji ? (props.emoji.license ?? '') : '');
+let isSensitive = $ref(props.emoji ? props.emoji.isSensitive : false);
+let localOnly = $ref(props.emoji ? props.emoji.localOnly : false);
+let roleIdsThatCanBeUsedThisEmojiAsReaction = $ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
+let rolesThatCanBeUsedThisEmojiAsReaction = $ref([]);
+let file = $ref();
+
+watch($$(roleIdsThatCanBeUsedThisEmojiAsReaction), async () => {
+ rolesThatCanBeUsedThisEmojiAsReaction = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
+}, { immediate: true });
+
+const imgUrl = computed(() => file ? file.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
const emit = defineEmits<{
- (ev: 'done', v: { deleted?: boolean, updated?: any }): void,
+ (ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
(ev: 'closed'): void
}>();
-function ok() {
- update();
+async function changeImage(ev) {
+ file = await selectFile(ev.currentTarget ?? ev.target, null);
}
-async function update() {
- await os.apiWithDialog('admin/emoji/update', {
- id: props.emoji.id,
+async function addRole() {
+ const roles = await os.api('admin/roles/list');
+ const currentRoleIds = rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id);
+
+ const { canceled, result: role } = await os.select({
+ items: roles.filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
+ });
+ if (canceled) return;
+
+ rolesThatCanBeUsedThisEmojiAsReaction.push(role);
+}
+
+async function removeRole(role, ev) {
+ rolesThatCanBeUsedThisEmojiAsReaction = rolesThatCanBeUsedThisEmojiAsReaction.filter(x => x.id !== role.id);
+}
+
+async function done() {
+ const params = {
name,
category,
aliases: aliases.split(' '),
license: license === '' ? null : license,
- });
+ isSensitive,
+ localOnly,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id),
+ };
+
+ if (file) {
+ params.fileId = file.id;
+ }
- emit('done', {
- updated: {
+ if (props.emoji) {
+ await os.apiWithDialog('admin/emoji/update', {
id: props.emoji.id,
- name,
- category,
- aliases: aliases.split(' '),
- license: license === '' ? null : license,
- },
- });
+ ...params,
+ });
+
+ emit('done', {
+ updated: {
+ id: props.emoji.id,
+ ...params,
+ },
+ });
+
+ dialog.close();
+ } else {
+ const created = await os.apiWithDialog('admin/emoji/add', params);
+
+ emit('done', {
+ created: created,
+ });
- dialog.close();
+ dialog.close();
+ }
}
async function del() {
@@ -100,9 +184,47 @@ async function del() {
</script>
<style lang="scss" module>
+.imgs {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+.imgContainer {
+ padding: 8px;
+ border-radius: 6px;
+}
+
.img {
display: block;
height: 64px;
- margin: 0 auto;
+ width: 64px;
+ object-fit: contain;
+}
+
+.roleItem {
+ display: flex;
+}
+
+.role {
+ flex: 1;
+}
+
+.roleUnassign {
+ width: 32px;
+ height: 32px;
+ margin-left: 8px;
+ align-self: center;
+}
+
+.footer {
+ position: sticky;
+ bottom: 0;
+ left: 0;
+ padding: 12px;
+ border-top: solid 0.5px var(--divider);
+ -webkit-backdrop-filter: var(--blur, blur(15px));
+ backdrop-filter: var(--blur, blur(15px));
}
</style>