Merge remote-tracking branch 'misskey/master' into feature/2024.9.0

This commit is contained in:
dakkar 2024-10-09 15:17:22 +01:00
commit f00576bce6
564 changed files with 19993 additions and 8169 deletions

View file

@ -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>();

View file

@ -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, stylewidthstyle
// 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) {

View file

@ -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');

View file

@ -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 {

View file

@ -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 {

View file

@ -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;

View file

@ -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';

View file

@ -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'));

View file

@ -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;

View file

@ -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';

View file

@ -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';

View file

@ -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']}`,
}),

View file

@ -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';

View file

@ -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;

View file

@ -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;
}

View file

@ -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%;

View 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
// URLparams
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,
}));
// paramsref
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>

View file

@ -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';

View file

@ -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;

View file

@ -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"

View file

@ -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>

View file

@ -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>

View file

@ -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';

View 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>

View file

@ -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

View file

@ -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';

View file

@ -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';

View file

@ -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>

View file

@ -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';

View file

@ -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}`,

View file

@ -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;

View file

@ -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';

View file

@ -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}`,

View file

@ -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';

View file

@ -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[];

View file

@ -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 {

View file

@ -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[];

View file

@ -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';

View file

@ -94,12 +94,12 @@ defineExpose({
--root-margin: 24px;
--headerHeight: 46px;
--headerHeightNarrow: 42px;
@media (max-width: 500px) {
--root-margin: 16px;
}
--headerHeight: 46px;
--headerHeightNarrow: 42px;
}
.header {

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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>>

View file

@ -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';

View file

@ -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;

View file

@ -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';

View file

@ -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;

View file

@ -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',

View file

@ -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[];

View file

@ -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 {

View file

@ -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>

View file

@ -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('');

View file

@ -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;

View file

@ -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);
}
}
}

View file

@ -36,6 +36,7 @@ const emit = defineEmits<{
.icon {
display: block;
width: 60px;
max-height: 60px;
font-size: 60px; // unicodewidth
margin: 0 auto;
object-fit: contain;

View file

@ -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; // unicodewidth
object-fit: contain;
margin: 0 auto;

View file

@ -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;

View file

@ -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;

View file

@ -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({

View file

@ -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')"
>

View file

@ -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';

View file

@ -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';

View file

@ -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;

View file

@ -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;

View file

@ -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';

View file

@ -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>

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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;

View file

@ -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';

View file

@ -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';

View file

@ -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;

View file

@ -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 {

View file

@ -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';

View file

@ -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);
}
}

View file

@ -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';

View file

@ -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<{

View file

@ -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';

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>;

View file

@ -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(),

View file

@ -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';

View file

@ -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);
}
});

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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';