Merge branch 'elk-zone:main' into main

pull/3389/head
Adityawarman Dewa Putra 2025-09-29 01:11:38 +07:00 zatwierdzone przez GitHub
commit c241aeeec5
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
38 zmienionych plików z 4110 dodań i 6112 usunięć

Wyświetl plik

@ -17,12 +17,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
# workaround for npm registry key change
# ref. `pnpm@10.1.0` / `pnpm@9.15.4` cannot be installed due to key id mismatch · Issue #612 · nodejs/corepack
# - https://github.com/nodejs/corepack/issues/612#issuecomment-2629496091
- run: npm i -g corepack@latest && corepack enable
- uses: actions/setup-node@v4.4.0
- uses: actions/setup-node@v5.0.0
with:
node-version-file: .nvmrc

Wyświetl plik

@ -16,7 +16,7 @@ jobs:
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Docker meta
id: metal
uses: docker/metadata-action@v5

Wyświetl plik

@ -0,0 +1,22 @@
name: ci
on:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
contents: read
jobs:
check-provenance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check provenance downgrades
uses: danielroe/provenance-action@41bcc969e579d9e29af08ba44fcbfdf95cee6e6c # v0.1.1
with:
fail-on-provenance-change: true

Wyświetl plik

@ -12,12 +12,12 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set node
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: .nvmrc

Wyświetl plik

@ -19,6 +19,6 @@ jobs:
name: Semantic Pull Request
steps:
- name: Validate PR title
uses: amannn/action-semantic-pull-request@v5.5.3
uses: amannn/action-semantic-pull-request@v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Wyświetl plik

@ -45,7 +45,7 @@ One could put Elk behind popular reverse proxies with SSL Handling like Traefik,
1. got into new source dir: ```cd elk```
1. create local storage directory for settings: ```mkdir elk-storage```
1. adjust permissions of storage dir: ```sudo chown 911:911 ./elk-storage```
1. start container: ```docker-compose up --build -d```
1. start container: ```docker compose up --build -d```
> [!NOTE]
> The provided Dockerfile creates a container which will eventually run Elk as non-root user and create a persistent named Docker volume upon first start (if that volume does not yet exist). This volume is always created with root permission. Failing to change the permissions of ```/elk/data``` inside this volume to UID:GID 911 (as specified for Elk in the Dockerfile) will prevent Elk from storing it's config for user accounts. You either have to fix the permission in the created named volume, or mount a directory with the correct permission to ```/elk/data``` into the container.

Wyświetl plik

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

Wyświetl plik

@ -30,21 +30,21 @@ const containerClass = computed(() => {
sticky top-0 z-20
pt="[env(safe-area-inset-top,0)]"
bg="[rgba(var(--rgb-bg-base),0.7)]"
class="native:lg:w-[calc(100vw-5rem)] native:xl:w-[calc(135%+(100vw-1200px)/2)]"
:class="{
'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 gap-3 items-center :overflow-hidden="!noOverflowHidden ? '' : false" py2 w-full>
<NuxtLink
v-if="backOnSmallScreen || back" flex="~ gap1" items-center btn-text p-0 xl:hidden
<div flex justify-between gap-2 min-h-53px px5 py1 :class="{ 'xl:hidden': $route.name !== 'tag' }" border="b base">
<div flex gap-2 items-center :overflow-hidden="!noOverflowHidden ? '' : false" w-full>
<button
v-if="backOnSmallScreen || back"
btn-text flex items-center ms="-3" p-3 xl:hidden
:aria-label="$t('nav.back')"
@click="$router.go(-1)"
>
<div i-ri:arrow-left-line class="rtl-flip" />
</NuxtLink>
<div :truncate="!noOverflowHidden ? '' : false" flex w-full data-tauri-drag-region class="native-mac:justify-start native-mac:text-center">
<div text-lg i-ri:arrow-left-line class="rtl-flip" />
</button>
<div :truncate="!noOverflowHidden ? '' : false" flex w-full class="native-mac:justify-start native-mac:text-center">
<slot name="title" />
</div>
<div sm:hidden h-7 w-1px />

Wyświetl plik

@ -13,9 +13,9 @@ watchEffect(() => {
}
const duration
= days.value * 24 * 60 * 60
+ hours.value * 60 * 60
+ minutes.value * 60
= days.value * 24 * 60 * 60
+ hours.value * 60 * 60
+ minutes.value * 60
if (duration <= 0) {
isValid.value = false

Wyświetl plik

@ -18,7 +18,7 @@ router.afterEach(() => {
</script>
<template>
<div flex justify-between sticky top-0 bg-base z-1 py-4 native:py-7 data-tauri-drag-region>
<div flex justify-between sticky top-0 bg-base z-1 py-4>
<NuxtLink
flex items-end gap-3
py2 px-5
@ -33,17 +33,16 @@ router.afterEach(() => {
{{ $t('app_name') }} <sup text-sm italic mt-1>{{ env === 'release' ? 'alpha' : env }}</sup>
</div>
</NuxtLink>
<div
hidden xl:flex items-center me-8 mt-2 gap-1
>
<CommonTooltip :content="$t('nav.back')">
<NuxtLink
<div hidden xl:flex items-center me-6 mt-2 gap-1>
<CommonTooltip :content="$t('nav.back')" :distance="0">
<button
type="button"
: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)"
>
<div text-xl i-ri:arrow-left-line class="rtl-flip" btn-text />
</NuxtLink>
<div text-xl i-ri:arrow-left-line class="rtl-flip" />
</button>
</CommonTooltip>
</div>
</div>

Wyświetl plik

@ -29,7 +29,7 @@ const emit = defineEmits<{
const { t } = useI18n()
const { threadItems, threadIsActive, publishThread } = threadComposer ?? useThreadComposer(draftKey)
const { threadItems, threadIsActive, publishThread, threadIsSending } = threadComposer ?? useThreadComposer(draftKey)
const draft = computed({
get: () => threadItems.value[draftItemIndex],
@ -237,9 +237,13 @@ function stopQuestionMarkPropagation(e: KeyboardEvent) {
e.stopImmediatePropagation()
}
const userSettings = useUserSettings()
const optimizeForLowPerformanceDevice = computed(() => getPreferences(userSettings.value, 'optimizeForLowPerformanceDevice'))
const languageDetectorInGlobalThis = 'LanguageDetector' in globalThis
let supportsLanguageDetector = languageDetectorInGlobalThis && await (globalThis as any).LanguageDetector.availability() === 'available'
let languageDetector: { detect: (arg0: string) => any }
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
@ -255,26 +259,36 @@ function countLetters(text: string) {
return letters.length
}
async function detectLanguage() {
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))[0].detectedLanguage
const detectedLanguage = (await languageDetector.detect(text, { signal: detectLanguageAbortController.signal }))[0].detectedLanguage
draft.value.params.language = detectedLanguage === 'und' ? preferredLanguage.value : detectedLanguage.substring(0, 2)
}
catch {
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>
<template>
@ -563,18 +577,18 @@ async function detectLanguage() {
<button
v-if="!threadIsActive || isFinalItemOfThread"
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"
:disabled="isPublishDisabled || isExceedingCharacterLimit"
:aria-disabled="isPublishDisabled || isExceedingCharacterLimit || threadIsSending" aria-describedby="publish-tooltip"
:disabled="isPublishDisabled || isExceedingCharacterLimit || threadIsSending"
@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 />
</span>
<span v-if="failedMessages.length" block>
<div block i-carbon:face-dizzy-filled />
</span>
<template v-if="threadIsActive">
<span>{{ $t('action.publish_thread') }} </span>
<span>{{ !threadIsSending ? $t('action.publish_thread') : $t('state.publishing') }} </span>
</template>
<template v-else>
<span v-if="draft.editingStatus">{{ $t('action.save_changes') }}</span>

Wyświetl plik

@ -67,6 +67,11 @@ const sanitizer = sanitize({
li: {
value: keep,
},
// Hollo supports <ruby> tags
// https://github.com/fedify-dev/hollo/blob/80e7184aa805f579be8712ff9231be655343c661/src/xss.ts#L92-L94
ruby: {},
rp: {},
rt: {},
})
/**
@ -104,11 +109,12 @@ export function parseMastodonHTML(
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/`/g, '&#96;')
.replace(/\*/g, '&ast;')
const classes = lang ? ` class="language-${lang}"` : ''
return `><pre><code${classes}>${code}</code></pre>`
})
.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>` : ''
})
}

Wyświetl plik

@ -95,9 +95,9 @@ export async function toggleMuteAccount(relationship: mastodon.v1.Relationship,
relationship!.muting = !relationship!.muting
relationship = relationship!.muting
? await client.value.v1.accounts.$select(account.id).mute({
duration,
notifications,
})
duration,
notifications,
})
: await client.value.v1.accounts.$select(account.id).unmute()
}

Wyświetl plik

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

Wyświetl plik

@ -16,9 +16,9 @@ const instance = instanceStorage.value[currentServer.value]
</script>
<template>
<div h-full :data-mode="isHydrated && isGrayscale ? 'grayscale' : ''" data-tauri-drag-region>
<main flex w-full mxa lg:max-w-80rem class="native:grid native:sm:grid-cols-[auto_1fr] native:lg:grid-cols-[auto_minmax(600px,2fr)_1fr]">
<aside class="native:w-auto w-1/8 md:w-1/6 lg:w-1/5 xl:w-1/4 zen-hide" hidden sm:flex justify-end xl:me-4 native:me-0 relative>
<div h-full :data-mode="isHydrated && isGrayscale ? 'grayscale' : ''">
<main flex w-full mxa lg:max-w-80rem>
<aside class="w-1/8 md:w-1/6 lg:w-1/5 xl:w-1/4 zen-hide" hidden sm:flex justify-end xl:me-4 relative>
<div sticky top-0 w-20 xl:w-100 h-100dvh flex="~ col" lt-xl-items-center>
<slot name="left">
<div flex="~ col" overflow-y-auto justify-between h-full max-w-full overflow-x-hidden>
@ -60,7 +60,7 @@ const instance = instanceStorage.value[currentServer.value]
<NavBottom v-if="isHydrated" sm:hidden />
</div>
</div>
<aside v-if="isHydrated && !wideLayout" class="hidden lg:w-1/5 xl:w-1/4 sm:none xl:block native:w-full zen-hide">
<aside v-if="isHydrated && !wideLayout" class="hidden lg:w-1/5 xl:w-1/4 sm:none xl:block zen-hide">
<div sticky top-0 h-100dvh flex="~ col" gap-2 py3 ms-2>
<slot name="right">
<SearchWidget mt-4 mx-1 hidden xl:block />

Wyświetl plik

@ -10,6 +10,6 @@ catch (err) {
<template>
<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>
</template>

Wyświetl plik

@ -80,7 +80,7 @@ const locales: LocaleObjectData[] = [
file: 'en.json',
name: 'English',
},
({
{
// @ts-expect-error ar used as placeholder
code: 'ar',
file: 'ar.json',
@ -90,8 +90,8 @@ const locales: LocaleObjectData[] = [
const name = new Intl.PluralRules('ar-EG').select(choice)
return { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5 }[name]
},
} satisfies LocaleObjectData),
({
} satisfies LocaleObjectData,
{
code: 'ckb',
file: 'ckb.json',
name: 'کوردیی ناوەندی',
@ -100,8 +100,8 @@ const locales: LocaleObjectData[] = [
const name = new Intl.PluralRules('ckb').select(choice)
return { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5 }[name]
},
} satisfies LocaleObjectData),
({
} satisfies LocaleObjectData,
{
code: 'fa-IR',
file: 'fa-IR.json',
name: 'فارسی',
@ -110,7 +110,7 @@ const locales: LocaleObjectData[] = [
const name = new Intl.PluralRules('fa-IR').select(choice)
return { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5 }[name]
},
} satisfies LocaleObjectData),
} satisfies LocaleObjectData,
{
// @ts-expect-error ca used as placeholder
code: 'ca',

Wyświetl plik

@ -6,7 +6,7 @@ services:
volumes:
# make sure this directory has the same ownership as the elk user from the Dockerfile
# otherwise Elk will not be able to store configs for accounts
# e.q. mkdir ./elk-storage; sudo chown 911:911 ./elk-storage
# e.g., mkdir ./elk-storage; sudo chown 911:911 ./elk-storage
- './elk-storage:/elk/data'
ports:
- 5314:5314

Wyświetl plik

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

Wyświetl plik

@ -7,6 +7,8 @@
"route_loaded": "Page {0} chargée"
},
"account": {
"authorize": "Autoriser l'abonnement",
"authorized": "Vous avez autorisé la demande",
"avatar_description": "Avatar de {0}",
"blocked_by": "Ce compte vous a bloqué",
"blocked_domains": "Domaines bloqués",
@ -25,6 +27,7 @@
"follows_you": "@:account.follow_back",
"go_to_profile": "Aller à son profil",
"joined": "a rejoint",
"lock": "Verrouiller",
"moved_title": "a indiqué que son nouveau compte est désormais :",
"muted_users": "Comptes masqués",
"muting": "Masqué·e",
@ -37,7 +40,10 @@
"profile_description": "En-tête du profil de {0}",
"profile_personal_note": "Note personnelle",
"profile_unavailable": "Profil non accessible",
"reject": "Rejeter l'abonnement",
"rejected": "Vous avez rejeté la demande",
"request_follow": "Demander à suivre",
"requested": "{0} a demandé à vous suivre",
"unblock": "Débloquer",
"unfollow": "Ne plus suivre",
"unmute": "Réafficher",
@ -52,6 +58,7 @@
"boost": "Partager",
"boost_count": "{0}",
"boosted": "Partagé",
"clear": "Effacer",
"clear_publish_failed": "Effacer les erreurs de publication",
"clear_save_failed": "Effacer les erreurs de sauvegarde",
"clear_upload_failed": "Effacer les erreurs de téléversement de fichier",
@ -66,8 +73,10 @@
"favourited": "J'aime",
"more": "Plus",
"next": "Suivant",
"open_image_preview_dialog": "Ouvrir le dialogue d'aperçu de l'image",
"prev": "Précédent",
"publish": "Publier",
"publish_thread": "Publier le fil",
"reply": "Répondre",
"reply_count": "{0}",
"reset": "Réinitialiser",
@ -115,12 +124,14 @@
"block_account": {
"cancel": "Annuler",
"confirm": "Bloquer",
"description": "Voulez-vous vraiment bloquer {0} ?"
"description": "Voulez-vous vraiment bloquer {0} ?",
"title": "Bloquer le compte"
},
"block_domain": {
"cancel": "Annuler",
"confirm": "Bloquer",
"description": "Voulez-vous vraiment bloquer {0} ?"
"description": "Voulez-vous vraiment bloquer {0} ?",
"title": "Bloquer le domaine"
},
"common": {
"cancel": "Non",
@ -129,27 +140,37 @@
"delete_list": {
"cancel": "Annuler",
"confirm": "Supprimer",
"description": "Voulez-vous vraiment supprimer la liste \"{0}\" ?"
"description": "Voulez-vous vraiment supprimer la liste \"{0}\" ?",
"title": "Supprimer la liste"
},
"delete_posts": {
"cancel": "Annuler",
"confirm": "Supprimer",
"description": "Voulez-vous vraiment supprimer ce message ?"
"description": "Voulez-vous vraiment supprimer ce message ?",
"title": "Supprimer le message"
},
"mute_account": {
"cancel": "Annuler",
"confirm": "Mettre en sourdine",
"description": "Voulez-vous vraiment mettre en sourdine {0} ?"
"days": "jour|jour|jour",
"description": "Voulez-vous vraiment mettre en sourdine {0} ?",
"hours": "heures|heures|heures",
"minute": "minutes|minutes|minutes",
"notifications": "Mettre en sourdine les notifications",
"specify_duration": "Spécifier la durée de la mise en sourdine",
"title": "Mettre en sourdine le compte"
},
"show_reblogs": {
"cancel": "Annuler",
"confirm": "Afficher",
"description": "Voulez-vous vraiment afficher les partages de {0} ?"
"description": "Voulez-vous vraiment afficher les partages de {0} ?",
"title": "Afficher les partages"
},
"unfollow": {
"cancel": "Annuler",
"confirm": "Se désabonner",
"description": "Voulez-vous vraiment vous désabonner ?"
"description": "Voulez-vous vraiment vous désabonner ?",
"title": "Se désabonner"
}
},
"conversation": {
@ -202,9 +223,12 @@
"error": "Il y a eu une erreur lors de la création de la liste",
"error_prefix": "Erreur :",
"list_title_placeholder": "Nom de la liste",
"manage": "Gérer les listes",
"modify_account": "Modifier les listes de ce compte",
"remove_account": "Supprimer ce compte de listes",
"save": "Enregistrer les changements"
"save": "Enregistrer les changements",
"search_following_desc": "Chercher des personnes que vous suivez",
"search_following_placeholder": "Chercher parmi les personnes que vous suivez"
},
"magic_keys": {
"dialog_header": "Raccourcis clavier",
@ -214,14 +238,26 @@
"command_mode": "Mode commande",
"compose": "Composer",
"favourite": "J'aime",
"search": "Rechercher",
"show_new_items": "Afficher les nouveaux éléments",
"title": "Actions"
},
"media": {
"title": "Média"
},
"navigation": {
"go_to_bookmarks": "Signets",
"go_to_conversations": "Conversations",
"go_to_explore": "Explorer",
"go_to_favourites": "Favoris",
"go_to_federated": "Fédérés",
"go_to_home": "Accueil",
"go_to_lists": "Listes",
"go_to_local": "Local",
"go_to_notifications": "Notifications",
"go_to_profile": "Profil",
"go_to_search": "Rechercher",
"go_to_settings": "Paramètres",
"next_status": "Message suivant",
"previous_status": "Message précédent",
"shortcut_help": "Aide sur les raccourcis",
@ -276,13 +312,16 @@
"built_at": "Dernière compilation {0}",
"compose": "Composer",
"conversations": "Conversations",
"docs": "Documentation",
"explore": "Explorer",
"favourites": "Aimés",
"federated": "Fédérés",
"hashtags": "Hashtags",
"home": "Accueil",
"list": "Liste",
"lists": "Listes",
"local": "Local",
"more_menu": "Plus d'options",
"muted_users": "Comptes masqués",
"notifications": "Notifications",
"privacy": "Données privées",
@ -297,10 +336,12 @@
"zen_mode": "Mode Zen"
},
"notification": {
"and": "et",
"favourited_post": "a aimé votre message",
"followed_you": "vous suit",
"followed_you_count": "{0} personnes vous suivent|{0} personne vous suit|{0} personnes vous suivent",
"missing_type": "MISSING notification.type:",
"others": "{0} personnes|{0} personne|{0} personnes",
"reblogged_post": "a relayé votre message",
"reported": "{0} a signalé {1}",
"request_to_follow": "vous demande de le suivre",
@ -417,6 +458,8 @@
"label": "Paramètres de compte"
},
"interface": {
"bottom_nav": "Navigation inférieure",
"bottom_nav_instructions": "Choisissez jusqu'à cinq boutons de navigation inférieure favoris. Doit inclure le bouton \"Plus d'options\".",
"color_mode": "Couleur de thème",
"dark_mode": "Mode sombre",
"default": " (par défaut)",
@ -428,6 +471,7 @@
},
"language": {
"display_language": "Langue d'affichage",
"how_to_contribute": "Comment contribuer ?",
"label": "Langue",
"post_language": "Langue de publication",
"status": "État de la traduction : {0}/{1} ({2} %)",
@ -495,6 +539,8 @@
},
"notifications_settings": "Notifications",
"preferences": {
"embedded_media": "Lecteur multimédia intégré",
"embedded_media_description": "Affichez un lecteur intégré au lieu de la carte d'aperçu normale lors de l'expansion des liens de streaming de supports partagés.",
"enable_autoplay": "Activer la lecture automatique",
"enable_data_saving": "Activer l'économie de données",
"enable_data_saving_description": "Economise les données en évitant le chargement automatique des médias.",
@ -507,13 +553,16 @@
"hide_boost_count": "Masquer les compteurs de partages",
"hide_favorite_count": "Masquer les compteurs de favoris",
"hide_follower_count": "Masquer les compteurs d'abonné·e·s",
"hide_gif_indi_on_posts": "Masquer l'indicateur de gif sur les messages",
"hide_news": "Masquer les actualités",
"hide_reply_count": "Masquer les compteurs de réponses",
"hide_translation": "Masquer traduction",
"hide_username_emojis": "Masquer les emojis sur le nom d'utilisateur",
"hide_username_emojis_description": "Masque les emojis des noms d'utilisateur dans la timeline. \nLes emojis seront toujours visibles sur leurs profils.",
"label": "Préférences",
"optimize_for_low_performance_device": "Optimiser pour un dispositif à faible performance",
"title": "Fonctionnalités expérimentales",
"unmute_videos": "Son de vidéo par défaut",
"use_star_favorite_icon": "Utiliser l'icône de l'étoile en favoris",
"user_picker": "User Picker",
"user_picker_description": "Affiche tous les avatars des comptes connectés en bas à gauche afin que vous puissiez basculer rapidement entre eux.",
@ -556,7 +605,11 @@
},
"state": {
"attachments_exceed_server_limit": "Le nombre de pièces jointes a dépassé la limite par message.",
"attachments_limit_audio_error": "Taille maximum d'audio dépassée : {0}",
"attachments_limit_error": "Limite par publication dépassée",
"attachments_limit_image_error": "Taille maximum d'image dépassée : {0}",
"attachments_limit_unknown_error": "Taille maximum de fichier dépassée : {0}",
"attachments_limit_video_error": "Taille maximum de vidéo dépassée : {0}",
"edited": "(Édité)",
"editing": "Édition",
"loading": "Chargement...",
@ -573,15 +626,18 @@
},
"boosted_by": "Partagé par",
"edited": "Edité {0}",
"embedded_warning": "Lire ceci peut révéler votre adresse IP à d'autres.",
"favourited_by": "Aimé par",
"filter_hidden_phrase": "Filtré par",
"filter_show_anyway": "Montrer coûte que coûte",
"gif": "GIF",
"img_alt": {
"ALT": "ALT",
"desc": "Description",
"dismiss": "Fermer",
"read": "Lire la description de {0}"
},
"pinned": "Messages épinglés",
"poll": {
"count": "{0} votes",
"ends": "se clôt {0}",
@ -663,6 +719,7 @@
"year_past": "il y a 0 année|l'année dernière|il y a {n} années"
},
"timeline": {
"no_posts": "Pas de messages ici !",
"show_new_items": "Voir le nouveau message|Voir les {v} nouveaux messages",
"view_older_posts": "Les messages plus anciens d'autres instances peuvent ne pas être affichés."
},
@ -675,6 +732,7 @@
"add_emojis": "Ajouter des émoticônes",
"add_media": "Ajouter des images, une vidéo ou un fichier audio",
"add_publishable_content": "Ajouter du contenu à publier",
"add_thread_item": "Ajouter un message au fil",
"change_content_visibility": "Ajuster la confidentialité du message",
"change_language": "Changer la langue",
"emoji": "Emoji",
@ -684,6 +742,8 @@
"open_editor_tools": "Outils d'édition",
"pick_an_icon": "Choisir une icône",
"publish_failed": "Fermez les messages ayant échoué en haut de l'éditeur pour republier les messages",
"remove_thread_item": "Supprimer le message du fil",
"start_thread": "Commencer un fil",
"toggle_bold": "Appliquer/retirer le gras",
"toggle_code_block": "Ajouter un bloc de code",
"toggle_italic": "Appliquer/retirer l'italique"

Wyświetl plik

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

Wyświetl plik

@ -1,68 +0,0 @@
import { rm } from 'node:fs/promises'
import { addImports, addImportsSources, addPlugin, createResolver, defineNuxtModule, useNuxt } from '@nuxt/kit'
import { resolveModulePath } from 'exsolve'
const mockProxy = resolveModulePath('mocked-exports/proxy', { from: import.meta.url })
export default defineNuxtModule({
meta: {
name: 'tauri',
},
setup() {
const nuxt = useNuxt()
const { resolve } = createResolver(import.meta.url)
if (!process.env.TAURI_PLATFORM)
return
if (nuxt.options.dev)
nuxt.options.ssr = false
nuxt.options.pwa.disable = true
nuxt.options.sourcemap.client = false
nuxt.options.alias = {
...nuxt.options.alias,
'unstorage/drivers/fs': mockProxy,
'unstorage/drivers/cloudflare-kv-http': mockProxy,
'#storage-config': resolve('./runtime/storage-config'),
'node:events': 'unenv/runtime/node/events/index',
'#build-info': resolve('./runtime/build-info'),
}
nuxt.hook('vite:extend', ({ config }) => {
config.build!.target = ['chrome100', 'safari15']
config.envPrefix = [...config.envPrefix || [], 'VITE_', 'TAURI_']
})
// prevent creation of server routes
nuxt.hook('nitro:config', (config) => {
config.srcDir = './_nonexistent'
config.scanDirs = []
})
addImportsSources({
from: 'h3',
imports: ['defineEventHandler', 'getQuery', 'getRouterParams', 'readBody', 'sendRedirect'] as Array<keyof typeof import('h3')>,
})
nuxt.options.imports.dirs = nuxt.options.imports.dirs || []
nuxt.options.imports.dirs.push(resolve('../../server/utils'))
addImports({ name: 'useStorage', from: resolve('./runtime/storage') })
addPlugin(resolve('./runtime/logging.client'))
addPlugin(resolve('./runtime/nitro.client'))
// cleanup files copied from the public folder that we don't need
nuxt.hook('close', async () => {
await rm('.output/public/_redirects')
await rm('.output/public/apple-touch-icon.png')
await rm('.output/public/elk-og.png')
await rm('.output/public/favicon.ico')
await rm('.output/public/pwa-192x192.png')
await rm('.output/public/pwa-512x512.png')
await rm('.output/public/robots.txt')
})
},
})

Wyświetl plik

@ -1 +0,0 @@
export const env = useAppConfig().env

Wyświetl plik

@ -1,18 +0,0 @@
import * as log from 'tauri-plugin-log-api'
// When running inside Tauri, catch all logs from 3rd party packages and direct them to the unified logging stream
export default defineNuxtPlugin(() => {
// eslint-disable-next-line no-global-assign
console = {
...console,
trace: log.trace,
debug: log.debug,
log: log.info,
warn: log.warn,
error: log.error,
}
window.addEventListener('unhandledrejection', err =>
log.error(err.reason))
window.addEventListener('error', err => log.error(err.error), true)
})

Wyświetl plik

@ -1,73 +0,0 @@
import type { FetchResponse } from 'ofetch'
import {
createApp,
createRouter,
defineLazyEventHandler,
toNodeListener,
} from 'h3'
import { fetchNodeRequestHandler } from 'node-mock-http'
import { createFetch } from 'ofetch'
const handlers = [
{
route: '/api/:server/oauth',
handler: defineLazyEventHandler(() => import('~~/server/api/[server]/oauth/[origin]').then(r => r.default || r)),
},
{
route: '/api/:server/login',
handler: defineLazyEventHandler(() => import('~~/server/api/[server]/login').then(r => r.default || r)),
},
{
route: '/api/list-servers',
handler: defineLazyEventHandler(() => import('~~/server/api/list-servers').then(r => r.default || r)),
},
]
// @ts-expect-error undeclared global window property
window.__NUXT__.config = {
// @ts-expect-error undeclared global window property
...window.__NUXT__.config,
storage: {},
}
export default defineNuxtPlugin(async () => {
const config = useRuntimeConfig()
const h3App = createApp({
debug: import.meta.dev,
// TODO: add global error handler
// onError: (err, event) => {
// console.log({ err, event })
// },
})
const router = createRouter()
for (const h of handlers)
router.use(h.route, h.handler)
// @ts-expect-error TODO: fix
h3App.use(config.app.baseURL, router)
const nodeHandler = toNodeListener(h3App)
const localFetch: typeof fetch = async (input, init) => {
if (!input.toString().startsWith('/')) {
return globalThis.fetch(input.toString(), init)
}
return await fetchNodeRequestHandler(nodeHandler, input.toString(), init)
}
// @ts-expect-error error types are subtly different here in a future nitro version
globalThis.$fetch = createFetch({
fetch: localFetch,
Headers,
defaults: { baseURL: config.app.baseURL },
})
const route = useRoute()
if (route.path.startsWith('/api')) {
const result = (await ($fetch.raw as any)(route.fullPath)) as FetchResponse<unknown>
if (result.headers.get('location'))
location.href = result.headers.get('location')!
}
})

Wyświetl plik

@ -1,2 +0,0 @@
export const driver = undefined
export const fsBase = ''

Wyświetl plik

@ -1,29 +0,0 @@
import { Store } from 'tauri-plugin-store-api'
import { createStorage } from 'unstorage'
const store = new Store('.servers.dat')
const storage = createStorage()
storage.mount('servers', {
getKeys() {
return store.keys()
},
async removeItem(key: string) {
await store.delete(key)
},
clear() {
return store.clear()
},
hasItem(key: string) {
return store.has(key)
},
setItem(key: string, value: any) {
return store.set(key, value)
},
getItem(key: string) {
return store.get(key)
},
})
export function useStorage() {
return storage
}

Wyświetl plik

@ -40,7 +40,6 @@ export default defineNuxtConfig({
'~~/modules/emoji-mart-translation',
'~~/modules/purge-comments',
'~~/modules/build-env',
'~~/modules/tauri/index',
'~~/modules/pwa/index', // change to '@vite-pwa/nuxt' once released and remove pwa module
'stale-dep/nuxt',
],
@ -62,6 +61,9 @@ export default defineNuxtConfig({
experimental: {
payloadExtraction: false,
renderJsonPayloads: true,
// Temporary workaround to avoid hash mismatch issue
// ref. https://github.com/elk-zone/elk/issues/3385#issuecomment-3335167005
entryImportMap: false,
},
css: [
'@unocss/reset/tailwind.css',
@ -69,9 +71,7 @@ export default defineNuxtConfig({
'~/styles/default-theme.css',
'~/styles/vars.css',
'~/styles/global.css',
...process.env.TAURI_PLATFORM === 'macos'
? []
: ['~/styles/scrollbars.css'],
'~/styles/scrollbars.css',
'~/styles/tiptap.css',
'~/styles/dropdown.css',
],

Wyświetl plik

@ -2,7 +2,7 @@
"name": "@elk-zone/elk",
"type": "module",
"version": "0.16.0",
"packageManager": "pnpm@9.15.9",
"packageManager": "pnpm@10.17.0",
"license": "MIT",
"homepage": "https://elk.zone/",
"main": "./nuxt.config.ts",
@ -39,7 +39,7 @@
"@iconify/json": "^2.2.170",
"@iconify/utils": "^2.1.22",
"@nuxt/devtools": "^2.4.1",
"@nuxt/test-utils": "^3.19.0",
"@nuxt/test-utils": "^3.19.2",
"@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/i18n": "^9.5.4",
"@pinia/nuxt": "^0.11.0",
@ -68,7 +68,7 @@
"@vueuse/motion": "2.2.6",
"@vueuse/nuxt": "^13.2.0",
"blurhash": "^2.0.5",
"browser-fs-access": "^0.35.0",
"browser-fs-access": "^0.38.0",
"cheerio": "^1.0.0",
"chroma-js": "^3.0.0",
"emoji-mart": "^5.5.2",
@ -88,7 +88,6 @@
"masto": "^6.10.4",
"mocked-exports": "^0.1.1",
"node-emoji": "^2.1.3",
"node-mock-http": "^1.0.0",
"nuxt-security": "^2.2.0",
"page-lifecycle": "^0.1.2",
"pinia": "^3.0.2",
@ -101,8 +100,6 @@
"stale-dep": "^0.8.0",
"std-env": "^3.7.0",
"string-length": "^5.0.1",
"tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log",
"tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store",
"theme-vitesse": "^0.8.0",
"tiny-decode": "^0.1.3",
"tippy.js": "^6.3.7",
@ -117,8 +114,8 @@
"ws": "^8.15.1"
},
"devDependencies": {
"@antfu/eslint-config": "^4.13.1",
"@antfu/ni": "^24.4.0",
"@antfu/eslint-config": "^5.4.1",
"@antfu/ni": "^26.0.1",
"@types/chroma-js": "^3.1.1",
"@types/file-saver": "^2.0.7",
"@types/fnando__sparkline": "^0.3.7",
@ -127,21 +124,21 @@
"@types/wicg-file-system-access": "^2023.10.6",
"@types/ws": "^8.18.1",
"@unlazy/nuxt": "^0.12.4",
"@unocss/eslint-config": "^66.3.2",
"@unocss/eslint-config": "^66.4.2",
"@vue/test-utils": "2.4.6",
"bumpp": "^10.2.0",
"bumpp": "^10.2.3",
"consola": "^3.4.2",
"eslint": "^9.27.0",
"eslint": "^9.36.0",
"eslint-plugin-format": "^1.0.1",
"flat": "^6.0.1",
"fs-extra": "^11.3.0",
"lint-staged": "^15.5.2",
"nuxt": "^3.17.6",
"prettier": "^3.5.3",
"sharp": "^0.34.2",
"fs-extra": "^11.3.1",
"lint-staged": "^16.1.6",
"nuxt": "^3.18.1",
"prettier": "^3.6.2",
"sharp": "^0.34.3",
"sharp-ico": "^0.1.5",
"simple-git-hooks": "^2.13.0",
"tsx": "^4.20.3",
"simple-git-hooks": "^2.13.1",
"tsx": "^4.20.5",
"typescript": "^5.4.4",
"vitest": "3.2.4",
"vue-tsc": "^2.1.6"
@ -152,8 +149,8 @@
}
},
"resolutions": {
"nuxt-component-meta": "0.12.0",
"unstorage": "^1.16.0",
"nuxt-component-meta": "0.14.0",
"unstorage": "^1.17.1",
"vitest": "3.2.4",
"vue": "^3.5.4"
},

28
page-lifecycle.d.ts vendored
Wyświetl plik

@ -1,17 +1,17 @@
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 {
newState: PageLifecycleState
oldState: PageLifecycleState
}
interface PageLifecycle extends EventTarget {
get state(): PageLifecycleState
get pageWasDiscarded(): boolean
addUnsavedChanges: (id: symbol | any) => void
removeUnsavedChanges: (id: symbol | any) => void
addEventListener: (type: string, listener: (evt: PageLifecycleEvent) => void) => void
}
const lifecycle: PageLifecycle
export default lifecycle
interface PageLifecycleEvent extends Event {
newState: PageLifecycleState
oldState: PageLifecycleState
}
interface PageLifecycle extends EventTarget {
get state(): PageLifecycleState
get pageWasDiscarded(): boolean
addUnsavedChanges: (id: symbol | any) => void
removeUnsavedChanges: (id: symbol | any) => void
addEventListener: (type: string, listener: (evt: PageLifecycleEvent) => void) => void
}
const lifecycle: PageLifecycle
export default lifecycle
}

Plik diff jest za duży Load Diff

Wyświetl plik

@ -7,7 +7,7 @@ import { flatten } from 'flat'
import { countryLocaleVariants, currentLocales } from '../config/i18n'
export const localeData: [code: string, file: string[], title: string][]
= currentLocales.map((l: any) => [l.code, l.files ? l.files : [l.file!], l.name ?? l.code])
= currentLocales.map((l: any) => [l.code, l.files ? l.files : [l.file!], l.name ?? l.code])
function merge(src: Record<string, any>, dst: Record<string, any>) {
for (const key in src) {

Wyświetl plik

@ -1,6 +1,6 @@
import { stringifyQuery } from 'ufo'
import { defaultUserAgent } from '~~/server/utils/shared'
import { defaultUserAgent, invalidateApp } from '~~/server/utils/shared'
export default defineEventHandler(async (event) => {
let { server, origin } = getRouterParams(event)
@ -43,7 +43,51 @@ export default defineEventHandler(async (event) => {
const url = `/signin/callback?${stringifyQuery({ server, token: result.access_token, vapid_key: app.vapid_key })}`
await sendRedirect(event, url, 302)
}
catch {
catch (error: any) {
// Check for invalid client error (OAuth app deleted)
if (error?.data?.error === 'invalid_client'
|| (error?.statusCode === 401 && error?.data?.error_description?.includes('Client authentication failed'))) {
// Invalidate cached app and retry once
await invalidateApp(origin, server)
try {
const newApp = await getApp(origin, server)
if (!newApp) {
throw createError({
statusCode: 400,
statusMessage: `Failed to re-register app for server: ${server}`,
})
}
const retryResult: any = await $fetch(`https://${server}/oauth/token`, {
method: 'POST',
headers: {
'user-agent': defaultUserAgent,
},
body: {
client_id: newApp.client_id,
client_secret: newApp.client_secret,
redirect_uri: getRedirectURI(origin, server),
grant_type: 'authorization_code',
code,
scope: 'read write follow push',
},
retry: 1,
})
const url = `/signin/callback?${stringifyQuery({ server, token: retryResult.access_token, vapid_key: newApp.vapid_key })}`
await sendRedirect(event, url, 302)
return
}
catch {
throw createError({
statusCode: 400,
statusMessage: 'OAuth application recovery failed. Please try again.',
})
}
}
// Other errors (network, invalid code, etc.)
throw createError({
statusCode: 400,
statusMessage: 'Could not complete log in.',

Wyświetl plik

@ -60,7 +60,7 @@ async function fetchAppInfo(origin: string, server: string) {
},
body: {
client_name: APP_NAME + (env !== 'release' ? ` (${env})` : ''),
website: 'https://elk.zone',
website: origin,
redirect_uris: getRedirectURI(origin, server),
scopes: 'read write follow push',
},
@ -111,6 +111,12 @@ export async function deleteApp(server: string) {
await storage.removeItem(key)
}
export async function invalidateApp(origin: string, server: string) {
const host = origin.replace(/^https?:\/\//, '').replace(/\W/g, '-').replace(/\?.*$/, '')
const key = `servers:v4:${server}:${host}.json`.toLowerCase()
await storage.removeItem(key)
}
export async function listServers() {
const keys = await storage.getKeys('servers:v4:')
const servers = new Set<string>()

Wyświetl plik

@ -1,5 +1,12 @@
// 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 injected html, with a known language 1`] = `

Wyświetl plik

@ -186,6 +186,16 @@ describe('content-rich', () => {
`)
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', () => {

Wyświetl plik

@ -13,7 +13,7 @@ function status(id: string, filtered?: mastodon.v1.FilterContext): mastodon.v1.S
if (filtered) {
fakeStatus.filtered
= [
= [
{
filter: {
filterAction: 'hide',

Wyświetl plik

@ -1,5 +1,3 @@
import type { Variant } from 'unocss'
import process from 'node:process'
import { variantParentMatcher } from '@unocss/preset-mini/utils'
import {
@ -108,29 +106,14 @@ export default defineConfig({
},
},
variants: [
...(process.env.TAURI_PLATFORM
? <Variant<any>[]>[(matcher) => {
if (!matcher.startsWith('native:'))
return
return {
matcher: matcher.slice(7),
layer: 'native',
}
}]
: []),
...(process.env.TAURI_PLATFORM !== 'macos'
? <Variant<any>[]>[
(matcher) => {
if (!matcher.startsWith('native-mac:'))
return
return {
matcher: matcher.slice(11),
layer: 'native-mac',
}
},
]
: []
),
(matcher) => {
if (!matcher.startsWith('native-mac:'))
return
return {
matcher: matcher.slice(11),
layer: 'native-mac',
}
},
variantParentMatcher('fullscreen', '@media (display-mode: fullscreen)'),
variantParentMatcher('coarse-pointer', '@media (pointer: coarse)'),
],