Compare commits

..

204 Commits

Author SHA1 Message Date
Shpuld Shpludson b13d8f7e63 Merge branch 'develop' into 'master'
Update MASTER for 2.4.2

See merge request pleroma/pleroma-fe!1421
2022-01-09 18:37:01 +00:00
Shpuld Shpludson 756f7bf7c2 Merge branch 'shpuld-develop-patch-58245' into 'develop'
Update CHANGELOG.md

See merge request pleroma/pleroma-fe!1423
2022-01-09 18:29:46 +00:00
Shpuld Shpludson 4cd27acf7f Update CHANGELOG.md 2022-01-09 18:26:35 +00:00
Shpuld Shpludson 030c374def Merge branch 'shpuld-develop-patch-87791' into 'develop'
Update CHANGELOG.md

See merge request pleroma/pleroma-fe!1422
2022-01-09 18:21:14 +00:00
Shpuld Shpludson 056f5f547a Update CHANGELOG.md 2022-01-09 18:16:44 +00:00
HJ d22e04eaf6 Merge branch 'allow_to_cancel_follow_request' into 'develop'
Allow canceling a follow request

See merge request pleroma/pleroma-fe!1416
2021-12-28 11:43:24 +00:00
Ilja 4587f37dd7 Allow canceling a follow request
When a follow request is sent, but not (yet) accepted, the behaviour is now to cancel the request instead of re sending.

The reason is double
* You couldn't cancel a follow request if you change your mind and the request wasn't answered yet
* Instances don't always correctly process a new follow request when the following is already happening. If something went wrong (e;g. the target server thinks you're following, but your instance thinks you're not yet), it's better to first sent an unfollow. This is the behaviour that Mastodon and most probably most other clients have. Therefore this flow is more tested and expected by other instances.
2021-12-12 18:09:21 +01:00
HJ a20f1794d0 Merge branch 'simplePolicy_reasons_for_instance_specific_policies' into 'develop'
Simple policy reasons for instance specific policies

See merge request pleroma/pleroma-fe!1263
2021-12-03 18:14:32 +00:00
Ilja b4cfda4a20 Simple policy reasons for instance specific policies 2021-12-03 18:14:32 +00:00
HJ ea0887a15e Merge branch 'fix/escape-display-name' into 'develop'
entity_normalizer: Escape name when parsing user

See merge request pleroma/pleroma-fe!1415
2021-11-16 17:45:14 +00:00
rinpatch d36b45ad43 entity_normalizer: Escape name when parsing user
In January 2020 Pleroma backend stopped escaping HTML in display names
and passed that responsibility on frontends, compliant with Mastodon's
version of Mastodon API [1]. Pleroma-FE was subsequently modified to
escape the display name [2], however only in the "name_html" field. This
was fine however, since that's what the code rendering display names used.

However, 2 months ago an MR [3] refactoring the way the frontend does emoji
and mention rendering was merged. One of the things it did was moving away
from doing emoji rendering in the entity normalizer and use the unescaped
'user.name' in the rendering code, resulting in HTML injection being
possible again.

This patch escapes 'user.name' as well, as far as I can tell there is no
actual use for an unescaped display name in frontend code, especially
when it comes from MastoAPI, where it is not supposed to be HTML.

[1]: https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1052
[2]: https://git.pleroma.social/pleroma/pleroma/-/merge_requests/2167
[3]: https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1392
2021-11-16 20:35:23 +03:00
HJ ef5bbc4e5f Merge branch 'themeApply' into 'develop'
Minor QOL improvement: Theme tab Apply and Reset buttons shouldn't require scrolling

See merge request pleroma/pleroma-fe!1397
2021-09-09 22:00:20 +00:00
HJ 370f1e55ad Merge branch 'develop' into 'themeApply'
# Conflicts:
#   CHANGELOG.md
2021-09-09 21:51:39 +00:00
HJ a8a82ad12f Merge branch 'showMobileNewPost' into 'develop'
New user option: Always show floating New Post button

See merge request pleroma/pleroma-fe!1395
2021-09-09 12:19:53 +00:00
HJ 1c53528433 Merge branch 'fix-favico-badge-chrome' into 'develop'
fix favico badge not working on chrome

See merge request pleroma/pleroma-fe!1391
2021-09-07 16:17:31 +00:00
HJ 8af1f08539 Merge branch 'better-still-emoji' into 'develop'
Status HTML parsing - better emoji and mentions rendering

Closes #935

See merge request pleroma/pleroma-fe!1392
2021-09-07 16:15:41 +00:00
Shpuld Shpludson 25a8b48bf2 Merge branch 'from/develop/tusooa/fix-mobile-shoutbox-display' into 'develop'
Fix mobile shoutbox display

See merge request pleroma/pleroma-fe!1404
2021-09-07 14:46:39 +00:00
HJ 59bab829a6 Merge branch 'weblate-pleroma-pleroma-fe' into 'develop'
Translations update from Weblate

See merge request pleroma/pleroma-fe!1412
2021-09-05 12:31:23 +00:00
Hồ Nhất Duy 13468f2a89 Translated using Weblate (Vietnamese)
Currently translated at 50.0% (358 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/vi/
2021-09-05 04:46:01 +00:00
@liimee 5bb471a68e Translated using Weblate (Indonesian)
Currently translated at 67.3% (482 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/id/
2021-09-05 04:45:45 +00:00
Kana ff5ed29ec1 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (716 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/zh_Hans/
2021-09-05 04:45:43 +00:00
tarteka fa75a3a615 Translated using Weblate (Basque)
Currently translated at 80.7% (578 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/eu/
2021-09-05 04:45:43 +00:00
HJ 057e3dac85 Merge branch 'weblate-pleroma-pleroma-fe' into 'develop'
Translations update from Weblate

See merge request pleroma/pleroma-fe!1388
2021-09-02 16:58:11 +00:00
Hồ Nhất Duy 6e57170626 Added translation using Weblate (Vietnamese) 2021-09-02 14:47:32 +00:00
@liimee 98da3ad124 Translated using Weblate (Indonesian)
Currently translated at 60.6% (434 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/id/
2021-08-30 15:24:16 +00:00
@liimee 144cee6d34 Translated using Weblate (Indonesian)
Currently translated at 58.6% (420 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/id/
2021-08-30 15:24:16 +00:00
tarteka 0543c8d536 Translated using Weblate (Basque)
Currently translated at 80.4% (576 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/eu/
2021-08-30 15:24:16 +00:00
marcin mikołajczak 9319666f04 Translated using Weblate (Polish)
Currently translated at 98.8% (708 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/pl/
2021-08-30 15:24:16 +00:00
@liimee ca9652b30b Translated using Weblate (Indonesian)
Currently translated at 56.9% (408 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/id/
2021-08-30 15:24:16 +00:00
tarteka 21af736fe1 Translated using Weblate (Basque)
Currently translated at 79.8% (572 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/eu/
2021-08-30 15:24:16 +00:00
tarteka 78ba8be969 Translated using Weblate (Spanish)
Currently translated at 100.0% (716 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/es/
2021-08-30 15:24:16 +00:00
@liimee 29f229daad Translated using Weblate (Indonesian)
Currently translated at 50.1% (359 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/id/
2021-08-30 15:24:16 +00:00
marcin mikołajczak 5049ee575f Translated using Weblate (Polish)
Currently translated at 98.7% (707 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/pl/
2021-08-30 15:24:16 +00:00
tarteka 32ed71501a Translated using Weblate (Spanish)
Currently translated at 100.0% (716 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/es/
2021-08-30 15:24:16 +00:00
@liimee a7a736c7b8 Added translation using Weblate (Indonesian) 2021-08-30 15:24:16 +00:00
tarteka 5cbb71e588 Translated using Weblate (Spanish)
Currently translated at 100.0% (716 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/es/
2021-08-30 15:24:16 +00:00
M. Strange fa2e5deae2 Translated using Weblate (Catalan)
Currently translated at 97.4% (698 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/ca/
2021-08-30 15:24:16 +00:00
titizen a3bfa63d05 Translated using Weblate (Catalan)
Currently translated at 95.3% (683 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/ca/
2021-08-30 15:24:16 +00:00
M. Strange 1ef2bb93fe Translated using Weblate (Catalan)
Currently translated at 92.0% (659 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/ca/
2021-08-30 15:24:16 +00:00
titizen c38ab7234d Translated using Weblate (Catalan)
Currently translated at 92.0% (659 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/ca/
2021-08-30 15:24:16 +00:00
ZEN 791293c709 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (716 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/uk/
2021-08-30 15:24:16 +00:00
M. Strange 8574db1cf1 Translated using Weblate (Catalan)
Currently translated at 56.1% (402 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/ca/
2021-08-30 15:24:15 +00:00
Snow 90d553f4be Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (716 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/zh_Hant/
2021-08-30 15:24:15 +00:00
Haelwenn (lanodan) Monnier d3139a92b3 Translated using Weblate (French)
Currently translated at 100.0% (716 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/fr/
2021-08-30 15:24:15 +00:00
Ben Is bc08f998cf Translated using Weblate (Italian)
Currently translated at 100.0% (716 of 716 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/it/
2021-08-30 15:24:15 +00:00
Tirifto f72671a1aa Translated using Weblate (Esperanto)
Currently translated at 100.0% (715 of 715 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/eo/
2021-08-30 15:24:15 +00:00
Ben Is 738e7923e4 Translated using Weblate (Italian)
Currently translated at 100.0% (715 of 715 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/it/
2021-08-30 15:24:15 +00:00
retiolus b3f15fe3e1 Translated using Weblate (Catalan)
Currently translated at 44.4% (318 of 715 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/ca/
2021-08-30 15:24:15 +00:00
ZEN 761f91f7ef Translated using Weblate (Ukrainian)
Currently translated at 100.0% (715 of 715 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/uk/
2021-08-30 15:24:15 +00:00
Issabella Deinschnitzel c509ed357a Translated using Weblate (German)
Currently translated at 100.0% (715 of 715 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/de/
2021-08-30 15:24:15 +00:00
Ben Is 18871684c7 Translated using Weblate (Italian)
Currently translated at 100.0% (715 of 715 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/it/
2021-08-30 15:24:15 +00:00
Kana 33e2bcce31 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (714 of 714 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/zh_Hans/
2021-08-30 15:24:15 +00:00
Ben Is 4d529c13ba Translated using Weblate (Italian)
Currently translated at 100.0% (714 of 714 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/it/
2021-08-30 15:24:15 +00:00
Ben Is 0e53b2916e Translated using Weblate (Italian)
Currently translated at 100.0% (714 of 714 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/it/
2021-08-30 15:24:15 +00:00
Tirifto 04a49e4c42 Translated using Weblate (Esperanto)
Currently translated at 99.7% (709 of 711 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/eo/
2021-08-30 15:24:15 +00:00
ZEN f57f61ca53 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (711 of 711 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/uk/
2021-08-30 15:24:15 +00:00
Ben Is 4302db5975 Translated using Weblate (Italian)
Currently translated at 100.0% (711 of 711 strings)

Translation: Pleroma/Pleroma-FE
Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/it/
2021-08-30 15:24:15 +00:00
Shpuld Shpludson cdcefc2b73 Merge branch 'fix-ext-profile' into 'develop'
fix ext profile bug

See merge request pleroma/pleroma-fe!1409
2021-08-30 15:24:06 +00:00
Henry Jameson 4d73eaa6ce fix spacing before hashtags 2021-08-23 21:36:18 +03:00
Henry Jameson 39494439d3 very minimalist hashtaglink implementation, also you can middle-click
mentions now.
2021-08-23 20:57:21 +03:00
Henry Jameson c3576211cb fix tests 2021-08-18 21:17:51 +03:00
Henry Jameson cbb34e2b0e fix expanded mentions spacing 2021-08-18 20:58:26 +03:00
Henry Jameson e98a2af39e hopefully final fix for spacings 2021-08-18 20:54:04 +03:00
Henry Jameson dbdc5e050f fix ext profile bug 2021-08-16 01:41:52 +03:00
Henry Jameson 0087d33c75 fix "+X more" sticking 2021-08-15 18:41:13 +03:00
Henry Jameson 8cc1ad67df fix links sticking to mentionsline 2021-08-15 18:11:38 +03:00
Henry Jameson f16658adfc fix tests 2021-08-15 02:59:14 +03:00
Henry Jameson 68b4323181 prevent infinite update loops 2021-08-15 02:55:45 +03:00
Henry Jameson 7d67e8f1cc remove obsolete tests 2021-08-15 02:44:36 +03:00
Henry Jameson 530ac4442b removed useless code, review change, fixed bug with tall statuses 2021-08-15 02:41:53 +03:00
Henry Jameson 4465de5241 fixed mentions line again 2021-08-14 22:03:09 +03:00
Henry Jameson 97e86381c8 remove old emoji added, everything emoji-bearing uses RichContent now 2021-08-13 13:12:33 +03:00
Henry Jameson 4c974f5ca2 richcontent support in polls, user cards and user profiles 2021-08-13 13:06:42 +03:00
Henry Jameson 6c6df29ed3 support richcontent in polls 2021-08-13 12:19:57 +03:00
Henry Jameson add5921b8b fix tests, add performance test (skipped, doesn't assert anything),
tweak max mentions count
2021-08-12 19:37:04 +03:00
Henry Jameson 2182af4058 made the code responsible for showing unwritten mentions actually work 2021-08-12 03:09:28 +03:00
Henry Jameson 2cfff1b8b9 remove new options for style and separate line, now groups all chained
mentions on a mentionsline regardless of placement. fixes spacing
2021-08-12 02:56:40 +03:00
Shpuld Shpludson 51d3d8d255 Merge branch 'develop' into 'master'
Update MASTER for 2.4.0

See merge request pleroma/pleroma-fe!1406
2021-08-08 13:16:00 +00:00
Shpuld Shpuldson cc170aa3ec Update master with 2.4.0 2021-08-08 16:14:22 +03:00
Tusooa Zhu ef277ae4e2 Fix mobile shoutbox 2021-08-03 20:11:06 -04:00
eris f35e3d0f3f Fix merge conflict in CHANGELOG
# Conflicts:
#   CHANGELOG.md
2021-07-22 20:47:36 +00:00
eris 179af131ee Fix changelog merge conflict
# Conflicts:
#   CHANGELOG.md
2021-07-22 20:46:41 +00:00
Henry Jameson a0eaac2216 fix tests 2021-06-22 21:09:29 +03:00
Henry Jameson a258182522 fix non-notifying mentions and original mention display 2021-06-22 20:16:26 +03:00
eris 6125dc885a Update for latest develop merges to CHANGELOG 2021-06-20 21:06:59 +00:00
Henry Jameson c6831a3810 fix not escaping some stuff 2021-06-18 21:42:46 +03:00
Henry Jameson 8fe4355a6b fix rich images 2021-06-18 21:29:47 +03:00
Henry Jameson b68fb7738b Merge remote-tracking branch 'origin/develop' into better-still-emoji
* origin/develop:
  Use proper setting name
  Use cleaner instance config check for shoutbox setting
  Make locale language cleaner
  Don't shorten shoutbox to SB
  Fix lint error
  Update CHANGELOG.md
  New option: Hide shoutbox
2021-06-18 02:27:57 +03:00
Henry Jameson 1717a3aaf2 fix chats again 2021-06-16 12:44:04 +03:00
Henry Jameson 25bf28f051 added tests just in case 2021-06-16 02:11:24 +03:00
Henry Jameson ad3a2fd4e5 fixed "invisible" spans inside links 2021-06-16 01:20:20 +03:00
eris 139a0d1562 Merge branch 'develop' into 'themeApply'
# Conflicts:
#   CHANGELOG.md
2021-06-15 21:50:39 +00:00
eris cab0095989 Merge branch 'develop' into 'showMobileNewPost'
# Conflicts:
#   CHANGELOG.md
#   src/App.js
2021-06-15 21:49:33 +00:00
Henry Jameson 4aac0125e5 fixed bug with hashtags 2021-06-15 14:43:44 +03:00
Eris e67f295497 Update CHANGELOG.md 2021-06-14 23:32:18 +00:00
Eris 312a237ca4 Revert duplicate buttons and move existing buttons to bottom-right corner independent of scroll 2021-06-14 23:31:16 +00:00
Eris 4639e30cb8 Fix config naming for consistency 2021-06-14 20:41:34 +00:00
Eris b88e6b8ab0 Update CHANGELOG.md 2021-06-14 20:11:57 +00:00
Eris 8fa0331771 Add apply and reset themes to top of theme tab 2021-06-14 20:09:28 +00:00
Eris 67c9d8bd55 revert gitignore file change 2021-06-14 18:49:37 +00:00
Eris adfe56a3a3 New option: Always show floating New Post button 2021-06-14 17:54:40 +00:00
Henry Jameson 7309f8ce1a lint 2021-06-14 10:31:07 +03:00
Henry Jameson c21b1cf898 do the impossible, fix the unfixable 2021-06-14 10:30:08 +03:00
Henry Jameson 636dbdaba8 more fixes 2021-06-13 22:22:59 +03:00
Henry Jameson 1fdfc42159 fix mentions in chats 2021-06-13 21:43:45 +03:00
Henry Jameson 609dc5da0c fix chats messages 2021-06-13 21:42:25 +03:00
Henry Jameson bebafa1a2c refactored line converter, untied its logic from greentexting, better
handling of broken cases
2021-06-13 15:24:29 +03:00
HJ e825021ef1 Apply 1 suggestion(s) to 1 file(s) 2021-06-12 18:55:18 +00:00
Henry Jameson 9c70f3e4df fixed a bug + made a testcase out of it 2021-06-12 21:49:56 +03:00
Henry Jameson 2c60a9b638 fix next relply-row bleeding through popover 2021-06-12 20:51:36 +03:00
Henry Jameson 18fb7516cc lint 2021-06-12 20:44:14 +03:00
Henry Jameson 418f029789 review + fixes 2021-06-12 20:43:29 +03:00
Henry Jameson 90a188f2c3 cleanup 2021-06-12 19:54:34 +03:00
Henry Jameson cd44556750 restructure and tests
squash! restructure and tests
2021-06-12 19:54:30 +03:00
Henry Jameson ca6c7d5b10 fix tags gluing 2021-06-12 17:20:21 +03:00
Henry Jameson 24f3681ac1 fix color of reply row, fix overflow in status-popover 2021-06-12 17:11:49 +03:00
Henry Jameson 647e4476f9 fix long post fader 2021-06-12 16:25:37 +03:00
Henry Jameson c1bd36dc6f change how "first" line is determined. Allow one mention in the
beginning for hellthread style
2021-06-12 16:15:22 +03:00
Henry Jameson ffc501eb23 cleanup 2021-06-11 13:38:08 +03:00
Henry Jameson 9421501c1e lint & cleanup 2021-06-11 11:52:50 +03:00
Henry Jameson 5834790d0b fix #935 2021-06-11 11:50:05 +03:00
Henry Jameson f819227bed fixed console errors, improved user-selecting, added cyantexting 2021-06-11 11:49:32 +03:00
Henry Jameson 255f47fe56 fix infinite loop 2021-06-11 11:05:28 +03:00
Henry Jameson f883d2f75c better handling of hellthreads with mentions at bottom 2021-06-11 03:11:58 +03:00
Henry Jameson b84aeff6bf stylistic changes 2021-06-10 18:52:23 +03:00
Henry Jameson cc00af7a31 Hellthread(tm) Certified 2021-06-10 18:52:01 +03:00
Henry Jameson 0f73e96194 don't hide mentions for OPs 2021-06-10 15:11:57 +03:00
Henry Jameson 0263834faa mentions on same line as replies 2021-06-10 14:01:26 +03:00
Henry Jameson 6bff7cc6ef use icon instead of symbol for @ in mentions links 2021-06-10 13:29:59 +03:00
Henry Jameson 0260693f51 stylistic improvements for single-line mentions 2021-06-10 13:22:36 +03:00
Henry Jameson 394fd462dc proper cachin of headTailLinks, show mentions in notificaitons always 2021-06-10 13:01:00 +03:00
Henry Jameson c6c478f4cf moved mentions onto reply line, replies moved below post body 2021-06-10 12:29:58 +03:00
Henry Jameson aec867b300 Moved greentext to RichContent, improved how first mentions are
restored, now shows mentions not uh, mention in post body
2021-06-10 12:29:58 +03:00
Henry Jameson 566964992a fix long posts having weird gradient 2021-06-08 19:37:18 +03:00
Henry Jameson 5c655b6675 lint 2021-06-08 17:19:38 +03:00
Henry Jameson 8c8237418c fix repeats having wrong mentions 2021-06-08 17:14:22 +03:00
Henry Jameson 963f1679e0 fix console errors 2021-06-08 17:14:22 +03:00
Henry Jameson a3b8e7ad99 missing localization 2021-06-08 16:37:13 +03:00
Henry Jameson 7ae85c8318 change defaults 2021-06-08 14:51:42 +03:00
Henry Jameson 0ae3985a52 bump limit to a saner one 2021-06-08 14:36:41 +03:00
Henry Jameson 2f383c2c01 moved mentions into a separate component - MentionLine, added collapsing
of mentions when there's too many of 'em
2021-06-08 14:34:47 +03:00
Henry Jameson 73127f0e25 fix empty spaces again 2021-06-08 13:42:16 +03:00
Henry Jameson 9ea370033a configurable mentions placement 2021-06-08 12:58:28 +03:00
Henry Jameson 3abd357694 moving mentions into separate row 2021-06-08 11:38:44 +03:00
Henry Jameson 0583a6b863 moved transparent button styles into button itself 2021-06-08 10:14:49 +03:00
Henry Jameson 6bc9886db4 tweaking the spacings 2021-06-08 01:25:03 +03:00
Henry Jameson ccdf892483 remove weird vertical align 2021-06-08 01:21:45 +03:00
Henry Jameson 5740a79dbd faint @ 2021-06-08 00:06:26 +03:00
Henry Jameson e6d5ddcbb6 better modifier, no background for unhighlighted mentions 2021-06-08 00:03:59 +03:00
Henry Jameson c3e122ff6f smaller mentions 2021-06-07 23:48:01 +03:00
Henry Jameson 7d6fc044fb new mentions look 2021-06-07 23:42:04 +03:00
Henry Jameson 6199788f28 fix tall emojis being cropped 2021-06-07 20:44:32 +03:00
Henry Jameson 8045d1866e localization 2021-06-07 20:25:31 +03:00
Henry Jameson 6090327236 moved some post styles into status body since they inferfere with usernames 2021-06-07 20:02:09 +03:00
Henry Jameson 5e83672274 fixed some strange error 2021-06-07 20:01:57 +03:00
Henry Jameson aa38223e87 lint 2021-06-07 19:51:04 +03:00
Henry Jameson 8e9f5d7580 renamed StatusText to StatusBody for clarity, fixed chats 2021-06-07 19:50:38 +03:00
Henry Jameson 50aa379038 new component - StatusText, to separate post's text from its attachments 2021-06-07 18:41:55 +03:00
Henry Jameson 04fa1f0b2d some docs, added richcontent to usernames in status, updated stillImage
to allow scale of "gif" label
2021-06-07 18:41:47 +03:00
Henry Jameson aec05686d0 lint, fix warnings 2021-06-07 18:41:47 +03:00
Henry Jameson b0ae32e309 made getAttrs correctly handle both ' and " 2021-06-07 18:41:47 +03:00
Henry Jameson 22c8f71945 mention link 2021-06-07 18:41:47 +03:00
Henry Jameson 1923ed84d4 more tests 2021-06-07 18:41:47 +03:00
Henry Jameson a2459c2187 move styles to richcontent 2021-06-07 18:41:47 +03:00
Henry Jameson be79643bcf fix emoji processor not leaving string as-is if no emoji are found 2021-06-07 18:41:47 +03:00
Henry Jameson 35dedf8416 lint 2021-06-07 18:41:47 +03:00
Henry Jameson 5970ddf9ac fix escaped apostrophes 2021-06-07 18:41:47 +03:00
Henry Jameson 20ce646852 [WIP] MUCH better approach to replacing emojis with still versions 2021-06-07 18:41:47 +03:00
Henry Jameson 2a2483f4c9 handle multiple favicons (different sizes) 2021-06-02 12:47:54 +03:00
Henry Jameson 008e711e11 fix favico badge not working on chrome 2021-06-02 12:15:31 +03:00
Shpuld Shpludson c3fcbbd918 Merge branch 'rc/2.3.0' into 'master'
2.3.0 to MASTER

See merge request pleroma/pleroma-fe!1366
2021-03-01 18:15:46 +00:00
Shpuld Shpludson 0ac34b3014 Merge branch 'master' into 'rc/2.3.0'
# Conflicts:
#   CHANGELOG.md
2021-03-01 18:06:55 +00:00
Shpuld Shpuldson 06cde8ad06 fix changelog entries in wrong places 2021-03-01 20:04:27 +02:00
Shpuld Shpuldson 434f9cdd7e changelog date 2021-03-01 19:59:57 +02:00
Shpuld Shpuldson 35cde98d2c Merge branch 'fix/punycode-buggy' into rc/2.3.0 2021-03-01 09:27:19 +02:00
Shpuld Shpuldson 44e687653c set changelog version 2021-02-26 13:01:09 +02:00
Shpuld Shpludson 320418d524 Merge branch 'rc/2.2.3' into 'master'
rc 2.2.3 to MASTER

See merge request pleroma/pleroma-fe!1328
2021-01-18 13:44:31 +00:00
Shpuld Shpludson 8f55cb151c Merge branch 'master' into 'rc/2.2.3'
# Conflicts:
#   CHANGELOG.md
2021-01-18 13:35:53 +00:00
Shpuld Shpuldson 4f0c43bd84 rc 2.2.3 2021-01-18 15:30:42 +02:00
Shpuld Shpludson a39866308c Merge branch 'rc/2.2.2' into 'master'
prepare master for 2.2.2

See merge request pleroma/pleroma-fe!1315
2020-12-22 16:00:12 +00:00
Shpuld Shpludson 00cb8d9dce Merge branch 'master' into 'rc/2.2.2'
# Conflicts:
#   CHANGELOG.md
2020-12-22 15:51:18 +00:00
Shpuld Shpuldson 19bde84f6d mark unreleased as 2.2.2 in changelog 2020-12-22 17:49:13 +02:00
Shpuld Shpuldson a7567ce6d0 set patch date to correct 2020-11-11 22:32:44 +02:00
Shpuld Shpuldson 8e87e3d88b Update MASTER with develop for 2.2.1 2020-11-09 11:13:49 +02:00
Shpuld Shpuldson 6aa276374f update changelog for 2.2.0 2020-11-06 19:35:01 +02:00
Shpuld Shpludson 23232e1c8f Merge branch 'develop' into 'master'
Merge develop into master for 2.2.0

See merge request pleroma/pleroma-fe!1278
2020-11-06 17:27:25 +00:00
Shpuld Shpludson b225c3578f Merge branch 'rc/2.1.2' into 'master'
Merge 2.1.2 to MASTER

See merge request pleroma/pleroma-fe!1236
2020-09-17 17:26:22 +00:00
Shpuld Shpludson 30efbaab34 Merge branch 'master' into 'rc/2.1.2'
# Conflicts:
#   CHANGELOG.md
2020-09-17 17:17:21 +00:00
Shpuld Shpuldson 8ca0586c0f update changelog for 2.1.2 2020-09-17 20:15:08 +03:00
Shpuld Shpludson 938887ef91 Merge branch 'rc/2.1.1' into 'master'
Update master with 2.1.1

See merge request pleroma/pleroma-fe!1231
2020-09-08 09:46:19 +00:00
Shpuld Shpludson f31bc5310e Merge branch 'master' into 'rc/2.1.1'
# Conflicts:
#   CHANGELOG.md
2020-09-08 09:35:36 +00:00
Shpuld Shpuldson 925bf5b5a4 update changelog with 2.1.1 2020-09-08 12:30:45 +03:00
Shpuld Shpludson e768ec1fca Merge branch '2.1.0-rc0' into 'master'
2.1.0 into master

See merge request pleroma/pleroma-fe!1217
2020-08-27 14:45:03 +00:00
Shpuld Shpuldson d09f43ba7a Merge branch '2.1.0-rc0' of git.pleroma.social:pleroma/pleroma-fe into 2.1.0-rc0 2020-08-27 17:13:43 +03:00
Shpuld Shpuldson 748c4d8c71 fix boomarks mistake in changelog 2020-08-27 17:13:14 +03:00
Shpuld Shpludson c7ddfefe34 Merge branch 'master' into '2.1.0-rc0'
# Conflicts:
#   CHANGELOG.md
#   src/services/api/api.service.js
2020-08-27 13:59:56 +00:00
Shpuld Shpuldson c0205d582a add missing release to changelog to fix conflicts 2020-08-27 16:56:47 +03:00
Shpuld Shpuldson 4ac882a3b0 add back unreleased/patch 2020-08-27 16:51:16 +03:00
Shpuld Shpuldson bdbc4b27b6 set 2.1.0 date and remove rc from changelog 2020-08-27 16:49:01 +03:00
Shpuld Shpuldson 42598fc675 change changelog 2020-08-26 14:28:43 +03:00
Shpuld Shpludson 5d49edc823 Merge branch 'rc/2.0.5' into 'master'
Update MASTER for 2.0.5 patch

See merge request pleroma/pleroma-fe!1105
2020-05-12 17:36:05 +00:00
Shpuld Shpuldson 0bc0a8d5f5 update changelog for 2.0.5 2020-05-12 20:27:37 +03:00
Shpuld Shpuldson 726d5279c1 Revert "remove with_move param"
This reverts commit 02c8a9e314.
2020-05-12 20:04:00 +03:00
Shpuld Shpludson a0f780c455 Merge branch 'rc/2.0.3' into 'master'
Update MASTER with 2.0.3 for real

See merge request pleroma/pleroma-fe!1099
2020-05-02 14:17:27 +00:00
101 changed files with 4695 additions and 1120 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
{ {
"presets": ["@babel/preset-env"], "presets": ["@babel/preset-env", "@vue/babel-preset-jsx"],
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-transform-vue-jsx"], "plugins": ["@babel/plugin-transform-runtime", "lodash"],
"comments": false "comments": false
} }
+15
View File
@@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [2.4.2] - 2022-01-09
### Added
- Added Apply and Reset buttons to the bottom of theme tab to minimize UI travel
- Implemented user option to always show floating New Post button (normally mobile-only)
- Display reasons for instance specific policies
- Added functionality to cancel follow request
### Fixed
- Fixed link to external profile not working on user profiles
- Fixed mobile shoutbox display
- Fixed favicon badge not working in Chrome
- Escape html more properly in subject/display name
## [2.4.0] - 2021-08-08 ## [2.4.0] - 2021-08-08
### Added ### Added
- Added a quick settings to timeline header for easier access - Added a quick settings to timeline header for easier access
@@ -15,6 +29,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed ### Fixed
- Fixed follow request count showing in the wrong location in mobile view - Fixed follow request count showing in the wrong location in mobile view
## [2.3.0] - 2021-03-01 ## [2.3.0] - 2021-03-01
### Fixed ### Fixed
- Button to remove uploaded media in post status form is now properly placed and sized. - Button to remove uploaded media in post status form is now properly placed and sized.
+2 -2
View File
@@ -47,8 +47,8 @@
"@babel/preset-env": "^7.7.6", "@babel/preset-env": "^7.7.6",
"@babel/register": "^7.7.4", "@babel/register": "^7.7.4",
"@ungap/event-target": "^0.1.0", "@ungap/event-target": "^0.1.0",
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0", "@vue/babel-helper-vue-jsx-merge-props": "^1.2.1",
"@vue/babel-plugin-transform-vue-jsx": "^1.1.2", "@vue/babel-preset-jsx": "^1.2.4",
"@vue/test-utils": "^1.0.0-beta.26", "@vue/test-utils": "^1.0.0-beta.26",
"autoprefixer": "^6.4.0", "autoprefixer": "^6.4.0",
"babel-eslint": "^7.0.0", "babel-eslint": "^7.0.0",
+3
View File
@@ -73,6 +73,9 @@ export default {
this.$store.state.instance.instanceSpecificPanelContent this.$store.state.instance.instanceSpecificPanelContent
}, },
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
shoutboxPosition () {
return this.$store.getters.mergedConfig.showNewPostButton || false
},
hideShoutbox () { hideShoutbox () {
return this.$store.getters.mergedConfig.hideShoutbox return this.$store.getters.mergedConfig.hideShoutbox
}, },
+4
View File
@@ -88,6 +88,10 @@ a {
font-family: sans-serif; font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif); font-family: var(--interfaceFont, sans-serif);
&.-sublime {
background: transparent;
}
i[class*=icon-], i[class*=icon-],
.svg-inline--fa { .svg-inline--fa {
color: $fallback--text; color: $fallback--text;
+1
View File
@@ -53,6 +53,7 @@
v-if="currentUser && shout && !hideShoutbox" v-if="currentUser && shout && !hideShoutbox"
:floating="true" :floating="true"
class="floating-shout mobile-hidden" class="floating-shout mobile-hidden"
:class="{ 'left': shoutboxPosition }"
/> />
<MobilePostStatusButton /> <MobilePostStatusButton />
<UserReportingModal /> <UserReportingModal />
@@ -1,5 +1,6 @@
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const BasicUserCard = { const BasicUserCard = {
@@ -13,7 +14,8 @@ const BasicUserCard = {
}, },
components: { components: {
UserCard, UserCard,
UserAvatar UserAvatar,
RichContent
}, },
methods: { methods: {
toggleUserExpanded () { toggleUserExpanded () {
@@ -25,17 +25,11 @@
:title="user.name" :title="user.name"
class="basic-user-card-user-name" class="basic-user-card-user-name"
> >
<!-- eslint-disable vue/no-v-html --> <RichContent
<span
v-if="user.name_html"
class="basic-user-card-user-name-value" class="basic-user-card-user-name-value"
v-html="user.name_html" :html="user.name"
:emoji="user.emoji"
/> />
<!-- eslint-enable vue/no-v-html -->
<span
v-else
class="basic-user-card-user-name-value"
>{{ user.name }}</span>
</div> </div>
<div> <div>
<router-link <router-link
@@ -1,5 +1,5 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
import StatusContent from '../status_content/status_content.vue' import StatusBody from '../status_content/status_content.vue'
import fileType from 'src/services/file_type/file_type.service' import fileType from 'src/services/file_type/file_type.service'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import AvatarList from '../avatar_list/avatar_list.vue' import AvatarList from '../avatar_list/avatar_list.vue'
@@ -16,7 +16,7 @@ const ChatListItem = {
AvatarList, AvatarList,
Timeago, Timeago,
ChatTitle, ChatTitle,
StatusContent StatusBody
}, },
computed: { computed: {
...mapState({ ...mapState({
@@ -38,12 +38,14 @@ const ChatListItem = {
}, },
messageForStatusContent () { messageForStatusContent () {
const message = this.chat.lastMessage const message = this.chat.lastMessage
const messageEmojis = message ? message.emojis : []
const isYou = message && message.account_id === this.currentUser.id const isYou = message && message.account_id === this.currentUser.id
const content = message ? (this.attachmentInfo || message.content) : '' const content = message ? (this.attachmentInfo || message.content) : ''
const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content
return { return {
summary: '', summary: '',
statusnet_html: messagePreview, emojis: messageEmojis,
raw_html: messagePreview,
text: messagePreview, text: messagePreview,
attachments: [] attachments: []
} }
@@ -77,18 +77,15 @@
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
} }
.StatusContent { .chat-preview-body {
img.emoji { --emoji-size: 1.4em;
width: 1.4em;
height: 1.4em;
}
} }
.time-wrapper { .time-wrapper {
line-height: 1.4em; line-height: 1.4em;
} }
.single-line { .chat-preview-body {
padding-right: 1em; padding-right: 1em;
} }
} }
@@ -29,7 +29,8 @@
</div> </div>
</div> </div>
<div class="chat-preview"> <div class="chat-preview">
<StatusContent <StatusBody
class="chat-preview-body"
:status="messageForStatusContent" :status="messageForStatusContent"
:single-line="true" :single-line="true"
/> />
+3 -2
View File
@@ -57,8 +57,9 @@ const ChatMessage = {
messageForStatusContent () { messageForStatusContent () {
return { return {
summary: '', summary: '',
statusnet_html: this.message.content, emojis: this.message.emojis,
text: this.message.content, raw_html: this.message.content || '',
text: this.message.content || '',
attachments: this.message.attachments attachments: this.message.attachments
} }
}, },
@@ -89,8 +89,9 @@
} }
.without-attachment { .without-attachment {
.status-content { .message-content {
&::after { // TODO figure out how to do it properly
.RichContent::after {
margin-right: 5.4em; margin-right: 5.4em;
content: " "; content: " ";
display: inline-block; display: inline-block;
@@ -162,6 +163,7 @@
.visible { .visible {
opacity: 1; opacity: 1;
} }
} }
.chat-message-date-separator { .chat-message-date-separator {
@@ -71,6 +71,7 @@
</Popover> </Popover>
</div> </div>
<StatusContent <StatusContent
class="message-content"
:status="messageForStatusContent" :status="messageForStatusContent"
:full-content="true" :full-content="true"
> >
@@ -14,7 +14,7 @@ export default {
if (this.inProgress || this.relationship.following) { if (this.inProgress || this.relationship.following) {
return this.$t('user_card.follow_unfollow') return this.$t('user_card.follow_unfollow')
} else if (this.relationship.requested) { } else if (this.relationship.requested) {
return this.$t('user_card.follow_again') return this.$t('user_card.follow_cancel')
} else { } else {
return this.$t('user_card.follow') return this.$t('user_card.follow')
} }
@@ -33,7 +33,7 @@ export default {
}, },
methods: { methods: {
onClick () { onClick () {
this.relationship.following ? this.unfollow() : this.follow() this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow()
}, },
follow () { follow () {
this.inProgress = true this.inProgress = true
@@ -0,0 +1,36 @@
import { extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
const HashtagLink = {
name: 'HashtagLink',
props: {
url: {
required: true,
type: String
},
content: {
required: true,
type: String
},
tag: {
required: false,
type: String,
default: ''
}
},
methods: {
onClick () {
const tag = this.tag || extractTagFromUrl(this.url)
if (tag) {
const link = this.generateTagLink(tag)
this.$router.push(link)
} else {
window.open(this.url, '_blank')
}
},
generateTagLink (tag) {
return `/tag/${tag}`
}
}
}
export default HashtagLink
@@ -0,0 +1,6 @@
.HashtagLink {
position: relative;
white-space: normal;
display: inline-block;
color: var(--link);
}
@@ -0,0 +1,19 @@
<template>
<span
class="HashtagLink"
>
<!-- eslint-disable vue/no-v-html -->
<a
:href="url"
class="original"
target="_blank"
@click.prevent="onClick"
v-html="content"
/>
<!-- eslint-enable vue/no-v-html -->
</span>
</template>
<script src="./hashtag_link.js"/>
<style lang="scss" src="./hashtag_link.scss"/>
@@ -0,0 +1,95 @@
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters, mapState } from 'vuex'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faAt
} from '@fortawesome/free-solid-svg-icons'
library.add(
faAt
)
const MentionLink = {
name: 'MentionLink',
props: {
url: {
required: true,
type: String
},
content: {
required: true,
type: String
},
userId: {
required: false,
type: String
},
userScreenName: {
required: false,
type: String
}
},
methods: {
onClick () {
const link = generateProfileLink(
this.userId || this.user.id,
this.userScreenName || this.user.screen_name
)
this.$router.push(link)
}
},
computed: {
user () {
return this.url && this.$store && this.$store.getters.findUserByUrl(this.url)
},
isYou () {
// FIXME why user !== currentUser???
return this.user && this.user.id === this.currentUser.id
},
userName () {
return this.user && this.userNameFullUi.split('@')[0]
},
userNameFull () {
return this.user && this.user.screen_name
},
userNameFullUi () {
return this.user && this.user.screen_name_ui
},
highlight () {
return this.user && this.mergedConfig.highlight[this.user.screen_name]
},
highlightType () {
return this.highlight && ('-' + this.highlight.type)
},
highlightClass () {
if (this.highlight) return highlightClass(this.user)
},
style () {
if (this.highlight) {
const {
backgroundColor,
backgroundPosition,
backgroundImage,
...rest
} = highlightStyle(this.highlight)
return rest
}
},
classnames () {
return [
{
'-you': this.isYou,
'-highlighted': this.highlight
},
this.highlightType
]
},
...mapGetters(['mergedConfig']),
...mapState({
currentUser: state => state.users.currentUser
})
}
}
export default MentionLink
@@ -0,0 +1,91 @@
.MentionLink {
position: relative;
white-space: normal;
display: inline-block;
color: var(--link);
& .new,
& .original {
display: inline-block;
border-radius: 2px;
}
.full {
position: absolute;
display: inline-block;
pointer-events: none;
opacity: 0;
top: 100%;
left: 0;
height: 100%;
word-wrap: normal;
white-space: nowrap;
transition: opacity 0.2s ease;
z-index: 1;
margin-top: 0.25em;
padding: 0.5em;
user-select: all;
}
.short {
user-select: none;
}
& .short,
& .full {
white-space: nowrap;
}
.new {
&.-you {
& .shortName,
& .full {
font-weight: 600;
}
}
.at {
color: var(--link);
opacity: 0.8;
display: inline-block;
height: 50%;
line-height: 1;
padding: 0 0.1em;
vertical-align: -25%;
margin: 0;
}
&.-striped {
& .userName,
& .full {
background-image:
repeating-linear-gradient(
135deg,
var(--____highlight-tintColor),
var(--____highlight-tintColor) 5px,
var(--____highlight-tintColor2) 5px,
var(--____highlight-tintColor2) 10px
);
}
}
&.-solid {
& .userName,
& .full {
background-image: linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2));
}
}
&.-side {
& .userName,
& .userNameFull {
box-shadow: 0 -5px 3px -4px inset var(--____highlight-solidColor);
}
}
}
&:hover .new .full {
opacity: 1;
pointer-events: initial;
}
}
@@ -0,0 +1,56 @@
<template>
<span
class="MentionLink"
>
<!-- eslint-disable vue/no-v-html -->
<a
v-if="!user"
:href="url"
class="original"
target="_blank"
v-html="content"
/>
<!-- eslint-enable vue/no-v-html -->
<span
v-if="user"
class="new"
:style="style"
:class="classnames"
>
<a
class="short button-unstyled"
:href="url"
@click.prevent="onClick"
>
<!-- eslint-disable vue/no-v-html -->
<FAIcon
size="sm"
icon="at"
class="at"
/><span class="shortName"><span
class="userName"
v-html="userName"
/></span>
<span
v-if="isYou"
class="you"
>{{ $t('status.you') }}</span>
<!-- eslint-enable vue/no-v-html -->
</a>
<span
v-if="userName !== userNameFull"
class="full popover-default"
:class="[highlightType]"
>
<span
class="userNameFull"
v-text="'@' + userNameFull"
/>
</span>
</span>
</span>
</template>
<script src="./mention_link.js"/>
<style lang="scss" src="./mention_link.scss"/>
@@ -0,0 +1,37 @@
import MentionLink from 'src/components/mention_link/mention_link.vue'
import { mapGetters } from 'vuex'
export const MENTIONS_LIMIT = 5
const MentionsLine = {
name: 'MentionsLine',
props: {
mentions: {
required: true,
type: Array
}
},
data: () => ({ expanded: false }),
components: {
MentionLink
},
computed: {
mentionsComputed () {
return this.mentions.slice(0, MENTIONS_LIMIT)
},
extraMentions () {
return this.mentions.slice(MENTIONS_LIMIT)
},
manyMentions () {
return this.extraMentions.length > 0
},
...mapGetters(['mergedConfig'])
},
methods: {
toggleShowMore () {
this.expanded = !this.expanded
}
}
}
export default MentionsLine
@@ -0,0 +1,11 @@
.MentionsLine {
.showMoreLess {
white-space: normal;
color: var(--link);
}
.fullExtraMentions,
.mention-link:not(:last-child) {
margin-right: 0.25em;
}
}
@@ -0,0 +1,43 @@
<template>
<span class="MentionsLine">
<MentionLink
v-for="mention in mentionsComputed"
:key="mention.index"
class="mention-link"
:content="mention.content"
:url="mention.url"
:first-mention="false"
/><span
v-if="manyMentions"
class="extraMentions"
>
<span
v-if="expanded"
class="fullExtraMentions"
>
<MentionLink
v-for="mention in extraMentions"
:key="mention.index"
class="mention-link"
:content="mention.content"
:url="mention.url"
:first-mention="false"
/>
</span><button
v-if="!expanded"
class="button-unstyled showMoreLess"
@click="toggleShowMore"
>
{{ $t('status.plus_more', { number: extraMentions.length }) }}
</button><button
v-if="expanded"
class="button-unstyled showMoreLess"
@click="toggleShowMore"
>
{{ $t('general.show_less') }}
</button>
</span>
</span>
</template>
<script src="./mentions_line.js" ></script>
<style lang="scss" src="./mentions_line.scss" />
@@ -44,6 +44,9 @@ const MobilePostStatusButton = {
return this.autohideFloatingPostButton && (this.hidden || this.inputActive) return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
}, },
isPersistent () {
return !!this.$store.getters.mergedConfig.showNewPostButton
},
autohideFloatingPostButton () { autohideFloatingPostButton () {
return !!this.$store.getters.mergedConfig.autohideFloatingPostButton return !!this.$store.getters.mergedConfig.autohideFloatingPostButton
} }
@@ -2,7 +2,7 @@
<div v-if="isLoggedIn"> <div v-if="isLoggedIn">
<button <button
class="button-default new-status-button" class="button-default new-status-button"
:class="{ 'hidden': isHidden }" :class="{ 'hidden': isHidden, 'always-show': isPersistent }"
@click="openPostForm" @click="openPostForm"
> >
<FAIcon icon="pen" /> <FAIcon icon="pen" />
@@ -47,7 +47,7 @@
} }
@media all and (min-width: 801px) { @media all and (min-width: 801px) {
.new-status-button { .new-status-button:not(.always-show) {
display: none; display: none;
} }
} }
@@ -1,17 +1,56 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
import { get } from 'lodash' import { get } from 'lodash'
/**
* This is for backwards compatibility. We originally didn't recieve
* extra info like a reason why an instance was rejected/quarantined/etc.
* Because we didn't want to break backwards compatibility it was decided
* to add an extra "info" key.
*/
const toInstanceReasonObject = (instances, info, key) => {
return instances.map(instance => {
if (info[key] && info[key][instance] && info[key][instance]['reason']) {
return { instance: instance, reason: info[key][instance]['reason'] }
}
return { instance: instance, reason: '' }
})
}
const MRFTransparencyPanel = { const MRFTransparencyPanel = {
computed: { computed: {
...mapState({ ...mapState({
federationPolicy: state => get(state, 'instance.federationPolicy'), federationPolicy: state => get(state, 'instance.federationPolicy'),
mrfPolicies: state => get(state, 'instance.federationPolicy.mrf_policies', []), mrfPolicies: state => get(state, 'instance.federationPolicy.mrf_policies', []),
quarantineInstances: state => get(state, 'instance.federationPolicy.quarantined_instances', []), quarantineInstances: state => toInstanceReasonObject(
acceptInstances: state => get(state, 'instance.federationPolicy.mrf_simple.accept', []), get(state, 'instance.federationPolicy.quarantined_instances', []),
rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []), get(state, 'instance.federationPolicy.quarantined_instances_info', []),
ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []), 'quarantined_instances'
mediaNsfwInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []), ),
mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', []), acceptInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.accept', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'accept'
),
rejectInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.reject', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'reject'
),
ftlRemovalInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'federated_timeline_removal'
),
mediaNsfwInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'media_nsfw'
),
mediaRemovalInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.media_removal', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'media_removal'
),
keywordsFtlRemoval: state => get(state, 'instance.federationPolicy.mrf_keyword.federated_timeline_removal', []), keywordsFtlRemoval: state => get(state, 'instance.federationPolicy.mrf_keyword.federated_timeline_removal', []),
keywordsReject: state => get(state, 'instance.federationPolicy.mrf_keyword.reject', []), keywordsReject: state => get(state, 'instance.federationPolicy.mrf_keyword.reject', []),
keywordsReplace: state => get(state, 'instance.federationPolicy.mrf_keyword.replace', []) keywordsReplace: state => get(state, 'instance.federationPolicy.mrf_keyword.replace', [])
@@ -0,0 +1,21 @@
.mrf-section {
margin: 1em;
table {
width:100%;
text-align: left;
padding-left:10px;
padding-bottom:20px;
th, td {
width: 180px;
max-width: 360px;
overflow: hidden;
vertical-align: text-top;
}
th+th, td+td {
width: auto;
}
}
}
@@ -31,13 +31,24 @@
<p>{{ $t("about.mrf.simple.accept_desc") }}</p> <p>{{ $t("about.mrf.simple.accept_desc") }}</p>
<ul> <table>
<li <tr>
v-for="instance in acceptInstances" <th>{{ $t("about.mrf.simple.instance") }}</th>
:key="instance" <th>{{ $t("about.mrf.simple.reason") }}</th>
v-text="instance" </tr>
/> <tr
</ul> v-for="entry in acceptInstances"
:key="entry.instance + '_accept'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div> </div>
<div v-if="rejectInstances.length"> <div v-if="rejectInstances.length">
@@ -45,13 +56,24 @@
<p>{{ $t("about.mrf.simple.reject_desc") }}</p> <p>{{ $t("about.mrf.simple.reject_desc") }}</p>
<ul> <table>
<li <tr>
v-for="instance in rejectInstances" <th>{{ $t("about.mrf.simple.instance") }}</th>
:key="instance" <th>{{ $t("about.mrf.simple.reason") }}</th>
v-text="instance" </tr>
/> <tr
</ul> v-for="entry in rejectInstances"
:key="entry.instance + '_reject'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div> </div>
<div v-if="quarantineInstances.length"> <div v-if="quarantineInstances.length">
@@ -59,13 +81,24 @@
<p>{{ $t("about.mrf.simple.quarantine_desc") }}</p> <p>{{ $t("about.mrf.simple.quarantine_desc") }}</p>
<ul> <table>
<li <tr>
v-for="instance in quarantineInstances" <th>{{ $t("about.mrf.simple.instance") }}</th>
:key="instance" <th>{{ $t("about.mrf.simple.reason") }}</th>
v-text="instance" </tr>
/> <tr
</ul> v-for="entry in quarantineInstances"
:key="entry.instance + '_quarantine'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div> </div>
<div v-if="ftlRemovalInstances.length"> <div v-if="ftlRemovalInstances.length">
@@ -73,13 +106,24 @@
<p>{{ $t("about.mrf.simple.ftl_removal_desc") }}</p> <p>{{ $t("about.mrf.simple.ftl_removal_desc") }}</p>
<ul> <table>
<li <tr>
v-for="instance in ftlRemovalInstances" <th>{{ $t("about.mrf.simple.instance") }}</th>
:key="instance" <th>{{ $t("about.mrf.simple.reason") }}</th>
v-text="instance" </tr>
/> <tr
</ul> v-for="entry in ftlRemovalInstances"
:key="entry.instance + '_ftl_removal'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div> </div>
<div v-if="mediaNsfwInstances.length"> <div v-if="mediaNsfwInstances.length">
@@ -87,13 +131,24 @@
<p>{{ $t("about.mrf.simple.media_nsfw_desc") }}</p> <p>{{ $t("about.mrf.simple.media_nsfw_desc") }}</p>
<ul> <table>
<li <tr>
v-for="instance in mediaNsfwInstances" <th>{{ $t("about.mrf.simple.instance") }}</th>
:key="instance" <th>{{ $t("about.mrf.simple.reason") }}</th>
v-text="instance" </tr>
/> <tr
</ul> v-for="entry in mediaNsfwInstances"
:key="entry.instance + '_media_nsfw'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div> </div>
<div v-if="mediaRemovalInstances.length"> <div v-if="mediaRemovalInstances.length">
@@ -101,13 +156,24 @@
<p>{{ $t("about.mrf.simple.media_removal_desc") }}</p> <p>{{ $t("about.mrf.simple.media_removal_desc") }}</p>
<ul> <table>
<li <tr>
v-for="instance in mediaRemovalInstances" <th>{{ $t("about.mrf.simple.instance") }}</th>
:key="instance" <th>{{ $t("about.mrf.simple.reason") }}</th>
v-text="instance" </tr>
/> <tr
</ul> v-for="entry in mediaRemovalInstances"
:key="entry.instance + '_media_removal'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div> </div>
<h2 v-if="hasKeywordPolicies"> <h2 v-if="hasKeywordPolicies">
@@ -161,7 +227,6 @@
<script src="./mrf_transparency_panel.js"></script> <script src="./mrf_transparency_panel.js"></script>
<style lang="scss"> <style lang="scss">
.mrf-section { @import '../../_variables.scss';
margin: 1em; @import './mrf_transparency_panel.scss';
}
</style> </style>
+3 -1
View File
@@ -4,6 +4,7 @@ import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue' import Timeago from '../timeago/timeago.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@@ -44,7 +45,8 @@ const Notification = {
UserAvatar, UserAvatar,
UserCard, UserCard,
Timeago, Timeago,
Status Status,
RichContent
}, },
methods: { methods: {
toggleUserExpanded () { toggleUserExpanded () {
@@ -2,6 +2,8 @@
// TODO Copypaste from Status, should unify it somehow // TODO Copypaste from Status, should unify it somehow
.Notification { .Notification {
--emoji-size: 14px;
&.-muted { &.-muted {
padding: 0.25em 0.6em; padding: 0.25em 0.6em;
height: 1.2em; height: 1.2em;
+8 -6
View File
@@ -51,12 +51,14 @@
<span class="notification-details"> <span class="notification-details">
<div class="name-and-action"> <div class="name-and-action">
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<bdi <bdi v-if="!!notification.from_profile.name_html">
v-if="!!notification.from_profile.name_html" <RichContent
class="username" class="username"
:title="'@'+notification.from_profile.screen_name_ui" :title="'@'+notification.from_profile.screen_name_ui"
v-html="notification.from_profile.name_html" :html="notification.from_profile.name_html"
/> :emoji="notification.from_profile.emoji"
/>
</bdi>
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
<span <span
v-else v-else
@@ -148,13 +148,6 @@
max-width: 100%; max-width: 100%;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
img {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
} }
.timeago { .timeago {
+7 -3
View File
@@ -1,10 +1,14 @@
import Timeago from '../timeago/timeago.vue' import Timeago from 'components/timeago/timeago.vue'
import RichContent from 'components/rich_content/rich_content.jsx'
import { forEach, map } from 'lodash' import { forEach, map } from 'lodash'
export default { export default {
name: 'Poll', name: 'Poll',
props: ['basePoll'], props: ['basePoll', 'emoji'],
components: { Timeago }, components: {
Timeago,
RichContent
},
data () { data () {
return { return {
loading: false, loading: false,
+10 -4
View File
@@ -17,8 +17,11 @@
<span class="result-percentage"> <span class="result-percentage">
{{ percentageForOption(option.votes_count) }}% {{ percentageForOption(option.votes_count) }}%
</span> </span>
<!-- eslint-disable-next-line vue/no-v-html --> <RichContent
<span v-html="option.title_html" /> :html="option.title_html"
:handle-links="false"
:emoji="emoji"
/>
</div> </div>
<div <div
class="result-fill" class="result-fill"
@@ -42,8 +45,11 @@
:value="index" :value="index"
> >
<label class="option-vote"> <label class="option-vote">
<!-- eslint-disable-next-line vue/no-v-html --> <RichContent
<div v-html="option.title_html" /> :html="option.title_html"
:handle-links="false"
:emoji="emoji"
/>
</label> </label>
</div> </div>
</div> </div>
@@ -0,0 +1,327 @@
import Vue from 'vue'
import { unescape, flattenDeep } from 'lodash'
import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_converter/utility.service.js'
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
import StillImage from 'src/components/still-image/still-image.vue'
import MentionsLine, { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.vue'
import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue'
import './rich_content.scss'
/**
* RichContent, The Über-powered component for rendering Post HTML.
*
* This takes post HTML and does multiple things to it:
* - Groups all mentions into <MentionsLine>, this affects all mentions regardles
* of where they are (beginning/middle/end), even single mentions are converted
* to a <MentionsLine> containing single <MentionLink>.
* - Replaces emoji shortcodes with <StillImage>'d images.
*
* There are two problems with this component's architecture:
* 1. Parsing HTML and rendering are inseparable. Attempts to separate the two
* proven to be a massive overcomplication due to amount of things done here.
* 2. We need to output both render and some extra data, which seems to be imp-
* possible in vue. Current solution is to emit 'parseReady' event when parsing
* is done within render() function.
*
* Apart from that one small hiccup with emit in render this _should_ be vue3-ready
*/
export default Vue.component('RichContent', {
name: 'RichContent',
props: {
// Original html content
html: {
required: true,
type: String
},
attentions: {
required: false,
default: () => []
},
// Emoji object, as in status.emojis, note the "s" at the end...
emoji: {
required: true,
type: Array
},
// Whether to handle links or not (posts: yes, everything else: no)
handleLinks: {
required: false,
type: Boolean,
default: false
},
// Meme arrows
greentext: {
required: false,
type: Boolean,
default: false
}
},
// NEVER EVER TOUCH DATA INSIDE RENDER
render (h) {
// Pre-process HTML
const { newHtml: html } = preProcessPerLine(this.html, this.greentext)
let currentMentions = null // Current chain of mentions, we group all mentions together
// This is used to recover spacing removed when parsing mentions
let lastSpacing = ''
const lastTags = [] // Tags that appear at the end of post body
const writtenMentions = [] // All mentions that appear in post body
const invisibleMentions = [] // All mentions that go beyond the limiter (see MentionsLine)
// to collapse too many mentions in a row
const writtenTags = [] // All tags that appear in post body
// unique index for vue "tag" property
let mentionIndex = 0
let tagsIndex = 0
const renderImage = (tag) => {
return <StillImage
{...{ attrs: getAttrs(tag) }}
class="img"
/>
}
const renderHashtag = (attrs, children, encounteredTextReverse) => {
const linkData = getLinkData(attrs, children, tagsIndex++)
writtenTags.push(linkData)
if (!encounteredTextReverse) {
lastTags.push(linkData)
}
return <HashtagLink {...{ props: linkData }}/>
}
const renderMention = (attrs, children) => {
const linkData = getLinkData(attrs, children, mentionIndex++)
linkData.notifying = this.attentions.some(a => a.statusnet_profile_url === linkData.url)
writtenMentions.push(linkData)
if (currentMentions === null) {
currentMentions = []
}
currentMentions.push(linkData)
if (currentMentions.length > MENTIONS_LIMIT) {
invisibleMentions.push(linkData)
}
if (currentMentions.length === 1) {
return <MentionsLine mentions={ currentMentions } />
} else {
return ''
}
}
// Processor to use with html_tree_converter
const processItem = (item, index, array, what) => {
// Handle text nodes - just add emoji
if (typeof item === 'string') {
const emptyText = item.trim() === ''
if (item.includes('\n')) {
currentMentions = null
}
if (emptyText) {
// don't include spaces when processing mentions - we'll include them
// in MentionsLine
lastSpacing = item
return currentMentions !== null ? item.trim() : item
}
currentMentions = null
if (item.includes(':')) {
item = ['', processTextForEmoji(
item,
this.emoji,
({ shortcode, url }) => {
return <StillImage
class="emoji img"
src={url}
title={`:${shortcode}:`}
alt={`:${shortcode}:`}
/>
}
)]
}
return item
}
// Handle tag nodes
if (Array.isArray(item)) {
const [opener, children, closer] = item
const Tag = getTagName(opener)
const attrs = getAttrs(opener)
const previouslyMentions = currentMentions !== null
/* During grouping of mentions we trim all the empty text elements
* This padding is added to recover last space removed in case
* we have a tag right next to mentions
*/
const mentionsLinePadding =
// Padding is only needed if we just finished parsing mentions
previouslyMentions &&
// Don't add padding if content is string and has padding already
!(children && typeof children[0] === 'string' && children[0].match(/^\s/))
? lastSpacing
: ''
switch (Tag) {
case 'br':
currentMentions = null
break
case 'img': // replace images with StillImage
return ['', [mentionsLinePadding, renderImage(opener)], '']
case 'a': // replace mentions with MentionLink
if (!this.handleLinks) break
if (attrs['class'] && attrs['class'].includes('mention')) {
// Handling mentions here
return renderMention(attrs, children)
} else {
currentMentions = null
break
}
case 'span':
if (this.handleLinks && attrs['class'] && attrs['class'].includes('h-card')) {
return ['', children.map(processItem), '']
}
}
if (children !== undefined) {
return [
'',
[
mentionsLinePadding,
[opener, children.map(processItem), closer]
],
''
]
} else {
return ['', [mentionsLinePadding, item], '']
}
}
}
// Processor for back direction (for finding "last" stuff, just easier this way)
let encounteredTextReverse = false
const processItemReverse = (item, index, array, what) => {
// Handle text nodes - just add emoji
if (typeof item === 'string') {
const emptyText = item.trim() === ''
if (emptyText) return item
if (!encounteredTextReverse) encounteredTextReverse = true
return unescape(item)
} else if (Array.isArray(item)) {
// Handle tag nodes
const [opener, children] = item
const Tag = opener === '' ? '' : getTagName(opener)
switch (Tag) {
case 'a': // replace mentions with MentionLink
if (!this.handleLinks) break
const attrs = getAttrs(opener)
// should only be this
if (
(attrs['class'] && attrs['class'].includes('hashtag')) || // Pleroma style
(attrs['rel'] === 'tag') // Mastodon style
) {
return renderHashtag(attrs, children, encounteredTextReverse)
} else {
attrs.target = '_blank'
const newChildren = [...children].reverse().map(processItemReverse).reverse()
return <a {...{ attrs }}>
{ newChildren }
</a>
}
case '':
return [...children].reverse().map(processItemReverse).reverse()
}
// Render tag as is
if (children !== undefined) {
const newChildren = Array.isArray(children)
? [...children].reverse().map(processItemReverse).reverse()
: children
return <Tag {...{ attrs: getAttrs(opener) }}>
{ newChildren }
</Tag>
} else {
return <Tag/>
}
}
return item
}
const pass1 = convertHtmlToTree(html).map(processItem)
const pass2 = [...pass1].reverse().map(processItemReverse).reverse()
// DO NOT USE SLOTS they cause a re-render feedback loop here.
// slots updated -> rerender -> emit -> update up the tree -> rerender -> ...
// at least until vue3?
const result = <span class="RichContent">
{ pass2 }
</span>
const event = {
lastTags,
writtenMentions,
writtenTags,
invisibleMentions
}
// DO NOT MOVE TO UPDATE. BAD IDEA.
this.$emit('parseReady', event)
return result
}
})
const getLinkData = (attrs, children, index) => {
const stripTags = (item) => {
if (typeof item === 'string') {
return item
} else {
return item[1].map(stripTags).join('')
}
}
const textContent = children.map(stripTags).join('')
return {
index,
url: attrs.href,
tag: attrs['data-tag'],
content: flattenDeep(children).join(''),
textContent
}
}
/** Pre-processing HTML
*
* Currently this does one thing:
* - add green/cyantexting
*
* @param {String} html - raw HTML to process
* @param {Boolean} greentext - whether to enable greentexting or not
*/
export const preProcessPerLine = (html, greentext) => {
const greentextHandle = new Set(['p', 'div'])
const lines = convertHtmlToLines(html)
const newHtml = lines.reverse().map((item, index, array) => {
if (!item.text) return item
const string = item.text
// Greentext stuff
if (
// Only if greentext is engaged
greentext &&
// Only handle p's and divs. Don't want to affect blockquotes, code etc
item.level.every(l => greentextHandle.has(l)) &&
// Only if line begins with '>' or '<'
(string.includes('&gt;') || string.includes('&lt;'))
) {
const cleanedString = string.replace(/<[^>]+?>/gi, '') // remove all tags
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
.trim()
if (cleanedString.startsWith('&gt;')) {
return `<span class='greentext'>${string}</span>`
} else if (cleanedString.startsWith('&lt;')) {
return `<span class='cyantext'>${string}</span>`
}
}
return string
}).reverse().join('')
return { newHtml }
}
@@ -0,0 +1,64 @@
.RichContent {
blockquote {
margin: 0.2em 0 0.2em 2em;
font-style: italic;
}
pre {
overflow: auto;
}
code,
samp,
kbd,
var,
pre {
font-family: var(--postCodeFont, monospace);
}
p {
margin: 0 0 1em 0;
}
p:last-child {
margin: 0 0 0 0;
}
h1 {
font-size: 1.1em;
line-height: 1.2em;
margin: 1.4em 0;
}
h2 {
font-size: 1.1em;
margin: 1em 0;
}
h3 {
font-size: 1em;
margin: 1.2em 0;
}
h4 {
margin: 1.1em 0;
}
.img {
display: inline-block;
}
.emoji {
display: inline-block;
width: var(--emoji-size, 32px);
height: var(--emoji-size, 32px);
}
.img,
video {
max-width: 100%;
max-height: 400px;
vertical-align: middle;
object-fit: contain;
}
}
@@ -122,6 +122,11 @@
{{ $t('settings.sensitive_by_default') }} {{ $t('settings.sensitive_by_default') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<BooleanSetting path="alwaysShowNewPostButton">
{{ $t('settings.always_show_post_button') }}
</BooleanSetting>
</li>
<li> <li>
<BooleanSetting path="autohideFloatingPostButton"> <BooleanSetting path="autohideFloatingPostButton">
{{ $t('settings.autohide_floating_post_button') }} {{ $t('settings.autohide_floating_post_button') }}
@@ -24,7 +24,7 @@ library.add(
const ProfileTab = { const ProfileTab = {
data () { data () {
return { return {
newName: this.$store.state.users.currentUser.name, newName: this.$store.state.users.currentUser.name_unescaped,
newBio: unescape(this.$store.state.users.currentUser.description), newBio: unescape(this.$store.state.users.currentUser.description),
newLocked: this.$store.state.users.currentUser.locked, newLocked: this.$store.state.users.currentUser.locked,
newNoRichText: this.$store.state.users.currentUser.no_rich_text, newNoRichText: this.$store.state.users.currentUser.no_rich_text,
@@ -475,7 +475,7 @@ export default {
this.loadThemeFromLocalStorage(false, true) this.loadThemeFromLocalStorage(false, true)
break break
case 'file': case 'file':
console.err('Forcing snapshout from file is not supported yet') console.error('Forcing snapshot from file is not supported yet')
break break
} }
this.dismissWarning() this.dismissWarning()
@@ -270,6 +270,9 @@
.apply-container { .apply-container {
justify-content: center; justify-content: center;
position: absolute;
bottom: 8px;
right: 5px;
} }
.radius-item, .radius-item,
+8 -1
View File
@@ -79,12 +79,19 @@
.floating-shout { .floating-shout {
position: fixed; position: fixed;
right: 0px;
bottom: 0px; bottom: 0px;
z-index: 1000; z-index: 1000;
max-width: 25em; max-width: 25em;
} }
.floating-shout.left {
left: 0px;
}
.floating-shout:not(.left) {
right: 0px;
}
.shout-panel { .shout-panel {
.shout-heading { .shout-heading {
cursor: pointer; cursor: pointer;
@@ -49,6 +49,7 @@ const SideDrawer = {
currentUser () { currentUser () {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
}, },
shout () { return this.$store.state.shout.channel.state === 'joined' },
unseenNotifications () { unseenNotifications () {
return unseenNotificationsFromStore(this.$store) return unseenNotificationsFromStore(this.$store)
}, },
+2 -2
View File
@@ -106,10 +106,10 @@
</router-link> </router-link>
</li> </li>
<li <li
v-if="chat" v-if="shout"
@click="toggleDrawer" @click="toggleDrawer"
> >
<router-link :to="{ name: 'chat-panel' }"> <router-link :to="{ name: 'shout-panel' }">
<FAIcon <FAIcon
fixed-width fixed-width
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
+36 -4
View File
@@ -9,9 +9,12 @@ import UserAvatar from '../user_avatar/user_avatar.vue'
import AvatarList from '../avatar_list/avatar_list.vue' import AvatarList from '../avatar_list/avatar_list.vue'
import Timeago from '../timeago/timeago.vue' import Timeago from '../timeago/timeago.vue'
import StatusContent from '../status_content/status_content.vue' import StatusContent from '../status_content/status_content.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import StatusPopover from '../status_popover/status_popover.vue' import StatusPopover from '../status_popover/status_popover.vue'
import UserListPopover from '../user_list_popover/user_list_popover.vue' import UserListPopover from '../user_list_popover/user_list_popover.vue'
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue' import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
import MentionLink from 'src/components/mention_link/mention_link.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { muteWordHits } from '../../services/status_parser/status_parser.js' import { muteWordHits } from '../../services/status_parser/status_parser.js'
@@ -68,7 +71,10 @@ const Status = {
StatusPopover, StatusPopover,
UserListPopover, UserListPopover,
EmojiReactions, EmojiReactions,
StatusContent StatusContent,
RichContent,
MentionLink,
MentionsLine
}, },
props: [ props: [
'statusoid', 'statusoid',
@@ -92,7 +98,8 @@ const Status = {
userExpanded: false, userExpanded: false,
mediaPlaying: [], mediaPlaying: [],
suspendable: true, suspendable: true,
error: null error: null,
headTailLinks: null
} }
}, },
computed: { computed: {
@@ -132,12 +139,15 @@ const Status = {
}, },
replyProfileLink () { replyProfileLink () {
if (this.isReply) { if (this.isReply) {
return this.generateUserProfileLink(this.status.in_reply_to_user_id, this.replyToName) const user = this.$store.getters.findUser(this.status.in_reply_to_user_id)
// FIXME Why user not found sometimes???
return user ? user.statusnet_profile_url : 'NOT_FOUND'
} }
}, },
retweet () { return !!this.statusoid.retweeted_status }, retweet () { return !!this.statusoid.retweeted_status },
retweeterUser () { return this.statusoid.user },
retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name_ui }, retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name_ui },
retweeterHtml () { return this.statusoid.user.name_html }, retweeterHtml () { return this.statusoid.user.name },
retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) }, retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) },
status () { status () {
if (this.retweet) { if (this.retweet) {
@@ -156,6 +166,25 @@ const Status = {
muteWordHits () { muteWordHits () {
return muteWordHits(this.status, this.muteWords) return muteWordHits(this.status, this.muteWords)
}, },
mentionsLine () {
if (!this.headTailLinks) return []
const writtenSet = new Set(this.headTailLinks.writtenMentions.map(_ => _.url))
return this.status.attentions.filter(attn => {
// no reply user
return attn.id !== this.status.in_reply_to_user_id &&
// no self-replies
attn.statusnet_profile_url !== this.status.user.statusnet_profile_url &&
// don't include if mentions is written
!writtenSet.has(attn.statusnet_profile_url)
}).map(attn => ({
url: attn.statusnet_profile_url,
content: attn.screen_name,
userId: attn.id
}))
},
hasMentionsLine () {
return this.mentionsLine.length > 0
},
muted () { muted () {
if (this.statusoid.user.id === this.currentUser.id) return false if (this.statusoid.user.id === this.currentUser.id) return false
const { status } = this const { status } = this
@@ -303,6 +332,9 @@ const Status = {
}, },
removeMediaPlaying (id) { removeMediaPlaying (id) {
this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id) this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id)
},
setHeadTailLinks (headTailLinks) {
this.headTailLinks = headTailLinks
} }
}, },
watch: { watch: {
+20 -29
View File
@@ -1,10 +1,10 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
$status-margin: 0.75em; $status-margin: 0.75em;
.Status { .Status {
min-width: 0; min-width: 0;
white-space: normal;
&:hover { &:hover {
--_still-image-img-visibility: visible; --_still-image-img-visibility: visible;
@@ -93,12 +93,8 @@ $status-margin: 0.75em;
margin-right: 0.4em; margin-right: 0.4em;
text-overflow: ellipsis; text-overflow: ellipsis;
.emoji { --_still_image-label-scale: 0.25;
width: 14px; --emoji-size: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain;
}
} }
.status-favicon { .status-favicon {
@@ -155,35 +151,24 @@ $status-margin: 0.75em;
} }
} }
.glued-label {
display: inline-flex;
white-space: nowrap;
}
.timeago { .timeago {
margin-right: 0.2em; margin-right: 0.2em;
} }
.heading-reply-row { & .heading-reply-row {
position: relative; position: relative;
align-content: baseline; align-content: baseline;
font-size: 12px; font-size: 12px;
line-height: 18px; line-height: 160%;
max-width: 100%; max-width: 100%;
display: flex;
flex-wrap: wrap;
align-items: stretch; align-items: stretch;
} }
.reply-to-and-accountname {
display: flex;
height: 18px;
margin-right: 0.5em;
max-width: 100%;
.reply-to-link {
white-space: nowrap;
word-break: break-word;
text-overflow: ellipsis;
overflow-x: hidden;
}
}
& .reply-to-popover, & .reply-to-popover,
& .reply-to-no-popover { & .reply-to-no-popover {
min-width: 0; min-width: 0;
@@ -220,21 +205,27 @@ $status-margin: 0.75em;
} }
} }
.reply-to { & .mentions,
& .reply-to {
white-space: nowrap;
position: relative; position: relative;
padding-right: 0.25em;
} }
.reply-to-text { & .mentions-text,
& .reply-to-text {
color: var(--faint);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.replies-separator { .mentions-line {
margin-left: 0.4em; display: inline;
} }
.replies { .replies {
margin-top: 0.25em;
line-height: 18px; line-height: 18px;
font-size: 12px; font-size: 12px;
display: flex; display: flex;
+74 -40
View File
@@ -1,5 +1,4 @@
<template> <template>
<!-- eslint-disable vue/no-v-html -->
<div <div
v-if="!hideStatus" v-if="!hideStatus"
class="Status" class="Status"
@@ -89,8 +88,12 @@
<router-link <router-link
v-if="retweeterHtml" v-if="retweeterHtml"
:to="retweeterProfileLink" :to="retweeterProfileLink"
v-html="retweeterHtml" >
/> <RichContent
:html="retweeterHtml"
:emoji="retweeterUser.emoji"
/>
</router-link>
<router-link <router-link
v-else v-else
:to="retweeterProfileLink" :to="retweeterProfileLink"
@@ -145,8 +148,12 @@
v-if="status.user.name_html" v-if="status.user.name_html"
class="status-username" class="status-username"
:title="status.user.name" :title="status.user.name"
v-html="status.user.name_html" >
/> <RichContent
:html="status.user.name"
:emoji="status.user.emoji"
/>
</h4>
<h4 <h4
v-else v-else
class="status-username" class="status-username"
@@ -214,11 +221,13 @@
</button> </button>
</span> </span>
</div> </div>
<div
<div class="heading-reply-row"> v-if="isReply || hasMentionsLine"
<div class="heading-reply-row"
>
<span
v-if="isReply" v-if="isReply"
class="reply-to-and-accountname" class="glued-label"
> >
<StatusPopover <StatusPopover
v-if="!isPreview" v-if="!isPreview"
@@ -238,7 +247,7 @@
flip="horizontal" flip="horizontal"
/> />
<span <span
class="faint-link reply-to-text" class="reply-to-text"
> >
{{ $t('status.reply_to') }} {{ $t('status.reply_to') }}
</span> </span>
@@ -251,50 +260,76 @@
> >
<span class="reply-to-text">{{ $t('status.reply_to') }}</span> <span class="reply-to-text">{{ $t('status.reply_to') }}</span>
</span> </span>
<router-link <MentionLink
class="reply-to-link" :content="replyToName"
:title="replyToName" :url="replyProfileLink"
:to="replyProfileLink" :user-id="status.in_reply_to_user_id"
> :user-screen-name="status.in_reply_to_screen_name"
{{ replyToName }} :first-mention="false"
</router-link> />
<span </span>
v-if="replies && replies.length"
class="faint replies-separator" <!-- This little wrapper is made for sole purpose of "gluing" -->
> <!-- "Mentions" label to the first mention -->
- <span
</span> v-if="hasMentionsLine"
</div> class="glued-label"
<div
v-if="inConversation && !isPreview && replies && replies.length"
class="replies"
> >
<span class="faint">{{ $t('status.replies_list') }}</span> <span
<StatusPopover class="mentions"
v-for="reply in replies" :aria-label="$t('tool_tip.mentions')"
:key="reply.id" @click.prevent="gotoOriginal(status.in_reply_to_status_id)"
:status-id="reply.id"
> >
<button <span
class="button-unstyled -link reply-link" class="mentions-text"
@click.prevent="gotoOriginal(reply.id)"
> >
{{ reply.name }} {{ $t('status.mentions') }}
</button> </span>
</StatusPopover> </span>
</div> <MentionsLine
v-if="hasMentionsLine"
:mentions="mentionsLine.slice(0, 1)"
class="mentions-line-first"
/>
</span>
<MentionsLine
v-if="hasMentionsLine"
:mentions="mentionsLine.slice(1)"
class="mentions-line"
/>
</div> </div>
</div> </div>
<StatusContent <StatusContent
ref="content"
:status="status" :status="status"
:no-heading="noHeading" :no-heading="noHeading"
:highlight="highlight" :highlight="highlight"
:focused="isFocused" :focused="isFocused"
@mediaplay="addMediaPlaying($event)" @mediaplay="addMediaPlaying($event)"
@mediapause="removeMediaPlaying($event)" @mediapause="removeMediaPlaying($event)"
@parseReady="setHeadTailLinks"
/> />
<div
v-if="inConversation && !isPreview && replies && replies.length"
class="replies"
>
<span class="faint">{{ $t('status.replies_list') }}</span>
<StatusPopover
v-for="reply in replies"
:key="reply.id"
:status-id="reply.id"
>
<button
class="button-unstyled -link reply-link"
@click.prevent="gotoOriginal(reply.id)"
>
{{ reply.name }}
</button>
</StatusPopover>
</div>
<transition name="fade"> <transition name="fade">
<div <div
v-if="!hidePostStats && isFocused && combinedFavsAndRepeatsUsers.length > 0" v-if="!hidePostStats && isFocused && combinedFavsAndRepeatsUsers.length > 0"
@@ -402,7 +437,6 @@
</div> </div>
</template> </template>
</div> </div>
<!-- eslint-enable vue/no-v-html -->
</template> </template>
<script src="./status.js" ></script> <script src="./status.js" ></script>
+127
View File
@@ -0,0 +1,127 @@
import fileType from 'src/services/file_type/file_type.service'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faFile,
faMusic,
faImage,
faLink,
faPollH
} from '@fortawesome/free-solid-svg-icons'
library.add(
faFile,
faMusic,
faImage,
faLink,
faPollH
)
const StatusContent = {
name: 'StatusContent',
props: [
'status',
'focused',
'noHeading',
'fullContent',
'singleLine'
],
data () {
return {
showingTall: this.fullContent || (this.inConversation && this.focused),
showingLongSubject: false,
// not as computed because it sets the initial state which will be changed later
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
postLength: this.status.text.length,
parseReadyDone: false
}
},
computed: {
localCollapseSubjectDefault () {
return this.mergedConfig.collapseMessageWithSubject
},
// This is a bit hacky, but we want to approximate post height before rendering
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
// as well as approximate line count by counting characters and approximating ~80
// per line.
//
// Using max-height + overflow: auto for status components resulted in false positives
// very often with japanese characters, and it was very annoying.
tallStatus () {
const lengthScore = this.status.raw_html.split(/<p|<br/).length + this.postLength / 80
return lengthScore > 20
},
longSubject () {
return this.status.summary.length > 240
},
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
mightHideBecauseSubject () {
return !!this.status.summary && this.localCollapseSubjectDefault
},
mightHideBecauseTall () {
return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
},
hideSubjectStatus () {
return this.mightHideBecauseSubject && !this.expandingSubject
},
hideTallStatus () {
return this.mightHideBecauseTall && !this.showingTall
},
showingMore () {
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
},
attachmentTypes () {
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
},
...mapGetters(['mergedConfig'])
},
components: {
RichContent
},
mounted () {
this.status.attentions && this.status.attentions.forEach(attn => {
const { id } = attn
this.$store.dispatch('fetchUserIfMissing', id)
})
},
methods: {
onParseReady (event) {
if (this.parseReadyDone) return
this.parseReadyDone = true
this.$emit('parseReady', event)
const { writtenMentions, invisibleMentions } = event
writtenMentions
.filter(mention => !mention.notifying)
.forEach(mention => {
const { content, url } = mention
const cleanedString = content.replace(/<[^>]+?>/gi, '') // remove all tags
if (!cleanedString.startsWith('@')) return
const handle = cleanedString.slice(1)
const host = url.replace(/^https?:\/\//, '').replace(/\/.+?$/, '')
this.$store.dispatch('fetchUserIfMissing', `${handle}@${host}`)
})
/* This is a bit of a hack to make current tall status detector work
* with rich mentions. Invisible mentions are detected at RichContent level
* and also we generate plaintext version of mentions by stripping tags
* so here we subtract from post length by each mention that became invisible
* via MentionsLine
*/
this.postLength = invisibleMentions.reduce((acc, mention) => {
return acc - mention.textContent.length - 1
}, this.postLength)
},
toggleShowMore () {
if (this.mightHideBecauseTall) {
this.showingTall = !this.showingTall
} else if (this.mightHideBecauseSubject) {
this.expandingSubject = !this.expandingSubject
}
},
generateTagLink (tag) {
return `/tag/${tag}`
}
}
}
export default StatusContent
+118
View File
@@ -0,0 +1,118 @@
@import '../../_variables.scss';
.StatusBody {
.emoji {
--_still_image-label-scale: 0.5;
}
& .text,
& .summary {
font-family: var(--postFont, sans-serif);
white-space: pre-wrap;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
line-height: 1.4em;
}
.summary {
display: block;
font-style: italic;
padding-bottom: 0.5em;
}
.text {
&.-single-line {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
height: 1.4em;
}
}
.summary-wrapper {
margin-bottom: 0.5em;
border-style: solid;
border-width: 0 0 1px 0;
border-color: var(--border, $fallback--border);
flex-grow: 0;
&.-tall {
position: relative;
.summary {
max-height: 2em;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.text-wrapper {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
&.-tall-status {
position: relative;
height: 220px;
overflow-x: hidden;
overflow-y: hidden;
z-index: 1;
.media-body {
min-height: 0;
mask:
linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
linear-gradient(to top, white, white);
/* Autoprefixed seem to ignore this one, and also syntax is different */
-webkit-mask-composite: xor;
mask-composite: exclude;
}
}
}
& .tall-status-hider,
& .tall-subject-hider,
& .status-unhider,
& .cw-status-hider {
display: inline-block;
word-break: break-all;
width: 100%;
text-align: center;
}
.tall-status-hider {
position: absolute;
height: 70px;
margin-top: 150px;
line-height: 110px;
z-index: 2;
}
.tall-subject-hider {
// position: absolute;
padding-bottom: 0.5em;
}
& .status-unhider,
& .cw-status-hider {
word-break: break-all;
svg {
color: inherit;
}
}
.greentext {
color: $fallback--cGreen;
color: var(--postGreentext, $fallback--cGreen);
}
.cyantext {
color: var(--postCyantext, $fallback--cBlue);
}
}
@@ -0,0 +1,97 @@
<template>
<div class="StatusBody">
<div class="body">
<div
v-if="status.summary_raw_html"
class="summary-wrapper"
:class="{ '-tall': (longSubject && !showingLongSubject) }"
>
<RichContent
class="media-body summary"
:html="status.summary_raw_html"
:emoji="status.emojis"
/>
<button
v-if="longSubject && showingLongSubject"
class="button-unstyled -link tall-subject-hider"
@click.prevent="showingLongSubject=false"
>
{{ $t("status.hide_full_subject") }}
</button>
<button
v-else-if="longSubject"
class="button-unstyled -link tall-subject-hider"
@click.prevent="showingLongSubject=true"
>
{{ $t("status.show_full_subject") }}
</button>
</div>
<div
:class="{'-tall-status': hideTallStatus}"
class="text-wrapper"
>
<button
v-if="hideTallStatus"
class="button-unstyled -link tall-status-hider"
:class="{ '-focused': focused }"
@click.prevent="toggleShowMore"
>
{{ $t("general.show_more") }}
</button>
<RichContent
v-if="!hideSubjectStatus && !(singleLine && status.summary_raw_html)"
:class="{ '-single-line': singleLine }"
class="text media-body"
:html="status.raw_html"
:emoji="status.emojis"
:handle-links="true"
:greentext="mergedConfig.greentext"
:attentions="status.attentions"
@parseReady="onParseReady"
/>
<button
v-if="hideSubjectStatus"
class="button-unstyled -link cw-status-hider"
@click.prevent="toggleShowMore"
>
{{ $t("status.show_content") }}
<FAIcon
v-if="attachmentTypes.includes('image')"
icon="image"
/>
<FAIcon
v-if="attachmentTypes.includes('video')"
icon="video"
/>
<FAIcon
v-if="attachmentTypes.includes('audio')"
icon="music"
/>
<FAIcon
v-if="attachmentTypes.includes('unknown')"
icon="file"
/>
<FAIcon
v-if="status.poll && status.poll.options"
icon="poll-h"
/>
<FAIcon
v-if="status.card"
icon="link"
/>
</button>
<button
v-if="showingMore && !fullContent"
class="button-unstyled -link status-unhider"
@click.prevent="toggleShowMore"
>
{{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
</button>
</div>
</div>
<slot v-if="!hideSubjectStatus" />
</div>
</template>
<script src="./status_body.js" ></script>
<style lang="scss" src="./status_body.scss" />
+3 -118
View File
@@ -1,11 +1,9 @@
import Attachment from '../attachment/attachment.vue' import Attachment from '../attachment/attachment.vue'
import Poll from '../poll/poll.vue' import Poll from '../poll/poll.vue'
import Gallery from '../gallery/gallery.vue' import Gallery from '../gallery/gallery.vue'
import StatusBody from 'src/components/status_body/status_body.vue'
import LinkPreview from '../link-preview/link-preview.vue' import LinkPreview from '../link-preview/link-preview.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service' import fileType from 'src/services/file_type/file_type.service'
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
import { mapGetters, mapState } from 'vuex' import { mapGetters, mapState } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@@ -35,52 +33,11 @@ const StatusContent = {
'fullContent', 'fullContent',
'singleLine' 'singleLine'
], ],
data () {
return {
showingTall: this.fullContent || (this.inConversation && this.focused),
showingLongSubject: false,
// not as computed because it sets the initial state which will be changed later
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
}
},
computed: { computed: {
localCollapseSubjectDefault () {
return this.mergedConfig.collapseMessageWithSubject
},
hideAttachments () { hideAttachments () {
return (this.mergedConfig.hideAttachments && !this.inConversation) || return (this.mergedConfig.hideAttachments && !this.inConversation) ||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation) (this.mergedConfig.hideAttachmentsInConv && this.inConversation)
}, },
// This is a bit hacky, but we want to approximate post height before rendering
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
// as well as approximate line count by counting characters and approximating ~80
// per line.
//
// Using max-height + overflow: auto for status components resulted in false positives
// very often with japanese characters, and it was very annoying.
tallStatus () {
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
return lengthScore > 20
},
longSubject () {
return this.status.summary.length > 240
},
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
mightHideBecauseSubject () {
return !!this.status.summary && this.localCollapseSubjectDefault
},
mightHideBecauseTall () {
return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
},
hideSubjectStatus () {
return this.mightHideBecauseSubject && !this.expandingSubject
},
hideTallStatus () {
return this.mightHideBecauseTall && !this.showingTall
},
showingMore () {
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
},
nsfwClickthrough () { nsfwClickthrough () {
if (!this.status.nsfw) { if (!this.status.nsfw) {
return false return false
@@ -118,45 +75,11 @@ const StatusContent = {
file => !fileType.fileMatchesSomeType(this.galleryTypes, file) file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
) )
}, },
attachmentTypes () {
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
},
maxThumbnails () { maxThumbnails () {
return this.mergedConfig.maxThumbnails return this.mergedConfig.maxThumbnails
}, },
postBodyHtml () {
const html = this.status.statusnet_html
if (this.mergedConfig.greentext) {
try {
if (html.includes('&gt;')) {
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
return processHtml(html, (string) => {
if (string.includes('&gt;') &&
string
.replace(/<[^>]+?>/gi, '') // remove all tags
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
.trim()
.startsWith('&gt;')) {
return `<span class='greentext'>${string}</span>`
} else {
return string
}
})
} else {
return html
}
} catch (e) {
console.err('Failed to process status html', e)
return html
}
} else {
return html
}
},
...mapGetters(['mergedConfig']), ...mapGetters(['mergedConfig']),
...mapState({ ...mapState({
betterShadow: state => state.interface.browserSupport.cssFilter,
currentUser: state => state.users.currentUser currentUser: state => state.users.currentUser
}) })
}, },
@@ -164,48 +87,10 @@ const StatusContent = {
Attachment, Attachment,
Poll, Poll,
Gallery, Gallery,
LinkPreview LinkPreview,
StatusBody
}, },
methods: { methods: {
linkClicked (event) {
const target = event.target.closest('.status-content a')
if (target) {
if (target.className.match(/mention/)) {
const href = target.href
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
if (attn) {
event.stopPropagation()
event.preventDefault()
const link = this.generateUserProfileLink(attn.id, attn.screen_name)
this.$router.push(link)
return
}
}
if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
// Extract tag name from dataset or link url
const tag = target.dataset.tag || extractTagFromUrl(target.href)
if (tag) {
const link = this.generateTagLink(tag)
this.$router.push(link)
return
}
}
window.open(target.href, '_blank')
}
},
toggleShowMore () {
if (this.mightHideBecauseTall) {
this.showingTall = !this.showingTall
} else if (this.mightHideBecauseSubject) {
this.expandingSubject = !this.expandingSubject
}
},
generateUserProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
},
generateTagLink (tag) {
return `/tag/${tag}`
},
setMedia () { setMedia () {
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
return () => this.$store.dispatch('setMedia', attachments) return () => this.$store.dispatch('setMedia', attachments)
+41 -270
View File
@@ -1,133 +1,55 @@
<template> <template>
<!-- eslint-disable vue/no-v-html -->
<div class="StatusContent"> <div class="StatusContent">
<slot name="header" /> <slot name="header" />
<div <StatusBody
v-if="status.summary_html" :status="status"
class="summary-wrapper" :single-line="singleLine"
:class="{ 'tall-subject': (longSubject && !showingLongSubject) }" @parseReady="$emit('parseReady', $event)"
> >
<div v-if="status.poll && status.poll.options">
<Poll
:base-poll="status.poll"
:emoji="status.emojis"
/>
</div>
<div <div
class="media-body summary" v-if="status.attachments.length !== 0"
@click.prevent="linkClicked" class="attachments media-body"
v-html="status.summary_html"
/>
<button
v-if="longSubject && showingLongSubject"
class="button-unstyled -link tall-subject-hider"
@click.prevent="showingLongSubject=false"
> >
{{ $t("status.hide_full_subject") }} <attachment
</button> v-for="attachment in nonGalleryAttachments"
<button :key="attachment.id"
v-else-if="longSubject" class="non-gallery"
class="button-unstyled -link tall-subject-hider" :size="attachmentSize"
:class="{ 'tall-subject-hider_focused': focused }" :nsfw="nsfwClickthrough"
@click.prevent="showingLongSubject=true" :attachment="attachment"
> :allow-play="true"
{{ $t("status.show_full_subject") }} :set-media="setMedia()"
</button> @play="$emit('mediaplay', attachment.id)"
</div> @pause="$emit('mediapause', attachment.id)"
<div />
:class="{'tall-status': hideTallStatus}" <gallery
class="status-content-wrapper" v-if="galleryAttachments.length > 0"
> :nsfw="nsfwClickthrough"
<button :attachments="galleryAttachments"
v-if="hideTallStatus" :set-media="setMedia()"
class="button-unstyled -link tall-status-hider" />
:class="{ 'tall-status-hider_focused': focused }" </div>
@click.prevent="toggleShowMore"
>
{{ $t("general.show_more") }}
</button>
<div <div
v-if="!hideSubjectStatus" v-if="status.card && !noHeading"
:class="{ 'single-line': singleLine }" class="link-preview media-body"
class="status-content media-body"
@click.prevent="linkClicked"
v-html="postBodyHtml"
/>
<button
v-if="hideSubjectStatus"
class="button-unstyled -link cw-status-hider"
@click.prevent="toggleShowMore"
> >
{{ $t("status.show_content") }} <link-preview
<FAIcon :card="status.card"
v-if="attachmentTypes.includes('image')" :size="attachmentSize"
icon="image" :nsfw="nsfwClickthrough"
/> />
<FAIcon </div>
v-if="attachmentTypes.includes('video')" </StatusBody>
icon="video"
/>
<FAIcon
v-if="attachmentTypes.includes('audio')"
icon="music"
/>
<FAIcon
v-if="attachmentTypes.includes('unknown')"
icon="file"
/>
<FAIcon
v-if="status.poll && status.poll.options"
icon="poll-h"
/>
<FAIcon
v-if="status.card"
icon="link"
/>
</button>
<button
v-if="showingMore && !fullContent"
class="button-unstyled -link status-unhider"
@click.prevent="toggleShowMore"
>
{{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
</button>
</div>
<div v-if="status.poll && status.poll.options && !hideSubjectStatus">
<poll :base-poll="status.poll" />
</div>
<div
v-if="status.attachments.length !== 0 && (!hideSubjectStatus || showingLongSubject)"
class="attachments media-body"
>
<attachment
v-for="attachment in nonGalleryAttachments"
:key="attachment.id"
class="non-gallery"
:size="attachmentSize"
:nsfw="nsfwClickthrough"
:attachment="attachment"
:allow-play="true"
:set-media="setMedia()"
@play="$emit('mediaplay', attachment.id)"
@pause="$emit('mediapause', attachment.id)"
/>
<gallery
v-if="galleryAttachments.length > 0"
:nsfw="nsfwClickthrough"
:attachments="galleryAttachments"
:set-media="setMedia()"
/>
</div>
<div
v-if="status.card && !hideSubjectStatus && !noHeading"
class="link-preview media-body"
>
<link-preview
:card="status.card"
:size="attachmentSize"
:nsfw="nsfwClickthrough"
/>
</div>
<slot name="footer" /> <slot name="footer" />
</div> </div>
<!-- eslint-enable vue/no-v-html -->
</template> </template>
<script src="./status_content.js" ></script> <script src="./status_content.js" ></script>
@@ -139,156 +61,5 @@ $status-margin: 0.75em;
.StatusContent { .StatusContent {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
.status-content-wrapper {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
}
.tall-status {
position: relative;
height: 220px;
overflow-x: hidden;
overflow-y: hidden;
z-index: 1;
.status-content {
min-height: 0;
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
linear-gradient(to top, white, white);
/* Autoprefixed seem to ignore this one, and also syntax is different */
-webkit-mask-composite: xor;
mask-composite: exclude;
}
}
.tall-status-hider {
display: inline-block;
word-break: break-all;
position: absolute;
height: 70px;
margin-top: 150px;
width: 100%;
text-align: center;
line-height: 110px;
z-index: 2;
}
.status-unhider, .cw-status-hider {
width: 100%;
text-align: center;
display: inline-block;
word-break: break-all;
svg {
color: inherit;
}
}
img, video {
max-width: 100%;
max-height: 400px;
vertical-align: middle;
object-fit: contain;
&.emoji {
width: 32px;
height: 32px;
}
}
.summary-wrapper {
margin-bottom: 0.5em;
border-style: solid;
border-width: 0 0 1px 0;
border-color: var(--border, $fallback--border);
flex-grow: 0;
}
.summary {
font-style: italic;
padding-bottom: 0.5em;
}
.tall-subject {
position: relative;
.summary {
max-height: 2em;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.tall-subject-hider {
display: inline-block;
word-break: break-all;
// position: absolute;
width: 100%;
text-align: center;
padding-bottom: 0.5em;
}
.status-content {
font-family: var(--postFont, sans-serif);
line-height: 1.4em;
white-space: pre-wrap;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
blockquote {
margin: 0.2em 0 0.2em 2em;
font-style: italic;
}
pre {
overflow: auto;
}
code, samp, kbd, var, pre {
font-family: var(--postCodeFont, monospace);
}
p {
margin: 0 0 1em 0;
}
p:last-child {
margin: 0 0 0 0;
}
h1 {
font-size: 1.1em;
line-height: 1.2em;
margin: 1.4em 0;
}
h2 {
font-size: 1.1em;
margin: 1.0em 0;
}
h3 {
font-size: 1em;
margin: 1.2em 0;
}
h4 {
margin: 1.1em 0;
}
&.single-line {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
height: 1.4em;
}
}
}
.greentext {
color: $fallback--cGreen;
color: var(--postGreentext, $fallback--cGreen);
} }
</style> </style>
+3 -2
View File
@@ -30,7 +30,7 @@
position: relative; position: relative;
line-height: 0; line-height: 0;
overflow: hidden; overflow: hidden;
display: flex; display: inline-flex;
align-items: center; align-items: center;
canvas { canvas {
@@ -47,12 +47,13 @@
img { img {
width: 100%; width: 100%;
min-height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
} }
&.animated { &.animated {
&::before { &::before {
zoom: var(--_still_image-label-scale, 1);
content: 'gif'; content: 'gif';
position: absolute; position: absolute;
line-height: 10px; line-height: 10px;
+3 -1
View File
@@ -5,6 +5,7 @@ import FollowButton from '../follow_button/follow_button.vue'
import ModerationTools from '../moderation_tools/moderation_tools.vue' import ModerationTools from '../moderation_tools/moderation_tools.vue'
import AccountActions from '../account_actions/account_actions.vue' import AccountActions from '../account_actions/account_actions.vue'
import Select from '../select/select.vue' import Select from '../select/select.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@@ -120,7 +121,8 @@ export default {
AccountActions, AccountActions,
ProgressButton, ProgressButton,
FollowButton, FollowButton,
Select Select,
RichContent
}, },
methods: { methods: {
muteUser () { muteUser () {
+16 -49
View File
@@ -38,21 +38,12 @@
</router-link> </router-link>
<div class="user-summary"> <div class="user-summary">
<div class="top-line"> <div class="top-line">
<!-- eslint-disable vue/no-v-html --> <RichContent
<div
v-if="user.name_html"
:title="user.name" :title="user.name"
class="user-name" class="user-name"
v-html="user.name_html" :html="user.name"
:emoji="user.emoji"
/> />
<!-- eslint-enable vue/no-v-html -->
<div
v-else
:title="user.name"
class="user-name"
>
{{ user.name }}
</div>
<button <button
v-if="!isOtherUser && user.is_local" v-if="!isOtherUser && user.is_local"
class="button-unstyled edit-profile-button" class="button-unstyled edit-profile-button"
@@ -65,7 +56,7 @@
:title="$t('user_card.edit_profile')" :title="$t('user_card.edit_profile')"
/> />
</button> </button>
<button <a
v-if="isOtherUser && !user.is_local" v-if="isOtherUser && !user.is_local"
:href="user.statusnet_profile_url" :href="user.statusnet_profile_url"
target="_blank" target="_blank"
@@ -75,7 +66,7 @@
class="icon" class="icon"
icon="external-link-alt" icon="external-link-alt"
/> />
</button> </a>
<AccountActions <AccountActions
v-if="isOtherUser && loggedIn" v-if="isOtherUser && loggedIn"
:user="user" :user="user"
@@ -267,20 +258,12 @@
<span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span> <span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span>
</div> </div>
</div> </div>
<!-- eslint-disable vue/no-v-html --> <RichContent
<p v-if="!hideBio"
v-if="!hideBio && user.description_html"
class="user-card-bio" class="user-card-bio"
@click.prevent="linkClicked" :html="user.description_html"
v-html="user.description_html" :emoji="user.emoji"
/> />
<!-- eslint-enable vue/no-v-html -->
<p
v-else-if="!hideBio"
class="user-card-bio"
>
{{ user.description }}
</p>
</div> </div>
</div> </div>
</template> </template>
@@ -293,9 +276,10 @@
.user-card { .user-card {
position: relative; position: relative;
&:hover .Avatar { &:hover {
--_still-image-img-visibility: visible; --_still-image-img-visibility: visible;
--_still-image-canvas-visibility: hidden; --_still-image-canvas-visibility: hidden;
--_still-image-label-visibility: hidden;
} }
.panel-heading { .panel-heading {
@@ -339,12 +323,12 @@
} }
} }
p {
margin-bottom: 0;
}
&-bio { &-bio {
text-align: center; text-align: center;
display: block;
line-height: 18px;
padding: 1em;
margin: 0;
a { a {
color: $fallback--link; color: $fallback--link;
@@ -356,11 +340,6 @@
vertical-align: middle; vertical-align: middle;
max-width: 100%; max-width: 100%;
max-height: 400px; max-height: 400px;
&.emoji {
width: 32px;
height: 32px;
}
} }
} }
@@ -462,13 +441,6 @@
// big one // big one
z-index: 1; z-index: 1;
img {
width: 26px;
height: 26px;
vertical-align: middle;
object-fit: contain
}
.top-line { .top-line {
display: flex; display: flex;
} }
@@ -481,12 +453,7 @@
margin-right: 1em; margin-right: 1em;
font-size: 15px; font-size: 15px;
img { --emoji-size: 14px;
object-fit: contain;
height: 16px;
width: 16px;
vertical-align: middle;
}
} }
.bottom-line { .bottom-line {
+3 -1
View File
@@ -4,6 +4,7 @@ import FollowCard from '../follow_card/follow_card.vue'
import Timeline from '../timeline/timeline.vue' import Timeline from '../timeline/timeline.vue'
import Conversation from '../conversation/conversation.vue' import Conversation from '../conversation/conversation.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import List from '../list/list.vue' import List from '../list/list.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more' import withLoadMore from '../../hocs/with_load_more/with_load_more'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@@ -164,7 +165,8 @@ const UserProfile = {
FriendList, FriendList,
FollowCard, FollowCard,
TabSwitcher, TabSwitcher,
Conversation Conversation,
RichContent
} }
} }
+12 -8
View File
@@ -20,20 +20,24 @@
:key="index" :key="index"
class="user-profile-field" class="user-profile-field"
> >
<!-- eslint-disable vue/no-v-html -->
<dt <dt
:title="user.fields_text[index].name" :title="user.fields_text[index].name"
class="user-profile-field-name" class="user-profile-field-name"
@click.prevent="linkClicked" >
v-html="field.name" <RichContent
/> :html="field.name"
:emoji="user.emoji"
/>
</dt>
<dd <dd
:title="user.fields_text[index].value" :title="user.fields_text[index].value"
class="user-profile-field-value" class="user-profile-field-value"
@click.prevent="linkClicked" >
v-html="field.value" <RichContent
/> :html="field.value"
<!-- eslint-enable vue/no-v-html --> :emoji="user.emoji"
/>
</dd>
</dl> </dl>
</div> </div>
<tab-switcher <tab-switcher
+498 -46
View File
@@ -10,11 +10,12 @@
"text_limit": "Límit de text", "text_limit": "Límit de text",
"title": "Funcionalitats", "title": "Funcionalitats",
"who_to_follow": "A qui seguir", "who_to_follow": "A qui seguir",
"pleroma_chat_messages": "Xat de Pleroma" "pleroma_chat_messages": "Xat de Pleroma",
"upload_limit": "Límit de càrrega"
}, },
"finder": { "finder": {
"error_fetching_user": "No s'ha pogut carregar l'usuari/a", "error_fetching_user": "No s'ha pogut carregar l'usuari/a",
"find_user": "Find user" "find_user": "Trobar usuari"
}, },
"general": { "general": {
"apply": "Aplica", "apply": "Aplica",
@@ -32,7 +33,16 @@
"error_retry": "Si us plau, prova de nou", "error_retry": "Si us plau, prova de nou",
"generic_error": "Hi ha hagut un error", "generic_error": "Hi ha hagut un error",
"loading": "Carregant…", "loading": "Carregant…",
"more": "Més" "more": "Més",
"flash_content": "Fes clic per mostrar el contingut Flash utilitzant Ruffle (experimental, pot no funcionar).",
"flash_security": "Tingues en compte que això pot ser potencialment perillós, ja que el contingut Flash encara és un codi arbitrari.",
"flash_fail": "No s'ha pogut carregar el contingut del flaix, consulta la consola per als detalls.",
"role": {
"moderator": "Moderador/a",
"admin": "Administrador/a"
},
"dismiss": "Descartar",
"peek": "Donar un cop d'ull"
}, },
"login": { "login": {
"login": "Inicia sessió", "login": "Inicia sessió",
@@ -45,15 +55,20 @@
"enter_recovery_code": "Posa un codi de recuperació", "enter_recovery_code": "Posa un codi de recuperació",
"authentication_code": "Codi d'autenticació", "authentication_code": "Codi d'autenticació",
"hint": "Entra per participar a la conversa", "hint": "Entra per participar a la conversa",
"description": "Entra amb OAuth" "description": "Entra amb OAuth",
"heading": {
"totp": "Autenticació de dos factors",
"recovery": "Recuperació de dos factors"
},
"enter_two_factor_code": "Introdueix un codi de dos factors"
}, },
"nav": { "nav": {
"chat": "Xat local públic", "chat": "Xat local públic",
"friend_requests": "Soŀlicituds de connexió", "friend_requests": "Solicituds de seguiment",
"mentions": "Mencions", "mentions": "Mencions",
"public_tl": "Flux públic del node", "public_tl": "Línia temporal pública",
"timeline": "Flux personal", "timeline": "Flux personal",
"twkn": "Flux de la xarxa coneguda", "twkn": "Xarxa coneguda",
"chats": "Xats", "chats": "Xats",
"timelines": "Línies de temps", "timelines": "Línies de temps",
"preferences": "Preferències", "preferences": "Preferències",
@@ -62,19 +77,25 @@
"dms": "Missatges directes", "dms": "Missatges directes",
"interactions": "Interaccions", "interactions": "Interaccions",
"back": "Enrere", "back": "Enrere",
"administration": "Administració" "administration": "Administració",
"about": "Quant a",
"bookmarks": "Marcadors",
"user_search": "Cerca d'usuaris",
"home_timeline": "Línea temporal personal"
}, },
"notifications": { "notifications": {
"broken_favorite": "No es coneix aquest estat. S'està cercant.", "broken_favorite": "Publicació desconeguda, s'està cercant",
"favorited_you": "ha marcat un estat teu", "favorited_you": "ha marcat un estat teu",
"followed_you": "ha començat a seguir-te", "followed_you": "ha començat a seguir-te",
"load_older": "Carrega més notificacions", "load_older": "Carrega més notificacions",
"notifications": "Notificacions", "notifications": "Notificacions",
"read": "Read!", "read": "Llegit!",
"repeated_you": "ha repetit el teu estat", "repeated_you": "ha repetit el teu estat",
"migrated_to": "migrat a", "migrated_to": "migrat a",
"no_more_notifications": "No més notificacions", "no_more_notifications": "No més notificacions",
"follow_request": "et vol seguir" "follow_request": "et vol seguir",
"reacted_with": "ha reaccionat amb {0}",
"error": "Error obtenint notificacions: {0}"
}, },
"post_status": { "post_status": {
"account_not_locked_warning": "El teu compte no està {0}. Qualsevol persona pot seguir-te per llegir les teves entrades reservades només a seguidores.", "account_not_locked_warning": "El teu compte no està {0}. Qualsevol persona pot seguir-te per llegir les teves entrades reservades només a seguidores.",
@@ -83,24 +104,33 @@
"content_type": { "content_type": {
"text/plain": "Text pla", "text/plain": "Text pla",
"text/markdown": "Markdown", "text/markdown": "Markdown",
"text/html": "HTML" "text/html": "HTML",
"text/bbcode": "BBCode"
}, },
"content_warning": "Assumpte (opcional)", "content_warning": "Assumpte (opcional)",
"default": "Em sento…", "default": "Acabe d'aterrar a L.A.",
"direct_warning": "Aquesta entrada només serà visible per les usuràries que etiquetis", "direct_warning": "Aquesta entrada només serà visible per les usuràries que etiquetis",
"posting": "Publicació", "posting": "Publicació",
"scope": { "scope": {
"direct": "Directa - Publica només per les usuàries etiquetades", "direct": "Directa - publica només per als usuaris etiquetats",
"private": "Només seguidors/es - Publica només per comptes que et segueixin", "private": "Només seguidors/es - publica només per comptes que et segueixin",
"public": "Pública - Publica als fluxos públics", "public": "Pública - publica als fluxos públics",
"unlisted": "Silenciosa - No la mostris en fluxos públics" "unlisted": "Silenciosa - no la mostris en fluxos públics"
}, },
"scope_notice": { "scope_notice": {
"private": "Aquesta entrada serà visible només per a qui et segueixi", "private": "Aquesta entrada serà visible només per a qui et segueixi",
"public": "Aquesta entrada serà visible per a tothom" "public": "Aquesta entrada serà visible per a tothom",
"unlisted": "Aquesta entrada no es veurà ni a la Línia de temps local ni a la Línia de temps federada"
}, },
"preview_empty": "Buida", "preview_empty": "Buida",
"preview": "Vista prèvia" "preview": "Vista prèvia",
"direct_warning_to_first_only": "Aquesta publicació només serà visible per als usuaris mencionats al principi del missatge.",
"empty_status_error": "No es pot publicar un estat buit sense fitxers adjunts",
"media_description": "Descripció multimèdia",
"direct_warning_to_all": "Aquesta publicació serà visible per a tots els usuaris mencionats.",
"new_status": "Publicar un nou estat",
"post": "Publicació",
"media_description_error": "Ha fallat la pujada del contingut. Prova de nou"
}, },
"registration": { "registration": {
"bio": "Presentació", "bio": "Presentació",
@@ -118,13 +148,19 @@
"username_required": "no es pot deixar en blanc" "username_required": "no es pot deixar en blanc"
}, },
"fullname_placeholder": "p. ex. Lain Iwakura", "fullname_placeholder": "p. ex. Lain Iwakura",
"username_placeholder": "p. ex. lain" "username_placeholder": "p. ex. lain",
"captcha": "CAPTCHA",
"register": "Registrar-se",
"reason": "Raó per a registrar-se",
"bio_placeholder": "p.e.\nHola, sóc la Lain.\nSóc una noia anime que viu a un suburbi de Japó. Potser em coneixes per Wired.",
"reason_placeholder": "Aquesta instància aprova els registres manualment.\nExplica a l'administració per què vols registrar-te.",
"new_captcha": "Clica a la imatge per obtenir un nou captcha"
}, },
"settings": { "settings": {
"attachmentRadius": "Adjunts", "attachmentRadius": "Adjunts",
"attachments": "Adjunts", "attachments": "Adjunts",
"avatar": "Avatar", "avatar": "Avatar",
"avatarAltRadius": "Avatars en les notificacions", "avatarAltRadius": "Avatars (notificacions)",
"avatarRadius": "Avatars", "avatarRadius": "Avatars",
"background": "Fons de pantalla", "background": "Fons de pantalla",
"bio": "Presentació", "bio": "Presentació",
@@ -134,8 +170,8 @@
"cOrange": "Taronja (marca com a preferit)", "cOrange": "Taronja (marca com a preferit)",
"cRed": "Vermell (canceŀla)", "cRed": "Vermell (canceŀla)",
"change_password": "Canvia la contrasenya", "change_password": "Canvia la contrasenya",
"change_password_error": "No s'ha pogut canviar la contrasenya", "change_password_error": "No s'ha pogut canviar la contrasenya.",
"changed_password": "S'ha canviat la contrasenya", "changed_password": "S'ha canviat la contrasenya correctament!",
"collapse_subject": "Replega les entrades amb títol", "collapse_subject": "Replega les entrades amb títol",
"confirm_new_password": "Confirma la nova contrasenya", "confirm_new_password": "Confirma la nova contrasenya",
"current_avatar": "L'avatar actual", "current_avatar": "L'avatar actual",
@@ -176,7 +212,7 @@
"new_password": "Contrasenya nova", "new_password": "Contrasenya nova",
"notification_visibility": "Notifica'm quan algú", "notification_visibility": "Notifica'm quan algú",
"notification_visibility_follows": "Comença a seguir-me", "notification_visibility_follows": "Comença a seguir-me",
"notification_visibility_likes": "Marca com a preferida una entrada meva", "notification_visibility_likes": "Favorits",
"notification_visibility_mentions": "Em menciona", "notification_visibility_mentions": "Em menciona",
"notification_visibility_repeats": "Republica una entrada meva", "notification_visibility_repeats": "Republica una entrada meva",
"no_rich_text_description": "Neteja el formatat de text de totes les entrades", "no_rich_text_description": "Neteja el formatat de text de totes les entrades",
@@ -193,7 +229,7 @@
"profile_banner": "Fons de perfil", "profile_banner": "Fons de perfil",
"profile_tab": "Perfil", "profile_tab": "Perfil",
"radii_help": "Configura l'arrodoniment de les vores (en píxels)", "radii_help": "Configura l'arrodoniment de les vores (en píxels)",
"replies_in_timeline": "Replies in timeline", "replies_in_timeline": "Respostes al flux",
"reply_visibility_all": "Mostra totes les respostes", "reply_visibility_all": "Mostra totes les respostes",
"reply_visibility_following": "Mostra només les respostes a entrades meves o d'usuàries que jo segueixo", "reply_visibility_following": "Mostra només les respostes a entrades meves o d'usuàries que jo segueixo",
"reply_visibility_self": "Mostra només les respostes a entrades meves", "reply_visibility_self": "Mostra només les respostes a entrades meves",
@@ -216,7 +252,7 @@
"true": "sí" "true": "sí"
}, },
"show_moderator_badge": "Mostra una insígnia de Moderació en el meu perfil", "show_moderator_badge": "Mostra una insígnia de Moderació en el meu perfil",
"show_admin_badge": "Mostra una insígnia d'Administració en el meu perfil", "show_admin_badge": "Mostra una insígnia \"d'Administració\" en el meu perfil",
"hide_followers_description": "No mostris qui m'està seguint", "hide_followers_description": "No mostris qui m'està seguint",
"hide_follows_description": "No mostris a qui segueixo", "hide_follows_description": "No mostris a qui segueixo",
"notification_visibility_emoji_reactions": "Reaccions", "notification_visibility_emoji_reactions": "Reaccions",
@@ -254,25 +290,257 @@
"allow_following_move": "Permet el seguiment automàtic quan un compte a qui seguim es mou", "allow_following_move": "Permet el seguiment automàtic quan un compte a qui seguim es mou",
"mfa": { "mfa": {
"scan": { "scan": {
"secret_code": "Clau" "secret_code": "Clau",
"title": "Escanejar",
"desc": "S'està usant l'aplicació two-factor, escaneja aquest codi QR o introdueix la clau de text:"
}, },
"authentication_methods": "Mètodes d'autenticació", "authentication_methods": "Mètodes d'autenticació",
"waiting_a_recovery_codes": "Rebent còpies de seguretat dels codis…", "waiting_a_recovery_codes": "Rebent còpies de seguretat dels codis…",
"recovery_codes": "Codis de recuperació.", "recovery_codes": "Codis de recuperació.",
"warning_of_generate_new_codes": "Quan generes nous codis de recuperació, els antics ja no funcionaran més.", "warning_of_generate_new_codes": "Quan generes nous codis de recuperació, els antics ja no funcionaran més.",
"generate_new_recovery_codes": "Genera nous codis de recuperació" "generate_new_recovery_codes": "Genera nous codis de recuperació",
"otp": "OTP",
"confirm_and_enable": "Confirmar i habilitar OTP",
"recovery_codes_warning": "Anote els codis o guarda'ls en un lloc segur, o no els veuràs una altra volta. Si perds l'accés a la teua aplicació 2FA i els codis de recuperació, no podràs accedir al compte.",
"title": "Autenticació de dos factors",
"setup_otp": "Configurar OTP",
"wait_pre_setup_otp": "preconfiguració OTP",
"verify": {
"desc": "Per habilitar l'autenticació two-factor, introdueix el codi des de la teva aplicació two-factor:"
}
}, },
"enter_current_password_to_confirm": "Posar la contrasenya actual per confirmar la teva identitat", "enter_current_password_to_confirm": "Posar la contrasenya actual per confirmar la teva identitat",
"security": "Seguretat", "security": "Seguretat",
"app_name": "Nom de l'aplicació" "app_name": "Nom de l'aplicació",
"subject_line_mastodon": "Com a mastodon: copiar com és",
"mute_export_button": "Exportar silenciats a un fitxer csv",
"mute_import_error": "Error al importar silenciats",
"mutes_imported": "Silenciats importats! Processar-los portarà una estona.",
"import_mutes_from_a_csv_file": "Importar silenciats des d'un fitxer csv",
"word_filter": "Filtre de paraules",
"hide_media_previews": "Ocultar les vistes prèvies multimèdia",
"hide_filtered_statuses": "Amagar estats filtrats",
"play_videos_in_modal": "Reproduir vídeos en un marc emergent",
"file_export_import": {
"errors": {
"invalid_file": "El fitxer seleccionat no és vàlid com a còpia de seguretat de la configuració. No s'ha realitzat cap canvi."
},
"backup_settings": "Còpia de seguretat de la configuració a un fitxer",
"backup_settings_theme": "Còpia de seguretat de la configuració i tema a un fitxer",
"restore_settings": "Restaurar configuració des d'un fitxer",
"backup_restore": "Còpia de seguretat de la configuració"
},
"user_mutes": "Usuaris",
"subject_line_email": "Com a l'email: \"re: tema\"",
"search_user_to_block": "Busca a qui vols bloquejar",
"save": "Guardar els canvis",
"use_contain_fit": "No retallar els adjunts en miniatures",
"reset_profile_background": "Restablir fons del perfil",
"reset_profile_banner": "Restablir banner del perfil",
"emoji_reactions_on_timeline": "Mostrar reaccions emoji al flux",
"max_thumbnails": "Quantitat màxima de miniatures per publicació",
"hide_user_stats": "Amagar les estadístiques de l'usuari (p. ex. el nombre de seguidors)",
"reset_banner_confirm": "Realment vols restablir el banner?",
"reset_background_confirm": "Realment vols restablir el fons del perfil?",
"subject_input_always_show": "Sempre mostrar el camp del tema",
"subject_line_noop": "No copiar",
"subject_line_behavior": "Copiar el tema a les respostes",
"search_user_to_mute": "Busca a qui vols silenciar",
"mute_export": "Exportar silenciats",
"scope_copy": "Copiar visibilitat quan contestes (En els missatges directes sempre es copia)",
"reset_avatar": "Restablir avatar",
"right_sidebar": "Mostrar barra lateral a la dreta",
"no_blocks": "No hi han bloquejats",
"no_mutes": "No hi han silenciats",
"hide_follows_count_description": "No mostrar el nombre de comptes que segueixo",
"mute_import": "Importar silenciats",
"hide_all_muted_posts": "Ocultar publicacions silenciades",
"hide_wallpaper": "Amagar el fons de la instància",
"notification_visibility_moves": "Usuari Migrat",
"reply_visibility_following_short": "Mostrar respostes als meus seguidors",
"reply_visibility_self_short": "Mostrar respostes només a un mateix",
"autohide_floating_post_button": "Ocultar automàticament el botó 'Nova Publicació' (mòbil)",
"minimal_scopes_mode": "Minimitzar les opcions de visibilitat de la publicació",
"sensitive_by_default": "Marcar publicacions com a sensibles per defecte",
"useStreamingApi": "Rebre publicacions i notificacions en temps real",
"hide_isp": "Ocultar el panell especific de la instància",
"preload_images": "Precarregar les imatges",
"setting_changed": "La configuració és diferent a la predeterminada",
"hide_followers_count_description": "No mostrar el nombre de seguidors",
"reset_avatar_confirm": "Realment vols restablir l'avatar?",
"accent": "Accent",
"useStreamingApiWarning": "(No recomanat, experimental, pot ometre publicacions)",
"style": {
"fonts": {
"family": "Nom de la font",
"size": "Mida (en píxels)",
"custom": "Personalitza",
"_tab_label": "Fonts",
"help": "Selecciona la font per als elements de la interfície. Per a \"personalitzat\" deus escriure el nom de la font exactament com apareix al sistema.",
"components": {
"post": "Text de les publicacions",
"postCode": "Text monoespai en publicació (text enriquit)",
"input": "Camps d'entrada",
"interface": "Interfície"
}
},
"preview": {
"input": "Acabo d'aterrar a Los Angeles.",
"button": "Botó",
"mono": "contingut",
"content": "Contingut",
"header": "Previsualització",
"header_faint": "Això està bé",
"error": "Exemple d'error",
"faint_link": "Manual d'ajuda",
"checkbox": "He llegit els termes i condicions",
"link": "un bonic enllaç"
},
"shadows": {
"spread": "Difon",
"filter_hint": {
"drop_shadow_syntax": "{0} no suporta el paràmetre {1} i la paraula clau {2}.",
"avatar_inset": "Tingues en compte que combinar ombres interiors i no interiors als avatars podria donar resultats inesperats amb avatars transparents.",
"inset_classic": "Les ombres interiors estaran usant {0}",
"always_drop_shadow": "Advertència, aquesta ombra sempre utilitza {0} quan el navegador ho suporta.",
"spread_zero": "Ombres amb propagació > 0 apareixeran com si estigueren posades a zero"
},
"components": {
"popup": "Texts i finestres emergents (popups & tooltips)",
"panel": "Panell",
"panelHeader": "Capçalera del panell",
"avatar": "Avatar de l'usuari (en vista de perfil)",
"input": "Camp d'entrada",
"buttonHover": "Botó (surant)",
"buttonPressed": "Botó (pressionat)",
"topBar": "Barra superior",
"buttonPressedHover": "Botó (surant i pressionat)",
"avatarStatus": "Avatar de l'usuari (en vista de publicació)",
"button": "Botó"
},
"hintV3": "per a les ombres també pots usar la notació {0} per a utilitzar un altre espai de color.",
"blur": "Difuminat",
"component": "Component",
"override": "Sobreescriure",
"shadow_id": "Ombra #{value}",
"_tab_label": "Ombra i il·luminació",
"inset": "Ombra interior"
},
"switcher": {
"use_snapshot": "Versió antiga",
"help": {
"future_version_imported": "El fitxer importat es va crear per a una versió del front-end més recent.",
"migration_snapshot_ok": "Per a estar segurs, s'ha carregat la instantània del tema. Pots intentar carregar les dades del tema.",
"migration_napshot_gone": "Per alguna raó, faltava la instantània, algunes coses podrien veure's diferents del que recordes.",
"snapshot_source_mismatch": "Conflicte de versions: probablement el front-end s'ha revertit i actualitzat una altra volta, si has canviat el tema en una versió anterior, segurament vols utilitzar la versió antiga; d'altra banda utilitza la nova versió.",
"v2_imported": "El fitxer que has importat va ser creat per a un front-end més antic. Intentem maximitzar la compatibilitat, però podrien haver inconsistències.",
"fe_upgraded": "El motor de temes de PleromaFE es va actualitzar després de l'actualització de la versió.",
"snapshot_missing": "No hi havia cap instantània del tema al fitxer, per tant podria veure's diferent del previst originalment.",
"upgraded_from_v2": "PleromaFE s'ha actualitzat, el tema pot veure's un poc diferent de com recordes.",
"fe_downgraded": "Versió de PleromaFE revertida.",
"older_version_imported": "El fitxer que has importat va ser creat en una versió del front-end més antiga."
},
"keep_as_is": "Mantindre com està",
"save_load_hint": "Les opcions \"Mantindre\" conserven les opcions configurades actualment al seleccionar o carregar temes, també emmagatzema aquestes opcions quan s'exporta un tema. Quan es desactiven totes les caselles de verificació, el tema exportat ho guardarà tot.",
"keep_color": "Mantindre colors",
"keep_opacity": "Mantindre opacitat",
"keep_shadows": "Mantindre ombres",
"keep_fonts": "Mantindre fonts",
"keep_roundness": "Mantindre rodoneses",
"clear_all": "Netejar tot",
"reset": "Reinciar",
"load_theme": "Carregar tema",
"use_source": "Nova versió",
"clear_opacity": "Netejar opacitat"
},
"common": {
"contrast": {
"hint": "El ràtio de contrast és {ratio}. {level} {context}",
"level": {
"bad": "no compleix amb cap pauta d'accecibilitat",
"aaa": "Compleix amb el nivell AA (recomanat)",
"aa": "Compleix amb el nivell AA (mínim)"
},
"context": {
"18pt": "per a textos grans (+18pt)",
"text": "per a textos"
}
},
"opacity": "Opacitat",
"color": "Color"
},
"advanced_colors": {
"badge": "Fons de insígnies",
"inputs": "Camps d'entrada",
"wallpaper": "Fons de pantalla",
"pressed": "Pressionat",
"chat": {
"outgoing": "Eixint",
"border": "Borde",
"incoming": "Entrants"
},
"borders": "Bordes",
"panel_header": "Capçalera del panell",
"buttons": "Botons",
"faint_text": "Text esvaït",
"poll": "Gràfica de l'enquesta",
"toggled": "Commutat",
"alert": "Fons d'alertes",
"alert_error": "Error",
"alert_warning": "Precaució",
"post": "Publicacions/Biografies d'usuaris",
"badge_notification": "Notificacions",
"selectedMenu": "Element del menú seleccionat",
"tabs": "Pestanyes",
"_tab_label": "Avançat",
"alert_neutral": "Neutral",
"popover": "Suggeriments, menús, superposicions",
"top_bar": "Barra superior",
"highlight": "Elements destacats",
"disabled": "Deshabilitat",
"icons": "Icones",
"selectedPost": "Publicació seleccionada",
"underlay": "Subratllat"
},
"common_colors": {
"main": "Colors comuns",
"rgbo": "Icones, accents, insígnies",
"foreground_hint": "mira la pestanya \"Avançat\" per a un control més detallat",
"_tab_label": "Comú"
},
"radii": {
"_tab_label": "Rodonesa"
}
},
"version": {
"frontend_version": "Versió \"Frontend\"",
"backend_version": "Versió \"backend\"",
"title": "Versió"
},
"theme_help_v2_1": "També pots anular alguns components de color i opacitat activant la casella. Usa el botó \"Esborrar tot\" per esborrar totes les anulacions.",
"type_domains_to_mute": "Buscar dominis per a silenciar",
"greentext": "Text verd (meme arrows)",
"fun": "Divertit",
"notification_setting_filters": "Filtres",
"virtual_scrolling": "Optimitzar la representació del flux",
"notification_setting_block_from_strangers": "Bloqueja les notificacions dels usuaris que no segueixes",
"enable_web_push_notifications": "Habilitar notificacions del navegador",
"notification_blocks": "Bloquejar a un usuari para totes les notificacions i també les cancel·la.",
"more_settings": "Més opcions",
"notification_setting_privacy": "Privacitat",
"upload_a_photo": "Pujar una foto",
"notification_setting_hide_notification_contents": "Amagar el remitent i els continguts de les notificacions push",
"notifications": "Notificacions",
"notification_mutes": "Per a deixar de rebre notificacions d'un usuari en concret, silencia'l-ho.",
"theme_help_v2_2": "Les icones per baix d'algunes entrades són indicadors del contrast del fons/text, desplaça el ratolí per a més informació. Tingues en compte que quan s'utilitzen indicadors de contrast de transparència es mostra el pitjor cas possible."
}, },
"time": { "time": {
"day": "{0} dia", "day": "{0} dia",
"days": "{0} dies", "days": "{0} dies",
"day_short": "{0} dia", "day_short": "{0} dia",
"days_short": "{0} dies", "days_short": "{0} dies",
"hour": "{0} hour", "hour": "{0} hora",
"hours": "{0} hours", "hours": "{0} hores",
"hour_short": "{0}h", "hour_short": "{0}h",
"hours_short": "{0}h", "hours_short": "{0}h",
"in_future": "in {0}", "in_future": "in {0}",
@@ -287,12 +555,12 @@
"months_short": "{0} mesos", "months_short": "{0} mesos",
"now": "ara mateix", "now": "ara mateix",
"now_short": "ara mateix", "now_short": "ara mateix",
"second": "{0} second", "second": "{0} segon",
"seconds": "{0} seconds", "seconds": "{0} segons",
"second_short": "{0}s", "second_short": "{0}s",
"seconds_short": "{0}s", "seconds_short": "{0}s",
"week": "{0} setm.", "week": "{0} setmana",
"weeks": "{0} setm.", "weeks": "{0} setmanes",
"week_short": "{0} setm.", "week_short": "{0} setm.",
"weeks_short": "{0} setm.", "weeks_short": "{0} setm.",
"year": "{0} any", "year": "{0} any",
@@ -308,7 +576,13 @@
"no_retweet_hint": "L'entrada és només per a seguidores o és \"directa\", i per tant no es pot republicar", "no_retweet_hint": "L'entrada és només per a seguidores o és \"directa\", i per tant no es pot republicar",
"repeated": "republicat", "repeated": "republicat",
"show_new": "Mostra els nous", "show_new": "Mostra els nous",
"up_to_date": "Actualitzat" "up_to_date": "Actualitzat",
"socket_reconnected": "Connexió a temps real establerta",
"socket_broke": "Connexió a temps real perduda: codi CloseEvent {0}",
"error": "Error de càrrega de la línia de temps: {0}",
"no_statuses": "No hi ha entrades",
"reload": "Recarrega",
"no_more_statuses": "No hi ha més entrades"
}, },
"user_card": { "user_card": {
"approve": "Aprova", "approve": "Aprova",
@@ -324,13 +598,59 @@
"muted": "Silenciat", "muted": "Silenciat",
"per_day": "per dia", "per_day": "per dia",
"remote_follow": "Seguiment remot", "remote_follow": "Seguiment remot",
"statuses": "Estats" "statuses": "Estats",
"unblock_progress": "Desbloquejant…",
"unmute": "Deixa de silenciar",
"follow_progress": "Sol·licitant…",
"admin_menu": {
"force_nsfw": "Marca totes les entrades amb \"No segur per a entorns laborals\"",
"strip_media": "Esborra els audiovisuals de les entrades",
"disable_any_subscription": "Deshabilita completament seguir algú",
"quarantine": "Deshabilita la federació a les entrades de les usuàries",
"moderation": "Moderació",
"delete_user_confirmation": "Estàs completament segur/a? Aquesta acció no es pot desfer.",
"revoke_admin": "Revoca l'Admin",
"activate_account": "Activa el compte",
"deactivate_account": "Desactiva el compte",
"revoke_moderator": "Revoca Moderació",
"delete_account": "Esborra el compte",
"disable_remote_subscription": "Deshabilita seguir algú des d'una instància remota",
"delete_user": "Esborra la usuària",
"grant_admin": "Concedir permisos d'Administració",
"grant_moderator": "Concedir permisos de Moderació"
},
"edit_profile": "Edita el perfil",
"hidden": "Amagat",
"follow_sent": "Petició enviada!",
"unmute_progress": "Deixant de silenciar…",
"bot": "Bot",
"mute_progress": "Silenciant…",
"favorites": "Favorits",
"mention": "Menció",
"follow_unfollow": "Deixa de seguir",
"subscribe": "Subscriu-te",
"show_repeats": "Mostra les repeticions",
"report": "Report",
"its_you": "Ets tu!",
"unblock": "Desbloqueja",
"block_progress": "Bloquejant…",
"message": "Missatge",
"unsubscribe": "Anul·la la subscripció",
"hide_repeats": "Amaga les repeticions",
"highlight": {
"disabled": "Sense ressaltat",
"solid": "Fons sòlid",
"striped": "Fons a ratlles",
"side": "Ratlla lateral"
}
}, },
"user_profile": { "user_profile": {
"timeline_title": "Flux personal" "timeline_title": "Flux personal",
"profile_loading_error": "Disculpes, hi ha hagut un error carregant aquest perfil.",
"profile_does_not_exist": "Disculpes, aquest perfil no existeix."
}, },
"who_to_follow": { "who_to_follow": {
"more": "More", "more": "Més",
"who_to_follow": "A qui seguir" "who_to_follow": "A qui seguir"
}, },
"selectable_list": { "selectable_list": {
@@ -342,10 +662,19 @@
}, },
"interactions": { "interactions": {
"load_older": "Carrega antigues interaccions", "load_older": "Carrega antigues interaccions",
"favs_repeats": "Repeticions i favorits" "favs_repeats": "Repeticions i favorits",
"follows": "Nous seguidors"
}, },
"emoji": { "emoji": {
"stickers": "Adhesius" "stickers": "Adhesius",
"keep_open": "Mantindre el selector obert",
"custom": "Emojis personalitzats",
"unicode": "Emojis unicode",
"load_all_hint": "Carregat el primer emoji {saneAmount}, carregar tots els emoji pot causar problemes de rendiment.",
"emoji": "Emoji",
"search_emoji": "Buscar un emoji",
"add_emoji": "Inserir un emoji",
"load_all": "Carregant tots els {emojiAmount} emoji"
}, },
"polls": { "polls": {
"expired": "L'enquesta va acabar fa {0}", "expired": "L'enquesta va acabar fa {0}",
@@ -357,7 +686,11 @@
"votes": "vots", "votes": "vots",
"option": "Opció", "option": "Opció",
"add_option": "Afegeix opció", "add_option": "Afegeix opció",
"add_poll": "Afegeix enquesta" "add_poll": "Afegeix enquesta",
"expiry": "Temps de vida de l'enquesta",
"people_voted_count": "{count} persona ha votat | {count} persones han votat",
"votes_count": "{count} vot | {count} vots",
"not_enough_options": "L'enquesta no té suficients opcions úniques"
}, },
"media_modal": { "media_modal": {
"next": "Següent", "next": "Següent",
@@ -365,7 +698,8 @@
}, },
"importer": { "importer": {
"error": "Ha succeït un error mentre s'importava aquest arxiu.", "error": "Ha succeït un error mentre s'importava aquest arxiu.",
"success": "Importat amb èxit." "success": "Importat amb èxit.",
"submit": "Enviar"
}, },
"image_cropper": { "image_cropper": {
"cancel": "Cancel·la", "cancel": "Cancel·la",
@@ -379,7 +713,9 @@
}, },
"domain_mute_card": { "domain_mute_card": {
"mute_progress": "Silenciant…", "mute_progress": "Silenciant…",
"mute": "Silencia" "mute": "Silencia",
"unmute": "Deixar de silenciar",
"unmute_progress": "Deixant de silenciar…"
}, },
"about": { "about": {
"staff": "Equip responsable", "staff": "Equip responsable",
@@ -391,16 +727,132 @@
"reject": "Rebutja", "reject": "Rebutja",
"accept_desc": "Aquesta instància només accepta missatges de les següents instàncies:", "accept_desc": "Aquesta instància només accepta missatges de les següents instàncies:",
"accept": "Accepta", "accept": "Accepta",
"simple_policies": "Polítiques específiques de la instància" "simple_policies": "Polítiques específiques de la instància",
"ftl_removal_desc": "Aquesta instància elimina les següents instàncies del flux de la xarxa coneguda:",
"ftl_removal": "Eliminació de la línia de temps coneguda",
"media_nsfw_desc": "Aquesta instància obliga el contingut multimèdia a establir-se com a sensible dins de les publicacions en les següents instàncies:",
"media_removal": "Eliminació de la multimèdia",
"media_removal_desc": "Aquesta instància elimina els suports multimèdia de les publicacions en les següents instàncies:",
"media_nsfw": "Forçar contingut multimèdia com a sensible"
}, },
"mrf_policies_desc": "Les polítiques MRF controlen el comportament federat de la instància. Les següents polítiques estan habilitades:", "mrf_policies_desc": "Les polítiques MRF controlen el comportament federat de la instància. Les següents polítiques estan habilitades:",
"mrf_policies": "Polítiques MRF habilitades", "mrf_policies": "Polítiques MRF habilitades",
"keyword": { "keyword": {
"replace": "Reemplaça", "replace": "Reemplaça",
"reject": "Rebutja", "reject": "Rebutja",
"keyword_policies": "Polítiques de paraules clau" "keyword_policies": "Filtratge per paraules clau",
"is_replaced_by": "→",
"ftl_removal": "Eliminació de la línia de temps federada"
}, },
"federation": "Federació" "federation": "Federació"
} }
},
"shoutbox": {
"title": "Gàbia de Grills"
},
"status": {
"delete": "Esborra l'entrada",
"delete_confirm": "Segur que vols esborrar aquesta entrada?",
"thread_muted_and_words": ", té les paraules:",
"show_full_subject": "Mostra tot el tema",
"show_content": "Mostra el contingut",
"repeats": "Repeticions",
"bookmark": "Marcadors",
"status_unavailable": "Entrada no disponible",
"expand": "Expandeix",
"copy_link": "Copia l'enllaç a l'entrada",
"hide_full_subject": "Amaga tot el tema",
"favorites": "Favorits",
"replies_list": "Contestacions:",
"mute_conversation": "Silencia la conversa",
"thread_muted": "Fil silenciat",
"hide_content": "Amaga el contingut",
"status_deleted": "S'ha esborrat aquesta entrada",
"nsfw": "No segur per a entorns laborals",
"unbookmark": "Desmarca",
"external_source": "Font externa",
"unpin": "Deixa de destacar al perfil",
"pinned": "Destacat",
"reply_to": "Contesta a",
"pin": "Destaca al perfil",
"unmute_conversation": "Deixa de silenciar la conversa"
},
"user_reporting": {
"additional_comments": "Comentaris addicionals",
"forward_description": "Aquest compte és d'un altre servidor. Vols enviar una còpia del report allà també?",
"forward_to": "Endavant a {0}",
"generic_error": "Hi ha hagut un error mentre s'estava processant la teva sol·licitud.",
"title": "Reportant {0}",
"add_comment_description": "Aquest report serà enviat a la moderació a la instància. Pots donar una explicació de per què estàs reportant aquest compte:",
"submit": "Envia"
},
"tool_tip": {
"add_reaction": "Afegeix una Reacció",
"accept_follow_request": "Accepta la sol·licitud de seguir",
"repeat": "Repeteix",
"reply": "Respon",
"favorite": "Favorit",
"user_settings": "Configuració d'usuària",
"reject_follow_request": "Rebutja la sol·licitud de seguir",
"bookmark": "Marcador",
"media_upload": "Pujar multimèdia"
},
"search": {
"no_results": "No hi ha resultats",
"people": "Persones",
"hashtags": "Etiquetes",
"people_talking": "{count} persones parlant"
},
"upload": {
"file_size_units": {
"B": "B",
"KiB": "KiB",
"GiB": "GiB",
"TiB": "TiB",
"MiB": "MiB"
},
"error": {
"base": "La pujada ha fallat.",
"file_too_big": "Fitxer massa gran [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Prova de nou d'aquí una estona",
"message": "La pujada ha fallat: {0}"
}
},
"errors": {
"storage_unavailable": "Pleroma no ha pogut accedir a l'emmagatzematge del navegador. El teu inici de sessió o configuració no es desaran i et pots trobar algun altre problema. Prova a habilitar les galetes."
},
"password_reset": {
"password_reset": "Reinicia la contrasenya",
"forgot_password": "Has oblidat la contrasenya?",
"too_many_requests": "Has arribat al límit d'intents. Prova de nou d'aquí una estona.",
"password_reset_required_but_mailer_is_disabled": "Has de reiniciar la teva contrasenya però el reinici de la contrasenya està deshabilitat. Si us plau, contacta l'administració de la teva instància.",
"placeholder": "El teu correu electrònic o nom d'usuària",
"instruction": "Introdueix la teva adreça de correu electrònic o nom d'usuària. T'enviarem un enllaç per reiniciar la teva contrasenya.",
"return_home": "Torna a la pàgina principal",
"password_reset_required": "Has de reiniciar la teva contrasenya per iniciar la sessió.",
"password_reset_disabled": "El reinici de la contrasenya està deshabilitat. Si us plau, contacta l'administració de la teva instància.",
"check_email": "Comprova que has rebut al correu electrònic un enllaç per reiniciar la teva contrasenya."
},
"file_type": {
"image": "Imatge",
"file": "Fitxer",
"video": "Vídeo",
"audio": "Àudio"
},
"chats": {
"chats": "Xats",
"new": "Nou xat",
"delete_confirm": "Realment vols esborrar aquest missatge?",
"error_sending_message": "Alguna cosa ha fallat quan s'enviava el missatge.",
"more": "Més",
"delete": "Esborra",
"empty_message_error": "No es pot publicar un missatge buit",
"you": "Tu:",
"message_user": "Missatge {nickname}",
"error_loading_chat": "Alguna cosa ha fallat quan es carregava el xat.",
"empty_chat_list_placeholder": "Encara no tens cap xat. Crea un nou xat!"
},
"display_date": {
"today": "Avui"
} }
} }
-1
View File
@@ -407,7 +407,6 @@
"follow": "Sledovat", "follow": "Sledovat",
"follow_sent": "Požadavek odeslán!", "follow_sent": "Požadavek odeslán!",
"follow_progress": "Odeslílám požadavek…", "follow_progress": "Odeslílám požadavek…",
"follow_again": "Odeslat požadavek znovu?",
"follow_unfollow": "Přestat sledovat", "follow_unfollow": "Přestat sledovat",
"followees": "Sledovaní", "followees": "Sledovaní",
"followers": "Sledující", "followers": "Sledující",
+9 -5
View File
@@ -9,7 +9,7 @@
"scope_options": "Reichweitenoptionen", "scope_options": "Reichweitenoptionen",
"text_limit": "Zeichenlimit", "text_limit": "Zeichenlimit",
"title": "Funktionen", "title": "Funktionen",
"who_to_follow": "Wem folgen?", "who_to_follow": "Vorschläge",
"upload_limit": "Maximale Upload Größe", "upload_limit": "Maximale Upload Größe",
"pleroma_chat_messages": "Pleroma Chat" "pleroma_chat_messages": "Pleroma Chat"
}, },
@@ -39,7 +39,10 @@
"close": "Schliessen", "close": "Schliessen",
"retry": "Versuche es erneut", "retry": "Versuche es erneut",
"error_retry": "Bitte versuche es erneut", "error_retry": "Bitte versuche es erneut",
"loading": "Lade…" "loading": "Lade…",
"flash_content": "Klicken, um den Flash-Inhalt mit Ruffle anzuzeigen (Die Funktion ist experimentell und funktioniert daher möglicherweise nicht).",
"flash_security": "Diese Funktion stellt möglicherweise eine Risiko dar, weil Flash-Inhalte weiterhin potentiell gefährlich sind.",
"flash_fail": "Falsh-Inhalt konnte nicht geladen werden, Details werden in der Konsole angezeigt."
}, },
"login": { "login": {
"login": "Anmelden", "login": "Anmelden",
@@ -538,7 +541,9 @@
"reset_background_confirm": "Hintergrund wirklich zurücksetzen?", "reset_background_confirm": "Hintergrund wirklich zurücksetzen?",
"reset_banner_confirm": "Banner wirklich zurücksetzen?", "reset_banner_confirm": "Banner wirklich zurücksetzen?",
"reset_avatar_confirm": "Avatar wirklich zurücksetzen?", "reset_avatar_confirm": "Avatar wirklich zurücksetzen?",
"reset_profile_banner": "Profilbanner zurücksetzen" "reset_profile_banner": "Profilbanner zurücksetzen",
"hide_shoutbox": "Shoutbox der Instanz verbergen",
"right_sidebar": "Seitenleiste rechts anzeigen"
}, },
"timeline": { "timeline": {
"collapse": "Einklappen", "collapse": "Einklappen",
@@ -564,7 +569,6 @@
"follow": "Folgen", "follow": "Folgen",
"follow_sent": "Anfrage gesendet!", "follow_sent": "Anfrage gesendet!",
"follow_progress": "Anfragen…", "follow_progress": "Anfragen…",
"follow_again": "Anfrage erneut senden?",
"follow_unfollow": "Folgen beenden", "follow_unfollow": "Folgen beenden",
"followees": "Folgt", "followees": "Folgt",
"followers": "Folgende", "followers": "Folgende",
@@ -779,7 +783,7 @@
"error_sending_message": "Beim Senden der Nachricht ist ein Fehler aufgetreten.", "error_sending_message": "Beim Senden der Nachricht ist ein Fehler aufgetreten.",
"error_loading_chat": "Beim Laden des Chats ist ein Fehler aufgetreten.", "error_loading_chat": "Beim Laden des Chats ist ein Fehler aufgetreten.",
"delete_confirm": "Soll diese Nachricht wirklich gelöscht werden?", "delete_confirm": "Soll diese Nachricht wirklich gelöscht werden?",
"empty_message_error": "Die Nachricht darf nicht leer sein.", "empty_message_error": "Die Nachricht darf nicht leer sein",
"delete": "Löschen", "delete": "Löschen",
"message_user": "Nachricht an {nickname} senden", "message_user": "Nachricht an {nickname} senden",
"empty_chat_list_placeholder": "Es sind noch keine Chats vorhanden. Jetzt einen Chat starten!", "empty_chat_list_placeholder": "Es sind noch keine Chats vorhanden. Jetzt einen Chat starten!",
+11 -2
View File
@@ -13,6 +13,9 @@
"mrf_policies_desc": "MRF policies manipulate the federation behaviour of the instance. The following policies are enabled:", "mrf_policies_desc": "MRF policies manipulate the federation behaviour of the instance. The following policies are enabled:",
"simple": { "simple": {
"simple_policies": "Instance-specific policies", "simple_policies": "Instance-specific policies",
"instance": "Instance",
"reason": "Reason",
"not_applicable": "N/A",
"accept": "Accept", "accept": "Accept",
"accept_desc": "This instance only accepts messages from the following instances:", "accept_desc": "This instance only accepts messages from the following instances:",
"reject": "Reject", "reject": "Reject",
@@ -259,6 +262,8 @@
"security": "Security", "security": "Security",
"setting_changed": "Setting is different from default", "setting_changed": "Setting is different from default",
"enter_current_password_to_confirm": "Enter your current password to confirm your identity", "enter_current_password_to_confirm": "Enter your current password to confirm your identity",
"mentions_new_style": "Fancier mention links",
"mentions_new_place": "Put mentions on a separate line",
"mfa": { "mfa": {
"otp": "OTP", "otp": "OTP",
"setup_otp": "Setup OTP", "setup_otp": "Setup OTP",
@@ -350,6 +355,7 @@
"hide_isp": "Hide instance-specific panel", "hide_isp": "Hide instance-specific panel",
"hide_shoutbox": "Hide instance shoutbox", "hide_shoutbox": "Hide instance shoutbox",
"right_sidebar": "Show sidebar on the right side", "right_sidebar": "Show sidebar on the right side",
"always_show_post_button": "Always show floating New Post button",
"hide_wallpaper": "Hide instance wallpaper", "hide_wallpaper": "Hide instance wallpaper",
"preload_images": "Preload images", "preload_images": "Preload images",
"use_one_click_nsfw": "Open NSFW attachments with just one click", "use_one_click_nsfw": "Open NSFW attachments with just one click",
@@ -698,6 +704,7 @@
"unbookmark": "Unbookmark", "unbookmark": "Unbookmark",
"delete_confirm": "Do you really want to delete this status?", "delete_confirm": "Do you really want to delete this status?",
"reply_to": "Reply to", "reply_to": "Reply to",
"mentions": "Mentions",
"replies_list": "Replies:", "replies_list": "Replies:",
"mute_conversation": "Mute conversation", "mute_conversation": "Mute conversation",
"unmute_conversation": "Unmute conversation", "unmute_conversation": "Unmute conversation",
@@ -712,7 +719,9 @@
"hide_content": "Hide content", "hide_content": "Hide content",
"status_deleted": "This post was deleted", "status_deleted": "This post was deleted",
"nsfw": "NSFW", "nsfw": "NSFW",
"expand": "Expand" "expand": "Expand",
"you": "(You)",
"plus_more": "+{number} more"
}, },
"user_card": { "user_card": {
"approve": "Approve", "approve": "Approve",
@@ -722,9 +731,9 @@
"edit_profile": "Edit profile", "edit_profile": "Edit profile",
"favorites": "Favorites", "favorites": "Favorites",
"follow": "Follow", "follow": "Follow",
"follow_cancel": "Cancel request",
"follow_sent": "Request sent!", "follow_sent": "Request sent!",
"follow_progress": "Requesting…", "follow_progress": "Requesting…",
"follow_again": "Send request again?",
"follow_unfollow": "Unfollow", "follow_unfollow": "Unfollow",
"followees": "Following", "followees": "Following",
"followers": "Followers", "followers": "Followers",
+39 -17
View File
@@ -39,7 +39,10 @@
"role": { "role": {
"moderator": "Reguligisto", "moderator": "Reguligisto",
"admin": "Administranto" "admin": "Administranto"
} },
"flash_content": "Klaku por montri enhavon de Flash per Ruffle. (Eksperimente, eble ne funkcios.)",
"flash_security": "Sciu, ke tio povas esti danĝera, ĉar la enhavo de Flash ja estas arbitra programo.",
"flash_fail": "Malsukcesis enlegi enhavon de Flash; vidu detalojn en konzolo."
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "Tondi bildon", "crop_picture": "Tondi bildon",
@@ -87,7 +90,8 @@
"interactions": "Interagoj", "interactions": "Interagoj",
"administration": "Administrado", "administration": "Administrado",
"bookmarks": "Legosignoj", "bookmarks": "Legosignoj",
"timelines": "Historioj" "timelines": "Historioj",
"home_timeline": "Hejma historio"
}, },
"notifications": { "notifications": {
"broken_favorite": "Nekonata stato, serĉante ĝin…", "broken_favorite": "Nekonata stato, serĉante ĝin…",
@@ -119,10 +123,10 @@
"direct_warning": "Ĉi tiu afiŝo estos videbla nur por ĉiuj menciitaj uzantoj.", "direct_warning": "Ĉi tiu afiŝo estos videbla nur por ĉiuj menciitaj uzantoj.",
"posting": "Afiŝante", "posting": "Afiŝante",
"scope": { "scope": {
"direct": "Rekta Afiŝi nur al menciitaj uzantoj", "direct": "Rekta afiŝi nur al menciitaj uzantoj",
"private": "Nur abonantoj Afiŝi nur al abonantoj", "private": "Nur abonantoj afiŝi nur al abonantoj",
"public": "Publika Afiŝi al publikaj historioj", "public": "Publika afiŝi al publikaj historioj",
"unlisted": "Nelistigita Ne afiŝi al publikaj historioj" "unlisted": "Nelistigita ne afiŝi al publikaj historioj"
}, },
"scope_notice": { "scope_notice": {
"unlisted": "Ĉi tiu afiŝo ne estos videbla en la Publika historio kaj La tuta konata reto", "unlisted": "Ĉi tiu afiŝo ne estos videbla en la Publika historio kaj La tuta konata reto",
@@ -135,7 +139,8 @@
"preview": "Antaŭrigardo", "preview": "Antaŭrigardo",
"direct_warning_to_first_only": "Ĉi tiu afiŝo estas nur videbla al uzantoj menciitaj je la komenco de la mesaĝo.", "direct_warning_to_first_only": "Ĉi tiu afiŝo estas nur videbla al uzantoj menciitaj je la komenco de la mesaĝo.",
"direct_warning_to_all": "Ĉi tiu afiŝo estos videbla al ĉiuj menciitaj uzantoj.", "direct_warning_to_all": "Ĉi tiu afiŝo estos videbla al ĉiuj menciitaj uzantoj.",
"media_description": "Priskribo de vidaŭdaĵo" "media_description": "Priskribo de vidaŭdaĵo",
"post": "Afiŝo"
}, },
"registration": { "registration": {
"bio": "Priskribo", "bio": "Priskribo",
@@ -143,7 +148,7 @@
"fullname": "Prezenta nomo", "fullname": "Prezenta nomo",
"password_confirm": "Konfirmo de pasvorto", "password_confirm": "Konfirmo de pasvorto",
"registration": "Registriĝo", "registration": "Registriĝo",
"token": "Invita ĵetono", "token": "Invita peco",
"captcha": "TESTO DE HOMECO", "captcha": "TESTO DE HOMECO",
"new_captcha": "Klaku la bildon por akiri novan teston", "new_captcha": "Klaku la bildon por akiri novan teston",
"username_placeholder": "ekz. lain", "username_placeholder": "ekz. lain",
@@ -158,7 +163,8 @@
"password_confirmation_match": "samu la pasvorton" "password_confirmation_match": "samu la pasvorton"
}, },
"reason_placeholder": "Ĉi-node oni aprobas registriĝojn permane.\nSciigu la administrantojn kial vi volas registriĝi.", "reason_placeholder": "Ĉi-node oni aprobas registriĝojn permane.\nSciigu la administrantojn kial vi volas registriĝi.",
"reason": "Kialo registriĝi" "reason": "Kialo registriĝi",
"register": "Registriĝi"
}, },
"settings": { "settings": {
"app_name": "Nomo de aplikaĵo", "app_name": "Nomo de aplikaĵo",
@@ -244,9 +250,9 @@
"show_admin_badge": "Montri la insignon de administranto en mia profilo", "show_admin_badge": "Montri la insignon de administranto en mia profilo",
"show_moderator_badge": "Montri la insignon de reguligisto en mia profilo", "show_moderator_badge": "Montri la insignon de reguligisto en mia profilo",
"nsfw_clickthrough": "Ŝalti traklakan kaŝadon de kunsendaĵoj kaj antaŭmontroj de ligiloj por konsternaj statoj", "nsfw_clickthrough": "Ŝalti traklakan kaŝadon de kunsendaĵoj kaj antaŭmontroj de ligiloj por konsternaj statoj",
"oauth_tokens": "Ĵetonoj de OAuth", "oauth_tokens": "Pecoj de OAuth",
"token": "Ĵetono", "token": "Peco",
"refresh_token": "Ĵetono de aktualigo", "refresh_token": "Aktualiga peco",
"valid_until": "Valida ĝis", "valid_until": "Valida ĝis",
"revoke_token": "Senvalidigi", "revoke_token": "Senvalidigi",
"panelRadius": "Bretoj", "panelRadius": "Bretoj",
@@ -532,7 +538,22 @@
"hide_all_muted_posts": "Kaŝi silentigitajn afiŝojn", "hide_all_muted_posts": "Kaŝi silentigitajn afiŝojn",
"hide_media_previews": "Kaŝi antaŭrigardojn al vidaŭdaĵoj", "hide_media_previews": "Kaŝi antaŭrigardojn al vidaŭdaĵoj",
"word_filter": "Vortofiltro", "word_filter": "Vortofiltro",
"reply_visibility_self_short": "Montri nur respondojn por mi" "reply_visibility_self_short": "Montri nur respondojn por mi",
"file_export_import": {
"errors": {
"file_slightly_new": "Etversio de dosiero malsamas, iuj agordoj eble ne funkcios",
"file_too_old": "Nekonforma ĉefa versio: {fileMajor}, versio de dosiero estas tro malnova kaj nesubtenata (minimuma estas {feMajor})",
"file_too_new": "Nekonforma ĉefa versio: {fileMajor}, ĉi tiu PleromaFE (agordoj je versio {feMajor}) tro malnovas por tio",
"invalid_file": "La elektita dosiero ne estas subtenata savkopio de agordoj de Pleroma. Nenio ŝanĝiĝis."
},
"restore_settings": "Rehavi agordojn el dosiero",
"backup_settings_theme": "Savkopii agordojn kaj haŭton al dosiero",
"backup_settings": "Savkopii agordojn al dosiero",
"backup_restore": "Savkopio de agordoj"
},
"right_sidebar": "Montri flankan breton dekstre",
"save": "Konservi ŝanĝojn",
"hide_shoutbox": "Kaŝi kriujon de nodo"
}, },
"timeline": { "timeline": {
"collapse": "Maletendi", "collapse": "Maletendi",
@@ -546,7 +567,9 @@
"no_more_statuses": "Neniuj pliaj statoj", "no_more_statuses": "Neniuj pliaj statoj",
"no_statuses": "Neniuj statoj", "no_statuses": "Neniuj statoj",
"reload": "Enlegi ree", "reload": "Enlegi ree",
"error": "Eraris akirado de historio: {0}" "error": "Eraris akirado de historio: {0}",
"socket_reconnected": "Realtempa konekto fariĝis",
"socket_broke": "Realtempa konekto perdiĝis: CloseEvent code {0}"
}, },
"user_card": { "user_card": {
"approve": "Aprobi", "approve": "Aprobi",
@@ -557,7 +580,6 @@
"follow": "Aboni", "follow": "Aboni",
"follow_sent": "Peto sendiĝis!", "follow_sent": "Peto sendiĝis!",
"follow_progress": "Petante…", "follow_progress": "Petante…",
"follow_again": "Ĉu sendi peton ree?",
"follow_unfollow": "Malaboni", "follow_unfollow": "Malaboni",
"followees": "Abonatoj", "followees": "Abonatoj",
"followers": "Abonantoj", "followers": "Abonantoj",
@@ -696,7 +718,7 @@
"media_nsfw": "Devige marki vidaŭdaĵojn konsternaj", "media_nsfw": "Devige marki vidaŭdaĵojn konsternaj",
"media_removal_desc": "Ĉi tiu nodo forigas vidaŭdaĵojn de afiŝoj el la jenaj nodoj:", "media_removal_desc": "Ĉi tiu nodo forigas vidaŭdaĵojn de afiŝoj el la jenaj nodoj:",
"media_removal": "Forigo de vidaŭdaĵoj", "media_removal": "Forigo de vidaŭdaĵoj",
"ftl_removal": "Forigo el la historio de «La tuta konata reto»", "ftl_removal": "Forigo el la historio de «Konata reto»",
"quarantine_desc": "Ĉi tiu nodo sendos nur publikajn afiŝojn al la jenaj nodoj:", "quarantine_desc": "Ĉi tiu nodo sendos nur publikajn afiŝojn al la jenaj nodoj:",
"quarantine": "Kvaranteno", "quarantine": "Kvaranteno",
"reject_desc": "Ĉi tiu nodo ne akceptos mesaĝojn de la jenaj nodoj:", "reject_desc": "Ĉi tiu nodo ne akceptos mesaĝojn de la jenaj nodoj:",
@@ -704,7 +726,7 @@
"accept_desc": "Ĉi tiu nodo nur akceptas mesaĝojn de la jenaj nodoj:", "accept_desc": "Ĉi tiu nodo nur akceptas mesaĝojn de la jenaj nodoj:",
"accept": "Akcepti", "accept": "Akcepti",
"simple_policies": "Specialaj politikoj de la nodo", "simple_policies": "Specialaj politikoj de la nodo",
"ftl_removal_desc": "Ĉi tiu nodo forigas la jenajn nodojn el la historio de «La tuta konata reto»:" "ftl_removal_desc": "Ĉi tiu nodo forigas la jenajn nodojn el la historio de «Konata reto»:"
}, },
"mrf_policies": "Ŝaltis politikon de Mesaĝa ŝanĝilaro (MRF)", "mrf_policies": "Ŝaltis politikon de Mesaĝa ŝanĝilaro (MRF)",
"keyword": { "keyword": {
+17 -9
View File
@@ -43,7 +43,10 @@
"role": { "role": {
"admin": "Administrador/a", "admin": "Administrador/a",
"moderator": "Moderador/a" "moderator": "Moderador/a"
} },
"flash_content": "Haga clic para mostrar contenido Flash usando Ruffle (experimental, puede que no funcione).",
"flash_security": "Tenga en cuenta que esto puede ser potencialmente peligroso ya que el contenido Flash sigue siendo código arbitrario.",
"flash_fail": "No se pudo cargar el contenido flash, consulte la consola para obtener más detalles."
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "Recortar la foto", "crop_picture": "Recortar la foto",
@@ -147,7 +150,7 @@
"favs_repeats": "Favoritos y repetidos", "favs_repeats": "Favoritos y repetidos",
"follows": "Nuevos seguidores", "follows": "Nuevos seguidores",
"load_older": "Cargar interacciones más antiguas", "load_older": "Cargar interacciones más antiguas",
"moves": "Usuario Migrado" "moves": "Usuario migrado"
}, },
"post_status": { "post_status": {
"new_status": "Publicar un nuevo estado", "new_status": "Publicar un nuevo estado",
@@ -181,7 +184,7 @@
"preview_empty": "Vacío", "preview_empty": "Vacío",
"preview": "Vista previa", "preview": "Vista previa",
"media_description": "Descripción multimedia", "media_description": "Descripción multimedia",
"post": "Publicación" "post": "Publicar"
}, },
"registration": { "registration": {
"bio": "Biografía", "bio": "Biografía",
@@ -585,13 +588,18 @@
"save": "Guardar los cambios", "save": "Guardar los cambios",
"file_export_import": { "file_export_import": {
"errors": { "errors": {
"invalid_file": "El archivo seleccionado no es válido como copia de seguridad de Pleroma. No se han realizado cambios." "invalid_file": "El archivo seleccionado no es válido como copia de seguridad de Pleroma. No se han realizado cambios.",
"file_too_new": "Versión principal incompatible: {fileMajor}, este \"FrontEnd\" de Pleroma (versión de configuración {feMajor}) es demasiado antiguo para manejarlo",
"file_too_old": "Versión principal incompatible: {fileMajor}, la versión del archivo es demasiado antigua y no es compatible (versión mínima {FeMajor})",
"file_slightly_new": "La versión secundaria del archivo es diferente, es posible que algunas configuraciones no se carguen"
}, },
"restore_settings": "Restaurar ajustes desde archivo", "restore_settings": "Restaurar ajustes desde archivo",
"backup_settings_theme": "Copia de seguridad de la configuración y tema a archivo", "backup_settings_theme": "Descargar la copia de seguridad de la configuración y del tema",
"backup_settings": "Copia de seguridad de la configuración a archivo", "backup_settings": "Descargar la copia de seguridad de la configuración",
"backup_restore": "Copia de seguridad de la configuración" "backup_restore": "Copia de seguridad de la configuración"
} },
"hide_shoutbox": "Ocultar cuadro de diálogo de la instancia",
"right_sidebar": "Mostrar la barra lateral a la derecha"
}, },
"time": { "time": {
"day": "{0} día", "day": "{0} día",
@@ -679,7 +687,6 @@
"follow": "Seguir", "follow": "Seguir",
"follow_sent": "¡Solicitud enviada!", "follow_sent": "¡Solicitud enviada!",
"follow_progress": "Solicitando…", "follow_progress": "Solicitando…",
"follow_again": "¿Enviar solicitud de nuevo?",
"follow_unfollow": "Dejar de seguir", "follow_unfollow": "Dejar de seguir",
"followees": "Siguiendo", "followees": "Siguiendo",
"followers": "Seguidores", "followers": "Seguidores",
@@ -735,7 +742,8 @@
"solid": "Fondo sólido", "solid": "Fondo sólido",
"disabled": "Sin resaltado" "disabled": "Sin resaltado"
}, },
"bot": "Bot" "bot": "Bot",
"edit_profile": "Edita el perfil"
}, },
"user_profile": { "user_profile": {
"timeline_title": "Línea temporal del usuario", "timeline_title": "Línea temporal del usuario",
+36 -11
View File
@@ -43,7 +43,10 @@
"role": { "role": {
"moderator": "Moderatzailea", "moderator": "Moderatzailea",
"admin": "Administratzailea" "admin": "Administratzailea"
} },
"flash_content": "Klik egin Flash edukia erakusteko Ruffle erabilita (esperimentala, baliteke ez ibiltzea).",
"flash_security": "Kontuan izan arriskutsua izan daitekeela, Flash edukia kode arbitrarioa baita.",
"flash_fail": "Ezin izan da Flash edukia kargatu. Ikusi kontsola xehetasunetarako."
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "Moztu argazkia", "crop_picture": "Moztu argazkia",
@@ -96,7 +99,8 @@
"preferences": "Hobespenak", "preferences": "Hobespenak",
"chats": "Txatak", "chats": "Txatak",
"timelines": "Denbora-lerroak", "timelines": "Denbora-lerroak",
"bookmarks": "Laster-markak" "bookmarks": "Laster-markak",
"home_timeline": "Denbora-lerro pertsonala"
}, },
"notifications": { "notifications": {
"broken_favorite": "Egoera ezezaguna, bilatzen…", "broken_favorite": "Egoera ezezaguna, bilatzen…",
@@ -136,7 +140,8 @@
"add_emoji": "Emoji bat gehitu", "add_emoji": "Emoji bat gehitu",
"custom": "Ohiko emojiak", "custom": "Ohiko emojiak",
"unicode": "Unicode emojiak", "unicode": "Unicode emojiak",
"load_all": "{emojiAmount} emoji guztiak kargatzen" "load_all": "{emojiAmount} emoji guztiak kargatzen",
"load_all_hint": "Lehenengo {saneAmount} emojia kargatuta, emoji guztiak kargatzeak errendimendu arazoak sor ditzake."
}, },
"stickers": { "stickers": {
"add_sticker": "Pegatina gehitu" "add_sticker": "Pegatina gehitu"
@@ -144,7 +149,8 @@
"interactions": { "interactions": {
"favs_repeats": "Errepikapen eta gogokoak", "favs_repeats": "Errepikapen eta gogokoak",
"follows": "Jarraitzaile berriak", "follows": "Jarraitzaile berriak",
"load_older": "Kargatu elkarrekintza zaharragoak" "load_older": "Kargatu elkarrekintza zaharragoak",
"moves": "Erabiltzailea migratuta"
}, },
"post_status": { "post_status": {
"new_status": "Mezu berri bat idatzi", "new_status": "Mezu berri bat idatzi",
@@ -172,14 +178,20 @@
"private": "Jarraitzaileentzako bakarrik: bidali jarraitzaileentzat bakarrik", "private": "Jarraitzaileentzako bakarrik: bidali jarraitzaileentzat bakarrik",
"public": "Publikoa: bistaratu denbora-lerro publikoetan", "public": "Publikoa: bistaratu denbora-lerro publikoetan",
"unlisted": "Zerrendatu gabea: ez bidali denbora-lerro publikoetara" "unlisted": "Zerrendatu gabea: ez bidali denbora-lerro publikoetara"
} },
"media_description_error": "Ezin izan da artxiboa eguneratu, saiatu berriro",
"preview": "Aurrebista",
"media_description": "Media deskribapena",
"preview_empty": "Hutsik",
"post": "Bidali",
"empty_status_error": "Ezin da argitaratu ezer idatzi gabe edo eranskinik gabe"
}, },
"registration": { "registration": {
"bio": "Biografia", "bio": "Biografia",
"email": "E-posta", "email": "E-posta",
"fullname": "Erakutsi izena", "fullname": "Erakutsi izena",
"password_confirm": "Pasahitza berretsi", "password_confirm": "Pasahitza berretsi",
"registration": "Izena ematea", "registration": "Sortu kontua",
"token": "Gonbidapen txartela", "token": "Gonbidapen txartela",
"captcha": "CAPTCHA", "captcha": "CAPTCHA",
"new_captcha": "Klikatu irudia captcha berri bat lortzeko", "new_captcha": "Klikatu irudia captcha berri bat lortzeko",
@@ -193,7 +205,10 @@
"password_required": "Ezin da hutsik utzi", "password_required": "Ezin da hutsik utzi",
"password_confirmation_required": "Ezin da hutsik utzi", "password_confirmation_required": "Ezin da hutsik utzi",
"password_confirmation_match": "Pasahitzaren berdina izan behar du" "password_confirmation_match": "Pasahitzaren berdina izan behar du"
} },
"reason": "Kontua sortzeko arrazoia",
"reason_placeholder": "Instantzia honek kontu berriak eskuz onartzen ditu.\nJakinarazi administrazioari zergatik erregistratu nahi duzun.",
"register": "Erregistratu"
}, },
"selectable_list": { "selectable_list": {
"select_all": "Hautatu denak" "select_all": "Hautatu denak"
@@ -210,7 +225,7 @@
"title": "Bi-faktore autentifikazioa", "title": "Bi-faktore autentifikazioa",
"generate_new_recovery_codes": "Sortu berreskuratze kode berriak", "generate_new_recovery_codes": "Sortu berreskuratze kode berriak",
"warning_of_generate_new_codes": "Berreskuratze kode berriak sortzean, zure berreskuratze kode zaharrak ez dute balioko.", "warning_of_generate_new_codes": "Berreskuratze kode berriak sortzean, zure berreskuratze kode zaharrak ez dute balioko.",
"recovery_codes": "Berreskuratze kodea", "recovery_codes": "Berreskuratze kodea.",
"waiting_a_recovery_codes": "Babes-kopia kodeak jasotzen…", "waiting_a_recovery_codes": "Babes-kopia kodeak jasotzen…",
"recovery_codes_warning": "Idatzi edo gorde kodeak leku seguruan - bestela ez dituzu berriro ikusiko. Zure 2FA aplikaziorako sarbidea eta berreskuratze kodeak galduz gero, zure kontutik blokeatuta egongo zara.", "recovery_codes_warning": "Idatzi edo gorde kodeak leku seguruan - bestela ez dituzu berriro ikusiko. Zure 2FA aplikaziorako sarbidea eta berreskuratze kodeak galduz gero, zure kontutik blokeatuta egongo zara.",
"authentication_methods": "Autentifikazio metodoa", "authentication_methods": "Autentifikazio metodoa",
@@ -468,7 +483,7 @@
"button": "Botoia", "button": "Botoia",
"text": "Hamaika {0} eta {1}", "text": "Hamaika {0} eta {1}",
"mono": "edukia", "mono": "edukia",
"input": "Jadanik Los Angeles-en", "input": "Jadanik Los Angeles-en.",
"faint_link": "laguntza", "faint_link": "laguntza",
"fine_print": "Irakurri gure {0} ezer erabilgarria ikasteko!", "fine_print": "Irakurri gure {0} ezer erabilgarria ikasteko!",
"header_faint": "Ondo dago", "header_faint": "Ondo dago",
@@ -480,7 +495,11 @@
"title": "Bertsioa", "title": "Bertsioa",
"backend_version": "Backend bertsioa", "backend_version": "Backend bertsioa",
"frontend_version": "Frontend bertsioa" "frontend_version": "Frontend bertsioa"
} },
"save": "Aldaketak gorde",
"setting_changed": "Ezarpena lehenetsitakoaren desberdina da",
"allow_following_move": "Baimendu jarraipen automatikoa, jarraitzen duzun kontua beste instantzia batera eramaten denean",
"new_email": "E-posta berria"
}, },
"time": { "time": {
"day": "{0} egun", "day": "{0} egun",
@@ -550,7 +569,6 @@
"follow": "Jarraitu", "follow": "Jarraitu",
"follow_sent": "Eskaera bidalita!", "follow_sent": "Eskaera bidalita!",
"follow_progress": "Eskatzen…", "follow_progress": "Eskatzen…",
"follow_again": "Eskaera berriro bidali?",
"follow_unfollow": "Jarraitzeari utzi", "follow_unfollow": "Jarraitzeari utzi",
"followees": "Jarraitzen", "followees": "Jarraitzen",
"followers": "Jarraitzaileak", "followers": "Jarraitzaileak",
@@ -691,5 +709,12 @@
}, },
"shoutbox": { "shoutbox": {
"title": "Oihu-kutxa" "title": "Oihu-kutxa"
},
"errors": {
"storage_unavailable": "Pleromak ezin izan du nabigatzailearen biltegira sartu. Hasiera-saioa edo tokiko ezarpenak ez dira gordeko eta ustekabeko arazoak sor ditzake. Saiatu cookie-ak gaitzen."
},
"remote_user_resolver": {
"searching_for": "Bilatzen",
"error": "Ez da aurkitu."
} }
} }
+2 -2
View File
@@ -579,7 +579,8 @@
"hide_full_subject": "Piilota koko otsikko", "hide_full_subject": "Piilota koko otsikko",
"show_content": "Näytä sisältö", "show_content": "Näytä sisältö",
"hide_content": "Piilota sisältö", "hide_content": "Piilota sisältö",
"status_deleted": "Poistettu viesti" "status_deleted": "Poistettu viesti",
"you": "(sinä)"
}, },
"user_card": { "user_card": {
"approve": "Hyväksy", "approve": "Hyväksy",
@@ -589,7 +590,6 @@
"follow": "Seuraa", "follow": "Seuraa",
"follow_sent": "Pyyntö lähetetty!", "follow_sent": "Pyyntö lähetetty!",
"follow_progress": "Pyydetään…", "follow_progress": "Pyydetään…",
"follow_again": "Lähetä pyyntö uudestaan?",
"follow_unfollow": "Älä seuraa", "follow_unfollow": "Älä seuraa",
"followees": "Seuraa", "followees": "Seuraa",
"followers": "Seuraajat", "followers": "Seuraajat",
+22 -5
View File
@@ -43,7 +43,10 @@
"role": { "role": {
"moderator": "Modo'", "moderator": "Modo'",
"admin": "Admin" "admin": "Admin"
} },
"flash_content": "Clique pour afficher le contenu Flash avec Ruffle (Expérimental, peut ne pas fonctionner).",
"flash_security": "Cela reste potentiellement dangereux, Flash restant du code arbitraire.",
"flash_fail": "Échec de chargement du contenu Flash, voir la console pour les détails."
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "Rogner l'image", "crop_picture": "Rogner l'image",
@@ -282,7 +285,7 @@
"new_password": "Nouveau mot de passe", "new_password": "Nouveau mot de passe",
"notification_visibility": "Types de notifications à afficher", "notification_visibility": "Types de notifications à afficher",
"notification_visibility_follows": "Suivis", "notification_visibility_follows": "Suivis",
"notification_visibility_likes": "J'aime", "notification_visibility_likes": "Favoris",
"notification_visibility_mentions": "Mentionnés", "notification_visibility_mentions": "Mentionnés",
"notification_visibility_repeats": "Partages", "notification_visibility_repeats": "Partages",
"no_rich_text_description": "Ne formatez pas le texte", "no_rich_text_description": "Ne formatez pas le texte",
@@ -553,7 +556,21 @@
"hide_wallpaper": "Cacher le fond d'écran", "hide_wallpaper": "Cacher le fond d'écran",
"hide_all_muted_posts": "Cacher les messages masqués", "hide_all_muted_posts": "Cacher les messages masqués",
"word_filter": "Filtrage par mots", "word_filter": "Filtrage par mots",
"save": "Enregistrer les changements" "save": "Enregistrer les changements",
"file_export_import": {
"backup_settings_theme": "Sauvegarder les paramètres et le thème dans un fichier",
"errors": {
"invalid_file": "Le fichier sélectionné n'est pas un format supporté pour les sauvegarde Pleroma. Aucun changement n'a été fait.",
"file_too_new": "Version majeure incompatible. {fileMajor}, ce PleromaFE ({feMajor}) est trop ancien",
"file_too_old": "Version majeure incompatible : {fileMajor}, la version du fichier est trop vielle et n'est plus supportée (vers. min. {feMajor})",
"file_slightly_new": "La version mineure du fichier est différente, quelques paramètres on pût ne pas chargés"
},
"backup_restore": "Sauvegarde des Paramètres",
"backup_settings": "Sauvegarder les paramètres dans un fichier",
"restore_settings": "Restaurer les paramètres depuis un fichier"
},
"hide_shoutbox": "Cacher la shoutbox de l'instance",
"right_sidebar": "Afficher le paneau latéral à droite"
}, },
"timeline": { "timeline": {
"collapse": "Fermer", "collapse": "Fermer",
@@ -607,7 +624,6 @@
"follow": "Suivre", "follow": "Suivre",
"follow_sent": "Demande envoyée !", "follow_sent": "Demande envoyée !",
"follow_progress": "Demande en cours…", "follow_progress": "Demande en cours…",
"follow_again": "Renvoyer la demande ?",
"follow_unfollow": "Désabonner", "follow_unfollow": "Désabonner",
"followees": "Suivis", "followees": "Suivis",
"followers": "Vous suivent", "followers": "Vous suivent",
@@ -663,7 +679,8 @@
"side": "Coté rayé", "side": "Coté rayé",
"striped": "Fond rayé" "striped": "Fond rayé"
}, },
"bot": "Robot" "bot": "Robot",
"edit_profile": "Éditer le profil"
}, },
"user_profile": { "user_profile": {
"timeline_title": "Flux du compte", "timeline_title": "Flux du compte",
-1
View File
@@ -312,7 +312,6 @@
"follow": "עקוב", "follow": "עקוב",
"follow_sent": "בקשה נשלחה!", "follow_sent": "בקשה נשלחה!",
"follow_progress": "מבקש…", "follow_progress": "מבקש…",
"follow_again": "שלח בקשה שוב?",
"follow_unfollow": "בטל עקיבה", "follow_unfollow": "בטל עקיבה",
"followees": "נעקבים", "followees": "נעקבים",
"followers": "עוקבים", "followers": "עוקבים",
+621
View File
@@ -0,0 +1,621 @@
{
"settings": {
"style": {
"preview": {
"link": "sebuah tautan yang kecil nan bagus",
"header": "Pratinjau",
"error": "Contoh kesalahan",
"button": "Tombol",
"input": "Baru saja mendarat di L.A.",
"faint_link": "manual berguna",
"fine_print": "Baca {0} kami untuk belajar sesuatu yang tak ada gunanya!",
"header_faint": "Ini baik-baik saja",
"checkbox": "Saya telah membaca sekilas syarat dan ketentuan"
},
"advanced_colors": {
"alert_neutral": "Neutral",
"alert_warning": "Peringatan",
"alert_error": "Kesalahan",
"_tab_label": "Lanjutan",
"post": "Postingan/Bio pengguna",
"popover": "Tooltip, menu, popover",
"badge_notification": "Notifikasi",
"top_bar": "Bar atas",
"borders": "",
"buttons": "Tombol",
"wallpaper": "Latar belakang",
"panel_header": "Header panel",
"icons": "Ikon-ikon",
"disabled": "Dinonaktifkan"
},
"common_colors": {
"main": "Warna umum",
"_tab_label": "Umum"
},
"common": {
"contrast": {
"context": {
"text": "untuk teks",
"18pt": "Untuk teks besar (18pt+)"
}
},
"color": "Warna"
},
"switcher": {
"help": {
"upgraded_from_v2": "PleromaFE telah diperbarui, tema dapat terlihat sedikit berbeda dari apa yang Anda ingat.",
"future_version_imported": "Berkas yang Anda impor dibuat pada versi FE yang lebih baru.",
"older_version_imported": "Berkas yang Anda impor dibuat pada versi FE yang lebih lama.",
"fe_upgraded": "Mesin tema PleromaFE diperbarui setelah pembaruan versi."
},
"use_source": "Versi baru",
"use_snapshot": "Versi lama",
"load_theme": "Muat tema"
},
"fonts": {
"_tab_label": "Font",
"components": {
"interface": "Antarmuka",
"post": "Teks postingan"
},
"family": "Nama font",
"size": "Ukuran (dalam px)",
"weight": "Berat (ketebalan)"
},
"shadows": {
"components": {
"panel": "Panel",
"panelHeader": "Header panel"
}
}
},
"notification_setting_privacy": "Privasi",
"notifications": "Notifikasi",
"values": {
"true": "ya",
"false": "tidak"
},
"user_settings": "Pengaturan Pengguna",
"upload_a_photo": "Unggah foto",
"theme": "Tema",
"text": "Teks",
"settings": "Pengaturan",
"security_tab": "Keamanan",
"saving_ok": "Pengaturan disimpan",
"profile_tab": "Profil",
"profile_background": "Latar belakang profil",
"token": "Token",
"oauth_tokens": "Token OAuth",
"show_moderator_badge": "Tampilkan lencana \"Moderator\" di profil saya",
"show_admin_badge": "Tampilkan lencana \"Admin\" di profil saya",
"new_password": "Kata sandi baru",
"new_email": "Surel baru",
"name_bio": "Nama & bio",
"name": "Nama",
"profile_fields": {
"value": "Isi",
"name": "Label",
"label": "Metadata profil"
},
"limited_availability": "Tidak tersedia di browser Anda",
"invalid_theme_imported": "Berkas yang dipilih bukan sebuah tema yang didukung Pleroma. Tidak ada perbuahan yang dibuat pada tema Anda.",
"interfaceLanguage": "Bahasa antarmuka",
"interface": "Antarmuka",
"instance_default_simple": "(bawaan)",
"instance_default": "(bawaan: {value})",
"general": "Umum",
"delete_account_error": "Ada masalah ketika menghapus akun Anda. Jika ini terus terjadi harap hubungi adminstrator instansi Anda.",
"delete_account_description": "Hapus data Anda secara permanen dan menonaktifkan akun Anda.",
"delete_account": "Hapus akun",
"data_import_export_tab": "Impor / ekspor data",
"current_password": "Kata sandi saat ini",
"confirm_new_password": "Konfirmasi kata sandi baru",
"version": {
"title": "Versi",
"backend_version": "Versi backend",
"frontend_version": "Versi frontend"
},
"security": "Keamanan",
"changed_password": "Kata sandi berhasil diubah!",
"change_password_error": "Ada masalah ketika mengubah kata sandi Anda.",
"change_password": "Ubah kata sandi",
"changed_email": "Surel berhasil diubah!",
"change_email_error": "Ada masalah ketika mengubah surel Anda.",
"change_email": "Ubah surel",
"cRed": "Merah (Batal)",
"cBlue": "Biru (Balas, ikuti)",
"btnRadius": "Tombol",
"bot": "Ini adalah akun bot",
"block_export": "Ekspor blokiran",
"bio": "Bio",
"background": "Latar belakang",
"avatarRadius": "Avatar",
"avatar": "Avatar",
"attachments": "Lampiran",
"mfa": {
"scan": {
"title": "Pindai"
},
"confirm_and_enable": "Konfirmasi & aktifkan OTP",
"setup_otp": "Siapkan OTP",
"otp": "OTP",
"recovery_codes_warning": "Tulis kode-kode nya atau simpan mereka di tempat yang aman - jika tidak Anda tidak akan melihat mereka lagi. Jika Anda tidak dapat mengakses aplikasi 2FA Anda dan kode pemulihan Anda hilang Anda tidak akan bisa mengakses akun Anda.",
"authentication_methods": "Metode otentikasi",
"recovery_codes": "Kode pemulihan.",
"warning_of_generate_new_codes": "Ketika Anda menghasilkan kode pemulihan baru, kode lama Anda berhenti bekerja.",
"generate_new_recovery_codes": "Hasilkan kode pemulihan baru",
"title": "Otentikasi Dua-faktor",
"waiting_a_recovery_codes": "Menerima kode cadangan…",
"verify": {
"desc": "Untuk mengaktifkan otentikasi dua-faktor, masukkan kode dari aplikasi dua-faktor Anda:"
}
},
"app_name": "Nama aplikasi",
"save": "Simpan perubahan",
"valid_until": "Valid hingga",
"follow_import_error": "Terjadi kesalahan ketika mengimpor pengikut",
"emoji_reactions_on_timeline": "Tampilkan reaksi emoji pada linimasa",
"chatMessageRadius": "Pesan obrolan",
"cOrange": "Jingga (Favorit)",
"avatarAltRadius": "Avatar (notifikasi)",
"hide_shoutbox": "Sembunyikan kotak suara instansi",
"hide_followers_count_description": "Jangan tampilkan jumlah pengikut",
"hide_follows_count_description": "Jangan tampilkan jumlah mengikuti",
"hide_followers_description": "Jangan tampilkan siapa yang mengikuti saya",
"hide_follows_description": "Jangan tampilkan siapa yang saya ikuti",
"notification_visibility_emoji_reactions": "Reaksi",
"notification_visibility_follows": "Diikuti",
"notification_visibility_moves": "Pengguna Bermigrasi",
"notification_visibility_repeats": "Ulangan",
"notification_visibility_mentions": "Sebutan",
"notification_visibility_likes": "Favorit",
"notification_visibility": "Jenis notifikasi yang perlu ditampilkan",
"links": "Tautan",
"hide_user_stats": "Sembunyikan statistik pengguna (contoh. jumlah pengikut)",
"hide_post_stats": "Sembunyikan statistik postingan (contoh. jumlah favorit)",
"use_one_click_nsfw": "Buka lampiran NSFW hanya dengan satu klik",
"hide_wallpaper": "Sembunyikan latar belakang instansi",
"blocks_imported": "Blokiran diimpor! Pemrosesannya mungkin memakan sedikit waktu.",
"block_import_error": "Terjadi kesalahan ketika mengimpor blokiran",
"block_import": "Impor blokiran",
"block_export_button": "Ekspor blokiran Anda menjadi berkas csv",
"blocks_tab": "Blokiran",
"delete_account_instructions": "Ketik kata sandi Anda pada input di bawah untuk mengkonfirmasi penghapusan akun.",
"mutes_and_blocks": "Bisuan dan Blokiran",
"enter_current_password_to_confirm": "Masukkan kata sandi Anda saat ini untuk mengkonfirmasi identitas Anda",
"filtering": "Penyaringan",
"word_filter": "Penyaring kata",
"avatar_size_instruction": "Ukuran minimum gambar avatar yang disarankan adalah 150x150 piksel.",
"attachmentRadius": "Lampiran",
"cGreen": "Hijau (Retweet)",
"max_thumbnails": "Jumlah thumbnail maksimum per postingan",
"loop_video": "Ulang-ulang video",
"loop_video_silent_only": "Ulang-ulang video tanpa suara (seperti \"gif\" Mastodon)",
"pause_on_unfocused": "Jeda aliran ketika tab di dalam fokus",
"reply_visibility_following": "Hanya tampilkan balasan yang ditujukan kepada saya atau orang yang saya ikuti",
"reply_visibility_following_short": "Tampilkan balasan ke orang yang saya ikuti",
"saving_err": "Terjadi kesalahan ketika menyimpan pengaturan",
"search_user_to_block": "Cari siapa yang Anda ingin blokir",
"search_user_to_mute": "Cari siapa yang ingin Anda bisukan",
"set_new_avatar": "Tetapkan avatar baru",
"set_new_profile_background": "Tetapkan latar belakang profil baru",
"subject_line_behavior": "Salin subyek ketika membalas",
"subject_line_email": "Seperti surel: \"re: subyek\"",
"subject_line_mastodon": "Seperti mastodon: salin saja",
"subject_line_noop": "Jangan salin",
"useStreamingApiWarning": "(Tidak disarankan, eksperimental, diketahui dapat melewati postingan-postingan)",
"fun": "Seru",
"enable_web_push_notifications": "Aktifkan notifikasi push web",
"more_settings": "Lebih banyak pengaturan",
"reply_visibility_all": "Tampilkan semua balasan",
"reply_visibility_self": "Hanya tampilkan balasan yang ditujukan kepada saya"
},
"about": {
"mrf": {
"keyword": {
"reject": "Tolak",
"is_replaced_by": "→"
},
"simple": {
"quarantine_desc": "Instansi ini hanya akan mengirim postingan publik ke instansi-instansi berikut:",
"quarantine": "Karantina",
"reject_desc": "Instansi ini tidak akan menerima pesan dari instansi-instansi berikut:",
"reject": "Tolak",
"accept_desc": "Instansi ini hanya menerima pesan dari instansi-instansi berikut:",
"accept": "Terima"
},
"federation": "Federasi",
"mrf_policies": "Kebijakan MRF yang diaktifkan"
},
"staff": "Staf"
},
"time": {
"day": "{0} hari",
"days": "{0} hari",
"day_short": "{0}h",
"days_short": "{0}h",
"hour": "{0} jam",
"hours": "{0} jam",
"hour_short": "{0}j",
"hours_short": "{0}j",
"in_future": "dalam {0}",
"in_past": "{0} yang lalu",
"minute": "{0} menit",
"minutes": "{0} menit",
"minute_short": "{0}m",
"minutes_short": "{0}m",
"month": "{0} bulan",
"months": "{0} bulan",
"month_short": "{0}b",
"months_short": "{0}b",
"now": "baru saja",
"now_short": "sekarang",
"second": "{0} detik",
"seconds": "{0} detik",
"second_short": "{0}d",
"seconds_short": "{0}d",
"week": "{0} pekan",
"weeks": "{0} pekan",
"week_short": "{0}p",
"weeks_short": "{0}p",
"year": "{0} tahun",
"years": "{0} tahun",
"year_short": "{0}t",
"years_short": "{0}t"
},
"timeline": {
"conversation": "Percakapan",
"error": "Terjadi kesalahan memuat linimasa: {0}",
"no_retweet_hint": "Postingan ditandai sebagai hanya-pengikut atau langsung dan tidak dapat diulang",
"repeated": "diulangi",
"reload": "Muat ulang",
"no_more_statuses": "Tidak ada status lagi",
"no_statuses": "Tidak ada status"
},
"status": {
"favorites": "Favorit",
"repeats": "Ulangan",
"delete": "Hapus status",
"pin": "Sematkan di profil",
"unpin": "Berhenti menyematkan dari profil",
"pinned": "Disematkan",
"delete_confirm": "Apakah Anda benar-benar ingin menghapus status ini?",
"reply_to": "Balas ke",
"replies_list": "Balasan:",
"mute_conversation": "Bisukan percakapan",
"unmute_conversation": "Berhenti membisikan percakapan",
"status_unavailable": "Status tidak tersedia",
"thread_muted_and_words": ", memiliki kata:",
"hide_content": "",
"show_content": "",
"status_deleted": "Postingan ini telah dihapus",
"nsfw": "NSFW"
},
"user_card": {
"block": "Blokir",
"blocked": "Diblokir!",
"deny": "Tolak",
"edit_profile": "Sunting profil",
"favorites": "Favorit",
"follow": "Ikuti",
"follow_sent": "Permintaan dikirim!",
"follow_progress": "Meminta…",
"mute": "Bisukan",
"muted": "Dibisukan",
"per_day": "per hari",
"report": "Laporkan",
"statuses": "Status",
"unblock": "Berhenti memblokir",
"block_progress": "Memblokir…",
"unmute": "Berhenti membisukan",
"mute_progress": "Membisukan…",
"hide_repeats": "Sembunyikan ulangan",
"show_repeats": "Tampilkan ulangan",
"bot": "Bot",
"admin_menu": {
"moderation": "Moderasi",
"activate_account": "Aktifkan akun",
"deactivate_account": "Nonaktifkan akun",
"delete_account": "Hapus akun",
"force_nsfw": "Tandai semua postingan sebagai NSFW",
"strip_media": "Hapus media dari postingan-postingan",
"delete_user": "Hapus pengguna",
"delete_user_confirmation": "Apakah Anda benar-benar yakin? Tindakan ini tidak dapat dibatalkan."
},
"follow_unfollow": "Berhenti mengikuti",
"followees": "Mengikuti",
"followers": "Pengikut",
"following": "Diikuti!",
"follows_you": "Mengikuti Anda!",
"hidden": "Disembunyikan",
"its_you": "Ini Anda!",
"media": "Media",
"mention": "Sebut",
"message": "Kirimkan pesan"
},
"user_profile": {
"timeline_title": "Linimasa pengguna"
},
"user_reporting": {
"title": "Melaporkan {0}",
"add_comment_description": "Laporan ini akan dikirim ke moderator instansi Anda. Anda dapat menyediakan penjelasan mengapa Anda melaporkan akun ini di bawah:",
"additional_comments": "Komentar tambahan",
"forward_description": "Akun ini berada di server lain. Kirim salinan dari laporannya juga?",
"submit": "Kirim",
"generic_error": "Sebuah kesalahan terjadi ketika memproses permintaan Anda."
},
"notifications": {
"favorited_you": "memfavoritkan status Anda",
"reacted_with": "bereaksi dengan {0}",
"no_more_notifications": "Tidak ada notifikasi lagi",
"repeated_you": "mengulangi status Anda",
"read": "Dibaca!",
"notifications": "Notifikasi",
"follow_request": "ingin mengikuti Anda",
"followed_you": "mengikuti Anda",
"error": "Terjadi kesalahan ketika memuat notifikasi: {0}",
"migrated_to": "bermigrasi ke",
"load_older": "Muat notifikasi yang lebih lama",
"broken_favorite": "Status tak diketahui, mencarinya…"
},
"who_to_follow": {
"more": "Lebih banyak"
},
"tool_tip": {
"media_upload": "Unggah media",
"repeat": "Ulangi",
"reply": "Balas",
"favorite": "Favorit",
"add_reaction": "Tambahkan Reaksi",
"user_settings": "Pengaturan Pengguna"
},
"upload": {
"error": {
"base": "Pengunggahan gagal.",
"message": "Pengunggahan gagal: {0}",
"file_too_big": "Berkas terlalu besar [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Coba lagi nanti"
},
"file_size_units": {
"B": "B",
"KiB": "KiB",
"MiB": "MiB",
"GiB": "GiB",
"TiB": "TiB"
}
},
"search": {
"people": "Orang",
"hashtags": "Tagar",
"person_talking": "{count} orang berbicara",
"people_talking": "{count} orang berbicara",
"no_results": "Tidak ada hasil"
},
"password_reset": {
"forgot_password": "Lupa kata sandi?",
"placeholder": "Surel atau nama pengguna Anda",
"return_home": "Kembali ke halaman beranda",
"too_many_requests": "Anda telah mencapai batas percobaan, coba lagi nanti.",
"instruction": "Masukkan surel atau nama pengguna Anda. Kami akan mengirimkan Anda tautan untuk mengatur ulang kata sandi.",
"password_reset": "Pengatur-ulangan kata sandi",
"password_reset_disabled": "Pengatur-ulangan kata sandi dinonaktifkan. Hubungi administrator instansi Anda.",
"password_reset_required": "Anda harus mengatur ulang kata sandi Anda untuk masuk.",
"password_reset_required_but_mailer_is_disabled": "Anda harus mengatur ulang kata sandi, tetapi pengatur-ulangan kata sandi dinonaktifkan. Silakan hubungi administrator instansi Anda."
},
"chats": {
"you": "Anda:",
"message_user": "Kirim Pesan ke {nickname}",
"delete": "Hapus",
"chats": "Obrolan",
"new": "Obrolan Baru",
"empty_message_error": "Tidak dapat memposting pesan yang kosong",
"more": "Lebih banyak",
"delete_confirm": "Apakah Anda benar-benar ingin menghapus pesan ini?",
"error_loading_chat": "Sesuatu yang salah terjadi ketika memuat obrolan.",
"error_sending_message": "Sesuatu yang salah terjadi ketika mengirim pesan.",
"empty_chat_list_placeholder": "Anda belum memiliki obrolan. Buat sbeuah obrolan baru!"
},
"file_type": {
"audio": "Audio",
"video": "Video",
"image": "Gambar",
"file": "Berkas"
},
"registration": {
"bio_placeholder": "contoh.\nHai, aku Lain.\nAku seorang putri anime yang tinggal di pinggiran kota Jepang. Kamu mungkin mengenal aku dari Wired.",
"validations": {
"password_confirmation_required": "tidak boleh kosong",
"password_required": "tidak boleh kosong",
"email_required": "tidak boleh kosong",
"fullname_required": "tidak boleh kosong",
"username_required": "tidak boleh kosong"
},
"register": "Daftar",
"fullname_placeholder": "contoh. Lain Iwakura",
"username_placeholder": "contoh. lain",
"new_captcha": "Klik gambarnya untuk mendapatkan captcha baru",
"captcha": "CAPTCHA",
"token": "Token undangan",
"password_confirm": "Konfirmasi kata sandi",
"email": "Surel",
"bio": "Bio",
"reason_placeholder": "Instansi ini menerima pendaftaran secara manual.\nBeritahu administrasinya mengapa Anda ingin mendaftar.",
"reason": "Alasan mendaftar",
"registration": "Pendaftaran"
},
"post_status": {
"preview_empty": "Kosong",
"default": "Baru saja mendarat di L.A.",
"content_warning": "Subyek (opsional)",
"content_type": {
"text/bbcode": "BBCode",
"text/markdown": "Markdown",
"text/html": "HTML",
"text/plain": "Teks biasa"
},
"media_description": "Keterangan media",
"attachments_sensitive": "Tandai lampiran sebagai sensitif",
"scope": {
"public": "Publik - posting ke linimasa publik",
"private": "Hanya-pengikut - posting hanya kepada pengikut",
"direct": "Langsung - posting hanya kepada pengguna yang disebut"
},
"preview": "Pratinjau",
"post": "Posting",
"posting": "Memposting",
"direct_warning_to_first_only": "Postingan ini akan terlihat oleh pengguna yang disebutkan di awal pesan.",
"direct_warning_to_all": "Postingan ini akan terlihat oleh pengguna yang disebutkan.",
"scope_notice": {
"private": "Postingan ini akan terlihat hanya oleh pengikut Anda",
"public": "Postingan ini akan terlihat oleh siapa saja"
},
"media_description_error": "Gagal memperbarui media, coba lagi",
"empty_status_error": "Tidak dapat memposting status kosong tanpa berkas",
"account_not_locked_warning_link": "terkunci",
"account_not_locked_warning": "Akun Anda tidak {0}. Siapapun dapat mengikuti Anda untuk melihat postingan hanya-pengikut Anda.",
"new_status": "Posting status baru"
},
"general": {
"apply": "Terapkan",
"flash_fail": "Gagal memuat konten flash, lihat console untuk keterangan.",
"flash_security": "Harap ingat ini dapat menjadi berbahaya karena konten Flash masih termasuk arbitrary code.",
"flash_content": "Klik untuk menampilkan konten Flash menggunakan Ruffle (Eksperimental, mungkin tidak bekerja).",
"role": {
"moderator": "Moderator",
"admin": "Admin"
},
"peek": "Intip",
"close": "Tutup",
"verify": "Verifikasi",
"confirm": "Konfirmasi",
"enable": "Aktifkan",
"disable": "Nonaktifkan",
"cancel": "Batal",
"show_less": "Tampilkan lebih sedikit",
"show_more": "Tampilkan lebih banyak",
"optional": "opsional",
"retry": "Coba lagi",
"error_retry": "Harap coba lagi",
"generic_error": "Terjadi kesalahan",
"loading": "Memuat…",
"more": "Lebih banyak",
"submit": "Kirim"
},
"remote_user_resolver": {
"error": "Tidak ditemukan."
},
"emoji": {
"load_all": "Memuat semua {emojiAmount} emoji",
"load_all_hint": "Memuat {saneAmount} emoji pertama, memuat semua emoji dapat menyebabkan masalah performa.",
"unicode": "Emoji unicode",
"add_emoji": "Sisipkan emoji",
"search_emoji": "Cari emoji",
"emoji": "Emoji",
"stickers": "Stiker",
"keep_open": "Tetap buka pemilih",
"custom": "Emoji kustom"
},
"polls": {
"expired": "Japat berakhir {0} yang lalu",
"expires_in": "Japat berakhir dalam {0}",
"expiry": "Usia japat",
"type": "Jenis japat",
"vote": "Pilih",
"votes_count": "{count} suara | {count} suara",
"people_voted_count": "{count} orang memilih | {count} orang memilih",
"votes": "suara",
"option": "Opsi",
"add_option": "Tambahkan opsi",
"add_poll": "Tambahkan japat",
"not_enough_options": "Terlalu sedikit opsi yang unik pada japat"
},
"nav": {
"preferences": "Preferensi",
"search": "Cari",
"user_search": "Pencarian Pengguna",
"home_timeline": "Linimasa beranda",
"timeline": "Linimasa",
"public_tl": "Linimasa publik",
"interactions": "Interaksi",
"mentions": "Sebutan",
"back": "Kembali",
"administration": "Administrasi",
"about": "Tentang",
"timelines": "Linimasa",
"chats": "Obrolan",
"dms": "Pesan langsung",
"friend_requests": "Ingin mengikuti"
},
"media_modal": {
"next": "Selanjutnya",
"previous": "Sebelum"
},
"login": {
"recovery_code": "Kode pemulihan",
"enter_recovery_code": "Masukkan kode pemulihan",
"authentication_code": "Kode otentikasi",
"hint": "Masuk untuk ikut berdiskusi",
"username": "Nama pengguna",
"register": "Daftar",
"placeholder": "contoh: lain",
"password": "Kata sandi",
"logout": "Keluar",
"description": "Masuk dengan OAuth",
"login": "Masuk",
"heading": {
"totp": "Otentikasi dua-faktor"
},
"enter_two_factor_code": "Masukkan kode dua-faktor"
},
"importer": {
"error": "Terjadi kesalahan ketika mnengimpor berkas ini.",
"success": "Berhasil mengimpor.",
"submit": "Kirim"
},
"image_cropper": {
"cancel": "Batal",
"save_without_cropping": "Simpan tanpa memotong",
"save": "Simpan",
"crop_picture": "Potong gambar"
},
"finder": {
"find_user": "Cari pengguna",
"error_fetching_user": "Terjadi kesalahan ketika memuat pengguna"
},
"features_panel": {
"title": "Fitur-fitur",
"text_limit": "Batas teks",
"gopher": "Gopher",
"pleroma_chat_messages": "Pleroma Obrolan",
"chat": "Obrolan",
"upload_limit": "Batas unggahan"
},
"exporter": {
"processing": "Memproses, Anda akan segera diminta untuk mengunduh berkas Anda",
"export": "Ekspor"
},
"domain_mute_card": {
"unmute": "Berhenti membisukan",
"mute_progress": "Membisukan…",
"mute": "Bisukan",
"unmute_progress": "Memberhentikan pembisuan…"
},
"display_date": {
"today": "Hari Ini"
},
"selectable_list": {
"select_all": "Pilih semua"
},
"interactions": {
"moves": "Pengguna yang bermigrasi",
"follows": "Pengikut baru",
"favs_repeats": "Ulangan dan favorit",
"load_older": "Muat interaksi yang lebih tua"
},
"errors": {
"storage_unavailable": "Pleroma tidak dapat mengakses penyimpanan browser. Login Anda atau pengaturan lokal Anda tidak akan tersimpan dan masalah yang tidak terduga dapat terjadi. Coba mengaktifkan kuki."
},
"shoutbox": {
"title": "Kotak Suara"
}
}
+15 -10
View File
@@ -21,7 +21,10 @@
"role": { "role": {
"moderator": "Moderatore", "moderator": "Moderatore",
"admin": "Amministratore" "admin": "Amministratore"
} },
"flash_fail": "Contenuto Flash non caricato, vedi console del browser.",
"flash_content": "Mostra contenuto Flash tramite Ruffle (funzione in prova).",
"flash_security": "Può essere pericoloso perché i contenuti in Flash sono eseguibili."
}, },
"nav": { "nav": {
"mentions": "Menzioni", "mentions": "Menzioni",
@@ -65,13 +68,13 @@
"current_avatar": "La tua icona attuale", "current_avatar": "La tua icona attuale",
"current_profile_banner": "Il tuo stendardo attuale", "current_profile_banner": "Il tuo stendardo attuale",
"filtering": "Filtri", "filtering": "Filtri",
"filtering_explanation": "Tutti i post contenenti queste parole saranno silenziati, una per riga", "filtering_explanation": "Tutti i messaggi contenenti queste parole saranno silenziati, una per riga",
"hide_attachments_in_convo": "Nascondi gli allegati presenti nelle conversazioni", "hide_attachments_in_convo": "Nascondi gli allegati presenti nelle conversazioni",
"hide_attachments_in_tl": "Nascondi gli allegati presenti nelle sequenze", "hide_attachments_in_tl": "Nascondi gli allegati presenti nelle sequenze",
"name": "Nome", "name": "Nome",
"name_bio": "Nome ed introduzione", "name_bio": "Nome ed introduzione",
"nsfw_clickthrough": "Fai click per visualizzare gli allegati offuscati", "nsfw_clickthrough": "Fai click per visualizzare gli allegati offuscati",
"profile_background": "Sfondo della tua pagina", "profile_background": "Sfondo del tuo profilo",
"profile_banner": "Gonfalone del tuo profilo", "profile_banner": "Gonfalone del tuo profilo",
"set_new_avatar": "Scegli una nuova icona", "set_new_avatar": "Scegli una nuova icona",
"set_new_profile_background": "Scegli un nuovo sfondo", "set_new_profile_background": "Scegli un nuovo sfondo",
@@ -365,8 +368,8 @@
"search_user_to_mute": "Cerca utente da silenziare", "search_user_to_mute": "Cerca utente da silenziare",
"search_user_to_block": "Cerca utente da bloccare", "search_user_to_block": "Cerca utente da bloccare",
"autohide_floating_post_button": "Nascondi automaticamente il pulsante di composizione (mobile)", "autohide_floating_post_button": "Nascondi automaticamente il pulsante di composizione (mobile)",
"show_moderator_badge": "Mostra l'insegna di moderatore sulla mia pagina", "show_moderator_badge": "Mostra l'insegna di moderatore sul mio profilo",
"show_admin_badge": "Mostra l'insegna di amministratore sulla mia pagina", "show_admin_badge": "Mostra l'insegna di amministratore sul mio profilo",
"hide_followers_count_description": "Non mostrare quanti seguaci ho", "hide_followers_count_description": "Non mostrare quanti seguaci ho",
"hide_follows_count_description": "Non mostrare quanti utenti seguo", "hide_follows_count_description": "Non mostrare quanti utenti seguo",
"hide_followers_description": "Non mostrare i miei seguaci", "hide_followers_description": "Non mostrare i miei seguaci",
@@ -443,7 +446,9 @@
"backup_settings_theme": "Archivia impostazioni e tema localmente", "backup_settings_theme": "Archivia impostazioni e tema localmente",
"backup_settings": "Archivia impostazioni localmente", "backup_settings": "Archivia impostazioni localmente",
"backup_restore": "Archiviazione impostazioni" "backup_restore": "Archiviazione impostazioni"
} },
"right_sidebar": "Mostra barra laterale a destra",
"hide_shoutbox": "Nascondi muro dei graffiti"
}, },
"timeline": { "timeline": {
"error_fetching": "Errore nell'aggiornamento", "error_fetching": "Errore nell'aggiornamento",
@@ -511,7 +516,6 @@
"its_you": "Sei tu!", "its_you": "Sei tu!",
"hidden": "Nascosto", "hidden": "Nascosto",
"follow_unfollow": "Disconosci", "follow_unfollow": "Disconosci",
"follow_again": "Reinvio richiesta?",
"follow_progress": "Richiedo…", "follow_progress": "Richiedo…",
"follow_sent": "Richiesta inviata!", "follow_sent": "Richiesta inviata!",
"favorites": "Preferiti", "favorites": "Preferiti",
@@ -522,7 +526,8 @@
"striped": "A righe", "striped": "A righe",
"solid": "Un colore", "solid": "Un colore",
"disabled": "Nessun risalto" "disabled": "Nessun risalto"
} },
"edit_profile": "Modifica profilo"
}, },
"chat": { "chat": {
"title": "Chat" "title": "Chat"
@@ -660,7 +665,7 @@
}, },
"domain_mute_card": { "domain_mute_card": {
"mute": "Silenzia", "mute": "Silenzia",
"mute_progress": "Silenzio…", "mute_progress": "Procedo…",
"unmute": "Ascolta", "unmute": "Ascolta",
"unmute_progress": "Procedo…" "unmute_progress": "Procedo…"
}, },
@@ -701,7 +706,7 @@
}, },
"interactions": { "interactions": {
"favs_repeats": "Condivisi e Graditi", "favs_repeats": "Condivisi e Graditi",
"load_older": "Carica vecchie interazioni", "load_older": "Carica interazioni precedenti",
"moves": "Utenti migrati", "moves": "Utenti migrati",
"follows": "Nuovi seguìti" "follows": "Nuovi seguìti"
}, },
-1
View File
@@ -567,7 +567,6 @@
"follow": "フォロー", "follow": "フォロー",
"follow_sent": "リクエストを、おくりました!", "follow_sent": "リクエストを、おくりました!",
"follow_progress": "リクエストしています…", "follow_progress": "リクエストしています…",
"follow_again": "ふたたびリクエストをおくりますか?",
"follow_unfollow": "フォローをやめる", "follow_unfollow": "フォローをやめる",
"followees": "フォロー", "followees": "フォロー",
"followers": "フォロワー", "followers": "フォロワー",
-1
View File
@@ -679,7 +679,6 @@
"follow": "フォロー", "follow": "フォロー",
"follow_sent": "リクエストを送りました!", "follow_sent": "リクエストを送りました!",
"follow_progress": "リクエストしています…", "follow_progress": "リクエストしています…",
"follow_again": "再びリクエストを送りますか?",
"follow_unfollow": "フォローをやめる", "follow_unfollow": "フォローをやめる",
"followees": "フォロー", "followees": "フォロー",
"followers": "フォロワー", "followers": "フォロワー",
-1
View File
@@ -428,7 +428,6 @@
"follow": "팔로우", "follow": "팔로우",
"follow_sent": "요청 보내짐!", "follow_sent": "요청 보내짐!",
"follow_progress": "요청 중…", "follow_progress": "요청 중…",
"follow_again": "요청을 다시 보낼까요?",
"follow_unfollow": "팔로우 중지", "follow_unfollow": "팔로우 중지",
"followees": "팔로우 중", "followees": "팔로우 중",
"followers": "팔로워", "followers": "팔로워",
-1
View File
@@ -516,7 +516,6 @@
"follow": "Følg", "follow": "Følg",
"follow_sent": "Forespørsel sendt!", "follow_sent": "Forespørsel sendt!",
"follow_progress": "Forespør…", "follow_progress": "Forespør…",
"follow_again": "Gjenta forespørsel?",
"follow_unfollow": "Avfølg", "follow_unfollow": "Avfølg",
"followees": "Følger", "followees": "Følger",
"followers": "Følgere", "followers": "Følgere",
+4 -1
View File
@@ -565,9 +565,9 @@
"deny": "Weigeren", "deny": "Weigeren",
"favorites": "Favorieten", "favorites": "Favorieten",
"follow": "Volgen", "follow": "Volgen",
"follow_cancel": "Aanvraag annuleren",
"follow_sent": "Aanvraag verzonden!", "follow_sent": "Aanvraag verzonden!",
"follow_progress": "Aanvragen…", "follow_progress": "Aanvragen…",
"follow_again": "Aanvraag opnieuw zenden?",
"follow_unfollow": "Stop volgen", "follow_unfollow": "Stop volgen",
"followees": "Aan het volgen", "followees": "Aan het volgen",
"followers": "Volgers", "followers": "Volgers",
@@ -670,6 +670,9 @@
"mrf_policies": "Ingeschakelde MRF-regels", "mrf_policies": "Ingeschakelde MRF-regels",
"simple": { "simple": {
"simple_policies": "Instantiespecifieke regels", "simple_policies": "Instantiespecifieke regels",
"instance": "Instantie",
"reason": "Reden",
"not_applicable": "n.v.t.",
"accept": "Accepteren", "accept": "Accepteren",
"accept_desc": "Deze instantie accepteert alleen berichten van de volgende instanties:", "accept_desc": "Deze instantie accepteert alleen berichten van de volgende instanties:",
"reject": "Afwijzen", "reject": "Afwijzen",
-1
View File
@@ -465,7 +465,6 @@
"follow": "Seguir", "follow": "Seguir",
"follow_sent": "Demanda enviada!", "follow_sent": "Demanda enviada!",
"follow_progress": "Demanda…", "follow_progress": "Demanda…",
"follow_again": "Tornar enviar la demanda?",
"follow_unfollow": "Quitar de seguir", "follow_unfollow": "Quitar de seguir",
"followees": "Abonaments", "followees": "Abonaments",
"followers": "Seguidors", "followers": "Seguidors",
+59 -20
View File
@@ -19,8 +19,8 @@
"reject_desc": "Ta instancja odrzuca posty z wymienionych instancji:", "reject_desc": "Ta instancja odrzuca posty z wymienionych instancji:",
"quarantine": "Kwarantanna", "quarantine": "Kwarantanna",
"quarantine_desc": "Ta instancja wysyła tylko publiczne posty do wymienionych instancji:", "quarantine_desc": "Ta instancja wysyła tylko publiczne posty do wymienionych instancji:",
"ftl_removal": "Usunięcie z \"Całej znanej sieci\"", "ftl_removal": "Usunięcie z Całej znanej sieci",
"ftl_removal_desc": "Ta instancja usuwa wymienionych instancje z \"Całej znanej sieci\":", "ftl_removal_desc": "Ta instancja usuwa wymienionych instancje z Całej znanej sieci:",
"media_removal": "Usuwanie multimediów", "media_removal": "Usuwanie multimediów",
"media_removal_desc": "Ta instancja usuwa multimedia z postów od wymienionych instancji:", "media_removal_desc": "Ta instancja usuwa multimedia z postów od wymienionych instancji:",
"media_nsfw": "Multimedia ustawione jako wrażliwe", "media_nsfw": "Multimedia ustawione jako wrażliwe",
@@ -75,7 +75,13 @@
"loading": "Ładowanie…", "loading": "Ładowanie…",
"retry": "Spróbuj ponownie", "retry": "Spróbuj ponownie",
"peek": "Spójrz", "peek": "Spójrz",
"error_retry": "Spróbuj ponownie" "error_retry": "Spróbuj ponownie",
"flash_content": "Naciśnij, aby wyświetlić zawartości Flash z użyciem Ruffle (eksperymentalnie, może nie działać).",
"flash_fail": "Nie udało się załadować treści flash, zajrzyj do konsoli, aby odnaleźć szczegóły.",
"role": {
"moderator": "Moderator",
"admin": "Administrator"
}
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "Przytnij obrazek", "crop_picture": "Przytnij obrazek",
@@ -118,7 +124,7 @@
"friend_requests": "Prośby o możliwość obserwacji", "friend_requests": "Prośby o możliwość obserwacji",
"mentions": "Wzmianki", "mentions": "Wzmianki",
"interactions": "Interakcje", "interactions": "Interakcje",
"dms": "Wiadomości prywatne", "dms": "Wiadomości bezpośrednie",
"public_tl": "Publiczna oś czasu", "public_tl": "Publiczna oś czasu",
"timeline": "Oś czasu", "timeline": "Oś czasu",
"twkn": "Znana sieć", "twkn": "Znana sieć",
@@ -128,7 +134,8 @@
"preferences": "Preferencje", "preferences": "Preferencje",
"bookmarks": "Zakładki", "bookmarks": "Zakładki",
"chats": "Czaty", "chats": "Czaty",
"timelines": "Osie czasu" "timelines": "Osie czasu",
"home_timeline": "Główna oś czasu"
}, },
"notifications": { "notifications": {
"broken_favorite": "Nieznany status, szukam go…", "broken_favorite": "Nieznany status, szukam go…",
@@ -156,7 +163,9 @@
"expiry": "Czas trwania ankiety", "expiry": "Czas trwania ankiety",
"expires_in": "Ankieta kończy się za {0}", "expires_in": "Ankieta kończy się za {0}",
"expired": "Ankieta skończyła się {0} temu", "expired": "Ankieta skończyła się {0} temu",
"not_enough_options": "Zbyt mało unikalnych opcji w ankiecie" "not_enough_options": "Zbyt mało unikalnych opcji w ankiecie",
"people_voted_count": "{count} osoba zagłosowała | {count} osoby zagłosowały | {count} osób zagłosowało",
"votes_count": "{count} głos | {count} głosy | {count} głosów"
}, },
"emoji": { "emoji": {
"stickers": "Naklejki", "stickers": "Naklejki",
@@ -197,16 +206,17 @@
"unlisted": "Ten post nie będzie widoczny na publicznej osi czasu i całej znanej sieci" "unlisted": "Ten post nie będzie widoczny na publicznej osi czasu i całej znanej sieci"
}, },
"scope": { "scope": {
"direct": "Bezpośredni Tylko dla wspomnianych użytkowników", "direct": "Bezpośredni tylko dla wspomnianych użytkowników",
"private": "Tylko dla obserwujących Umieść dla osób, które cię obserwują", "private": "Tylko dla obserwujących umieść dla osób, które cię obserwują",
"public": "Publiczny Umieść na publicznych osiach czasu", "public": "Publiczny umieść na publicznych osiach czasu",
"unlisted": "Niewidoczny Nie umieszczaj na publicznych osiach czasu" "unlisted": "Niewidoczny nie umieszczaj na publicznych osiach czasu"
}, },
"preview_empty": "Pusty", "preview_empty": "Pusty",
"preview": "Podgląd", "preview": "Podgląd",
"empty_status_error": "Nie można wysłać pustego wpisu bez plików", "empty_status_error": "Nie można wysłać pustego wpisu bez plików",
"media_description_error": "Nie udało się zaktualizować mediów, spróbuj ponownie", "media_description_error": "Nie udało się zaktualizować mediów, spróbuj ponownie",
"media_description": "Opis mediów" "media_description": "Opis mediów",
"post": "Opublikuj"
}, },
"registration": { "registration": {
"bio": "Bio", "bio": "Bio",
@@ -227,7 +237,10 @@
"password_required": "nie może być puste", "password_required": "nie może być puste",
"password_confirmation_required": "nie może być puste", "password_confirmation_required": "nie może być puste",
"password_confirmation_match": "musi być takie jak hasło" "password_confirmation_match": "musi być takie jak hasło"
} },
"reason": "Powód rejestracji",
"reason_placeholder": "Ta instancja ręcznie zatwierdza rejestracje.\nPoinformuj administratora, dlaczego chcesz się zarejestrować.",
"register": "Zarejestruj się"
}, },
"remote_user_resolver": { "remote_user_resolver": {
"remote_user_resolver": "Wyszukiwarka użytkowników nietutejszych", "remote_user_resolver": "Wyszukiwarka użytkowników nietutejszych",
@@ -281,7 +294,7 @@
"cGreen": "Zielony (powtórzenia)", "cGreen": "Zielony (powtórzenia)",
"cOrange": "Pomarańczowy (ulubione)", "cOrange": "Pomarańczowy (ulubione)",
"cRed": "Czerwony (anuluj)", "cRed": "Czerwony (anuluj)",
"change_email": "Zmień email", "change_email": "Zmień e-mail",
"change_email_error": "Wystąpił problem podczas zmiany emaila.", "change_email_error": "Wystąpił problem podczas zmiany emaila.",
"changed_email": "Pomyślnie zmieniono email!", "changed_email": "Pomyślnie zmieniono email!",
"change_password": "Zmień hasło", "change_password": "Zmień hasło",
@@ -345,7 +358,7 @@
"use_contain_fit": "Nie przycinaj załączników na miniaturach", "use_contain_fit": "Nie przycinaj załączników na miniaturach",
"name": "Imię", "name": "Imię",
"name_bio": "Imię i bio", "name_bio": "Imię i bio",
"new_email": "Nowy email", "new_email": "Nowy e-mail",
"new_password": "Nowe hasło", "new_password": "Nowe hasło",
"notification_visibility": "Rodzaje powiadomień do wyświetlania", "notification_visibility": "Rodzaje powiadomień do wyświetlania",
"notification_visibility_follows": "Obserwacje", "notification_visibility_follows": "Obserwacje",
@@ -361,8 +374,8 @@
"hide_followers_description": "Nie pokazuj kto mnie obserwuje", "hide_followers_description": "Nie pokazuj kto mnie obserwuje",
"hide_follows_count_description": "Nie pokazuj licznika obserwowanych", "hide_follows_count_description": "Nie pokazuj licznika obserwowanych",
"hide_followers_count_description": "Nie pokazuj licznika obserwujących", "hide_followers_count_description": "Nie pokazuj licznika obserwujących",
"show_admin_badge": "Pokazuj odznakę Administrator na moim profilu", "show_admin_badge": "Pokazuj odznakę Administrator na moim profilu",
"show_moderator_badge": "Pokazuj odznakę Moderator na moim profilu", "show_moderator_badge": "Pokazuj odznakę Moderator na moim profilu",
"nsfw_clickthrough": "Włącz domyślne ukrywanie załączników o treści nieprzyzwoitej (NSFW)", "nsfw_clickthrough": "Włącz domyślne ukrywanie załączników o treści nieprzyzwoitej (NSFW)",
"oauth_tokens": "Tokeny OAuth", "oauth_tokens": "Tokeny OAuth",
"token": "Token", "token": "Token",
@@ -600,7 +613,27 @@
"mute_import": "Import wyciszeń", "mute_import": "Import wyciszeń",
"mute_export_button": "Wyeksportuj swoje wyciszenia do pliku .csv", "mute_export_button": "Wyeksportuj swoje wyciszenia do pliku .csv",
"mute_export": "Eksport wyciszeń", "mute_export": "Eksport wyciszeń",
"hide_wallpaper": "Ukryj tło instancji" "hide_wallpaper": "Ukryj tło instancji",
"save": "Zapisz zmiany",
"setting_changed": "Opcja różni się od domyślnej",
"right_sidebar": "Pokaż pasek boczny po prawej",
"file_export_import": {
"errors": {
"invalid_file": "Wybrany plik nie jest obsługiwaną kopią zapasową ustawień Pleromy. Nie dokonano żadnych zmian."
},
"backup_restore": "Kopia zapasowa ustawień",
"backup_settings": "Kopia zapasowa ustawień do pliku",
"backup_settings_theme": "Kopia zapasowa ustawień i motywu do pliku",
"restore_settings": "Przywróć ustawienia z pliku"
},
"more_settings": "Więcej ustawień",
"word_filter": "Filtr słów",
"hide_media_previews": "Ukryj podgląd mediów",
"hide_all_muted_posts": "Ukryj wyciszone słowa",
"reply_visibility_following_short": "Pokazuj odpowiedzi obserwującym",
"reply_visibility_self_short": "Pokazuj odpowiedzi tylko do mnie",
"sensitive_by_default": "Domyślnie oznaczaj wpisy jako wrażliwe",
"hide_shoutbox": "Ukryj shoutbox instancji"
}, },
"time": { "time": {
"day": "{0} dzień", "day": "{0} dzień",
@@ -648,7 +681,9 @@
"no_more_statuses": "Brak kolejnych statusów", "no_more_statuses": "Brak kolejnych statusów",
"no_statuses": "Brak statusów", "no_statuses": "Brak statusów",
"reload": "Odśwież", "reload": "Odśwież",
"error": "Błąd pobierania osi czasu: {0}" "error": "Błąd pobierania osi czasu: {0}",
"socket_broke": "Utracono połączenie w czasie rzeczywistym: kod CloseEvent {0}",
"socket_reconnected": "Osiągnięto połączenie w czasie rzeczywistym"
}, },
"status": { "status": {
"favorites": "Ulubione", "favorites": "Ulubione",
@@ -686,7 +721,6 @@
"follow": "Obserwuj", "follow": "Obserwuj",
"follow_sent": "Wysłano prośbę!", "follow_sent": "Wysłano prośbę!",
"follow_progress": "Wysyłam prośbę…", "follow_progress": "Wysyłam prośbę…",
"follow_again": "Wysłać prośbę ponownie?",
"follow_unfollow": "Przestań obserwować", "follow_unfollow": "Przestań obserwować",
"followees": "Obserwowani", "followees": "Obserwowani",
"followers": "Obserwujący", "followers": "Obserwujący",
@@ -731,7 +765,12 @@
"delete_user": "Usuń użytkownika", "delete_user": "Usuń użytkownika",
"delete_user_confirmation": "Czy jesteś absolutnie pewny(-a)? Ta operacja nie może być cofnięta." "delete_user_confirmation": "Czy jesteś absolutnie pewny(-a)? Ta operacja nie może być cofnięta."
}, },
"message": "Napisz" "message": "Napisz",
"edit_profile": "Edytuj profil",
"highlight": {
"disabled": "Bez wyróżnienia"
},
"bot": "Bot"
}, },
"user_profile": { "user_profile": {
"timeline_title": "Oś czasu użytkownika", "timeline_title": "Oś czasu użytkownika",
-1
View File
@@ -575,7 +575,6 @@
"follow": "Seguir", "follow": "Seguir",
"follow_sent": "Pedido enviado!", "follow_sent": "Pedido enviado!",
"follow_progress": "Enviando…", "follow_progress": "Enviando…",
"follow_again": "Enviar solicitação novamente?",
"follow_unfollow": "Deixar de seguir", "follow_unfollow": "Deixar de seguir",
"followees": "Seguindo", "followees": "Seguindo",
"followers": "Seguidores", "followers": "Seguidores",
-1
View File
@@ -550,7 +550,6 @@
"follow": "Читать", "follow": "Читать",
"follow_sent": "Запрос отправлен!", "follow_sent": "Запрос отправлен!",
"follow_progress": "Запрашиваем…", "follow_progress": "Запрашиваем…",
"follow_again": "Запросить еще раз?",
"follow_unfollow": "Перестать читать", "follow_unfollow": "Перестать читать",
"followees": "Читаемые", "followees": "Читаемые",
"followers": "Читатели", "followers": "Читатели",
-1
View File
@@ -310,7 +310,6 @@
"user_card.follow": "Follow", "user_card.follow": "Follow",
"user_card.follow_sent": "Request sent!", "user_card.follow_sent": "Request sent!",
"user_card.follow_progress": "Requesting…", "user_card.follow_progress": "Requesting…",
"user_card.follow_again": "Send request again?",
"user_card.follow_unfollow": "Unfollow", "user_card.follow_unfollow": "Unfollow",
"user_card.followees": "Following", "user_card.followees": "Following",
"user_card.followers": "Followers", "user_card.followers": "Followers",
+9 -4
View File
@@ -21,7 +21,10 @@
"role": { "role": {
"moderator": "Модератор", "moderator": "Модератор",
"admin": "Адміністратор" "admin": "Адміністратор"
} },
"flash_content": "Натисніть для перегляду змісту Flash за допомогою Ruffle (експериментально, може не працювати).",
"flash_security": "Ця функція може становити ризик, оскільки Flash-вміст все ще є потенційно небезпечним.",
"flash_fail": "Не вдалося завантажити Flash-вміст, докладнішу інформацію дивись у консолі."
}, },
"finder": { "finder": {
"error_fetching_user": "Користувача не знайдено", "error_fetching_user": "Користувача не знайдено",
@@ -633,7 +636,9 @@
"backup_settings_theme": "Резервне копіювання налаштувань та теми у файл", "backup_settings_theme": "Резервне копіювання налаштувань та теми у файл",
"backup_settings": "Резервне копіювання налаштувань у файл", "backup_settings": "Резервне копіювання налаштувань у файл",
"backup_restore": "Резервне копіювання налаштувань" "backup_restore": "Резервне копіювання налаштувань"
} },
"right_sidebar": "Показувати бокову панель справа",
"hide_shoutbox": "Приховати оголошення інстансу"
}, },
"selectable_list": { "selectable_list": {
"select_all": "Вибрати все" "select_all": "Вибрати все"
@@ -743,7 +748,6 @@
"message": "Повідомлення", "message": "Повідомлення",
"follow": "Підписатись", "follow": "Підписатись",
"follow_unfollow": "Відписатись", "follow_unfollow": "Відписатись",
"follow_again": "Відправити запит знову?",
"follow_sent": "Запит відправлено!", "follow_sent": "Запит відправлено!",
"blocked": "Заблоковано!", "blocked": "Заблоковано!",
"admin_menu": { "admin_menu": {
@@ -799,7 +803,8 @@
"solid": "Суцільний фон", "solid": "Суцільний фон",
"disabled": "Не виділяти" "disabled": "Не виділяти"
}, },
"bot": "Бот" "bot": "Бот",
"edit_profile": "Редагувати профіль"
}, },
"status": { "status": {
"copy_link": "Скопіювати посилання на допис", "copy_link": "Скопіювати посилання на допис",
+435
View File
@@ -0,0 +1,435 @@
{
"about": {
"mrf": {
"federation": "Liên hợp",
"keyword": {
"keyword_policies": "Chính sách quan trọng",
"reject": "Từ chối",
"replace": "Thay thế",
"is_replaced_by": "→",
"ftl_removal": "Giới hạn chung"
},
"mrf_policies": "Kích hoạt chính sách MRF",
"simple": {
"simple_policies": "Quy tắc máy chủ",
"accept": "Đồng ý",
"accept_desc": "Máy chủ này chỉ chấp nhận tin nhắn từ những máy chủ:",
"reject": "Từ chối",
"quarantine": "Bảo hành",
"quarantine_desc": "Máy chủ này sẽ gửi tút công khai đến những máy chủ:",
"ftl_removal": "Giới hạn chung",
"media_removal": "Ẩn Media",
"media_removal_desc": "Media từ những máy chủ sau sẽ bị ẩn:",
"media_nsfw": "Áp đặt nhạy cảm",
"media_nsfw_desc": "Nội dung từ những máy chủ sau sẽ bị tự động gắn nhãn nhạy cảm:",
"reject_desc": "Máy chủ này không chấp nhận tin nhắn từ những máy chủ:",
"ftl_removal_desc": "Nội dung từ những máy chủ sau sẽ bị ẩn:"
},
"mrf_policies_desc": "Các chính sách MRF kiểm soát sự liên hợp của máy chủ. Các chính sách sau được bật:"
},
"staff": "Nhân viên"
},
"domain_mute_card": {
"mute": "Ẩn",
"mute_progress": "Đang ẩn…",
"unmute": "Ngưng ẩn",
"unmute_progress": "Đang ngưng ẩn…"
},
"exporter": {
"export": "Xuất dữ liệu",
"processing": "Đang chuẩn bị tập tin cho bạn tải về"
},
"features_panel": {
"chat": "Chat",
"pleroma_chat_messages": "Pleroma Chat",
"gopher": "Gopher",
"media_proxy": "Proxy media",
"text_limit": "Giới hạn ký tự",
"title": "Tính năng",
"who_to_follow": "Đề xuất theo dõi",
"upload_limit": "Giới hạn tải lên",
"scope_options": "Đa dạng kiểu đăng"
},
"finder": {
"error_fetching_user": "Lỗi người dùng",
"find_user": "Tìm người dùng"
},
"shoutbox": {
"title": "Chat cùng nhau"
},
"general": {
"apply": "Áp dụng",
"submit": "Gửi tặng",
"more": "Nhiều hơn",
"loading": "Đang tải…",
"generic_error": "Đã có lỗi xảy ra",
"error_retry": "Xin hãy thử lại",
"retry": "Thử lại",
"optional": "tùy chọn",
"show_more": "Xem thêm",
"show_less": "Thu gọn",
"dismiss": "Bỏ qua",
"cancel": "Hủy bỏ",
"disable": "Tắt",
"enable": "Bật",
"confirm": "Xác nhận",
"verify": "Xác thực",
"close": "Đóng",
"peek": "Thu gọn",
"role": {
"admin": "Quản trị viên",
"moderator": "Kiểm duyệt viên"
},
"flash_security": "Lưu ý rằng điều này có thể tiềm ẩn nguy hiểm vì nội dung Flash là mã lập trình tùy ý.",
"flash_fail": "Tải nội dung Flash thất bại, tham khảo chi tiết trong console.",
"flash_content": "Nhấn để hiện nội dung Flash bằng Ruffle (Thử nghiệm, có thể không dùng được)."
},
"image_cropper": {
"crop_picture": "Cắt hình ảnh",
"save": "Lưu",
"save_without_cropping": "Bỏ qua cắt",
"cancel": "Hủy bỏ"
},
"importer": {
"submit": "Gửi đi",
"success": "Đã nhập dữ liệu thành công.",
"error": "Có lỗi xảy ra khi nhập dữ liệu từ tập tin này."
},
"login": {
"login": "Đăng nhập",
"description": "Đăng nhập bằng OAuth",
"logout": "Đăng xuất",
"password": "Mật khẩu",
"placeholder": "vd: cobetronxinh",
"register": "Đăng ký",
"username": "Tên người dùng",
"hint": "Đăng nhập để cùng trò chuyện",
"authentication_code": "Mã truy cập",
"enter_recovery_code": "Nhập mã khôi phục",
"recovery_code": "Mã khôi phục",
"heading": {
"totp": "Xác thực hai bước",
"recovery": "Khôi phục hai bước"
},
"enter_two_factor_code": "Nhập mã xác thực hai bước"
},
"media_modal": {
"previous": "Trước đó",
"next": "Kế tiếp"
},
"nav": {
"about": "Về máy chủ này",
"administration": "Vận hành bởi",
"back": "Quay lại",
"friend_requests": "Yêu cầu theo dõi",
"mentions": "Lượt nhắc đến",
"interactions": "Giao tiếp",
"dms": "Nhắn tin",
"public_tl": "Bảng tin máy chủ",
"timeline": "Bảng tin",
"home_timeline": "Bảng tin của bạn",
"twkn": "Thế giới",
"bookmarks": "Đã lưu",
"user_search": "Tìm kiếm người dùng",
"search": "Tìm kiếm",
"who_to_follow": "Đề xuất theo dõi",
"preferences": "Thiết lập",
"timelines": "Bảng tin",
"chats": "Chat"
},
"notifications": {
"broken_favorite": "Trạng thái chưa rõ, đang tìm kiếm…",
"favorited_you": "thích tút của bạn",
"followed_you": "theo dõi bạn",
"follow_request": "yêu cầu theo dõi bạn",
"load_older": "Xem những thông báo cũ hơn",
"notifications": "Thông báo",
"read": "Đọc!",
"repeated_you": "chia sẻ tút của bạn",
"no_more_notifications": "Không còn thông báo nào",
"migrated_to": "chuyển sang",
"reacted_with": "chạm tới {0}",
"error": "Lỗi xử lý thông báo: {0}"
},
"polls": {
"add_poll": "Tạo bình chọn",
"option": "Lựa chọn",
"votes": "người bình chọn",
"people_voted_count": "{count} người bình chọn | {count} người bình chọn",
"vote": "Bình chọn",
"type": "Kiểu bình chọn",
"single_choice": "Chỉ được chọn một lựa chọn",
"multiple_choices": "Cho phép chọn nhiều lựa chọn",
"expiry": "Thời hạn bình chọn",
"expires_in": "Bình chọn kết thúc sau {0}",
"not_enough_options": "Không đủ lựa chọn tối thiểu",
"add_option": "Thêm lựa chọn",
"votes_count": "{count} bình chọn | {count} bình chọn",
"expired": "Bình chọn đã kết thúc {0} trước"
},
"emoji": {
"stickers": "Sticker",
"emoji": "Emoji",
"keep_open": "Mở khung lựa chọn",
"search_emoji": "Tìm emoji",
"add_emoji": "Nhập emoji",
"custom": "Tùy chỉnh emoji",
"unicode": "Unicode emoji",
"load_all_hint": "Tải trước {saneAmount} emoji, tải toàn bộ emoji có thể gây xử lí chậm.",
"load_all": "Đang tải {emojiAmount} emoji"
},
"interactions": {
"favs_repeats": "Tương tác",
"follows": "Lượt theo dõi mới",
"moves": "Người dùng chuyển đi",
"load_older": "Xem tương tác cũ hơn"
},
"post_status": {
"new_status": "Đăng tút",
"account_not_locked_warning": "Tài khoản của bạn chưa {0}. Bất kỳ ai cũng có thể xem những tút dành cho người theo dõi của bạn.",
"account_not_locked_warning_link": "đã khóa",
"attachments_sensitive": "Đánh dấu media là nhạy cảm",
"media_description": "Mô tả media",
"content_type": {
"text/plain": "Văn bản",
"text/html": "HTML",
"text/markdown": "Markdown",
"text/bbcode": "BBCode"
},
"content_warning": "Tiêu đề (tùy chọn)",
"default": "Just landed in L.A.",
"direct_warning_to_first_only": "Người đầu tiên được nhắc đến mới có thể thấy tút này.",
"posting": "Đang đăng tút",
"post": "Đăng",
"preview": "Xem trước",
"preview_empty": "Trống",
"empty_status_error": "Không thể đăng một tút trống và không có media",
"media_description_error": "Cập nhật media thất bại, thử lại sau",
"scope_notice": {
"private": "Chỉ những người theo dõi bạn mới thấy tút này",
"unlisted": "Tút này sẽ không hiện trong bảng tin máy chủ và thế giới",
"public": "Mọi người đều có thể thấy tút này"
},
"scope": {
"public": "Công khai - hiện trên bảng tin máy chủ",
"private": "Riêng tư - Chỉ dành cho người theo dõi",
"unlisted": "Hạn chế - không hiện trên bảng tin",
"direct": "Tin nhắn - chỉ người được nhắc đến mới thấy"
},
"direct_warning_to_all": "Những ai được nhắc đến sẽ đều thấy tút này."
},
"registration": {
"bio": "Tiểu sử",
"email": "Email",
"fullname": "Tên hiển thị",
"password_confirm": "Xác nhận mật khẩu",
"registration": "Đăng ký",
"token": "Lời mời",
"captcha": "CAPTCHA",
"new_captcha": "Nhấn vào hình ảnh để đổi captcha mới",
"username_placeholder": "vd: cobetronxinh",
"fullname_placeholder": "vd: Cô Bé Tròn Xinh",
"bio_placeholder": "vd:\nHi, I'm Cô Bé Tròn Xinh.\nIm an anime girl living in suburban Vietnam. You may know me from the school.",
"reason": "Lý do đăng ký",
"reason_placeholder": "Máy chủ này phê duyệt đăng ký thủ công.\nHãy cho quản trị viên biết lý do bạn muốn đăng ký.",
"register": "Đăng ký",
"validations": {
"username_required": "không được để trống",
"fullname_required": "không được để trống",
"email_required": "không được để trống",
"password_confirmation_required": "không được để trống",
"password_confirmation_match": "phải trùng khớp với mật khẩu",
"password_required": "không được để trống"
}
},
"remote_user_resolver": {
"remote_user_resolver": "Giải quyết người dùng từ xa",
"searching_for": "Tìm kiếm",
"error": "Không tìm thấy."
},
"selectable_list": {
"select_all": "Chọn tất cả"
},
"settings": {
"app_name": "Tên app",
"save": "Lưu thay đổi",
"security": "Bảo mật",
"enter_current_password_to_confirm": "Nhập mật khẩu để xác thực",
"mfa": {
"otp": "OTP",
"setup_otp": "Thiết lập OTP",
"wait_pre_setup_otp": "hậu thiết lập OTP",
"confirm_and_enable": "Xác nhận và kích hoạt OTP",
"title": "Xác thực hai bước",
"recovery_codes": "Những mã khôi phục.",
"waiting_a_recovery_codes": "Đang nhận mã khôi phục…",
"authentication_methods": "Phương pháp xác thực",
"scan": {
"title": "Quét",
"desc": "Sử dụng app xác thực hai bước để quét mã QR hoặc nhập mã khôi phục:",
"secret_code": "Mã"
},
"verify": {
"desc": "Để bật xác thực hai bước, nhập mã từ app của bạn:"
},
"generate_new_recovery_codes": "Tạo mã khôi phục mới",
"warning_of_generate_new_codes": "Khi tạo mã khôi phục mới, những mã khôi phục cũ sẽ không sử dụng được nữa.",
"recovery_codes_warning": "Hãy viết lại mã và cất ở một nơi an toàn - những mã này sẽ không xuất hiện lại nữa. Nếu mất quyền sử dụng app 2FA app và mã khôi phục, tài khoản của bạn sẽ không thể truy cập."
},
"allow_following_move": "Cho phép tự động theo dõi lại khi tài khoản đang theo dõi chuyển sang máy chủ khác",
"attachmentRadius": "Tập tin tải lên",
"attachments": "Tập tin tải lên",
"avatar": "Ảnh đại diện",
"avatarAltRadius": "Ảnh đại diện (thông báo)",
"avatarRadius": "Ảnh đại diện",
"background": "Ảnh nền",
"bio": "Tiểu sử",
"block_export": "Xuất danh sách chặn",
"block_import": "Nhập danh sách chặn",
"block_import_error": "Lỗi khi nhập danh sách chặn",
"mute_export": "Xuất danh sách ẩn",
"mute_export_button": "Xuất danh sách ẩn ra tập tin CSV",
"mute_import": "Nhập danh sách ẩn",
"mute_import_error": "Lỗi khi nhập danh sách ẩn",
"mutes_imported": "Đã nhập danh sách ẩn! Sẽ mất một lúc nữa để hoàn thành.",
"import_mutes_from_a_csv_file": "Nhập danh sách ẩn từ tập tin CSV",
"blocks_tab": "Danh sách chặn",
"bot": "Đây là tài khoản Bot",
"btnRadius": "Nút",
"cBlue": "Xanh (Trả lời, theo dõi)",
"cOrange": "Cam (Thích)",
"cRed": "Đỏ (Hủy bỏ)",
"change_email": "Đổi email",
"change_email_error": "Có lỗi xảy ra khi đổi email.",
"changed_email": "Đã đổi email thành công!",
"change_password": "Đổi mật khẩu",
"changed_password": "Đổi mật khẩu thành công!",
"chatMessageRadius": "Tin nhắn chat",
"follows_imported": "Đã nhập danh sách theo dõi! Sẽ mất một lúc nữa để hoàn thành.",
"collapse_subject": "Thu gọn những tút có tựa đề",
"composing": "Thu gọn",
"current_password": "Mật khẩu cũ",
"mutes_and_blocks": "Ẩn và Chặn",
"data_import_export_tab": "Nhập / Xuất dữ liệu",
"default_vis": "Kiểu đăng tút mặc định",
"delete_account": "Xóa tài khoản",
"delete_account_error": "Có lỗi khi xóa tài khoản. Xin liên hệ quản trị viên máy chủ để tìm hiểu.",
"delete_account_instructions": "Nhập mật khẩu bên dưới để xác nhận.",
"domain_mutes": "Máy chủ",
"avatar_size_instruction": "Kích cỡ tối thiểu 150x150 pixels.",
"pad_emoji": "Nhớ chừa khoảng cách khi chèn emoji",
"emoji_reactions_on_timeline": "Hiện tương tác emoji trên bảng tin",
"export_theme": "Lưu mẫu",
"filtering": "Bộ lọc",
"filtering_explanation": "Những tút chứa từ sau sẽ bị ẩn, mỗi chữ một hàng",
"word_filter": "Bộ lọc từ ngữ",
"follow_export": "Xuất danh sách theo dõi",
"follow_import": "Nhập danh sách theo dõi",
"follow_import_error": "Lỗi khi nhập danh sách theo dõi",
"accent": "Màu chủ đạo",
"foreground": "Màu phối",
"general": "Chung",
"hide_attachments_in_convo": "Ẩn tập tin đính kèm trong thảo luận",
"hide_media_previews": "Ẩn xem trước media",
"hide_all_muted_posts": "Ẩn những tút đã ẩn",
"hide_muted_posts": "Ẩn tút từ các người dùng đã ẩn",
"max_thumbnails": "Số ảnh xem trước tối đa cho mỗi tút",
"hide_isp": "Ẩn thanh bên của máy chủ",
"hide_shoutbox": "Ẩn thanh chat máy chủ",
"hide_wallpaper": "Ẩn ảnh nền máy chủ",
"preload_images": "Tải trước hình ảnh",
"use_one_click_nsfw": "Xem nội dung nhạy cảm bằng cách nhấn vào",
"hide_user_stats": "Ẩn số liệu người dùng (vd: số người theo dõi)",
"hide_filtered_statuses": "Ẩn những tút đã lọc",
"import_followers_from_a_csv_file": "Nhập danh sách theo dõi từ tập tin CSV",
"import_theme": "Tải mẫu có sẵn",
"inputRadius": "Chỗ nhập vào",
"checkboxRadius": "Hộp kiểm",
"instance_default": "(mặc định: {value})",
"instance_default_simple": "(mặc định)",
"interface": "Giao diện",
"interfaceLanguage": "Ngôn ngữ",
"limited_availability": "Trình duyệt không hỗ trợ",
"links": "Liên kết",
"lock_account_description": "Tự phê duyệt yêu cầu theo dõi",
"loop_video": "Lặp lại video",
"loop_video_silent_only": "Chỉ lặp lại những video không có âm thanh",
"mutes_tab": "Ẩn",
"play_videos_in_modal": "Phát video trong khung hình riêng",
"file_export_import": {
"backup_restore": "Sao lưu",
"backup_settings": "Thiết lập sao lưu",
"restore_settings": "Khôi phục thiết lập từ tập tin",
"errors": {
"invalid_file": "Tập tin đã chọn không hỗ trợ bởi Pleroma. Giữ nguyên mọi thay đổi.",
"file_too_old": "Phiên bản không tương thích: {fileMajor}, phiên bản tập tin quá cũ và không được hỗ trợ (min. set. ver. {feMajor})",
"file_slightly_new": "Phiên bản tập tin khác biệt, không thể áp dụng một vài thay đổi",
"file_too_new": "Phiên bản không tương thích: {fileMajor}, phiên bản PleromaFE(settings ver {feMajor}) của máy chủ này quá cũ để sử dụng"
},
"backup_settings_theme": "Thiết lập sao lưu dữ liệu và giao diện"
},
"profile_fields": {
"label": "Metadata",
"add_field": "Thêm mục",
"name": "Nhãn",
"value": "Nội dung"
},
"use_contain_fit": "Không cắt ảnh đính kèm trong bản xem trước",
"name": "Tên",
"name_bio": "Tên & tiểu sử",
"new_email": "Email mới",
"new_password": "Mật khẩu mới",
"notification_visibility_follows": "Theo dõi",
"notification_visibility_mentions": "Lượt nhắc",
"notification_visibility_repeats": "Chia sẻ",
"notification_visibility_moves": "Chuyển máy chủ",
"notification_visibility_emoji_reactions": "Tương tác",
"no_blocks": "Không có chặn",
"no_mutes": "Không có ẩn",
"hide_follows_description": "Ẩn danh sách những người tôi theo dõi",
"hide_followers_description": "Ẩn danh sách những người theo dõi tôi",
"hide_followers_count_description": "Ẩn số lượng người theo dõi tôi",
"show_admin_badge": "Hiện huy hiệu \"Quản trị viên\" trên trang của tôi",
"show_moderator_badge": "Hiện huy hiệu \"Kiểm duyệt viên\" trên trang của tôi",
"oauth_tokens": "OAuth tokens",
"token": "Token",
"refresh_token": "Làm tươi token",
"valid_until": "Có giá trị tới",
"revoke_token": "Gỡ",
"panelRadius": "Panels",
"pause_on_unfocused": "Dừng phát khi đang lướt các tút khác",
"presets": "Mẫu có sẵn",
"profile_background": "Ảnh nền trang cá nhân",
"profile_banner": "Ảnh bìa trang cá nhân",
"profile_tab": "Trang cá nhân",
"radii_help": "Thiết lập góc bo tròn (bằng pixels)",
"replies_in_timeline": "Trả lời trong bảng tin",
"reply_visibility_all": "Hiện toàn bộ trả lời",
"reply_visibility_self": "Chỉ hiện những trả lời có nhắc tới tôi",
"reply_visibility_following_short": "Hiện trả lời có những người tôi theo dõi",
"reply_visibility_self_short": "Hiện trả lời của bản thân",
"setting_changed": "Thiết lập khác với mặc định",
"block_export_button": "Xuất danh sách chặn ra tập tin CSV",
"blocks_imported": "Đã nhập danh sách chặn! Sẽ mất một lúc nữa để hoàn thành.",
"cGreen": "Green (Chia sẻ)",
"change_password_error": "Có lỗi xảy ra khi đổi mật khẩu.",
"confirm_new_password": "Xác nhận mật khẩu mới",
"delete_account_description": "Xóa vĩnh viễn mọi dữ liệu và vô hiệu hóa tài khoản của bạn.",
"discoverable": "Hiện tài khoản trong công cụ tìm kiếm và những tính năng khác",
"follow_export_button": "Xuất danh sách theo dõi ra tập tin CSV",
"hide_attachments_in_tl": "Ẩn tập tin đính kèm trong bảng tin",
"right_sidebar": "Hiện thanh bên bên phải",
"hide_post_stats": "Ẩn tương tác của tút (vd: số lượt thích)",
"import_blocks_from_a_csv_file": "Nhập danh sách chặn từ tập tin CSV",
"invalid_theme_imported": "Tập tin đã chọn không hỗ trợ bởi Pleroma. Giao diện của bạn sẽ giữ nguyên.",
"notification_visibility": "Những loại thông báo sẽ hiện",
"notification_visibility_likes": "Thích",
"no_rich_text_description": "Không hiện rich text trong các tút",
"hide_follows_count_description": "Ẩn số lượng người tôi theo dõi",
"nsfw_clickthrough": "Cho phép nhấn vào xem các tút nhạy cảm",
"reply_visibility_following": "Chỉ hiện những trả lời có nhắc tới tôi hoặc từ những người mà tôi theo dõi"
},
"errors": {
"storage_unavailable": "Pleroma không thể truy cập lưu trữ trình duyệt. Thông tin đăng nhập và những thiết lập tạm thời sẽ bị mất. Hãy cho phép cookies."
}
}
+9 -4
View File
@@ -43,7 +43,10 @@
"role": { "role": {
"moderator": "监察员", "moderator": "监察员",
"admin": "管理员" "admin": "管理员"
} },
"flash_content": "点击以使用 Ruffle 显示 Flash 内容(实验性,可能无效)。",
"flash_security": "注意这可能有潜在的危险,因为 Flash 内容仍然是任意的代码。",
"flash_fail": "Flash 内容加载失败,请在控制台查看详情。"
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "裁剪图片", "crop_picture": "裁剪图片",
@@ -584,7 +587,9 @@
"backup_settings_theme": "备份设置和主题到文件", "backup_settings_theme": "备份设置和主题到文件",
"backup_settings": "备份设置到文件", "backup_settings": "备份设置到文件",
"backup_restore": "设置备份" "backup_restore": "设置备份"
} },
"right_sidebar": "在右侧显示侧边栏",
"hide_shoutbox": "隐藏实例留言板"
}, },
"time": { "time": {
"day": "{0} 天", "day": "{0} 天",
@@ -672,7 +677,6 @@
"follow": "关注", "follow": "关注",
"follow_sent": "请求已发送!", "follow_sent": "请求已发送!",
"follow_progress": "请求中…", "follow_progress": "请求中…",
"follow_again": "再次发送请求?",
"follow_unfollow": "取消关注", "follow_unfollow": "取消关注",
"followees": "正在关注", "followees": "正在关注",
"followers": "关注者", "followers": "关注者",
@@ -724,7 +728,8 @@
"striped": "条纹背景", "striped": "条纹背景",
"solid": "单一颜色背景", "solid": "单一颜色背景",
"disabled": "不突出显示" "disabled": "不突出显示"
} },
"edit_profile": "编辑个人资料"
}, },
"user_profile": { "user_profile": {
"timeline_title": "用户时间线", "timeline_title": "用户时间线",
+9 -4
View File
@@ -115,7 +115,10 @@
"role": { "role": {
"moderator": "主持人", "moderator": "主持人",
"admin": "管理員" "admin": "管理員"
} },
"flash_content": "點擊以使用 Ruffle 顯示 Flash 內容(實驗性,可能無效)。",
"flash_security": "請注意,這可能有潜在的危險,因為Flash內容仍然是武斷的程式碼。",
"flash_fail": "無法加載flash內容,請參閱控制台瞭解詳細資訊。"
}, },
"finder": { "finder": {
"find_user": "尋找用戶", "find_user": "尋找用戶",
@@ -556,7 +559,9 @@
"backup_settings": "備份設置到文件", "backup_settings": "備份設置到文件",
"backup_restore": "設定備份" "backup_restore": "設定備份"
}, },
"sensitive_by_default": "默認標記發文為敏感內容" "sensitive_by_default": "默認標記發文為敏感內容",
"right_sidebar": "在右側顯示側邊欄",
"hide_shoutbox": "隱藏實例留言框"
}, },
"chats": { "chats": {
"more": "更多", "more": "更多",
@@ -766,7 +771,6 @@
"follow": "關注", "follow": "關注",
"follow_sent": "請求已發送!", "follow_sent": "請求已發送!",
"follow_progress": "請求中…", "follow_progress": "請求中…",
"follow_again": "再次發送請求?",
"follow_unfollow": "取消關注", "follow_unfollow": "取消關注",
"followees": "正在關注", "followees": "正在關注",
"followers": "關注者", "followers": "關注者",
@@ -797,7 +801,8 @@
"striped": "條紋背景", "striped": "條紋背景",
"side": "彩條" "side": "彩條"
}, },
"bot": "機器人" "bot": "機器人",
"edit_profile": "編輯個人資料"
}, },
"user_profile": { "user_profile": {
"timeline_title": "用戶時間線", "timeline_title": "用戶時間線",
+1
View File
@@ -35,6 +35,7 @@ export const defaultState = {
loopVideoSilentOnly: true, loopVideoSilentOnly: true,
streaming: false, streaming: false,
emojiReactionsOnTimeline: true, emojiReactionsOnTimeline: true,
alwaysShowNewPostButton: false,
autohideFloatingPostButton: false, autohideFloatingPostButton: false,
pauseOnUnfocused: true, pauseOnUnfocused: true,
stopGifs: false, stopGifs: false,
+5
View File
@@ -246,6 +246,11 @@ export const getters = {
} }
return result return result
}, },
findUserByUrl: state => query => {
return state.users
.find(u => u.statusnet_profile_url &&
u.statusnet_profile_url.toLowerCase() === query.toLowerCase())
},
relationship: state => id => { relationship: state => id => {
const rel = id && state.relationships[id] const rel = id && state.relationships[id]
return rel || { id, loading: true } return rel || { id, loading: true }
@@ -54,17 +54,20 @@ export const parseUser = (data) => {
return output return output
} }
output.name = data.display_name output.emoji = data.emojis
output.name_html = addEmojis(escape(data.display_name), data.emojis) output.name = escape(data.display_name)
output.name_html = output.name
output.name_unescaped = data.display_name
output.description = data.note output.description = data.note
output.description_html = addEmojis(data.note, data.emojis) // TODO cleanup this shit, output.description is overriden with source data
output.description_html = data.note
output.fields = data.fields output.fields = data.fields
output.fields_html = data.fields.map(field => { output.fields_html = data.fields.map(field => {
return { return {
name: addEmojis(escape(field.name), data.emojis), name: escape(field.name),
value: addEmojis(field.value, data.emojis) value: field.value
} }
}) })
output.fields_text = data.fields.map(field => { output.fields_text = data.fields.map(field => {
@@ -239,16 +242,6 @@ export const parseAttachment = (data) => {
return output return output
} }
export const addEmojis = (string, emojis) => {
const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g
return emojis.reduce((acc, emoji) => {
const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&')
return acc.replace(
new RegExp(`:${regexSafeShortCode}:`, 'g'),
`<img src='${emoji.url}' alt=':${emoji.shortcode}:' title=':${emoji.shortcode}:' class='emoji' />`
)
}, string)
}
export const parseStatus = (data) => { export const parseStatus = (data) => {
const output = {} const output = {}
@@ -266,7 +259,8 @@ export const parseStatus = (data) => {
output.type = data.reblog ? 'retweet' : 'status' output.type = data.reblog ? 'retweet' : 'status'
output.nsfw = data.sensitive output.nsfw = data.sensitive
output.statusnet_html = addEmojis(data.content, data.emojis) output.raw_html = data.content
output.emojis = data.emojis
output.tags = data.tags output.tags = data.tags
@@ -293,13 +287,13 @@ export const parseStatus = (data) => {
output.retweeted_status = parseStatus(data.reblog) output.retweeted_status = parseStatus(data.reblog)
} }
output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis) output.summary_raw_html = escape(data.spoiler_text)
output.external_url = data.url output.external_url = data.url
output.poll = data.poll output.poll = data.poll
if (output.poll) { if (output.poll) {
output.poll.options = (output.poll.options || []).map(field => ({ output.poll.options = (output.poll.options || []).map(field => ({
...field, ...field,
title_html: addEmojis(escape(field.title), data.emojis) title_html: escape(field.title)
})) }))
} }
output.pinned = data.pinned output.pinned = data.pinned
@@ -325,7 +319,7 @@ export const parseStatus = (data) => {
output.nsfw = data.nsfw output.nsfw = data.nsfw
} }
output.statusnet_html = data.statusnet_html output.raw_html = data.statusnet_html
output.text = data.text output.text = data.text
output.in_reply_to_status_id = data.in_reply_to_status_id output.in_reply_to_status_id = data.in_reply_to_status_id
@@ -444,11 +438,8 @@ export const parseChatMessage = (message) => {
output.id = message.id output.id = message.id
output.created_at = new Date(message.created_at) output.created_at = new Date(message.created_at)
output.chat_id = message.chat_id output.chat_id = message.chat_id
if (message.content) { output.emojis = message.emojis
output.content = addEmojis(message.content, message.emojis) output.content = message.content
} else {
output.content = ''
}
if (message.attachment) { if (message.attachment) {
output.attachments = [parseAttachment(message.attachment)] output.attachments = [parseAttachment(message.attachment)]
} else { } else {
+37 -31
View File
@@ -1,52 +1,58 @@
import { find } from 'lodash'
const createFaviconService = () => { const createFaviconService = () => {
let favimg, favcanvas, favcontext, favicon const favicons = []
const faviconWidth = 128 const faviconWidth = 128
const faviconHeight = 128 const faviconHeight = 128
const badgeRadius = 32 const badgeRadius = 32
const initFaviconService = () => { const initFaviconService = () => {
const nodes = document.getElementsByTagName('link') const nodes = document.querySelectorAll('link[rel="icon"]')
favicon = find(nodes, node => node.rel === 'icon') nodes.forEach(favicon => {
if (favicon) { if (favicon) {
favcanvas = document.createElement('canvas') const favcanvas = document.createElement('canvas')
favcanvas.width = faviconWidth favcanvas.width = faviconWidth
favcanvas.height = faviconHeight favcanvas.height = faviconHeight
favimg = new Image() const favimg = new Image()
favimg.src = favicon.href favimg.crossOrigin = 'anonymous'
favcontext = favcanvas.getContext('2d') favimg.src = favicon.href
} const favcontext = favcanvas.getContext('2d')
favicons.push({ favcanvas, favimg, favcontext, favicon })
}
})
} }
const isImageLoaded = (img) => img.complete && img.naturalHeight !== 0 const isImageLoaded = (img) => img.complete && img.naturalHeight !== 0
const clearFaviconBadge = () => { const clearFaviconBadge = () => {
if (!favimg || !favcontext || !favicon) return if (favicons.length === 0) return
favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => {
if (!favimg || !favcontext || !favicon) return
favcontext.clearRect(0, 0, faviconWidth, faviconHeight) favcontext.clearRect(0, 0, faviconWidth, faviconHeight)
if (isImageLoaded(favimg)) { if (isImageLoaded(favimg)) {
favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight) favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
} }
favicon.href = favcanvas.toDataURL('image/png') favicon.href = favcanvas.toDataURL('image/png')
})
} }
const drawFaviconBadge = () => { const drawFaviconBadge = () => {
if (!favimg || !favcontext || !favcontext) return if (favicons.length === 0) return
clearFaviconBadge() clearFaviconBadge()
favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => {
if (!favimg || !favcontext || !favcontext) return
const style = getComputedStyle(document.body) const style = getComputedStyle(document.body)
const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}` const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}`
if (isImageLoaded(favimg)) { if (isImageLoaded(favimg)) {
favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight) favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
} }
favcontext.fillStyle = badgeColor favcontext.fillStyle = badgeColor
favcontext.beginPath() favcontext.beginPath()
favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false) favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false)
favcontext.fill() favcontext.fill()
favicon.href = favcanvas.toDataURL('image/png') favicon.href = favcanvas.toDataURL('image/png')
})
} }
return { return {
@@ -0,0 +1,136 @@
import { getTagName } from './utility.service.js'
/**
* This is a tiny purpose-built HTML parser/processor. This basically detects
* any type of visual newline and converts entire HTML into a array structure.
*
* Text nodes are represented as object with single property - text - containing
* the visual line. Intended usage is to process the array with .map() in which
* map function returns a string and resulting array can be converted back to html
* with a .join('').
*
* Generally this isn't very useful except for when you really need to either
* modify visual lines (greentext i.e. simple quoting) or do something with
* first/last line.
*
* known issue: doesn't handle CDATA so nested CDATA might not work well
*
* @param {Object} input - input data
* @return {(string|{ text: string })[]} processed html in form of a list.
*/
export const convertHtmlToLines = (html = '') => {
// Elements that are implicitly self-closing
// https://developer.mozilla.org/en-US/docs/Glossary/empty_element
const emptyElements = new Set([
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
])
// Block-level element (they make a visual line)
// https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
const blockElements = new Set([
'address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'dd',
'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li', 'main',
'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul'
])
// br is very weird in a way that it's technically not block-level, it's
// essentially converted to a \n (or \r\n). There's also wbr but it doesn't
// guarantee linebreak, only suggest it.
const linebreakElements = new Set(['br'])
const visualLineElements = new Set([
...blockElements.values(),
...linebreakElements.values()
])
// All block-level elements that aren't empty elements, i.e. not <hr>
const nonEmptyElements = new Set(visualLineElements)
// Difference
for (let elem of emptyElements) {
nonEmptyElements.delete(elem)
}
// All elements that we are recognizing
const allElements = new Set([
...nonEmptyElements.values(),
...emptyElements.values()
])
let buffer = [] // Current output buffer
const level = [] // How deep we are in tags and which tags were there
let textBuffer = '' // Current line content
let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
if (textBuffer.trim().length > 0) {
buffer.push({ level: [...level], text: textBuffer })
} else {
buffer.push(textBuffer)
}
textBuffer = ''
}
const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
flush()
buffer.push(tag)
}
const handleOpen = (tag) => { // handles opening tags
flush()
buffer.push(tag)
level.unshift(getTagName(tag))
}
const handleClose = (tag) => { // handles closing tags
if (level[0] === getTagName(tag)) {
flush()
buffer.push(tag)
level.shift()
} else { // Broken case
textBuffer += tag
}
}
for (let i = 0; i < html.length; i++) {
const char = html[i]
if (char === '<' && tagBuffer === null) {
tagBuffer = char
} else if (char !== '>' && tagBuffer !== null) {
tagBuffer += char
} else if (char === '>' && tagBuffer !== null) {
tagBuffer += char
const tagFull = tagBuffer
tagBuffer = null
const tagName = getTagName(tagFull)
if (allElements.has(tagName)) {
if (linebreakElements.has(tagName)) {
handleBr(tagFull)
} else if (nonEmptyElements.has(tagName)) {
if (tagFull[1] === '/') {
handleClose(tagFull)
} else if (tagFull[tagFull.length - 2] === '/') {
// self-closing
handleBr(tagFull)
} else {
handleOpen(tagFull)
}
} else {
textBuffer += tagFull
}
} else {
textBuffer += tagFull
}
} else if (char === '\n') {
handleBr(char)
} else {
textBuffer += char
}
}
if (tagBuffer) {
textBuffer += tagBuffer
}
flush()
return buffer
}
@@ -0,0 +1,97 @@
import { getTagName } from './utility.service.js'
/**
* This is a not-so-tiny purpose-built HTML parser/processor. This parses html
* and converts it into a tree structure representing tag openers/closers and
* children.
*
* Structure follows this pattern: [opener, [...children], closer] except root
* node which is just [...children]. Text nodes can only be within children and
* are represented as strings.
*
* Intended use is to convert HTML structure and then recursively iterate over it
* most likely using a map. Very useful for dynamically rendering html replacing
* tags with JSX elements in a render function.
*
* known issue: doesn't handle CDATA so CDATA might not work well
* known issue: doesn't handle HTML comments
*
* @param {Object} input - input data
* @return {string} processed html
*/
export const convertHtmlToTree = (html = '') => {
// Elements that are implicitly self-closing
// https://developer.mozilla.org/en-US/docs/Glossary/empty_element
const emptyElements = new Set([
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
])
// TODO For future - also parse HTML5 multi-source components?
const buffer = [] // Current output buffer
const levels = [['', buffer]] // How deep we are in tags and which tags were there
let textBuffer = '' // Current line content
let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
const getCurrentBuffer = () => {
return levels[levels.length - 1][1]
}
const flushText = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
if (textBuffer === '') return
getCurrentBuffer().push(textBuffer)
textBuffer = ''
}
const handleSelfClosing = (tag) => {
getCurrentBuffer().push([tag])
}
const handleOpen = (tag) => {
const curBuf = getCurrentBuffer()
const newLevel = [tag, []]
levels.push(newLevel)
curBuf.push(newLevel)
}
const handleClose = (tag) => {
const currentTag = levels[levels.length - 1]
if (getTagName(levels[levels.length - 1][0]) === getTagName(tag)) {
currentTag.push(tag)
levels.pop()
} else {
getCurrentBuffer().push(tag)
}
}
for (let i = 0; i < html.length; i++) {
const char = html[i]
if (char === '<' && tagBuffer === null) {
flushText()
tagBuffer = char
} else if (char !== '>' && tagBuffer !== null) {
tagBuffer += char
} else if (char === '>' && tagBuffer !== null) {
tagBuffer += char
const tagFull = tagBuffer
tagBuffer = null
const tagName = getTagName(tagFull)
if (tagFull[1] === '/') {
handleClose(tagFull)
} else if (emptyElements.has(tagName) || tagFull[tagFull.length - 2] === '/') {
// self-closing
handleSelfClosing(tagFull)
} else {
handleOpen(tagFull)
}
} else {
textBuffer += char
}
}
if (tagBuffer) {
textBuffer += tagBuffer
}
flushText()
return buffer
}
@@ -0,0 +1,73 @@
/**
* Extract tag name from tag opener/closer.
*
* @param {String} tag - tag string, i.e. '<a href="...">'
* @return {String} - tagname, i.e. "div"
*/
export const getTagName = (tag) => {
const result = /(?:<\/(\w+)>|<(\w+)\s?.*?\/?>)/gi.exec(tag)
return result && (result[1] || result[2])
}
/**
* Extract attributes from tag opener.
*
* @param {String} tag - tag string, i.e. '<a href="...">'
* @return {Object} - map of attributes key = attribute name, value = attribute value
* attributes without values represented as boolean true
*/
export const getAttrs = tag => {
const innertag = tag
.substring(1, tag.length - 1)
.replace(new RegExp('^' + getTagName(tag)), '')
.replace(/\/?$/, '')
.trim()
const attrs = Array.from(innertag.matchAll(/([a-z0-9-]+)(?:=("[^"]+?"|'[^']+?'))?/gi))
.map(([trash, key, value]) => [key, value])
.map(([k, v]) => {
if (!v) return [k, true]
return [k, v.substring(1, v.length - 1)]
})
return Object.fromEntries(attrs)
}
/**
* Finds shortcodes in text
*
* @param {String} text - original text to find emojis in
* @param {{ url: String, shortcode: Sring }[]} emoji - list of shortcodes to find
* @param {Function} processor - function to call on each encountered emoji,
* function is passed single object containing matching emoji ({ url, shortcode })
* return value will be inserted into resulting array instead of :shortcode:
* @return {Array} resulting array with non-emoji parts of text and whatever {processor}
* returned for emoji
*/
export const processTextForEmoji = (text, emojis, processor) => {
const buffer = []
let textBuffer = ''
for (let i = 0; i < text.length; i++) {
const char = text[i]
if (char === ':') {
const next = text.slice(i + 1)
let found = false
for (let emoji of emojis) {
if (next.slice(0, emoji.shortcode.length + 1) === (emoji.shortcode + ':')) {
found = emoji
break
}
}
if (found) {
buffer.push(textBuffer)
textBuffer = ''
buffer.push(processor(found))
i += found.shortcode.length + 1
} else {
textBuffer += char
}
} else {
textBuffer += char
}
}
if (textBuffer) buffer.push(textBuffer)
return buffer
}
@@ -11,9 +11,12 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
const rootState = store.rootState || store.state const rootState = store.rootState || store.state
const timelineData = rootState.statuses.notifications const timelineData = rootState.statuses.notifications
const hideMutedPosts = getters.mergedConfig.hideMutedPosts const hideMutedPosts = getters.mergedConfig.hideMutedPosts
const allowFollowingMove = rootState.users.currentUser.allow_following_move
args['withMuted'] = !hideMutedPosts args['withMuted'] = !hideMutedPosts
args['withMove'] = !allowFollowingMove
args['timeline'] = 'notifications' args['timeline'] = 'notifications'
if (older) { if (older) {
if (timelineData.minId !== Number.POSITIVE_INFINITY) { if (timelineData.minId !== Number.POSITIVE_INFINITY) {
+6
View File
@@ -369,6 +369,12 @@ export const SLOT_INHERITANCE = {
textColor: 'preserve' textColor: 'preserve'
}, },
postCyantext: {
depends: ['cBlue'],
layer: 'bg',
textColor: 'preserve'
},
border: { border: {
depends: ['fg'], depends: ['fg'],
opacity: 'border', opacity: 'border',
@@ -1,94 +0,0 @@
/**
* This is a tiny purpose-built HTML parser/processor. This basically detects any type of visual newline and
* allows it to be processed, useful for greentexting, mostly
*
* known issue: doesn't handle CDATA so nested CDATA might not work well
*
* @param {Object} input - input data
* @param {(string) => string} processor - function that will be called on every line
* @return {string} processed html
*/
export const processHtml = (html, processor) => {
const handledTags = new Set(['p', 'br', 'div'])
const openCloseTags = new Set(['p', 'div'])
let buffer = '' // Current output buffer
const level = [] // How deep we are in tags and which tags were there
let textBuffer = '' // Current line content
let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
// Extracts tag name from tag, i.e. <span a="b"> => span
const getTagName = (tag) => {
const result = /(?:<\/(\w+)>|<(\w+)\s?[^/]*?\/?>)/gi.exec(tag)
return result && (result[1] || result[2])
}
const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
if (textBuffer.trim().length > 0) {
buffer += processor(textBuffer)
} else {
buffer += textBuffer
}
textBuffer = ''
}
const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
flush()
buffer += tag
}
const handleOpen = (tag) => { // handles opening tags
flush()
buffer += tag
level.push(tag)
}
const handleClose = (tag) => { // handles closing tags
flush()
buffer += tag
if (level[level.length - 1] === tag) {
level.pop()
}
}
for (let i = 0; i < html.length; i++) {
const char = html[i]
if (char === '<' && tagBuffer === null) {
tagBuffer = char
} else if (char !== '>' && tagBuffer !== null) {
tagBuffer += char
} else if (char === '>' && tagBuffer !== null) {
tagBuffer += char
const tagFull = tagBuffer
tagBuffer = null
const tagName = getTagName(tagFull)
if (handledTags.has(tagName)) {
if (tagName === 'br') {
handleBr(tagFull)
} else if (openCloseTags.has(tagName)) {
if (tagFull[1] === '/') {
handleClose(tagFull)
} else if (tagFull[tagFull.length - 2] === '/') {
// self-closing
handleBr(tagFull)
} else {
handleOpen(tagFull)
}
}
} else {
textBuffer += tagFull
}
} else if (char === '\n') {
handleBr(char)
} else {
textBuffer += char
}
}
if (tagBuffer) {
textBuffer += tagBuffer
}
flush()
return buffer
}
@@ -8,6 +8,11 @@ const highlightStyle = (prefs) => {
const solidColor = `rgb(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)})` const solidColor = `rgb(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)})`
const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .1)` const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .1)`
const tintColor2 = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .2)` const tintColor2 = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .2)`
const customProps = {
'--____highlight-solidColor': solidColor,
'--____highlight-tintColor': tintColor,
'--____highlight-tintColor2': tintColor2
}
if (type === 'striped') { if (type === 'striped') {
return { return {
backgroundImage: [ backgroundImage: [
@@ -17,11 +22,13 @@ const highlightStyle = (prefs) => {
`${tintColor2} 20px,`, `${tintColor2} 20px,`,
`${tintColor2} 40px` `${tintColor2} 40px`
].join(' '), ].join(' '),
backgroundPosition: '0 0' backgroundPosition: '0 0',
...customProps
} }
} else if (type === 'solid') { } else if (type === 'solid') {
return { return {
backgroundColor: tintColor2 backgroundColor: tintColor2,
...customProps
} }
} else if (type === 'side') { } else if (type === 'side') {
return { return {
@@ -31,7 +38,8 @@ const highlightStyle = (prefs) => {
`${solidColor} 2px,`, `${solidColor} 2px,`,
`transparent 6px` `transparent 6px`
].join(' '), ].join(' '),
backgroundPosition: '0 0' backgroundPosition: '0 0',
...customProps
} }
} }
} }
@@ -0,0 +1,480 @@
import { mount, shallowMount, createLocalVue } from '@vue/test-utils'
import RichContent from 'src/components/rich_content/rich_content.jsx'
const localVue = createLocalVue()
const attentions = []
const makeMention = (who) => {
attentions.push({ statusnet_profile_url: `https://fake.tld/@${who}` })
return `<span class="h-card"><a class="u-url mention" href="https://fake.tld/@${who}">@<span>${who}</span></a></span>`
}
const p = (...data) => `<p>${data.join('')}</p>`
const compwrap = (...data) => `<span class="RichContent">${data.join('')}</span>`
const mentionsLine = (times) => [
'<mentionsline-stub mentions="',
new Array(times).fill('[object Object]').join(','),
'"></mentionsline-stub>'
].join('')
describe('RichContent', () => {
it('renders simple post without exploding', () => {
const html = p('Hello world!')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(html))
})
it('unescapes everything as needed', () => {
const html = [
p('Testing &#39;em all'),
'Testing &#39;em all'
].join('')
const expected = [
p('Testing \'em all'),
'Testing \'em all'
].join('')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('replaces mention with mentionsline', () => {
const html = p(
makeMention('John'),
' how are you doing today?'
)
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(p(
mentionsLine(1),
' how are you doing today?'
)))
})
it('replaces mentions at the end of the hellpost', () => {
const html = [
p('How are you doing today, fine gentlemen?'),
p(
makeMention('John'),
makeMention('Josh'),
makeMention('Jeremy')
)
].join('')
const expected = [
p(
'How are you doing today, fine gentlemen?'
),
// TODO fix this extra line somehow?
p(
'<mentionsline-stub mentions="',
'[object Object],',
'[object Object],',
'[object Object]',
'"></mentionsline-stub>'
)
].join('')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('Does not touch links if link handling is disabled', () => {
const html = [
[
makeMention('Jack'),
'let\'s meet up with ',
makeMention('Janet')
].join(''),
[
makeMention('John'),
makeMention('Josh'),
makeMention('Jeremy')
].join('')
].join('\n')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: false,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(html))
})
it('Adds greentext and cyantext to the post', () => {
const html = [
'&gt;preordering videogames',
'&gt;any year'
].join('\n')
const expected = [
'<span class="greentext">&gt;preordering videogames</span>',
'<span class="greentext">&gt;any year</span>'
].join('\n')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: false,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('Does not add greentext and cyantext if setting is set to false', () => {
const html = [
'&gt;preordering videogames',
'&gt;any year'
].join('\n')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: false,
greentext: false,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(html))
})
it('Adds emoji to post', () => {
const html = p('Ebin :DDDD :spurdo:')
const expected = p(
'Ebin :DDDD ',
'<anonymous-stub alt=":spurdo:" src="about:blank" title=":spurdo:" class="emoji img"></anonymous-stub>'
)
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: false,
greentext: false,
emoji: [{ url: 'about:blank', shortcode: 'spurdo' }],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('Doesn\'t add nonexistent emoji to post', () => {
const html = p('Lol :lol:')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: false,
greentext: false,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(html))
})
it('Greentext + last mentions', () => {
const html = [
'&gt;quote',
makeMention('lol'),
'&gt;quote',
'&gt;quote'
].join('\n')
const expected = [
'<span class="greentext">&gt;quote</span>',
mentionsLine(1),
'<span class="greentext">&gt;quote</span>',
'<span class="greentext">&gt;quote</span>'
].join('\n')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('One buggy example', () => {
const html = [
'Bruh',
'Bruh',
[
makeMention('foo'),
makeMention('bar'),
makeMention('baz')
].join(''),
'Bruh'
].join('<br>')
const expected = [
'Bruh',
'Bruh',
mentionsLine(3),
'Bruh'
].join('<br>')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('buggy example/hashtags', () => {
const html = [
'<p>',
'<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg">',
'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>',
' <a class="hashtag" data-tag="nou" href="https://shitposter.club/tag/nou">',
'#nou</a>',
' <a class="hashtag" data-tag="screencap" href="https://shitposter.club/tag/screencap">',
'#screencap</a>',
' </p>'
].join('')
const expected = [
'<p>',
'<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg" target="_blank">',
'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>',
' <hashtaglink-stub url="https://shitposter.club/tag/nou" content="#nou" tag="nou">',
'</hashtaglink-stub>',
' <hashtaglink-stub url="https://shitposter.club/tag/screencap" content="#screencap" tag="screencap">',
'</hashtaglink-stub>',
' </p>'
].join('')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('rich contents of a mention are handled properly', () => {
attentions.push({ statusnet_profile_url: 'lol' })
const html = [
p(
'<a href="lol" class="mention">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>'
),
p(
'Testing'
)
].join('')
const expected = [
p(
'<span class="MentionsLine">',
'<span class="MentionLink mention-link">',
'<a href="lol" target="_blank" class="original">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
' ',
'<!---->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
'</span>',
'<!---->', // v-if placeholder, mentionsline's extra mentions and stuff
'</span>'
),
p(
'Testing'
)
].join('')
const wrapper = mount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('rich contents of a link are handled properly', () => {
const html = [
'<p>',
'Freenode is dead.</p>',
'<p>',
'<a href="https://isfreenodedeadyet.com/">',
'<span>',
'https://</span>',
'<span>',
'isfreenodedeadyet.com/</span>',
'<span>',
'</span>',
'</a>',
'</p>'
].join('')
const expected = [
'<p>',
'Freenode is dead.</p>',
'<p>',
'<a href="https://isfreenodedeadyet.com/" target="_blank">',
'<span>',
'https://</span>',
'<span>',
'isfreenodedeadyet.com/</span>',
'<span>',
'</span>',
'</a>',
'</p>'
].join('')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it.skip('[INFORMATIVE] Performance testing, 10 000 simple posts', () => {
const amount = 20
const onePost = p(
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
' i just landed in l a where are you'
)
const TestComponent = {
template: `
<div v-if="!vhtml">
${new Array(amount).fill(`<RichContent html="${onePost}" :greentext="true" :handleLinks="handeLinks" :emoji="[]" :attentions="attentions"/>`)}
</div>
<div v-else="vhtml">
${new Array(amount).fill(`<div v-html="${onePost}"/>`)}
</div>
`,
props: ['handleLinks', 'attentions', 'vhtml']
}
console.log(1)
const ptest = (handleLinks, vhtml) => {
const t0 = performance.now()
const wrapper = mount(TestComponent, {
localVue,
propsData: {
attentions,
handleLinks,
vhtml
}
})
const t1 = performance.now()
wrapper.destroy()
const t2 = performance.now()
return `Mount: ${t1 - t0}ms, destroy: ${t2 - t1}ms, avg ${(t1 - t0) / amount}ms - ${(t2 - t1) / amount}ms per item`
}
console.log(`${amount} items with links handling:`)
console.log(ptest(true))
console.log(`${amount} items without links handling:`)
console.log(ptest(false))
console.log(`${amount} items plain v-html:`)
console.log(ptest(false, true))
})
})
@@ -1,4 +1,4 @@
import { parseStatus, parseUser, parseNotification, addEmojis, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' import { parseStatus, parseUser, parseNotification, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
import mastoapidata from '../../../../fixtures/mastoapi.json' import mastoapidata from '../../../../fixtures/mastoapi.json'
import qvitterapidata from '../../../../fixtures/statuses.json' import qvitterapidata from '../../../../fixtures/statuses.json'
@@ -23,7 +23,6 @@ const makeMockStatusQvitter = (overrides = {}) => {
repeat_num: 0, repeat_num: 0,
repeated: false, repeated: false,
statusnet_conversation_id: '16300488', statusnet_conversation_id: '16300488',
statusnet_html: '<p>haha benis</p>',
summary: null, summary: null,
tags: [], tags: [],
text: 'haha benis', text: 'haha benis',
@@ -232,22 +231,6 @@ describe('API Entities normalizer', () => {
expect(parsedRepeat).to.have.property('retweeted_status') expect(parsedRepeat).to.have.property('retweeted_status')
expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef') expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef')
}) })
it('adds emojis to post content', () => {
const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), content: 'Makes you think :thinking:' })
const parsedPost = parseStatus(post)
expect(parsedPost).to.have.property('statusnet_html').that.contains('<img')
})
it('adds emojis to subject line', () => {
const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), spoiler_text: 'CW: 300 IQ :thinking:' })
const parsedPost = parseStatus(post)
expect(parsedPost).to.have.property('summary_html').that.contains('<img')
})
}) })
}) })
@@ -261,35 +244,6 @@ describe('API Entities normalizer', () => {
expect(parseUser(remote)).to.have.property('is_local', false) expect(parseUser(remote)).to.have.property('is_local', false)
}) })
it('adds emojis to user name', () => {
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), display_name: 'The :thinking: thinker' })
const parsedUser = parseUser(user)
expect(parsedUser).to.have.property('name_html').that.contains('<img')
})
it('adds emojis to user bio', () => {
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), note: 'Hello i like to :thinking: a lot' })
const parsedUser = parseUser(user)
expect(parsedUser).to.have.property('description_html').that.contains('<img')
})
it('adds emojis to user profile fields', () => {
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: ':thinking:', value: ':image:' }] })
const parsedUser = parseUser(user)
expect(parsedUser).to.have.property('fields_html').to.be.an('array')
const field = parsedUser.fields_html[0]
expect(field).to.have.property('name').that.contains('<img')
expect(field).to.have.property('value').that.contains('<img')
})
it('removes html tags from user profile fields', () => { it('removes html tags from user profile fields', () => {
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: 'user', value: '<a rel="me" href="https://example.com/@user">@user</a>' }] }) const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: 'user', value: '<a rel="me" href="https://example.com/@user">@user</a>' }] })
@@ -355,41 +309,6 @@ describe('API Entities normalizer', () => {
}) })
}) })
describe('MastoAPI emoji adder', () => {
const emojis = makeMockEmojiMasto()
const imageHtml = '<img src="https://example.com/image.png" alt=":image:" title=":image:" class="emoji" />'
.replace(/"/g, '\'')
const thinkHtml = '<img src="https://example.com/think.png" alt=":thinking:" title=":thinking:" class="emoji" />'
.replace(/"/g, '\'')
it('correctly replaces shortcodes in supplied string', () => {
const result = addEmojis('This post has :image: emoji and :thinking: emoji', emojis)
expect(result).to.include(thinkHtml)
expect(result).to.include(imageHtml)
})
it('handles consecutive emojis correctly', () => {
const result = addEmojis('Lelel emoji spam :thinking::thinking::thinking::thinking:', emojis)
expect(result).to.include(thinkHtml + thinkHtml + thinkHtml + thinkHtml)
})
it('Doesn\'t replace nonexistent emojis', () => {
const result = addEmojis('Admin add the :tenshi: emoji', emojis)
expect(result).to.equal('Admin add the :tenshi: emoji')
})
it('Doesn\'t blow up on regex special characters', () => {
const emojis = makeMockEmojiMasto([{
shortcode: 'c++'
}, {
shortcode: '[a-z] {|}*'
}])
const result = addEmojis('This post has :c++: emoji and :[a-z] {|}*: emoji', emojis)
expect(result).to.include('title=\':c++:\'')
expect(result).to.include('title=\':[a-z] {|}*:\'')
})
})
describe('Link header pagination', () => { describe('Link header pagination', () => {
it('Parses min and max ids as integers', () => { it('Parses min and max ids as integers', () => {
const linkHeader = '<https://example.com/api/v1/notifications?max_id=861676>; rel="next", <https://example.com/api/v1/notifications?min_id=861741>; rel="prev"' const linkHeader = '<https://example.com/api/v1/notifications?max_id=861676>; rel="next", <https://example.com/api/v1/notifications?min_id=861741>; rel="prev"'
@@ -0,0 +1,171 @@
import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
const greentextHandle = new Set(['p', 'div'])
const mapOnlyText = (processor) => (input) => {
if (input.text && input.level.every(l => greentextHandle.has(l))) {
return processor(input.text)
} else if (input.text) {
return input.text
} else {
return input
}
}
describe('html_line_converter', () => {
describe('with processor that keeps original line should not make any changes to HTML when', () => {
const processorKeep = (line) => line
it('fed with regular HTML with newlines', () => {
const inputOutput = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>'
const result = convertHtmlToLines(inputOutput)
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
expect(comparableResult).to.eql(inputOutput)
})
it('fed with possibly broken HTML with invalid tags/composition', () => {
const inputOutput = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>'
const result = convertHtmlToLines(inputOutput)
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
expect(comparableResult).to.eql(inputOutput)
})
it('fed with very broken HTML with broken composition', () => {
const inputOutput = '</p> lmao what </div> whats going on <div> wha <p>'
const result = convertHtmlToLines(inputOutput)
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
expect(comparableResult).to.eql(inputOutput)
})
it('fed with sorta valid HTML but tags aren\'t closed', () => {
const inputOutput = 'just leaving a <div> hanging'
const result = convertHtmlToLines(inputOutput)
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
expect(comparableResult).to.eql(inputOutput)
})
it('fed with not really HTML at this point... tags that aren\'t finished', () => {
const inputOutput = 'do you expect me to finish this <div class='
const result = convertHtmlToLines(inputOutput)
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
expect(comparableResult).to.eql(inputOutput)
})
it('fed with dubiously valid HTML (p within p and also div inside p)', () => {
const inputOutput = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>'
const result = convertHtmlToLines(inputOutput)
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
expect(comparableResult).to.eql(inputOutput)
})
it('fed with maybe valid HTML? self-closing divs and ps', () => {
const inputOutput = 'a <div class="what"/> what now <p aria-label="wtf"/> ?'
const result = convertHtmlToLines(inputOutput)
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
expect(comparableResult).to.eql(inputOutput)
})
it('fed with valid XHTML containing a CDATA', () => {
const inputOutput = 'Yes, it is me, <![CDATA[DIO]]>'
const result = convertHtmlToLines(inputOutput)
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
expect(comparableResult).to.eql(inputOutput)
})
it('fed with some recognized but not handled elements', () => {
const inputOutput = 'testing images\n\n<img src="benis.png">'
const result = convertHtmlToLines(inputOutput)
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
expect(comparableResult).to.eql(inputOutput)
})
})
describe('with processor that replaces lines with word "_" should match expected line when', () => {
const processorReplace = (line) => '_'
it('fed with regular HTML with newlines', () => {
const input = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>'
const output = '_<br/>_<p class="lol">_</p>_\n_<p >_<br>_</p> <br>\n<br/>'
const result = convertHtmlToLines(input)
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
expect(comparableResult).to.eql(output)
})
it('fed with possibly broken HTML with invalid tags/composition', () => {
const input = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>'
const output = '_'
const result = convertHtmlToLines(input)
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
expect(comparableResult).to.eql(output)
})
it('fed with very broken HTML with broken composition', () => {
const input = '</p> lmao what </div> whats going on <div> wha <p>'
const output = '_<div>_<p>'
const result = convertHtmlToLines(input)
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
expect(comparableResult).to.eql(output)
})
it('fed with sorta valid HTML but tags aren\'t closed', () => {
const input = 'just leaving a <div> hanging'
const output = '_<div>_'
const result = convertHtmlToLines(input)
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
expect(comparableResult).to.eql(output)
})
it('fed with not really HTML at this point... tags that aren\'t finished', () => {
const input = 'do you expect me to finish this <div class='
const output = '_'
const result = convertHtmlToLines(input)
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
expect(comparableResult).to.eql(output)
})
it('fed with dubiously valid HTML (p within p and also div inside p)', () => {
const input = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>'
const output = '_<p>_\n_<p>_</p>_<br/><div>_</div></p>'
const result = convertHtmlToLines(input)
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
expect(comparableResult).to.eql(output)
})
it('fed with maybe valid HTML? (XHTML) self-closing divs and ps', () => {
const input = 'a <div class="what"/> what now <p aria-label="wtf"/> ?'
const output = '_<div class="what"/>_<p aria-label="wtf"/>_'
const result = convertHtmlToLines(input)
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
expect(comparableResult).to.eql(output)
})
it('fed with valid XHTML containing a CDATA', () => {
const input = 'Yes, it is me, <![CDATA[DIO]]>'
const output = '_'
const result = convertHtmlToLines(input)
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
expect(comparableResult).to.eql(output)
})
it('Testing handling ignored blocks', () => {
const input = `
<pre><code>&gt; rei = &quot;0&quot;
&#39;0&#39;
&gt; rei == 0
true
&gt; rei == null
false</code></pre><blockquote>That, christian-like JS diagram but its evangelion instead.</blockquote>
`
const result = convertHtmlToLines(input)
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
expect(comparableResult).to.eql(input)
})
it('Testing handling ignored blocks 2', () => {
const input = `
<blockquote>An SSL error has happened.</blockquote><p>Shakespeare</p>
`
const output = `
<blockquote>An SSL error has happened.</blockquote><p>_</p>
`
const result = convertHtmlToLines(input)
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
expect(comparableResult).to.eql(output)
})
})
})
@@ -0,0 +1,132 @@
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
describe('html_tree_converter', () => {
describe('convertHtmlToTree', () => {
it('converts html into a tree structure', () => {
const input = '1 <p>2</p> <b>3<img src="a">4</b>5'
expect(convertHtmlToTree(input)).to.eql([
'1 ',
[
'<p>',
['2'],
'</p>'
],
' ',
[
'<b>',
[
'3',
['<img src="a">'],
'4'
],
'</b>'
],
'5'
])
})
it('converts html to tree while preserving tag formatting', () => {
const input = '1 <p >2</p><b >3<img src="a">4</b>5'
expect(convertHtmlToTree(input)).to.eql([
'1 ',
[
'<p >',
['2'],
'</p>'
],
[
'<b >',
[
'3',
['<img src="a">'],
'4'
],
'</b>'
],
'5'
])
})
it('converts semi-broken html', () => {
const input = '1 <br> 2 <p> 42'
expect(convertHtmlToTree(input)).to.eql([
'1 ',
['<br>'],
' 2 ',
[
'<p>',
[' 42']
]
])
})
it('realistic case 1', () => {
const input = '<p><span class="h-card"><a class="u-url mention" data-user="9wRC6T2ZZiKWJ0vUi8" href="https://cawfee.club/users/benis" rel="ugc">@<span>benis</span></a></span> <span class="h-card"><a class="u-url mention" data-user="194" href="https://shigusegubu.club/users/hj" rel="ugc">@<span>hj</span></a></span> nice</p>'
expect(convertHtmlToTree(input)).to.eql([
[
'<p>',
[
[
'<span class="h-card">',
[
[
'<a class="u-url mention" data-user="9wRC6T2ZZiKWJ0vUi8" href="https://cawfee.club/users/benis" rel="ugc">',
[
'@',
[
'<span>',
[
'benis'
],
'</span>'
]
],
'</a>'
]
],
'</span>'
],
' ',
[
'<span class="h-card">',
[
[
'<a class="u-url mention" data-user="194" href="https://shigusegubu.club/users/hj" rel="ugc">',
[
'@',
[
'<span>',
[
'hj'
],
'</span>'
]
],
'</a>'
]
],
'</span>'
],
' nice'
],
'</p>'
]
])
})
it('realistic case 2', () => {
const inputOutput = 'Country improv: give me a city<br/>Audience: Memphis<br/>Improv troupe: come on, a better one<br/>Audience: el paso'
expect(convertHtmlToTree(inputOutput)).to.eql([
'Country improv: give me a city',
[
'<br/>'
],
'Audience: Memphis',
[
'<br/>'
],
'Improv troupe: come on, a better one',
[
'<br/>'
],
'Audience: el paso'
])
})
})
})
@@ -0,0 +1,37 @@
import { processTextForEmoji, getAttrs } from 'src/services/html_converter/utility.service.js'
describe('html_converter utility', () => {
describe('processTextForEmoji', () => {
it('processes all emoji in text', () => {
const input = 'Hello from finland! :lol: We have best water! :lmao:'
const emojis = [
{ shortcode: 'lol', src: 'LOL' },
{ shortcode: 'lmao', src: 'LMAO' }
]
const processor = ({ shortcode, src }) => ({ shortcode, src })
expect(processTextForEmoji(input, emojis, processor)).to.eql([
'Hello from finland! ',
{ shortcode: 'lol', src: 'LOL' },
' We have best water! ',
{ shortcode: 'lmao', src: 'LMAO' }
])
})
it('leaves text as is', () => {
const input = 'Number one: that\'s terror'
const emojis = []
const processor = ({ shortcode, src }) => ({ shortcode, src })
expect(processTextForEmoji(input, emojis, processor)).to.eql([
'Number one: that\'s terror'
])
})
})
describe('getAttrs', () => {
it('extracts arguments from tag', () => {
const input = '<img src="boop" cool ebin=\'true\'>'
const output = { src: 'boop', cool: true, ebin: 'true' }
expect(getAttrs(input)).to.eql(output)
})
})
})
@@ -1,96 +0,0 @@
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
describe('TinyPostHTMLProcessor', () => {
describe('with processor that keeps original line should not make any changes to HTML when', () => {
const processorKeep = (line) => line
it('fed with regular HTML with newlines', () => {
const inputOutput = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>'
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
})
it('fed with possibly broken HTML with invalid tags/composition', () => {
const inputOutput = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>'
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
})
it('fed with very broken HTML with broken composition', () => {
const inputOutput = '</p> lmao what </div> whats going on <div> wha <p>'
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
})
it('fed with sorta valid HTML but tags aren\'t closed', () => {
const inputOutput = 'just leaving a <div> hanging'
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
})
it('fed with not really HTML at this point... tags that aren\'t finished', () => {
const inputOutput = 'do you expect me to finish this <div class='
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
})
it('fed with dubiously valid HTML (p within p and also div inside p)', () => {
const inputOutput = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>'
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
})
it('fed with maybe valid HTML? self-closing divs and ps', () => {
const inputOutput = 'a <div class="what"/> what now <p aria-label="wtf"/> ?'
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
})
it('fed with valid XHTML containing a CDATA', () => {
const inputOutput = 'Yes, it is me, <![CDATA[DIO]]>'
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
})
})
describe('with processor that replaces lines with word "_" should match expected line when', () => {
const processorReplace = (line) => '_'
it('fed with regular HTML with newlines', () => {
const input = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>'
const output = '_<br/>_<p class="lol">_</p>_\n_<p >_<br>_</p> <br>\n<br/>'
expect(processHtml(input, processorReplace)).to.eql(output)
})
it('fed with possibly broken HTML with invalid tags/composition', () => {
const input = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>'
const output = '_'
expect(processHtml(input, processorReplace)).to.eql(output)
})
it('fed with very broken HTML with broken composition', () => {
const input = '</p> lmao what </div> whats going on <div> wha <p>'
const output = '</p>_</div>_<div>_<p>'
expect(processHtml(input, processorReplace)).to.eql(output)
})
it('fed with sorta valid HTML but tags aren\'t closed', () => {
const input = 'just leaving a <div> hanging'
const output = '_<div>_'
expect(processHtml(input, processorReplace)).to.eql(output)
})
it('fed with not really HTML at this point... tags that aren\'t finished', () => {
const input = 'do you expect me to finish this <div class='
const output = '_'
expect(processHtml(input, processorReplace)).to.eql(output)
})
it('fed with dubiously valid HTML (p within p and also div inside p)', () => {
const input = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>'
const output = '_<p>_\n_<p>_</p>_<br/><div>_</div></p>'
expect(processHtml(input, processorReplace)).to.eql(output)
})
it('fed with maybe valid HTML? self-closing divs and ps', () => {
const input = 'a <div class="what"/> what now <p aria-label="wtf"/> ?'
const output = '_<div class="what"/>_<p aria-label="wtf"/>_'
expect(processHtml(input, processorReplace)).to.eql(output)
})
it('fed with valid XHTML containing a CDATA', () => {
const input = 'Yes, it is me, <![CDATA[DIO]]>'
const output = '_'
expect(processHtml(input, processorReplace)).to.eql(output)
})
})
})

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