merge: upstream
This commit is contained in:
commit
4dd23a3793
217 changed files with 6773 additions and 2275 deletions
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
|
||||
<MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user?.avatarBlurhash" :cover="true" :onlyAvgColor="true"/>
|
||||
<MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user.avatarBlurhash" :cover="true" :onlyAvgColor="true"/>
|
||||
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
|
||||
<div v-if="user.isCat" :class="[$style.ears]">
|
||||
<div :class="$style.earLeft">
|
||||
|
|
@ -23,6 +23,16 @@ 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=""
|
||||
>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
|
|
@ -47,22 +57,33 @@ const props = withDefaults(defineProps<{
|
|||
link?: boolean;
|
||||
preview?: boolean;
|
||||
indicator?: boolean;
|
||||
decoration?: {
|
||||
url: string;
|
||||
angle?: number;
|
||||
flipH?: boolean;
|
||||
flipV?: boolean;
|
||||
};
|
||||
forceShowDecoration?: boolean;
|
||||
}>(), {
|
||||
target: null,
|
||||
link: false,
|
||||
preview: false,
|
||||
indicator: false,
|
||||
decoration: undefined,
|
||||
forceShowDecoration: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'click', v: MouseEvent): void;
|
||||
}>();
|
||||
|
||||
const showDecoration = props.forceShowDecoration || defaultStore.state.showAvatarDecorations;
|
||||
|
||||
const bound = $computed(() => props.link
|
||||
? { to: userPage(props.user), target: props.target }
|
||||
: {});
|
||||
|
||||
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages
|
||||
const url = $computed(() => (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.enableDataSaverMode)
|
||||
? getStaticImageUrl(props.user.avatarUrl)
|
||||
: props.user.avatarUrl);
|
||||
|
||||
|
|
@ -71,6 +92,30 @@ 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
return scaleX === 1 ? undefined : `${scaleX} 1`;
|
||||
}
|
||||
|
||||
let color = $ref<string | undefined>();
|
||||
|
||||
watch(() => props.user.avatarBlurhash, () => {
|
||||
|
|
@ -134,7 +179,7 @@ watch(() => props.user.avatarBlurhash, () => {
|
|||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 20%;
|
||||
|
|
@ -278,4 +323,13 @@ watch(() => props.user.avatarBlurhash, () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.decoration {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
32
packages/frontend/src/components/global/MkFooterSpacer.vue
Normal file
32
packages/frontend/src/components/global/MkFooterSpacer.vue
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.spacer, defaultStore.reactiveState.darkMode ? $style.dark : $style.light]"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defaultStore } from '@/store.js';
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.spacer {
|
||||
box-sizing: border-box;
|
||||
padding: 32px;
|
||||
margin: 0 auto;
|
||||
height: 300px;
|
||||
background-clip: content-box;
|
||||
background-size: auto auto;
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
|
||||
&.light {
|
||||
background-image: repeating-linear-gradient(135deg, transparent, transparent 16px, #00000026 16px, #00000026 20px );
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background-image: repeating-linear-gradient(135deg, transparent, transparent 16px, #FFFFFF16 16px, #FFFFFF16 20px );
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -17,6 +17,7 @@ import MkSparkle from '@/components/MkSparkle.vue';
|
|||
import MkA from '@/components/global/MkA.vue';
|
||||
import { host } from '@/config.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { nyaize as doNyaize } from '@/scripts/nyaize.js';
|
||||
|
||||
const QUOTE_STYLE = `
|
||||
display: block;
|
||||
|
|
@ -27,21 +28,27 @@ border-left: solid 3px var(--fg);
|
|||
opacity: 0.7;
|
||||
`.split('\n').join(' ');
|
||||
|
||||
export default function(props: {
|
||||
type MfmProps = {
|
||||
text: string;
|
||||
plain?: boolean;
|
||||
nowrap?: boolean;
|
||||
author?: Misskey.entities.UserLite;
|
||||
i?: Misskey.entities.UserLite;
|
||||
i?: Misskey.entities.UserLite | null;
|
||||
isNote?: boolean;
|
||||
emojiUrls?: string[];
|
||||
rootScale?: number;
|
||||
}) {
|
||||
const isNote = props.isNote !== undefined ? props.isNote : true;
|
||||
nyaize: boolean | 'account';
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function(props: MfmProps) {
|
||||
const isNote = props.isNote ?? true;
|
||||
const shouldNyaize = props.nyaize ? props.nyaize === 'account' ? props.author?.isCat ? props.author?.speakAsCat : false : false : false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (props.text == null || props.text === '') return;
|
||||
|
||||
const ast = (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
|
||||
const rootAst = (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
|
||||
|
||||
const validTime = (t: string | null | undefined) => {
|
||||
if (t == null) return null;
|
||||
|
|
@ -54,11 +61,15 @@ export default function(props: {
|
|||
* Gen Vue Elements from MFM AST
|
||||
* @param ast MFM AST
|
||||
* @param scale How times large the text is
|
||||
* @param disableNyaize Whether nyaize is disabled or not
|
||||
*/
|
||||
const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => {
|
||||
const genEl = (ast: mfm.MfmNode[], scale: number, disableNyaize = false) => ast.map((token): VNode | string | (VNode | string)[] => {
|
||||
switch (token.type) {
|
||||
case 'text': {
|
||||
const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
|
||||
let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
|
||||
if (!disableNyaize && shouldNyaize) {
|
||||
text = doNyaize(text);
|
||||
}
|
||||
|
||||
if (!props.plain) {
|
||||
const res: (VNode | string)[] = [];
|
||||
|
|
@ -260,7 +271,7 @@ export default function(props: {
|
|||
key: Math.random(),
|
||||
url: token.props.url,
|
||||
rel: 'nofollow noopener',
|
||||
}, genEl(token.children, scale))];
|
||||
}, genEl(token.children, scale, true))];
|
||||
}
|
||||
|
||||
case 'mention': {
|
||||
|
|
@ -299,11 +310,11 @@ export default function(props: {
|
|||
if (!props.nowrap) {
|
||||
return [h('div', {
|
||||
style: QUOTE_STYLE,
|
||||
}, genEl(token.children, scale))];
|
||||
}, genEl(token.children, scale, true))];
|
||||
} else {
|
||||
return [h('span', {
|
||||
style: QUOTE_STYLE,
|
||||
}, genEl(token.children, scale))];
|
||||
}, genEl(token.children, scale, true))];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -358,7 +369,7 @@ export default function(props: {
|
|||
}
|
||||
|
||||
case 'plain': {
|
||||
return [h('span', genEl(token.children, scale))];
|
||||
return [h('span', genEl(token.children, scale, true))];
|
||||
}
|
||||
|
||||
default: {
|
||||
|
|
@ -373,5 +384,5 @@ export default function(props: {
|
|||
return h('span', {
|
||||
// https://codeday.me/jp/qa/20190424/690106.html
|
||||
style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;',
|
||||
}, genEl(ast, props.rootScale ?? 1));
|
||||
}, genEl(rootAst, props.rootScale ?? 1));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,9 +134,11 @@ async function enter(el: HTMLElement) {
|
|||
|
||||
setTimeout(renderTab, 170);
|
||||
}
|
||||
|
||||
function afterEnter(el: HTMLElement) {
|
||||
//el.style.width = '';
|
||||
}
|
||||
|
||||
async function leave(el: HTMLElement) {
|
||||
const elementWidth = el.getBoundingClientRect().width;
|
||||
el.style.width = elementWidth + 'px';
|
||||
|
|
@ -145,6 +147,7 @@ async function leave(el: HTMLElement) {
|
|||
el.style.width = '0';
|
||||
el.style.paddingLeft = '0';
|
||||
}
|
||||
|
||||
function afterLeave(el: HTMLElement) {
|
||||
el.style.width = '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<time :title="absolute">
|
||||
<time :title="absolute" :class="{ [$style.old1]: colored && (ago > 60 * 60 * 24 * 90), [$style.old2]: colored && (ago > 60 * 60 * 24 * 180) }">
|
||||
<template v-if="invalid">{{ i18n.ts._ago.invalid }}</template>
|
||||
<template v-else-if="mode === 'relative'">{{ relative }}</template>
|
||||
<template v-else-if="mode === 'absolute'">{{ absolute }}</template>
|
||||
|
|
@ -22,6 +22,7 @@ const props = withDefaults(defineProps<{
|
|||
time: Date | string | number | null;
|
||||
origin?: Date | null;
|
||||
mode?: 'relative' | 'absolute' | 'detail';
|
||||
colored?: boolean;
|
||||
}>(), {
|
||||
origin: isChromatic() ? new Date('2023-04-01T00:00:00Z') : null,
|
||||
mode: 'relative',
|
||||
|
|
@ -75,3 +76,13 @@ if (!invalid && props.origin === null && (props.mode === 'relative' || props.mod
|
|||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.old1 {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.old1.old2 {
|
||||
color: var(--error);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -31,23 +31,28 @@ import * as os from '@/os.js';
|
|||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||
import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
url: string;
|
||||
rel?: string;
|
||||
}>();
|
||||
showUrlPreview?: boolean;
|
||||
}>(), {
|
||||
showUrlPreview: true,
|
||||
});
|
||||
|
||||
const self = props.url.startsWith(local);
|
||||
const url = new URL(props.url);
|
||||
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('invalid url');
|
||||
const el = ref();
|
||||
|
||||
useTooltip(el, (showing) => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
|
||||
showing,
|
||||
url: props.url,
|
||||
source: el.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
if (props.showUrlPreview) {
|
||||
useTooltip(el, (showing) => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
|
||||
showing,
|
||||
url: props.url,
|
||||
source: el.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
}
|
||||
|
||||
const schema = url.protocol;
|
||||
const hostname = decodePunycode(url.hostname);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue