wip
This commit is contained in:
		
							
								
								
									
										98
									
								
								packages/embed/src/components/EmCustomEmoji.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								packages/embed/src/components/EmCustomEmoji.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,98 @@
 | 
			
		||||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<img
 | 
			
		||||
	v-if="errored && fallbackToImage"
 | 
			
		||||
	:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
 | 
			
		||||
	src="/client-assets/dummy.png"
 | 
			
		||||
	:title="alt"
 | 
			
		||||
/>
 | 
			
		||||
<span v-else-if="errored">:{{ customEmojiName }}:</span>
 | 
			
		||||
<img
 | 
			
		||||
	v-else
 | 
			
		||||
	:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
 | 
			
		||||
	:src="url"
 | 
			
		||||
	:alt="alt"
 | 
			
		||||
	:title="alt"
 | 
			
		||||
	decoding="async"
 | 
			
		||||
	@error="errored = true"
 | 
			
		||||
	@load="errored = false"
 | 
			
		||||
/>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, inject, ref } from 'vue';
 | 
			
		||||
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
 | 
			
		||||
import { customEmojisMap } from '@/custom-emojis.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	name: string;
 | 
			
		||||
	normal?: boolean;
 | 
			
		||||
	noStyle?: boolean;
 | 
			
		||||
	host?: string | null;
 | 
			
		||||
	url?: string;
 | 
			
		||||
	useOriginalSize?: boolean;
 | 
			
		||||
	menu?: boolean;
 | 
			
		||||
	menuReaction?: boolean;
 | 
			
		||||
	fallbackToImage?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', ''));
 | 
			
		||||
const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
 | 
			
		||||
 | 
			
		||||
