1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-17 07:55:27 +02:00

Merge branch 'develop' into pag-back

This commit is contained in:
tamaina
2023-08-13 11:17:12 +00:00
205 changed files with 2001 additions and 1020 deletions

View File

@@ -0,0 +1,47 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import MkAnnouncementDialog from './MkAnnouncementDialog.vue';
export const Default = {
render(args) {
return {
components: {
MkAnnouncementDialog,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
},
template: '<MkAnnouncementDialog v-bind="props" />',
};
},
args: {
announcement: {
id: '1',
title: 'Title',
text: 'Text',
createdAt: new Date().toISOString(),
updatedAt: null,
icon: 'info',
imageUrl: null,
display: 'dialog',
needConfirmationToRead: false,
forYou: true,
},
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkAnnouncementDialog>;

View File

@@ -0,0 +1,104 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModal ref="modal" :zPriority="'middle'" @closed="$emit('closed')" @click="onBgClick">
<div ref="rootEl" :class="$style.root">
<div :class="$style.header">
<span :class="$style.icon">
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
</span>
<span :class="$style.title">{{ announcement.title }}</span>
</div>
<div :class="$style.text"><Mfm :text="announcement.text"/></div>
<MkButton primary full @click="ok">{{ i18n.ts.ok }}</MkButton>
</div>
</MkModal>
</template>
<script lang="ts" setup>
import { onMounted, shallowRef } from 'vue';
import * as misskey from 'misskey-js';
import * as os from '@/os';
import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n';
import { $i, updateAccount } from '@/account';
const props = withDefaults(defineProps<{
announcement: misskey.entities.Announcement;
}>(), {
});
const rootEl = shallowRef<HTMLDivElement>();
const modal = shallowRef<InstanceType<typeof MkModal>>();
async function ok() {
if (props.announcement.needConfirmationToRead) {
const confirm = await os.confirm({
type: 'question',
title: i18n.ts._announcement.readConfirmTitle,
text: i18n.t('_announcement.readConfirmText', { title: props.announcement.title }),
});
if (confirm.canceled) return;
}
modal.value.close();
os.api('i/read-announcement', { announcementId: props.announcement.id });
updateAccount({
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
});
}
function onBgClick() {
rootEl.value.animate([{
offset: 0,
transform: 'scale(1)',
}, {
offset: 0.5,
transform: 'scale(1.1)',
}, {
offset: 1,
transform: 'scale(1)',
}], {
duration: 100,
});
}
onMounted(() => {
});
</script>
<style lang="scss" module>
.root {
margin: auto;
position: relative;
padding: 32px;
min-width: 320px;
max-width: 480px;
box-sizing: border-box;
background: var(--panel);
border-radius: var(--radius);
}
.header {
font-size: 120%;
}
.icon {
margin-right: 0.5em;
}
.title {
font-weight: bold;
}
.text {
margin: 1em 0;
}
</style>

View File

@@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="banner" :style="bannerStyle">
<div class="fade"></div>
<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
<div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
<div class="status">
<div>
<i class="ti ti-users ti-fw"></i>
@@ -102,6 +103,19 @@ const bannerStyle = computed(() => {
border-radius: 6px;
color: #fff;
}
> .sensitiveIndicator {
position: absolute;
z-index: 1;
bottom: 16px;
left: 16px;
background: rgba(0, 0, 0, 0.7);
color: var(--warn);
border-radius: 6px;
font-weight: bold;
font-size: 1em;
padding: 4px 7px;
}
}
> article {

View File

@@ -156,7 +156,7 @@ watch(q, () => {
const newQ = q.value.replace(/:/g, '').toLowerCase();
const searchCustom = () => {
const max = 8;
const max = 100;
const emojis = customEmojis.value;
const matches = new Set<Misskey.entities.CustomEmoji>();
@@ -219,7 +219,7 @@ watch(q, () => {
};
const searchUnicode = () => {
const max = 8;
const max = 100;
const emojis = emojilist;
const matches = new Set<UnicodeEmojiDef>();

View File

@@ -24,7 +24,6 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<source
:src="video.url"
:type="video.type"
>
</video>
<i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>

View File

@@ -35,14 +35,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
</button>
<span v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" :class="$style.item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</MkSwitch>
</span>
<button v-else-if="item.type === 'parent'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="showChildren(item, $event)">
<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)" />
<span :class="$style.switchText">{{ item.text }}</span>
</button>
<div v-else-if="item.type === 'parent'" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<span>{{ item.text }}</span>
<span :class="$style.caret"><i class="ti ti-chevron-right ti-fw"></i></span>
</button>
</div>
<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
@@ -55,19 +56,24 @@ SPDX-License-Identifier: AGPL-3.0-only
</span>
</div>
<div v-if="childMenu">
<XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned"/>
<XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned" @close="close(false)"/>
</div>
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
<script lang="ts">
import { Ref, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { focusPrev, focusNext } from '@/scripts/focus';
import MkSwitch from '@/components/MkSwitch.vue';
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
import MkSwitchButton from '@/components/MkSwitch.button.vue';
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { isTouchUsing } from '@/scripts/touch';
const childrenCache = new WeakMap<MenuParent, MenuItem[]>();
</script>
<script lang="ts" setup>
const XChild = defineAsyncComponent(() => import('./MkMenu.child.vue'));
const props = defineProps<{
@@ -81,6 +87,7 @@ const props = defineProps<{
const emit = defineEmits<{
(ev: 'close', actioned?: boolean): void;
(ev: 'hide'): void;
}>();
let itemsEl = $shallowRef<HTMLDivElement>();
@@ -97,6 +104,8 @@ let keymap = $computed(() => ({
let childShowingItem = $ref<MenuItem | null>();
let preferClick = isTouchUsing || props.asDrawer;
watch(() => props.items, () => {
const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined);
@@ -116,7 +125,7 @@ watch(() => props.items, () => {
immediate: true,
});
let childMenu = ref<MenuItem[] | null>();
const childMenu = ref<MenuItem[] | null>();
let childTarget = $shallowRef<HTMLElement | null>();
function closeChild() {
@@ -129,11 +138,11 @@ function childActioned() {
close(true);
}
function onGlobalMousedown(event: MouseEvent) {
const onGlobalMousedown = (event: MouseEvent) => {
if (childTarget && (event.target === childTarget || childTarget.contains(event.target))) return;
if (child && child.checkHit(event)) return;
closeChild();
}
};
let childCloseTimer: null | number = null;
function onItemMouseEnter(item) {
@@ -145,31 +154,30 @@ function onItemMouseLeave(item) {
if (childCloseTimer) window.clearTimeout(childCloseTimer);
}
let childrenCache = new WeakMap();
async function showChildren(item: MenuItem, ev: MouseEvent) {
const children = ref([]);
if (childrenCache.has(item)) {
children.value = childrenCache.get(item);
} else {
if (typeof item.children === 'function') {
children.value = [{
type: 'pending',
}];
item.children().then(x => {
children.value = x;
childrenCache.set(item, x);
});
async function showChildren(item: MenuParent, ev: MouseEvent) {
const children = await (async () => {
if (childrenCache.has(item)) {
return childrenCache.get(item)!;
} else {
children.value = item.children;
if (typeof item.children === 'function') {
return Promise.resolve(item.children());
} else {
return item.children;
}
}
}
})();
childrenCache.set(item, children);
if (props.asDrawer) {
os.popupMenu(children, ev.currentTarget ?? ev.target);
close();
os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
emit('close');
});
emit('hide');
} else {
childTarget = ev.currentTarget ?? ev.target;
childMenu = children;
// これでもリアクティビティは保たれる
childMenu.value = children;
childShowingItem = item;
}
}
@@ -191,10 +199,15 @@ function focusDown() {
focusNext(document.activeElement);
}
function switchItem(item: MenuSwitch & { ref: any }) {
if (item.disabled) return;
item.ref = !item.ref;
}
onMounted(() => {
if (props.viaKeyboard) {
nextTick(() => {
focusNext(itemsEl.children[0], true, false);
if (itemsEl) focusNext(itemsEl.children[0], true, false);
});
}
@@ -342,6 +355,7 @@ onBeforeUnmount(() => {
}
&.parent {
pointer-events: auto;
display: flex;
align-items: center;
cursor: default;
@@ -357,6 +371,37 @@ onBeforeUnmount(() => {
}
}
.switch {
position: relative;
display: flex;
transition: all 0.2s ease;
user-select: none;
cursor: pointer;
}
.switchDisabled {
cursor: not-allowed;
}
.switchButton {
margin-left: -2px;
}
.switchText {
margin-left: 8px;
margin-top: 2px;
overflow: hidden;
text-overflow: ellipsis;
}
.switchInput {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
.icon {
margin-right: 8px;
}

View File

@@ -408,14 +408,16 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }), ev).then(focus);
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }), menuButton.value, {
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
os.popupMenu(menu, menuButton.value, {
viaKeyboard,
}).then(focus);
}).then(focus).finally(cleanup);
}
async function clip() {

View File

@@ -172,7 +172,6 @@ import { showMovedDialog } from '@/scripts/show-moved-dialog';
const props = defineProps<{
note: misskey.entities.Note;
pinned?: boolean;
}>();
const inChannel = inject('inChannel', null);
@@ -385,14 +384,16 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), ev).then(focus);
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), menuButton.value, {
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
os.popupMenu(menu, menuButton.value, {
viaKeyboard,
}).then(focus);
}).then(focus).finally(cleanup);
}
async function clip() {

View File

@@ -37,7 +37,6 @@ import { userPage } from '@/filters/user';
defineProps<{
note: misskey.entities.Note;
pinned?: boolean;
}>();
</script>

View File

@@ -31,7 +31,6 @@ import { $i } from '@/account';
const props = defineProps<{
note: misskey.entities.Note;
pinned?: boolean;
}>();
const showContent = $ref(false);

View File

@@ -4,13 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModal ref="modal" v-slot="{ type, maxHeight }" :zPriority="'high'" :src="src" :transparentBg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')">
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="modal.close()"/>
<MkModal ref="modal" v-slot="{ type, maxHeight }" :manualShowing="manualShowing" :zPriority="'high'" :src="src" :transparentBg="true" @click="click" @close="onModalClose" @closed="onModalClosed">
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="onMenuClose" @hide="hide"/>
</MkModal>
</template>
<script lang="ts" setup>
import { } from 'vue';
import { ref } from 'vue';
import MkModal from './MkModal.vue';
import MkMenu from './MkMenu.vue';
import { MenuItem } from '@/types/menu';
@@ -29,6 +29,46 @@ const emit = defineEmits<{
}>();
let modal = $shallowRef<InstanceType<typeof MkModal>>();
const manualShowing = ref(true);
const hiding = ref(false);
function click() {
close();
}
function onModalClose() {
emit('closing');
}
function onMenuClose() {
close();
if (hiding.value) {
// hidingであればclosedを発火
emit('closed');
}
}
function onModalClosed() {
if (!hiding.value) {
// hidingでなければclosedを発火
emit('closed');
}
}
function hide() {
manualShowing.value = false;
hiding.value = true;
// closeは呼ぶ必要がある
modal?.close();
}
function close() {
manualShowing.value = false;
// closeは呼ぶ必要がある
modal?.close();
}
</script>
<style lang="scss" module>

View File

@@ -0,0 +1,88 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<span
v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff"
:class="{
[$style.button]: true,
[$style.buttonChecked]: checked,
[$style.buttonDisabled]: props.disabled
}"
data-cy-switch-toggle
@click.prevent.stop="toggle"
>
<div :class="{ [$style.knob]: true, [$style.knobChecked]: checked }"></div>
</span>
</template>
<script lang="ts" setup>
import { toRefs, Ref } from 'vue';
import { i18n } from '@/i18n';
const props = withDefaults(defineProps<{
checked: boolean | Ref<boolean>;
disabled?: boolean;
}>(), {
disabled: false,
});
const emit = defineEmits<{
(ev: 'toggle'): void;
}>();
const checked = toRefs(props).checked;
const toggle = () => {
emit('toggle');
};
</script>
<style lang="scss" module>
.button {
position: relative;
display: inline-flex;
flex-shrink: 0;
margin: 0;
box-sizing: border-box;
width: 32px;
height: 23px;
outline: none;
background: var(--switchOffBg);
background-clip: content-box;
border: solid 1px var(--switchOffBg);
border-radius: 999px;
cursor: pointer;
transition: inherit;
user-select: none;
}
.buttonChecked {
background-color: var(--switchOnBg) !important;
border-color: var(--switchOnBg) !important;
}
.buttonDisabled {
cursor: not-allowed;
}
.knob {
position: absolute;
top: 3px;
width: 15px;
height: 15px;
border-radius: 999px;
transition: all 0.2s ease;
&:not(.knobChecked) {
left: 3px;
background: var(--switchOffFg);
}
}
.knobChecked {
left: 12px;
background: var(--switchOnFg);
}
</style>

View File

@@ -12,12 +12,15 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.input"
@keydown.enter="toggle"
>
<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" :class="$style.button" data-cy-switch-toggle @click.prevent="toggle">
<div :class="$style.knob"></div>
</span>
<XButton :checked="checked" :disabled="disabled" @toggle="toggle"/>
<span :class="$style.body">
<!-- TODO: 無名slotの方は廃止 -->
<span :class="$style.label" @click="toggle"><slot name="label"></slot><slot></slot></span>
<span :class="$style.label">
<span @click="toggle">
<slot name="label"></slot><slot></slot>
</span>
<span v-if="helpText" v-tooltip:dialog="helpText" class="_button _help" :class="$style.help"><i class="ti ti-help-circle"></i></span>
</span>
<p :class="$style.caption"><slot name="caption"></slot></p>
</span>
</div>
@@ -25,26 +28,22 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { toRefs, Ref } from 'vue';
import { i18n } from '@/i18n';
import XButton from '@/components/MkSwitch.button.vue';
const props = defineProps<{
modelValue: boolean | Ref<boolean>;
disabled?: boolean;
helpText?: string;
}>();
const emit = defineEmits<{
(ev: 'update:modelValue', v: boolean): void;
}>();
let button = $shallowRef<HTMLElement>();
const checked = toRefs(props).modelValue;
const toggle = () => {
if (props.disabled) return;
emit('update:modelValue', !checked.value);
if (!checked.value) {
}
};
</script>
@@ -66,17 +65,8 @@ const toggle = () => {
cursor: not-allowed;
}
&.checked {
> .button {
background-color: var(--switchOnBg) !important;
border-color: var(--switchOnBg) !important;
> .knob {
left: 12px;
background: var(--switchOnFg);
}
}
}
//&.checked {
//}
}
.input {
@@ -86,36 +76,6 @@ const toggle = () => {
opacity: 0;
margin: 0;
}
.button {
position: relative;
display: inline-flex;
flex-shrink: 0;
margin: 0;
box-sizing: border-box;
width: 32px;
height: 23px;
outline: none;
background: var(--switchOffBg);
background-clip: content-box;
border: solid 1px var(--switchOffBg);
border-radius: 999px;
cursor: pointer;
transition: inherit;
user-select: none;
}
.knob {
position: absolute;
top: 3px;
left: 3px;
width: 15px;
height: 15px;
background: var(--switchOffFg);
border-radius: 999px;
transition: all 0.2s ease;
}
.body {
margin-left: 12px;
margin-top: 2px;
@@ -140,4 +100,10 @@ const toggle = () => {
display: none;
}
}
.help {
margin-left: 0.5em;
font-size: 85%;
vertical-align: top;
}
</style>

View File

@@ -28,7 +28,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template v-else-if="tweetId && tweetExpanded">
<div ref="twitter">
<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
<iframe
ref="tweet"
allow="fullscreen;web-share"
sandbox="allow-popups allow-scripts allow-same-origin"
scrolling="no"
:style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }"
:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"
></iframe>
</div>
<div :class="$style.action">
<MkButton :small="true" inline @click="tweetExpanded = false">

View File

@@ -0,0 +1,145 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:width="400"
@close="dialog.close()"
@closed="$emit('closed')"
>
<template v-if="announcement" #header>:{{ announcement.title }}:</template>
<template v-else #header>New announcement</template>
<div>
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps_m">
<MkInput v-model="title">
<template #label>{{ i18n.ts.title }}</template>
</MkInput>
<MkTextarea v-model="text">
<template #label>{{ i18n.ts.text }}</template>
</MkTextarea>
<MkRadios v-model="icon">
<template #label>{{ i18n.ts.icon }}</template>
<option value="info"><i class="ti ti-info-circle"></i></option>
<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option>
<option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option>
<option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option>
</MkRadios>
<MkRadios v-model="display">
<template #label>{{ i18n.ts.display }}</template>
<option value="normal">{{ i18n.ts.normal }}</option>
<option value="banner">{{ i18n.ts.banner }}</option>
<option value="dialog">{{ i18n.ts.dialog }}</option>
</MkRadios>
<MkSwitch v-model="needConfirmationToRead">
{{ i18n.ts._announcement.needConfirmationToRead }}
<template #caption>{{ i18n.ts._announcement.needConfirmationToReadDescription }}</template>
</MkSwitch>
<MkButton v-if="announcement" 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.announcement ? i18n.ts.update : i18n.ts.create }}</MkButton>
</div>
</div>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { } from 'vue';
import * as misskey from 'misskey-js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkRadios from '@/components/MkRadios.vue';
const props = defineProps<{
user: misskey.entities.User,
announcement?: any,
}>();
let dialog = $ref(null);
let title: string = $ref(props.announcement ? props.announcement.title : '');
let text: string = $ref(props.announcement ? props.announcement.text : '');
let icon: string = $ref(props.announcement ? props.announcement.icon : 'info');
let display: string = $ref(props.announcement ? props.announcement.display : 'dialog');
let needConfirmationToRead = $ref(props.announcement ? props.announcement.needConfirmationToRead : false);
const emit = defineEmits<{
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
(ev: 'closed'): void
}>();
async function done() {
const params = {
title: title,
text: text,
icon: icon,
imageUrl: null,
display: display,
needConfirmationToRead: needConfirmationToRead,
userId: props.user.id,
};
if (props.announcement) {
await os.apiWithDialog('admin/announcements/update', {
id: props.announcement.id,
...params,
});
emit('done', {
updated: {
id: props.announcement.id,
...params,
},
});
dialog.close();
} else {
const created = await os.apiWithDialog('admin/announcements/create', params);
emit('done', {
created: created,
});
dialog.close();
}
}
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('removeAreYouSure', { x: title }),
});
if (canceled) return;
os.api('admin/announcements/delete', {
id: props.announcement.id,
}).then(() => {
emit('done', {
deleted: true,
});
dialog.close();
});
}
</script>
<style lang="scss" module>
.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>

View File

@@ -86,7 +86,8 @@ let top = $ref(0);
let left = $ref(0);
function showMenu(ev: MouseEvent) {
os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
const { menu, cleanup } = getUserMenu(user);
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
}
onMounted(() => {