Compare commits

..

1 commit

Author SHA1 Message Date
renovate[bot]
486c093716
fix(deps): update dependency nuxt-security to ^0.14.0 2024-02-24 16:49:59 +00:00
156 changed files with 3603 additions and 7103 deletions

15
.eslintignore Normal file
View file

@ -0,0 +1,15 @@
*.css
*.png
*.ico
*.toml
*.patch
*.txt
Dockerfile
public/
public-dev/
public-staging/
https-dev-config/localhost.crt
https-dev-config/localhost.key
Dockerfile
elk-translation-status.json
docs/translation-status.json

19
.eslintrc Normal file
View file

@ -0,0 +1,19 @@
{
"extends": "@antfu",
"ignorePatterns": ["!pages/public"],
"overrides": [
{
"files": ["locales/**.json"],
"rules": {
"jsonc/sort-keys": "error"
}
}
],
"rules": {
"vue/no-restricted-syntax":["error", {
"selector": "VElement[name='a']",
"message": "Use NuxtLink instead."
}],
"n/prefer-global/process": "off"
}
}

View file

@ -2,4 +2,4 @@
name: 🚀 New feature proposal
about: Propose a new feature
labels: 's: pending triage'
---
---

View file

@ -21,7 +21,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 18
cache: pnpm
- name: 📦 Install dependencies
@ -32,7 +32,6 @@ jobs:
- name: 🧪 Test project
run: pnpm test:ci
timeout-minutes: 10
- name: 📝 Lint
run: pnpm lint

View file

@ -38,7 +38,7 @@ jobs:
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.metal.outputs.tags }}
labels: ${{ steps.metal.outputs.labels }}

45
.vscode/settings.json vendored
View file

@ -5,6 +5,10 @@
"unmute",
"unstorage"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.formatOnSave": false,
"files.associations": {
"*.css": "postcss"
},
@ -19,44 +23,7 @@
"i18n-ally.preferredDelimiter": "_",
"i18n-ally.sortKeys": true,
"i18n-ally.sourceLanguage": "en",
// Enable the ESlint flat config support
"eslint.experimental.useFlatConfig": true,
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off" },
{ "rule": "*-indent", "severity": "off" },
{ "rule": "*-spacing", "severity": "off" },
{ "rule": "*-spaces", "severity": "off" },
{ "rule": "*-order", "severity": "off" },
{ "rule": "*-dangle", "severity": "off" },
{ "rule": "*-newline", "severity": "off" },
{ "rule": "*quotes", "severity": "off" },
{ "rule": "*semi", "severity": "off" }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml"
]
"volar.completion.preferredTagNameCase": "pascal",
"volar.completion.preferredAttrNameCase": "kebab"
}

View file

@ -1,22 +0,0 @@
steps:
- name: build docker
image: docker:25-cli
secrets: [user, pass]
commands:
- apk add git
- REPO=$(echo "$CI_REPO" | tr '[:upper:]' '[:lower:]')
- REGISTRY="dev.cat-enby.club"
- MAJOR=$(echo ${CI_COMMIT_TAG} | cut -d '.' -f 1 | tr -d 'v')
- MINOR=$(echo ${CI_COMMIT_TAG} | cut -d '.' -f 2)
- PATCH=$(echo ${CI_COMMIT_TAG} | cut -d '.' -f 3 | cut -d '-' -f 1)
- docker buildx build -t $${REGISTRY}/$${REPO}:v$${MAJOR:-0}.$${MINOR:-0}.$${PATCH-0} -t $${REGISTRY}/$${REPO}:v$${MAJOR:-0}.$${MINOR} -t $${REGISTRY}/$$REPO:v$${MAJOR:-0} -t $${REGISTRY}/$$REPO:latest .
- docker login --username $USER --password $PASS $${REGISTRY}
- docker push $${REGISTRY}/$${REPO}:v$${MAJOR:-0}.$${MINOR:-0}.$${PATCH-0}
- docker push $${REGISTRY}/$${REPO}:v$${MAJOR:-0}.$${MINOR}
- docker push $${REGISTRY}/$${REPO}:v$${MAJOR:-0}
- docker push $${REGISTRY}/$${REPO}:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
when:
- repo: nikurasu:elk-test-ci
- event: tag

View file

