Compare commits

...

146 commits

Author SHA1 Message Date
b217b4dc25
Merge branch 'pull-v0.13.2'
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-04-07 14:35:26 +02:00
f2edec194f
feat(config): switch default instance
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
set fedi.femby.page as default instance
2024-04-07 14:31:19 +02:00
087f8cc992
fix(ci): add missing docker pushes 2024-04-07 14:29:15 +02:00
6efbf8768b
fix(ci): add secrets 2024-04-07 14:29:15 +02:00
cd934fcd55
fix(ci): docker login 2024-04-07 14:29:15 +02:00
7f1e97a37c
fix(ci): remove docker login 2024-04-07 14:29:15 +02:00
58c84b2f9b
fix(ci): add docker login 2024-04-07 14:29:14 +02:00
322ffeef12
feat(ci): add docker push test 2024-04-07 14:29:14 +02:00
4a8110a52e
fix(ci): add missing flags docker buildx 2024-04-07 14:29:14 +02:00
d481c0ae7a
fix(ci): added missing flag for cut 2024-04-07 14:29:14 +02:00
68ffb627e7
fix(ci): deleted sh uncompatible line 2024-04-07 14:29:14 +02:00
b221687139
feat(ci): Made ci sh compatible (hopefully^^) 2024-04-07 14:29:14 +02:00
7b10620cca
feat(ci): Add build tags 2024-04-07 14:29:13 +02:00
498face3cf
test(ci): 2024-04-07 14:29:13 +02:00
672ff46072
test(ci): add git to image 2024-04-07 14:29:13 +02:00
81b5f8b8e7
test(ci): another syntax test 2024-04-07 14:29:13 +02:00
721e55be06
test(ci): another syntax test 2024-04-07 14:29:13 +02:00
eba4cc0757
test(ci): another syntax test 2024-04-07 14:29:13 +02:00
80818ed52d
test(ci): syntax test 2024-04-07 14:28:32 +02:00
patak-dev
25fb7c1c97 chore: release v0.13.2 2024-04-05 18:29:15 +02:00
TAKAHASHI Shuuji
839aa52e86
fix: adjust background spacing in direct message (#2764) 2024-04-05 16:23:52 +00:00
TAKAHASHI Shuuji
9ff55289ea
ci(ci.yml): limit maximum execution time of ci test (#2763) 2024-04-05 16:19:53 +00:00
Joaquín Sánchez
73293fbcd3
chore: update nuxt to 3.11.2 (#2755) 2024-04-05 14:34:42 +00:00
TAKAHASHI Shuuji
a27c218802
chore: update github avatar images (#2762) 2024-04-05 14:34:19 +00:00
Emanuel Pina
f8fc0efadc
feat(i18n): Update portuguese from Portugal translation (#2756) 2024-04-05 09:16:30 +00:00
Joaquín Sánchez
618a5b2df3
chore(ui): use full width in settings toggle items (#2754) 2024-04-05 09:16:11 +00:00
lazzzis
1146dca5f6
fix(ui): media preview card is misaligned (#2751) 2024-04-05 09:15:44 +00:00
Joaquín Sánchez
f86e856ee6
feat(i18n): add missing spanish translations (gifs, docs and contributing) (#2753) 2024-04-04 18:36:03 +00:00
renovate[bot]
6d13d61227
chore(deps): update devdependencies (#2725)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: TAKAHASHI Shuuji <shuuji3@gmail.com>
2024-04-04 16:47:27 +00:00
Francesco
0de9825bf2
feat(i18n): Update it-IT locale (#2750) 2024-04-04 13:25:09 +00:00
Joaquín Sánchez
3f0b234cc4
feat(ui): add max. file size check before upload attachment (#2709)
Co-authored-by: TAKAHASHI Shuuji <shuuji3@gmail.com>
2024-04-04 10:28:18 +00:00
lazzzis
8f04ea8eee
feat(ui): improve gif support (#2752) 2024-04-04 10:27:52 +00:00
Emanuel Pina
7dcafa3fe0
feat(i18n): Update portuguese from Portugal translation (#2747) 2024-04-04 05:17:03 +00:00
Joaquín Sánchez
bead2183b2
fix(ui): don't scroll on settings item click when external or _blank target link (#2742) 2024-04-03 13:59:42 +00:00
patak
59dda09cd4
fix: notifications in update timeline (#2740) 2024-04-03 13:59:24 +00:00
lazzzis
d0b115751f
feat(ui): style blockquote (#2744) 2024-04-03 04:17:52 +00:00
lazzzis
c6787aae3f
fix(ui): clicking on custom emoji does not navigate to status detail (#2743) 2024-04-03 04:17:30 +00:00
patak
9025416ab3
feat: update info dialog (#2741) 2024-04-02 03:17:45 +00:00
TAKAHASHI Shuuji
aa28257754
feat: add "Documentation" link to "About" page (#2734) 2024-04-01 14:57:11 +00:00
TAKAHASHI Shuuji
d807e06fa0
refactor: various typo fixes (#2735) 2024-04-01 14:56:30 +00:00
TAKAHASHI Shuuji
611d556936
feat: put sign-in icon to default "Sign in" button (#2736) 2024-04-01 14:55:31 +00:00
Xabi
4313002950
feat(i18n): update eu-ES.json (#2737) 2024-04-01 14:54:37 +00:00
TAKAHASHI Shuuji
de11a60b17
feat: add "How to contribute?" link next to language status (#2733) 2024-04-01 05:27:47 +00:00
TAKAHASHI Shuuji
5064b269e7
feat: show background color for direct post to reduce mistake (#2732) 2024-04-01 05:26:51 +00:00
TAKAHASHI Shuuji
d8d9975756
fix: hide duplicated status actions items on details page in zen mode (#2731) 2024-04-01 05:25:53 +00:00
TAKAHASHI Shuuji
eee671cdc3
fix: improve keyboard operability especially on search page and editor (#2730)
Co-authored-by: patak <583075+patak-dev@users.noreply.github.com>
2024-04-01 05:24:42 +00:00
lazzzis
587c063aba
fix(ui): remove a duplicated command (#2718) 2024-03-30 04:19:22 +00:00
lazzzis
28514e956d
fix(ui): wrong polls are removed (#2720) 2024-03-30 04:18:33 +00:00
lazzzis
42aeb8fa35
fix(ui): prevent from navigating to search page when opening command panel (#2719) 2024-03-30 04:15:58 +00:00
Joaquín Sánchez
f6f50a582e
fix(ui): change status actions title (#2717) 2024-03-29 20:04:55 +00:00
patak-dev
f86818867b chore: release v0.13.1 2024-03-29 16:37:28 +01:00
Sma11X
82d962a54b
fix: add missing notification event type (#2714) 2024-03-29 15:31:53 +00:00
renovate[bot]
1b189043e4
chore(deps): update devdependencies (major) (#2400)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 08:51:07 +00:00
renovate[bot]
a4867566d9
chore(deps): update devdependencies (#2697)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 08:34:06 +00:00
renovate[bot]
0757db69b2
chore(deps): update dependency @types/prettier to v3 (#2712)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 08:33:11 +00:00
renovate[bot]
f0de25c992
chore(deps): update dependency @antfu/eslint-config to ^2.9.0 (#2711)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 08:32:33 +00:00
TAKAHASHI Shuuji
660549b08b
chore: update masto to v6.7.0 (#2708) 2024-03-21 15:18:53 +00:00
Joaquín Sánchez
7807730118
feat(i18n): add missing spanish mute duration dialog entries (#2696) 2024-03-21 12:37:57 +00:00
Joaquín Sánchez
b526db0860
chore: update i18n module to 8.2.0 (#2703) 2024-03-21 12:13:28 +00:00
Sma11X
0133324ded
fix: correct local timeline stream (#2707) 2024-03-21 10:08:51 +00:00
TAKAHASHI Shuuji
e9ab0cd40b
fix: prevent showing notification errors for dev for known emoji reaction types (#2704) 2024-03-19 15:04:16 +00:00
Joaquín Sánchez
9251ec496b
chore: add ofetch to typescript.hoist (#2693)
Co-authored-by: Daniel Roe <daniel@roe.dev>
2024-03-19 12:56:14 +00:00
renovate[bot]
bd4cd02b2b
chore(deps): update pnpm to v8.15.5 (#2698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 12:40:13 +00:00
Joaquín Sánchez
74ccfece5d
chore: update nuxt to 3.11.1 (#2702) 2024-03-19 12:36:25 +00:00
35b4c80311
fix(ci): add missing docker pushes
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-03-18 14:50:56 +01:00
f6a081d199 Merge pull request 'feat-add-woodpecker' (#1) from feat-add-woodpecker into main
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #1
2024-03-18 13:42:55 +00:00
5e419045ed
fix(ci): add secrets
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2024-03-18 14:24:49 +01:00
cfdbb66b96
fix(ci): docker login 2024-03-18 14:16:41 +01:00
c717aace86
fix(ci): remove docker login 2024-03-18 14:10:31 +01:00
6713329a5e
fix(ci): add docker login 2024-03-18 14:00:02 +01:00
7470468077
feat(ci): add docker push test 2024-03-18 13:53:05 +01:00
9044b3bcad
fix(ci): add missing flags docker buildx 2024-03-18 13:41:39 +01:00
5bd9dce91e
fix(ci): added missing flag for cut 2024-03-18 13:39:40 +01:00
12e14cfc05
fix(ci): deleted sh uncompatible line 2024-03-18 13:37:51 +01:00
1ea4399765
feat(ci): Made ci sh compatible (hopefully^^) 2024-03-18 13:34:44 +01:00
991cb812da
feat(ci): Add build tags 2024-03-18 13:26:08 +01:00
Sma11X
c89e499f96
fix: pre tag overwritten by default style (#2699) 2024-03-18 11:34:03 +00:00
Dohány Tamás
89e3582dd7
feat(i18n): Update hu-HU.json (#2694) 2024-03-17 19:11:42 +00:00
TAKAHASHI Shuuji
48c013709a
ci(docker.yml): support linux/arm64 container (#2691) 2024-03-17 15:15:04 +00:00
renovate[bot]
f90f0a2e61
chore(deps): update pnpm to v8.15.4 (#2627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-17 15:14:19 +00:00
renovate[bot]
c58b585855
chore(deps): update lint (#2399)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-17 15:14:04 +00:00
Joaquín Sánchez
ded2e0f3d7
chore: update nuxt to 3.11.0 (#2692) 2024-03-17 14:32:02 +00:00
9b77efe1d7
test(ci): 2024-03-16 15:52:25 +01:00
aec670a903
test(ci): add git to image 2024-03-16 15:48:38 +01:00
1218f92133
test(ci): another syntax test 2024-03-16 15:45:13 +01:00
7c0986a005
test(ci): another syntax test 2024-03-16 15:42:49 +01:00
ac01207283
test(ci): another syntax test 2024-03-16 15:37:04 +01:00
ef169d682f
test(ci): syntax test 2024-03-16 15:33:04 +01:00
Joaquín Sánchez
21d5633233
chore: bump to eslint-config to 2.8.1 (#2685) 2024-03-13 06:39:28 +00:00
Joaquín Sánchez
7703565c75
fix(ui): hashtags not working when composing (#2686) 2024-03-12 20:47:05 +00:00
cuithon
5a9546ec0a
chore: fix typo (#2681)
Signed-off-by: cuithon <dscs@outlook.com>
2024-03-12 07:58:20 +00:00
patak
bc30a8bd82 chore: release v0.13.0 2024-03-11 12:08:52 +01:00
Duy
c432c2bd0d
feat(i18n): Update vi-VN.json (#2664) 2024-03-11 10:55:18 +00:00
Francesco
364fbd350b
feat(i18n): Update it-IT locale (#2666) 2024-03-11 10:55:05 +00:00
Xabi
c64580f782
feat(i18n): update eu-ES.json (#2670) 2024-03-11 10:54:53 +00:00
Emanuel Pina
e7dfdafd59
feat(i18n): Update portuguese from Portugal translation (#2671) 2024-03-11 10:54:38 +00:00
Ayo Ayco
b06ec9356d
feat(i18n): update Tagalog translations (#2677) 2024-03-11 10:54:20 +00:00
TAKAHASHI Shuuji
3b1a66c93c
fix: fix vue/no-ref-as-operand and vue/return-in-computed-property ESLint errors (#2679) 2024-03-11 10:53:25 +00:00
TAKAHASHI Shuuji
ed8a1811cc
chore: upgrade @vueuse/core from 10.8.0 to 10.9.0 (#2674) 2024-03-10 18:38:37 +00:00
TAKAHASHI Shuuji
dfbe2e080d
fix: prevent empty search keyword to send invalid request (#2676) 2024-03-10 18:37:32 +00:00
TAKAHASHI Shuuji
0fd9374e8c
fix: fix incorrect follow status on followers and following pages (#2669) 2024-03-09 19:31:40 +00:00
TAKAHASHI Shuuji
1c8e48bee4
fix: show loading spinner on follow button while fetching account relationship (#2667)
Co-authored-by: patak <583075+patak-dev@users.noreply.github.com>
2024-03-09 18:44:44 +00:00
TAKAHASHI Shuuji
3448335356
feat: allow to set mute duration and notifications mute option (#2665) 2024-03-09 09:52:41 +00:00
Joaquín Sánchez
4954473f50
chore: extract bg and theme colors to constants (#2662) 2024-03-07 19:15:35 +00:00
TAKAHASHI Shuuji
efa17caf5e
fix: consistent hover highlight styling in mobile bottom navigation menus (#2661) 2024-03-07 19:14:20 +00:00
Joaquín Sánchez
df165f0023
fix(pwa): wrong web manifest colors (#2659) 2024-03-07 14:55:27 +00:00
TAKAHASHI Shuuji
0f583ece28
feat: remember last accessed explore tab (#2658) 2024-03-07 14:33:25 +00:00
Ivan Demchuk
d579977790
feat(i18n): update Ukrainian translations (#2660) 2024-03-07 13:41:24 +00:00
TAKAHASHI Shuuji
8786c83db7
fix: remember last accessed notification tab (#2654) 2024-03-06 22:00:07 +00:00
patak
1ce913e69d chore: release v0.12.1 2024-03-06 16:52:16 +01:00
Joaquín Sánchez
48a8b74e7c
fix(ui): mentions not working when composing (#2655) 2024-03-06 15:42:41 +00:00
patak
1ff13952b0 chore: release v0.12.0 2024-03-06 08:48:14 +01:00
Francesco
02f7c4b291
feat(i18n): Update it-IT locale (#2652) 2024-03-05 20:12:05 +00:00
Joaquín Sánchez
9da77637b2
chore: bump to eslint-config v2.8.0 (#2651)
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
2024-03-05 14:48:58 +00:00
Joaquín Sánchez
62f70250d5
fix(ui): wrong reply to account (#2649) 2024-03-05 13:21:58 +00:00
Joaquín Sánchez
873c62e9ef
feat(i18n): add missing nav.hashtags entry for Spanish translation (#2650) 2024-03-05 13:21:12 +00:00
Emanuel Pina
b1ff1e6277
feat(i18n): Update portuguese from Portugal translation (#2648) 2024-03-05 13:20:44 +00:00
TAKAHASHI Shuuji
f644148844
feat: introduce new "Followed tags" page (#2642)
Co-authored-by: patak <583075+patak-dev@users.noreply.github.com>
2024-03-05 10:27:10 +00:00
Joaquín Sánchez
3120bbb77f
feat(content-rich html parsing): add paragraphs LTR/RTL direction support (#2545) 2024-03-05 06:25:58 +00:00
renovate[bot]
6cbe65c9d8
chore(deps): update devdependencies (#2646)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-04 20:39:12 +00:00
qezwan
1c908363cb
feat(i18n): Add central kurdish locale(ckb) (#2332)
Co-authored-by: userquin <userquin@gmail.com>
2024-03-04 20:28:57 +00:00
Jafar Farganlooj
c01a15c930
feat(i18n): Add Persian translation (#2535)
Co-authored-by: userquin <userquin@gmail.com>
2024-03-04 19:59:03 +00:00
nonnullish
0c15aa55d8
fix: fix emoji placement (#2626) (#2645) 2024-03-04 19:56:59 +00:00
Joaquín Sánchez
9f04e17e57
fix(ui): avoid fetching status account in replying to until visible (#2638) 2024-03-04 19:55:02 +00:00
Joaquín Sánchez
308b50cbad
feat(ui): fetch account data on demand (#2632) 2024-03-04 19:20:13 +00:00
TAKAHASHI Shuuji
e44833b18a
feat: show tag hover card when hovering cursor on hashtag links (#2621)
Co-authored-by: userquin <userquin@gmail.com>
2024-03-04 16:45:25 +00:00
Joaquín Sánchez
0fa87f71a4
chore(tests): fix vitest can't terminate worker (#2644) 2024-03-04 16:41:38 +00:00
Emanuel Pina
edfbe2c3ed
feat(i18n): Update portuguese from Portugal translation (#2633) 2024-03-04 16:02:35 +00:00
Joaquín Sánchez
70c7e93919
refactor: update no reactivity transform changes (#2639) 2024-03-04 16:01:56 +00:00
TAKAHASHI Shuuji
95e466146d
fix: show correct reply target user account in reply post header (#2640) 2024-02-29 20:55:46 +00:00
Joaquín Sánchez
efec212a9f
fix(pwa): update pwa plugin to fix broken prompt (#2634) 2024-02-29 16:55:31 +00:00
Kevin Pliester
1844af0a41
feat(i18n): German translation for new shortcuts (#2641) 2024-02-29 16:09:04 +00:00
Joaquín Sánchez
72b80d4984
fix(ui): missing replying to links (#2637) 2024-02-28 18:02:09 +00:00
Francesco
6dc5a68c80
feat(i18n): Update it-IT locale (#2630) 2024-02-26 13:22:51 +00:00
TAKAHASHI Shuuji
310b32c123
fix: allow to edit alt description of attached image again (#2631) 2024-02-26 13:11:21 +00:00
Joaquín Sánchez
748dd5e19f
fix(cache): return cached account as promise (#2623) 2024-02-25 19:43:34 +00:00
Joaquín Sánchez
c00d6f7bf8
feat(ui): add missing goto magic keys spanish translation entries (#2625) 2024-02-25 19:39:57 +00:00
Joaquín Sánchez
fc5d248094
fix(ui): account mentions not being fetched when visible (#2624) 2024-02-25 19:28:38 +00:00
Joaquín Sánchez
6f20ce5bba
chore(test): add hanging-process reporter on CI (#2622) 2024-02-25 14:13:27 +00:00
TAKAHASHI Shuuji
edcc8741bf
feat: add several new shortcut keys for navigation (#2618) 2024-02-24 19:28:56 +00:00
renovate[bot]
3584151fab
fix(deps): update tiptap to v2.2.4 (#2398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: userquin <userquin@gmail.com>
2024-02-24 19:26:14 +00:00
Joaquín Sánchez
efb6967e6a
fix(ui): help preview tabindex, auto focus and buttons (#2616) 2024-02-24 18:24:55 +00:00
Joaquín Sánchez
eddbb1eee9
chore: cleanup isHydrated (#2614)
Co-authored-by: patak <583075+patak-dev@users.noreply.github.com>
2024-02-24 18:24:19 +00:00
Joaquín Sánchez
6b40319723
fix(ui): wrong tabindex usage 2 (#2617) 2024-02-24 18:23:37 +00:00
Joaquín Sánchez
913e2892f7
fix(ui): wrong tabindex usage (#2615) 2024-02-24 18:13:12 +00:00
renovate[bot]
a3c5272e07
chore(deps): update devdependencies (#2388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-24 16:56:38 +00:00
156 changed files with 7120 additions and 3620 deletions

View file

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

View file

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

View file

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

View file

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

45
.vscode/settings.json vendored
View file

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

22
.woodpecker.yml Normal file
View file

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

View file

@ -21,7 +21,6 @@ To develop and test the Elk package:
2. Ensure using the latest Node.js (16.x). 2. Ensure using the latest Node.js (16.x).
If you have [nvm](https://github.com/nvm-sh/nvm), you can run `nvm i` to install the required version. If you have [nvm](https://github.com/nvm-sh/nvm), you can run `nvm i` to install the required version.
3. The package manager used to install and link dependencies must be [pnpm](https://pnpm.io/) v7. To use it you must first enable [Corepack](https://github.com/nodejs/corepack) by running `corepack enable`. (Note: on Linux in a standard Node 16+ environment, you should follow the instructions to install via Node's `corepack` rather than using the `curl` command) 3. The package manager used to install and link dependencies must be [pnpm](https://pnpm.io/) v7. To use it you must first enable [Corepack](https://github.com/nodejs/corepack) by running `corepack enable`. (Note: on Linux in a standard Node 16+ environment, you should follow the instructions to install via Node's `corepack` rather than using the `curl` command)
4. Check out a branch where you can work and commit your changes: 4. Check out a branch where you can work and commit your changes:
@ -84,7 +83,7 @@ Simple approach used by most websites of relying on direction set in HTML elemen
We've added some `UnoCSS` utilities styles to help you with that: We've added some `UnoCSS` utilities styles to help you with that:
- Do not use `left/right` padding and margin: for example `pl-1`. Use `padding-inline-start/end` instead. So `pl-1` should be `ps-1`, `pr-1` should be `pe-1`. The same rules apply to margin. - Do not use `left/right` padding and margin: for example `pl-1`. Use `padding-inline-start/end` instead. So `pl-1` should be `ps-1`, `pr-1` should be `pe-1`. The same rules apply to margin.
- Do not use `rtl-` classes, such as `rtl-left-0`. - Do not use `rtl-` classes, such as `rtl-left-0`.
- For icons that should be rotated for RTL, add `class="rtl-flip"`. This can only be used for icons outside of elements with `dir="auto"`, such as timeline, and is the only exception from the rule above. For icons inside the timeline, it might not work as expected. - For icons that should be rotated for RTL, add `class="rtl-flip"`. This can only be used for icons outside of elements with `dir="auto"`, such as timeline, and is the only exception to the rule above. For icons inside the timeline, it might not work as expected.
- For absolute positioned elements, don't use `left/right`: for example `left-0`. Use `inset-inline-start/end` instead. `UnoCSS` shortcuts are `inset-is` for `inset-inline-start` and `inset-ie` for `inset-inline-end`. Example: `left-0` should be replaced with `inset-is-0`. - For absolute positioned elements, don't use `left/right`: for example `left-0`. Use `inset-inline-start/end` instead. `UnoCSS` shortcuts are `inset-is` for `inset-inline-start` and `inset-ie` for `inset-inline-end`. Example: `left-0` should be replaced with `inset-is-0`.
- If you need to change the border radius for an entire left or right side, use `border-inline-start/end`. `UnoCSS` shortcuts are `rounded-is` for left side, `rounded-ie` for right side. Example: `rounded-l-5` should be replaced with `rounded-ie-5`. - If you need to change the border radius for an entire left or right side, use `border-inline-start/end`. `UnoCSS` shortcuts are `rounded-is` for left side, `rounded-ie` for right side. Example: `rounded-l-5` should be replaced with `rounded-ie-5`.
- If you need to change the border radius for one corner, use `border-start-end-radius` and similar rules. `UnoCSS` shortcuts are `rounded` + top/bottom as either `-bs` (top) or `-be` (bottom) + left/right as either `-is` (left) or `-ie` (right). Example: `rounded-tl-0` should be replaced with `rounded-bs-is-0`. - If you need to change the border radius for one corner, use `border-start-end-radius` and similar rules. `UnoCSS` shortcuts are `rounded` + top/bottom as either `-bs` (top) or `-be` (bottom) + left/right as either `-is` (left) or `-ie` (right). Example: `rounded-tl-0` should be replaced with `rounded-bs-is-0`.

View file

@ -52,7 +52,6 @@ One could put Elk behind popular reverse proxies with SSL Handling like Traefik,
> [!NOTE] > [!NOTE]
> The provided Dockerfile creates a container which will eventually run Elk as non-root user and create a persistent named Docker volume upon first start (if that volume does not yet exist). This volume is always created with root permission. Failing to change the permissions of ```/elk/data``` inside this volume to UID:GID 911 (as specified for Elk in the Dockerfile) will prevent Elk from storing it's config for user accounts. You either have to fix the permission in the created named volume, or mount a directory with the correct permission to ```/elk/data``` into the container. > The provided Dockerfile creates a container which will eventually run Elk as non-root user and create a persistent named Docker volume upon first start (if that volume does not yet exist). This volume is always created with root permission. Failing to change the permissions of ```/elk/data``` inside this volume to UID:GID 911 (as specified for Elk in the Dockerfile) will prevent Elk from storing it's config for user accounts. You either have to fix the permission in the created named volume, or mount a directory with the correct permission to ```/elk/data``` into the container.
### Ecosystem ### 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: These are known deployments using Elk as an alternative Web client for Mastodon servers or as a base for other projects in the fediverse:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -44,7 +44,7 @@ When you are ready to submit work back to the main Elk repo, create a PR.
- base branch should be **main** - base branch should be **main**
- Head repository should be your fork - Head repository should be your fork
- Compare branch should be your working branch you want to submit - Compare branch should be your working branch you want to submit
If you don't see four drop downs, be sure you are comparing across forks. If you don't see four drop-downs, be sure you are comparing across forks.
10. Add a description of the changes your request makes 10. Add a description of the changes your request makes
11. Select **Add Pull Request** 11. Select **Add Pull Request**
@ -65,7 +65,7 @@ Avoid screenshots until Elk reaches a stable release.
### Standards ### Standards
Write in **American English** using spelling as found in [Merriam Webster](https://www.merriam-webster.com). Write in **American English** using spelling as found in [Merriam-Webster](https://www.merriam-webster.com).
Translation and localization will be handled separately as/when availability or necessity allow. Translation and localization will be handled separately as/when availability or necessity allow.
Use [**semantic linefeeds**](https://rhodesmill.org/brandon/2012/one-sentence-per-line/) with no more than one sentence per line. Use [**semantic linefeeds**](https://rhodesmill.org/brandon/2012/one-sentence-per-line/) with no more than one sentence per line.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

37
eslint.config.js Normal file
View file

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

687
locales/ckb.json Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

715
locales/fa-IR.json Normal file
View file

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

View file

@ -214,8 +214,7 @@
"command_mode": "Mode commande", "command_mode": "Mode commande",
"compose": "Composer", "compose": "Composer",
"favourite": "J'aime", "favourite": "J'aime",
"title": "Actions", "title": "Actions"
"zen_mode": "Mode Zen"
}, },
"media": { "media": {
"title": "Média" "title": "Média"
@ -550,9 +549,6 @@
"label": "Comptes connectés" "label": "Comptes connectés"
} }
}, },
"share-target": {
"title": ""
},
"share_target": { "share_target": {
"description": "Elk peut être configuré pour que vous puissiez partager du contenu à partir d'autres applications, installez simplement Elk sur votre appareil ou ordinateur et connectez-vous.", "description": "Elk peut être configuré pour que vous puissiez partager du contenu à partir d'autres applications, installez simplement Elk sur votre appareil ou ordinateur et connectez-vous.",
"hint": "Pour partager du contenu avec Elk, Elk doit être installé et vous devez être connecté.", "hint": "Pour partager du contenu avec Elk, Elk doit être installé et vous devez être connecté.",
@ -605,7 +601,6 @@
"edited": "a édité {0}" "edited": "a édité {0}"
}, },
"tab": { "tab": {
"account": "Compte",
"accounts": "Comptes", "accounts": "Comptes",
"for_you": "Pour vous", "for_you": "Pour vous",
"hashtags": "Hashtags", "hashtags": "Hashtags",

View file

@ -27,6 +27,7 @@
"follows_you": "Követ téged", "follows_you": "Követ téged",
"go_to_profile": "Ugrás a profilhoz", "go_to_profile": "Ugrás a profilhoz",
"joined": "Csatlakozott", "joined": "Csatlakozott",
"lock": "Zárolt",
"moved_title": "jelezte, hogy az új fiók mostantól:", "moved_title": "jelezte, hogy az új fiók mostantól:",
"muted_users": "Némított felhasználók", "muted_users": "Némított felhasználók",
"muting": "Némított", "muting": "Némított",
@ -79,7 +80,7 @@
"save": "Ment", "save": "Ment",
"save_changes": "Változások mentése", "save_changes": "Változások mentése",
"sign_in": "Bejelentkezés", "sign_in": "Bejelentkezés",
"sign_in_to": "Bejelentkezés: {0}", "sign_in_to": "Bejelentkezve: {0}",
"switch_account": "Fiók váltás", "switch_account": "Fiók váltás",
"vote": "Szavazás" "vote": "Szavazás"
}, },
@ -94,7 +95,7 @@
"activate": "Aktivál", "activate": "Aktivál",
"complete": "Befejez", "complete": "Befejez",
"compose_desc": "Új bejegyzés írása", "compose_desc": "Új bejegyzés írása",
"n-people-in-the-past-n-days": "{0} felhasználó az elmúlt {1} napban", "n_people_in_the_past_n_days": "{0} ember az elmúlt {1} napban",
"select_lang": "Nyelv kiválasztása", "select_lang": "Nyelv kiválasztása",
"sign_in_desc": "Létező fiók hozzáadása", "sign_in_desc": "Létező fiók hozzáadása",
"switch_account": "Váltás: {0}", "switch_account": "Váltás: {0}",
@ -120,12 +121,14 @@
"block_account": { "block_account": {
"cancel": "Mégsem", "cancel": "Mégsem",
"confirm": "Blokkol", "confirm": "Blokkol",
"description": "Biztosan blokkolja? {0}" "description": "Biztosan blokkolja? {0}",
"title": "Hozzáférés blokkolása"
}, },
"block_domain": { "block_domain": {
"cancel": "Mégsem", "cancel": "Mégsem",
"confirm": "Blokkol", "confirm": "Blokkol",
"description": "Biztosan blokkolja? {0}" "description": "Biztosan blokkolja? {0}",
"title": "Domain blokkolása"
}, },
"common": { "common": {
"cancel": "Mégsem", "cancel": "Mégsem",
@ -134,27 +137,37 @@
"delete_list": { "delete_list": {
"cancel": "Mégsem", "cancel": "Mégsem",
"confirm": "Töröl", "confirm": "Töröl",
"description": "Biztosan törli a listát? \"{0}\"" "description": "Biztosan törli a listát? \"{0}\"",
"title": "Lista törlése"
}, },
"delete_posts": { "delete_posts": {
"cancel": "Mégsem", "cancel": "Mégsem",
"confirm": "Töröl", "confirm": "Töröl",
"description": "Biztosan törli a bejegyzést?" "description": "Biztosan törli a bejegyzést?",
"title": "Bejegyzés törlése"
}, },
"mute_account": { "mute_account": {
"cancel": "Mégsem", "cancel": "Mégsem",
"confirm": "Némít", "confirm": "Némít",
"description": "Biztosan némítja? {0}" "days": "napok|nap|napok",
"description": "Biztosan némítja? {0}",
"hours": "órák|óra|órák",
"minute": "perc|perc|perc",
"notifications": "Értesítések némítása",
"specify_duration": "Határozzon meg némítási időtartamot",
"title": "Hozzáférés némítása"
}, },
"show_reblogs": { "show_reblogs": {
"cancel": "Mégsem", "cancel": "Mégsem",
"confirm": "Mutat", "confirm": "Mutat",
"description": "Biztosan megjeleníti a Turbót tőle? {0}" "description": "Biztosan megjeleníti a Kiemelést tőle? {0}",
"title": "Kiemelés megjenítése"
}, },
"unfollow": { "unfollow": {
"cancel": "Mégsem", "cancel": "Mégsem",
"confirm": "Követés leállítása", "confirm": "Követés leállítása",
"description": "Biztosan leállítja a követését?" "description": "Biztosan leállítja a követését?",
"title": "Követés elállítása"
} }
}, },
"conversation": { "conversation": {
@ -169,7 +182,7 @@
}, },
"error": { "error": {
"account_not_found": "Fiók {0} nem található", "account_not_found": "Fiók {0} nem található",
"explore_list-empty": "Nincs most itt semmi... Nézz vissza később!", "explore_list_empty": "Nincs most semmi érdekes. Térj vissza később!",
"file_size_cannot_exceed_n_mb": "A fájl mérete nem haladhatja meg a {0}MB-ot", "file_size_cannot_exceed_n_mb": "A fájl mérete nem haladhatja meg a {0}MB-ot",
"sign_in_error": "Nem lehetséges a csatlakozás.", "sign_in_error": "Nem lehetséges a csatlakozás.",
"status_not_found": "A bejegyzés nem található", "status_not_found": "A bejegyzés nem található",
@ -220,14 +233,25 @@
"compose": "Közzétesz", "compose": "Közzétesz",
"favourite": "Kedvenc", "favourite": "Kedvenc",
"search": "Keresés", "search": "Keresés",
"show_new_items": "Új elemek mutatása",
"title": "Műveletek" "title": "Műveletek"
}, },
"media": { "media": {
"title": "Média" "title": "Média"
}, },
"navigation": { "navigation": {
"go_to_bookmarks": "Könyvjelzők",
"go_to_conversations": "Párbeszédek",
"go_to_explore": "Felfedez",
"go_to_favourites": "Kedvencek",
"go_to_federated": "Összesített",
"go_to_home": "Otthon", "go_to_home": "Otthon",
"go_to_lists": "Listák",
"go_to_local": "Helyi",
"go_to_notifications": "Értesítések", "go_to_notifications": "Értesítések",
"go_to_profile": "Profil",
"go_to_search": "Keresés",
"go_to_settings": "Beállítások",
"next_status": "Következő bejegyzés", "next_status": "Következő bejegyzés",
"previous_status": "Előző bejegyzés", "previous_status": "Előző bejegyzés",
"shortcut_help": "Gyors segítség", "shortcut_help": "Gyors segítség",
@ -285,6 +309,7 @@
"explore": "Felfedezés", "explore": "Felfedezés",
"favourites": "Kedvencek", "favourites": "Kedvencek",
"federated": "Összesített", "federated": "Összesített",
"hashtags": "Címkék",
"home": "Otthon", "home": "Otthon",
"list": "Lista", "list": "Lista",
"lists": "Listák", "lists": "Listák",
@ -316,7 +341,7 @@
"placeholder": { "placeholder": {
"content_warning": "Írja ide figyelmeztetését", "content_warning": "Írja ide figyelmeztetését",
"default_1": "Mi jár a fejedben?", "default_1": "Mi jár a fejedben?",
"reply_to_account": "Válasz: {0}", "reply_to_account": "Válasz erre: {0}",
"replying": "Válaszol" "replying": "Válaszol"
}, },
"polls": { "polls": {
@ -517,10 +542,12 @@
"hide_follower_count": "Követők/Követések számláló elrejtése", "hide_follower_count": "Követők/Követések számláló elrejtése",
"hide_news": "Hírek elrejtése", "hide_news": "Hírek elrejtése",
"hide_reply_count": "Visszajelzések számláló elrejtése", "hide_reply_count": "Visszajelzések számláló elrejtése",
"hide_tag_hover_card": "Lebegő kártya elrejtése",
"hide_translation": "Fordítás elrejtése", "hide_translation": "Fordítás elrejtése",
"hide_username_emojis": "Felhasználói emoji elrejtése", "hide_username_emojis": "Felhasználói emoji elrejtése",
"hide_username_emojis_description": "Elrejti a hangulatjeleket a felhasználónevek elől az idővonalakban. A hangulatjelek továbbra is láthatók lesznek a profiljukban.", "hide_username_emojis_description": "Elrejti a hangulatjeleket a felhasználónevek elől az idővonalakban. A hangulatjelek továbbra is láthatók lesznek a profiljukban.",
"label": "Preferenciák", "label": "Preferenciák",
"optimize_for_low_performance_device": "Alacsony teljesítményre optimalizálás",
"title": "Kísérleti képességek", "title": "Kísérleti képességek",
"use_star_favorite_icon": "Használja a csillag favikont", "use_star_favorite_icon": "Használja a csillag favikont",
"user_picker": "Felhasználók váltása", "user_picker": "Felhasználók váltása",
@ -557,10 +584,10 @@
"label": "Bejelentkezett felhasználók" "label": "Bejelentkezett felhasználók"
} }
}, },
"share-target": { "share_target": {
"description": "Az Elk konfigurálható úgy, hogy más alkalmazásokból is megoszthasson tartalmat, egyszerűen telepítse az Elket eszközére vagy számítógépére, és jelentkezzen be.", "description": "Az Elk konfigurálható úgy, hogy más alkalmazásokból is megoszthasson tartalmat, egyszerűen telepítse az Elket eszközére vagy számítógépére, és jelentkezzen be.",
"hint": "A tartalom Elkkel való megosztásához telepíteni kell az Elket, és be kell jelentkeznie.", "hint": "A tartalom megosztásához telepíteni kell az Elket, és be kell jelentkeznie.",
"title": "Megosztás Elkkel" "title": "Oszd meg Elkkel"
}, },
"state": { "state": {
"attachments_exceed_server_limit": "A mellékletek száma meghaladta a bejegyzésenkénti korlátot.", "attachments_exceed_server_limit": "A mellékletek száma meghaladta a bejegyzésenkénti korlátot.",

View file

@ -121,13 +121,13 @@
"block_account": { "block_account": {
"cancel": "Annulla", "cancel": "Annulla",
"confirm": "Blocca", "confirm": "Blocca",
"description": "Confermi di voler bloccare {0}?", "description": "Bloccare {0}?",
"title": "Blocca account" "title": "Blocca account"
}, },
"block_domain": { "block_domain": {
"cancel": "Annulla", "cancel": "Annulla",
"confirm": "Blocca", "confirm": "Blocca",
"description": "Confermi di voler bloccare {0}?", "description": "Bloccare {0}?",
"title": "Blocca dominio" "title": "Blocca dominio"
}, },
"common": { "common": {
@ -137,31 +137,36 @@
"delete_list": { "delete_list": {
"cancel": "Annulla", "cancel": "Annulla",
"confirm": "Elimina", "confirm": "Elimina",
"description": "Confermi di voler eliminare la lista \"{0}\"?", "description": "Eliminare la lista \"{0}\"?",
"title": "Elimina lista" "title": "Elimina lista"
}, },
"delete_posts": { "delete_posts": {
"cancel": "Annulla", "cancel": "Annulla",
"confirm": "Elimina", "confirm": "Elimina",
"description": "Confermi di voler eliminare questo post?", "description": "Eliminare questo post?",
"title": "Elimina post" "title": "Elimina post"
}, },
"mute_account": { "mute_account": {
"cancel": "Annulla", "cancel": "Annulla",
"confirm": "Silenzia", "confirm": "Silenzia",
"description": "Confermi di voler silenziare {0}?", "days": "giorni|giorno|giorni",
"description": "Silenziare {0}?",
"hours": "ore|ora|ore",
"minute": "minuti|minuto|minuti",
"notifications": "Silenzia notifiche",
"specify_duration": "Specifica durata silenziamento",
"title": "Silenzia account" "title": "Silenzia account"
}, },
"show_reblogs": { "show_reblogs": {
"cancel": "Annulla", "cancel": "Annulla",
"confirm": "Mostra", "confirm": "Mostra",
"description": "Confermi di voler mostrare i post potenziati da {0}?", "description": "Mostrare i post potenziati da {0}?",
"title": "Mostra potenziamenti" "title": "Mostra potenziamenti"
}, },
"unfollow": { "unfollow": {
"cancel": "Annulla", "cancel": "Annulla",
"confirm": "Smetti di seguire", "confirm": "Smetti di seguire",
"description": "Confermi di voler smettere di seguire?", "description": "Smettere di seguire?",
"title": "Smetti di seguire" "title": "Smetti di seguire"
} }
}, },
@ -228,14 +233,25 @@
"compose": "Componi", "compose": "Componi",
"favourite": "Apprezza", "favourite": "Apprezza",
"search": "Cerca", "search": "Cerca",
"show_new_items": "Mostra nuovi elementi",
"title": "Azioni" "title": "Azioni"
}, },
"media": { "media": {
"title": "Media" "title": "Media"
}, },
"navigation": { "navigation": {
"go_to_bookmarks": "Segnalibri",
"go_to_conversations": "Conversazioni",
"go_to_explore": "Esplora",
"go_to_favourites": "Preferiti",
"go_to_federated": "Federata",
"go_to_home": "Home", "go_to_home": "Home",
"go_to_lists": "Liste",
"go_to_local": "Locale",
"go_to_notifications": "Notifiche", "go_to_notifications": "Notifiche",
"go_to_profile": "Profilo",
"go_to_search": "Cerca",
"go_to_settings": "Impostazioni",
"next_status": "Post successivo", "next_status": "Post successivo",
"previous_status": "Post precedente", "previous_status": "Post precedente",
"shortcut_help": "Aiuto scorciatoie", "shortcut_help": "Aiuto scorciatoie",
@ -290,9 +306,11 @@
"built_at": "Sviluppato {0}", "built_at": "Sviluppato {0}",
"compose": "Componi", "compose": "Componi",
"conversations": "Conversazioni", "conversations": "Conversazioni",
"docs": "Documentazione",
"explore": "Esplora", "explore": "Esplora",
"favourites": "Preferiti", "favourites": "Preferiti",
"federated": "Federata", "federated": "Federata",
"hashtags": "Hashtag",
"home": "Home", "home": "Home",
"list": "Lista", "list": "Lista",
"lists": "Liste", "lists": "Liste",
@ -442,6 +460,7 @@
}, },
"language": { "language": {
"display_language": "Lingua interfaccia", "display_language": "Lingua interfaccia",
"how_to_contribute": "Come posso contribuire?",
"label": "Lingua", "label": "Lingua",
"post_language": "Lingua di pubblicazione", "post_language": "Lingua di pubblicazione",
"status": "Stato traduzione: {0}/{1} ({2}%)", "status": "Stato traduzione: {0}/{1} ({2}%)",
@ -525,6 +544,7 @@
"hide_follower_count": "Nascondi contatore seguaci/seguiti", "hide_follower_count": "Nascondi contatore seguaci/seguiti",
"hide_news": "Nascondi notizie", "hide_news": "Nascondi notizie",
"hide_reply_count": "Nascondi contatore risposte", "hide_reply_count": "Nascondi contatore risposte",
"hide_tag_hover_card": "Nascondi anteprima tag al passaggio del mouse",
"hide_translation": "Nascondi traduzione", "hide_translation": "Nascondi traduzione",
"hide_username_emojis": "Nascondi emoji dai nomi utente", "hide_username_emojis": "Nascondi emoji dai nomi utente",
"hide_username_emojis_description": "Nasconde le emoji all'interno dei nomi utente nella cronologia. Le emoji saranno comunque visibili nelle pagine dei profili.", "hide_username_emojis_description": "Nasconde le emoji all'interno dei nomi utente nella cronologia. Le emoji saranno comunque visibili nelle pagine dei profili.",

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