Merge branch 'main' into shuuji3/feat/emoji-reactions

This commit is contained in:
TAKAHASHI Shuuji 2025-08-14 02:01:10 +09:00 committed by GitHub
commit e174f23b66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 4643 additions and 2374 deletions

View file

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
const { as = 'div', active } = defineProps<{ const { as = 'div', active } = defineProps<{
as: any as?: string
active: boolean active: boolean
}>() }>()

View file

@ -35,15 +35,16 @@ const containerClass = computed(() => {
'backdrop-blur': !getPreferences(userSettings, 'optimizeForLowPerformanceDevice'), 'backdrop-blur': !getPreferences(userSettings, 'optimizeForLowPerformanceDevice'),
}" }"
> >
<div flex justify-between px5 py2 :class="{ 'xl:hidden': $route.name !== 'tag' }" class="native:xl:flex" border="b base"> <div flex justify-between gap-2 min-h-53px px5 py1 :class="{ 'xl:hidden': $route.name !== 'tag' }" class="native:xl:flex" border="b base">
<div flex gap-3 items-center :overflow-hidden="!noOverflowHidden ? '' : false" py2 w-full> <div flex gap-2 items-center :overflow-hidden="!noOverflowHidden ? '' : false" w-full>
<NuxtLink <button
v-if="backOnSmallScreen || back" flex="~ gap1" items-center btn-text p-0 xl:hidden v-if="backOnSmallScreen || back"
btn-text flex items-center ms="-3" p-3 xl:hidden
:aria-label="$t('nav.back')" :aria-label="$t('nav.back')"
@click="$router.go(-1)" @click="$router.go(-1)"
> >
<div i-ri:arrow-left-line class="rtl-flip" /> <div text-lg i-ri:arrow-left-line class="rtl-flip" />
</NuxtLink> </button>
<div :truncate="!noOverflowHidden ? '' : false" flex w-full data-tauri-drag-region class="native-mac:justify-start native-mac:text-center"> <div :truncate="!noOverflowHidden ? '' : false" flex w-full data-tauri-drag-region class="native-mac:justify-start native-mac:text-center">
<slot name="title" /> <slot name="title" />
</div> </div>

View file

@ -33,17 +33,16 @@ router.afterEach(() => {
{{ $t('app_name') }} <sup text-sm italic mt-1>{{ env === 'release' ? 'alpha' : env }}</sup> {{ $t('app_name') }} <sup text-sm italic mt-1>{{ env === 'release' ? 'alpha' : env }}</sup>
</div> </div>
</NuxtLink> </NuxtLink>
<div <div hidden xl:flex items-center me-6 mt-2 gap-1>
hidden xl:flex items-center me-8 mt-2 gap-1 <CommonTooltip :content="$t('nav.back')" :distance="0">
> <button
<CommonTooltip :content="$t('nav.back')"> type="button"
<NuxtLink
:aria-label="$t('nav.back')" :aria-label="$t('nav.back')"
:class="{ 'pointer-events-none op0': !back || back === '/', 'xl:flex': $route.name !== 'tag' }" btn-text p-3 :class="{ 'pointer-events-none op0': !back || back === '/', 'xl:flex': $route.name !== 'tag' }"
@click="$router.go(-1)" @click="$router.go(-1)"
> >
<div text-xl i-ri:arrow-left-line class="rtl-flip" btn-text /> <div text-xl i-ri:arrow-left-line class="rtl-flip" />
</NuxtLink> </button>
</CommonTooltip> </CommonTooltip>
</div> </div>
</div> </div>

View file

@ -29,7 +29,7 @@ const emit = defineEmits<{
const { t } = useI18n() const { t } = useI18n()
const { threadItems, threadIsActive, publishThread } = threadComposer ?? useThreadComposer(draftKey) const { threadItems, threadIsActive, publishThread, threadIsSending } = threadComposer ?? useThreadComposer(draftKey)
const draft = computed({ const draft = computed({
get: () => threadItems.value[draftItemIndex], get: () => threadItems.value[draftItemIndex],
@ -236,6 +236,59 @@ function stopQuestionMarkPropagation(e: KeyboardEvent) {
if (e.key === '?') if (e.key === '?')
e.stopImmediatePropagation() e.stopImmediatePropagation()
} }
const userSettings = useUserSettings()
const optimizeForLowPerformanceDevice = computed(() => getPreferences(userSettings.value, 'optimizeForLowPerformanceDevice'))
const languageDetectorInGlobalThis = 'LanguageDetector' in globalThis
let supportsLanguageDetector = !optimizeForLowPerformanceDevice.value && languageDetectorInGlobalThis && await (globalThis as any).LanguageDetector.availability() === 'available'
let languageDetector: { detect: (arg0: string, option: { signal: AbortSignal }) => any }
// If the API is supported, but the model not loaded yet
if (languageDetectorInGlobalThis && !supportsLanguageDetector) {
// trigger the model download
(globalThis as any).LanguageDetector.create().then((_languageDetector: { detect: (arg0: string) => any }) => {
supportsLanguageDetector = true
languageDetector = _languageDetector
})
}
function countLetters(text: string) {
const segmenter = new Intl.Segmenter('und', { granularity: 'grapheme' })
const letters = [...segmenter.segment(text)]
return letters.length
}
let detectLanguageAbortController = new AbortController()
const detectLanguage = useDebounceFn(async () => {
if (!supportsLanguageDetector) {
return
}
if (!languageDetector) {
// maybe we dont want to mess with this with abort....
languageDetector = await (globalThis as any).LanguageDetector.create()
}
// we stop previously running language detection process
detectLanguageAbortController.abort()
detectLanguageAbortController = new AbortController()
const text = htmlToText(editor.value?.getHTML() || '')
if (!text || countLetters(text) <= 5) {
draft.value.params.language = preferredLanguage.value
return
}
try {
const detectedLanguage = (await languageDetector.detect(text, { signal: detectLanguageAbortController.signal }))[0].detectedLanguage
draft.value.params.language = detectedLanguage === 'und' ? preferredLanguage.value : detectedLanguage.substring(0, 2)
}
catch (e) {
// if error or abort we end up there
if ((e as Error).name !== 'AbortError') {
console.error(e)
}
draft.value.params.language = preferredLanguage.value
}
}, 500)
</script> </script>
<template> <template>
@ -310,6 +363,7 @@ function stopQuestionMarkPropagation(e: KeyboardEvent) {
}" }"
@keydown="stopQuestionMarkPropagation" @keydown="stopQuestionMarkPropagation"
@keydown.esc.prevent="editor?.commands.blur()" @keydown.esc.prevent="editor?.commands.blur()"
@keyup="detectLanguage"
/> />
</div> </div>
@ -523,18 +577,18 @@ function stopQuestionMarkPropagation(e: KeyboardEvent) {
<button <button
v-if="!threadIsActive || isFinalItemOfThread" v-if="!threadIsActive || isFinalItemOfThread"
btn-solid rounded-3 text-sm w-full flex="~ gap1" items-center md:w-fit class="publish-button" btn-solid rounded-3 text-sm w-full flex="~ gap1" items-center md:w-fit class="publish-button"
:aria-disabled="isPublishDisabled || isExceedingCharacterLimit" aria-describedby="publish-tooltip" :aria-disabled="isPublishDisabled || isExceedingCharacterLimit || threadIsSending" aria-describedby="publish-tooltip"
:disabled="isPublishDisabled || isExceedingCharacterLimit" :disabled="isPublishDisabled || isExceedingCharacterLimit || threadIsSending"
@click="publish" @click="publish"
> >
<span v-if="isSending" block animate-spin preserve-3d> <span v-if="isSending || threadIsSending" block animate-spin preserve-3d>
<div block i-ri:loader-2-fill /> <div block i-ri:loader-2-fill />
</span> </span>
<span v-if="failedMessages.length" block> <span v-if="failedMessages.length" block>
<div block i-carbon:face-dizzy-filled /> <div block i-carbon:face-dizzy-filled />
</span> </span>
<template v-if="threadIsActive"> <template v-if="threadIsActive">
<span>{{ $t('action.publish_thread') }} </span> <span>{{ !threadIsSending ? $t('action.publish_thread') : $t('state.publishing') }} </span>
</template> </template>
<template v-else> <template v-else>
<span v-if="draft.editingStatus">{{ $t('action.save_changes') }}</span> <span v-if="draft.editingStatus">{{ $t('action.save_changes') }}</span>

View file

@ -11,7 +11,7 @@ const {
withAction?: boolean withAction?: boolean
}>() }>()
const { translation } = useTranslation(status, getLanguageCode()) const { translation } = await useTranslation(status, getLanguageCode())
const emojisObject = useEmojisFallback(() => status.emojis) const emojisObject = useEmojisFallback(() => status.emojis)
const vnode = computed(() => { const vnode = computed(() => {

View file

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { mastodon } from 'masto' import type { mastodon } from 'masto'
const { actions = true, older, newer, hasOlder, hasNewer, main, ...props } = defineProps<{ const { actions = true, older, newer, hasOlder, hasNewer, main, account, ...props } = defineProps<{
status: mastodon.v1.Status status: mastodon.v1.Status
followedTag?: string | null followedTag?: string | null
actions?: boolean actions?: boolean
@ -20,6 +20,7 @@ const { actions = true, older, newer, hasOlder, hasNewer, main, ...props } = def
// When looking into a detailed view of a post, we can simplify the replying badges // When looking into a detailed view of a post, we can simplify the replying badges
// to the main expanded post // to the main expanded post
main?: mastodon.v1.Status main?: mastodon.v1.Status
account?: mastodon.v1.Account
}>() }>()
const userSettings = useUserSettings() const userSettings = useUserSettings()
@ -60,7 +61,10 @@ const timeago = useTimeAgo(() => status.value.createdAt, timeAgoOptions)
const isSelfReply = computed(() => status.value.inReplyToAccountId === status.value.account.id) const isSelfReply = computed(() => status.value.inReplyToAccountId === status.value.account.id)
const collapseRebloggedBy = computed(() => rebloggedBy.value?.id === status.value.account.id) const collapseRebloggedBy = computed(() => rebloggedBy.value?.id === status.value.account.id)
const isDM = computed(() => status.value.visibility === 'direct') const isDM = computed(() => status.value.visibility === 'direct')
const isPinned = computed(() => status.value.pinned) const isPinned = computed(
() =>
!!props.status.pinned && account?.id === status.value.account.id,
)
const showUpperBorder = computed(() => newer && !directReply.value) const showUpperBorder = computed(() => newer && !directReply.value)
const showReplyTo = computed(() => !replyToMain.value && !directReply.value) const showReplyTo = computed(() => !replyToMain.value && !directReply.value)

View file

@ -9,7 +9,7 @@ const {
toggle: _toggleTranslation, toggle: _toggleTranslation,
translation, translation,
enabled: isTranslationEnabled, enabled: isTranslationEnabled,
} = useTranslation(status, getLanguageCode()) } = await useTranslation(status, getLanguageCode())
const preferenceHideTranslation = usePreferences('hideTranslation') const preferenceHideTranslation = usePreferences('hideTranslation')
const showButton = computed(() => const showButton = computed(() =>

View file

@ -39,11 +39,11 @@ function getFollowedTag(status: mastodon.v1.Status): string | null {
<template #default="{ item, older, newer, active }"> <template #default="{ item, older, newer, active }">
<template v-if="virtualScroller"> <template v-if="virtualScroller">
<DynamicScrollerItem :item="item" :active="active" tag="article"> <DynamicScrollerItem :item="item" :active="active" tag="article">
<StatusCard :followed-tag="getFollowedTag(item)" :status="item" :context="context" :older="older" :newer="newer" /> <StatusCard :followed-tag="getFollowedTag(item)" :status="item" :context="context" :older="older" :newer="newer" :account="account" />
</DynamicScrollerItem> </DynamicScrollerItem>
</template> </template>
<template v-else> <template v-else>
<StatusCard :followed-tag="getFollowedTag(item)" :status="item" :context="context" :older="older" :newer="newer" /> <StatusCard :followed-tag="getFollowedTag(item)" :status="item" :context="context" :older="older" :newer="newer" :account="account" />
</template> </template>
</template> </template>
<template v-if="context === 'account' " #done="{ items }"> <template v-if="context === 'account' " #done="{ items }">

View file

@ -104,11 +104,12 @@ export function parseMastodonHTML(
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(/>/g, '&gt;') .replace(/>/g, '&gt;')
.replace(/`/g, '&#96;') .replace(/`/g, '&#96;')
.replace(/\*/g, '&ast;')
const classes = lang ? ` class="language-${lang}"` : '' const classes = lang ? ` class="language-${lang}"` : ''
return `><pre><code${classes}>${code}</code></pre>` return `><pre><code${classes}>${code}</code></pre>`
}) })
.replace(/`([^`\n]*)`/g, (_1, raw) => { .replace(/`([^`\n]*)`/g, (_1, raw) => {
return raw ? `<code>${htmlToText(raw).replace(/</g, '&lt;').replace(/>/g, '&gt;')}</code>` : '' return raw ? `<code>${htmlToText(raw).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\*/g, '&ast;')}</code>` : ''
}) })
} }

View file

@ -72,7 +72,7 @@ export const statusVisibilities = [
}, },
{ {
value: 'unlisted', value: 'unlisted',
icon: 'i-ri:lock-unlock-line', icon: 'i-ri:moon-line',
}, },
{ {
value: 'private', value: 'private',

View file

@ -42,6 +42,10 @@ export const supportedTranslationCodes = [
'zh', 'zh',
] as const ] as const
const translationAPISupported = 'Translator' in globalThis && 'LanguageDetector' in globalThis
const anchorMarkupRegEx = /<a[^>]*>.*?<\/a>/g
export function getLanguageCode() { export function getLanguageCode() {
let code = 'en' let code = 'en'
const getCode = (code: string) => code.replace(/-.*$/, '') const getCode = (code: string) => code.replace(/-.*$/, '')
@ -58,6 +62,13 @@ interface TranslationErr {
} }
} }
function replaceTranslatedLinksWithOriginal(text: string) {
return text.replace(anchorMarkupRegEx, (match) => {
const tagLink = anchorMarkupRegEx.exec(text)
return tagLink ? tagLink[0] : match
})
}
export async function translateText(text: string, from: string | null | undefined, to: string) { export async function translateText(text: string, from: string | null | undefined, to: string) {
const config = useRuntimeConfig() const config = useRuntimeConfig()
const status = ref({ const status = ref({
@ -65,7 +76,6 @@ export async function translateText(text: string, from: string | null | undefine
error: '', error: '',
text: '', text: '',
}) })
const regex = /<a[^>]*>.*?<\/a>/g
try { try {
const response = await ($fetch as any)(config.public.translateApi, { const response = await ($fetch as any)(config.public.translateApi, {
method: 'POST', method: 'POST',
@ -78,11 +88,7 @@ export async function translateText(text: string, from: string | null | undefine
}, },
}) as TranslationResponse }) as TranslationResponse
status.value.success = true status.value.success = true
// replace the translated links with the original status.value.text = replaceTranslatedLinksWithOriginal(response.translatedText)
status.value.text = response.translatedText.replace(regex, (match) => {
const tagLink = regex.exec(text)
return tagLink ? tagLink[0] : match
})
} }
catch (err) { catch (err) {
// TODO: improve type // TODO: improve type
@ -102,17 +108,27 @@ const translations = new WeakMap<mastodon.v1.Status | mastodon.v1.StatusEdit, {
error: string error: string
}>() }>()
export function useTranslation(status: mastodon.v1.Status | mastodon.v1.StatusEdit, to: string) { export async function useTranslation(status: mastodon.v1.Status | mastodon.v1.StatusEdit, to: string) {
if (!translations.has(status)) if (!translations.has(status))
translations.set(status, reactive({ visible: false, text: '', success: false, error: '' })) translations.set(status, reactive({ visible: false, text: '', success: false, error: '' }))
const translation = translations.get(status)! const translation = translations.get(status)!
const userSettings = useUserSettings() const userSettings = useUserSettings()
const shouldTranslate = 'language' in status && status.language && status.language !== to let shouldTranslate = false
&& supportedTranslationCodes.includes(to as any) if ('language' in status) {
shouldTranslate = typeof status.language === 'string' && status.language !== to && !userSettings.value.disabledTranslationLanguages.includes(status.language)
if (!translationAPISupported) {
shouldTranslate = shouldTranslate && supportedTranslationCodes.includes(to as any)
&& supportedTranslationCodes.includes(status.language as any) && supportedTranslationCodes.includes(status.language as any)
&& !userSettings.value.disabledTranslationLanguages.includes(status.language) }
else {
shouldTranslate = shouldTranslate && (await (globalThis as any).Translator.availability({
sourceLanguage: status.language,
targetLanguage: to,
})) !== 'unavailable'
}
}
const enabled = /*! !useRuntimeConfig().public.translateApi && */ shouldTranslate const enabled = /*! !useRuntimeConfig().public.translateApi && */ shouldTranslate
async function toggle() { async function toggle() {
@ -120,12 +136,57 @@ export function useTranslation(status: mastodon.v1.Status | mastodon.v1.StatusEd
return return
if (!translation.text) { if (!translation.text) {
const translated = await translateText(status.content, status.language, to) let translated = {
value: {
error: '',
text: '',
success: false,
},
}
if (translationAPISupported && 'language' in status) {
let sourceLanguage = status.language
if (!sourceLanguage) {
const languageDetector = await (globalThis as any).LanguageDetector.create()
// Make sure HTML markup doesn't derail language detection.
const div = document.createElement('div')
div.innerHTML = status.content
// eslint-disable-next-line unicorn/prefer-dom-node-text-content
const detectedLanguages = await languageDetector.detect(div.innerText)
sourceLanguage = detectedLanguages[0].detectedLanguage
if (sourceLanguage === 'und') {
throw new Error('Could not detect source language.')
}
}
const translator = await (globalThis as any).Translator.create({
sourceLanguage,
targetLanguage: to,
})
try {
let text = await translator.translate(status.content)
text = replaceTranslatedLinksWithOriginal(text)
translated.value = {
error: '',
text,
success: true,
}
}
catch (error) {
translated.value = {
error: (error as Error).message,
text: '',
success: false,
}
}
}
else {
if ('language' in status) {
translated = await translateText(status.content, status.language, to)
}
}
translation.error = translated.value.error translation.error = translated.value.error
translation.text = translated.value.text translation.text = translated.value.text
translation.success = translated.value.success translation.success = translated.value.success
} }
translation.visible = !translation.visible translation.visible = !translation.visible
} }

View file

@ -11,6 +11,8 @@ export function useThreadComposer(draftKey: string, initial?: () => DraftItem) {
*/ */
const threadIsActive = computed<boolean>(() => draftItems.value.length > 1) const threadIsActive = computed<boolean>(() => draftItems.value.length > 1)
const threadIsSending = ref(false)
/** /**
* Add an item to the thread * Add an item to the thread
*/ */
@ -44,6 +46,7 @@ export function useThreadComposer(draftKey: string, initial?: () => DraftItem) {
async function publishThread() { async function publishThread() {
const allFailedMessages: Array<string> = [] const allFailedMessages: Array<string> = []
const isAReplyThread = Boolean(draftItems.value[0].params.inReplyToId) const isAReplyThread = Boolean(draftItems.value[0].params.inReplyToId)
threadIsSending.value = true
let lastPublishedStatus: mastodon.v1.Status | null = null let lastPublishedStatus: mastodon.v1.Status | null = null
let amountPublished = 0 let amountPublished = 0
@ -72,6 +75,7 @@ export function useThreadComposer(draftKey: string, initial?: () => DraftItem) {
} }
// Remove all published items from the thread // Remove all published items from the thread
draftItems.value.splice(0, amountPublished) draftItems.value.splice(0, amountPublished)
threadIsSending.value = false
// If we have errors, return them // If we have errors, return them
if (allFailedMessages.length > 0) if (allFailedMessages.length > 0)
@ -90,5 +94,6 @@ export function useThreadComposer(draftKey: string, initial?: () => DraftItem) {
addThreadItem, addThreadItem,
removeThreadItem, removeThreadItem,
publishThread, publishThread,
threadIsSending,
} }
} }

View file

@ -10,6 +10,6 @@ catch (err) {
<template> <template>
<MainContent text-base grid gap-3 m3> <MainContent text-base grid gap-3 m3>
<img rounded-3 :src="instance.thumbnail.url"> <img v-if="instance !== undefined" rounded-3 :src="instance.thumbnail.url">
</MainContent> </MainContent>
</template> </template>

View file

@ -13,6 +13,6 @@
}, },
"devDependencies": { "devDependencies": {
"@nuxt-themes/docus": "^1.15.1", "@nuxt-themes/docus": "^1.15.1",
"nuxt": "^3.17.3" "nuxt": "^3.18.1"
} }
} }

View file

@ -165,7 +165,8 @@
}, },
"preferences": { "preferences": {
"grayscale_mode": "Tema en escala de grises", "grayscale_mode": "Tema en escala de grises",
"hide_username_emojis_description": "Oculta, de la historia, los emojis en los nombres de usuario. Los emojis seguirán visibles en los perfiles." "hide_username_emojis_description": "Oculta, de la historia, los emojis en los nombres de usuario. Los emojis seguirán visibles en los perfiles.",
"unmute_videos": "Sonido de video activado por defecto"
}, },
"profile": { "profile": {
"appearance": { "appearance": {

View file

@ -226,7 +226,9 @@
"manage": "Administrar listas", "manage": "Administrar listas",
"modify_account": "Modificar listas con cuenta", "modify_account": "Modificar listas con cuenta",
"remove_account": "Eliminar cuenta de la lista", "remove_account": "Eliminar cuenta de la lista",
"save": "Guardar" "save": "Guardar",
"search_following_desc": "Buscar personas a las que sigues",
"search_following_placeholder": "Buscar entre las personas a las que sigues"
}, },
"magic_keys": { "magic_keys": {
"dialog_header": "Atajos de teclado", "dialog_header": "Atajos de teclado",
@ -334,10 +336,12 @@
"zen_mode": "Modo Zen" "zen_mode": "Modo Zen"
}, },
"notification": { "notification": {
"and": "y",
"favourited_post": "marcó como favorita tu publicación", "favourited_post": "marcó como favorita tu publicación",
"followed_you": "te ha seguido", "followed_you": "te ha seguido",
"followed_you_count": "{0} personas te siguieron|{0} persona te siguió|{0} personas te siguieron", "followed_you_count": "{0} personas te siguieron|{0} persona te siguió|{0} personas te siguieron",
"missing_type": "MISSING notification.type:", "missing_type": "MISSING notification.type:",
"others": "{0} personas|{0} persona|{0} personas",
"reblogged_post": "retooteó tu publicación", "reblogged_post": "retooteó tu publicación",
"reported": "{0} reportó {1}", "reported": "{0} reportó {1}",
"request_to_follow": "ha solicitado seguirte", "request_to_follow": "ha solicitado seguirte",
@ -559,6 +563,7 @@
"label": "Preferencias", "label": "Preferencias",
"optimize_for_low_performance_device": "Optimizar para dispositivos de bajo rendimiento", "optimize_for_low_performance_device": "Optimizar para dispositivos de bajo rendimiento",
"title": "Funcionalidades experimentales", "title": "Funcionalidades experimentales",
"unmute_videos": "Sonido de vídeo activado por defecto",
"use_star_favorite_icon": "Utilizar icono de estrella para favoritos", "use_star_favorite_icon": "Utilizar icono de estrella para favoritos",
"user_picker": "Selector de usuarios", "user_picker": "Selector de usuarios",
"user_picker_description": "Muestra todos los avatares de las cuentas registradas en la parte inferior izquierda para que puedas cambiar rápidamente entre ellos.", "user_picker_description": "Muestra todos los avatares de las cuentas registradas en la parte inferior izquierda para que puedas cambiar rápidamente entre ellos.",
@ -637,7 +642,8 @@
"poll": { "poll": {
"count": "{0} votos|{0} voto|{0} votos", "count": "{0} votos|{0} voto|{0} votos",
"ends": "finaliza {0}", "ends": "finaliza {0}",
"finished": "finalizada {0}" "finished": "finalizada {0}",
"update": "Actualizar encuesta"
}, },
"replying_to": "Respondiendo a {0}", "replying_to": "Respondiendo a {0}",
"show_full_thread": "Mostrar hilo completo", "show_full_thread": "Mostrar hilo completo",

View file

@ -1,5 +1,4 @@
import type { Ref } from 'vue' import type { Ref, UnwrapNestedRefs } from 'vue'
import type { UnwrapNestedRefs } from 'vue'
export interface PwaInjection { export interface PwaInjection {
isInstalled: boolean isInstalled: boolean

View file

@ -68,7 +68,7 @@
"@vueuse/motion": "2.2.6", "@vueuse/motion": "2.2.6",
"@vueuse/nuxt": "^13.2.0", "@vueuse/nuxt": "^13.2.0",
"blurhash": "^2.0.5", "blurhash": "^2.0.5",
"browser-fs-access": "^0.35.0", "browser-fs-access": "^0.38.0",
"cheerio": "^1.0.0", "cheerio": "^1.0.0",
"chroma-js": "^3.0.0", "chroma-js": "^3.0.0",
"emoji-mart": "^5.5.2", "emoji-mart": "^5.5.2",
@ -117,7 +117,7 @@
"ws": "^8.15.1" "ws": "^8.15.1"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^4.13.1", "@antfu/eslint-config": "^5.1.0",
"@antfu/ni": "^24.4.0", "@antfu/ni": "^24.4.0",
"@types/chroma-js": "^3.1.1", "@types/chroma-js": "^3.1.1",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
@ -127,23 +127,23 @@
"@types/wicg-file-system-access": "^2023.10.6", "@types/wicg-file-system-access": "^2023.10.6",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"@unlazy/nuxt": "^0.12.4", "@unlazy/nuxt": "^0.12.4",
"@unocss/eslint-config": "^66.1.2", "@unocss/eslint-config": "^66.4.1",
"@vue/test-utils": "2.4.6", "@vue/test-utils": "2.4.6",
"bumpp": "^10.1.1", "bumpp": "^10.2.2",
"consola": "^3.4.2", "consola": "^3.4.2",
"eslint": "^9.27.0", "eslint": "^9.32.0",
"eslint-plugin-format": "^1.0.1", "eslint-plugin-format": "^1.0.1",
"flat": "^6.0.1", "flat": "^6.0.1",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
"lint-staged": "^15.5.2", "lint-staged": "^15.5.2",
"nuxt": "^3.17.3", "nuxt": "^3.18.1",
"prettier": "^3.5.3", "prettier": "^3.6.2",
"sharp": "^0.34.1", "sharp": "^0.34.3",
"sharp-ico": "^0.1.5", "sharp-ico": "^0.1.5",
"simple-git-hooks": "^2.13.0", "simple-git-hooks": "^2.13.1",
"tsx": "^4.19.4", "tsx": "^4.20.3",
"typescript": "^5.4.4", "typescript": "^5.4.4",
"vitest": "3.1.3", "vitest": "3.2.4",
"vue-tsc": "^2.1.6" "vue-tsc": "^2.1.6"
}, },
"pnpm": { "pnpm": {
@ -152,9 +152,9 @@
} }
}, },
"resolutions": { "resolutions": {
"nuxt-component-meta": "0.11.0", "nuxt-component-meta": "0.13.0",
"unstorage": "^1.16.0", "unstorage": "^1.16.1",
"vitest": "3.1.3", "vitest": "3.2.4",
"vue": "^3.5.4" "vue": "^3.5.4"
}, },
"simple-git-hooks": { "simple-git-hooks": {

File diff suppressed because it is too large Load diff

View file

@ -60,7 +60,7 @@ async function fetchAppInfo(origin: string, server: string) {
}, },
body: { body: {
client_name: APP_NAME + (env !== 'release' ? ` (${env})` : ''), client_name: APP_NAME + (env !== 'release' ? ` (${env})` : ''),
website: 'https://elk.zone', website: origin,
redirect_uris: getRedirectURI(origin, server), redirect_uris: getRedirectURI(origin, server),
scopes: 'read write follow push', scopes: 'read write follow push',
}, },

View file

@ -1,5 +1,12 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`content-rich > asterisk paris in code block 1`] = `"<p><pre class="code-block">1 * 2 * 3</pre></p>"`;
exports[`content-rich > asterisk paris in inline code 1`] = `
"<p><code>1 * 2 * 3</code></p>
"
`;
exports[`content-rich > block with backticks 1`] = `"<p><pre class="code-block">[(\`number string) (\`tag string)]</pre></p>"`; exports[`content-rich > block with backticks 1`] = `"<p><pre class="code-block">[(\`number string) (\`tag string)]</pre></p>"`;
exports[`content-rich > block with injected html, with a known language 1`] = ` exports[`content-rich > block with injected html, with a known language 1`] = `

View file

@ -186,6 +186,16 @@ describe('content-rich', () => {
`) `)
expect(formatted).toMatchSnapshot() expect(formatted).toMatchSnapshot()
}) })
it ('asterisk paris in inline code', async () => {
const { formatted } = await render('<p>`1 * 2 * 3`</p>')
expect(formatted).toMatchSnapshot()
})
it ('asterisk paris in code block', async () => {
const { formatted } = await render('<p>```<br />1 * 2 * 3<br />```</p>')
expect(formatted).toMatchSnapshot()
})
}) })
describe('editor', () => { describe('editor', () => {