@ -8,7 +8,7 @@ For guidelines on contributing to the documentation, refer to the [docs README](
### Online
You can use [StackBlitz Codeflow](https://stackblitz.com/codeflow) to fix bugs or implement features. You'll also see a Codeflow button on PRs to review them without a local setup. Once the elk repo has been cloned in Codeflow, the dev server will start automatically and print the URL to open the App. You should receive a prompt in the bottom-right suggesting to open it in the Editor or in another Tab. To learn more, check out the [Codeflow docs](https://developer.stackblitz.com/codeflow/what-is-codeflow).
You can use [StackBlitz Codeflow](https://stackblitz.com/codeflow) to fix bugs or implement features. You'll also see a Codeflow button on PRs to review them without a local setup. Once the elk repo has been cloned in Codeflow, the dev server will start automatically and print the URL to open the App. You should receive a prompt in the bottom-right suggesting to open it in the Editor or in another Tab. To learn more, check out the [Codeflow docs](https://developer.stackblitz.com/codeflow/what-is-codeflow).
[![Open in Codeflow](https://developer.stackblitz.com/img/open_in_codeflow.svg)](https://pr.new/elk-zone/elk)
@ -21,6 +21,7 @@ To develop and test the Elk package:
2. Ensure using the latest Node.js (16.x).
If you have [nvm](https://github.com/nvm-sh/nvm), you can run `nvm i` to install the required version.
3. The package manager used to install and link dependencies must be [pnpm](https://pnpm.io/) v7. To use it you must first enable [Corepack](https://github.com/nodejs/corepack) by running `corepack enable`. (Note: on Linux in a standard Node 16+ environment, you should follow the instructions to install via Node's `corepack` rather than using the `curl` command)
4. Check out a branch where you can work and commit your changes:
@ -83,7 +84,7 @@ Simple approach used by most websites of relying on direction set in HTML elemen
We've added some `UnoCSS` utilities styles to help you with that:
- Do not use `left/right` padding and margin: for example `pl-1`. Use `padding-inline-start/end` instead. So `pl-1` should be `ps-1`, `pr-1` should be `pe-1`. The same rules apply to margin.
- Do not use `rtl-` classes, such as `rtl-left-0`.
- For icons that should be rotated for RTL, add `class="rtl-flip"`. This can only be used for icons outside of elements with `dir="auto"`, such as timeline, and is the only exception to the rule above. For icons inside the timeline, it might not work as expected.
- For icons that should be rotated for RTL, add `class="rtl-flip"`. This can only be used for icons outside of elements with `dir="auto"`, such as timeline, and is the only exception from the rule above. For icons inside the timeline, it might not work as expected.
- For absolute positioned elements, don't use `left/right`: for example `left-0`. Use `inset-inline-start/end` instead. `UnoCSS` shortcuts are `inset-is` for `inset-inline-start` and `inset-ie` for `inset-inline-end`. Example: `left-0` should be replaced with `inset-is-0`.
- If you need to change the border radius for an entire left or right side, use `border-inline-start/end`. `UnoCSS` shortcuts are `rounded-is` for left side, `rounded-ie` for right side. Example: `rounded-l-5` should be replaced with `rounded-ie-5`.
- If you need to change the border radius for one corner, use `border-start-end-radius` and similar rules. `UnoCSS` shortcuts are `rounded` + top/bottom as either `-bs` (top) or `-be` (bottom) + left/right as either `-is` (left) or `-ie` (right). Example: `rounded-tl-0` should be replaced with `rounded-bs-is-0`.

View file

@ -39,8 +39,8 @@ The Elk team maintains a deployment at:
### Self-Host Docker Deployment
In order to host Elk yourself you can use the provided Dockerfile to build a container with elk. Be aware, that Elk only loads properly if the connection is done via SSL/TLS. The Docker container itself does not provide any SSL/TLS handling. You'll have to add this bit yourself.
One could put Elk behind popular reverse proxies with SSL Handling like Traefik, NGINX etc.
In order to host Elk yourself you can use the provided Dockerfile to build a container with elk. Be aware, that Elk only loads properly if the connection is done via SSL/TLS. The Docker container itself does not provide any SSL/TLS handling. You'll have to add this bit yourself.
One could put Elk behind popular reverse proxies with SSL Handling like Traefik, NGINX etc.
1. checkout source ```git clone https://github.com/elk-zone/elk.git```
1. got into new source dir: ```cd elk```
@ -52,6 +52,7 @@ One could put Elk behind popular reverse proxies with SSL Handling like Traefik,
> [!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.
### Ecosystem
These are known deployments using Elk as an alternative Web client for Mastodon servers or as a base for other projects in the fediverse:
@ -106,7 +107,7 @@ We're really excited that you're interested in contributing to Elk! Before submi
### Online
You can use [StackBlitz Codeflow](https://stackblitz.com/codeflow) to fix bugs or implement features. You'll also see a Codeflow button on PRs to review them without a local setup. Once the elk repo has been cloned in Codeflow, the dev server will start automatically and print the URL to open the App. You should receive a prompt in the bottom-right suggesting to open it in the Editor or in another Tab. To learn more, check out the [Codeflow docs](https://developer.stackblitz.com/codeflow/what-is-codeflow).
You can use [StackBlitz Codeflow](https://stackblitz.com/codeflow) to fix bugs or implement features. You'll also see a Codeflow button on PRs to review them without a local setup. Once the elk repo has been cloned in Codeflow, the dev server will start automatically and print the URL to open the App. You should receive a prompt in the bottom-right suggesting to open it in the Editor or in another Tab. To learn more, check out the [Codeflow docs](https://developer.stackblitz.com/codeflow/what-is-codeflow).
[![Open in Codeflow](https://developer.stackblitz.com/img/open_in_codeflow.svg)](https://pr.new/elk-zone/elk)
@ -157,8 +158,8 @@ You can consult the [PWA documentation](https://docs.elk.zone/pwa) to learn more
## 👨‍💻 Contributors
<a href="https://github.com/elk-zone/elk/graphs/contributors">
<img src="https://contrib.rocks/image?repo=elk-zone/elk" />
</a>
<img src="https://contrib.rocks/image?repo=elk-zone/elk" />
</a>
## 📄 License

View file

@ -13,7 +13,6 @@ const { t } = useI18n()
const isSelf = useSelfAccount(() => account)
const enable = computed(() => !isSelf.value && currentUser.value)
const relationship = computed(() => props.relationship || useRelationship(account).value)
const isLoading = computed(() => relationship.value === undefined)
const { client } = useMasto()
@ -63,10 +62,6 @@ const buttonStyle = computed(() => {
if (relationship.value ? relationship.value.following : context === 'following')
return `text-base ${relationship.value?.followedBy ? 'border-strong' : 'border-base'}`
// If loading, use a plain style
if (isLoading.value)
return 'text-base border-base'
// If not following, use a button style
return 'text-inverted bg-primary border-primary'
})
@ -82,33 +77,28 @@ const buttonStyle = computed(() => {
:hover="!relationship?.blocking && !relationship?.muting && relationship?.following ? 'border-red text-red' : 'bg-base border-primary text-primary'"
@click="relationship?.blocking ? unblock() : relationship?.muting ? unmute() : toggleFollowAccount(relationship!, account)"
>
<template v-if="isLoading">
<span i-svg-spinners-180-ring-with-bg />
<template v-if="relationship?.blocking">
<span elk-group-hover="hidden">{{ $t('account.blocking') }}</span>
<span hidden elk-group-hover="inline">{{ $t('account.unblock') }}</span>
</template>
<template v-if="relationship?.muting">
<span elk-group-hover="hidden">{{ $t('account.muting') }}</span>
<span hidden elk-group-hover="inline">{{ $t('account.unmute') }}</span>
</template>
<template v-else-if="relationship ? relationship.following : context === 'following'">
<span elk-group-hover="hidden">{{ relationship?.followedBy ? $t('account.mutuals') : $t('account.following') }}</span>
<span hidden elk-group-hover="inline">{{ $t('account.unfollow') }}</span>
</template>
<template v-else-if="relationship?.requested">
<span elk-group-hover="hidden">{{ $t('account.follow_requested') }}</span>
<span hidden elk-group-hover="inline">{{ $t('account.withdraw_follow_request') }}</span>
</template>
<template v-else-if="relationship ? relationship.followedBy : context === 'followedBy'">
<span elk-group-hover="hidden">{{ $t('account.follows_you') }}</span>
<span hidden elk-group-hover="inline">{{ account.locked ? $t('account.request_follow') : $t('account.follow_back') }}</span>
</template>
<template v-else>
<template v-if="relationship?.blocking">
<span elk-group-hover="hidden">{{ $t('account.blocking') }}</span>
<span hidden elk-group-hover="inline">{{ $t('account.unblock') }}</span>
</template>
<template v-if="relationship?.muting">
<span elk-group-hover="hidden">{{ $t('account.muting') }}</span>
<span hidden elk-group-hover="inline">{{ $t('account.unmute') }}</span>
</template>
<template v-else-if="relationship ? relationship.following : context === 'following'">
<span elk-group-hover="hidden">{{ relationship?.followedBy ? $t('account.mutuals') : $t('account.following') }}</span>
<span hidden elk-group-hover="inline">{{ $t('account.unfollow') }}</span>
</template>
<template v-else-if="relationship?.requested">
<span elk-group-hover="hidden">{{ $t('account.follow_requested') }}</span>
<span hidden elk-group-hover="inline">{{ $t('account.withdraw_follow_request') }}</span>
</template>
<template v-else-if="relationship ? relationship.followedBy : context === 'followedBy'">
<span elk-group-hover="hidden">{{ $t('account.follows_you') }}</span>
<span hidden elk-group-hover="inline">{{ account.locked ? $t('account.request_follow') : $t('account.follow_back') }}</span>
</template>
<template v-else>
<span>{{ account.locked ? $t('account.request_follow') : $t('account.follow') }}</span>
</template>
<span>{{ account.locked ? $t('account.request_follow') : $t('account.follow') }}</span>
</template>
</button>
</template>

View file

@ -2,33 +2,39 @@
import type { mastodon } from 'masto'
import { fetchAccountByHandle } from '~/composables/cache'
type WatcherType = [acc?: mastodon.v1.Account | null, h?: string, v?: boolean]
type WatcherType = [acc?: mastodon.v1.Account, h?: string, v?: boolean]
defineOptions({
inheritAttrs: false,
})
const props = defineProps<{
account?: mastodon.v1.Account | null
account?: mastodon.v1.Account
handle?: string
disabled?: boolean
}>()
const accountHover = ref()
const hovered = useElementHover(accountHover)
const hoverCard = ref()
const targetIsVisible = ref(false)
const account = ref<mastodon.v1.Account | null | undefined>(props.account)
useIntersectionObserver(
hoverCard,
([{ intersectionRatio }]) => {
targetIsVisible.value = intersectionRatio <= 0.75
},
)
watch(
() => [props.account, props.handle, hovered.value] satisfies WatcherType,
() => [props.account, props.handle, targetIsVisible.value] satisfies WatcherType,
([newAccount, newHandle, newVisible], oldProps) => {
if (!newVisible || process.test)
return
if (newAccount) {
account.value = newAccount
return
}
if (!newVisible)
return
if (newHandle) {
const [_oldAccount, oldHandle, _oldVisible] = oldProps ?? [undefined, undefined, false]
if (!oldHandle || newHandle !== oldHandle || !account.value) {
@ -43,22 +49,15 @@ watch(
}
account.value = undefined
},
{ immediate: true, flush: 'post' },
}, { immediate: true, flush: 'post' },
)
const userSettings = useUserSettings()
</script>
<template>
<span ref="accountHover">
<VMenu
v-if="!disabled && account && !getPreferences(userSettings, 'hideAccountHoverCard')"
placement="bottom-start"
:delay="{ show: 500, hide: 100 }"
v-bind="$attrs"
:close-on-content-click="false"
>
<span ref="hoverCard">
<VMenu v-if="!disabled && account && !getPreferences(userSettings, 'hideAccountHoverCard')" placement="bottom-start" :delay="{ show: 500, hide: 100 }" v-bind="$attrs" :close-on-content-click="false">
<slot />
<template #popper>
<AccountHoverCard v-if="account" :account="account" />

View file

@ -25,16 +25,13 @@ function shareAccount() {
}
async function toggleReblogs() {
if (!relationship.value!.showingReblogs) {
const dialogChoice = await openConfirmDialog({
title: t('confirm.show_reblogs.title'),
description: t('confirm.show_reblogs.description', [account.acct]),
confirm: t('confirm.show_reblogs.confirm'),
cancel: t('confirm.show_reblogs.cancel'),
})
if (dialogChoice.choice !== 'confirm')
return
}
if (!relationship.value!.showingReblogs && await openConfirmDialog({
title: t('confirm.show_reblogs.title'),
description: t('confirm.show_reblogs.description', [account.acct]),
confirm: t('confirm.show_reblogs.confirm'),
cancel: t('confirm.show_reblogs.cancel'),
}) !== 'confirm')
return
const showingReblogs = !relationship.value?.showingReblogs
relationship.value = await client.value.v1.accounts.$select(account.id).follow({ reblogs: showingReblogs })

View file

@ -1,45 +0,0 @@
<script setup lang="ts">
import type { mastodon } from 'masto'
defineOptions({
inheritAttrs: false,
})
const { tagName, disabled } = defineProps<{
tagName?: string
disabled?: boolean
}>()
const tag = ref<mastodon.v1.Tag>()
const tagHover = ref()
const hovered = useElementHover(tagHover)
watch(hovered, (newHovered) => {
if (newHovered && tagName) {
fetchTag(tagName).then((t) => {
tag.value = t
})
}
})
const userSettings = useUserSettings()
</script>
<template>
<span ref="tagHover">
<VMenu
v-if="!disabled && !getPreferences(userSettings, 'hideTagHoverCard')"
placement="bottom-start"
:delay="{ show: 500, hide: 100 }"
v-bind="$attrs"
:close-on-content-click="false"
>
<slot />
<template #popper>
<TagCardSkeleton v-if="!tag" />
<TagCard v-else :tag="tag" />
</template>
</VMenu>
<slot v-else />
</span>
</template>

View file

@ -22,7 +22,7 @@ onMounted(() => {
const commandMode = computed(() => input.value.startsWith('>'))
const query = computed(() => commandMode.value ? '' : input.value.trim())
const query = computed(() => commandMode ? '' : input.value.trim())
const { accounts, hashtags, loading } = useSearch(query)
@ -61,7 +61,7 @@ const searchResult = computed<QueryResult>(() => {
}
})
const result = computed<QueryResult>(() => commandMode.value
const result = computed<QueryResult>(() => commandMode
? registry.query(scopes.value.map(s => s.id).join('.'), input.value.slice(1).trim())
: searchResult.value,
)

View file

@ -4,8 +4,6 @@ defineProps<{
hover?: boolean
iconChecked?: string
iconUnchecked?: string
checkedIconColor?: string
prependCheckbox?: boolean
}>()
const modelValue = defineModel<boolean | null>()
</script>
@ -17,12 +15,9 @@ const modelValue = defineModel<boolean | null>()
v-bind="$attrs"
@click.prevent="modelValue = !modelValue"
>
<span v-if="label && !prependCheckbox" flex-1 ms-2 pointer-events-none>{{ label }}</span>
<span v-if="label" flex-1 ms-2 pointer-events-none>{{ label }}</span>
<span
:class="[
modelValue ? (iconChecked ?? 'i-ri:checkbox-line') : (iconUnchecked ?? 'i-ri:checkbox-blank-line'),
modelValue && checkedIconColor,
]"
:class="modelValue ? (iconChecked ?? 'i-ri:checkbox-line') : (iconUnchecked ?? 'i-ri:checkbox-blank-line')"
text-lg
aria-hidden="true"
/>
@ -31,7 +26,6 @@ const modelValue = defineModel<boolean | null>()
type="checkbox"
sr-only
>
<span v-if="label && prependCheckbox" flex-1 ms-2 pointer-events-none>{{ label }}</span>
</label>
</template>

View file

@ -8,7 +8,6 @@ import type { UnwrapRef } from 'vue'
const {
paginator,
stream,
eventType,
keyProp = 'id',
virtualScroller = false,
preprocess,
@ -18,7 +17,6 @@ const {
keyProp?: keyof T
virtualScroller?: boolean
stream?: mastodon.streaming.Subscription
eventType?: 'update' | 'notification'
preprocess?: (items: (U | T)[]) => U[]
endMessage?: boolean | string
}>()
@ -46,7 +44,7 @@ defineSlots<{
const { t } = useI18n()
const nuxtApp = useNuxtApp()
const { items, prevItems, update, state, endAnchor, error } = usePaginator(paginator, toRef(() => stream), eventType, preprocess)
const { items, prevItems, update, state, endAnchor, error } = usePaginator(paginator, toRef(() => stream), preprocess)
nuxtApp.hook('elk-logo:click', () => {
update()

View file

@ -33,7 +33,7 @@ useCommands(() => command
:to="option.to"
:replace="replace"
relative flex flex-auto cursor-pointer sm:px6 px2 rounded transition-all
tabindex="0"
tabindex="1"
hover:bg-active transition-100
exact-active-class="children:(text-secondary !border-primary !op100 !text-base)"
@click="!preventScrollTop && $scrollToTop()"
@ -46,7 +46,7 @@ useCommands(() => command
</template>
<template v-if="isHydrated && moreOptions?.options?.length">
<CommonDropdown placement="bottom" flex cursor-pointer mx-1.25rem>
<CommonTooltip placement="top" no-auto-focus :content="moreOptions.tooltip || t('action.more')">
<CommonTooltip placement="top" :content="moreOptions.tooltip || t('action.more')">
<button
cursor-pointer
flex

View file

@ -1,9 +1,5 @@
<script setup lang="ts">
const { as = 'div', active } = defineProps<{
as: any
active: boolean
}>()
const { as = 'div', active } = defineProps<{ as: any; active: boolean }>()
const el = ref()
watch(() => active, (active) => {

View file

@ -19,8 +19,8 @@ const tabs = computed(() => {
})
})
function toValidName(option: string) {
return option.toLowerCase().replace(/[^a-zA-Z0-9]/g, '-')
function toValidName(otpion: string) {
return otpion.toLowerCase().replace(/[^a-zA-Z0-9]/g, '-')
}
useCommands(() => command
@ -49,7 +49,7 @@ useCommands(() => command
><label
flex flex-auto cursor-pointer px3 m1 rounded transition-all
:for="`tab-${toValidName(option.name)}`"
tabindex="0"
tabindex="1"
hover:bg-active transition-100
@keypress.enter="modelValue = option.name"
><span

View file

@ -2,14 +2,12 @@
const emit = defineEmits<{
(event: 'close'): void
}>()
const vAutoFocus = (el: HTMLElement) => el.focus()
</script>
<template>
<div my-8 px-3 sm:px-8 md:max-w-200 flex="~ col gap-4" relative>
<button v-auto-focus type="button" btn-action-icon absolute top--8 right-0 m1 aria-label="Close" @click="emit('close')">
<span i-ri:close-line />
<button btn-action-icon absolute top--8 right-0 m1 aria-label="Close" @click="emit('close')">
<div i-ri:close-line />
</button>
<img :alt="$t('app_logo')" :src="`/${''}logo.svg`" w-20 h-20 height="80" width="80" mxa class="rtl-flip">
@ -30,12 +28,10 @@ const vAutoFocus = (el: HTMLElement) => el.focus()
</NuxtLink>
{{ $t('help.desc_para6') }}
</p>
<NuxtLink hover:text-primary href="https://github.com/sponsors/elk-zone" target="_blank">
{{ $t('help.desc_para3') }}
</NuxtLink>
<p flex="~ gap-2 wrap justify-center" mxa>
{{ $t('help.desc_para3') }}
<p flex="~ gap-2 wrap" mxa>
<template v-for="team of elkTeamMembers" :key="team.github">
<NuxtLink :href="team.link" target="_blank" external rounded-full transition duration-300 border="~ transparent" hover="scale-105 border-primary">
<NuxtLink :href="`https://github.com/sponsors/${team.github}`" target="_blank" external rounded-full transition duration-300 border="~ transparent" hover="scale-105 border-primary">
<img :src="`/avatars/${team.github}-100x100.png`" :alt="team.display" rounded-full w-15 h-15 height="60" width="60">
</NuxtLink>
</template>
@ -46,7 +42,7 @@ const vAutoFocus = (el: HTMLElement) => el.focus()
</NuxtLink>
</p>
<button type="button" btn-solid mxa @click="emit('close')">
<button btn-solid mxa tabindex="2" @click="emit('close')">
{{ $t('action.enter_app') }}
</button>
</div>

View file

@ -68,7 +68,7 @@ async function removeList() {
actionError.value = undefined
await nextTick()
if (confirmDelete.choice === 'confirm') {
if (confirmDelete === 'confirm') {
await nextTick()
try {
await client.v1.lists.$select(list.value.id).remove()
@ -92,7 +92,7 @@ async function removeList() {
async function clearError() {
actionError.value = undefined
await nextTick()
if (isEditing.value)
if (isEditing)
input.value?.focus()
else
deleteBtn.value?.focus()

View file

@ -40,10 +40,6 @@ const shortcutItemGroups = computed<ShortcutItemGroup[]>(() => [
// description: t('magic_keys.groups.navigation.previous_status'),
// shortcut: { keys: ['k'], isSequence: false },
// },
{
description: t('magic_keys.groups.navigation.go_to_search'),
shortcut: { keys: ['/'], isSequence: false },
},
{
description: t('magic_keys.groups.navigation.go_to_home'),
shortcut: { keys: ['g', 'h'], isSequence: true },
@ -52,42 +48,6 @@ const shortcutItemGroups = computed<ShortcutItemGroup[]>(() => [
description: t('magic_keys.groups.navigation.go_to_notifications'),
shortcut: { keys: ['g', 'n'], isSequence: true },
},
{
description: t('magic_keys.groups.navigation.go_to_conversations'),
shortcut: { keys: ['g', 'c'], isSequence: true },
},
{
description: t('magic_keys.groups.navigation.go_to_favourites'),
shortcut: { keys: ['g', 'f'], isSequence: true },
},
{
description: t('magic_keys.groups.navigation.go_to_bookmarks'),
shortcut: { keys: ['g', 'b'], isSequence: true },
},
{
description: t('magic_keys.groups.navigation.go_to_explore'),
shortcut: { keys: ['g', 'e'], isSequence: true },
},
{
description: t('magic_keys.groups.navigation.go_to_local'),
shortcut: { keys: ['g', 'l'], isSequence: true },
},
{
description: t('magic_keys.groups.navigation.go_to_federated'),
shortcut: { keys: ['g', 't'], isSequence: true },
},
{
description: t('magic_keys.groups.navigation.go_to_lists'),
shortcut: { keys: ['g', 'i'], isSequence: true },
},
{
description: t('magic_keys.groups.navigation.go_to_settings'),
shortcut: { keys: ['g', 's'], isSequence: true },
},
{
description: t('magic_keys.groups.navigation.go_to_profile'),
shortcut: { keys: ['g', 'p'], isSequence: true },
},
],
},
{

View file

@ -1,45 +0,0 @@
<script setup lang="ts">
const model = defineModel<number>()
const isValid = defineModel<boolean>('isValid')
const days = ref<number | ''>(0)
const hours = ref<number | ''>(1)
const minutes = ref<number | ''>(0)
watchEffect(() => {
if (days.value === '' || hours.value === '' || minutes.value === '') {
isValid.value = false
return
}
const duration
= days.value * 24 * 60 * 60
+ hours.value * 60 * 60
+ minutes.value * 60
if (duration <= 0) {
isValid.value = false
return
}
isValid.value = true
model.value = duration
})
</script>
<template>
<div flex flex-grow-0 gap-2>
<label flex items-center gap-2>
<input v-model="days" type="number" min="0" max="1999" input-base :class="!isValid ? 'input-error' : null">
{{ $t('confirm.mute_account.days', days === '' ? 0 : days) }}
</label>
<label flex items-center gap-2>
<input v-model="hours" type="number" min="0" max="24" input-base :class="!isValid ? 'input-error' : null">
{{ $t('confirm.mute_account.hours', hours === '' ? 0 : hours) }}
</label>
<label flex items-center gap-2>
<input v-model="minutes" type="number" min="0" max="59" step="5" input-base :class="!isValid ? 'input-error' : null">
{{ $t('confirm.mute_account.minute', minutes === '' ? 0 : minutes) }}
</label>
</div>
</template>

View file

@ -1,34 +1,11 @@
<script setup lang="ts">
import type { ConfirmDialogChoice, ConfirmDialogOptions } from '~/types'
import DurationPicker from '~/components/modal/DurationPicker.vue'
import type { ConfirmDialogChoice, ConfirmDialogLabel } from '~/types'
const props = defineProps<ConfirmDialogOptions>()
defineProps<ConfirmDialogLabel>()
const emit = defineEmits<{
(evt: 'choice', choice: ConfirmDialogChoice): void
}>()
const hasDuration = ref(false)
const isValidDuration = ref(true)
const duration = ref(60 * 60) // default to 1 hour
const shouldMuteNotifications = ref(true)
const isMute = computed(() => props.extraOptionType === 'mute')
function handleChoice(choice: ConfirmDialogChoice['choice']) {
const dialogChoice = {
choice,
...isMute.value && {
extraOptions: {
mute: {
duration: hasDuration.value ? duration.value : 0,
notifications: shouldMuteNotifications.value,
},
},
},
}
emit('choice', dialogChoice)
}
</script>
<template>
@ -39,17 +16,11 @@ function handleChoice(choice: ConfirmDialogChoice['choice']) {
<div v-if="description">
{{ description }}
</div>
<div v-if="isMute" flex-col flex gap-4>
<CommonCheckbox v-model="hasDuration" :label="$t('confirm.mute_account.specify_duration')" prepend-checkbox checked-icon-color="text-primary" />
<DurationPicker v-if="hasDuration" v-model="duration" v-model:is-valid="isValidDuration" />
<CommonCheckbox v-model="shouldMuteNotifications" :label="$t('confirm.mute_account.notifications')" prepend-checkbox checked-icon-color="text-primary" />
</div>
<div flex justify-end gap-2>
<button btn-text @click="handleChoice('cancel')">
<button btn-text @click="emit('choice', 'cancel')">
{{ cancel || $t('confirm.common.cancel') }}
</button>
<button btn-solid :disabled="!isValidDuration" @click="handleChoice('confirm')">
<button btn-solid @click="emit('choice', 'confirm')">
{{ confirm || $t('confirm.common.confirm') }}
</button>
</div>

View file

@ -37,7 +37,7 @@ onUnmounted(() => locked.value = false)
</script>
<template>
<div relative h-full w-full flex pt-12 @click="onClick">
<div relative h-full w-full flex pt-12 w-100vh @click="onClick">
<button
v-if="hasNext" pointer-events-auto btn-action-icon bg="black/20" :aria-label="$t('action.previous')"
hover:bg="black/40" dark:bg="white/30" dark-hover:bg="white/20" absolute top="1/2" right-1 z5

View file

@ -15,7 +15,7 @@ const emit = defineEmits<{
const modelValue = defineModel<number>({ required: true })
const slideGap = 20
const doubleTapThreshold = 250
const doubleTapTreshold = 250
const view = ref()
const slider = ref()
@ -36,8 +36,6 @@ const isPinching = ref(false)
const maxZoomOut = ref(1)
const isZoomedIn = computed(() => scale.value > 1)
const enableAutoplay = usePreferences('enableAutoplay')
function goToFocusedSlide() {
scale.value = 1
x.value = slide.value[modelValue.value].offsetLeft * scale.value
@ -149,7 +147,7 @@ function handleLastDrag(tap: boolean, swipe: Vector2, movement: Vector2, positio
let lastTapAt = 0
function handleTap([positionX, positionY]: Vector2) {
const now = Date.now()
const isDoubleTap = now - lastTapAt < doubleTapThreshold
const isDoubleTap = now - lastTapAt < doubleTapTreshold
lastTapAt = now
if (!isDoubleTap)
@ -220,7 +218,7 @@ function handleZoomDrag([deltaX, deltaY]: Vector2) {
function handleSlideDrag([movementX, movementY]: Vector2) {
goToFocusedSlide()
if (Math.abs(movementY) > Math.abs(movementX)) // vertical movement is more than horizontal
if (Math.abs(movementY) > Math.abs(movementX)) // vertical movement is more then horizontal
y.value -= movementY / scale.value
else
x.value -= movementX / scale.value
@ -266,12 +264,8 @@ const imageStyle = computed(() => ({
items-center
justify-center
>
<component
:is="item.type === 'gifv' ? 'video' : 'img'"
<img
ref="image"
:autoplay="enableAutoplay"
controls
loop
select-none
max-w-full
max-h-full
@ -279,7 +273,7 @@ const imageStyle = computed(() => ({
:draggable="false"
:src="item.url || item.previewUrl"
:alt="item.description || ''"
/>
>
</div>
</div>
</div>

View file

@ -1,12 +1,8 @@
<script setup lang="ts">
// only one icon can be lit up at the same time
import { STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE, STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE } from '~/constants'
const moreMenuVisible = ref(false)
const { notifications } = useNotifications()
const lastAccessedNotificationRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE, '')
const lastAccessedExploreRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE, '')
</script>
<template>
@ -23,7 +19,7 @@ const lastAccessedExploreRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_EXPLO
<NuxtLink to="/search" :aria-label="$t('nav.search')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
<div i-ri:search-line />
</NuxtLink>
<NuxtLink :to="`/notifications/${lastAccessedNotificationRoute}`" :aria-label="$t('nav.notifications')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
<NuxtLink to="/notifications" :aria-label="$t('nav.notifications')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
<div flex relative>
<div class="i-ri:notification-4-line" text-xl />
<div v-if="notifications" class="top-[-0.3rem] right-[-0.3rem]" absolute font-bold rounded-full h-4 w-4 text-xs bg-primary text-inverted flex items-center justify-center>
@ -36,8 +32,8 @@ const lastAccessedExploreRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_EXPLO
</NuxtLink>
</template>
<template v-else>
<NuxtLink :to="`/${currentServer}/explore/${lastAccessedExploreRoute}`" :aria-label="$t('nav.explore')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
<div i-ri:compass-3-line />
<NuxtLink :to="`/${currentServer}/explore`" :aria-label="$t('nav.explore')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
<div i-ri:hashtag />
</NuxtLink>
<NuxtLink group :to="`/${currentServer}/public/local`" :aria-label="$t('nav.local')" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
<div i-ri:group-2-line />

View file

@ -1,13 +1,9 @@
<script setup lang="ts">
import { STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE, STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE } from '~/constants'
const { command } = defineProps<{
command?: boolean
}>()
const { notifications } = useNotifications()
const useStarFavoriteIcon = usePreferences('useStarFavoriteIcon')
const lastAccessedNotificationRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE, '')
const lastAccessedExploreRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE, '')
</script>
<template>
@ -16,7 +12,7 @@ const lastAccessedExploreRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_EXPLO
<div class="spacer" shrink xl:hidden />
<NavSideItem :text="$t('nav.home')" to="/home" icon="i-ri:home-5-line" user-only :command="command" />
<NavSideItem :text="$t('nav.notifications')" :to="`/notifications/${lastAccessedNotificationRoute}`" icon="i-ri:notification-4-line" user-only :command="command">
<NavSideItem :text="$t('nav.notifications')" to="/notifications" icon="i-ri:notification-4-line" user-only :command="command">
<template #icon>
<div flex relative>
<div class="i-ri:notification-4-line" text-xl />
@ -34,11 +30,10 @@ const lastAccessedExploreRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_EXPLO
<NavSideItem :text="$t('action.compose')" to="/compose" icon="i-ri:quill-pen-line" user-only :command="command" />
<div class="spacer" shrink hidden sm:block />
<NavSideItem :text="$t('nav.explore')" :to="isHydrated ? `/${currentServer}/explore/${lastAccessedExploreRoute}` : `/explore/${lastAccessedExploreRoute}`" icon="i-ri:compass-3-line" :command="command" />
<NavSideItem :text="$t('nav.explore')" :to="isHydrated ? `/${currentServer}/explore` : '/explore'" icon="i-ri:hashtag" :command="command" />
<NavSideItem :text="$t('nav.local')" :to="isHydrated ? `/${currentServer}/public/local` : '/public/local'" icon="i-ri:group-2-line " :command="command" />
<NavSideItem :text="$t('nav.federated')" :to="isHydrated ? `/${currentServer}/public` : '/public'" icon="i-ri:earth-line" :command="command" />
<NavSideItem :text="$t('nav.lists')" :to="isHydrated ? `/${currentServer}/lists` : '/lists'" icon="i-ri:list-check" user-only :command="command" />
<NavSideItem :text="$t('nav.hashtags')" to="/hashtags" icon="i-ri:hashtag" user-only :command="command" />
<div class="spacer" shrink hidden sm:block />
<NavSideItem :text="$t('nav.settings')" to="/settings" icon="i-ri:settings-3-line" :command="command" />

View file

@ -57,21 +57,11 @@ const noUserVisual = computed(() => isHydrated.value && props.userOnly && !curre
<div
class="item"
flex items-center gap4
w-fit rounded-3
px2 mx3 sm:mxa
xl="ml0 mr5 px5 w-auto"
:class="isSmallScreen
? `
w-full
px5 sm:mxa
transition-colors duration-200 transform
hover-bg-gray-100 hover-dark:(bg-gray-700 text-white)
` : `
w-fit rounded-3
px2 mx3 sm:mxa
transition-100
elk-group-hover-bg-active
group-focus-visible:ring-2
group-focus-visible:ring-current
`"
transition-100
elk-group-hover="bg-active" group-focus-visible:ring="2 current"
>
<slot name="icon">
<div :class="icon" text-xl />

View file

@ -34,13 +34,7 @@ const { busy, oauth, singleInstanceServer } = useSignIn()
<strong>{{ currentServer }}</strong>
</i18n-t>
</button>
<button
v-else
flex="~ row"
gap-x-1 items-center justify-center btn-solid text-sm px-2 py-1 xl:hidden
@click="openSigninDialog()"
>
<span aria-hidden="true" block i-ri:login-circle-line class="rtl-flip" />
<button v-else btn-solid text-sm px-2 py-1 text-center xl:hidden @click="openSigninDialog()">
{{ $t('action.sign_in') }}
</button>
</template>

View file

@ -4,13 +4,6 @@ import type { mastodon } from 'masto'
const { notification } = defineProps<{
notification: mastodon.v1.Notification
}>()
const { t } = useI18n()
// well-known emoji reactions types Elk does not support yet
const unsupportedEmojiReactionTypes = ['pleroma:emoji_reaction', 'reaction']
if (unsupportedEmojiReactionTypes.includes(notification.type))
console.warn(`[DEV] ${t('notification.missing_type')} '${notification.type}' (notification.id: ${notification.id})`)
</script>
<template>
@ -95,8 +88,7 @@ if (unsupportedEmojiReactionTypes.includes(notification.type))
<template v-else-if="notification.type === 'mention' || notification.type === 'poll' || notification.type === 'status'">
<StatusCard :status="notification.status!" />
</template>
<template v-else-if="!unsupportedEmojiReactionTypes.includes(notification.type)">
<!-- prevent showing errors for dev for known emoji reaction types -->
<template v-else>
<!-- type 'favourite' and 'reblog' should always rendered by NotificationGroupedLikes -->
<div text-red font-bold>
[DEV] {{ $t('notification.missing_type') }} '{{ notification.type }}'

View file

@ -25,7 +25,7 @@ function includeNotificationsForStatusCard({ type, status }: mastodon.v1.Notific
// Group by type (and status when applicable)
function groupId(item: mastodon.v1.Notification): string {
// If the update is related to a status, group notifications from the same account (boost + favorite the same status)
// If the update is related to an status, group notifications from the same account (boost + favorite the same status)
const id = item.status
? {
status: item.status?.id,
@ -171,7 +171,6 @@ const { formatNumber } = useHumanReadableNumber()
:paginator="paginator"
:preprocess="preprocess"
:stream="stream"
eventType="notification"
:virtualScroller="virtualScroller"
>
<template #updater="{ number, update }">

View file

@ -35,8 +35,8 @@ const showWarning = computed(() => {
return false
return isSupported
&& (!isSubscribed.value || !notificationPermission.value || notificationPermission.value === 'prompt')
&& !(hiddenNotification.value[currentUser.value?.account?.acct ?? ''])
&& (!isSubscribed.value || !notificationPermission.value || notificationPermission.value === 'prompt')
&& !(hiddenNotification.value[currentUser.value?.account?.acct ?? ''] === true)
})
async function saveSettings() {

View file

@ -19,8 +19,7 @@ const emit = defineEmits<{
const maxDescriptionLength = 1500
const isEditDialogOpen = ref(false)
const description = ref(props.attachment.description ?? '')
const description = computed(() => props.attachment.description ?? '')
function toggleApply() {
isEditDialogOpen.value = false
emit('setDescription', description.value)

View file

@ -30,14 +30,8 @@ const draftState = useDraft(draftKey, initial)
const { draft } = draftState
const {
isExceedingAttachmentLimit,
isUploading,
failedAttachments,
isOverDropZone,
uploadAttachments,
pickAttachments,
setDescription,
removeAttachment,
isExceedingAttachmentLimit, isUploading, failedAttachments, isOverDropZone,
uploadAttachments, pickAttachments, setDescription, removeAttachment,
dropZoneRef,
} = useUploadMediaAttachment(draft)
@ -74,7 +68,7 @@ function trimPollOptions() {
const trimmedOptions = draft.value.params.poll!.options.slice(0, indexLastNonEmpty + 1)
if (currentInstance.value?.configuration
&& trimmedOptions.length >= currentInstance.value?.configuration?.polls.maxOptions)
&& trimmedOptions.length >= currentInstance.value?.configuration?.polls.maxOptions)
draft.value.params.poll!.options = trimmedOptions
else
draft.value.params.poll!.options = [...trimmedOptions, '']
@ -87,32 +81,30 @@ function editPollOptionDraft(event: Event, index: number) {
}
function deletePollOption(index: number) {
const newPollOptions = draft.value.params.poll!.options.slice()
newPollOptions.splice(index, 1)
draft.value.params.poll!.options = newPollOptions
draft.value.params.poll!.options = draft.value.params.poll!.options.slice().splice(index, 1)
trimPollOptions()
}
const expiresInOptions = computed(() => [
{
seconds: 1 * 60 * 60,
label: t('time_ago_options.hour_future', 1),
label: isHydrated.value ? t('time_ago_options.hour_future', 1) : '',
},
{
seconds: 2 * 60 * 60,
label: t('time_ago_options.hour_future', 2),
label: isHydrated.value ? t('time_ago_options.hour_future', 2) : '',
},
{
seconds: 1 * 24 * 60 * 60,
label: t('time_ago_options.day_future', 1),
label: isHydrated.value ? t('time_ago_options.day_future', 1) : '',
},
{
seconds: 2 * 24 * 60 * 60,
label: t('time_ago_options.day_future', 2),
label: isHydrated.value ? t('time_ago_options.day_future', 2) : '',
},
{
seconds: 7 * 24 * 60 * 60,
label: t('time_ago_options.day_future', 7),
label: isHydrated.value ? t('time_ago_options.day_future', 7) : '',
},
])
@ -140,7 +132,7 @@ const characterCount = computed(() => {
length -= fullMatch.length - (before + username).length - 1 // - 1 for the @
if (draft.value.mentions) {
// + 1 is needed as mentions always need a space separator at the end
// + 1 is needed as mentions always need a space seperator at the end
length += draft.value.mentions.map((mention) => {
const [handle] = mention.split('@')
return `@${handle}`
@ -158,8 +150,6 @@ const isExceedingCharacterLimit = computed(() => {
const postLanguageDisplay = computed(() => languagesNameList.find(i => i.code === (draft.value.params.language || preferredLanguage))?.nativeName)
const isDM = computed(() => draft.value.params.visibility === 'direct')
async function handlePaste(evt: ClipboardEvent) {
const files = evt.clipboardData?.files
if (!files || files.length === 0)
@ -279,16 +269,12 @@ onDeactivated(() => {
</ol>
</CommonErrorMessage>
<div relative flex-1 flex flex-col min-h-30>
<div relative flex-1 flex flex-col>
<EditorContent
:editor="editor"
flex max-w-full
:class="{
'md:max-h-[calc(100vh-200px)] sm:max-h-[calc(100vh-400px)] max-h-35 of-y-auto overscroll-contain': shouldExpanded,
'py2 px3.5 bg-dm rounded-4 me--1 ms--1 mt--1': isDM,
}"
:class="shouldExpanded ? 'min-h-30 md:max-h-[calc(100vh-200px)] sm:max-h-[calc(100vh-400px)] max-h-35 of-y-auto overscroll-contain' : ''"
@keydown="stopQuestionMarkPropagation"
@keydown.esc.prevent="editor?.commands.blur()"
/>
</div>

View file

@ -77,12 +77,11 @@ function activate() {
ps-3
pe-1
ml-1
:placeholder="t('nav.search')"
:placeholder="isHydrated ? t('nav.search') : ''"
pb="1px"
placeholder-text-secondary
@keydown.down.prevent="shift(1)"
@keydown.up.prevent="shift(-1)"
@keydown.esc.prevent="input?.blur()"
@keypress.enter="activate"
>
<button v-if="query.length" btn-action-icon text-secondary @click="query = ''; input?.focus()">

View file

@ -10,22 +10,20 @@ const props = defineProps<{
external?: true
large?: true
match?: boolean
target?: string
}>()
const router = useRouter()
const scrollOnClick = computed(() => props.to && !(props.target === '_blank' || props.external))
useCommand({
scope: 'Settings',
name: () => props.text
?? (props.to
? typeof props.to === 'string'
? props.to
: props.to.name
: ''
),
?? (props.to
? typeof props.to === 'string'
? props.to
: props.to.name
: ''
),
description: () => props.description,
icon: () => props.icon || '',
visible: () => props.command && props.to,
@ -41,12 +39,11 @@ useCommand({
:disabled="disabled"
:to="to"
:external="external"
:target="target"
exact-active-class="text-primary"
:class="disabled ? 'op25 pointer-events-none ' : match ? 'text-primary' : ''"
block w-full group focus:outline-none
:tabindex="disabled ? -1 : null"
@click="scrollOnClick ? $scrollToTop() : undefined"
@click="to ? $scrollToTop() : undefined"
>
<div
w-full flex px5 py3 md:gap2 gap4 items-center

View file

@ -8,7 +8,8 @@ const dropdown = ref<any>()
const fieldIcons = computed(() =>
Array.from({ length: maxAccountFieldCount.value }, (_, i) =>
getAccountFieldIcon(form.value.fieldsAttributes[i].name)),
getAccountFieldIcon(form.value.fieldsAttributes[i].name),
),
)
const fieldCount = computed(() => {

View file

@ -16,7 +16,7 @@ const { disabled = false } = defineProps<{
:class="disabled ? 'opacity-50 cursor-not-allowed' : ''"
>
<div
w-full flex px5 py3 md:gap2 gap4 items-center
w-full flex w-fit px5 py3 md:gap2 gap4 items-center
transition-250
:class="disabled ? '' : 'group-hover:bg-active'"
group-focus-visible:ring="2 current"

View file

@ -55,7 +55,7 @@ function reply() {
<div flex-1>
<StatusActionButton
:content="$t(status.reblogged ? 'action.boosted' : 'action.boost')"
:content="$t('action.boost')"
:text="!getPreferences(userSettings, 'hideBoostCount') && status.reblogsCount ? status.reblogsCount : ''"
color="text-green" hover="text-green" elk-group-hover="bg-green/10"
icon="i-ri:repeat-line"
@ -77,7 +77,7 @@ function reply() {
<div flex-1>
<StatusActionButton
:content="$t(status.favourited ? 'action.favourited' : 'action.favourite')"
:content="$t('action.favourite')"
:text="!getPreferences(userSettings, 'hideFavoriteCount') && status.favouritesCount ? status.favouritesCount : ''"
:color="useStarFavoriteIcon ? 'text-yellow' : 'text-rose'"
:hover="useStarFavoriteIcon ? 'text-yellow' : 'text-rose'"
@ -100,7 +100,7 @@ function reply() {
<div flex-none>
<StatusActionButton
:content="$t(status.bookmarked ? 'action.bookmarked' : 'action.bookmark')"
:content="$t('action.bookmark')"
:color="useStarFavoriteIcon ? 'text-rose' : 'text-yellow'"
:hover="useStarFavoriteIcon ? 'text-rose' : 'text-yellow'"
:elk-group-hover="useStarFavoriteIcon ? 'bg-rose/10' : 'bg-yellow/10' "

View file

@ -62,13 +62,12 @@ async function shareLink(status: mastodon.v1.Status) {
}
async function deleteStatus() {
const confirmDelete = await openConfirmDialog({
if (await openConfirmDialog({
title: t('confirm.delete_posts.title'),
description: t('confirm.delete_posts.description'),
confirm: t('confirm.delete_posts.confirm'),
cancel: t('confirm.delete_posts.cancel'),
})
if (confirmDelete.choice !== 'confirm')
}) !== 'confirm')
return
removeCachedStatus(status.value.id)
@ -81,13 +80,12 @@ async function deleteStatus() {
}
async function deleteAndRedraft() {
const confirmDelete = await openConfirmDialog({
if (await openConfirmDialog({
title: t('confirm.delete_posts.title'),
description: t('confirm.delete_posts.description'),
confirm: t('confirm.delete_posts.confirm'),
cancel: t('confirm.delete_posts.cancel'),
})
if (confirmDelete.choice !== 'confirm')
}) !== 'confirm')
return
if (import.meta.dev) {
@ -144,7 +142,7 @@ function showFavoritedAndBoostedBy() {
<template #popper>
<div flex="~ col">
<template v-if="getPreferences(userSettings, 'zenMode') && !details">
<template v-if="getPreferences(userSettings, 'zenMode')">
<CommonDropdownItem
:text="$t('action.reply')"
icon="i-ri:chat-1-line"

View file

@ -68,7 +68,6 @@ const video = ref<HTMLVideoElement | undefined>()
const prefersReducedMotion = usePreferredReducedMotion()
const isAudio = computed(() => attachment.type === 'audio')
const isVideo = computed(() => attachment.type === 'video')
const isGif = computed(() => attachment.type === 'gifv')
const enableAutoplay = usePreferences('enableAutoplay')
@ -165,7 +164,7 @@ watch(shouldLoadAttachment, () => {
<button
type="button"
relative
@click="!shouldLoadAttachment ? loadAttachment() : openMediaPreview(attachments ? attachments : [attachment], attachments?.indexOf(attachment) || 0)"
@click="!shouldLoadAttachment ? loadAttachment() : null"
>
<video
ref="video"
@ -249,13 +248,12 @@ watch(shouldLoadAttachment, () => {
</button>
</template>
<div
:class="isAudio ? [] : [
v-if="attachment.description && !getPreferences(userSettings, 'hideAltIndicatorOnPosts')" :class="isAudio ? [] : [
'absolute left-2',
isVideo ? 'top-2' : 'bottom-2',
]"
flex gap-col-2
>
<VDropdown v-if="attachment.description && !getPreferences(userSettings, 'hideAltIndicatorOnPosts')" :distance="6" placement="bottom-start">
<VDropdown :distance="6" placement="bottom-start">
<button
font-bold text-sm
:class="isAudio
@ -283,14 +281,6 @@ watch(shouldLoadAttachment, () => {
</div>
</template>
</VDropdown>
<div v-if="isGif && !getPreferences(userSettings, 'hideGifIndicatorOnPosts')">
<button
aria-hidden font-bold text-sm
rounded-1 bg-black:65 text-white px1.2 py0.2 pointer-events-none
>
{{ $t('status.gif') }}
</button>
</div>
</div>
</div>
</template>

View file

@ -63,8 +63,8 @@ const isSelfReply = computed(() => status.value.inReplyToAccountId === status.va
const collapseRebloggedBy = computed(() => rebloggedBy.value?.id === status.value.account.id)
const isDM = computed(() => status.value.visibility === 'direct')
const showUpperBorder = computed(() => props.newer && !directReply.value)
const showReplyTo = computed(() => !replyToMain.value && !directReply.value)
const showUpperBorder = computed(() => props.newer && !directReply)
const showReplyTo = computed(() => !replyToMain && !directReply)
const forceShow = ref(false)
</script>

View file

@ -30,14 +30,14 @@ const hideAllMedia = computed(
},
)
const embeddedMediaPreference = usePreferences('experimentalEmbeddedMedia')
const allowEmbeddedMedia = computed(() => status.card?.html && embeddedMediaPreference.value)
const allowEmbeddedMedia = computed(() => status.card?.html && embeddedMediaPreference)
</script>
<template>
<div
space-y-3
:class="{
'py2 px3.5 bg-dm rounded-4 me--1': isDM,
'pt2 pb0.5 px3.5 bg-dm rounded-4 me--1': isDM,
'ms--3.5 mt--1 ms--1': isDM && context !== 'details',
}"
>
@ -68,6 +68,7 @@ const allowEmbeddedMedia = computed(() => status.card?.html && embeddedMediaPref
:status="status.reblog" border="~ rounded"
:actions="false"
/>
<div v-if="isDM" />
</StatusSpoiler>
</div>
</template>

View file

@ -31,7 +31,7 @@ useHydratedHead({
<template>
<div :id="`status-${status.id}`" flex flex-col gap-2 pt2 pb1 ps-3 pe-4 relative :lang="status.language ?? undefined" aria-roledescription="status-details">
<StatusActionsMore :status="status" :details="true" absolute inset-ie-2 top-2 @after-edit="$emit('refetchStatus')" />
<StatusActionsMore :status="status" absolute inset-ie-2 top-2 @after-edit="$emit('refetchStatus')" />
<NuxtLink :to="getAccountRoute(status.account)" rounded-full hover:bg-active transition-100 pe5 me-a>
<AccountHoverWrapper :account="status.account">
<AccountInfo :account="status.account" />

View file

@ -42,7 +42,7 @@ const tabs = [
>
<div
relative flex flex-auto cursor-pointer sm:px6 px2 rounded transition-all
tabindex="0"
tabindex="1"
hover:bg-active transition-100
@click="option.onClick"
>

View file

@ -14,8 +14,7 @@ function onclick(evt: MouseEvent | KeyboardEvent) {
const path = evt.composedPath() as HTMLElement[]
const el = path.find(el => ['A', 'BUTTON', 'IMG', 'VIDEO'].includes(el.tagName?.toUpperCase()))
const text = window.getSelection()?.toString()
const isCustomEmoji = el?.parentElement?.classList.contains('custom-emoji')
if ((!el && !text) || isCustomEmoji)
if (!el && !text)
go(evt)
}

View file

@ -1,56 +1,21 @@
<script setup lang="ts">
import type { mastodon } from 'masto'
import { fetchAccountById } from '~/composables/cache'
type WatcherType = [status?: mastodon.v1.Status, v?: boolean]
const props = defineProps<{
const {
status,
isSelfReply = false,
} = defineProps<{
status: mastodon.v1.Status
isSelfReply: boolean
}>()
const link = ref()
const targetIsVisible = ref(false)
const isSelf = computed(() => props.status.inReplyToAccountId === props.status.account.id)
const account = ref<mastodon.v1.Account | null | undefined>(isSelf.value ? props.status.account : undefined)
useIntersectionObserver(
link,
([{ intersectionRatio }]) => {
targetIsVisible.value = intersectionRatio > 0.1
},
)
watch(
() => [props.status, targetIsVisible.value] satisfies WatcherType,
([newStatus, newVisible]) => {
if (newStatus.account && newStatus.inReplyToAccountId === newStatus.account.id) {
account.value = newStatus.account
return
}
if (!newVisible)
return
const newId = newStatus.inReplyToAccountId
if (newId) {
fetchAccountById(newStatus.inReplyToAccountId).then((acc) => {
if (newId === props.status.inReplyToAccountId)
account.value = acc
})
return
}
account.value = undefined
},
{ immediate: true, flush: 'post' },
)
const isSelf = computed(() => status.inReplyToAccountId === status.account.id)
const account = isSelf ? computed(() => status.account) : useAccountById(status.inReplyToAccountId)
</script>
<template>
<NuxtLink
v-if="status.inReplyToId"
ref="link"
flex="~ gap2" items-center h-auto text-sm text-secondary
:to="getStatusInReplyToRoute(status)"
:title="$t('status.replying_to', [account ? getDisplayName(account) : $t('status.someone')])"

View file

@ -1,10 +1,5 @@
<script setup lang="ts">
const props = defineProps<{
enabled?: boolean
filter?: boolean
isDM?: boolean
sensitiveNonSpoiler?: boolean
}>()
const props = defineProps<{ enabled?: boolean; filter?: boolean; isDM?: boolean; sensitiveNonSpoiler?: boolean }>()
const expandSpoilers = computed(() => {
const expandCW = currentUser.value ? getExpandSpoilersByDefault(currentUser.value.account) : false

View file

@ -1,7 +1,9 @@
<script setup lang="ts">
<script lang="ts" setup>
import type { mastodon } from 'masto'
const { tag } = defineProps<{
const {
tag,
} = defineProps<{
tag: mastodon.v1.Tag
}>()
@ -30,21 +32,19 @@ function go(evt: MouseEvent | KeyboardEvent) {
<template>
<div
block p4 hover:bg-active flex justify-between cursor-pointer flex-gap-2
block p4 hover:bg-active flex justify-between cursor-pointer
@click="onclick"
@keydown.enter="onclick"
>
<div flex flex-gap-2>
<TagActionButton :tag="tag" />
<div>
<h4 flex items-center text-size-base leading-normal font-medium line-clamp-1 break-all ws-pre-wrap>
<bdi>
<span>#</span>
<span hover:underline>{{ tag.name }}</span>
</bdi>
</h4>
<CommonTrending v-if="tag.history" :history="tag.history" text-sm text-secondary line-clamp-1 ws-pre-wrap break-all />
</div>
<div>
<h4 flex items-center text-size-base leading-normal font-medium line-clamp-1 break-all ws-pre-wrap>
<TagActionButton :tag="tag" />
<bdi>
<span>#</span>
<span hover:underline>{{ tag.name }}</span>
</bdi>
</h4>
<CommonTrending v-if="tag.history" :history="tag.history" text-sm text-secondary line-clamp-1 ws-pre-wrap break-all />
</div>
<div v-if="tag.history" flex items-center>
<CommonTrendingCharts :history="tag.history" />

View file

@ -1,5 +1,5 @@
<template>
<div p4 flex justify-between gap-4>
<div p4 flex justify-between>
<div flex="~ col 1 gap-2">
<div flex class="skeleton-loading-bg" h-5 w-30 rounded />
<div flex class="skeleton-loading-bg" h-4 w-45 rounded />

View file

@ -1,27 +1,18 @@
<script setup lang="ts">
import type { mastodon } from 'masto'
import { STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE } from '~/constants'
const { filter } = defineProps<{
filter?: mastodon.v1.NotificationType
}>()
const route = useRoute()
const lastAccessedNotificationRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE, '')
const options = { limit: 30, types: filter ? [filter] : [] }
// Default limit is 20 notifications, and servers are normally caped to 30
const paginator = useMastoClient().v1.notifications.list(options)
const stream = useStreaming(client => client.user.notification.subscribe())
lastAccessedNotificationRoute.value = route.path.replace(/\/notifications\/?/, '')
const { clearNotifications } = useNotifications()
onActivated(() => {
clearNotifications()
lastAccessedNotificationRoute.value = route.path.replace(/\/notifications\/?/, '')
})
onActivated(clearNotifications)
</script>
<template>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
const paginator = useMastoClient().v1.timelines.public.list({ limit: 30, local: true })
const stream = useStreaming(client => client.public.local.subscribe())
const stream = useStreaming(client => client.direct.subscribe())
</script>
<template>

View file

@ -72,7 +72,7 @@ function onEnter(e: KeyboardEvent) {
}
function escapeAutocomplete(evt: KeyboardEvent) {
if (!autocompleteShow.value)
if (!autocompleteShow)
return
autocompleteShow.value = false
evt.stopPropagation()

View file

@ -3,10 +3,8 @@ import type { BuildInfo } from '~~/types'
export interface Team {
github: string
display: string
twitter?: string
twitter: string
mastodon: string
link: string
sponsors?: string
}
export const elkTeamMembers: Team[] = [
@ -15,43 +13,24 @@ export const elkTeamMembers: Team[] = [
display: 'Anthony Fu',
twitter: 'antfu7',
mastodon: 'antfu@webtoo.ls',
link: '/m.webtoo.ls/@antfu',
},
{
github: 'patak-dev',
display: 'Patak',
twitter: 'patak_dev',
mastodon: 'patak@webtoo.ls',
link: '/m.webtoo.ls/@patak',
},
{
github: 'danielroe',
display: 'Daniel Roe',
twitter: 'danielcroe',
mastodon: 'daniel@roe.dev',
link: '/mastodon.roe.dev/@daniel',
},
{
github: 'sxzz',
display: '三咲智子 Kevin Deng',
twitter: 'sanxiaozhizi',
mastodon: 'sxzz@webtoo.ls',
link: '/m.webtoo.ls/@sxzz',
},
{
github: 'userquin',
display: 'Joaquín Sánchez',
twitter: 'userquin',
mastodon: 'userquin@webtoo.ls',
link: '/m.webtoo.ls/@userquin',
sponsors: 'elk-zone', // sponsors/userquin isn't enabled
},
{
github: 'shuuji3',
display: 'TAKAHASHI Shuuji',
mastodon: 'shuuji3@webtoo.ls',
link: '/m.webtoo.ls/@shuuji3',
sponsors: 'elk-zone', // sponsors/shuuji3 isn't enabled
},
].sort(() => Math.random() - 0.5)

View file

@ -23,8 +23,7 @@ export function fetchStatus(id: string, force = false): Promise<mastodon.v1.Stat
const key = `${server}:${userId}:status:${id}`
const cached = cache.get(key)
if (cached && !force)
return Promise.resolve(cached)
return cached
const promise = useMastoClient().v1.statuses.$select(id).fetch()
.then((status) => {
cacheStatus(status)
@ -43,8 +42,7 @@ export function fetchAccountById(id?: string | null): Promise<mastodon.v1.Accoun
const key = `${server}:${userId}:account:${id}`
const cached = cache.get(key)
if (cached)
return Promise.resolve(cached)
return cached
const domain = getInstanceDomainFromServer(server)
const promise = useMastoClient().v1.accounts.$select(id).fetch()
.then((r) => {
@ -66,7 +64,7 @@ export async function fetchAccountByHandle(acct: string): Promise<mastodon.v1.Ac
const key = `${server}:${userId}:account:${userAcct}`
const cached = cache.get(key)
if (cached)
return Promise.resolve(cached)
return cached
async function lookupAccount() {
const client = useMastoClient()
@ -84,30 +82,13 @@ export async function fetchAccountByHandle(acct: string): Promise<mastodon.v1.Ac
return account
}
const promise = lookupAccount()
const account = lookupAccount()
.then((r) => {
cacheAccount(r, server, true)
return r
})
cache.set(key, promise)
return promise
}
export function fetchTag(tagName: string, force = false): Promise<mastodon.v1.Tag> {
const server = currentServer.value
const userId = currentUser.value?.account.id
const key = `${server}:${userId}:tag:${tagName}`
const cached = cache.get(key)
if (cached && !force)
return Promise.resolve(cached)
const promise = useMastoClient().v1.tags.$select(tagName).fetch()
.then((tag) => {
cacheTag(tag)
return tag
})
cache.set(key, promise)
return promise
cache.set(key, account)
return account
}
export function useAccountById(id?: string | null) {
@ -130,8 +111,3 @@ export function cacheAccount(account: mastodon.v1.Account, server = currentServe
setCached(`${server}:${userId}:account:${account.id}`, account, override)
setCached(`${server}:${userId}:account:${userAcct}`, account, override)
}
export function cacheTag(tag: mastodon.v1.Tag, server = currentServer.value, override?: boolean) {
const userId = currentUser.value?.account.id
setCached(`${server}:${userId}:tag:${tag.name}`, tag, override)
}

View file

@ -170,8 +170,7 @@ export const useCommandRegistry = defineStore('command', () => {
const indexed = cmds.map((cmd, index) => ({ ...cmd, index }))
const grouped = new Map<CommandScopeNames, CommandQueryResultItem[]>(
scopes.map(scope => [scope, []]),
)
scopes.map(scope => [scope, []]))
for (const cmd of indexed) {
const scope = cmd.scope ?? ''
grouped.get(scope)!.push({
@ -245,12 +244,24 @@ export function useCommands(cmds: () => CommandProvider[]) {
export function provideGlobalCommands() {
const { locale, t } = useI18n()
const { locales } = useI18n() as { locales: ComputedRef<LocaleObject[]> }
const router = useRouter()
const users = useUsers()
const masto = useMasto()
const colorMode = useColorMode()
const userSettings = useUserSettings()
const { singleInstanceServer, oauth } = useSignIn()
useCommand({
scope: 'Navigation',
name: () => t('nav.settings'),
icon: 'i-ri:settings-3-line',
onActivate() {
router.push('/settings')
},
})
useCommand({
scope: 'Preferences',

View file

@ -494,10 +494,7 @@ function _markdownProcess(value: string) {
let start = 0
while (true) {
let found: {
match: RegExpMatchArray
replacer: (c: (string | Node)[]) => Node
} | undefined
let found: { match: RegExpMatchArray; replacer: (c: (string | Node)[]) => Node } | undefined
for (const [re, replacer] of _markdownReplacements) {
re.lastIndex = start
@ -527,21 +524,10 @@ function transformMarkdown(node: Node) {
return _markdownProcess(node.value)
}
function addBdiParagraphs(node: Node) {
if (node.name === 'p' && !('dir' in node.attributes) && node.children?.length && node.children.length > 1)
node.attributes.dir = 'auto'
return node
}
function transformParagraphs(node: Node): Node | Node[] {
// Add bdi to paragraphs
addBdiParagraphs(node)
// For top level paragraphs, inject an empty <p> to preserve status paragraphs in our editor (except for the last one)
if (node.parent?.type === DOCUMENT_NODE && node.name === 'p' && node.parent.children.at(-1) !== node)
return [node, h('p')]
return node
}
@ -623,7 +609,7 @@ function transformCollapseMentions(status?: mastodon.v1.Status, inReplyToStatus?
// We have a special case for single mentions that are part of a reply.
// We already have the replying to badge in this case or the status is connected to the previous one.
// This is needed because the status doesn't include the in Reply to handle, only the account id.
// This is needed because the status doesn't included the in Reply to handle, only the account id.
// But this covers the majority of cases.
const showMentions = !(contextualMentionsCount === 0 || (mentionsCount === 1 && status?.inReplyToAccountId))
const grouped = contextualMentionsCount > 2

View file

@ -10,7 +10,6 @@ import Emoji from '~/components/emoji/Emoji.vue'
import ContentCode from '~/components/content/ContentCode.vue'
import ContentMentionGroup from '~/components/content/ContentMentionGroup.vue'
import AccountHoverWrapper from '~/components/account/AccountHoverWrapper.vue'
import TagHoverWrapper from '~/components/account/TagHoverWrapper.vue'
function getTextualAstComponents(astChildren: Node[]): string {
return astChildren
@ -129,13 +128,11 @@ function handleMention(el: Node) {
addBdiNode(el)
return h(AccountHoverWrapper, { handle, class: 'inline-block' }, () => nodeToVNode(el))
}
const matchTag = href.match(TagLinkRE)
if (matchTag) {
const [, , tagName] = matchTag
const [, , name] = matchTag
addBdiNode(el)
el.attributes.href = `/${currentServer.value}/tags/${tagName}`
return h(TagHoverWrapper, { tagName, class: 'inline-block' }, () => nodeToVNode(el))
el.attributes.href = `/${currentServer.value}/tags/${name}`
}
}
}

View file

@ -1,9 +1,9 @@
import type { mastodon } from 'masto'
import type { ConfirmDialogChoice, ConfirmDialogOptions, Draft, ErrorDialogData } from '~/types'
import type { ConfirmDialogChoice, ConfirmDialogLabel, Draft, ErrorDialogData } from '~/types'
import { STORAGE_KEY_FIRST_VISIT } from '~/constants'
export const confirmDialogChoice = ref<ConfirmDialogChoice>()
export const confirmDialogLabel = ref<ConfirmDialogOptions>()
export const confirmDialogLabel = ref<ConfirmDialogLabel>()
export const errorDialogData = ref<ErrorDialogData>()
export const mediaPreviewList = ref<mastodon.v1.MediaAttachment[]>([])
@ -39,7 +39,7 @@ export function openSigninDialog() {
isSigninDialogOpen.value = true
}
export async function openConfirmDialog(label: ConfirmDialogOptions | string): Promise<ConfirmDialogChoice> {
export async function openConfirmDialog(label: ConfirmDialogLabel | string): Promise<ConfirmDialogChoice> {
confirmDialogLabel.value = typeof label === 'string' ? { title: label } : label
confirmDialogChoice.value = undefined
isConfirmDialogOpen.value = true

View file

@ -29,10 +29,8 @@ export function useHumanReadableNumber() {
}
}
export function useFormattedDateTime(
value: MaybeRefOrGetter<string | number | Date | undefined | null>,
options: Intl.DateTimeFormatOptions = { dateStyle: 'long', timeStyle: 'medium' },
) {
export function useFormattedDateTime(value: MaybeRefOrGetter<string | number | Date | undefined | null>,
options: Intl.DateTimeFormatOptions = { dateStyle: 'long', timeStyle: 'medium' }) {
const { locale } = useI18n()
const formatter = computed(() => Intl.DateTimeFormat(locale.value, options))
return computed(() => {
@ -77,32 +75,3 @@ export function useTimeAgoOptions(short = false): UseTimeAgoOptions<false> {
},
}
}
export function useFileSizeFormatter() {
const { locale } = useI18n()
const formatters = computed(() => ([
Intl.NumberFormat(locale.value, {
style: 'unit',
unit: 'megabyte',
unitDisplay: 'narrow',
maximumFractionDigits: 0,
}),
Intl.NumberFormat(locale.value, {
style: 'unit',
unit: 'kilobyte',
unitDisplay: 'narrow',
maximumFractionDigits: 0,
}),
]))
const megaByte = 1024 * 1024
function formatFileSize(size: number) {
return size >= megaByte
? formatters.value[0].format(size / megaByte)
: formatters.value[1].format(size / 1024)
}
return { formatFileSize }
}

View file

@ -36,11 +36,9 @@ export function useMagicSequence(keys: string[]): ComputedRef<boolean> {
down = false
success.value = true
}
},
{
}, {
deep: true,
},
)
})
return computed(() => success.value)
}

View file

@ -36,25 +36,26 @@ export function usePublish(options: {
const { params, attachments } = draft.value
const firstEmptyInputIndex = params.poll?.options.findIndex(option => option.trim().length === 0)
return isEmpty.value
|| options.isUploading.value
|| isSending.value
|| (attachments.length === 0 && !params.status)
|| failedMessages.value.length > 0
|| (attachments.length > 0 && params.poll !== null && params.poll !== undefined)
|| ((params.poll !== null && params.poll !== undefined)
&& (
(firstEmptyInputIndex !== -1
&& firstEmptyInputIndex !== params.poll.options.length - 1
)
|| params.poll.options.findLastIndex(option => option.trim().length > 0) + 1 < 2
|| (new Set(params.poll.options).size !== params.poll.options.length)
|| (currentInstance.value?.configuration?.polls.maxCharactersPerOption !== undefined
&& params.poll.options.find(option => option.length > currentInstance.value!.configuration!.polls.maxCharactersPerOption) !== undefined
)
))
|| options.isUploading.value
|| isSending.value
|| (attachments.length === 0 && !params.status)
|| failedMessages.value.length > 0
|| (attachments.length > 0 && params.poll !== null && params.poll !== undefined)
|| ((params.poll !== null && params.poll !== undefined)
&& (
(firstEmptyInputIndex !== -1
&& firstEmptyInputIndex !== params.poll.options.length - 1
)
|| params.poll.options.findLastIndex(option => option.trim().length > 0) + 1 < 2
|| (new Set(params.poll.options).size !== params.poll.options.length)
|| (currentInstance.value?.configuration?.polls.maxCharactersPerOption !== undefined
&& params.poll.options.find(option => option.length > currentInstance.value!.configuration!.polls.maxCharactersPerOption) !== undefined
)
)
)
})
watch(draft, () => {
watch(() => draft, () => {
if (failedMessages.value.length > 0)
failedMessages.value.length = 0
}, { deep: true })
@ -155,7 +156,6 @@ export type MediaAttachmentUploadError = [filename: string, message: string]
export function useUploadMediaAttachment(draft: Ref<Draft>) {
const { client } = useMasto()
const { t } = useI18n()
const { formatFileSize } = useFileSizeFormatter()
const isUploading = ref<boolean>(false)
const isExceedingAttachmentLimit = ref<boolean>(false)
@ -164,7 +164,7 @@ export function useUploadMediaAttachment(draft: Ref<Draft>) {
const maxPixels = computed(() => {
return currentInstance.value?.configuration?.mediaAttachments?.imageMatrixLimit
?? 4096 ** 2
?? 4096 ** 2
})
const loadImage = (inputFile: Blob) => new Promise<HTMLImageElement>((resolve, reject) => {
@ -225,32 +225,8 @@ export function useUploadMediaAttachment(draft: Ref<Draft>) {
// TODO: display some kind of message if too many media are selected
// DONE
const limit = currentInstance.value!.configuration?.statuses.maxMediaAttachments || 4
const maxVideoSize = currentInstance.value!.configuration?.mediaAttachments.videoSizeLimit || 0
const maxImageSize = currentInstance.value!.configuration?.mediaAttachments.imageSizeLimit || 0
for (const file of files.slice(0, limit)) {
if (draft.value.attachments.length < limit) {
if (file.type.startsWith('image/')) {
if (maxImageSize > 0 && file.size > maxImageSize) {
failedAttachments.value = [...failedAttachments.value, [file.name, t('state.attachments_limit_image_error', [formatFileSize(maxImageSize)])]]
continue
}
}
else {
if (maxVideoSize > 0 && file.size > maxVideoSize) {
const key
= file.type.startsWith('audio/')
? 'state.attachments_limit_audio_error'
: file.type.startsWith('video/')
? 'state.attachments_limit_video_error'
: 'state.attachments_limit_unknown_error'
const errorMessage = t(key, [formatFileSize(maxVideoSize)])
failedAttachments.value = [
...failedAttachments.value,
[file.name, errorMessage],
]
continue
}
}
isExceedingAttachmentLimit.value = false
try {
const attachment = await client.value.v1.media.create({

View file

@ -11,12 +11,9 @@ let timeoutHandle: NodeJS.Timeout | undefined
export function useRelationship(account: mastodon.v1.Account): Ref<mastodon.v1.Relationship | undefined> {
if (!currentUser.value)
return ref()
let relationship = requestedRelationships.get(account.id)
if (relationship)
return relationship
// allow batch relationship requests
relationship = ref<mastodon.v1.Relationship | undefined>()
requestedRelationships.set(account.id, relationship)
if (timeoutHandle)
@ -25,19 +22,14 @@ export function useRelationship(account: mastodon.v1.Account): Ref<mastodon.v1.R
timeoutHandle = undefined
fetchRelationships()
}, 100)
return relationship
}
async function fetchRelationships() {
const requested = Array.from(requestedRelationships.entries()).filter(([, r]) => !r.value)
const relationships = await useMastoClient().v1.accounts.relationships.fetch({ id: requested.map(([id]) => id) })
for (const relationship of relationships) {
const requestedToUpdate = requested.find(([id]) => id === relationship.id)
if (!requestedToUpdate)
continue
requestedToUpdate[1].value = relationship
}
for (let i = 0; i < requested.length; i++)
requested[i][1].value = relationships[i]
}
export async function toggleFollowAccount(relationship: mastodon.v1.Relationship, account: mastodon.v1.Account) {
@ -47,13 +39,12 @@ export async function toggleFollowAccount(relationship: mastodon.v1.Relationship
const unfollow = relationship!.following || relationship!.requested
if (unfollow) {
const confirmUnfollow = await openConfirmDialog({
if (await openConfirmDialog({
title: i18n.t('confirm.unfollow.title'),
description: i18n.t('confirm.unfollow.description', [`@${account.acct}`]),
confirm: i18n.t('confirm.unfollow.confirm'),
cancel: i18n.t('confirm.unfollow.cancel'),
})
if (confirmUnfollow.choice !== 'confirm')
}) !== 'confirm')
return
}
@ -75,28 +66,18 @@ export async function toggleMuteAccount(relationship: mastodon.v1.Relationship,
const { client } = useMasto()
const i18n = useNuxtApp().$i18n
let duration = 0 // default 0 == indefinite
let notifications = true // default true = mute notifications
if (!relationship!.muting) {
const confirmMute = await openConfirmDialog({
title: i18n.t('confirm.mute_account.title'),
description: i18n.t('confirm.mute_account.description', [account.acct]),
confirm: i18n.t('confirm.mute_account.confirm'),
cancel: i18n.t('confirm.mute_account.cancel'),
extraOptionType: 'mute',
})
if (confirmMute.choice !== 'confirm')
return
duration = confirmMute.extraOptions!.mute.duration
notifications = confirmMute.extraOptions!.mute.notifications
}
if (!relationship!.muting && await openConfirmDialog({
title: i18n.t('confirm.mute_account.title'),
description: i18n.t('confirm.mute_account.description', [account.acct]),
confirm: i18n.t('confirm.mute_account.confirm'),
cancel: i18n.t('confirm.mute_account.cancel'),
}) !== 'confirm')
return
relationship!.muting = !relationship!.muting
relationship = relationship!.muting
? await client.value.v1.accounts.$select(account.id).mute({
duration,
notifications,
// TODO support more options
})
: await client.value.v1.accounts.$select(account.id).unmute()
}
@ -105,16 +86,13 @@ export async function toggleBlockAccount(relationship: mastodon.v1.Relationship,
const { client } = useMasto()
const i18n = useNuxtApp().$i18n
if (!relationship!.blocking) {
const confirmBlock = await openConfirmDialog({
title: i18n.t('confirm.block_account.title'),
description: i18n.t('confirm.block_account.description', [account.acct]),
confirm: i18n.t('confirm.block_account.confirm'),
cancel: i18n.t('confirm.block_account.cancel'),
})
if (confirmBlock.choice !== 'confirm')
return
}
if (!relationship!.blocking && await openConfirmDialog({
title: i18n.t('confirm.block_account.title'),
description: i18n.t('confirm.block_account.description', [account.acct]),
confirm: i18n.t('confirm.block_account.confirm'),
cancel: i18n.t('confirm.block_account.cancel'),
}) !== 'confirm')
return
relationship!.blocking = !relationship!.blocking
relationship = await client.value.v1.accounts.$select(account.id)[relationship!.blocking ? 'block' : 'unblock']()
@ -124,16 +102,13 @@ export async function toggleBlockDomain(relationship: mastodon.v1.Relationship,
const { client } = useMasto()
const i18n = useNuxtApp().$i18n
if (!relationship!.domainBlocking) {
const confirmDomainBlock = await openConfirmDialog({
title: i18n.t('confirm.block_domain.title'),
description: i18n.t('confirm.block_domain.description', [getServerName(account)]),
confirm: i18n.t('confirm.block_domain.confirm'),
cancel: i18n.t('confirm.block_domain.cancel'),
})
if (confirmDomainBlock.choice !== 'confirm')
return
}
if (!relationship!.domainBlocking && await openConfirmDialog({
title: i18n.t('confirm.block_domain.title'),
description: i18n.t('confirm.block_domain.description', [getServerName(account)]),
confirm: i18n.t('confirm.block_domain.confirm'),
cancel: i18n.t('confirm.block_domain.cancel'),
}) !== 'confirm')
return
relationship!.domainBlocking = !relationship!.domainBlocking
await client.value.v1.domainBlocks[relationship!.domainBlocking ? 'create' : 'remove']({ domain: getServerName(account) })

View file

@ -59,11 +59,11 @@ export function useSearch(query: MaybeRefOrGetter<string>, options: UseSearchOpt
}
watch(() => resolveUnref(query), () => {
loading.value = !!(q.value && isHydrated.value)
loading.value = !!(q && isHydrated.value)
})
debouncedWatch(() => resolveUnref(query), async () => {
if (!q.value || !isHydrated.value)
if (!q || !isHydrated.value)
return
loading.value = true
@ -87,7 +87,7 @@ export function useSearch(query: MaybeRefOrGetter<string>, options: UseSearchOpt
}, { debounce: 300 })
const next = async () => {
if (!q.value || !isHydrated.value || !paginator)
if (!q || !isHydrated.value || !paginator)
return
loading.value = true

View file

@ -20,7 +20,7 @@ function getDefaultVisibility(currentVisibility: mastodon.v1.StatusVisibility) {
// the post more private than the replying to post
const preferredVisibility = currentUser.value?.account.source.privacy || 'public'
return ALL_VISIBILITY.indexOf(currentVisibility)
> ALL_VISIBILITY.indexOf(preferredVisibility)
> ALL_VISIBILITY.indexOf(preferredVisibility)
? currentVisibility
: preferredVisibility
}

View file

@ -95,12 +95,7 @@ export async function translateText(text: string, from: string | null | undefine
return status
}
const translations = new WeakMap<mastodon.v1.Status | mastodon.v1.StatusEdit, {
visible: boolean
text: string
success: boolean
error: string
}>()
const translations = new WeakMap<mastodon.v1.Status | mastodon.v1.StatusEdit, { visible: boolean; text: string; success: boolean; error: string }>()
export function useTranslation(status: mastodon.v1.Status | mastodon.v1.StatusEdit, to: string) {
if (!translations.has(status))

View file

@ -5,7 +5,6 @@ import type { PaginatorState } from '~/types'
export function usePaginator<T, P, U = T>(
_paginator: mastodon.Paginator<T[], P>,
stream: Ref<mastodon.streaming.Subscription | undefined>,
eventType: 'update' | 'notification' = 'update',
preprocess: (items: (T | U)[]) => U[] = items => items as unknown as U[],
buffer = 10,
) {
@ -35,10 +34,10 @@ export function usePaginator<T, P, U = T>(
return
for await (const entry of stream) {
if (entry.event === eventType) {
if (entry.event === 'update') {
const status = entry.payload
if ('uri' in status)
if ('uri' in entry)
cacheStatus(status, undefined, true)
const index = prevItems.value.findIndex((i: any) => i.id === status.id)

View file

@ -6,12 +6,10 @@ import type {
} from '~/composables/push-notifications/types'
import { PushSubscriptionError } from '~/composables/push-notifications/types'
export async function createPushSubscription(
user: RequiredUserLogin,
export async function createPushSubscription(user: RequiredUserLogin,
notificationData: CreatePushNotification,
policy: mastodon.v1.WebPushSubscriptionPolicy = 'all',
force = false,
): Promise<mastodon.v1.WebPushSubscription | undefined> {
force = false): Promise<mastodon.v1.WebPushSubscription | undefined> {
const { server: serverEndpoint, vapidKey } = user
return await getRegistration()

View file

@ -64,7 +64,7 @@ export function usePushManager() {
policy?: mastodon.v1.WebPushSubscriptionPolicy,
force?: boolean,
): Promise<SubscriptionResult> => {
if (!isSupported.value)
if (!isSupported)
return 'not-supported'
if (!currentUser.value)
@ -87,10 +87,7 @@ export function usePushManager() {
currentUser.value.pushSubscription = await createPushSubscription(
{
pushSubscription,
server,
token,
vapidKey,
pushSubscription, server, token, vapidKey,
},
notificationData ?? {
alerts: {
@ -112,7 +109,7 @@ export function usePushManager() {
}
const unsubscribe = async () => {
if (!isSupported.value || !isSubscribed.value || !currentUser.value)
if (!isSupported || !isSubscribed || !currentUser.value)
return false
await removePushNotifications(currentUser.value)

View file

@ -2,6 +2,5 @@ import { breakpointsTailwind } from '@vueuse/core'
export const breakpoints = useBreakpoints(breakpointsTailwind)
export const isSmallScreen = breakpoints.smallerOrEqual('sm')
export const isMediumOrLargeScreen = breakpoints.between('sm', 'xl')
export const isExtraLargeScreen = breakpoints.smallerOrEqual('xl')

View file

@ -9,7 +9,6 @@ export type ColorMode = 'light' | 'dark' | 'system'
export interface PreferencesSettings {
hideAltIndicatorOnPosts: boolean
hideGifIndicatorOnPosts: boolean
hideBoostCount: boolean
hideReplyCount: boolean
hideFavoriteCount: boolean
@ -17,7 +16,6 @@ export interface PreferencesSettings {
hideTranslation: boolean
hideUsernameEmojis: boolean
hideAccountHoverCard: boolean
hideTagHoverCard: boolean
hideNews: boolean
grayscaleMode: boolean
enableAutoplay: boolean
@ -65,7 +63,6 @@ export function getDefaultLanguage(languages: string[]) {
export const DEFAULT__PREFERENCES_SETTINGS: PreferencesSettings = {
hideAltIndicatorOnPosts: false,
hideGifIndicatorOnPosts: false,
hideBoostCount: false,
hideReplyCount: false,
hideFavoriteCount: false,
@ -73,7 +70,6 @@ export const DEFAULT__PREFERENCES_SETTINGS: PreferencesSettings = {
hideTranslation: false,
hideUsernameEmojis: false,
hideAccountHoverCard: false,
hideTagHoverCard: false,
hideNews: false,
grayscaleMode: false,
enableAutoplay: true,

View file

@ -5,10 +5,7 @@ const highlighter = ref<Highlighter>()
const registeredLang = ref(new Map<string, boolean>())
let shikiImport: Promise<void> | undefined
export function useHighlighter(lang: Lang): {
promise?: Promise<void>
highlighter?: Highlighter
} {
export function useHighlighter(lang: Lang): { promise?: Promise<void>; highlighter?: Highlighter } {
if (!shikiImport) {
shikiImport = import('shiki')
.then(async ({ getHighlighter }) => {

View file

@ -55,17 +55,11 @@ export function useTiptap(options: UseTiptapOptions) {
},
}),
Mention.configure({
renderHTML({ options, node }) {
return ['span', { 'data-type': 'mention', 'data-id': node.attrs.id }, `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`]
},
suggestion: TiptapMentionSuggestion,
}),
Mention
.extend({ name: 'hashtag' })
.configure({
renderHTML({ options, node }) {
return ['span', { 'data-type': 'hashtag', 'data-id': node.attrs.id }, `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`]
},
suggestion: TiptapHashtagSuggestion,
}),
Mention

View file

@ -16,11 +16,7 @@ declare module '@tiptap/core' {
/**
* Insert a custom emoji.
*/
insertCustomEmoji: (options: {
src: string
alt?: string
title?: string
}) => ReturnType
insertCustomEmoji: (options: { src: string; alt?: string; title?: string }) => ReturnType
/**
* Insert a emoji.
*/

View file

@ -1,42 +1,36 @@
import type { ExtendedRegExpMatchArray, InputRuleFinder, nodeInputRule } from '@tiptap/core'
import type { NodeType } from '@tiptap/pm/model'
import {
InputRule,
Node,
callOrReturn,
mergeAttributes,
nodeInputRule,
nodePasteRule,
} from '@tiptap/core'
import { emojiRegEx, getEmojiAttributes } from '~/config/emojis'
function wrapHandler<T extends (...args: any[]) => any>(handler: T): T {
return <T>((...args: any[]) => {
try {
return handler(...args)
}
catch (e) {
return null
}
})
}
function createEmojiRule<NR extends typeof nodeInputRule | typeof nodePasteRule>(
nodeRule: NR,
type: Parameters<NR>[0]['type'],
): ReturnType<NR>[] {
function createEmojiRule<NR extends typeof nodeInputRule | typeof nodePasteRule>(nodeRule: NR,
type: Parameters<NR>[0]['type']): ReturnType<NR>[] {
const rule = nodeRule({
find: emojiRegEx as RegExp,
type,
getAttributes: (match: ExtendedRegExpMatchArray) => {
getAttributes: (match) => {
const [native] = match
return getEmojiAttributes(native)
},
}) as ReturnType<NR>
// Error catch for unsupported emoji
rule.handler = wrapHandler(rule.handler.bind(rule))
const handler = rule.handler.bind(rule)
rule.handler = (...args) => {
try {
return handler(...args)
}
catch (e) {
return null
}
}
return [rule]
return [
rule,
]
}
export const TiptapPluginEmoji = Node.create({
@ -84,34 +78,7 @@ export const TiptapPluginEmoji = Node.create({
},
addInputRules() {
function emojiInputRule(config: {
find: InputRuleFinder
type: NodeType
getAttributes?:
| Record<string, any>
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
| false
| null
}) {
return new InputRule({
find: config.find,
handler: ({ state, range, match }) => {
const attributes = callOrReturn(config.getAttributes, undefined, match) || {}
const { tr } = state
const start = range.from
const end = range.to
tr.insert(start, config.type.create(attributes)).delete(
tr.mapping.map(start),
tr.mapping.map(end),
)
tr.scrollIntoView()
},
})
}
return createEmojiRule(emojiInputRule, this.type)
return createEmojiRule(nodeInputRule, this.type)
},
addPasteRules() {

View file

@ -14,6 +14,7 @@ export default defineI18nConfig(() => {
missingWarn: true,
datetimeFormats,
numberFormats,
pluralRules,
// eslint-disable-next-line @typescript-eslint/comma-dangle
pluralRules
}
})

View file

@ -88,26 +88,6 @@ const locales: LocaleObjectData[] = [
return { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5 }[name]
},
} satisfies LocaleObjectData),
({
code: 'ckb',
file: 'ckb.json',
name: 'کوردیی ناوەندی',
dir: 'rtl',
pluralRule: (choice: number) => {
const name = new Intl.PluralRules('ckb').select(choice)
return { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5 }[name]
},
} satisfies LocaleObjectData),
({
code: 'fa-IR',
file: 'fa-IR.json',
name: 'فارسی',
dir: 'rtl',
pluralRule: (choice: number) => {
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),
{
code: 'ca',
file: 'ca.json',

View file

@ -22,16 +22,7 @@ export const STORAGE_KEY_HIDE_EXPLORE_TAGS_TIPS = 'elk-hide-explore-tags-tips'
export const STORAGE_KEY_NOTIFICATION = 'elk-notification'
export const STORAGE_KEY_NOTIFICATION_POLICY = 'elk-notification-policy'
export const STORAGE_KEY_PWA_HIDE_INSTALL = 'elk-pwa-hide-install'
export const STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE = 'elk-last-accessed-notification-route'
export const STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE = 'elk-last-accessed-explore-route'
export const HANDLED_MASTO_URLS = /^(https?:\/\/)?([\w\d-]+\.)+\w+\/(@[@\w\d-\.]+)(\/objects)?(\/\d+)?$/
export const NOTIFICATION_FILTER_TYPES: mastodon.v1.NotificationType[] = ['status', 'reblog', 'follow', 'follow_request', 'favourite', 'poll', 'update', 'admin.sign_up', 'admin.report']
export const THEME_COLORS = {
themeDark: '#111111',
themeLight: '#fafafa',
backgroundDark: '#fafafa',
backgroundLight: '#111111',
} as const

View file

@ -30,7 +30,7 @@ We use [Docus](https://docus.dev) as the site generator and deploy through Netli
When you are ready to submit work back to the main Elk repo, create a PR.
1. If it has been a bit, synchronize your fork with the upstream repo on GitHub.
1. If it has been a bit, synchronize your fork with the upstream repo on GitHub.
2. Do your work in a branch on your fork
Use `git checkout -b branchNameToUse` to create a working branch separate from `main`.
3. Do your work in your preferred editor
@ -44,7 +44,7 @@ When you are ready to submit work back to the main Elk repo, create a PR.
- base branch should be **main**
- Head repository should be your fork
- Compare branch should be your working branch you want to submit
If you don't see four drop-downs, be sure you are comparing across forks.
If you don't see four drop downs, be sure you are comparing across forks.
10. Add a description of the changes your request makes
11. Select **Add Pull Request**
@ -65,7 +65,7 @@ Avoid screenshots until Elk reaches a stable release.
### Standards
Write in **American English** using spelling as found in [Merriam-Webster](https://www.merriam-webster.com).
Write in **American English** using spelling as found in [Merriam Webster](https://www.merriam-webster.com).
Translation and localization will be handled separately as/when availability or necessity allow.
Use [**semantic linefeeds**](https://rhodesmill.org/brandon/2012/one-sentence-per-line/) with no more than one sentence per line.

View file

@ -14,10 +14,10 @@ css({
gap: '0.5rem',
fontSize: '1.5rem',
},
'img': {
img: {
flexShrink: 0,
aspectRatio: '1/1',
height: '2.5rem',
},
height: '2.5rem'
}
})
</style>

View file

@ -1,6 +1,6 @@
<script>
export default {
name: 'ToggleIcon',
name: 'ToogleIcon',
props: { up: Boolean },
}
</script>

View file

@ -61,8 +61,8 @@ async function copyToClipboard() {
try {
await navigator.clipboard.writeText([
`# ${localeTitle.value}`,
(localeTab.value === 'missing' ? missingEntries.value : outdatedEntries.value).join('\n'),
].join('\n'))
(localeTab.value === 'missing' ? missingEntries.value : outdatedEntries.value).join('\n')].join('\n'),
)
copied.value = true
setTimeout(() => copied.value = false, 750)
}
@ -116,7 +116,7 @@ async function copyToClipboard() {
>
<td :class="[{ expandable: !isSource }]">
<div>
<ToggleIcon v-if="!isSource" :up="hidden || key !== locale" />
<ToogleIcon v-if="!isSource" :up="hidden || key !== locale" />
{{ title }}
</div>
</td>

View file

@ -35,6 +35,7 @@ You can think of the service as something similar to Twitter, but with some key
For more details about Mastodon, see their [website](https://joinmastodon.org/), [blog](https://blog.joinmastodon.org), [docs](https://docs.joinmastodon.org), and [GitHub Repository](https://github.com/mastodon/mastodon).
## What is a Mastodon Client?
A Mastodon Client is software that serves up the posts and activities from a Mastodon server using the [Mastodon API](https://docs.joinmastodon.org/api/).
@ -79,6 +80,7 @@ Desktop
[TheDesk](https://thedesk.top/en/), [Whalebird](https://whalebird.social/en), or [Sengi](https://nicolasconstant.github.io/sengi/)
::
All of these apps provide some combination of the features a typical Mastodon user expects for their account.
### Browser-based Mastodon Clients

View file

@ -67,6 +67,7 @@ Options include:
Elk reorders timeline posts that are connected to each other to display them in a connected thread in the timelines.
This helps keep the conversation context for a post and its replies.
## Improved notifications
Elk groups boosts and likes to the same post when they occur together in the notification history.
@ -75,6 +76,7 @@ This greatly simplifies your notification history.
<!-- ## GitHub HTML cards -->
<!-- - markdown support
- GitHub HTML cards
- and so on... -->

View file

@ -4,7 +4,7 @@ We're really excited that you're interested in contributing to Elk! Before submi
## Online
You can use [StackBlitz Codeflow](https://stackblitz.com/codeflow) to fix bugs or implement features. You'll also see a Codeflow button on PRs to review them without a local setup. Once the elk repo has been cloned in Codeflow, the dev server will start automatically and print the URL to open the App. You should receive a prompt in the bottom-right suggesting to open it in the Editor or in another Tab. To learn more, check out the [Codeflow docs](https://developer.stackblitz.com/codeflow/what-is-codeflow).
You can use [StackBlitz Codeflow](https://stackblitz.com/codeflow) to fix bugs or implement features. You'll also see a Codeflow button on PRs to review them without a local setup. Once the elk repo has been cloned in Codeflow, the dev server will start automatically and print the URL to open the App. You should receive a prompt in the bottom-right suggesting to open it in the Editor or in another Tab. To learn more, check out the [Codeflow docs](https://developer.stackblitz.com/codeflow/what-is-codeflow).
[![Open in Codeflow](https://developer.stackblitz.com/img/open_in_codeflow.svg)](https://pr.new/elk-zone/elk)

View file

@ -56,6 +56,6 @@ On your project page open the Deploys tab, click on "Trigger deploy" and "Deploy
## Use a custom domain
If you want to use a custom domain, go to "Domain settings" on your Netlify project page, and press "Add custom domain". If your domain is not bought from Netlify, it will ask you to add a CNAME record. Do that.
Once the custom domain is added, you'll need to add an SSL/TLS certificate. At the bottom of the page press "Verify DNS configuration" and if it succeeds, press "Provision certificate". If that fails, you may need to wait some time until your DNS propagates.
Once the custom domain is added, you'll need to add an SSL/TLS certificate. At the bottom of the page press "Verify DNS configuration" and if it succeeds, press "Provision certificate". If that fails, you may need to wait some time until your DNS propagetes.
And that's it! Enjoy your instance's Elk!

View file

@ -69,7 +69,7 @@ If you deleted any account from your backend with push notification subscription
- Scroll down in the page and locate "Permissions > Notifications"
- Click on "Configuration" button
- Locate "Elk" website in the list and select it, then click on "Remove website" button
- Safari (WIP)
- Safari (WIP)
### Permission denied: enable notifications in your browser
@ -186,7 +186,7 @@ To debug the PWA service worker in your mobile browser, you will need to:
- Cache: `[x]` Cache storage and `[x]` Application cache
- Click on Clear site data button
- Go to `Application > Service Workers` and check the current service worker is missing or has the status deleted or reduntant.
6) Disable port forwarding: open the "Port forwarding..." modal again on the `chrome://inspect/#devices` page and either uncheck the "Enable port forwarding" option or remove the entry from the list and click "Done".
6) Disable port forwarding: open the "Port forwarding..." modal again on the `chrome://inspect/#devices` page and either uncheck the "Enable port forwarding" option or remove the entry from the list and click "Done".
## PWA web manifest and related applications

View file

@ -35,3 +35,4 @@ When we update this privacy notice, the updated version will be indicated by a n
## Contact
If you have questions or comments about this notice, you may raise an issue at https://github.com/elk-zone/elk.

View file

@ -1,10 +1,10 @@
[build]
publish = "dist"
command = "pnpm generate"
publish = "dist"
command = "pnpm generate"
# Allow previewing docs
[[redirects]]
from = "/docs/*"
to = "/:splat"
status = 200
force = true
from = "/docs/*"
to = "/:splat"
status = 200
force = true

View file

@ -1,8 +1,3 @@
export default defineNuxtConfig({
extends: '@nuxt-themes/docus',
vite: {
optimizeDeps: {
include: ['scule'],
},
},
})

View file

@ -13,6 +13,6 @@
},
"devDependencies": {
"@nuxt-themes/docus": "^1.15.0",
"nuxt": "^3.11.2"
"nuxt": "^3.10.3"
}
}

View file

@ -1,37 +0,0 @@
// @ts-check
import antfu from '@antfu/eslint-config'
export default await antfu(
{
unocss: false,
vue: {
overrides: {
'vue/no-restricted-syntax': ['error', {
selector: 'VElement[name=\'a\']',
message: 'Use NuxtLink instead.',
}],
},
},
ignores: [
'public/**',
'public-dev/**',
'public-staging/**',
'https-dev-config/**',
'elk-translation-status.json',
'docs/translation-status.json',
],
},
{
rules: {
// TODO: migrate all process reference to `import.meta.env` and remove this rule
'node/prefer-global/process': 'off',
},
},
// Sort local files
{
files: ['locales/**.json'],
rules: {
'jsonc/sort-keys': 'error',
},
},
)

View file

@ -1,687 +0,0 @@
{
"a11y": {
"loading_page": "خرمەخرمی لاپەڕە، تکایە چاوەڕوان بن",
"loading_titled_page": "خرمەی لاپەڕەی {0}، تکایە چاوەڕوان بن",
"locale_changed": "زمان گۆڕدرا بۆ {0}",
"locale_changing": "گۆڕینی زمان، تکایە چاوەڕوان بن",
"route_loaded": "لاپەڕەی {0} بارکراوە"
},
"account": {
"avatar_description": "{0}بە ئاڤاتاری",
"blocked_by": "تۆ لەلایەن ئەم بەکارهێنەرەوە بلۆک کراویت.",
"blocked_domains": "دۆمەینەکانی بلۆککراو",
"blocked_users": "بەکارهێنەرانی بلۆککراو",
"blocking": "بلۆک کراوە",
"bot": "بۆت",
"favourites": "دڵخواز",
"follow": "بەدواداچوون",
"follow_back": "دوا بکەوە",
"follow_requested": "داواکراوە",
"followers": "هۆگران",
"followers_count": "هۆگر {0}|هۆگر {0}|{0} هۆگر",
"following": "شوێنکەوتوو",
"following_count": "{0} شوێنکەوتوو",
"follows_you": "شوێنت دەکەوێت",
"go_to_profile": "بڕۆ بۆ پڕۆفایلی",
"joined": "بەشداری کردووە",
"moved_title": "ئاماژەی بەوە کردووە، لە ئێستادا هەژمارە نوێیەکەیان بەم شێوەیەیە:",
"muted_users": "بەکارهێنەرە بێدەنگەکان",
"muting": "بێدەنگ",
"mutuals": "دوولایەنە",
"notifications_on_post_disable": "واز لە ئاگادارکردنەوەم بێنە کاتێک {username} پۆست دەکات",
"notifications_on_post_enable": "کاتێک {username} پۆست دەکات ئاگادارم بکەرەوە",
"pinned": "سەنجاق کراوە",
"posts": "بابەت",
"posts_count": "{0} بابەت|{0} بابەت|{0} بابەت",
"profile_description": "سەردێڕی پڕۆفایلی {0}",
"profile_personal_note": "تێبینی کەسی",
"profile_unavailable": "پڕۆفایلی بەردەست نییە",
"request_follow": "داواکاری بۆ بەدواداچوون",
"unblock": "بلۆک بکەرەوە",
"unfollow": "بەدوایدا مەچۆ",
"unmute": "بێدەنگی لابەرە",
"view_other_followers": "لەوانەیە شوێنکەوتووانی حاڵەتەکانی تر نیشان نەدرێن.",
"view_other_following": "لە حاڵەتەکانی تر ناتوانرێت ئاشکرا بکرێت."
},
"action": {
"apply": "به‌کاربردن",
"bookmark": "نیشانەکردن",
"bookmarked": "نیشانە کراوە",
"boost": "هاوبەشکردن",
"boost_count": "{0}",
"boosted": "هاوبەشکرا",
"clear_publish_failed": "هەڵەکانی بڵاوکردنەوە پاک بکەرەوە",
"clear_save_failed": "پاککردنی هەڵەی پاشکەوتکراوە",
"clear_upload_failed": "هەڵەکانی بارکردنی پەڕگە پاک بکەرەوە",
"close": "داخستن",
"compose": "نووسین",
"confirm": "دڵنیاکردنەوە",
"done": "تەواو",
"edit": "دەستکاری",
"enter_app": "چوونەناو ئەپ",
"favourite": "په‌سند",
"favourite_count": "{0}",
"favourited": "پەسەندکراوە",
"more": "زیاتر",
"next": "دواتر",
"prev": "پێشتر",
"publish": "بڵاوكردنه‌وه‌",
"reply": "وەڵامدانەوە",
"reply_count": "{0}",
"reset": "ڕێکخستنه‌وه‌",
"save": "پاشه‌که‌وت",
"save_changes": "پاشکەوتی گۆرانکاری",
"sign_in": "چوونەژوورەوە",
"sign_in_to": "چوونە ژوورەوە بۆ {0}",
"switch_account": "گۆڕینی هەژمارە",
"vote": "دەنگدان"
},
"app_desc_short": "ڕاژەخوازێکی وێبی ماستۆدۆن",
"app_logo": "Elk لۆگۆی",
"app_name": "Elk",
"attachment": {
"edit_title": "وەسف",
"remove_label": "لابردنی لکێندراو"
},
"command": {
"activate": "چالاککردن",
"complete": "تەواو",
"compose_desc": "نووسینی بابەتێکی نوێ",
"select_lang": "زمان هەڵبژێرە",
"sign_in_desc": "هەژمارەیێکی ئامادە زیاد بکە",
"switch_account": "بیگۆڕە بۆ {0}",
"switch_account_desc": "بیگۆڕە بۆ ئەژمێرێکی تر",
"toggle_dark_mode": "گۆڕینی دۆخی تاریک",
"toggle_zen_mode": "دۆخی zen بگۆڕە"
},
"common": {
"end_of_list": "کۆتایی لیستەکە",
"error": "هەڵە",
"fetching": "هێنانی ...",
"in": "لە",
"no_bookmarks": "هێشتا هیچ بابەتێک نیشانە نەکراوە",
"no_favourites": "هێشتا هیچ بابەتێک پەسەند نەکراوە",
"not_found": "هەڵەی ٤٠٤، نەدۆزرایه‌وه‌",
"offline_desc": "وا دیارە تۆ لە دەرەوەی هێڵ بیت. تکایە پەیوەندی تۆڕەکەت بپشکنە."
},
"compose": {
"draft_title": "ڕەشنووس {0}",
"drafts": "ڕەشنووسەکان ({v})"
},
"confirm": {
"block_account": {
"cancel": "هه‌ڵوه‌شاندنه‌وه",
"confirm": "بلۆک",
"title": "ئایا دڵنیای کە دەتەوێت {0} بلۆک بکەیت؟"
},
"block_domain": {
"cancel": "هه‌ڵوه‌شاندنه‌وه",
"confirm": "بلۆک",
"title": "ئایا دڵنیای کە دەتەوێت {0} بلۆک بکەیت؟"
},
"common": {
"cancel": "نەخێر",
"confirm": "بەڵێ"
},
"delete_list": {
"cancel": "هه‌ڵوه‌شاندنه‌وه",
"confirm": "سڕینەوە",
"title": "ئایا دڵنیای کە دەتەوێت لیستی \"{0}\" بسڕیتەوە؟"
},
"delete_posts": {
"cancel": "هه‌ڵوه‌شاندنه‌وه",
"confirm": "سڕینەوە",
"title": "دڵنیای دەتەوێت ئەم بابەتە بسڕیتەوە؟"
},
"mute_account": {
"cancel": "هه‌ڵوه‌شاندنه‌وه",
"confirm": "بێدەنگ",
"title": "ئایا دڵنیای کە دەتەوێت {0} بێدەنگ بکەیت؟"
},
"show_reblogs": {
"cancel": "هه‌ڵوه‌شاندنه‌وه",
"confirm": "پیشاندان",
"title": "ئایا دڵنیای کە دەتەوێت هاوبەشیەکان لە {0}ەوە پیشان بدەیت؟"
},
"unfollow": {
"cancel": "هه‌ڵوه‌شاندنه‌وه",
"confirm": "شوێن نەکەوتن",
"title": "دڵنیای کە دەتەوێت شوێنی نەکەویتەوە؟"
}
},
"conversation": {
"with": "لەگەڵ"
},
"custom_cards": {
"stackblitz": {
"lines": "هێڵەکان {0}",
"open": "کراوە",
"snippet_from": "پارچەیەک لە {0}"
}
},
"error": {
"account_not_found": "هەژمار {0} نەدۆزرایەوە",
"file_size_cannot_exceed_n_mb": "قەبارەی پەڕگە ناتوانرێت لە {0} مێگابایت تێپەڕێت",
"sign_in_error": "ناتوانیت پەیوەندی بکەیت بە ڕاژەکار.",
"status_not_found": "بابەت نەدۆزرایەوە",
"unsupported_file_format": "پەڕەگە پشتگیری نەکراو"
},
"help": {
"build_preview": {
"desc1": "لە ئێستادا ئێوە وشانی پێشوەختەی Elk لە کۆمەڵگەوە دەبینن - {0}.",
"desc2": "لەوانەیە گۆڕانکاری بێ پێداچوونەوە یان تەنانەت زیانبەخشی تێدابێت.",
"desc3": "بە هەژمارە ڕاستەقینەکەتەوە مەچۆژوور",
"title": "پێشبینینی بڵاوکردنەوە"
},
"desc_highlight": "چاوەڕوانی هەندێک هەڵە و تایبەتمەندی ونبوو لێرە و لەوێ بن.",
"desc_para1": "سوپاس بۆ ئارەزووی ئێوە بۆ تاقیکردنەوەی Elk، ڕاژەخوازی ماستۆدۆنەکەمان لە پێشکەوتندایە!",
"desc_para2": "ئێمە زۆر کار لەسەر گەشەپێدانەکە دەکەین و بە تێپەڕبوونی کات باشتری دەکەین.",
"desc_para3": "بۆ بەرزکردنەوەی گەشەپێدان، دەتوانیت لە ڕێگەی پاڵپشتیەکانی گیتهاب پشتیوانی تاقمەکەمان بکەیت. هیوادارین چێژ لە Elk وەربگرن!",
"desc_para4": "Elk سەرچاوە کراوەیە. ئەگەر دەتەوێت یارمەتی بدەیت لە تاقیکەرەوە، فیدباک بنێرە، یان بەشداری بکە،",
"desc_para5": "لە گیتهاب نزیکمان بکەرەوە",
"desc_para6": "و بەشداری بکەن.",
"footer_team": "گەشەپێدارانی Elk",
"title": "ئێلک(Elk) لە پێشبینیدایە!"
},
"language": {
"search": "گەڕان"
},
"list": {
"add_account": "هەژمارە زیاد بکە بۆ لیستەکە",
"cancel_edit": "هەڵوەشاندنەوەی دەستکاریکردن",
"clear_error": "هەڵە پاک بکەرەوە",
"create": "دروستکردن",
"delete": "ئەم لیستە بسڕەوە",
"delete_error": "لە کاتی سڕینەوەی لیستەکەدا هەڵەیەک ڕوویدا",
"edit": "دەستکاری ئەم لیستە بکە",
"edit_error": "لە کاتی نوێکردنەوەی لیستەکەدا هەڵەیەک ڕوویدا",
"error": "لە کاتی دروستکردنی لیستەکەدا هەڵەیەک ڕوویدا",
"error_prefix": "هەڵە:",
"list_title_placeholder": "ناونیشانی لیستەکە",
"modify_account": "دەستکاری لیستەکان بکە بە هەژمارە",
"remove_account": "هەژمارە لە لیستەکە دەربهێنە",
"save": "پاشکەوتی گۆڕانکاری"
},
"magic_keys": {
"dialog_header": "قەدبڕی تەختەکلیل",
"groups": {
"actions": {
"boost": "هابەشیکردن",
"command_mode": "دۆخی فەرمان",
"compose": "نووسین",
"favourite": "دڵخواز",
"title": "کردارەکان"
},
"media": {
"title": "ڕاگه‌یاندن"
},
"navigation": {
"go_to_home": "سەرەتا",
"go_to_notifications": "ئاگاداری",
"next_status": "دۆخی داهاتوو",
"previous_status": "دۆخی پێشوو",
"shortcut_help": "یارمەتی کورتەڕێ",
"title": "ڕێدۆز"
}
},
"sequence_then": "ئیتر"
},
"menu": {
"add_personal_note": "زیادکردنی تێبینی کەسی بۆ {0}",
"block_account": "بلۆک {0}",
"block_domain": " بلۆککردنی دۆمەینی {0}",
"copy_link_to_post": "بەستەری ئەم بابەتە ڕوونووس بکە",
"copy_original_link_to_post": "بەستەری ئەسڵی ڕوونووس بکە بۆ ئەم بابەتە",
"delete": "سڕینەوە",
"delete_and_redraft": "سڕینەوە یا دەستکاری",
"direct_message_account": "پەیامی ڕاستەوخۆ {0}",
"edit": "دەستکاری",
"hide_reblogs": "شاردنەوەی هاوبەشیەکان لە {0}",
"mention_account": "ئاماژە بە {0}",
"mute_account": "بێدەنگکردنی {0}",
"mute_conversation": "ئەم بابەتە بێدەنگ بکەن",
"open_in_original_site": "کردنەوە لە ماڵپەڕی سەرەکی",
"pin_on_profile": "سنجاقکردن لەسەر پرۆفایل",
"remove_personal_note": "لابردنی تێبینی کەسی لە {0}",
"report_account": "{0} گوزارشی",
"share_post": "ئەم بابەتە هاوبەش بکە",
"show_favourited_and_boosted_by": "نیشانی بدە کێ حەزی لێیەتی و هابەشیکردووە",
"show_reblogs": "هاوبەشیەکان نیشان بدە لە {0}",
"show_untranslated": "پیشاندانی ڕێکنەخراو",
"toggle_theme": {
"dark": "گۆڕینی شێوازی تاریک",
"light": "گۆڕینی شێوازی ڕۆژ"
},
"translate_post": "بابەت وەربگێڕە",
"unblock_account": "{0} بلۆکەکە لاببە",
"unblock_domain": "ئازادکردنی {0} دۆمەین",
"unfollow_account": "وازهێنان لە شوێنکەوتووان {0}",
"unmute_account": "بێدەنگی {0} لابەرە",
"unmute_conversation": "بێدەنگی لەم بابەتە لابەرە",
"unpin_on_profile": "سنجاقکردن لابەرە لە پرۆفایل"
},
"modals": {
"aria_label_close": "داخستن"
},
"nav": {
"back": "بگەڕێوە",
"blocked_domains": "دۆمەینە بلۆککراوەکان",
"blocked_users": "بەکارهێنەرە بلۆککراوەکان",
"bookmarks": "نیشانکراوەکان",
"built_at": "{0} دروستکراو",
"compose": "نووسین",
"conversations": "گفتوگۆکان",
"explore": "گەڕان",
"favourites": "دڵخوازەکان",
"federated": "یەکگرتن",
"home": "سەرەتا",
"list": "لیست",
"lists": "لیستەکان",
"local": "خۆماڵی",
"muted_users": "بەکارهێنەرە بێدەنگەکان",
"notifications": "ئاگادارییەکان",
"privacy": "تایبەتێتی",
"profile": "پرۆفایل",
"search": "گەڕان",
"select_feature_flags": "گۆڕینی تایبەتمەندی ئاڵاکان",
"select_font_size": "ئەندازەی پێنووس",
"select_language": "پیشاندانی زمان",
"settings": "ڕێکخستنه‌کان",
"show_intro": "پیشاندانی پێشەکی",
"toggle_theme": "گۆڕینی ڕووکار",
"zen_mode": "شێوەی Zen"
},
"notification": {
"favourited_post": "بابەتەکەتی پەسەند کرد",
"followed_you": "دوای تۆ کەوت",
"followed_you_count": "{0} بەکارهێنەر دوای تۆ کەوتن{0} کەبەکارهێنەر بەدوات کەوتن|{0} بەکارهێنەر دوای تۆ کەوتن",
"missing_type": "MISSING notification.type:",
"reblogged_post": "هاوبەشیکردنی بابەتی تۆ",
"reported": "{0} گوزارشی {1}",
"request_to_follow": "داوای کردووە کە شوێنتان بکەوێت ",
"signed_up": "بوو به ئه ندام",
"update_status": "بابەتەکەیان نوێکردەوە"
},
"placeholder": {
"content_warning": "لێرەدا هۆشدارییەکەت بنووسە",
"default_1": "چی لە مێشکتدایە؟",
"reply_to_account": "وەڵامی {0}",
"replying": "وەڵامدانەوە"
},
"polls": {
"allow_multiple": "ڕێگە بە چەندین هەڵبژاردن بدە",
"cancel": "هه‌ڵوه‌شاندنه‌وه",
"create": "دروستکردنی ڕاپرسی",
"disallow_multiple": "ڕێگەنەدان بە هەڵبژاردنی فرە",
"expiration": "بەسەرچوونی ماوەی ڕاپرسی",
"hide_votes": "Hide vote totals until the end",
"option_placeholder": "هەڵبژاردنی ڕاپرسی {current}/{max}",
"remove_option": "هەڵبژاردن لاببە",
"settings": "بژاردەکانی ڕاپرسی",
"show_votes": "هەمیشە کۆی دەنگەکان پیشان بدە"
},
"pwa": {
"dismiss": " ڕێپێنەدان",
"install": "دامەزراندن",
"install_title": "دامەزراندنی Elk",
"screenshots": {
"dark": "وێنەی ڕوومێزی Elk کە لە دۆخی تاریکدا کاردەکات",
"light": "وێنەی ڕوومێزی Elk کە لە دۆخی ڕووناکیدا کاردەکات"
},
"title": "نوێکردنەوەی نوێی Elk بەردەستە!",
"update": "نوێکردنەوە",
"update_available_short": "نوێکردنەوەی Elk",
"webmanifest": {
"canary": {
"description": "ڕاژەخوازێکی ماستۆدۆن (کاناری)",
"name": "Elk (کاناری)",
"short_name": "Elk (کاناری)"
},
"dev": {
"description": "ڕاژەخوازێکی وێبی ماستۆدۆن (پەرەپێدەر)",
"name": "Elk (پەرەپێدەر)",
"short_name": "Elk (پەرەپێدەر)"
},
"preview": {
"description": "ڕاژەخوازێکی وێبی ماستۆدۆن (پێش‌بینین)",
"name": "Elk (پێش‌بینین)",
"short_name": "Elk (پێش‌بینین)"
},
"release": {
"description": "ڕاژەخوازێکی وێبی ماستۆدۆن",
"name": "Elk",
"short_name": "Elk"
}
}
},
"report": {
"additional_comments": "سەرنجی زیادە",
"another_server": "ئەو بەکارهێنەرەی کە تۆ ڕاپۆرتی دەکەیت لە ڕاژەیێکی ترەوەیە",
"anything_else": "ئایا شتێکی تر هەیە کە پێت وایە پێویستە بزانین؟",
"block_desc": "چیتر هیچ بابەتێک لەم بەکارهێنەرەوە نابینیت. ئەوان ناتوانن پۆستەکانت ببینن و شوێنتکەون بکەن. ئەوان دەتوانن بزانن کە بلۆک کراون.",
"dontlike": "حەزم لێ نیە",
"dontlike_desc": "شتێک نییە کە تۆ بتەوێت بیبینیت",
"forward": "بەڵێ، ئەم گوزارشە بنێرە بۆ {0}",
"forward_question": "ئایا دەتەوێت ڕوونووسێکی بێناو لەم گوزارشە بنێری بۆ ئەو ڕاژەکارەش",
"further_actions": {
"limit": {
"description": "ئەمەی خوارەوە هەڵبژاردنەکانتە بۆ کۆنترۆڵکردنی ئەوەی دەیبینیت:",
"title": "ناتەوێت ئەمە ببینیت؟"
},
"report": {
"description": "لە کاتێکدا ئێمە پێداچوونەوە بۆ ئەمە دەکەین، ئەمە ئەو کردارانەن کە دەتوانیت ئەنجامی بدەیت:",
"title": "سوپاس بۆ گوزارشەکەت ئێمە سەیری ئەمە دەکەین."
}
},
"limiting": "سنووردارکردنی {0}",
"mute_desc": "چیتر هیچ بابەتێکی ئەم بەکارهێنەرە نابینیت. ئەوان هێشتا دەتوانن شوێنت بکەون و بابەتەکانت ببینن. ئه وان نازانن که ئه وان هه ر ده بن.",
"other": "شتێکی ترە",
"other_desc": "کێشەکە ناگونجێت بۆ پۆلەکانی تر",
"reporting": "{0} گوزارش",
"select_many": "هەموو ئەو شتانە دیاریبکە کە جێبەجێ دەکرێن:",
"select_one": "باشترین هاورێ هەڵبژێرە",
"select_posts": "ئایا هیچ بابەتێک هەیە کە پشتگیری ئەم گوزارشە بکات؟",
"select_posts_other": "ئایا هیچ بابەتێکی دیکە هەیە کە پشتگیری ئەم گوزارشە بکات؟",
"spam": "ئەوە سپام",
"spam_desc": "بەستەرە زیانبەخشەکان، پەیوەندی ساختە، یان وەڵامی دووبارەبوونەوە",
"submit": "گوزارش بنێرە",
"unfollow_desc": "چیتر بابەتەکانی ئەم بەکارهێنەرە لە پەڕەی ماڵەکەتدا نابینیت. هێشتا دەتوانیت بابەتەکانیان لە شوێنێکی تر ببینیت.",
"violation": "یەکێک یان زیاتر لە یاساکانی ڕاژەکار پێشێل دەکات",
"whats_wrong_account": "پێمان بڵێ کێشەی ئەم هەژمارە چییە",
"whats_wrong_post": "پێمان بڵێ کێشەی ئەم بابەتە چییە"
},
"search": {
"search_desc": "گەڕان بۆ بەکارهێنەران و هاشتاگەکان",
"search_empty": "نەیتوانی هیچ شتێک بدۆزێتەوە بۆ ئەم مەرجە گەڕانە"
},
"settings": {
"about": {
"built_at": "بنیات نان",
"label": "دەربارە",
"meet_the_team": "تاقمەکە بناسە",
"sponsor_action": "پشتیوانمان بە",
"sponsor_action_desc": "بۆ پشتگیری کردنی تیمی گەشەپێدانی Elk",
"sponsors": "پشتیوانەکان",
"sponsors_body_1": "Elk بە هۆی پشتیوانە بەخشەندە و یارمەتییەکانی، پێزانینی هەیە بۆ :",
"sponsors_body_2": "وە هەموو ئەو کۆمپانیا و کەسانەی کە پشتیوانی دەستەکەی Elk و ئەندامەکان دەکەن.",
"sponsors_body_3": "ئەگەر چێژ لە ئەپەکە وەردەگریت، بیر لەوە بکەرەوە کە پشتیوانیمان بکەیت:",
"version": "وشان"
},
"account_settings": {
"description": "دەستکاری ڕێکخستنەکانی هەژمارەکەت بکە لە ڕوخساری ماستۆدۆن",
"label": "ڕێکخستنەکانی هەژمارە"
},
"interface": {
"color_mode": "شێوەی ڕەنگ",
"dark_mode": "تاریک",
"default": " (بنەڕەت)",
"font_size": "ئەندازەی پێنووس",
"label": "ڕووکار",
"light_mode": "ڕۆشن",
"system_mode": "سیستەم",
"theme_color": "ڕەنگی ڕووکار"
},
"language": {
"display_language": "زمانی پیشاندان",
"label": "زمان",
"post_language": "زمانی بڵاوکردنەوە",
"status": "دۆخی وەرگێڕان: {0}/{1} ({2}٪)",
"translations": {
"add": "زیادکردن",
"choose_language": "زمانێک هەڵبژێرە",
"heading": "وەرگێڕان",
"hide_specific": "وەرگێڕانی دیاریکراو بشارەوە",
"remove": "لابردن"
}
},
"notifications": {
"label": "ئاگادارییەکان",
"notifications": {
"label": "ڕێکخستنەکانی ئاگادارکردنەوەکان"
},
"push_notifications": {
"alerts": {
"favourite": "دڵخوازەکان",
"follow": "شوێنکەوتووی نوێ",
"mention": "ئاماژە",
"poll": "دەنگ",
"reblog": "بابەتەکەت دووبارە بنووسەوە",
"title": "چ ئاگادارکردنەوەیەک وەربگرین؟"
},
"description": "تەنانەت کاتێک کە Elk بەکارناهێنیت ئاگادارکردنەوەکان وەربگرە.",
"instructions": "لەبیرت نەچێت گۆڕانکارییەکانت بە بەکارهێنانی دوگمەی @:settings.notifications.push_notifications.save_settings پاشەکەوت بکەیت!",
"label": "ڕێکخستنەکانی ئاگادارکردنەوەکانی پاڵدان",
"policy": {
"all": "لە هەر کەسێکەوە",
"followed": "لە کەسانێک کە شوێنیان دەکەوم",
"follower": "لە کەسانێک کە شوێنم دەکەون",
"none": "لە هیچ کەس",
"title": "لە کێ دەتوانم ئاگادارکردنەوە وەرگرم؟"
},
"save_settings": "ڕێکخستنەکان پاشەکەوت بکە",
"subscription_error": {
"clear_error": "هەڵە بسڕەوە",
"error_hint": "دەتوانیت ڕاوێژ بە لیستی پرسیارە زۆرەکان بکەیت بۆ ئەوەی هەوڵی چارەسەرکردنی کێشەکە بدەیت: {0}.",
"invalid_vapid_key": "پێدەچێت کلیلی گشتی VAPID نادروست بێت.",
"permission_denied": "مۆڵەت ڕەتکرایەوە: ئاگادارکردنەوەکان لە وێبگەڕەکەتدا چالاک بکە.",
"repo_link": "کۆگای Elk لە گیتهاب",
"request_error": "هەڵەیەک لە کاتی داوای بەشداریکردندا ڕوویدا، دووبارە هەوڵبدەرەوە و ئەگەر هەڵەکە بەردەوام بوو، تکایە کێشەکە گوزارش بدە بە کۆگای Elk.",
"title": "نەتوانرا بەشداری بکات بۆ ئاگادارکردنەوەکانی پاڵدان",
"too_many_registrations": "بەهۆی سنووردارکردنی وێبگەڕەوە، Elk ناتوانێت خزمەتگوزاری ئاگادارکردنەوەی پاڵنەر بەکاربهێنێت بۆ چەندین هەژمار لەسەر ڕاژەکارە جیاوازەکان. پێویستە بەشداری بکەیت لە پاڵنانی ئاگانامەکان لەسەر هەژمارێکی تر و دووبارە هەوڵ بدە.",
"vapid_not_supported": "وێبگەڕەکەت پشتگیری لە ئاگادارییەکانی Web Push دەکات، بەڵام پێناچێت پرۆتۆکۆڵی VAPID جێبەجێ بکات."
},
"title": "ڕێکخستنەکانی ئاگادارکردنەوەکانی پاڵدان",
"undo_settings": "گۆڕانکارییەکان بگەڕێنەوە",
"unsubscribe": "ئاگادارکردنەوەکانی پاڵدان لەکاربخە",
"unsupported": "وێبگەڕەکەت پشتگیری لە ئاگادارکردنەوەکانی پاڵدان ناکات.",
"warning": {
"enable_close": "داخستن",
"enable_description": "بۆ وەرگرتنی ئاگادارکردنەوەکان کاتێک Elk کراوە نییە، ئاگادارکردنەوەکانی پاڵدان چالاک بکە. دەتوانیت بە وردی کۆنتڕۆڵی ئەوە بکەیت کە چ جۆرە کارلێکێک ئاگادارییەکانی پاڵنەر دروست دەکات لە ڕێگەی دوگمەی \"@:settings.notifications.show_btn{'\"'}ی سەرەوە کاتێک چالاک کرا.",
"enable_description_desktop": "بۆ وەرگرتنی ئاگادارکردنەوە کاتێک Elk کراوە نییە، ئاگادارکردنەوەکانی پاڵدان چالاک بکە. دەتوانیت بە وردی کۆنتڕۆڵی ئەوە بکەیت کە چ جۆرە کارلێکێک ئاگادارکردنەوەی پاڵدان دروست دەکات لە \"ڕێکخستنەکان > ئاگادارکردنەوەکان > ڕێکخستنەکانی ئاگادارکردنەوەکانی پاڵدان\" کاتێک چالاک کرا.",
"enable_description_mobile": "هەروەها دەتوانیت بە بەکارهێنانی مینیوی گەشتکردن \"ڕێکخستنەکان > ئاگادارکردنەوەکان > ڕێکخستنەکانی ئاگادارکردنەوە پاڵدان\"، دەستت بگات بە ڕێکخستنەکان.",
"enable_description_settings": "بۆ وەرگرتنی ئاگادارکردنەوەکان کاتێک Elk کراوە نییە، ئاگادارکردنەوەکانی پاڵدان چالاک بکە. دەتوانیت بە وردی کۆنتڕۆڵی ئەوە بکەیت کە چ جۆرە کارلێکێک ئاگادارکردنەوەی پاڵنەر لەسەر هەمان ئەم شاشەیە دروست دەکات کاتێک چالاکیان دەکەیت.",
"enable_desktop": "ئاگادارکردنەوەکانی پاڵدان چالاک بکە",
"enable_title": "هەرگیز هیچ شتێک لەدەست مەدە",
"re_auth": "وا دیارە ڕاژەکارەکەت پشتگیری لە ئاگادارکردنەوەکانی پاڵدان ناکات. هەوڵبدە بچیتە دەرەوە و دووبارە بچۆرەوە ژوورەوە، ئەگەر ئەم پەیامە هێشتا دەرکەوت پەیوەندی بە بەڕێوەبەری سێرڤەرەکەتەوە بکە."
}
},
"show_btn": "بڕۆ بۆ ڕێکخستنەکانی ئاگادارکردنەوەکان",
"under_construction": "لەژێر دروستکردنەوەدایە"
},
"notifications_settings": "ئاگاداریەکان",
"preferences": {
"enable_autoplay": "چالاککردنی لێدانی خۆکار",
"enable_data_saving": "چالاککردنی پاشەکەوتکردنی زانیاری",
"enable_data_saving_description": "زانیاری پاشەکەوت بکە بە ڕێگریکردن لە لکێندراوەکان لە بارکردنی خۆکارانە.",
"enable_pinch_to_zoom": "چالاککردنی پینچ بۆ گەورەنوێیی",
"github_cards": "کاردەکانی گیتهاب",
"github_cards_description": "کاتێک بەستەری گیتهاب بڵاودەکرێتەوە، کارتێکی HTML بە بەکارهێنانی مێتای گرافی کۆمەڵایەتی لە جیاتی وێنەی کۆمەڵایەتی پیشان دەدرێت.",
"grayscale_mode": "دۆخی خۆڵەمێشی",
"hide_account_hover_card": "شاردنەوەی کارتی هۆڤەری ئەکاونت",
"hide_alt_indi_on_posts": "شاردنەوەی نیشاندەری سەردێر لەسەر بابەتەکان",
"hide_boost_count": "شاردنەوەی ژمارەی هابەشکردنەوە",
"hide_favorite_count": "ژمارەی دڵخوازەکان بشارەوە",
"hide_follower_count": "ژمارەی شوێنکەوتوو/شوێنکەوتووان بشارەوە",
"hide_news": "شاردنەوەی هەواڵەکان",
"hide_reply_count": "ژمارەی وەڵامەکان بشارەوە",
"hide_translation": "وەرگێڕان بشارەوە",
"hide_username_emojis": "شاردنەوەی ئیمۆجیەکانی ناوی بەکارهێنەر",
"hide_username_emojis_description": "شاردنەوەی ئیمۆجیەکان لە ناوی بەکارهێنەر لە هێڵی کات. ئیمۆجییەکان هێشتا لە پرۆفایلەکانیاندا دەبینرێن.",
"label": "هەڵبژاردن",
"title": "تایبەتمەندییە تاقیکارییەکان",
"use_star_favorite_icon": "ئایکۆنی دڵخوازی ئەستێرە بەکاربهێنە",
"user_picker": "هەڵبژێرەری بەکارهێنەر",
"user_picker_description": "هەموو وێنۆچکەکانی هەژمارەکانی تۆمارکراوەکان لە خوارەوەی چەپ پیشان دەدات بۆ ئەوەی بتوانیت بە خێرایی لە نێوانیاندا بگۆڕیت.",
"virtual_scroll": "سکڕۆڵکردنی مەجازی",
"virtual_scroll_description": "لیستی مەجازی لە هێڵە کاتییەکاندا بەکاربهێنە، بۆ ئەوەی ژمارەیەکی زیاتر لە بابەتە بە شێوەیەکی کارا ڕەندەر بکرێت.",
"wellbeing": "خۆشگوزەرانی",
"zen_mode": "شێوەی Zen",
"zen_mode_description": "لایەک بشارەوە مەگەر ئاماژەدەری ماوسەکە لەسەریان نەبێت. هەروەها هەندێک توخم لە هێڵی کاتی بشارەوە."
},
"profile": {
"appearance": {
"bio": "ژیاننامە",
"description": "دەستکاری وێنۆچکە، ناوی بەکارهێنەر، پڕۆفایل و هتد.",
"display_name": "ناوی پیشاندراو",
"label": "پیشان‌دان",
"profile_metadata": "مێتاداتای پڕۆفایلی",
"profile_metadata_desc": "دەتوانیت تا {0} شت وەک خشتەیەک لە پڕۆفایلی خۆتدا پیشان بدەیت",
"profile_metadata_label": "نیشانە",
"profile_metadata_value": "ناوەڕۆک",
"title": "دەستکاریکردنی پرۆفایل"
},
"featured_tags": {
"description": "خەڵک دەتوانن لە ژێر ئەم هاشتاگانەدا لە بابەتە گشتیەکانتاندا بگەڕێن.",
"label": "هاشتاگی تایبەت"
},
"label": "پرۆفایل"
},
"select_a_settings": "ڕێکخستنێک هەڵبژێرە",
"users": {
"export": "هەناردەکردنی تۆکنەکانی بەکارهێنەر",
"import": "هاوردەکردنی تۆکنەکانی بەکارهێنەر",
"label": "بەکارهێنەرانی چوونەژوورەوە"
}
},
"state": {
"attachments_exceed_server_limit": "ژمارەی هاوپێچەکان سنووری بۆ هەر بابەتێک تێپەڕاند.",
"attachments_limit_error": "سنوور بۆ هەر بابەتێک تێپەڕی",
"edited": "(دەستکاری کراوە)",
"editing": "دەستکاریکردن",
"loading": "خرمەخرم ...",
"publish_failed": "بڵاوکردنەوە سەرکەوتوو نەبوو",
"publishing": "بڵاوکردنەوە",
"save_failed": "هەڵگرتن سەرکەوتوو نەبوو",
"upload_failed": "بارکردن سەرکەوتوو نەبوو",
"uploading": "بارکردنەوە..."
},
"status": {
"account": {
"suspended_message": "هەژمارکردنی ئەم دۆخە ڕاگیراوە.",
"suspended_show": "بەهەرحاڵ ناوەڕۆک نیشان بدە؟"
},
"boosted_by": "هاوبەشکردن لە لایەن",
"edited": "{0} دەستکاریکراو",
"favourited_by": "پەسەندکراوە لەلایەن",
"filter_hidden_phrase": "پاڵێوراوە لەلایەن",
"filter_show_anyway": "بەهەر حاڵ پیشان بدە",
"img_alt": {
"ALT": "ALT",
"desc": "وەسف",
"dismiss": "ڕێپێنەدان",
"read": "وەسفی {0} بخوێنەوە"
},
"poll": {
"count": "{0} دەنگ|{0} دەنگ|{0} دەنگ",
"ends": "کۆتایی بە {0} دێت",
"finished": "تەواو بوو {0}"
},
"replying_to": "وەڵامدانەوەی {0}",
"show_full_thread": "تەوەرەی تەواو پیشان بدە",
"someone": "کەسێک",
"spoiler_media_hidden": "میدیای شاراوە",
"spoiler_show_less": "کەمتر پیشان بدە",
"spoiler_show_more": "زیاتر نیشان دە",
"thread": "Thread",
"try_original_site": "سایتی ئەسڵی تاقی بکەرەوە"
},
"status_history": {
"created": "دروستکراوە {0}",
"edited": "دەستکاری کراوە {0}"
},
"tab": {
"accounts": "هەژمارەکان",
"for_you": "بۆ تۆ",
"hashtags": "هاشتاگەکان",
"list": "لیست",
"media": "مێدیا",
"news": "هەواڵ",
"notifications_all": "هەموو",
"notifications_mention": "باسکردن",
"posts": "بابەتەکان",
"posts_with_replies": "بابەت و وەڵام"
},
"tag": {
"follow": "بەدواداچوون",
"follow_label": "شوێنکەوتنی تاگی {0} بکە",
"unfollow": "شوێن نەکەوتن",
"unfollow_label": "شوێنکەوتنی تاگی {0} مەکە"
},
"time_ago_options": {
"day_future": "لە 0 ڕۆژدا|سبەی|لە {n} ڕۆژدا",
"day_past": "0 ڕۆژ پێش ئێستا|دوێنێ|{n} ڕۆژ پێش ئێستا",
"hour_future": "لە 0 کاتژمێردا|لە 1 کاتژمێردا|لە {n} کاتژمێردا",
"hour_past": "0 کاتژمێر پێش ئێستا|1 کاتژمێر پێش ئێستا|{n} کاتژمێر پێش ئێستا",
"just_now": "هەر ئێستا",
"minute_future": "لە 0 خولەکدا|لە 1 خولەکدا|لە {n} خولەکدا",
"minute_past": "0 خولەک پێش ئێستا|1 خولەک پێش ئێستا|{n} خولەک پێش ئێستا",
"month_future": "لە 0 مانگ|مانگی داهاتوو|لە {n} مانگ",
"month_past": "0 مانگ پێش ئێستا|مانگی ڕابردوو|{n} مانگ پێش ئێستا",
"second_future": "تەنها ئێستا|لە {n} چرکە|لە {n} چرکە",
"second_past": "تەنها ئێستا|{n} چرکە پێش ئێستا|{n} چرکە پێش ئێستا",
"short_day_future": "لە {n}ڕۆژ",
"short_day_past": "{n}ڕۆژ",
"short_hour_future": "لە {n}کاژێر",
"short_hour_past": "{n}کاژێر",
"short_minute_future": "لە {n}خولەک",
"short_minute_past": "{n}خولەک",
"short_month_future": "لە {n}مانگ",
"short_month_past": "{n}مانگ",
"short_second_future": "لە {n}چرکه",
"short_second_past": "{n}چرکه",
"short_week_future": "لامەرکەزیدا {n}هەفتە",
"short_week_past": "{n}هەفتە",
"short_year_future": "لە {n}ساڵ",
"short_year_past": "{n}ساڵ",
"week_future": "لە 0 هەفتە|هەفتەی داهاتوو|لە {n} هەفتە",
"week_past": "0 هەفتە پێش ئێستا|هەفتەی ڕابردوو|{n} هەفتە پێش ئێستا",
"year_future": "لە 0 ساڵ|ساڵی داهاتوو|لە {n} ساڵ",
"year_past": "0 ساڵ لەمەوبەر|ساڵی ڕابردوو|{n} ساڵ لەمەوبەر"
},
"timeline": {
"show_new_items": "{v} بابەتی نوێ پیشان بدە|{v} بابەتی نوێ پیشان بدە|{v} بابەتی نوێ پیشان بدە",
"view_older_posts": "ڕەنگە بابەتە کۆنەکانی نموونەکانی تر پیشان نەدرێت."
},
"title": {
"federated_timeline": "هێڵی کاتی فێدرێرکراو",
"local_timeline": "هێڵی کاتی ناوخۆیی"
},
"tooltip": {
"add_content_warning": "ئاگادارکردنەوەی ناوەڕۆک زیاد بکە",
"add_emojis": "ئیمۆجی زیاد بکە",
"add_media": "زیادکردنی وێنە، ڤیدیۆ یان پەڕگەیێکی دەنگی",
"add_publishable_content": "ناوەڕۆک زیاد بکە بۆ بڵاوکردنەوە",
"change_content_visibility": "گۆڕینی بینینی ناوەڕۆک",
"change_language": "گۆڕینی زمان",
"emoji": "ئیمۆجی",
"explore_links_intro": "ئەم هەواڵانە لەلایەن ئەندامان و ڕاژەکارەکانی دیکەی تۆڕی کۆمەڵایەتییەوە باس دەکرێن.",
"explore_posts_intro": "ئەم بابەتانە لەم ڕاژەکارە و ڕاژەکارەکانی تر لە تۆڕی لامەرکەزی ئێستا لەسەر ئەم ڕاژەکارە ڕاکێشن.",
"explore_tags_intro": "ئەم هاشتاگانە لە ئێستادا لە نێو ئەندامانی سەر ئەم ڕاژەکارە و ڕاژەکارەکانی تری تۆڕی لامەرکەزیدا جێگەی خۆیان دەگرن.",
"open_editor_tools": "ئامرازەکانی سەرنوسەر",
"pick_an_icon": "ئایکۆنێک هەڵبژێرە",
"publish_failed": "نامە شکستخواردووەکان لە سەرەوەی سەرنوسەر دابخە بۆ دووبارە بڵاوکردنەوەی پۆستەکان",
"toggle_bold": "قەڵەو کردن",
"toggle_code_block": "بلۆکی کۆد",
"toggle_italic": "لار کردن"
},
"user": {
"add_existing": "هەژمارەیەکی بەردەست زیاد بکە",
"server_address_label": "ناونیشانی ڕاژەکاری ماستۆدۆن",
"sign_in_desc": "چوونەژوورەوە بۆ شوێنکەوتنی پڕۆفایل یان هاشتاگەکان، دڵخوازەکان، هاوبەشکردن و وەڵامدانەوەی بابەتەکان، یان کارلێککردن لە هەژمارەکەتەوە لەسەر ڕاژەکارێکی جیاواز.",
"sign_in_notice_title": "بینینی {0} داتا گشتیەکان",
"sign_out_account": "چوونە دەرەوە {0}",
"single_instance_sign_in_desc": "چوونەژوورەوە بۆ شوێنکەوتنی پڕۆفایل یان هاشتاگەکان، دڵخوازەکان، هاوبەشکردن و وەڵامدانەوەی بابەتەکان،.",
"tip_no_account": "ئەگەر هێشتا هەژمارەیێکی ماستۆدۆنت نییە، {0}.",
"tip_register_account": "ڕاژەکارەکەت هەڵبژێرە و یەکێکیان تۆمار بکە"
},
"visibility": {
"direct": "ڕاستەوخۆ",
"direct_desc": "بینراو تەنها بۆ بەکارهێنەرە ئاماژەپێکراو",
"private": "تەنها شوێنکەوتووان",
"private_desc": "بینراو تەنها بۆ شوێنکەوتووان",
"public": "گشتی",
"public_desc": "بینراو بۆ هەمووان",
"unlisted": "لیست نەکراوە",
"unlisted_desc": "بینراو بۆ هەمووان، بەڵام هەڵبژێردراو لە تایبەتمەندییەکانی دۆزینەوە"
}
}

View file

@ -218,18 +218,8 @@
"title": "Medien"
},
"navigation": {
"go_to_bookmarks": "Favoriten",
"go_to_conversations": "Direktnachrichten",
"go_to_explore": "Entdecken",
"go_to_favourites": "Favoriten",
"go_to_federated": "Föderiert",
"go_to_home": "Startseite",
"go_to_lists": "Listen",
"go_to_local": "Lokal",
"go_to_notifications": "Benachrichtigungen",
"go_to_profile": "Profil",
"go_to_search": "Suche",
"go_to_settings": "Einstellungen",
"next_status": "Nächster Status",
"previous_status": "Vorheriger Status",
"shortcut_help": "Tastaturkürzel-Hilfe",

View file

@ -149,12 +149,7 @@
"mute_account": {
"cancel": "Cancel",
"confirm": "Mute",
"days": "days|day|days",
"description": "Are you sure you want to mute {0}?",
"hours": "hours|hour|hours",
"minute": "minutes|minute|minutes",
"notifications": "Mute notifications",
"specify_duration": "Specify mute duration",
"title": "Mute account"
},
"show_reblogs": {
@ -196,14 +191,14 @@
"title": "Preview deploy"
},
"desc_highlight": "Expect some bugs and missing features here and there.",
"desc_para1": "Elk is a nimble Mastodon web client. You can login to your Mastodon account and use it to interact with the fediverse.",
"desc_para2": "Elk is Open Source and we're actively improving it as a community project. Join us and let's build it together!",
"desc_para1": "Thanks for your interest in trying out Elk, our work-in-progress Mastodon web client!",
"desc_para2": "we are working hard on the development and improving it over time.",
"desc_para3": "To boost development, you can sponsor the Team through GitHub Sponsors. We hope you enjoy Elk!",
"desc_para4": "If you'd like to report a bug, help us testing, give feedback, or contribute,",
"desc_para4": "Elk is Open Source. If you'd like to help with testing, giving feedback, or contributing,",
"desc_para5": "reach out to us on GitHub",
"desc_para6": "and get involved.",
"footer_team": "The Elk Team",
"title": "Welcome to Elk!"
"title": "Elk is in Preview!"
},
"language": {
"search": "Search"
@ -231,7 +226,7 @@
"boost": "Boost",
"command_mode": "Command mode",
"compose": "Compose",
"favourite": "Favorite",
"favourite": "Favourite",
"search": "Search",
"show_new_items": "Show new items",
"title": "Actions"
@ -240,18 +235,8 @@
"title": "Media"
},
"navigation": {
"go_to_bookmarks": "Bookmarks",
"go_to_conversations": "Conversations",
"go_to_explore": "Explore",
"go_to_favourites": "Favorites",
"go_to_federated": "Federated",
"go_to_home": "Home",
"go_to_lists": "Lists",
"go_to_local": "Local",
"go_to_notifications": "Notifications",
"go_to_profile": "Profile",
"go_to_search": "Search",
"go_to_settings": "Settings",
"next_status": "Next post",
"previous_status": "Previous post",
"shortcut_help": "Shortcut help",
@ -306,11 +291,9 @@
"built_at": "Built {0}",
"compose": "Compose",
"conversations": "Conversations",
"docs": "Documentation",
"explore": "Explore",
"favourites": "Favorites",
"federated": "Federated",
"hashtags": "Hashtags",
"home": "Home",
"list": "List",
"lists": "Lists",
@ -460,7 +443,6 @@
},
"language": {
"display_language": "Display Language",
"how_to_contribute": "How to contribute?",
"label": "Language",
"post_language": "Posting Language",
"status": "Translation status: {0}/{1} ({2}%)",
@ -542,10 +524,8 @@
"hide_boost_count": "Hide boost count",
"hide_favorite_count": "Hide favorite count",
"hide_follower_count": "Hide following/follower count",
"hide_gif_indi_on_posts": "Hide gif indicator on posts",
"hide_news": "Hide news",
"hide_reply_count": "Hide reply count",
"hide_tag_hover_card": "Hide tag hover card",
"hide_translation": "Hide translation",
"hide_username_emojis": "Hide username emojis",
"hide_username_emojis_description": "Hides emojis from usernames in timelines. Emojis will still be visible in their profiles.",
@ -594,11 +574,7 @@
},
"state": {
"attachments_exceed_server_limit": "The number of attachments exceeded the limit per post.",
"attachments_limit_audio_error": "Maximum audio size exceeded: {0}",
"attachments_limit_error": "Limit per post exceeded",
"attachments_limit_image_error": "Maximum image size exceeded: {0}",
"attachments_limit_unknown_error": "Maximum file size exceeded: {0}",
"attachments_limit_video_error": "Maximum video size exceeded: {0}",
"edited": "(Edited)",
"editing": "Editing",
"loading": "Loading...",
@ -619,7 +595,6 @@
"favourited_by": "Favorited By",
"filter_hidden_phrase": "Filtered by",
"filter_show_anyway": "Show anyway",
"gif": "GIF",
"img_alt": {
"ALT": "ALT",
"desc": "Description",

View file

@ -84,16 +84,6 @@
"add_account": "Añadir cuenta a la lista",
"remove_account": "Quitar cuenta de la lista"
},
"magic_keys": {
"groups": {
"navigation": {
"go_to_conversations": "Mensajes directos",
"go_to_federated": "Historia federada",
"go_to_local": "Historia local",
"go_to_settings": "Preferencias"
}
}
},
"menu": {
"block_domain": "Ocultar dominio {0}",
"delete_and_redraft": "Eliminar y volver a borrador",
@ -184,9 +174,6 @@
"label": "Usuarios en línea"
}
},
"state": {
"attachments_limit_video_error": "Tamaño máximo de video excedido: {0}"
},
"status": {
"spoiler_show_less": "Menos"
},

View file

@ -149,12 +149,7 @@
"mute_account": {
"cancel": "Cancelar",
"confirm": "Silenciar",
"days": "días|día|días",
"description": "¿Estás seguro que quieres silenciar a {0}?",
"hours": "horas|hora|horas",
"minute": "minutos|minuto|minutos",
"notifications": "Silenciar notificaciones",
"specify_duration": "Especificar la duración del silenciado",
"title": "Silenciar cuenta"
},
"show_reblogs": {
@ -240,18 +235,8 @@
"title": "Multimedia"
},
"navigation": {
"go_to_bookmarks": "Marcadores",
"go_to_conversations": "Conversaciones",
"go_to_explore": "Explorar",
"go_to_favourites": "Favoritas",
"go_to_federated": "Federados",
"go_to_home": "Inicio",
"go_to_lists": "Listas",
"go_to_local": "Local",
"go_to_notifications": "Notificaciones",
"go_to_profile": "Perfil",
"go_to_search": "Buscar",
"go_to_settings": "Ajustes",
"next_status": "Siguiente estado",
"previous_status": "Anterior estado",
"shortcut_help": "Atajo de ayuda",
@ -306,11 +291,9 @@
"built_at": "Compilado {0}",
"compose": "Redactar",
"conversations": "Conversaciones",
"docs": "Documentación",
"explore": "Explorar",
"favourites": "Favoritas",
"federated": "Federados",
"hashtags": "Etiquetas",
"home": "Inicio",
"list": "Lista",
"lists": "Listas",
@ -460,7 +443,6 @@
},
"language": {
"display_language": "Idioma de pantalla",
"how_to_contribute": "¿Cómo contribuir?",
"label": "Idioma",
"post_language": "Idioma de publicación",
"status": "Estado traducción: {0}/{1} ({2}%)",
@ -542,10 +524,8 @@
"hide_boost_count": "Ocultar contador de retoots",
"hide_favorite_count": "Ocultar número de publicaciones favoritas",
"hide_follower_count": "Ocultar número de seguidores",
"hide_gif_indi_on_posts": "Ocultar indicador de gif en publicaciones",
"hide_news": "Ocultar noticias",
"hide_reply_count": "Ocultar número de respuestas",
"hide_tag_hover_card": "Ocultar tarjeta flotante de etiqueta",
"hide_translation": "Ocultar traducción",
"hide_username_emojis": "Ocultar emojis en el nombre de usuario",
"hide_username_emojis_description": "Oculta los emojis de los nombres de usuarios en la línea de tiempo. Los emojis permanecerán visibles en sus perfiles.",
@ -594,11 +574,7 @@
},
"state": {
"attachments_exceed_server_limit": "Número máximo de archivos adjuntos por publicación excedido.",
"attachments_limit_audio_error": "Tamaño máximo de audio excedido: {0}",
"attachments_limit_error": "Límite por publicación excedido",
"attachments_limit_image_error": "Tamaño máximo de imagen excedido: {0}",
"attachments_limit_unknown_error": "Tamaño máximo de archivo excedido: {0}",
"attachments_limit_video_error": "Tamaño máximo de vídeo excedido: {0}",
"edited": "(Editado)",
"editing": "Editando",
"loading": "Cargando...",
@ -619,7 +595,6 @@
"favourited_by": "Marcado como favorita por",
"filter_hidden_phrase": "Filtrado por",
"filter_show_anyway": "Mostrar de todas formas",
"gif": "GIF",
"img_alt": {
"ALT": "ALT",
"desc": "Descripción",
@ -716,7 +691,7 @@
"tooltip": {
"add_content_warning": "Añadir advertencia de contenido",
"add_emojis": "Agregar emojis",
"add_media": "Añadir imágenes, vídeo o audio",
"add_media": "Añadir imágenes, video o audio",
"add_publishable_content": "Publicar contenido",
"change_content_visibility": "Cambiar visibilidad de contenido",
"change_language": "Cambiar idioma",

View file

@ -149,12 +149,7 @@
"mute_account": {
"cancel": "Utzi",
"confirm": "Mututu",
"days": "egun|egun|egun",
"description": "Ziur {0} mututu nahi duzula?",
"hours": "ordu|ordu|ordu",
"minute": "minutu|minutu|minutu",
"notifications": "Mututu jakinarazpenak",
"specify_duration": "Zehaztu mututzearen iraupena",
"title": "Mututu kontua"
},
"show_reblogs": {
@ -233,27 +228,16 @@
"compose": "Idatzi",
"favourite": "Egin gogoko",
"search": "Bilatu",
"show_new_items": "Erakutsi elementu berriak",
"title": "Ekintzak"
},
"media": {
"title": "Multimedia"
},
"navigation": {
"go_to_bookmarks": "Laster-markak",
"go_to_conversations": "Elkarrizketak",
"go_to_explore": "Arakatu",
"go_to_favourites": "Gogokoak",
"go_to_federated": "Federatua",
"go_to_home": "Hasiera",
"go_to_lists": "Zerrendak",
"go_to_local": "Lokala",
"go_to_notifications": "Jakinarazpenak",
"go_to_profile": "Profila",
"go_to_search": "Bilatu",
"go_to_settings": "Ezarpenak",
"next_status": "Hurrengo bidalketa",
"previous_status": "Aurreko bidalketa",
"next_status": "Hurrengo egoera",
"previous_status": "Aurreko egoera",
"shortcut_help": "Laster-teklak",
"title": "Nabigazioa"
}
@ -309,7 +293,6 @@
"explore": "Arakatu",
"favourites": "Gogokoak",
"federated": "Federatua",
"hashtags": "Traolak",
"home": "Hasiera",
"list": "Zerrenda",
"lists": "Zerrendak",
@ -321,22 +304,22 @@
"search": "Bilaketa",
"select_feature_flags": "Aldatu ezaugarrien adierazleak (Toggle Feature Flags)",
"select_font_size": "Letra-tipoaren tamaina",
"select_language": "Interfazearen hizkuntza",
"select_language": "Hizkuntza",
"settings": "Ezarpenak",
"show_intro": "Erakutsi aurkezpena",
"toggle_theme": "Gai argia/iluna",
"zen_mode": "ZEN modua"
},
"notification": {
"favourited_post": "(e)k zure bidalketa gogoko egin du",
"favourited_post": "zure bidalketa gogoko egin du",
"followed_you": "(e)k jarraitu dizu",
"followed_you_count": "{0} pertsonak jarraitu dizute|pertsona {0}ek jarraitu dizu|{0} pertsonak jarraitu dizute",
"missing_type": "MISSING notification.type:",
"reblogged_post": "(e)k zure bidalketari bultzada eman dio",
"reblogged_post": "zure bidalketari bultzada eman dio",
"reported": "{0}(e)k {1} salatu du",
"request_to_follow": "(e)k jarraipen-eskaera bidali dizu",
"signed_up": "(e)k izena eman du",
"update_status": "(e)k bidalketa eguneratu du"
"request_to_follow": "jarraipen-eskaera bidali dizu",
"signed_up": "izena eman du",
"update_status": "bidalketa eguneratu du"
},
"placeholder": {
"content_warning": "Idatzi oharra hemen",
@ -391,7 +374,7 @@
}
},
"report": {
"additional_comments": "Iruzkin gehigarriak",
"additional_comments": "Ohar gehigarriak",
"another_server": "Salatzen ari zaren erabiltzailea beste zerbitzari batekoa da",
"anything_else": "Ba al dago jakin beharko genukeen beste ezer?",
"block_desc": "Ez duzu erabiltzaile honen bidalketarik ikusiko. Ezin izango ditu zure bidalketak ikusi edo zuri jarraitu. Blokeatu duzula jakingo du.",
@ -412,10 +395,10 @@
"limiting": "Mugatu {0}",
"mute_desc": "Ez duzu erabiltzaile honen bidalketarik ikusiko. Zure bidalketak ikusi eta zuri jarraitu diezazuke. Ez du jakingo mututu duzunik.",
"other": "Beste zerbait da",
"other_desc": "Arazoa ez dator bat beste kategoriekin",
"other_desc": "Arazoa ez da beste kategorietan sartzen",
"reporting": "{0} salatzen",
"select_many": "Hautatu dagozkion guztiak:",
"select_one": "Aukeratu hobekien doakiona:",
"select_many": "Hautatu bat datozen guztiak:",
"select_one": "Aukeratu hobekien datorrena:",
"select_posts": "Salaketa hau sostengatzen duen bidalketarik al dago?",
"select_posts_other": "Ba al dago salaketa sostengatzen duen beste bidalketarik?",
"spam": "Spama da",
@ -458,10 +441,9 @@
"theme_color": "Gaiaren kolorea"
},
"language": {
"display_language": "Interfazearen hizkuntza",
"how_to_contribute": "Nola lagun dezaket?",
"display_language": "Hizkuntza",
"label": "Hizkuntza",
"post_language": "Bidalketen hizkuntza",
"post_language": "Argitalpenaren hizkuntza",
"status": "Itzulpenaren egoera: {1} kateetatik {0} itzulita (%{2}a)",
"translations": {
"add": "Gehitu",
@ -536,14 +518,13 @@
"github_cards": "GitHub txartelak",
"github_cards_description": "GitHub-eko esteka argitaratzean, HTML txartel irisgarri bat erakusten du gizarte-grafikoaren metadatuak erabiliz, irudi sozialaren ordez.",
"grayscale_mode": "Gris modua",
"hide_account_hover_card": "Ezkutatu kontuen aurrebista-txartelak sagua gainetik pasatzean",
"hide_account_hover_card": "Ezkutatu kontuaren geruza (Hide account hover card)",
"hide_alt_indi_on_posts": "Ezkutatu ALT adierazlea bidalketetan",
"hide_boost_count": "Ezkutatu bultzaden kopurua",
"hide_favorite_count": "Ezkutatu gogokoen kopurua",
"hide_follower_count": "Ezkutatu jarraitzaile kopurua",
"hide_news": "Ezkutatu berriak",
"hide_reply_count": "Ezkutatu erantzunen kopurua",
"hide_tag_hover_card": "Ezkutatu traolen aurrebista-txartelak sagua gainetik pasatzean",
"hide_translation": "Ezkutatu itzulpenak",
"hide_username_emojis": "Ezkutatu emojiak erabiltzaile izenetan",
"hide_username_emojis_description": "Denbora-lerroetan erabiltzaile izenetako emojiak ezkutatzen ditu. Emojiak ikusgai egoten jarraituko dute euren profiletan.",
@ -556,7 +537,7 @@
"virtual_scroll": "Korritze birtuala",
"virtual_scroll_description": "Zerrenda birtual bat erabiltzen du denbora-lerroetan, item kopuru handiagoa modu eraginkorrean erakutsi ahal izateko.",
"wellbeing": "Ongizatea",
"zen_mode": "ZEN modua",
"zen_mode": "Zen modua",
"zen_mode_description": "Aldamenetako elementuak ezkutatzen ditu xagua gainean jarri arte. Denbora-lerroko elementu batzuk ere ezkutatzen ditu."
},
"profile": {
@ -604,7 +585,7 @@
},
"status": {
"account": {
"suspended_message": "Bidalketa honen egileari zigorra jarri zaio.",
"suspended_message": "Argitalpen honen egileari zigorra jarri zaio.",
"suspended_show": "Erakutsi edukia hala ere?"
},
"boosted_by": "Bultzatu dute:",

View file

@ -1,715 +0,0 @@
{
"a11y": {
"loading_page": "در حال بارگذاری صفحه، لطفا صبر کنید",
"loading_titled_page": "در حال بارگذاری صفحه {0}، لطفا صبر کنید",
"locale_changed": "زبان به {0} تغییر کرد",
"locale_changing": "در حال تغییر زبان، لطفا صبر کنید",
"route_loaded": "صفحه {0} بارگذاری شد"
},
"account": {
"authorize": "اجازه دنبال کردن بدهید",
"authorized": "شما درخواست را تأیید کردید",
"avatar_description": "آواتار {0}",
"blocked_by": "شما توسط این کاربر مسدود شده‌اید.",
"blocked_domains": "دامنه‌های مسدود شده",
"blocked_users": "کاربران مسدود شده",
"blocking": "مسدود شده",
"bot": "ربات",
"copy_account_name": "رونوشت از نام حساب",
"favourites": "موارد مورد علاقه",
"follow": "دنبال کنید",
"follow_back": "دنبال کردن متقابل",
"follow_requested": "درخواست شده",
"followers": "دنبال‌کنندگان",
"followers_count": "{0} دنبال‌کننده",
"following": "دنبال‌شده",
"following_count": "{0} در حال دنبال کردن",
"follows_you": "شما را دنبال می‌کند",
"go_to_profile": "به نمایه بروید",
"joined": "پیوسته",
"moved_title": "اعلام کرده که این حساب کاربری جدید ایشان است:",
"muted_users": "کاربران بی‌صدا شده",
"muting": "بی صدا",
"mutuals": "مشترکات",
"notifications_on_post_disable": "دیگر هنگامی که {username} فرسته ای ارسال می‌کند مرا با خبر نکن",
"notifications_on_post_enable": "هنگامی که {username} فرسته ای‌ را ارسال‌ می‌کند مرا با خبر کن",
"pinned": "سنجاق شده",
"posts": "فرسته‌ها",
"posts_count": "{0} فرسته|{0} فرسته|{0} فرسته",
"profile_description": "توضیحات بالای صفحه نمایه {0}",
"profile_personal_note": "یادداشت شخصی",
"profile_unavailable": "نمایه در دسترس نیست",
"reject": "رد درخواست",
"rejected": "درخواست را رد کردید",
"request_follow": "درخواست برای دنبال کردن",
"requested": "{0} درخواست دنبال کردن شما را دارد",
"unblock": "رفع مسدودیت",
"unfollow": "دنبال نکردن",
"unmute": "باصدا",
"view_other_followers": "دنبال‌کنندگان از نمونه‌های دیگر ممکن است نمایش داده نشوند.",
"view_other_following": "دنبال شوندگان از نمونه‌های دیگر ممکن است نمایش داده نشوند.",
"withdraw_follow_request": "درخواست دنبال‌کردن را پس بگیرید"
},
"action": {
"apply": "اعمال",
"bookmark": "نشانک",
"bookmarked": "نشانک زده شده",
"boost": "تقویت",
"boost_count": "{0}",
"boosted": "تقویت شده",
"clear_publish_failed": "پاک کردن خطاهای انتشار",
"clear_save_failed": "پاک کردن خطاهای ذخیره‌سازی",
"clear_upload_failed": "پاک کردن خطاهای بارگذاری فایل",
"close": "بستن",
"compose": "نوشتن",
"confirm": "تایید",
"done": "انجام شد",
"edit": "ویرایش",
"enter_app": "ورود به برنامه",
"favourite": "مورد علاقه",
"favourite_count": "{0}",
"favourited": "به علاقه‌مندی‌ها افزوده شد",
"more": "بیشتر",
"next": "بعدی",
"prev": "قبلی",
"publish": "انتشار",
"reply": "پاسخ",
"reply_count": "{0}",
"reset": "بازنشانی",
"save": "ذخیره",
"save_changes": "ذخیره تغییرات",
"sign_in": "ورود",
"sign_in_to": "ورود به {0}",
"switch_account": "تغییر حساب کاربری",
"vote": "رای"
},
"app_desc_short": "یک کارخواه وب چابک برای ماستودون",
"app_logo": "لوگوی نرم‌افزار",
"app_name": "Elk",
"attachment": {
"edit_title": "توضیحات",
"remove_label": "حذف پیوست"
},
"command": {
"activate": "فعال‌سازی",
"complete": "کامل کردن",
"compose_desc": "نوشتن فرسته جدید",
"n_people_in_the_past_n_days": "{0} نفر در {1} روز گذشته",
"select_lang": "انتخاب زبان",
"sign_in_desc": "افزودن حساب کاربری موجود",
"switch_account": "تغییر به {0}",
"switch_account_desc": "تغییر حساب به حساب دیگری",
"toggle_dark_mode": "تغییر حالت تاریک",
"toggle_zen_mode": "تغییر حالت آرام"
},
"common": {
"end_of_list": "پایان لیست",
"error": "خطا",
"fetching": "در حال بارگیری...",
"in": "در",
"no_bookmarks": "هنوز هیچ فرسته نشانه گذاری شده ای وجود ندارد",
"no_favourites": "فرسته پسندیده شده ای هنوز وجود ندارد",
"not_found": "۴۰۴ پیدا نشد",
"offline_desc": "به نظر می‌رسد شما آفلاین هستید. لطفا اتصال شبکه خود را بررسی کنید."
},
"confirm": {
"block_account": {
"cancel": "لغو",
"confirm": "مسدود کردن",
"title": "آیا از مسدود کردن {0} اطمینان دارید؟"
},
"block_domain": {
"cancel": "لغو",
"confirm": "مسدود کردن",
"title": "آیا از مسدود کردن دامنه {0} اطمینان دارید؟"
},
"common": {
"cancel": "خیر",
"confirm": "بله"
},
"delete_list": {
"cancel": "لغو",
"confirm": "حذف",
"title": "آیا از حذف لیست {0} اطمینان دارید؟"
},
"delete_posts": {
"cancel": "لغو",
"confirm": "حذف",
"title": "آیا از حذف این فرسته اطمینان دارید؟"
},
"mute_account": {
"cancel": "لغو",
"confirm": "بی‌صدا کردن",
"title": "آیا از بی‌صدا کردن {0} اطمینان دارید؟"
},
"show_reblogs": {
"cancel": "لغو",
"confirm": "نمایش",
"title": "آیا می‌خواهید بازنشرها از {0} را نمایش دهید؟"
},
"unfollow": {
"cancel": "لغو",
"confirm": "دنبال نکردن",
"title": "آیا از دنبال نکردن اطمینان دارید؟"
}
},
"conversation": {
"with": "با"
},
"custom_cards": {
"stackblitz": {
"lines": "خطوط {0}",
"open": "باز کردن",
"snippet_from": "تکه کد از {0}"
}
},
"error": {
"account_not_found": "حساب {0} یافت نشد",
"explore_list_empty": "هیچ چیز تازه‌ای وجود ندارد. بعداً بررسی کنید!",
"file_size_cannot_exceed_n_mb": "حجم فایل نمی‌تواند از {0}MB بیشتر باشد",
"sign_in_error": "امکان برقراری ارتباط با سرور وجود ندارد.",
"status_not_found": "فرسته یافت نشد",
"unsupported_file_format": "قالب فایل پشتیبانی نمی‌شود"
},
"help": {
"build_preview": {
"desc1": "شما در حال مشاهده نسخه پیش‌نمایش جامعه نرم‌افزار هستید - {0}.",
"desc2": "ممکن است شامل تغییرات بررسی نشده یا حتی مخرب باشد.",
"desc3": "با حساب کاربری واقعی خود وارد نشوید.",
"title": "استقرار پیش‌نمایش"
},
"desc_highlight": "انتظار برخی اشکالات و ویژگی‌های ناقص را داشته باشید.",
"desc_para1": "از علاقه شما به امتحان کردن نرم‌افزار، کارخواه وب در دست توسعه ماستودون تشکر می‌کنیم!",
"desc_para2": "ما به سختی در حال کار بر روی توسعه و بهبود آن با گذر زمان هستیم.",
"desc_para3": "برای تقویت توسعه، می‌توانید از طریق GitHub Sponsors از تیم حمایت کنید. امیدواریم از نرم‌افزار لذت ببرید!",
"desc_para4": "نرم‌افزار یک منبع باز است. اگر دوست دارید در تست‌کردن، ارائه بازخورد، یا مشارکت کمک کنید،",
"desc_para5": "از طریق GitHub با ما تماس بگیرید",
"desc_para6": "و مشارکت کنید.",
"footer_team": "تیم نرم‌افزار",
"title": "نرم‌افزار در حالت پیش نمایش است!"
},
"language": {
"search": "جستجو"
},
"list": {
"add_account": "اضافه کردن حساب به لیست",
"cancel_edit": "لغو ویرایش",
"clear_error": "پاک کردن خطا",
"create": "ایجاد کردن",
"delete": "حذف این لیست",
"delete_error": "خطایی هنگام حذف لیست رخ داد",
"edit": "ویرایش این لیست",
"edit_error": "خطایی هنگام به‌روزرسانی لیست رخ داد",
"error": "خطایی هنگام ایجاد لیست رخ داد",
"error_prefix": "خطا: ",
"list_title_placeholder": "عنوان لیست",
"modify_account": "تغییر دادن لیست‌ها با حساب کاربری",
"remove_account": "حذف حساب از لیست",
"save": "ذخیره تغییرات"
},
"magic_keys": {
"dialog_header": "میانبرهای صفحه‌کلید",
"groups": {
"actions": {
"boost": "تقویت",
"command_mode": "حالت فرمان",
"compose": "تهیه فرسته",
"favourite": "مورد علاقه",
"search": "جستجو",
"title": "اقدامات"
},
"media": {
"title": "رسانه"
},
"navigation": {
"go_to_home": "خانه",
"go_to_notifications": "اعلان‌ها",
"next_status": "فرسته بعدی",
"previous_status": "فرسته قبلی",
"shortcut_help": "راهنمای میانبرها",
"title": "ناوبری"
}
},
"sequence_then": "در ادامه دنباله"
},
"menu": {
"add_personal_note": "افزودن یادداشت شخصی به {0}",
"block_account": "مسدود کردن {0}",
"block_domain": "مسدود کردن دامنه {0}",
"copy_link_to_post": "کپی لینک این فرسته",
"copy_original_link_to_post": "کپی لینک اصلی این فرسته",
"delete": "حذف",
"delete_and_redraft": "حذف و بازنویسی",
"direct_message_account": "پیام خصوصی {0}",
"edit": "ویرایش",
"hide_reblogs": "پنهان کردن بوست‌ها از {0}",
"mention_account": "ذکر {0}",
"mute_account": "ساکت کردن {0}",
"mute_conversation": "ساکت کردن این فرسته",
"open_in_original_site": "باز کردن در سایت اصلی",
"pin_on_profile": "به نمایه سنجاق کنید",
"remove_personal_note": "یادداشت شخصی را از {0} حذف کنید",
"report_account": "گزارش {0}",
"share_account": "اشتراک‌گذاری {0}",
"share_post": "اشتراک‌گذاری این فرسته",
"show_favourited_and_boosted_by": "نمایش کسانی که پسند و به اشتراک گذاشته‌اند",
"show_reblogs": "نمایش به اشتراک‌گذاری‌ها از {0}",
"show_untranslated": "نمایش بدون ترجمه",
"toggle_theme": {
"dark": "حالت تاریک را تغییر دهید",
"light": "حالت روشن را تغییر دهید"
},
"translate_post": "ترجمه فرسته",
"unblock_account": "رفع انسداد {0}",
"unblock_domain": "رفع انسداد دامنه {0}",
"unfollow_account": "دنبال نکردن {0}",
"unmute_account": "بی‌صدا نکردن {0}",
"unmute_conversation": "بی‌صدا نکردن این فرسته",
"unpin_on_profile": "از سنجاق خارج کردن در نمایه"
},
"modals": {
"aria_label_close": "بستن"
},
"nav": {
"back": "بازگشت",
"blocked_domains": "دامنه‌های مسدود شده",
"blocked_users": "کاربران مسدود شده",
"bookmarks": "نشان‌ها",
"built_at": "ساخته شده {0}",
"compose": "نوشتن",
"conversations": "گفتگوها",
"explore": "کاوش کنید",
"favourites": "موارد مورد علاقه",
"federated": "عمومی",
"home": "خانه",
"list": "لیست",
"lists": "لیست‌ها",
"local": "محلی",
"muted_users": "کاربران با صدای خاموش",
"notifications": "اعلانات",
"privacy": "حریم خصوصی",
"profile": "نمایه",
"search": "جستجو",
"select_feature_flags": "تغییر ویژگی‌های پرچم",
"select_font_size": "اندازه قلم",
"select_language": "زبان نمایش",
"settings": "تنظیمات",
"show_intro": "نمایش مقدمه",
"toggle_theme": "تغییر تم",
"zen_mode": "حالت آرام"
},
"notification": {
"favourited_post": "فرسته شما را پسندید",
"followed_you": "شما را دنبال کرد",
"followed_you_count": "{0} نفر شما را دنبال کردند|{0} نفر شما را دنبال کرد|{0} نفر شما را دنبال کردند",
"missing_type": "اعلان از نوع ناقص:",
"reblogged_post": "فرسته شما را باز نشر کرد",
"reported": "{0} گزارش داد {1}",
"request_to_follow": "درخواست دنبال کردن شما را داد",
"signed_up": "ثبت نام کرد",
"update_status": "فرسته خود را به‌روزرسانی کرد"
},
"placeholder": {
"content_warning": "اخطار خود را اینجا بنویسید",
"default_1": "چه فکری می‌کنید؟",
"reply_to_account": "پاسخ به {0}",
"replying": "در حال پاسخ دادن"
},
"polls": {
"allow_multiple": "امکان انتخاب چندگزینه‌ای",
"cancel": "لغو",
"create": "ایجاد نظرسنجی",
"disallow_multiple": "عدم امکان انتخاب چندگزینه‌ای",
"expiration": "انقضای نظرسنجی",
"hide_votes": "مخفی کردن تعداد آرا تا پایان",
"option_placeholder": "گزینه نظرسنجی {current}/{max}",
"remove_option": "حذف گزینه",
"settings": "تنظیمات نظرسنجی",
"show_votes": "نمایش دائمی تعداد آرا"
},
"pwa": {
"dismiss": "رد کردن",
"install": "نصب کردن",
"install_title": "نصب Elk",
"screenshots": {
"dark": "تصویری از Elk در حالت تیره",
"light": "تصویری از Elk در حالت روشن"
},
"title": "به‌روزرسانی جدید Elk موجود است!",
"update": "به‌روزرسانی",
"update_available_short": "به‌روزرسانی Elk",
"webmanifest": {
"canary": {
"description": "یک مشتری وب سبک و پیشرفته مستودون (canary)",
"name": "Elk (پیش‌انتشار)",
"short_name": "Elk (پیش‌انتشار)"
},
"dev": {
"description": "یک کارخواه سبک و پیشرفته ماستودون (پیش‌نمایش)",
"name": "Elk (dev)",
"short_name": "Elk (dev)"
},
"preview": {
"description": "یک کارخواه وب چابک برای ماستودون (پیش نمایش)",
"name": "نرم‌افزار (پیش نمایش)",
"short_name": "نرم‌افزار (پیش نمایش)"
},
"release": {
"description": "یک کارخواه وب چابک برای ماستودون",
"name": "نرم‌افزار",
"short_name": "نرم‌افزار"
}
}
},
"report": {
"additional_comments": "نظرات اضافی",
"another_server": "کاربری که در حال گزارش آن هستید از سرور دیگری است",
"anything_else": "آیا چیز دیگری هست که فکر می‌کنید ما باید بدانیم؟",
"block_desc": "دیگر فرسته‌های این کاربر را مشاهده نخواهید کرد. آن‌ها نمی‌توانند فرسته‌های شما را ببینند و یا شما را دنبال کنند. آن‌ها قادر خواهند بود درک کنند که مسدود شده‌اند.",
"dontlike": "من دوست ندارم",
"dontlike_desc": "چیزی نیست که مایل به دیدن آن باشید",
"forward": "بله، این گزارش را برای {0} ارسال کنید",
"forward_question": "آیا می‌خواهید یک نسخه ناشناس از این گزارش را نیز برای آن سرور ارسال کنید؟",
"further_actions": {
"limit": {
"description": "در اینجا گزینه‌های شما برای کنترل آنچه می‌بینید موجود است:",
"title": "نمی‌خواهید این را ببینید؟"
},
"report": {
"description": "در حالی که ما این موضوع را بررسی می‌کنیم، در اینجا اقداماتی هست که می‌توانید انجام دهید:",
"title": "از گزارش شما متشکریم، ما به آن رسیدگی خواهیم کرد."
}
},
"limiting": "محدود کردن {0}",
"mute_desc": "دیگر هیچ فرستهی از این کاربر را نخواهید دید. آن‌ها همچنان می‌توانند شما را دنبال کنند و فرسته‌های شما را ببینند. آن‌ها متوجه بی‌صدا شدن نخواهند شد.",
"other": "مورد دیگری است",
"other_desc": "مشکلی که مطرح شده در دسته‌بندی‌های دیگر نمی‌گنجد",
"reporting": "گزارش {0}",
"select_many": "تمام موارد مرتبط را انتخاب کنید:",
"select_one": "بهترین تطابق را انتخاب کنید:",
"select_posts": "آیا فرسته‌هایی وجود دارد که این گزارش را پشتیبانی می‌کند؟",
"select_posts_other": "آیا فرسته‌های دیگری وجود دارد که این گزارش را پشتیبانی می‌کند؟",
"spam": "هرزنامه است",
"spam_desc": "پیوندهای مخرب، تعاملات تقلبی، یا پاسخ‌های تکراری",
"submit": "ارسال گزارش",
"unfollow_desc": "دیگر فرسته‌های این کاربر را در فید خانگی خود نخواهید دید. ممکن است در جاهای دیگر فرسته‌های آن‌ها را ببینید.",
"violation": "یک یا چند قوانین سرور را نقض می‌کند",
"whats_wrong_account": "بگویید چه مشکلی با این حساب کاربری وجود دارد",
"whats_wrong_post": "بگویید چه مشکلی با این فرسته وجود دارد"
},
"search": {
"search_desc": "جستجوی افراد و برچسب‌ها",
"search_empty": "هیچ موردی برای اصطلاحات جستجو شده پیدا نشد"
},
"settings": {
"about": {
"built_at": "ساخته شده در",
"label": "درباره",
"meet_the_team": "آشنایی با تیم",
"sponsor_action": "حمایت از ما",
"sponsor_action_desc": "برای حمایت از تیم توسعه‌دهنده Elk,",
"sponsors": "حامیان",
"sponsors_body_1": "Elk با حمایت سخاوتمندانه و کمک‌ های:",
"sponsors_body_2": "و تمام شرکت‌ها و افرادی که از تیم Elk و اعضاء آن حمایت می‌کنند، امکان‌پذیر شده است.",
"sponsors_body_3": "اگر از برنامه لذت می‌برید، لطفا از ما حمایت کنید:",
"version": "نسخه"
},
"account_settings": {
"description": "تنظیمات حساب کاربری خود را در رابط کاربری ماستودون ویرایش کنید",
"label": "تنظیمات حساب کاربری"
},
"interface": {
"color_mode": "حالت رنگ",
"dark_mode": "تاریک",
"default": " (پیش‌فرض)",
"font_size": "اندازه فونت",
"label": "رابط کاربری",
"light_mode": "روشن",
"system_mode": "سیستم",
"theme_color": "رنگ تم"
},
"language": {
"display_language": "زبان نمایش",
"label": "زبان",
"post_language": "زبان ارسال",
"status": "وضعیت ترجمه: {0}/{1} ({2}٪)",
"translations": {
"add": "اضافه کردن",
"choose_language": "انتخاب زبان",
"heading": "ترجمه‌ها",
"hide_specific": "پنهان کردن ترجمه‌های خاص",
"remove": "حذف کردن"
}
},
"notifications": {
"label": "اعلان‌ها",
"notifications": {
"label": "تنظیمات اعلان‌ها"
},
"push_notifications": {
"alerts": {
"favourite": "مورد علاقه‌ها",
"follow": "دنبال‌کنندگان جدید",
"mention": "اشاره‌ها",
"poll": "نظرسنجی‌ها",
"reblog": "انتشارات",
"title": "کدام اعلان‌ها را دریافت کنیم؟"
},
"description": "اعلان‌ها را حتی زمانی که از Elk استفاده نمی‌کنید دریافت کنید.",
"instructions": "فراموش نکنید که تغییرات خود را با استفاده از دکمه ذخیره تنظیمات اعلان‌های سریع بالا ذخیره کنید!",
"label": "تنظیمات اعلان‌های سریع",
"policy": {
"all": "از همه",
"followed": "از افرادی که دنبال می‌کنم",
"follower": "از افرادی که من را دنبال می‌کنند",
"none": "از هیچ‌کس",
"title": "از چه کسی می‌توانم اعلان‌ها را دریافت کنم؟"
},
"save_settings": "ذخیره تنظیمات",
"subscription_error": {
"clear_error": "پاک کردن خطا",
"error_hint": "می‌توانید به لیست سوالات متداول مراجعه کنید تا مشکل را حل کنید: {0}.",
"invalid_vapid_key": "کلید عمومی VAPID به نظر نامعتبر است.",
"permission_denied": "اجازه دسترسی رد شد: اعلان‌ها را در مرورگر خود فعال کنید.",
"repo_link": "مخزن Elk در گیت‌هاب",
"request_error": "هنگام درخواست اشتراک‌گذاری خطایی رخ داد، دوباره تلاش کنید و اگر مشکل ادامه یافت، لطفاً موضوع را در مخزن Elk گزارش دهید.",
"title": "توانایی اشتراک‌گذاری در اعلان‌های سریع وجود ندارد",
"too_many_registrations": "به دلیل محدودیت‌های مرورگر، Elk نمی‌تواند از سرویس اعلان‌های سریع برای حساب‌های متعدد در سرورهای مختلف استفاده کند. باید از اعلان‌های سریع در یک حساب دیگر لغو اشتراک کنید و دوباره تلاش کنید.",
"vapid_not_supported": "مرورگر شما از اعلان‌های وب پشتیبانی می‌کند، اما به نظر می‌رسد پروتکل VAPID را پیاده‌سازی نمی‌کند."
},
"title": "تنظیمات اعلان‌های سریع",
"undo_settings": "لغو تغییرات",
"unsubscribe": "غیرفعال کردن اعلان‌های سریع",
"unsupported": "مرورگر شما از اعلان‌های سریع پشتیبانی نمی‌کند.",
"warning": {
"enable_close": "بستن",
"enable_description": "برای دریافت اعلان‌ها وقتی Elk باز نیست، اعلان‌های سریع را فعال کنید. شما می‌توانید دقیقاً کنترل کنید که چه نوع تعاملاتی اعلان‌های سریع ایجاد کند از طریق دکمه فوق که بعد از فعال‌سازی نشان داده می‌شود.",
"enable_description_desktop": "برای دریافت اعلان‌ها وقتی Elk باز نیست، اعلان‌های سریع را فعال کنید. شما می‌توانید دقیقاً کنترل کنید که چه نوع تعاملاتی اعلان‌های سریع ایجاد کند در تنظیمات > اعلان‌ها > تنظیمات اعلان‌های سریع پس از فعال‌سازی.",
"enable_description_mobile": "همچنین می‌توانید از منوی ناوبری به تنظیمات > اعلان‌ها > تنظیمات اعلان‌های سریع دسترسی پیدا کنید.",
"enable_description_settings": "برای دریافت اعلان‌ها وقتی Elk باز نیست، اعلان‌های سریع را فعال کنید. شما خواهید توانست دقیقاً کنترل کنید که چه نوع تعاملاتی اعلان‌های سریع ایجاد کند در همین صفحه پس از فعال‌سازی آن‌ها.",
"enable_desktop": "فعال‌سازی اعلان‌های سریع",
"enable_title": "هیچ‌چیزی را از دست ندهید",
"re_auth": "به نظر می‌رسد سرور شما از اعلان‌های سریع پشتیبانی نمی‌کند. خروج و ورود مجدد را امتحان کنید، اگر این پیام همچنان ظاهر شد با مدیر سرور خود تماس بگیرید."
}
},
"show_btn": "رفتن به تنظیمات اعلان‌ها",
"under_construction": "در دست ساخت"
},
"notifications_settings": "اعلان‌ها",
"preferences": {
"embedded_media": "پخش‌کننده رسانه توکار",
"embedded_media_description": "نمایش یک پخش‌کننده توکار به جای کارت پیش‌نمایش معمولی هنگام گسترش پیوندهای رسانه‌ای به اشتراک گذاشته شده.",
"enable_autoplay": "فعال‌سازی خودکارپخش",
"enable_data_saving": "فعال‌سازی صرفه‌جویی در مصرف داده",
"enable_data_saving_description": "صرفه‌جویی در داده‌ها با جلوگیری از بارگذاری خودکار پیوست‌ها.",
"enable_pinch_to_zoom": "فعال‌سازی بزرگنمایی با دو انگشت",
"github_cards": "کارت‌های گیت‌هاب",
"github_cards_description": "هنگام ارسال پیوند گیت‌هاب، یک کارت HTML قابل دسترس با استفاده از نمایش شبکه‌های اجتماعی به نمایش درمی‌آید به جای تصویر اجتماعی.",
"grayscale_mode": "حالت خاکستری",
"hide_account_hover_card": "پنهان کردن کارت شناور حساب",
"hide_alt_indi_on_posts": "پنهان کردن نشانگر جایگزین در فرسته‌ها",
"hide_boost_count": "پنهان کردن شمارش انتشارات",
"hide_favorite_count": "پنهان کردن شمارش موردهای علاقه‌مندی",
"hide_follower_count": "پنهان کردن شمارش دنبال‌کنندگان و دنبال‌شوندگان",
"hide_news": "پنهان کردن خبرها",
"hide_reply_count": "پنهان کردن شمارش پاسخ‌ها",
"hide_translation": "پنهان کردن ترجمه",
"hide_username_emojis": "پنهان کردن اموجی‌های نام کاربری",
"hide_username_emojis_description": "پنهان کردن اموجی‌ها از نام‌های کاربری در زمان خطوط. اموجی‌ها هنوز در نمایه‌های آن‌ها قابل مشاهده خواهند بود.",
"label": "ترجیحات",
"title": "ویژگی‌های تجربی",
"use_star_favorite_icon": "استفاده از نماد ستاره موردهای علاقه‌مندی",
"user_picker": "انتخاب‌کننده کاربر",
"user_picker_description": "نمایش تمام آواتارهای حساب‌های ورود شده در پایین-چپ تا شما بتوانید به سرعت بین آن‌ها تعویض کنید.",
"virtual_scroll": "پیمایش مجازی",
"virtual_scroll_description": "استفاده از لیست مجازی در خطوط زمانی، برای نمایش تعداد بیشتری آیتم با عملکرد بهتر.",
"wellbeing": "سلامتی",
"zen_mode": "حالت آرامش",
"zen_mode_description": "پنهان سازی حواشی مگر آنکه نشانگر موس روی آنها باشد. همچنین پنهان‌سازی بعضی المان‌ها از خط زمانی."
},
"profile": {
"appearance": {
"bio": "بیوگرافی",
"description": "ویرایش آواتار، نام کاربری، نمایه و غیره.",
"display_name": "نام نمایشی",
"label": "ظاهر",
"profile_metadata": "توضیحات نمایه",
"profile_metadata_desc": "شما می‌توانید تا {0} مورد را به صورت جدول در نمایه خود نمایش دهید",
"profile_metadata_label": "برچسب",
"profile_metadata_value": "محتوا",
"title": "ویرایش نمایه"
},
"featured_tags": {
"description": "افراد می‌توانند فرسته‌های عمومی شما را زیر این برچسب‌ها مرور کنند.",
"label": "برچسب‌های برجسته",
"under_construction": "در دست ساخت"
},
"label": "نمایه"
},
"select_a_settings": "یک تنظیم را انتخاب کنید",
"users": {
"export": "صادر کردن توکن‌های کاربر",
"import": "وارد کردن توکن‌های کاربر",
"label": "کاربران وارد شده"
}
},
"share_target": {
"description": "نرم‌افزار را می‌توانید طوری پیکربندی کنید که از سایر برنامه‌ها محتوا را به اشتراک بگذارید، تنها کافیست نرم‌افزار را روی دستگاه یا کامپیوتر خود نصب کرده و وارد شوید.",
"hint": "برای به اشتراک گذاشتن محتوا با نرم‌افزار، باید نرم‌افزار نصب شده باشد و شما وارد حساب کاربری خود شده باشید.",
"title": "به اشتراک گذاشتن با نرم‌افزار"
},
"state": {
"attachments_exceed_server_limit": "تعداد پیوست‌ها از حد مجاز هر فرسته تجاوز کرده است.",
"attachments_limit_error": "حد مجاز فرسته تجاوز شده",
"edited": "(ویرایش شده)",
"editing": "در حال ویرایش",
"loading": "در حال بارگذاری...",
"publish_failed": "انتشار ناموفق بود",
"publishing": "در حال انتشار",
"save_failed": "ذخیره ناموفق بود",
"upload_failed": "بارگذاری ناموفق بود",
"uploading": "در حال بارگذاری..."
},
"status": {
"account": {
"suspended_message": "حساب کاربری این فرسته تعلیق شده است.",
"suspended_show": "با این حال محتوا را نشان دهم؟"
},
"boosted_by": "تقویت شده توسط",
"edited": "ویرایش شده {0}",
"embedded_warning": "پخش این ممکن است آدرس IP شما را برای دیگران آشکار کند.",
"favourited_by": "مورد علاقه توسط",
"filter_hidden_phrase": "مخفی شده توسط فیلتر",
"filter_show_anyway": "با این حال نشان بده",
"img_alt": {
"ALT": "ALT",
"desc": "توصیف",
"dismiss": "رد کردن",
"read": "خواندن توصیف {0}"
},
"poll": {
"count": "{0} رای|{0} رای|{0} رای",
"ends": "پایان {0}",
"finished": "به پایان رسید {0}"
},
"replying_to": "در پاسخ به {0}",
"show_full_thread": "نمایش کامل رشته",
"someone": "یک نفر",
"spoiler_media_hidden": "رسانه مخفی شده",
"spoiler_show_less": "نمایش کمتر",
"spoiler_show_more": "نمایش بیشتر",
"thread": "رشته",
"try_original_site": "سایت اصلی را امتحان کنید"
},
"status_history": {
"created": "ایجاد شده {0}",
"edited": "ویرایش شده {0}"
},
"tab": {
"accounts": "حساب‌ها",
"for_you": "برای شما",
"hashtags": "برچسب‌ها",
"list": "لیست",
"media": "رسانه",
"news": "اخبار",
"notifications_admin": {
"report": "گزارش",
"sign_up": "ثبت نام"
},
"notifications_all": "همه",
"notifications_favourite": "مورد علاقه",
"notifications_follow": "دنبال کردن",
"notifications_follow_request": "درخواست دنبال کردن",
"notifications_mention": "اشاره",
"notifications_more_tooltip": "نوع اعلان‌ها را بر اساس نوع فیلتر کنید",
"notifications_poll": "نظرسنجی",
"notifications_reblog": "بازنشر",
"notifications_status": "وضعیت",
"notifications_update": "بروزرسانی",
"posts": "فرسته‌ها",
"posts_with_replies": "فرسته‌ها و پاسخ‌ها"
},
"tag": {
"follow": "دنبال کردن",
"follow_label": "دنبال کردن برچسب {0}",
"unfollow": "انصراف از دنبال کردن",
"unfollow_label": "انصراف از دنبال کردن برچسب {0}"
},
"time_ago_options": {
"day_future": "در 0 روز|فردا|در {n} روز",
"day_past": "0 روز پیش|دیروز|{n} روز پیش",
"hour_future": "در 0 ساعت|در 1 ساعت|در {n} ساعت",
"hour_past": "0 ساعت پیش|1 ساعت پیش|{n} ساعت پیش",
"just_now": "همین الان",
"minute_future": "در 0 دقیقه|در 1 دقیقه|در {n} دقیقه",
"minute_past": "0 دقیقه پیش|1 دقیقه پیش|{n} دقیقه پیش",
"month_future": "در 0 ماه|ماه بعد|در {n} ماه",
"month_past": "0 ماه پیش|ماه گذشته|{n} ماه پیش",
"second_future": "همین الان|در {n} ثانیه|در {n} ثانیه",
"second_past": "همین الان|{n} ثانیه پیش|{n} ثانیه پیش",
"short_day_future": "در {n}روز",
"short_day_past": "{n}روز",
"short_hour_future": "در {n}س",
"short_hour_past": "{n}س",
"short_minute_future": "در {n}دقیقه",
"short_minute_past": "{n}دقیقه",
"short_month_future": "در {n}م",
"short_month_past": "{n}م",
"short_second_future": "در {n}ث",
"short_second_past": "{n}ث",
"short_week_future": "در {n}هفته",
"short_week_past": "{n}هفته",
"short_year_future": "در {n}سال",
"short_year_past": "{n}سال",
"week_future": "در 0 هفته|هفته بعد|در {n} هفته",
"week_past": "0 هفته پیش|هفته گذشته|{n} هفته پیش",
"year_future": "در 0 سال|سال بعد|در {n} سال",
"year_past": "0 سال پیش|سال گذشته|{n} سال پیش"
},
"timeline": {
"show_new_items": "نمایش {v} مورد جدید|نمایش {v} مورد جدید|نمایش {v} مورد جدید",
"view_older_posts": "فرسته‌های قدیمی‌تر از دیگر موارد ممکن است نمایش داده نشوند."
},
"title": {
"federated_timeline": "جریان اتحادی",
"local_timeline": "جریان محلی"
},
"tooltip": {
"add_content_warning": "اضافه کردن هشدار محتوا",
"add_emojis": "اضافه کردن شکلک‌ها",
"add_media": "اضافه کردن تصاویر، ویدیو یا فایل صوتی",
"add_publishable_content": "اضافه کردن محتوای قابل انتشار",
"change_content_visibility": "تغییر دیده‌بانی محتوا",
"change_language": "تغییر زبان",
"emoji": "شکلک",
"explore_links_intro": "این داستان‌های خبری، در حال حاضر توسط افراد در این و دیگر نمونه‌های شبکه غیرمتمرکز مورد بحث قرار می‌گیرد.",
"explore_posts_intro": "این فرسته‌ها از این و دیگر نمونه‌های در شبکه غیرمتمرکز هم اکنون در این سرور محبوبیت دارند.",
"explore_tags_intro": "این برچسب‌ها در حال حاضر میان افراد در این و دیگر سرورهای شبکه غیرمتمرکز مشهور شده‌اند.",
"open_editor_tools": "ابزار ویراستار",
"pick_an_icon": "انتخاب یک آیکون",
"publish_failed": "پیام‌های ناموفق را در بالای ویرایشگر ببندید تا مجدد فرسته‌ها را انتشار دهید",
"toggle_bold": "تغییر به حالت توپر",
"toggle_code_block": "تغییر به بلوک کد",
"toggle_italic": "تغییر به حالت کج"
},
"user": {
"add_existing": "اضافه کردن حساب کاربری موجود",
"server_address_label": "آدرس سرور ماستودون",
"sign_in_desc": "برای دنبال کردن نمایه‌ها یا برچسب‌ها، پسندیدن، به اشتراک‌گذاری و پاسخ دادن به فرسته‌ها وارد شوید، یا از حساب کاربری خود در سرور دیگر ارتباط برقرار کنید.",
"sign_in_notice_title": "مشاهده داده‌های عمومی {0}",
"sign_out_account": "خروج از حساب کاربری {0}",
"single_instance_sign_in_desc": "برای دنبال کردن نمایه‌ها یا برچسب‌ها، پسندیدن، به اشتراک‌گذاری و پاسخ دادن به فرسته‌ها وارد شوید.",
"tip_no_account": "اگر هنوز حساب کاربری ماستودون ندارید، {0}.",
"tip_register_account": "سرور خود را انتخاب کرده و یکی بسازید"
},
"visibility": {
"direct": "مستقیم",
"direct_desc": "فقط برای کاربران منشن‌شده قابل مشاهده",
"private": "فقط دنبال‌کنندگان",
"private_desc": "فقط برای دنبال‌کنندگان قابل مشاهده",
"public": "عمومی",
"public_desc": "برای همه قابل مشاهده",
"unlisted": "فهرست نشده",
"unlisted_desc": "برای همه قابل مشاهده اما از ویژگی‌های کشف خارج شده‌است"
}
}

Some files were not shown because too many files have changed in this diff Show more