Merge branch 'main' into shuuji3/feat/emoji-reactions
This commit is contained in:
commit
e174f23b66
24 changed files with 4643 additions and 2374 deletions
|
|
@ -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
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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(() => {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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(() =>
|
||||||
|
|
|
||||||
|
|
@ -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 }">
|
||||||
|
|
|
||||||
|
|
@ -104,11 +104,12 @@ export function parseMastodonHTML(
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/`/g, '`')
|
.replace(/`/g, '`')
|
||||||
|
.replace(/\*/g, '*')
|
||||||
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, '<').replace(/>/g, '>')}</code>` : ''
|
return raw ? `<code>${htmlToText(raw).replace(/</g, '<').replace(/>/g, '>').replace(/\*/g, '*')}</code>` : ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -95,9 +95,9 @@ export async function toggleMuteAccount(relationship: mastodon.v1.Relationship,
|
||||||
relationship!.muting = !relationship!.muting
|
relationship!.muting = !relationship!.muting
|
||||||
relationship = relationship!.muting
|
relationship = relationship!.muting
|
||||||
? await client.value.v1.accounts.$select(account.id).mute({
|
? await client.value.v1.accounts.$select(account.id).mute({
|
||||||
duration,
|
duration,
|
||||||
notifications,
|
notifications,
|
||||||
})
|
})
|
||||||
: await client.value.v1.accounts.$select(account.id).unmute()
|
: await client.value.v1.accounts.$select(account.id).unmute()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
&& supportedTranslationCodes.includes(status.language as any)
|
shouldTranslate = typeof status.language === 'string' && status.language !== to && !userSettings.value.disabledTranslationLanguages.includes(status.language)
|
||||||
&& !userSettings.value.disabledTranslationLanguages.includes(status.language)
|
if (!translationAPISupported) {
|
||||||
|
shouldTranslate = shouldTranslate && supportedTranslationCodes.includes(to as any)
|
||||||
|
&& supportedTranslationCodes.includes(status.language as any)
|
||||||
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
3
modules/pwa/runtime/types.d.ts
vendored
3
modules/pwa/runtime/types.d.ts
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
28
package.json
28
package.json
|
|
@ -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": {
|
||||||
|
|
|
||||||
28
page-lifecycle.d.ts
vendored
28
page-lifecycle.d.ts
vendored
|
|
@ -1,17 +1,17 @@
|
||||||
declare module 'page-lifecycle/dist/lifecycle.mjs' {
|
declare module 'page-lifecycle/dist/lifecycle.mjs' {
|
||||||
type PageLifecycleState = 'active' | 'passive' | 'hidden' | 'frozen' | 'terminated'
|
type PageLifecycleState = 'active' | 'passive' | 'hidden' | 'frozen' | 'terminated'
|
||||||
|
|
||||||
interface PageLifecycleEvent extends Event {
|
interface PageLifecycleEvent extends Event {
|
||||||
newState: PageLifecycleState
|
newState: PageLifecycleState
|
||||||
oldState: PageLifecycleState
|
oldState: PageLifecycleState
|
||||||
}
|
}
|
||||||
interface PageLifecycle extends EventTarget {
|
interface PageLifecycle extends EventTarget {
|
||||||
get state(): PageLifecycleState
|
get state(): PageLifecycleState
|
||||||
get pageWasDiscarded(): boolean
|
get pageWasDiscarded(): boolean
|
||||||
addUnsavedChanges: (id: symbol | any) => void
|
addUnsavedChanges: (id: symbol | any) => void
|
||||||
removeUnsavedChanges: (id: symbol | any) => void
|
removeUnsavedChanges: (id: symbol | any) => void
|
||||||
addEventListener: (type: string, listener: (evt: PageLifecycleEvent) => void) => void
|
addEventListener: (type: string, listener: (evt: PageLifecycleEvent) => void) => void
|
||||||
}
|
}
|
||||||
const lifecycle: PageLifecycle
|
const lifecycle: PageLifecycle
|
||||||
export default lifecycle
|
export default lifecycle
|
||||||
}
|
}
|
||||||
|
|
|
||||||
6709
pnpm-lock.yaml
6709
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -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',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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`] = `
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue