merge: upstream

This commit is contained in:
Marie 2023-12-23 02:09:23 +01:00
commit 5db583a3eb
701 changed files with 50809 additions and 13660 deletions

View file

@ -4,16 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu" v-on:click.stop>
<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu" @click.stop>
<slot></slot>
</a>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { url } from '@/config.js';
import { popout as popout_ } from '@/scripts/popout.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js';
@ -28,7 +28,7 @@ const props = withDefaults(defineProps<{
const router = useRouter();
const active = $computed(() => {
const active = computed(() => {
if (props.activeClass == null) return false;
const resolved = router.resolve(props.to);
if (resolved == null) return false;
@ -56,11 +56,11 @@ function onContextmenu(ev) {
action: () => {
router.push(props.to, 'forcePage');
},
}, null, {
}, { type: 'divider' }, {
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.openInNewTab,
action: () => {
window.open(props.to, '_blank');
window.open(props.to, '_blank', 'noopener');
},
}, {
icon: 'ph-link ph-bold ph-lg',

View file

@ -4,11 +4,8 @@
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from '@storybook/jest';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3';
import MkAd from './MkAd.vue';
import { i18n } from '@/i18n.js';
let lock: Promise<undefined> | undefined;

View file

@ -96,7 +96,7 @@ const choseAd = (): Ad | null => {
};
const chosen = ref(choseAd());
const shouldHide = $ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
const shouldHide = ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
function reduceFrequency(): void {
if (chosen.value == null) return;

View file

@ -23,21 +23,24 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</div>
<img
v-if="showDecoration && (decoration || user.avatarDecorations.length > 0)"
:class="[$style.decoration]"
:src="decoration?.url ?? user.avatarDecorations[0].url"
:style="{
rotate: getDecorationAngle(),
scale: getDecorationScale(),
}"
alt=""
>
<template v-if="showDecoration">
<img
v-for="decoration in decorations ?? user.avatarDecorations"
:class="[$style.decoration]"
:src="decoration.url"
:style="{
rotate: getDecorationAngle(decoration),
scale: getDecorationScale(decoration),
translate: getDecorationOffset(decoration),
}"
alt=""
>
</template>
</component>
</template>
<script lang="ts" setup>
import { watch } from 'vue';
import { watch, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
import MkA from './MkA.vue';
@ -47,9 +50,9 @@ import { acct, userPage } from '@/filters/user.js';
import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
import { defaultStore } from '@/store.js';
const animation = $ref(defaultStore.state.animation);
const squareAvatars = $ref(defaultStore.state.squareAvatars);
const useBlurEffect = $ref(defaultStore.state.useBlurEffect);
const animation = ref(defaultStore.state.animation);
const squareAvatars = ref(defaultStore.state.squareAvatars);
const useBlurEffect = ref(defaultStore.state.useBlurEffect);
const props = withDefaults(defineProps<{
user: Misskey.entities.User;
@ -57,19 +60,14 @@ const props = withDefaults(defineProps<{
link?: boolean;
preview?: boolean;
indicator?: boolean;
decoration?: {
url: string;
angle?: number;
flipH?: boolean;
flipV?: boolean;
};
decorations?: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>[];
forceShowDecoration?: boolean;
}>(), {
target: null,
link: false,
preview: false,
indicator: false,
decoration: undefined,
decorations: undefined,
forceShowDecoration: false,
});
@ -79,11 +77,11 @@ const emit = defineEmits<{
const showDecoration = props.forceShowDecoration || defaultStore.state.showAvatarDecorations;
const bound = $computed(() => props.link
const bound = computed(() => props.link
? { to: userPage(props.user), target: props.target }
: {});
const url = $computed(() => (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.enableDataSaverMode)
const url = computed(() => (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar)
? getStaticImageUrl(props.user.avatarUrl)
: props.user.avatarUrl);
@ -92,34 +90,26 @@ function onClick(ev: MouseEvent): void {
emit('click', ev);
}
function getDecorationAngle() {
let angle;
if (props.decoration) {
angle = props.decoration.angle ?? 0;
} else if (props.user.avatarDecorations.length > 0) {
angle = props.user.avatarDecorations[0].angle ?? 0;
} else {
angle = 0;
}
function getDecorationAngle(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
const angle = decoration.angle ?? 0;
return angle === 0 ? undefined : `${angle * 360}deg`;
}
function getDecorationScale() {
let scaleX;
if (props.decoration) {
scaleX = props.decoration.flipH ? -1 : 1;
} else if (props.user.avatarDecorations.length > 0) {
scaleX = props.user.avatarDecorations[0].flipH ? -1 : 1;
} else {
scaleX = 1;
}
function getDecorationScale(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
const scaleX = decoration.flipH ? -1 : 1;
return scaleX === 1 ? undefined : `${scaleX} 1`;
}
let color = $ref<string | undefined>();
function getDecorationOffset(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
const offsetX = decoration.offsetX ?? 0;
const offsetY = decoration.offsetY ?? 0;
return offsetX === 0 && offsetY === 0 ? undefined : `${offsetX * 100}% ${offsetY * 100}%`;
}
const color = ref<string | undefined>();
watch(() => props.user.avatarBlurhash, () => {
color = extractAvgColorFromBlurhash(props.user.avatarBlurhash);
color.value = extractAvgColorFromBlurhash(props.user.avatarBlurhash);
}, {
immediate: true,
});

View file

@ -19,12 +19,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, inject } from 'vue';
import { computed, inject, ref } from 'vue';
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
import { defaultStore } from '@/store.js';
import { customEmojisMap } from '@/custom-emojis.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';
const props = defineProps<{
@ -71,7 +72,7 @@ const url = computed(() => {
});
const alt = computed(() => `:${customEmojiName.value}:`);
let errored = $ref(url.value == null);
const errored = ref(url.value == null);
function onClick(ev: MouseEvent) {
if (props.menu) {
@ -90,6 +91,7 @@ function onClick(ev: MouseEvent) {
icon: 'ph-smiley ph-bold ph-lg',
action: () => {
react(`:${props.name}:`);
sound.play('reaction');
},
}] : [])], ev.currentTarget ?? ev.target);
}

View file

@ -16,6 +16,7 @@ import { defaultStore } from '@/store.js';
import { 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';
const props = defineProps<{
@ -56,6 +57,7 @@ function onClick(ev: MouseEvent) {
icon: 'ph-smiley ph-bold ph-lg',
action: () => {
react(props.emoji);
sound.play('reaction');
},
}] : [])], ev.currentTarget ?? ev.target);
}

View file

@ -0,0 +1,53 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div ref="rootEl" :class="$style.root">
<div v-if="!showing" :class="$style.placeholder"></div>
<slot v-else></slot>
</div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, onActivated, onBeforeUnmount, ref, shallowRef } from 'vue';
const rootEl = shallowRef<HTMLDivElement>();
const showing = ref(false);
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
showing.value = true;
}
},
);
onMounted(() => {
nextTick(() => {
observer.observe(rootEl.value!);
});
});
onActivated(() => {
nextTick(() => {
observer.observe(rootEl.value!);
});
});
onBeforeUnmount(() => {
observer.disconnect();
});
</script>
<style lang="scss" module>
.root {
display: block;
}
.placeholder {
display: block;
min-height: 150px;
}
</style>

View file

@ -37,7 +37,7 @@ type MfmProps = {
isNote?: boolean;
emojiUrls?: string[];
rootScale?: number;
nyaize: boolean | 'respect';
nyaize?: boolean | 'respect';
parsedNodes?: mfm.MfmNode[] | null;
enableEmojiMenu?: boolean;
enableEmojiMenuReaction?: boolean;
@ -110,26 +110,30 @@ export default function(props: MfmProps) {
case 'fn': {
// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
let style;
let style: string | undefined;
switch (token.props.name) {
case 'tada': {
const speed = validTime(token.props.args.speed) ?? '1s';
style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : '');
const delay = validTime(token.props.args.delay) ?? '0s';
style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both; animation-delay: ${delay};` : '');
break;
}
case 'jelly': {
const speed = validTime(token.props.args.speed) ?? '1s';
style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
const delay = validTime(token.props.args.delay) ?? '0s';
style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both; animation-delay: ${delay};` : '');
break;
}
case 'twitch': {
const speed = validTime(token.props.args.speed) ?? '0.5s';
style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : '';
const delay = validTime(token.props.args.delay) ?? '0s';
style = useAnim ? `animation: mfm-twitch ${speed} ease infinite; animation-delay: ${delay};` : '';
break;
}
case 'shake': {
const speed = validTime(token.props.args.speed) ?? '0.5s';
style = useAnim ? `animation: mfm-shake ${speed} ease infinite;` : '';
const delay = validTime(token.props.args.delay) ?? '0s';
style = useAnim ? `animation: mfm-shake ${speed} ease infinite; animation-delay: ${delay};` : '';
break;
}
case 'spin': {
@ -142,17 +146,20 @@ export default function(props: MfmProps) {
token.props.args.y ? 'mfm-spinY' :
'mfm-spin';
const speed = validTime(token.props.args.speed) ?? '1.5s';
style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
const delay = validTime(token.props.args.delay) ?? '0s';
style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction}; animation-delay: ${delay};` : '';
break;
}
case 'jump': {
const speed = validTime(token.props.args.speed) ?? '0.75s';
style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : '';
const delay = validTime(token.props.args.delay) ?? '0s';
style = useAnim ? `animation: mfm-jump ${speed} linear infinite; animation-delay: ${delay};` : '';
break;
}
case 'bounce': {
const speed = validTime(token.props.args.speed) ?? '0.75s';
style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
const delay = validTime(token.props.args.delay) ?? '0s';
style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom; animation-delay: ${delay};` : '';
break;
}
case 'flip': {
@ -202,7 +209,8 @@ export default function(props: MfmProps) {
}, genEl(token.children, scale));
}
const speed = validTime(token.props.args.speed) ?? '1s';
style = `animation: mfm-rainbow ${speed} linear infinite;`;
const delay = validTime(token.props.args.delay) ?? '0s';
style = `animation: mfm-rainbow ${speed} linear infinite; animation-delay: ${delay};`;
break;
}
case 'sparkle': {
@ -249,11 +257,17 @@ export default function(props: MfmProps) {
case 'ruby': {
if (token.children.length === 1) {
const child = token.children[0];
const text = child.type === 'text' ? child.props.text : '';
let text = child.type === 'text' ? child.props.text : '';
if (!disableNyaize && shouldNyaize) {
text = doNyaize(text);
}
return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]);
} else {
const rt = token.children.at(-1)!;
const text = rt.type === 'text' ? rt.props.text : '';
let text = rt.type === 'text' ? rt.props.text : '';
if (!disableNyaize && shouldNyaize) {
text = doNyaize(text);
}
return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]);
}
}
@ -275,7 +289,7 @@ export default function(props: MfmProps) {
]);
}
}
if (style == null) {
if (style === undefined) {
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
} else {
return h('span', {

View file

@ -50,23 +50,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, inject } from 'vue';
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 { globalEvents } from '@/events.js';
import { injectPageMetadata } from '@/scripts/page-metadata.js';
import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
import { PageHeaderItem } from '@/types/page-header.js';
const props = withDefaults(defineProps<{
tabs?: Tab[];
tab?: string;
actions?: {
text: string;
icon: string;
highlighted?: boolean;
handler: (ev: MouseEvent) => void;
}[];
actions?: PageHeaderItem[] | null;
thin?: boolean;
displayMyAvatar?: boolean;
displayBackButton?: boolean;
@ -85,13 +81,13 @@ const metadata = injectPageMetadata();
const hideTitle = inject('shouldOmitHeaderTitle', false);
const thin_ = props.thin || inject('shouldHeaderThin', false);
let el = $shallowRef<HTMLElement | undefined>(undefined);
const el = shallowRef<HTMLElement | undefined>(undefined);
const bg = ref<string | undefined>(undefined);
let narrow = $ref(false);
const hasTabs = $computed(() => props.tabs.length > 0);
const hasActions = $computed(() => props.actions && props.actions.length > 0);
const show = $computed(() => {
return !hideTitle || hasTabs || hasActions;
const narrow = ref(false);
const hasTabs = computed(() => props.tabs.length > 0);
const hasActions = computed(() => props.actions && props.actions.length > 0);
const show = computed(() => {
return !hideTitle || hasTabs.value || hasActions.value;
});
const preventDrag = (ev: TouchEvent) => {
@ -99,8 +95,8 @@ const preventDrag = (ev: TouchEvent) => {
};
const top = () => {
if (el) {
scrollToTop(el as HTMLElement, { behavior: 'smooth' });
if (el.value) {
scrollToTop(el.value as HTMLElement, { behavior: 'smooth' });
}
};
@ -131,14 +127,14 @@ onMounted(() => {
calcBg();
globalEvents.on('themeChanged', calcBg);
if (el && el.parentElement) {
narrow = el.parentElement.offsetWidth < 500;
if (el.value && el.value.parentElement) {
narrow.value = el.value.parentElement.offsetWidth < 500;
ro = new ResizeObserver((entries, observer) => {
if (el && el.parentElement && document.body.contains(el as HTMLElement)) {
narrow = el.parentElement.offsetWidth < 500;
if (el.value && el.value.parentElement && document.body.contains(el.value as HTMLElement)) {
narrow.value = el.value.parentElement.offsetWidth < 500;
}
});
ro.observe(el.parentElement as HTMLElement);
ro.observe(el.value.parentElement as HTMLElement);
}
});

View file

@ -18,36 +18,36 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue';
import { $$ } from 'vue/macros';
import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, shallowRef } from 'vue';
import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const';
const rootEl = $shallowRef<HTMLElement>();
const headerEl = $shallowRef<HTMLElement>();
const footerEl = $shallowRef<HTMLElement>();
const bodyEl = $shallowRef<HTMLElement>();
const rootEl = shallowRef<HTMLElement>();
const headerEl = shallowRef<HTMLElement>();
const footerEl = shallowRef<HTMLElement>();
const bodyEl = shallowRef<HTMLElement>();
let headerHeight = $ref<string | undefined>();
let childStickyTop = $ref(0);
const headerHeight = ref<string | undefined>();
const childStickyTop = ref(0);
const parentStickyTop = inject<Ref<number>>(CURRENT_STICKY_TOP, ref(0));
provide(CURRENT_STICKY_TOP, $$(childStickyTop));
provide(CURRENT_STICKY_TOP, childStickyTop);
let footerHeight = $ref<string | undefined>();
let childStickyBottom = $ref(0);
const footerHeight = ref<string | undefined>();
const childStickyBottom = ref(0);
const parentStickyBottom = inject<Ref<number>>(CURRENT_STICKY_BOTTOM, ref(0));
provide(CURRENT_STICKY_BOTTOM, $$(childStickyBottom));
provide(CURRENT_STICKY_BOTTOM, childStickyBottom);
const calc = () => {
// KeepAlive null
if (headerEl != null) {
childStickyTop = parentStickyTop.value + headerEl.offsetHeight;
headerHeight = headerEl.offsetHeight.toString();
if (headerEl.value != null) {
childStickyTop.value = parentStickyTop.value + headerEl.value.offsetHeight;
headerHeight.value = headerEl.value.offsetHeight.toString();
}
// KeepAlive null
if (footerEl != null) {
childStickyBottom = parentStickyBottom.value + footerEl.offsetHeight;
footerHeight = footerEl.offsetHeight.toString();
if (footerEl.value != null) {
childStickyBottom.value = parentStickyBottom.value + footerEl.value.offsetHeight;
footerHeight.value = footerEl.value.offsetHeight.toString();
}
};
@ -62,28 +62,28 @@ onMounted(() => {
watch([parentStickyTop, parentStickyBottom], calc);
watch($$(childStickyTop), () => {
bodyEl.style.setProperty('--stickyTop', `${childStickyTop}px`);
watch(childStickyTop, () => {
bodyEl.value.style.setProperty('--stickyTop', `${childStickyTop.value}px`);
}, {
immediate: true,
});
watch($$(childStickyBottom), () => {
bodyEl.style.setProperty('--stickyBottom', `${childStickyBottom}px`);
watch(childStickyBottom, () => {
bodyEl.value.style.setProperty('--stickyBottom', `${childStickyBottom.value}px`);
}, {
immediate: true,
});
headerEl.style.position = 'sticky';
headerEl.style.top = 'var(--stickyTop, 0)';
headerEl.style.zIndex = '1000';
headerEl.value.style.position = 'sticky';
headerEl.value.style.top = 'var(--stickyTop, 0)';
headerEl.value.style.zIndex = '1000';
footerEl.style.position = 'sticky';
footerEl.style.bottom = 'var(--stickyBottom, 0)';
footerEl.style.zIndex = '1000';
footerEl.value.style.position = 'sticky';
footerEl.value.style.bottom = 'var(--stickyBottom, 0)';
footerEl.value.style.zIndex = '1000';
observer.observe(headerEl);
observer.observe(footerEl);
observer.observe(headerEl.value);
observer.observe(footerEl.value);
});
onUnmounted(() => {
@ -91,6 +91,6 @@ onUnmounted(() => {
});
defineExpose({
rootEl: $$(rootEl),
rootEl: rootEl,
});
</script>

View file

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import isChromatic from 'chromatic/isChromatic';
import { onMounted, onUnmounted } from 'vue';
import { onMounted, onUnmounted, ref, computed } from 'vue';
import { i18n } from '@/i18n.js';
import { dateTimeFormat } from '@/scripts/intl-const.js';
@ -28,35 +28,48 @@ const props = withDefaults(defineProps<{
mode: 'relative',
});
const _time = props.time == null ? NaN :
typeof props.time === 'number' ? props.time :
(props.time instanceof Date ? props.time : new Date(props.time)).getTime();
function getDateSafe(n: Date | string | number) {
try {
if (n instanceof Date) {
return n;
}
return new Date(n);
} catch (err) {
return {
getTime: () => NaN,
};
}
}
// eslint-disable-next-line vue/no-setup-props-destructure
const _time = props.time == null ? NaN : getDateSafe(props.time).getTime();
const invalid = Number.isNaN(_time);
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
let now = $ref((props.origin ?? new Date()).getTime());
const ago = $computed(() => (now - _time) / 1000/*ms*/);
// eslint-disable-next-line vue/no-setup-props-destructure
const now = ref((props.origin ?? new Date()).getTime());
const ago = computed(() => (now.value - _time) / 1000/*ms*/);
const relative = $computed<string>(() => {
const relative = computed<string>(() => {
if (props.mode === 'absolute') return ''; // absoluterelative使
if (invalid) return i18n.ts._ago.invalid;
return (
ago >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago / 31536000).toString() }) :
ago >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago / 2592000).toString() }) :
ago >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago / 604800).toString() }) :
ago >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago / 86400).toString() }) :
ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago / 3600).toString() }) :
ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
ago >= -3 ? i18n.ts._ago.justNow :
ago < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago / 31536000).toString() }) :
ago < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago / 2592000).toString() }) :
ago < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago / 604800).toString() }) :
ago < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago / 86400).toString() }) :
ago < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago / 3600).toString() }) :
ago < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago / 60)).toString() }) :
i18n.t('_timeIn.seconds', { n: (~~(-ago % 60)).toString() })
ago.value >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago.value / 31536000).toString() }) :
ago.value >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago.value / 2592000).toString() }) :
ago.value >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago.value / 604800).toString() }) :
ago.value >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago.value / 86400).toString() }) :
ago.value >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago.value / 3600).toString() }) :
ago.value >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago.value / 60)).toString() }) :
ago.value >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago.value % 60)).toString() }) :
ago.value >= -3 ? i18n.ts._ago.justNow :
ago.value < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago.value / 31536000).toString() }) :
ago.value < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago.value / 2592000).toString() }) :
ago.value < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago.value / 604800).toString() }) :
ago.value < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago.value / 86400).toString() }) :
ago.value < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago.value / 3600).toString() }) :
ago.value < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago.value / 60)).toString() }) :
i18n.t('_timeIn.seconds', { n: (~~(-ago.value % 60)).toString() })
);
});
@ -64,8 +77,8 @@ let tickId: number;
let currentInterval: number;
function tick() {
now = (new Date()).getTime();
const nextInterval = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000;
now.value = (new Date()).getTime();
const nextInterval = ago.value < 60 ? 10000 : ago.value < 3600 ? 60000 : 180000;
if (currentInterval !== nextInterval) {
if (tickId) window.clearInterval(tickId);

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<component
:is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel" :target="target"
:is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel ?? 'nofollow noopener'" :target="target"
@contextmenu.stop="() => {}"
>
<template v-if="!self">

View file

@ -5,7 +5,6 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from '@storybook/jest';
import { userEvent, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3';
import { userDetailed } from '../../../.storybook/fakes';
import MkUserName from './MkUserName.vue';

View file

@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { inject, onBeforeUnmount, provide } from 'vue';
import { inject, onBeforeUnmount, provide, shallowRef, ref } from 'vue';
import { Resolved, Router } from '@/nirax';
import { defaultStore } from '@/store.js';
@ -46,16 +46,16 @@ function resolveNested(current: Resolved, d = 0): Resolved | null {
}
const current = resolveNested(router.current)!;
let currentPageComponent = $shallowRef(current.route.component);
let currentPageProps = $ref(current.props);
let key = $ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
const currentPageComponent = shallowRef(current.route.component);
const currentPageProps = ref(current.props);
const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
function onChange({ resolved, key: newKey }) {
const current = resolveNested(resolved);
if (current == null) return;
currentPageComponent = current.route.component;
currentPageProps = current.props;
key = current.route.path + JSON.stringify(Object.fromEntries(current.props));
currentPageComponent.value = current.route.component;
currentPageProps.value = current.props;
key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props));
}
router.addListener('change', onChange);