const rawUrl = computed(() => {
 | 
			
		||||
	if (props.url) {
 | 
			
		||||
		return props.url;
 | 
			
		||||
	}
 | 
			
		||||
	if (isLocal.value) {
 | 
			
		||||
		return customEmojisMap.get(customEmojiName.value)?.url ?? null;
 | 
			
		||||
	}
 | 
			
		||||
	return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const url = computed(() => {
 | 
			
		||||
	if (rawUrl.value == null) return undefined;
 | 
			
		||||
 | 
			
		||||
	const proxied =
 | 
			
		||||
		(rawUrl.value.startsWith('/emoji/') || (props.useOriginalSize && isLocal.value))
 | 
			
		||||
			? rawUrl.value
 | 
			
		||||
			: getProxiedImageUrl(
 | 
			
		||||
				rawUrl.value,
 | 
			
		||||
				props.useOriginalSize ? undefined : 'emoji',
 | 
			
		||||
				false,
 | 
			
		||||
				true,
 | 
			
		||||
			);
 | 
			
		||||
	return proxied;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const alt = computed(() => `:${customEmojiName.value}:`);
 | 
			
		||||
const errored = ref(url.value == null);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	height: 2em;
 | 
			
		||||
	vertical-align: middle;
 | 
			
		||||
	transition: transform 0.2s ease;
 | 
			
		||||
 | 
			
		||||
	&:hover {
 | 
			
		||||
		transform: scale(1.2);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.normal {
 | 
			
		||||
	height: 1.25em;
 | 
			
		||||
	vertical-align: -0.25em;
 | 
			
		||||
 | 
			
		||||
	&:hover {
 | 
			
		||||
		transform: none;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.noStyle {
 | 
			
		||||
	height: auto !important;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										28
									
								
								packages/embed/src/components/EmEmoji.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								packages/embed/src/components/EmEmoji.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<span :alt="props.emoji">{{ colorizedNativeEmoji }}</span>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { colorizeEmoji } from '@/scripts/emojilist.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	emoji: string;
 | 
			
		||||
	menu?: boolean;
 | 
			
		||||
	menuReaction?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji));
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	height: 1.25em;
 | 
			
		||||
	vertical-align: -0.25em;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										107
									
								
								packages/embed/src/components/EmTime.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								packages/embed/src/components/EmTime.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<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>
 | 
			
		||||
	<template v-else-if="mode === 'detail'">{{ absolute }} ({{ relative }})</template>
 | 
			
		||||
</time>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, onUnmounted, ref, computed } from 'vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { dateTimeFormat } from '@/scripts/intl-const.js';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	time: Date | string | number | null;
 | 
			
		||||
	origin?: Date | null;
 | 
			
		||||
	mode?: 'relative' | 'absolute' | 'detail';
 | 
			
		||||
	colored?: boolean;
 | 
			
		||||
}>(), {
 | 
			
		||||
	origin: null,
 | 
			
		||||
	mode: 'relative',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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-reactivity-loss
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
 | 
			
		||||
const now = ref(props.origin?.getTime() ?? Date.now());
 | 
			
		||||
const ago = computed(() => (now.value - _time) / 1000/*ms*/);
 | 
			
		||||
 | 
			
		||||
const relative = computed<string>(() => {
 | 
			
		||||
	if (props.mode === 'absolute') return ''; // absoluteではrelativeを使わないので計算しない
 | 
			
		||||
	if (invalid) return i18n.ts._ago.invalid;
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		ago.value >= 31536000 ? i18n.tsx._ago.yearsAgo({ n: Math.round(ago.value / 31536000).toString() }) :
 | 
			
		||||
		ago.value >= 2592000 ? i18n.tsx._ago.monthsAgo({ n: Math.round(ago.value / 2592000).toString() }) :
 | 
			
		||||
		ago.value >= 604800 ? i18n.tsx._ago.weeksAgo({ n: Math.round(ago.value / 604800).toString() }) :
 | 
			
		||||
		ago.value >= 86400 ? i18n.tsx._ago.daysAgo({ n: Math.round(ago.value / 86400).toString() }) :
 | 
			
		||||
		ago.value >= 3600 ? i18n.tsx._ago.hoursAgo({ n: Math.round(ago.value / 3600).toString() }) :
 | 
			
		||||
		ago.value >= 60 ? i18n.tsx._ago.minutesAgo({ n: (~~(ago.value / 60)).toString() }) :
 | 
			
		||||
		ago.value >= 10 ? i18n.tsx._ago.secondsAgo({ n: (~~(ago.value % 60)).toString() }) :
 | 
			
		||||
		ago.value >= -3 ? i18n.ts._ago.justNow :
 | 
			
		||||
		ago.value < -31536000 ? i18n.tsx._timeIn.years({ n: Math.round(-ago.value / 31536000).toString() }) :
 | 
			
		||||
		ago.value < -2592000 ? i18n.tsx._timeIn.months({ n: Math.round(-ago.value / 2592000).toString() }) :
 | 
			
		||||
		ago.value < -604800 ? i18n.tsx._timeIn.weeks({ n: Math.round(-ago.value / 604800).toString() }) :
 | 
			
		||||
		ago.value < -86400 ? i18n.tsx._timeIn.days({ n: Math.round(-ago.value / 86400).toString() }) :
 | 
			
		||||
		ago.value < -3600 ? i18n.tsx._timeIn.hours({ n: Math.round(-ago.value / 3600).toString() }) :
 | 
			
		||||
		ago.value < -60 ? i18n.tsx._timeIn.minutes({ n: (~~(-ago.value / 60)).toString() }) :
 | 
			
		||||
		i18n.tsx._timeIn.seconds({ n: (~~(-ago.value % 60)).toString() })
 | 
			
		||||
	);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
let tickId: number;
 | 
			
		||||
let currentInterval: number;
 | 
			
		||||
 | 
			
		||||
function tick() {
 | 
			
		||||
	now.value = Date.now();
 | 
			
		||||
	const nextInterval = ago.value < 60 ? 10000 : ago.value < 3600 ? 60000 : 180000;
 | 
			
		||||
 | 
			
		||||
	if (currentInterval !== nextInterval) {
 | 
			
		||||
		if (tickId) window.clearInterval(tickId);
 | 
			
		||||
		currentInterval = nextInterval;
 | 
			
		||||
		tickId = window.setInterval(tick, nextInterval);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (!invalid && props.origin === null && (props.mode === 'relative' || props.mode === 'detail')) {
 | 
			
		||||
	onMounted(() => {
 | 
			
		||||
		tick();
 | 
			
		||||
	});
 | 
			
		||||
	onUnmounted(() => {
 | 
			
		||||
		if (tickId) window.clearInterval(tickId);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.old1 {
 | 
			
		||||
	color: var(--warn);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.old1.old2 {
 | 
			
		||||
	color: var(--error);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
		Reference in New Issue
	
	Block a user