Merge remote-tracking branch 'misskey/master' into feature/2024.9.0
This commit is contained in:
commit
f00576bce6
564 changed files with 19993 additions and 8169 deletions
|
|
@ -16,7 +16,7 @@ import { ref } from 'vue';
|
|||
import * as Misskey from 'misskey-js';
|
||||
import MkMention from './MkMention.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { host as localHost } from '@/config.js';
|
||||
import { host as localHost } from '@@/js/config.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
|
||||
const user = ref<Misskey.entities.UserLite>();
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
||||
</template>
|
||||
</MkFolder>
|
||||
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="{ textAlign: c.align, backgroundColor: c.bgColor, color: c.fgColor, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
|
||||
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="containerStyle">
|
||||
<template v-for="child in c.children" :key="child">
|
||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/>
|
||||
</template>
|
||||
|
|
@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Ref, ref } from 'vue';
|
||||
import { Ref, ref, computed } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
|
@ -97,6 +97,29 @@ function g(id) {
|
|||
} as AsUiRoot;
|
||||
}
|
||||
|
||||
const containerStyle = computed(() => {
|
||||
if (c.type !== 'container') return undefined;
|
||||
|
||||
// width, color, styleのうち一つでも指定があれば、枠線がちゃんと表示されるようにwidthとstyleのデフォルト値を設定
|
||||
// radiusは単に角を丸める用途もあるため除外
|
||||
const isBordered = c.borderWidth ?? c.borderColor ?? c.borderStyle;
|
||||
|
||||
const border = isBordered ? {
|
||||
borderWidth: c.borderWidth ?? '1px',
|
||||
borderColor: c.borderColor ?? 'var(--divider)',
|
||||
borderStyle: c.borderStyle ?? 'solid',
|
||||
} : undefined;
|
||||
|
||||
return {
|
||||
textAlign: c.align,
|
||||
backgroundColor: c.bgColor,
|
||||
color: c.fgColor,
|
||||
padding: c.padding ? `${c.padding}px` : 0,
|
||||
borderRadius: (c.borderRadius ?? (c.rounded ? 8 : 0)) + 'px',
|
||||
...border,
|
||||
};
|
||||
});
|
||||
|
||||
const valueForSwitch = ref('default' in c && typeof c.default === 'boolean' ? c.default : false);
|
||||
|
||||
function onSwitchUpdate(v) {
|
||||
|
|
|
|||
|
|
@ -46,17 +46,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts">
|
||||
import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import { emojilist, getEmojiName } from '@@/js/emojilist.js';
|
||||
import contains from '@/scripts/contains.js';
|
||||
import { char2twemojiFilePath, char2fluentEmojiFilePath, char2tossfaceFilePath } from '@/scripts/emoji-base.js';
|
||||
import { char2twemojiFilePath, char2fluentEmojiFilePath, char2tossfaceFilePath } from '@@/js/emoji-base.js';
|
||||
import { acct } from '@/filters/user.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { emojilist, getEmojiName } from '@/scripts/emojilist.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import { MFM_TAGS, MFM_PARAMS } from '@/const.js';
|
||||
import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js';
|
||||
import { searchEmoji, EmojiDef } from '@/scripts/search-emoji.js';
|
||||
|
||||
const lib = emojilist.filter(x => x.category !== 'flags');
|
||||
|
|
|
|||
|
|
@ -171,11 +171,11 @@ function onMousedown(evt: MouseEvent): void {
|
|||
background: var(--accent);
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: var(--X8);
|
||||
background: hsl(from var(--accent) h s calc(l + 5));
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
background: var(--X8);
|
||||
background: hsl(from var(--accent) h s calc(l + 5));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -220,15 +220,16 @@ function onMousedown(evt: MouseEvent): void {
|
|||
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: linear-gradient(90deg, var(--X8), var(--X8));
|
||||
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
background: linear-gradient(90deg, var(--X8), var(--X8));
|
||||
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
font-weight: bold;
|
||||
color: #ff2a2a;
|
||||
|
||||
&.primary {
|
||||
|
|
@ -246,7 +247,7 @@ function onMousedown(evt: MouseEvent): void {
|
|||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ const bannerStyle = computed(() => {
|
|||
left: 0;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
background: linear-gradient(0deg, var(--panel), var(--X15));
|
||||
background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
|
||||
}
|
||||
|
||||
> .name {
|
||||
|
|
|
|||
|
|
@ -13,29 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/* eslint-disable id-denylist --
|
||||
Chart.js has a `data` attribute in most chart definitions, which triggers the
|
||||
id-denylist violation when setting it. This is causing about 60+ lint issues.
|
||||
As this is part of Chart.js's API it makes sense to disable the check here.
|
||||
*/
|
||||
import { onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/scripts/chart-vline.js';
|
||||
import { alpha } from '@/scripts/color.js';
|
||||
import date from '@/filters/date.js';
|
||||
import bytes from '@/filters/bytes.js';
|
||||
import { initChart } from '@/scripts/init-chart.js';
|
||||
import { chartLegend } from '@/scripts/chart-legend.js';
|
||||
import MkChartLegend from '@/components/MkChartLegend.vue';
|
||||
|
||||
initChart();
|
||||
|
||||
type ChartSrc =
|
||||
<script lang="ts">
|
||||
export type ChartSrc =
|
||||
| 'federation'
|
||||
| 'ap-request'
|
||||
| 'users'
|
||||
|
|
@ -62,7 +41,30 @@ type ChartSrc =
|
|||
| 'per-user-pv'
|
||||
| 'per-user-following'
|
||||
| 'per-user-followers'
|
||||
| 'per-user-drive'
|
||||
| 'per-user-drive';
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/* eslint-disable id-denylist --
|
||||
Chart.js has a `data` attribute in most chart definitions, which triggers the
|
||||
id-denylist violation when setting it. This is causing about 60+ lint issues.
|
||||
As this is part of Chart.js's API it makes sense to disable the check here.
|
||||
*/
|
||||
import { onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/scripts/chart-vline.js';
|
||||
import { alpha } from '@/scripts/color.js';
|
||||
import date from '@/filters/date.js';
|
||||
import bytes from '@/filters/bytes.js';
|
||||
import { initChart } from '@/scripts/init-chart.js';
|
||||
import { chartLegend } from '@/scripts/chart-legend.js';
|
||||
import MkChartLegend from '@/components/MkChartLegend.vue';
|
||||
|
||||
initChart();
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
src: ChartSrc;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import * as game from '@/scripts/clicker-game.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div :class="$style.codeBlockRoot">
|
||||
<button :class="$style.codeBlockCopyButton" class="_button" @click="copy">
|
||||
<button v-if="copyButton" :class="$style.codeBlockCopyButton" class="_button" @click="copy">
|
||||
<i class="ti ti-copy"></i>
|
||||
</button>
|
||||
<Suspense>
|
||||
|
|
@ -32,12 +32,17 @@ import { defaultStore } from '@/store.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
code: string;
|
||||
forceShow?: boolean;
|
||||
copyButton?: boolean;
|
||||
lang?: string;
|
||||
}>();
|
||||
}>(), {
|
||||
copyButton: true,
|
||||
forceShow: false,
|
||||
});
|
||||
|
||||
const show = ref(!defaultStore.state.dataSaver.code);
|
||||
const show = ref(props.forceShow === true ? true : !defaultStore.state.dataSaver.code);
|
||||
|
||||
const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));
|
||||
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ onUnmounted(() => {
|
|||
left: 0;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
background: linear-gradient(0deg, var(--panel), var(--X15));
|
||||
background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
|
||||
|
||||
> .fadeLabel {
|
||||
display: inline-block;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue';
|
||||
import MkMenu from './MkMenu.vue';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import contains from '@/scripts/contains.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import * as os from '@/os.js';
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
|
|||
import * as os from '@/os.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { apiUrl } from '@/config.js';
|
||||
import { apiUrl } from '@@/js/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ export default defineComponent({
|
|||
setup(props, { slots, expose }) {
|
||||
const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫
|
||||
|
||||
function getDateText(time: string) {
|
||||
const date = new Date(time).getDate();
|
||||
const month = new Date(time).getMonth() + 1;
|
||||
function getDateText(dateInstance: Date) {
|
||||
const date = dateInstance.getDate();
|
||||
const month = dateInstance.getMonth() + 1;
|
||||
return i18n.tsx.monthAndDay({
|
||||
month: month.toString(),
|
||||
day: date.toString(),
|
||||
|
|
@ -62,9 +62,16 @@ export default defineComponent({
|
|||
})[0];
|
||||
if (el.key == null && item.id) el.key = item.id;
|
||||
|
||||
const date = new Date(item.createdAt);
|
||||
const nextDate = props.items[i + 1] ? new Date(props.items[i + 1].createdAt) : null;
|
||||
|
||||
if (
|
||||
i !== props.items.length - 1 &&
|
||||
new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()
|
||||
nextDate != null && (
|
||||
date.getFullYear() !== nextDate.getFullYear() ||
|
||||
date.getMonth() !== nextDate.getMonth() ||
|
||||
date.getDate() !== nextDate.getDate()
|
||||
)
|
||||
) {
|
||||
const separator = h('div', {
|
||||
class: $style['separator'],
|
||||
|
|
@ -78,12 +85,12 @@ export default defineComponent({
|
|||
h('i', {
|
||||
class: `ti ti-chevron-up ${$style['date-1-icon']}`,
|
||||
}),
|
||||
getDateText(item.createdAt),
|
||||
getDateText(date),
|
||||
]),
|
||||
h('span', {
|
||||
class: $style['date-2'],
|
||||
}, [
|
||||
getDateText(props.items[i + 1].createdAt),
|
||||
getDateText(nextDate),
|
||||
h('i', {
|
||||
class: `ti ti-chevron-down ${$style['date-2-icon']}`,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import { host } from '@/config.js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { defaultStore } from '@/store.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
folder: Misskey.entities.DriveFolder;
|
||||
|
|
|
|||
|
|
@ -634,7 +634,9 @@ function fetchMoreFiles() {
|
|||
}
|
||||
|
||||
function getMenu() {
|
||||
const menu: MenuItem[] = [{
|
||||
const menu: MenuItem[] = [];
|
||||
|
||||
menu.push({
|
||||
type: 'switch',
|
||||
text: i18n.ts.keepOriginalUploading,
|
||||
ref: keepOriginal,
|
||||
|
|
@ -652,19 +654,25 @@ function getMenu() {
|
|||
}, { type: 'divider' }, {
|
||||
text: folder.value ? folder.value.name : i18n.ts.drive,
|
||||
type: 'label',
|
||||
}, folder.value ? {
|
||||
text: i18n.ts.renameFolder,
|
||||
icon: 'ti ti-forms',
|
||||
action: () => { if (folder.value) renameFolder(folder.value); },
|
||||
} : undefined, folder.value ? {
|
||||
text: i18n.ts.deleteFolder,
|
||||
icon: 'ti ti-trash',
|
||||
action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
|
||||
} : undefined, {
|
||||
});
|
||||
|
||||
if (folder.value) {
|
||||
menu.push({
|
||||
text: i18n.ts.renameFolder,
|
||||
icon: 'ti ti-forms',
|
||||
action: () => { if (folder.value) renameFolder(folder.value); },
|
||||
}, {
|
||||
text: i18n.ts.deleteFolder,
|
||||
icon: 'ti ti-trash',
|
||||
action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
|
||||
});
|
||||
}
|
||||
|
||||
menu.push({
|
||||
text: i18n.ts.createFolder,
|
||||
icon: 'ti ti-folder-plus',
|
||||
action: () => { createFolder(); },
|
||||
}];
|
||||
});
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div ref="thumbnail" :class="$style.root">
|
||||
<div
|
||||
ref="thumbnail"
|
||||
:class="[
|
||||
$style.root,
|
||||
{ [$style.sensitiveHighlight]: highlightWhenSensitive && file.isSensitive },
|
||||
]"
|
||||
>
|
||||
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/>
|
||||
<i v-else-if="is === 'image'" class="ti ti-photo" :class="$style.icon"></i>
|
||||
<i v-else-if="is === 'video'" class="ti ti-video" :class="$style.icon"></i>
|
||||
|
|
@ -27,6 +33,7 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
|||
const props = defineProps<{
|
||||
file: Misskey.entities.DriveFile;
|
||||
fit: 'cover' | 'contain';
|
||||
highlightWhenSensitive?: boolean;
|
||||
}>();
|
||||
|
||||
const is = computed(() => {
|
||||
|
|
@ -67,6 +74,18 @@ const isThumbnailAvailable = computed(() => {
|
|||
overflow: clip;
|
||||
}
|
||||
|
||||
.sensitiveHighlight::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
box-shadow: inset 0 0 0 4px var(--warn);
|
||||
}
|
||||
|
||||
.iconSub {
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
|
|
|
|||
414
packages/frontend/src/components/MkEmbedCodeGenDialog.vue
Normal file
414
packages/frontend/src/components/MkEmbedCodeGenDialog.vue
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialogEl"
|
||||
:width="1000"
|
||||
:height="600"
|
||||
:scroll="false"
|
||||
:withOkButton="false"
|
||||
@close="cancel()"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template>
|
||||
|
||||
<div :class="$style.embedCodeGenRoot">
|
||||
<Transition
|
||||
mode="out-in"
|
||||
:enterActiveClass="$style.transition_x_enterActive"
|
||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||
:enterFromClass="$style.transition_x_enterFrom"
|
||||
:leaveToClass="$style.transition_x_leaveTo"
|
||||
>
|
||||
<div v-if="phase === 'input'" key="input" :class="$style.embedCodeGenInputRoot">
|
||||
<div
|
||||
:class="$style.embedCodeGenPreviewRoot"
|
||||
>
|
||||
<MkLoading v-if="iframeLoading" :class="$style.embedCodeGenPreviewSpinner"/>
|
||||
<div :class="$style.embedCodeGenPreviewWrapper">
|
||||
<div class="_acrylic" :class="$style.embedCodeGenPreviewTitle">{{ i18n.ts.preview }}</div>
|
||||
<div ref="resizerRootEl" :class="$style.embedCodeGenPreviewResizerRoot" inert>
|
||||
<div
|
||||
:class="$style.embedCodeGenPreviewResizer"
|
||||
:style="{ transform: iframeStyle }"
|
||||
>
|
||||
<iframe
|
||||
ref="iframeEl"
|
||||
:src="embedPreviewUrl"
|
||||
:class="$style.embedCodeGenPreviewIframe"
|
||||
:style="{ height: `${iframeHeight}px` }"
|
||||
@load="iframeOnLoad"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.embedCodeGenSettings" class="_gaps">
|
||||
<MkInput v-if="isEmbedWithScrollbar" v-model="maxHeight" type="number" :min="0">
|
||||
<template #label>{{ i18n.ts._embedCodeGen.maxHeight }}</template>
|
||||
<template #suffix>px</template>
|
||||
<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-model="colorMode">
|
||||
<template #label>{{ i18n.ts.theme }}</template>
|
||||
<option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option>
|
||||
<option value="light">{{ i18n.ts.light }}</option>
|
||||
<option value="dark">{{ i18n.ts.dark }}</option>
|
||||
</MkSelect>
|
||||
<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
|
||||
<MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
|
||||
<MkSwitch v-model="border">{{ i18n.ts._embedCodeGen.border }}</MkSwitch>
|
||||
<MkInfo v-if="isEmbedWithScrollbar && (!maxHeight || maxHeight <= 0)" warn>{{ i18n.ts._embedCodeGen.maxHeightWarn }}</MkInfo>
|
||||
<MkInfo v-if="typeof maxHeight === 'number' && (maxHeight <= 0 || maxHeight > 700)">{{ i18n.ts._embedCodeGen.previewIsNotActual }}</MkInfo>
|
||||
<div class="_buttons">
|
||||
<MkButton :disabled="iframeLoading" @click="applyToPreview">{{ i18n.ts._embedCodeGen.applyToPreview }}</MkButton>
|
||||
<MkButton :disabled="iframeLoading" primary @click="generate">{{ i18n.ts._embedCodeGen.generateCode }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="phase === 'result'" key="result" :class="$style.embedCodeGenResultRoot">
|
||||
<div :class="$style.embedCodeGenResultWrapper" class="_gaps">
|
||||
<div class="_gaps_s">
|
||||
<div :class="$style.embedCodeGenResultHeadingIcon"><i class="ti ti-check"></i></div>
|
||||
<div :class="$style.embedCodeGenResultHeading">{{ i18n.ts._embedCodeGen.codeGenerated }}</div>
|
||||
<div :class="$style.embedCodeGenResultDescription">{{ i18n.ts._embedCodeGen.codeGeneratedDescription }}</div>
|
||||
</div>
|
||||
<div class="_gaps_s">
|
||||
<MkCode :code="result" lang="html" :forceShow="true" :copyButton="false" :class="$style.embedCodeGenResultCode"/>
|
||||
<MkButton :class="$style.embedCodeGenResultButtons" rounded primary @click="doCopy"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
|
||||
</div>
|
||||
<MkButton :class="$style.embedCodeGenResultButtons" rounded transparent @click="close">{{ i18n.ts.close }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { shallowRef, ref, computed, nextTick, onMounted, onDeactivated, onUnmounted } from 'vue';
|
||||
import { url } from '@@/js/config.js';
|
||||
import { embedRouteWithScrollbar } from '@@/js/embed-page.js';
|
||||
import type { EmbeddableEntity, EmbedParams } from '@@/js/embed-page.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
import MkCode from '@/components/MkCode.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { normalizeEmbedParams, getEmbedCode } from '@/scripts/get-embed-code.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'ok'): void;
|
||||
(ev: 'cancel'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
entity: EmbeddableEntity;
|
||||
id: string;
|
||||
params?: EmbedParams;
|
||||
}>();
|
||||
|
||||
//#region Modalの制御
|
||||
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
function cancel() {
|
||||
emit('cancel');
|
||||
dialogEl.value?.close();
|
||||
}
|
||||
|
||||
function close() {
|
||||
dialogEl.value?.close();
|
||||
}
|
||||
|
||||
const phase = ref<'input' | 'result'>('input');
|
||||
//#endregion
|
||||
|
||||
//#region 埋め込みURL生成・カスタマイズ
|
||||
|
||||
// 本URL生成用params
|
||||
const paramsForUrl = computed<EmbedParams>(() => ({
|
||||
header: header.value,
|
||||
maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined,
|
||||
colorMode: colorMode.value === 'auto' ? undefined : colorMode.value,
|
||||
rounded: rounded.value,
|
||||
border: border.value,
|
||||
}));
|
||||
|
||||
// プレビュー用params(手動で更新を掛けるのでref)
|
||||
const paramsForPreview = ref<EmbedParams>(props.params ?? {});
|
||||
|
||||
const embedPreviewUrl = computed(() => {
|
||||
const paramClass = new URLSearchParams(normalizeEmbedParams(paramsForPreview.value));
|
||||
if (paramClass.has('maxHeight')) {
|
||||
const maxHeight = parseInt(paramClass.get('maxHeight')!);
|
||||
paramClass.set('maxHeight', maxHeight === 0 ? '500' : Math.min(maxHeight, 700).toString()); // プレビューであまりにも縮小されると見づらいため、700pxまでに制限
|
||||
}
|
||||
return `${url}/embed/${props.entity}/${props.id}${paramClass.toString() ? '?' + paramClass.toString() : ''}`;
|
||||
});
|
||||
|
||||
const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(props.entity));
|
||||
const header = ref(props.params?.header ?? true);
|
||||
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? undefined : 500);
|
||||
|
||||
const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto');
|
||||
const rounded = ref(props.params?.rounded ?? true);
|
||||
const border = ref(props.params?.border ?? true);
|
||||
|
||||
function applyToPreview() {
|
||||
const currentPreviewUrl = embedPreviewUrl.value;
|
||||
|
||||
paramsForPreview.value = {
|
||||
header: header.value,
|
||||
maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined,
|
||||
colorMode: colorMode.value === 'auto' ? undefined : colorMode.value,
|
||||
rounded: rounded.value,
|
||||
border: border.value,
|
||||
};
|
||||
|
||||
nextTick(() => {
|
||||
if (currentPreviewUrl === embedPreviewUrl.value) {
|
||||
// URLが変わらなくてもリロード
|
||||
iframeEl.value?.contentWindow?.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const result = ref('');
|
||||
|
||||
function generate() {
|
||||
result.value = getEmbedCode(`/embed/${props.entity}/${props.id}`, paramsForUrl.value);
|
||||
phase.value = 'result';
|
||||
}
|
||||
|
||||
function doCopy() {
|
||||
copyToClipboard(result.value);
|
||||
os.success();
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region プレビューのリサイズ
|
||||
const resizerRootEl = shallowRef<HTMLDivElement>();
|
||||
const iframeLoading = ref(true);
|
||||
const iframeEl = shallowRef<HTMLIFrameElement>();
|
||||
const iframeHeight = ref(0);
|
||||
const iframeScale = ref(1);
|
||||
const iframeStyle = computed(() => {
|
||||
return `translate(-50%, -50%) scale(${iframeScale.value})`;
|
||||
});
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
calcScale();
|
||||
});
|
||||
|
||||
function iframeOnLoad() {
|
||||
iframeEl.value?.contentWindow?.addEventListener('beforeunload', () => {
|
||||
iframeLoading.value = true;
|
||||
nextTick(() => {
|
||||
iframeHeight.value = 0;
|
||||
iframeScale.value = 1;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function windowEventHandler(event: MessageEvent) {
|
||||
if (event.source !== iframeEl.value?.contentWindow) {
|
||||
return;
|
||||
}
|
||||
if (event.data.type === 'misskey:embed:ready') {
|
||||
iframeEl.value!.contentWindow?.postMessage({
|
||||
type: 'misskey:embedParent:registerIframeId',
|
||||
payload: {
|
||||
iframeId: 'embedCodeGen', // 同じタイミングで複数のembed iframeがある際の区別用なのでここではなんでもいい
|
||||
},
|
||||
});
|
||||
}
|
||||
if (event.data.type === 'misskey:embed:changeHeight') {
|
||||
iframeHeight.value = event.data.payload.height;
|
||||
nextTick(() => {
|
||||
calcScale();
|
||||
iframeLoading.value = false; // 初回の高さ変更まで待つ
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function calcScale() {
|
||||
if (!resizerRootEl.value) return;
|
||||
const previewWidth = resizerRootEl.value.clientWidth - 40; // 左右の余白 20pxずつ
|
||||
const previewHeight = resizerRootEl.value.clientHeight - 40; // 上下の余白 20pxずつ
|
||||
const iframeWidth = 500;
|
||||
const scale = Math.min(previewWidth / iframeWidth, previewHeight / iframeHeight.value, 1); // 拡大はしないので1を上限に
|
||||
iframeScale.value = scale;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', windowEventHandler);
|
||||
if (!resizerRootEl.value) return;
|
||||
resizeObserver.observe(resizerRootEl.value);
|
||||
});
|
||||
|
||||
function reset() {
|
||||
window.removeEventListener('message', windowEventHandler);
|
||||
resizeObserver.disconnect();
|
||||
|
||||
// プレビューのリセット
|
||||
iframeHeight.value = 0;
|
||||
iframeScale.value = 1;
|
||||
iframeLoading.value = true;
|
||||
result.value = '';
|
||||
phase.value = 'input';
|
||||
}
|
||||
|
||||
onDeactivated(() => {
|
||||
reset();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
reset();
|
||||
});
|
||||
//#endregion
|
||||
</script>
|
||||
|
||||
<style module>
|
||||
.transition_x_enterActive,
|
||||
.transition_x_leaveActive {
|
||||
transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
|
||||
}
|
||||
.transition_x_enterFrom {
|
||||
opacity: 0;
|
||||
transform: translateX(50px);
|
||||
}
|
||||
.transition_x_leaveTo {
|
||||
opacity: 0;
|
||||
transform: translateX(-50px);
|
||||
}
|
||||
|
||||
.embedCodeGenRoot {
|
||||
container-type: inline-size;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.embedCodeGenInputRoot {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 400px;
|
||||
}
|
||||
|
||||
.embedCodeGenPreviewRoot {
|
||||
position: relative;
|
||||
background-color: var(--bg);
|
||||
background-size: auto auto;
|
||||
background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--panel) 6px, var(--panel) 12px);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.embedCodeGenPreviewWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
.embedCodeGenPreviewTitle {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
.embedCodeGenPreviewSpinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
.embedCodeGenPreviewResizerRoot {
|
||||
position: relative;
|
||||
flex: 1 0;
|
||||
}
|
||||
|
||||
.embedCodeGenPreviewResizer {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.embedCodeGenPreviewIframe {
|
||||
display: block;
|
||||
border: none;
|
||||
width: 500px;
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
.embedCodeGenSettings {
|
||||
padding: 24px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.embedCodeGenResultRoot {
|
||||
box-sizing: border-box;
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.embedCodeGenResultHeading {
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.embedCodeGenResultHeadingIcon {
|
||||
margin: 0 auto;
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
text-align: center;
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
font-size: 24px;
|
||||
line-height: 64px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.embedCodeGenResultDescription {
|
||||
text-align: center;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.embedCodeGenResultWrapper,
|
||||
.embedCodeGenResultCode {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.embedCodeGenResultButtons {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@container (max-width: 800px) {
|
||||
.embedCodeGenInputRoot {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, Ref } from 'vue';
|
||||
import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js';
|
||||
import { CustomEmojiFolderTree, getEmojiName } from '@@/js/emojilist.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
|
||||
|
|
|
|||
|
|
@ -117,7 +117,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, computed, watch, onMounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XSection from '@/components/MkEmojiPicker.section.vue';
|
||||
import {
|
||||
emojilist,
|
||||
emojiCharByCategory,
|
||||
|
|
@ -126,7 +125,8 @@ import {
|
|||
getEmojiName,
|
||||
CustomEmojiFolderTree,
|
||||
getUnicodeEmoji,
|
||||
} from '@/scripts/emojilist.js';
|
||||
} from '@@/js/emojilist.js';
|
||||
import XSection from '@/components/MkEmojiPicker.section.vue';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { isTouchUsing } from '@/scripts/touch.js';
|
||||
|
|
@ -611,6 +611,7 @@ defineExpose({
|
|||
width: auto;
|
||||
height: auto;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
|
|
@ -717,7 +718,7 @@ defineExpose({
|
|||
|
||||
> .item {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
padding: 0 3px;
|
||||
width: var(--eachSize);
|
||||
height: var(--eachSize);
|
||||
contain: strict;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
ref="modal"
|
||||
v-slot="{ type, maxHeight }"
|
||||
:zPriority="'middle'"
|
||||
:preferType="defaultStore.state.emojiPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
|
||||
:preferType="defaultStore.state.emojiPickerStyle"
|
||||
:hasInteractionWithOtherFocusTrappedEls="true"
|
||||
:transparentBg="true"
|
||||
:manualShowing="manualShowing"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
class="file _button"
|
||||
>
|
||||
<div v-if="file.isSensitive" class="sensitive-label">{{ i18n.ts.sensitive }}</div>
|
||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain" :highlightWhenSensitive="true"/>
|
||||
<div v-if="viewMode === 'list'" class="body">
|
||||
<div>
|
||||
<small style="opacity: 0.7;">{{ file.name }}</small>
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSpacer :marginMin="14" :marginMax="22">
|
||||
<slot></slot>
|
||||
</MkSpacer>
|
||||
<div v-if="$slots.footer" :class="$style.footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</KeepAlive>
|
||||
</Transition>
|
||||
|
|
@ -136,7 +139,7 @@ onMounted(() => {
|
|||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 9px 12px 9px 12px;
|
||||
background: var(--buttonBg);
|
||||
background: var(--folderHeaderBg);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
border-radius: var(--radius-sm);
|
||||
|
|
@ -144,7 +147,7 @@ onMounted(() => {
|
|||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background: var(--buttonHoverBg);
|
||||
background: var(--folderHeaderHoverBg);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
|
|
@ -153,7 +156,7 @@ onMounted(() => {
|
|||
|
||||
&.active {
|
||||
color: var(--accent);
|
||||
background: var(--buttonHoverBg);
|
||||
background: var(--folderHeaderHoverBg);
|
||||
}
|
||||
|
||||
&.opened {
|
||||
|
|
@ -224,4 +227,18 @@ onMounted(() => {
|
|||
background: var(--bg);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky !important;
|
||||
z-index: 1;
|
||||
bottom: var(--stickyBottom, 0px);
|
||||
left: 0;
|
||||
padding: 12px;
|
||||
background: var(--acrylicBg);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
background-size: auto auto;
|
||||
background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--panel) 5px, var(--panel) 10px);
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
|
||||
</template>
|
||||
<template v-else-if="isFollowing">
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.youFollowing }}</span><i class="ti ti-minus"></i>
|
||||
</template>
|
||||
<template v-else-if="!isFollowing && user.isLocked">
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
|
||||
|
|
@ -43,7 +43,7 @@ import { useStream } from '@/stream.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { host } from '@/config.js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
|
|
|
|||
49
packages/frontend/src/components/MkFormFooter.vue
Normal file
49
packages/frontend/src/components/MkFormFooter.vue
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.text">{{ i18n.tsx.thereAreNChanges({ n: form.modifiedCount.value }) }}</div>
|
||||
<div style="margin-left: auto;" class="_buttons">
|
||||
<MkButton danger rounded @click="form.discard"><i class="ti ti-x"></i> {{ i18n.ts.discard }}</MkButton>
|
||||
<MkButton primary rounded @click="form.save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkButton from './MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
form: {
|
||||
modifiedCount: {
|
||||
value: number;
|
||||
};
|
||||
discard: () => void;
|
||||
save: () => void;
|
||||
};
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--warn);
|
||||
font-size: 90%;
|
||||
animation: modified-blink 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes modified-blink {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
|
|
@ -23,8 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts">
|
||||
import DrawBlurhash from '@/workers/draw-blurhash?worker';
|
||||
import TestWebGL2 from '@/workers/test-webgl2?worker';
|
||||
import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch.js';
|
||||
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js';
|
||||
import { WorkerMultiDispatch } from '@@/js/worker-multi-dispatch.js';
|
||||
import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
|
||||
|
||||
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
|
||||
// テスト環境で Web Worker インスタンスは作成できない
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { instanceName } from '@/config.js';
|
||||
import { instanceName } from '@@/js/config.js';
|
||||
import { instance as Instance } from '@/instance.js';
|
||||
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else-if="isExpired" style="color: var(--error)">{{ i18n.ts.expired }}</span>
|
||||
<span v-else style="color: var(--success)">{{ i18n.ts.unused }}</span>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="_buttons">
|
||||
<MkButton v-if="!invite.used && !isExpired" primary rounded @click="copyInviteCode()"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
|
||||
<MkButton v-if="!invite.used || moderator" danger rounded @click="deleteCode()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="_gaps_s" :class="$style.root">
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.items">
|
||||
<div>
|
||||
<div :class="$style.label">{{ i18n.ts.invitationCode }}</div>
|
||||
|
|
@ -49,10 +55,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div><MkTime :time="invite.createdAt" mode="absolute"/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.buttons">
|
||||
<MkButton v-if="!invite.used && !isExpired" primary rounded @click="copyInviteCode()"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
|
||||
<MkButton v-if="!invite.used || moderator" danger rounded @click="deleteCode()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</template>
|
||||
|
|
@ -121,9 +123,4 @@ function copyInviteCode() {
|
|||
width: var(--height);
|
||||
height: var(--height);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import { url as local } from '@/config.js';
|
||||
import { url as local } from '@@/js/config.js';
|
||||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||
import * as os from '@/os.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
|
|
|
|||
|
|
@ -175,9 +175,7 @@ async function show() {
|
|||
const menuShowing = ref(false);
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
let menu: MenuItem[] = [];
|
||||
|
||||
menu = [
|
||||
const menu: MenuItem[] = [
|
||||
// TODO: 再生キューに追加
|
||||
{
|
||||
type: 'switch',
|
||||
|
|
@ -225,7 +223,7 @@ function showMenu(ev: MouseEvent) {
|
|||
menu.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
type: 'link' as const,
|
||||
type: 'link',
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-info-circle',
|
||||
to: `/my/drive/file/${props.audio.id}`,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive]" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click="onclick">
|
||||
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive]" @click="onclick">
|
||||
<component
|
||||
:is="disableImageLink ? 'div' : 'a'"
|
||||
v-bind="disableImageLink ? {
|
||||
|
|
@ -54,6 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { watch, ref, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
import bytes from '@/filters/bytes.js';
|
||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||
|
|
@ -75,7 +76,6 @@ const props = withDefaults(defineProps<{
|
|||
});
|
||||
|
||||
const hide = ref(true);
|
||||
const darkMode = ref<boolean>(defaultStore.state.darkMode);
|
||||
|
||||
const url = computed(() => (props.raw || defaultStore.state.loadRawImages)
|
||||
? props.image.url
|
||||
|
|
@ -112,27 +112,39 @@ watch(() => props.image, () => {
|
|||
});
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
text: i18n.ts.hide,
|
||||
icon: 'ti ti-eye-off',
|
||||
action: () => {
|
||||
hide.value = true;
|
||||
},
|
||||
}, ...(iAmModerator ? [{
|
||||
text: i18n.ts.markAsSensitive,
|
||||
icon: 'ti ti-eye-exclamation',
|
||||
danger: true,
|
||||
action: () => {
|
||||
os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
|
||||
},
|
||||
}] : []), ...($i?.id === props.image.userId ? [{
|
||||
type: 'divider' as const,
|
||||
}, {
|
||||
type: 'link' as const,
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-info-circle',
|
||||
to: `/my/drive/file/${props.image.id}`,
|
||||
}] : [])], ev.currentTarget ?? ev.target);
|
||||
});
|
||||
|
||||
if (iAmModerator) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.markAsSensitive,
|
||||
icon: 'ti ti-eye-exclamation',
|
||||
danger: true,
|
||||
action: () => {
|
||||
os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if ($i?.id === props.image.userId) {
|
||||
menuItems.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
type: 'link',
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-info-circle',
|
||||
to: `/my/drive/file/${props.image.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
@ -197,10 +209,19 @@ function showMenu(ev: MouseEvent) {
|
|||
position: relative;
|
||||
//box-shadow: 0 0 0 1px var(--divider) inset;
|
||||
background: var(--bg);
|
||||
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
html[data-color-scheme=dark] .visible {
|
||||
--c: rgb(255 255 255 / 2%);
|
||||
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
|
||||
}
|
||||
|
||||
html[data-color-scheme=light] .visible {
|
||||
--c: rgb(0 0 0 / 2%);
|
||||
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ import XImage from '@/components/MkMediaImage.vue';
|
|||
import XVideo from '@/components/MkMediaVideo.vue';
|
||||
import XModPlayer from '@/components/SkModPlayer.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES } from '@/const.js';
|
||||
import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES } from '@@/js/const.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { focusParent } from '@/scripts/focus.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -195,9 +195,7 @@ async function show() {
|
|||
const menuShowing = ref(false);
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
let menu: MenuItem[] = [];
|
||||
|
||||
menu = [
|
||||
const menu: MenuItem[] = [
|
||||
// TODO: 再生キューに追加
|
||||
{
|
||||
type: 'switch',
|
||||
|
|
@ -250,7 +248,7 @@ function showMenu(ev: MouseEvent) {
|
|||
menu.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
type: 'link' as const,
|
||||
type: 'link',
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-info-circle',
|
||||
to: `/my/drive/file/${props.video.id}`,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { toUnicode } from 'punycode';
|
||||
import { computed } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { host as localHost } from '@/config.js';
|
||||
import { host as localHost } from '@@/js/config.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, onUnmounted, provide, shallowRef, watch } from 'vue';
|
||||
import MkMenu from './MkMenu.vue';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const props = defineProps<{
|
||||
items: MenuItem[];
|
||||
|
|
|
|||
|
|
@ -4,17 +4,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div role="menu" @focusin.passive.stop="() => {}">
|
||||
<div
|
||||
role="menu"
|
||||
:class="{
|
||||
[$style.root]: true,
|
||||
[$style.center]: align === 'center',
|
||||
[$style.big]: big,
|
||||
[$style.asDrawer]: asDrawer,
|
||||
}"
|
||||
@focusin.passive.stop="() => {}"
|
||||
>
|
||||
<div
|
||||
ref="itemsEl"
|
||||
v-hotkey="keymap"
|
||||
tabindex="0"
|
||||
class="_popup _shadow"
|
||||
:class="{
|
||||
[$style.root]: true,
|
||||
[$style.center]: align === 'center',
|
||||
[$style.asDrawer]: asDrawer,
|
||||
}"
|
||||
:class="$style.menu"
|
||||
:style="{
|
||||
width: (width && !asDrawer) ? `${width}px` : '',
|
||||
maxHeight: maxHeight ? `min(${maxHeight}px, calc(100dvh - 32px))` : 'calc(100dvh - 32px)',
|
||||
|
|
@ -200,6 +205,8 @@ const emit = defineEmits<{
|
|||
(ev: 'hide'): void;
|
||||
}>();
|
||||
|
||||
const big = isTouchUsing;
|
||||
|
||||
const isNestingMenu = inject<boolean>('isNestingMenu', false);
|
||||
|
||||
const itemsEl = shallowRef<HTMLElement>();
|
||||
|
|
@ -297,6 +304,8 @@ async function showRadioOptions(item: MenuRadio, ev: Event) {
|
|||
}
|
||||
|
||||
async function showChildren(item: MenuParent, ev: Event) {
|
||||
ev.stopPropagation();
|
||||
|
||||
const children: MenuItem[] = await (async () => {
|
||||
if (childrenCache.has(item)) {
|
||||
return childrenCache.get(item)!;
|
||||
|
|
@ -418,6 +427,58 @@ onBeforeUnmount(() => {
|
|||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
&.center {
|
||||
> .menu {
|
||||
> .item {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.big:not(.asDrawer) {
|
||||
> .menu {
|
||||
> .item {
|
||||
padding: 6px 20px;
|
||||
font-size: 1em;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.asDrawer {
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
|
||||
> .menu {
|
||||
padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0;
|
||||
width: 100%;
|
||||
border-radius: var(--radius-lg);
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
|
||||
> .item {
|
||||
font-size: 1em;
|
||||
padding: 12px 24px;
|
||||
|
||||
&::before {
|
||||
width: calc(100% - 24px);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
> .icon {
|
||||
margin-right: 14px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
> .divider {
|
||||
margin: 12px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
padding: 8px 0;
|
||||
box-sizing: border-box;
|
||||
max-width: 100vw;
|
||||
|
|
@ -428,39 +489,6 @@ onBeforeUnmount(() => {
|
|||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&.center {
|
||||
> .item {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&.asDrawer {
|
||||
padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0;
|
||||
width: 100%;
|
||||
border-radius: var(--radius-lg);
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
|
||||
> .item {
|
||||
font-size: 1em;
|
||||
padding: 12px 24px;
|
||||
|
||||
&::before {
|
||||
width: calc(100% - 24px);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
> .icon {
|
||||
margin-right: 14px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
> .divider {
|
||||
margin: 12px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { watch, ref } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
|
||||
const props = defineProps<{
|
||||
src: number[];
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ const zIndex = os.claimZIndex(props.zPriority);
|
|||
const useSendAnime = ref(false);
|
||||
const type = computed<ModalTypes>(() => {
|
||||
if (props.preferType === 'auto') {
|
||||
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
|
||||
if ((defaultStore.state.menuStyle === 'drawer') || (defaultStore.state.menuStyle === 'auto' && isTouchUsing && deviceKind === 'smartphone')) {
|
||||
return 'drawer';
|
||||
} else {
|
||||
return props.src != null ? 'popup' : 'dialog';
|
||||
|
|
|
|||
|
|
@ -94,12 +94,12 @@ defineExpose({
|
|||
|
||||
--root-margin: 24px;
|
||||
|
||||
--headerHeight: 46px;
|
||||
--headerHeightNarrow: 42px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
--root-margin: 16px;
|
||||
}
|
||||
|
||||
--headerHeight: 46px;
|
||||
--headerHeightNarrow: 42px;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div style="container-type: inline-size;">
|
||||
<bdi>
|
||||
<p v-if="appearNote.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :isBlock="true" :author="appearNote.user" :nyaize="'respect'"/>
|
||||
<Mfm
|
||||
v-if="appearNote.cw != ''"
|
||||
:text="appearNote.cw"
|
||||
:author="appearNote.user"
|
||||
:nyaize="'respect'"
|
||||
:enableEmojiMenu="true"
|
||||
:enableEmojiMenuReaction="true"
|
||||
:isBlock="true"
|
||||
/>
|
||||
<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
|
||||
|
|
@ -148,7 +156,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i class="ph-heart ph-bold ph-lg"></i>
|
||||
</button>
|
||||
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()" @click.stop>
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i>
|
||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||
<i v-else class="ph-smiley ph-bold ph-lg"></i>
|
||||
|
|
@ -192,6 +200,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue';
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||
import MkNoteHeader from '@/components/MkNoteHeader.vue';
|
||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
|
|
@ -224,13 +233,13 @@ import { deepClone } from '@/scripts/clone.js';
|
|||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { getNoteSummary } from '@/scripts/get-note-summary.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
import { shouldCollapsed } from '@/scripts/collapsed.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
|
||||
import { host } from '@/config.js';
|
||||
import { shouldCollapsed } from '@@/js/collapsed.js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||
|
|
@ -744,16 +753,6 @@ function onContextmenu(ev: MouseEvent): void {
|
|||
return;
|
||||
}
|
||||
|
||||
const isLink = (el: HTMLElement): boolean => {
|
||||
if (el.tagName === 'A') return true;
|
||||
// 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。
|
||||
if (el.tagName === 'AUDIO') return true;
|
||||
if (el.parentElement) {
|
||||
return isLink(el.parentElement);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (ev.target && isLink(ev.target as HTMLElement)) return;
|
||||
if (window.getSelection()?.toString() !== '') return;
|
||||
|
||||
|
|
@ -884,7 +883,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
// 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
|
||||
// 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?)
|
||||
//content-visibility: auto;
|
||||
//contain-intrinsic-size: 0 128px;
|
||||
//contain-intrinsic-size: 0 128px;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
|
|
@ -1128,7 +1127,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
//background: linear-gradient(0deg, var(--panel), var(--X15));
|
||||
//background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
|
||||
|
||||
&:hover > .collapsedLabel {
|
||||
background: var(--panelHighlight);
|
||||
|
|
|
|||
|
|
@ -69,7 +69,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</header>
|
||||
<div :class="$style.noteContent">
|
||||
<p v-if="appearNote.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :isBlock="true" :author="appearNote.user" :nyaize="'respect'"/>
|
||||
<Mfm
|
||||
v-if="appearNote.cw != ''"
|
||||
:text="appearNote.cw"
|
||||
:author="appearNote.user"
|
||||
:nyaize="'respect'"
|
||||
:enableEmojiMenu="true"
|
||||
:enableEmojiMenuReaction="true"
|
||||
:isBlock="true"
|
||||
/>
|
||||
<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll"/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent">
|
||||
|
|
@ -149,7 +157,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i class="ph-heart ph-bold ph-lg"></i>
|
||||
</button>
|
||||
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i>
|
||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||
<i v-else class="ph-smiley ph-bold ph-lg"></i>
|
||||
|
|
@ -227,6 +235,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed, inject, onMounted, provide, ref, shallowRef, watch } from 'vue';
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
|
||||
|
|
@ -250,9 +259,10 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
|
|||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { host } from '@/config.js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js';
|
||||
import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js';
|
||||
|
||||
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||
|
|
@ -703,14 +713,6 @@ function toggleReact() {
|
|||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
const isLink = (el: HTMLElement): boolean => {
|
||||
if (el.tagName === 'A') return true;
|
||||
if (el.parentElement) {
|
||||
return isLink(el.parentElement);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (ev.target && isLink(ev.target as HTMLElement)) return;
|
||||
if (window.getSelection()?.toString() !== '') return;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<header :class="$style.root">
|
||||
<div v-if="mock" :class="$style.name">
|
||||
<MkUserName :user="note.user"/>
|
||||
</div>
|
||||
<MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
|
||||
<div :class="$style.username"><MkAcct :user="note.user"/></div>
|
||||
<component :is="defaultStore.state.enableCondensedLine ? 'MkCondensedLine' : 'div'" :minScale="0.5" style="min-width: 0;">
|
||||
<div style="display: flex; white-space: nowrap; align-items: baseline;">
|
||||
<div v-if="mock" :class="$style.name">
|
||||
<MkUserName :user="note.user"/>
|
||||
</div>
|
||||
<MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
|
||||
<div :class="$style.username"><MkAcct :user="note.user"/></div>
|
||||
</div>
|
||||
</component>
|
||||
<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!"/>
|
||||
</div>
|
||||
|
|
@ -43,6 +47,7 @@ import { notePage } from '@/filters/note.js';
|
|||
import { userPage } from '@/filters/user.js';
|
||||
import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js';
|
||||
import { popupMenu } from '@/os.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const props = defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
||||
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
|
||||
<MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/>
|
||||
<img v-else-if="'icon' in notification" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
|
||||
<MkAvatar v-else-if="notification.type === 'exportCompleted'" :class="$style.icon" :user="$i" link preview/>
|
||||
<img v-else-if="'icon' in notification && notification.icon != null" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
|
||||
<div
|
||||
:class="[$style.subIcon, {
|
||||
[$style.t_follow]: notification.type === 'follow',
|
||||
|
|
@ -25,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
[$style.t_quote]: notification.type === 'quote',
|
||||
[$style.t_pollEnded]: notification.type === 'pollEnded',
|
||||
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
|
||||
[$style.t_exportCompleted]: notification.type === 'exportCompleted',
|
||||
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
|
||||
[$style.t_pollEnded]: notification.type === 'edited',
|
||||
}]"
|
||||
|
|
@ -38,6 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
||||
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
||||
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
||||
<i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i>
|
||||
<template v-else-if="notification.type === 'roleAssigned'">
|
||||
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
|
||||
<i v-else class="ti ti-badges"></i>
|
||||
|
|
@ -49,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:withTooltip="true"
|
||||
:reaction="notification.reaction.replace(/^:(\w+):$/, ':$1@.:')"
|
||||
:noStyle="true"
|
||||
style="width: 100%; height: 100%;"
|
||||
style="width: 100%; height: 100% !important; object-fit: contain;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -60,6 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
|
||||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
||||
<span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span>
|
||||
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
||||
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
||||
|
|
@ -102,10 +106,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
|
||||
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
|
||||
</MkA>
|
||||
<MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`">
|
||||
{{ i18n.ts.showFile }}
|
||||
</MkA>
|
||||
<template v-else-if="notification.type === 'follow'">
|
||||
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
||||
</template>
|
||||
<span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
|
||||
<template v-else-if="notification.type === 'followRequestAccepted'">
|
||||
<div :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</div>
|
||||
<div v-if="notification.message" :class="$style.text" style="opacity: 0.6; font-style: oblique;">
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
<span>{{ notification.message }}</span>
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="notification.type === 'receiveFollowRequest'">
|
||||
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span>
|
||||
<div v-if="full && !followRequestDone" :class="$style.followRequestCommands">
|
||||
|
|
@ -126,7 +140,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:withTooltip="true"
|
||||
:reaction="reaction.reaction.replace(/^:(\w+):$/, ':$1@.:')"
|
||||
:noStyle="true"
|
||||
style="width: 100%; height: 100%;"
|
||||
style="width: 100%; height: 100% !important; object-fit: contain;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -171,6 +185,20 @@ const props = withDefaults(defineProps<{
|
|||
full: false,
|
||||
});
|
||||
|
||||
type ExportCompletedNotification = Misskey.entities.Notification & { type: 'exportCompleted' };
|
||||
|
||||
const exportEntityName = {
|
||||
antenna: i18n.ts.antennas,
|
||||
blocking: i18n.ts.blockedUsers,
|
||||
clip: i18n.ts.clips,
|
||||
customEmoji: i18n.ts.customEmojis,
|
||||
favorite: i18n.ts.favorites,
|
||||
following: i18n.ts.following,
|
||||
muting: i18n.ts.mutedUsers,
|
||||
note: i18n.ts.notes,
|
||||
userList: i18n.ts.lists,
|
||||
} as const satisfies Record<ExportCompletedNotification['exportedEntity'], string>;
|
||||
|
||||
const followRequestDone = ref(false);
|
||||
|
||||
const acceptFollowRequest = () => {
|
||||
|
|
@ -200,6 +228,14 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
|||
overflow-wrap: break-word;
|
||||
display: flex;
|
||||
contain: content;
|
||||
|
||||
--eventFollow: #36aed2;
|
||||
--eventRenote: #36d298;
|
||||
--eventReply: #007aff;
|
||||
--eventReactionHeart: var(--love);
|
||||
--eventReaction: #e99a0b;
|
||||
--eventAchievement: #cb9a11;
|
||||
--eventOther: #88a6b7;
|
||||
}
|
||||
|
||||
.head {
|
||||
|
|
@ -308,6 +344,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_exportCompleted {
|
||||
padding: 3px;
|
||||
background: var(--eventOther);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_roleAssigned {
|
||||
padding: 3px;
|
||||
background: var(--eventOther);
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import MkSwitch from './MkSwitch.vue';
|
|||
import MkInfo from './MkInfo.vue';
|
||||
import MkButton from './MkButton.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { notificationTypes } from '@/const.js';
|
||||
import { notificationTypes } from '@@/js/const.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import XNotification from '@/components/MkNotification.vue';
|
|||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { notificationTypes } from '@/const.js';
|
||||
import { notificationTypes } from '@@/js/const.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ onUnmounted(() => {
|
|||
left: 0;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
//background: linear-gradient(0deg, var(--panel), var(--X15));
|
||||
//background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
|
||||
|
||||
> .fadeLabel {
|
||||
display: inline-block;
|
||||
|
|
|
|||
|
|
@ -34,13 +34,13 @@ import RouterView from '@/components/global/RouterView.vue';
|
|||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import { popout as _popout } from '@/scripts/popout.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { url } from '@/config.js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import { useScrollPositionManager } from '@/nirax.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||
import { openingWindowsCount } from '@/os.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { getScrollContainer } from '@/scripts/scroll.js';
|
||||
import { getScrollContainer } from '@@/js/scroll.js';
|
||||
import { useRouterFactory } from '@/router/supplier.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
import MkUserName from './global/MkUserName.vue';
|
||||
|
|
|
|||
|
|
@ -45,10 +45,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts">
|
||||
import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch, type Ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
|
||||
import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js';
|
||||
import { useDocumentVisibility } from '@/scripts/use-document-visibility.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { MisskeyEntity } from '@/types/date-separated-list.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
@ -126,8 +126,6 @@ const items = ref<MisskeyEntityMap>(new Map());
|
|||
*/
|
||||
const queue = ref<MisskeyEntityMap>(new Map());
|
||||
|
||||
const offset = ref(0);
|
||||
|
||||
/**
|
||||
* 初期化中かどうか(trueならMkLoadingで全て隠す)
|
||||
*/
|
||||
|
|
@ -180,7 +178,9 @@ watch([backed, contentEl], () => {
|
|||
if (!backed.value) {
|
||||
if (!contentEl.value) return;
|
||||
|
||||
scrollRemove.value = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl.value, executeQueue, TOLERANCE);
|
||||
scrollRemove.value = props.pagination.reversed
|
||||
? onScrollBottom(contentEl.value, executeQueue, TOLERANCE)
|
||||
: onScrollTop(contentEl.value, (topVisible) => { if (topVisible) executeQueue(); }, TOLERANCE);
|
||||
} else {
|
||||
if (scrollRemove.value) scrollRemove.value();
|
||||
scrollRemove.value = null;
|
||||
|
|
@ -230,7 +230,6 @@ async function init(): Promise<void> {
|
|||
more.value = true;
|
||||
}
|
||||
|
||||
offset.value = res.length;
|
||||
error.value = false;
|
||||
fetching.value = false;
|
||||
|
||||
|
|
@ -254,7 +253,7 @@ const fetchMore = async (): Promise<void> => {
|
|||
...params,
|
||||
limit: SECOND_FETCH_LIMIT,
|
||||
...(offsetMode ? {
|
||||
offset: offset.value,
|
||||
offset: items.value.size,
|
||||
} : {
|
||||
untilId: Array.from(items.value.keys()).at(-1),
|
||||
}),
|
||||
|
|
@ -304,7 +303,6 @@ const fetchMore = async (): Promise<void> => {
|
|||
moreFetching.value = false;
|
||||
}
|
||||
}
|
||||
offset.value += res.length;
|
||||
}, err => {
|
||||
moreFetching.value = false;
|
||||
});
|
||||
|
|
@ -319,7 +317,7 @@ const fetchMoreAhead = async (): Promise<void> => {
|
|||
...params,
|
||||
limit: SECOND_FETCH_LIMIT,
|
||||
...(offsetMode ? {
|
||||
offset: offset.value,
|
||||
offset: items.value.size,
|
||||
} : {
|
||||
sinceId: Array.from(items.value.keys()).at(-1),
|
||||
}),
|
||||
|
|
@ -331,7 +329,6 @@ const fetchMoreAhead = async (): Promise<void> => {
|
|||
items.value = concatMapWithArray(items.value, res);
|
||||
more.value = true;
|
||||
}
|
||||
offset.value += res.length;
|
||||
moreFetching.value = false;
|
||||
}, err => {
|
||||
moreFetching.value = false;
|
||||
|
|
|
|||
|
|
@ -31,14 +31,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||
import { sum } from '@/scripts/array.js';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { host } from '@/config.js';
|
||||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
|
||||
const props = defineProps<{
|
||||
noteId: string;
|
||||
|
|
@ -85,9 +85,10 @@ if (props.poll.expiresAt) {
|
|||
}
|
||||
|
||||
const vote = async (id) => {
|
||||
if (props.readOnly || closed.value || isVoted.value) return;
|
||||
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
|
||||
if (props.readOnly || closed.value || isVoted.value) return;
|
||||
if (!props.poll.multiple) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { ref, shallowRef } from 'vue';
|
||||
import MkModal from './MkModal.vue';
|
||||
import MkMenu from './MkMenu.vue';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
defineProps<{
|
||||
items: MenuItem[];
|
||||
|
|
|
|||
|
|
@ -106,11 +106,11 @@ import * as mfm from '@transfem-org/sfm-js';
|
|||
import * as Misskey from 'misskey-js';
|
||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||
import { toASCII } from 'punycode/';
|
||||
import { host, url } from '@@/js/config.js';
|
||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
import MkNotePreview from '@/components/MkNotePreview.vue';
|
||||
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
||||
import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
||||
import { host, url } from '@/config.js';
|
||||
import { erase, unique } from '@/scripts/array.js';
|
||||
import { extractMentions } from '@/scripts/extract-mentions.js';
|
||||
import { formatTimeString } from '@/scripts/format-time-string.js';
|
||||
|
|
@ -247,7 +247,7 @@ const submitText = computed((): string => {
|
|||
});
|
||||
|
||||
const textLength = computed((): number => {
|
||||
return (text.value + imeText.value).trim().length + (cw.value?.trim().length ?? 0);
|
||||
return (text.value + imeText.value).length + (cw.value?.length ?? 0);
|
||||
});
|
||||
|
||||
const maxTextLength = computed((): number => {
|
||||
|
|
@ -1183,13 +1183,13 @@ defineExpose({
|
|||
|
||||
&:not(:disabled):hover {
|
||||
> .inner {
|
||||
background: linear-gradient(90deg, var(--X8), var(--X8));
|
||||
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
> .inner {
|
||||
background: linear-gradient(90deg, var(--X8), var(--X8));
|
||||
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1256,6 +1256,15 @@ defineExpose({
|
|||
min-height: 75px;
|
||||
max-height: 150px;
|
||||
overflow: auto;
|
||||
background-size: auto auto;
|
||||
}
|
||||
|
||||
html[data-color-scheme=dark] .preview {
|
||||
background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, #0004 5px, #0004 10px);
|
||||
}
|
||||
|
||||
html[data-color-scheme=light] .preview {
|
||||
background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, #00000005 5px, #00000005 10px);
|
||||
}
|
||||
|
||||
.targetNote {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
|||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
|
|
@ -70,7 +71,7 @@ async function detachAndDeleteMedia(file: Misskey.entities.DriveFile) {
|
|||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('driveFileDeleteConfirm', { name: file.name }),
|
||||
text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }),
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
|
@ -143,7 +144,10 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
|
|||
if (menuShowing) return;
|
||||
|
||||
const isImage = file.type.startsWith('image/');
|
||||
os.popupMenu([{
|
||||
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
text: i18n.ts.renameFile,
|
||||
icon: 'ti ti-forms',
|
||||
action: () => { rename(file); },
|
||||
|
|
@ -155,11 +159,17 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
|
|||
text: i18n.ts.describeFile,
|
||||
icon: 'ti ti-text-caption',
|
||||
action: () => { describe(file); },
|
||||
}, ...isImage ? [{
|
||||
text: i18n.ts.cropImage,
|
||||
icon: 'ti ti-crop',
|
||||
action: () : void => { crop(file); },
|
||||
}] : [], {
|
||||
});
|
||||
|
||||
if (isImage) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.cropImage,
|
||||
icon: 'ti ti-crop',
|
||||
action: () : void => { crop(file); },
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
text: i18n.ts.attachCancel,
|
||||
|
|
@ -170,7 +180,9 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
|
|||
icon: 'ti ti-trash',
|
||||
danger: true,
|
||||
action: () => { detachAndDeleteMedia(file); },
|
||||
}], ev.currentTarget ?? ev.target).then(() => menuShowing = false);
|
||||
});
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target).then(() => menuShowing = false);
|
||||
menuShowing = true;
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
|||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkRadio from '@/components/MkRadio.vue';
|
||||
import * as os from '@/os.js';
|
||||
import * as config from '@/config.js';
|
||||
import * as config from '@@/js/config.js';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
const text = ref('');
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { getScrollContainer } from '@/scripts/scroll.js';
|
||||
import { getScrollContainer } from '@@/js/scroll.js';
|
||||
import { isHorizontalSwipeSwiping } from '@/scripts/touch.js';
|
||||
|
||||
const SCROLL_STOP = 10;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div class="timctyfi" :class="{ disabled, easing }">
|
||||
<div class="label"><slot name="label"></slot></div>
|
||||
<div class="label">
|
||||
<slot name="label"></slot>
|
||||
</div>
|
||||
<div v-adaptive-border class="body">
|
||||
<div ref="containerEl" class="container">
|
||||
<div class="track">
|
||||
|
|
@ -14,15 +16,25 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="steps && showTicks" class="ticks">
|
||||
<div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div>
|
||||
</div>
|
||||
<div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div>
|
||||
<div
|
||||
ref="thumbEl"
|
||||
class="thumb"
|
||||
:style="{ left: thumbPosition + 'px' }"
|
||||
@mouseenter.passive="onMouseenter"
|
||||
@mousedown="onMousedown"
|
||||
@touchstart="onMousedown"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption"><slot name="caption"></slot></div>
|
||||
<div class="caption">
|
||||
<slot name="caption"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch, shallowRef } from 'vue';
|
||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
|
||||
import { isTouchUsing } from '@/scripts/touch.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
|
@ -101,12 +113,36 @@ const steps = computed(() => {
|
|||
}
|
||||
});
|
||||
|
||||
const tooltipForDragShowing = ref(false);
|
||||
const tooltipForHoverShowing = ref(false);
|
||||
|
||||
function onMouseenter() {
|
||||
if (isTouchUsing) return;
|
||||
|
||||
tooltipForHoverShowing.value = true;
|
||||
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), {
|
||||
showing: computed(() => tooltipForHoverShowing.value && !tooltipForDragShowing.value),
|
||||
text: computed(() => {
|
||||
return props.textConverter(finalValue.value);
|
||||
}),
|
||||
targetElement: thumbEl,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
|
||||
thumbEl.value!.addEventListener('mouseleave', () => {
|
||||
tooltipForHoverShowing.value = false;
|
||||
}, { once: true, passive: true });
|
||||
}
|
||||
|
||||
function onMousedown(ev: MouseEvent | TouchEvent) {
|
||||
ev.preventDefault();
|
||||
|
||||
const tooltipShowing = ref(true);
|
||||
tooltipForDragShowing.value = true;
|
||||
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), {
|
||||
showing: tooltipShowing,
|
||||
showing: tooltipForDragShowing,
|
||||
text: computed(() => {
|
||||
return props.textConverter(finalValue.value);
|
||||
}),
|
||||
|
|
@ -137,7 +173,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
|
|||
|
||||
const onMouseup = () => {
|
||||
document.head.removeChild(style);
|
||||
tooltipShowing.value = false;
|
||||
tooltipForDragShowing.value = false;
|
||||
window.removeEventListener('mousemove', onDrag);
|
||||
window.removeEventListener('touchmove', onDrag);
|
||||
window.removeEventListener('mouseup', onMouseup);
|
||||
|
|
@ -261,12 +297,12 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
|
|||
> .container {
|
||||
> .track {
|
||||
> .highlight {
|
||||
transition: width 0.2s cubic-bezier(0,0,0,1);
|
||||
transition: width 0.2s cubic-bezier(0, 0, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
> .thumb {
|
||||
transition: left 0.2s cubic-bezier(0,0,0,1);
|
||||
transition: left 0.2s cubic-bezier(0, 0, 0, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const emit = defineEmits<{
|
|||
.icon {
|
||||
display: block;
|
||||
width: 60px;
|
||||
max-height: 60px;
|
||||
font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
|
||||
margin: 0 auto;
|
||||
object-fit: contain;
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { getEmojiName } from '@@/js/emojilist.js';
|
||||
import MkTooltip from './MkTooltip.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import { getEmojiName } from '@/scripts/emojilist.js';
|
||||
|
||||
defineProps<{
|
||||
showing: boolean;
|
||||
|
|
@ -63,6 +63,7 @@ function getReactionName(reaction: string): string {
|
|||
.reactionIcon {
|
||||
display: block;
|
||||
width: 60px;
|
||||
max-height: 60px;
|
||||
font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
|
||||
object-fit: contain;
|
||||
margin: 0 auto;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { computed, inject, onMounted, shallowRef, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { getUnicodeEmoji } from '@@/js/emojilist.js';
|
||||
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
|
||||
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
|
|
@ -34,7 +35,6 @@ import { i18n } from '@/i18n.js';
|
|||
import * as sound from '@/scripts/sound.js';
|
||||
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
|
||||
import { customEmojisMap } from '@/custom-emojis.js';
|
||||
import { getUnicodeEmoji } from '@/scripts/emojilist.js';
|
||||
|
||||
const props = defineProps<{
|
||||
reaction: string;
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string | null;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password>
|
||||
<MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
|
||||
</MkInput>
|
||||
|
|
@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
|
||||
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
|
||||
<p>{{ i18n.ts.useSecurityKey }}</p>
|
||||
<MkButton v-if="!queryingKey" @click="queryKey">
|
||||
<MkButton v-if="!queryingKey" @click="query2FaKey">
|
||||
{{ i18n.ts.retry }}
|
||||
</MkButton>
|
||||
</div>
|
||||
|
|
@ -45,10 +45,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
|
||||
</div>
|
||||
<div class="twofa-group totp-group _gaps">
|
||||
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
|
||||
<template #label>{{ i18n.ts.password }}</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
</MkInput>
|
||||
<MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
|
||||
<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
|
||||
<template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
|
||||
|
|
@ -57,6 +53,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!totpLogin && usePasswordLessLogin" :class="$style.orHr">
|
||||
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
|
||||
</div>
|
||||
<div v-if="!totpLogin && usePasswordLessLogin" class="twofa-group tap-group">
|
||||
<MkButton v-if="!queryingKey" type="submit" :disabled="signing" style="margin: auto auto;" rounded large primary @click="onPasskeyLogin">
|
||||
<i class="ti ti-device-usb" style="font-size: medium;"></i>
|
||||
{{ signing ? i18n.ts.loggingIn : i18n.ts.signinWithPasskey }}
|
||||
</MkButton>
|
||||
<p v-if="queryingKey">{{ i18n.ts.useSecurityKey }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
|
@ -66,21 +72,24 @@ import { defineAsyncComponent, ref } from 'vue';
|
|||
import { toUnicode } from 'punycode/';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
|
||||
import { SigninWithPasskeyResponse } from 'misskey-js/entities.js';
|
||||
import { query, extractDomain } from '@@/js/url.js';
|
||||
import { host as configHost } from '@@/js/config.js';
|
||||
import MkDivider from './MkDivider.vue';
|
||||
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { host as configHost } from '@/config.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { query, extractDomain } from '@/scripts/url.js';
|
||||
import { login } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { showSystemAccountDialog } from '@/scripts/show-system-account-dialog.js';
|
||||
|
||||
const signing = ref(false);
|
||||
const user = ref<Misskey.entities.UserDetailed | null>(null);
|
||||
const usePasswordLessLogin = ref<Misskey.entities.UserDetailed['usePasswordLessLogin']>(true);
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const token = ref('');
|
||||
|
|
@ -89,6 +98,7 @@ const totpLogin = ref(false);
|
|||
const isBackupCode = ref(false);
|
||||
const queryingKey = ref(false);
|
||||
let credentialRequest: CredentialRequestOptions | null = null;
|
||||
const passkey_context = ref('');
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'login', v: any): void;
|
||||
|
|
@ -111,8 +121,10 @@ function onUsernameChange(): void {
|
|||
username: username.value,
|
||||
}).then(userResponse => {
|
||||
user.value = userResponse;
|
||||
usePasswordLessLogin.value = userResponse.usePasswordLessLogin;
|
||||
}, () => {
|
||||
user.value = null;
|
||||
usePasswordLessLogin.value = true;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +134,7 @@ function onLogin(res: any): Promise<void> | void {
|
|||
}
|
||||
}
|
||||
|
||||
async function queryKey(): Promise<void> {
|
||||
async function query2FaKey(): Promise<void> {
|
||||
if (credentialRequest == null) return;
|
||||
queryingKey.value = true;
|
||||
await webAuthnRequest(credentialRequest)
|
||||
|
|
@ -151,6 +163,47 @@ async function queryKey(): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
function onPasskeyLogin(): void {
|
||||
signing.value = true;
|
||||
if (webAuthnSupported()) {
|
||||
misskeyApi('signin-with-passkey', {})
|
||||
.then((res: SigninWithPasskeyResponse) => {
|
||||
totpLogin.value = false;
|
||||
signing.value = false;
|
||||
queryingKey.value = true;
|
||||
passkey_context.value = res.context ?? '';
|
||||
credentialRequest = parseRequestOptionsFromJSON({
|
||||
publicKey: res.option,
|
||||
});
|
||||
})
|
||||
.then(() => queryPasskey())
|
||||
.catch(loginFailed);
|
||||
}
|
||||
}
|
||||
|
||||
async function queryPasskey(): Promise<void> {
|
||||
if (credentialRequest == null) return;
|
||||
queryingKey.value = true;
|
||||
console.log('Waiting passkey auth...');
|
||||
await webAuthnRequest(credentialRequest)
|
||||
.catch((err) => {
|
||||
console.warn('Passkey Auth fail!: ', err);
|
||||
queryingKey.value = false;
|
||||
return Promise.reject(null);
|
||||
}).then(credential => {
|
||||
credentialRequest = null;
|
||||
queryingKey.value = false;
|
||||
signing.value = true;
|
||||
return misskeyApi('signin-with-passkey', {
|
||||
credential: credential.toJSON(),
|
||||
context: passkey_context.value,
|
||||
});
|
||||
}).then((res: SigninWithPasskeyResponse) => {
|
||||
emit('login', res.signinResponse);
|
||||
return onLogin(res.signinResponse);
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit(): void {
|
||||
signing.value = true;
|
||||
if (!totpLogin.value && user.value && user.value.twoFactorEnabled) {
|
||||
|
|
@ -165,7 +218,7 @@ function onSubmit(): void {
|
|||
publicKey: res,
|
||||
});
|
||||
})
|
||||
.then(() => queryKey())
|
||||
.then(() => query2FaKey())
|
||||
.catch(loginFailed);
|
||||
} else {
|
||||
totpLogin.value = true;
|
||||
|
|
@ -217,6 +270,30 @@ function loginFailed(err: any): void {
|
|||
});
|
||||
break;
|
||||
}
|
||||
case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.loginFailed,
|
||||
text: i18n.ts.unknownWebAuthnKey,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.loginFailed,
|
||||
text: i18n.ts.passkeyVerificationFailed,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.loginFailed,
|
||||
text: i18n.ts.passkeyVerificationSucceededButPasswordlessLoginDisabled,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.error(err);
|
||||
os.alert({
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
:height="430"
|
||||
:height="450"
|
||||
@close="onClose"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ import * as Misskey from 'misskey-js';
|
|||
import MkButton from './MkButton.vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
|
||||
import * as config from '@/config.js';
|
||||
import * as config from '@@/js/config.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { login } from '@/account.js';
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { host } from '@/config.js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ import MkMediaList from '@/components/MkMediaList.vue';
|
|||
import MkPoll from '@/components/MkPoll.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { shouldCollapsed } from '@/scripts/collapsed.js';
|
||||
import { shouldCollapsed } from '@@/js/collapsed.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import * as os from '@/os.js';
|
||||
|
|
@ -110,7 +110,7 @@ watch(() => props.expandAllCws, (expandAllCws) => {
|
|||
left: 0;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
//background: linear-gradient(0deg, var(--panel), var(--X15));
|
||||
// background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
|
||||
|
||||
> .fadeLabel {
|
||||
display: inline-block;
|
||||
|
|
|
|||
|
|
@ -100,14 +100,14 @@ defineProps<{
|
|||
|
||||
&.grid {
|
||||
> .group {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
|
||||
& + .group {
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
|
||||
> .title {
|
||||
font-size: 1em;
|
||||
opacity: 0.7;
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@
|
|||
*/
|
||||
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
export type SystemWebhookEventType = 'abuseReport' | 'abuseReportResolved';
|
||||
export type SystemWebhookEventType = Misskey.entities.SystemWebhook['on'][number];
|
||||
|
||||
export type MkSystemWebhookEditorProps = {
|
||||
mode: 'create' | 'edit';
|
||||
|
|
|
|||
|
|
@ -35,16 +35,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
|
||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
|
||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
|
||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
|
||||
</MkSwitch>
|
||||
<div class="_gaps">
|
||||
<div class="_gaps_s">
|
||||
<div :class="$style.switchBox">
|
||||
<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
|
||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
|
||||
</MkSwitch>
|
||||
<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.abuseReport)" @click="test('abuseReport')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.switchBox">
|
||||
<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
|
||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
|
||||
</MkSwitch>
|
||||
<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.abuseReportResolved)" @click="test('abuseReportResolved')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.switchBox">
|
||||
<MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
|
||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
|
||||
</MkSwitch>
|
||||
<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.userCreated)" @click="test('userCreated')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="mode === 'edit'" :class="$style.description">
|
||||
{{ i18n.ts._webhookSettings.testRemarks }}
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
|
|
@ -66,6 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import {
|
||||
|
|
@ -180,6 +196,21 @@ async function loadingScope<T>(fn: () => Promise<T>): Promise<T> {
|
|||
}
|
||||
}
|
||||
|
||||
async function test(type: Misskey.entities.SystemWebhook['on'][number]): Promise<void> {
|
||||
if (!id.value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
await os.apiWithDialog('admin/system-webhook/test', {
|
||||
webhookId: id.value,
|
||||
type,
|
||||
override: {
|
||||
secret: secret.value,
|
||||
url: url.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadingScope(async () => {
|
||||
switch (mode.value) {
|
||||
|
|
@ -235,4 +266,29 @@ onMounted(async () => {
|
|||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
|
||||
.switchBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
|
||||
.testButton {
|
||||
$buttonSize: 28px;
|
||||
padding: 0;
|
||||
width: $buttonSize;
|
||||
min-width: $buttonSize;
|
||||
max-width: $buttonSize;
|
||||
height: $buttonSize;
|
||||
margin-left: auto;
|
||||
line-height: normal;
|
||||
font-size: 90%;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.85em;
|
||||
padding: 8px 0 0 0;
|
||||
color: var(--fgTransparentWeak);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ import XSensitive from '@/components/MkTutorialDialog.Sensitive.vue';
|
|||
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { host } from '@/config.js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { onMounted, shallowRef } from 'vue';
|
|||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkSparkle from '@/components/MkSparkle.vue';
|
||||
import { version } from '@/config.js';
|
||||
import { version } from '@@/js/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { confetti } from '@/scripts/confetti.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -85,12 +85,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue';
|
||||
import type { summaly } from '@misskey-dev/summaly';
|
||||
import { url as local } from '@/config.js';
|
||||
import { url as local } from '@@/js/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { versatileLang } from '@/scripts/intl-const.js';
|
||||
import { versatileLang } from '@@/js/intl-const.js';
|
||||
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { host as currentHost, hostname } from '@/config.js';
|
||||
import { host as currentHost, hostname } from '@@/js/config.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'ok', selected: Misskey.entities.UserDetailed): void;
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue';
|
|||
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { host } from '@/config.js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import * as os from '@/os.js';
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ import XSignupDialog from '@/components/MkSignupDialog.vue';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkTimeline from '@/components/MkTimeline.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { instanceName } from '@/config.js';
|
||||
import { instanceName } from '@@/js/config.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import MkButton from '@/components/MkButton.vue';
|
|||
import { widgets as widgetDefs } from '@/widgets/index.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
|
|
@ -98,13 +99,6 @@ const updateWidget = (id, data) => {
|
|||
|
||||
function onContextmenu(widget: Widget, ev: MouseEvent) {
|
||||
const element = ev.target as HTMLElement | null;
|
||||
const isLink = (el: HTMLElement): boolean => {
|
||||
if (el.tagName === 'A') return true;
|
||||
if (el.parentElement) {
|
||||
return isLink(el.parentElement);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (element && isLink(element)) return;
|
||||
if (element && (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(element.tagName) || element.attributes['contenteditable'])) return;
|
||||
if (window.getSelection()?.toString() !== '') return;
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue';
|
||||
import contains from '@/scripts/contains.js';
|
||||
import * as os from '@/os.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
|
|
@ -508,10 +508,6 @@ defineExpose({
|
|||
.header {
|
||||
--height: 39px;
|
||||
|
||||
&.mini {
|
||||
--height: 32px;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
|
@ -524,6 +520,10 @@ defineExpose({
|
|||
//border-bottom: solid 1px var(--divider);
|
||||
font-size: 90%;
|
||||
font-weight: bold;
|
||||
|
||||
&.mini {
|
||||
--height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.headerButton {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import { versatileLang } from '@/scripts/intl-const.js';
|
||||
import { versatileLang } from '@@/js/intl-const.js';
|
||||
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -60,18 +60,18 @@ const props = defineProps<{
|
|||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 14px;
|
||||
background: var(--buttonBg);
|
||||
background: var(--folderHeaderBg);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.9em;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background: var(--buttonHoverBg);
|
||||
background: var(--folderHeaderHoverBg);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--accent);
|
||||
background: var(--buttonHoverBg);
|
||||
background: var(--folderHeaderHoverBg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export type MkABehavior = 'window' | 'browser' | null;
|
|||
import { computed, inject, shallowRef } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { url } from '@/config.js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkCondensedLine v-if="defaultStore.state.enableCondensedLineForAcct" :minScale="2 / 3">
|
||||
<span>@{{ user.username }}</span>
|
||||
<span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span>
|
||||
</MkCondensedLine>
|
||||
<span v-else>
|
||||
<span>
|
||||
<span>@{{ user.username }}</span>
|
||||
<span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span>
|
||||
</span>
|
||||
|
|
@ -17,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { toUnicode } from 'punycode/';
|
||||
import { host as hostRaw } from '@/config.js';
|
||||
import { host as hostRaw } from '@@/js/config.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
defineProps<{
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { ref, computed } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { url as local, host } from '@/config.js';
|
||||
import { url as local, host } from '@@/js/config.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import * as os from '@/os.js';
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-if="showDecoration">
|
||||
<img
|
||||
v-for="decoration in decorations ?? user.avatarDecorations"
|
||||
:class="[$style.decoration]"
|
||||
:class="[$style.decoration, { [$style.decorationBlink]: decoration.blink }]"
|
||||
:src="getDecorationUrl(decoration)"
|
||||
:style="{
|
||||
rotate: getDecorationAngle(decoration),
|
||||
|
|
@ -43,10 +43,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { watch, ref, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
|
||||
import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
|
||||
import MkA from './MkA.vue';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js';
|
||||
import { acct, userPage } from '@/filters/user.js';
|
||||
import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
|
@ -61,7 +61,7 @@ const props = withDefaults(defineProps<{
|
|||
link?: boolean;
|
||||
preview?: boolean;
|
||||
indicator?: boolean;
|
||||
decorations?: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>[];
|
||||
decorations?: (Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'> & { blink?: boolean; })[];
|
||||
forceShowDecoration?: boolean;
|
||||
}>(), {
|
||||
target: null,
|
||||
|
|
@ -336,4 +336,17 @@ watch(() => props.user.avatarBlurhash, () => {
|
|||
width: 200%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.decorationBlink {
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% {
|
||||
filter: brightness(2);
|
||||
}
|
||||
50% {
|
||||
filter: brightness(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<span :class="$style.container">
|
||||
<span ref="content" :class="$style.content">
|
||||
<span ref="content" :class="$style.content" :style="{ maxWidth: `${100 / minScale}%` }">
|
||||
<slot/>
|
||||
</span>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
|||
import * as sound from '@/scripts/sound.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
|
|
@ -86,7 +87,10 @@ const errored = ref(url.value == null);
|
|||
function onClick(ev: MouseEvent) {
|
||||
if (props.menu) {
|
||||
ev.stopPropagation();
|
||||
os.popupMenu([{
|
||||
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
type: 'label',
|
||||
text: `:${props.name}:`,
|
||||
}, {
|
||||
|
|
@ -96,14 +100,20 @@ function onClick(ev: MouseEvent) {
|
|||
copyToClipboard(`:${props.name}:`);
|
||||
os.success();
|
||||
},
|
||||
}, ...(props.menuReaction && react ? [{
|
||||
text: i18n.ts.doReaction,
|
||||
icon: 'ph-smiley ph-bold ph-lg',
|
||||
action: () => {
|
||||
react(`:${props.name}:`);
|
||||
sound.playMisskeySfx('reaction');
|
||||
},
|
||||
}] : []), {
|
||||
});
|
||||
|
||||
if (props.menuReaction && react) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.doReaction,
|
||||
icon: 'ph-smiley ph-bold ph-lg',
|
||||
action: () => {
|
||||
react(`:${props.name}:`);
|
||||
sound.playMisskeySfx('reaction');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
text: i18n.ts.info,
|
||||
icon: 'ti ti-info-circle',
|
||||
action: async () => {
|
||||
|
|
@ -115,7 +125,9 @@ function onClick(ev: MouseEvent) {
|
|||
closed: () => dispose(),
|
||||
});
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
});
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject } from 'vue';
|
||||
import { char2fluentEmojiFilePath, char2twemojiFilePath, char2tossfaceFilePath } from '@/scripts/emoji-base.js';
|
||||
import { colorizeEmoji, getEmojiName } from '@@/js/emojilist.js';
|
||||
import { char2fluentEmojiFilePath, char2twemojiFilePath, char2tossfaceFilePath } from '@@/js/emoji-base.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js';
|
||||
import * as os from '@/os.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const props = defineProps<{
|
||||
emoji: string;
|
||||
|
|
@ -40,7 +41,10 @@ function computeTitle(event: PointerEvent): void {
|
|||
function onClick(ev: MouseEvent) {
|
||||
if (props.menu) {
|
||||
ev.stopPropagation();
|
||||
os.popupMenu([{
|
||||
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
type: 'label',
|
||||
text: props.emoji,
|
||||
}, {
|
||||
|
|
@ -50,14 +54,20 @@ function onClick(ev: MouseEvent) {
|
|||
copyToClipboard(props.emoji);
|
||||
os.success();
|
||||
},
|
||||
}, ...(props.menuReaction && react ? [{
|
||||
text: i18n.ts.doReaction,
|
||||
icon: 'ph-smiley ph-bold ph-lg',
|
||||
action: () => {
|
||||
react(props.emoji);
|
||||
sound.playMisskeySfx('reaction');
|
||||
},
|
||||
}] : [])], ev.currentTarget ?? ev.target);
|
||||
});
|
||||
|
||||
if (props.menuReaction && react) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.doReaction,
|
||||
icon: 'ph-smiley ph-bold ph-lg',
|
||||
action: () => {
|
||||
react(props.emoji);
|
||||
sound.playMisskeySfx('reaction');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -2,16 +2,15 @@
|
|||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { expect, within } from '@storybook/test';
|
||||
import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.js';
|
||||
import MkMfm from './MkMfm.js';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkMisskeyFlavoredMarkdown,
|
||||
MkMfm,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
|
|
@ -25,7 +24,7 @@ export const Default = {
|
|||
};
|
||||
},
|
||||
},
|
||||
template: '<MkMisskeyFlavoredMarkdown v-bind="props" />',
|
||||
template: '<MkMfm v-bind="props" />',
|
||||
};
|
||||
},
|
||||
async play({ canvasElement, args }) {
|
||||
|
|
@ -54,25 +53,25 @@ export const Default = {
|
|||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
|
||||
} satisfies StoryObj<typeof MkMfm>;
|
||||
export const Plain = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
plain: true,
|
||||
},
|
||||
} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
|
||||
} satisfies StoryObj<typeof MkMfm>;
|
||||
export const Nowrap = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
nowrap: true,
|
||||
},
|
||||
} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
|
||||
} satisfies StoryObj<typeof MkMfm>;
|
||||
export const IsNotNote = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
isNote: false,
|
||||
},
|
||||
} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
|
||||
} satisfies StoryObj<typeof MkMfm>;
|
||||
|
|
@ -18,10 +18,15 @@ import MkCodeInline from '@/components/MkCodeInline.vue';
|
|||
import MkGoogle from '@/components/MkGoogle.vue';
|
||||
import MkSparkle from '@/components/MkSparkle.vue';
|
||||
import MkA, { MkABehavior } from '@/components/global/MkA.vue';
|
||||
import { host } from '@/config.js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { nyaize as doNyaize } from '@/scripts/nyaize.js';
|
||||
import { safeParseFloat } from '@/scripts/safe-parse.js';
|
||||
|
||||
function safeParseFloat(str: unknown): number | null {
|
||||
if (typeof str !== 'string' || str === '') return null;
|
||||
const num = parseFloat(str);
|
||||
if (isNaN(num)) return null;
|
||||
return num;
|
||||
}
|
||||
|
||||
const QUOTE_STYLE = `
|
||||
display: block;
|
||||
|
|
@ -92,7 +97,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
case 'text': {
|
||||
let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
|
||||
if (!disableNyaize && shouldNyaize) {
|
||||
text = doNyaize(text);
|
||||
text = Misskey.nyaize(text);
|
||||
}
|
||||
|
||||
if (!props.plain) {
|
||||
|
|
@ -340,14 +345,14 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
const child = token.children[0];
|
||||
let text = child.type === 'text' ? child.props.text : '';
|
||||
if (!disableNyaize && shouldNyaize) {
|
||||
text = doNyaize(text);
|
||||
text = Misskey.nyaize(text);
|
||||
}
|
||||
return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]);
|
||||
} else {
|
||||
const rt = token.children.at(-1)!;
|
||||
let text = rt.type === 'text' ? rt.props.text : '';
|
||||
if (!disableNyaize && shouldNyaize) {
|
||||
text = doNyaize(text);
|
||||
text = Misskey.nyaize(text);
|
||||
}
|
||||
return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]);
|
||||
}
|
||||
|
|
@ -459,7 +464,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
}
|
||||
|
||||
case 'emojiCode': {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (props.author?.host == null) {
|
||||
return [h(MkCustomEmoji, {
|
||||
key: Math.random(),
|
||||
|
|
@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { onMounted, onUnmounted, ref, inject, shallowRef, computed } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import XTabs, { Tab } from './MkPageHeader.tabs.vue';
|
||||
import { scrollToTop } from '@/scripts/scroll.js';
|
||||
import { scrollToTop } from '@@/js/scroll.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||
import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
ref="bodyEl"
|
||||
:data-sticky-container-header-height="headerHeight"
|
||||
:data-sticky-container-footer-height="footerHeight"
|
||||
style="position: relative; z-index: 0;"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
|
@ -24,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, shallowRef } from 'vue';
|
||||
|
||||
import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const.js';
|
||||
import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@@/js/const.js';
|
||||
|
||||
const rootEl = shallowRef<HTMLElement>();
|
||||
const headerEl = shallowRef<HTMLElement>();
|
||||
|
|
@ -83,14 +84,14 @@ onMounted(() => {
|
|||
if (headerEl.value != null) {
|
||||
headerEl.value.style.position = 'sticky';
|
||||
headerEl.value.style.top = 'var(--stickyTop, 0)';
|
||||
headerEl.value.style.zIndex = '1000';
|
||||
headerEl.value.style.zIndex = '1';
|
||||
observer.observe(headerEl.value);
|
||||
}
|
||||
|
||||
if (footerEl.value != null) {
|
||||
footerEl.value.style.position = 'sticky';
|
||||
footerEl.value.style.bottom = 'var(--stickyBottom, 0)';
|
||||
footerEl.value.style.zIndex = '1000';
|
||||
footerEl.value.style.zIndex = '1';
|
||||
observer.observe(footerEl.value);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { expect } from '@storybook/test';
|
|||
import { StoryObj } from '@storybook/vue3';
|
||||
import MkTime from './MkTime.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { dateTimeFormat } from '@/scripts/intl-const.js';
|
||||
import { dateTimeFormat } from '@@/js/intl-const.js';
|
||||
const now = new Date('2023-04-01T00:00:00.000Z');
|
||||
const future = new Date('2024-04-01T00:00:00.000Z');
|
||||
const oneHourAgo = new Date(now.getTime() - 3600000);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import isChromatic from 'chromatic/isChromatic';
|
||||
import { onMounted, onUnmounted, ref, computed } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { dateTimeFormat } from '@/scripts/intl-const.js';
|
||||
import { dateTimeFormat } from '@@/js/intl-const.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
time: Date | string | number | null;
|
||||
|
|
|
|||
|
|
@ -29,14 +29,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import { toUnicode as decodePunycode } from 'punycode/';
|
||||
import { url as local } from '@/config.js';
|
||||
import { url as local } from '@@/js/config.js';
|
||||
import * as os from '@/os.js';
|
||||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||
import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { MkABehavior } from '@/components/global/MkA.vue';
|
||||
import { warningExternalWebsite } from '@/scripts/warning-external-website.js';
|
||||
|
||||
function safeURIDecode(str: string): string {
|
||||
try {
|
||||
return decodeURIComponent(str);
|
||||
} catch {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
url: string;
|
||||
rel?: string;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { App } from 'vue';
|
||||
|
||||
import Mfm from './global/MkMisskeyFlavoredMarkdown.js';
|
||||
import Mfm from './global/MkMfm.js';
|
||||
import MkA from './global/MkA.vue';
|
||||
import MkAcct from './global/MkAcct.vue';
|
||||
import MkAvatar from './global/MkAvatar.vue';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue