Merge branch 'main' into feature/emoji-autocomplete

This commit is contained in:
Daniel Roe 2023-01-16 01:03:02 +00:00 committed by GitHub
commit 17e8130638
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
170 changed files with 3169 additions and 1141 deletions

View file

@ -10,6 +10,8 @@ NUXT_CLOUDFLARE_API_TOKEN=
NUXT_STORAGE_DRIVER= NUXT_STORAGE_DRIVER=
NUXT_STORAGE_FS_BASE= NUXT_STORAGE_FS_BASE=
NUXT_ADMIN_KEY=
NUXT_PUBLIC_DISABLE_VERSION_CHECK= NUXT_PUBLIC_DISABLE_VERSION_CHECK=
NUXT_GITHUB_CLIENT_ID= NUXT_GITHUB_CLIENT_ID=

26
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,26 @@
<!-- Thank you for contributing! -->
### Description
<!-- Please insert your description here and provide especially info about the "what" this PR is solving -->
### Additional context
<!-- e.g. is there anything you'd like reviewers to focus on? -->
---
### What is the purpose of this pull request? <!-- (put an "X" next to an item) -->
- [ ] Bug fix
- [ ] New Feature
- [ ] Documentation update
- [ ] Translations update
- [ ] Other
### Before submitting the PR, please make sure you do the following
- [ ] Read the [Contributing Guidelines](https://github.com/elk-zone/elk/blob/main/CONTRIBUTING.md).
- [ ] Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
- [ ] Provide related snapshots or videos.
- [ ] Provide a description in this PR that addresses **what** the PR is solving, or reference the issue that it solves (e.g. `fixes #123`).

View file

@ -93,9 +93,9 @@ We are using [vue-i18n](https://vue-i18n.intlify.dev/) via [nuxt-i18n](https://i
1. Add a new file in [locales](./locales) folder with the language code as the filename. 1. Add a new file in [locales](./locales) folder with the language code as the filename.
2. Copy [en-US](./locales/en-US.json) and translate the strings. 2. Copy [en-US](./locales/en-US.json) and translate the strings.
3. Add the language to the `locales` array in [config/i18n.ts](./config/i18n.ts#L13) 3. Add the language to the `locales` array in [config/i18n.ts](./config/i18n.ts#L12), below `en` variants and `ar-EG`.
4. If the language is `right-to-left`, add `dir` option with `rtl` value, for example, for [ar-EG](./config/i18n.ts#L79) 4. If the language is `right-to-left`, add `dir` option with `rtl` value, for example, for [ar-EG](./config/i18n.ts#L27)
5. If the language requires special pluralization rules, add `pluralRule` callback option, for example, for [ar-EG](./config/i18n.ts#L80) 5. If the language requires special pluralization rules, add `pluralRule` callback option, for example, for [ar-EG](./config/i18n.ts#L27)
Check [Pluralization rule callback](https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization) for more info. Check [Pluralization rule callback](https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization) for more info.

View file

@ -28,11 +28,20 @@ A nimble Mastodon web client
It is already quite usable, but it isn't ready for wide adoption yet. We recommend you use it if you would like to help us build it. We appreciate your feedback and contributions. Check out the [Open Issues](https://github.com/elk-zone/elk/issues) and jump in the action. Join the [Elk discord server](https://chat.elk.zone) to chat with us and learn more about the project. It is already quite usable, but it isn't ready for wide adoption yet. We recommend you use it if you would like to help us build it. We appreciate your feedback and contributions. Check out the [Open Issues](https://github.com/elk-zone/elk/issues) and jump in the action. Join the [Elk discord server](https://chat.elk.zone) to chat with us and learn more about the project.
The client is deployed on: ## Official Deployment
The Elk team maintains a deployment at:
- 🦌 Production: [elk.zone](https://elk.zone) - 🦌 Production: [elk.zone](https://elk.zone)
- 🐙 Canary: [main.elk.zone](https://main.elk.zone) (deploys on every commit to `main` branch) - 🐙 Canary: [main.elk.zone](https://main.elk.zone) (deploys on every commit to `main` branch)
## 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:
- [elk.h4.io](https://elk.h4.io) - Use Elk for the `h4.io` Server
- [elk.universeodon.com](https://elk.universeodon.com) - Use Elk for the Universeodon Server
## 💖 Sponsors ## 💖 Sponsors
We are grateful for the generous sponsorship and help of: We are grateful for the generous sponsorship and help of:

View file

@ -3,6 +3,7 @@ setupPageHeader()
provideGlobalCommands() provideGlobalCommands()
const route = useRoute() const route = useRoute()
if (process.server && !route.path.startsWith('/settings')) { if (process.server && !route.path.startsWith('/settings')) {
useHead({ useHead({
meta: [ meta: [

View file

@ -8,15 +8,15 @@ const { account, command, context, ...props } = defineProps<{
command?: boolean command?: boolean
}>() }>()
const isSelf = $computed(() => currentUser.value?.account.id === account.id) const isSelf = $(useSelfAccount(() => account))
const enable = $computed(() => !isSelf && currentUser.value) const enable = $computed(() => !isSelf && currentUser.value)
const relationship = $computed(() => props.relationship || useRelationship(account).value) const relationship = $computed(() => props.relationship || useRelationship(account).value)
const masto = useMasto() const { client } = $(useMasto())
async function toggleFollow() { async function toggleFollow() {
relationship!.following = !relationship!.following relationship!.following = !relationship!.following
try { try {
const newRel = await masto.v1.accounts[relationship!.following ? 'follow' : 'unfollow'](account.id) const newRel = await client.v1.accounts[relationship!.following ? 'follow' : 'unfollow'](account.id)
Object.assign(relationship!, newRel) Object.assign(relationship!, newRel)
} }
catch (err) { catch (err) {
@ -29,7 +29,7 @@ async function toggleFollow() {
async function unblock() { async function unblock() {
relationship!.blocking = false relationship!.blocking = false
try { try {
const newRel = await masto.v1.accounts.unblock(account.id) const newRel = await client.v1.accounts.unblock(account.id)
Object.assign(relationship!, newRel) Object.assign(relationship!, newRel)
} }
catch (err) { catch (err) {
@ -42,7 +42,7 @@ async function unblock() {
async function unmute() { async function unmute() {
relationship!.muting = false relationship!.muting = false
try { try {
const newRel = await masto.v1.accounts.unmute(account.id) const newRel = await client.v1.accounts.unmute(account.id)
Object.assign(relationship!, newRel) Object.assign(relationship!, newRel)
} }
catch (err) { catch (err) {

View file

@ -6,6 +6,8 @@ const { account } = defineProps<{
command?: boolean command?: boolean
}>() }>()
const { client } = $(useMasto())
const { t } = useI18n() const { t } = useI18n()
const createdAt = $(useFormattedDateTime(() => account.createdAt, { const createdAt = $(useFormattedDateTime(() => account.createdAt, {
@ -14,6 +16,8 @@ const createdAt = $(useFormattedDateTime(() => account.createdAt, {
year: 'numeric', year: 'numeric',
})) }))
const relationship = $(useRelationship(account))
const namedFields = ref<mastodon.v1.AccountField[]>([]) const namedFields = ref<mastodon.v1.AccountField[]>([])
const iconFields = ref<mastodon.v1.AccountField[]>([]) const iconFields = ref<mastodon.v1.AccountField[]>([])
@ -39,6 +43,18 @@ function previewAvatar() {
}]) }])
} }
async function toggleNotifications() {
relationship!.notifying = !relationship?.notifying
try {
const newRel = await client.v1.accounts.follow(account.id, { notify: relationship?.notifying })
Object.assign(relationship!, newRel)
}
catch {
// TODO error handling
relationship!.notifying = !relationship?.notifying
}
}
watchEffect(() => { watchEffect(() => {
const named: mastodon.v1.AccountField[] = [] const named: mastodon.v1.AccountField[] = []
const icons: mastodon.v1.AccountField[] = [] const icons: mastodon.v1.AccountField[] = []
@ -59,7 +75,8 @@ watchEffect(() => {
iconFields.value = icons iconFields.value = icons
}) })
const isSelf = $computed(() => currentUser.value?.account.id === account.id) const isSelf = $(useSelfAccount(() => account))
const isNotifiedOnPost = $computed(() => !!relationship?.notifying)
</script> </script>
<template> <template>
@ -83,6 +100,17 @@ const isSelf = $computed(() => currentUser.value?.account.id === account.id)
</div> </div>
<div absolute top-18 inset-ie-0 flex gap-2 items-center> <div absolute top-18 inset-ie-0 flex gap-2 items-center>
<AccountMoreButton :account="account" :command="command" /> <AccountMoreButton :account="account" :command="command" />
<button
v-if="!isSelf && relationship?.following"
:aria-pressed="isNotifiedOnPost"
:aria-label="t('account.notify_on_post', { username: `@${account.username}` })"
rounded-full p2 border-1 transition-colors
:class="isNotifiedOnPost ? 'text-primary border-primary hover:bg-red/20 hover:text-red hover:border-red' : 'border-base hover:text-primary'"
@click="toggleNotifications"
>
<span v-if="isNotifiedOnPost" i-ri:bell-fill block text-current />
<span v-else i-ri-bell-line block text-current />
</button>
<AccountFollowButton :account="account" :command="command" /> <AccountFollowButton :account="account" :command="command" />
<!-- Edit profile --> <!-- Edit profile -->
<NuxtLink <NuxtLink
@ -93,11 +121,6 @@ const isSelf = $computed(() => currentUser.value?.account.id === account.id)
> >
{{ $t('settings.profile.appearance.title') }} {{ $t('settings.profile.appearance.title') }}
</NuxtLink> </NuxtLink>
<!-- <button flex gap-1 items-center w-full rounded op75 hover="op100 text-purple" group>
<div rounded p2 group-hover="bg-rose/10">
<div i-ri:bell-line />
</div>
</button> -->
</div> </div>
</div> </div>
<div v-if="account.note" max-h-100 overflow-y-auto> <div v-if="account.note" max-h-100 overflow-y-auto>

View file

@ -12,7 +12,7 @@ const { link = true, avatar = true } = defineProps<{
<AccountHoverWrapper :account="account"> <AccountHoverWrapper :account="account">
<NuxtLink <NuxtLink
:to="link ? getAccountRoute(account) : undefined" :to="link ? getAccountRoute(account) : undefined"
:class="link ? 'text-link-rounded ms-0 ps-0' : ''" :class="link ? 'text-link-rounded -ml-1.8rem pl-1.8rem rtl-(ml0 pl-0.5rem -mr-1.8rem pr-1.8rem)' : ''"
min-w-0 flex gap-2 items-center min-w-0 flex gap-2 items-center
> >
<AccountAvatar v-if="avatar" :account="account" w-5 h-5 /> <AccountAvatar v-if="avatar" :account="account" w-5 h-5 />

View file

@ -7,39 +7,49 @@ const { account } = defineProps<{
}>() }>()
let relationship = $(useRelationship(account)) let relationship = $(useRelationship(account))
const isSelf = $computed(() => currentUser.value?.account.id === account.id) const isSelf = $(useSelfAccount(() => account))
const masto = useMasto() const { t } = useI18n()
const toggleMute = async () => { const { client } = $(useMasto())
// TODO: Add confirmation
const isConfirmed = async (title: string) => {
return await openConfirmDialog(t('common.confirm_dialog.title', [title])) === 'confirm'
}
const toggleMute = async (title: string) => {
if (!await isConfirmed(title))
return
relationship!.muting = !relationship!.muting relationship!.muting = !relationship!.muting
relationship = relationship!.muting relationship = relationship!.muting
? await masto.v1.accounts.mute(account.id, { ? await client.v1.accounts.mute(account.id, {
// TODO support more options // TODO support more options
}) })
: await masto.v1.accounts.unmute(account.id) : await client.v1.accounts.unmute(account.id)
} }
const toggleBlockUser = async () => { const toggleBlockUser = async (title: string) => {
// TODO: Add confirmation if (!await isConfirmed(title))
return
relationship!.blocking = !relationship!.blocking relationship!.blocking = !relationship!.blocking
relationship = await masto.v1.accounts[relationship!.blocking ? 'block' : 'unblock'](account.id) relationship = await client.v1.accounts[relationship!.blocking ? 'block' : 'unblock'](account.id)
} }
const toggleBlockDomain = async () => { const toggleBlockDomain = async (title: string) => {
// TODO: Add confirmation if (!await isConfirmed(title))
return
relationship!.domainBlocking = !relationship!.domainBlocking relationship!.domainBlocking = !relationship!.domainBlocking
await masto.v1.domainBlocks[relationship!.domainBlocking ? 'block' : 'unblock'](getServerName(account)) await client.v1.domainBlocks[relationship!.domainBlocking ? 'block' : 'unblock'](getServerName(account))
} }
const toggleReblogs = async () => { const toggleReblogs = async (title: string) => {
// TODO: Add confirmation if (!await isConfirmed(title))
return
const showingReblogs = !relationship?.showingReblogs const showingReblogs = !relationship?.showingReblogs
relationship = await masto.v1.accounts.follow(account.id, { reblogs: showingReblogs }) relationship = await client.v1.accounts.follow(account.id, { reblogs: showingReblogs })
} }
</script> </script>
@ -80,14 +90,14 @@ const toggleReblogs = async () => {
icon="i-ri:repeat-line" icon="i-ri:repeat-line"
:text="$t('menu.show_reblogs', [`@${account.acct}`])" :text="$t('menu.show_reblogs', [`@${account.acct}`])"
:command="command" :command="command"
@click="toggleReblogs" @click="toggleReblogs($t('menu.show_reblogs', [`@${account.acct}`]))"
/> />
<CommonDropdownItem <CommonDropdownItem
v-else v-else
:text="$t('menu.hide_reblogs', [`@${account.acct}`])" :text="$t('menu.hide_reblogs', [`@${account.acct}`])"
icon="i-ri:repeat-line" icon="i-ri:repeat-line"
:command="command" :command="command"
@click="toggleReblogs" @click="toggleReblogs($t('menu.hide_reblogs', [`@${account.acct}`]))"
/> />
<CommonDropdownItem <CommonDropdownItem
@ -95,14 +105,14 @@ const toggleReblogs = async () => {
:text="$t('menu.mute_account', [`@${account.acct}`])" :text="$t('menu.mute_account', [`@${account.acct}`])"
icon="i-ri:volume-up-fill" icon="i-ri:volume-up-fill"
:command="command" :command="command"
@click="toggleMute" @click="toggleMute($t('menu.mute_account', [`@${account.acct}`]))"
/> />
<CommonDropdownItem <CommonDropdownItem
v-else v-else
:text="$t('menu.unmute_account', [`@${account.acct}`])" :text="$t('menu.unmute_account', [`@${account.acct}`])"
icon="i-ri:volume-mute-line" icon="i-ri:volume-mute-line"
:command="command" :command="command"
@click="toggleMute" @click="toggleMute($t('menu.unmute_account', [`@${account.acct}`]))"
/> />
<CommonDropdownItem <CommonDropdownItem
@ -110,14 +120,14 @@ const toggleReblogs = async () => {
:text="$t('menu.block_account', [`@${account.acct}`])" :text="$t('menu.block_account', [`@${account.acct}`])"
icon="i-ri:forbid-2-line" icon="i-ri:forbid-2-line"
:command="command" :command="command"
@click="toggleBlockUser" @click="toggleBlockUser($t('menu.block_account', [`@${account.acct}`]))"
/> />
<CommonDropdownItem <CommonDropdownItem
v-else v-else
:text="$t('menu.unblock_account', [`@${account.acct}`])" :text="$t('menu.unblock_account', [`@${account.acct}`])"
icon="i-ri:checkbox-circle-line" icon="i-ri:checkbox-circle-line"
:command="command" :command="command"
@click="toggleBlockUser" @click="toggleBlockUser($t('menu.unblock_account', [`@${account.acct}`]))"
/> />
<template v-if="getServerName(account) !== currentServer"> <template v-if="getServerName(account) !== currentServer">
@ -126,14 +136,14 @@ const toggleReblogs = async () => {
:text="$t('menu.block_domain', [getServerName(account)])" :text="$t('menu.block_domain', [getServerName(account)])"
icon="i-ri:shut-down-line" icon="i-ri:shut-down-line"
:command="command" :command="command"
@click="toggleBlockDomain" @click="toggleBlockDomain($t('menu.block_domain', [getServerName(account)]))"
/> />
<CommonDropdownItem <CommonDropdownItem
v-else v-else
:text="$t('menu.unblock_domain', [getServerName(account)])" :text="$t('menu.unblock_domain', [getServerName(account)])"
icon="i-ri:restart-line" icon="i-ri:restart-line"
:command="command" :command="command"
@click="toggleBlockDomain" @click="toggleBlockDomain($t('menu.unblock_domain', [getServerName(account)]))"
/> />
</template> </template>
</template> </template>

View file

@ -1,10 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Paginator, mastodon } from 'masto' import type { Paginator, mastodon } from 'masto'
const { paginator } = defineProps<{ const { paginator, account, context } = defineProps<{
paginator: Paginator<mastodon.v1.Account[], mastodon.DefaultPaginationParams> paginator: Paginator<mastodon.v1.Account[], mastodon.DefaultPaginationParams>
context?: 'following' | 'followers'
account?: mastodon.v1.Account
relationshipContext?: 'followedBy' | 'following' relationshipContext?: 'followedBy' | 'following'
}>() }>()
const fallbackContext = $computed(() => {
return ['following', 'followers'].includes(context!)
})
const showOriginSite = $computed(() =>
account && account.id !== currentUser.value?.account.id && getServerName(account) !== currentServer.value,
)
</script> </script>
<template> <template>
@ -17,5 +26,18 @@ const { paginator } = defineProps<{
border="b base" py2 px4 border="b base" py2 px4
/> />
</template> </template>
<template v-if="fallbackContext && showOriginSite" #done>
<div p5 text-secondary text-center flex flex-col items-center gap1>
<span italic>{{ $t(`account.view_other_${context}`) }}</span>
<NuxtLink
:href="account!.url" target="_blank" external
flex="~ gap-1" items-center text-primary
hover="underline text-primary-active"
>
<div i-ri:external-link-fill />
{{ $t('menu.open_in_original_site') }}
</NuxtLink>
</div>
</template>
</CommonPaginator> </CommonPaginator>
</template> </template>

View file

@ -40,7 +40,7 @@ const userSettings = useUserSettings()
</template> </template>
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink
v-if="!getWellnessSetting(userSettings, 'hideFollowerCount')" v-if="!getPreferences(userSettings, 'hideFollowerCount')"
:to="getAccountFollowersRoute(account)" :to="getAccountFollowersRoute(account)"
replace text-secondary replace text-secondary
exact-active-class="text-primary" exact-active-class="text-primary"

View file

@ -88,17 +88,19 @@ watch(file, (image, _, onCleanup) => {
w-full w-full
h-full h-full
> >
<div absolute bg="black/50" text-white rounded-full text-xl w12 h12 flex justify-center items-center hover="bg-black/40 text-primary"> <span absolute bg="black/50" text-white rounded-full text-xl w12 h12 flex justify-center items-center hover="bg-black/40 text-primary">
<div i-ri:upload-line /> <span block i-ri:upload-line />
</div> </span>
<div <span
v-if="loading" v-if="loading"
absolute inset-0 absolute inset-0
bg="black/30" text-white bg="black/30" text-white
flex justify-center items-center flex justify-center items-center
> >
<div class="i-ri:loader-4-line animate-spin animate-duration-[2.5s]" text-4xl /> <span class="animate-spin animate-duration-[2.5s] preserve-3d">
</div> <span block i-ri:loader-4-line text-4xl />
</span>
</span>
</label> </label>
</template> </template>

View file

@ -42,7 +42,7 @@ defineSlots<{
const { t } = useI18n() const { t } = useI18n()
const { items, prevItems, update, state, endAnchor, error } = usePaginator(paginator, stream, eventType, preprocess) const { items, prevItems, update, state, endAnchor, error } = usePaginator(paginator, $$(stream), eventType, preprocess)
</script> </script>
<template> <template>

View file

@ -9,6 +9,7 @@ defineProps<{
<template> <template>
<VTooltip <VTooltip
v-bind="$attrs" v-bind="$attrs"
auto-hide
> >
<slot /> <slot />
<template #popper> <template #popper>

View file

@ -18,5 +18,6 @@ const highlighted = computed(() => {
</script> </script>
<template> <template>
<pre class="code-block" v-html="highlighted" /> <pre v-if="lang" class="code-block" v-html="highlighted" />
<pre v-else class="code-block">{{ raw }}</pre>
</template> </template>

View file

@ -17,7 +17,7 @@ defineProps<{
<div flex justify-between px5 py2 :class="{ 'xl:hidden': $route.name !== 'tag' }"> <div flex justify-between px5 py2 :class="{ 'xl:hidden': $route.name !== 'tag' }">
<div flex gap-3 items-center overflow-hidden py2> <div flex gap-3 items-center overflow-hidden py2>
<NuxtLink <NuxtLink
v-if="backOnSmallScreen || back" flex="~ gap1" items-center btn-text p-0 lg:hidden v-if="backOnSmallScreen || back" flex="~ gap1" items-center btn-text p-0 xl:hidden
:aria-label="$t('nav.back')" :aria-label="$t('nav.back')"
@click="$router.go(-1)" @click="$router.go(-1)"
> >
@ -31,7 +31,7 @@ defineProps<{
<div flex items-center flex-shrink-0 gap-x-2> <div flex items-center flex-shrink-0 gap-x-2>
<slot name="actions" /> <slot name="actions" />
<PwaBadge lg:hidden /> <PwaBadge lg:hidden />
<NavUser v-if="isMastoInitialised" /> <NavUser v-if="isHydrated" />
<NavUserSkeleton v-else /> <NavUserSkeleton v-else />
</div> </div>
</div> </div>

View file

@ -51,7 +51,7 @@ const handleFavouritedBoostedByClose = () => {
</script> </script>
<template> <template>
<template v-if="isMastoInitialised"> <template v-if="isHydrated">
<ModalDialog v-model="isSigninDialogOpen" py-4 px-8 max-w-125> <ModalDialog v-model="isSigninDialogOpen" py-4 px-8 max-w-125>
<UserSignIn /> <UserSignIn />
</ModalDialog> </ModalDialog>

View file

@ -53,6 +53,10 @@ const { modelValue: visible } = defineModel<{
modelValue: boolean modelValue: boolean
}>() }>()
defineOptions({
inheritAttrs: false,
})
const deactivated = useDeactivated() const deactivated = useDeactivated()
const route = useRoute() const route = useRoute()
@ -132,12 +136,6 @@ useEventListener('keydown', (e: KeyboardEvent) => {
}) })
</script> </script>
<script lang="ts">
export default {
inheritAttrs: false,
}
</script>
<template> <template>
<Teleport to="body"> <Teleport to="body">
<!-- Dialog component --> <!-- Dialog component -->

View file

@ -10,7 +10,7 @@ const moreMenuVisible = ref(false)
class="after-content-empty after:(h-[calc(100%+0.5px)] w-0.1px pointer-events-none)" class="after-content-empty after:(h-[calc(100%+0.5px)] w-0.1px pointer-events-none)"
> >
<!-- These weird styles above are used for scroll locking, don't change it unless you know exactly what you're doing. --> <!-- These weird styles above are used for scroll locking, don't change it unless you know exactly what you're doing. -->
<template v-if="isMastoInitialised && currentUser"> <template v-if="currentUser">
<NuxtLink to="/home" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop"> <NuxtLink to="/home" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
<div i-ri:home-5-line /> <div i-ri:home-5-line />
</NuxtLink> </NuxtLink>
@ -24,7 +24,7 @@ const moreMenuVisible = ref(false)
<div i-ri:at-line /> <div i-ri:at-line />
</NuxtLink> </NuxtLink>
</template> </template>
<template v-if="isMastoInitialised && !currentUser"> <template v-else>
<NuxtLink :to="`/${currentServer}/explore`" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop"> <NuxtLink :to="`/${currentServer}/explore`" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
<div i-ri:hashtag /> <div i-ri:hashtag />
</NuxtLink> </NuxtLink>

View file

@ -16,7 +16,7 @@ const { notifications } = useNotifications()
<NavSideItem :text="$t('nav.notifications')" to="/notifications" 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> <template #icon>
<div flex relative> <div flex relative>
<div class="i-ri:notification-4-line" md:text-size-inherit text-xl /> <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> <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>
{{ notifications < 10 ? notifications : '•' }} {{ notifications < 10 ? notifications : '•' }}
</div> </div>
@ -29,9 +29,9 @@ const { notifications } = useNotifications()
<NavSideItem :text="$t('action.compose')" to="/compose" icon="i-ri:quill-pen-line" user-only :command="command" /> <NavSideItem :text="$t('action.compose')" to="/compose" icon="i-ri:quill-pen-line" user-only :command="command" />
<div shrink hidden sm:block mt-4 /> <div shrink hidden sm:block mt-4 />
<NavSideItem :text="$t('nav.explore')" :to="isMastoInitialised ? `/${currentServer}/explore` : '/explore'" icon="i-ri:hashtag" :command="command" /> <NavSideItem :text="$t('nav.explore')" :to="isHydrated ? `/${currentServer}/explore` : '/explore'" icon="i-ri:hashtag" :command="command" />
<NavSideItem :text="$t('nav.local')" :to="isMastoInitialised ? `/${currentServer}/public/local` : '/public/local'" icon="i-ri:group-2-line " :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="isMastoInitialised ? `/${currentServer}/public` : '/public'" icon="i-ri:earth-line" :command="command" /> <NavSideItem :text="$t('nav.federated')" :to="isHydrated ? `/${currentServer}/public` : '/public'" icon="i-ri:earth-line" :command="command" />
<div shrink hidden sm:block mt-4 /> <div shrink hidden sm:block mt-4 />
<NavSideItem :text="$t('nav.settings')" to="/settings" icon="i-ri:settings-3-line" :command="command" /> <NavSideItem :text="$t('nav.settings')" to="/settings" icon="i-ri:settings-3-line" :command="command" />

View file

@ -29,7 +29,7 @@ useCommand({
}) })
let activeClass = $ref('text-primary') let activeClass = $ref('text-primary')
onMastoInit(async () => { onHydrated(async () => {
// TODO: force NuxtLink to reevaluate, we now we are in this route though, so we should force it to active // TODO: force NuxtLink to reevaluate, we now we are in this route though, so we should force it to active
// we don't have currentServer defined until later // we don't have currentServer defined until later
activeClass = '' activeClass = ''
@ -39,8 +39,8 @@ onMastoInit(async () => {
// Optimize rendering for the common case of being logged in, only show visual feedback for disabled user-only items // Optimize rendering for the common case of being logged in, only show visual feedback for disabled user-only items
// when we know there is no user. // when we know there is no user.
const noUserDisable = computed(() => !isMastoInitialised.value || (props.userOnly && !currentUser.value)) const noUserDisable = computed(() => !isHydrated.value || (props.userOnly && !currentUser.value))
const noUserVisual = computed(() => isMastoInitialised.value && props.userOnly && !currentUser.value) const noUserVisual = computed(() => isHydrated.value && props.userOnly && !currentUser.value)
</script> </script>
<template> <template>
@ -66,7 +66,7 @@ const noUserVisual = computed(() => isMastoInitialised.value && props.userOnly &
<div :class="icon" text-xl /> <div :class="icon" text-xl />
</slot> </slot>
<slot> <slot>
<span block sm:hidden xl:block>{{ isHydrated ? text : '&nbsp;' }}</span> <span block sm:hidden xl:block select-none>{{ isHydrated ? text : '&nbsp;' }}</span>
</slot> </slot>
</div> </div>
</CommonTooltip> </CommonTooltip>

View file

@ -17,6 +17,7 @@ router.afterEach(() => {
flex items-end gap-4 flex items-end gap-4
py2 px-5 py2 px-5
text-2xl text-2xl
select-none
focus-visible:ring="2 current" focus-visible:ring="2 current"
to="/" to="/"
external external

View file

@ -1,5 +1,5 @@
<template> <template>
<VDropdown v-if="isMastoInitialised && currentUser" sm:hidden> <VDropdown v-if="isHydrated && currentUser" sm:hidden>
<div style="-webkit-touch-callout: none;"> <div style="-webkit-touch-callout: none;">
<AccountAvatar <AccountAvatar
ref="avatar" ref="avatar"

View file

@ -66,7 +66,10 @@ const isLegacyAccount = computed(() => !currentUser.value?.vapidKey)
:disabled="busy || isLegacyAccount" :disabled="busy || isLegacyAccount"
@click="$emit('subscribe')" @click="$emit('subscribe')"
> >
<span aria-hidden="true" :class="busy && animate ? 'i-ri:loader-2-fill animate-spin' : 'i-ri:check-line'" /> <span v-if="busy && animate" aria-hidden="true" block animate-spin preserve-3d>
<span block i-ri:loader-2-fill aria-hidden="true" />
</span>
<span v-else aria-hidden="true" block i-ri:check-line />
<span>{{ $t('settings.notifications.push_notifications.warning.enable_desktop') }}</span> <span>{{ $t('settings.notifications.push_notifications.warning.enable_desktop') }}</span>
</button> </button>
<slot name="error" /> <slot name="error" />

View file

@ -147,7 +147,10 @@ onActivated(() => (busy = false))
:class="busy || !saveEnabled ? 'border-transparent' : null" :class="busy || !saveEnabled ? 'border-transparent' : null"
:disabled="busy || !saveEnabled" :disabled="busy || !saveEnabled"
> >
<span :class="busy && animateSave ? 'i-ri:loader-2-fill animate-spin' : 'i-ri:save-2-fill'" /> <span v-if="busy && animateSave" aria-hidden="true" block animate-spin preserve-3d>
<span block i-ri:loader-2-fill aria-hidden="true" />
</span>
<span v-else block aria-hidden="true" i-ri:save-2-fill />
{{ $t('settings.notifications.push_notifications.save_settings') }} {{ $t('settings.notifications.push_notifications.save_settings') }}
</button> </button>
<button <button
@ -157,7 +160,7 @@ onActivated(() => (busy = false))
:disabled="busy || !saveEnabled" :disabled="busy || !saveEnabled"
@click="undoChanges" @click="undoChanges"
> >
<span aria-hidden="true" class="i-material-symbols:undo-rounded" /> <span aria-hidden="true" class="block i-material-symbols:undo-rounded" />
{{ $t('settings.notifications.push_notifications.undo_settings') }} {{ $t('settings.notifications.push_notifications.undo_settings') }}
</button> </button>
</div> </div>
@ -169,7 +172,10 @@ onActivated(() => (busy = false))
:class="busy ? 'border-transparent' : null" :class="busy ? 'border-transparent' : null"
:disabled="busy" :disabled="busy"
> >
<span aria-hidden="true" :class="busy && animateRemoveSubscription ? 'i-ri:loader-2-fill animate-spin' : 'i-material-symbols:cancel-rounded'" /> <span v-if="busy && animateRemoveSubscription" aria-hidden="true" block animate-spin preserve-3d>
<span block i-ri:loader-2-fill aria-hidden="true" />
</span>
<span v-else block aria-hidden="true" i-material-symbols:cancel-rounded />
{{ $t('settings.notifications.push_notifications.unsubscribe') }} {{ $t('settings.notifications.push_notifications.unsubscribe') }}
</button> </button>
</form> </form>

View file

@ -1,6 +1,6 @@
<script setup> <script setup>
const disabled = computed(() => !isMastoInitialised.value || !currentUser.value) const disabled = computed(() => !isHydrated.value || !currentUser.value)
const disabledVisual = computed(() => isMastoInitialised.value && !currentUser.value) const disabledVisual = computed(() => isHydrated.value && !currentUser.value)
</script> </script>
<template> <template>

View file

@ -44,7 +44,7 @@ const hideEmojiPicker = () => {
</script> </script>
<template> <template>
<CommonTooltip content="Add emojis"> <CommonTooltip :content="$t('tooltip.add_emojis')">
<VDropdown <VDropdown
auto-boundary-max-size auto-boundary-max-size
@apply-show="openEmojiPicker()" @apply-show="openEmojiPicker()"

View file

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { EditorContent } from '@tiptap/vue-3' import { EditorContent } from '@tiptap/vue-3'
import type { mastodon } from 'masto' import type { mastodon } from 'masto'
import type { Ref } from 'vue'
import type { Draft } from '~/types' import type { Draft } from '~/types'
const { const {
@ -90,6 +89,19 @@ async function publish() {
emit('published', status) emit('published', status)
} }
useWebShareTarget(async ({ data: { data, action } }: any) => {
if (action !== 'compose-with-shared-data')
return
editor.value?.commands.focus('end')
if (data.text !== undefined)
editor.value?.commands.insertContent(data.text)
if (data.files !== undefined)
await uploadAttachments(data.files)
})
defineExpose({ defineExpose({
focusEditor: () => { focusEditor: () => {
editor.value?.commands?.focus?.() editor.value?.commands?.focus?.()
@ -98,7 +110,7 @@ defineExpose({
</script> </script>
<template> <template>
<div v-if="isMastoInitialised && currentUser" flex="~ col gap-4" py3 px2 sm:px4> <div v-if="isHydrated && currentUser" flex="~ col gap-4" py3 px2 sm:px4>
<template v-if="draft.editingStatus"> <template v-if="draft.editingStatus">
<div flex="~ col gap-1"> <div flex="~ col gap-1">
<div id="state-editing" text-secondary self-center> <div id="state-editing" text-secondary self-center>
@ -145,7 +157,9 @@ defineExpose({
</div> </div>
<div v-if="isUploading" flex gap-1 items-center text-sm p1 text-primary> <div v-if="isUploading" flex gap-1 items-center text-sm p1 text-primary>
<div i-ri:loader-2-fill animate-spin /> <div animate-spin preserve-3d>
<div i-ri:loader-2-fill />
</div>
{{ $t('state.uploading') }} {{ $t('state.uploading') }}
</div> </div>
<div <div
@ -199,7 +213,7 @@ defineExpose({
<div flex gap-4> <div flex gap-4>
<div w-12 h-full sm:block hidden /> <div w-12 h-full sm:block hidden />
<div <div
v-if="shouldExpanded" flex="~ gap-1 1 wrap" m="s--1" pt-2 justify="between" max-w-full v-if="shouldExpanded" flex="~ gap-1 1 wrap" m="s--1" pt-2 justify="end" max-w-full
border="t base" border="t base"
> >
<PublishEmojiPicker <PublishEmojiPicker
@ -274,7 +288,9 @@ defineExpose({
aria-describedby="publish-tooltip" aria-describedby="publish-tooltip"
@click="publish" @click="publish"
> >
<div v-if="isSending" i-ri:loader-2-fill animate-spin /> <span v-if="isSending" block animate-spin preserve-3d>
<div block i-ri:loader-2-fill />
</span>
<span v-if="draft.editingStatus">{{ $t('action.save_changes') }}</span> <span v-if="draft.editingStatus">{{ $t('action.save_changes') }}</span>
<span v-else-if="draft.params.inReplyToId">{{ $t('action.reply') }}</span> <span v-else-if="draft.params.inReplyToId">{{ $t('action.reply') }}</span>
<span v-else>{{ !isSending ? $t('action.publish') : $t('state.publishing') }}</span> <span v-else>{{ !isSending ? $t('action.publish') : $t('state.publishing') }}</span>

View file

@ -66,6 +66,7 @@ const activate = () => {
bg-transparent bg-transparent
outline="focus:none" outline="focus:none"
pe-4 pe-4
select-none
:placeholder="isHydrated ? t('nav.search') : ''" :placeholder="isHydrated ? t('nav.search') : ''"
pb="1px" pb="1px"
placeholder-text-secondary placeholder-text-secondary

View file

@ -9,9 +9,9 @@ function setColorMode(mode: ColorMode) {
</script> </script>
<template> <template>
<div flex="~ gap4" w-full> <div flex="~ gap4 wrap" w-full>
<button <button
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base ws-nowrap
:tabindex="colorMode.preference === 'dark' ? 0 : -1" :tabindex="colorMode.preference === 'dark' ? 0 : -1"
:class="colorMode.preference === 'dark' ? 'pointer-events-none' : 'filter-saturate-0'" :class="colorMode.preference === 'dark' ? 'pointer-events-none' : 'filter-saturate-0'"
@click="setColorMode('dark')" @click="setColorMode('dark')"
@ -20,7 +20,7 @@ function setColorMode(mode: ColorMode) {
{{ $t('settings.interface.dark_mode') }} {{ $t('settings.interface.dark_mode') }}
</button> </button>
<button <button
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base ws-nowrap
:tabindex="colorMode.preference === 'light' ? 0 : -1" :tabindex="colorMode.preference === 'light' ? 0 : -1"
:class="colorMode.preference === 'light' ? 'pointer-events-none' : 'filter-saturate-0'" :class="colorMode.preference === 'light' ? 'pointer-events-none' : 'filter-saturate-0'"
@click="setColorMode('light')" @click="setColorMode('light')"
@ -29,7 +29,7 @@ function setColorMode(mode: ColorMode) {
{{ $t('settings.interface.light_mode') }} {{ $t('settings.interface.light_mode') }}
</button> </button>
<button <button
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base ws-nowrap
:tabindex="colorMode.preference === 'system' ? 0 : -1" :tabindex="colorMode.preference === 'system' ? 0 : -1"
:class="colorMode.preference === 'system' ? 'pointer-events-none' : 'filter-saturate-0'" :class="colorMode.preference === 'system' ? 'pointer-events-none' : 'filter-saturate-0'"
@click="setColorMode('system')" @click="setColorMode('system')"

View file

@ -49,7 +49,7 @@ useCommand({
<component <component
:is="as" :is="as"
v-bind="$attrs" ref="el" v-bind="$attrs" ref="el"
w-fit flex gap-1 items-center w-fit flex gap-1 items-center transition-all
rounded group rounded group
:hover=" !disabled ? hover : undefined" :hover=" !disabled ? hover : undefined"
focus:outline-none focus:outline-none

View file

@ -55,7 +55,7 @@ const reply = () => {
<div flex-1> <div flex-1>
<StatusActionButton <StatusActionButton
:content="$t('action.boost')" :content="$t('action.boost')"
:text="!getWellnessSetting(userSettings, 'hideBoostCount') && status.reblogsCount ? status.reblogsCount : ''" :text="!getPreferences(userSettings, 'hideBoostCount') && status.reblogsCount ? status.reblogsCount : ''"
color="text-green" hover="text-green" group-hover="bg-green/10" color="text-green" hover="text-green" group-hover="bg-green/10"
icon="i-ri:repeat-line" icon="i-ri:repeat-line"
active-icon="i-ri:repeat-fill" active-icon="i-ri:repeat-fill"
@ -64,7 +64,7 @@ const reply = () => {
:command="command" :command="command"
@click="toggleReblog()" @click="toggleReblog()"
> >
<template v-if="status.reblogsCount && !getWellnessSetting(userSettings, 'hideBoostCount')" #text> <template v-if="status.reblogsCount && !getPreferences(userSettings, 'hideBoostCount')" #text>
<CommonLocalizedNumber <CommonLocalizedNumber
keypath="action.boost_count" keypath="action.boost_count"
:count="status.reblogsCount" :count="status.reblogsCount"
@ -76,7 +76,7 @@ const reply = () => {
<div flex-1> <div flex-1>
<StatusActionButton <StatusActionButton
:content="$t('action.favourite')" :content="$t('action.favourite')"
:text="!getWellnessSetting(userSettings, 'hideFavoriteCount') && status.favouritesCount ? status.favouritesCount : ''" :text="!getPreferences(userSettings, 'hideFavoriteCount') && status.favouritesCount ? status.favouritesCount : ''"
color="text-rose" hover="text-rose" group-hover="bg-rose/10" color="text-rose" hover="text-rose" group-hover="bg-rose/10"
icon="i-ri:heart-3-line" icon="i-ri:heart-3-line"
active-icon="i-ri:heart-3-fill" active-icon="i-ri:heart-3-fill"
@ -85,7 +85,7 @@ const reply = () => {
:command="command" :command="command"
@click="toggleFavourite()" @click="toggleFavourite()"
> >
<template v-if="status.favouritesCount && !getWellnessSetting(userSettings, 'hideFavoriteCount')" #text> <template v-if="status.favouritesCount && !getPreferences(userSettings, 'hideFavoriteCount')" #text>
<CommonLocalizedNumber <CommonLocalizedNumber
keypath="action.favourite_count" keypath="action.favourite_count"
:count="status.favouritesCount" :count="status.favouritesCount"

View file

@ -39,7 +39,7 @@ const toggleTranslation = async () => {
isLoading.translation = false isLoading.translation = false
} }
const masto = useMasto() const { client } = $(useMasto())
const getPermalinkUrl = (status: mastodon.v1.Status) => { const getPermalinkUrl = (status: mastodon.v1.Status) => {
const url = getStatusPermalinkRoute(status) const url = getStatusPermalinkRoute(status)
@ -70,7 +70,7 @@ const deleteStatus = async () => {
return return
removeCachedStatus(status.id) removeCachedStatus(status.id)
await masto.v1.statuses.remove(status.id) await client.v1.statuses.remove(status.id)
if (route.name === 'status') if (route.name === 'status')
router.back() router.back()
@ -88,7 +88,7 @@ const deleteAndRedraft = async () => {
} }
removeCachedStatus(status.id) removeCachedStatus(status.id)
await masto.v1.statuses.remove(status.id) await client.v1.statuses.remove(status.id)
await openPublishDialog('dialog', await getDraftFromStatus(status), true) await openPublishDialog('dialog', await getDraftFromStatus(status), true)
// Go to the new status, if the page is the old status // Go to the new status, if the page is the old status
@ -214,7 +214,7 @@ const showFavoritedAndBoostedBy = () => {
@click="toggleTranslation" @click="toggleTranslation"
/> />
<template v-if="isMastoInitialised && currentUser"> <template v-if="isHydrated && currentUser">
<template v-if="isAuthor"> <template v-if="isAuthor">
<CommonDropdownItem <CommonDropdownItem
:text="status.pinned ? $t('menu.unpin_on_profile') : $t('menu.pin_on_profile')" :text="status.pinned ? $t('menu.unpin_on_profile') : $t('menu.pin_on_profile')"

View file

@ -188,7 +188,7 @@ useIntersectionObserver(video, (entries) => {
{{ $t('status.img_alt.dismiss') }} {{ $t('status.img_alt.dismiss') }}
</button> </button>
</div> </div>
<p> <p whitespace-pre-wrap>
{{ attachment.description }} {{ attachment.description }}
</p> </p>
</div> </div>

View file

@ -9,6 +9,8 @@ const props = withDefaults(defineProps<{
actions: true, actions: true,
}) })
const userSettings = useUserSettings()
const status = $computed(() => { const status = $computed(() => {
if (props.status.reblog && props.status.reblog) if (props.status.reblog && props.status.reblog)
return props.status.reblog return props.status.reblog
@ -59,7 +61,7 @@ const isDM = $computed(() => status.visibility === 'direct')
{{ status.application?.name }} {{ status.application?.name }}
</div> </div>
</div> </div>
<div border="t base" pt-2> <div border="t base" py-2>
<StatusActions v-if="actions" :status="status" details :command="command" /> <StatusActions v-if="actions" :status="status" details :command="command" />
</div> </div>
</div> </div>

View file

@ -3,8 +3,10 @@ import { favouritedBoostedByStatusId } from '~/composables/dialog'
const type = ref<'favourited-by' | 'boosted-by'>('favourited-by') const type = ref<'favourited-by' | 'boosted-by'>('favourited-by')
const { client } = $(useMasto())
function load() { function load() {
return useMasto().v1.statuses[type.value === 'favourited-by' ? 'listFavouritedBy' : 'listRebloggedBy'](favouritedBoostedByStatusId.value!) return client.v1.statuses[type.value === 'favourited-by' ? 'listFavouritedBy' : 'listRebloggedBy'](favouritedBoostedByStatusId.value!)
} }
const paginator = $computed(() => load()) const paginator = $computed(() => load())

View file

@ -15,7 +15,8 @@ const expiredTimeAgo = useTimeAgo(poll.expiresAt!, timeAgoOptions)
const expiredTimeFormatted = useFormattedDateTime(poll.expiresAt!) const expiredTimeFormatted = useFormattedDateTime(poll.expiresAt!)
const { formatPercentage } = useHumanReadableNumber() const { formatPercentage } = useHumanReadableNumber()
const masto = useMasto() const { client } = $(useMasto())
async function vote(e: Event) { async function vote(e: Event) {
const formData = new FormData(e.target as HTMLFormElement) const formData = new FormData(e.target as HTMLFormElement)
const choices = formData.getAll('choices') as string[] const choices = formData.getAll('choices') as string[]
@ -30,25 +31,29 @@ async function vote(e: Event) {
poll.votersCount = (poll.votersCount || 0) + 1 poll.votersCount = (poll.votersCount || 0) + 1
cacheStatus({ ...status, poll }, undefined, true) cacheStatus({ ...status, poll }, undefined, true)
await masto.v1.polls.vote(poll.id, { choices }) await client.v1.polls.vote(poll.id, { choices })
} }
const votersCount = $computed(() => poll.votersCount ?? 0) const votersCount = $computed(() => poll.votersCount ?? 0)
</script> </script>
<template> <template>
<div flex flex-col w-full items-stretch gap-3 dir="auto"> <div flex flex-col w-full items-stretch gap-2 py3 dir="auto">
<form v-if="!poll.voted && !poll.expired" flex flex-col gap-4 accent-primary @click.stop="noop" @submit.prevent="vote"> <form v-if="!poll.voted && !poll.expired" flex="~ col gap3" accent-primary @click.stop="noop" @submit.prevent="vote">
<label v-for="(option, index) of poll.options" :key="index" flex items-center gap-2 px-2> <label v-for="(option, index) of poll.options" :key="index" flex="~ gap2" items-center>
<input name="choices" :value="index" :type="poll.multiple ? 'checkbox' : 'radio'"> <input name="choices" :value="index" :type="poll.multiple ? 'checkbox' : 'radio'">
{{ option.title }} {{ option.title }}
</label> </label>
<button btn-solid> <button btn-solid mt-1>
{{ $t('action.vote') }} {{ $t('action.vote') }}
</button> </button>
</form> </form>
<template v-else> <template v-else>
<div v-for="(option, index) of poll.options" :key="index" py-1 relative :style="{ '--bar-width': toPercentage((option.votesCount || 0) / poll.votesCount) }"> <div
v-for="(option, index) of poll.options"
:key="index" py-1 relative
:style="{ '--bar-width': toPercentage((option.votesCount || 0) / poll.votesCount) }"
>
<div flex justify-between pb-2 w-full> <div flex justify-between pb-2 w-full>
<span inline-flex align-items> <span inline-flex align-items>
{{ option.title }} {{ option.title }}
@ -61,7 +66,7 @@ const votersCount = $computed(() => poll.votersCount ?? 0)
</div> </div>
</div> </div>
</template> </template>
<div text-sm flex="~ inline" gap-x-1> <div text-sm flex="~ inline" gap-x-1 text-secondary>
<CommonLocalizedNumber <CommonLocalizedNumber
keypath="status.poll.count" keypath="status.poll.count"
:count="poll.votesCount" :count="poll.votesCount"

View file

@ -21,7 +21,7 @@ const isSquare = $computed(() => (
)) ))
const providerName = $computed(() => props.card.providerName ? props.card.providerName : new URL(props.card.url).hostname) const providerName = $computed(() => props.card.providerName ? props.card.providerName : new URL(props.card.url).hostname)
const gitHubCards = $(useFeatureFlag('experimentalGitHubCards')) const gitHubCards = $(usePreferences('experimentalGitHubCards'))
// TODO: handle card.type: 'photo' | 'video' | 'rich'; // TODO: handle card.type: 'photo' | 'video' | 'rich';
const cardTypeIconMap: Record<mastodon.v1.PreviewCardType, string> = { const cardTypeIconMap: Record<mastodon.v1.PreviewCardType, string> = {

View file

@ -6,7 +6,7 @@ const { status } = defineProps<{
status: mastodon.v1.Status status: mastodon.v1.Status
}>() }>()
const paginator = useMasto().v1.statuses.listHistory(status.id) const paginator = useMastoClient().v1.statuses.listHistory(status.id)
const showHistory = (edit: mastodon.v1.StatusEdit) => { const showHistory = (edit: mastodon.v1.StatusEdit) => {
openEditHistoryDialog(edit) openEditHistoryDialog(edit)

View file

@ -9,13 +9,13 @@ const emit = defineEmits<{
(event: 'change'): void (event: 'change'): void
}>() }>()
const masto = useMasto() const { client } = $(useMasto())
const toggleFollowTag = async () => { const toggleFollowTag = async () => {
if (tag.following) if (tag.following)
await masto.v1.tags.unfollow(tag.name) await client.v1.tags.unfollow(tag.name)
else else
await masto.v1.tags.follow(tag.name) await client.v1.tags.follow(tag.name)
emit('change') emit('change')
} }

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().v1.blocks.list() const paginator = useMastoClient().v1.blocks.list()
</script> </script>
<template> <template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().v1.bookmarks.list() const paginator = useMastoClient().v1.bookmarks.list()
</script> </script>
<template> <template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().v1.conversations.list() const paginator = useMastoClient().v1.conversations.list()
</script> </script>
<template> <template>

View file

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
const masto = useMasto() const { client } = $(useMasto())
const paginator = masto.v1.domainBlocks.list() const paginator = client.v1.domainBlocks.list()
const unblock = async (domain: string) => { const unblock = async (domain: string) => {
await masto.v1.domainBlocks.unblock(domain) await client.v1.domainBlocks.unblock(domain)
} }
</script> </script>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().v1.favourites.list() const paginator = useMastoClient().v1.favourites.list()
</script> </script>
<template> <template>

View file

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().v1.timelines.listHome({ limit: 30 }) const paginator = useMastoClient().v1.timelines.listHome({ limit: 30 })
const stream = useMasto().v1.stream.streamUser() const stream = $(useStreaming(client => client.v1.stream.streamUser()))
onBeforeUnmount(() => stream?.then(s => s.disconnect()))
</script> </script>
<template> <template>

View file

@ -1,11 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
// Default limit is 20 notifications, and servers are normally caped to 30 // Default limit is 20 notifications, and servers are normally caped to 30
const paginator = useMasto().v1.notifications.list({ limit: 30, types: ['mention'] }) const paginator = useMastoClient().v1.notifications.list({ limit: 30, types: ['mention'] })
const stream = $(useStreaming(client => client.v1.stream.streamUser()))
const { clearNotifications } = useNotifications() const { clearNotifications } = useNotifications()
onActivated(clearNotifications) onActivated(clearNotifications)
const stream = useMasto().v1.stream.streamUser()
</script> </script>
<template> <template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().v1.mutes.list() const paginator = useMastoClient().v1.mutes.list()
</script> </script>
<template> <template>

View file

@ -1,11 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
// Default limit is 20 notifications, and servers are normally caped to 30 // Default limit is 20 notifications, and servers are normally caped to 30
const paginator = useMasto().v1.notifications.list({ limit: 30 }) const paginator = useMastoClient().v1.notifications.list({ limit: 30 })
const stream = useStreaming(client => client.v1.stream.streamUser())
const { clearNotifications } = useNotifications() const { clearNotifications } = useNotifications()
onActivated(clearNotifications) onActivated(clearNotifications)
const stream = useMasto().v1.stream.streamUser()
</script> </script>
<template> <template>

View file

@ -14,7 +14,7 @@ const { paginator, stream, account, buffer = 10 } = defineProps<{
}>() }>()
const { formatNumber } = useHumanReadableNumber() const { formatNumber } = useHumanReadableNumber()
const virtualScroller = $(useFeatureFlag('experimentalVirtualScroller')) const virtualScroller = $(usePreferences('experimentalVirtualScroller'))
const showOriginSite = $computed(() => const showOriginSite = $computed(() =>
account && account.id !== currentUser.value?.account.id && getServerName(account) !== currentServer.value, account && account.id !== currentUser.value?.account.id && getServerName(account) !== currentServer.value,

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().v1.accounts.listStatuses(currentUser.value!.account.id, { pinned: true }) const paginator = useMastoClient().v1.accounts.listStatuses(currentUser.value!.account.id, { pinned: true })
</script> </script>
<template> <template>

View file

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().v1.timelines.listPublic({ limit: 30 }) const paginator = useMastoClient().v1.timelines.listPublic({ limit: 30 })
const stream = useMasto().v1.stream.streamPublicTimeline() const stream = useStreaming(client => client.v1.stream.streamPublicTimeline())
onBeforeUnmount(() => stream.then(s => s.disconnect()))
</script> </script>
<template> <template>

View file

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().v1.timelines.listPublic({ limit: 30, local: true }) const paginator = useMastoClient().v1.timelines.listPublic({ limit: 30, local: true })
const stream = useMasto().v1.stream.streamCommunityTimeline() const stream = useStreaming(client => client.v1.stream.streamCommunityTimeline())
onBeforeUnmount(() => stream.then(s => s.disconnect()))
</script> </script>
<template> <template>

View file

@ -46,7 +46,9 @@ defineExpose({
<div v-if="isPending || items.length" relative bg-base text-base shadow border="~ base rounded" text-sm py-2 overflow-x-hidden overflow-y-auto max-h-100> <div v-if="isPending || items.length" relative bg-base text-base shadow border="~ base rounded" text-sm py-2 overflow-x-hidden overflow-y-auto max-h-100>
<template v-if="isPending"> <template v-if="isPending">
<div flex gap-1 items-center p2 animate-pulse> <div flex gap-1 items-center p2 animate-pulse>
<div i-ri:loader-2-line animate-spin /> <div animate-spin preserve-3d>
<div i-ri:loader-2-line />
</div>
<span>Fetching...</span> <span>Fetching...</span>
</div> </div>
</template> </template>

View file

@ -46,7 +46,9 @@ defineExpose({
<div v-if="isPending || items.length" relative bg-base text-base shadow border="~ base rounded" text-sm py-2 overflow-x-hidden overflow-y-auto max-h-100> <div v-if="isPending || items.length" relative bg-base text-base shadow border="~ base rounded" text-sm py-2 overflow-x-hidden overflow-y-auto max-h-100>
<template v-if="isPending"> <template v-if="isPending">
<div flex gap-1 items-center p2 animate-pulse> <div flex gap-1 items-center p2 animate-pulse>
<div i-ri:loader-2-line animate-spin /> <div animate-spin preserve-3d>
<div i-ri:loader-2-line />
</div>
<span>Fetching...</span> <span>Fetching...</span>
</div> </div>
</template> </template>

View file

@ -1,5 +1,5 @@
<template> <template>
<VDropdown :distance="0" placement="top-start"> <VDropdown :distance="0" placement="top-start" strategy="fixed">
<button btn-action-icon :aria-label="$t('action.switch_account')"> <button btn-action-icon :aria-label="$t('action.switch_account')">
<div :class="{ 'hidden xl:block': currentUser }" i-ri:more-2-line /> <div :class="{ 'hidden xl:block': currentUser }" i-ri:more-2-line />
<AccountAvatar v-if="currentUser" xl:hidden :account="currentUser.account" w-9 h-9 square /> <AccountAvatar v-if="currentUser" xl:hidden :account="currentUser.account" w-9 h-9 square />

View file

@ -2,14 +2,13 @@
import type { UserLogin } from '~/types' import type { UserLogin } from '~/types'
const all = useUsers() const all = useUsers()
const router = useRouter() const router = useRouter()
const masto = useMasto()
const switchUser = (user: UserLogin) => { const clickUser = (user: UserLogin) => {
if (user.account.id === currentUser.value?.account.id) if (user.account.id === currentUser.value?.account.id)
router.push(getAccountRoute(user.account)) router.push(getAccountRoute(user.account))
else else
masto.loginTo(user) switchUser(user)
} }
</script> </script>
@ -24,7 +23,7 @@ const switchUser = (user: UserLogin) => {
aria-label="Switch user" aria-label="Switch user"
:class="user.account.id === currentUser?.account.id ? '' : 'op25 grayscale'" :class="user.account.id === currentUser?.account.id ? '' : 'op25 grayscale'"
hover="filter-none op100" hover="filter-none op100"
@click="switchUser(user)" @click="clickUser(user)"
> >
<AccountAvatar w-13 h-13 :account="user.account" square /> <AccountAvatar w-13 h-13 :account="user.account" square />
</button> </button>

View file

@ -11,6 +11,9 @@ let knownServers = $ref<string[]>([])
let autocompleteIndex = $ref(0) let autocompleteIndex = $ref(0)
let autocompleteShow = $ref(false) let autocompleteShow = $ref(false)
const users = useUsers()
const userSettings = useUserSettings()
async function oauth() { async function oauth() {
if (busy) if (busy)
return return
@ -25,12 +28,15 @@ async function oauth() {
server = server.split('/')[0] server = server.split('/')[0]
try { try {
location.href = await (globalThis.$fetch as any)(`/api/${server || publicServer.value}/login`, { const url = await (globalThis.$fetch as any)(`/api/${server || publicServer.value}/login`, {
method: 'POST', method: 'POST',
body: { body: {
force_login: users.value.some(u => u.server === server),
origin: location.origin, origin: location.origin,
lang: userSettings.value.language,
}, },
}) })
location.href = url
} }
catch (err) { catch (err) {
console.error(err) console.error(err)
@ -208,7 +214,10 @@ onClickOutside($$(input), () => {
</span> </span>
</div> </div>
<button flex="~ row" gap-x-2 items-center btn-solid mt2 :disabled="!server || busy"> <button flex="~ row" gap-x-2 items-center btn-solid mt2 :disabled="!server || busy">
<span aria-hidden="true" inline-block :class="busy ? 'i-ri:loader-2-fill animate animate-spin' : 'i-ri:login-circle-line'" class="rtl-flip" /> <span v-if="busy" aria-hidden="true" block animate animate-spin preserve-3d class="rtl-flip">
<span block i-ri:loader-2-fill aria-hidden="true" />
</span>
<span v-else aria-hidden="true" block i-ri:login-circle-line class="rtl-flip" />
{{ $t('action.sign_in') }} {{ $t('action.sign_in') }}
</button> </button>
</form> </form>

View file

@ -1,6 +1,6 @@
<template> <template>
<div p8 lg:flex="~ col gap2" hidden> <div p8 lg:flex="~ col gap2" hidden>
<p v-if="isMastoInitialised" text-sm> <p v-if="isHydrated" text-sm>
<i18n-t keypath="user.sign_in_notice_title"> <i18n-t keypath="user.sign_in_notice_title">
<strong>{{ currentServer }}</strong> <strong>{{ currentServer }}</strong>
</i18n-t> </i18n-t>
@ -8,7 +8,7 @@
<p text-sm text-secondary> <p text-sm text-secondary>
{{ $t('user.sign_in_desc') }} {{ $t('user.sign_in_desc') }}
</p> </p>
<button btn-solid rounded-3 text-center mt-2 @click="openSigninDialog()"> <button btn-solid rounded-3 text-center mt-2 select-none @click="openSigninDialog()">
{{ $t('action.sign_in') }} {{ $t('action.sign_in') }}
</button> </button>
</div> </div>

View file

@ -15,12 +15,11 @@ const sorted = computed(() => {
}) })
const router = useRouter() const router = useRouter()
const masto = useMasto() const clickUser = (user: UserLogin) => {
const switchUser = (user: UserLogin) => {
if (user.account.id === currentUser.value?.account.id) if (user.account.id === currentUser.value?.account.id)
router.push(getAccountRoute(user.account)) router.push(getAccountRoute(user.account))
else else
masto.loginTo(user) switchUser(user)
} }
</script> </script>
@ -31,7 +30,7 @@ const switchUser = (user: UserLogin) => {
flex rounded px4 py3 text-left flex rounded px4 py3 text-left
hover:bg-active cursor-pointer transition-100 hover:bg-active cursor-pointer transition-100
aria-label="Switch user" aria-label="Switch user"
@click="switchUser(user)" @click="clickUser(user)"
> >
<AccountInfo :account="user.account" :hover-card="false" square /> <AccountInfo :account="user.account" :hover-card="false" square />
<div flex-auto /> <div flex-auto />
@ -45,7 +44,7 @@ const switchUser = (user: UserLogin) => {
@click="openSigninDialog" @click="openSigninDialog"
/> />
<CommonDropdownItem <CommonDropdownItem
v-if="isMastoInitialised && currentUser" v-if="isHydrated && currentUser"
:text="$t('user.sign_out_account', [getFullHandle(currentUser.account)])" :text="$t('user.sign_out_account', [getFullHandle(currentUser.account)])"
icon="i-ri:logout-box-line rtl-flip" icon="i-ri:logout-box-line rtl-flip"
@click="signout" @click="signout"

View file

@ -19,11 +19,12 @@ function removeCached(key: string) {
export function fetchStatus(id: string, force = false): Promise<mastodon.v1.Status> { export function fetchStatus(id: string, force = false): Promise<mastodon.v1.Status> {
const server = currentServer.value const server = currentServer.value
const key = `${server}:status:${id}` const userId = currentUser.value?.account.id
const key = `${server}:${userId}:status:${id}`
const cached = cache.get(key) const cached = cache.get(key)
if (cached && !force) if (cached && !force)
return cached return cached
const promise = useMasto().v1.statuses.fetch(id) const promise = useMastoClient().v1.statuses.fetch(id)
.then((status) => { .then((status) => {
cacheStatus(status) cacheStatus(status)
return status return status
@ -37,12 +38,13 @@ export function fetchAccountById(id?: string | null): Promise<mastodon.v1.Accoun
return Promise.resolve(null) return Promise.resolve(null)
const server = currentServer.value const server = currentServer.value
const key = `${server}:account:${id}` const userId = currentUser.value?.account.id
const key = `${server}:${userId}:account:${id}`
const cached = cache.get(key) const cached = cache.get(key)
if (cached) if (cached)
return cached return cached
const domain = currentInstance.value?.uri const domain = currentInstance.value ? getInstanceDomain(currentInstance.value) : null
const promise = useMasto().v1.accounts.fetch(id) const promise = useMastoClient().v1.accounts.fetch(id)
.then((r) => { .then((r) => {
if (r.acct && !r.acct.includes('@') && domain) if (r.acct && !r.acct.includes('@') && domain)
r.acct = `${r.acct}@${domain}` r.acct = `${r.acct}@${domain}`
@ -56,16 +58,28 @@ export function fetchAccountById(id?: string | null): Promise<mastodon.v1.Accoun
export async function fetchAccountByHandle(acct: string): Promise<mastodon.v1.Account> { export async function fetchAccountByHandle(acct: string): Promise<mastodon.v1.Account> {
const server = currentServer.value const server = currentServer.value
const key = `${server}:account:${acct}` const userId = currentUser.value?.account.id
const key = `${server}:${userId}:account:${acct}`
const cached = cache.get(key) const cached = cache.get(key)
if (cached) if (cached)
return cached return cached
const domain = currentInstance.value?.uri const domain = currentInstance.value ? getInstanceDomain(currentInstance.value) : undefined
const account = useMasto().v1.accounts.lookup({ acct })
.then((r) => {
if (r.acct && !r.acct.includes('@') && domain)
r.acct = `${r.acct}@${domain}`
async function lookupAccount() {
const client = useMastoClient()
let account: mastodon.v1.Account
if (!isGotoSocial.value)
account = await client.v1.accounts.lookup({ acct })
else
account = (await client.v1.search({ q: `@${acct}`, type: 'accounts' })).accounts[0]
if (account.acct && !account.acct.includes('@') && domain)
account.acct = `${account.acct}@${domain}`
return account
}
const account = lookupAccount()
.then((r) => {
cacheAccount(r, server, true) cacheAccount(r, server, true)
return r return r
}) })
@ -82,14 +96,17 @@ export function useAccountById(id?: string | null) {
} }
export function cacheStatus(status: mastodon.v1.Status, server = currentServer.value, override?: boolean) { export function cacheStatus(status: mastodon.v1.Status, server = currentServer.value, override?: boolean) {
setCached(`${server}:status:${status.id}`, status, override) const userId = currentUser.value?.account.id
setCached(`${server}:${userId}:status:${status.id}`, status, override)
} }
export function removeCachedStatus(id: string, server = currentServer.value) { export function removeCachedStatus(id: string, server = currentServer.value) {
removeCached(`${server}:status:${id}`) const userId = currentUser.value?.account.id
removeCached(`${server}:${userId}:status:${id}`)
} }
export function cacheAccount(account: mastodon.v1.Account, server = currentServer.value, override?: boolean) { export function cacheAccount(account: mastodon.v1.Account, server = currentServer.value, override?: boolean) {
setCached(`${server}:account:${account.id}`, account, override) const userId = currentUser.value?.account.id
setCached(`${server}:account:${account.acct}`, account, override) setCached(`${server}:${userId}:account:${account.id}`, account, override)
setCached(`${server}:${userId}:account:${account.acct}`, account, override)
} }

View file

@ -337,7 +337,7 @@ export const provideGlobalCommands = () => {
icon: 'i-ri:user-shared-line', icon: 'i-ri:user-shared-line',
onActivate() { onActivate() {
masto.loginTo(user) loginTo(masto, user)
}, },
}))) })))
useCommand({ useCommand({

View file

@ -35,6 +35,12 @@ const sanitizer = sanitize({
code: { code: {
class: filterClasses(/^language-\w+$/), class: filterClasses(/^language-\w+$/),
}, },
// other elements supported in glitch
h1: {},
ol: {},
ul: {},
li: {},
em: {},
}) })
/** /**
@ -163,7 +169,7 @@ export function treeToText(input: Node): string {
if ('children' in input) if ('children' in input)
body = (input.children as Node[]).map(n => treeToText(n)).join('') body = (input.children as Node[]).map(n => treeToText(n)).join('')
if (input.name === 'img') { if (input.name === 'img' || input.name === 'picture') {
if (input.attributes.class?.includes('custom-emoji')) if (input.attributes.class?.includes('custom-emoji'))
return `:${input.attributes['data-emoji-id']}:` return `:${input.attributes['data-emoji-id']}:`
if (input.attributes.class?.includes('iconify-emoji')) if (input.attributes.class?.includes('iconify-emoji'))
@ -320,11 +326,34 @@ function replaceCustomEmoji(customEmojis: Record<string, mastodon.v1.CustomEmoji
if (i % 2 === 0) if (i % 2 === 0)
return name return name
const emoji = customEmojis[name] const emoji = customEmojis[name] as mastodon.v1.CustomEmoji
if (!emoji) if (!emoji)
return `:${name}:` return `:${name}:`
return h('img', { 'src': emoji.url, 'alt': `:${name}:`, 'class': 'custom-emoji', 'data-emoji-id': name }) return h(
'picture',
{
'alt': `:${name}:`,
'class': 'custom-emoji',
'data-emoji-id': name,
},
[
h(
'source',
{
srcset: emoji.staticUrl,
media: '(prefers-reduced-motion: reduce)',
},
),
h(
'img',
{
src: emoji.url,
alt: `:${name}:`,
},
),
],
)
}).filter(Boolean) }).filter(Boolean)
} }
} }

View file

@ -19,8 +19,8 @@ export async function updateCustomEmojis() {
if (Date.now() - currentCustomEmojis.value.lastUpdate < TTL) if (Date.now() - currentCustomEmojis.value.lastUpdate < TTL)
return return
const masto = useMasto() const { client } = $(useMasto())
const emojis = await masto.v1.customEmojis.list() const emojis = await client.v1.customEmojis.list()
Object.assign(currentCustomEmojis.value, { Object.assign(currentCustomEmojis.value, {
lastUpdate: Date.now(), lastUpdate: Date.now(),
emojis, emojis,

View file

@ -3,6 +3,8 @@ import type { Ref } from 'vue'
import { del, get, set, update } from 'idb-keyval' import { del, get, set, update } from 'idb-keyval'
import type { UseIDBOptions } from '@vueuse/integrations/useIDBKeyval' import type { UseIDBOptions } from '@vueuse/integrations/useIDBKeyval'
const isIDBSupported = !process.test && typeof indexedDB !== 'undefined'
export async function useAsyncIDBKeyval<T>( export async function useAsyncIDBKeyval<T>(
key: IDBValidKey, key: IDBValidKey,
initialValue: MaybeComputedRef<T>, initialValue: MaybeComputedRef<T>,
@ -22,6 +24,8 @@ export async function useAsyncIDBKeyval<T>(
const rawInit: T = resolveUnref(initialValue) const rawInit: T = resolveUnref(initialValue)
async function read() { async function read() {
if (!isIDBSupported)
return
try { try {
const rawValue = await get<T>(key) const rawValue = await get<T>(key)
if (rawValue === undefined) { if (rawValue === undefined) {
@ -40,6 +44,8 @@ export async function useAsyncIDBKeyval<T>(
await read() await read()
async function write() { async function write() {
if (!isIDBSupported)
return
try { try {
if (data.value == null) { if (data.value == null) {
await del(key) await del(key)

View file

@ -17,7 +17,7 @@ export function getServerName(account: mastodon.v1.Account) {
if (account.acct?.includes('@')) if (account.acct?.includes('@'))
return account.acct.split('@')[1] return account.acct.split('@')[1]
// We should only lack the server name if we're on the same server as the account // We should only lack the server name if we're on the same server as the account
return currentInstance.value?.uri || '' return currentInstance.value ? getInstanceDomain(currentInstance.value) : ''
} }
export function getFullHandle(account: mastodon.v1.Account) { export function getFullHandle(account: mastodon.v1.Account) {
@ -38,7 +38,7 @@ export function toShortHandle(fullHandle: string) {
export function extractAccountHandle(account: mastodon.v1.Account) { export function extractAccountHandle(account: mastodon.v1.Account) {
let handle = getFullHandle(account).slice(1) let handle = getFullHandle(account).slice(1)
const uri = currentInstance.value?.uri ?? currentServer.value const uri = currentInstance.value ? getInstanceDomain(currentInstance.value) : currentServer.value
if (currentInstance.value && handle.endsWith(`@${uri}`)) if (currentInstance.value && handle.endsWith(`@${uri}`))
handle = handle.slice(0, -uri.length - 1) handle = handle.slice(0, -uri.length - 1)

View file

@ -1,11 +1,115 @@
import type { ElkMasto } from '~/types' import type { Pausable } from '@vueuse/core'
import type { CreateClientParams, WsEvents, mastodon } from 'masto'
import { createClient, fetchV1Instance } from 'masto'
import type { Ref } from 'vue'
import type { ElkInstance } from '../users'
import type { Mutable } from '~/types/utils'
import type { UserLogin } from '~/types'
export const createMasto = () => {
let client = $shallowRef<mastodon.Client>(undefined as never)
let params = $ref<Mutable<CreateClientParams>>()
const canStreaming = $computed(() => !!params?.streamingApiUrl)
const setParams = (newParams: Partial<CreateClientParams>) => {
const p = { ...params, ...newParams } as CreateClientParams
client = createClient(p)
params = p
}
return {
client: $$(client),
params: readonly($$(params)),
canStreaming: $$(canStreaming),
setParams,
}
}
export type ElkMasto = ReturnType<typeof createMasto>
export const useMasto = () => useNuxtApp().$masto as ElkMasto export const useMasto = () => useNuxtApp().$masto as ElkMasto
export const useMastoClient = () => useMasto().client.value
export const isMastoInitialised = computed(() => process.client && useMasto().loggedIn.value) export function mastoLogin(masto: ElkMasto, user: Pick<UserLogin, 'server' | 'token'>) {
const { setParams } = $(masto)
export const onMastoInit = (cb: () => unknown) => { const server = user.server
watchOnce(isMastoInitialised, () => { const url = `https://${server}`
cb() const instance: ElkInstance = reactive(getInstanceCache(server) || { uri: server, accountDomain: server })
}, { immediate: isMastoInitialised.value }) setParams({
url,
accessToken: user?.token,
disableVersionCheck: true,
streamingApiUrl: instance?.urls?.streamingApi,
})
fetchV1Instance({ url }).then((newInstance) => {
Object.assign(instance, newInstance)
setParams({
streamingApiUrl: newInstance.urls.streamingApi,
})
instances.value[server] = newInstance
})
return instance
}
interface UseStreamingOptions<Controls extends boolean> {
/**
* Expose more controls
*
* @default false
*/
controls?: Controls
/**
* Connect on calling
*
* @default true
*/
immediate?: boolean
}
export function useStreaming(
cb: (client: mastodon.Client) => Promise<WsEvents>,
options: UseStreamingOptions<true>,
): { stream: Ref<Promise<WsEvents> | undefined> } & Pausable
export function useStreaming(
cb: (client: mastodon.Client) => Promise<WsEvents>,
options?: UseStreamingOptions<false>,
): Ref<Promise<WsEvents> | undefined>
export function useStreaming(
cb: (client: mastodon.Client) => Promise<WsEvents>,
{ immediate = true, controls }: UseStreamingOptions<boolean> = {},
): ({ stream: Ref<Promise<WsEvents> | undefined> } & Pausable) | Ref<Promise<WsEvents> | undefined> {
const { canStreaming, client } = useMasto()
const isActive = ref(immediate)
const stream = ref<Promise<WsEvents>>()
function pause() {
isActive.value = false
}
function resume() {
isActive.value = true
}
function cleanup() {
if (stream.value) {
stream.value.then(s => s.disconnect()).catch(() => Promise.resolve())
stream.value = undefined
}
}
watchEffect(() => {
cleanup()
if (canStreaming.value && isActive.value)
stream.value = cb(client.value)
})
tryOnBeforeUnmount(() => isActive.value = false)
if (controls)
return { stream, isActive, pause, resume }
else
return stream
} }

View file

@ -4,32 +4,39 @@ const notifications = reactive<Record<string, undefined | [Promise<WsEvents>, st
export const useNotifications = () => { export const useNotifications = () => {
const id = currentUser.value?.account.id const id = currentUser.value?.account.id
const masto = useMasto()
const { client, canStreaming } = $(useMasto())
async function clearNotifications() { async function clearNotifications() {
if (!id || !notifications[id]) if (!id || !notifications[id])
return return
const lastReadId = notifications[id]![1][0] const lastReadId = notifications[id]![1][0]
notifications[id]![1] = [] notifications[id]![1] = []
if (lastReadId) {
await masto.v1.markers.create({ await client.v1.markers.create({
notifications: { lastReadId }, notifications: { lastReadId },
}) })
} }
}
async function connect(): Promise<void> { async function connect(): Promise<void> {
if (!isMastoInitialised.value || !id || notifications[id] || !currentUser.value?.token) if (!isHydrated.value || !id || notifications[id] || !currentUser.value?.token)
return return
const stream = masto.v1.stream.streamUser() let resolveStream
const stream = new Promise<WsEvents>(resolve => resolveStream = resolve)
notifications[id] = [stream, []] notifications[id] = [stream, []]
await until($$(canStreaming)).toBe(true)
client.v1.stream.streamUser().then(resolveStream)
stream.then(s => s.on('notification', (n) => { stream.then(s => s.on('notification', (n) => {
if (notifications[id]) if (notifications[id])
notifications[id]![1].unshift(n.id) notifications[id]![1].unshift(n.id)
})) }))
const position = await masto.v1.markers.fetch({ timeline: ['notifications'] }) const position = await client.v1.markers.fetch({ timeline: ['notifications'] })
const paginator = masto.v1.notifications.list({ limit: 30 }) const paginator = client.v1.notifications.list({ limit: 30 })
do { do {
const result = await paginator.next() const result = await paginator.next()
if (!result.done && result.value.length) { if (!result.done && result.value.length) {
@ -53,10 +60,10 @@ export const useNotifications = () => {
} }
watch(currentUser, disconnect) watch(currentUser, disconnect)
if (isMastoInitialised.value)
onHydrated(() => {
connect() connect()
else })
watchOnce(isMastoInitialised, connect)
return { return {
notifications: computed(() => id ? notifications[id]?.[1].length ?? 0 : 0), notifications: computed(() => id ? notifications[id]?.[1].length ?? 0 : 0),

View file

@ -12,7 +12,7 @@ export const usePublish = (options: {
}) => { }) => {
const { expanded, isUploading, initialDraft } = $(options) const { expanded, isUploading, initialDraft } = $(options)
let { draft, isEmpty } = $(options.draftState) let { draft, isEmpty } = $(options.draftState)
const masto = useMasto() const { client } = $(useMasto())
let isSending = $ref(false) let isSending = $ref(false)
const isExpanded = $ref(false) const isExpanded = $ref(false)
@ -51,9 +51,9 @@ export const usePublish = (options: {
let status: mastodon.v1.Status let status: mastodon.v1.Status
if (!draft.editingStatus) if (!draft.editingStatus)
status = await masto.v1.statuses.create(payload) status = await client.v1.statuses.create(payload)
else else
status = await masto.v1.statuses.update(draft.editingStatus.id, payload) status = await client.v1.statuses.update(draft.editingStatus.id, payload)
if (draft.params.inReplyToId) if (draft.params.inReplyToId)
navigateToStatus({ status }) navigateToStatus({ status })
@ -83,7 +83,7 @@ export type MediaAttachmentUploadError = [filename: string, message: string]
export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => { export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => {
const draft = $(draftRef) const draft = $(draftRef)
const masto = useMasto() const { client } = $(useMasto())
const { t } = useI18n() const { t } = useI18n()
let isUploading = $ref<boolean>(false) let isUploading = $ref<boolean>(false)
@ -96,12 +96,12 @@ export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => {
failedAttachments = [] failedAttachments = []
// TODO: display some kind of message if too many media are selected // TODO: display some kind of message if too many media are selected
// DONE // DONE
const limit = currentInstance.value!.configuration.statuses.maxMediaAttachments || 4 const limit = currentInstance.value!.configuration?.statuses.maxMediaAttachments || 4
for (const file of files.slice(0, limit)) { for (const file of files.slice(0, limit)) {
if (draft.attachments.length < limit) { if (draft.attachments.length < limit) {
isExceedingAttachmentLimit = false isExceedingAttachmentLimit = false
try { try {
const attachment = await masto.v1.mediaAttachments.create({ const attachment = await client.v1.mediaAttachments.create({
file, file,
}) })
draft.attachments.push(attachment) draft.attachments.push(attachment)
@ -121,7 +121,7 @@ export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => {
} }
async function pickAttachments() { async function pickAttachments() {
const mimeTypes = currentInstance.value!.configuration.mediaAttachments.supportedMimeTypes const mimeTypes = currentInstance.value!.configuration?.mediaAttachments.supportedMimeTypes
const files = await fileOpen({ const files = await fileOpen({
description: 'Attachments', description: 'Attachments',
multiple: true, multiple: true,
@ -132,7 +132,7 @@ export const useUploadMediaAttachment = (draftRef: Ref<Draft>) => {
async function setDescription(att: mastodon.v1.MediaAttachment, description: string) { async function setDescription(att: mastodon.v1.MediaAttachment, description: string) {
att.description = description att.description = description
await masto.v1.mediaAttachments.update(att.id, { description: att.description }) await client.v1.mediaAttachments.update(att.id, { description: att.description })
} }
function removeAttachment(index: number) { function removeAttachment(index: number) {

View file

@ -27,7 +27,7 @@ export function useRelationship(account: mastodon.v1.Account): Ref<mastodon.v1.R
async function fetchRelationships() { async function fetchRelationships() {
const requested = Array.from(requestedRelationships.entries()).filter(([, r]) => !r.value) const requested = Array.from(requestedRelationships.entries()).filter(([, r]) => !r.value)
const relationships = await useMasto().v1.accounts.fetchRelationships(requested.map(([id]) => id)) const relationships = await useMastoClient().v1.accounts.fetchRelationships(requested.map(([id]) => id))
for (let i = 0; i < requested.length; i++) for (let i = 0; i < requested.length; i++)
requested[i][1].value = relationships[i] requested[i][1].value = relationships[i]
} }

View file

@ -22,7 +22,7 @@ export type SearchResult = HashTagSearchResult | AccountSearchResult | StatusSea
export function useSearch(query: MaybeComputedRef<string>, options: UseSearchOptions = {}) { export function useSearch(query: MaybeComputedRef<string>, options: UseSearchOptions = {}) {
const done = ref(false) const done = ref(false)
const masto = useMasto() const { client } = $(useMasto())
const loading = ref(false) const loading = ref(false)
const accounts = ref<AccountSearchResult[]>([]) const accounts = ref<AccountSearchResult[]>([])
const hashtags = ref<HashTagSearchResult[]>([]) const hashtags = ref<HashTagSearchResult[]>([])
@ -59,11 +59,11 @@ export function useSearch(query: MaybeComputedRef<string>, options: UseSearchOpt
} }
watch(() => resolveUnref(query), () => { watch(() => resolveUnref(query), () => {
loading.value = !!(q && isMastoInitialised.value) loading.value = !!(q && isHydrated.value)
}) })
debouncedWatch(() => resolveUnref(query), async () => { debouncedWatch(() => resolveUnref(query), async () => {
if (!q || !isMastoInitialised.value) if (!q || !isHydrated.value)
return return
loading.value = true loading.value = true
@ -72,7 +72,7 @@ export function useSearch(query: MaybeComputedRef<string>, options: UseSearchOpt
* Based on the source it seems like modifying the params when calling next would result in a new search, * Based on the source it seems like modifying the params when calling next would result in a new search,
* but that doesn't seem to be the case. So instead we just create a new paginator with the new params. * but that doesn't seem to be the case. So instead we just create a new paginator with the new params.
*/ */
paginator = masto.v2.search({ paginator = client.v2.search({
q, q,
...resolveUnref(options), ...resolveUnref(options),
resolve: !!currentUser.value, resolve: !!currentUser.value,
@ -87,7 +87,7 @@ export function useSearch(query: MaybeComputedRef<string>, options: UseSearchOpt
}, { debounce: 300 }) }, { debounce: 300 })
const next = async () => { const next = async () => {
if (!q || !isMastoInitialised.value || !paginator) if (!q || !isHydrated.value || !paginator)
return return
loading.value = true loading.value = true

View file

@ -9,7 +9,7 @@ export interface StatusActionsProps {
export function useStatusActions(props: StatusActionsProps) { export function useStatusActions(props: StatusActionsProps) {
let status = $ref<mastodon.v1.Status>({ ...props.status }) let status = $ref<mastodon.v1.Status>({ ...props.status })
const masto = useMasto() const { client } = $(useMasto())
watch( watch(
() => props.status, () => props.status,
@ -61,7 +61,7 @@ export function useStatusActions(props: StatusActionsProps) {
const toggleReblog = () => toggleStatusAction( const toggleReblog = () => toggleStatusAction(
'reblogged', 'reblogged',
() => masto.v1.statuses[status.reblogged ? 'unreblog' : 'reblog'](status.id).then((res) => { () => client.v1.statuses[status.reblogged ? 'unreblog' : 'reblog'](status.id).then((res) => {
if (status.reblogged) if (status.reblogged)
// returns the original status // returns the original status
return res.reblog! return res.reblog!
@ -72,23 +72,23 @@ export function useStatusActions(props: StatusActionsProps) {
const toggleFavourite = () => toggleStatusAction( const toggleFavourite = () => toggleStatusAction(
'favourited', 'favourited',
() => masto.v1.statuses[status.favourited ? 'unfavourite' : 'favourite'](status.id), () => client.v1.statuses[status.favourited ? 'unfavourite' : 'favourite'](status.id),
'favouritesCount', 'favouritesCount',
) )
const toggleBookmark = () => toggleStatusAction( const toggleBookmark = () => toggleStatusAction(
'bookmarked', 'bookmarked',
() => masto.v1.statuses[status.bookmarked ? 'unbookmark' : 'bookmark'](status.id), () => client.v1.statuses[status.bookmarked ? 'unbookmark' : 'bookmark'](status.id),
) )
const togglePin = async () => toggleStatusAction( const togglePin = async () => toggleStatusAction(
'pinned', 'pinned',
() => masto.v1.statuses[status.pinned ? 'unpin' : 'pin'](status.id), () => client.v1.statuses[status.pinned ? 'unpin' : 'pin'](status.id),
) )
const toggleMute = async () => toggleStatusAction( const toggleMute = async () => toggleStatusAction(
'muted', 'muted',
() => masto.v1.statuses[status.muted ? 'unmute' : 'mute'](status.id), () => client.v1.statuses[status.muted ? 'unmute' : 'mute'](status.id),
) )
return { return {

View file

@ -4,7 +4,7 @@ import { STORAGE_KEY_DRAFTS } from '~/constants'
import type { Draft, DraftMap } from '~/types' import type { Draft, DraftMap } from '~/types'
import type { Mutable } from '~/types/utils' import type { Mutable } from '~/types/utils'
export const currentUserDrafts = process.server ? computed<DraftMap>(() => ({})) : useUserLocalStorage<DraftMap>(STORAGE_KEY_DRAFTS, () => ({})) export const currentUserDrafts = process.server || process.test ? computed<DraftMap>(() => ({})) : useUserLocalStorage<DraftMap>(STORAGE_KEY_DRAFTS, () => ({}))
export const builtinDraftKeys = [ export const builtinDraftKeys = [
'dialog', 'dialog',

View file

@ -1,9 +1,10 @@
import type { Paginator, WsEvents, mastodon } from 'masto' import type { Paginator, WsEvents, mastodon } from 'masto'
import type { Ref } from 'vue'
import type { PaginatorState } from '~/types' import type { PaginatorState } from '~/types'
export function usePaginator<T, P, U = T>( export function usePaginator<T, P, U = T>(
_paginator: Paginator<T[], P>, _paginator: Paginator<T[], P>,
stream?: Promise<WsEvents>, stream: Ref<Promise<WsEvents> | undefined>,
eventType: 'notification' | 'update' = 'update', eventType: 'notification' | 'update' = 'update',
preprocess: (items: (T | U)[]) => U[] = items => items as unknown as U[], preprocess: (items: (T | U)[]) => U[] = items => items as unknown as U[],
buffer = 10, buffer = 10,
@ -13,7 +14,7 @@ export function usePaginator<T, P, U = T>(
// so clone it // so clone it
const paginator = _paginator.clone() const paginator = _paginator.clone()
const state = ref<PaginatorState>(isMastoInitialised.value ? 'idle' : 'loading') const state = ref<PaginatorState>(isHydrated.value ? 'idle' : 'loading')
const items = ref<U[]>([]) const items = ref<U[]>([])
const nextItems = ref<U[]>([]) const nextItems = ref<U[]>([])
const prevItems = ref<T[]>([]) const prevItems = ref<T[]>([])
@ -29,6 +30,7 @@ export function usePaginator<T, P, U = T>(
prevItems.value = [] prevItems.value = []
} }
watch(stream, (stream) => {
stream?.then((s) => { stream?.then((s) => {
s.on(eventType, (status) => { s.on(eventType, (status) => {
if ('uri' in status) if ('uri' in status)
@ -60,6 +62,7 @@ export function usePaginator<T, P, U = T>(
data.splice(index, 1) data.splice(index, 1)
}) })
}) })
}, { immediate: true })
async function loadNext() { async function loadNext() {
if (state.value !== 'idle') if (state.value !== 'idle')
@ -101,8 +104,8 @@ export function usePaginator<T, P, U = T>(
bound.update() bound.update()
}, 1000) }, 1000)
if (!isMastoInitialised.value) { if (!isHydrated.value) {
onMastoInit(() => { onHydrated(() => {
state.value = 'idle' state.value = 'idle'
loadNext() loadNext()
}) })

View file

@ -132,5 +132,5 @@ async function sendSubscriptionToBackend(
data, data,
} }
return await useMasto().v1.webPushSubscriptions.create(params) return await useMastoClient().v1.webPushSubscriptions.create(params)
} }

View file

@ -13,7 +13,7 @@ const supportsPushNotifications = typeof window !== 'undefined'
&& 'getKey' in PushSubscription.prototype && 'getKey' in PushSubscription.prototype
export const usePushManager = () => { export const usePushManager = () => {
const masto = useMasto() const { client } = $(useMasto())
const isSubscribed = ref(false) const isSubscribed = ref(false)
const notificationPermission = ref<PermissionState | undefined>( const notificationPermission = ref<PermissionState | undefined>(
Notification.permission === 'denied' Notification.permission === 'denied'
@ -168,7 +168,7 @@ export const usePushManager = () => {
if (policyChanged) if (policyChanged)
await subscribe(data, policy, true) await subscribe(data, policy, true)
else else
currentUser.value.pushSubscription = await masto.v1.webPushSubscriptions.update({ data }) currentUser.value.pushSubscription = await client.v1.webPushSubscriptions.update({ data })
policyChanged && await nextTick() policyChanged && await nextTick()

View file

@ -1,45 +1,44 @@
import { DEFAULT_FONT_SIZE, DEFAULT_LANGUAGE } from '~/constants' import { DEFAULT_FONT_SIZE } from '~/constants'
export type FontSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' export type FontSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
export type ColorMode = 'light' | 'dark' | 'system' export type ColorMode = 'light' | 'dark' | 'system'
export interface FeatureFlags { export interface PreferencesSettings {
hideBoostCount: boolean
hideFavoriteCount: boolean
hideFollowerCount: boolean
experimentalVirtualScroller: boolean experimentalVirtualScroller: boolean
experimentalGitHubCards: boolean experimentalGitHubCards: boolean
experimentalUserPicker: boolean experimentalUserPicker: boolean
} }
export interface WellnessSettings {
hideBoostCount: boolean
hideFavoriteCount: boolean
hideFollowerCount: boolean
}
export interface UserSettings { export interface UserSettings {
featureFlags: Partial<FeatureFlags> preferences: Partial<PreferencesSettings>
wellnessSettings: Partial<WellnessSettings>
colorMode?: ColorMode colorMode?: ColorMode
fontSize: FontSize fontSize: FontSize
language: string language: string
zenMode?: boolean zenMode: boolean
} }
export function getDefaultUserSettings(): UserSettings { export function getDefaultLanguage(languages: string[]) {
if (process.server)
return 'en-US'
return matchLanguages(languages, navigator.languages) || 'en-US'
}
export function getDefaultUserSettings(locales: string[]): UserSettings {
return { return {
language: DEFAULT_LANGUAGE, language: getDefaultLanguage(locales),
fontSize: DEFAULT_FONT_SIZE, fontSize: DEFAULT_FONT_SIZE,
featureFlags: {}, zenMode: false,
wellnessSettings: {}, preferences: {},
} }
} }
export const DEFAULT_WELLNESS_SETTINGS: WellnessSettings = { export const DEFAULT__PREFERENCES_SETTINGS: PreferencesSettings = {
hideBoostCount: false, hideBoostCount: false,
hideFavoriteCount: false, hideFavoriteCount: false,
hideFollowerCount: false, hideFollowerCount: false,
}
export const DEFAULT_FEATURE_FLAGS: FeatureFlags = {
experimentalVirtualScroller: true, experimentalVirtualScroller: true,
experimentalGitHubCards: true, experimentalGitHubCards: true,
experimentalUserPicker: true, experimentalUserPicker: true,

View file

@ -1,53 +1,35 @@
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { FeatureFlags, UserSettings, WellnessSettings } from './definition' import type { VueI18n } from 'vue-i18n'
import type { LocaleObject } from 'vue-i18n-routing'
import type { PreferencesSettings, UserSettings } from './definition'
import { STORAGE_KEY_SETTINGS } from '~/constants' import { STORAGE_KEY_SETTINGS } from '~/constants'
export const useUserSettings = () => { export function useUserSettings() {
if (process.server) const i18n = useNuxtApp().vueApp.config.globalProperties.$i18n as VueI18n
return useState('user-settings', getDefaultUserSettings) const { locales } = i18n
return useUserLocalStorage(STORAGE_KEY_SETTINGS, getDefaultUserSettings) const supportLanguages = (locales as LocaleObject[]).map(locale => locale.code)
return useUserLocalStorage<UserSettings>(STORAGE_KEY_SETTINGS, () => getDefaultUserSettings(supportLanguages))
} }
// TODO: refactor & simplify this // TODO: refactor & simplify this
export function useWellnessSetting<T extends keyof WellnessSettings>(name: T): Ref<WellnessSettings[T]> { export function usePreferences<T extends keyof PreferencesSettings>(name: T): Ref<PreferencesSettings[T]> {
const userSettings = useUserSettings() const userSettings = useUserSettings()
return computed({ return computed({
get() { get() {
return getWellnessSetting(userSettings.value, name) return getPreferences(userSettings.value, name)
}, },
set(value) { set(value) {
userSettings.value.wellnessSettings[name] = value userSettings.value.preferences[name] = value
}, },
}) })
} }
export function getWellnessSetting<T extends keyof WellnessSettings>(userSettings: UserSettings, name: T): WellnessSettings[T] { export function getPreferences<T extends keyof PreferencesSettings>(userSettings: UserSettings, name: T): PreferencesSettings[T] {
return userSettings?.wellnessSettings?.[name] ?? DEFAULT_WELLNESS_SETTINGS[name] return userSettings?.preferences?.[name] ?? DEFAULT__PREFERENCES_SETTINGS[name]
} }
export function toggleWellnessSetting(key: keyof WellnessSettings) { export function togglePreferences(key: keyof PreferencesSettings) {
const flag = useWellnessSetting(key) const flag = usePreferences(key)
flag.value = !flag.value
}
export function useFeatureFlag<T extends keyof FeatureFlags>(name: T): Ref<FeatureFlags[T]> {
const userSettings = useUserSettings()
return computed({
get() {
return getFeatureFlag(userSettings.value, name)
},
set(value) {
userSettings.value.featureFlags[name] = value
},
})
}
export function getFeatureFlag<T extends keyof FeatureFlags>(userSettings: UserSettings, name: T): FeatureFlags[T] {
return userSettings?.featureFlags?.[name] ?? DEFAULT_FEATURE_FLAGS[name]
}
export function toggleFeatureFlag(key: keyof FeatureFlags) {
const flag = useFeatureFlag(key)
flag.value = !flag.value flag.value = !flag.value
} }

View file

@ -32,10 +32,11 @@ export function useHightlighter(lang: Lang) {
.then(() => { .then(() => {
registeredLang.value.set(lang, true) registeredLang.value.set(lang, true)
}) })
.catch((e) => { .catch(() => {
console.error(`[shiki] Failed to load language ${lang}`) const fallbackLang = 'md'
console.error(e) shiki.value?.loadLanguage(fallbackLang).then(() => {
registeredLang.value.set(lang, false) registeredLang.value.set(fallbackLang, true)
})
}) })
return undefined return undefined
} }
@ -47,10 +48,22 @@ export function useShikiTheme() {
return useColorMode().value === 'dark' ? 'vitesse-dark' : 'vitesse-light' return useColorMode().value === 'dark' ? 'vitesse-dark' : 'vitesse-light'
} }
const HTML_ENTITIES = {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'\'': '&apos;',
'"': '&quot;',
} as Record<string, string>
function escapeHtml(text: string) {
return text.replace(/[<>&'"]/g, ch => HTML_ENTITIES[ch])
}
export function highlightCode(code: string, lang: Lang) { export function highlightCode(code: string, lang: Lang) {
const shiki = useHightlighter(lang) const shiki = useHightlighter(lang)
if (!shiki) if (!shiki)
return code return escapeHtml(code)
return shiki.codeToHtml(code, { return shiki.codeToHtml(code, {
lang, lang,

View file

@ -43,7 +43,7 @@ function getDecorations({
findChildren(doc, node => node.type.name === name) findChildren(doc, node => node.type.name === name)
.forEach((block) => { .forEach((block) => {
let from = block.pos + 1 let from = block.pos + 1
const language = block.node.attrs.language || 'text' const language = block.node.attrs.language
const shiki = useHightlighter(language) const shiki = useHightlighter(language)

View file

@ -23,7 +23,7 @@ export const MentionSuggestion: Partial<SuggestionOptions> = {
if (query.length === 0) if (query.length === 0)
return [] return []
const results = await useMasto().v2.search({ q: query, type: 'accounts', limit: 25, resolve: true }) const results = await useMastoClient().v2.search({ q: query, type: 'accounts', limit: 25, resolve: true })
return results.accounts return results.accounts
}, },
render: createSuggestionRenderer(TiptapMentionList), render: createSuggestionRenderer(TiptapMentionList),
@ -36,7 +36,7 @@ export const HashtagSuggestion: Partial<SuggestionOptions> = {
if (query.length === 0) if (query.length === 0)
return [] return []
const results = await useMasto().v2.search({ const results = await useMastoClient().v2.search({
q: query, q: query,
type: 'hashtags', type: 'hashtags',
limit: 25, limit: 25,
@ -113,10 +113,13 @@ function createSuggestionRenderer(component: Component): SuggestionOptions['rend
// Use arrow function here because Nuxt will transform it incorrectly as Vue hook causing the build to fail // Use arrow function here because Nuxt will transform it incorrectly as Vue hook causing the build to fail
onBeforeUpdate: (props) => { onBeforeUpdate: (props) => {
renderer.updateProps({ ...props, isPending: true }) props.editor.isFocused && renderer.updateProps({ ...props, isPending: true })
}, },
onUpdate(props) { onUpdate(props) {
if (!props.editor.isFocused)
return
renderer.updateProps({ ...props, isPending: false }) renderer.updateProps({ ...props, isPending: false })
if (!props.clientRect) if (!props.clientRect)

View file

@ -1,11 +1,14 @@
import { createClient, fetchV1Instance } from 'masto'
import type { mastodon } from 'masto' import type { mastodon } from 'masto'
import type { Ref } from 'vue' import type { EffectScope, Ref } from 'vue'
import type { MaybeComputedRef, RemovableRef } from '@vueuse/core' import type { MaybeComputedRef, RemovableRef } from '@vueuse/core'
import type { ElkMasto, UserLogin } from '~/types' import type { ElkMasto } from './masto/masto'
import type { UserLogin } from '~/types'
import type { Overwrite } from '~/types/utils'
import { import {
DEFAULT_POST_CHARS_LIMIT, DEFAULT_POST_CHARS_LIMIT,
STORAGE_KEY_CURRENT_USER, STORAGE_KEY_CURRENT_USER,
STORAGE_KEY_CURRENT_USER_HANDLE,
STORAGE_KEY_NODES,
STORAGE_KEY_NOTIFICATION, STORAGE_KEY_NOTIFICATION,
STORAGE_KEY_NOTIFICATION_POLICY, STORAGE_KEY_NOTIFICATION_POLICY,
STORAGE_KEY_SERVERS, STORAGE_KEY_SERVERS,
@ -40,9 +43,17 @@ const initializeUsers = async (): Promise<Ref<UserLogin[]> | RemovableRef<UserLo
} }
const users = await initializeUsers() const users = await initializeUsers()
const instances = useLocalStorage<Record<string, mastodon.v1.Instance>>(STORAGE_KEY_SERVERS, mock ? mock.server : {}, { deep: true }) export const instances = useLocalStorage<Record<string, mastodon.v1.Instance>>(STORAGE_KEY_SERVERS, mock ? mock.server : {}, { deep: true })
export const nodes = useLocalStorage<Record<string, any>>(STORAGE_KEY_NODES, {}, { deep: true })
const currentUserId = useLocalStorage<string>(STORAGE_KEY_CURRENT_USER, mock ? mock.user.account.id : '') const currentUserId = useLocalStorage<string>(STORAGE_KEY_CURRENT_USER, mock ? mock.user.account.id : '')
export type ElkInstance = Partial<mastodon.v1.Instance> & {
uri: string
/** support GoToSocial */
accountDomain?: string | null
}
export const getInstanceCache = (server: string): mastodon.v1.Instance | undefined => instances.value[server]
export const currentUser = computed<UserLogin | undefined>(() => { export const currentUser = computed<UserLogin | undefined>(() => {
if (currentUserId.value) { if (currentUserId.value) {
const user = users.value.find(user => user.account?.id === currentUserId.value) const user = users.value.find(user => user.account?.id === currentUserId.value)
@ -53,13 +64,20 @@ export const currentUser = computed<UserLogin | undefined>(() => {
return users.value[0] return users.value[0]
}) })
const publicInstance = ref<mastodon.v1.Instance | null>(null) const publicInstance = ref<ElkInstance | null>(null)
export const currentInstance = computed<null | mastodon.v1.Instance>(() => currentUser.value ? instances.value[currentUser.value.server] ?? null : publicInstance.value) export const currentInstance = computed<null | ElkInstance>(() => currentUser.value ? instances.value[currentUser.value.server] ?? null : publicInstance.value)
export const isGlitchEdition = computed(() => currentInstance.value?.version.includes('+glitch'))
export function getInstanceDomain(instance: ElkInstance) {
return instance.accountDomain || instance.uri
}
export const publicServer = ref('') export const publicServer = ref('')
export const currentServer = computed<string>(() => currentUser.value?.server || publicServer.value) export const currentServer = computed<string>(() => currentUser.value?.server || publicServer.value)
export const currentNodeInfo = computed<null | Record<string, any>>(() => nodes.value[currentServer.value] || null)
export const isGotoSocial = computed(() => currentNodeInfo.value?.software?.name === 'gotosocial')
export const isGlitchEdition = computed(() => currentInstance.value?.version?.includes('+glitch'))
// when multiple tabs: we need to reload window when sign in, switch account or sign out // when multiple tabs: we need to reload window when sign in, switch account or sign out
if (process.client) { if (process.client) {
const windowReload = () => { const windowReload = () => {
@ -89,102 +107,81 @@ if (process.client) {
window.addEventListener('visibilitychange', windowReload, { capture: true }) window.addEventListener('visibilitychange', windowReload, { capture: true })
} }
}, { immediate: true, flush: 'post' }) }, { immediate: true, flush: 'post' })
}
export const currentUserHandle = computed(() => currentUser.value?.account.id // for injected script to read
? `${currentUser.value.account.acct}@${currentInstance.value?.uri || currentServer.value}` const currentUserHandle = computed(() => currentUser.value?.account.acct || '')
: '[anonymous]', watchEffect(() => {
) localStorage.setItem(STORAGE_KEY_CURRENT_USER_HANDLE, currentUserHandle.value)
})
}
export const useUsers = () => users export const useUsers = () => users
export const useSelfAccount = (user: MaybeComputedRef<mastodon.v1.Account | undefined>) => export const useSelfAccount = (user: MaybeComputedRef<mastodon.v1.Account | undefined>) =>
computed(() => currentUser.value && resolveUnref(user)?.id === currentUser.value.account.id) computed(() => currentUser.value && resolveUnref(user)?.id === currentUser.value.account.id)
export const characterLimit = computed(() => currentInstance.value?.configuration.statuses.maxCharacters ?? DEFAULT_POST_CHARS_LIMIT) export const characterLimit = computed(() => currentInstance.value?.configuration?.statuses.maxCharacters ?? DEFAULT_POST_CHARS_LIMIT)
async function loginTo(user?: Omit<UserLogin, 'account'> & { account?: mastodon.v1.AccountCredentials }) { export async function loginTo(masto: ElkMasto, user: Overwrite<UserLogin, { account?: mastodon.v1.AccountCredentials }>) {
const route = useRoute() const { client } = $(masto)
const router = useRouter() const instance = mastoLogin(masto, user)
const server = user?.server || route.params.server as string || publicServer.value
const url = `https://${server}` // GoToSocial only API
const instance = await fetchV1Instance({ const url = `https://${user.server}`
url, fetch(`${url}/nodeinfo/2.0`).then(r => r.json()).then((info) => {
}) nodes.value[user.server] = info
const masto = createClient({ }).catch(() => undefined)
url,
streamingApiUrl: instance.urls.streamingApi,
accessToken: user?.token,
disableVersionCheck: true,
})
if (!user?.token) { if (!user?.token) {
publicServer.value = server publicServer.value = user.server
publicInstance.value = instance publicInstance.value = instance
return
} }
else { function getUser() {
try { return users.value.find(u => u.server === user.server && u.token === user.token)
}
const account = getUser()?.account
if (account)
currentUserId.value = account.id
const [me, pushSubscription] = await Promise.all([ const [me, pushSubscription] = await Promise.all([
masto.v1.accounts.verifyCredentials(), fetchAccountInfo(client, user.server),
// if PWA is not enabled, don't get push subscription // if PWA is not enabled, don't get push subscription
useRuntimeConfig().public.pwaEnabled useRuntimeConfig().public.pwaEnabled
// we get 404 response instead empty data // we get 404 response instead empty data
? masto.v1.webPushSubscriptions.fetch().catch(() => Promise.resolve(undefined)) ? client.v1.webPushSubscriptions.fetch().catch(() => Promise.resolve(undefined))
: Promise.resolve(undefined), : Promise.resolve(undefined),
]) ])
if (!me.acct.includes('@')) const existingUser = getUser()
me.acct = `${me.acct}@${instance.uri}` if (existingUser) {
existingUser.account = me
user.account = me existingUser.pushSubscription = pushSubscription
user.pushSubscription = pushSubscription
currentUserId.value = me.id
instances.value[server] = instance
if (!users.value.some(u => u.server === user.server && u.token === user.token))
users.value.push(user as UserLogin)
} }
catch (err) { else {
console.error(err) users.value.push({
await signout() ...user,
} account: me,
} pushSubscription,
// This only cleans up the URL; page content should stay the same
if (route.path === '/signin/callback') {
await router.push('/home')
}
else if ('server' in route.params && user?.token && !useNuxtApp()._processingMiddleware) {
await router.push({
...route,
force: true,
}) })
} }
return masto currentUserId.value = me.id
} }
export function setAccountInfo(userId: string, account: mastodon.v1.AccountCredentials) { export async function fetchAccountInfo(client: mastodon.Client, server: string) {
const index = getUsersIndexByUserId(userId) const account = await client.v1.accounts.verifyCredentials()
if (index === -1)
return false
users.value[index].account = account
return true
}
export async function pullMyAccountInfo() {
const account = await useMasto().v1.accounts.verifyCredentials()
if (!account.acct.includes('@')) if (!account.acct.includes('@'))
account.acct = `${account.acct}@${currentInstance.value!.uri}` account.acct = `${account.acct}@${server}`
cacheAccount(account, server, true)
setAccountInfo(currentUserId.value, account) return account
cacheAccount(account, currentServer.value, true)
} }
export function getUsersIndexByUserId(userId: string) { export async function refreshAccountInfo() {
return users.value.findIndex(u => u.account?.id === userId) const account = await fetchAccountInfo(useMastoClient(), currentServer.value)
currentUser.value!.account = account
return account
} }
export async function removePushNotificationData(user: UserLogin, fromSWPushManager = true) { export async function removePushNotificationData(user: UserLogin, fromSWPushManager = true) {
@ -221,7 +218,23 @@ export async function removePushNotifications(user: UserLogin) {
return return
// unsubscribe push notifications // unsubscribe push notifications
await useMasto().v1.webPushSubscriptions.remove().catch(() => Promise.resolve()) await useMastoClient().v1.webPushSubscriptions.remove().catch(() => Promise.resolve())
}
export async function switchUser(user: UserLogin) {
const masto = useMasto()
await loginTo(masto, user)
// This only cleans up the URL; page content should stay the same
const route = useRoute()
const router = useRouter()
if ('server' in route.params && user?.token && !useNuxtApp()._processingMiddleware) {
await router.push({
...route,
force: true,
})
}
} }
export async function signout() { export async function signout() {
@ -256,7 +269,7 @@ export async function signout() {
if (!currentUserId.value) if (!currentUserId.value)
await useRouter().push('/') await useRouter().push('/')
await masto.loginTo(currentUser.value) loginTo(masto, currentUser.value)
} }
export function checkLogin() { export function checkLogin() {
@ -267,10 +280,24 @@ export function checkLogin() {
return true return true
} }
interface UseUserLocalStorageCache {
scope: EffectScope
value: Ref<Record<string, any>>
}
/** /**
* Create reactive storage for the current user * Create reactive storage for the current user
*/ */
export function useUserLocalStorage<T extends object>(key: string, initial: () => T) { export function useUserLocalStorage<T extends object>(key: string, initial: () => T): Ref<T> {
if (process.server || process.test)
return shallowRef(initial())
// @ts-expect-error bind value to the function
const map: Map<string, UseUserLocalStorageCache> = useUserLocalStorage._ = useUserLocalStorage._ || new Map()
if (!map.has(key)) {
const scope = effectScope(true)
const value = scope.run(() => {
const all = useLocalStorage<Record<string, T>>(key, {}, { deep: true }) const all = useLocalStorage<Record<string, T>>(key, {}, { deep: true })
return computed(() => { return computed(() => {
const id = currentUser.value?.account.id const id = currentUser.value?.account.id
@ -279,6 +306,11 @@ export function useUserLocalStorage<T extends object>(key: string, initial: () =
all.value[id] = Object.assign(initial(), all.value[id] || {}) all.value[id] = Object.assign(initial(), all.value[id] || {})
return all.value[id] return all.value[id]
}) })
})
map.set(key, { scope, value: value! })
}
return map.get(key)!.value as Ref<T>
} }
/** /**
@ -291,63 +323,11 @@ export function clearUserLocalStorage(account?: mastodon.v1.Account) {
return return
const id = `${account.acct}@${currentInstance.value?.uri || currentServer.value}` const id = `${account.acct}@${currentInstance.value?.uri || currentServer.value}`
// @ts-expect-error bind value to the function // @ts-expect-error bind value to the function
;(useUserLocalStorage._ as Map<string, Ref<Record<string, any>>> | undefined)?.forEach((storage) => { const cacheMap = useUserLocalStorage._ as Map<string, UseUserLocalStorageCache> | undefined
if (storage.value[id]) cacheMap?.forEach(({ value }) => {
delete storage.value[id] if (value.value[id])
delete value.value[id]
}) })
} }
export const createMasto = () => {
const api = shallowRef<mastodon.Client | null>(null)
const apiPromise = ref<Promise<mastodon.Client> | null>(null)
const initialised = computed(() => !!api.value)
const masto = new Proxy({} as ElkMasto, {
get(_, key: keyof ElkMasto) {
if (key === 'loggedIn')
return initialised
if (key === 'loginTo') {
return (...args: any[]): Promise<mastodon.Client> => {
return apiPromise.value = loginTo(...args).then((r) => {
api.value = r
return masto
}).catch(() => {
// Show error page when Mastodon server is down
throw createError({
fatal: true,
statusMessage: 'Could not log into account.',
})
})
}
}
if (api.value && key in api.value)
return api.value[key as keyof mastodon.Client]
if (!api.value) {
return new Proxy({}, {
get(_, subkey) {
if (typeof subkey === 'string' && subkey.startsWith('iterate')) {
return (...args: any[]) => {
let paginator: any
function next() {
paginator = paginator || (api.value as any)?.[key][subkey](...args)
return paginator.next()
}
return { next }
}
}
return (...args: any[]) => apiPromise.value?.then((r: any) => r[key][subkey](...args))
},
})
}
return undefined
},
})
return masto
}

View file

@ -6,6 +6,10 @@ import { useHead } from '#head'
export const isHydrated = ref(false) export const isHydrated = ref(false)
export const onHydrated = (cb: () => unknown) => {
watchOnce(isHydrated, () => cb(), { immediate: isHydrated.value })
}
/** /**
* ### Whether the current component is running in the background * ### Whether the current component is running in the background
* *

View file

@ -0,0 +1,24 @@
export function useWebShareTarget(listener?: (message: MessageEvent) => void) {
if (process.server)
return
onBeforeMount(() => {
// PWA must be installed to use share target
if (useNuxtApp().$pwa.isInstalled && 'serviceWorker' in navigator) {
if (listener)
navigator.serviceWorker.addEventListener('message', listener)
navigator.serviceWorker.getRegistration()
.then((registration) => {
if (registration && registration.active) {
// we need to signal the service worker that we are ready to receive data
registration.active.postMessage({ action: 'ready-to-receive' })
}
})
.catch(err => console.error('Could not get registration', err))
if (listener)
onBeforeUnmount(() => navigator.serviceWorker.removeEventListener('message', listener))
}
})
}

View file

@ -20,6 +20,16 @@ const locales: LocaleObjectData[] = [
file: 'en-GB.json', file: 'en-GB.json',
name: 'English (UK)', name: 'English (UK)',
}, },
({
code: 'ar-EG',
file: 'ar-EG.json',
name: 'العربية',
dir: 'rtl',
pluralRule: (choice: number) => {
const name = new Intl.PluralRules('ar-EG').select(choice)
return { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5 }[name]
},
} satisfies LocaleObjectData),
{ {
code: 'de-DE', code: 'de-DE',
file: 'de-DE.json', file: 'de-DE.json',
@ -72,16 +82,16 @@ const locales: LocaleObjectData[] = [
file: 'cs-CZ.json', file: 'cs-CZ.json',
name: 'Česky', name: 'Česky',
}, },
({ {
code: 'ar-EG', code: 'pt-PT',
file: 'ar-EG.json', file: 'pt-PT.json',
name: 'العربية', name: 'Português',
dir: 'rtl', },
pluralRule: (choice: number) => { {
const name = new Intl.PluralRules('ar-EG').select(choice) code: 'tr-TR',
return { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5 }[name] file: 'tr-TR.json',
name: 'Türkçe',
}, },
} satisfies LocaleObjectData),
].sort((a, b) => a.code.localeCompare(b.code)) ].sort((a, b) => a.code.localeCompare(b.code))
const datetimeFormats = Object.values(locales).reduce((acc, data) => { const datetimeFormats = Object.values(locales).reduce((acc, data) => {

View file

@ -2,12 +2,13 @@ export const APP_NAME = 'Elk'
export const DEFAULT_POST_CHARS_LIMIT = 500 export const DEFAULT_POST_CHARS_LIMIT = 500
export const DEFAULT_FONT_SIZE = 'md' export const DEFAULT_FONT_SIZE = 'md'
export const DEFAULT_LANGUAGE = 'en-US'
export const STORAGE_KEY_DRAFTS = 'elk-drafts' export const STORAGE_KEY_DRAFTS = 'elk-drafts'
export const STORAGE_KEY_USERS = 'elk-users' export const STORAGE_KEY_USERS = 'elk-users'
export const STORAGE_KEY_SERVERS = 'elk-servers' export const STORAGE_KEY_SERVERS = 'elk-servers'
export const STORAGE_KEY_NODES = 'elk-nodes'
export const STORAGE_KEY_CURRENT_USER = 'elk-current-user' export const STORAGE_KEY_CURRENT_USER = 'elk-current-user'
export const STORAGE_KEY_CURRENT_USER_HANDLE = 'elk-current-user-handle'
export const STORAGE_KEY_NOTIFY_TAB = 'elk-notify-tab' export const STORAGE_KEY_NOTIFY_TAB = 'elk-notify-tab'
export const STORAGE_KEY_FIRST_VISIT = 'elk-first-visit' export const STORAGE_KEY_FIRST_VISIT = 'elk-first-visit'
export const STORAGE_KEY_SETTINGS = 'elk-settings' export const STORAGE_KEY_SETTINGS = 'elk-settings'
@ -20,7 +21,4 @@ export const STORAGE_KEY_NOTIFICATION_POLICY = 'elk-notification-policy'
export const COOKIE_MAX_AGE = 10 * 365 * 24 * 60 * 60 * 1000 export const COOKIE_MAX_AGE = 10 * 365 * 24 * 60 * 60 * 1000
export const COOKIE_KEY_FONT_SIZE = 'elk-font-size'
export const COOKIE_KEY_LOCALE = 'elk-lang'
export const HANDLED_MASTO_URLS = /^(https?:\/\/)?([\w\d-]+\.)+\w+\/(@[@\w\d-\.]+)(\/objects)?(\/\d+)?$/ export const HANDLED_MASTO_URLS = /^(https?:\/\/)?([\w\d-]+\.)+\w+\/(@[@\w\d-\.]+)(\/objects)?(\/\d+)?$/

View file

@ -3,13 +3,3 @@
<NuxtPage /> <NuxtPage />
</AppLayout> </AppLayout>
</template> </template>
<style>
@font-face {
font-display: swap;
font-family: 'DM Sans';
font-style: normal;
font-weight: 400;
src: url(/fonts/DM-sans-v11.ttf) format('truetype');
}
</style>

View file

@ -1,9 +1,6 @@
import { defineTheme, palette } from 'pinceau' import { defineTheme, palette } from 'pinceau'
export default defineTheme({ export default defineTheme({
font: {
sans: 'DM Sans',
},
color: { color: {
primary: palette('#d98018'), primary: palette('#d98018'),
}, },

View file

@ -11,17 +11,17 @@ const errorCodes: Record<number, string> = {
404: 'Page not found', 404: 'Page not found',
} }
if (process.dev)
console.error(error)
const defaultMessage = 'Something went wrong' const defaultMessage = 'Something went wrong'
const message = error.message ?? errorCodes[error.statusCode!] ?? defaultMessage const message = error.message ?? errorCodes[error.statusCode!] ?? defaultMessage
const state = ref<'error' | 'reloading'>('error') const state = ref<'error' | 'reloading'>('error')
const masto = useMasto()
const reload = async () => { const reload = async () => {
state.value = 'reloading' state.value = 'reloading'
try { try {
if (!masto.loggedIn.value)
await masto.loginTo(currentUser.value)
clearError({ redirect: currentUser.value ? '/home' : `/${currentServer.value}/public/local` }) clearError({ redirect: currentUser.value ? '/home' : `/${currentServer.value}/public/local` })
} }
catch (err) { catch (err) {
@ -47,7 +47,9 @@ const reload = async () => {
{{ message }} {{ message }}
</div> </div>
<button flex items-center gap-2 justify-center btn-solid text-center :disabled="state === 'reloading'"> <button flex items-center gap-2 justify-center btn-solid text-center :disabled="state === 'reloading'">
<span v-if="state === 'reloading'" i-ri:loader-2-fill animate-spin inline-block /> <span v-if="state === 'reloading'" block animate-spin preserve-3d>
<span block i-ri:loader-2-fill />
</span>
{{ state === 'reloading' ? 'Reloading' : 'Reload' }} {{ state === 'reloading' ? 'Reloading' : 'Reload' }}
</button> </button>
</form> </form>

View file

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useFeatureFlag } from '~/composables/settings' import { usePreferences } from '~/composables/settings'
const route = useRoute() const route = useRoute()
const userSettings = useUserSettings() const userSettings = useUserSettings()
@ -7,13 +7,13 @@ const userSettings = useUserSettings()
const wideLayout = computed(() => route.meta.wideLayout ?? false) const wideLayout = computed(() => route.meta.wideLayout ?? false)
const showUserPicker = logicAnd( const showUserPicker = logicAnd(
useFeatureFlag('experimentalUserPicker'), usePreferences('experimentalUserPicker'),
() => useUsers().value.length > 1, () => useUsers().value.length > 1,
) )
</script> </script>
<template> <template>
<div h-full :class="{ zen: userSettings.zenMode }"> <div h-full>
<main flex w-full mxa lg:max-w-80rem> <main flex w-full mxa lg:max-w-80rem>
<aside class="hidden sm:flex w-1/8 md:w-1/6 lg:w-1/5 xl:w-1/4 justify-end xl:me-4 zen-hide" relative> <aside class="hidden sm:flex w-1/8 md:w-1/6 lg:w-1/5 xl:w-1/4 justify-end xl:me-4 zen-hide" relative>
<div sticky top-0 w-20 xl:w-100 h-screen flex="~ col" lt-xl-items-center> <div sticky top-0 w-20 xl:w-100 h-screen flex="~ col" lt-xl-items-center>
@ -22,7 +22,7 @@ const showUserPicker = logicAnd(
<NavTitle /> <NavTitle />
<NavSide command /> <NavSide command />
<div flex-auto /> <div flex-auto />
<div v-if="isMastoInitialised" flex flex-col> <div v-if="isHydrated" flex flex-col>
<div hidden xl:block> <div hidden xl:block>
<UserSignInEntry v-if="!currentUser" /> <UserSignInEntry v-if="!currentUser" />
</div> </div>

View file

@ -240,12 +240,6 @@
"description": "قم بتحرير إعدادات حسابك في موقع Mastodon الأصلي", "description": "قم بتحرير إعدادات حسابك في موقع Mastodon الأصلي",
"label": "إعدادت الحساب" "label": "إعدادت الحساب"
}, },
"feature_flags": {
"github_cards": "GitHub بطاقات",
"title": "الميزات التجريبية",
"user_picker": "الشريط الجانبي لمبدل المستخدم",
"virtual_scroll": "التمرير الافتراضي"
},
"interface": { "interface": {
"color_mode": "وضع اللون", "color_mode": "وضع اللون",
"dark_mode": "الوضع الداكن", "dark_mode": "الوضع الداكن",
@ -315,7 +309,14 @@
}, },
"notifications_settings": "التنبيهات", "notifications_settings": "التنبيهات",
"preferences": { "preferences": {
"label": "التفضيلات" "github_cards": "GitHub بطاقات",
"hide_boost_count": "إخفاء عدد المشاركات",
"hide_favorite_count": "إخفاء عدد المفضلة",
"hide_follower_count": "إخفاء عدد المتابعين",
"label": "التفضيلات",
"title": "الميزات التجريبية",
"user_picker": "الشريط الجانبي لمبدل المستخدم",
"virtual_scroll": "التمرير الافتراضي"
}, },
"profile": { "profile": {
"appearance": { "appearance": {
@ -338,14 +339,6 @@
"export": "Export User Tokens", "export": "Export User Tokens",
"import": "Import User Tokens", "import": "Import User Tokens",
"label": "المستخدمون المسجلون" "label": "المستخدمون المسجلون"
},
"wellness": {
"feature": {
"hide_boost_count": "إخفاء عدد المشاركات",
"hide_favorite_count": "إخفاء عدد المفضلة",
"hide_follower_count": "إخفاء عدد المتابعين"
},
"label": "صحة النفس"
} }
}, },
"state": { "state": {

View file

@ -1,9 +1,17 @@
{ {
"a11y": {
"loading_page": "Seite wird geladen, bitte warten",
"loading_titled_page": "Seite {0} wird geladen, bitte warten",
"locale_changed": "Sprache wurde zu {0} geändert",
"locale_changing": "Sprache wird geändert, bitte warten",
"route_loaded": "Seite {0} geladen"
},
"account": { "account": {
"avatar_description": "{0}'s Avatar", "avatar_description": "{0}'s Avatar",
"blocked_by": "Du wurdest von diesem Account geblockt", "blocked_by": "Du wurdest von diesem Account geblockt",
"blocked_domains": "Gesperrte Domänen", "blocked_domains": "Gesperrte Domänen",
"blocked_users": "Gesperrte Accounts", "blocked_users": "Gesperrte Accounts",
"blocking": "Blockiert",
"bot": "BOT", "bot": "BOT",
"favourites": "Favoriten", "favourites": "Favoriten",
"follow": "Folgen", "follow": "Folgen",
@ -18,29 +26,44 @@
"joined": "Beigetreten", "joined": "Beigetreten",
"moved_title": "hat angegeben, dass dies der neue Account ist:", "moved_title": "hat angegeben, dass dies der neue Account ist:",
"muted_users": "Stummgeschaltete Accounts", "muted_users": "Stummgeschaltete Accounts",
"muting": "Stummgeschaltet",
"mutuals": "Freunde", "mutuals": "Freunde",
"notify_on_post": "Benachrichtige mich, wenn {username} etwas postet",
"pinned": "Angepinnt", "pinned": "Angepinnt",
"posts": "Beiträge", "posts": "Beiträge",
"posts_count": "{0} Beiträge", "posts_count": "{0} Beiträge",
"profile_description": "{0}'s Profil", "profile_description": "{0}'s Profil",
"profile_unavailable": "Profil nicht verfügbar", "profile_unavailable": "Profil nicht verfügbar",
"unfollow": "Entfolgen" "unblock": "Entblocken",
"unfollow": "Entfolgen",
"unmute": "Stummschaltung aufheben",
"view_other_followers": "Follower aus anderen Instanzen werden möglicherweise nicht angezeigt.",
"view_other_following": "Das Folgen von anderen Instanzen wird möglicherweise nicht angezeigt."
}, },
"action": { "action": {
"apply": "Anwenden",
"bookmark": "Lesezeichen", "bookmark": "Lesezeichen",
"bookmarked": "Gemerkt", "bookmarked": "Gemerkt",
"boost": "Teilen", "boost": "Teilen",
"boost_count": "{0}",
"boosted": "Geteilt", "boosted": "Geteilt",
"clear_upload_failed": "Fehler beim Hochladen von Dateien entfernen",
"close": "Schliessen", "close": "Schliessen",
"compose": "Verfassen", "compose": "Verfassen",
"confirm": "Bestätigen",
"edit": "Bearbeiten",
"enter_app": "App öffnen", "enter_app": "App öffnen",
"favourite": "Favorisieren", "favourite": "Favorisieren",
"favourite_count": "{0}",
"favourited": "Favorisiert", "favourited": "Favorisiert",
"more": "Mehr", "more": "Mehr",
"next": "Nächster", "next": "Nächster",
"prev": "Vorheriger", "prev": "Vorheriger",
"publish": "Veröffentlichen", "publish": "Veröffentlichen",
"reply": "Antworten", "reply": "Antworten",
"reply_count": "{0}",
"reset": "Zurücksetzen",
"save": "Speichern",
"save_changes": "Änderungen speichern", "save_changes": "Änderungen speichern",
"sign_in": "Anmelden", "sign_in": "Anmelden",
"switch_account": "Account wechseln", "switch_account": "Account wechseln",
@ -49,6 +72,10 @@
"app_desc_short": "Ein flinker Mastodon Web-Client", "app_desc_short": "Ein flinker Mastodon Web-Client",
"app_logo": "Elk Logo", "app_logo": "Elk Logo",
"app_name": "Elk", "app_name": "Elk",
"attachment": {
"edit_title": "Beschreibung",
"remove_label": "Anhang entfernen"
},
"command": { "command": {
"activate": "Aktivieren", "activate": "Aktivieren",
"complete": "Vollständig", "complete": "Vollständig",
@ -62,25 +89,40 @@
"toggle_zen_mode": "Zen-Modus ändern" "toggle_zen_mode": "Zen-Modus ändern"
}, },
"common": { "common": {
"confirm_dialog": {
"cancel": "Abbrechen",
"confirm": "OK",
"title": "Bist du sicher, {0}?"
},
"end_of_list": "Ende der Liste", "end_of_list": "Ende der Liste",
"error": "FEHLER", "error": "FEHLER",
"in": "in",
"not_found": "404 Nicht Gefunden", "not_found": "404 Nicht Gefunden",
"offline_desc": "Anscheinend bist du offline. Bitte überprüfe deine Netzwerkverbindung." "offline_desc": "Anscheinend bist du offline. Bitte überprüfe deine Netzwerkverbindung."
}, },
"compose": {
"draft_title": "Entwurf {0}",
"drafts": "Entwürfe ({v})"
},
"conversation": { "conversation": {
"with": "mit" "with": "mit"
}, },
"error": { "error": {
"account_not_found": "Account {0} nicht gefunden", "account_not_found": "Account {0} nicht gefunden",
"explore-list-empty": "Momentan ist nichts im Trend. Schau später nochmal vorbei!", "explore-list-empty": "Momentan ist nichts im Trend. Schau später nochmal vorbei!",
"file_size_cannot_exceed_n_mb": "Die Dateigröße darf {0} MB nicht überschreiten",
"sign_in_error": "Kann nicht zu Server verbinden", "sign_in_error": "Kann nicht zu Server verbinden",
"status_not_found": "Beitrag nicht gefunden" "status_not_found": "Beitrag nicht gefunden",
"unsupported_file_format": "Nicht unterstütztes Dateiformat"
}, },
"help": { "help": {
"desc_highlight": "Erwarte hier und da einige Bugs und fehlende Funktionen.", "desc_highlight": "Erwarte hier und da einige Bugs und fehlende Funktionen.",
"desc_para1": "Danke für dein Interesse in Elk, unser noch in der Bearbeitung befindliche, generische Mastodon Client!", "desc_para1": "Danke für dein Interesse in Elk, unser noch in der Bearbeitung befindliche, generische Mastodon Client!",
"desc_para2": "Wir arbeiten hart an der Entwicklung und verbessern ihn mit der Zeit. Und wir laden dich schon sehr bald ein uns zu helfen, sobald wir ihn Quelloffen machen!", "desc_para2": "Wir arbeiten hart an der Entwicklung und verbessern ihn mit der Zeit. Und wir laden dich schon sehr bald ein uns zu helfen, sobald wir ihn Quelloffen machen!",
"desc_para3": "Doch in der Zwischenzeit kannst du der Entwicklung aushelfen, indem du unsere Teammitglieder durch die unten stehenden Links unterstützt.", "desc_para3": "Doch in der Zwischenzeit kannst du der Entwicklung aushelfen, indem du unsere Teammitglieder durch die unten stehenden Links unterstützt.",
"desc_para4": "Elk ist Open Source. \nWenn du beim Testen helfen, Feedback geben oder einen Beitrag leisten möchtest,",
"desc_para5": "Kontaktiere uns auf GitHub",
"desc_para6": "und mache mit.",
"title": "Elk ist in der Alpha!" "title": "Elk ist in der Alpha!"
}, },
"language": { "language": {
@ -92,13 +134,22 @@
"copy_link_to_post": "Link zu diesem Beitrag kopieren", "copy_link_to_post": "Link zu diesem Beitrag kopieren",
"delete": "Löschen", "delete": "Löschen",
"delete_and_redraft": "Löschen und neu erstellen", "delete_and_redraft": "Löschen und neu erstellen",
"delete_confirm": {
"cancel": "Abbrechen",
"confirm": "Löschen",
"title": "Möchtest du diesen Beitrag wirklich löschen?"
},
"direct_message_account": "Direktnachricht an {0}", "direct_message_account": "Direktnachricht an {0}",
"edit": "Bearbeiten", "edit": "Bearbeiten",
"hide_reblogs": "Boosts von {0} ausblenden",
"mention_account": "Erwähne {0}", "mention_account": "Erwähne {0}",
"mute_account": "{0} stummschalten", "mute_account": "{0} stummschalten",
"mute_conversation": "Diesem Beitrag stummschalten", "mute_conversation": "Diesem Beitrag stummschalten",
"open_in_original_site": "Auf Originalseite öffnen", "open_in_original_site": "Auf Originalseite öffnen",
"pin_on_profile": "An Profil anpinnen", "pin_on_profile": "An Profil anpinnen",
"share_post": "Teile diesen Beitrag",
"show_favourited_and_boosted_by": "Zeige mir, wer favorisiert und geboostet hat",
"show_reblogs": "Boosts von {0} anzeigen",
"show_untranslated": "Übersetzung schliessen", "show_untranslated": "Übersetzung schliessen",
"toggle_theme": { "toggle_theme": {
"dark": "Dunkles Farbschema aktivieren", "dark": "Dunkles Farbschema aktivieren",
@ -112,18 +163,24 @@
"unpin_on_profile": "Von Profil lösen" "unpin_on_profile": "Von Profil lösen"
}, },
"nav": { "nav": {
"back": "Zurück",
"blocked_domains": "Gesperrte Domänen",
"blocked_users": "Blockierte Benutzer",
"bookmarks": "Lesezeichen", "bookmarks": "Lesezeichen",
"built_at": "Letzter Build: {0}", "built_at": "Letzter Build: {0}",
"compose": "Verfassen",
"conversations": "Direktnachrichten", "conversations": "Direktnachrichten",
"explore": "Entdecken", "explore": "Entdecken",
"favourites": "Favoriten", "favourites": "Favoriten",
"federated": "Föderiert", "federated": "Föderiert",
"home": "Startseite", "home": "Startseite",
"local": "Lokal", "local": "Lokal",
"muted_users": "Stummgeschaltete Benutzer",
"notifications": "Mitteilungen", "notifications": "Mitteilungen",
"profile": "Profil", "profile": "Profil",
"search": "Suche", "search": "Suche",
"select_feature_flags": "Feature-Flags aktivieren", "select_feature_flags": "Feature-Flags aktivieren",
"select_font_size": "Schriftgröße",
"select_language": "Sprache auswählen", "select_language": "Sprache auswählen",
"settings": "Einstellungen", "settings": "Einstellungen",
"show_intro": "Intro anzeigen", "show_intro": "Intro anzeigen",
@ -144,7 +201,36 @@
"content_warning": "Schreib hier deine Warnung", "content_warning": "Schreib hier deine Warnung",
"default_1": "Was geht dir gerade durch den Kopf?", "default_1": "Was geht dir gerade durch den Kopf?",
"reply_to_account": "Antwort an {0}", "reply_to_account": "Antwort an {0}",
"replying": "Antworten" "replying": "Antworten",
"the_thread": "der Thread"
},
"pwa": {
"dismiss": "Ignorieren",
"title": "Neues Elk-Update verfügbar!",
"update": "Aktualisieren",
"update_available_short": "Elk aktualisieren",
"webmanifest": {
"canary": {
"description": "Ein flinker Mastodon-Webclient (Canary)",
"name": "Elk (canary)",
"short_name": "Elk (canary)"
},
"dev": {
"description": "Ein flinker Mastodon-Webclient (dev)",
"name": "Elk (dev)",
"short_name": "Elk (dev)"
},
"preview": {
"description": "Ein flinker Mastodon-Webclient (Vorschau)",
"name": "Elk (Vorschau)",
"short_name": "Elk (Vorschau)"
},
"release": {
"description": "Ein flinker Mastodon-Webclient",
"name": "Elk",
"short_name": "Elk"
}
}
}, },
"search": { "search": {
"search_desc": "Suche nach Accounts & Hashtags", "search_desc": "Suche nach Accounts & Hashtags",
@ -152,10 +238,21 @@
}, },
"settings": { "settings": {
"about": { "about": {
"label": "Info" "label": "Info",
"meet_the_team": "Triff das Team",
"sponsor_action": "Sponser uns",
"sponsor_action_desc": "Um das Team bei der Entwicklung von Elk zu unterstützen",
"sponsors": "Sponsoren",
"sponsors_body_1": "Elk wird ermöglicht durch das großzügige Sponsoring und die Hilfe von:",
"sponsors_body_2": "Und alle Unternehmen und Einzelpersonen, die das Elk Team und die Mitglieder sponsern.",
"sponsors_body_3": "Wenn dir die App gefällt, erwäge uns zu sponsern:"
},
"account_settings": {
"description": "Bearbeite Kontoeinstellungen in der Mastodon-Benutzeroberfläche",
"label": "Account Einstellungen"
}, },
"feature_flags": { "feature_flags": {
"github_cards": "GitHub-Karten", "github_cards": "GitHub Cards",
"title": "Experimentelle Funktionen", "title": "Experimentelle Funktionen",
"user_picker": "Benutzerauswahl", "user_picker": "Benutzerauswahl",
"virtual_scroll": "Virtuelles Scrollen" "virtual_scroll": "Virtuelles Scrollen"
@ -166,14 +263,79 @@
"default": " (Standard)", "default": " (Standard)",
"font_size": "Schriftgröße", "font_size": "Schriftgröße",
"label": "Oberfläche", "label": "Oberfläche",
"light_mode": "Helles Farbschema" "light_mode": "Helles Farbschema",
"size_label": {
"lg": "Groß",
"md": "Mittel",
"sm": "Klein",
"xl": "Extra groß",
"xs": "Extra klein"
},
"system_mode": "System"
}, },
"language": { "language": {
"display_language": "Anzeigesprache", "display_language": "Anzeigesprache",
"label": "Sprache" "label": "Sprache"
}, },
"notifications": {
"label": "Benachrichtigungen",
"notifications": {
"label": "Benachrichtigungseinstellungen"
},
"push_notifications": {
"alerts": {
"favourite": "Favoriten",
"follow": "Neue Follower",
"mention": "Erwähnungen",
"poll": "Umfragen",
"reblog": "Reblogge deinen Beitrag",
"title": "Welche Benachrichtigungen erhalten?"
},
"description": "Erhalte Benachrichtigungen, auch wenn du Elk nicht verwendest.",
"instructions": "Vergesse nicht, Änderungen mit der Schaltfläche @:settings.notifications.push_notifications.save_settings zu speichern!",
"label": "Einstellungen für Push-Benachrichtigungen",
"policy": {
"all": "Von irgendjemandem",
"followed": "Von Accounts, denen ich folge",
"follower": "Von Menschen, die mir folgen",
"none": "Von niemandem",
"title": "Von wem kann ich Benachrichtigungen erhalten?"
},
"save_settings": "Einstellungen speichern",
"subscription_error": {
"clear_error": "Fehler aufräumen",
"permission_denied": "Berechtigung verweigert: Aktiviere Benachrichtigungen im Browser.",
"request_error": "Beim Anfordern des Abonnements ist ein Fehler aufgetreten. Versuche es erneut. Wenn der Fehler weiterhin besteht, melde das Problem bitte dem Elk-Repository.",
"title": "Push-Benachrichtigungen konnten nicht abonniert werden",
"too_many_registrations": "Aufgrund von Browserbeschränkungen kann Elk den Push-Benachrichtigungsdienst nicht für mehrere Konten auf verschiedenen Servern verwenden. \nDu solltest Push-Benachrichtigungen für andere Konten abbestellen und es erneut versuchen."
},
"title": "Einstellungen für Push-Benachrichtigungen",
"undo_settings": "Änderungen rückgängig machen",
"unsubscribe": "Push-Benachrichtigungen deaktivieren",
"unsupported": "Ihr Browser unterstützt keine Push-Benachrichtigungen.",
"warning": {
"enable_close": "Schließen",
"enable_description": "Um Benachrichtigungen zu erhalten, wenn Elk nicht geöffnet ist, aktiviere Push-Benachrichtigungen. \nDu kannst genau steuern, welche Arten von Interaktionen Push-Benachrichtigungen generieren, indem du die Schaltfläche \"@:settings.notifications.show_btn{'\"'} oben aktivierest, sobald sie aktiviert ist.",
"enable_description_desktop": "Um Benachrichtigungen zu erhalten, wenn Elk nicht geöffnet ist, aktiviere Push-Benachrichtigungen. \nDu kannst unter „Einstellungen > Benachrichtigungen > Einstellungen für Push-Benachrichtigungen“ genau steuern, welche Arten von Interaktionen Push-Benachrichtigungen generieren, sobald diese aktiviert sind.",
"enable_description_mobile": "Du erreichst die Einstellungen auch über das Navigationsmenü „Einstellungen > Benachrichtigungen > Push-Benachrichtigungseinstellungen“.",
"enable_description_settings": "Um Benachrichtigungen zu erhalten, wenn Elk nicht geöffnet ist, aktiviere Push-Benachrichtigungen. \nDu kannst genau steuern, welche Arten von Interaktionen Push-Benachrichtigungen auf demselben Bildschirm generieren, sobald du sie aktivierst.",
"enable_desktop": "Aktiviere Push-Benachrichtigungen",
"enable_title": "Verpasse nie wieder Benachrichtigungen",
"re_auth": "Offenbar unterstützt dein Server keine Push-Benachrichtigungen. \nVersuche dich abzumelden und erneut anzumelden. Wenn diese Meldung weiterhin angezeigt wird, wende dich an dein Serveradministrator."
}
},
"show_btn": "Gehe zu den Benachrichtigungseinstellungen"
},
"notifications_settings": "Benachrichtigungen",
"preferences": { "preferences": {
"label": "Einstellungen" "github_cards": "GitHub Cards",
"hide_boost_count": "Boost-Zähler ausblenden",
"hide_favorite_count": "Favoritenzahl ausblenden",
"hide_follower_count": "Anzahl der Follower ausblenden",
"label": "Einstellungen",
"title": "Experimentelle Funktionen",
"user_picker": "Benutzerauswahl",
"virtual_scroll": "Virtuelles Scrollen"
}, },
"profile": { "profile": {
"appearance": { "appearance": {
@ -198,14 +360,25 @@
"label": "Eingeloggte Benutzer" "label": "Eingeloggte Benutzer"
} }
}, },
"share-target": {
"description": "Elk kann so konfiguriert werden, dass du Inhalte aus anderen Anwendungen teilen kannst, installiere einfach Elk auf deinem Gerät oder Computer und melden dich an.",
"hint": "Um Inhalte mit Elk zu teilen, muss Elk installiert sein und du musst angemeldet sein.",
"title": "Teile via Elk"
},
"state": { "state": {
"attachments_exceed_server_limit": "Die Anzahl der Anhänge hat das Limit pro Beitrag überschritten.",
"attachments_limit_error": "Limit pro Beitrag überschritten",
"edited": "(bearbeitet)", "edited": "(bearbeitet)",
"editing": "Bearbeiten", "editing": "Bearbeiten",
"loading": "Laden...", "loading": "Laden...",
"publishing": "Veröffentlichung",
"upload_failed": "Upload fehlgeschlagen",
"uploading": "Hochladen..." "uploading": "Hochladen..."
}, },
"status": { "status": {
"boosted_by": "Boosted von",
"edited": "Zuletzt bearbeitet: {0}", "edited": "Zuletzt bearbeitet: {0}",
"favourited_by": "Favorisiert von",
"filter_hidden_phrase": "Versteckt durch", "filter_hidden_phrase": "Versteckt durch",
"filter_removed_phrase": "Entfernt durch Filter", "filter_removed_phrase": "Entfernt durch Filter",
"filter_show_anyway": "Trotzdem zeigen", "filter_show_anyway": "Trotzdem zeigen",
@ -219,9 +392,12 @@
"finished": "Beendet: {0}" "finished": "Beendet: {0}"
}, },
"reblogged": "{0} teilte", "reblogged": "{0} teilte",
"replying_to": "Antworten auf {0}",
"show_full_thread": "Vollständigen Thread anzeigen",
"someone": "Jemand", "someone": "Jemand",
"spoiler_show_less": "Zeige weniger", "spoiler_show_less": "Zeige weniger",
"spoiler_show_more": "Zeige mehr", "spoiler_show_more": "Zeige mehr",
"thread": "Thread",
"try_original_site": "Versuche die original Seite" "try_original_site": "Versuche die original Seite"
}, },
"status_history": { "status_history": {
@ -276,7 +452,8 @@
"year_past": "vor 0 Jahren|letztes Jahren|vor {n} Jahren" "year_past": "vor 0 Jahren|letztes Jahren|vor {n} Jahren"
}, },
"timeline": { "timeline": {
"show_new_items": "Zeige {v} neue Beiträge|Zeige {v} neuen Beitrag|Zeige {v} neue Beiträge" "show_new_items": "Zeige {v} neue Beiträge|Zeige {v} neuen Beitrag|Zeige {v} neue Beiträge",
"view_older_posts": "Ältere Beiträge aus anderen Instanzen werden möglicherweise nicht angezeigt."
}, },
"title": { "title": {
"federated_timeline": "Föderierte Timeline", "federated_timeline": "Föderierte Timeline",
@ -284,9 +461,12 @@
}, },
"tooltip": { "tooltip": {
"add_content_warning": "Inhaltswarnung hinzufügen", "add_content_warning": "Inhaltswarnung hinzufügen",
"add_emojis": "Emojis hinzufügen",
"add_media": "Bilder, ein Video oder eine Audiodatei hinzufügen", "add_media": "Bilder, ein Video oder eine Audiodatei hinzufügen",
"add_publishable_content": "Füge Inhalte zum Veröffentlichen hinzu",
"change_content_visibility": "Sichtbarkeit von Inhalten ändern", "change_content_visibility": "Sichtbarkeit von Inhalten ändern",
"change_language": "Sprache ändern", "change_language": "Sprache ändern",
"emoji": "Emoji",
"explore_links_intro": "Diese Nachrichten werden gerade von Leuten auf diesem und anderen Servern des dezentralen Netzwerks besprochen.", "explore_links_intro": "Diese Nachrichten werden gerade von Leuten auf diesem und anderen Servern des dezentralen Netzwerks besprochen.",
"explore_posts_intro": "Diese Beiträge von diesem Server gewinnen gerade unter den Leuten von diesem und anderen Servern des dezentralen Netzweks an Reichweite.", "explore_posts_intro": "Diese Beiträge von diesem Server gewinnen gerade unter den Leuten von diesem und anderen Servern des dezentralen Netzweks an Reichweite.",
"explore_tags_intro": "Diese Hashtags gewinnen gerade unter den Leuten von diesem und anderen Servern des dezentralen Netzweks an Reichweite.", "explore_tags_intro": "Diese Hashtags gewinnen gerade unter den Leuten von diesem und anderen Servern des dezentralen Netzweks an Reichweite.",
@ -296,6 +476,7 @@
"add_existing": "Bestehendes Konto hinzufügen", "add_existing": "Bestehendes Konto hinzufügen",
"server_address_label": "Mastodon Server Adresse", "server_address_label": "Mastodon Server Adresse",
"sign_in_desc": "Melde dich an, um Profilen oder Hashtags zu folgen, Beiträge zu favorisieren, zu teilen und zu beantworten oder von deinem Konto auf einem anderen Server aus zu interagieren.", "sign_in_desc": "Melde dich an, um Profilen oder Hashtags zu folgen, Beiträge zu favorisieren, zu teilen und zu beantworten oder von deinem Konto auf einem anderen Server aus zu interagieren.",
"sign_in_notice_title": "Anzeigen von {0} öffentlichen Daten",
"sign_out_account": "{0} abmelden", "sign_out_account": "{0} abmelden",
"tip_no_account": "Wenn du noch kein Mastodon-Konto hast, {0}.", "tip_no_account": "Wenn du noch kein Mastodon-Konto hast, {0}.",
"tip_register_account": "wähle einen Server aus und registriere eines" "tip_register_account": "wähle einen Server aus und registriere eines"

View file

@ -107,7 +107,7 @@
"desc_highlight": "Expect some bugs and missing features here and there.", "desc_highlight": "Expect some bugs and missing features here and there.",
"desc_para1": "Thanks for your interest in trying out Elk, our work-in-progress Mastodon web client!", "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_para2": "we are working hard on the development and improving it over time.",
"desc_para3": "To help boosting out development, you can sponsor the Team through GitHub Sponsors. We hope you enjoy Elk!", "desc_para3": "To boost development, you can sponsor the Team through GitHub Sponsors. We hope you enjoy Elk!",
"desc_para4": "Elk is Open Source. If you'd like to help with testing, giving feedback, or contributing,", "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_para5": "reach out to us on GitHub",
"desc_para6": "and get involved.", "desc_para6": "and get involved.",
@ -198,11 +198,11 @@
}, },
"interface": { "interface": {
"color_mode": "Color Mode", "color_mode": "Color Mode",
"dark_mode": "Dark Mode", "dark_mode": "Dark",
"default": " (default)", "default": " (default)",
"font_size": "Font Size", "font_size": "Font Size",
"label": "Interface", "label": "Interface",
"light_mode": "Light Mode", "light_mode": "Light",
"system_mode": "System" "system_mode": "System"
}, },
"language": { "language": {
@ -282,6 +282,11 @@
"label": "Logged in users" "label": "Logged in users"
} }
}, },
"share-target": {
"description": "Elk can be configured so that you can share content from other applications, simply install Elk on your device or computer and sign in.",
"hint": "In order to share content with Elk, Elk must be installed and you must be signed in.",
"title": "Share with Elk"
},
"state": { "state": {
"attachments_exceed_server_limit": "The number of attachments exceeded the limit per post.", "attachments_exceed_server_limit": "The number of attachments exceeded the limit per post.",
"attachments_limit_error": "Limit per post exceeded", "attachments_limit_error": "Limit per post exceeded",

View file

@ -28,6 +28,7 @@
"muted_users": "Muted users", "muted_users": "Muted users",
"muting": "Muted", "muting": "Muted",
"mutuals": "Mutuals", "mutuals": "Mutuals",
"notify_on_post": "Notify me when {username} posts",
"pinned": "Pinned", "pinned": "Pinned",
"posts": "Posts", "posts": "Posts",
"posts_count": "{0} Posts|{0} Post|{0} Posts", "posts_count": "{0} Posts|{0} Post|{0} Posts",
@ -35,7 +36,9 @@
"profile_unavailable": "Profile unavailable", "profile_unavailable": "Profile unavailable",
"unblock": "Unblock", "unblock": "Unblock",
"unfollow": "Unfollow", "unfollow": "Unfollow",
"unmute": "Unmute" "unmute": "Unmute",
"view_other_followers": "Followers from other instances may not be displayed.",
"view_other_following": "Following from other instances may not be displayed."
}, },
"action": { "action": {
"apply": "Apply", "apply": "Apply",
@ -89,7 +92,7 @@
"confirm_dialog": { "confirm_dialog": {
"cancel": "No", "cancel": "No",
"confirm": "Yes", "confirm": "Yes",
"title": "Are you sure?" "title": "Are you sure {0}?"
}, },
"end_of_list": "End of the list", "end_of_list": "End of the list",
"error": "ERROR", "error": "ERROR",
@ -116,7 +119,7 @@
"desc_highlight": "Expect some bugs and missing features here and there.", "desc_highlight": "Expect some bugs and missing features here and there.",
"desc_para1": "Thanks for your interest in trying out Elk, our work-in-progress Mastodon web client!", "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_para2": "we are working hard on the development and improving it over time.",
"desc_para3": "To help boosting out development, you can sponsor the Team through GitHub Sponsors. We hope you enjoy Elk!", "desc_para3": "To boost development, you can sponsor the Team through GitHub Sponsors. We hope you enjoy Elk!",
"desc_para4": "Elk is Open Source. If you'd like to help with testing, giving feedback, or contributing,", "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_para5": "reach out to us on GitHub",
"desc_para6": "and get involved.", "desc_para6": "and get involved.",
@ -165,6 +168,7 @@
"blocked_users": "Blocked users", "blocked_users": "Blocked users",
"bookmarks": "Bookmarks", "bookmarks": "Bookmarks",
"built_at": "Built {0}", "built_at": "Built {0}",
"compose": "Compose",
"conversations": "Conversations", "conversations": "Conversations",
"explore": "Explore", "explore": "Explore",
"favourites": "Favorites", "favourites": "Favorites",
@ -247,19 +251,13 @@
"description": "Edit your account settings in Mastodon UI", "description": "Edit your account settings in Mastodon UI",
"label": "Account settings" "label": "Account settings"
}, },
"feature_flags": {
"github_cards": "GitHub Cards",
"title": "Experimental Features",
"user_picker": "User Picker",
"virtual_scroll": "Virtual Scrolling"
},
"interface": { "interface": {
"color_mode": "Color Mode", "color_mode": "Color Mode",
"dark_mode": "Dark Mode", "dark_mode": "Dark",
"default": " (default)", "default": " (default)",
"font_size": "Font Size", "font_size": "Font Size",
"label": "Interface", "label": "Interface",
"light_mode": "Light Mode", "light_mode": "Light",
"size_label": { "size_label": {
"lg": "Large", "lg": "Large",
"md": "Medium", "md": "Medium",
@ -324,7 +322,14 @@
}, },
"notifications_settings": "Notifications", "notifications_settings": "Notifications",
"preferences": { "preferences": {
"label": "Preferences" "github_cards": "GitHub Cards",
"hide_boost_count": "Hide boost count",
"hide_favorite_count": "Hide favorite count",
"hide_follower_count": "Hide follower count",
"label": "Preferences",
"title": "Experimental Features",
"user_picker": "User Picker",
"virtual_scroll": "Virtual Scrolling"
}, },
"profile": { "profile": {
"appearance": { "appearance": {
@ -347,16 +352,13 @@
"export": "Export User Tokens", "export": "Export User Tokens",
"import": "Import User Tokens", "import": "Import User Tokens",
"label": "Logged in users" "label": "Logged in users"
},
"wellness": {
"feature": {
"hide_boost_count": "Hide boost count",
"hide_favorite_count": "Hide favorite count",
"hide_follower_count": "Hide follower count"
},
"label": "Wellness"
} }
}, },
"share-target": {
"description": "Elk can be configured so that you can share content from other applications, simply install Elk on your device or computer and sign in.",
"hint": "In order to share content with Elk, Elk must be installed and you must be signed in.",
"title": "Share with Elk"
},
"state": { "state": {
"attachments_exceed_server_limit": "The number of attachments exceeded the limit per post.", "attachments_exceed_server_limit": "The number of attachments exceeded the limit per post.",
"attachments_limit_error": "Limit per post exceeded", "attachments_limit_error": "Limit per post exceeded",
@ -453,6 +455,7 @@
}, },
"tooltip": { "tooltip": {
"add_content_warning": "Add content warning", "add_content_warning": "Add content warning",
"add_emojis": "Add emojis",
"add_media": "Add images, a video or an audio file", "add_media": "Add images, a video or an audio file",
"add_publishable_content": "Add content to publish", "add_publishable_content": "Add content to publish",
"change_content_visibility": "Change content visibility", "change_content_visibility": "Change content visibility",

View file

@ -1,9 +1,9 @@
{ {
"a11y": { "a11y": {
"loading_page": "Cargando página, espere por favor", "loading_page": "Cargando página, espera por favor",
"loading_titled_page": "Cargando página {0}, espere por favor", "loading_titled_page": "Cargando página {0}, espera por favor",
"locale_changed": "Idioma cambiado a {0}", "locale_changed": "Idioma cambiado a {0}",
"locale_changing": "Cambiando idioma, espere por favor", "locale_changing": "Cambiando idioma, espera por favor",
"route_loaded": "Página {0} cargada" "route_loaded": "Página {0} cargada"
}, },
"account": { "account": {
@ -13,7 +13,7 @@
"blocked_users": "Usuarios bloqueados", "blocked_users": "Usuarios bloqueados",
"blocking": "Bloqueado", "blocking": "Bloqueado",
"bot": "BOT", "bot": "BOT",
"favourites": "Favoritos", "favourites": "Favoritas",
"follow": "Seguir", "follow": "Seguir",
"follow_back": "Seguir de vuelta", "follow_back": "Seguir de vuelta",
"follow_requested": "Solicitado", "follow_requested": "Solicitado",
@ -50,9 +50,9 @@
"confirm": "Confirmar", "confirm": "Confirmar",
"edit": "Editar", "edit": "Editar",
"enter_app": "Entrar", "enter_app": "Entrar",
"favourite": "Favorito", "favourite": "Favorita",
"favourite_count": "{0}", "favourite_count": "{0}",
"favourited": "Marcado como favorito", "favourited": "Marcado como favorita",
"more": "Más", "more": "Más",
"next": "Siguiente", "next": "Siguiente",
"prev": "Anterior", "prev": "Anterior",
@ -95,7 +95,7 @@
"error": "ERROR", "error": "ERROR",
"in": "en", "in": "en",
"not_found": "404 No Encontrado", "not_found": "404 No Encontrado",
"offline_desc": "Al parecer estás fuera de línea. Por favor, comprueba tu conexión a la red." "offline_desc": "Al parecer no tienes conexión a internet. Por favor, comprueba tu conexión a la red."
}, },
"compose": { "compose": {
"draft_title": "Borrador {0}", "draft_title": "Borrador {0}",
@ -133,7 +133,7 @@
"delete_and_redraft": "Borrar y volver a borrador", "delete_and_redraft": "Borrar y volver a borrador",
"delete_confirm": { "delete_confirm": {
"cancel": "Cancelar", "cancel": "Cancelar",
"confirm": "Borrar", "confirm": "Eliminar",
"title": "¿Estás seguro que deseas eliminar esta publicación?" "title": "¿Estás seguro que deseas eliminar esta publicación?"
}, },
"direct_message_account": "Mensaje directo a {0}", "direct_message_account": "Mensaje directo a {0}",
@ -145,7 +145,7 @@
"open_in_original_site": "Abrir página original", "open_in_original_site": "Abrir página original",
"pin_on_profile": "Fijar en tu perfil", "pin_on_profile": "Fijar en tu perfil",
"share_post": "Compartir esta publicación", "share_post": "Compartir esta publicación",
"show_favourited_and_boosted_by": "Mostrar quien marcó como favorito y quien retooteó", "show_favourited_and_boosted_by": "Mostrar quien marcó como favorita y quien retooteó",
"show_reblogs": "Mostrar retoots de {0}", "show_reblogs": "Mostrar retoots de {0}",
"show_untranslated": "Mostrar original", "show_untranslated": "Mostrar original",
"toggle_theme": { "toggle_theme": {
@ -167,7 +167,7 @@
"built_at": "Compilado {0}", "built_at": "Compilado {0}",
"conversations": "Conversaciones", "conversations": "Conversaciones",
"explore": "Explorar", "explore": "Explorar",
"favourites": "Favoritos", "favourites": "Favoritas",
"federated": "Federados", "federated": "Federados",
"home": "Inicio", "home": "Inicio",
"local": "Local", "local": "Local",
@ -179,12 +179,12 @@
"select_font_size": "Cambiar tamaño de letra", "select_font_size": "Cambiar tamaño de letra",
"select_language": "Cambiar idioma", "select_language": "Cambiar idioma",
"settings": "Ajustes", "settings": "Ajustes",
"show_intro": "Mostrar intro", "show_intro": "Mostrar introducción",
"toggle_theme": "Cambiar tema", "toggle_theme": "Cambiar modo de color",
"zen_mode": "Modo Zen" "zen_mode": "Modo Zen"
}, },
"notification": { "notification": {
"favourited_post": "marcó tu publicación como favorito", "favourited_post": "marcó como favorita tu publicación",
"followed_you": "te ha seguido", "followed_you": "te ha seguido",
"followed_you_count": "{0} personas te siguieron|{0} persona te siguió|{0} personas te siguieron", "followed_you_count": "{0} personas te siguieron|{0} persona te siguió|{0} personas te siguieron",
"missing_type": "MISSING notification.type:", "missing_type": "MISSING notification.type:",
@ -230,29 +230,30 @@
}, },
"search": { "search": {
"search_desc": "Buscar personas y etiquetas", "search_desc": "Buscar personas y etiquetas",
"search_empty": "No se pudo encontrar nada para estos términos de búsqueda" "search_empty": "No hubo resultados para estos términos de búsqueda"
}, },
"settings": { "settings": {
"about": { "about": {
"label": "Acerca de" "label": "Acerca de",
"meet_the_team": "Conoce al equipo",
"sponsor_action": "Patrocinar",
"sponsor_action_desc": "Apoya al equipo detrás de Elk",
"sponsors": "Patrocinadores",
"sponsors_body_1": "Elk es posible gracias al generoso patrocinio y apoyo de:",
"sponsors_body_2": "Y todas las empresas y personas que patrocinan al equipo de Elk y sus miembros.",
"sponsors_body_3": "Si estás disfrutando de la aplicación, considera patrocinarnos:"
}, },
"account_settings": { "account_settings": {
"description": "Edita los ajustes de tu cuenta en la interfaz de Mastodon", "description": "Edita los ajustes de tu cuenta en la interfaz de Mastodon",
"label": "Ajustes de cuenta" "label": "Ajustes de cuenta"
}, },
"feature_flags": {
"github_cards": "Tarjetas GitHub",
"title": "Funcionalidades experimentales",
"user_picker": "Selector de usuarios",
"virtual_scroll": "Desplazamiento virtual"
},
"interface": { "interface": {
"color_mode": "Modo Color", "color_mode": "Modos de color",
"dark_mode": "Modo Oscuro", "dark_mode": "Modo oscuro",
"default": " (por defecto)", "default": " (por defecto)",
"font_size": "Tamaño de Letra", "font_size": "Tamaño de Letra",
"label": "Interfaz", "label": "Interfaz",
"light_mode": "Modo Claro", "light_mode": "Modo claro",
"size_label": { "size_label": {
"lg": "Grande", "lg": "Grande",
"md": "Mediana", "md": "Mediana",
@ -272,7 +273,7 @@
}, },
"push_notifications": { "push_notifications": {
"alerts": { "alerts": {
"favourite": "Favoritos", "favourite": "Favoritas",
"follow": "Nuevos seguidores", "follow": "Nuevos seguidores",
"mention": "Menciones", "mention": "Menciones",
"poll": "Encuestas", "poll": "Encuestas",
@ -316,12 +317,19 @@
}, },
"notifications_settings": "Notificaciones", "notifications_settings": "Notificaciones",
"preferences": { "preferences": {
"label": "Preferencias" "github_cards": "Tarjetas GitHub",
"hide_boost_count": "Ocultar contador de retoots",
"hide_favorite_count": "Ocultar contador de favoritas",
"hide_follower_count": "Ocultar contador de seguidores",
"label": "Preferencias",
"title": "Funcionalidades experimentales",
"user_picker": "Selector de usuarios",
"virtual_scroll": "Desplazamiento virtual"
}, },
"profile": { "profile": {
"appearance": { "appearance": {
"bio": "Biografía", "bio": "Biografía",
"description": "Editar avatar, nombre de usuario, perfil, etc.", "description": "Modificar avatar, nombre de usuario, perfil, etc.",
"display_name": "Nombre a mostrar", "display_name": "Nombre a mostrar",
"label": "Apariencia", "label": "Apariencia",
"profile_metadata": "Metadatos de perfil", "profile_metadata": "Metadatos de perfil",
@ -329,7 +337,7 @@
"title": "Editar perfil" "title": "Editar perfil"
}, },
"featured_tags": { "featured_tags": {
"description": "Las personas pueden navegar por tus publicaciones públicas con estas hashtags.", "description": "Las personas pueden navegar por tus publicaciones públicas con estos hashtags.",
"label": "Hashtags destacados" "label": "Hashtags destacados"
}, },
"label": "Perfil" "label": "Perfil"
@ -341,19 +349,25 @@
"label": "Usuarios conectados" "label": "Usuarios conectados"
} }
}, },
"share-target": {
"description": "Elk puede ser configurado para que pueda compartir contenido desde otras aplicaciones, simplemente tiene que instalar Elk en su dispositivo u ordenador e iniciar sesión.",
"hint": "Para poder compartir contenido con Elk, debes instalar Elk e iniciar sesión.",
"title": "Compartir con Elk"
},
"state": { "state": {
"attachments_exceed_server_limit": "Número máximo de archivos adjuntos por publicación excedido.", "attachments_exceed_server_limit": "Número máximo de archivos adjuntos por publicación excedido.",
"attachments_limit_error": "Límite por publicación excedido", "attachments_limit_error": "Límite por publicación excedido",
"edited": "(Editado)", "edited": "(Editado)",
"editing": "Editando", "editing": "Editando",
"loading": "Cargando...", "loading": "Cargando...",
"publishing": "Publicando",
"upload_failed": "Subida fallida", "upload_failed": "Subida fallida",
"uploading": "Subiendo..." "uploading": "Subiendo..."
}, },
"status": { "status": {
"boosted_by": "Retooteado por", "boosted_by": "Retooteado por",
"edited": "Editado {0}", "edited": "Editado {0}",
"favourited_by": "Marcado como favorito por", "favourited_by": "Marcado como favorita por",
"filter_hidden_phrase": "Filtrado por", "filter_hidden_phrase": "Filtrado por",
"filter_removed_phrase": "Eliminado por filtrado", "filter_removed_phrase": "Eliminado por filtrado",
"filter_show_anyway": "Mostrar de todas formas", "filter_show_anyway": "Mostrar de todas formas",
@ -436,20 +450,21 @@
}, },
"tooltip": { "tooltip": {
"add_content_warning": "Añadir advertencia de contenido", "add_content_warning": "Añadir advertencia de contenido",
"add_emojis": "Añadir emojis",
"add_media": "Añadir imágenes, video o audio", "add_media": "Añadir imágenes, video o audio",
"add_publishable_content": "Agregar contenido a publicar", "add_publishable_content": "Publicar contenido",
"change_content_visibility": "Cambiar visibilidad de contenido", "change_content_visibility": "Cambiar visibilidad de contenido",
"change_language": "Cambiar idioma", "change_language": "Cambiar idioma",
"emoji": "Emoji", "emoji": "Emoji",
"explore_links_intro": "Estas noticias están siendo comentadas por la gente en este y otros servidores de la red descentralizada en este momento.", "explore_links_intro": "Estas noticias están siendo comentadas ahora mismo por los usuarios de este y otros servidores de la red descentralizada.",
"explore_posts_intro": "Estos mensajes de este y otros servidores de la red descentralizada están ganando tracción en este servidor en este momento.", "explore_posts_intro": "Estos mensajes de este y otros servidores de la red descentralizada están siendo tendencia ahora mismo en este servidor.",
"explore_tags_intro": "Estas etiquetas están ganando tracción entre la gente de este y otros servidores de la red descentralizada en este momento.", "explore_tags_intro": "Estas etiquetas están siendo tendencia ahora mismo entre los usuarios de este y otros servidores de la red descentralizada.",
"toggle_code_block": "Cambiar a bloque de código" "toggle_code_block": "Cambiar a bloque de código"
}, },
"user": { "user": {
"add_existing": "Agregar una cuenta existente", "add_existing": "Agregar una cuenta existente",
"server_address_label": "Dirección de Servidor de Mastodon", "server_address_label": "Dirección de Servidor de Mastodon",
"sign_in_desc": "Inicia sesión para seguir perfiles o hashtags, marcar como favorito, compartir and responder a publicaciones, o interactuar desde tu usuario con un servidor diferente.", "sign_in_desc": "Inicia sesión para seguir perfiles o hashtags, marcar cómo favorita, compartir y responder a publicaciones, o interactuar con un servidor diferente con tu usuario.",
"sign_in_notice_title": "Viendo información pública de {0}", "sign_in_notice_title": "Viendo información pública de {0}",
"sign_out_account": "Cerrar sesión {0}", "sign_out_account": "Cerrar sesión {0}",
"tip_no_account": "Si aún no tienes una cuenta Mastodon, {0}.", "tip_no_account": "Si aún no tienes una cuenta Mastodon, {0}.",

View file

@ -11,9 +11,9 @@
"blocked_by": "Vous êtes bloqué·e par cet·te utilisateur·ice.", "blocked_by": "Vous êtes bloqué·e par cet·te utilisateur·ice.",
"blocked_domains": "Domaines bloqués", "blocked_domains": "Domaines bloqués",
"blocked_users": "Utilisateur·ice·s bloqué·e·s", "blocked_users": "Utilisateur·ice·s bloqué·e·s",
"blocking": "Blocked", "blocking": "Bloqué·e",
"bot": "Automatisé", "bot": "Automatisé",
"favourites": "Appréciés", "favourites": "Aimés",
"follow": "Suivre", "follow": "Suivre",
"follow_back": "Suivre en retour", "follow_back": "Suivre en retour",
"follow_requested": "Abonnement demandé", "follow_requested": "Abonnement demandé",
@ -27,15 +27,15 @@
"moved_title": "a indiqué que son nouveau compte est désormais :", "moved_title": "a indiqué que son nouveau compte est désormais :",
"muted_users": "Utilisateur·ice·s masqué·e·s", "muted_users": "Utilisateur·ice·s masqué·e·s",
"muting": "Masqué·e", "muting": "Masqué·e",
"mutuals": "@:account.following", "mutuals": "Abonné·e·s",
"pinned": "Épinglés", "pinned": "Épinglés",
"posts": "Messages", "posts": "Messages",
"posts_count": "{0} Messages", "posts_count": "{0} Messages",
"profile_description": "En-tête du profil de {0}", "profile_description": "En-tête du profil de {0}",
"profile_unavailable": "Profil non accessible", "profile_unavailable": "Profil non accessible",
"unblock": "Unblock", "unblock": "Débloquer",
"unfollow": "Ne plus suivre", "unfollow": "Ne plus suivre",
"unmute": "Unmute" "unmute": "Réafficher"
}, },
"action": { "action": {
"apply": "Appliquer", "apply": "Appliquer",
@ -49,8 +49,8 @@
"confirm": "Confirmer", "confirm": "Confirmer",
"edit": "Éditer", "edit": "Éditer",
"enter_app": "Entrer dans l'application", "enter_app": "Entrer dans l'application",
"favourite": "Ajouter aux messages appréciés", "favourite": "J'aime",
"favourited": "Ajouté aux messages appréciés", "favourited": "Aimé",
"more": "Plus", "more": "Plus",
"next": "Suivant", "next": "Suivant",
"prev": "Précédent", "prev": "Précédent",
@ -142,7 +142,7 @@
"open_in_original_site": "Ouvrir sur le site d'origine", "open_in_original_site": "Ouvrir sur le site d'origine",
"pin_on_profile": "Épingler sur le profil", "pin_on_profile": "Épingler sur le profil",
"share_post": "Partager ce message", "share_post": "Partager ce message",
"show_favourited_and_boosted_by": "Montrer qui a apprécié et partagé", "show_favourited_and_boosted_by": "Montrer qui a aimé et partagé",
"show_reblogs": "Voir les partages de {0}", "show_reblogs": "Voir les partages de {0}",
"show_untranslated": "Montrer le message non-traduit", "show_untranslated": "Montrer le message non-traduit",
"toggle_theme": { "toggle_theme": {
@ -164,7 +164,7 @@
"built_at": "Dernière compilation {0}", "built_at": "Dernière compilation {0}",
"conversations": "Conversations", "conversations": "Conversations",
"explore": "Explorer", "explore": "Explorer",
"favourites": "Appréciés", "favourites": "Favoris",
"federated": "Fédérés", "federated": "Fédérés",
"home": "Accueil", "home": "Accueil",
"local": "Local", "local": "Local",
@ -181,7 +181,7 @@
"zen_mode": "Mode Zen" "zen_mode": "Mode Zen"
}, },
"notification": { "notification": {
"favourited_post": "apprécie votre message", "favourited_post": "a aimé votre message",
"followed_you": "vous suit", "followed_you": "vous suit",
"followed_you_count": "{0} personnes vous suivent|{0} personne vous suit|{0} personnes vous suivent", "followed_you_count": "{0} personnes vous suivent|{0} personne vous suit|{0} personnes vous suivent",
"missing_type": "MISSING notification.type:", "missing_type": "MISSING notification.type:",
@ -244,12 +244,6 @@
"description": "Modifiez les paramètres de votre compte dans l'interface de Mastodon", "description": "Modifiez les paramètres de votre compte dans l'interface de Mastodon",
"label": "Paramètres de compte" "label": "Paramètres de compte"
}, },
"feature_flags": {
"github_cards": "GitHub Cards",
"title": "Fonctionnalités expérimentales",
"user_picker": "User Picker",
"virtual_scroll": "Défilement virtuel"
},
"interface": { "interface": {
"color_mode": "Couleur de thème", "color_mode": "Couleur de thème",
"dark_mode": "Mode sombre", "dark_mode": "Mode sombre",
@ -277,11 +271,11 @@
}, },
"push_notifications": { "push_notifications": {
"alerts": { "alerts": {
"favourite": "Apprécier", "favourite": "Messages aimés",
"follow": "Nouveaux abonné·e·s", "follow": "Nouveaux abonné·e·s",
"mention": "Mentions", "mention": "Mentions",
"poll": "Sondages", "poll": "Sondages",
"reblog": "A republié votre message", "reblog": "Messages partagés",
"title": "Quelles notifications recevoir ?" "title": "Quelles notifications recevoir ?"
}, },
"description": "Recevez des notifications même lorsque vous n'utilisez pas Elk.", "description": "Recevez des notifications même lorsque vous n'utilisez pas Elk.",
@ -320,7 +314,14 @@
}, },
"notifications_settings": "Notifications", "notifications_settings": "Notifications",
"preferences": { "preferences": {
"label": "Préférences" "github_cards": "GitHub Cards",
"hide_boost_count": "Cacher les compteurs de partages",
"hide_favorite_count": "Cacher les compteurs de favoris",
"hide_follower_count": "Cacher les compteurs d'abonné·e·s",
"label": "Préférences",
"title": "Fonctionnalités expérimentales",
"user_picker": "User Picker",
"virtual_scroll": "Défilement virtuel"
}, },
"profile": { "profile": {
"appearance": { "appearance": {
@ -343,14 +344,6 @@
"export": "Exporter les tokens d'utilisateur·ice", "export": "Exporter les tokens d'utilisateur·ice",
"import": "Importer des tokens d'utilisateur·ice", "import": "Importer des tokens d'utilisateur·ice",
"label": "Utilisateur·ice·s connecté·e·s" "label": "Utilisateur·ice·s connecté·e·s"
},
"wellness": {
"feature": {
"hide_boost_count": "Cacher les compteurs de partages",
"hide_favorite_count": "Cacher les compteurs d'appréciations",
"hide_follower_count": "Cacher les compteurs d'abonné·e·s"
},
"label": "Bien-être"
} }
}, },
"state": { "state": {
@ -366,7 +359,7 @@
"status": { "status": {
"boosted_by": "Partagé par", "boosted_by": "Partagé par",
"edited": "Edité {0}", "edited": "Edité {0}",
"favourited_by": "Apprécié par", "favourited_by": "Aimé par",
"filter_hidden_phrase": "Filtré par", "filter_hidden_phrase": "Filtré par",
"filter_removed_phrase": "Caché par le filtre", "filter_removed_phrase": "Caché par le filtre",
"filter_show_anyway": "Montrer coûte que coûte", "filter_show_anyway": "Montrer coûte que coûte",
@ -462,7 +455,7 @@
"user": { "user": {
"add_existing": "Ajouter un compte existant", "add_existing": "Ajouter un compte existant",
"server_address_label": "Adresse du serveur mastodon", "server_address_label": "Adresse du serveur mastodon",
"sign_in_desc": "Connectez-vous pour suivre des profils ou des hashtags, dire que vous appréciez, partager et répondre à des messages, ou interagir à partir de votre compte sur un autre serveur...", "sign_in_desc": "Connectez-vous pour suivre des profils ou des hashtags, aimer, partagez et répondre à des messages, ou interagir à partir de votre compte d'autre serveur.",
"sign_in_notice_title": "Affichage de {0} données publiques", "sign_in_notice_title": "Affichage de {0} données publiques",
"sign_out_account": "Se déconnecter de {0}", "sign_out_account": "Se déconnecter de {0}",
"tip_no_account": "Si vous n'avez pas encore de compte Mastodon, {0}.", "tip_no_account": "Si vous n'avez pas encore de compte Mastodon, {0}.",

View file

@ -107,7 +107,7 @@
"desc_highlight": "Expect some bugs and missing features here and there.", "desc_highlight": "Expect some bugs and missing features here and there.",
"desc_para1": "Thanks for your interest in trying out Elk, our work-in-progress Mastodon web client!", "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_para2": "we are working hard on the development and improving it over time.",
"desc_para3": "To help boosting out development, you can sponsor the Team through GitHub Sponsors. We hope you enjoy Elk!", "desc_para3": "To boost development, you can sponsor the Team through GitHub Sponsors. We hope you enjoy Elk!",
"desc_para4": "Elk is Open Source. If you'd like to help with testing, giving feedback, or contributing,", "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_para5": "reach out to us on GitHub",
"desc_para6": "and get involved.", "desc_para6": "and get involved.",

495
locales/pt-PT.json Normal file
View file

@ -0,0 +1,495 @@
{
"a11y": {
"loading_page": "A carregar página, por favor aguarde",
"loading_titled_page": "A carregar página {0}, por favor aguarde",
"locale_changed": "Idioma alterado para {0}",
"locale_changing": "A alterar idioma, por favor aguarde",
"route_loaded": "Página {0} carregada"
},
"account": {
"avatar_description": "Imagem de perfil de {0}",
"blocked_by": "Está bloqueado por este utilizador.",
"blocked_domains": "Domínios bloqueados",
"blocked_users": "Utilizadores bloqueados",
"blocking": "Bloqueado",
"bot": "BOT",
"favourites": "Favoritos",
"follow": "Seguir",
"follow_back": "Seguir de volta",
"follow_requested": "Pedido",
"followers": "Seguidores",
"followers_count": "{0} Seguidores|{0} Seguidor|{0} Seguidores",
"following": "A seguir",
"following_count": "A seguir {0}",
"follows_you": "Segue-o",
"go_to_profile": "Ir para o perfil",
"joined": "Juntou-se a",
"moved_title": "indicou que a sua novo conta é agora:",
"muted_users": "Utilizadores silenciados",
"muting": "Silenciados",
"mutuals": "Mútuos",
"notify_on_post": "Notifique-me quando {username} publicar",
"pinned": "Fixado",
"posts": "Publicações",
"posts_count": "{0} Publicações|{0} Publicação|{0} Publicações",
"profile_description": "Descrição de perfil de {0}",
"profile_unavailable": "Perfil indisponível",
"unblock": "Desbloquear",
"unfollow": "Deixar de seguir",
"unmute": "Deixar de silenciar",
"view_other_followers": "Os seguidores de outras instâncias podem não ser exibidos.",
"view_other_following": "As pessoas que segue de outras instâncias podem não ser exibidas."
},
"action": {
"apply": "Aplicar",
"bookmark": "Salvar",
"bookmarked": "Adicionado aos itens salvos",
"boost": "Partilhar",
"boost_count": "{0}",
"boosted": "Partilhado",
"clear_upload_failed": "Limpar erro de carregamento de ficheiro",
"close": "Fechar",
"compose": "Compor",
"confirm": "Confirmar",
"edit": "Editar",
"enter_app": "Entrar na App",
"favourite": "Adicionar aos favoritos",
"favourite_count": "{0}",
"favourited": "Adicionado aos favoritos",
"more": "Mais",
"next": "Próximo",
"prev": "Anterior",
"publish": "Publicar",
"reply": "Responder",
"reply_count": "{0}",
"reset": "Repor",
"save": "Guardar",
"save_changes": "Guardar alterações",
"sign_in": "Entrar",
"switch_account": "Mudar contar",
"vote": "Votar"
},
"app_desc_short": "Uma ágil aplicação web para o Mastodon",
"app_logo": "Logo do Elk",
"app_name": "Elk",
"attachment": {
"edit_title": "Descrição",
"remove_label": "Remover anexo"
},
"command": {
"activate": "Ativar",
"complete": "Completar",
"compose_desc": "Escrever uma nova publicação",
"n-people-in-the-past-n-days": "{0} pessoas nos últimos {1} dias",
"select_lang": "Selecionar idioma",
"sign_in_desc": "Adicionar uma conta existente",
"switch_account": "Mudar para {0}",
"switch_account_desc": "Mudar para outra conta",
"toggle_dark_mode": "Alternar modo escuro",
"toggle_zen_mode": "Alternar modo zen"
},
"common": {
"confirm_dialog": {
"cancel": "Não",
"confirm": "Sim",
"title": "Tem a certeza?"
},
"end_of_list": "Fim da lista",
"error": "ERRO",
"in": "em",
"not_found": "404 Não Encontrado",
"offline_desc": "Parece que está offline. Por favor, confirme a sua conexão à internet."
},
"compose": {
"draft_title": "Rascunho {0}",
"drafts": "Rascunhos ({v})"
},
"conversation": {
"with": "com"
},
"error": {
"account_not_found": "Conta {0} não encontrada",
"explore-list-empty": "Nada está em tendência agora. Confirme mais tarde!",
"file_size_cannot_exceed_n_mb": "O tamanho do ficheiro não pode exceder {0}MB",
"sign_in_error": "Não é possível conectar ao servidor.",
"status_not_found": "Publicação não encontrada",
"unsupported_file_format": "Formato de ficheiro não suportado"
},
"help": {
"desc_highlight": "Espere alguns problemas e funcionalidades em falta.",
"desc_para1": "Obrigado pelo seu interesse em experimentar o Elk, o nosso aplicativo web para o Mastodon, ainda em construção!",
"desc_para2": "Estamos a trabalhar arduamente no seu desenvolvimento e melhoria ao longo do tempo.",
"desc_para3": "Para ajudar a impulsionar o desenvolvimento, pode patrocionar a Equipa através do GitHub Sponsors. Esperamos que aprecie o Elk!",
"desc_para4": "Elk é um software de código aberto. Se quiser ajudar a testar a aplicação, dando o seu feedback ou contributo,",
"desc_para5": "pode encontrar-nos no GitHub",
"desc_para6": "e participar.",
"title": "Elk está em Antevisão!"
},
"language": {
"search": "Procurar"
},
"menu": {
"block_account": "Bloquear {0}",
"block_domain": "Bloquear domínio {0}",
"copy_link_to_post": "Copiar ligação para esta publicação",
"delete": "Eliminar",
"delete_and_redraft": "Eliminar & re-editar",
"delete_confirm": {
"cancel": "Cancelar",
"confirm": "Eliminar",
"title": "Tem a certeza que pretende eliminar esta publicação?"
},
"direct_message_account": "Mensagem direta a {0}",
"edit": "Editar",
"hide_reblogs": "Esconder partilhas de {0}",
"mention_account": "Mencionar {0}",
"mute_account": "Silenciar {0}",
"mute_conversation": "Silenciar esta publicação",
"open_in_original_site": "Abrir no sítio original",
"pin_on_profile": "Fixar no perfil",
"share_post": "Partilhar esta publicação",
"show_favourited_and_boosted_by": "Mostrar quem adicionou aos favoritos e partilhou",
"show_reblogs": "Mostrar partilhas de {0}",
"show_untranslated": "Mostrar não traduzidas",
"toggle_theme": {
"dark": "Alternar modo escuro",
"light": "Alternar modo claro"
},
"translate_post": "Traduzir publicação",
"unblock_account": "Desbloquear {0}",
"unblock_domain": "Desbloquear domínio {0}",
"unmute_account": "Deixar de silenciar {0}",
"unmute_conversation": "Deixar de silenciar esta publicação",
"unpin_on_profile": "Desafixar do perfil"
},
"nav": {
"back": "Voltar",
"blocked_domains": "Domínios bloqueados",
"blocked_users": "Utilizadores bloqueados",
"bookmarks": "Itens Salvos",
"built_at": "Produzido {0}",
"compose": "Compor",
"conversations": "Conversações",
"explore": "Explorar",
"favourites": "Favoritos",
"federated": "Federada",
"home": "Início",
"local": "Local",
"muted_users": "Utilizadores silenciados",
"notifications": "Notificações",
"profile": "Perfil",
"search": "Procurar",
"select_feature_flags": "Alternar Funcionalidades",
"select_font_size": "Tamanho da Fonte",
"select_language": "Idioma de Apresentação",
"settings": "Definições",
"show_intro": "Mostrar introdução",
"toggle_theme": "Alternar Tema",
"zen_mode": "Modo Zen"
},
"notification": {
"favourited_post": "adicionou a sua publicação aos favoritos",
"followed_you": "começou a segui-lo",
"followed_you_count": "{0} pessoas seguem-no|{0} pessoa segue-o|{0} pessoas seguem-no",
"missing_type": "notification.type em FALTA:",
"reblogged_post": "partilhou a sua publicação",
"request_to_follow": "pediu para segui-lo",
"signed_up": "inscreveu-se",
"update_status": "atualizou a sua publicação"
},
"placeholder": {
"content_warning": "Escreva aqui o seu aviso",
"default_1": "Em que está a pensar?",
"reply_to_account": "Responder a {0}",
"replying": "Respondedo",
"the_thread": "a conversa"
},
"pwa": {
"dismiss": "Dispensar",
"title": "Nova atualização do Elk disponível!",
"update": "Atualizar",
"update_available_short": "Atualizar Elk",
"webmanifest": {
"canary": {
"description": "Uma ágil aplicação web para o Mastodon (canary)",
"name": "Elk (canary)",
"short_name": "Elk (canary)"
},
"dev": {
"description": "Uma ágil aplicação web para o Mastodon (dev)",
"name": "Elk (dev)",
"short_name": "Elk (dev)"
},
"preview": {
"description": "Uma ágil aplicação web para o Mastodon (preview)",
"name": "Elk (preview)",
"short_name": "Elk (preview)"
},
"release": {
"description": "Uma ágil aplicação web para o Mastodon",
"name": "Elk",
"short_name": "Elk"
}
}
},
"search": {
"search_desc": "Procure por pessoas e hashtags",
"search_empty": "Não foi possível encontrar nada para os termos que pesquisou"
},
"settings": {
"about": {
"label": "Sobre",
"meet_the_team": "Conheça a equipa",
"sponsor_action": "Patrocine-nos",
"sponsor_action_desc": "Para ajudar a equipa que desenvolve o Elk",
"sponsors": "Patrocinadores",
"sponsors_body_1": "O Elk é possível graças ao genoroso patrocinio e ajuda de:",
"sponsors_body_2": "E todas as empresas e pessoas que apoiam a Equipa do Elk e os seus membros.",
"sponsors_body_3": "Se está a gostar de utilizar esta aplicação, considere apoiar-nos:"
},
"account_settings": {
"description": "Editar as configurações da sua conta na aplicação web do Mastodon",
"label": "Configurações da conta"
},
"feature_flags": {
"github_cards": "Cartões do GitHub",
"title": "Funcionalidades Experimentais",
"user_picker": "Selecionador de Utilizador",
"virtual_scroll": "Deslocamento Virtual"
},
"interface": {
"color_mode": "Modo de cores",
"dark_mode": "Modo Escuro",
"default": " (padrão)",
"font_size": "Tamanho da fonte",
"label": "Apresentação",
"light_mode": "Modo Claro",
"size_label": {
"lg": "Grande",
"md": "Médio",
"sm": "Pequeno",
"xl": "Extra grande",
"xs": "Extra pequeno"
},
"system_mode": "Sistema"
},
"language": {
"display_language": "Idioma de Apresentação",
"label": "Idioma"
},
"notifications": {
"label": "Notificações",
"notifications": {
"label": "Configurar notificações"
},
"push_notifications": {
"alerts": {
"favourite": "Favoritos",
"follow": "Novos seguidores",
"mention": "Menções",
"poll": "Votações",
"reblog": "Partilha das sua publicação",
"title": "Que notificações quer receber?"
},
"description": "Receba notificações mesmo quando não está a utilizar o Elk.",
"instructions": "Não esqueça de salvar as suas alterações utilizando o botão @:settings.notifications.push_notifications.save_settings!",
"label": "Configurar notificações push",
"policy": {
"all": "De todos",
"followed": "De pessoas que sigo",
"follower": "De pessoas que me seguem",
"none": "De ninguém",
"title": "De quem quer receber notificações?"
},
"save_settings": "Salvar configurações",
"subscription_error": {
"clear_error": "Limpar erro",
"permission_denied": "Permissão negada: habilite as notificações no seu browser.",
"request_error": "Um erro ocorreu durante o pedido de subcrição, tente novamente e se o erro persistir, por favor reporte o problema no repositório do Elk.",
"title": "Náo é possível subscrever as notificações push",
"too_many_registrations": "Devido a limitações do browser, o Elk não consegue utilizar o serviço de notificações push para multiplas contas em diferentes servidores. Deve cancelar a subcrição de notificações push nas outras contas e tentar novamente."
},
"title": "Configuração de notificações push",
"undo_settings": "Reverter alterações",
"unsubscribe": "Desabilitar notificações push",
"unsupported": "O seu browser não suporta notificações push.",
"warning": {
"enable_close": "Fechar",
"enable_description": "Para receber notificações quanto o Elk não está aberto, habilite as notificações push. Poderá controlar com precisão que tipos de interações geram notificações push através do \"@:settings.notifications.show_btn{'\"'} botão acima, uma vez habilitadas.",
"enable_description_desktop": "Para receber notificações quanto o Elk não está aberto, habilite as notificações push. Poderá controlar com precisão que tipos de interações geram notificações push em \"Preferências > Notificaçõess > Configuração de notificações push\", uma vez habilitadas.",
"enable_description_mobile": "Pode também aceder às configurações através do menu de navegação \"Preferências > Notificações > Configuração de notificações push\".",
"enable_description_settings": "Para receber notificações quanto o Elk não está aberto, habilite as notificações push. Poderá controlar com precisão que tipos de interações geram notificações neste mesmo ecrã, uma vez habilitadas.",
"enable_desktop": "Habilitar notificações push",
"enable_title": "Nunca perca nada",
"re_auth": "Parece que o seu servidor não suporta notificações push. Tenta desconectar e voltar a entrar, se esta mensagem permanecer contacte o administrador do seu servidor."
}
},
"show_btn": "Ir para a configuração de nofiticações"
},
"notifications_settings": "Notificações",
"preferences": {
"label": "Preferências"
},
"profile": {
"appearance": {
"bio": "Bio",
"description": "Editar imagem de perfil, nome, perfil, etc.",
"display_name": "Nome de apresentação",
"label": "Aspecto",
"profile_metadata": "Metadados de perfil",
"profile_metadata_desc": "Pode ter até {0} itens expostos, em forma de tabela, no seu perfil",
"title": "Editar perfil"
},
"featured_tags": {
"description": "As pessoas podem encontrar as suas publicações públicas que incluem essas hashtags.",
"label": "Hashtags destacadas"
},
"label": "Perfil"
},
"select_a_settings": "Selecionar uma configuração",
"users": {
"export": "Exportar Tokens de Acesso",
"import": "Importar Tokens de Acesso",
"label": "Utilizadores conectados"
},
"wellness": {
"feature": {
"hide_boost_count": "Esconder contagem de partilhas",
"hide_favorite_count": "Esconder contagem de favoritos",
"hide_follower_count": "Esconder contagem de seguidores"
},
"label": "Bem-estar"
}
},
"share-target": {
"description": "Elk pode ser configurado para que possa partilhar conteúdos de outras aplicações, basta instalar Elk no seu dispositivo ou computador e iniciar sessão.",
"hint": "Para poder partilhar conteúdo com o Elk, este tem de estar instalado e você ter iniciado sessão.",
"title": "Partilhar com o Elk"
},
"state": {
"attachments_exceed_server_limit": "O número de anexos excedeu o limite permitido por publicação.",
"attachments_limit_error": "Limite permitido por publicação excedido",
"edited": "(Editado)",
"editing": "Editando",
"loading": "Carregando...",
"publishing": "Publicando",
"upload_failed": "Falhou carregamento",
"uploading": "A carregar..."
},
"status": {
"boosted_by": "Partilhada Por",
"edited": "Editada {0}",
"favourited_by": "Adicionada Aos Favoritos Por",
"filter_hidden_phrase": "Filtrada por",
"filter_removed_phrase": "Removida pelo filtro",
"filter_show_anyway": "Mostrar mesmo assim",
"img_alt": {
"desc": "Descrição",
"dismiss": "Dispensar"
},
"poll": {
"count": "{0} votos|{0} voto|{0} votos",
"ends": "termina {0}",
"finished": "terminou {0}"
},
"reblogged": "{0} partilhou",
"replying_to": "Respondendo a {0}",
"show_full_thread": "Mostrar toda a conversa",
"someone": "alguém",
"spoiler_show_less": "Mostrar menos",
"spoiler_show_more": "Mostrar mais",
"thread": "Conversa",
"try_original_site": "Tentar o sítio original"
},
"status_history": {
"created": "criada {0}",
"edited": "editada {0}"
},
"tab": {
"for_you": "Para sí",
"hashtags": "Hashtags",
"media": "Media",
"news": "Notícias",
"notifications_all": "Todas",
"notifications_mention": "Menções",
"posts": "Publicações",
"posts_with_replies": "Publicações e Respostas"
},
"tag": {
"follow": "Seguir",
"follow_label": "Seguir hashtag {0}",
"unfollow": "Deixar de seguir",
"unfollow_label": "Deixar de seguir hashtag {0}"
},
"time_ago_options": {
"day_future": "em 0 dias|amanhã|em {n} diass",
"day_past": "0 dias atrás|ontem|{n} dias atrás",
"hour_future": "em 0 horas|em 1 hora|em {n} horas",
"hour_past": "0 horas atrás|1 hora atrás|{n} horas aatás",
"just_now": "agora mesmo",
"minute_future": "em 0 minutos|em 1 minuto|em {n} minutos",
"minute_past": "0 minutos atrás|1 minuto atrás|{n} minutos atrás",
"month_future": "em 0 mês|próximo mês|em {n} meses",
"month_past": "0 meses atrás|mês passado|{n} meses atrás",
"second_future": "agora mesmo|em {n} segundos|em {n} segundos",
"second_past": "agora mesmo|{n} segundo atrás|{n} segundos atrás",
"short_day_future": "em {n}d",
"short_day_past": "{n}d",
"short_hour_future": "em {n}h",
"short_hour_past": "{n}h",
"short_minute_future": "em {n}min",
"short_minute_past": "{n}min",
"short_month_future": "em {n}M",
"short_month_past": "{n}M",
"short_second_future": "em {n}s",
"short_second_past": "{n}s",
"short_week_future": "in {n}S",
"short_week_past": "{n}S",
"short_year_future": "in {n}A",
"short_year_past": "{n}A",
"week_future": "em 0 semanas|próxima semana|em {n} semanas",
"week_past": "0 semanas atrás|semana passada|{n} semanas atrás",
"year_future": "em 0 anos|próximo ano|em {n} anos",
"year_past": "0 anos atrás|ano passado|{n} anos atrás"
},
"timeline": {
"show_new_items": "Mostrar {v} novos itens|Mostrar {v} novo item|Mostrar {v} novos itens",
"view_older_posts": "Publicações antigas de outras instâncias podem não ser apresentadas."
},
"title": {
"federated_timeline": "Cronologia Federada",
"local_timeline": "Cronologia Local"
},
"tooltip": {
"add_content_warning": "Adicionar aviso de conteúdo",
"add_emojis": "Adicionar emojis",
"add_media": "Adicionar imagens, um video ou um ficheiro audio",
"add_publishable_content": "Adicionar conteúdo a publicar",
"change_content_visibility": "Alterar visibilidade do conteúdo",
"change_language": "Alterar idioma",
"emoji": "Emoji",
"explore_links_intro": "Estas notícias estão, neste momento, a ser faladas por pessoas neste e noutros servidores da rede descentralizada.",
"explore_posts_intro": "Estas publicações deste e de outros servidores na rede descentralizada estão, neste momento, a ganhar popularidade neste servidor.",
"explore_tags_intro": "Estes hashtags estão, neste momento, a ganhar popularidade entre as pessoas neste e noutros servidores da rede descentralizada.",
"toggle_code_block": "Alternar bloco de código"
},
"user": {
"add_existing": "Adicionar uma conta existente",
"server_address_label": "Endereço do Servidor Mastodon",
"sign_in_desc": "Entre, para seguir pessoas ou hashtags, adicionar aos favoritos, partilhar e responder a publicações, ou interagir a partir da sua conta de outro servidor.",
"sign_in_notice_title": "A visualizar os dados públicos de {0}",
"sign_out_account": "Desconectar {0}",
"tip_no_account": "Se ainda não tem uma conta Mastodon, {0}.",
"tip_register_account": "escolha um servidor e inscreva-se"
},
"visibility": {
"direct": "Direta",
"direct_desc": "Visível apenas pelos utilizadores mencionados",
"private": "Apenas seguidores",
"private_desc": "Visível apenas pelos seus seguidores",
"public": "Publico",
"public_desc": "Visível por todos",
"unlisted": "Não listada",
"unlisted_desc": "Visível por todos, mas não incluida nas funcionalidades de divulgação"
}
}

479
locales/tr-TR.json Normal file
View file

@ -0,0 +1,479 @@
{
"a11y": {
"loading_page": "Sayfa yükleniyor, lütfen bekleyin",
"loading_titled_page": "Sayfa {0} yükleniyor, lütfen bekleyin",
"locale_changed": "Dil değiştirildi, yeni dil: {0}",
"locale_changing": "Dil değiştiriliyor, lütfen bekleyin",
"route_loaded": "Sayfa {0} yüklendi"
},
"account": {
"avatar_description": "{0} avatarı",
"blocked_by": "Bu kullanıcı sizi engellemiş",
"blocked_domains": "Engellenen alan adları",
"blocked_users": "Engellenen kullanıcılar",
"blocking": "Engelli",
"bot": "BOT",
"favourites": "Favoriles",
"follow": "Takip et",
"follow_back": "Geri takip et",
"follow_requested": "İstek gönderildi",
"followers": "Takipçiler",
"followers_count": "{0} Takipçi",
"following": "Takip edilenler",
"following_count": "{0} takip edilen",
"follows_you": "Seni takip ediyor",
"go_to_profile": "Profile git",
"joined": "Katıldı",
"moved_title": "belirttiği yeni hesabı:",
"muted_users": "Susturulmuş kullanıcılar",
"muting": "Susturulmuş",
"mutuals": "Karşılıklı takip",
"pinned": "Sabitlendi",
"posts": "Gönderiler",
"posts_count": "{0} Gönderi",
"profile_description": "{0} profil başlığı",
"profile_unavailable": "Profil mevcut değil",
"unblock": "Engeli kaldır",
"unfollow": "Takibi bırak",
"unmute": "Susturulmayı kaldır"
},
"action": {
"apply": "Uygula",
"bookmark": "Yer imlerine ekle",
"bookmarked": "Yer imlerine eklendi",
"boost": "Boost",
"boost_count": "{0}",
"boosted": "Boost edildi",
"clear_upload_failed": "Dosya yükleme hatalarını temizle",
"close": "Kapat",
"compose": "Oluştur",
"confirm": "Onayla",
"edit": "Düzenle",
"enter_app": "Uygulamaya gir",
"favourite": "Favorilere ekle",
"favourite_count": "{0}",
"favourited": "Favorilere eklendi",
"more": "Daha fazla",
"next": "Sonraki",
"prev": "Önceki",
"publish": "Yayımla",
"reply": "Cevap ver",
"reply_count": "{0}",
"reset": "Sıfırla",
"save": "Kaydet",
"save_changes": "Değişikleri kaydet",
"sign_in": "Giriş yap",
"switch_account": "Hesap değiştir",
"vote": "Oy ver"
},
"app_desc_short": "Hızlı bir Mastodon web istemcisi",
"app_logo": "Elk Logosu",
"app_name": "Elk",
"attachment": {
"edit_title": "Açıklama",
"remove_label": "Eki kaldır"
},
"command": {
"activate": "Etkinleştir",
"complete": "Tamamla",
"compose_desc": "Yeni bir gönderi yaz",
"n-people-in-the-past-n-days": "geçen {1} gündeki {0} kişi",
"select_lang": "Dil seç",
"sign_in_desc": "Var olan bir hesap ekle",
"switch_account": "{0} hesabına geç",
"switch_account_desc": "Başka bir hesaba geç",
"toggle_dark_mode": "Karanlık mod durumunu değiştir",
"toggle_zen_mode": "Zen mod durumunu değiştir"
},
"common": {
"confirm_dialog": {
"cancel": "Hayır",
"confirm": "Evet",
"title": "Emin misiniz?"
},
"end_of_list": "Listenin sonu",
"error": "HATA",
"in": "içinde",
"not_found": "404 Bulunamadı",
"offline_desc": "Çevrimdışısınız gibi görünüyor. Lütfen internet bağlantınızı kontrol edin."
},
"compose": {
"draft_title": "Taslak {0}",
"drafts": "Taslaklar ({v})"
},
"conversation": {
"with": "ile"
},
"error": {
"account_not_found": "Hesap {0} bulunamadı",
"explore-list-empty": "Şu anda hiçbir şey trend değil. Daha sonra tekrar kontrol edin!",
"file_size_cannot_exceed_n_mb": "Dosya boyutu {0}MB'ı geçemez",
"sign_in_error": "Sunucuya bağlanılamadı.",
"status_not_found": "Gönderi bulunamadı",
"unsupported_file_format": "Desteklenmeyen dosya biçimi"
},
"help": {
"desc_highlight": "Orada burada bir kaç hata ve eksik özellik bekleyin.",
"desc_para1": "Elk'i, bizim çalışması devam eden Mastodon web istemcimizi, denemedeki ilginiz için teşekkürler!",
"desc_para2": "Zaman içinde geliştirmek ve iyileştirmek için çok çalışıyoruz.",
"desc_para3": "Geliştirmemizi hızlandırmak için takıma Github Sponsors üzerinden sponsor olabilirsinizi. Umarız Elk'i beğenirsiniz!",
"desc_para4": "Elk açık kaynaklıdır. Test etmek, geri dönüş vermek veya katkıda bulunmak isterseniz,",
"desc_para5": "GitHub'da bize ulaşın",
"desc_para6": "ve dahil olun.",
"title": "Elk ön izlemede!"
},
"language": {
"search": "Ara"
},
"menu": {
"block_account": "Engele {0}",
"block_domain": "Alan adı {0} engelle",
"copy_link_to_post": "Bu gönderinin linkini kopyala",
"delete": "Sil",
"delete_and_redraft": "Sil & yeniden taslak yap",
"delete_confirm": {
"cancel": "İptal et",
"confirm": "Sil",
"title": "Bu gönderiyi silmek istediğinizden emin misiniz?"
},
"direct_message_account": "{0} özel mesaj gönder",
"edit": "Düzenle",
"hide_reblogs": "{0} boostlarını gizle",
"mention_account": "{0} etiketle",
"mute_account": "{0} sustur",
"mute_conversation": "Bu gönderiyi sustur",
"open_in_original_site": "Orijinal sitede aç",
"pin_on_profile": "Profilde sabitle",
"share_post": "Bu gönderiyi paylaş",
"show_favourited_and_boosted_by": "Favoriye ekleyenleri ve boost edenleri göster",
"show_reblogs": "{0} boostlarını göster",
"show_untranslated": "Çevrilmemiş halini göster",
"toggle_theme": {
"dark": "Karanlık mod durumunu değiştir",
"light": "Aydınlık mod durumunu değiştir"
},
"translate_post": "Gönderiyi çevir",
"unblock_account": "{0} engelini kaldır",
"unblock_domain": "Alan adı {0} engelini kaldır",
"unmute_account": "{0} sesini aç",
"unmute_conversation": "Gönderinin sesini aç",
"unpin_on_profile": "Profildeki sabiti kaldır"
},
"nav": {
"back": "Geri git",
"blocked_domains": "Engellenen alan adları",
"blocked_users": "Engellenen kullanıcılar",
"bookmarks": "Yer imleri",
"built_at": "{0} derlendi",
"conversations": "Konuşmalar",
"explore": "Keşfet",
"favourites": "Favoriler",
"federated": "Federe",
"home": "Ev",
"local": "Yerel",
"muted_users": "Susturulmuş kullanıcılar",
"notifications": "Bildirimler",
"profile": "Profil",
"search": "Ara",
"select_feature_flags": "Özelliklerin Durumunu Değiştir",
"select_font_size": "Font Boyutu",
"select_language": "Görünüm Dili",
"settings": "Ayarlar",
"show_intro": "Girişi göster",
"toggle_theme": "Temayı Değiştir",
"zen_mode": "Zen Modu"
},
"notification": {
"favourited_post": "gönderini favoriledi",
"followed_you": "Seni takip etti",
"followed_you_count": "{0} kişi seni takip etti",
"missing_type": "EKSİK notification.type:",
"reblogged_post": "gönderini yeniden blogladı",
"request_to_follow": "Takip isteği attı",
"signed_up": "Kaydoldu",
"update_status": "Gönderisini güncelledi"
},
"placeholder": {
"content_warning": "Uyarını buraya yaz",
"default_1": "Aklında ne var?",
"reply_to_account": "{0} cevap ver",
"replying": "Cevap veriliyor",
"the_thread": "konu"
},
"pwa": {
"dismiss": "Görmezden gel",
"title": "Yeni Elk güncellemesi mevcut!",
"update": "Güncelle",
"update_available_short": "Elk'i güncelle",
"webmanifest": {
"canary": {
"description": "Hızlı bir Mastodon web istemcisi (canary)",
"name": "Elk (canary)",
"short_name": "Elk (canary)"
},
"dev": {
"description": "Hızlı bir Mastodon web istemcisi (dev)",
"name": "Elk (dev)",
"short_name": "Elk (dev)"
},
"preview": {
"description": "Hızlı bir Mastodon web istemcisi (preview)",
"name": "Elk (preview)",
"short_name": "Elk (preview)"
},
"release": {
"description": "Hızlı bir Mastodon web istemcisi",
"name": "Elk",
"short_name": "Elk"
}
}
},
"search": {
"search_desc": "İnsanları & etiketleri ara",
"search_empty": "Bu arama için sonuç bulunamadı"
},
"settings": {
"about": {
"label": "Hakkında",
"meet_the_team": "Takım ile buluş",
"sponsor_action": "Bize spponsor ol",
"sponsor_action_desc": "Elk'i geliştiren takıma destek olmak için",
"sponsors": "Sponsorlar",
"sponsors_body_1": "Elk cömert sponsorluk ve şunların yardımı sayesinde mümkün oldu:",
"sponsors_body_2": "Ve Elk takımına ve üyelerine sponsor olan tüm şirketler ve şahıslar.",
"sponsors_body_3": "Eğer uygulamadan hoşlandıysanız bize sponsor olmayı düşünün:"
},
"account_settings": {
"description": "Mastodon UI'da hesap ayarlarını değiştir",
"label": "Hesap ayarları"
},
"interface": {
"color_mode": "Renk Modu",
"dark_mode": "Karanlık Mod",
"default": " (varsayılan)",
"font_size": "Font Boyutu",
"label": "Arayüz",
"light_mode": "Aydınlık Mod",
"size_label": {
"lg": "Büyük",
"md": "Orta",
"sm": "Küçük",
"xl": "Çok büyük",
"xs": "Çok küçük"
},
"system_mode": "Sistem"
},
"language": {
"display_language": "Görünüm Dili",
"label": "Dil"
},
"notifications": {
"label": "Bildirimler",
"notifications": {
"label": "Bildirim ayarları"
},
"push_notifications": {
"alerts": {
"favourite": "Favoriler",
"follow": "Yeni takipçiler",
"mention": "Bahsetmeler",
"poll": "Anketler",
"reblog": "Gönderinizi yeniden bloglamalar",
"title": "Hangi bildirimleri alacaksınız??"
},
"description": "Elk'i kullanmıyorken bile bildirimleri alın.",
"instructions": "@:settings.notifications.push_notifications.save_settings butonunu kullanarak değişikleri kaydetmeyi unutmayın!",
"label": "Anlık bildirim ayarları",
"policy": {
"all": "Herkesden",
"followed": "Takip ettiğim kişilerden",
"follower": "Takipçilerden",
"none": "Kimseden",
"title": "Kimden bildirim alabilirim??"
},
"save_settings": "Ayarları kaydet",
"subscription_error": {
"clear_error": "Hatayı temizle",
"permission_denied": "Erişim engellendi: tarayıcınızda bildirimleri etkinleştirin.",
"request_error": "Abonelik talep edilirken bir hata oluştu, tekrar deneyin ve hata devam ederse lütfen sorunu Elk deposuna bildirin.",
"title": "Anlık bildirimlere abone olunamadı",
"too_many_registrations": "Tarayıcı kısıtlamaları nedeniyle Elk, farklı sunuculardaki birden çok hesap için anlık bildirimler hizmetini kullanamaz. Başka bir hesaptaki anlık bildirim aboneliğinden çıkmalı ve tekrar denemelisiniz."
},
"title": "Anlık bildirim ayarları",
"undo_settings": "Değişiklikleri geri al",
"unsubscribe": "Anlık bildirimleri devre dışı bırak",
"unsupported": "Tarayıcınız anlık bildirimleri desteklemiyor.",
"warning": {
"enable_close": "Kapat",
"enable_description": "Elk açık değilken bildirim almak için anlık bildirimleri etkinleştirin. Etkinleştirildikten sonra yukarıdaki \"@:settings.notifications.show_btn{'\"'} düğmesini kullanarak tam olarak ne tür etkileşimlerin anlık bildirimler oluşturduğunu kontrol edebilirsiniz.",
"enable_description_desktop": "Elk açık değilken bildirim almak için anlık bildirimleri etkinleştirin. Etkinleştirildikten sonra \"Ayarlar > Bildirimler > Anlık bildirim ayarları\"nda hangi tür etkileşimlerin anlık bildirimler oluşturduğunu tam olarak kontrol edebilirsiniz.",
"enable_description_mobile": "Ayarlara \"Ayarlar > Bildirimler > Anlık bildirim ayarları\" gezinme menüsünü kullanarak da erişebilirsiniz.",
"enable_description_settings": "Elk açık değilken bildirim almak için anlık bildirimleri etkinleştirin. Etkinleştirdikten sonra, aynı ekranda hangi tür etkileşimlerin anlık bildirimleri oluşturduğunu tam olarak kontrol edebileceksiniz.",
"enable_desktop": "Anlık bildirimleri etkinleştir",
"enable_title": "Hiçbirşeyi kaçırma",
"re_auth": "Görünüşe göre sunucunuz anlık bildirimleri desteklemiyor. Çıkış yapmayı deneyin ve tekrar giriş yapın, bu mesaj hala görünüyorsa sunucu yöneticinizle iletişime geçin."
}
},
"show_btn": "Bildirim ayarlarına git"
},
"notifications_settings": "Bildirimler",
"preferences": {
"github_cards": "GitHub Cards",
"hide_boost_count": "Boost sayısını gizle",
"hide_favorite_count": "Favori sayısını gizle",
"hide_follower_count": "Takipçi sayısını gizle",
"label": "Ayarlar",
"title": "Deneysel Özellikler",
"user_picker": "Kullanıcı Seçici",
"virtual_scroll": "Görsel Kaydırma"
},
"profile": {
"appearance": {
"bio": "Açıklama",
"description": "avatar, kullanıcı adı, profil vb. düzenle",
"display_name": "Görünen ad",
"label": "Görünüm",
"profile_metadata": "Profil üstverisi",
"profile_metadata_desc": "Profilinizde bir tablo olarak görüntülenen en fazla {0} öğeye sahip olabilirsiniz.",
"title": "Profili düzenle"
},
"featured_tags": {
"description": "İnsanlar bu etiketlerler altında herkese açık gönderilerinize göz atabilir.",
"label": "Öne çıkan etiketler"
},
"label": "Profil"
},
"select_a_settings": "Bir ayar seç",
"users": {
"export": "Kullanıcı Tokenlerini Dışa Aktar",
"import": "Kullanıcı Tokenlerini İçe Aktar",
"label": "Giriş yapılan kullanıcılar"
}
},
"state": {
"attachments_exceed_server_limit": "Ek sayısı gönderi başına sınırı aştı.",
"attachments_limit_error": "Gönderi başına sınır aşıldı",
"edited": "(Düzenlendi)",
"editing": "Düzenleniyor",
"loading": "Yükleniyor...",
"publishing": "Yayımlanıyor",
"upload_failed": "Yükleme başarısız",
"uploading": "Yükleniyor..."
},
"status": {
"boosted_by": "Tarafından boostlandı:",
"edited": "Düzenlendi {0}",
"favourited_by": "Tarafından favorilendi:",
"filter_hidden_phrase": "Tarafından filtrelendi:",
"filter_removed_phrase": "Filtre tarafından silindi",
"filter_show_anyway": "Yine de göster",
"img_alt": {
"desc": "Açıklama",
"dismiss": "Görmezden gel"
},
"poll": {
"count": "{0} oy",
"ends": "{0} biter",
"finished": "{0} bitti"
},
"reblogged": "{0} yeniden blogladı",
"replying_to": "{0} cevap veriliyor",
"show_full_thread": "Tüm konuyu göster",
"someone": "biri",
"spoiler_show_less": "Daha az göster",
"spoiler_show_more": "Daha çok göster",
"thread": "Konu",
"try_original_site": "Orijinal siteyi dene"
},
"status_history": {
"created": "{0} oluşturuldu",
"edited": "{0} düzenlendi"
},
"tab": {
"for_you": "Senin için",
"hashtags": "Etiketler",
"media": "Medya",
"news": "Haberler",
"notifications_all": "Hepsi",
"notifications_mention": "Bahsetmeler",
"posts": "Gönderiler",
"posts_with_replies": "Gönderiler & Yanıtlar"
},
"tag": {
"follow": "Takip et",
"follow_label": "{0} etiketini takip et",
"unfollow": "Takibi bırak",
"unfollow_label": "{0} etiketini takibi bırak"
},
"time_ago_options": {
"day_future": "{n} gün içinde",
"day_past": "{n} gün önce",
"hour_future": "{n} saat içinde",
"hour_past": "{n} saat önce",
"just_now": "şimdi",
"minute_future": "{n} dakika içinde",
"minute_past": "{n} dakika önce",
"month_future": "{n} ay içinde",
"month_past": "{n} ay önce",
"second_future": "şimdi|{n} saniye içinde",
"second_past": "şimdi|{n} saniye önce",
"short_day_future": "{n} günde",
"short_day_past": "{n}d",
"short_hour_future": "{n} saatte",
"short_hour_past": "{n}h",
"short_minute_future": "{n} dakikada",
"short_minute_past": "{n}min",
"short_month_future": "{n} ayda",
"short_month_past": "{n}mo",
"short_second_future": "{n} saniyede",
"short_second_past": "{n}s",
"short_week_future": "{n} haftada",
"short_week_past": "{n}w",
"short_year_future": "{n} yılda",
"short_year_past": "{n}y",
"week_future": "{n} hafta içinde",
"week_past": "{n} hafta önce",
"year_future": "{n} yıl içinde",
"year_past": "{n} yıl önce"
},
"timeline": {
"show_new_items": "{v} yeni öğe göster",
"view_older_posts": "Diğer sunuculardan eski gönderiler görüntülenmeyebilir."
},
"title": {
"federated_timeline": "Federe Edilmiş Zaman Akışı",
"local_timeline": "Yerel Zaman Akışı"
},
"tooltip": {
"add_content_warning": "İçerik uyarısı ekle",
"add_emojis": "Emoji ekle",
"add_media": "resim, video yada ses dosyası ekle",
"add_publishable_content": "Yayımlanacak içerik ekle",
"change_content_visibility": "İçerik görünürlüğünü değiştir",
"change_language": "Dil değiştir",
"emoji": "Emoji",
"explore_links_intro": "Bu haberler, şu anda merkezi olmayan ağın bu ve diğer sunucularındaki insanlar tarafından konuşuluyor.",
"explore_posts_intro": "Bu gönderiler, şu anda merkezi olmayan ağın bu ve diğer sunucularındaki insanlar tarafından ilgi görüyor.",
"explore_tags_intro": "Bu etiketler, şu anda merkezi olmayan ağın bu ve diğer sunucularındaki insanlar tarafından ilgi görüyor.",
"toggle_code_block": "Kod bloğu durumunu değiştir"
},
"user": {
"add_existing": "Var olan bir hesap ekle",
"server_address_label": "Mastodon Sunucu Adresi",
"sign_in_desc": "Profilleri veya hashtag'leri takip etmek, favorilere eklemek, gönderileri paylaşmak ve yanıtlamak veya farklı bir sunucudaki hesabınızdan etkileşim kurmak için oturum açın.",
"sign_in_notice_title": "{0} herkese açık veri görüntüleniyor",
"sign_out_account": "{0} çıkış yap",
"tip_no_account": "Eğer bir Mastodon hesabınız yoksa, {0}.",
"tip_register_account": "sunucunuzu seçin ve kaydolun"
},
"visibility": {
"direct": "Direkt",
"direct_desc": "Sadece bahsedilen kullanıcılara görünür",
"private": "Sadece takipçiler",
"private_desc": "Sadece takipçilere görünür",
"public": "Herkese açık",
"public_desc": "Herkese görünür",
"unlisted": "Liste dışı",
"unlisted_desc": "Herkes tarafından görülebilir, ancak keşif özellikleri devre dışı bırakılmıştır"
}
}

View file

@ -35,7 +35,9 @@
"profile_unavailable": "个人资料不可见", "profile_unavailable": "个人资料不可见",
"unblock": "取消拉黑", "unblock": "取消拉黑",
"unfollow": "取消关注", "unfollow": "取消关注",
"unmute": "取消屏蔽" "unmute": "取消屏蔽",
"view_other_followers": "其他站点上的关注者可能不会在这里显示。",
"view_other_following": "其他站点上正在关注的人可能不会在这里显示。"
}, },
"action": { "action": {
"apply": "应用", "apply": "应用",
@ -63,7 +65,7 @@
"switch_account": "切换帐号", "switch_account": "切换帐号",
"vote": "投票" "vote": "投票"
}, },
"app_desc_short": "用 🧡 制作的 Mastodon 客户端", "app_desc_short": "一个灵巧的 Mastodon 客户端",
"app_logo": "应用图标", "app_logo": "应用图标",
"app_name": "鹿鸣", "app_name": "鹿鸣",
"attachment": { "attachment": {
@ -86,7 +88,7 @@
"confirm_dialog": { "confirm_dialog": {
"cancel": "否", "cancel": "否",
"confirm": "是", "confirm": "是",
"title": "你确定吗?" "title": "你确定 {0} 吗?"
}, },
"end_of_list": "列表到底啦", "end_of_list": "列表到底啦",
"error": "错误", "error": "错误",
@ -162,6 +164,7 @@
"blocked_users": "已拉黑的用户", "blocked_users": "已拉黑的用户",
"bookmarks": "书签", "bookmarks": "书签",
"built_at": "构建于 {0}", "built_at": "构建于 {0}",
"compose": "撰写",
"conversations": "私信", "conversations": "私信",
"explore": "探索", "explore": "探索",
"favourites": "喜欢", "favourites": "喜欢",
@ -204,22 +207,22 @@
"update_available_short": "更新鹿鸣", "update_available_short": "更新鹿鸣",
"webmanifest": { "webmanifest": {
"canary": { "canary": {
"description": "用 🧡 制作的 Mastodon 客户端Canary", "description": "一个灵巧的 Mastodon 客户端Canary",
"name": "鹿鸣 Canary", "name": "鹿鸣 Canary",
"short_name": "鹿鸣 Canary" "short_name": "鹿鸣 Canary"
}, },
"dev": { "dev": {
"description": "用 🧡 制作的 Mastodon 客户端(开发版)", "description": "一个灵巧的 Mastodon 客户端(开发版)",
"name": "鹿鸣 开发版", "name": "鹿鸣 开发版",
"short_name": "鹿鸣 开发版" "short_name": "鹿鸣 开发版"
}, },
"preview": { "preview": {
"description": "用 🧡 制作的 Mastodon 客户端(预览版)", "description": "一个灵巧的 Mastodon 客户端(预览版)",
"name": "鹿鸣 预览版", "name": "鹿鸣 预览版",
"short_name": "鹿鸣 预览版" "short_name": "鹿鸣 预览版"
}, },
"release": { "release": {
"description": "用 🧡 制作的 Mastodon 客户端", "description": "一个灵巧的 Mastodon 客户端",
"name": "鹿鸣", "name": "鹿鸣",
"short_name": "鹿鸣" "short_name": "鹿鸣"
} }
@ -237,26 +240,21 @@
"description": "在 Mastodon UI 中编辑你的账号设置", "description": "在 Mastodon UI 中编辑你的账号设置",
"label": "账号设置" "label": "账号设置"
}, },
"feature_flags": {
"github_cards": "GitHub 卡片",
"title": "实验功能",
"user_picker": "用户选择器",
"virtual_scroll": "虚拟滚动"
},
"interface": { "interface": {
"color_mode": "颜色", "color_mode": "颜色",
"dark_mode": "深色模式", "dark_mode": "深色",
"default": "(默认)", "default": "(默认)",
"font_size": "字号", "font_size": "字号",
"label": "外观", "label": "外观",
"light_mode": "浅色模式", "light_mode": "浅色",
"size_label": { "size_label": {
"lg": "大", "lg": "大",
"md": "中", "md": "中",
"sm": "小", "sm": "小",
"xl": "特大", "xl": "特大",
"xs": "特小" "xs": "特小"
} },
"system_mode": "跟随系统"
}, },
"language": { "language": {
"display_language": "首选语言", "display_language": "首选语言",
@ -312,7 +310,14 @@
}, },
"notifications_settings": "通知", "notifications_settings": "通知",
"preferences": { "preferences": {
"label": "首选项" "github_cards": "GitHub 卡片",
"hide_boost_count": "隐藏转发数",
"hide_favorite_count": "隐藏收藏数",
"hide_follower_count": "隐藏关注者数",
"label": "首选项",
"title": "实验功能",
"user_picker": "用户选择器",
"virtual_scroll": "虚拟滚动"
}, },
"profile": { "profile": {
"appearance": { "appearance": {
@ -432,6 +437,7 @@
}, },
"tooltip": { "tooltip": {
"add_content_warning": "添加内容警告标识", "add_content_warning": "添加内容警告标识",
"add_emojis": "添加表情符号",
"add_media": "添加图片、视频或者音频文件", "add_media": "添加图片、视频或者音频文件",
"add_publishable_content": "添加要发布的内容", "add_publishable_content": "添加要发布的内容",
"change_content_visibility": "修改内容是否可见", "change_content_visibility": "修改内容是否可见",

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