logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: e768ec1fca2f7580d111b0878a9695b0c8b9dbb1
parent 5d49edc823ba2ea3e34d4fd6c5efcc84ef9712f7
Author: Shpuld Shpludson <shp@cock.li>
Date:   Thu, 27 Aug 2020 14:45:03 +0000

Merge branch '2.1.0-rc0' into 'master'

2.1.0 into master

See merge request pleroma/pleroma-fe!1217

Diffstat:

M.gitlab-ci.yml3++-
A.stylelintrc.json19+++++++++++++++++++
MCHANGELOG.md46++++++++++++++++++++++++++++++++++++++++++++++
MREADME.md7+++----
Mdocs/CONFIGURATION.md96++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mdocs/USER_GUIDE.md4+---
Adocs/index.md8++++++++
Mpackage.json16+++++++++-------
Msrc/App.js23+++++++++++++++++++----
Msrc/App.scss100+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/App.vue19++++++++++++-------
Msrc/_variables.scss1+
Msrc/boot/after_store.js107++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/boot/routes.js21+++++++++++++++------
Msrc/components/account_actions/account_actions.js12++++++++++++
Msrc/components/account_actions/account_actions.vue8++++++++
Asrc/components/async_component_error/async_component_error.vue41+++++++++++++++++++++++++++++++++++++++++
Msrc/components/attachment/attachment.js32+++++++++++++++++++++++---------
Msrc/components/attachment/attachment.vue39++++++++++++++++++++++++---------------
Asrc/components/bookmark_timeline/bookmark_timeline.js17+++++++++++++++++
Asrc/components/bookmark_timeline/bookmark_timeline.vue9+++++++++
Asrc/components/chat/chat.js333+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/chat/chat.scss162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/chat/chat.vue100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/chat/chat_layout_utils.js26++++++++++++++++++++++++++
Asrc/components/chat_list/chat_list.js37+++++++++++++++++++++++++++++++++++++
Asrc/components/chat_list/chat_list.vue64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/chat_list_item/chat_list_item.js67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/chat_list_item/chat_list_item.scss94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/chat_list_item/chat_list_item.vue52++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/chat_message/chat_message.js96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/chat_message/chat_message.scss164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/chat_message/chat_message.vue99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/chat_message_date/chat_message_date.vue24++++++++++++++++++++++++
Asrc/components/chat_new/chat_new.js73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/chat_new/chat_new.scss29+++++++++++++++++++++++++++++
Asrc/components/chat_new/chat_new.vue46++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/chat_panel/chat_panel.vue84++++++++++++++++++++++++++++++++++++++++---------------------------------------
Asrc/components/chat_title/chat_title.js26++++++++++++++++++++++++++
Asrc/components/chat_title/chat_title.vue67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/checkbox/checkbox.vue2+-
Msrc/components/conversation/conversation.vue33+++++++++++++++++++++++----------
Msrc/components/domain_mute_card/domain_mute_card.js11+++++++++++
Msrc/components/domain_mute_card/domain_mute_card.vue15+++++++++++++++
Msrc/components/emoji_input/emoji_input.js60+++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/components/emoji_input/emoji_input.vue5++++-
Msrc/components/emoji_input/suggestor.js6+++---
Msrc/components/emoji_reactions/emoji_reactions.js4++--
Msrc/components/emoji_reactions/emoji_reactions.vue65+++--------------------------------------------------------------
Msrc/components/extra_buttons/extra_buttons.js10++++++++++
Msrc/components/extra_buttons/extra_buttons.vue17+++++++++++++++++
Msrc/components/features_panel/features_panel.js1+
Msrc/components/features_panel/features_panel.vue3+++
Msrc/components/follow_card/follow_card.vue4++--
Msrc/components/gallery/gallery.vue5++---
Asrc/components/global_notice_list/global_notice_list.js15+++++++++++++++
Asrc/components/global_notice_list/global_notice_list.vue77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/interface_language_switcher/interface_language_switcher.vue3+--
Msrc/components/media_modal/media_modal.js2++
Msrc/components/media_modal/media_modal.vue10++++++++++
Msrc/components/media_upload/media_upload.js42+++++++++++++++++++++---------------------
Msrc/components/media_upload/media_upload.vue7++++---
Msrc/components/mobile_nav/mobile_nav.js9+++++++--
Msrc/components/mobile_nav/mobile_nav.vue5+++++
Msrc/components/mobile_post_status_button/mobile_post_status_button.js7+++++++
Msrc/components/modal/modal.vue31+++++++++++++++++++++++++++----
Msrc/components/nav_panel/nav_panel.js29+++++++++++++++++++++--------
Msrc/components/nav_panel/nav_panel.vue31+++++++++++++++----------------
Msrc/components/notification/notification.js12+++++++++---
Asrc/components/notification/notification.scss52++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/notification/notification.vue9++++-----
Msrc/components/notifications/notifications.js19++++++++++++-------
Msrc/components/notifications/notifications.scss37++++++++++++++-----------------------
Asrc/components/panel_loading/panel_loading.vue29+++++++++++++++++++++++++++++
Msrc/components/poll/poll.vue3++-
Msrc/components/poll/poll_form.js1+
Msrc/components/popover/popover.js17+++++++++++++----
Msrc/components/popover/popover.vue5++++-
Msrc/components/post_status_form/post_status_form.js281+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msrc/components/post_status_form/post_status_form.vue272++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/components/react_button/react_button.js7+++++--
Dsrc/components/settings/settings.js128-------------------------------------------------------------------------------
Dsrc/components/settings/settings.vue424-------------------------------------------------------------------------------
Asrc/components/settings_modal/helpers/shared_computed_object.js58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/settings_modal.js42++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/settings_modal.scss51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/settings_modal.vue54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/settings_modal_content.js34++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/settings_modal_content.scss43+++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/settings_modal_content.vue73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/data_import_export_tab.js65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/data_import_export_tab.vue43+++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/filtering_tab.js47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/filtering_tab.vue86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/general_tab.js31+++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/general_tab.vue262+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/mutes_and_blocks_tab.js136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/mutes_and_blocks_tab.scss29+++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/mutes_and_blocks_tab.vue171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/notifications_tab.js27+++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/notifications_tab.vue34++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/profile_tab.js253+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/profile_tab.scss128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/profile_tab.vue289++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/components/user_settings/confirm.js -> src/components/settings_modal/tabs/security_tab/confirm.js0
Rsrc/components/user_settings/confirm.vue -> src/components/settings_modal/tabs/security_tab/confirm.vue0
Rsrc/components/user_settings/mfa.js -> src/components/settings_modal/tabs/security_tab/mfa.js0
Asrc/components/settings_modal/tabs/security_tab/mfa.vue173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/components/user_settings/mfa_backup_codes.js -> src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js0
Asrc/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue35+++++++++++++++++++++++++++++++++++
Rsrc/components/user_settings/mfa_totp.js -> src/components/settings_modal/tabs/security_tab/mfa_totp.js0
Rsrc/components/user_settings/mfa_totp.vue -> src/components/settings_modal/tabs/security_tab/mfa_totp.vue0
Asrc/components/settings_modal/tabs/security_tab/security_tab.js106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/security_tab/security_tab.vue143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/components/style_switcher/preview.vue -> src/components/settings_modal/tabs/theme_tab/preview.vue0
Asrc/components/settings_modal/tabs/theme_tab/theme_tab.js761+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/theme_tab/theme_tab.scss345+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/theme_tab/theme_tab.vue1032+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/version_tab.js24++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/version_tab.vue31+++++++++++++++++++++++++++++++
Msrc/components/side_drawer/side_drawer.js16+++++++++++++++-
Msrc/components/side_drawer/side_drawer.vue59++++++++++++++++++++++++++---------------------------------
Msrc/components/staff_panel/staff_panel.js4++++
Msrc/components/status/status.js118++++++++++++++++++++++++++++++++++++++++----------------------------------------
Asrc/components/status/status.scss414+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/status/status.vue525++++++++++++++-----------------------------------------------------------------
Msrc/components/status_content/status_content.js32++++++++++----------------------
Msrc/components/status_content/status_content.vue179++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msrc/components/status_popover/status_popover.js4++++
Msrc/components/status_popover/status_popover.vue8+++++---
Msrc/components/still-image/still-image.js3++-
Msrc/components/still-image/still-image.vue57+++++++++++++++++++++++++++++++--------------------------
Dsrc/components/style_switcher/style_switcher.js758-------------------------------------------------------------------------------
Dsrc/components/style_switcher/style_switcher.scss335-------------------------------------------------------------------------------
Dsrc/components/style_switcher/style_switcher.vue956-------------------------------------------------------------------------------
Msrc/components/tab_switcher/tab_switcher.js54+++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/components/tab_switcher/tab_switcher.scss268++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/components/timeline/timeline.js25++++++++++++++-----------
Msrc/components/timeline/timeline.vue24+++++++++++++++++-------
Asrc/components/timeline_menu/timeline_menu.js63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/timeline_menu/timeline_menu.vue180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/user_avatar/user_avatar.js16+++++-----------
Msrc/components/user_avatar/user_avatar.vue8+++++---
Msrc/components/user_card/user_card.vue44+++++++++++++++++++++-----------------------
Asrc/components/user_list_popover/user_list_popover.js18++++++++++++++++++
Asrc/components/user_list_popover/user_list_popover.vue71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/user_panel/user_panel.vue4+---
Msrc/components/user_profile/user_profile.js10++++++++++
Msrc/components/user_profile/user_profile.vue75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/user_reporting_modal/user_reporting_modal.vue3++-
Dsrc/components/user_settings/mfa.vue173-------------------------------------------------------------------------------
Dsrc/components/user_settings/mfa_backup_codes.vue33---------------------------------
Dsrc/components/user_settings/user_settings.js393-------------------------------------------------------------------------------
Dsrc/components/user_settings/user_settings.vue728-------------------------------------------------------------------------------
Msrc/components/video_attachment/video_attachment.vue2++
Msrc/components/who_to_follow_panel/who_to_follow_panel.js17+++++++++--------
Msrc/hocs/with_load_more/with_load_more.scss4++++
Msrc/i18n/ar.json405+++++++++++++++++++++++++++++++++++++++----------------------------------------
Msrc/i18n/ca.json10++++------
Msrc/i18n/cs.json7++-----
Msrc/i18n/de.json237+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msrc/i18n/en.json146++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/i18n/eo.json551++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/i18n/es.json58+++++++++++++++++++++++++---------------------------------
Msrc/i18n/et.json393++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/i18n/eu.json36++++++++++++++----------------------
Msrc/i18n/fi.json500+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/i18n/fr.json1254+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msrc/i18n/ga.json8+++-----
Msrc/i18n/he.json20+++++++++-----------
Msrc/i18n/hu.json2--
Msrc/i18n/it.json783++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/i18n/ja_easy.json81++++++++++++++++++++++++++++++++++++-------------------------------------------
Msrc/i18n/ja_pedantic.json7-------
Msrc/i18n/ko.json2--
Msrc/i18n/messages.js67++++++++++++++++++++++++++++++++++++++++---------------------------
Msrc/i18n/nb.json7-------
Msrc/i18n/nl.json638+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Msrc/i18n/oc.json10+---------
Msrc/i18n/pl.json187+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/i18n/pt.json2--
Msrc/i18n/ro.json2--
Msrc/i18n/ru.json130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Asrc/i18n/service_worker_messages.js35+++++++++++++++++++++++++++++++++++
Msrc/i18n/te.json698+++++++++++++++++++++++++++++++++++++++----------------------------------------
Msrc/i18n/zh.json162++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Asrc/lib/notification-i18n-loader.js12++++++++++++
Msrc/lib/persisted_state.js5++---
Msrc/main.js26++++++++++++++++++++------
Msrc/modules/api.js29++++++++++++++++++++++++-----
Asrc/modules/chats.js234+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/modules/config.js20+++++++++++++++++---
Msrc/modules/instance.js77++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Msrc/modules/interface.js77++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/modules/media_viewer.js2+-
Msrc/modules/statuses.js127++++++++++++++++++++++++++++++++++++-------------------------------------------
Msrc/modules/users.js30++++++++++++++++++++++++------
Msrc/services/api/api.service.js219+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/services/backend_interactor_service/backend_interactor_service.js4----
Asrc/services/chat_service/chat_service.js151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/services/chat_utils/chat_utils.js19+++++++++++++++++++
Asrc/services/desktop_notification_utils/desktop_notification_utils.js9+++++++++
Msrc/services/entity_normalizer/entity_normalizer.service.js67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/services/follow_request_fetcher/follow_request_fetcher.service.js1+
Msrc/services/notification_utils/notification_utils.js84++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/services/notifications_fetcher/notifications_fetcher.service.js12++++++++----
Asrc/services/resettable_async_component.js32++++++++++++++++++++++++++++++++
Msrc/services/status_parser/status_parser.js18+++++++-----------
Msrc/services/status_poster/status_poster.service.js30+++++++++++++++++++++++++-----
Msrc/services/style_setter/style_setter.js3++-
Msrc/services/theme_data/pleromafe.js99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/services/theme_data/theme_data.service.js9++++++---
Msrc/services/timeline_fetcher/timeline_fetcher.service.js23+++++++++++++++--------
Msrc/services/window_utils/window_utils.js5+++++
Msrc/sw.js47+++++++++++++++++++++++++++++++++++++++--------
Mstatic/config.json33+++++++++++++++++++--------------
Mstatic/fontello.json42++++++++++++++++++++++++++++++++++++++++++
Mstatic/terms-of-service.html7++++++-
Mstatic/themes/redmond-xx-se.json4+++-
Mstatic/themes/redmond-xx.json4+++-
Mstatic/themes/redmond-xxi.json4+++-
Mtest/unit/specs/boot/routes.spec.js10+++++++++-
Mtest/unit/specs/modules/statuses.spec.js8+++++---
Mtest/unit/specs/modules/users.spec.js36++++++++++++++++++++++++++++++++++++
Atest/unit/specs/services/chat_service/chat_service.spec.js88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/unit/specs/services/entity_normalizer/entity_normalizer.spec.js43++++++++++++++++++++++++++++++++++++++-----
Dtest/unit/specs/services/status_parser/status_parses.spec.js17-----------------
Myarn.lock1491+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
228 files changed, 16626 insertions(+), 7433 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml @@ -1,7 +1,7 @@ # This file is a template, and might need editing before it works on your project. # Official framework image. Look for the different tagged releases at: # https://hub.docker.com/r/library/node/tags/ -image: node:8 +image: node:10 stages: - lint @@ -14,6 +14,7 @@ lint: script: - yarn - npm run lint + - npm run stylelint test: stage: test diff --git a/.stylelintrc.json b/.stylelintrc.json @@ -0,0 +1,19 @@ +{ + "extends": [ + "stylelint-rscss/config", + "stylelint-config-recommended", + "stylelint-config-standard" + ], + "rules": { + "declaration-no-important": true, + "rscss/no-descendant-combinator": false, + "rscss/class-format": [ + true, + { + "component": "pascal-case", + "variant": "^-[a-z]\\w+", + "element": "^[a-z]\\w+" + } + ] + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -2,9 +2,53 @@ 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/). + ## [Unreleased] +## [Unreleased patch] + +## [2.1.0] - 2020-08-28 +### Add +- Autocomplete domains from list of known instances +- 'Bot' settings option and badge +- Added profile meta data fields that can be set in profile settings +- Added option to reset avatar/banner in profile settings +- Descriptions can be set on uploaded files before posting +- Added status preview option to preview your statuses before posting +- When a post is a reply to an unavailable post, the 'Reply to'-text has a strike-through style +- Added ability to see all favoriting or repeating users when hovering the number on highlighted statuses +- Bookmarks + ### Changed +- Change heart to thumbs up in reaction picker +- Close the media modal on navigation events +- Add colons to the emoji alt text, to make them copyable +- Add better visual indication for drag-and-drop for files +- When disabling attachments, the placeholder links now show an icon and the description instead of just IMAGE or VIDEO etc +- Remove unnecessary options for 'automatic loading when loading older' and 'reply previews' +- Greentext now has separate color slot for it - Removed the use of with_move parameters when fetching notifications +- Push notifications now are the same as normal notfication, and are localized. +- Updated Notification Settings to match new BE API + +### Fixed +- Custom Emoji will display in poll options now. +- Status ellipsis menu closes properly when selecting certain options +- Cropped images look correct in Chrome +- Newlines in the muted words settings work again +- Clicking on non-latin hashtags won't open a new window +- Uploading and drag-dropping multiple files works correctly now. +- Subject field now appears disabled when posting +- Fix status ellipsis menu being cut off in notifications column +- Fixed autocomplete sometimes not returning the right user when there's already some results +- Videos and audio and misc files show description as alt/title properly now +- Clicking on non-image/video files no longer opens an empty modal +- Audio files can now be played back in the frontend with hidden attachments +- Videos are not cropped awkwardly in the uploads section anymore +- Reply filtering options in Settings -> Filtering now work again using filtering on server +- Don't show just blank-screen when cookies are disabled +- Add status idempotency to prevent accidental double posting when posting returns an error +- Weird bug related to post being sent seemingly after pasting with keyboard (hopefully) +- Multiple issues with muted statuses/notifications ## [2.0.5] - 2020-05-12 ### Add @@ -77,7 +121,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Ability to change user's email - About page - Added remote user redirect + ### Changed - changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes + ### Fixed - improved hotkey behavior on autocomplete popup diff --git a/README.md b/README.md @@ -1,8 +1,8 @@ -# pleroma_fe +# Pleroma-FE -> A single column frontend for both Pleroma and GS servers. +> A single column frontend designed for Pleroma. -![screenshot](https://i.imgur.com/DJVqSJ0.png) +![screenshot](/uploads/796c5ecf985ed1e2b0943ee0df131ed0/DJVqSJ0.png) # For Translators @@ -11,7 +11,6 @@ To translate Pleroma-FE, add your language to [src/i18n/messages.js](https://git # FOR ADMINS You don't need to build Pleroma-FE yourself. Those using the Pleroma backend will be able to use it out of the box. -For the GNU social backend, check out https://git.pleroma.social/pleroma/pleroma-fe/wikis/dual-boot-with-qvitter to see how to run Pleroma-FE and Qvitter at the same time. ## Build Setup diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md @@ -19,32 +19,69 @@ There's currently no mechanism for user-settings synchronization across several ## Options -### `theme` -Default theme used for new users. De-facto instance-default, user can change theme. +### `alwaysShowSubjectInput` +`true` - will always show subject line input, `false` - only show when it's not empty (i.e. replying). To hide subject line input completely, set it to `false` and `subjectLineBehavior` to `"noop"` ### `background` Default image background. Be aware of using too big images as they may take longer to load. Currently image is fitted with `background-size: cover` which means "scaled and cropped", currently left-aligned. De-facto instance default, user can choose their own background, if they remove their own background, instance default will be used instead. +### `collapseMessageWithSubject` +Collapse post content when post has a subject line (content warning). Instance-default. + +### `disableChat` +hides the chat (TODO: even if it's enabled on backend) + +### `greentext` +Changes lines prefixed with the `>` character to have a green text color + +### `hideFilteredStatuses` +Removes filtered statuses from timelines. + +### `hideMutedPosts` +Removes muted statuses from timelines. + +### `hidePostStats` +Hide repeats/favorites counters for posts. + +### `hideSitename` +Hide instance name in header. + +### `hideUserStats` +Hide followers/friends counters for users. + +### `loginMethod` +`"password"` - show simple password field +`"token"` - show button to log in with external method (will redirect to login form, more details in BE documentation) + ### `logo`, `logoMask`, `logoMargin` Instance `logo`, could be any image, including svg. By default it assumes logo used will be monochrome-with-alpha one, this is done to be compatible with both light and dark themes, so that white logo designed with dark theme in mind won't be invisible over light theme, this is done via [CSS3 Masking](https://www.html5rocks.com/en/tutorials/masking/adobe/). Basically - it will take alpha channel of the image and fill non-transparent areas of it with solid color. If you really want colorful logo - it can be done by setting `logoMask` to `false`. `logoMargin` allows you to adjust vertical margins between logo boundary and navbar borders. The idea is that to have logo's image without any extra margins and instead adjust them to your need in layout. -### `redirectRootNoLogin`, `redirectRootLogin` -These two settings should point to where FE should redirect visitor when they login/open up website root +### `minimalScopesMode` +Limit scope selection to *Direct*, *User default* and *Scope of post replying to*. This also makes it impossible to reply to a DM with a non-DM post from PleromaFE. -### `chatDisabled` -hides the chat (TODO: even if it's enabled on backend) +### `nsfwCensorImage` +Use custom image for NSFW'd images -### `showInstanceSpecificPanel` -This allows you to include arbitrary HTML content in a panel below navigation menu. PleromaFE looks for an html page `instance/panel.html`, by default it's not provided in FE, but BE bundles some [default one](https://git.pleroma.social/pleroma/pleroma/blob/develop/priv/static/instance/panel.html). De-facto instance-defaults, since user can hide instance-specific panel. +### `postContentType` +Default post formatting option (markdown/bbcode/plaintext/etc...) -### `collapseMessageWithSubject` -Collapse post content when post has a subject line (content warning). Instance-default. +### `redirectRootNoLogin`, `redirectRootLogin` +These two settings should point to where FE should redirect visitor when they login/open up website root ### `scopeCopy` Copy post scope (visibility) when replying to a post. Instance-default. +### `sidebarRight` +Change alignment of sidebar and panels to the right. Defaults to `false`. + +### `showFeaturesPanel` +Show panel showcasing instance features/settings to logged-out visitors + +### `showInstanceSpecificPanel` +This allows you to include arbitrary HTML content in a panel below navigation menu. PleromaFE looks for an html page `instance/panel.html`, by default it's not provided in FE, but BE bundles some [default one](https://git.pleroma.social/pleroma/pleroma/blob/develop/priv/static/instance/panel.html). De-facto instance-defaults, since user can hide instance-specific panel. + ### `subjectLineBehavior` How to handle subject line (CW) when replying to a post. * `"email"` - like EMail - prepend `re: ` to subject line if it doesn't already start with it. @@ -52,39 +89,22 @@ How to handle subject line (CW) when replying to a post. * `"noop"` - do not copy Instance-default. -### `postContentType` -Default post formatting option (markdown/bbcode/plaintext/etc...) - -### `alwaysShowSubjectInput` -`true` - will always show subject line input, `false` - only show when it's not empty (i.e. replying). To hide subject line input completely, set it to `false` and `subjectLineBehavior` to `"noop"` - -### `hidePostStats` and `hideUserStats` -Hide counters for posts and users respectively, i.e. hiding repeats/favorites counts for posts, hiding followers/friends counts for users. This is just cosmetic and aimed to ease pressure and bias imposed by stat numbers of people and/or posts. (as an example: so that people care less about how many followers someone has since they can't see that info) - -### `loginMethod` -`"password"` - show simple password field -`"token"` - show button to log in with external method (will redirect to login form, more details in BE documentation) +### `theme` +Default theme used for new users. De-facto instance-default, user can change theme. ### `webPushNotifications` Enables [PushAPI](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) - based notifications for users. Instance-default. -### `noAttachmentLinks` -**TODO Currently doesn't seem to be doing anything code-wise**, but implication is to disable adding links for attachments, which looks nicer but breaks compatibility with old GNU/Social servers. -### `nsfwCensorImage` -Use custom image for NSFW'd images - -### `showFeaturesPanel` -Show panel showcasing instance features/settings to logged-out visitors - -### `hideSitename` -Hide instance name in header ## Indirect configuration Some features are configured depending on how backend is configured. In general the approach is "if backend allows it there's no need to hide it, if backend doesn't allow it there's no need to show it. ### Chat -**TODO somewhat broken, see: chatDisabled** chat can be disabled by disabling it in backend +**TODO somewhat broken, see: disableChat** chat can be disabled by disabling it in backend + +### Private Mode +If the `private` instance setting is enabled in the backend, features that are not accessible without authentication, such as the timelines and search will be disabled for unauthenticated users. ### Rich text formatting in post formatting Rich text formatting options are displayed depending on how many formatting options are enabled on backend, if you don't want your users to use rich text at all you can only allow "text/plain" one, frontend then will only display post text format as a label instead of dropdown (just so that users know for example if you only allow Markdown, only BBCode or only Plain text) @@ -92,13 +112,3 @@ Rich text formatting options are displayed depending on how many formatting opti ### Who to follow This is a panel intended for users to find people to follow based on randomness or on post contents. Being potentially privacy unfriendly feature it needs to be enabled and configured in backend to be enabled. -### Safe DM message display - -Setting this will change the warning text that is displayed for direct messages. - -ATTENTION: If you actually want the behavior to change. You will need to set the appropriate option at the backend. See the backend documentation for information about that. - -DO NOT activate this without checking the backend configuration first! - -### Private Mode -If the `private` instance setting is enabled in the backend, features that are not accessible without authentication, such as the timelines and search will be disabled for unauthenticated users. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md @@ -8,8 +8,6 @@ > > --Catbag -Pleroma-FE user interface is modeled after Qvitter which is modeled after older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options. - ## Posting, reading, basic functions. After registering and logging in you're presented with your timeline in right column and new post form with timeline list and notifications in the left column. @@ -33,7 +31,7 @@ will become Note that you can only use emoji defined on your instance, you cannot "copy" someone else's emoji, and will have to ask your administrator to copy emoji from other instance to yours. Lastly, there's two convenience options for emoji: an emoji picker (smiley face to the right of "submit" button) and autocomplete suggestions - when you start typing :shortcode: it will automatically try to suggest you emoj and complete the shortcode for you if you select one. **Note** that if emoji doesn't show up in suggestions nor in emoji picker it means there's no such emoji on your instance, if shortcode doesn't match any defined emoji it will appear as text. * **Attachments** are fairly simple - you can attach any file to a post as long as the file is within maximum size limits. If you're uploading explicit material you can mark all of your attachments as sensitive (or add `#nsfw` tag) - it will hide the images and videos behind a warning so that it won't be displayed instantly. -* **Subject line** also known as **CW** (Content Warning) could be used as a header to the post and/or to warn others about contents of the post having something that might upset somebody or something among those lines. Several applications allow to hide post content leaving only subject line visible. As a side-effect using subject line will also mark your images as sensitive (see above). +* **Subject line** also known as **CW** (Content Warning) could be used as a header to the post and/or to warn others about contents of the post having something that might upset somebody or something among those lines. Several applications allow to hide post content leaving only subject line visible. Using a subject line will not mark your images as sensitive, you will have to do that explicitly (see above). * **Visiblity scope** controls who will be able to see your posts. There are four scopes available: 1. `Public`: This is the default, and some fediverse software like GNU Social only supports this. This means that your post is accessible by anyone and will be shown in the public timelines. diff --git a/docs/index.md b/docs/index.md @@ -0,0 +1,8 @@ +# Introduction to Pleroma-FE +## What is Pleroma-FE? + +Pleroma-FE is the default user-facing frontend for Pleroma. It's user interface is modeled after Qvitter which is modeled after an older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options. + +## How can I use it? + +If your instance uses Pleroma-FE, you can acces it by going to your instance (e.g. <https://pleroma.soykaf.com>). You can read more about it's basic functionality in the [Pleroma-FE User Guide](./USER_GUIDE.md). We also have [a guide for administrators](./CONFIGURATION.md) and for [hackers/contributors](./HACKING.md). diff --git a/package.json b/package.json @@ -11,6 +11,7 @@ "unit:watch": "karma start test/unit/karma.conf.js --single-run=false", "e2e": "node test/e2e/runner.js", "test": "npm run unit && npm run e2e", + "stylelint": "npx stylelint src/components/status/status.scss", "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs", "lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs" }, @@ -22,21 +23,18 @@ "cropperjs": "^1.4.3", "diff": "^3.0.1", "escape-html": "^1.0.3", - "karma-mocha-reporter": "^2.2.1", "localforage": "^1.5.0", - "object-path": "^0.11.3", + "parse-link-header": "^1.0.1", "phoenix": "^1.3.0", "portal-vue": "^2.1.4", - "sanitize-html": "^1.13.0", "v-click-outside": "^2.1.1", - "vue": "^2.5.13", + "vue": "^2.6.11", "vue-chat-scroll": "^1.2.1", "vue-i18n": "^7.3.2", "vue-router": "^3.0.1", - "vue-template-compiler": "^2.3.4", + "vue-template-compiler": "^2.6.11", "vuelidate": "^0.7.4", - "vuex": "^3.0.1", - "whatwg-fetch": "^2.0.3" + "vuex": "^3.0.1" }, "devDependencies": { "@babel/core": "^7.7.5", @@ -82,6 +80,7 @@ "karma-coverage": "^1.1.1", "karma-firefox-launcher": "^1.1.0", "karma-mocha": "^1.2.0", + "karma-mocha-reporter": "^2.2.1", "karma-sinon-chai": "^2.0.2", "karma-sourcemap-loader": "^0.3.7", "karma-spec-reporter": "0.0.26", @@ -103,6 +102,9 @@ "shelljs": "^0.7.4", "sinon": "^2.1.0", "sinon-chai": "^2.8.0", + "stylelint": "^13.6.1", + "stylelint-config-standard": "^20.0.0", + "stylelint-rscss": "^0.4.0", "url-loader": "^1.1.2", "vue-loader": "^14.0.0", "vue-style-loader": "^4.0.0", diff --git a/src/App.js b/src/App.js @@ -6,13 +6,15 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance import FeaturesPanel from './components/features_panel/features_panel.vue' import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' import ChatPanel from './components/chat_panel/chat_panel.vue' +import SettingsModal from './components/settings_modal/settings_modal.vue' import MediaModal from './components/media_modal/media_modal.vue' import SideDrawer from './components/side_drawer/side_drawer.vue' import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue' import MobileNav from './components/mobile_nav/mobile_nav.vue' import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue' import PostStatusModal from './components/post_status_modal/post_status_modal.vue' -import { windowWidth } from './services/window_utils/window_utils' +import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue' +import { windowWidth, windowHeight } from './services/window_utils/window_utils' export default { name: 'app', @@ -29,8 +31,10 @@ export default { SideDrawer, MobilePostStatusButton, MobileNav, + SettingsModal, UserReportingModal, - PostStatusModal + PostStatusModal, + GlobalNoticeList }, data: () => ({ mobileActivePanel: 'timeline', @@ -45,7 +49,8 @@ export default { }), created () { // Load the locale from the storage - this.$i18n.locale = this.$store.getters.mergedConfig.interfaceLanguage + const val = this.$store.getters.mergedConfig.interfaceLanguage + this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) window.addEventListener('resize', this.updateMobileState) }, destroyed () { @@ -99,7 +104,12 @@ export default { }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, isMobileLayout () { return this.$store.state.interface.mobileLayout }, - privateMode () { return this.$store.state.instance.private } + privateMode () { return this.$store.state.instance.private }, + sidebarAlign () { + return { + 'order': this.$store.state.instance.sidebarRight ? 99 : 0 + } + } }, methods: { scrollToTop () { @@ -112,12 +122,17 @@ export default { onSearchBarToggled (hidden) { this.searchBarHidden = hidden }, + openSettingsModal () { + this.$store.dispatch('openSettingsModal') + }, updateMobileState () { const mobileLayout = windowWidth() <= 800 + const layoutHeight = windowHeight() const changed = mobileLayout !== this.isMobileLayout if (changed) { this.$store.dispatch('setMobileLayout', mobileLayout) } + this.$store.dispatch('setLayoutHeight', layoutHeight) } } } diff --git a/src/App.scss b/src/App.scss @@ -47,6 +47,7 @@ html { } body { + overscroll-behavior-y: none; font-family: sans-serif; font-family: var(--interfaceFont, sans-serif); margin: 0; @@ -319,7 +320,7 @@ option { i[class*=icon-] { color: $fallback--icon; - color: var(--icon, $fallback--icon) + color: var(--icon, $fallback--icon); } .btn-block { @@ -566,7 +567,7 @@ main-router { min-height: 0; box-sizing: border-box; margin: 0; - margin-left: .25em; + margin-left: .5em; min-width: 1px; align-self: stretch; } @@ -858,53 +859,12 @@ nav { display: block; margin-right: 0.8em; } -} - -.setting-item { - border-bottom: 2px solid var(--fg, $fallback--fg); - margin: 1em 1em 1.4em; - padding-bottom: 1.4em; - - > div { - margin-bottom: .5em; - &:last-child { - margin-bottom: 0; - } - } - - &:last-child { - border-bottom: none; - padding-bottom: 0; - margin-bottom: 1em; - } - - select { - min-width: 10em; - } - - textarea { - width: 100%; - max-width: 100%; - height: 100px; - } - - .unavailable, - .unavailable i { - color: var(--cRed, $fallback--cRed); - color: $fallback--cRed; - } - - .btn { - min-height: 28px; - min-width: 10em; - padding: 0 2em; - } - - .number-input { - max-width: 6em; + .main { + margin-bottom: 7em; } } + .select-multiple { display: flex; .option-list { @@ -969,3 +929,51 @@ nav { background-color: $fallback--fg; background-color: var(--panel, $fallback--fg); } + +.unread-chat-count { + font-size: 0.9em; + font-weight: bolder; + font-style: normal; + position: absolute; + right: 0.6rem; + padding: 0 0.3em; + min-width: 1.3rem; + min-height: 1.3rem; + max-height: 1.3rem; + line-height: 1.3rem; +} + +.chat-layout { + // Needed for smoother chat navigation in the desktop Safari (otherwise the chat layout "jumps" as the chat opens). + overflow: hidden; + height: 100%; + + // Ensures the fixed position of the mobile browser bars on scroll up / down events. + // Prevents the mobile browser bars from overlapping or hiding the message posting form. + @media all and (max-width: 800px) { + body { + height: 100%; + } + + #app { + height: 100%; + overflow: hidden; + min-height: auto; + } + + #app_bg_wrapper { + overflow: hidden; + } + + .main { + overflow: hidden; + height: 100%; + } + + #content { + padding-top: 0; + height: 100%; + overflow: visible; + } + } +} diff --git a/src/App.vue b/src/App.vue @@ -46,15 +46,16 @@ @toggled="onSearchBarToggled" @click.stop.native /> - <router-link + <a + href="#" class="mobile-hidden" - :to="{ name: 'settings'}" + @click.stop="openSettingsModal" > <i class="button-icon icon-cog nav-icon" :title="$t('nav.preferences')" /> - </router-link> + </a> <a v-if="currentUser && currentUser.role === 'admin'" href="/pleroma/admin/#/login-pleroma" @@ -76,11 +77,15 @@ </div> </div> </nav> + <div class="app-bg-wrapper app-container-wrapper" /> <div id="content" class="container underlay" > - <div class="sidebar-flexer mobile-hidden"> + <div + class="sidebar-flexer mobile-hidden" + :style="sidebarAlign" + > <div class="sidebar-bounds"> <div class="sidebar-scroller"> <div class="sidebar"> @@ -108,9 +113,7 @@ {{ $t("login.hint") }} </router-link> </div> - <transition name="fade"> - <router-view /> - </transition> + <router-view /> </div> <media-modal /> </div> @@ -122,7 +125,9 @@ <MobilePostStatusButton /> <UserReportingModal /> <PostStatusModal /> + <SettingsModal /> <portal-target name="modal" /> + <GlobalNoticeList /> </div> </template> diff --git a/src/_variables.scss b/src/_variables.scss @@ -27,5 +27,6 @@ $fallback--tooltipRadius: 5px; $fallback--avatarRadius: 4px; $fallback--avatarAltRadius: 10px; $fallback--attachmentRadius: 10px; +$fallback--chatMessageRadius: 10px; $fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; diff --git a/src/boot/after_store.js b/src/boot/after_store.js @@ -8,38 +8,72 @@ import backendInteractorService from '../services/backend_interactor_service/bac import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' import { applyTheme } from '../services/style_setter/style_setter.js' -const getStatusnetConfig = async ({ store }) => { +let staticInitialResults = null + +const parsedInitialResults = () => { + if (!document.getElementById('initial-results')) { + return null + } + if (!staticInitialResults) { + staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent) + } + return staticInitialResults +} + +const decodeUTF8Base64 = (data) => { + const rawData = atob(data) + const array = Uint8Array.from([...rawData].map((char) => char.charCodeAt(0))) + const text = new TextDecoder().decode(array) + return text +} + +const preloadFetch = async (request) => { + const data = parsedInitialResults() + if (!data || !data[request]) { + return window.fetch(request) + } + const decoded = decodeUTF8Base64(data[request]) + const requestData = JSON.parse(decoded) + return { + ok: true, + json: () => requestData, + text: () => requestData + } +} + +const getInstanceConfig = async ({ store }) => { try { - const res = await window.fetch('/api/statusnet/config.json') + const res = await preloadFetch('/api/v1/instance') if (res.ok) { const data = await res.json() - const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey, safeDMMentionsEnabled } = data.site - - store.dispatch('setInstanceOption', { name: 'name', value: name }) - store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') }) - store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) }) - store.dispatch('setInstanceOption', { name: 'server', value: server }) - store.dispatch('setInstanceOption', { name: 'safeDM', value: safeDMMentionsEnabled !== '0' }) - - // TODO: default values for this stuff, added if to not make it break on - // my dev config out of the box. - if (uploadlimit) { - store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadlimit.uploadlimit) }) - store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadlimit.avatarlimit) }) - store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadlimit.backgroundlimit) }) - store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadlimit.bannerlimit) }) - } + const textlimit = data.max_toot_chars + const vapidPublicKey = data.pleroma.vapid_public_key + + store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit }) if (vapidPublicKey) { store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) } + } else { + throw (res) + } + } catch (error) { + console.error('Could not load instance config, potentially fatal') + console.error(error) + } +} - return data.site.pleromafe +const getBackendProvidedConfig = async ({ store }) => { + try { + const res = await window.fetch('/api/pleroma/frontend_configurations') + if (res.ok) { + const data = await res.json() + return data.pleroma_fe } else { throw (res) } } catch (error) { - console.error('Could not load statusnet config, potentially fatal') + console.error('Could not load backend-provided frontend config, potentially fatal') console.error(error) } } @@ -108,9 +142,9 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { copyInstanceOption('subjectLineBehavior') copyInstanceOption('postContentType') copyInstanceOption('alwaysShowSubjectInput') - copyInstanceOption('noAttachmentLinks') copyInstanceOption('showFeaturesPanel') copyInstanceOption('hideSitename') + copyInstanceOption('sidebarRight') return store.dispatch('setTheme', config['theme']) } @@ -132,7 +166,7 @@ const getTOS = async ({ store }) => { const getInstancePanel = async ({ store }) => { try { - const res = await window.fetch('/instance/panel.html') + const res = await preloadFetch('/instance/panel.html') if (res.ok) { const html = await res.text() store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html }) @@ -189,24 +223,34 @@ const getAppSecret = async ({ store }) => { const resolveStaffAccounts = ({ store, accounts }) => { const nicknames = accounts.map(uri => uri.split('/').pop()) - nicknames.map(nickname => store.dispatch('fetchUser', nickname)) store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames }) } const getNodeInfo = async ({ store }) => { try { - const res = await window.fetch('/nodeinfo/2.0.json') + const res = await preloadFetch('/nodeinfo/2.0.json') if (res.ok) { const data = await res.json() const metadata = data.metadata const features = metadata.features + store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName }) + store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations }) store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) + store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') }) store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') }) + store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') }) store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) + const uploadLimits = metadata.uploadLimits + store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) + store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) }) + store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) }) + store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) }) + store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits }) + store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames }) store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats }) @@ -257,7 +301,7 @@ const getNodeInfo = async ({ store }) => { const setConfig = async ({ store }) => { // apiConfig, staticConfig - const configInfos = await Promise.all([getStatusnetConfig({ store }), getStaticConfig()]) + const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()]) const apiConfig = configInfos[0] const staticConfig = configInfos[1] @@ -280,6 +324,11 @@ const checkOAuthToken = async ({ store }) => { const afterStoreSetup = async ({ store, i18n }) => { const width = windowWidth() store.dispatch('setMobileLayout', width <= 800) + + const overrides = window.___pleromafe_dev_overrides || {} + const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin + store.dispatch('setInstanceOption', { name: 'server', value: server }) + await setConfig({ store }) const { customTheme, customThemeSource } = store.state.config @@ -299,16 +348,18 @@ const afterStoreSetup = async ({ store, i18n }) => { } // Now we can try getting the server settings and logging in + // Most of these are preloaded into the index.html so blocking is minimized await Promise.all([ checkOAuthToken({ store }), - getTOS({ store }), getInstancePanel({ store }), - getStickers({ store }), - getNodeInfo({ store }) + getNodeInfo({ store }), + getInstanceConfig({ store }) ]) // Start fetching things that don't need to block the UI store.dispatch('fetchMutes') + getTOS({ store }) + getStickers({ store }) const router = new VueRouter({ mode: 'history', diff --git a/src/boot/routes.js b/src/boot/routes.js @@ -2,15 +2,16 @@ import PublicTimeline from 'components/public_timeline/public_timeline.vue' import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue' import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue' import TagTimeline from 'components/tag_timeline/tag_timeline.vue' +import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue' import ConversationPage from 'components/conversation-page/conversation-page.vue' import Interactions from 'components/interactions/interactions.vue' import DMs from 'components/dm_timeline/dm_timeline.vue' +import ChatList from 'components/chat_list/chat_list.vue' +import Chat from 'components/chat/chat.vue' import UserProfile from 'components/user_profile/user_profile.vue' import Search from 'components/search/search.vue' -import Settings from 'components/settings/settings.vue' import Registration from 'components/registration/registration.vue' import PasswordReset from 'components/password_reset/password_reset.vue' -import UserSettings from 'components/user_settings/user_settings.vue' import FollowRequests from 'components/follow_requests/follow_requests.vue' import OAuthCallback from 'components/oauth_callback/oauth_callback.vue' import Notifications from 'components/notifications/notifications.vue' @@ -29,7 +30,7 @@ export default (store) => { } } - return [ + let routes = [ { name: 'root', path: '/', redirect: _to => { @@ -42,6 +43,7 @@ export default (store) => { { name: 'public-timeline', path: '/main/public', component: PublicTimeline }, { name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute }, { name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline }, + { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline }, { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, { name: 'remote-user-profile-acct', path: '/remote-users/(@?):username([^/@]+)@:hostname([^/@]+)', @@ -56,19 +58,26 @@ export default (store) => { { name: 'external-user-profile', path: '/users/:id', component: UserProfile }, { name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute }, { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, - { name: 'settings', path: '/settings', component: Settings }, { name: 'registration', path: '/registration', component: Registration }, { name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true }, { name: 'registration-token', path: '/registration/:token', component: Registration }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, - { name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute }, { name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute }, { name: 'login', path: '/login', component: AuthForm }, - { name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) }, + { name: 'chat-panel', path: '/chat-panel', component: ChatPanel, props: () => ({ floating: false }) }, { name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }, { name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) }, { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute }, { name: 'about', path: '/about', component: About }, { name: 'user-profile', path: '/(users/)?:name', component: UserProfile } ] + + if (store.state.instance.pleromaChatMessagesAvailable) { + routes = routes.concat([ + { name: 'chat', path: '/users/:username/chats/:recipient_id', component: Chat, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute }, + { name: 'chats', path: '/users/:username/chats', component: ChatList, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute } + ]) + } + + return routes } diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js @@ -1,3 +1,4 @@ +import { mapState } from 'vuex' import ProgressButton from '../progress_button/progress_button.vue' import Popover from '../popover/popover.vue' @@ -27,7 +28,18 @@ const AccountActions = { }, reportUser () { this.$store.dispatch('openUserReportingModal', this.user.id) + }, + openChat () { + this.$router.push({ + name: 'chat', + params: { recipient_id: this.user.id } + }) } + }, + computed: { + ...mapState({ + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable + }) } } diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue @@ -3,6 +3,7 @@ <Popover trigger="click" placement="bottom" + :bound-to="{ x: 'container' }" > <div slot="content" @@ -49,6 +50,13 @@ > {{ $t('user_card.report') }} </button> + <button + v-if="pleromaChatMessagesAvailable" + class="btn btn-default btn-block dropdown-item" + @click="openChat" + > + {{ $t('user_card.message') }} + </button> </div> </div> <div diff --git a/src/components/async_component_error/async_component_error.vue b/src/components/async_component_error/async_component_error.vue @@ -0,0 +1,41 @@ +<template> + <div class="async-component-error"> + <div> + <h4> + {{ $t('general.generic_error') }} + </h4> + <p> + {{ $t('general.error_retry') }} + </p> + <button + class="btn" + @click="retry" + > + {{ $t('general.retry') }} + </button> + </div> + </div> +</template> + +<script> +export default { + methods: { + retry () { + this.$emit('resetAsyncComponent') + } + } +} +</script> + +<style lang="scss"> +.async-component-error { + display: flex; + height: 100%; + align-items: center; + justify-content: center; + .btn { + margin: .5em; + padding: .5em 2em; + } +} +</style> diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js @@ -8,7 +8,6 @@ const Attachment = { props: [ 'attachment', 'nsfw', - 'statusId', 'size', 'allowPlay', 'setMedia', @@ -30,9 +29,21 @@ const Attachment = { VideoAttachment }, computed: { - usePlaceHolder () { + usePlaceholder () { return this.size === 'hide' || this.type === 'unknown' }, + placeholderName () { + if (this.attachment.description === '' || !this.attachment.description) { + return this.type.toUpperCase() + } + return this.attachment.description + }, + placeholderIconClass () { + if (this.type === 'image') return 'icon-picture' + if (this.type === 'video') return 'icon-video' + if (this.type === 'audio') return 'icon-music' + return 'icon-doc' + }, referrerpolicy () { return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer' }, @@ -49,7 +60,15 @@ const Attachment = { return this.size === 'small' }, fullwidth () { - return this.type === 'html' || this.type === 'audio' + if (this.size === 'hide') return false + return this.type === 'html' || this.type === 'audio' || this.type === 'unknown' + }, + useModal () { + const modalTypes = this.size === 'hide' ? ['image', 'video', 'audio'] + : this.mergedConfig.playVideosInModal + ? ['image', 'video'] + : ['image'] + return modalTypes.includes(this.type) }, ...mapGetters(['mergedConfig']) }, @@ -60,12 +79,7 @@ const Attachment = { } }, openModal (event) { - const modalTypes = this.mergedConfig.playVideosInModal - ? ['image', 'video'] - : ['image'] - if (fileTypeService.fileMatchesSomeType(modalTypes, this.attachment) || - this.usePlaceHolder - ) { + if (this.useModal) { event.stopPropagation() event.preventDefault() this.setMedia() diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue @@ -1,6 +1,7 @@ <template> <div - v-if="usePlaceHolder" + v-if="usePlaceholder" + :class="{ 'fullwidth': fullwidth }" @click="openModal" > <a @@ -8,8 +9,11 @@ class="placeholder" target="_blank" :href="attachment.url" + :alt="attachment.description" + :title="attachment.description" > - [{{ nsfw ? "NSFW/" : "" }}{{ type.toUpperCase() }}] + <span :class="placeholderIconClass" /> + <b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }} </a> </div> <div @@ -22,6 +26,8 @@ v-if="hidden" class="image-attachment" :href="attachment.url" + :alt="attachment.description" + :title="attachment.description" @click.prevent="toggleHidden" > <img @@ -51,14 +57,15 @@ :class="{'hidden': hidden && preloadImage }" :href="attachment.url" target="_blank" - :title="attachment.description" @click="openModal" > <StillImage + class="image" :referrerpolicy="referrerpolicy" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url" :image-load-handler="onImageLoad" + :alt="attachment.description" /> </a> @@ -83,6 +90,8 @@ <audio v-if="type === 'audio'" :src="attachment.url" + :alt="attachment.description" + :title="attachment.description" controls /> @@ -116,22 +125,19 @@ display: flex; flex-wrap: wrap; - .attachment.media-upload-container { - flex: 0 0 auto; - max-height: 200px; + .non-gallery { max-width: 100%; - display: flex; - align-items: center; - video { - max-width: 100%; - } } .placeholder { - margin-right: 8px; - margin-bottom: 4px; + display: inline-block; + padding: 0.3em 1em 0.3em 0; color: $fallback--link; color: var(--postLink, $fallback--link); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 100%; } .nsfw-placeholder { @@ -276,8 +282,11 @@ } .image-attachment { - width: 100%; - height: 100%; + &, + & .image { + width: 100%; + height: 100%; + } &.hidden { display: none; diff --git a/src/components/bookmark_timeline/bookmark_timeline.js b/src/components/bookmark_timeline/bookmark_timeline.js @@ -0,0 +1,17 @@ +import Timeline from '../timeline/timeline.vue' + +const Bookmarks = { + computed: { + timeline () { + return this.$store.state.statuses.timelines.bookmarks + } + }, + components: { + Timeline + }, + destroyed () { + this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) + } +} + +export default Bookmarks diff --git a/src/components/bookmark_timeline/bookmark_timeline.vue b/src/components/bookmark_timeline/bookmark_timeline.vue @@ -0,0 +1,9 @@ +<template> + <Timeline + :title="$t('nav.bookmarks')" + :timeline="timeline" + :timeline-name="'bookmarks'" + /> +</template> + +<script src="./bookmark_timeline.js"></script> diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js @@ -0,0 +1,333 @@ +import _ from 'lodash' +import { WSConnectionStatus } from '../../services/api/api.service.js' +import { mapGetters, mapState } from 'vuex' +import ChatMessage from '../chat_message/chat_message.vue' +import PostStatusForm from '../post_status_form/post_status_form.vue' +import ChatTitle from '../chat_title/chat_title.vue' +import chatService from '../../services/chat_service/chat_service.js' +import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight } from './chat_layout_utils.js' + +const BOTTOMED_OUT_OFFSET = 10 +const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150 +const SAFE_RESIZE_TIME_OFFSET = 100 + +const Chat = { + components: { + ChatMessage, + ChatTitle, + PostStatusForm + }, + data () { + return { + jumpToBottomButtonVisible: false, + hoveredMessageChainId: undefined, + lastScrollPosition: {}, + scrollableContainerHeight: '100%', + errorLoadingChat: false + } + }, + created () { + this.startFetching() + window.addEventListener('resize', this.handleLayoutChange) + }, + mounted () { + window.addEventListener('scroll', this.handleScroll) + if (typeof document.hidden !== 'undefined') { + document.addEventListener('visibilitychange', this.handleVisibilityChange, false) + } + + this.$nextTick(() => { + this.updateScrollableContainerHeight() + this.handleResize() + }) + this.setChatLayout() + }, + destroyed () { + window.removeEventListener('scroll', this.handleScroll) + window.removeEventListener('resize', this.handleLayoutChange) + this.unsetChatLayout() + if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false) + this.$store.dispatch('clearCurrentChat') + }, + computed: { + recipient () { + return this.currentChat && this.currentChat.account + }, + recipientId () { + return this.$route.params.recipient_id + }, + formPlaceholder () { + if (this.recipient) { + return this.$t('chats.message_user', { nickname: this.recipient.screen_name }) + } else { + return '' + } + }, + chatViewItems () { + return chatService.getView(this.currentChatMessageService) + }, + newMessageCount () { + return this.currentChatMessageService && this.currentChatMessageService.newMessageCount + }, + streamingEnabled () { + return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED + }, + ...mapGetters([ + 'currentChat', + 'currentChatMessageService', + 'findOpenedChatByRecipientId', + 'mergedConfig' + ]), + ...mapState({ + backendInteractor: state => state.api.backendInteractor, + mastoUserSocketStatus: state => state.api.mastoUserSocketStatus, + mobileLayout: state => state.interface.mobileLayout, + layoutHeight: state => state.interface.layoutHeight, + currentUser: state => state.users.currentUser + }) + }, + watch: { + chatViewItems () { + // We don't want to scroll to the bottom on a new message when the user is viewing older messages. + // Therefore we need to know whether the scroll position was at the bottom before the DOM update. + const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET) + this.$nextTick(() => { + if (bottomedOutBeforeUpdate) { + this.scrollDown({ forceRead: !document.hidden }) + } + }) + }, + '$route': function () { + this.startFetching() + }, + layoutHeight () { + this.handleResize({ expand: true }) + }, + mastoUserSocketStatus (newValue) { + if (newValue === WSConnectionStatus.JOINED) { + this.fetchChat({ isFirstFetch: true }) + } + } + }, + methods: { + // Used to animate the avatar near the first message of the message chain when any message belonging to the chain is hovered + onMessageHover ({ isHovered, messageChainId }) { + this.hoveredMessageChainId = isHovered ? messageChainId : undefined + }, + onFilesDropped () { + this.$nextTick(() => { + this.handleResize() + this.updateScrollableContainerHeight() + }) + }, + handleVisibilityChange () { + this.$nextTick(() => { + if (!document.hidden && this.bottomedOut(BOTTOMED_OUT_OFFSET)) { + this.scrollDown({ forceRead: true }) + } + }) + }, + setChatLayout () { + // This is a hacky way to adjust the global layout to the mobile chat (without modifying the rest of the app). + // This layout prevents empty spaces from being visible at the bottom + // of the chat on iOS Safari (`safe-area-inset`) when + // - the on-screen keyboard appears and the user starts typing + // - the user selects the text inside the input area + // - the user selects and deletes the text that is multiple lines long + // TODO: unify the chat layout with the global layout. + let html = document.querySelector('html') + if (html) { + html.classList.add('chat-layout') + } + + this.$nextTick(() => { + this.updateScrollableContainerHeight() + }) + }, + unsetChatLayout () { + let html = document.querySelector('html') + if (html) { + html.classList.remove('chat-layout') + } + }, + handleLayoutChange () { + this.$nextTick(() => { + this.updateScrollableContainerHeight() + this.scrollDown() + }) + }, + // Ensures the proper position of the posting form in the mobile layout (the mobile browser panel does not overlap or hide it) + updateScrollableContainerHeight () { + const header = this.$refs.header + const footer = this.$refs.footer + const inner = this.mobileLayout ? window.document.body : this.$refs.inner + this.scrollableContainerHeight = scrollableContainerHeight(inner, header, footer) + 'px' + }, + // Preserves the scroll position when OSK appears or the posting form changes its height. + handleResize (opts = {}) { + const { expand = false, delayed = false } = opts + + if (delayed) { + setTimeout(() => { + this.handleResize({ ...opts, delayed: false }) + }, SAFE_RESIZE_TIME_OFFSET) + return + } + + this.$nextTick(() => { + this.updateScrollableContainerHeight() + + const { offsetHeight = undefined } = this.lastScrollPosition + this.lastScrollPosition = getScrollPosition(this.$refs.scrollable) + + const diff = this.lastScrollPosition.offsetHeight - offsetHeight + if (diff < 0 || (!this.bottomedOut() && expand)) { + this.$nextTick(() => { + this.updateScrollableContainerHeight() + this.$refs.scrollable.scrollTo({ + top: this.$refs.scrollable.scrollTop - diff, + left: 0 + }) + }) + } + }) + }, + scrollDown (options = {}) { + const { behavior = 'auto', forceRead = false } = options + const scrollable = this.$refs.scrollable + if (!scrollable) { return } + this.$nextTick(() => { + scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior }) + }) + if (forceRead || this.newMessageCount > 0) { + this.readChat() + } + }, + readChat () { + if (!(this.currentChatMessageService && this.currentChatMessageService.lastMessage)) { return } + if (document.hidden) { return } + const lastReadId = this.currentChatMessageService.lastMessage.id + this.$store.dispatch('readChat', { id: this.currentChat.id, lastReadId }) + }, + bottomedOut (offset) { + return isBottomedOut(this.$refs.scrollable, offset) + }, + reachedTop () { + const scrollable = this.$refs.scrollable + return scrollable && scrollable.scrollTop <= 0 + }, + handleScroll: _.throttle(function () { + if (!this.currentChat) { return } + + if (this.reachedTop()) { + this.fetchChat({ maxId: this.currentChatMessageService.minId }) + } else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) { + this.jumpToBottomButtonVisible = false + if (this.newMessageCount > 0) { + this.readChat() + } + } else { + this.jumpToBottomButtonVisible = true + } + }, 100), + handleScrollUp (positionBeforeLoading) { + const positionAfterLoading = getScrollPosition(this.$refs.scrollable) + this.$refs.scrollable.scrollTo({ + top: getNewTopPosition(positionBeforeLoading, positionAfterLoading), + left: 0 + }) + }, + fetchChat ({ isFirstFetch = false, fetchLatest = false, maxId }) { + const chatMessageService = this.currentChatMessageService + if (!chatMessageService) { return } + if (fetchLatest && this.streamingEnabled) { return } + + const chatId = chatMessageService.chatId + const fetchOlderMessages = !!maxId + const sinceId = fetchLatest && chatMessageService.lastMessage && chatMessageService.lastMessage.id + + this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId }) + .then((messages) => { + // Clear the current chat in case we're recovering from a ws connection loss. + if (isFirstFetch) { + chatService.clear(chatMessageService) + } + + const positionBeforeUpdate = getScrollPosition(this.$refs.scrollable) + this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => { + this.$nextTick(() => { + if (fetchOlderMessages) { + this.handleScrollUp(positionBeforeUpdate) + } + + if (isFirstFetch) { + this.updateScrollableContainerHeight() + } + }) + }) + }) + }, + async startFetching () { + let chat = this.findOpenedChatByRecipientId(this.recipientId) + if (!chat) { + try { + chat = await this.backendInteractor.getOrCreateChat({ accountId: this.recipientId }) + } catch (e) { + console.error('Error creating or getting a chat', e) + this.errorLoadingChat = true + } + } + if (chat) { + this.$nextTick(() => { + this.scrollDown({ forceRead: true }) + }) + this.$store.dispatch('addOpenedChat', { chat }) + this.doStartFetching() + } + }, + doStartFetching () { + this.$store.dispatch('startFetchingCurrentChat', { + fetcher: () => setInterval(() => this.fetchChat({ fetchLatest: true }), 5000) + }) + this.fetchChat({ isFirstFetch: true }) + }, + sendMessage ({ status, media }) { + const params = { + id: this.currentChat.id, + content: status + } + + if (media[0]) { + params.mediaId = media[0].id + } + + return this.backendInteractor.sendChatMessage(params) + .then(data => { + this.$store.dispatch('addChatMessages', { chatId: this.currentChat.id, messages: [data] }).then(() => { + this.$nextTick(() => { + this.handleResize() + // When the posting form size changes because of a media attachment, we need an extra resize + // to account for the potential delay in the DOM update. + setTimeout(() => { + this.updateScrollableContainerHeight() + }, SAFE_RESIZE_TIME_OFFSET) + this.scrollDown({ forceRead: true }) + }) + }) + + return data + }) + .catch(error => { + console.error('Error sending message', error) + return { + error: this.$t('chats.error_sending_message') + } + }) + }, + goBack () { + this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } }) + } + } +} + +export default Chat diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss @@ -0,0 +1,162 @@ +.chat-view { + display: flex; + height: calc(100vh - 60px); + width: 100%; + + .chat-title { + // prevents chat header jumping on when the user avatar loads + height: 28px; + } + + .chat-view-inner { + height: auto; + width: 100%; + overflow: visible; + display: flex; + margin: 0.5em 0.5em 0 0.5em; + } + + .chat-view-body { + background-color: var(--chatBg, $fallback--bg); + display: flex; + flex-direction: column; + width: 100%; + overflow: visible; + min-height: 100%; + margin: 0 0 0 0; + border-radius: 10px 10px 0 0; + border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0 ; + + &::after { + border-radius: 0; + } + } + + .scrollable-message-list { + padding: 0 0.8em; + height: 100%; + overflow-y: scroll; + overflow-x: hidden; + display: flex; + flex-direction: column; + } + + .footer { + position: sticky; + bottom: 0; + } + + .chat-view-heading { + align-items: center; + justify-content: space-between; + top: 50px; + display: flex; + z-index: 2; + position: sticky; + overflow: hidden; + } + + .go-back-button { + cursor: pointer; + margin-right: 1.4em; + + i { + display: flex; + align-items: center; + } + } + + .jump-to-bottom-button { + width: 2.5em; + height: 2.5em; + border-radius: 100%; + position: absolute; + right: 1.3em; + top: -3.2em; + background-color: $fallback--fg; + background-color: var(--btn, $fallback--fg); + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3), 0px 2px 4px rgba(0, 0, 0, 0.3); + z-index: 10; + transition: 0.35s all; + transition-timing-function: cubic-bezier(0, 1, 0.5, 1); + opacity: 0; + visibility: hidden; + cursor: pointer; + + &.visible { + opacity: 1; + visibility: visible; + } + + i { + font-size: 1em; + color: $fallback--text; + color: var(--text, $fallback--text); + } + + .unread-message-count { + font-size: 0.8em; + left: 50%; + transform: translate(-50%, 0); + border-radius: 100%; + margin-top: -1rem; + padding: 0; + } + + .chat-loading-error { + width: 100%; + display: flex; + align-items: flex-end; + height: 100%; + + .error { + width: 100%; + } + } + } + + @media all and (max-width: 800px) { + height: 100%; + overflow: hidden; + + .chat-view-inner { + overflow: hidden; + height: 100%; + margin-top: 0; + margin-left: 0; + margin-right: 0; + } + + .chat-view-body { + display: flex; + min-height: auto; + overflow: hidden; + height: 100%; + margin: 0; + border-radius: 0; + } + + .chat-view-heading { + position: static; + z-index: 9999; + top: 0; + margin-top: 0; + border-radius: 0; + } + + .scrollable-message-list { + display: unset; + overflow-y: scroll; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + } + + .footer { + position: sticky; + bottom: auto; + } + } +} diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue @@ -0,0 +1,100 @@ +<template> + <div class="chat-view"> + <div class="chat-view-inner"> + <div + id="nav" + ref="inner" + class="panel-default panel chat-view-body" + > + <div + ref="header" + class="panel-heading chat-view-heading mobile-hidden" + > + <a + class="go-back-button" + @click="goBack" + > + <i class="button-icon icon-left-open" /> + </a> + <div class="title text-center"> + <ChatTitle + :user="recipient" + :with-avatar="true" + /> + </div> + </div> + <template> + <div + ref="scrollable" + class="scrollable-message-list" + :style="{ height: scrollableContainerHeight }" + @scroll="handleScroll" + > + <template v-if="!errorLoadingChat"> + <ChatMessage + v-for="chatViewItem in chatViewItems" + :key="chatViewItem.id" + :author="recipient" + :chat-view-item="chatViewItem" + :hovered-message-chain="chatViewItem.messageChainId === hoveredMessageChainId" + @hover="onMessageHover" + /> + </template> + <div + v-else + class="chat-loading-error" + > + <div class="alert error"> + {{ $t('chats.error_loading_chat') }} + </div> + </div> + </div> + <div + ref="footer" + class="panel-body footer" + > + <div + class="jump-to-bottom-button" + :class="{ 'visible': jumpToBottomButtonVisible }" + @click="scrollDown({ behavior: 'smooth' })" + > + <i class="icon-down-open"> + <div + v-if="newMessageCount" + class="badge badge-notification unread-chat-count unread-message-count" + > + {{ newMessageCount }} + </div> + </i> + </div> + <PostStatusForm + :disable-subject="true" + :disable-scope-selector="true" + :disable-notice="true" + :disable-lock-warning="true" + :disable-polls="true" + :disable-sensitivity-checkbox="true" + :disable-submit="errorLoadingChat || !currentChat" + :disable-preview="true" + :post-handler="sendMessage" + :submit-on-enter="!mobileLayout" + :preserve-focus="!mobileLayout" + :auto-focus="!mobileLayout" + :placeholder="formPlaceholder" + :file-limit="1" + max-height="160" + emoji-picker-placement="top" + @resize="handleResize" + /> + </div> + </template> + </div> + </div> + </div> +</template> + +<script src="./chat.js"></script> +<style lang="scss"> +@import '../../_variables.scss'; +@import './chat.scss'; +</style> diff --git a/src/components/chat/chat_layout_utils.js b/src/components/chat/chat_layout_utils.js @@ -0,0 +1,26 @@ +// Captures a scroll position +export const getScrollPosition = (el) => { + return { + scrollTop: el.scrollTop, + scrollHeight: el.scrollHeight, + offsetHeight: el.offsetHeight + } +} + +// A helper function that is used to keep the scroll position fixed as the new elements are added to the top +// Takes two scroll positions, before and after the update. +export const getNewTopPosition = (previousPosition, newPosition) => { + return previousPosition.scrollTop + (newPosition.scrollHeight - previousPosition.scrollHeight) +} + +export const isBottomedOut = (el, offset = 0) => { + if (!el) { return } + const scrollHeight = el.scrollTop + offset + const totalHeight = el.scrollHeight - el.offsetHeight + return totalHeight <= scrollHeight +} + +// Height of the scrollable container. The dynamic height is needed to ensure the mobile browser panel doesn't overlap or hide the posting form. +export const scrollableContainerHeight = (inner, header, footer) => { + return inner.offsetHeight - header.clientHeight - footer.clientHeight +} diff --git a/src/components/chat_list/chat_list.js b/src/components/chat_list/chat_list.js @@ -0,0 +1,37 @@ +import { mapState, mapGetters } from 'vuex' +import ChatListItem from '../chat_list_item/chat_list_item.vue' +import ChatNew from '../chat_new/chat_new.vue' +import List from '../list/list.vue' + +const ChatList = { + components: { + ChatListItem, + List, + ChatNew + }, + computed: { + ...mapState({ + currentUser: state => state.users.currentUser + }), + ...mapGetters(['sortedChatList']) + }, + data () { + return { + isNew: false + } + }, + created () { + this.$store.dispatch('fetchChats', { latest: true }) + }, + methods: { + cancelNewChat () { + this.isNew = false + this.$store.dispatch('fetchChats', { latest: true }) + }, + newChat () { + this.isNew = true + } + } +} + +export default ChatList diff --git a/src/components/chat_list/chat_list.vue b/src/components/chat_list/chat_list.vue @@ -0,0 +1,64 @@ +<template> + <div v-if="isNew"> + <ChatNew @cancel="cancelNewChat" /> + </div> + <div + v-else + class="chat-list panel panel-default" + > + <div class="panel-heading"> + <span class="title"> + {{ $t("chats.chats") }} + </span> + <button @click="newChat"> + {{ $t("chats.new") }} + </button> + </div> + <div class="panel-body"> + <div + v-if="sortedChatList.length > 0" + class="timeline" + > + <List :items="sortedChatList"> + <template + slot="item" + slot-scope="{item}" + > + <ChatListItem + :key="item.id" + :compact="false" + :chat="item" + /> + </template> + </List> + </div> + <div + v-else + class="emtpy-chat-list-alert" + > + <span>{{ $t('chats.empty_chat_list_placeholder') }}</span> + </div> + </div> + </div> +</template> + +<script src="./chat_list.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.chat-list { + min-height: 25em; + margin-bottom: 0; +} + +.emtpy-chat-list-alert { + padding: 3em; + font-size: 1.2em; + display: flex; + justify-content: center; + color: $fallback--text; + color: var(--faint, $fallback--text); +} + +</style> diff --git a/src/components/chat_list_item/chat_list_item.js b/src/components/chat_list_item/chat_list_item.js @@ -0,0 +1,67 @@ +import { mapState } from 'vuex' +import StatusContent from '../status_content/status_content.vue' +import fileType from 'src/services/file_type/file_type.service' +import UserAvatar from '../user_avatar/user_avatar.vue' +import AvatarList from '../avatar_list/avatar_list.vue' +import Timeago from '../timeago/timeago.vue' +import ChatTitle from '../chat_title/chat_title.vue' + +const ChatListItem = { + name: 'ChatListItem', + props: [ + 'chat' + ], + components: { + UserAvatar, + AvatarList, + Timeago, + ChatTitle, + StatusContent + }, + computed: { + ...mapState({ + currentUser: state => state.users.currentUser + }), + attachmentInfo () { + if (this.chat.lastMessage.attachments.length === 0) { return } + + const types = this.chat.lastMessage.attachments.map(file => fileType.fileType(file.mimetype)) + if (types.includes('video')) { + return this.$t('file_type.video') + } else if (types.includes('audio')) { + return this.$t('file_type.audio') + } else if (types.includes('image')) { + return this.$t('file_type.image') + } else { + return this.$t('file_type.file') + } + }, + messageForStatusContent () { + const message = this.chat.lastMessage + const isYou = message && message.account_id === this.currentUser.id + const content = message ? (this.attachmentInfo || message.content) : '' + const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content + return { + summary: '', + statusnet_html: messagePreview, + text: messagePreview, + attachments: [] + } + } + }, + methods: { + openChat (_e) { + if (this.chat.id) { + this.$router.push({ + name: 'chat', + params: { + username: this.currentUser.screen_name, + recipient_id: this.chat.account.id + } + }) + } + } + } +} + +export default ChatListItem diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss @@ -0,0 +1,94 @@ +.chat-list-item { + display: flex; + flex-direction: row; + padding: 0.75em; + height: 5em; + overflow: hidden; + box-sizing: border-box; + cursor: pointer; + + :focus { + outline: none; + } + + &:hover { + background-color: var(--selectedPost, $fallback--lightBg); + box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.1); + } + + .chat-list-item-left { + margin-right: 1em; + } + + .chat-list-item-center { + width: 100%; + box-sizing: border-box; + overflow: hidden; + word-wrap: break-word; + } + + .heading { + width: 100%; + display: inline-flex; + justify-content: space-between; + line-height: 1em; + } + + .heading-right { + white-space: nowrap; + } + + .name-and-account-name { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + flex-shrink: 1; + line-height: 1.4em; + } + + .chat-preview { + display: inline-flex; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin: 0.35em 0; + color: $fallback--text; + color: var(--faint, $fallback--text); + width: 100%; + } + + a { + color: var(--faintLink, $fallback--link); + text-decoration: none; + pointer-events: none; + } + + &:hover .animated.avatar { + canvas { + display: none; + } + img { + visibility: visible; + } + } + + .Avatar { + border-radius: $fallback--avatarAltRadius; + border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + } + + .StatusContent { + img.emoji { + width: 1.4em; + height: 1.4em; + } + } + + .time-wrapper { + line-height: 1.4em; + } + + .single-line { + padding-right: 1em; + } +} diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue @@ -0,0 +1,52 @@ +<template> + <div + class="chat-list-item" + @click.capture.prevent="openChat" + > + <div class="chat-list-item-left"> + <UserAvatar + :user="chat.account" + height="48px" + width="48px" + /> + </div> + <div class="chat-list-item-center"> + <div class="heading"> + <span + v-if="chat.account" + class="name-and-account-name" + > + <ChatTitle + :user="chat.account" + /> + </span> + <span class="heading-right" /> + </div> + <div class="chat-preview"> + <StatusContent + :status="messageForStatusContent" + :single-line="true" + /> + <div + v-if="chat.unread > 0" + class="badge badge-notification unread-chat-count" + > + {{ chat.unread }} + </div> + </div> + </div> + <div class="time-wrapper"> + <Timeago + :time="chat.updated_at" + :auto-update="60" + /> + </div> + </div> +</template> + +<script src="./chat_list_item.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; +@import './chat_list_item.scss'; +</style> diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js @@ -0,0 +1,96 @@ +import { mapState, mapGetters } from 'vuex' +import Popover from '../popover/popover.vue' +import Attachment from '../attachment/attachment.vue' +import UserAvatar from '../user_avatar/user_avatar.vue' +import Gallery from '../gallery/gallery.vue' +import LinkPreview from '../link-preview/link-preview.vue' +import StatusContent from '../status_content/status_content.vue' +import ChatMessageDate from '../chat_message_date/chat_message_date.vue' +import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' + +const ChatMessage = { + name: 'ChatMessage', + props: [ + 'author', + 'edited', + 'noHeading', + 'chatViewItem', + 'hoveredMessageChain' + ], + components: { + Popover, + Attachment, + StatusContent, + UserAvatar, + Gallery, + LinkPreview, + ChatMessageDate + }, + computed: { + // Returns HH:MM (hours and minutes) in local time. + createdAt () { + const time = this.chatViewItem.data.created_at + return time.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false }) + }, + isCurrentUser () { + return this.message.account_id === this.currentUser.id + }, + message () { + return this.chatViewItem.data + }, + userProfileLink () { + return generateProfileLink(this.author.id, this.author.screen_name, this.$store.state.instance.restrictedNicknames) + }, + isMessage () { + return this.chatViewItem.type === 'message' + }, + messageForStatusContent () { + return { + summary: '', + statusnet_html: this.message.content, + text: this.message.content, + attachments: this.message.attachments + } + }, + hasAttachment () { + return this.message.attachments.length > 0 + }, + ...mapState({ + betterShadow: state => state.interface.browserSupport.cssFilter, + currentUser: state => state.users.currentUser, + restrictedNicknames: state => state.instance.restrictedNicknames + }), + popoverMarginStyle () { + if (this.isCurrentUser) { + return {} + } else { + return { left: 50 } + } + }, + ...mapGetters(['mergedConfig', 'findUser']) + }, + data () { + return { + hovered: false, + menuOpened: false + } + }, + methods: { + onHover (bool) { + this.$emit('hover', { isHovered: bool, messageChainId: this.chatViewItem.messageChainId }) + }, + async deleteMessage () { + const confirmed = window.confirm(this.$t('chats.delete_confirm')) + if (confirmed) { + await this.$store.dispatch('deleteChatMessage', { + messageId: this.chatViewItem.data.id, + chatId: this.chatViewItem.data.chat_id + }) + } + this.hovered = false + this.menuOpened = false + } + } +} + +export default ChatMessage diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss @@ -0,0 +1,164 @@ +@import '../../_variables.scss'; + +.chat-message-wrapper { + &.hovered-message-chain { + .animated.Avatar { + canvas { + display: none; + } + img { + visibility: visible; + } + } + } + + .chat-message-menu { + transition: opacity 0.1s; + opacity: 0; + position: absolute; + top: -0.8em; + + button { + padding-top: 0.2em; + padding-bottom: 0.2em; + } + } + + .icon-ellipsis { + cursor: pointer; + + &:hover, .extra-button-popover.open & { + color: $fallback--text; + color: var(--text, $fallback--text); + } + + border-radius: $fallback--chatMessageRadius; + border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius); + } + + .popover { + width: 12em; + } + + .chat-message { + display: flex; + padding-bottom: 0.5em; + } + + .avatar-wrapper { + margin-right: 0.72em; + width: 32px; + } + + .link-preview, .attachments { + margin-bottom: 1em; + } + + .chat-message-inner { + display: flex; + flex-direction: column; + align-items: flex-start; + max-width: 80%; + min-width: 10em; + width: 100%; + + &.with-media { + width: 100%; + + .gallery-row { + overflow: hidden; + } + + .status { + width: 100%; + } + } + } + + .status { + border-radius: $fallback--chatMessageRadius; + border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius); + display: flex; + padding: 0.75em; + } + + .created-at { + position: relative; + float: right; + font-size: 0.8em; + margin: -1em 0 -0.5em 0; + font-style: italic; + opacity: 0.8; + } + + .without-attachment { + .status-content { + &::after { + margin-right: 5.4em; + content: " "; + display: inline-block; + } + } + } + + .incoming { + a { + color: var(--chatMessageIncomingLink, $fallback--link); + } + + .status { + color: var(--chatMessageIncomingText, $fallback--text); + background-color: var(--chatMessageIncomingBg, $fallback--bg); + border: 1px solid var(--chatMessageIncomingBorder, --border); + } + + .created-at { + a { + color: var(--chatMessageIncomingText, $fallback--text); + } + } + + .chat-message-menu { + left: 0.4rem; + } + } + + .outgoing { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-content: end; + justify-content: flex-end; + + a { + color: var(--chatMessageOutgoingLink, $fallback--link); + } + + .status { + color: var(--chatMessageOutgoingText, $fallback--text); + background-color: var(--chatMessageOutgoingBg, $fallback--lightBg); + border: 1px solid var(--chatMessageOutgoingBorder, --lightBg); + } + + .chat-message-inner { + align-items: flex-end; + } + + .chat-message-menu { + right: 0.4rem; + } + } + + .visible { + opacity: 1; + } +} + +.chat-message-date-separator { + text-align: center; + margin: 1.4em 0; + font-size: 0.9em; + user-select: none; + color: $fallback--text; + color: var(--faintedText, $fallback--text); +} diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue @@ -0,0 +1,99 @@ +<template> + <div + v-if="isMessage" + class="chat-message-wrapper" + :class="{ 'hovered-message-chain': hoveredMessageChain }" + @mouseover="onHover(true)" + @mouseleave="onHover(false)" + > + <div + class="chat-message" + :class="[{ 'outgoing': isCurrentUser, 'incoming': !isCurrentUser }]" + > + <div + v-if="!isCurrentUser" + class="avatar-wrapper" + > + <router-link + v-if="chatViewItem.isHead" + :to="userProfileLink" + > + <UserAvatar + :compact="true" + :better-shadow="betterShadow" + :user="author" + /> + </router-link> + </div> + <div class="chat-message-inner"> + <div + class="status-body" + :style="{ 'min-width': message.attachment ? '80%' : '' }" + > + <div + class="media status" + :class="{ 'without-attachment': !hasAttachment }" + style="position: relative" + @mouseenter="hovered = true" + @mouseleave="hovered = false" + > + <div + class="chat-message-menu" + :class="{ 'visible': hovered || menuOpened }" + > + <Popover + trigger="click" + placement="top" + :bound-to-selector="isCurrentUser ? '' : '.scrollable-message-list'" + :bound-to="{ x: 'container' }" + :margin="popoverMarginStyle" + @show="menuOpened = true" + @close="menuOpened = false" + > + <div slot="content"> + <div class="dropdown-menu"> + <button + class="dropdown-item dropdown-item-icon" + @click="deleteMessage" + > + <i class="icon-cancel" /> {{ $t("chats.delete") }} + </button> + </div> + </div> + <button + slot="trigger" + :title="$t('chats.more')" + > + <i class="icon-ellipsis" /> + </button> + </Popover> + </div> + <StatusContent + :status="messageForStatusContent" + :full-content="true" + > + <span + slot="footer" + class="created-at" + > + {{ createdAt }} + </span> + </StatusContent> + </div> + </div> + </div> + </div> + </div> + <div + v-else + class="chat-message-date-separator" + > + <ChatMessageDate :date="chatViewItem.date" /> + </div> +</template> + +<script src="./chat_message.js" ></script> +<style lang="scss"> +@import './chat_message.scss'; + +</style> diff --git a/src/components/chat_message_date/chat_message_date.vue b/src/components/chat_message_date/chat_message_date.vue @@ -0,0 +1,24 @@ +<template> + <time> + {{ displayDate }} + </time> +</template> + +<script> +export default { + name: 'Timeago', + props: ['date'], + computed: { + displayDate () { + const today = new Date() + today.setHours(0, 0, 0, 0) + + if (this.date.getTime() === today.getTime()) { + return this.$t('display_date.today') + } else { + return this.date.toLocaleDateString('en', { day: 'numeric', month: 'long' }) + } + } + } +} +</script> diff --git a/src/components/chat_new/chat_new.js b/src/components/chat_new/chat_new.js @@ -0,0 +1,73 @@ +import { mapState, mapGetters } from 'vuex' +import BasicUserCard from '../basic_user_card/basic_user_card.vue' +import UserAvatar from '../user_avatar/user_avatar.vue' + +const chatNew = { + components: { + BasicUserCard, + UserAvatar + }, + data () { + return { + suggestions: [], + userIds: [], + loading: false, + query: '' + } + }, + async created () { + const { chats } = await this.backendInteractor.chats() + chats.forEach(chat => this.suggestions.push(chat.account)) + }, + computed: { + users () { + return this.userIds.map(userId => this.findUser(userId)) + }, + availableUsers () { + if (this.query.length !== 0) { + return this.users + } else { + return this.suggestions + } + }, + ...mapState({ + currentUser: state => state.users.currentUser, + backendInteractor: state => state.api.backendInteractor + }), + ...mapGetters(['findUser']) + }, + methods: { + goBack () { + this.$emit('cancel') + }, + goToChat (user) { + this.$router.push({ name: 'chat', params: { recipient_id: user.id } }) + }, + onInput () { + this.search(this.query) + }, + addUser (user) { + this.selectedUserIds.push(user.id) + this.query = '' + }, + removeUser (userId) { + this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId) + }, + search (query) { + if (!query) { + this.loading = false + return + } + + this.loading = true + this.userIds = [] + this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts' }) + .then(data => { + this.loading = false + this.userIds = data.accounts.map(a => a.id) + }) + } + } +} + +export default chatNew diff --git a/src/components/chat_new/chat_new.scss b/src/components/chat_new/chat_new.scss @@ -0,0 +1,29 @@ +.chat-new { + .input-wrap { + display: flex; + margin: 0.7em 0.5em 0.7em 0.5em; + + input { + width: 100%; + } + } + + .icon-search { + font-size: 1.5em; + float: right; + margin-right: 0.3em; + } + + .member-list { + padding-bottom: 0.7rem; + } + + .basic-user-card:hover { + cursor: pointer; + background-color: var(--selectedPost, $fallback--lightBg); + } + + .go-back-button { + cursor: pointer; + } +} diff --git a/src/components/chat_new/chat_new.vue b/src/components/chat_new/chat_new.vue @@ -0,0 +1,46 @@ +<template> + <div + id="nav" + class="panel-default panel chat-new" + > + <div + ref="header" + class="panel-heading" + > + <a + class="go-back-button" + @click="goBack" + > + <i class="button-icon icon-left-open" /> + </a> + </div> + <div class="input-wrap"> + <div class="input-search"> + <i class="button-icon icon-search" /> + </div> + <input + ref="search" + v-model="query" + placeholder="Search people" + @input="onInput" + > + </div> + <div class="member-list"> + <div + v-for="user in availableUsers" + :key="user.id" + class="member" + > + <div @click.capture.prevent="goToChat(user)"> + <BasicUserCard :user="user" /> + </div> + </div> + </div> + </div> +</template> + +<script src="./chat_new.js"></script> +<style lang="scss"> +@import '../../_variables.scss'; +@import './chat_new.scss'; +</style> diff --git a/src/components/chat_panel/chat_panel.vue b/src/components/chat_panel/chat_panel.vue @@ -10,7 +10,7 @@ @click.stop.prevent="togglePanel" > <div class="title"> - <span>{{ $t('chat.title') }}</span> + <span>{{ $t('shoutbox.title') }}</span> <i v-if="floating" class="icon-cancel" @@ -64,7 +64,7 @@ > <div class="title"> <i class="icon-comment-empty" /> - {{ $t('chat.title') }} + {{ $t('shoutbox.title') }} </div> </div> </div> @@ -84,54 +84,56 @@ max-width: 25em; } -.chat-heading { - cursor: pointer; - .icon-comment-empty { - color: $fallback--text; - color: var(--text, $fallback--text); +.chat-panel { + .chat-heading { + cursor: pointer; + .icon-comment-empty { + color: $fallback--text; + color: var(--text, $fallback--text); + } } -} - -.chat-window { - overflow-y: auto; - overflow-x: hidden; - max-height: 20em; -} -.chat-window-container { - height: 100%; -} + .chat-window { + overflow-y: auto; + overflow-x: hidden; + max-height: 20em; + } -.chat-message { - display: flex; - padding: 0.2em 0.5em -} + .chat-window-container { + height: 100%; + } -.chat-avatar { - img { - height: 24px; - width: 24px; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); - margin-right: 0.5em; - margin-top: 0.25em; + .chat-message { + display: flex; + padding: 0.2em 0.5em } -} -.chat-input { - display: flex; - textarea { - flex: 1; - margin: 0.6em; - min-height: 3.5em; - resize: none; + .chat-avatar { + img { + height: 24px; + width: 24px; + border-radius: $fallback--avatarRadius; + border-radius: var(--avatarRadius, $fallback--avatarRadius); + margin-right: 0.5em; + margin-top: 0.25em; + } } -} -.chat-panel { - .title { + .chat-input { display: flex; - justify-content: space-between; + textarea { + flex: 1; + margin: 0.6em; + min-height: 3.5em; + resize: none; + } + } + + .chat-panel { + .title { + display: flex; + justify-content: space-between; + } } } </style> diff --git a/src/components/chat_title/chat_title.js b/src/components/chat_title/chat_title.js @@ -0,0 +1,26 @@ +import Vue from 'vue' +import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +import UserAvatar from '../user_avatar/user_avatar.vue' + +export default Vue.component('chat-title', { + name: 'ChatTitle', + components: { + UserAvatar + }, + props: [ + 'user', 'withAvatar' + ], + computed: { + title () { + return this.user ? this.user.screen_name : '' + }, + htmlTitle () { + return this.user ? this.user.name_html : '' + } + }, + methods: { + getUserProfileLink (user) { + return generateProfileLink(user.id, user.screen_name) + } + } +}) diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue @@ -0,0 +1,67 @@ +<template> + <!-- eslint-disable vue/no-v-html --> + <div + class="chat-title" + :title="title" + > + <router-link + v-if="withAvatar && user" + :to="getUserProfileLink(user)" + > + <UserAvatar + :user="user" + width="23px" + height="23px" + /> + </router-link> + <span + class="username" + v-html="htmlTitle" + /> + </div> + <!-- eslint-enable vue/no-v-html --> +</template> + +<script src="./chat_title.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.chat-title { + display: flex; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + align-items: center; + + .username { + max-width: 100%; + text-overflow: ellipsis; + white-space: nowrap; + display: inline; + word-wrap: break-word; + overflow: hidden; + text-overflow: ellipsis; + + .emoji { + width: 14px; + height: 14px; + vertical-align: middle; + object-fit: contain + } + } + + .Avatar { + width: 23px; + height: 23px; + margin-right: 0.5em; + + border-radius: $fallback--avatarAltRadius; + border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + + &.animated::before { + display: none; + } + } +} +</style> diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue @@ -52,7 +52,7 @@ export default { right: 0; top: 0; display: block; - content: '✔'; + content: '✓'; transition: color 200ms; width: 1.1em; height: 1.1em; diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue @@ -1,7 +1,7 @@ <template> <div - class="timeline panel-default" - :class="[isExpanded ? 'panel' : 'panel-disabled']" + class="Conversation" + :class="{ '-expanded' : isExpanded, 'panel' : isExpanded }" > <div v-if="isExpanded" @@ -28,7 +28,7 @@ :replies="getReplies(status.id)" :in-profile="inProfile" :profile-user-id="profileUserId" - class="status-fadein panel-body" + class="conversation-status status-fadein panel-body" @goto="setHighlight" @toggleExpanded="toggleExpanded" /> @@ -40,14 +40,27 @@ <style lang="scss"> @import '../../_variables.scss'; -.timeline { - .panel-disabled { - .status-el { - border-left: none; - border-bottom-width: 1px; - border-bottom-style: solid; +.Conversation { + .conversation-status { + border-left: none; + border-bottom-width: 1px; + border-bottom-style: solid; + border-bottom-color: var(--border, $fallback--border); + border-radius: 0; + } + + &.-expanded { + .conversation-status { + border-color: $fallback--border; border-color: var(--border, $fallback--border); - border-radius: 0; + border-left: 4px solid $fallback--cRed; + border-left: 4px solid var(--cRed, $fallback--cRed); + } + + .conversation-status:last-child { + border-bottom: none; + border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; + border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); } } } diff --git a/src/components/domain_mute_card/domain_mute_card.js b/src/components/domain_mute_card/domain_mute_card.js @@ -5,9 +5,20 @@ const DomainMuteCard = { components: { ProgressButton }, + computed: { + user () { + return this.$store.state.users.currentUser + }, + muted () { + return this.user.domainMutes.includes(this.domain) + } + }, methods: { unmuteDomain () { return this.$store.dispatch('unmuteDomain', this.domain) + }, + muteDomain () { + return this.$store.dispatch('muteDomain', this.domain) } } } diff --git a/src/components/domain_mute_card/domain_mute_card.vue b/src/components/domain_mute_card/domain_mute_card.vue @@ -4,6 +4,7 @@ {{ domain }} </div> <ProgressButton + v-if="muted" :click="unmuteDomain" class="btn btn-default" > @@ -12,6 +13,16 @@ {{ $t('domain_mute_card.unmute_progress') }} </template> </ProgressButton> + <ProgressButton + v-else + :click="muteDomain" + class="btn btn-default" + > + {{ $t('domain_mute_card.mute') }} + <template slot="progress"> + {{ $t('domain_mute_card.mute_progress') }} + </template> + </ProgressButton> </div> </template> @@ -34,5 +45,9 @@ button { width: 10em; } + + .autosuggest-results & { + padding-left: 1em; + } } </style> diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js @@ -79,6 +79,20 @@ const EmojiInput = { required: false, type: Boolean, default: false + }, + placement: { + /** + * Forces the panel to take a specific position relative to the input element. + * The 'auto' placement chooses either bottom or top depending on which has the available space (when both have available space, bottom is preferred). + */ + required: false, + type: String, // 'auto', 'top', 'bottom' + default: 'auto' + }, + newlineOnCtrlEnter: { + required: false, + type: Boolean, + default: false } }, data () { @@ -162,6 +176,11 @@ const EmojiInput = { input.elm.removeEventListener('input', this.onInput) } }, + watch: { + showSuggestions: function (newValue) { + this.$emit('shown', newValue) + } + }, methods: { triggerShowPicker () { this.showPicker = true @@ -190,7 +209,7 @@ const EmojiInput = { this.$emit('input', newValue) this.caret = 0 }, - insert ({ insertion, keepOpen }) { + insert ({ insertion, keepOpen, surroundingSpace = true }) { const before = this.value.substring(0, this.caret) || '' const after = this.value.substring(this.caret) || '' @@ -209,8 +228,8 @@ const EmojiInput = { * them, masto seem to be rendering :emoji::emoji: correctly now so why not */ const isSpaceRegex = /\s/ - const spaceBefore = !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0 ? ' ' : '' - const spaceAfter = !isSpaceRegex.exec(after[0]) && this.padEmoji ? ' ' : '' + const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : '' + const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : '' const newValue = [ before, @@ -367,6 +386,18 @@ const EmojiInput = { }, onKeyDown (e) { const { ctrlKey, shiftKey, key } = e + if (this.newlineOnCtrlEnter && ctrlKey && key === 'Enter') { + this.insert({ insertion: '\n', surroundingSpace: false }) + // Ensure only one new line is added on macos + e.stopPropagation() + e.preventDefault() + + // Scroll the input element to the position of the cursor + this.$nextTick(() => { + this.input.elm.blur() + this.input.elm.focus() + }) + } // Disable suggestions hotkeys if suggestions are hidden if (!this.temporarilyHideSuggestions) { if (key === 'Tab') { @@ -425,14 +456,29 @@ const EmojiInput = { this.caret = selectionStart }, resize () { - const { panel, picker } = this.$refs + const panel = this.$refs.panel if (!panel) return + const picker = this.$refs.picker.$el + const panelBody = this.$refs['panel-body'] const { offsetHeight, offsetTop } = this.input.elm const offsetBottom = offsetTop + offsetHeight - panel.style.top = offsetBottom + 'px' - picker.$el.style.top = offsetBottom + 'px' - picker.$el.style.bottom = 'auto' + this.setPlacement(panelBody, panel, offsetBottom) + this.setPlacement(picker, picker, offsetBottom) + }, + setPlacement (container, target, offsetBottom) { + if (!container || !target) return + + target.style.top = offsetBottom + 'px' + target.style.bottom = 'auto' + + if (this.placement === 'top' || (this.placement === 'auto' && this.overflowsBottom(container))) { + target.style.top = 'auto' + target.style.bottom = this.input.elm.offsetHeight + 'px' + } + }, + overflowsBottom (el) { + return el.getBoundingClientRect().bottom > window.innerHeight } } } diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue @@ -29,7 +29,10 @@ class="autocomplete-panel" :class="{ hide: !showSuggestions }" > - <div class="autocomplete-panel-body"> + <div + ref="panel-body" + class="autocomplete-panel-body" + > <div v-for="(suggestion, index) in suggestions" :key="index" diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js @@ -13,7 +13,7 @@ import { debounce } from 'lodash' const debounceUserSearch = debounce((data, input) => { data.updateUsersList(input) -}, 500, { leading: true, trailing: false }) +}, 500) export default data => input => { const firstChar = input[0] @@ -97,8 +97,8 @@ export const suggestUsers = data => input => { replacement: '@' + screen_name + ' ' })) - // BE search users if there are no matches - if (newUsers.length === 0 && data.updateUsersList) { + // BE search users to get more comprehensive results + if (data.updateUsersList) { debounceUserSearch(data, noPrefix) } return newUsers diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js @@ -1,5 +1,5 @@ import UserAvatar from '../user_avatar/user_avatar.vue' -import Popover from '../popover/popover.vue' +import UserListPopover from '../user_list_popover/user_list_popover.vue' const EMOJI_REACTION_COUNT_CUTOFF = 12 @@ -7,7 +7,7 @@ const EmojiReactions = { name: 'EmojiReactions', components: { UserAvatar, - Popover + UserListPopover }, props: ['status'], data: () => ({ diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue @@ -1,44 +1,11 @@ <template> <div class="emoji-reactions"> - <Popover + <UserListPopover v-for="(reaction) in emojiReactions" :key="reaction.name" - trigger="hover" - placement="top" - :offset="{ y: 5 }" + :users="accountsForEmoji[reaction.name]" > - <div - slot="content" - class="reacted-users" - > - <div v-if="accountsForEmoji[reaction.name].length"> - <div - v-for="(account) in accountsForEmoji[reaction.name]" - :key="account.id" - class="reacted-user" - > - <UserAvatar - :user="account" - class="avatar-small" - :compact="true" - /> - <div class="reacted-user-names"> - <!-- eslint-disable vue/no-v-html --> - <span - class="reacted-user-name" - v-html="account.name_html" - /> - <!-- eslint-enable vue/no-v-html --> - <span class="reacted-user-screen-name">{{ account.screen_name }}</span> - </div> - </div> - </div> - <div v-else> - <i class="icon-spin4 animate-spin" /> - </div> - </div> <button - slot="trigger" class="emoji-reaction btn btn-default" :class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }" @click="emojiOnClick(reaction.name, $event)" @@ -47,7 +14,7 @@ <span class="reaction-emoji">{{ reaction.name }}</span> <span>{{ reaction.count }}</span> </button> - </Popover> + </UserListPopover> <a v-if="tooManyReactions" class="emoji-reaction-expand faint" @@ -69,32 +36,6 @@ flex-wrap: wrap; } -.reacted-users { - padding: 0.5em; -} - -.reacted-user { - padding: 0.25em; - display: flex; - flex-direction: row; - - .reacted-user-names { - display: flex; - flex-direction: column; - margin-left: 0.5em; - min-width: 5em; - - img { - width: 1em; - height: 1em; - } - } - - .reacted-user-screen-name { - font-size: 9px; - } -} - .emoji-reaction { padding: 0 0.5em; margin-right: 0.5em; diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js @@ -34,6 +34,16 @@ const ExtraButtons = { navigator.clipboard.writeText(this.statusLink) .then(() => this.$emit('onSuccess')) .catch(err => this.$emit('onError', err.error.error)) + }, + bookmarkStatus () { + this.$store.dispatch('bookmark', { id: this.status.id }) + .then(() => this.$emit('onSuccess')) + .catch(err => this.$emit('onError', err.error.error)) + }, + unbookmarkStatus () { + this.$store.dispatch('unbookmark', { id: this.status.id }) + .then(() => this.$emit('onSuccess')) + .catch(err => this.$emit('onError', err.error.error)) } }, computed: { diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue @@ -3,6 +3,7 @@ trigger="click" placement="top" class="extra-button-popover" + :bound-to="{ x: 'container' }" > <div slot="content" @@ -40,6 +41,22 @@ <i class="icon-pin" /><span>{{ $t("status.unpin") }}</span> </button> <button + v-if="!status.bookmarked" + class="dropdown-item dropdown-item-icon" + @click.prevent="bookmarkStatus" + @click="close" + > + <i class="icon-bookmark-empty" /><span>{{ $t("status.bookmark") }}</span> + </button> + <button + v-if="status.bookmarked" + class="dropdown-item dropdown-item-icon" + @click.prevent="unbookmarkStatus" + @click="close" + > + <i class="icon-bookmark" /><span>{{ $t("status.unbookmark") }}</span> + </button> + <button v-if="canDelete" class="dropdown-item dropdown-item-icon" @click.prevent="deleteStatus" diff --git a/src/components/features_panel/features_panel.js b/src/components/features_panel/features_panel.js @@ -1,6 +1,7 @@ const FeaturesPanel = { computed: { chat: function () { return this.$store.state.instance.chatAvailable }, + pleromaChatMessages: function () { return this.$store.state.instance.pleromaChatMessagesAvailable }, gopher: function () { return this.$store.state.instance.gopherAvailable }, whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled }, mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable }, diff --git a/src/components/features_panel/features_panel.vue b/src/components/features_panel/features_panel.vue @@ -11,6 +11,9 @@ <li v-if="chat"> {{ $t('features_panel.chat') }} </li> + <li v-if="pleromaChatMessages"> + {{ $t('features_panel.pleroma_chat_messages') }} + </li> <li v-if="gopher"> {{ $t('features_panel.gopher') }} </li> diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue @@ -2,7 +2,7 @@ <basic-user-card :user="user"> <div class="follow-card-content-container"> <span - v-if="!noFollowsYou && relationship.followed_by" + v-if="isMe || (!noFollowsYou && relationship.followed_by)" class="faint" > {{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }} @@ -15,7 +15,7 @@ <RemoteFollow :user="user" /> </div> </template> - <template v-else> + <template v-else-if="!isMe"> <FollowButton :relationship="relationship" :label-following="$t('user_card.follow_unfollow')" diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue @@ -50,9 +50,7 @@ align-content: stretch; } - // FIXME: specificity problem with this and .attachments.attachment - // we shouldn't have the need for .image here - .attachment.image { + .gallery-row-inner .attachment { margin: 0 0.5em 0 0; flex-grow: 1; height: 100%; @@ -78,6 +76,7 @@ video, canvas { object-fit: contain; + height: 100%; } } diff --git a/src/components/global_notice_list/global_notice_list.js b/src/components/global_notice_list/global_notice_list.js @@ -0,0 +1,15 @@ + +const GlobalNoticeList = { + computed: { + notices () { + return this.$store.state.interface.globalNotices + } + }, + methods: { + closeNotice (notice) { + this.$store.dispatch('removeGlobalNotice', notice) + } + } +} + +export default GlobalNoticeList diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue @@ -0,0 +1,77 @@ +<template> + <div class="global-notice-list"> + <div + v-for="(notice, index) in notices" + :key="index" + class="alert global-notice" + :class="{ ['global-' + notice.level]: true }" + > + <div class="notice-message"> + {{ $t(notice.messageKey, notice.messageArgs) }} + </div> + <i + class="button-icon icon-cancel" + @click="closeNotice(notice)" + /> + </div> + </div> +</template> + +<script src="./global_notice_list.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.global-notice-list { + position: fixed; + top: 50px; + width: 100%; + pointer-events: none; + z-index: 1001; + display: flex; + flex-direction: column; + align-items: center; + + .global-notice { + pointer-events: auto; + text-align: center; + width: 40em; + max-width: calc(100% - 3em); + display: flex; + padding-left: 1.5em; + line-height: 2em; + .notice-message { + flex: 1 1 100%; + } + i { + flex: 0 0; + width: 1.5em; + cursor: pointer; + } + } + + .global-error { + background-color: var(--alertPopupError, $fallback--cRed); + color: var(--alertPopupErrorText, $fallback--text); + i { + color: var(--alertPopupErrorText, $fallback--text); + } + } + + .global-warning { + background-color: var(--alertPopupWarning, $fallback--cOrange); + color: var(--alertPopupWarningText, $fallback--text); + i { + color: var(--alertPopupWarningText, $fallback--text); + } + } + + .global-info { + background-color: var(--alertPopupNeutral, $fallback--fg); + color: var(--alertPopupNeutralText, $fallback--text); + i { + color: var(--alertPopupNeutralText, $fallback--text); + } + } +} +</style> diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -32,7 +32,7 @@ import _ from 'lodash' export default { computed: { languageCodes () { - return Object.keys(languagesObject) + return languagesObject.languages }, languageNames () { @@ -43,7 +43,6 @@ export default { get: function () { return this.$store.getters.mergedConfig.interfaceLanguage }, set: function (val) { this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) - this.$i18n.locale = val } } }, diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js @@ -84,10 +84,12 @@ const MediaModal = { } }, mounted () { + window.addEventListener('popstate', this.hide) document.addEventListener('keyup', this.handleKeyupEvent) document.addEventListener('keydown', this.handleKeydownEvent) }, destroyed () { + window.removeEventListener('popstate', this.hide) document.removeEventListener('keyup', this.handleKeyupEvent) document.removeEventListener('keydown', this.handleKeydownEvent) } diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue @@ -8,6 +8,8 @@ v-if="type === 'image'" class="modal-image" :src="currentMedia.url" + :alt="currentMedia.description" + :title="currentMedia.description" @touchstart.stop="mediaTouchStart" @touchmove.stop="mediaTouchMove" @click="hide" @@ -18,6 +20,14 @@ :attachment="currentMedia" :controls="true" /> + <audio + v-if="type === 'audio'" + class="modal-image" + :src="currentMedia.url" + :alt="currentMedia.description" + :title="currentMedia.description" + controls + /> <button v-if="canNavigate" :title="$t('media_modal.previous')" diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js @@ -5,10 +5,15 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for const mediaUpload = { data () { return { - uploading: false, + uploadCount: 0, uploadReady: true } }, + computed: { + uploading () { + return this.uploadCount > 0 + } + }, methods: { uploadFile (file) { const self = this @@ -23,29 +28,21 @@ const mediaUpload = { formData.append('file', file) self.$emit('uploading') - self.uploading = true + self.uploadCount++ statusPosterService.uploadMedia({ store, formData }) .then((fileData) => { self.$emit('uploaded', fileData) - self.uploading = false + self.decreaseUploadCount() }, (error) => { // eslint-disable-line handle-callback-err self.$emit('upload-failed', 'default') - self.uploading = false + self.decreaseUploadCount() }) }, - fileDrop (e) { - if (e.dataTransfer.files.length > 0) { - e.preventDefault() // allow dropping text like before - this.uploadFile(e.dataTransfer.files[0]) - } - }, - fileDrag (e) { - let types = e.dataTransfer.types - if (types.contains('Files')) { - e.dataTransfer.dropEffect = 'copy' - } else { - e.dataTransfer.dropEffect = 'none' + decreaseUploadCount () { + this.uploadCount-- + if (this.uploadCount === 0) { + this.$emit('all-uploaded') } }, clearFile () { @@ -54,20 +51,23 @@ const mediaUpload = { this.uploadReady = true }) }, - change ({ target }) { - for (var i = 0; i < target.files.length; i++) { - let file = target.files[i] + multiUpload (files) { + for (const file of files) { this.uploadFile(file) } + }, + change ({ target }) { + this.multiUpload(target.files) } }, props: [ - 'dropFiles' + 'dropFiles', + 'disabled' ], watch: { 'dropFiles': function (fileInfos) { if (!this.uploading) { - this.uploadFile(fileInfos[0]) + this.multiUpload(fileInfos) } } } diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue @@ -1,9 +1,7 @@ <template> <div class="media-upload" - @drop.prevent - @dragover.prevent="fileDrag" - @drop="fileDrop" + :class="{ disabled: disabled }" > <label class="label" @@ -19,6 +17,7 @@ /> <input v-if="uploadReady" + :disabled="disabled" type="file" style="position: fixed; top: -100em" multiple="true" @@ -31,6 +30,8 @@ <script src="./media_upload.js" ></script> <style lang="scss"> +@import '../../_variables.scss'; + .media-upload { .label { display: inline-block; diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js @@ -2,6 +2,7 @@ import SideDrawer from '../side_drawer/side_drawer.vue' import Notifications from '../notifications/notifications.vue' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' import GestureService from '../../services/gesture_service/gesture_service' +import { mapGetters } from 'vuex' const MobileNav = { components: { @@ -30,7 +31,11 @@ const MobileNav = { return this.unseenNotifications.length }, hideSitename () { return this.$store.state.instance.hideSitename }, - sitename () { return this.$store.state.instance.name } + sitename () { return this.$store.state.instance.name }, + isChat () { + return this.$route.name === 'chat' + }, + ...mapGetters(['unreadChatCount']) }, methods: { toggleMobileSidebar () { @@ -64,7 +69,7 @@ const MobileNav = { this.$refs.notifications.markAsSeen() }, onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) { - if (this.$store.getters.mergedConfig.autoLoad && scrollTop + clientHeight >= scrollHeight) { + if (scrollTop + clientHeight >= scrollHeight) { this.$refs.notifications.fetchOlderNotifications() } } diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue @@ -3,6 +3,7 @@ <nav id="nav" class="nav-bar container" + :class="{ 'mobile-hidden': isChat }" > <div class="mobile-inner-nav" @@ -15,6 +16,10 @@ @click.stop.prevent="toggleMobileSidebar()" > <i class="button-icon icon-menu" /> + <div + v-if="unreadChatCount" + class="alert-dot" + /> </a> <router-link v-if="!hideSitename" diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js @@ -1,5 +1,10 @@ import { debounce } from 'lodash' +const HIDDEN_FOR_PAGES = new Set([ + 'chats', + 'chat' +]) + const MobilePostStatusButton = { data () { return { @@ -27,6 +32,8 @@ const MobilePostStatusButton = { return !!this.$store.state.users.currentUser }, isHidden () { + if (HIDDEN_FOR_PAGES.has(this.$route.name)) { return true } + return this.autohideFloatingPostButton && (this.hidden || this.inputActive) }, autohideFloatingPostButton () { diff --git a/src/components/modal/modal.vue b/src/components/modal/modal.vue @@ -1,8 +1,9 @@ <template> <div v-show="isOpen" - v-body-scroll-lock="isOpen" + v-body-scroll-lock="isOpen && !noBackground" class="modal-view" + :class="classes" @click.self="$emit('backdropClicked')" > <slot /> @@ -15,6 +16,18 @@ export default { isOpen: { type: Boolean, default: true + }, + noBackground: { + type: Boolean, + default: false + } + }, + computed: { + classes () { + return { + 'modal-background': !this.noBackground, + 'open': this.isOpen + } } } } @@ -32,12 +45,22 @@ export default { justify-content: center; align-items: center; overflow: auto; + pointer-events: none; animation-duration: 0.2s; - background-color: rgba(0, 0, 0, 0.5); animation-name: modal-background-fadein; + opacity: 0; + + > * { + pointer-events: initial; + } + + &.modal-background { + pointer-events: initial; + background-color: rgba(0, 0, 0, 0.5); + } - body:not(.scroll-locked) & { - opacity: 0; + &.open { + opacity: 1; } } diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js @@ -1,4 +1,5 @@ -import { mapState } from 'vuex' +import { timelineNames } from '../timeline_menu/timeline_menu.js' +import { mapState, mapGetters } from 'vuex' const NavPanel = { created () { @@ -6,13 +7,25 @@ const NavPanel = { this.$store.dispatch('startFetchingFollowRequests') } }, - computed: mapState({ - currentUser: state => state.users.currentUser, - chat: state => state.chat.channel, - followRequestCount: state => state.api.followRequests.length, - privateMode: state => state.instance.private, - federating: state => state.instance.federating - }) + computed: { + onTimelineRoute () { + return !!timelineNames()[this.$route.name] + }, + timelinesRoute () { + if (this.$store.state.interface.lastTimeline) { + return this.$store.state.interface.lastTimeline + } + return this.currentUser ? 'friends' : 'public-timeline' + }, + ...mapState({ + currentUser: state => state.users.currentUser, + followRequestCount: state => state.api.followRequests.length, + privateMode: state => state.instance.private, + federating: state => state.instance.federating, + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable + }), + ...mapGetters(['unreadChatCount']) + } } export default NavPanel diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue @@ -2,9 +2,12 @@ <div class="nav-panel"> <div class="panel panel-default"> <ul> - <li v-if="currentUser"> - <router-link :to="{ name: 'friends' }"> - <i class="button-icon icon-home-2" /> {{ $t("nav.timeline") }} + <li v-if="currentUser || !privateMode"> + <router-link + :to="{ name: timelinesRoute }" + :class="onTimelineRoute && 'router-link-active'" + > + <i class="button-icon icon-home-2" /> {{ $t("nav.timelines") }} </router-link> </li> <li v-if="currentUser"> @@ -12,9 +15,15 @@ <i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }} </router-link> </li> - <li v-if="currentUser"> - <router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }"> - <i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }} + <li v-if="currentUser && pleromaChatMessagesAvailable"> + <router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }"> + <div + v-if="unreadChatCount" + class="badge badge-notification unread-chat-count" + > + {{ unreadChatCount }} + </div> + <i class="button-icon icon-chat" /> {{ $t("nav.chats") }} </router-link> </li> <li v-if="currentUser && currentUser.locked"> @@ -28,16 +37,6 @@ </span> </router-link> </li> - <li v-if="currentUser || !privateMode"> - <router-link :to="{ name: 'public-timeline' }"> - <i class="button-icon icon-users" /> {{ $t("nav.public_tl") }} - </router-link> - </li> - <li v-if="federating && (currentUser || !privateMode)"> - <router-link :to="{ name: 'public-external-timeline' }"> - <i class="button-icon icon-globe" /> {{ $t("nav.twkn") }} - </router-link> - </li> <li> <router-link :to="{ name: 'about' }"> <i class="button-icon icon-info-circled" /> {{ $t("nav.about") }} diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js @@ -1,3 +1,5 @@ +import StatusContent from '../status_content/status_content.vue' +import { mapState } from 'vuex' import Status from '../status/status.vue' import UserAvatar from '../user_avatar/user_avatar.vue' import UserCard from '../user_card/user_card.vue' @@ -16,10 +18,11 @@ const Notification = { }, props: [ 'notification' ], components: { - Status, + StatusContent, UserAvatar, UserCard, - Timeago + Timeago, + Status }, methods: { toggleUserExpanded () { @@ -79,7 +82,10 @@ const Notification = { }, isStatusNotification () { return isStatusNotification(this.notification.type) - } + }, + ...mapState({ + currentUser: state => state.users.currentUser + }) } } diff --git a/src/components/notification/notification.scss b/src/components/notification/notification.scss @@ -0,0 +1,52 @@ +// TODO Copypaste from Status, should unify it somehow +.Notification { + &.-muted { + padding: 0.25em 0.6em; + height: 1.2em; + line-height: 1.2em; + text-overflow: ellipsis; + overflow: hidden; + display: flex; + flex-wrap: nowrap; + + & .status-username, + & .mute-thread, + & .mute-words { + word-wrap: normal; + word-break: normal; + white-space: nowrap; + } + + & .status-username, + & .mute-words { + text-overflow: ellipsis; + overflow: hidden; + } + + .status-username { + font-weight: normal; + flex: 0 1 auto; + margin-right: 0.2em; + font-size: smaller; + } + + .mute-thread { + flex: 0 0 auto; + } + + .mute-words { + flex: 1 0 5em; + margin-left: 0.2em; + + &::before { + content: ' '; + } + } + + .unmute { + flex: 0 0 auto; + margin-left: auto; + display: block; + } + } +} diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue @@ -7,7 +7,7 @@ <div v-else> <div v-if="needMute && !unmuted" - class="container muted" + class="Notification container -muted" > <small> <router-link :to="userProfileLink"> @@ -157,11 +157,9 @@ </router-link> </div> <template v-else> - <status + <status-content class="faint" - :compact="true" - :statusoid="notification.action" - :no-heading="true" + :status="notification.action" /> </template> </div> @@ -170,3 +168,4 @@ </template> <script src="./notification.js"></script> +<style src="./notification.scss" lang="scss"></style> diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js @@ -1,3 +1,4 @@ +import { mapGetters } from 'vuex' import Notification from '../notification/notification.vue' import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js' import { @@ -27,6 +28,11 @@ const Notifications = { seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT } }, + created () { + const store = this.$store + const credentials = store.state.users.currentUser.credentials + notificationsFetcher.fetchAndUpdate({ store, credentials }) + }, computed: { mainClass () { return this.minimalMode ? '' : 'panel panel-default' @@ -46,23 +52,22 @@ const Notifications = { unseenCount () { return this.unseenNotifications.length }, + unseenCountTitle () { + return this.unseenCount + (this.unreadChatCount) + }, loading () { return this.$store.state.statuses.notifications.loading }, notificationsToDisplay () { return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount) - } + }, + ...mapGetters(['unreadChatCount']) }, components: { Notification }, - created () { - const { dispatch } = this.$store - - dispatch('fetchAndUpdateNotifications') - }, watch: { - unseenCount (count) { + unseenCountTitle (count) { if (count > 0) { this.$store.dispatch('setPageTitle', `(${count})`) } else { diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss @@ -36,8 +36,10 @@ border-bottom: 1px solid; border-color: $fallback--border; border-color: var(--border, $fallback--border); + word-wrap: break-word; + word-break: break-word; - &:hover .animated.avatar { + &:hover .animated.Avatar { canvas { display: none; } @@ -46,37 +48,20 @@ } } - .muted { - padding: .25em .6em; - } - .non-mention { display: flex; flex: 1; flex-wrap: nowrap; padding: 0.6em; min-width: 0; + .avatar-container { width: 32px; height: 32px; } - .status-el { - .status { - padding: 0.25em 0; - color: $fallback--faint; - color: var(--faint, $fallback--faint); - a { - color: var(--faintLink); - } - .status-content a { - color: var(--postFaintLink); - } - } - padding: 0; - .media-body { - margin: 0; - } - } + + --link: var(--faintLink); + --text: var(--faint); } .follow-request-accept { @@ -113,7 +98,8 @@ } } - .status-el { + /* TODO cleanup this */ + .Status { flex: 1; } @@ -125,6 +111,11 @@ flex: 1; padding-left: 0.8em; min-width: 0; + + .timeago { + min-width: 3em; + text-align: right; + } } .emoji-reaction-emoji { diff --git a/src/components/panel_loading/panel_loading.vue b/src/components/panel_loading/panel_loading.vue @@ -0,0 +1,29 @@ +<template> + <div class="panel-loading"> + <span class="loading-text"> + <i class="icon-spin4 animate-spin" /> + {{ $t('general.loading') }} + </span> + </div> +</template> + +<style lang="scss"> +@import 'src/_variables.scss'; + +.panel-loading { + display: flex; + height: 100%; + align-items: center; + justify-content: center; + font-size: 2em; + color: $fallback--text; + color: var(--text, $fallback--text); + .loading-text i { + font-size: 3em; + line-height: 0; + vertical-align: middle; + color: $fallback--text; + color: var(--text, $fallback--text); + } +} +</style> diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue @@ -17,7 +17,7 @@ <span class="result-percentage"> {{ percentageForOption(option.votes_count) }}% </span> - <span>{{ option.title }}</span> + <span v-html="option.title_html" /> </div> <div class="result-fill" @@ -96,6 +96,7 @@ align-items: center; padding: 0.1em 0.25em; z-index: 1; + word-break: break-word; } .result-percentage { width: 3.5em; diff --git a/src/components/poll/poll_form.js b/src/components/poll/poll_form.js @@ -75,6 +75,7 @@ export default { deleteOption (index, event) { if (this.options.length > 2) { this.options.splice(index, 1) + this.updatePollToParent() } }, convertExpiryToUnit (unit, amount) { diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js @@ -1,4 +1,3 @@ - const Popover = { name: 'Popover', props: { @@ -10,13 +9,18 @@ const Popover = { // 'container' for using offsetParent as boundaries for either axis // or 'viewport' boundTo: Object, + // Takes a selector to use as a replacement for the parent container + // for getting boundaries for x an y axis + boundToSelector: String, // Takes a top/bottom/left/right object, how much space to leave // between boundary and popover element margin: Object, // Takes a x/y object and tells how many pixels to offset from // anchor point on either axis offset: Object, - // Additional styles you may want for the popover container + // Replaces the classes you may want for the popover container. + // Use 'popover-default' in addition to get the default popover + // styles with your custom class. popoverClass: String }, data () { @@ -27,6 +31,10 @@ const Popover = { } }, methods: { + containerBoundingClientRect () { + const container = this.boundToSelector ? this.$el.closest(this.boundToSelector) : this.$el.offsetParent + return container.getBoundingClientRect() + }, updateStyles () { if (this.hidden) { this.styles = { @@ -45,7 +53,8 @@ const Popover = { // Minor optimization, don't call a slow reflow call if we don't have to const parentBounds = this.boundTo && (this.boundTo.x === 'container' || this.boundTo.y === 'container') && - this.$el.offsetParent.getBoundingClientRect() + this.containerBoundingClientRect() + const margin = this.margin || {} // What are the screen bounds for the popover? Viewport vs container @@ -99,7 +108,7 @@ const Popover = { // single translate or translate3d resulted in blurry text. this.styles = { opacity: 1, - transform: `translateX(${Math.floor(translateX)}px) translateY(${Math.floor(translateY)}px)` + transform: `translateX(${Math.round(translateX)}px) translateY(${Math.round(translateY)}px)` } }, showPopover () { diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue @@ -14,7 +14,7 @@ ref="content" :style="styles" class="popover" - :class="popoverClass" + :class="popoverClass || 'popover-default'" > <slot name="content" @@ -34,6 +34,9 @@ z-index: 8; position: absolute; min-width: 0; +} + +.popover-default { transition: opacity 0.3s; box-shadow: 1px 1px 4px rgba(0,0,0,.6); diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js @@ -3,11 +3,13 @@ import MediaUpload from '../media_upload/media_upload.vue' import ScopeSelector from '../scope_selector/scope_selector.vue' import EmojiInput from '../emoji_input/emoji_input.vue' import PollForm from '../poll/poll_form.vue' +import Attachment from '../attachment/attachment.vue' +import StatusContent from '../status_content/status_content.vue' import fileTypeService from '../../services/file_type/file_type.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js' -import { reject, map, uniqBy } from 'lodash' +import { reject, map, uniqBy, debounce } from 'lodash' import suggestor from '../emoji_input/suggestor.js' -import { mapGetters } from 'vuex' +import { mapGetters, mapState } from 'vuex' import Checkbox from '../checkbox/checkbox.vue' const buildMentionsString = ({ user, attentions = [] }, currentUser) => { @@ -25,27 +27,54 @@ const buildMentionsString = ({ user, attentions = [] }, currentUser) => { return mentions.length > 0 ? mentions.join(' ') + ' ' : '' } +// Converts a string with px to a number like '2px' -> 2 +const pxStringToNumber = (str) => { + return Number(str.substring(0, str.length - 2)) +} + const PostStatusForm = { props: [ 'replyTo', 'repliedUser', 'attentions', 'copyMessageScope', - 'subject' + 'subject', + 'disableSubject', + 'disableScopeSelector', + 'disableNotice', + 'disableLockWarning', + 'disablePolls', + 'disableSensitivityCheckbox', + 'disableSubmit', + 'disablePreview', + 'placeholder', + 'maxHeight', + 'postHandler', + 'preserveFocus', + 'autoFocus', + 'fileLimit', + 'submitOnEnter', + 'emojiPickerPlacement' ], components: { MediaUpload, EmojiInput, PollForm, ScopeSelector, - Checkbox + Checkbox, + Attachment, + StatusContent }, mounted () { + this.updateIdempotencyKey() this.resize(this.$refs.textarea) - const textLength = this.$refs.textarea.value.length - this.$refs.textarea.setSelectionRange(textLength, textLength) if (this.replyTo) { + const textLength = this.$refs.textarea.value.length + this.$refs.textarea.setSelectionRange(textLength, textLength) + } + + if (this.replyTo || this.autoFocus) { this.$refs.textarea.focus() } }, @@ -68,7 +97,7 @@ const PostStatusForm = { return { dropFiles: [], - submitDisabled: false, + uploadingFiles: false, error: null, posting: false, highlighted: 0, @@ -78,11 +107,18 @@ const PostStatusForm = { nsfw: false, files: [], poll: {}, + mediaDescriptions: {}, visibility: scope, contentType }, caret: 0, - pollFormVisible: false + pollFormVisible: false, + showDropIcon: 'hide', + dropStopTimeout: null, + preview: null, + previewLoading: false, + emojiInputShown: false, + idempotencyKey: '' } }, computed: { @@ -102,7 +138,7 @@ const PostStatusForm = { ...this.$store.state.instance.customEmoji ], users: this.$store.state.users.users, - updateUsersList: (input) => this.$store.dispatch('searchUsers', input) + updateUsersList: (query) => this.$store.dispatch('searchUsers', { query }) }) }, emojiSuggestor () { @@ -151,28 +187,81 @@ const PostStatusForm = { }, pollsAvailable () { return this.$store.state.instance.pollsAvailable && - this.$store.state.instance.pollLimits.max_options >= 2 + this.$store.state.instance.pollLimits.max_options >= 2 && + this.disablePolls !== true }, hideScopeNotice () { - return this.$store.getters.mergedConfig.hideScopeNotice + return this.disableNotice || this.$store.getters.mergedConfig.hideScopeNotice }, pollContentError () { return this.pollFormVisible && this.newStatus.poll && this.newStatus.poll.error }, - ...mapGetters(['mergedConfig']) + showPreview () { + return !this.disablePreview && (!!this.preview || this.previewLoading) + }, + emptyStatus () { + return this.newStatus.status.trim() === '' && this.newStatus.files.length === 0 + }, + uploadFileLimitReached () { + return this.newStatus.files.length >= this.fileLimit + }, + ...mapGetters(['mergedConfig']), + ...mapState({ + mobileLayout: state => state.interface.mobileLayout + }) + }, + watch: { + 'newStatus': { + deep: true, + handler () { + this.statusChanged() + } + } }, methods: { - postStatus (newStatus) { + statusChanged () { + this.autoPreview() + this.updateIdempotencyKey() + }, + clearStatus () { + const newStatus = this.newStatus + this.newStatus = { + status: '', + spoilerText: '', + files: [], + visibility: newStatus.visibility, + contentType: newStatus.contentType, + poll: {}, + mediaDescriptions: {} + } + this.pollFormVisible = false + this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile() + this.clearPollForm() + if (this.preserveFocus) { + this.$nextTick(() => { + this.$refs.textarea.focus() + }) + } + let el = this.$el.querySelector('textarea') + el.style.height = 'auto' + el.style.height = undefined + this.error = null + if (this.preview) this.previewStatus() + }, + async postStatus (event, newStatus, opts = {}) { if (this.posting) { return } - if (this.submitDisabled) { return } + if (this.disableSubmit) { return } + if (this.emojiInputShown) { return } + if (this.submitOnEnter) { + event.stopPropagation() + event.preventDefault() + } - if (this.newStatus.status === '') { - if (this.newStatus.files.length === 0) { - this.error = 'Cannot post an empty status with no files' - return - } + if (this.emptyStatus) { + this.error = this.$t('post_status.empty_status_error') + return } const poll = this.pollFormVisible ? this.newStatus.poll : {} @@ -182,7 +271,16 @@ const PostStatusForm = { } this.posting = true - statusPoster.postStatus({ + + try { + await this.setAllMediaDescriptions() + } catch (e) { + this.error = this.$t('post_status.media_description_error') + this.posting = false + return + } + + const postingOptions = { status: newStatus.status, spoilerText: newStatus.spoilerText || null, visibility: newStatus.visibility, @@ -191,54 +289,98 @@ const PostStatusForm = { store: this.$store, inReplyToStatusId: this.replyTo, contentType: newStatus.contentType, - poll - }).then((data) => { + poll, + idempotencyKey: this.idempotencyKey + } + + const postHandler = this.postHandler ? this.postHandler : statusPoster.postStatus + + postHandler(postingOptions).then((data) => { if (!data.error) { - this.newStatus = { - status: '', - spoilerText: '', - files: [], - visibility: newStatus.visibility, - contentType: newStatus.contentType, - poll: {} - } - this.pollFormVisible = false - this.$refs.mediaUpload.clearFile() - this.clearPollForm() - this.$emit('posted') - let el = this.$el.querySelector('textarea') - el.style.height = 'auto' - el.style.height = undefined - this.error = null + this.clearStatus() + this.$emit('posted', data) } else { this.error = data.error } this.posting = false }) }, + previewStatus () { + if (this.emptyStatus && this.newStatus.spoilerText.trim() === '') { + this.preview = { error: this.$t('post_status.preview_empty') } + this.previewLoading = false + return + } + const newStatus = this.newStatus + this.previewLoading = true + statusPoster.postStatus({ + status: newStatus.status, + spoilerText: newStatus.spoilerText || null, + visibility: newStatus.visibility, + sensitive: newStatus.nsfw, + media: [], + store: this.$store, + inReplyToStatusId: this.replyTo, + contentType: newStatus.contentType, + poll: {}, + preview: true + }).then((data) => { + // Don't apply preview if not loading, because it means + // user has closed the preview manually. + if (!this.previewLoading) return + if (!data.error) { + this.preview = data + } else { + this.preview = { error: data.error } + } + }).catch((error) => { + this.preview = { error } + }).finally(() => { + this.previewLoading = false + }) + }, + debouncePreviewStatus: debounce(function () { this.previewStatus() }, 500), + autoPreview () { + if (!this.preview) return + this.previewLoading = true + this.debouncePreviewStatus() + }, + closePreview () { + this.preview = null + this.previewLoading = false + }, + togglePreview () { + if (this.showPreview) { + this.closePreview() + } else { + this.previewStatus() + } + }, addMediaFile (fileInfo) { this.newStatus.files.push(fileInfo) - this.enableSubmit() + this.$emit('resize', { delayed: true }) }, removeMediaFile (fileInfo) { let index = this.newStatus.files.indexOf(fileInfo) this.newStatus.files.splice(index, 1) + this.$emit('resize') }, uploadFailed (errString, templateArgs) { templateArgs = templateArgs || {} this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs) - this.enableSubmit() }, - disableSubmit () { - this.submitDisabled = true + startedUploadingFiles () { + this.uploadingFiles = true }, - enableSubmit () { - this.submitDisabled = false + finishedUploadingFiles () { + this.$emit('resize') + this.uploadingFiles = false }, type (fileInfo) { return fileTypeService.fileType(fileInfo.mimetype) }, paste (e) { + this.autoPreview() this.resize(e) if (e.clipboardData.files.length > 0) { // prevent pasting of file as text @@ -250,13 +392,27 @@ const PostStatusForm = { } }, fileDrop (e) { - if (e.dataTransfer.files.length > 0) { + if (e.dataTransfer && e.dataTransfer.types.includes('Files')) { e.preventDefault() // allow dropping text like before this.dropFiles = e.dataTransfer.files + clearTimeout(this.dropStopTimeout) + this.showDropIcon = 'hide' } }, + fileDragStop (e) { + // The false-setting is done with delay because just using leave-events + // directly caused unwanted flickering, this is not perfect either but + // much less noticable. + clearTimeout(this.dropStopTimeout) + this.showDropIcon = 'fade' + this.dropStopTimeout = setTimeout(() => (this.showDropIcon = 'hide'), 500) + }, fileDrag (e) { - e.dataTransfer.dropEffect = 'copy' + e.dataTransfer.dropEffect = this.uploadFileLimitReached ? 'none' : 'copy' + if (e.dataTransfer && e.dataTransfer.types.includes('Files')) { + clearTimeout(this.dropStopTimeout) + this.showDropIcon = 'show' + } }, onEmojiInputInput (e) { this.$nextTick(() => { @@ -270,6 +426,7 @@ const PostStatusForm = { // Reset to default height for empty form, nothing else to do here. if (target.value === '') { target.style.height = null + this.$emit('resize') this.$refs['emoji-input'].resize() return } @@ -281,7 +438,7 @@ const PostStatusForm = { * scroll is different for `Window` and `Element`s */ const bottomBottomPaddingStr = window.getComputedStyle(bottomRef)['padding-bottom'] - const bottomBottomPadding = Number(bottomBottomPaddingStr.substring(0, bottomBottomPaddingStr.length - 2)) + const bottomBottomPadding = pxStringToNumber(bottomBottomPaddingStr) const scrollerRef = this.$el.closest('.sidebar-scroller') || this.$el.closest('.post-form-modal-view') || @@ -290,10 +447,12 @@ const PostStatusForm = { // Getting info about padding we have to account for, removing 'px' part const topPaddingStr = window.getComputedStyle(target)['padding-top'] const bottomPaddingStr = window.getComputedStyle(target)['padding-bottom'] - const topPadding = Number(topPaddingStr.substring(0, topPaddingStr.length - 2)) - const bottomPadding = Number(bottomPaddingStr.substring(0, bottomPaddingStr.length - 2)) + const topPadding = pxStringToNumber(topPaddingStr) + const bottomPadding = pxStringToNumber(bottomPaddingStr) const vertPadding = topPadding + bottomPadding + const oldHeight = pxStringToNumber(target.style.height) + /* Explanation: * * https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight @@ -322,8 +481,15 @@ const PostStatusForm = { // BEGIN content size update target.style.height = 'auto' - const newHeight = target.scrollHeight - vertPadding + const heightWithoutPadding = Math.floor(target.scrollHeight - vertPadding) + let newHeight = this.maxHeight ? Math.min(heightWithoutPadding, this.maxHeight) : heightWithoutPadding + // This is a bit of a hack to combat target.scrollHeight being different on every other input + // on some browsers for whatever reason. Don't change the height if difference is 1px or less. + if (Math.abs(newHeight - oldHeight) <= 1) { + newHeight = oldHeight + } target.style.height = `${newHeight}px` + this.$emit('resize', newHeight) // END content size update // We check where the bottom border of form-bottom element is, this uses findOffset @@ -374,6 +540,21 @@ const PostStatusForm = { }, dismissScopeNotice () { this.$store.dispatch('setOption', { name: 'hideScopeNotice', value: true }) + }, + setMediaDescription (id) { + const description = this.newStatus.mediaDescriptions[id] + if (!description || description.trim() === '') return + return statusPoster.setMediaDescription({ store: this.$store, id, description }) + }, + setAllMediaDescriptions () { + const ids = this.newStatus.files.map(file => file.id) + return Promise.all(ids.map(id => this.setMediaDescription(id))) + }, + handleEmojiInputShow (value) { + this.emojiInputShown = value + }, + updateIdempotencyKey () { + this.idempotencyKey = Date.now().toString() } } } diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue @@ -5,11 +5,20 @@ > <form autocomplete="off" - @submit.prevent="postStatus(newStatus)" + @submit.prevent + @dragover.prevent="fileDrag" > + <div + v-show="showDropIcon !== 'hide'" + :style="{ animation: showDropIcon === 'show' ? 'fade-in 0.25s' : 'fade-out 0.5s' }" + class="drop-indicator" + :class="[uploadFileLimitReached ? 'icon-block' : 'icon-upload']" + @dragleave="fileDragStop" + @drop.stop="fileDrop" + /> <div class="form-group"> <i18n - v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'" + v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private' && !disableLockWarning" path="post_status.account_not_locked_warning" tag="p" class="visibility-notice" @@ -61,18 +70,56 @@ <span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span> <span v-else>{{ $t('post_status.direct_warning_to_all') }}</span> </p> + <div + v-if="!disablePreview" + class="preview-heading faint" + > + <a + class="preview-toggle faint" + @click.stop.prevent="togglePreview" + > + {{ $t('post_status.preview') }} + <i :class="showPreview ? 'icon-left-open' : 'icon-right-open'" /> + </a> + <i + v-show="previewLoading" + class="icon-spin3 animate-spin" + /> + </div> + <div + v-if="showPreview" + class="preview-container" + > + <div + v-if="!preview" + class="preview-status" + > + {{ $t('general.loading') }} + </div> + <div + v-else-if="preview.error" + class="preview-status preview-error" + > + {{ preview.error }} + </div> + <StatusContent + v-else + :status="preview" + class="preview-status" + /> + </div> <EmojiInput - v-if="newStatus.spoilerText || alwaysShowSubject" + v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)" v-model="newStatus.spoilerText" enable-emoji-picker :suggest="emojiSuggestor" class="form-control" > <input - v-model="newStatus.spoilerText" type="text" :placeholder="$t('post_status.content_warning')" + :disabled="posting" class="form-post-subject" > </EmojiInput> @@ -80,25 +127,29 @@ ref="emoji-input" v-model="newStatus.status" :suggest="emojiUserSuggestor" + :placement="emojiPickerPlacement" class="form-control main-input" enable-emoji-picker hide-emoji-button + :newline-on-ctrl-enter="submitOnEnter" enable-sticker-picker @input="onEmojiInputInput" @sticker-uploaded="addMediaFile" @sticker-upload-failed="uploadFailed" + @shown="handleEmojiInputShow" > <textarea ref="textarea" v-model="newStatus.status" - :placeholder="$t('post_status.default')" + :placeholder="placeholder || $t('post_status.default')" rows="1" + cols="1" :disabled="posting" class="form-post-body" - @keydown.meta.enter="postStatus(newStatus)" - @keyup.ctrl.enter="postStatus(newStatus)" - @drop="fileDrop" - @dragover.prevent="fileDrag" + :class="{ 'scrollable-form': !!maxHeight }" + @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)" + @keydown.meta.enter="postStatus($event, newStatus)" + @keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)" @input="resize" @compositionupdate="resize" @paste="paste" @@ -111,7 +162,10 @@ {{ charactersLeft }} </p> </EmojiInput> - <div class="visibility-tray"> + <div + v-if="!disableScopeSelector" + class="visibility-tray" + > <scope-selector :show-all="showAllScopes" :user-default="userDefaultScope" @@ -169,9 +223,11 @@ ref="mediaUpload" class="media-upload-icon" :drop-files="dropFiles" - @uploading="disableSubmit" + :disabled="uploadFileLimitReached" + @uploading="startedUploadingFiles" @uploaded="addMediaFile" @upload-failed="uploadFailed" + @all-uploaded="finishedUploadingFiles" /> <div class="emoji-icon" @@ -208,11 +264,13 @@ > {{ $t('general.submit') }} </button> + <!-- touchstart is used to keep the OSK at the same position after a message send --> <button v-else - :disabled="submitDisabled" - type="submit" + :disabled="uploadingFiles || disableSubmit" class="btn btn-default" + @touchstart.stop.prevent="postStatus($event, newStatus)" + @click.stop.prevent="postStatus($event, newStatus)" > {{ $t('general.submit') }} </button> @@ -237,31 +295,22 @@ class="fa button-icon icon-cancel" @click="removeMediaFile(file)" /> - <div class="media-upload-container attachment"> - <img - v-if="type(file) === 'image'" - class="thumbnail media-upload" - :src="file.url" - > - <video - v-if="type(file) === 'video'" - :src="file.url" - controls - /> - <audio - v-if="type(file) === 'audio'" - :src="file.url" - controls - /> - <a - v-if="type(file) === 'unknown'" - :href="file.url" - >{{ file.url }}</a> - </div> + <attachment + :attachment="file" + :set-media="() => $store.dispatch('setMedia', newStatus.files)" + size="small" + allow-play="false" + /> + <input + v-model="newStatus.mediaDescriptions[file.id]" + type="text" + :placeholder="$t('post_status.media_description')" + @keydown.enter.prevent="" + > </div> </div> <div - v-if="newStatus.files.length > 0" + v-if="newStatus.files.length > 0 && !disableSensitivityCheckbox" class="upload_settings" > <Checkbox v-model="newStatus.nsfw"> @@ -295,14 +344,8 @@ } .post-status-form { - .visibility-tray { - display: flex; - justify-content: space-between; - padding-top: 5px; - } -} + position: relative; -.post-status-form { .form-bottom { display: flex; justify-content: space-between; @@ -328,6 +371,51 @@ max-width: 10em; } + .preview-heading { + padding-left: 0.5em; + display: flex; + width: 100%; + + .icon-spin3 { + margin-left: auto; + } + } + + .preview-toggle { + display: flex; + cursor: pointer; + user-select: none; + + &:hover { + text-decoration: underline; + } + i { + margin-left: 0.2em; + font-size: 0.8em; + transform: rotate(90deg); + } + } + + .preview-container { + margin-bottom: 1em; + } + + .preview-error { + font-style: italic; + color: $fallback--faint; + color: var(--faint, $fallback--faint); + } + + .preview-status { + border: 1px solid $fallback--border; + border: 1px solid var(--border, $fallback--border); + border-radius: $fallback--tooltipRadius; + border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + padding: 0.5em; + margin: 0; + line-height: 1.4em; + } + .text-format { .only-format { color: $fallback--faint; @@ -335,6 +423,12 @@ } } + .visibility-tray { + display: flex; + justify-content: space-between; + padding-top: 5px; + } + .media-upload-icon, .poll-icon, .emoji-icon { font-size: 26px; flex: 1; @@ -346,6 +440,19 @@ color: var(--lightText, $fallback--lightText); } } + + &.disabled { + i { + cursor: not-allowed; + color: $fallback--icon; + color: var(--btnDisabledText, $fallback--icon); + + &:hover { + color: $fallback--icon; + color: var(--btnDisabledText, $fallback--icon); + } + } + } } // Order is not necessary but a good indicator @@ -373,11 +480,9 @@ } .media-upload-wrapper { - flex: 0 0 auto; - max-width: 100%; - min-width: 50px; margin-right: .2em; margin-bottom: .5em; + width: 18em; .icon-cancel { display: inline-block; @@ -391,6 +496,20 @@ border-bottom-left-radius: 0; border-bottom-right-radius: 0; } + + img, video { + object-fit: contain; + max-height: 10em; + } + + .video { + max-height: 10em; + } + + input { + flex: 1; + width: 100%; + } } .status-input-wrapper { @@ -400,28 +519,13 @@ flex-direction: column; } - .attachments { + .media-upload-wrapper .attachments { padding: 0 0.5em; .attachment { margin: 0; + padding: 0; position: relative; - flex: 0 0 auto; - border: 1px solid $fallback--border; - border: 1px solid var(--border, $fallback--border); - text-align: center; - - audio { - min-width: 300px; - flex: 1 0 auto; - } - - a { - display: block; - text-align: left; - line-height: 1.2; - padding: .5em; - } } i { @@ -446,7 +550,8 @@ form { display: flex; flex-direction: column; - padding: 0.6em; + margin: 0.6em; + position: relative; } .form-group { @@ -473,6 +578,10 @@ padding-bottom: 1.75em; min-height: 1px; box-sizing: content-box; + + &.scrollable-form { + overflow-y: auto; + } } .main-input { @@ -504,5 +613,42 @@ cursor: pointer; z-index: 4; } + + @keyframes fade-in { + from { opacity: 0; } + to { opacity: 0.6; } + } + + @keyframes fade-out { + from { opacity: 0.6; } + to { opacity: 0; } + } + + .drop-indicator { + position: absolute; + z-index: 1; + width: 100%; + height: 100%; + font-size: 5em; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.6; + color: $fallback--text; + color: var(--text, $fallback--text); + background-color: $fallback--bg; + background-color: var(--bg, $fallback--bg); + border-radius: $fallback--tooltipRadius; + border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border: 2px dashed $fallback--text; + border: 2px dashed var(--text, $fallback--text); + } +} + +// todo: unify with attachment.vue (otherwise the uploaded images are not minified unless a status with an attachment was displayed before) +img.media-upload, .media-upload-container > video { + line-height: 0; + max-height: 200px; + max-width: 100%; } </style> diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js @@ -24,11 +24,14 @@ const ReactButton = { }, computed: { commonEmojis () { - return ['❤️', '😠', '👀', '😂', '🔥'] + return ['👍', '😠', '👀', '😂', '🔥'] }, emojis () { if (this.filterWord !== '') { - return this.$store.state.instance.emoji.filter(emoji => emoji.displayText.includes(this.filterWord)) + const filterWordLowercase = this.filterWord.toLowerCase() + return this.$store.state.instance.emoji.filter(emoji => + emoji.displayText.toLowerCase().includes(filterWordLowercase) + ) } return this.$store.state.instance.emoji || [] }, diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js @@ -1,128 +0,0 @@ -/* eslint-env browser */ -import { filter, trim } from 'lodash' - -import TabSwitcher from '../tab_switcher/tab_switcher.js' -import StyleSwitcher from '../style_switcher/style_switcher.vue' -import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue' -import { extractCommit } from '../../services/version/version.service' -import { instanceDefaultProperties, defaultState as configDefaultState } from '../../modules/config.js' -import Checkbox from '../checkbox/checkbox.vue' - -const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/' -const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/' - -const multiChoiceProperties = [ - 'postContentType', - 'subjectLineBehavior' -] - -const settings = { - data () { - const instance = this.$store.state.instance - - return { - loopSilentAvailable: - // Firefox - Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || - // Chrome-likes - Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') || - // Future spec, still not supported in Nightly 63 as of 08/2018 - Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'), - - backendVersion: instance.backendVersion, - frontendVersion: instance.frontendVersion - } - }, - components: { - TabSwitcher, - StyleSwitcher, - InterfaceLanguageSwitcher, - Checkbox - }, - computed: { - user () { - return this.$store.state.users.currentUser - }, - currentSaveStateNotice () { - return this.$store.state.interface.settings.currentSaveStateNotice - }, - postFormats () { - return this.$store.state.instance.postFormats || [] - }, - instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }, - frontendVersionLink () { - return pleromaFeCommitUrl + this.frontendVersion - }, - backendVersionLink () { - return pleromaBeCommitUrl + extractCommit(this.backendVersion) - }, - // Getting localized values for instance-default properties - ...instanceDefaultProperties - .filter(key => multiChoiceProperties.includes(key)) - .map(key => [ - key + 'DefaultValue', - function () { - return this.$store.getters.instanceDefaultConfig[key] - } - ]) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), - ...instanceDefaultProperties - .filter(key => !multiChoiceProperties.includes(key)) - .map(key => [ - key + 'LocalizedValue', - function () { - return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key]) - } - ]) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), - // Generating computed values for vuex properties - ...Object.keys(configDefaultState) - .map(key => [key, { - get () { return this.$store.getters.mergedConfig[key] }, - set (value) { - this.$store.dispatch('setOption', { name: key, value }) - } - }]) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), - // Special cases (need to transform values or perform actions first) - muteWordsString: { - get () { return this.$store.getters.mergedConfig.muteWords.join('\n') }, - set (value) { - this.$store.dispatch('setOption', { - name: 'muteWords', - value: filter(value.split('\n'), (word) => trim(word).length > 0) - }) - } - }, - useStreamingApi: { - get () { return this.$store.getters.mergedConfig.useStreamingApi }, - set (value) { - const promise = value - ? this.$store.dispatch('enableMastoSockets') - : this.$store.dispatch('disableMastoSockets') - - promise.then(() => { - this.$store.dispatch('setOption', { name: 'useStreamingApi', value }) - }).catch((e) => { - console.error('Failed starting MastoAPI Streaming socket', e) - this.$store.dispatch('disableMastoSockets') - this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false }) - }) - } - } - }, - // Updating nested properties - watch: { - notificationVisibility: { - handler (value) { - this.$store.dispatch('setOption', { - name: 'notificationVisibility', - value: this.$store.getters.mergedConfig.notificationVisibility - }) - }, - deep: true - } - } -} - -export default settings diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue @@ -1,424 +0,0 @@ -<template> - <div class="settings panel panel-default"> - <div class="panel-heading"> - <div class="title"> - {{ $t('settings.settings') }} - </div> - - <transition name="fade"> - <template v-if="currentSaveStateNotice"> - <div - v-if="currentSaveStateNotice.error" - class="alert error" - @click.prevent - > - {{ $t('settings.saving_err') }} - </div> - - <div - v-if="!currentSaveStateNotice.error" - class="alert transparent" - @click.prevent - > - {{ $t('settings.saving_ok') }} - </div> - </template> - </transition> - </div> - <div class="panel-body"> - <keep-alive> - <tab-switcher> - <div :label="$t('settings.general')"> - <div class="setting-item"> - <h2>{{ $t('settings.interface') }}</h2> - <ul class="setting-list"> - <li> - <interface-language-switcher /> - </li> - <li v-if="instanceSpecificPanelPresent"> - <Checkbox v-model="hideISP"> - {{ $t('settings.hide_isp') }} - </Checkbox> - </li> - </ul> - </div> - <div class="setting-item"> - <h2>{{ $t('nav.timeline') }}</h2> - <ul class="setting-list"> - <li> - <Checkbox v-model="hideMutedPosts"> - {{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }} - </Checkbox> - </li> - <li> - <Checkbox v-model="collapseMessageWithSubject"> - {{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }} - </Checkbox> - </li> - <li> - <Checkbox v-model="streaming"> - {{ $t('settings.streaming') }} - </Checkbox> - <ul - class="setting-list suboptions" - :class="[{disabled: !streaming}]" - > - <li> - <Checkbox - v-model="pauseOnUnfocused" - :disabled="!streaming" - > - {{ $t('settings.pause_on_unfocused') }} - </Checkbox> - </li> - </ul> - </li> - <li> - <Checkbox v-model="useStreamingApi"> - {{ $t('settings.useStreamingApi') }} - <br> - <small> - {{ $t('settings.useStreamingApiWarning') }} - </small> - </Checkbox> - </li> - <li> - <Checkbox v-model="autoLoad"> - {{ $t('settings.autoload') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="hoverPreview"> - {{ $t('settings.reply_link_preview') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="emojiReactionsOnTimeline"> - {{ $t('settings.emoji_reactions_on_timeline') }} - </Checkbox> - </li> - </ul> - </div> - - <div class="setting-item"> - <h2>{{ $t('settings.composing') }}</h2> - <ul class="setting-list"> - <li> - <Checkbox v-model="scopeCopy"> - {{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }} - </Checkbox> - </li> - <li> - <Checkbox v-model="alwaysShowSubjectInput"> - {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }} - </Checkbox> - </li> - <li> - <div> - {{ $t('settings.subject_line_behavior') }} - <label - for="subjectLineBehavior" - class="select" - > - <select - id="subjectLineBehavior" - v-model="subjectLineBehavior" - > - <option value="email"> - {{ $t('settings.subject_line_email') }} - {{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }} - </option> - <option value="masto"> - {{ $t('settings.subject_line_mastodon') }} - {{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }} - </option> - <option value="noop"> - {{ $t('settings.subject_line_noop') }} - {{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }} - </option> - </select> - <i class="icon-down-open" /> - </label> - </div> - </li> - <li v-if="postFormats.length > 0"> - <div> - {{ $t('settings.post_status_content_type') }} - <label - for="postContentType" - class="select" - > - <select - id="postContentType" - v-model="postContentType" - > - <option - v-for="postFormat in postFormats" - :key="postFormat" - :value="postFormat" - > - {{ $t(`post_status.content_type["${postFormat}"]`) }} - {{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }} - </option> - </select> - <i class="icon-down-open" /> - </label> - </div> - </li> - <li> - <Checkbox v-model="minimalScopesMode"> - {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }} - </Checkbox> - </li> - <li> - <Checkbox v-model="autohideFloatingPostButton"> - {{ $t('settings.autohide_floating_post_button') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="padEmoji"> - {{ $t('settings.pad_emoji') }} - </Checkbox> - </li> - </ul> - </div> - - <div class="setting-item"> - <h2>{{ $t('settings.attachments') }}</h2> - <ul class="setting-list"> - <li> - <Checkbox v-model="hideAttachments"> - {{ $t('settings.hide_attachments_in_tl') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="hideAttachmentsInConv"> - {{ $t('settings.hide_attachments_in_convo') }} - </Checkbox> - </li> - <li> - <label for="maxThumbnails"> - {{ $t('settings.max_thumbnails') }} - </label> - <input - id="maxThumbnails" - v-model.number="maxThumbnails" - class="number-input" - type="number" - min="0" - step="1" - > - </li> - <li> - <Checkbox v-model="hideNsfw"> - {{ $t('settings.nsfw_clickthrough') }} - </Checkbox> - </li> - <ul class="setting-list suboptions"> - <li> - <Checkbox - v-model="preloadImage" - :disabled="!hideNsfw" - > - {{ $t('settings.preload_images') }} - </Checkbox> - </li> - <li> - <Checkbox - v-model="useOneClickNsfw" - :disabled="!hideNsfw" - > - {{ $t('settings.use_one_click_nsfw') }} - </Checkbox> - </li> - </ul> - <li> - <Checkbox v-model="stopGifs"> - {{ $t('settings.stop_gifs') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="loopVideo"> - {{ $t('settings.loop_video') }} - </Checkbox> - <ul - class="setting-list suboptions" - :class="[{disabled: !streaming}]" - > - <li> - <Checkbox - v-model="loopVideoSilentOnly" - :disabled="!loopVideo || !loopSilentAvailable" - > - {{ $t('settings.loop_video_silent_only') }} - </Checkbox> - <div - v-if="!loopSilentAvailable" - class="unavailable" - > - <i class="icon-globe" />! {{ $t('settings.limited_availability') }} - </div> - </li> - </ul> - </li> - <li> - <Checkbox v-model="playVideosInModal"> - {{ $t('settings.play_videos_in_modal') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="useContainFit"> - {{ $t('settings.use_contain_fit') }} - </Checkbox> - </li> - </ul> - </div> - - <div class="setting-item"> - <h2>{{ $t('settings.notifications') }}</h2> - <ul class="setting-list"> - <li> - <Checkbox v-model="webPushNotifications"> - {{ $t('settings.enable_web_push_notifications') }} - </Checkbox> - </li> - </ul> - </div> - - <div class="setting-item"> - <h2>{{ $t('settings.fun') }}</h2> - <ul class="setting-list"> - <li> - <Checkbox v-model="greentext"> - {{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }} - </Checkbox> - </li> - </ul> - </div> - </div> - - <div :label="$t('settings.theme')"> - <div class="setting-item"> - <style-switcher /> - </div> - </div> - - <div :label="$t('settings.filtering')"> - <div class="setting-item"> - <div class="select-multiple"> - <span class="label">{{ $t('settings.notification_visibility') }}</span> - <ul class="option-list"> - <li> - <Checkbox v-model="notificationVisibility.likes"> - {{ $t('settings.notification_visibility_likes') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="notificationVisibility.repeats"> - {{ $t('settings.notification_visibility_repeats') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="notificationVisibility.follows"> - {{ $t('settings.notification_visibility_follows') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="notificationVisibility.mentions"> - {{ $t('settings.notification_visibility_mentions') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="notificationVisibility.moves"> - {{ $t('settings.notification_visibility_moves') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="notificationVisibility.emojiReactions"> - {{ $t('settings.notification_visibility_emoji_reactions') }} - </Checkbox> - </li> - </ul> - </div> - <div> - {{ $t('settings.replies_in_timeline') }} - <label - for="replyVisibility" - class="select" - > - <select - id="replyVisibility" - v-model="replyVisibility" - > - <option - value="all" - selected - >{{ $t('settings.reply_visibility_all') }}</option> - <option value="following">{{ $t('settings.reply_visibility_following') }}</option> - <option value="self">{{ $t('settings.reply_visibility_self') }}</option> - </select> - <i class="icon-down-open" /> - </label> - </div> - <div> - <Checkbox v-model="hidePostStats"> - {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }} - </Checkbox> - </div> - <div> - <Checkbox v-model="hideUserStats"> - {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }} - </Checkbox> - </div> - </div> - <div class="setting-item"> - <div> - <p>{{ $t('settings.filtering_explanation') }}</p> - <textarea - id="muteWords" - v-model="muteWordsString" - /> - </div> - <div> - <Checkbox v-model="hideFilteredStatuses"> - {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }} - </Checkbox> - </div> - </div> - </div> - <div :label="$t('settings.version.title')"> - <div class="setting-item"> - <ul class="setting-list"> - <li> - <p>{{ $t('settings.version.backend_version') }}</p> - <ul class="option-list"> - <li> - <a - :href="backendVersionLink" - target="_blank" - >{{ backendVersion }}</a> - </li> - </ul> - </li> - <li> - <p>{{ $t('settings.version.frontend_version') }}</p> - <ul class="option-list"> - <li> - <a - :href="frontendVersionLink" - target="_blank" - >{{ frontendVersion }}</a> - </li> - </ul> - </li> - </ul> - </div> - </div> - </tab-switcher> - </keep-alive> - </div> - </div> -</template> - -<script src="./settings.js"> -</script> diff --git a/src/components/settings_modal/helpers/shared_computed_object.js b/src/components/settings_modal/helpers/shared_computed_object.js @@ -0,0 +1,58 @@ +import { + instanceDefaultProperties, + multiChoiceProperties, + defaultState as configDefaultState +} from 'src/modules/config.js' + +const SharedComputedObject = () => ({ + user () { + return this.$store.state.users.currentUser + }, + // Getting localized values for instance-default properties + ...instanceDefaultProperties + .filter(key => multiChoiceProperties.includes(key)) + .map(key => [ + key + 'DefaultValue', + function () { + return this.$store.getters.instanceDefaultConfig[key] + } + ]) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), + ...instanceDefaultProperties + .filter(key => !multiChoiceProperties.includes(key)) + .map(key => [ + key + 'LocalizedValue', + function () { + return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key]) + } + ]) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), + // Generating computed values for vuex properties + ...Object.keys(configDefaultState) + .map(key => [key, { + get () { return this.$store.getters.mergedConfig[key] }, + set (value) { + this.$store.dispatch('setOption', { name: key, value }) + } + }]) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), + // Special cases (need to transform values or perform actions first) + useStreamingApi: { + get () { return this.$store.getters.mergedConfig.useStreamingApi }, + set (value) { + const promise = value + ? this.$store.dispatch('enableMastoSockets') + : this.$store.dispatch('disableMastoSockets') + + promise.then(() => { + this.$store.dispatch('setOption', { name: 'useStreamingApi', value }) + }).catch((e) => { + console.error('Failed starting MastoAPI Streaming socket', e) + this.$store.dispatch('disableMastoSockets') + this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false }) + }) + } + } +}) + +export default SharedComputedObject diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js @@ -0,0 +1,42 @@ +import Modal from 'src/components/modal/modal.vue' +import PanelLoading from 'src/components/panel_loading/panel_loading.vue' +import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue' +import getResettableAsyncComponent from 'src/services/resettable_async_component.js' + +const SettingsModal = { + components: { + Modal, + SettingsModalContent: getResettableAsyncComponent( + () => import('./settings_modal_content.vue'), + { + loading: PanelLoading, + error: AsyncComponentError, + delay: 0 + } + ) + }, + methods: { + closeModal () { + this.$store.dispatch('closeSettingsModal') + }, + peekModal () { + this.$store.dispatch('togglePeekSettingsModal') + } + }, + computed: { + currentSaveStateNotice () { + return this.$store.state.interface.settings.currentSaveStateNotice + }, + modalActivated () { + return this.$store.state.interface.settingsModalState !== 'hidden' + }, + modalOpenedOnce () { + return this.$store.state.interface.settingsModalLoaded + }, + modalPeeked () { + return this.$store.state.interface.settingsModalState === 'minimized' + } + } +} + +export default SettingsModal diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss @@ -0,0 +1,51 @@ +@import 'src/_variables.scss'; +.settings-modal { + overflow: hidden; + + &.peek { + .settings-modal-panel { + /* Explanation: + * Modal is positioned vertically centered. + * 100vh - 100% = Distance between modal's top+bottom boundaries and screen + * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen + * + 100% - we move modal completely off-screen, it's top boundary touches + * bottom of the screen + * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible + */ + transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px)); + + @media all and (max-width: 800px) { + /* For mobile, the modal takes 100% of the available screen. + This ensures the minimized modal is always 50px above the browser bottom bar regardless of whether or not it is visible. + */ + transform: translateY(calc(100% - 50px)); + } + } + } + + .settings-modal-panel { + overflow: hidden; + transition: transform; + transition-timing-function: ease-in-out; + transition-duration: 300ms; + width: 1000px; + max-width: 90vw; + height: 90vh; + + @media all and (max-width: 800px) { + max-width: 100vw; + height: 100%; + } + + >.panel-body { + height: 100%; + overflow-y: hidden; + + .btn { + min-height: 28px; + min-width: 10em; + padding: 0 2em; + } + } + } +} diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue @@ -0,0 +1,54 @@ +<template> + <Modal + :is-open="modalActivated" + class="settings-modal" + :class="{ peek: modalPeeked }" + :no-background="modalPeeked" + > + <div class="settings-modal-panel panel"> + <div class="panel-heading"> + <span class="title"> + {{ $t('settings.settings') }} + </span> + <transition name="fade"> + <template v-if="currentSaveStateNotice"> + <div + v-if="currentSaveStateNotice.error" + class="alert error" + @click.prevent + > + {{ $t('settings.saving_err') }} + </div> + + <div + v-if="!currentSaveStateNotice.error" + class="alert transparent" + @click.prevent + > + {{ $t('settings.saving_ok') }} + </div> + </template> + </transition> + <button + class="btn" + @click="peekModal" + > + {{ $t('general.peek') }} + </button> + <button + class="btn" + @click="closeModal" + > + {{ $t('general.close') }} + </button> + </div> + <div class="panel-body"> + <SettingsModalContent v-if="modalOpenedOnce" /> + </div> + </div> + </Modal> +</template> + +<script src="./settings_modal.js"></script> + +<style src="./settings_modal.scss" lang="scss"></style> diff --git a/src/components/settings_modal/settings_modal_content.js b/src/components/settings_modal/settings_modal_content.js @@ -0,0 +1,34 @@ +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js' + +import DataImportExportTab from './tabs/data_import_export_tab.vue' +import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue' +import NotificationsTab from './tabs/notifications_tab.vue' +import FilteringTab from './tabs/filtering_tab.vue' +import SecurityTab from './tabs/security_tab/security_tab.vue' +import ProfileTab from './tabs/profile_tab.vue' +import GeneralTab from './tabs/general_tab.vue' +import VersionTab from './tabs/version_tab.vue' +import ThemeTab from './tabs/theme_tab/theme_tab.vue' + +const SettingsModalContent = { + components: { + TabSwitcher, + + DataImportExportTab, + MutesAndBlocksTab, + NotificationsTab, + FilteringTab, + SecurityTab, + ProfileTab, + GeneralTab, + VersionTab, + ThemeTab + }, + computed: { + isLoggedIn () { + return !!this.$store.state.users.currentUser + } + } +} + +export default SettingsModalContent diff --git a/src/components/settings_modal/settings_modal_content.scss b/src/components/settings_modal/settings_modal_content.scss @@ -0,0 +1,43 @@ +@import 'src/_variables.scss'; +.settings_tab-switcher { + height: 100%; + + .setting-item { + border-bottom: 2px solid var(--fg, $fallback--fg); + margin: 1em 1em 1.4em; + padding-bottom: 1.4em; + + > div { + margin-bottom: .5em; + &:last-child { + margin-bottom: 0; + } + } + + &:last-child { + border-bottom: none; + padding-bottom: 0; + margin-bottom: 1em; + } + + select { + min-width: 10em; + } + + textarea { + width: 100%; + max-width: 100%; + height: 100px; + } + + .unavailable, + .unavailable i { + color: var(--cRed, $fallback--cRed); + color: $fallback--cRed; + } + + .number-input { + max-width: 6em; + } + } +} diff --git a/src/components/settings_modal/settings_modal_content.vue b/src/components/settings_modal/settings_modal_content.vue @@ -0,0 +1,73 @@ +<template> + <tab-switcher + ref="tabSwitcher" + class="settings_tab-switcher" + :side-tab-bar="true" + :scrollable-tabs="true" + > + <div + :label="$t('settings.general')" + icon="wrench" + > + <GeneralTab /> + </div> + <div + v-if="isLoggedIn" + :label="$t('settings.profile_tab')" + icon="user" + > + <ProfileTab /> + </div> + <div + v-if="isLoggedIn" + :label="$t('settings.security_tab')" + icon="lock" + > + <SecurityTab /> + </div> + <div + :label="$t('settings.filtering')" + icon="filter" + > + <FilteringTab /> + </div> + <div + :label="$t('settings.theme')" + icon="brush" + > + <ThemeTab /> + </div> + <div + v-if="isLoggedIn" + :label="$t('settings.notifications')" + icon="bell-ringing-o" + > + <NotificationsTab /> + </div> + <div + v-if="isLoggedIn" + :label="$t('settings.data_import_export_tab')" + icon="download" + > + <DataImportExportTab /> + </div> + <div + v-if="isLoggedIn" + :label="$t('settings.mutes_and_blocks')" + :fullHeight="true" + icon="eye-off" + > + <MutesAndBlocksTab /> + </div> + <div + :label="$t('settings.version.title')" + icon="info-circled" + > + <VersionTab /> + </div> + </tab-switcher> +</template> + +<script src="./settings_modal_content.js"></script> + +<style src="./settings_modal_content.scss" lang="scss"></style> diff --git a/src/components/settings_modal/tabs/data_import_export_tab.js b/src/components/settings_modal/tabs/data_import_export_tab.js @@ -0,0 +1,65 @@ +import Importer from 'src/components/importer/importer.vue' +import Exporter from 'src/components/exporter/exporter.vue' +import Checkbox from 'src/components/checkbox/checkbox.vue' + +const DataImportExportTab = { + data () { + return { + activeTab: 'profile', + newDomainToMute: '' + } + }, + created () { + this.$store.dispatch('fetchTokens') + }, + components: { + Importer, + Exporter, + Checkbox + }, + computed: { + user () { + return this.$store.state.users.currentUser + } + }, + methods: { + getFollowsContent () { + return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id }) + .then(this.generateExportableUsersContent) + }, + getBlocksContent () { + return this.$store.state.api.backendInteractor.fetchBlocks() + .then(this.generateExportableUsersContent) + }, + importFollows (file) { + return this.$store.state.api.backendInteractor.importFollows({ file }) + .then((status) => { + if (!status) { + throw new Error('failed') + } + }) + }, + importBlocks (file) { + return this.$store.state.api.backendInteractor.importBlocks({ file }) + .then((status) => { + if (!status) { + throw new Error('failed') + } + }) + }, + generateExportableUsersContent (users) { + // Get addresses + return users.map((user) => { + // check is it's a local user + if (user && user.is_local) { + // append the instance address + // eslint-disable-next-line no-undef + return user.screen_name + '@' + location.hostname + } + return user.screen_name + }).join('\n') + } + } +} + +export default DataImportExportTab diff --git a/src/components/settings_modal/tabs/data_import_export_tab.vue b/src/components/settings_modal/tabs/data_import_export_tab.vue @@ -0,0 +1,43 @@ +<template> + <div + :label="$t('settings.data_import_export_tab')" + > + <div class="setting-item"> + <h2>{{ $t('settings.follow_import') }}</h2> + <p>{{ $t('settings.import_followers_from_a_csv_file') }}</p> + <Importer + :submit-handler="importFollows" + :success-message="$t('settings.follows_imported')" + :error-message="$t('settings.follow_import_error')" + /> + </div> + <div class="setting-item"> + <h2>{{ $t('settings.follow_export') }}</h2> + <Exporter + :get-content="getFollowsContent" + filename="friends.csv" + :export-button-label="$t('settings.follow_export_button')" + /> + </div> + <div class="setting-item"> + <h2>{{ $t('settings.block_import') }}</h2> + <p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p> + <Importer + :submit-handler="importBlocks" + :success-message="$t('settings.blocks_imported')" + :error-message="$t('settings.block_import_error')" + /> + </div> + <div class="setting-item"> + <h2>{{ $t('settings.block_export') }}</h2> + <Exporter + :get-content="getBlocksContent" + filename="blocks.csv" + :export-button-label="$t('settings.block_export_button')" + /> + </div> + </div> +</template> + +<script src="./data_import_export_tab.js"></script> +<!-- <style lang="scss" src="./profile.scss"></style> --> diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js @@ -0,0 +1,47 @@ +import { filter, trim } from 'lodash' +import Checkbox from 'src/components/checkbox/checkbox.vue' + +import SharedComputedObject from '../helpers/shared_computed_object.js' + +const FilteringTab = { + data () { + return { + muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n') + } + }, + components: { + Checkbox + }, + computed: { + ...SharedComputedObject(), + muteWordsString: { + get () { + return this.muteWordsStringLocal + }, + set (value) { + this.muteWordsStringLocal = value + this.$store.dispatch('setOption', { + name: 'muteWords', + value: filter(value.split('\n'), (word) => trim(word).length > 0) + }) + } + } + }, + // Updating nested properties + watch: { + notificationVisibility: { + handler (value) { + this.$store.dispatch('setOption', { + name: 'notificationVisibility', + value: this.$store.getters.mergedConfig.notificationVisibility + }) + }, + deep: true + }, + replyVisibility () { + this.$store.dispatch('queueFlushAll') + } + } +} + +export default FilteringTab diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue @@ -0,0 +1,86 @@ +<template> + <div :label="$t('settings.filtering')"> + <div class="setting-item"> + <div class="select-multiple"> + <span class="label">{{ $t('settings.notification_visibility') }}</span> + <ul class="option-list"> + <li> + <Checkbox v-model="notificationVisibility.likes"> + {{ $t('settings.notification_visibility_likes') }} + </Checkbox> + </li> + <li> + <Checkbox v-model="notificationVisibility.repeats"> + {{ $t('settings.notification_visibility_repeats') }} + </Checkbox> + </li> + <li> + <Checkbox v-model="notificationVisibility.follows"> + {{ $t('settings.notification_visibility_follows') }} + </Checkbox> + </li> + <li> + <Checkbox v-model="notificationVisibility.mentions"> + {{ $t('settings.notification_visibility_mentions') }} + </Checkbox> + </li> + <li> + <Checkbox v-model="notificationVisibility.moves"> + {{ $t('settings.notification_visibility_moves') }} + </Checkbox> + </li> + <li> + <Checkbox v-model="notificationVisibility.emojiReactions"> + {{ $t('settings.notification_visibility_emoji_reactions') }} + </Checkbox> + </li> + </ul> + </div> + <div> + {{ $t('settings.replies_in_timeline') }} + <label + for="replyVisibility" + class="select" + > + <select + id="replyVisibility" + v-model="replyVisibility" + > + <option + value="all" + selected + >{{ $t('settings.reply_visibility_all') }}</option> + <option value="following">{{ $t('settings.reply_visibility_following') }}</option> + <option value="self">{{ $t('settings.reply_visibility_self') }}</option> + </select> + <i class="icon-down-open" /> + </label> + </div> + <div> + <Checkbox v-model="hidePostStats"> + {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }} + </Checkbox> + </div> + <div> + <Checkbox v-model="hideUserStats"> + {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }} + </Checkbox> + </div> + </div> + <div class="setting-item"> + <div> + <p>{{ $t('settings.filtering_explanation') }}</p> + <textarea + id="muteWords" + v-model="muteWordsString" + /> + </div> + <div> + <Checkbox v-model="hideFilteredStatuses"> + {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }} + </Checkbox> + </div> + </div> + </div> +</template> +<script src="./filtering_tab.js"></script> diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js @@ -0,0 +1,31 @@ +import Checkbox from 'src/components/checkbox/checkbox.vue' +import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' + +import SharedComputedObject from '../helpers/shared_computed_object.js' + +const GeneralTab = { + data () { + return { + loopSilentAvailable: + // Firefox + Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || + // Chrome-likes + Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') || + // Future spec, still not supported in Nightly 63 as of 08/2018 + Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks') + } + }, + components: { + Checkbox, + InterfaceLanguageSwitcher + }, + computed: { + postFormats () { + return this.$store.state.instance.postFormats || [] + }, + instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }, + ...SharedComputedObject() + } +} + +export default GeneralTab diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue @@ -0,0 +1,262 @@ +<template> + <div :label="$t('settings.general')"> + <div class="setting-item"> + <h2>{{ $t('settings.interface') }}</h2> + <ul class="setting-list"> + <li> + <interface-language-switcher /> + </li> + <li v-if="instanceSpecificPanelPresent"> + <Checkbox v-model="hideISP"> + {{ $t('settings.hide_isp') }} + </Checkbox> + </li> + </ul> + </div> + <div class="setting-item"> + <h2>{{ $t('nav.timeline') }}</h2> + <ul class="setting-list"> + <li> + <Checkbox v-model="hideMutedPosts"> + {{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }} + </Checkbox> + </li> + <li> + <Checkbox v-model="collapseMessageWithSubject"> + {{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }} + </Checkbox> + </li> + <li> + <Checkbox v-model="streaming"> + {{ $t('settings.streaming') }} + </Checkbox> + <ul + class="setting-list suboptions" + :class="[{disabled: !streaming}]" + > + <li> + <Checkbox + v-model="pauseOnUnfocused" + :disabled="!streaming" + > + {{ $t('settings.pause_on_unfocused') }} + </Checkbox> + </li> + </ul> + </li> + <li> + <Checkbox v-model="useStreamingApi"> + {{ $t('settings.useStreamingApi') }} + <br> + <small> + {{ $t('settings.useStreamingApiWarning') }} + </small> + </Checkbox> + </li> + <li> + <Checkbox v-model="emojiReactionsOnTimeline"> + {{ $t('settings.emoji_reactions_on_timeline') }} + </Checkbox> + </li> + </ul> + </div> + + <div class="setting-item"> + <h2>{{ $t('settings.composing') }}</h2> + <ul class="setting-list"> + <li> + <Checkbox v-model="scopeCopy"> + {{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }} + </Checkbox> + </li> + <li> + <Checkbox v-model="alwaysShowSubjectInput"> + {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }} + </Checkbox> + </li> + <li> + <div> + {{ $t('settings.subject_line_behavior') }} + <label + for="subjectLineBehavior" + class="select" + > + <select + id="subjectLineBehavior" + v-model="subjectLineBehavior" + > + <option value="email"> + {{ $t('settings.subject_line_email') }} + {{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }} + </option> + <option value="masto"> + {{ $t('settings.subject_line_mastodon') }} + {{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }} + </option> + <option value="noop"> + {{ $t('settings.subject_line_noop') }} + {{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }} + </option> + </select> + <i class="icon-down-open" /> + </label> + </div> + </li> + <li v-if="postFormats.length > 0"> + <div> + {{ $t('settings.post_status_content_type') }} + <label + for="postContentType" + class="select" + > + <select + id="postContentType" + v-model="postContentType" + > + <option + v-for="postFormat in postFormats" + :key="postFormat" + :value="postFormat" + > + {{ $t(`post_status.content_type["${postFormat}"]`) }} + {{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }} + </option> + </select> + <i class="icon-down-open" /> + </label> + </div> + </li> + <li> + <Checkbox v-model="minimalScopesMode"> + {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }} + </Checkbox> + </li> + <li> + <Checkbox v-model="autohideFloatingPostButton"> + {{ $t('settings.autohide_floating_post_button') }} + </Checkbox> + </li> + <li> + <Checkbox v-model="padEmoji"> + {{ $t('settings.pad_emoji') }} + </Checkbox> + </li> + </ul> + </div> + + <div class="setting-item"> + <h2>{{ $t('settings.attachments') }}</h2> + <ul class="setting-list"> + <li> + <Checkbox v-model="hideAttachments"> + {{ $t('settings.hide_attachments_in_tl') }} + </Checkbox> + </li> + <li> + <Checkbox v-model="hideAttachmentsInConv"> + {{ $t('settings.hide_attachments_in_convo') }} + </Checkbox> + </li> + <li> + <label for="maxThumbnails"> + {{ $t('settings.max_thumbnails') }} + </label> + <input + id="maxThumbnails" + v-model.number="maxThumbnails" + class="number-input" + type="number" + min="0" + step="1" + > + </li> + <li> + <Checkbox v-model="hideNsfw"> + {{ $t('settings.nsfw_clickthrough') }} + </Checkbox> + </li> + <ul class="setting-list suboptions"> + <li> + <Checkbox + v-model="preloadImage" + :disabled="!hideNsfw" + > + {{ $t('settings.preload_images') }} + </Checkbox> + </li> + <li> + <Checkbox + v-model="useOneClickNsfw" + :disabled="!hideNsfw" + > + {{ $t('settings.use_one_click_nsfw') }} + </Checkbox> + </li> + </ul> + <li> + <Checkbox v-model="stopGifs"> + {{ $t('settings.stop_gifs') }} + </Checkbox> + </li> + <li> + <Checkbox v-model="loopVideo"> + {{ $t('settings.loop_video') }} + </Checkbox> + <ul + class="setting-list suboptions" + :class="[{disabled: !streaming}]" + > + <li> + <Checkbox + v-model="loopVideoSilentOnly" + :disabled="!loopVideo || !loopSilentAvailable" + > + {{ $t('settings.loop_video_silent_only') }} + </Checkbox> + <div + v-if="!loopSilentAvailable" + class="unavailable" + > + <i class="icon-globe" />! {{ $t('settings.limited_availability') }} + </div> + </li> + </ul> + </li> + <li> + <Checkbox v-model="playVideosInModal"> + {{ $t('settings.play_videos_in_modal') }} + </Checkbox> + </li> + <li> + <Checkbox v-model="useContainFit"> + {{ $t('settings.use_contain_fit') }} + </Checkbox> + </li> + </ul> + </div> + + <div class="setting-item"> + <h2>{{ $t('settings.notifications') }}</h2> + <ul class="setting-list"> + <li> + <Checkbox v-model="webPushNotifications"> + {{ $t('settings.enable_web_push_notifications') }} + </Checkbox> + </li> + </ul> + </div> + + <div class="setting-item"> + <h2>{{ $t('settings.fun') }}</h2> + <ul class="setting-list"> + <li> + <Checkbox v-model="greentext"> + {{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }} + </Checkbox> + </li> + </ul> + </div> + </div> +</template> + +<script src="./general_tab.js"></script> diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js @@ -0,0 +1,136 @@ +import get from 'lodash/get' +import map from 'lodash/map' +import reject from 'lodash/reject' +import Autosuggest from 'src/components/autosuggest/autosuggest.vue' +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js' +import BlockCard from 'src/components/block_card/block_card.vue' +import MuteCard from 'src/components/mute_card/mute_card.vue' +import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue' +import SelectableList from 'src/components/selectable_list/selectable_list.vue' +import ProgressButton from 'src/components/progress_button/progress_button.vue' +import withSubscription from 'src/components/../hocs/with_subscription/with_subscription' +import Checkbox from 'src/components/checkbox/checkbox.vue' + +const BlockList = withSubscription({ + fetch: (props, $store) => $store.dispatch('fetchBlocks'), + select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []), + childPropName: 'items' +})(SelectableList) + +const MuteList = withSubscription({ + fetch: (props, $store) => $store.dispatch('fetchMutes'), + select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []), + childPropName: 'items' +})(SelectableList) + +const DomainMuteList = withSubscription({ + fetch: (props, $store) => $store.dispatch('fetchDomainMutes'), + select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []), + childPropName: 'items' +})(SelectableList) + +const MutesAndBlocks = { + data () { + return { + activeTab: 'profile' + } + }, + created () { + this.$store.dispatch('fetchTokens') + this.$store.dispatch('getKnownDomains') + }, + components: { + TabSwitcher, + BlockList, + MuteList, + DomainMuteList, + BlockCard, + MuteCard, + DomainMuteCard, + ProgressButton, + Autosuggest, + Checkbox + }, + computed: { + knownDomains () { + return this.$store.state.instance.knownDomains + }, + user () { + return this.$store.state.users.currentUser + } + }, + methods: { + importFollows (file) { + return this.$store.state.api.backendInteractor.importFollows({ file }) + .then((status) => { + if (!status) { + throw new Error('failed') + } + }) + }, + importBlocks (file) { + return this.$store.state.api.backendInteractor.importBlocks({ file }) + .then((status) => { + if (!status) { + throw new Error('failed') + } + }) + }, + generateExportableUsersContent (users) { + // Get addresses + return users.map((user) => { + // check is it's a local user + if (user && user.is_local) { + // append the instance address + // eslint-disable-next-line no-undef + return user.screen_name + '@' + location.hostname + } + return user.screen_name + }).join('\n') + }, + activateTab (tabName) { + this.activeTab = tabName + }, + filterUnblockedUsers (userIds) { + return reject(userIds, (userId) => { + const relationship = this.$store.getters.relationship(this.userId) + return relationship.blocking || userId === this.user.id + }) + }, + filterUnMutedUsers (userIds) { + return reject(userIds, (userId) => { + const relationship = this.$store.getters.relationship(this.userId) + return relationship.muting || userId === this.user.id + }) + }, + queryUserIds (query) { + return this.$store.dispatch('searchUsers', { query }) + .then((users) => map(users, 'id')) + }, + blockUsers (ids) { + return this.$store.dispatch('blockUsers', ids) + }, + unblockUsers (ids) { + return this.$store.dispatch('unblockUsers', ids) + }, + muteUsers (ids) { + return this.$store.dispatch('muteUsers', ids) + }, + unmuteUsers (ids) { + return this.$store.dispatch('unmuteUsers', ids) + }, + filterUnMutedDomains (urls) { + return urls.filter(url => !this.user.domainMutes.includes(url)) + }, + queryKnownDomains (query) { + return new Promise((resolve, reject) => { + resolve(this.knownDomains.filter(url => url.toLowerCase().includes(query))) + }) + }, + unmuteDomains (domains) { + return this.$store.dispatch('unmuteDomains', domains) + } + } +} + +export default MutesAndBlocks diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss b/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss @@ -0,0 +1,29 @@ +.mutes-and-blocks-tab { + height: 100%; + + .usersearch-wrapper { + padding: 1em; + } + + .bulk-actions { + text-align: right; + padding: 0 1em; + min-height: 28px; + } + + .bulk-action-button { + width: 10em + } + + .domain-mute-form { + padding: 1em; + display: flex; + flex-direction: column + } + + .domain-mute-button { + align-self: flex-end; + margin-top: 1em; + width: 10em + } +} diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue @@ -0,0 +1,171 @@ +<template> + <tab-switcher + :scrollable-tabs="true" + class="mutes-and-blocks-tab" + > + <div :label="$t('settings.blocks_tab')"> + <div class="usersearch-wrapper"> + <Autosuggest + :filter="filterUnblockedUsers" + :query="queryUserIds" + :placeholder="$t('settings.search_user_to_block')" + > + <BlockCard + slot-scope="row" + :user-id="row.item" + /> + </Autosuggest> + </div> + <BlockList + :refresh="true" + :get-key="i => i" + > + <template + slot="header" + slot-scope="{selected}" + > + <div class="bulk-actions"> + <ProgressButton + v-if="selected.length > 0" + class="btn btn-default bulk-action-button" + :click="() => blockUsers(selected)" + > + {{ $t('user_card.block') }} + <template slot="progress"> + {{ $t('user_card.block_progress') }} + </template> + </ProgressButton> + <ProgressButton + v-if="selected.length > 0" + class="btn btn-default" + :click="() => unblockUsers(selected)" + > + {{ $t('user_card.unblock') }} + <template slot="progress"> + {{ $t('user_card.unblock_progress') }} + </template> + </ProgressButton> + </div> + </template> + <template + slot="item" + slot-scope="{item}" + > + <BlockCard :user-id="item" /> + </template> + <template slot="empty"> + {{ $t('settings.no_blocks') }} + </template> + </BlockList> + </div> + + <div :label="$t('settings.mutes_tab')"> + <tab-switcher> + <div label="Users"> + <div class="usersearch-wrapper"> + <Autosuggest + :filter="filterUnMutedUsers" + :query="queryUserIds" + :placeholder="$t('settings.search_user_to_mute')" + > + <MuteCard + slot-scope="row" + :user-id="row.item" + /> + </Autosuggest> + </div> + <MuteList + :refresh="true" + :get-key="i => i" + > + <template + slot="header" + slot-scope="{selected}" + > + <div class="bulk-actions"> + <ProgressButton + v-if="selected.length > 0" + class="btn btn-default" + :click="() => muteUsers(selected)" + > + {{ $t('user_card.mute') }} + <template slot="progress"> + {{ $t('user_card.mute_progress') }} + </template> + </ProgressButton> + <ProgressButton + v-if="selected.length > 0" + class="btn btn-default" + :click="() => unmuteUsers(selected)" + > + {{ $t('user_card.unmute') }} + <template slot="progress"> + {{ $t('user_card.unmute_progress') }} + </template> + </ProgressButton> + </div> + </template> + <template + slot="item" + slot-scope="{item}" + > + <MuteCard :user-id="item" /> + </template> + <template slot="empty"> + {{ $t('settings.no_mutes') }} + </template> + </MuteList> + </div> + + <div :label="$t('settings.domain_mutes')"> + <div class="domain-mute-form"> + <Autosuggest + :filter="filterUnMutedDomains" + :query="queryKnownDomains" + :placeholder="$t('settings.type_domains_to_mute')" + > + <DomainMuteCard + slot-scope="row" + :domain="row.item" + /> + </Autosuggest> + </div> + <DomainMuteList + :refresh="true" + :get-key="i => i" + > + <template + slot="header" + slot-scope="{selected}" + > + <div class="bulk-actions"> + <ProgressButton + v-if="selected.length > 0" + class="btn btn-default" + :click="() => unmuteDomains(selected)" + > + {{ $t('domain_mute_card.unmute') }} + <template slot="progress"> + {{ $t('domain_mute_card.unmute_progress') }} + </template> + </ProgressButton> + </div> + </template> + <template + slot="item" + slot-scope="{item}" + > + <DomainMuteCard :domain="item" /> + </template> + <template slot="empty"> + {{ $t('settings.no_mutes') }} + </template> + </DomainMuteList> + </div> + </tab-switcher> + </div> + </tab-switcher> +</template> + +<script src="./mutes_and_blocks_tab.js"></script> +<style lang="scss" src="./mutes_and_blocks_tab.scss"></style> diff --git a/src/components/settings_modal/tabs/notifications_tab.js b/src/components/settings_modal/tabs/notifications_tab.js @@ -0,0 +1,27 @@ +import Checkbox from 'src/components/checkbox/checkbox.vue' + +const NotificationsTab = { + data () { + return { + activeTab: 'profile', + notificationSettings: this.$store.state.users.currentUser.notification_settings, + newDomainToMute: '' + } + }, + components: { + Checkbox + }, + computed: { + user () { + return this.$store.state.users.currentUser + } + }, + methods: { + updateNotificationSettings () { + this.$store.state.api.backendInteractor + .updateNotificationSettings({ settings: this.notificationSettings }) + } + } +} + +export default NotificationsTab diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue @@ -0,0 +1,34 @@ +<template> + <div :label="$t('settings.notifications')"> + <div class="setting-item"> + <h2>{{ $t('settings.notification_setting_filters') }}</h2> + <p> + <Checkbox v-model="notificationSettings.block_from_strangers"> + {{ $t('settings.notification_setting_block_from_strangers') }} + </Checkbox> + </p> + </div> + + <div class="setting-item"> + <h2>{{ $t('settings.notification_setting_privacy') }}</h2> + <p> + <Checkbox v-model="notificationSettings.hide_notification_contents"> + {{ $t('settings.notification_setting_hide_notification_contents') }} + </Checkbox> + </p> + </div> + <div class="setting-item"> + <p>{{ $t('settings.notification_mutes') }}</p> + <p>{{ $t('settings.notification_blocks') }}</p> + <button + class="btn btn-default" + @click="updateNotificationSettings" + > + {{ $t('general.submit') }} + </button> + </div> + </div> +</template> + +<script src="./notifications_tab.js"></script> +<!-- <style lang="scss" src="./profile.scss"></style> --> diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js @@ -0,0 +1,253 @@ +import unescape from 'lodash/unescape' +import merge from 'lodash/merge' +import ImageCropper from 'src/components/image_cropper/image_cropper.vue' +import ScopeSelector from 'src/components/scope_selector/scope_selector.vue' +import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js' +import ProgressButton from 'src/components/progress_button/progress_button.vue' +import EmojiInput from 'src/components/emoji_input/emoji_input.vue' +import suggestor from 'src/components/emoji_input/suggestor.js' +import Autosuggest from 'src/components/autosuggest/autosuggest.vue' +import Checkbox from 'src/components/checkbox/checkbox.vue' + +const ProfileTab = { + data () { + return { + newName: this.$store.state.users.currentUser.name, + newBio: unescape(this.$store.state.users.currentUser.description), + newLocked: this.$store.state.users.currentUser.locked, + newNoRichText: this.$store.state.users.currentUser.no_rich_text, + newDefaultScope: this.$store.state.users.currentUser.default_scope, + newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })), + hideFollows: this.$store.state.users.currentUser.hide_follows, + hideFollowers: this.$store.state.users.currentUser.hide_followers, + hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count, + hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count, + showRole: this.$store.state.users.currentUser.show_role, + role: this.$store.state.users.currentUser.role, + discoverable: this.$store.state.users.currentUser.discoverable, + bot: this.$store.state.users.currentUser.bot, + allowFollowingMove: this.$store.state.users.currentUser.allow_following_move, + pickAvatarBtnVisible: true, + bannerUploading: false, + backgroundUploading: false, + banner: null, + bannerPreview: null, + background: null, + backgroundPreview: null, + bannerUploadError: null, + backgroundUploadError: null + } + }, + components: { + ScopeSelector, + ImageCropper, + EmojiInput, + Autosuggest, + ProgressButton, + Checkbox + }, + computed: { + user () { + return this.$store.state.users.currentUser + }, + emojiUserSuggestor () { + return suggestor({ + emoji: [ + ...this.$store.state.instance.emoji, + ...this.$store.state.instance.customEmoji + ], + users: this.$store.state.users.users, + updateUsersList: (query) => this.$store.dispatch('searchUsers', { query }) + }) + }, + emojiSuggestor () { + return suggestor({ emoji: [ + ...this.$store.state.instance.emoji, + ...this.$store.state.instance.customEmoji + ] }) + }, + userSuggestor () { + return suggestor({ + users: this.$store.state.users.users, + updateUsersList: (query) => this.$store.dispatch('searchUsers', { query }) + }) + }, + fieldsLimits () { + return this.$store.state.instance.fieldsLimits + }, + maxFields () { + return this.fieldsLimits ? this.fieldsLimits.maxFields : 0 + }, + defaultAvatar () { + return this.$store.state.instance.server + this.$store.state.instance.defaultAvatar + }, + defaultBanner () { + return this.$store.state.instance.server + this.$store.state.instance.defaultBanner + }, + isDefaultAvatar () { + const baseAvatar = this.$store.state.instance.defaultAvatar + return !(this.$store.state.users.currentUser.profile_image_url) || + this.$store.state.users.currentUser.profile_image_url.includes(baseAvatar) + }, + isDefaultBanner () { + const baseBanner = this.$store.state.instance.defaultBanner + return !(this.$store.state.users.currentUser.cover_photo) || + this.$store.state.users.currentUser.cover_photo.includes(baseBanner) + }, + isDefaultBackground () { + return !(this.$store.state.users.currentUser.background_image) + }, + avatarImgSrc () { + const src = this.$store.state.users.currentUser.profile_image_url_original + return (!src) ? this.defaultAvatar : src + }, + bannerImgSrc () { + const src = this.$store.state.users.currentUser.cover_photo + return (!src) ? this.defaultBanner : src + } + }, + methods: { + updateProfile () { + this.$store.state.api.backendInteractor + .updateProfile({ + params: { + note: this.newBio, + locked: this.newLocked, + // Backend notation. + /* eslint-disable camelcase */ + display_name: this.newName, + fields_attributes: this.newFields.filter(el => el != null), + default_scope: this.newDefaultScope, + no_rich_text: this.newNoRichText, + hide_follows: this.hideFollows, + hide_followers: this.hideFollowers, + discoverable: this.discoverable, + bot: this.bot, + allow_following_move: this.allowFollowingMove, + hide_follows_count: this.hideFollowsCount, + hide_followers_count: this.hideFollowersCount, + show_role: this.showRole + /* eslint-enable camelcase */ + } }).then((user) => { + this.newFields.splice(user.fields.length) + merge(this.newFields, user.fields) + this.$store.commit('addNewUsers', [user]) + this.$store.commit('setCurrentUser', user) + }) + }, + changeVis (visibility) { + this.newDefaultScope = visibility + }, + addField () { + if (this.newFields.length < this.maxFields) { + this.newFields.push({ name: '', value: '' }) + return true + } + return false + }, + deleteField (index, event) { + this.$delete(this.newFields, index) + }, + uploadFile (slot, e) { + const file = e.target.files[0] + if (!file) { return } + if (file.size > this.$store.state.instance[slot + 'limit']) { + const filesize = fileSizeFormatService.fileSizeFormat(file.size) + const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit']) + this[slot + 'UploadError'] = [ + this.$t('upload.error.base'), + this.$t( + 'upload.error.file_too_big', + { + filesize: filesize.num, + filesizeunit: filesize.unit, + allowedsize: allowedsize.num, + allowedsizeunit: allowedsize.unit + } + ) + ].join(' ') + return + } + // eslint-disable-next-line no-undef + const reader = new FileReader() + reader.onload = ({ target }) => { + const img = target.result + this[slot + 'Preview'] = img + this[slot] = file + } + reader.readAsDataURL(file) + }, + resetAvatar () { + const confirmed = window.confirm(this.$t('settings.reset_avatar_confirm')) + if (confirmed) { + this.submitAvatar(undefined, '') + } + }, + resetBanner () { + const confirmed = window.confirm(this.$t('settings.reset_banner_confirm')) + if (confirmed) { + this.submitBanner('') + } + }, + resetBackground () { + const confirmed = window.confirm(this.$t('settings.reset_background_confirm')) + if (confirmed) { + this.submitBackground('') + } + }, + submitAvatar (cropper, file) { + const that = this + return new Promise((resolve, reject) => { + function updateAvatar (avatar) { + that.$store.state.api.backendInteractor.updateProfileImages({ avatar }) + .then((user) => { + that.$store.commit('addNewUsers', [user]) + that.$store.commit('setCurrentUser', user) + resolve() + }) + .catch((err) => { + reject(new Error(that.$t('upload.error.base') + ' ' + err.message)) + }) + } + + if (cropper) { + cropper.getCroppedCanvas().toBlob(updateAvatar, file.type) + } else { + updateAvatar(file) + } + }) + }, + submitBanner (banner) { + if (!this.bannerPreview && banner !== '') { return } + + this.bannerUploading = true + this.$store.state.api.backendInteractor.updateProfileImages({ banner }) + .then((user) => { + this.$store.commit('addNewUsers', [user]) + this.$store.commit('setCurrentUser', user) + this.bannerPreview = null + }) + .catch((err) => { + this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message + }) + .then(() => { this.bannerUploading = false }) + }, + submitBackground (background) { + if (!this.backgroundPreview && background !== '') { return } + + this.backgroundUploading = true + this.$store.state.api.backendInteractor.updateProfileImages({ background }).then((data) => { + if (!data.error) { + this.$store.commit('addNewUsers', [data]) + this.$store.commit('setCurrentUser', data) + this.backgroundPreview = null + } else { + this.backgroundUploadError = this.$t('upload.error.base') + data.error + } + this.backgroundUploading = false + }) + } + } +} + +export default ProfileTab diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss @@ -0,0 +1,128 @@ +@import '../../../_variables.scss'; +.profile-tab { + .bio { + margin: 0; + } + + .visibility-tray { + padding-top: 5px; + } + + input[type=file] { + padding: 5px; + height: auto; + } + + .banner-background-preview { + max-width: 100%; + width: 300px; + position: relative; + + img { + width: 100%; + } + } + + .uploading { + font-size: 1.5em; + margin: 0.25em; + } + + .name-changer { + width: 100%; + } + + .current-avatar-container { + position: relative; + width: 150px; + height: 150px; + } + + .current-avatar { + display: block; + width: 100%; + height: 100%; + border-radius: $fallback--avatarRadius; + border-radius: var(--avatarRadius, $fallback--avatarRadius); + } + + .reset-button { + position: absolute; + top: 0.2em; + right: 0.2em; + border-radius: $fallback--tooltipRadius; + border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + background-color: rgba(0, 0, 0, 0.6); + opacity: 0.7; + color: white; + width: 1.5em; + height: 1.5em; + text-align: center; + line-height: 1.5em; + font-size: 1.5em; + cursor: pointer; + &:hover { + opacity: 1; + } + } + + .oauth-tokens { + width: 100%; + + th { + text-align: left; + } + + .actions { + text-align: right; + } + } + + &-usersearch-wrapper { + padding: 1em; + } + + &-bulk-actions { + text-align: right; + padding: 0 1em; + min-height: 28px; + + button { + width: 10em; + } + } + + &-domain-mute-form { + padding: 1em; + display: flex; + flex-direction: column; + + button { + align-self: flex-end; + margin-top: 1em; + width: 10em; + } + } + + .setting-subitem { + margin-left: 1.75em; + } + + .profile-fields { + display: flex; + + &>.emoji-input { + flex: 1 1 auto; + margin: 0 .2em .5em; + min-width: 0; + } + + &>.icon-container { + width: 20px; + + &>.icon-cancel { + vertical-align: sub; + } + } + } +} diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue @@ -0,0 +1,289 @@ +<template> + <div class="profile-tab"> + <div class="setting-item"> + <h2>{{ $t('settings.name_bio') }}</h2> + <p>{{ $t('settings.name') }}</p> + <EmojiInput + v-model="newName" + enable-emoji-picker + :suggest="emojiSuggestor" + > + <input + id="username" + v-model="newName" + classname="name-changer" + > + </EmojiInput> + <p>{{ $t('settings.bio') }}</p> + <EmojiInput + v-model="newBio" + enable-emoji-picker + :suggest="emojiUserSuggestor" + > + <textarea + v-model="newBio" + classname="bio" + /> + </EmojiInput> + <p> + <Checkbox v-model="newLocked"> + {{ $t('settings.lock_account_description') }} + </Checkbox> + </p> + <div> + <label for="default-vis">{{ $t('settings.default_vis') }}</label> + <div + id="default-vis" + class="visibility-tray" + > + <scope-selector + :show-all="true" + :user-default="newDefaultScope" + :initial-scope="newDefaultScope" + :on-scope-change="changeVis" + /> + </div> + </div> + <p> + <Checkbox v-model="newNoRichText"> + {{ $t('settings.no_rich_text_description') }} + </Checkbox> + </p> + <p> + <Checkbox v-model="hideFollows"> + {{ $t('settings.hide_follows_description') }} + </Checkbox> + </p> + <p class="setting-subitem"> + <Checkbox + v-model="hideFollowsCount" + :disabled="!hideFollows" + > + {{ $t('settings.hide_follows_count_description') }} + </Checkbox> + </p> + <p> + <Checkbox v-model="hideFollowers"> + {{ $t('settings.hide_followers_description') }} + </Checkbox> + </p> + <p class="setting-subitem"> + <Checkbox + v-model="hideFollowersCount" + :disabled="!hideFollowers" + > + {{ $t('settings.hide_followers_count_description') }} + </Checkbox> + </p> + <p> + <Checkbox v-model="allowFollowingMove"> + {{ $t('settings.allow_following_move') }} + </Checkbox> + </p> + <p v-if="role === 'admin' || role === 'moderator'"> + <Checkbox v-model="showRole"> + <template v-if="role === 'admin'"> + {{ $t('settings.show_admin_badge') }} + </template> + <template v-if="role === 'moderator'"> + {{ $t('settings.show_moderator_badge') }} + </template> + </Checkbox> + </p> + <p> + <Checkbox v-model="discoverable"> + {{ $t('settings.discoverable') }} + </Checkbox> + </p> + <div v-if="maxFields > 0"> + <p>{{ $t('settings.profile_fields.label') }}</p> + <div + v-for="(_, i) in newFields" + :key="i" + class="profile-fields" + > + <EmojiInput + v-model="newFields[i].name" + enable-emoji-picker + hide-emoji-button + :suggest="userSuggestor" + > + <input + v-model="newFields[i].name" + :placeholder="$t('settings.profile_fields.name')" + > + </EmojiInput> + <EmojiInput + v-model="newFields[i].value" + enable-emoji-picker + hide-emoji-button + :suggest="userSuggestor" + > + <input + v-model="newFields[i].value" + :placeholder="$t('settings.profile_fields.value')" + > + </EmojiInput> + <div + class="icon-container" + > + <i + v-show="newFields.length > 1" + class="icon-cancel" + @click="deleteField(i)" + /> + </div> + </div> + <a + v-if="newFields.length < maxFields" + class="add-field faint" + @click="addField" + > + <i class="icon-plus" /> + {{ $t("settings.profile_fields.add_field") }} + </a> + </div> + <p> + <Checkbox v-model="bot"> + {{ $t('settings.bot') }} + </Checkbox> + </p> + <button + :disabled="newName && newName.length === 0" + class="btn btn-default" + @click="updateProfile" + > + {{ $t('general.submit') }} + </button> + </div> + <div class="setting-item"> + <h2>{{ $t('settings.avatar') }}</h2> + <p class="visibility-notice"> + {{ $t('settings.avatar_size_instruction') }} + </p> + <div class="current-avatar-container"> + <img + :src="user.profile_image_url_original" + class="current-avatar" + > + <i + v-if="!isDefaultAvatar && pickAvatarBtnVisible" + :title="$t('settings.reset_avatar')" + class="reset-button icon-cancel" + type="button" + @click="resetAvatar" + /> + </div> + <p>{{ $t('settings.set_new_avatar') }}</p> + <button + v-show="pickAvatarBtnVisible" + id="pick-avatar" + class="btn" + type="button" + > + {{ $t('settings.upload_a_photo') }} + </button> + <image-cropper + trigger="#pick-avatar" + :submit-handler="submitAvatar" + @open="pickAvatarBtnVisible=false" + @close="pickAvatarBtnVisible=true" + /> + </div> + <div class="setting-item"> + <h2>{{ $t('settings.profile_banner') }}</h2> + <div class="banner-background-preview"> + <img :src="user.cover_photo"> + <i + v-if="!isDefaultBanner" + :title="$t('settings.reset_profile_banner')" + class="reset-button icon-cancel" + type="button" + @click="resetBanner" + /> + </div> + <p>{{ $t('settings.set_new_profile_banner') }}</p> + <img + v-if="bannerPreview" + class="banner-background-preview" + :src="bannerPreview" + > + <div> + <input + type="file" + @change="uploadFile('banner', $event)" + > + </div> + <i + v-if="bannerUploading" + class=" icon-spin4 animate-spin uploading" + /> + <button + v-else-if="bannerPreview" + class="btn btn-default" + @click="submitBanner(banner)" + > + {{ $t('general.submit') }} + </button> + <div + v-if="bannerUploadError" + class="alert error" + > + Error: {{ bannerUploadError }} + <i + class="button-icon icon-cancel" + @click="clearUploadError('banner')" + /> + </div> + </div> + <div class="setting-item"> + <h2>{{ $t('settings.profile_background') }}</h2> + <div class="banner-background-preview"> + <img :src="user.background_image"> + <i + v-if="!isDefaultBackground" + :title="$t('settings.reset_profile_background')" + class="reset-button icon-cancel" + type="button" + @click="resetBackground" + /> + </div> + <p>{{ $t('settings.set_new_profile_background') }}</p> + <img + v-if="backgroundPreview" + class="banner-background-preview" + :src="backgroundPreview" + > + <div> + <input + type="file" + @change="uploadFile('background', $event)" + > + </div> + <i + v-if="backgroundUploading" + class=" icon-spin4 animate-spin uploading" + /> + <button + v-else-if="backgroundPreview" + class="btn btn-default" + @click="submitBackground(background)" + > + {{ $t('general.submit') }} + </button> + <div + v-if="backgroundUploadError" + class="alert error" + > + Error: {{ backgroundUploadError }} + <i + class="button-icon icon-cancel" + @click="clearUploadError('background')" + /> + </div> + </div> + </div> +</template> + +<script src="./profile_tab.js"></script> +<style lang="scss" src="./profile_tab.scss"></style> diff --git a/src/components/user_settings/confirm.js b/src/components/settings_modal/tabs/security_tab/confirm.js diff --git a/src/components/user_settings/confirm.vue b/src/components/settings_modal/tabs/security_tab/confirm.vue diff --git a/src/components/user_settings/mfa.js b/src/components/settings_modal/tabs/security_tab/mfa.js diff --git a/src/components/settings_modal/tabs/security_tab/mfa.vue b/src/components/settings_modal/tabs/security_tab/mfa.vue @@ -0,0 +1,173 @@ +<template> + <div + v-if="readyInit && settings.available" + class="setting-item mfa-settings" + > + <div class="mfa-heading"> + <h2>{{ $t('settings.mfa.title') }}</h2> + </div> + + <div> + <div + v-if="!setupInProgress" + class="setting-item" + > + <!-- Enabled methods --> + <h3>{{ $t('settings.mfa.authentication_methods') }}</h3> + <totp-item + :settings="settings" + @deactivate="fetchSettings" + @activate="activateOTP" + /> + <br> + + <div v-if="settings.enabled"> + <!-- backup codes block--> + <recovery-codes + v-if="!confirmNewBackupCodes" + :backup-codes="backupCodes" + /> + <button + v-if="!confirmNewBackupCodes" + class="btn btn-default" + @click="getBackupCodes" + > + {{ $t('settings.mfa.generate_new_recovery_codes') }} + </button> + + <div v-if="confirmNewBackupCodes"> + <confirm + :disabled="backupCodes.inProgress" + @confirm="confirmBackupCodes" + @cancel="cancelBackupCodes" + > + <p class="warning"> + {{ $t('settings.mfa.warning_of_generate_new_codes') }} + </p> + </confirm> + </div> + </div> + </div> + + <div v-if="setupInProgress"> + <!-- setup block--> + + <h3>{{ $t('settings.mfa.setup_otp') }}</h3> + + <recovery-codes + v-if="!setupOTPInProgress" + :backup-codes="backupCodes" + /> + + <button + v-if="canSetupOTP" + class="btn btn-default" + @click="cancelSetup" + > + {{ $t('general.cancel') }} + </button> + + <button + v-if="canSetupOTP" + class="btn btn-default" + @click="setupOTP" + > + {{ $t('settings.mfa.setup_otp') }} + </button> + + <template v-if="setupOTPInProgress"> + <i v-if="prepareOTP">{{ $t('settings.mfa.wait_pre_setup_otp') }}</i> + + <div v-if="confirmOTP"> + <div class="setup-otp"> + <div class="qr-code"> + <h4>{{ $t('settings.mfa.scan.title') }}</h4> + <p>{{ $t('settings.mfa.scan.desc') }}</p> + <qrcode + :value="otpSettings.provisioning_uri" + :options="{ width: 200 }" + /> + <p> + {{ $t('settings.mfa.scan.secret_code') }}: + {{ otpSettings.key }} + </p> + </div> + + <div class="verify"> + <h4>{{ $t('general.verify') }}</h4> + <p>{{ $t('settings.mfa.verify.desc') }}</p> + <input + v-model="otpConfirmToken" + type="text" + > + + <p>{{ $t('settings.enter_current_password_to_confirm') }}:</p> + <input + v-model="currentPassword" + type="password" + > + <div class="confirm-otp-actions"> + <button + class="btn btn-default" + @click="doConfirmOTP" + > + {{ $t('settings.mfa.confirm_and_enable') }} + </button> + <button + class="btn btn-default" + @click="cancelSetup" + > + {{ $t('general.cancel') }} + </button> + </div> + <div + v-if="error" + class="alert error" + > + {{ error }} + </div> + </div> + </div> + </div> + </template> + </div> + </div> + </div> +</template> + +<script src="./mfa.js"></script> +<style lang="scss"> +@import '../../../../_variables.scss'; +.mfa-settings { + .mfa-heading, .method-item { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: baseline; + } + + .warning { + color: $fallback--cOrange; + color: var(--cOrange, $fallback--cOrange); + } + + .setup-otp { + display: flex; + justify-content: center; + flex-wrap: wrap; + .qr-code { + flex: 1; + padding-right: 10px; + } + .verify { flex: 1; } + .error { margin: 4px 0 0 0; } + .confirm-otp-actions { + button { + width: 15em; + margin-top: 5px; + } + + } + } +} +</style> diff --git a/src/components/user_settings/mfa_backup_codes.js b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js diff --git a/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue @@ -0,0 +1,35 @@ +<template> + <div class="mfa-backup-codes"> + <h4 v-if="displayTitle"> + {{ $t('settings.mfa.recovery_codes') }} + </h4> + <i v-if="inProgress">{{ $t('settings.mfa.waiting_a_recovery_codes') }}</i> + <template v-if="ready"> + <p class="alert warning"> + {{ $t('settings.mfa.recovery_codes_warning') }} + </p> + <ul class="backup-codes"> + <li + v-for="code in backupCodes.codes" + :key="code" + > + {{ code }} + </li> + </ul> + </template> + </div> +</template> +<script src="./mfa_backup_codes.js"></script> +<style lang="scss"> +@import '../../../../_variables.scss'; + +.mfa-backup-codes { + .warning { + color: $fallback--cOrange; + color: var(--cOrange, $fallback--cOrange); + } + .backup-codes { + font-family: var(--postCodeFont, monospace); + } +} +</style> diff --git a/src/components/user_settings/mfa_totp.js b/src/components/settings_modal/tabs/security_tab/mfa_totp.js diff --git a/src/components/user_settings/mfa_totp.vue b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.js b/src/components/settings_modal/tabs/security_tab/security_tab.js @@ -0,0 +1,106 @@ +import ProgressButton from 'src/components/progress_button/progress_button.vue' +import Checkbox from 'src/components/checkbox/checkbox.vue' +import Mfa from './mfa.vue' + +const SecurityTab = { + data () { + return { + newEmail: '', + changeEmailError: false, + changeEmailPassword: '', + changedEmail: false, + deletingAccount: false, + deleteAccountConfirmPasswordInput: '', + deleteAccountError: false, + changePasswordInputs: [ '', '', '' ], + changedPassword: false, + changePasswordError: false + } + }, + created () { + this.$store.dispatch('fetchTokens') + }, + components: { + ProgressButton, + Mfa, + Checkbox + }, + computed: { + user () { + return this.$store.state.users.currentUser + }, + pleromaBackend () { + return this.$store.state.instance.pleromaBackend + }, + oauthTokens () { + return this.$store.state.oauthTokens.tokens.map(oauthToken => { + return { + id: oauthToken.id, + appName: oauthToken.app_name, + validUntil: new Date(oauthToken.valid_until).toLocaleDateString() + } + }) + } + }, + methods: { + confirmDelete () { + this.deletingAccount = true + }, + deleteAccount () { + this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput }) + .then((res) => { + if (res.status === 'success') { + this.$store.dispatch('logout') + this.$router.push({ name: 'root' }) + } else { + this.deleteAccountError = res.error + } + }) + }, + changePassword () { + const params = { + password: this.changePasswordInputs[0], + newPassword: this.changePasswordInputs[1], + newPasswordConfirmation: this.changePasswordInputs[2] + } + this.$store.state.api.backendInteractor.changePassword(params) + .then((res) => { + if (res.status === 'success') { + this.changedPassword = true + this.changePasswordError = false + this.logout() + } else { + this.changedPassword = false + this.changePasswordError = res.error + } + }) + }, + changeEmail () { + const params = { + email: this.newEmail, + password: this.changeEmailPassword + } + this.$store.state.api.backendInteractor.changeEmail(params) + .then((res) => { + if (res.status === 'success') { + this.changedEmail = true + this.changeEmailError = false + } else { + this.changedEmail = false + this.changeEmailError = res.error + } + }) + }, + logout () { + this.$store.dispatch('logout') + this.$router.replace('/') + }, + revokeToken (id) { + if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) { + this.$store.dispatch('revokeToken', id) + } + } + } +} + +export default SecurityTab diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue @@ -0,0 +1,143 @@ +<template> + <div :label="$t('settings.security_tab')"> + <div class="setting-item"> + <h2>{{ $t('settings.change_email') }}</h2> + <div> + <p>{{ $t('settings.new_email') }}</p> + <input + v-model="newEmail" + type="email" + autocomplete="email" + > + </div> + <div> + <p>{{ $t('settings.current_password') }}</p> + <input + v-model="changeEmailPassword" + type="password" + autocomplete="current-password" + > + </div> + <button + class="btn btn-default" + @click="changeEmail" + > + {{ $t('general.submit') }} + </button> + <p v-if="changedEmail"> + {{ $t('settings.changed_email') }} + </p> + <template v-if="changeEmailError !== false"> + <p>{{ $t('settings.change_email_error') }}</p> + <p>{{ changeEmailError }}</p> + </template> + </div> + + <div class="setting-item"> + <h2>{{ $t('settings.change_password') }}</h2> + <div> + <p>{{ $t('settings.current_password') }}</p> + <input + v-model="changePasswordInputs[0]" + type="password" + > + </div> + <div> + <p>{{ $t('settings.new_password') }}</p> + <input + v-model="changePasswordInputs[1]" + type="password" + > + </div> + <div> + <p>{{ $t('settings.confirm_new_password') }}</p> + <input + v-model="changePasswordInputs[2]" + type="password" + > + </div> + <button + class="btn btn-default" + @click="changePassword" + > + {{ $t('general.submit') }} + </button> + <p v-if="changedPassword"> + {{ $t('settings.changed_password') }} + </p> + <p v-else-if="changePasswordError !== false"> + {{ $t('settings.change_password_error') }} + </p> + <p v-if="changePasswordError"> + {{ changePasswordError }} + </p> + </div> + + <div class="setting-item"> + <h2>{{ $t('settings.oauth_tokens') }}</h2> + <table class="oauth-tokens"> + <thead> + <tr> + <th>{{ $t('settings.app_name') }}</th> + <th>{{ $t('settings.valid_until') }}</th> + <th /> + </tr> + </thead> + <tbody> + <tr + v-for="oauthToken in oauthTokens" + :key="oauthToken.id" + > + <td>{{ oauthToken.appName }}</td> + <td>{{ oauthToken.validUntil }}</td> + <td class="actions"> + <button + class="btn btn-default" + @click="revokeToken(oauthToken.id)" + > + {{ $t('settings.revoke_token') }} + </button> + </td> + </tr> + </tbody> + </table> + </div> + <mfa /> + <div class="setting-item"> + <h2>{{ $t('settings.delete_account') }}</h2> + <p v-if="!deletingAccount"> + {{ $t('settings.delete_account_description') }} + </p> + <div v-if="deletingAccount"> + <p>{{ $t('settings.delete_account_instructions') }}</p> + <p>{{ $t('login.password') }}</p> + <input + v-model="deleteAccountConfirmPasswordInput" + type="password" + > + <button + class="btn btn-default" + @click="deleteAccount" + > + {{ $t('settings.delete_account') }} + </button> + </div> + <p v-if="deleteAccountError !== false"> + {{ $t('settings.delete_account_error') }} + </p> + <p v-if="deleteAccountError"> + {{ deleteAccountError }} + </p> + <button + v-if="!deletingAccount" + class="btn btn-default" + @click="confirmDelete" + > + {{ $t('general.submit') }} + </button> + </div> + </div> +</template> + +<script src="./security_tab.js"></script> +<!-- <style lang="scss" src="./profile.scss"></style> --> diff --git a/src/components/style_switcher/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -0,0 +1,761 @@ +import { set, delete as del } from 'vue' +import { + rgb2hex, + hex2rgb, + getContrastRatioLayers +} from 'src/services/color_convert/color_convert.js' +import { + DEFAULT_SHADOWS, + generateColors, + generateShadows, + generateRadii, + generateFonts, + composePreset, + getThemes, + shadows2to3, + colors2to3 +} from 'src/services/style_setter/style_setter.js' +import { + SLOT_INHERITANCE +} from 'src/services/theme_data/pleromafe.js' +import { + CURRENT_VERSION, + OPACITIES, + getLayers, + getOpacitySlot +} from 'src/services/theme_data/theme_data.service.js' +import ColorInput from 'src/components/color_input/color_input.vue' +import RangeInput from 'src/components/range_input/range_input.vue' +import OpacityInput from 'src/components/opacity_input/opacity_input.vue' +import ShadowControl from 'src/components/shadow_control/shadow_control.vue' +import FontControl from 'src/components/font_control/font_control.vue' +import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue' +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js' +import ExportImport from 'src/components/export_import/export_import.vue' +import Checkbox from 'src/components/checkbox/checkbox.vue' + +import Preview from './preview.vue' + +// List of color values used in v1 +const v1OnlyNames = [ + 'bg', + 'fg', + 'text', + 'link', + 'cRed', + 'cGreen', + 'cBlue', + 'cOrange' +].map(_ => _ + 'ColorLocal') + +const colorConvert = (color) => { + if (color.startsWith('--') || color === 'transparent') { + return color + } else { + return hex2rgb(color) + } +} + +export default { + data () { + return { + availableStyles: [], + selected: this.$store.getters.mergedConfig.theme, + themeWarning: undefined, + tempImportFile: undefined, + engineVersion: 0, + + previewShadows: {}, + previewColors: {}, + previewRadii: {}, + previewFonts: {}, + + shadowsInvalid: true, + colorsInvalid: true, + radiiInvalid: true, + + keepColor: false, + keepShadows: false, + keepOpacity: false, + keepRoundness: false, + keepFonts: false, + + ...Object.keys(SLOT_INHERITANCE) + .map(key => [key, '']) + .reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}), + + ...Object.keys(OPACITIES) + .map(key => [key, '']) + .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}), + + shadowSelected: undefined, + shadowsLocal: {}, + fontsLocal: {}, + + btnRadiusLocal: '', + inputRadiusLocal: '', + checkboxRadiusLocal: '', + panelRadiusLocal: '', + avatarRadiusLocal: '', + avatarAltRadiusLocal: '', + attachmentRadiusLocal: '', + tooltipRadiusLocal: '', + chatMessageRadiusLocal: '' + } + }, + created () { + const self = this + + getThemes() + .then((promises) => { + return Promise.all( + Object.entries(promises) + .map(([k, v]) => v.then(res => [k, res])) + ) + }) + .then(themes => themes.reduce((acc, [k, v]) => { + if (v) { + return { + ...acc, + [k]: v + } + } else { + return acc + } + }, {})) + .then((themesComplete) => { + self.availableStyles = themesComplete + }) + }, + mounted () { + this.loadThemeFromLocalStorage() + if (typeof this.shadowSelected === 'undefined') { + this.shadowSelected = this.shadowsAvailable[0] + } + }, + computed: { + themeWarningHelp () { + if (!this.themeWarning) return + const t = this.$t + const pre = 'settings.style.switcher.help.' + const { + origin, + themeEngineVersion, + type, + noActionsPossible + } = this.themeWarning + if (origin === 'file') { + // Loaded v2 theme from file + if (themeEngineVersion === 2 && type === 'wrong_version') { + return t(pre + 'v2_imported') + } + if (themeEngineVersion > CURRENT_VERSION) { + return t(pre + 'future_version_imported') + ' ' + + ( + noActionsPossible + ? t(pre + 'snapshot_missing') + : t(pre + 'snapshot_present') + ) + } + if (themeEngineVersion < CURRENT_VERSION) { + return t(pre + 'future_version_imported') + ' ' + + ( + noActionsPossible + ? t(pre + 'snapshot_missing') + : t(pre + 'snapshot_present') + ) + } + } else if (origin === 'localStorage') { + if (type === 'snapshot_source_mismatch') { + return t(pre + 'snapshot_source_mismatch') + } + // FE upgraded from v2 + if (themeEngineVersion === 2) { + return t(pre + 'upgraded_from_v2') + } + // Admin downgraded FE + if (themeEngineVersion > CURRENT_VERSION) { + return t(pre + 'fe_downgraded') + ' ' + + ( + noActionsPossible + ? t(pre + 'migration_snapshot_ok') + : t(pre + 'migration_snapshot_gone') + ) + } + // Admin upgraded FE + if (themeEngineVersion < CURRENT_VERSION) { + return t(pre + 'fe_upgraded') + ' ' + + ( + noActionsPossible + ? t(pre + 'migration_snapshot_ok') + : t(pre + 'migration_snapshot_gone') + ) + } + } + }, + selectedVersion () { + return Array.isArray(this.selected) ? 1 : 2 + }, + currentColors () { + return Object.keys(SLOT_INHERITANCE) + .map(key => [key, this[key + 'ColorLocal']]) + .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {}) + }, + currentOpacity () { + return Object.keys(OPACITIES) + .map(key => [key, this[key + 'OpacityLocal']]) + .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {}) + }, + currentRadii () { + return { + btn: this.btnRadiusLocal, + input: this.inputRadiusLocal, + checkbox: this.checkboxRadiusLocal, + panel: this.panelRadiusLocal, + avatar: this.avatarRadiusLocal, + avatarAlt: this.avatarAltRadiusLocal, + tooltip: this.tooltipRadiusLocal, + attachment: this.attachmentRadiusLocal, + chatMessage: this.chatMessageRadiusLocal + } + }, + preview () { + return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts) + }, + previewTheme () { + if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} } + return this.preview.theme + }, + // This needs optimization maybe + previewContrast () { + try { + if (!this.previewTheme.colors.bg) return {} + const colors = this.previewTheme.colors + const opacity = this.previewTheme.opacity + if (!colors.bg) return {} + const hints = (ratio) => ({ + text: ratio.toPrecision(3) + ':1', + // AA level, AAA level + aa: ratio >= 4.5, + aaa: ratio >= 7, + // same but for 18pt+ texts + laa: ratio >= 3, + laaa: ratio >= 4.5 + }) + const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {}) + + const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => { + const slotIsBaseText = key === 'text' || key === 'link' + const slotIsText = slotIsBaseText || ( + typeof value === 'object' && value !== null && value.textColor + ) + if (!slotIsText) return acc + const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value + const background = variant || layer + const opacitySlot = getOpacitySlot(background) + const textColors = [ + key, + ...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : []) + ] + + const layers = getLayers( + layer, + variant || layer, + opacitySlot, + colorsConverted, + opacity + ) + + return { + ...acc, + ...textColors.reduce((acc, textColorKey) => { + const newKey = slotIsBaseText + ? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1) + : textColorKey + return { + ...acc, + [newKey]: getContrastRatioLayers( + colorsConverted[textColorKey], + layers, + colorsConverted[textColorKey] + ) + } + }, {}) + } + }, {}) + + return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {}) + } catch (e) { + console.warn('Failure computing contrasts', e) + } + }, + previewRules () { + if (!this.preview.rules) return '' + return [ + ...Object.values(this.preview.rules), + 'color: var(--text)', + 'font-family: var(--interfaceFont, sans-serif)' + ].join(';') + }, + shadowsAvailable () { + return Object.keys(DEFAULT_SHADOWS).sort() + }, + currentShadowOverriden: { + get () { + return !!this.currentShadow + }, + set (val) { + if (val) { + set(this.shadowsLocal, this.shadowSelected, this.currentShadowFallback.map(_ => Object.assign({}, _))) + } else { + del(this.shadowsLocal, this.shadowSelected) + } + } + }, + currentShadowFallback () { + return (this.previewTheme.shadows || {})[this.shadowSelected] + }, + currentShadow: { + get () { + return this.shadowsLocal[this.shadowSelected] + }, + set (v) { + set(this.shadowsLocal, this.shadowSelected, v) + } + }, + themeValid () { + return !this.shadowsInvalid && !this.colorsInvalid && !this.radiiInvalid + }, + exportedTheme () { + const saveEverything = ( + !this.keepFonts && + !this.keepShadows && + !this.keepOpacity && + !this.keepRoundness && + !this.keepColor + ) + + const source = { + themeEngineVersion: CURRENT_VERSION + } + + if (this.keepFonts || saveEverything) { + source.fonts = this.fontsLocal + } + if (this.keepShadows || saveEverything) { + source.shadows = this.shadowsLocal + } + if (this.keepOpacity || saveEverything) { + source.opacity = this.currentOpacity + } + if (this.keepColor || saveEverything) { + source.colors = this.currentColors + } + if (this.keepRoundness || saveEverything) { + source.radii = this.currentRadii + } + + const theme = { + themeEngineVersion: CURRENT_VERSION, + ...this.previewTheme + } + + return { + // To separate from other random JSON files and possible future source formats + _pleroma_theme_version: 2, theme, source + } + } + }, + components: { + ColorInput, + OpacityInput, + RangeInput, + ContrastRatio, + ShadowControl, + FontControl, + TabSwitcher, + Preview, + ExportImport, + Checkbox + }, + methods: { + loadTheme ( + { + theme, + source, + _pleroma_theme_version: fileVersion + }, + origin, + forceUseSource = false + ) { + this.dismissWarning() + if (!source && !theme) { + throw new Error('Can\'t load theme: empty') + } + const version = (origin === 'localStorage' && !theme.colors) + ? 'l1' + : fileVersion + const snapshotEngineVersion = (theme || {}).themeEngineVersion + const themeEngineVersion = (source || {}).themeEngineVersion || 2 + const versionsMatch = themeEngineVersion === CURRENT_VERSION + const sourceSnapshotMismatch = ( + theme !== undefined && + source !== undefined && + themeEngineVersion !== snapshotEngineVersion + ) + // Force loading of source if user requested it or if snapshot + // is unavailable + const forcedSourceLoad = (source && forceUseSource) || !theme + if (!(versionsMatch && !sourceSnapshotMismatch) && + !forcedSourceLoad && + version !== 'l1' && + origin !== 'defaults' + ) { + if (sourceSnapshotMismatch && origin === 'localStorage') { + this.themeWarning = { + origin, + themeEngineVersion, + type: 'snapshot_source_mismatch' + } + } else if (!theme) { + this.themeWarning = { + origin, + noActionsPossible: true, + themeEngineVersion, + type: 'no_snapshot_old_version' + } + } else if (!versionsMatch) { + this.themeWarning = { + origin, + noActionsPossible: !source, + themeEngineVersion, + type: 'wrong_version' + } + } + } + this.normalizeLocalState(theme, version, source, forcedSourceLoad) + }, + forceLoadLocalStorage () { + this.loadThemeFromLocalStorage(true) + }, + dismissWarning () { + this.themeWarning = undefined + this.tempImportFile = undefined + }, + forceLoad () { + const { origin } = this.themeWarning + switch (origin) { + case 'localStorage': + this.loadThemeFromLocalStorage(true) + break + case 'file': + this.onImport(this.tempImportFile, true) + break + } + this.dismissWarning() + }, + forceSnapshot () { + const { origin } = this.themeWarning + switch (origin) { + case 'localStorage': + this.loadThemeFromLocalStorage(false, true) + break + case 'file': + console.err('Forcing snapshout from file is not supported yet') + break + } + this.dismissWarning() + }, + loadThemeFromLocalStorage (confirmLoadSource = false, forceSnapshot = false) { + const { + customTheme: theme, + customThemeSource: source + } = this.$store.getters.mergedConfig + if (!theme && !source) { + // Anon user or never touched themes + this.loadTheme( + this.$store.state.instance.themeData, + 'defaults', + confirmLoadSource + ) + } else { + this.loadTheme( + { + theme, + source: forceSnapshot ? theme : source + }, + 'localStorage', + confirmLoadSource + ) + } + }, + setCustomTheme () { + this.$store.dispatch('setOption', { + name: 'customTheme', + value: { + themeEngineVersion: CURRENT_VERSION, + ...this.previewTheme + } + }) + this.$store.dispatch('setOption', { + name: 'customThemeSource', + value: { + themeEngineVersion: CURRENT_VERSION, + shadows: this.shadowsLocal, + fonts: this.fontsLocal, + opacity: this.currentOpacity, + colors: this.currentColors, + radii: this.currentRadii + } + }) + }, + updatePreviewColorsAndShadows () { + this.previewColors = generateColors({ + opacity: this.currentOpacity, + colors: this.currentColors + }) + this.previewShadows = generateShadows( + { shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion }, + this.previewColors.theme.colors, + this.previewColors.mod + ) + }, + onImport (parsed, forceSource = false) { + this.tempImportFile = parsed + this.loadTheme(parsed, 'file', forceSource) + }, + importValidator (parsed) { + const version = parsed._pleroma_theme_version + return version >= 1 || version <= 2 + }, + clearAll () { + this.loadThemeFromLocalStorage() + }, + + // Clears all the extra stuff when loading V1 theme + clearV1 () { + Object.keys(this.$data) + .filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal')) + .filter(_ => !v1OnlyNames.includes(_)) + .forEach(key => { + set(this.$data, key, undefined) + }) + }, + + clearRoundness () { + Object.keys(this.$data) + .filter(_ => _.endsWith('RadiusLocal')) + .forEach(key => { + set(this.$data, key, undefined) + }) + }, + + clearOpacity () { + Object.keys(this.$data) + .filter(_ => _.endsWith('OpacityLocal')) + .forEach(key => { + set(this.$data, key, undefined) + }) + }, + + clearShadows () { + this.shadowsLocal = {} + }, + + clearFonts () { + this.fontsLocal = {} + }, + + /** + * This applies stored theme data onto form. Supports three versions of data: + * v3 (version >= 3) - newest version of themes which supports snapshots for better compatiblity + * v2 (version = 2) - newer version of themes. + * v1 (version = 1) - older version of themes (import from file) + * v1l (version = l1) - older version of theme (load from local storage) + * v1 and v1l differ because of way themes were stored/exported. + * @param {Object} theme - theme data (snapshot) + * @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type + * @param {Object} source - theme source - this will be used if compatible + * @param {Boolean} source - by default source won't be used if version doesn't match since it might render differently + * this allows importing source anyway + */ + normalizeLocalState (theme, version = 0, source, forceSource = false) { + let input + if (typeof source !== 'undefined') { + if (forceSource || source.themeEngineVersion === CURRENT_VERSION) { + input = source + version = source.themeEngineVersion + } else { + input = theme + } + } else { + input = theme + } + + const radii = input.radii || input + const opacity = input.opacity + const shadows = input.shadows || {} + const fonts = input.fonts || {} + const colors = !input.themeEngineVersion + ? colors2to3(input.colors || input) + : input.colors || input + + if (version === 0) { + if (input.version) version = input.version + // Old v1 naming: fg is text, btn is foreground + if (typeof colors.text === 'undefined' && typeof colors.fg !== 'undefined') { + version = 1 + } + // New v2 naming: text is text, fg is foreground + if (typeof colors.text !== 'undefined' && typeof colors.fg !== 'undefined') { + version = 2 + } + } + + this.engineVersion = version + + // Stuff that differs between V1 and V2 + if (version === 1) { + this.fgColorLocal = rgb2hex(colors.btn) + this.textColorLocal = rgb2hex(colors.fg) + } + + if (!this.keepColor) { + this.clearV1() + const keys = new Set(version !== 1 ? Object.keys(SLOT_INHERITANCE) : []) + if (version === 1 || version === 'l1') { + keys + .add('bg') + .add('link') + .add('cRed') + .add('cBlue') + .add('cGreen') + .add('cOrange') + } + + keys.forEach(key => { + const color = colors[key] + const hex = rgb2hex(colors[key]) + this[key + 'ColorLocal'] = hex === '#aN' ? color : hex + }) + } + + if (opacity && !this.keepOpacity) { + this.clearOpacity() + Object.entries(opacity).forEach(([k, v]) => { + if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return + this[k + 'OpacityLocal'] = v + }) + } + + if (!this.keepRoundness) { + this.clearRoundness() + Object.entries(radii).forEach(([k, v]) => { + // 'Radius' is kept mostly for v1->v2 localstorage transition + const key = k.endsWith('Radius') ? k.split('Radius')[0] : k + this[key + 'RadiusLocal'] = v + }) + } + + if (!this.keepShadows) { + this.clearShadows() + if (version === 2) { + this.shadowsLocal = shadows2to3(shadows, this.previewTheme.opacity) + } else { + this.shadowsLocal = shadows + } + this.shadowSelected = this.shadowsAvailable[0] + } + + if (!this.keepFonts) { + this.clearFonts() + this.fontsLocal = fonts + } + } + }, + watch: { + currentRadii () { + try { + this.previewRadii = generateRadii({ radii: this.currentRadii }) + this.radiiInvalid = false + } catch (e) { + this.radiiInvalid = true + console.warn(e) + } + }, + shadowsLocal: { + handler () { + if (Object.getOwnPropertyNames(this.previewColors).length === 1) return + try { + this.updatePreviewColorsAndShadows() + this.shadowsInvalid = false + } catch (e) { + this.shadowsInvalid = true + console.warn(e) + } + }, + deep: true + }, + fontsLocal: { + handler () { + try { + this.previewFonts = generateFonts({ fonts: this.fontsLocal }) + this.fontsInvalid = false + } catch (e) { + this.fontsInvalid = true + console.warn(e) + } + }, + deep: true + }, + currentColors () { + try { + this.updatePreviewColorsAndShadows() + this.colorsInvalid = false + this.shadowsInvalid = false + } catch (e) { + this.colorsInvalid = true + this.shadowsInvalid = true + console.warn(e) + } + }, + currentOpacity () { + try { + this.updatePreviewColorsAndShadows() + } catch (e) { + console.warn(e) + } + }, + selected () { + this.dismissWarning() + if (this.selectedVersion === 1) { + if (!this.keepRoundness) { + this.clearRoundness() + } + + if (!this.keepShadows) { + this.clearShadows() + } + + if (!this.keepOpacity) { + this.clearOpacity() + } + + if (!this.keepColor) { + this.clearV1() + + this.bgColorLocal = this.selected[1] + this.fgColorLocal = this.selected[2] + this.textColorLocal = this.selected[3] + this.linkColorLocal = this.selected[4] + this.cRedColorLocal = this.selected[5] + this.cGreenColorLocal = this.selected[6] + this.cBlueColorLocal = this.selected[7] + this.cOrangeColorLocal = this.selected[8] + } + } else if (this.selectedVersion >= 2) { + this.normalizeLocalState(this.selected.theme, 2, this.selected.source) + } + } + } +} diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss @@ -0,0 +1,345 @@ +@import 'src/_variables.scss'; +.theme-tab { + padding-bottom: 2em; + .theme-warning { + display: flex; + align-items: baseline; + margin-bottom: .5em; + .buttons { + .btn { + margin-bottom: .5em; + } + } + } + .preset-switcher { + margin-right: 1em; + } + + .style-control { + display: flex; + align-items: baseline; + margin-bottom: 5px; + + .label { + flex: 1; + } + + &.disabled { + input, select { + opacity: .5 + } + } + + .opt { + margin: .5em; + } + + .color-input { + flex: 0 0 0; + } + + input, select { + min-width: 3em; + margin: 0; + flex: 0; + + &[type=number] { + min-width: 5em; + } + + &[type=range] { + flex: 1; + min-width: 3em; + align-self: flex-start; + } + } + } + + .reset-container { + flex-wrap: wrap; + } + + .fonts-container, + .reset-container, + .apply-container, + .radius-container, + .color-container, + { + display: flex; + } + + .fonts-container, + .radius-container { + flex-direction: column; + } + + .color-container{ + > h4 { + width: 99%; + } + flex-wrap: wrap; + justify-content: space-between; + } + + .fonts-container, + .color-container, + .shadow-container, + .radius-container, + .presets-container { + margin: 1em 1em 0; + } + + .tab-header { + display: flex; + justify-content: space-between; + align-items: baseline; + width: 100%; + min-height: 30px; + margin-bottom: 1em; + + p { + flex: 1; + margin: 0; + margin-right: .5em; + } + } + + .tab-header-buttons { + display: flex; + flex-direction: column; + + .btn { + min-width: 1px; + flex: 0 auto; + padding: 0 1em; + margin-bottom: .5em; + } + } + + .shadow-selector { + .override { + flex: 1; + margin-left: .5em; + } + .select-container { + margin-top: -4px; + margin-bottom: -3px; + } + } + + .save-load, + .save-load-options { + display: flex; + justify-content: center; + align-items: baseline; + flex-wrap: wrap; + + .presets, + .import-export { + margin-bottom: .5em; + } + + .import-export { + display: flex; + } + + .override { + margin-left: .5em; + } + } + + .save-load-options { + flex-wrap: wrap; + margin-top: .5em; + justify-content: center; + .keep-option { + margin: 0 .5em .5em; + min-width: 25%; + } + } + + .preview-container { + border-top: 1px dashed; + border-bottom: 1px dashed; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + margin: 1em 0; + padding: 1em; + background: var(--body-background-image); + background-size: cover; + background-position: 50% 50%; + + .dummy { + .post { + font-family: var(--postFont); + display: flex; + + .content { + flex: 1; + + h4 { + margin-bottom: .25em; + } + + .icons { + margin-top: .5em; + display: flex; + + i { + margin-right: 1em; + } + } + } + } + + .after-post { + margin-top: 1em; + display: flex; + align-items: center; + } + + .avatar, .avatar-alt{ + background: linear-gradient(135deg, #b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%); + color: black; + font-family: sans-serif; + text-align: center; + margin-right: 1em; + } + + .avatar-alt { + flex: 0 auto; + margin-left: 28px; + font-size: 12px; + min-width: 20px; + min-height: 20px; + line-height: 20px; + border-radius: $fallback--avatarAltRadius; + border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + } + + .avatar { + flex: 0 auto; + width: 48px; + height: 48px; + font-size: 14px; + line-height: 48px; + } + + .actions { + display: flex; + align-items: baseline; + + .checkbox { + display: inline-flex; + align-items: baseline; + margin-right: 1em; + flex: 1; + } + } + + .separator { + margin: 1em; + border-bottom: 1px solid; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + } + + .panel-heading { + .badge, .alert, .btn, .faint { + margin-left: 1em; + white-space: nowrap; + } + .faint { + text-overflow: ellipsis; + min-width: 2em; + overflow-x: hidden; + } + .flex-spacer { + flex: 1; + } + } + .btn { + margin-left: 0; + padding: 0 1em; + min-width: 3em; + min-height: 30px; + } + } + } + + .apply-container { + justify-content: center; + } + + .radius-item, + .color-item { + min-width: 20em; + margin: 5px 6px 0 0; + display:flex; + flex-direction: column; + flex: 1 1 0; + + &.wide { + min-width: 60% + } + + &:not(.wide):nth-child(2n+1) { + margin-right: 7px; + + } + + .color, .opacity { + display:flex; + align-items: baseline; + } + } + + .radius-item { + flex-basis: auto; + } + + .theme-radius-rn, + .theme-color-cl { + border: 0; + box-shadow: none; + background: transparent; + color: var(--faint, $fallback--faint); + align-self: stretch; + } + + .theme-color-cl, + .theme-radius-in, + .theme-color-in { + margin-left: 4px; + } + + .theme-radius-in { + min-width: 1em; + } + + .theme-radius-in { + max-width: 7em; + flex: 1; + } + + .theme-radius-lb{ + max-width: 50em; + } + + .theme-preview-content { + padding: 20px; + } + + .apply-container { + .btn { + min-height: 28px; + min-width: 10em; + padding: 0 2em; + } + } + + .btn { + margin-left: .25em; + margin-right: .25em; + } +} diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue @@ -0,0 +1,1032 @@ +<template> + <div class="theme-tab"> + <div class="presets-container"> + <div class="save-load"> + <div + v-if="themeWarning" + class="theme-warning" + > + <div class="alert warning"> + {{ themeWarningHelp }} + </div> + <div class="buttons"> + <template v-if="themeWarning.type === 'snapshot_source_mismatch'"> + <button + class="btn" + @click="forceLoad" + > + {{ $t('settings.style.switcher.use_source') }} + </button> + <button + class="btn" + @click="forceSnapshot" + > + {{ $t('settings.style.switcher.use_snapshot') }} + </button> + </template> + <template v-else-if="themeWarning.noActionsPossible"> + <button + class="btn" + @click="dismissWarning" + > + {{ $t('general.dismiss') }} + </button> + </template> + <template v-else> + <button + class="btn" + @click="forceLoad" + > + {{ $t('settings.style.switcher.load_theme') }} + </button> + <button + class="btn" + @click="dismissWarning" + > + {{ $t('settings.style.switcher.keep_as_is') }} + </button> + </template> + </div> + </div> + <ExportImport + :export-object="exportedTheme" + :export-label="$t(&quot;settings.export_theme&quot;)" + :import-label="$t(&quot;settings.import_theme&quot;)" + :import-failed-text="$t(&quot;settings.invalid_theme_imported&quot;)" + :on-import="onImport" + :validator="importValidator" + > + <template slot="before"> + <div class="presets"> + {{ $t('settings.presets') }} + <label + for="preset-switcher" + class="select" + > + <select + id="preset-switcher" + v-model="selected" + class="preset-switcher" + > + <option + v-for="style in availableStyles" + :key="style.name" + :value="style" + :style="{ + backgroundColor: style[1] || (style.theme || style.source).colors.bg, + color: style[3] || (style.theme || style.source).colors.text + }" + > + {{ style[0] || style.name }} + </option> + </select> + <i class="icon-down-open" /> + </label> + </div> + </template> + </ExportImport> + </div> + <div class="save-load-options"> + <span class="keep-option"> + <Checkbox v-model="keepColor"> + {{ $t('settings.style.switcher.keep_color') }} + </Checkbox> + </span> + <span class="keep-option"> + <Checkbox v-model="keepShadows"> + {{ $t('settings.style.switcher.keep_shadows') }} + </Checkbox> + </span> + <span class="keep-option"> + <Checkbox v-model="keepOpacity"> + {{ $t('settings.style.switcher.keep_opacity') }} + </Checkbox> + </span> + <span class="keep-option"> + <Checkbox v-model="keepRoundness"> + {{ $t('settings.style.switcher.keep_roundness') }} + </Checkbox> + </span> + <span class="keep-option"> + <Checkbox v-model="keepFonts"> + {{ $t('settings.style.switcher.keep_fonts') }} + </Checkbox> + </span> + <p>{{ $t('settings.style.switcher.save_load_hint') }}</p> + </div> + </div> + + <preview :style="previewRules" /> + + <keep-alive> + <tab-switcher key="style-tweak"> + <div + :label="$t('settings.style.common_colors._tab_label')" + class="color-container" + > + <div class="tab-header"> + <p>{{ $t('settings.theme_help') }}</p> + <div class="tab-header-buttons"> + <button + class="btn" + @click="clearOpacity" + > + {{ $t('settings.style.switcher.clear_opacity') }} + </button> + <button + class="btn" + @click="clearV1" + > + {{ $t('settings.style.switcher.clear_all') }} + </button> + </div> + </div> + <p>{{ $t('settings.theme_help_v2_1') }}</p> + <h4>{{ $t('settings.style.common_colors.main') }}</h4> + <div class="color-item"> + <ColorInput + v-model="bgColorLocal" + name="bgColor" + :label="$t('settings.background')" + /> + <OpacityInput + v-model="bgOpacityLocal" + name="bgOpacity" + :fallback="previewTheme.opacity.bg" + /> + <ColorInput + v-model="textColorLocal" + name="textColor" + :label="$t('settings.text')" + /> + <ContrastRatio :contrast="previewContrast.bgText" /> + <ColorInput + v-model="accentColorLocal" + name="accentColor" + :fallback="previewTheme.colors.link" + :label="$t('settings.accent')" + :show-optional-tickbox="typeof linkColorLocal !== 'undefined'" + /> + <ColorInput + v-model="linkColorLocal" + name="linkColor" + :fallback="previewTheme.colors.accent" + :label="$t('settings.links')" + :show-optional-tickbox="typeof accentColorLocal !== 'undefined'" + /> + <ContrastRatio :contrast="previewContrast.bgLink" /> + </div> + <div class="color-item"> + <ColorInput + v-model="fgColorLocal" + name="fgColor" + :label="$t('settings.foreground')" + /> + <ColorInput + v-model="fgTextColorLocal" + name="fgTextColor" + :label="$t('settings.text')" + :fallback="previewTheme.colors.fgText" + /> + <ColorInput + v-model="fgLinkColorLocal" + name="fgLinkColor" + :label="$t('settings.links')" + :fallback="previewTheme.colors.fgLink" + /> + <p>{{ $t('settings.style.common_colors.foreground_hint') }}</p> + </div> + <h4>{{ $t('settings.style.common_colors.rgbo') }}</h4> + <div class="color-item"> + <ColorInput + v-model="cRedColorLocal" + name="cRedColor" + :label="$t('settings.cRed')" + /> + <ContrastRatio :contrast="previewContrast.bgCRed" /> + <ColorInput + v-model="cBlueColorLocal" + name="cBlueColor" + :label="$t('settings.cBlue')" + /> + <ContrastRatio :contrast="previewContrast.bgCBlue" /> + </div> + <div class="color-item"> + <ColorInput + v-model="cGreenColorLocal" + name="cGreenColor" + :label="$t('settings.cGreen')" + /> + <ContrastRatio :contrast="previewContrast.bgCGreen" /> + <ColorInput + v-model="cOrangeColorLocal" + name="cOrangeColor" + :label="$t('settings.cOrange')" + /> + <ContrastRatio :contrast="previewContrast.bgCOrange" /> + </div> + <p>{{ $t('settings.theme_help_v2_2') }}</p> + </div> + + <div + :label="$t('settings.style.advanced_colors._tab_label')" + class="color-container" + > + <div class="tab-header"> + <p>{{ $t('settings.theme_help') }}</p> + <button + class="btn" + @click="clearOpacity" + > + {{ $t('settings.style.switcher.clear_opacity') }} + </button> + <button + class="btn" + @click="clearV1" + > + {{ $t('settings.style.switcher.clear_all') }} + </button> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.post') }}</h4> + <ColorInput + v-model="postLinkColorLocal" + name="postLinkColor" + :fallback="previewTheme.colors.accent" + :label="$t('settings.links')" + /> + <ContrastRatio :contrast="previewContrast.postLink" /> + <ColorInput + v-model="postGreentextColorLocal" + name="postGreentextColor" + :fallback="previewTheme.colors.cGreen" + :label="$t('settings.greentext')" + /> + <ContrastRatio :contrast="previewContrast.postGreentext" /> + <h4>{{ $t('settings.style.advanced_colors.alert') }}</h4> + <ColorInput + v-model="alertErrorColorLocal" + name="alertError" + :label="$t('settings.style.advanced_colors.alert_error')" + :fallback="previewTheme.colors.alertError" + /> + <ColorInput + v-model="alertErrorTextColorLocal" + name="alertErrorText" + :label="$t('settings.text')" + :fallback="previewTheme.colors.alertErrorText" + /> + <ContrastRatio + :contrast="previewContrast.alertErrorText" + large="true" + /> + <ColorInput + v-model="alertWarningColorLocal" + name="alertWarning" + :label="$t('settings.style.advanced_colors.alert_warning')" + :fallback="previewTheme.colors.alertWarning" + /> + <ColorInput + v-model="alertWarningTextColorLocal" + name="alertWarningText" + :label="$t('settings.text')" + :fallback="previewTheme.colors.alertWarningText" + /> + <ContrastRatio + :contrast="previewContrast.alertWarningText" + large="true" + /> + <ColorInput + v-model="alertNeutralColorLocal" + name="alertNeutral" + :label="$t('settings.style.advanced_colors.alert_neutral')" + :fallback="previewTheme.colors.alertNeutral" + /> + <ColorInput + v-model="alertNeutralTextColorLocal" + name="alertNeutralText" + :label="$t('settings.text')" + :fallback="previewTheme.colors.alertNeutralText" + /> + <ContrastRatio + :contrast="previewContrast.alertNeutralText" + large="true" + /> + <OpacityInput + v-model="alertOpacityLocal" + name="alertOpacity" + :fallback="previewTheme.opacity.alert" + /> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.badge') }}</h4> + <ColorInput + v-model="badgeNotificationColorLocal" + name="badgeNotification" + :label="$t('settings.style.advanced_colors.badge_notification')" + :fallback="previewTheme.colors.badgeNotification" + /> + <ColorInput + v-model="badgeNotificationTextColorLocal" + name="badgeNotificationText" + :label="$t('settings.text')" + :fallback="previewTheme.colors.badgeNotificationText" + /> + <ContrastRatio + :contrast="previewContrast.badgeNotificationText" + large="true" + /> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4> + <ColorInput + v-model="panelColorLocal" + name="panelColor" + :fallback="previewTheme.colors.panel" + :label="$t('settings.background')" + /> + <OpacityInput + v-model="panelOpacityLocal" + name="panelOpacity" + :fallback="previewTheme.opacity.panel" + :disabled="panelColorLocal === 'transparent'" + /> + <ColorInput + v-model="panelTextColorLocal" + name="panelTextColor" + :fallback="previewTheme.colors.panelText" + :label="$t('settings.text')" + /> + <ContrastRatio + :contrast="previewContrast.panelText" + large="true" + /> + <ColorInput + v-model="panelLinkColorLocal" + name="panelLinkColor" + :fallback="previewTheme.colors.panelLink" + :label="$t('settings.links')" + /> + <ContrastRatio + :contrast="previewContrast.panelLink" + large="true" + /> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.top_bar') }}</h4> + <ColorInput + v-model="topBarColorLocal" + name="topBarColor" + :fallback="previewTheme.colors.topBar" + :label="$t('settings.background')" + /> + <ColorInput + v-model="topBarTextColorLocal" + name="topBarTextColor" + :fallback="previewTheme.colors.topBarText" + :label="$t('settings.text')" + /> + <ContrastRatio :contrast="previewContrast.topBarText" /> + <ColorInput + v-model="topBarLinkColorLocal" + name="topBarLinkColor" + :fallback="previewTheme.colors.topBarLink" + :label="$t('settings.links')" + /> + <ContrastRatio :contrast="previewContrast.topBarLink" /> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.inputs') }}</h4> + <ColorInput + v-model="inputColorLocal" + name="inputColor" + :fallback="previewTheme.colors.input" + :label="$t('settings.background')" + /> + <OpacityInput + v-model="inputOpacityLocal" + name="inputOpacity" + :fallback="previewTheme.opacity.input" + :disabled="inputColorLocal === 'transparent'" + /> + <ColorInput + v-model="inputTextColorLocal" + name="inputTextColor" + :fallback="previewTheme.colors.inputText" + :label="$t('settings.text')" + /> + <ContrastRatio :contrast="previewContrast.inputText" /> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.buttons') }}</h4> + <ColorInput + v-model="btnColorLocal" + name="btnColor" + :fallback="previewTheme.colors.btn" + :label="$t('settings.background')" + /> + <OpacityInput + v-model="btnOpacityLocal" + name="btnOpacity" + :fallback="previewTheme.opacity.btn" + :disabled="btnColorLocal === 'transparent'" + /> + <ColorInput + v-model="btnTextColorLocal" + name="btnTextColor" + :fallback="previewTheme.colors.btnText" + :label="$t('settings.text')" + /> + <ContrastRatio :contrast="previewContrast.btnText" /> + <ColorInput + v-model="btnPanelTextColorLocal" + name="btnPanelTextColor" + :fallback="previewTheme.colors.btnPanelText" + :label="$t('settings.style.advanced_colors.panel_header')" + /> + <ContrastRatio :contrast="previewContrast.btnPanelText" /> + <ColorInput + v-model="btnTopBarTextColorLocal" + name="btnTopBarTextColor" + :fallback="previewTheme.colors.btnTopBarText" + :label="$t('settings.style.advanced_colors.top_bar')" + /> + <ContrastRatio :contrast="previewContrast.btnTopBarText" /> + <h5>{{ $t('settings.style.advanced_colors.pressed') }}</h5> + <ColorInput + v-model="btnPressedColorLocal" + name="btnPressedColor" + :fallback="previewTheme.colors.btnPressed" + :label="$t('settings.background')" + /> + <ColorInput + v-model="btnPressedTextColorLocal" + name="btnPressedTextColor" + :fallback="previewTheme.colors.btnPressedText" + :label="$t('settings.text')" + /> + <ContrastRatio :contrast="previewContrast.btnPressedText" /> + <ColorInput + v-model="btnPressedPanelTextColorLocal" + name="btnPressedPanelTextColor" + :fallback="previewTheme.colors.btnPressedPanelText" + :label="$t('settings.style.advanced_colors.panel_header')" + /> + <ContrastRatio :contrast="previewContrast.btnPressedPanelText" /> + <ColorInput + v-model="btnPressedTopBarTextColorLocal" + name="btnPressedTopBarTextColor" + :fallback="previewTheme.colors.btnPressedTopBarText" + :label="$t('settings.style.advanced_colors.top_bar')" + /> + <ContrastRatio :contrast="previewContrast.btnPressedTopBarText" /> + <h5>{{ $t('settings.style.advanced_colors.disabled') }}</h5> + <ColorInput + v-model="btnDisabledColorLocal" + name="btnDisabledColor" + :fallback="previewTheme.colors.btnDisabled" + :label="$t('settings.background')" + /> + <ColorInput + v-model="btnDisabledTextColorLocal" + name="btnDisabledTextColor" + :fallback="previewTheme.colors.btnDisabledText" + :label="$t('settings.text')" + /> + <ColorInput + v-model="btnDisabledPanelTextColorLocal" + name="btnDisabledPanelTextColor" + :fallback="previewTheme.colors.btnDisabledPanelText" + :label="$t('settings.style.advanced_colors.panel_header')" + /> + <ColorInput + v-model="btnDisabledTopBarTextColorLocal" + name="btnDisabledTopBarTextColor" + :fallback="previewTheme.colors.btnDisabledTopBarText" + :label="$t('settings.style.advanced_colors.top_bar')" + /> + <h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5> + <ColorInput + v-model="btnToggledColorLocal" + name="btnToggledColor" + :fallback="previewTheme.colors.btnToggled" + :label="$t('settings.background')" + /> + <ColorInput + v-model="btnToggledTextColorLocal" + name="btnToggledTextColor" + :fallback="previewTheme.colors.btnToggledText" + :label="$t('settings.text')" + /> + <ContrastRatio :contrast="previewContrast.btnToggledText" /> + <ColorInput + v-model="btnToggledPanelTextColorLocal" + name="btnToggledPanelTextColor" + :fallback="previewTheme.colors.btnToggledPanelText" + :label="$t('settings.style.advanced_colors.panel_header')" + /> + <ContrastRatio :contrast="previewContrast.btnToggledPanelText" /> + <ColorInput + v-model="btnToggledTopBarTextColorLocal" + name="btnToggledTopBarTextColor" + :fallback="previewTheme.colors.btnToggledTopBarText" + :label="$t('settings.style.advanced_colors.top_bar')" + /> + <ContrastRatio :contrast="previewContrast.btnToggledTopBarText" /> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.tabs') }}</h4> + <ColorInput + v-model="tabColorLocal" + name="tabColor" + :fallback="previewTheme.colors.tab" + :label="$t('settings.background')" + /> + <ColorInput + v-model="tabTextColorLocal" + name="tabTextColor" + :fallback="previewTheme.colors.tabText" + :label="$t('settings.text')" + /> + <ContrastRatio :contrast="previewContrast.tabText" /> + <ColorInput + v-model="tabActiveTextColorLocal" + name="tabActiveTextColor" + :fallback="previewTheme.colors.tabActiveText" + :label="$t('settings.text')" + /> + <ContrastRatio :contrast="previewContrast.tabActiveText" /> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.borders') }}</h4> + <ColorInput + v-model="borderColorLocal" + name="borderColor" + :fallback="previewTheme.colors.border" + :label="$t('settings.style.common.color')" + /> + <OpacityInput + v-model="borderOpacityLocal" + name="borderOpacity" + :fallback="previewTheme.opacity.border" + :disabled="borderColorLocal === 'transparent'" + /> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.faint_text') }}</h4> + <ColorInput + v-model="faintColorLocal" + name="faintColor" + :fallback="previewTheme.colors.faint" + :label="$t('settings.text')" + /> + <ColorInput + v-model="faintLinkColorLocal" + name="faintLinkColor" + :fallback="previewTheme.colors.faintLink" + :label="$t('settings.links')" + /> + <ColorInput + v-model="panelFaintColorLocal" + name="panelFaintColor" + :fallback="previewTheme.colors.panelFaint" + :label="$t('settings.style.advanced_colors.panel_header')" + /> + <OpacityInput + v-model="faintOpacityLocal" + name="faintOpacity" + :fallback="previewTheme.opacity.faint" + /> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.underlay') }}</h4> + <ColorInput + v-model="underlayColorLocal" + name="underlay" + :label="$t('settings.style.advanced_colors.underlay')" + :fallback="previewTheme.colors.underlay" + /> + <OpacityInput + v-model="underlayOpacityLocal" + name="underlayOpacity" + :fallback="previewTheme.opacity.underlay" + :disabled="underlayOpacityLocal === 'transparent'" + /> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.poll') }}</h4> + <ColorInput + v-model="pollColorLocal" + name="poll" + :label="$t('settings.background')" + :fallback="previewTheme.colors.poll" + /> + <ColorInput + v-model="pollTextColorLocal" + name="pollText" + :label="$t('settings.text')" + :fallback="previewTheme.colors.pollText" + /> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.icons') }}</h4> + <ColorInput + v-model="iconColorLocal" + name="icon" + :label="$t('settings.style.advanced_colors.icons')" + :fallback="previewTheme.colors.icon" + /> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.highlight') }}</h4> + <ColorInput + v-model="highlightColorLocal" + name="highlight" + :label="$t('settings.background')" + :fallback="previewTheme.colors.highlight" + /> + <ColorInput + v-model="highlightTextColorLocal" + name="highlightText" + :label="$t('settings.text')" + :fallback="previewTheme.colors.highlightText" + /> + <ContrastRatio :contrast="previewContrast.highlightText" /> + <ColorInput + v-model="highlightLinkColorLocal" + name="highlightLink" + :label="$t('settings.links')" + :fallback="previewTheme.colors.highlightLink" + /> + <ContrastRatio :contrast="previewContrast.highlightLink" /> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.popover') }}</h4> + <ColorInput + v-model="popoverColorLocal" + name="popover" + :label="$t('settings.background')" + :fallback="previewTheme.colors.popover" + /> + <OpacityInput + v-model="popoverOpacityLocal" + name="popoverOpacity" + :fallback="previewTheme.opacity.popover" + :disabled="popoverOpacityLocal === 'transparent'" + /> + <ColorInput + v-model="popoverTextColorLocal" + name="popoverText" + :label="$t('settings.text')" + :fallback="previewTheme.colors.popoverText" + /> + <ContrastRatio :contrast="previewContrast.popoverText" /> + <ColorInput + v-model="popoverLinkColorLocal" + name="popoverLink" + :label="$t('settings.links')" + :fallback="previewTheme.colors.popoverLink" + /> + <ContrastRatio :contrast="previewContrast.popoverLink" /> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.selectedPost') }}</h4> + <ColorInput + v-model="selectedPostColorLocal" + name="selectedPost" + :label="$t('settings.background')" + :fallback="previewTheme.colors.selectedPost" + /> + <ColorInput + v-model="selectedPostTextColorLocal" + name="selectedPostText" + :label="$t('settings.text')" + :fallback="previewTheme.colors.selectedPostText" + /> + <ContrastRatio :contrast="previewContrast.selectedPostText" /> + <ColorInput + v-model="selectedPostLinkColorLocal" + name="selectedPostLink" + :label="$t('settings.links')" + :fallback="previewTheme.colors.selectedPostLink" + /> + <ContrastRatio :contrast="previewContrast.selectedPostLink" /> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.selectedMenu') }}</h4> + <ColorInput + v-model="selectedMenuColorLocal" + name="selectedMenu" + :label="$t('settings.background')" + :fallback="previewTheme.colors.selectedMenu" + /> + <ColorInput + v-model="selectedMenuTextColorLocal" + name="selectedMenuText" + :label="$t('settings.text')" + :fallback="previewTheme.colors.selectedMenuText" + /> + <ContrastRatio :contrast="previewContrast.selectedMenuText" /> + <ColorInput + v-model="selectedMenuLinkColorLocal" + name="selectedMenuLink" + :label="$t('settings.links')" + :fallback="previewTheme.colors.selectedMenuLink" + /> + <ContrastRatio :contrast="previewContrast.selectedMenuLink" /> + </div> + <div class="color-item"> + <h4>{{ $t('chats.chats') }}</h4> + <ColorInput + v-model="chatBgColorLocal" + name="chatBgColor" + :fallback="previewTheme.colors.bg || 1" + :label="$t('settings.background')" + /> + <h5>{{ $t('settings.style.advanced_colors.chat.incoming') }}</h5> + <ColorInput + v-model="chatMessageIncomingBgColorLocal" + name="chatMessageIncomingBgColor" + :fallback="previewTheme.colors.bg || 1" + :label="$t('settings.background')" + /> + <ColorInput + v-model="chatMessageIncomingTextColorLocal" + name="chatMessageIncomingTextColor" + :fallback="previewTheme.colors.text || 1" + :label="$t('settings.text')" + /> + <ColorInput + v-model="chatMessageIncomingLinkColorLocal" + name="chatMessageIncomingLinkColor" + :fallback="previewTheme.colors.link || 1" + :label="$t('settings.links')" + /> + <ColorInput + v-model="chatMessageIncomingBorderColorLocal" + name="chatMessageIncomingBorderLinkColor" + :fallback="previewTheme.colors.fg || 1" + :label="$t('settings.style.advanced_colors.chat.border')" + /> + <h5>{{ $t('settings.style.advanced_colors.chat.outgoing') }}</h5> + <ColorInput + v-model="chatMessageOutgoingBgColorLocal" + name="chatMessageOutgoingBgColor" + :fallback="previewTheme.colors.bg || 1" + :label="$t('settings.background')" + /> + <ColorInput + v-model="chatMessageOutgoingTextColorLocal" + name="chatMessageOutgoingTextColor" + :fallback="previewTheme.colors.text || 1" + :label="$t('settings.text')" + /> + <ColorInput + v-model="chatMessageOutgoingLinkColorLocal" + name="chatMessageOutgoingLinkColor" + :fallback="previewTheme.colors.link || 1" + :label="$t('settings.links')" + /> + <ColorInput + v-model="chatMessageOutgoingBorderColorLocal" + name="chatMessageOutgoingBorderLinkColor" + :fallback="previewTheme.colors.bg || 1" + :label="$t('settings.style.advanced_colors.chat.border')" + /> + </div> + </div> + + <div + :label="$t('settings.style.radii._tab_label')" + class="radius-container" + > + <div class="tab-header"> + <p>{{ $t('settings.radii_help') }}</p> + <button + class="btn" + @click="clearRoundness" + > + {{ $t('settings.style.switcher.clear_all') }} + </button> + </div> + <RangeInput + v-model="btnRadiusLocal" + name="btnRadius" + :label="$t('settings.btnRadius')" + :fallback="previewTheme.radii.btn" + max="16" + hard-min="0" + /> + <RangeInput + v-model="inputRadiusLocal" + name="inputRadius" + :label="$t('settings.inputRadius')" + :fallback="previewTheme.radii.input" + max="9" + hard-min="0" + /> + <RangeInput + v-model="checkboxRadiusLocal" + name="checkboxRadius" + :label="$t('settings.checkboxRadius')" + :fallback="previewTheme.radii.checkbox" + max="16" + hard-min="0" + /> + <RangeInput + v-model="panelRadiusLocal" + name="panelRadius" + :label="$t('settings.panelRadius')" + :fallback="previewTheme.radii.panel" + max="50" + hard-min="0" + /> + <RangeInput + v-model="avatarRadiusLocal" + name="avatarRadius" + :label="$t('settings.avatarRadius')" + :fallback="previewTheme.radii.avatar" + max="28" + hard-min="0" + /> + <RangeInput + v-model="avatarAltRadiusLocal" + name="avatarAltRadius" + :label="$t('settings.avatarAltRadius')" + :fallback="previewTheme.radii.avatarAlt" + max="28" + hard-min="0" + /> + <RangeInput + v-model="attachmentRadiusLocal" + name="attachmentRadius" + :label="$t('settings.attachmentRadius')" + :fallback="previewTheme.radii.attachment" + max="50" + hard-min="0" + /> + <RangeInput + v-model="tooltipRadiusLocal" + name="tooltipRadius" + :label="$t('settings.tooltipRadius')" + :fallback="previewTheme.radii.tooltip" + max="50" + hard-min="0" + /> + <RangeInput + v-model="chatMessageRadiusLocal" + name="chatMessageRadius" + :label="$t('settings.chatMessageRadius')" + :fallback="previewTheme.radii.chatMessage || 2" + max="50" + hard-min="0" + /> + </div> + + <div + :label="$t('settings.style.shadows._tab_label')" + class="shadow-container" + > + <div class="tab-header shadow-selector"> + <div class="select-container"> + {{ $t('settings.style.shadows.component') }} + <label + for="shadow-switcher" + class="select" + > + <select + id="shadow-switcher" + v-model="shadowSelected" + class="shadow-switcher" + > + <option + v-for="shadow in shadowsAvailable" + :key="shadow" + :value="shadow" + > + {{ $t('settings.style.shadows.components.' + shadow) }} + </option> + </select> + <i class="icon-down-open" /> + </label> + </div> + <div class="override"> + <label + for="override" + class="label" + > + {{ $t('settings.style.shadows.override') }} + </label> + <input + id="override" + v-model="currentShadowOverriden" + name="override" + class="input-override" + type="checkbox" + > + <label + class="checkbox-label" + for="override" + /> + </div> + <button + class="btn" + @click="clearShadows" + > + {{ $t('settings.style.switcher.clear_all') }} + </button> + </div> + <ShadowControl + v-model="currentShadow" + :ready="!!currentShadowFallback" + :fallback="currentShadowFallback" + /> + <div v-if="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'"> + <i18n + path="settings.style.shadows.filter_hint.always_drop_shadow" + tag="p" + > + <code>filter: drop-shadow()</code> + </i18n> + <p>{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}</p> + <i18n + path="settings.style.shadows.filter_hint.drop_shadow_syntax" + tag="p" + > + <code>drop-shadow</code> + <code>spread-radius</code> + <code>inset</code> + </i18n> + <i18n + path="settings.style.shadows.filter_hint.inset_classic" + tag="p" + > + <code>box-shadow</code> + </i18n> + <p>{{ $t('settings.style.shadows.filter_hint.spread_zero') }}</p> + </div> + </div> + + <div + :label="$t('settings.style.fonts._tab_label')" + class="fonts-container" + > + <div class="tab-header"> + <p>{{ $t('settings.style.fonts.help') }}</p> + <button + class="btn" + @click="clearFonts" + > + {{ $t('settings.style.switcher.clear_all') }} + </button> + </div> + <FontControl + v-model="fontsLocal.interface" + name="ui" + :label="$t('settings.style.fonts.components.interface')" + :fallback="previewTheme.fonts.interface" + no-inherit="1" + /> + <FontControl + v-model="fontsLocal.input" + name="input" + :label="$t('settings.style.fonts.components.input')" + :fallback="previewTheme.fonts.input" + /> + <FontControl + v-model="fontsLocal.post" + name="post" + :label="$t('settings.style.fonts.components.post')" + :fallback="previewTheme.fonts.post" + /> + <FontControl + v-model="fontsLocal.postCode" + name="postCode" + :label="$t('settings.style.fonts.components.postCode')" + :fallback="previewTheme.fonts.postCode" + /> + </div> + </tab-switcher> + </keep-alive> + + <div class="apply-container"> + <button + class="btn submit" + :disabled="!themeValid" + @click="setCustomTheme" + > + {{ $t('general.apply') }} + </button> + <button + class="btn" + @click="clearAll" + > + {{ $t('settings.style.switcher.reset') }} + </button> + </div> + </div> +</template> + +<script src="./theme_tab.js"></script> + +<style src="./theme_tab.scss" lang="scss"></style> diff --git a/src/components/settings_modal/tabs/version_tab.js b/src/components/settings_modal/tabs/version_tab.js @@ -0,0 +1,24 @@ +import { extractCommit } from 'src/services/version/version.service' + +const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/' +const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/' + +const VersionTab = { + data () { + const instance = this.$store.state.instance + return { + backendVersion: instance.backendVersion, + frontendVersion: instance.frontendVersion + } + }, + computed: { + frontendVersionLink () { + return pleromaFeCommitUrl + this.frontendVersion + }, + backendVersionLink () { + return pleromaBeCommitUrl + extractCommit(this.backendVersion) + } + } +} + +export default VersionTab diff --git a/src/components/settings_modal/tabs/version_tab.vue b/src/components/settings_modal/tabs/version_tab.vue @@ -0,0 +1,31 @@ +<template> + <div :label="$t('settings.version.title')"> + <div class="setting-item"> + <ul class="setting-list"> + <li> + <p>{{ $t('settings.version.backend_version') }}</p> + <ul class="option-list"> + <li> + <a + :href="backendVersionLink" + target="_blank" + >{{ backendVersion }}</a> + </li> + </ul> + </li> + <li> + <p>{{ $t('settings.version.frontend_version') }}</p> + <ul class="option-list"> + <li> + <a + :href="frontendVersionLink" + target="_blank" + >{{ frontendVersion }}</a> + </li> + </ul> + </li> + </ul> + </div> + </div> +</template> +<script src="./version_tab.js"> diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js @@ -1,3 +1,4 @@ +import { mapState, mapGetters } from 'vuex' import UserCard from '../user_card/user_card.vue' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' import GestureService from '../../services/gesture_service/gesture_service' @@ -47,7 +48,17 @@ const SideDrawer = { }, federating () { return this.$store.state.instance.federating - } + }, + timelinesRoute () { + if (this.$store.state.interface.lastTimeline) { + return this.$store.state.interface.lastTimeline + } + return this.currentUser ? 'friends' : 'public-timeline' + }, + ...mapState({ + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable + }), + ...mapGetters(['unreadChatCount']) }, methods: { toggleDrawer () { @@ -62,6 +73,9 @@ const SideDrawer = { }, touchMove (e) { GestureService.updateSwipe(e, this.closeGesture) + }, + openSettingsModal () { + this.$store.dispatch('openSettingsModal') } } } diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue @@ -40,33 +40,39 @@ </router-link> </li> <li - v-if="currentUser" + v-if="currentUser || !privateMode" @click="toggleDrawer" > - <router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }"> - <i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }} + <router-link :to="{ name: timelinesRoute }"> + <i class="button-icon icon-home-2" /> {{ $t("nav.timelines") }} </router-link> </li> <li - v-if="currentUser" + v-if="currentUser && pleromaChatMessagesAvailable" @click="toggleDrawer" > - <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> - <i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }} + <router-link + :to="{ name: 'chats', params: { username: currentUser.screen_name } }" + style="position: relative" + > + <i class="button-icon icon-chat" /> {{ $t("nav.chats") }} + <span + v-if="unreadChatCount" + class="badge badge-notification unread-chat-count" + > + {{ unreadChatCount }} + </span> </router-link> </li> </ul> - <ul> - <li - v-if="currentUser" - @click="toggleDrawer" - > - <router-link :to="{ name: 'friends' }"> - <i class="button-icon icon-home-2" /> {{ $t("nav.timeline") }} + <ul v-if="currentUser"> + <li @click="toggleDrawer"> + <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> + <i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }} </router-link> </li> <li - v-if="currentUser && currentUser.locked" + v-if="currentUser.locked" @click="toggleDrawer" > <router-link to="/friend-requests"> @@ -80,23 +86,7 @@ </router-link> </li> <li - v-if="currentUser || !privateMode" - @click="toggleDrawer" - > - <router-link to="/main/public"> - <i class="button-icon icon-users" /> {{ $t("nav.public_tl") }} - </router-link> - </li> - <li - v-if="federating && (currentUser || !privateMode)" - @click="toggleDrawer" - > - <router-link to="/main/all"> - <i class="button-icon icon-globe" /> {{ $t("nav.twkn") }} - </router-link> - </li> - <li - v-if="currentUser && chat" + v-if="chat" @click="toggleDrawer" > <router-link :to="{ name: 'chat' }"> @@ -122,9 +112,12 @@ </router-link> </li> <li @click="toggleDrawer"> - <router-link :to="{ name: 'settings' }"> + <a + href="#" + @click="openSettingsModal" + > <i class="button-icon icon-cog" /> {{ $t("settings.settings") }} - </router-link> + </a> </li> <li @click="toggleDrawer"> <router-link :to="{ name: 'about'}"> diff --git a/src/components/staff_panel/staff_panel.js b/src/components/staff_panel/staff_panel.js @@ -2,6 +2,10 @@ import map from 'lodash/map' import BasicUserCard from '../basic_user_card/basic_user_card.vue' const StaffPanel = { + created () { + const nicknames = this.$store.state.instance.staffAccounts + nicknames.forEach(nickname => this.$store.dispatch('fetchUserIfMissing', nickname)) + }, components: { BasicUserCard }, diff --git a/src/components/status/status.js b/src/components/status/status.js @@ -9,14 +9,31 @@ import AvatarList from '../avatar_list/avatar_list.vue' import Timeago from '../timeago/timeago.vue' import StatusContent from '../status_content/status_content.vue' import StatusPopover from '../status_popover/status_popover.vue' +import UserListPopover from '../user_list_popover/user_list_popover.vue' import EmojiReactions from '../emoji_reactions/emoji_reactions.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' -import { filter, unescape, uniqBy } from 'lodash' +import { muteWordHits } from '../../services/status_parser/status_parser.js' +import { unescape, uniqBy } from 'lodash' import { mapGetters, mapState } from 'vuex' const Status = { name: 'Status', + components: { + FavoriteButton, + ReactButton, + RetweetButton, + ExtraButtons, + PostStatusForm, + UserCard, + UserAvatar, + AvatarList, + Timeago, + StatusPopover, + UserListPopover, + EmojiReactions, + StatusContent + }, props: [ 'statusoid', 'expandable', @@ -44,6 +61,12 @@ const Status = { muteWords () { return this.mergedConfig.muteWords }, + showReasonMutedThread () { + return ( + this.status.thread_muted || + (this.status.reblog && this.status.reblog.thread_muted) + ) && !this.inConversation + }, repeaterClass () { const user = this.statusoid.user return highlightClass(user) @@ -93,26 +116,48 @@ const Status = { return !!this.currentUser }, muteWordHits () { - const statusText = this.status.text.toLowerCase() - const statusSummary = this.status.summary.toLowerCase() - const hits = filter(this.muteWords, (muteWord) => { - return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase()) - }) - - return hits + return muteWordHits(this.status, this.muteWords) }, muted () { - const relationship = this.$store.getters.relationship(this.status.user.id) - return !this.unmuted && ( - (!(this.inProfile && this.status.user.id === this.profileUserId) && relationship.muting) || - (!this.inConversation && this.status.thread_muted) || - this.muteWordHits.length > 0) + const { status } = this + const { reblog } = status + const relationship = this.$store.getters.relationship(status.user.id) + const relationshipReblog = reblog && this.$store.getters.relationship(reblog.user.id) + const reasonsToMute = ( + // Post is muted according to BE + status.muted || + // Reprööt of a muted post according to BE + (reblog && reblog.muted) || + // Muted user + relationship.muting || + // Muted user of a reprööt + (relationshipReblog && relationshipReblog.muting) || + // Thread is muted + status.thread_muted || + // Wordfiltered + this.muteWordHits.length > 0 + ) + const excusesNotToMute = ( + ( + this.inProfile && ( + // Don't mute user's posts on user timeline (except reblogs) + (!reblog && status.user.id === this.profileUserId) || + // Same as above but also allow self-reblogs + (reblog && reblog.user.id === this.profileUserId) + ) + ) || + // Don't mute statuses in muted conversation when said conversation is opened + (this.inConversation && status.thread_muted) + // No excuses if post has muted words + ) && !this.muteWordHits.length > 0 + + return !this.unmuted && !excusesNotToMute && reasonsToMute }, hideFilteredStatuses () { return this.mergedConfig.hideFilteredStatuses }, hideStatus () { - return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses) + return this.deleted || (this.muted && this.hideFilteredStatuses) }, isFocused () { // retweet or root of an expanded conversation @@ -135,37 +180,6 @@ const Status = { return user && user.screen_name } }, - hideReply () { - if (this.mergedConfig.replyVisibility === 'all') { - return false - } - if (this.inConversation || !this.isReply) { - return false - } - if (this.status.user.id === this.currentUser.id) { - return false - } - if (this.status.type === 'retweet') { - return false - } - const checkFollowing = this.mergedConfig.replyVisibility === 'following' - for (var i = 0; i < this.status.attentions.length; ++i) { - if (this.status.user.id === this.status.attentions[i].id) { - continue - } - // There's zero guarantee of this working. If we happen to have that user and their - // relationship in store then it will work, but there's kinda little chance of having - // them for people you're not following. - const relationship = this.$store.state.users.relationships[this.status.attentions[i].id] - if (checkFollowing && relationship && relationship.following) { - return false - } - if (this.status.attentions[i].id === this.currentUser.id) { - return false - } - } - return this.status.attentions.length > 0 - }, replySubject () { if (!this.status.summary) return '' const decodedSummary = unescape(this.status.summary) @@ -199,20 +213,6 @@ const Status = { currentUser: state => state.users.currentUser }) }, - components: { - FavoriteButton, - ReactButton, - RetweetButton, - ExtraButtons, - PostStatusForm, - UserCard, - UserAvatar, - AvatarList, - Timeago, - StatusPopover, - EmojiReactions, - StatusContent - }, methods: { visibilityIcon (visibility) { switch (visibility) { diff --git a/src/components/status/status.scss b/src/components/status/status.scss @@ -0,0 +1,414 @@ + +@import '../../_variables.scss'; + +$status-margin: 0.75em; + +.Status { + min-width: 0; + + &:hover { + --still-image-img: visible; + --still-image-canvas: hidden; + } + + &.-focused { + background-color: $fallback--lightBg; + background-color: var(--selectedPost, $fallback--lightBg); + color: $fallback--text; + color: var(--selectedPostText, $fallback--text); + + --lightText: var(--selectedPostLightText, $fallback--light); + --faint: var(--selectedPostFaintText, $fallback--faint); + --faintLink: var(--selectedPostFaintLink, $fallback--faint); + --postLink: var(--selectedPostPostLink, $fallback--faint); + --postFaintLink: var(--selectedPostFaintPostLink, $fallback--faint); + --icon: var(--selectedPostIcon, $fallback--icon); + } + + .status-container { + display: flex; + padding: $status-margin; + + &.-repeat { + padding-top: 0; + } + } + + .pin { + padding: $status-margin $status-margin 0; + display: flex; + align-items: center; + justify-content: flex-end; + } + + .left-side { + margin-right: $status-margin; + } + + .right-side { + flex: 1; + min-width: 0; + } + + .usercard { + margin-bottom: $status-margin; + } + + .status-username { + white-space: nowrap; + font-size: 14px; + overflow: hidden; + max-width: 85%; + font-weight: bold; + flex-shrink: 1; + margin-right: 0.4em; + text-overflow: ellipsis; + + .emoji { + width: 14px; + height: 14px; + vertical-align: middle; + object-fit: contain; + } + } + + .status-favicon { + height: 18px; + width: 18px; + margin-right: 0.4em; + } + + .status-heading { + margin-bottom: 0.5em; + } + + .heading-name-row { + display: flex; + justify-content: space-between; + line-height: 18px; + + a { + display: inline-block; + word-break: break-all; + } + } + + .account-name { + min-width: 1.6em; + margin-right: 0.4em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1 1 0; + } + + .heading-left { + display: flex; + min-width: 0; + } + + .heading-right { + display: flex; + flex-shrink: 0; + } + + .timeago { + margin-right: 0.2em; + } + + .heading-reply-row { + position: relative; + align-content: baseline; + font-size: 12px; + line-height: 18px; + max-width: 100%; + display: flex; + flex-wrap: wrap; + 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; + } + + .icon-reply { + // mirror the icon + transform: scaleX(-1); + } + } + + & .reply-to-popover, + & .reply-to-no-popover { + min-width: 0; + margin-right: 0.4em; + flex-shrink: 0; + } + + .reply-to-popover { + .reply-to:hover::before { + content: ''; + display: block; + position: absolute; + bottom: 0; + width: 100%; + border-bottom: 1px solid var(--faint); + pointer-events: none; + } + + .faint-link:hover { + // override default + text-decoration: none; + } + + &.-strikethrough { + .reply-to::after { + content: ''; + display: block; + position: absolute; + top: 50%; + width: 100%; + border-bottom: 1px solid var(--faint); + pointer-events: none; + } + } + } + + .reply-to { + display: flex; + position: relative; + } + + .reply-to-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-left: 0.2em; + } + + .replies-separator { + margin-left: 0.4em; + } + + .replies { + line-height: 18px; + font-size: 12px; + display: flex; + flex-wrap: wrap; + + & > * { + margin-right: 0.4em; + } + } + + .reply-link { + height: 17px; + } + + .repeat-info { + padding: 0.4em $status-margin; + line-height: 22px; + + .right-side { + display: flex; + align-content: center; + flex-wrap: wrap; + } + + i { + padding: 0 0.2em; + } + } + + .repeater-avatar { + border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + margin-left: 28px; + width: 20px; + height: 20px; + } + + .repeater-name { + text-overflow: ellipsis; + margin-right: 0; + + .emoji { + width: 14px; + height: 14px; + vertical-align: middle; + object-fit: contain; + } + } + + .status-fadein { + animation-duration: 0.4s; + animation-name: fadein; + } + + @keyframes fadein { + from { + opacity: 0; + } + + to { + opacity: 1; + } + } + + .status-actions { + position: relative; + width: 100%; + display: flex; + margin-top: $status-margin; + + > * { + max-width: 4em; + flex: 1; + } + } + + .button-reply { + &:not(.-disabled) { + cursor: pointer; + } + + &:not(.-disabled):hover, + &.-active { + color: $fallback--cBlue; + color: var(--cBlue, $fallback--cBlue); + } + } + + .muted { + padding: 0.25em 0.6em; + height: 1.2em; + line-height: 1.2em; + text-overflow: ellipsis; + overflow: hidden; + display: flex; + flex-wrap: nowrap; + + & .status-username, + & .mute-thread, + & .mute-words { + word-wrap: normal; + word-break: normal; + white-space: nowrap; + } + + & .status-username, + & .mute-words { + text-overflow: ellipsis; + overflow: hidden; + } + + .status-username { + font-weight: normal; + flex: 0 1 auto; + margin-right: 0.2em; + font-size: smaller; + } + + .mute-thread { + flex: 0 0 auto; + } + + .mute-words { + flex: 1 0 5em; + margin-left: 0.2em; + + &::before { + content: ' '; + } + } + + .unmute { + flex: 0 0 auto; + margin-left: auto; + display: block; + } + } + + .reply-form { + padding-top: 0; + padding-bottom: 0; + } + + .reply-body { + flex: 1; + } + + .favs-repeated-users { + margin-top: $status-margin; + } + + .stats { + width: 100%; + display: flex; + line-height: 1em; + } + + .avatar-row { + flex: 1; + overflow: hidden; + position: relative; + display: flex; + align-items: center; + + &::before { + content: ''; + position: absolute; + height: 100%; + width: 1px; + left: 0; + background-color: var(--faint, $fallback--faint); + } + } + + .stat-count { + margin-right: $status-margin; + user-select: none; + + .stat-title { + color: var(--faint, $fallback--faint); + font-size: 12px; + text-transform: uppercase; + position: relative; + } + + .stat-number { + font-weight: bolder; + font-size: 16px; + line-height: 1em; + } + + &:hover .stat-title { + text-decoration: underline; + } + } + + @media all and (max-width: 800px) { + .repeater-avatar { + margin-left: 20px; + } + + .avatar:not(.repeater-avatar) { + width: 40px; + height: 40px; + + // TODO define those other way somehow? + // stylelint-disable rscss/class-format + &.avatar-compact { + width: 32px; + height: 32px; + } + } + } +} diff --git a/src/components/status/status.vue b/src/components/status/status.vue @@ -2,8 +2,8 @@ <!-- eslint-disable vue/no-v-html --> <div v-if="!hideStatus" - class="status-el" - :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]" + class="Status" + :class="[{ '-focused': isFocused }, { '-conversation': inlineExpanded }]" > <div v-if="error" @@ -16,13 +16,34 @@ /> </div> <template v-if="muted && !isPreview"> - <div class="media status container muted"> - <small> + <div class="status-csontainer muted"> + <small class="status-username"> + <i + v-if="muted && retweet" + class="button-icon icon-retweet" + /> <router-link :to="userProfileLink"> {{ status.user.screen_name }} </router-link> </small> - <small class="muteWords">{{ muteWordHits.join(', ') }}</small> + <small + v-if="showReasonMutedThread" + class="mute-thread" + > + {{ $t('status.thread_muted') }} + </small> + <small + v-if="showReasonMutedThread && muteWordHits.length > 0" + class="mute-thread" + > + {{ $t('status.thread_muted_and_words') }} + </small> + <small + class="mute-words" + :title="muteWordHits.join(', ')" + > + {{ muteWordHits.join(', ') }} + </small> <a href="#" class="unmute" @@ -33,7 +54,7 @@ <template v-else> <div v-if="showPinned" - class="status-pin" + class="pin" > <i class="fa icon-pin faint" /> <span class="faint">{{ $t('status.pinned') }}</span> @@ -42,16 +63,19 @@ v-if="retweet && !noHeading && !inConversation" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" - class="media container retweet-info" + class="status-container repeat-info" > <UserAvatar v-if="retweet" - class="media-left" + class="left-side repeater-avatar" :better-shadow="betterShadow" :user="statusoid.user" /> - <div class="media-body faint"> - <span class="user-name"> + <div class="right-side faint"> + <span + class="status-username repeater-name" + :title="retweeter" + > <router-link v-if="retweeterHtml" :to="retweeterProfileLink" @@ -71,14 +95,14 @@ </div> <div - :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet && !inConversation }]" + :class="[userClass, { highlighted: userStyle, '-repeat': retweet && !inConversation }]" :style="[ userStyle ]" - class="media status" + class="status-container" :data-tags="tags" > <div v-if="!noHeading" - class="media-left" + class="left-side" > <router-link :to="userProfileLink" @@ -91,37 +115,45 @@ /> </router-link> </div> - <div class="status-body"> + <div class="right-side"> <UserCard v-if="userExpanded" :user-id="status.user.id" :rounded="true" :bordered="true" - class="status-usercard" + class="usercard" /> <div v-if="!noHeading" - class="media-heading" + class="status-heading" > <div class="heading-name-row"> - <div class="name-and-account-name"> + <div class="heading-left"> <h4 v-if="status.user.name_html" - class="user-name" + class="status-username" + :title="status.user.name" v-html="status.user.name_html" /> <h4 v-else - class="user-name" + class="status-username" + :title="status.user.name" > {{ status.user.name }} </h4> <router-link class="account-name" + :title="status.user.screen_name" :to="userProfileLink" > {{ status.user.screen_name }} </router-link> + <img + v-if="!!(status.user && status.user.favicon)" + class="status-favicon" + :src="status.user.favicon" + > </div> <span class="heading-right"> @@ -176,9 +208,10 @@ > <StatusPopover v-if="!isPreview" - :status-id="status.in_reply_to_status_id" + :status-id="status.parent_visible && status.in_reply_to_status_id" class="reply-to-popover" style="min-width: 0" + :class="{ '-strikethrough': !status.parent_visible }" > <a class="reply-to" @@ -186,17 +219,25 @@ :aria-label="$t('tool_tip.reply')" @click.prevent="gotoOriginal(status.in_reply_to_status_id)" > - <i class="button-icon icon-reply" /> - <span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span> + <i class="button-icon reply-button icon-reply" /> + <span + class="faint-link reply-to-text" + > + {{ $t('status.reply_to') }} + </span> </a> </StatusPopover> <span v-else - class="reply-to" + class="reply-to-no-popover" > <span class="reply-to-text">{{ $t('status.reply_to') }}</span> </span> - <router-link :to="replyProfileLink"> + <router-link + class="reply-to-link" + :title="replyToName" + :to="replyProfileLink" + > {{ replyToName }} </router-link> <span @@ -239,24 +280,30 @@ class="favs-repeated-users" > <div class="stats"> - <div + <UserListPopover v-if="statusFromGlobalRepository.rebloggedBy && statusFromGlobalRepository.rebloggedBy.length > 0" - class="stat-count" + :users="statusFromGlobalRepository.rebloggedBy" > - <a class="stat-title">{{ $t('status.repeats') }}</a> - <div class="stat-number"> - {{ statusFromGlobalRepository.rebloggedBy.length }} + <div class="stat-count"> + <a class="stat-title">{{ $t('status.repeats') }}</a> + <div class="stat-number"> + {{ statusFromGlobalRepository.rebloggedBy.length }} + </div> </div> - </div> - <div + </UserListPopover> + <UserListPopover v-if="statusFromGlobalRepository.favoritedBy && statusFromGlobalRepository.favoritedBy.length > 0" - class="stat-count" + :users="statusFromGlobalRepository.favoritedBy" > - <a class="stat-title">{{ $t('status.favorites') }}</a> - <div class="stat-number"> - {{ statusFromGlobalRepository.favoritedBy.length }} + <div + class="stat-count" + > + <a class="stat-title">{{ $t('status.favorites') }}</a> + <div class="stat-number"> + {{ statusFromGlobalRepository.favoritedBy.length }} + </div> </div> - </div> + </UserListPopover> <div class="avatar-row"> <AvatarList :users="combinedFavsAndRepeatsUsers" /> </div> @@ -271,19 +318,19 @@ <div v-if="!noHeading && !isPreview" - class="status-actions media-body" + class="status-actions" > <div> <i v-if="loggedIn" - class="button-icon icon-reply" + class="button-icon button-reply icon-reply" :title="$t('tool_tip.reply')" - :class="{'button-icon-active': replying}" + :class="{'-active': replying}" @click.prevent="toggleReplying" /> <i v-else - class="button-icon button-icon-disabled icon-reply" + class="button-icon button-reply -disabled icon-reply" :title="$t('tool_tip.reply')" /> <span v-if="status.replies_count > 0">{{ status.replies_count }}</span> @@ -311,7 +358,7 @@ </div> <div v-if="replying" - class="container" + class="status-container reply-form" > <PostStatusForm class="reply-body" @@ -329,398 +376,4 @@ </template> <script src="./status.js" ></script> -<style lang="scss"> -@import '../../_variables.scss'; - -$status-margin: 0.75em; - -.status-body { - flex: 1; - min-width: 0; -} - -.status-pin { - padding: $status-margin $status-margin 0; - display: flex; - align-items: center; - justify-content: flex-end; -} - -.media-left { - margin-right: $status-margin; -} - -.status-el { - overflow-wrap: break-word; - word-wrap: break-word; - word-break: break-word; - border-left-width: 0px; - min-width: 0; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - - border-left: 4px $fallback--cRed; - border-left: 4px var(--cRed, $fallback--cRed); - - &_focused { - background-color: $fallback--lightBg; - background-color: var(--selectedPost, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedPostText, $fallback--text); - --lightText: var(--selectedPostLightText, $fallback--light); - --faint: var(--selectedPostFaintText, $fallback--faint); - --faintLink: var(--selectedPostFaintLink, $fallback--faint); - --postLink: var(--selectedPostPostLink, $fallback--faint); - --postFaintLink: var(--selectedPostFaintPostLink, $fallback--faint); - --icon: var(--selectedPostIcon, $fallback--icon); - } - - .timeline & { - border-bottom-width: 1px; - border-bottom-style: solid; - } - - .media-body { - flex: 1; - padding: 0; - } - - .status-usercard { - margin-bottom: $status-margin; - } - - .user-name { - white-space: nowrap; - font-size: 14px; - overflow: hidden; - flex-shrink: 0; - max-width: 85%; - font-weight: bold; - - img { - width: 14px; - height: 14px; - vertical-align: middle; - object-fit: contain - } - } - - .media-heading { - padding: 0; - vertical-align: bottom; - flex-basis: 100%; - margin-bottom: 0.5em; - - small { - font-weight: lighter; - } - - .heading-name-row { - padding: 0; - display: flex; - justify-content: space-between; - line-height: 18px; - - a { - display: inline-block; - word-break: break-all; - } - - .name-and-account-name { - display: flex; - min-width: 0; - } - - .user-name { - flex-shrink: 1; - margin-right: 0.4em; - overflow: hidden; - text-overflow: ellipsis; - } - - .account-name { - min-width: 1.6em; - margin-right: 0.4em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1 1 0; - } - } - - .heading-right { - display: flex; - flex-shrink: 0; - } - - .timeago { - margin-right: 0.2em; - } - - .heading-reply-row { - position: relative; - align-content: baseline; - font-size: 12px; - line-height: 18px; - max-width: 100%; - display: flex; - flex-wrap: wrap; - align-items: stretch; - - > .reply-to-and-accountname > a { - overflow: hidden; - max-width: 100%; - text-overflow: ellipsis; - white-space: nowrap; - word-break: break-all; - } - } - - .reply-to-and-accountname { - display: flex; - height: 18px; - margin-right: 0.5em; - max-width: 100%; - .icon-reply { - transform: scaleX(-1); - } - } - - .reply-info { - display: flex; - } - - .reply-to-popover { - min-width: 0; - } - - .reply-to { - display: flex; - } - - .reply-to-text { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - margin: 0 0.4em 0 0.2em; - } - - .replies-separator { - margin-left: 0.4em; - } - - .replies { - line-height: 18px; - font-size: 12px; - display: flex; - flex-wrap: wrap; - & > * { - margin-right: 0.4em; - } - } - - .reply-link { - height: 17px; - } - } - - .retweet-info { - padding: 0.4em $status-margin; - margin: 0; - - .avatar.still-image { - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); - margin-left: 28px; - width: 20px; - height: 20px; - } - - .media-body { - font-size: 1em; - line-height: 22px; - - display: flex; - align-content: center; - flex-wrap: wrap; - - .user-name { - font-weight: bold; - overflow: hidden; - text-overflow: ellipsis; - - img { - width: 14px; - height: 14px; - vertical-align: middle; - object-fit: contain - } - } - - i { - padding: 0 0.2em; - } - - a { - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - } -} - -.status-fadein { - animation-duration: 0.4s; - animation-name: fadein; -} - -@keyframes fadein { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -.status-conversation { - border-left-style: solid; -} - -.status-actions { - position: relative; - width: 100%; - display: flex; - margin-top: $status-margin; - - > * { - max-width: 4em; - flex: 1; - } -} - -.button-icon.icon-reply { - &:not(.button-icon-disabled):hover, - &.button-icon-active { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); - } -} - -.button-icon.icon-reply { - &:not(.button-icon-disabled) { - cursor: pointer; - } -} - -.status:hover .animated.avatar { - canvas { - display: none; - } - img { - visibility: visible; - } -} - -.status { - display: flex; - padding: $status-margin; - &.is-retweet { - padding-top: 0; - } -} - -.status-conversation:last-child { - border-bottom: none; -} - -.muted { - padding: 0.25em 0.5em; - button { - margin-left: auto; - } - - .muteWords { - margin-left: 10px; - } -} - -a.unmute { - display: block; - margin-left: auto; -} - -.reply-body { - flex: 1; -} - -.favs-repeated-users { - margin-top: $status-margin; - - .stats { - width: 100%; - display: flex; - line-height: 1em; - - .stat-count { - margin-right: $status-margin; - - .stat-title { - color: var(--faint, $fallback--faint); - font-size: 12px; - text-transform: uppercase; - position: relative; - } - - .stat-number { - font-weight: bolder; - font-size: 16px; - line-height: 1em; - } - } - - .avatar-row { - flex: 1; - overflow: hidden; - position: relative; - display: flex; - align-items: center; - - &::before { - content: ''; - position: absolute; - height: 100%; - width: 1px; - left: 0; - background-color: var(--faint, $fallback--faint); - } - } - } -} - -@media all and (max-width: 800px) { - .status-el { - .retweet-info { - .avatar.still-image { - margin-left: 20px; - } - } - } - .status { - max-width: 100%; - } - - .status .avatar.still-image { - width: 40px; - height: 40px; - - &.avatar-compact { - width: 32px; - height: 32px; - } - } -} - -</style> +<style src="./status.scss" lang="scss"></style> diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js @@ -14,11 +14,12 @@ const StatusContent = { 'status', 'focused', 'noHeading', - 'fullContent' + 'fullContent', + 'singleLine' ], data () { return { - showingTall: this.inConversation && this.focused, + 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 @@ -44,14 +45,14 @@ const StatusContent = { return lengthScore > 20 }, longSubject () { - return this.status.summary.length > 900 + 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.tallStatus || this.localCollapseSubjectDefault) + return !!this.status.summary && this.localCollapseSubjectDefault }, mightHideBecauseTall () { - return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault) + return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault) }, hideSubjectStatus () { return this.mightHideBecauseSubject && !this.expandingSubject @@ -99,15 +100,8 @@ const StatusContent = { file => !fileType.fileMatchesSomeType(this.galleryTypes, file) ) }, - hasImageAttachments () { - return this.status.attachments.some( - file => fileType.fileType(file.mimetype) === 'image' - ) - }, - hasVideoAttachments () { - return this.status.attachments.some( - file => fileType.fileType(file.mimetype) === 'video' - ) + attachmentTypes () { + return this.status.attachments.map(file => fileType.fileType(file.mimetype)) }, maxThumbnails () { return this.mergedConfig.maxThumbnails @@ -142,12 +136,6 @@ const StatusContent = { return html } }, - contentHtml () { - if (!this.status.summary_html) { - return this.postBodyHtml - } - return this.status.summary_html + '<br />' + this.postBodyHtml - }, ...mapGetters(['mergedConfig']), ...mapState({ betterShadow: state => state.interface.browserSupport.cssFilter, @@ -176,8 +164,8 @@ const StatusContent = { } } if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) { - // Extract tag name from link url - const tag = extractTagFromUrl(target.href) + // 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) diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue @@ -1,47 +1,34 @@ <template> <!-- eslint-disable vue/no-v-html --> - <div class="status-body"> + <div class="StatusContent"> <slot name="header" /> <div - v-if="longSubject" - class="status-content-wrapper" - :class="{ 'tall-status': !showingLongSubject }" + v-if="status.summary_html" + class="summary-wrapper" + :class="{ 'tall-subject': (longSubject && !showingLongSubject) }" > - <a - v-if="!showingLongSubject" - class="tall-status-hider" - :class="{ 'tall-status-hider_focused': focused }" - href="#" - @click.prevent="showingLongSubject=true" - > - {{ $t("general.show_more") }} - <span - v-if="hasImageAttachments" - class="icon-picture" - /> - <span - v-if="hasVideoAttachments" - class="icon-video" - /> - <span - v-if="status.card" - class="icon-link" - /> - </a> <div - class="status-content media-body" + class="media-body summary" @click.prevent="linkClicked" - v-html="contentHtml" + v-html="status.summary_html" /> <a - v-if="showingLongSubject" + v-if="longSubject && showingLongSubject" href="#" - class="status-unhider" + class="tall-subject-hider" @click.prevent="showingLongSubject=false" - >{{ $t("general.show_less") }}</a> + >{{ $t("status.hide_full_subject") }}</a> + <a + v-else-if="longSubject" + class="tall-subject-hider" + :class="{ 'tall-subject-hider_focused': focused }" + href="#" + @click.prevent="showingLongSubject=true" + > + {{ $t("status.show_full_subject") }} + </a> </div> <div - v-else :class="{'tall-status': hideTallStatus}" class="status-content-wrapper" > @@ -51,31 +38,52 @@ :class="{ 'tall-status-hider_focused': focused }" href="#" @click.prevent="toggleShowMore" - >{{ $t("general.show_more") }}</a> + > + {{ $t("general.show_more") }} + </a> <div v-if="!hideSubjectStatus" + :class="{ 'single-line': singleLine }" class="status-content media-body" @click.prevent="linkClicked" - v-html="contentHtml" - /> - <div - v-else - class="status-content media-body" - @click.prevent="linkClicked" - v-html="status.summary_html" + v-html="postBodyHtml" /> <a v-if="hideSubjectStatus" href="#" class="cw-status-hider" @click.prevent="toggleShowMore" - >{{ $t("general.show_more") }}</a> + > + {{ $t("status.show_content") }} + <span + v-if="attachmentTypes.includes('image')" + class="icon-picture" + /> + <span + v-if="attachmentTypes.includes('video')" + class="icon-video" + /> + <span + v-if="attachmentTypes.includes('audio')" + class="icon-music" + /> + <span + v-if="attachmentTypes.includes('unknown')" + class="icon-doc" + /> + <span + v-if="status.card" + class="icon-link" + /> + </a> <a - v-if="showingMore" + v-if="showingMore && !fullContent" href="#" class="status-unhider" @click.prevent="toggleShowMore" - >{{ $t("general.show_less") }}</a> + > + {{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }} + </a> </div> <div v-if="status.poll && status.poll.options"> @@ -125,10 +133,16 @@ $status-margin: 0.75em; -.status-body { +.StatusContent { flex: 1; min-width: 0; + .status-content-wrapper { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + } + .tall-status { position: relative; height: 220px; @@ -136,7 +150,7 @@ $status-margin: 0.75em; overflow-y: hidden; z-index: 1; .status-content { - height: 100%; + 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 */ @@ -164,22 +178,57 @@ $status-margin: 0.75em; word-break: break-all; } + 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; - - img, video { - max-width: 100%; - max-height: 400px; - vertical-align: middle; - object-fit: contain; - - &.emoji { - width: 32px; - height: 32px; - } - } + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; blockquote { margin: 0.2em 0 0.2em 2em; @@ -221,20 +270,18 @@ $status-margin: 0.75em; h4 { margin: 1.1em 0; } + + &.single-line { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + height: 1.4em; + } } } .greentext { color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); + color: var(--postGreentext, $fallback--cGreen); } - -.timeline :not(.panel-disabled) > { - .status-el:last-child { - border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; - border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); - border-bottom: none; - } -} - </style> diff --git a/src/components/status_popover/status_popover.js b/src/components/status_popover/status_popover.js @@ -22,6 +22,10 @@ const StatusPopover = { methods: { enter () { if (!this.status) { + if (!this.statusId) { + this.error = true + return + } this.$store.dispatch('fetchStatus', this.statusId) .then(data => (this.error = false)) .catch(e => (this.error = true)) diff --git a/src/components/status_popover/status_popover.vue b/src/components/status_popover/status_popover.vue @@ -1,7 +1,7 @@ <template> <Popover trigger="hover" - popover-class="status-popover" + popover-class="popover-default status-popover" :bound-to="{ x: 'container' }" @show="enter" > @@ -38,7 +38,8 @@ <style lang="scss"> @import '../../_variables.scss'; -.status-popover { +/* popover styles load on-demand, so we need to override */ +.status-popover.popover { font-size: 1rem; min-width: 15em; max-width: 95%; @@ -52,7 +53,8 @@ box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5); box-shadow: var(--popupShadow); - .status-el.status-el { + /* TODO cleanup this */ + .Status.Status { border: none; } diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js @@ -4,7 +4,8 @@ const StillImage = { 'referrerpolicy', 'mimetype', 'imageLoadError', - 'imageLoadHandler' + 'imageLoadHandler', + 'alt' ], data () { return { diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue @@ -11,6 +11,8 @@ <img ref="src" :key="src" + :alt="alt" + :title="alt" :src="src" :referrerpolicy="referrerpolicy" @load="onLoad" @@ -23,33 +25,33 @@ <style lang="scss"> @import '../../_variables.scss'; + .still-image { position: relative; line-height: 0; overflow: hidden; - width: 100%; - height: 100%; + display: flex; + align-items: center; - &:hover canvas { - display: none; + canvas { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: 100%; + height: 100%; + object-fit: contain; + visibility: var(--still-image-canvas, visible); } img { width: 100%; - height: 100%; + min-height: 100%; object-fit: contain; } &.animated { - &:hover::before, - img { - visibility: hidden; - } - - &:hover img { - visibility: visible - } - &::before { content: 'gif'; position: absolute; @@ -57,25 +59,28 @@ font-size: 10px; top: 5px; left: 5px; - background: rgba(127,127,127,.5); - color: #FFF; + background: rgba(127, 127, 127, 0.5); + color: #fff; display: block; padding: 2px 4px; border-radius: $fallback--tooltipRadius; border-radius: var(--tooltipRadius, $fallback--tooltipRadius); z-index: 2; + visibility: var(--still-image-label-visibility, visible); } - } - canvas { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - width: 100%; - height: 100%; - object-fit: contain; + &:hover canvas { + display: none; + } + + &:hover::before, + img { + visibility: var(--still-image-img, hidden); + } + + &:hover img { + visibility: visible; + } } } </style> diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js @@ -1,758 +0,0 @@ -import { set, delete as del } from 'vue' -import { - rgb2hex, - hex2rgb, - getContrastRatioLayers -} from '../../services/color_convert/color_convert.js' -import { - DEFAULT_SHADOWS, - generateColors, - generateShadows, - generateRadii, - generateFonts, - composePreset, - getThemes, - shadows2to3, - colors2to3 -} from '../../services/style_setter/style_setter.js' -import { - SLOT_INHERITANCE -} from '../../services/theme_data/pleromafe.js' -import { - CURRENT_VERSION, - OPACITIES, - getLayers, - getOpacitySlot -} from '../../services/theme_data/theme_data.service.js' -import ColorInput from '../color_input/color_input.vue' -import RangeInput from '../range_input/range_input.vue' -import OpacityInput from '../opacity_input/opacity_input.vue' -import ShadowControl from '../shadow_control/shadow_control.vue' -import FontControl from '../font_control/font_control.vue' -import ContrastRatio from '../contrast_ratio/contrast_ratio.vue' -import TabSwitcher from '../tab_switcher/tab_switcher.js' -import Preview from './preview.vue' -import ExportImport from '../export_import/export_import.vue' -import Checkbox from '../checkbox/checkbox.vue' - -// List of color values used in v1 -const v1OnlyNames = [ - 'bg', - 'fg', - 'text', - 'link', - 'cRed', - 'cGreen', - 'cBlue', - 'cOrange' -].map(_ => _ + 'ColorLocal') - -const colorConvert = (color) => { - if (color.startsWith('--') || color === 'transparent') { - return color - } else { - return hex2rgb(color) - } -} - -export default { - data () { - return { - availableStyles: [], - selected: this.$store.getters.mergedConfig.theme, - themeWarning: undefined, - tempImportFile: undefined, - engineVersion: 0, - - previewShadows: {}, - previewColors: {}, - previewRadii: {}, - previewFonts: {}, - - shadowsInvalid: true, - colorsInvalid: true, - radiiInvalid: true, - - keepColor: false, - keepShadows: false, - keepOpacity: false, - keepRoundness: false, - keepFonts: false, - - ...Object.keys(SLOT_INHERITANCE) - .map(key => [key, '']) - .reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}), - - ...Object.keys(OPACITIES) - .map(key => [key, '']) - .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}), - - shadowSelected: undefined, - shadowsLocal: {}, - fontsLocal: {}, - - btnRadiusLocal: '', - inputRadiusLocal: '', - checkboxRadiusLocal: '', - panelRadiusLocal: '', - avatarRadiusLocal: '', - avatarAltRadiusLocal: '', - attachmentRadiusLocal: '', - tooltipRadiusLocal: '' - } - }, - created () { - const self = this - - getThemes() - .then((promises) => { - return Promise.all( - Object.entries(promises) - .map(([k, v]) => v.then(res => [k, res])) - ) - }) - .then(themes => themes.reduce((acc, [k, v]) => { - if (v) { - return { - ...acc, - [k]: v - } - } else { - return acc - } - }, {})) - .then((themesComplete) => { - self.availableStyles = themesComplete - }) - }, - mounted () { - this.loadThemeFromLocalStorage() - if (typeof this.shadowSelected === 'undefined') { - this.shadowSelected = this.shadowsAvailable[0] - } - }, - computed: { - themeWarningHelp () { - if (!this.themeWarning) return - const t = this.$t - const pre = 'settings.style.switcher.help.' - const { - origin, - themeEngineVersion, - type, - noActionsPossible - } = this.themeWarning - if (origin === 'file') { - // Loaded v2 theme from file - if (themeEngineVersion === 2 && type === 'wrong_version') { - return t(pre + 'v2_imported') - } - if (themeEngineVersion > CURRENT_VERSION) { - return t(pre + 'future_version_imported') + ' ' + - ( - noActionsPossible - ? t(pre + 'snapshot_missing') - : t(pre + 'snapshot_present') - ) - } - if (themeEngineVersion < CURRENT_VERSION) { - return t(pre + 'future_version_imported') + ' ' + - ( - noActionsPossible - ? t(pre + 'snapshot_missing') - : t(pre + 'snapshot_present') - ) - } - } else if (origin === 'localStorage') { - if (type === 'snapshot_source_mismatch') { - return t(pre + 'snapshot_source_mismatch') - } - // FE upgraded from v2 - if (themeEngineVersion === 2) { - return t(pre + 'upgraded_from_v2') - } - // Admin downgraded FE - if (themeEngineVersion > CURRENT_VERSION) { - return t(pre + 'fe_downgraded') + ' ' + - ( - noActionsPossible - ? t(pre + 'migration_snapshot_ok') - : t(pre + 'migration_snapshot_gone') - ) - } - // Admin upgraded FE - if (themeEngineVersion < CURRENT_VERSION) { - return t(pre + 'fe_upgraded') + ' ' + - ( - noActionsPossible - ? t(pre + 'migration_snapshot_ok') - : t(pre + 'migration_snapshot_gone') - ) - } - } - }, - selectedVersion () { - return Array.isArray(this.selected) ? 1 : 2 - }, - currentColors () { - return Object.keys(SLOT_INHERITANCE) - .map(key => [key, this[key + 'ColorLocal']]) - .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {}) - }, - currentOpacity () { - return Object.keys(OPACITIES) - .map(key => [key, this[key + 'OpacityLocal']]) - .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {}) - }, - currentRadii () { - return { - btn: this.btnRadiusLocal, - input: this.inputRadiusLocal, - checkbox: this.checkboxRadiusLocal, - panel: this.panelRadiusLocal, - avatar: this.avatarRadiusLocal, - avatarAlt: this.avatarAltRadiusLocal, - tooltip: this.tooltipRadiusLocal, - attachment: this.attachmentRadiusLocal - } - }, - preview () { - return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts) - }, - previewTheme () { - if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} } - return this.preview.theme - }, - // This needs optimization maybe - previewContrast () { - try { - if (!this.previewTheme.colors.bg) return {} - const colors = this.previewTheme.colors - const opacity = this.previewTheme.opacity - if (!colors.bg) return {} - const hints = (ratio) => ({ - text: ratio.toPrecision(3) + ':1', - // AA level, AAA level - aa: ratio >= 4.5, - aaa: ratio >= 7, - // same but for 18pt+ texts - laa: ratio >= 3, - laaa: ratio >= 4.5 - }) - const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {}) - - const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => { - const slotIsBaseText = key === 'text' || key === 'link' - const slotIsText = slotIsBaseText || ( - typeof value === 'object' && value !== null && value.textColor - ) - if (!slotIsText) return acc - const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value - const background = variant || layer - const opacitySlot = getOpacitySlot(background) - const textColors = [ - key, - ...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : []) - ] - - const layers = getLayers( - layer, - variant || layer, - opacitySlot, - colorsConverted, - opacity - ) - - return { - ...acc, - ...textColors.reduce((acc, textColorKey) => { - const newKey = slotIsBaseText - ? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1) - : textColorKey - return { - ...acc, - [newKey]: getContrastRatioLayers( - colorsConverted[textColorKey], - layers, - colorsConverted[textColorKey] - ) - } - }, {}) - } - }, {}) - - return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {}) - } catch (e) { - console.warn('Failure computing contrasts', e) - } - }, - previewRules () { - if (!this.preview.rules) return '' - return [ - ...Object.values(this.preview.rules), - 'color: var(--text)', - 'font-family: var(--interfaceFont, sans-serif)' - ].join(';') - }, - shadowsAvailable () { - return Object.keys(DEFAULT_SHADOWS).sort() - }, - currentShadowOverriden: { - get () { - return !!this.currentShadow - }, - set (val) { - if (val) { - set(this.shadowsLocal, this.shadowSelected, this.currentShadowFallback.map(_ => Object.assign({}, _))) - } else { - del(this.shadowsLocal, this.shadowSelected) - } - } - }, - currentShadowFallback () { - return (this.previewTheme.shadows || {})[this.shadowSelected] - }, - currentShadow: { - get () { - return this.shadowsLocal[this.shadowSelected] - }, - set (v) { - set(this.shadowsLocal, this.shadowSelected, v) - } - }, - themeValid () { - return !this.shadowsInvalid && !this.colorsInvalid && !this.radiiInvalid - }, - exportedTheme () { - const saveEverything = ( - !this.keepFonts && - !this.keepShadows && - !this.keepOpacity && - !this.keepRoundness && - !this.keepColor - ) - - const source = { - themeEngineVersion: CURRENT_VERSION - } - - if (this.keepFonts || saveEverything) { - source.fonts = this.fontsLocal - } - if (this.keepShadows || saveEverything) { - source.shadows = this.shadowsLocal - } - if (this.keepOpacity || saveEverything) { - source.opacity = this.currentOpacity - } - if (this.keepColor || saveEverything) { - source.colors = this.currentColors - } - if (this.keepRoundness || saveEverything) { - source.radii = this.currentRadii - } - - const theme = { - themeEngineVersion: CURRENT_VERSION, - ...this.previewTheme - } - - return { - // To separate from other random JSON files and possible future source formats - _pleroma_theme_version: 2, theme, source - } - } - }, - components: { - ColorInput, - OpacityInput, - RangeInput, - ContrastRatio, - ShadowControl, - FontControl, - TabSwitcher, - Preview, - ExportImport, - Checkbox - }, - methods: { - loadTheme ( - { - theme, - source, - _pleroma_theme_version: fileVersion - }, - origin, - forceUseSource = false - ) { - this.dismissWarning() - if (!source && !theme) { - throw new Error('Can\'t load theme: empty') - } - const version = (origin === 'localStorage' && !theme.colors) - ? 'l1' - : fileVersion - const snapshotEngineVersion = (theme || {}).themeEngineVersion - const themeEngineVersion = (source || {}).themeEngineVersion || 2 - const versionsMatch = themeEngineVersion === CURRENT_VERSION - const sourceSnapshotMismatch = ( - theme !== undefined && - source !== undefined && - themeEngineVersion !== snapshotEngineVersion - ) - // Force loading of source if user requested it or if snapshot - // is unavailable - const forcedSourceLoad = (source && forceUseSource) || !theme - if (!(versionsMatch && !sourceSnapshotMismatch) && - !forcedSourceLoad && - version !== 'l1' && - origin !== 'defaults' - ) { - if (sourceSnapshotMismatch && origin === 'localStorage') { - this.themeWarning = { - origin, - themeEngineVersion, - type: 'snapshot_source_mismatch' - } - } else if (!theme) { - this.themeWarning = { - origin, - noActionsPossible: true, - themeEngineVersion, - type: 'no_snapshot_old_version' - } - } else if (!versionsMatch) { - this.themeWarning = { - origin, - noActionsPossible: !source, - themeEngineVersion, - type: 'wrong_version' - } - } - } - this.normalizeLocalState(theme, version, source, forcedSourceLoad) - }, - forceLoadLocalStorage () { - this.loadThemeFromLocalStorage(true) - }, - dismissWarning () { - this.themeWarning = undefined - this.tempImportFile = undefined - }, - forceLoad () { - const { origin } = this.themeWarning - switch (origin) { - case 'localStorage': - this.loadThemeFromLocalStorage(true) - break - case 'file': - this.onImport(this.tempImportFile, true) - break - } - this.dismissWarning() - }, - forceSnapshot () { - const { origin } = this.themeWarning - switch (origin) { - case 'localStorage': - this.loadThemeFromLocalStorage(false, true) - break - case 'file': - console.err('Forcing snapshout from file is not supported yet') - break - } - this.dismissWarning() - }, - loadThemeFromLocalStorage (confirmLoadSource = false, forceSnapshot = false) { - const { - customTheme: theme, - customThemeSource: source - } = this.$store.getters.mergedConfig - if (!theme && !source) { - // Anon user or never touched themes - this.loadTheme( - this.$store.state.instance.themeData, - 'defaults', - confirmLoadSource - ) - } else { - this.loadTheme( - { - theme, - source: forceSnapshot ? theme : source - }, - 'localStorage', - confirmLoadSource - ) - } - }, - setCustomTheme () { - this.$store.dispatch('setOption', { - name: 'customTheme', - value: { - themeEngineVersion: CURRENT_VERSION, - ...this.previewTheme - } - }) - this.$store.dispatch('setOption', { - name: 'customThemeSource', - value: { - themeEngineVersion: CURRENT_VERSION, - shadows: this.shadowsLocal, - fonts: this.fontsLocal, - opacity: this.currentOpacity, - colors: this.currentColors, - radii: this.currentRadii - } - }) - }, - updatePreviewColorsAndShadows () { - this.previewColors = generateColors({ - opacity: this.currentOpacity, - colors: this.currentColors - }) - this.previewShadows = generateShadows( - { shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion }, - this.previewColors.theme.colors, - this.previewColors.mod - ) - }, - onImport (parsed, forceSource = false) { - this.tempImportFile = parsed - this.loadTheme(parsed, 'file', forceSource) - }, - importValidator (parsed) { - const version = parsed._pleroma_theme_version - return version >= 1 || version <= 2 - }, - clearAll () { - this.loadThemeFromLocalStorage() - }, - - // Clears all the extra stuff when loading V1 theme - clearV1 () { - Object.keys(this.$data) - .filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal')) - .filter(_ => !v1OnlyNames.includes(_)) - .forEach(key => { - set(this.$data, key, undefined) - }) - }, - - clearRoundness () { - Object.keys(this.$data) - .filter(_ => _.endsWith('RadiusLocal')) - .forEach(key => { - set(this.$data, key, undefined) - }) - }, - - clearOpacity () { - Object.keys(this.$data) - .filter(_ => _.endsWith('OpacityLocal')) - .forEach(key => { - set(this.$data, key, undefined) - }) - }, - - clearShadows () { - this.shadowsLocal = {} - }, - - clearFonts () { - this.fontsLocal = {} - }, - - /** - * This applies stored theme data onto form. Supports three versions of data: - * v3 (version >= 3) - newest version of themes which supports snapshots for better compatiblity - * v2 (version = 2) - newer version of themes. - * v1 (version = 1) - older version of themes (import from file) - * v1l (version = l1) - older version of theme (load from local storage) - * v1 and v1l differ because of way themes were stored/exported. - * @param {Object} theme - theme data (snapshot) - * @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type - * @param {Object} source - theme source - this will be used if compatible - * @param {Boolean} source - by default source won't be used if version doesn't match since it might render differently - * this allows importing source anyway - */ - normalizeLocalState (theme, version = 0, source, forceSource = false) { - let input - if (typeof source !== 'undefined') { - if (forceSource || source.themeEngineVersion === CURRENT_VERSION) { - input = source - version = source.themeEngineVersion - } else { - input = theme - } - } else { - input = theme - } - - const radii = input.radii || input - const opacity = input.opacity - const shadows = input.shadows || {} - const fonts = input.fonts || {} - const colors = !input.themeEngineVersion - ? colors2to3(input.colors || input) - : input.colors || input - - if (version === 0) { - if (input.version) version = input.version - // Old v1 naming: fg is text, btn is foreground - if (typeof colors.text === 'undefined' && typeof colors.fg !== 'undefined') { - version = 1 - } - // New v2 naming: text is text, fg is foreground - if (typeof colors.text !== 'undefined' && typeof colors.fg !== 'undefined') { - version = 2 - } - } - - this.engineVersion = version - - // Stuff that differs between V1 and V2 - if (version === 1) { - this.fgColorLocal = rgb2hex(colors.btn) - this.textColorLocal = rgb2hex(colors.fg) - } - - if (!this.keepColor) { - this.clearV1() - const keys = new Set(version !== 1 ? Object.keys(SLOT_INHERITANCE) : []) - if (version === 1 || version === 'l1') { - keys - .add('bg') - .add('link') - .add('cRed') - .add('cBlue') - .add('cGreen') - .add('cOrange') - } - - keys.forEach(key => { - const color = colors[key] - const hex = rgb2hex(colors[key]) - this[key + 'ColorLocal'] = hex === '#aN' ? color : hex - }) - } - - if (opacity && !this.keepOpacity) { - this.clearOpacity() - Object.entries(opacity).forEach(([k, v]) => { - if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return - this[k + 'OpacityLocal'] = v - }) - } - - if (!this.keepRoundness) { - this.clearRoundness() - Object.entries(radii).forEach(([k, v]) => { - // 'Radius' is kept mostly for v1->v2 localstorage transition - const key = k.endsWith('Radius') ? k.split('Radius')[0] : k - this[key + 'RadiusLocal'] = v - }) - } - - if (!this.keepShadows) { - this.clearShadows() - if (version === 2) { - this.shadowsLocal = shadows2to3(shadows, this.previewTheme.opacity) - } else { - this.shadowsLocal = shadows - } - this.shadowSelected = this.shadowsAvailable[0] - } - - if (!this.keepFonts) { - this.clearFonts() - this.fontsLocal = fonts - } - } - }, - watch: { - currentRadii () { - try { - this.previewRadii = generateRadii({ radii: this.currentRadii }) - this.radiiInvalid = false - } catch (e) { - this.radiiInvalid = true - console.warn(e) - } - }, - shadowsLocal: { - handler () { - if (Object.getOwnPropertyNames(this.previewColors).length === 1) return - try { - this.updatePreviewColorsAndShadows() - this.shadowsInvalid = false - } catch (e) { - this.shadowsInvalid = true - console.warn(e) - } - }, - deep: true - }, - fontsLocal: { - handler () { - try { - this.previewFonts = generateFonts({ fonts: this.fontsLocal }) - this.fontsInvalid = false - } catch (e) { - this.fontsInvalid = true - console.warn(e) - } - }, - deep: true - }, - currentColors () { - try { - this.updatePreviewColorsAndShadows() - this.colorsInvalid = false - this.shadowsInvalid = false - } catch (e) { - this.colorsInvalid = true - this.shadowsInvalid = true - console.warn(e) - } - }, - currentOpacity () { - try { - this.updatePreviewColorsAndShadows() - } catch (e) { - console.warn(e) - } - }, - selected () { - this.dismissWarning() - if (this.selectedVersion === 1) { - if (!this.keepRoundness) { - this.clearRoundness() - } - - if (!this.keepShadows) { - this.clearShadows() - } - - if (!this.keepOpacity) { - this.clearOpacity() - } - - if (!this.keepColor) { - this.clearV1() - - this.bgColorLocal = this.selected[1] - this.fgColorLocal = this.selected[2] - this.textColorLocal = this.selected[3] - this.linkColorLocal = this.selected[4] - this.cRedColorLocal = this.selected[5] - this.cGreenColorLocal = this.selected[6] - this.cBlueColorLocal = this.selected[7] - this.cOrangeColorLocal = this.selected[8] - } - } else if (this.selectedVersion >= 2) { - this.normalizeLocalState(this.selected.theme, 2, this.selected.source) - } - } - } -} diff --git a/src/components/style_switcher/style_switcher.scss b/src/components/style_switcher/style_switcher.scss @@ -1,335 +0,0 @@ -@import '../../_variables.scss'; -.style-switcher { - .theme-warning { - display: flex; - align-items: baseline; - margin-bottom: .5em; - .buttons { - .btn { - margin-bottom: .5em; - } - } - } - .preset-switcher { - margin-right: 1em; - } - - .style-control { - display: flex; - align-items: baseline; - margin-bottom: 5px; - - .label { - flex: 1; - } - - &.disabled { - input, select { - opacity: .5 - } - } - - .opt { - margin: .5em; - } - - .color-input { - flex: 0 0 0; - } - - input, select { - min-width: 3em; - margin: 0; - flex: 0; - - &[type=number] { - min-width: 5em; - } - - &[type=range] { - flex: 1; - min-width: 3em; - align-self: flex-start; - } - } - } - - .tab-switcher { - margin: 0 -1em; - } - - .reset-container { - flex-wrap: wrap; - } - - .fonts-container, - .reset-container, - .apply-container, - .radius-container, - .color-container, - { - display: flex; - } - - .fonts-container, - .radius-container { - flex-direction: column; - } - - .color-container{ - > h4 { - width: 99%; - } - flex-wrap: wrap; - justify-content: space-between; - } - - .fonts-container, - .color-container, - .shadow-container, - .radius-container, - .presets-container { - margin: 1em 1em 0; - } - - .tab-header { - display: flex; - justify-content: space-between; - align-items: baseline; - width: 100%; - min-height: 30px; - - .btn { - min-width: 1px; - flex: 0 auto; - padding: 0 1em; - } - - p { - flex: 1; - margin: 0; - margin-right: .5em; - } - - margin-bottom: 1em; - } - - .shadow-selector { - .override { - flex: 1; - margin-left: .5em; - } - .select-container { - margin-top: -4px; - margin-bottom: -3px; - } - } - - .save-load, - .save-load-options { - display: flex; - justify-content: center; - align-items: baseline; - flex-wrap: wrap; - - .presets, - .import-export { - margin-bottom: .5em; - } - - .import-export { - display: flex; - } - - .override { - margin-left: .5em; - } - } - - .save-load-options { - flex-wrap: wrap; - margin-top: .5em; - justify-content: center; - .keep-option { - margin: 0 .5em .5em; - min-width: 25%; - } - } - - .preview-container { - border-top: 1px dashed; - border-bottom: 1px dashed; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - margin: 1em -1em 0; - padding: 1em; - background: var(--body-background-image); - background-size: cover; - background-position: 50% 50%; - - .dummy { - .post { - font-family: var(--postFont); - display: flex; - - .content { - flex: 1; - - h4 { - margin-bottom: .25em; - } - - .icons { - margin-top: .5em; - display: flex; - - i { - margin-right: 1em; - } - } - } - } - - .after-post { - margin-top: 1em; - display: flex; - align-items: center; - } - - .avatar, .avatar-alt{ - background: linear-gradient(135deg, #b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%); - color: black; - font-family: sans-serif; - text-align: center; - margin-right: 1em; - } - - .avatar-alt { - flex: 0 auto; - margin-left: 28px; - font-size: 12px; - min-width: 20px; - min-height: 20px; - line-height: 20px; - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); - } - - .avatar { - flex: 0 auto; - width: 48px; - height: 48px; - font-size: 14px; - line-height: 48px; - } - - .actions { - display: flex; - align-items: baseline; - - .checkbox { - display: inline-flex; - align-items: baseline; - margin-right: 1em; - flex: 1; - } - } - - .separator { - margin: 1em; - border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - } - - .panel-heading { - .badge, .alert, .btn, .faint { - margin-left: 1em; - white-space: nowrap; - } - .faint { - text-overflow: ellipsis; - min-width: 2em; - overflow-x: hidden; - } - .flex-spacer { - flex: 1; - } - } - .btn { - margin-left: 0; - padding: 0 1em; - min-width: 3em; - min-height: 30px; - } - } - } - - .apply-container { - justify-content: center; - } - - .radius-item, - .color-item { - min-width: 20em; - margin: 5px 6px 0 0; - display:flex; - flex-direction: column; - flex: 1 1 0; - - &.wide { - min-width: 60% - } - - &:not(.wide):nth-child(2n+1) { - margin-right: 7px; - - } - - .color, .opacity { - display:flex; - align-items: baseline; - } - } - - .radius-item { - flex-basis: auto; - } - - .theme-radius-rn, - .theme-color-cl { - border: 0; - box-shadow: none; - background: transparent; - color: var(--faint, $fallback--faint); - align-self: stretch; - } - - .theme-color-cl, - .theme-radius-in, - .theme-color-in { - margin-left: 4px; - } - - .theme-radius-in { - min-width: 1em; - } - - .theme-radius-in { - max-width: 7em; - flex: 1; - } - - .theme-radius-lb{ - max-width: 50em; - } - - .theme-preview-content { - padding: 20px; - } - - .btn { - margin-left: .25em; - margin-right: .25em; - } -} diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue @@ -1,956 +0,0 @@ -<template> - <div class="style-switcher"> - <div class="presets-container"> - <div class="save-load"> - <div - v-if="themeWarning" - class="theme-warning" - > - <div class="alert warning"> - {{ themeWarningHelp }} - </div> - <div class="buttons"> - <template v-if="themeWarning.type === 'snapshot_source_mismatch'"> - <button - class="btn" - @click="forceLoad" - > - {{ $t('settings.style.switcher.use_source') }} - </button> - <button - class="btn" - @click="forceSnapshot" - > - {{ $t('settings.style.switcher.use_snapshot') }} - </button> - </template> - <template v-else-if="themeWarning.noActionsPossible"> - <button - class="btn" - @click="dismissWarning" - > - {{ $t('general.dismiss') }} - </button> - </template> - <template v-else> - <button - class="btn" - @click="forceLoad" - > - {{ $t('settings.style.switcher.load_theme') }} - </button> - <button - class="btn" - @click="dismissWarning" - > - {{ $t('settings.style.switcher.keep_as_is') }} - </button> - </template> - </div> - </div> - <ExportImport - :export-object="exportedTheme" - :export-label="$t(&quot;settings.export_theme&quot;)" - :import-label="$t(&quot;settings.import_theme&quot;)" - :import-failed-text="$t(&quot;settings.invalid_theme_imported&quot;)" - :on-import="onImport" - :validator="importValidator" - > - <template slot="before"> - <div class="presets"> - {{ $t('settings.presets') }} - <label - for="preset-switcher" - class="select" - > - <select - id="preset-switcher" - v-model="selected" - class="preset-switcher" - > - <option - v-for="style in availableStyles" - :key="style.name" - :value="style" - :style="{ - backgroundColor: style[1] || (style.theme || style.source).colors.bg, - color: style[3] || (style.theme || style.source).colors.text - }" - > - {{ style[0] || style.name }} - </option> - </select> - <i class="icon-down-open" /> - </label> - </div> - </template> - </ExportImport> - </div> - <div class="save-load-options"> - <span class="keep-option"> - <Checkbox v-model="keepColor"> - {{ $t('settings.style.switcher.keep_color') }} - </Checkbox> - </span> - <span class="keep-option"> - <Checkbox v-model="keepShadows"> - {{ $t('settings.style.switcher.keep_shadows') }} - </Checkbox> - </span> - <span class="keep-option"> - <Checkbox v-model="keepOpacity"> - {{ $t('settings.style.switcher.keep_opacity') }} - </Checkbox> - </span> - <span class="keep-option"> - <Checkbox v-model="keepRoundness"> - {{ $t('settings.style.switcher.keep_roundness') }} - </Checkbox> - </span> - <span class="keep-option"> - <Checkbox v-model="keepFonts"> - {{ $t('settings.style.switcher.keep_fonts') }} - </Checkbox> - </span> - <p>{{ $t('settings.style.switcher.save_load_hint') }}</p> - </div> - </div> - - <preview :style="previewRules" /> - - <keep-alive> - <tab-switcher key="style-tweak"> - <div - :label="$t('settings.style.common_colors._tab_label')" - class="color-container" - > - <div class="tab-header"> - <p>{{ $t('settings.theme_help') }}</p> - <button - class="btn" - @click="clearOpacity" - > - {{ $t('settings.style.switcher.clear_opacity') }} - </button> - <button - class="btn" - @click="clearV1" - > - {{ $t('settings.style.switcher.clear_all') }} - </button> - </div> - <p>{{ $t('settings.theme_help_v2_1') }}</p> - <h4>{{ $t('settings.style.common_colors.main') }}</h4> - <div class="color-item"> - <ColorInput - v-model="bgColorLocal" - name="bgColor" - :label="$t('settings.background')" - /> - <OpacityInput - v-model="bgOpacityLocal" - name="bgOpacity" - :fallback="previewTheme.opacity.bg" - /> - <ColorInput - v-model="textColorLocal" - name="textColor" - :label="$t('settings.text')" - /> - <ContrastRatio :contrast="previewContrast.bgText" /> - <ColorInput - v-model="accentColorLocal" - name="accentColor" - :fallback="previewTheme.colors.link" - :label="$t('settings.accent')" - :show-optional-tickbox="typeof linkColorLocal !== 'undefined'" - /> - <ColorInput - v-model="linkColorLocal" - name="linkColor" - :fallback="previewTheme.colors.accent" - :label="$t('settings.links')" - :show-optional-tickbox="typeof accentColorLocal !== 'undefined'" - /> - <ContrastRatio :contrast="previewContrast.bgLink" /> - </div> - <div class="color-item"> - <ColorInput - v-model="fgColorLocal" - name="fgColor" - :label="$t('settings.foreground')" - /> - <ColorInput - v-model="fgTextColorLocal" - name="fgTextColor" - :label="$t('settings.text')" - :fallback="previewTheme.colors.fgText" - /> - <ColorInput - v-model="fgLinkColorLocal" - name="fgLinkColor" - :label="$t('settings.links')" - :fallback="previewTheme.colors.fgLink" - /> - <p>{{ $t('settings.style.common_colors.foreground_hint') }}</p> - </div> - <h4>{{ $t('settings.style.common_colors.rgbo') }}</h4> - <div class="color-item"> - <ColorInput - v-model="cRedColorLocal" - name="cRedColor" - :label="$t('settings.cRed')" - /> - <ContrastRatio :contrast="previewContrast.bgCRed" /> - <ColorInput - v-model="cBlueColorLocal" - name="cBlueColor" - :label="$t('settings.cBlue')" - /> - <ContrastRatio :contrast="previewContrast.bgCBlue" /> - </div> - <div class="color-item"> - <ColorInput - v-model="cGreenColorLocal" - name="cGreenColor" - :label="$t('settings.cGreen')" - /> - <ContrastRatio :contrast="previewContrast.bgCGreen" /> - <ColorInput - v-model="cOrangeColorLocal" - name="cOrangeColor" - :label="$t('settings.cOrange')" - /> - <ContrastRatio :contrast="previewContrast.bgCOrange" /> - </div> - <p>{{ $t('settings.theme_help_v2_2') }}</p> - </div> - - <div - :label="$t('settings.style.advanced_colors._tab_label')" - class="color-container" - > - <div class="tab-header"> - <p>{{ $t('settings.theme_help') }}</p> - <button - class="btn" - @click="clearOpacity" - > - {{ $t('settings.style.switcher.clear_opacity') }} - </button> - <button - class="btn" - @click="clearV1" - > - {{ $t('settings.style.switcher.clear_all') }} - </button> - </div> - <div class="color-item"> - <h4>{{ $t('settings.style.advanced_colors.post') }}</h4> - <ColorInput - v-model="postLinkColorLocal" - name="postLinkColor" - :fallback="previewTheme.colors.accent" - :label="$t('settings.links')" - /> - <ContrastRatio :contrast="previewContrast.postLink" /> - <h4>{{ $t('settings.style.advanced_colors.alert') }}</h4> - <ColorInput - v-model="alertErrorColorLocal" - name="alertError" - :label="$t('settings.style.advanced_colors.alert_error')" - :fallback="previewTheme.colors.alertError" - /> - <ColorInput - v-model="alertErrorTextColorLocal" - name="alertErrorText" - :label="$t('settings.text')" - :fallback="previewTheme.colors.alertErrorText" - /> - <ContrastRatio - :contrast="previewContrast.alertErrorText" - large="true" - /> - <ColorInput - v-model="alertWarningColorLocal" - name="alertWarning" - :label="$t('settings.style.advanced_colors.alert_warning')" - :fallback="previewTheme.colors.alertWarning" - /> - <ColorInput - v-model="alertWarningTextColorLocal" - name="alertWarningText" - :label="$t('settings.text')" - :fallback="previewTheme.colors.alertWarningText" - /> - <ContrastRatio - :contrast="previewContrast.alertWarningText" - large="true" - /> - <ColorInput - v-model="alertNeutralColorLocal" - name="alertNeutral" - :label="$t('settings.style.advanced_colors.alert_neutral')" - :fallback="previewTheme.colors.alertNeutral" - /> - <ColorInput - v-model="alertNeutralTextColorLocal" - name="alertNeutralText" - :label="$t('settings.text')" - :fallback="previewTheme.colors.alertNeutralText" - /> - <ContrastRatio - :contrast="previewContrast.alertNeutralText" - large="true" - /> - <OpacityInput - v-model="alertOpacityLocal" - name="alertOpacity" - :fallback="previewTheme.opacity.alert" - /> - </div> - <div class="color-item"> - <h4>{{ $t('settings.style.advanced_colors.badge') }}</h4> - <ColorInput - v-model="badgeNotificationColorLocal" - name="badgeNotification" - :label="$t('settings.style.advanced_colors.badge_notification')" - :fallback="previewTheme.colors.badgeNotification" - /> - <ColorInput - v-model="badgeNotificationTextColorLocal" - name="badgeNotificationText" - :label="$t('settings.text')" - :fallback="previewTheme.colors.badgeNotificationText" - /> - <ContrastRatio - :contrast="previewContrast.badgeNotificationText" - large="true" - /> - </div> - <div class="color-item"> - <h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4> - <ColorInput - v-model="panelColorLocal" - name="panelColor" - :fallback="previewTheme.colors.panel" - :label="$t('settings.background')" - /> - <OpacityInput - v-model="panelOpacityLocal" - name="panelOpacity" - :fallback="previewTheme.opacity.panel" - :disabled="panelColorLocal === 'transparent'" - /> - <ColorInput - v-model="panelTextColorLocal" - name="panelTextColor" - :fallback="previewTheme.colors.panelText" - :label="$t('settings.text')" - /> - <ContrastRatio - :contrast="previewContrast.panelText" - large="true" - /> - <ColorInput - v-model="panelLinkColorLocal" - name="panelLinkColor" - :fallback="previewTheme.colors.panelLink" - :label="$t('settings.links')" - /> - <ContrastRatio - :contrast="previewContrast.panelLink" - large="true" - /> - </div> - <div class="color-item"> - <h4>{{ $t('settings.style.advanced_colors.top_bar') }}</h4> - <ColorInput - v-model="topBarColorLocal" - name="topBarColor" - :fallback="previewTheme.colors.topBar" - :label="$t('settings.background')" - /> - <ColorInput - v-model="topBarTextColorLocal" - name="topBarTextColor" - :fallback="previewTheme.colors.topBarText" - :label="$t('settings.text')" - /> - <ContrastRatio :contrast="previewContrast.topBarText" /> - <ColorInput - v-model="topBarLinkColorLocal" - name="topBarLinkColor" - :fallback="previewTheme.colors.topBarLink" - :label="$t('settings.links')" - /> - <ContrastRatio :contrast="previewContrast.topBarLink" /> - </div> - <div class="color-item"> - <h4>{{ $t('settings.style.advanced_colors.inputs') }}</h4> - <ColorInput - v-model="inputColorLocal" - name="inputColor" - :fallback="previewTheme.colors.input" - :label="$t('settings.background')" - /> - <OpacityInput - v-model="inputOpacityLocal" - name="inputOpacity" - :fallback="previewTheme.opacity.input" - :disabled="inputColorLocal === 'transparent'" - /> - <ColorInput - v-model="inputTextColorLocal" - name="inputTextColor" - :fallback="previewTheme.colors.inputText" - :label="$t('settings.text')" - /> - <ContrastRatio :contrast="previewContrast.inputText" /> - </div> - <div class="color-item"> - <h4>{{ $t('settings.style.advanced_colors.buttons') }}</h4> - <ColorInput - v-model="btnColorLocal" - name="btnColor" - :fallback="previewTheme.colors.btn" - :label="$t('settings.background')" - /> - <OpacityInput - v-model="btnOpacityLocal" - name="btnOpacity" - :fallback="previewTheme.opacity.btn" - :disabled="btnColorLocal === 'transparent'" - /> - <ColorInput - v-model="btnTextColorLocal" - name="btnTextColor" - :fallback="previewTheme.colors.btnText" - :label="$t('settings.text')" - /> - <ContrastRatio :contrast="previewContrast.btnText" /> - <ColorInput - v-model="btnPanelTextColorLocal" - name="btnPanelTextColor" - :fallback="previewTheme.colors.btnPanelText" - :label="$t('settings.style.advanced_colors.panel_header')" - /> - <ContrastRatio :contrast="previewContrast.btnPanelText" /> - <ColorInput - v-model="btnTopBarTextColorLocal" - name="btnTopBarTextColor" - :fallback="previewTheme.colors.btnTopBarText" - :label="$t('settings.style.advanced_colors.top_bar')" - /> - <ContrastRatio :contrast="previewContrast.btnTopBarText" /> - <h5>{{ $t('settings.style.advanced_colors.pressed') }}</h5> - <ColorInput - v-model="btnPressedColorLocal" - name="btnPressedColor" - :fallback="previewTheme.colors.btnPressed" - :label="$t('settings.background')" - /> - <ColorInput - v-model="btnPressedTextColorLocal" - name="btnPressedTextColor" - :fallback="previewTheme.colors.btnPressedText" - :label="$t('settings.text')" - /> - <ContrastRatio :contrast="previewContrast.btnPressedText" /> - <ColorInput - v-model="btnPressedPanelTextColorLocal" - name="btnPressedPanelTextColor" - :fallback="previewTheme.colors.btnPressedPanelText" - :label="$t('settings.style.advanced_colors.panel_header')" - /> - <ContrastRatio :contrast="previewContrast.btnPressedPanelText" /> - <ColorInput - v-model="btnPressedTopBarTextColorLocal" - name="btnPressedTopBarTextColor" - :fallback="previewTheme.colors.btnPressedTopBarText" - :label="$t('settings.style.advanced_colors.top_bar')" - /> - <ContrastRatio :contrast="previewContrast.btnPressedTopBarText" /> - <h5>{{ $t('settings.style.advanced_colors.disabled') }}</h5> - <ColorInput - v-model="btnDisabledColorLocal" - name="btnDisabledColor" - :fallback="previewTheme.colors.btnDisabled" - :label="$t('settings.background')" - /> - <ColorInput - v-model="btnDisabledTextColorLocal" - name="btnDisabledTextColor" - :fallback="previewTheme.colors.btnDisabledText" - :label="$t('settings.text')" - /> - <ColorInput - v-model="btnDisabledPanelTextColorLocal" - name="btnDisabledPanelTextColor" - :fallback="previewTheme.colors.btnDisabledPanelText" - :label="$t('settings.style.advanced_colors.panel_header')" - /> - <ColorInput - v-model="btnDisabledTopBarTextColorLocal" - name="btnDisabledTopBarTextColor" - :fallback="previewTheme.colors.btnDisabledTopBarText" - :label="$t('settings.style.advanced_colors.top_bar')" - /> - <h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5> - <ColorInput - v-model="btnToggledColorLocal" - name="btnToggledColor" - :fallback="previewTheme.colors.btnToggled" - :label="$t('settings.background')" - /> - <ColorInput - v-model="btnToggledTextColorLocal" - name="btnToggledTextColor" - :fallback="previewTheme.colors.btnToggledText" - :label="$t('settings.text')" - /> - <ContrastRatio :contrast="previewContrast.btnToggledText" /> - <ColorInput - v-model="btnToggledPanelTextColorLocal" - name="btnToggledPanelTextColor" - :fallback="previewTheme.colors.btnToggledPanelText" - :label="$t('settings.style.advanced_colors.panel_header')" - /> - <ContrastRatio :contrast="previewContrast.btnToggledPanelText" /> - <ColorInput - v-model="btnToggledTopBarTextColorLocal" - name="btnToggledTopBarTextColor" - :fallback="previewTheme.colors.btnToggledTopBarText" - :label="$t('settings.style.advanced_colors.top_bar')" - /> - <ContrastRatio :contrast="previewContrast.btnToggledTopBarText" /> - </div> - <div class="color-item"> - <h4>{{ $t('settings.style.advanced_colors.tabs') }}</h4> - <ColorInput - v-model="tabColorLocal" - name="tabColor" - :fallback="previewTheme.colors.tab" - :label="$t('settings.background')" - /> - <ColorInput - v-model="tabTextColorLocal" - name="tabTextColor" - :fallback="previewTheme.colors.tabText" - :label="$t('settings.text')" - /> - <ContrastRatio :contrast="previewContrast.tabText" /> - <ColorInput - v-model="tabActiveTextColorLocal" - name="tabActiveTextColor" - :fallback="previewTheme.colors.tabActiveText" - :label="$t('settings.text')" - /> - <ContrastRatio :contrast="previewContrast.tabActiveText" /> - </div> - <div class="color-item"> - <h4>{{ $t('settings.style.advanced_colors.borders') }}</h4> - <ColorInput - v-model="borderColorLocal" - name="borderColor" - :fallback="previewTheme.colors.border" - :label="$t('settings.style.common.color')" - /> - <OpacityInput - v-model="borderOpacityLocal" - name="borderOpacity" - :fallback="previewTheme.opacity.border" - :disabled="borderColorLocal === 'transparent'" - /> - </div> - <div class="color-item"> - <h4>{{ $t('settings.style.advanced_colors.faint_text') }}</h4> - <ColorInput - v-model="faintColorLocal" - name="faintColor" - :fallback="previewTheme.colors.faint" - :label="$t('settings.text')" - /> - <ColorInput - v-model="faintLinkColorLocal" - name="faintLinkColor" - :fallback="previewTheme.colors.faintLink" - :label="$t('settings.links')" - /> - <ColorInput - v-model="panelFaintColorLocal" - name="panelFaintColor" - :fallback="previewTheme.colors.panelFaint" - :label="$t('settings.style.advanced_colors.panel_header')" - /> - <OpacityInput - v-model="faintOpacityLocal" - name="faintOpacity" - :fallback="previewTheme.opacity.faint" - /> - </div> - <div class="color-item"> - <h4>{{ $t('settings.style.advanced_colors.underlay') }}</h4> - <ColorInput - v-model="underlayColorLocal" - name="underlay" - :label="$t('settings.style.advanced_colors.underlay')" - :fallback="previewTheme.colors.underlay" - /> - <OpacityInput - v-model="underlayOpacityLocal" - name="underlayOpacity" - :fallback="previewTheme.opacity.underlay" - :disabled="underlayOpacityLocal === 'transparent'" - /> - </div> - <div class="color-item"> - <h4>{{ $t('settings.style.advanced_colors.poll') }}</h4> - <ColorInput - v-model="pollColorLocal" - name="poll" - :label="$t('settings.background')" - :fallback="previewTheme.colors.poll" - /> - <ColorInput - v-model="pollTextColorLocal" - name="pollText" - :label="$t('settings.text')" - :fallback="previewTheme.colors.pollText" - /> - </div> - <div class="color-item"> - <h4>{{ $t('settings.style.advanced_colors.icons') }}</h4> - <ColorInput - v-model="iconColorLocal" - name="icon" - :label="$t('settings.style.advanced_colors.icons')" - :fallback="previewTheme.colors.icon" - /> - </div> - <div class="color-item"> - <h4>{{ $t('settings.style.advanced_colors.highlight') }}</h4> - <ColorInput - v-model="highlightColorLocal" - name="highlight" - :label="$t('settings.background')" - :fallback="previewTheme.colors.highlight" - /> - <ColorInput - v-model="highlightTextColorLocal" - name="highlightText" - :label="$t('settings.text')" - :fallback="previewTheme.colors.highlightText" - /> - <ContrastRatio :contrast="previewContrast.highlightText" /> - <ColorInput - v-model="highlightLinkColorLocal" - name="highlightLink" - :label="$t('settings.links')" - :fallback="previewTheme.colors.highlightLink" - /> - <ContrastRatio :contrast="previewContrast.highlightLink" /> - </div> - <div class="color-item"> - <h4>{{ $t('settings.style.advanced_colors.popover') }}</h4> - <ColorInput - v-model="popoverColorLocal" - name="popover" - :label="$t('settings.background')" - :fallback="previewTheme.colors.popover" - /> - <OpacityInput - v-model="popoverOpacityLocal" - name="popoverOpacity" - :fallback="previewTheme.opacity.popover" - :disabled="popoverOpacityLocal === 'transparent'" - /> - <ColorInput - v-model="popoverTextColorLocal" - name="popoverText" - :label="$t('settings.text')" - :fallback="previewTheme.colors.popoverText" - /> - <ContrastRatio :contrast="previewContrast.popoverText" /> - <ColorInput - v-model="popoverLinkColorLocal" - name="popoverLink" - :label="$t('settings.links')" - :fallback="previewTheme.colors.popoverLink" - /> - <ContrastRatio :contrast="previewContrast.popoverLink" /> - </div> - <div class="color-item"> - <h4>{{ $t('settings.style.advanced_colors.selectedPost') }}</h4> - <ColorInput - v-model="selectedPostColorLocal" - name="selectedPost" - :label="$t('settings.background')" - :fallback="previewTheme.colors.selectedPost" - /> - <ColorInput - v-model="selectedPostTextColorLocal" - name="selectedPostText" - :label="$t('settings.text')" - :fallback="previewTheme.colors.selectedPostText" - /> - <ContrastRatio :contrast="previewContrast.selectedPostText" /> - <ColorInput - v-model="selectedPostLinkColorLocal" - name="selectedPostLink" - :label="$t('settings.links')" - :fallback="previewTheme.colors.selectedPostLink" - /> - <ContrastRatio :contrast="previewContrast.selectedPostLink" /> - </div> - <div class="color-item"> - <h4>{{ $t('settings.style.advanced_colors.selectedMenu') }}</h4> - <ColorInput - v-model="selectedMenuColorLocal" - name="selectedMenu" - :label="$t('settings.background')" - :fallback="previewTheme.colors.selectedMenu" - /> - <ColorInput - v-model="selectedMenuTextColorLocal" - name="selectedMenuText" - :label="$t('settings.text')" - :fallback="previewTheme.colors.selectedMenuText" - /> - <ContrastRatio :contrast="previewContrast.selectedMenuText" /> - <ColorInput - v-model="selectedMenuLinkColorLocal" - name="selectedMenuLink" - :label="$t('settings.links')" - :fallback="previewTheme.colors.selectedMenuLink" - /> - <ContrastRatio :contrast="previewContrast.selectedMenuLink" /> - </div> - </div> - - <div - :label="$t('settings.style.radii._tab_label')" - class="radius-container" - > - <div class="tab-header"> - <p>{{ $t('settings.radii_help') }}</p> - <button - class="btn" - @click="clearRoundness" - > - {{ $t('settings.style.switcher.clear_all') }} - </button> - </div> - <RangeInput - v-model="btnRadiusLocal" - name="btnRadius" - :label="$t('settings.btnRadius')" - :fallback="previewTheme.radii.btn" - max="16" - hard-min="0" - /> - <RangeInput - v-model="inputRadiusLocal" - name="inputRadius" - :label="$t('settings.inputRadius')" - :fallback="previewTheme.radii.input" - max="9" - hard-min="0" - /> - <RangeInput - v-model="checkboxRadiusLocal" - name="checkboxRadius" - :label="$t('settings.checkboxRadius')" - :fallback="previewTheme.radii.checkbox" - max="16" - hard-min="0" - /> - <RangeInput - v-model="panelRadiusLocal" - name="panelRadius" - :label="$t('settings.panelRadius')" - :fallback="previewTheme.radii.panel" - max="50" - hard-min="0" - /> - <RangeInput - v-model="avatarRadiusLocal" - name="avatarRadius" - :label="$t('settings.avatarRadius')" - :fallback="previewTheme.radii.avatar" - max="28" - hard-min="0" - /> - <RangeInput - v-model="avatarAltRadiusLocal" - name="avatarAltRadius" - :label="$t('settings.avatarAltRadius')" - :fallback="previewTheme.radii.avatarAlt" - max="28" - hard-min="0" - /> - <RangeInput - v-model="attachmentRadiusLocal" - name="attachmentRadius" - :label="$t('settings.attachmentRadius')" - :fallback="previewTheme.radii.attachment" - max="50" - hard-min="0" - /> - <RangeInput - v-model="tooltipRadiusLocal" - name="tooltipRadius" - :label="$t('settings.tooltipRadius')" - :fallback="previewTheme.radii.tooltip" - max="50" - hard-min="0" - /> - </div> - - <div - :label="$t('settings.style.shadows._tab_label')" - class="shadow-container" - > - <div class="tab-header shadow-selector"> - <div class="select-container"> - {{ $t('settings.style.shadows.component') }} - <label - for="shadow-switcher" - class="select" - > - <select - id="shadow-switcher" - v-model="shadowSelected" - class="shadow-switcher" - > - <option - v-for="shadow in shadowsAvailable" - :key="shadow" - :value="shadow" - > - {{ $t('settings.style.shadows.components.' + shadow) }} - </option> - </select> - <i class="icon-down-open" /> - </label> - </div> - <div class="override"> - <label - for="override" - class="label" - > - {{ $t('settings.style.shadows.override') }} - </label> - <input - id="override" - v-model="currentShadowOverriden" - name="override" - class="input-override" - type="checkbox" - > - <label - class="checkbox-label" - for="override" - /> - </div> - <button - class="btn" - @click="clearShadows" - > - {{ $t('settings.style.switcher.clear_all') }} - </button> - </div> - <ShadowControl - v-model="currentShadow" - :ready="!!currentShadowFallback" - :fallback="currentShadowFallback" - /> - <div v-if="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'"> - <i18n - path="settings.style.shadows.filter_hint.always_drop_shadow" - tag="p" - > - <code>filter: drop-shadow()</code> - </i18n> - <p>{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}</p> - <i18n - path="settings.style.shadows.filter_hint.drop_shadow_syntax" - tag="p" - > - <code>drop-shadow</code> - <code>spread-radius</code> - <code>inset</code> - </i18n> - <i18n - path="settings.style.shadows.filter_hint.inset_classic" - tag="p" - > - <code>box-shadow</code> - </i18n> - <p>{{ $t('settings.style.shadows.filter_hint.spread_zero') }}</p> - </div> - </div> - - <div - :label="$t('settings.style.fonts._tab_label')" - class="fonts-container" - > - <div class="tab-header"> - <p>{{ $t('settings.style.fonts.help') }}</p> - <button - class="btn" - @click="clearFonts" - > - {{ $t('settings.style.switcher.clear_all') }} - </button> - </div> - <FontControl - v-model="fontsLocal.interface" - name="ui" - :label="$t('settings.style.fonts.components.interface')" - :fallback="previewTheme.fonts.interface" - no-inherit="1" - /> - <FontControl - v-model="fontsLocal.input" - name="input" - :label="$t('settings.style.fonts.components.input')" - :fallback="previewTheme.fonts.input" - /> - <FontControl - v-model="fontsLocal.post" - name="post" - :label="$t('settings.style.fonts.components.post')" - :fallback="previewTheme.fonts.post" - /> - <FontControl - v-model="fontsLocal.postCode" - name="postCode" - :label="$t('settings.style.fonts.components.postCode')" - :fallback="previewTheme.fonts.postCode" - /> - </div> - </tab-switcher> - </keep-alive> - - <div class="apply-container"> - <button - class="btn submit" - :disabled="!themeValid" - @click="setCustomTheme" - > - {{ $t('general.apply') }} - </button> - <button - class="btn" - @click="clearAll" - > - {{ $t('settings.style.switcher.reset') }} - </button> - </div> - </div> -</template> - -<script src="./style_switcher.js"></script> - -<style src="./style_switcher.scss" lang="scss"></style> diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js @@ -1,4 +1,5 @@ import Vue from 'vue' +import { mapState } from 'vuex' import './tab_switcher.scss' @@ -24,6 +25,11 @@ export default Vue.component('tab-switcher', { required: false, type: Boolean, default: false + }, + sideTabBar: { + required: false, + type: Boolean, + default: false } }, data () { @@ -39,7 +45,13 @@ export default Vue.component('tab-switcher', { } else { return this.active } - } + }, + settingsModalVisible () { + return this.settingsModalState === 'visible' + }, + ...mapState({ + settingsModalState: state => state.interface.settingsModalState + }) }, beforeUpdate () { const currentSlot = this.$slots.default[this.active] @@ -55,6 +67,9 @@ export default Vue.component('tab-switcher', { this.onSwitch.call(null, this.$slots.default[index].key) } this.active = index + if (this.scrollableTabs) { + this.$refs.contents.scrollTop = 0 + } } } }, @@ -64,7 +79,6 @@ export default Vue.component('tab-switcher', { if (!slot.tag) return const classesTab = ['tab'] const classesWrapper = ['tab-wrapper'] - if (this.activeIndex === index) { classesTab.push('active') classesWrapper.push('active') @@ -87,8 +101,14 @@ export default Vue.component('tab-switcher', { <button disabled={slot.data.attrs.disabled} onClick={this.activateTab(index)} - class={classesTab.join(' ')}> - {slot.data.attrs.label}</button> + class={classesTab.join(' ')} + type="button" + > + {!slot.data.attrs.icon ? '' : (<i class={'tab-icon icon-' + slot.data.attrs.icon}/>)} + <span class="text"> + {slot.data.attrs.label} + </span> + </button> </div> ) }) @@ -96,20 +116,32 @@ export default Vue.component('tab-switcher', { const contents = this.$slots.default.map((slot, index) => { if (!slot.tag) return const active = this.activeIndex === index - if (this.renderOnlyFocused) { - return active - ? <div class="active">{slot}</div> - : <div class="hidden"></div> + const classes = [ active ? 'active' : 'hidden' ] + if (slot.data.attrs.fullHeight) { + classes.push('full-height') } - return <div class={active ? 'active' : 'hidden' }>{slot}</div> + const renderSlot = (!this.renderOnlyFocused || active) + ? slot + : '' + + return ( + <div class={classes}> + { + this.sideTabBar + ? <h1 class="mobile-label">{slot.data.attrs.label}</h1> + : '' + } + {renderSlot} + </div> + ) }) return ( - <div class="tab-switcher"> + <div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}> <div class="tabs"> {tabs} </div> - <div class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}> + <div ref="contents" class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')} v-body-scroll-lock={this.settingsModalVisible}> {contents} </div> </div> diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss @@ -2,7 +2,144 @@ .tab-switcher { display: flex; - flex-direction: column; + + .tab-icon { + font-size: 2em; + display: block; + } + + &.top-tabs { + flex-direction: column; + + > .tabs { + width: 100%; + overflow-y: hidden; + overflow-x: auto; + padding-top: 5px; + flex-direction: row; + + &::after, &::before { + content: ''; + flex: 1 1 auto; + border-bottom: 1px solid; + border-bottom-color: $fallback--border; + border-bottom-color: var(--border, $fallback--border); + } + .tab-wrapper { + height: 28px; + + &:not(.active)::after { + left: 0; + right: 0; + bottom: 0; + border-bottom: 1px solid; + border-bottom-color: $fallback--border; + border-bottom-color: var(--border, $fallback--border); + } + } + .tab { + width: 100%; + min-width: 1px; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + padding-bottom: 99px; + margin-bottom: 6px - 99px; + } + } + .contents.scrollable-tabs { + flex-basis: 0; + } + } + + &.side-tabs { + flex-direction: row; + + @media all and (max-width: 800px) { + overflow-x: auto; + } + + > .contents { + flex: 1 1 auto; + } + + > .tabs { + flex: 0 0 auto; + overflow-y: auto; + overflow-x: hidden; + flex-direction: column; + + &::after, &::before { + flex-shrink: 0; + flex-basis: .5em; + content: ''; + border-right: 1px solid; + border-right-color: $fallback--border; + border-right-color: var(--border, $fallback--border); + } + + &::after { + flex-grow: 1; + } + + &::before { + flex-grow: 0; + } + + .tab-wrapper { + min-width: 10em; + display: flex; + flex-direction: column; + + @media all and (max-width: 800px) { + min-width: 1em; + } + + &:not(.active)::after { + top: 0; + right: 0; + bottom: 0; + border-right: 1px solid; + border-right-color: $fallback--border; + border-right-color: var(--border, $fallback--border); + } + + &::before { + flex: 0 0 6px; + content: ''; + border-right: 1px solid; + border-right-color: $fallback--border; + border-right-color: var(--border, $fallback--border); + } + + &:last-child .tab { + margin-bottom: 0; + } + } + + .tab { + flex: 1; + box-sizing: content-box; + min-width: 10em; + min-width: 1px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + padding-left: 1em; + padding-right: calc(1em + 200px); + margin-right: -200px; + margin-left: 1em; + + @media all and (max-width: 800px) { + padding-left: .25em; + padding-right: calc(.25em + 200px); + margin-right: calc(.25em - 200px); + margin-left: .25em; + .text { + display: none + } + } + } + } + } .contents { flex: 1 0 auto; @@ -11,88 +148,89 @@ .hidden { display: none; } + .full-height:not(.hidden) { + height: 100%; + display: flex; + flex-direction: column; + > *:not(.mobile-label) { + flex: 1; + } + } &.scrollable-tabs { - flex-basis: 0; overflow-y: auto; } } - .tabs { - display: flex; + + .tab { position: relative; - width: 100%; - overflow-y: hidden; - overflow-x: auto; - padding-top: 5px; - box-sizing: border-box; + white-space: nowrap; + padding: 6px 1em; + background-color: $fallback--fg; + background-color: var(--tab, $fallback--fg); - &::after, &::before { - display: block; - content: ''; - flex: 1 1 auto; - border-bottom: 1px solid; - border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); + &, &:active .tab-icon { + color: $fallback--text; + color: var(--tabText, $fallback--text); } - .tab-wrapper { - height: 28px; - position: relative; - display: flex; - flex: 0 0 auto; + &:not(.active) { + z-index: 4; - .tab { - width: 100%; - min-width: 1px; - position: relative; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - padding: 6px 1em; - padding-bottom: 99px; - margin-bottom: 6px - 99px; - white-space: nowrap; + &:hover { + z-index: 6; + } + } - color: $fallback--text; - color: var(--tabText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--tab, $fallback--fg); + &.active { + background: transparent; + z-index: 5; + color: $fallback--text; + color: var(--tabActiveText, $fallback--text); + } - &:not(.active) { - z-index: 4; + img { + max-height: 26px; + vertical-align: top; + margin-top: -5px; + } + } - &:hover { - z-index: 6; - } - } + .tabs { + display: flex; + position: relative; + box-sizing: border-box; - &.active { - background: transparent; - z-index: 5; - color: $fallback--text; - color: var(--tabActiveText, $fallback--text); - } + &::after, &::before { + display: block; + flex: 1 1 auto; + } + } - img { - max-height: 26px; - vertical-align: top; - margin-top: -5px; - } - } + .tab-wrapper { + position: relative; + display: flex; + flex: 0 0 auto; - &:not(.active) { - &::after { - content: ''; - position: absolute; - left: 0; - right: 0; - bottom: 0; - z-index: 7; - border-bottom: 1px solid; - border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); - } + &:not(.active) { + &::after { + content: ''; + position: absolute; + z-index: 7; } } + } + .mobile-label { + padding-left: .3em; + padding-bottom: .25em; + margin-top: .5em; + margin-left: .2em; + margin-bottom: .25em; + border-bottom: 1px solid var(--border, $fallback--border); + + @media all and (min-width: 800px) { + display: none; + } } } diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js @@ -1,6 +1,7 @@ import Status from '../status/status.vue' import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js' import Conversation from '../conversation/conversation.vue' +import TimelineMenu from '../timeline_menu/timeline_menu.vue' import { throttle, keyBy } from 'lodash' export const getExcludedStatusIdsByPinning = (statuses, pinnedStatusIds) => { @@ -35,6 +36,11 @@ const Timeline = { bottomedOut: false } }, + components: { + Status, + Conversation, + TimelineMenu + }, computed: { timelineError () { return this.$store.state.statuses.error @@ -45,11 +51,15 @@ const Timeline = { newStatusCount () { return this.timeline.newStatusCount }, - newStatusCountStr () { + showLoadButton () { + if (this.timelineError || this.errorData) return false + return this.timeline.newStatusCount > 0 || this.timeline.flushMarker !== 0 + }, + loadButtonString () { if (this.timeline.flushMarker !== 0) { - return '' + return this.$t('timeline.reload') } else { - return ` (${this.newStatusCount})` + return `${this.$t('timeline.show_new')} (${this.newStatusCount})` } }, classes () { @@ -70,10 +80,6 @@ const Timeline = { return keyBy(this.pinnedStatusIds) } }, - components: { - Status, - Conversation - }, created () { const store = this.$store const credentials = store.state.users.currentUser.credentials @@ -112,8 +118,6 @@ const Timeline = { if (e.key === '.') this.showNewStatuses() }, showNewStatuses () { - if (this.newStatusCount === 0) return - if (this.timeline.flushMarker !== 0) { this.$store.commit('clearTimeline', { timeline: this.timelineName, excludeUserId: true }) this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 }) @@ -135,7 +139,7 @@ const Timeline = { showImmediately: true, userId: this.userId, tag: this.tag - }).then(statuses => { + }).then(({ statuses }) => { store.commit('setLoading', { timeline: this.timelineName, value: false }) if (statuses && statuses.length === 0) { this.bottomedOut = true @@ -146,7 +150,6 @@ const Timeline = { const bodyBRect = document.body.getBoundingClientRect() const height = Math.max(bodyBRect.height, -(bodyBRect.y)) if (this.timeline.loading === false && - this.$store.getters.mergedConfig.autoLoad && this.$el.offsetHeight > 0 && (window.innerHeight + window.pageYOffset) >= (height - 750)) { this.fetchOlderStatuses() diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue @@ -1,9 +1,7 @@ <template> - <div :class="classes.root"> + <div :class="[classes.root, 'timeline']"> <div :class="classes.header"> - <div class="title"> - {{ title }} - </div> + <TimelineMenu v-if="!embedded" /> <div v-if="timelineError" class="loadmore-error alert error" @@ -19,14 +17,14 @@ {{ errorData.statusText }} </div> <button - v-if="timeline.newStatusCount > 0 && !timelineError && !errorData" + v-else-if="showLoadButton" class="loadmore-button" @click.prevent="showNewStatuses" > - {{ $t('timeline.show_new') }}{{ newStatusCountStr }} + {{ loadButtonString }} </button> <div - v-if="!timeline.newStatusCount > 0 && !timelineError && !errorData" + v-else class="loadmore-text faint" @click.prevent > @@ -106,4 +104,16 @@ opacity: 1; } } + +.timeline-heading { + max-width: 100%; + flex-wrap: nowrap; + .loadmore-button { + flex-shrink: 0; + } + .loadmore-text { + flex-shrink: 0; + line-height: 1em; + } +} </style> diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js @@ -0,0 +1,63 @@ +import Popover from '../popover/popover.vue' +import { mapState } from 'vuex' + +// Route -> i18n key mapping, exported andnot in the computed +// because nav panel benefits from the same information. +export const timelineNames = () => { + return { + 'friends': 'nav.timeline', + 'bookmarks': 'nav.bookmarks', + 'dms': 'nav.dms', + 'public-timeline': 'nav.public_tl', + 'public-external-timeline': 'nav.twkn', + 'tag-timeline': 'tag' + } +} + +const TimelineMenu = { + components: { + Popover + }, + data () { + return { + isOpen: false + } + }, + created () { + if (this.currentUser && this.currentUser.locked) { + this.$store.dispatch('startFetchingFollowRequests') + } + if (timelineNames()[this.$route.name]) { + this.$store.dispatch('setLastTimeline', this.$route.name) + } + }, + methods: { + openMenu () { + // $nextTick is too fast, animation won't play back but + // instead starts in fully open position. Low values + // like 1-5 work on fast machines but not on mobile, 25 + // seems like a good compromise that plays without significant + // added lag. + setTimeout(() => { + this.isOpen = true + }, 25) + }, + timelineName () { + const route = this.$route.name + if (route === 'tag-timeline') { + return '#' + this.$route.params.tag + } + const i18nkey = timelineNames()[this.$route.name] + return i18nkey ? this.$t(i18nkey) : route + } + }, + computed: { + ...mapState({ + currentUser: state => state.users.currentUser, + privateMode: state => state.instance.private, + federating: state => state.instance.federating + }) + } +} + +export default TimelineMenu diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue @@ -0,0 +1,180 @@ +<template> + <Popover + trigger="click" + class="timeline-menu" + :class="{ 'open': isOpen }" + :margin="{ left: -15, right: -200 }" + :bound-to="{ x: 'container' }" + popover-class="timeline-menu-popover-wrap" + @show="openMenu" + @close="() => isOpen = false" + > + <div + slot="content" + class="timeline-menu-popover panel panel-default" + > + <ul> + <li v-if="currentUser"> + <router-link :to="{ name: 'friends' }"> + <i class="button-icon icon-home-2" />{{ $t("nav.timeline") }} + </router-link> + </li> + <li v-if="currentUser"> + <router-link :to="{ name: 'bookmarks'}"> + <i class="button-icon icon-bookmark" />{{ $t("nav.bookmarks") }} + </router-link> + </li> + <li v-if="currentUser"> + <router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }"> + <i class="button-icon icon-mail-alt" />{{ $t("nav.dms") }} + </router-link> + </li> + <li v-if="currentUser || !privateMode"> + <router-link :to="{ name: 'public-timeline' }"> + <i class="button-icon icon-users" />{{ $t("nav.public_tl") }} + </router-link> + </li> + <li v-if="federating && (currentUser || !privateMode)"> + <router-link :to="{ name: 'public-external-timeline' }"> + <i class="button-icon icon-globe" />{{ $t("nav.twkn") }} + </router-link> + </li> + </ul> + </div> + <div + slot="trigger" + class="title timeline-menu-title" + > + <span>{{ timelineName() }}</span> + <i class="icon-down-open" /> + </div> + </Popover> +</template> + +<script src="./timeline_menu.js" ></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.timeline-menu { + flex-shrink: 1; + margin-right: auto; + min-width: 0; + width: 24rem; + .timeline-menu-popover-wrap { + overflow: hidden; + // Match panel heading padding to line up menu with bottom of heading + margin-top: 0.6rem; + padding: 0 15px 15px 15px; + } + .timeline-menu-popover { + width: 24rem; + max-width: 100vw; + margin: 0; + font-size: 1rem; + border-top-right-radius: 0; + border-top-left-radius: 0; + transform: translateY(-100%); + transition: transform 100ms; + } + .panel::after { + border-top-right-radius: 0; + border-top-left-radius: 0; + } + &.open .timeline-menu-popover { + transform: translateY(0); + } + + .timeline-menu-title { + margin: 0; + cursor: pointer; + display: flex; + user-select: none; + width: 100%; + + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + i { + margin-left: 0.6em; + flex-shrink: 0; + font-size: 1rem; + transition: transform 100ms; + } + } + + &.open .timeline-menu-title i { + color: $fallback--text; + color: var(--panelText, $fallback--text); + transform: rotate(180deg); + } + + .panel { + box-shadow: var(--popoverShadow); + } + + ul { + list-style: none; + margin: 0; + padding: 0; + } + + li { + border-bottom: 1px solid; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + padding: 0; + + &:last-child a { + border-bottom-right-radius: $fallback--panelRadius; + border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius); + border-bottom-left-radius: $fallback--panelRadius; + border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius); + } + + &:last-child { + border: none; + } + + i { + margin: 0 0.5em; + } + } + + a { + display: block; + padding: 0.6em 0; + + &:hover { + background-color: $fallback--lightBg; + background-color: var(--selectedMenu, $fallback--lightBg); + color: $fallback--link; + color: var(--selectedMenuText, $fallback--link); + --faint: var(--selectedMenuFaintText, $fallback--faint); + --faintLink: var(--selectedMenuFaintLink, $fallback--faint); + --lightText: var(--selectedMenuLightText, $fallback--lightText); + --icon: var(--selectedMenuIcon, $fallback--icon); + } + + &.router-link-active { + font-weight: bolder; + background-color: $fallback--lightBg; + background-color: var(--selectedMenu, $fallback--lightBg); + color: $fallback--text; + color: var(--selectedMenuText, $fallback--text); + --faint: var(--selectedMenuFaintText, $fallback--faint); + --faintLink: var(--selectedMenuFaintLink, $fallback--faint); + --lightText: var(--selectedMenuLightText, $fallback--lightText); + --icon: var(--selectedMenuIcon, $fallback--icon); + + &:hover { + text-decoration: underline; + } + } + } +} + +</style> diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js @@ -8,26 +8,20 @@ const UserAvatar = { ], data () { return { - showPlaceholder: false + showPlaceholder: false, + defaultAvatar: `${this.$store.state.instance.server + this.$store.state.instance.defaultAvatar}` } }, components: { StillImage }, - computed: { - imgSrc () { - return this.showPlaceholder ? '/images/avi.png' : this.user.profile_image_url_original - } - }, methods: { + imgSrc (src) { + return (!src || this.showPlaceholder) ? this.defaultAvatar : src + }, imageLoadError () { this.showPlaceholder = true } - }, - watch: { - src () { - this.showPlaceholder = false - } } } diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue @@ -1,9 +1,9 @@ <template> <StillImage - class="avatar" + class="Avatar" :alt="user.screen_name" :title="user.screen_name" - :src="imgSrc" + :src="imgSrc(user.profile_image_url_original)" :class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }" :image-load-error="imageLoadError" /> @@ -13,7 +13,9 @@ <style lang="scss"> @import '../../_variables.scss'; -.avatar.still-image { +.Avatar { + --still-image-label-visibility: hidden; + width: 48px; height: 48px; box-shadow: var(--avatarStatusShadow); diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue @@ -50,15 +50,6 @@ > {{ user.name }} </div> - <router-link - v-if="!isOtherUser" - :to="{ name: 'user-settings' }" - > - <i - class="button-icon icon-wrench usersettings" - :title="$t('tool_tip.user_settings')" - /> - </router-link> <a v-if="isOtherUser && !user.is_local" :href="user.statusnet_profile_url" @@ -75,14 +66,25 @@ <div class="bottom-line"> <router-link class="user-screen-name" + :title="user.screen_name" :to="userProfileLink(user)" > @{{ user.screen_name }} </router-link> - <span - v-if="!hideBio && !!visibleRole" - class="alert staff" - >{{ visibleRole }}</span> + <template v-if="!hideBio"> + <span + v-if="!!visibleRole" + class="alert user-role" + > + {{ visibleRole }} + </span> + <span + v-if="user.bot" + class="alert user-role" + > + bot + </span> + </template> <span v-if="user.locked"><i class="icon icon-lock" /></span> <span v-if="!mergedConfig.hideUserStats && !hideBio" @@ -118,7 +120,7 @@ type="color" > <label - for="style-switcher" + for="theme_tab" class="userHighlightSel select" > <select @@ -352,7 +354,7 @@ align-items: flex-start; max-height: 56px; - .avatar { + .Avatar { flex: 1 0 100%; width: 56px; height: 56px; @@ -362,13 +364,9 @@ } } - &:hover .animated.avatar { - canvas { - display: none; - } - img { - visibility: visible; - } + &:hover .Avatar { + --still-image-img: visible; + --still-image-canvas: hidden; } &-avatar-link { @@ -467,7 +465,7 @@ color: var(--text, $fallback--text); } - .staff { + .user-role { flex: none; text-transform: capitalize; color: $fallback--text; diff --git a/src/components/user_list_popover/user_list_popover.js b/src/components/user_list_popover/user_list_popover.js @@ -0,0 +1,18 @@ + +const UserListPopover = { + name: 'UserListPopover', + props: [ + 'users' + ], + components: { + Popover: () => import('../popover/popover.vue'), + UserAvatar: () => import('../user_avatar/user_avatar.vue') + }, + computed: { + usersCapped () { + return this.users.slice(0, 16) + } + } +} + +export default UserListPopover diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue @@ -0,0 +1,71 @@ +<template> + <Popover + trigger="hover" + placement="top" + :offset="{ y: 5 }" + > + <template slot="trigger"> + <slot /> + </template> + <div + slot="content" + class="user-list-popover" + > + <div v-if="users.length"> + <div + v-for="(user) in usersCapped" + :key="user.id" + class="user-list-row" + > + <UserAvatar + :user="user" + class="avatar-small" + :compact="true" + /> + <div class="user-list-names"> + <!-- eslint-disable vue/no-v-html --> + <span v-html="user.name_html" /> + <!-- eslint-enable vue/no-v-html --> + <span class="user-list-screen-name">{{ user.screen_name }}</span> + </div> + </div> + </div> + <div v-else> + <i class="icon-spin4 animate-spin" /> + </div> + </div> + </Popover> +</template> + +<script src="./user_list_popover.js" ></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.user-list-popover { + padding: 0.5em; + + .user-list-row { + padding: 0.25em; + display: flex; + flex-direction: row; + + .user-list-names { + display: flex; + flex-direction: column; + margin-left: 0.5em; + min-width: 5em; + + img { + width: 1em; + height: 1em; + } + } + + .user-list-screen-name { + font-size: 9px; + } + } +} + +</style> diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue @@ -10,9 +10,7 @@ :hide-bio="true" rounded="top" /> - <div class="panel-footer"> - <PostStatusForm /> - </div> + <PostStatusForm /> </div> <auth-form v-else diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js @@ -3,6 +3,7 @@ import UserCard from '../user_card/user_card.vue' import FollowCard from '../follow_card/follow_card.vue' import Timeline from '../timeline/timeline.vue' import Conversation from '../conversation/conversation.vue' +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js' import List from '../list/list.vue' import withLoadMore from '../../hocs/with_load_more/with_load_more' @@ -123,6 +124,14 @@ const UserProfile = { onTabSwitch (tab) { this.tab = tab this.$router.replace({ query: { tab } }) + }, + linkClicked ({ target }) { + if (target.tagName === 'SPAN') { + target = target.parentNode + } + if (target.tagName === 'A') { + window.open(target.href, '_blank') + } } }, watch: { @@ -146,6 +155,7 @@ const UserProfile = { FollowerList, FriendList, FollowCard, + TabSwitcher, Conversation } } diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue @@ -11,6 +11,32 @@ :allow-zooming-avatar="true" rounded="top" /> + <div + v-if="user.fields_html && user.fields_html.length > 0" + class="user-profile-fields" + > + <dl + v-for="(field, index) in user.fields_html" + :key="index" + class="user-profile-field" + > + <dt + :title="user.fields_text[index].name" + class="user-profile-field-name" + @click.prevent="linkClicked" + > + {{ field.name }} + </dt> + <!-- eslint-disable vue/no-v-html --> + <dd + :title="user.fields_text[index].value" + class="user-profile-field-value" + @click.prevent="linkClicked" + v-html="field.value" + /> + <!-- eslint-enable vue/no-v-html --> + </dl> + </div> <tab-switcher :active-tab="tab" :render-only-focused="true" @@ -108,11 +134,60 @@ <script src="./user_profile.js"></script> <style lang="scss"> +@import '../../_variables.scss'; .user-profile { flex: 2; flex-basis: 500px; + .user-profile-fields { + margin: 0 0.5em; + img { + object-fit: contain; + vertical-align: middle; + max-width: 100%; + max-height: 400px; + + &.emoji { + width: 18px; + height: 18px; + } + } + + .user-profile-field { + display: flex; + margin: 0.25em auto; + max-width: 32em; + border: 1px solid var(--border, $fallback--border); + border-radius: $fallback--inputRadius; + border-radius: var(--inputRadius, $fallback--inputRadius); + + .user-profile-field-name { + flex: 0 1 30%; + font-weight: 500; + text-align: right; + color: var(--lightText); + min-width: 120px; + border-right: 1px solid var(--border, $fallback--border); + } + + .user-profile-field-value { + flex: 1 1 70%; + color: var(--text); + margin: 0 0 0 0.25em; + } + + .user-profile-field-name, .user-profile-field-value { + line-height: 18px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + padding: 0.5em 1.5em; + box-sizing: border-box; + } + } + } + .userlist-placeholder { display: flex; justify-content: center; diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue @@ -146,7 +146,8 @@ display: flex; justify-content: space-between; - > .status-el { + /* TODO cleanup this */ + > .Status { flex: 1; } diff --git a/src/components/user_settings/mfa.vue b/src/components/user_settings/mfa.vue @@ -1,173 +0,0 @@ -<template> - <div - v-if="readyInit && settings.available" - class="setting-item mfa-settings" - > - <div class="mfa-heading"> - <h2>{{ $t('settings.mfa.title') }}</h2> - </div> - - <div> - <div - v-if="!setupInProgress" - class="setting-item" - > - <!-- Enabled methods --> - <h3>{{ $t('settings.mfa.authentication_methods') }}</h3> - <totp-item - :settings="settings" - @deactivate="fetchSettings" - @activate="activateOTP" - /> - <br> - - <div v-if="settings.enabled"> - <!-- backup codes block--> - <recovery-codes - v-if="!confirmNewBackupCodes" - :backup-codes="backupCodes" - /> - <button - v-if="!confirmNewBackupCodes" - class="btn btn-default" - @click="getBackupCodes" - > - {{ $t('settings.mfa.generate_new_recovery_codes') }} - </button> - - <div v-if="confirmNewBackupCodes"> - <confirm - :disabled="backupCodes.inProgress" - @confirm="confirmBackupCodes" - @cancel="cancelBackupCodes" - > - <p class="warning"> - {{ $t('settings.mfa.warning_of_generate_new_codes') }} - </p> - </confirm> - </div> - </div> - </div> - - <div v-if="setupInProgress"> - <!-- setup block--> - - <h3>{{ $t('settings.mfa.setup_otp') }}</h3> - - <recovery-codes - v-if="!setupOTPInProgress" - :backup-codes="backupCodes" - /> - - <button - v-if="canSetupOTP" - class="btn btn-default" - @click="cancelSetup" - > - {{ $t('general.cancel') }} - </button> - - <button - v-if="canSetupOTP" - class="btn btn-default" - @click="setupOTP" - > - {{ $t('settings.mfa.setup_otp') }} - </button> - - <template v-if="setupOTPInProgress"> - <i v-if="prepareOTP">{{ $t('settings.mfa.wait_pre_setup_otp') }}</i> - - <div v-if="confirmOTP"> - <div class="setup-otp"> - <div class="qr-code"> - <h4>{{ $t('settings.mfa.scan.title') }}</h4> - <p>{{ $t('settings.mfa.scan.desc') }}</p> - <qrcode - :value="otpSettings.provisioning_uri" - :options="{ width: 200 }" - /> - <p> - {{ $t('settings.mfa.scan.secret_code') }}: - {{ otpSettings.key }} - </p> - </div> - - <div class="verify"> - <h4>{{ $t('general.verify') }}</h4> - <p>{{ $t('settings.mfa.verify.desc') }}</p> - <input - v-model="otpConfirmToken" - type="text" - > - - <p>{{ $t('settings.enter_current_password_to_confirm') }}:</p> - <input - v-model="currentPassword" - type="password" - > - <div class="confirm-otp-actions"> - <button - class="btn btn-default" - @click="doConfirmOTP" - > - {{ $t('settings.mfa.confirm_and_enable') }} - </button> - <button - class="btn btn-default" - @click="cancelSetup" - > - {{ $t('general.cancel') }} - </button> - </div> - <div - v-if="error" - class="alert error" - > - {{ error }} - </div> - </div> - </div> - </div> - </template> - </div> - </div> - </div> -</template> - -<script src="./mfa.js"></script> -<style lang="scss"> -@import '../../_variables.scss'; -.warning { - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); -} -.mfa-settings { - .mfa-heading, .method-item { - overflow: hidden; - display: flex; - flex-wrap: wrap; - justify-content: space-between; - align-items: baseline; - } - - .setup-otp { - display: flex; - justify-content: center; - flex-wrap: wrap; - .qr-code { - flex: 1; - padding-right: 10px; - } - .verify { flex: 1; } - .error { margin: 4px 0 0 0; } - .confirm-otp-actions { - button { - width: 15em; - margin-top: 5px; - } - - } - } -} -</style> diff --git a/src/components/user_settings/mfa_backup_codes.vue b/src/components/user_settings/mfa_backup_codes.vue @@ -1,33 +0,0 @@ -<template> - <div> - <h4 v-if="displayTitle"> - {{ $t('settings.mfa.recovery_codes') }} - </h4> - <i v-if="inProgress">{{ $t('settings.mfa.waiting_a_recovery_codes') }}</i> - <template v-if="ready"> - <p class="alert warning"> - {{ $t('settings.mfa.recovery_codes_warning') }} - </p> - <ul class="backup-codes"> - <li - v-for="code in backupCodes.codes" - :key="code" - > - {{ code }} - </li> - </ul> - </template> - </div> -</template> -<script src="./mfa_backup_codes.js"></script> -<style lang="scss"> -@import '../../_variables.scss'; - -.warning { - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); -} -.backup-codes { - font-family: var(--postCodeFont, monospace); -} -</style> diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js @@ -1,393 +0,0 @@ -import unescape from 'lodash/unescape' -import get from 'lodash/get' -import map from 'lodash/map' -import reject from 'lodash/reject' -import TabSwitcher from '../tab_switcher/tab_switcher.js' -import ImageCropper from '../image_cropper/image_cropper.vue' -import StyleSwitcher from '../style_switcher/style_switcher.vue' -import ScopeSelector from '../scope_selector/scope_selector.vue' -import fileSizeFormatService from '../../services/file_size_format/file_size_format.js' -import BlockCard from '../block_card/block_card.vue' -import MuteCard from '../mute_card/mute_card.vue' -import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue' -import SelectableList from '../selectable_list/selectable_list.vue' -import ProgressButton from '../progress_button/progress_button.vue' -import EmojiInput from '../emoji_input/emoji_input.vue' -import suggestor from '../emoji_input/suggestor.js' -import Autosuggest from '../autosuggest/autosuggest.vue' -import Importer from '../importer/importer.vue' -import Exporter from '../exporter/exporter.vue' -import withSubscription from '../../hocs/with_subscription/with_subscription' -import Checkbox from '../checkbox/checkbox.vue' -import Mfa from './mfa.vue' - -const BlockList = withSubscription({ - fetch: (props, $store) => $store.dispatch('fetchBlocks'), - select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []), - childPropName: 'items' -})(SelectableList) - -const MuteList = withSubscription({ - fetch: (props, $store) => $store.dispatch('fetchMutes'), - select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []), - childPropName: 'items' -})(SelectableList) - -const DomainMuteList = withSubscription({ - fetch: (props, $store) => $store.dispatch('fetchDomainMutes'), - select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []), - childPropName: 'items' -})(SelectableList) - -const UserSettings = { - data () { - return { - newEmail: '', - newName: this.$store.state.users.currentUser.name, - newBio: unescape(this.$store.state.users.currentUser.description), - newLocked: this.$store.state.users.currentUser.locked, - newNoRichText: this.$store.state.users.currentUser.no_rich_text, - newDefaultScope: this.$store.state.users.currentUser.default_scope, - hideFollows: this.$store.state.users.currentUser.hide_follows, - hideFollowers: this.$store.state.users.currentUser.hide_followers, - hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count, - hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count, - showRole: this.$store.state.users.currentUser.show_role, - role: this.$store.state.users.currentUser.role, - discoverable: this.$store.state.users.currentUser.discoverable, - allowFollowingMove: this.$store.state.users.currentUser.allow_following_move, - pickAvatarBtnVisible: true, - bannerUploading: false, - backgroundUploading: false, - banner: null, - bannerPreview: null, - background: null, - backgroundPreview: null, - bannerUploadError: null, - backgroundUploadError: null, - changeEmailError: false, - changeEmailPassword: '', - changedEmail: false, - deletingAccount: false, - deleteAccountConfirmPasswordInput: '', - deleteAccountError: false, - changePasswordInputs: [ '', '', '' ], - changedPassword: false, - changePasswordError: false, - activeTab: 'profile', - notificationSettings: this.$store.state.users.currentUser.notification_settings, - newDomainToMute: '' - } - }, - created () { - this.$store.dispatch('fetchTokens') - }, - components: { - StyleSwitcher, - ScopeSelector, - TabSwitcher, - ImageCropper, - BlockList, - MuteList, - DomainMuteList, - EmojiInput, - Autosuggest, - BlockCard, - MuteCard, - DomainMuteCard, - ProgressButton, - Importer, - Exporter, - Mfa, - Checkbox - }, - computed: { - user () { - return this.$store.state.users.currentUser - }, - emojiUserSuggestor () { - return suggestor({ - emoji: [ - ...this.$store.state.instance.emoji, - ...this.$store.state.instance.customEmoji - ], - users: this.$store.state.users.users, - updateUsersList: (input) => this.$store.dispatch('searchUsers', input) - }) - }, - emojiSuggestor () { - return suggestor({ emoji: [ - ...this.$store.state.instance.emoji, - ...this.$store.state.instance.customEmoji - ] }) - }, - pleromaBackend () { - return this.$store.state.instance.pleromaBackend - }, - minimalScopesMode () { - return this.$store.state.instance.minimalScopesMode - }, - vis () { - return { - public: { selected: this.newDefaultScope === 'public' }, - unlisted: { selected: this.newDefaultScope === 'unlisted' }, - private: { selected: this.newDefaultScope === 'private' }, - direct: { selected: this.newDefaultScope === 'direct' } - } - }, - currentSaveStateNotice () { - return this.$store.state.interface.settings.currentSaveStateNotice - }, - oauthTokens () { - return this.$store.state.oauthTokens.tokens.map(oauthToken => { - return { - id: oauthToken.id, - appName: oauthToken.app_name, - validUntil: new Date(oauthToken.valid_until).toLocaleDateString() - } - }) - } - }, - methods: { - updateProfile () { - this.$store.state.api.backendInteractor - .updateProfile({ - params: { - note: this.newBio, - locked: this.newLocked, - // Backend notation. - /* eslint-disable camelcase */ - display_name: this.newName, - default_scope: this.newDefaultScope, - no_rich_text: this.newNoRichText, - hide_follows: this.hideFollows, - hide_followers: this.hideFollowers, - discoverable: this.discoverable, - allow_following_move: this.allowFollowingMove, - hide_follows_count: this.hideFollowsCount, - hide_followers_count: this.hideFollowersCount, - show_role: this.showRole - /* eslint-enable camelcase */ - } }).then((user) => { - this.$store.commit('addNewUsers', [user]) - this.$store.commit('setCurrentUser', user) - }) - }, - updateNotificationSettings () { - this.$store.state.api.backendInteractor - .updateNotificationSettings({ settings: this.notificationSettings }) - }, - changeVis (visibility) { - this.newDefaultScope = visibility - }, - uploadFile (slot, e) { - const file = e.target.files[0] - if (!file) { return } - if (file.size > this.$store.state.instance[slot + 'limit']) { - const filesize = fileSizeFormatService.fileSizeFormat(file.size) - const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit']) - this[slot + 'UploadError'] = this.$t('upload.error.base') + ' ' + this.$t('upload.error.file_too_big', { filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit }) - return - } - // eslint-disable-next-line no-undef - const reader = new FileReader() - reader.onload = ({ target }) => { - const img = target.result - this[slot + 'Preview'] = img - this[slot] = file - } - reader.readAsDataURL(file) - }, - submitAvatar (cropper, file) { - const that = this - return new Promise((resolve, reject) => { - function updateAvatar (avatar) { - that.$store.state.api.backendInteractor.updateAvatar({ avatar }) - .then((user) => { - that.$store.commit('addNewUsers', [user]) - that.$store.commit('setCurrentUser', user) - resolve() - }) - .catch((err) => { - reject(new Error(that.$t('upload.error.base') + ' ' + err.message)) - }) - } - - if (cropper) { - cropper.getCroppedCanvas().toBlob(updateAvatar, file.type) - } else { - updateAvatar(file) - } - }) - }, - clearUploadError (slot) { - this[slot + 'UploadError'] = null - }, - submitBanner () { - if (!this.bannerPreview) { return } - - this.bannerUploading = true - this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner }) - .then((user) => { - this.$store.commit('addNewUsers', [user]) - this.$store.commit('setCurrentUser', user) - this.bannerPreview = null - }) - .catch((err) => { - this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message - }) - .then(() => { this.bannerUploading = false }) - }, - submitBg () { - if (!this.backgroundPreview) { return } - let background = this.background - this.backgroundUploading = true - this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => { - if (!data.error) { - this.$store.commit('addNewUsers', [data]) - this.$store.commit('setCurrentUser', data) - this.backgroundPreview = null - } else { - this.backgroundUploadError = this.$t('upload.error.base') + data.error - } - this.backgroundUploading = false - }) - }, - importFollows (file) { - return this.$store.state.api.backendInteractor.importFollows({ file }) - .then((status) => { - if (!status) { - throw new Error('failed') - } - }) - }, - importBlocks (file) { - return this.$store.state.api.backendInteractor.importBlocks({ file }) - .then((status) => { - if (!status) { - throw new Error('failed') - } - }) - }, - generateExportableUsersContent (users) { - // Get addresses - return users.map((user) => { - // check is it's a local user - if (user && user.is_local) { - // append the instance address - // eslint-disable-next-line no-undef - return user.screen_name + '@' + location.hostname - } - return user.screen_name - }).join('\n') - }, - getFollowsContent () { - return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id }) - .then(this.generateExportableUsersContent) - }, - getBlocksContent () { - return this.$store.state.api.backendInteractor.fetchBlocks() - .then(this.generateExportableUsersContent) - }, - confirmDelete () { - this.deletingAccount = true - }, - deleteAccount () { - this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput }) - .then((res) => { - if (res.status === 'success') { - this.$store.dispatch('logout') - this.$router.push({ name: 'root' }) - } else { - this.deleteAccountError = res.error - } - }) - }, - changePassword () { - const params = { - password: this.changePasswordInputs[0], - newPassword: this.changePasswordInputs[1], - newPasswordConfirmation: this.changePasswordInputs[2] - } - this.$store.state.api.backendInteractor.changePassword(params) - .then((res) => { - if (res.status === 'success') { - this.changedPassword = true - this.changePasswordError = false - this.logout() - } else { - this.changedPassword = false - this.changePasswordError = res.error - } - }) - }, - changeEmail () { - const params = { - email: this.newEmail, - password: this.changeEmailPassword - } - this.$store.state.api.backendInteractor.changeEmail(params) - .then((res) => { - if (res.status === 'success') { - this.changedEmail = true - this.changeEmailError = false - } else { - this.changedEmail = false - this.changeEmailError = res.error - } - }) - }, - activateTab (tabName) { - this.activeTab = tabName - }, - logout () { - this.$store.dispatch('logout') - this.$router.replace('/') - }, - revokeToken (id) { - if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) { - this.$store.dispatch('revokeToken', id) - } - }, - filterUnblockedUsers (userIds) { - return reject(userIds, (userId) => { - const relationship = this.$store.getters.relationship(this.userId) - return relationship.blocking || userId === this.$store.state.users.currentUser.id - }) - }, - filterUnMutedUsers (userIds) { - return reject(userIds, (userId) => { - const relationship = this.$store.getters.relationship(this.userId) - return relationship.muting || userId === this.$store.state.users.currentUser.id - }) - }, - queryUserIds (query) { - return this.$store.dispatch('searchUsers', query) - .then((users) => map(users, 'id')) - }, - blockUsers (ids) { - return this.$store.dispatch('blockUsers', ids) - }, - unblockUsers (ids) { - return this.$store.dispatch('unblockUsers', ids) - }, - muteUsers (ids) { - return this.$store.dispatch('muteUsers', ids) - }, - unmuteUsers (ids) { - return this.$store.dispatch('unmuteUsers', ids) - }, - unmuteDomains (domains) { - return this.$store.dispatch('unmuteDomains', domains) - }, - muteDomain () { - return this.$store.dispatch('muteDomain', this.newDomainToMute) - .then(() => { this.newDomainToMute = '' }) - }, - identity (value) { - return value - } - } -} - -export default UserSettings diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue @@ -1,728 +0,0 @@ -<template> - <div class="settings panel panel-default"> - <div class="panel-heading"> - <div class="title"> - {{ $t('settings.user_settings') }} - </div> - <transition name="fade"> - <template v-if="currentSaveStateNotice"> - <div - v-if="currentSaveStateNotice.error" - class="alert error" - @click.prevent - > - {{ $t('settings.saving_err') }} - </div> - - <div - v-if="!currentSaveStateNotice.error" - class="alert transparent" - @click.prevent - > - {{ $t('settings.saving_ok') }} - </div> - </template> - </transition> - </div> - <div class="panel-body profile-edit"> - <tab-switcher> - <div :label="$t('settings.profile_tab')"> - <div class="setting-item"> - <h2>{{ $t('settings.name_bio') }}</h2> - <p>{{ $t('settings.name') }}</p> - <EmojiInput - v-model="newName" - enable-emoji-picker - :suggest="emojiSuggestor" - > - <input - id="username" - v-model="newName" - classname="name-changer" - > - </EmojiInput> - <p>{{ $t('settings.bio') }}</p> - <EmojiInput - v-model="newBio" - enable-emoji-picker - :suggest="emojiUserSuggestor" - > - <textarea - v-model="newBio" - classname="bio" - /> - </EmojiInput> - <p> - <Checkbox v-model="newLocked"> - {{ $t('settings.lock_account_description') }} - </Checkbox> - </p> - <div> - <label for="default-vis">{{ $t('settings.default_vis') }}</label> - <div - id="default-vis" - class="visibility-tray" - > - <scope-selector - :show-all="true" - :user-default="newDefaultScope" - :initial-scope="newDefaultScope" - :on-scope-change="changeVis" - /> - </div> - </div> - <p> - <Checkbox v-model="newNoRichText"> - {{ $t('settings.no_rich_text_description') }} - </Checkbox> - </p> - <p> - <Checkbox v-model="hideFollows"> - {{ $t('settings.hide_follows_description') }} - </Checkbox> - </p> - <p class="setting-subitem"> - <Checkbox - v-model="hideFollowsCount" - :disabled="!hideFollows" - > - {{ $t('settings.hide_follows_count_description') }} - </Checkbox> - </p> - <p> - <Checkbox v-model="hideFollowers"> - {{ $t('settings.hide_followers_description') }} - </Checkbox> - </p> - <p class="setting-subitem"> - <Checkbox - v-model="hideFollowersCount" - :disabled="!hideFollowers" - > - {{ $t('settings.hide_followers_count_description') }} - </Checkbox> - </p> - <p> - <Checkbox v-model="allowFollowingMove"> - {{ $t('settings.allow_following_move') }} - </Checkbox> - </p> - <p v-if="role === 'admin' || role === 'moderator'"> - <Checkbox v-model="showRole"> - <template v-if="role === 'admin'"> - {{ $t('settings.show_admin_badge') }} - </template> - <template v-if="role === 'moderator'"> - {{ $t('settings.show_moderator_badge') }} - </template> - </Checkbox> - </p> - <p> - <Checkbox v-model="discoverable"> - {{ $t('settings.discoverable') }} - </Checkbox> - </p> - <button - :disabled="newName && newName.length === 0" - class="btn btn-default" - @click="updateProfile" - > - {{ $t('general.submit') }} - </button> - </div> - <div class="setting-item"> - <h2>{{ $t('settings.avatar') }}</h2> - <p class="visibility-notice"> - {{ $t('settings.avatar_size_instruction') }} - </p> - <p>{{ $t('settings.current_avatar') }}</p> - <img - :src="user.profile_image_url_original" - class="current-avatar" - > - <p>{{ $t('settings.set_new_avatar') }}</p> - <button - v-show="pickAvatarBtnVisible" - id="pick-avatar" - class="btn" - type="button" - > - {{ $t('settings.upload_a_photo') }} - </button> - <image-cropper - trigger="#pick-avatar" - :submit-handler="submitAvatar" - @open="pickAvatarBtnVisible=false" - @close="pickAvatarBtnVisible=true" - /> - </div> - <div class="setting-item"> - <h2>{{ $t('settings.profile_banner') }}</h2> - <p>{{ $t('settings.current_profile_banner') }}</p> - <img - :src="user.cover_photo" - class="banner" - > - <p>{{ $t('settings.set_new_profile_banner') }}</p> - <img - v-if="bannerPreview" - class="banner" - :src="bannerPreview" - > - <div> - <input - type="file" - @change="uploadFile('banner', $event)" - > - </div> - <i - v-if="bannerUploading" - class=" icon-spin4 animate-spin uploading" - /> - <button - v-else-if="bannerPreview" - class="btn btn-default" - @click="submitBanner" - > - {{ $t('general.submit') }} - </button> - <div - v-if="bannerUploadError" - class="alert error" - > - Error: {{ bannerUploadError }} - <i - class="button-icon icon-cancel" - @click="clearUploadError('banner')" - /> - </div> - </div> - <div class="setting-item"> - <h2>{{ $t('settings.profile_background') }}</h2> - <p>{{ $t('settings.set_new_profile_background') }}</p> - <img - v-if="backgroundPreview" - class="bg" - :src="backgroundPreview" - > - <div> - <input - type="file" - @change="uploadFile('background', $event)" - > - </div> - <i - v-if="backgroundUploading" - class=" icon-spin4 animate-spin uploading" - /> - <button - v-else-if="backgroundPreview" - class="btn btn-default" - @click="submitBg" - > - {{ $t('general.submit') }} - </button> - <div - v-if="backgroundUploadError" - class="alert error" - > - Error: {{ backgroundUploadError }} - <i - class="button-icon icon-cancel" - @click="clearUploadError('background')" - /> - </div> - </div> - </div> - - <div :label="$t('settings.security_tab')"> - <div class="setting-item"> - <h2>{{ $t('settings.change_email') }}</h2> - <div> - <p>{{ $t('settings.new_email') }}</p> - <input - v-model="newEmail" - type="email" - autocomplete="email" - > - </div> - <div> - <p>{{ $t('settings.current_password') }}</p> - <input - v-model="changeEmailPassword" - type="password" - autocomplete="current-password" - > - </div> - <button - class="btn btn-default" - @click="changeEmail" - > - {{ $t('general.submit') }} - </button> - <p v-if="changedEmail"> - {{ $t('settings.changed_email') }} - </p> - <template v-if="changeEmailError !== false"> - <p>{{ $t('settings.change_email_error') }}</p> - <p>{{ changeEmailError }}</p> - </template> - </div> - - <div class="setting-item"> - <h2>{{ $t('settings.change_password') }}</h2> - <div> - <p>{{ $t('settings.current_password') }}</p> - <input - v-model="changePasswordInputs[0]" - type="password" - > - </div> - <div> - <p>{{ $t('settings.new_password') }}</p> - <input - v-model="changePasswordInputs[1]" - type="password" - > - </div> - <div> - <p>{{ $t('settings.confirm_new_password') }}</p> - <input - v-model="changePasswordInputs[2]" - type="password" - > - </div> - <button - class="btn btn-default" - @click="changePassword" - > - {{ $t('general.submit') }} - </button> - <p v-if="changedPassword"> - {{ $t('settings.changed_password') }} - </p> - <p v-else-if="changePasswordError !== false"> - {{ $t('settings.change_password_error') }} - </p> - <p v-if="changePasswordError"> - {{ changePasswordError }} - </p> - </div> - - <div class="setting-item"> - <h2>{{ $t('settings.oauth_tokens') }}</h2> - <table class="oauth-tokens"> - <thead> - <tr> - <th>{{ $t('settings.app_name') }}</th> - <th>{{ $t('settings.valid_until') }}</th> - <th /> - </tr> - </thead> - <tbody> - <tr - v-for="oauthToken in oauthTokens" - :key="oauthToken.id" - > - <td>{{ oauthToken.appName }}</td> - <td>{{ oauthToken.validUntil }}</td> - <td class="actions"> - <button - class="btn btn-default" - @click="revokeToken(oauthToken.id)" - > - {{ $t('settings.revoke_token') }} - </button> - </td> - </tr> - </tbody> - </table> - </div> - <mfa /> - <div class="setting-item"> - <h2>{{ $t('settings.delete_account') }}</h2> - <p v-if="!deletingAccount"> - {{ $t('settings.delete_account_description') }} - </p> - <div v-if="deletingAccount"> - <p>{{ $t('settings.delete_account_instructions') }}</p> - <p>{{ $t('login.password') }}</p> - <input - v-model="deleteAccountConfirmPasswordInput" - type="password" - > - <button - class="btn btn-default" - @click="deleteAccount" - > - {{ $t('settings.delete_account') }} - </button> - </div> - <p v-if="deleteAccountError !== false"> - {{ $t('settings.delete_account_error') }} - </p> - <p v-if="deleteAccountError"> - {{ deleteAccountError }} - </p> - <button - v-if="!deletingAccount" - class="btn btn-default" - @click="confirmDelete" - > - {{ $t('general.submit') }} - </button> - </div> - </div> - - <div - v-if="pleromaBackend" - :label="$t('settings.notifications')" - > - <div class="setting-item"> - <h2>{{ $t('settings.notification_setting_filters') }}</h2> - <div class="select-multiple"> - <span class="label">{{ $t('settings.notification_setting') }}</span> - <ul class="option-list"> - <li> - <Checkbox v-model="notificationSettings.follows"> - {{ $t('settings.notification_setting_follows') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="notificationSettings.followers"> - {{ $t('settings.notification_setting_followers') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="notificationSettings.non_follows"> - {{ $t('settings.notification_setting_non_follows') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="notificationSettings.non_followers"> - {{ $t('settings.notification_setting_non_followers') }} - </Checkbox> - </li> - </ul> - </div> - </div> - - <div class="setting-item"> - <h2>{{ $t('settings.notification_setting_privacy') }}</h2> - <p> - <Checkbox v-model="notificationSettings.privacy_option"> - {{ $t('settings.notification_setting_privacy_option') }} - </Checkbox> - </p> - </div> - <div class="setting-item"> - <p>{{ $t('settings.notification_mutes') }}</p> - <p>{{ $t('settings.notification_blocks') }}</p> - <button - class="btn btn-default" - @click="updateNotificationSettings" - > - {{ $t('general.submit') }} - </button> - </div> - </div> - - <div - v-if="pleromaBackend" - :label="$t('settings.data_import_export_tab')" - > - <div class="setting-item"> - <h2>{{ $t('settings.follow_import') }}</h2> - <p>{{ $t('settings.import_followers_from_a_csv_file') }}</p> - <Importer - :submit-handler="importFollows" - :success-message="$t('settings.follows_imported')" - :error-message="$t('settings.follow_import_error')" - /> - </div> - <div class="setting-item"> - <h2>{{ $t('settings.follow_export') }}</h2> - <Exporter - :get-content="getFollowsContent" - filename="friends.csv" - :export-button-label="$t('settings.follow_export_button')" - /> - </div> - <div class="setting-item"> - <h2>{{ $t('settings.block_import') }}</h2> - <p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p> - <Importer - :submit-handler="importBlocks" - :success-message="$t('settings.blocks_imported')" - :error-message="$t('settings.block_import_error')" - /> - </div> - <div class="setting-item"> - <h2>{{ $t('settings.block_export') }}</h2> - <Exporter - :get-content="getBlocksContent" - filename="blocks.csv" - :export-button-label="$t('settings.block_export_button')" - /> - </div> - </div> - - <div :label="$t('settings.blocks_tab')"> - <div class="profile-edit-usersearch-wrapper"> - <Autosuggest - :filter="filterUnblockedUsers" - :query="queryUserIds" - :placeholder="$t('settings.search_user_to_block')" - > - <BlockCard - slot-scope="row" - :user-id="row.item" - /> - </Autosuggest> - </div> - <BlockList - :refresh="true" - :get-key="identity" - > - <template - slot="header" - slot-scope="{selected}" - > - <div class="profile-edit-bulk-actions"> - <ProgressButton - v-if="selected.length > 0" - class="btn btn-default" - :click="() => blockUsers(selected)" - > - {{ $t('user_card.block') }} - <template slot="progress"> - {{ $t('user_card.block_progress') }} - </template> - </ProgressButton> - <ProgressButton - v-if="selected.length > 0" - class="btn btn-default" - :click="() => unblockUsers(selected)" - > - {{ $t('user_card.unblock') }} - <template slot="progress"> - {{ $t('user_card.unblock_progress') }} - </template> - </ProgressButton> - </div> - </template> - <template - slot="item" - slot-scope="{item}" - > - <BlockCard :user-id="item" /> - </template> - <template slot="empty"> - {{ $t('settings.no_blocks') }} - </template> - </BlockList> - </div> - - <div :label="$t('settings.mutes_tab')"> - <tab-switcher> - <div label="Users"> - <div class="profile-edit-usersearch-wrapper"> - <Autosuggest - :filter="filterUnMutedUsers" - :query="queryUserIds" - :placeholder="$t('settings.search_user_to_mute')" - > - <MuteCard - slot-scope="row" - :user-id="row.item" - /> - </Autosuggest> - </div> - <MuteList - :refresh="true" - :get-key="identity" - > - <template - slot="header" - slot-scope="{selected}" - > - <div class="profile-edit-bulk-actions"> - <ProgressButton - v-if="selected.length > 0" - class="btn btn-default" - :click="() => muteUsers(selected)" - > - {{ $t('user_card.mute') }} - <template slot="progress"> - {{ $t('user_card.mute_progress') }} - </template> - </ProgressButton> - <ProgressButton - v-if="selected.length > 0" - class="btn btn-default" - :click="() => unmuteUsers(selected)" - > - {{ $t('user_card.unmute') }} - <template slot="progress"> - {{ $t('user_card.unmute_progress') }} - </template> - </ProgressButton> - </div> - </template> - <template - slot="item" - slot-scope="{item}" - > - <MuteCard :user-id="item" /> - </template> - <template slot="empty"> - {{ $t('settings.no_mutes') }} - </template> - </MuteList> - </div> - - <div :label="$t('settings.domain_mutes')"> - <div class="profile-edit-domain-mute-form"> - <input - v-model="newDomainToMute" - :placeholder="$t('settings.type_domains_to_mute')" - type="text" - @keyup.enter="muteDomain" - > - <ProgressButton - class="btn btn-default" - :click="muteDomain" - > - {{ $t('domain_mute_card.mute') }} - <template slot="progress"> - {{ $t('domain_mute_card.mute_progress') }} - </template> - </ProgressButton> - </div> - <DomainMuteList - :refresh="true" - :get-key="identity" - > - <template - slot="header" - slot-scope="{selected}" - > - <div class="profile-edit-bulk-actions"> - <ProgressButton - v-if="selected.length > 0" - class="btn btn-default" - :click="() => unmuteDomains(selected)" - > - {{ $t('domain_mute_card.unmute') }} - <template slot="progress"> - {{ $t('domain_mute_card.unmute_progress') }} - </template> - </ProgressButton> - </div> - </template> - <template - slot="item" - slot-scope="{item}" - > - <DomainMuteCard :domain="item" /> - </template> - <template slot="empty"> - {{ $t('settings.no_mutes') }} - </template> - </DomainMuteList> - </div> - </tab-switcher> - </div> - </tab-switcher> - </div> - </div> -</template> - -<script src="./user_settings.js"> -</script> - -<style lang="scss"> -@import '../../_variables.scss'; - -.profile-edit { - .bio { - margin: 0; - } - - .visibility-tray { - padding-top: 5px; - } - - input[type=file] { - padding: 5px; - height: auto; - } - - .banner { - max-width: 100%; - } - - .uploading { - font-size: 1.5em; - margin: 0.25em; - } - - .name-changer { - width: 100%; - } - - .bg { - max-width: 100%; - } - - .current-avatar { - display: block; - width: 150px; - height: 150px; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); - } - - .oauth-tokens { - width: 100%; - - th { - text-align: left; - } - - .actions { - text-align: right; - } - } - - &-usersearch-wrapper { - padding: 1em; - } - - &-bulk-actions { - text-align: right; - padding: 0 1em; - min-height: 28px; - - button { - width: 10em; - } - } - - &-domain-mute-form { - padding: 1em; - display: flex; - flex-direction: column; - - button { - align-self: flex-end; - margin-top: 1em; - width: 10em; - } - } - - .setting-subitem { - margin-left: 1.75em; - } -} -</style> diff --git a/src/components/video_attachment/video_attachment.vue b/src/components/video_attachment/video_attachment.vue @@ -4,6 +4,8 @@ :src="attachment.url" :loop="loopVideo" :controls="controls" + :alt="attachment.description" + :title="attachment.description" playsinline @loadeddata="onVideoDataLoad" /> diff --git a/src/components/who_to_follow_panel/who_to_follow_panel.js b/src/components/who_to_follow_panel/who_to_follow_panel.js @@ -7,7 +7,7 @@ function showWhoToFollow (panel, reply) { panel.usersToFollow.forEach((toFollow, index) => { let user = shuffled[index] - let img = user.avatar || '/images/avi.png' + let img = user.avatar || this.$store.state.instance.defaultAvatar let name = user.acct toFollow.img = img @@ -38,13 +38,7 @@ function getWhoToFollow (panel) { const WhoToFollowPanel = { data: () => ({ - usersToFollow: new Array(3).fill().map(x => ( - { - img: '/images/avi.png', - name: '', - id: 0 - } - )) + usersToFollow: [] }), computed: { user: function () { @@ -68,6 +62,13 @@ const WhoToFollowPanel = { }, mounted: function () { + this.usersToFollow = new Array(3).fill().map(x => ( + { + img: this.$store.state.instance.defaultAvatar, + name: '', + id: 0 + } + )) if (this.suggestionsEnabled) { getWhoToFollow(this) } diff --git a/src/hocs/with_load_more/with_load_more.scss b/src/hocs/with_load_more/with_load_more.scss @@ -12,5 +12,9 @@ .error { font-size: 14px; } + + a { + cursor: pointer; + } } } diff --git a/src/i18n/ar.json b/src/i18n/ar.json @@ -1,206 +1,204 @@ { - "chat": { - "title": "الدردشة" + "chat": { + "title": "الدردشة" + }, + "features_panel": { + "chat": "الدردشة", + "gopher": "غوفر", + "media_proxy": "بروكسي الوسائط", + "scope_options": "", + "text_limit": "الحد الأقصى للنص", + "title": "الميّزات", + "who_to_follow": "للمتابعة" + }, + "finder": { + "error_fetching_user": "خطأ أثناء جلب صفحة المستخدم", + "find_user": "البحث عن مستخدِم" + }, + "general": { + "apply": "تطبيق", + "submit": "إرسال" + }, + "login": { + "login": "تسجيل الدخول", + "logout": "الخروج", + "password": "الكلمة السرية", + "placeholder": "مثال lain", + "register": "انشاء حساب", + "username": "إسم المستخدم" + }, + "nav": { + "chat": "الدردشة المحلية", + "friend_requests": "طلبات المتابَعة", + "mentions": "الإشارات", + "public_tl": "الخيط الزمني العام", + "timeline": "الخيط الزمني", + "twkn": "كافة الشبكة المعروفة" + }, + "notifications": { + "broken_favorite": "منشور مجهول، جارٍ البحث عنه…", + "favorited_you": "أعجِب بمنشورك", + "followed_you": "يُتابعك", + "load_older": "تحميل الإشعارات الأقدم", + "notifications": "الإخطارات", + "read": "مقروء!", + "repeated_you": "شارَك منشورك" + }, + "post_status": { + "account_not_locked_warning": "", + "account_not_locked_warning_link": "مقفل", + "attachments_sensitive": "اعتبر المرفقات كلها كمحتوى حساس", + "content_type": { + "text/plain": "نص صافٍ" }, - "features_panel": { - "chat": "الدردشة", - "gopher": "غوفر", - "media_proxy": "بروكسي الوسائط", - "scope_options": "", - "text_limit": "الحد الأقصى للنص", - "title": "الميّزات", - "who_to_follow": "للمتابعة" - }, - "finder": { - "error_fetching_user": "خطأ أثناء جلب صفحة المستخدم", - "find_user": "البحث عن مستخدِم" - }, - "general": { - "apply": "تطبيق", - "submit": "إرسال" - }, - "login": { - "login": "تسجيل الدخول", - "logout": "الخروج", - "password": "الكلمة السرية", - "placeholder": "مثال lain", - "register": "انشاء حساب", - "username": "إسم المستخدم" - }, - "nav": { - "chat": "الدردشة المحلية", - "friend_requests": "طلبات المتابَعة", - "mentions": "الإشارات", - "public_tl": "الخيط الزمني العام", - "timeline": "الخيط الزمني", - "twkn": "كافة الشبكة المعروفة" - }, - "notifications": { - "broken_favorite": "منشور مجهول، جارٍ البحث عنه…", - "favorited_you": "أعجِب بمنشورك", - "followed_you": "يُتابعك", - "load_older": "تحميل الإشعارات الأقدم", - "notifications": "الإخطارات", - "read": "مقروء!", - "repeated_you": "شارَك منشورك" - }, - "post_status": { - "account_not_locked_warning": "", - "account_not_locked_warning_link": "مقفل", - "attachments_sensitive": "اعتبر المرفقات كلها كمحتوى حساس", - "content_type": { - "text/plain": "نص صافٍ" - }, - "content_warning": "الموضوع (اختياري)", - "default": "وصلت للتوّ إلى لوس أنجلس.", - "direct_warning": "", - "posting": "النشر", - "scope": { - "direct": "", - "private": "", - "public": "علني - يُنشر على الخيوط الزمنية العمومية", - "unlisted": "غير مُدرَج - لا يُنشَر على الخيوط الزمنية العمومية" - } - }, - "registration": { - "bio": "السيرة الذاتية", - "email": "عنوان البريد الإلكتروني", - "fullname": "الإسم المعروض", - "password_confirm": "تأكيد الكلمة السرية", - "registration": "التسجيل", - "token": "رمز الدعوة" - }, - "settings": { - "attachmentRadius": "المُرفَقات", - "attachments": "المُرفَقات", - "autoload": "", - "avatar": "الصورة الرمزية", - "avatarAltRadius": "الصور الرمزية (الإشعارات)", - "avatarRadius": "الصور الرمزية", - "background": "الخلفية", - "bio": "السيرة الذاتية", - "btnRadius": "الأزرار", - "cBlue": "أزرق (الرد، المتابَعة)", - "cGreen": "أخضر (إعادة النشر)", - "cOrange": "برتقالي (مفضلة)", - "cRed": "أحمر (إلغاء)", - "change_password": "تغيير كلمة السر", - "change_password_error": "وقع هناك خلل أثناء تعديل كلمتك السرية.", - "changed_password": "تم تغيير كلمة المرور بنجاح!", - "collapse_subject": "", - "confirm_new_password": "تأكيد كلمة السر الجديدة", - "current_avatar": "صورتك الرمزية الحالية", - "current_password": "كلمة السر الحالية", - "current_profile_banner": "الرأسية الحالية لصفحتك الشخصية", - "data_import_export_tab": "تصدير واستيراد البيانات", - "default_vis": "أسلوب العرض الافتراضي", - "delete_account": "حذف الحساب", - "delete_account_description": "حذف حسابك و كافة منشوراتك نهائيًا.", - "delete_account_error": "", - "delete_account_instructions": "يُرجى إدخال كلمتك السرية أدناه لتأكيد عملية حذف الحساب.", - "export_theme": "حفظ النموذج", - "filtering": "التصفية", - "filtering_explanation": "سيتم إخفاء كافة المنشورات التي تحتوي على هذه الكلمات، كلمة واحدة في كل سطر", - "follow_export": "تصدير الاشتراكات", - "follow_export_button": "تصدير الاشتراكات كملف csv", - "follow_export_processing": "التصدير جارٍ، سوف يُطلَب منك تنزيل ملفك بعد حين", - "follow_import": "استيراد الاشتراكات", - "follow_import_error": "خطأ أثناء استيراد المتابِعين", - "follows_imported": "", - "foreground": "الأمامية", - "general": "الإعدادات العامة", - "hide_attachments_in_convo": "إخفاء المرفقات على المحادثات", - "hide_attachments_in_tl": "إخفاء المرفقات على الخيط الزمني", - "hide_post_stats": "", - "hide_user_stats": "", - "import_followers_from_a_csv_file": "", - "import_theme": "تحميل نموذج", - "inputRadius": "", - "instance_default": "", - "interfaceLanguage": "لغة الواجهة", - "invalid_theme_imported": "", - "limited_availability": "غير متوفر على متصفحك", - "links": "الروابط", - "lock_account_description": "", - "loop_video": "", - "loop_video_silent_only": "", - "name": "الاسم", - "name_bio": "الاسم والسيرة الذاتية", - "new_password": "كلمة السر الجديدة", - "no_rich_text_description": "", - "notification_visibility": "نوع الإشعارات التي تريد عرضها", - "notification_visibility_follows": "يتابع", - "notification_visibility_likes": "الإعجابات", - "notification_visibility_mentions": "الإشارات", - "notification_visibility_repeats": "", - "nsfw_clickthrough": "", - "oauth_tokens": "رموز OAuth", - "token": "رمز", - "refresh_token": "رمز التحديث", - "valid_until": "صالح حتى", - "revoke_token": "سحب", - "panelRadius": "", - "pause_on_unfocused": "", - "presets": "النماذج", - "profile_background": "خلفية الصفحة الشخصية", - "profile_banner": "رأسية الصفحة الشخصية", - "profile_tab": "الملف الشخصي", - "radii_help": "", - "replies_in_timeline": "الردود على الخيط الزمني", - "reply_link_preview": "", - "reply_visibility_all": "عرض كافة الردود", - "reply_visibility_following": "", - "reply_visibility_self": "", - "saving_err": "خطأ أثناء حفظ الإعدادات", - "saving_ok": "تم حفظ الإعدادات", - "security_tab": "الأمان", - "set_new_avatar": "اختيار صورة رمزية جديدة", - "set_new_profile_background": "اختيار خلفية جديدة للملف الشخصي", - "set_new_profile_banner": "اختيار رأسية جديدة للصفحة الشخصية", - "settings": "الإعدادات", - "stop_gifs": "", - "streaming": "", - "text": "النص", - "theme": "المظهر", - "theme_help": "", - "tooltipRadius": "", - "user_settings": "إعدادات المستخدم", - "values": { - "false": "لا", - "true": "نعم" - } - }, - "timeline": { - "collapse": "", - "conversation": "محادثة", - "error_fetching": "خطأ أثناء جلب التحديثات", - "load_older": "تحميل المنشورات القديمة", - "no_retweet_hint": "", - "repeated": "", - "show_new": "عرض الجديد", - "up_to_date": "تم تحديثه" - }, - "user_card": { - "approve": "قبول", - "block": "حظر", - "blocked": "تم حظره!", - "deny": "رفض", - "follow": "اتبع", - "followees": "", - "followers": "مُتابِعون", - "following": "", - "follows_you": "يتابعك!", - "mute": "كتم", - "muted": "تم كتمه", - "per_day": "في اليوم", - "remote_follow": "مُتابَعة عن بُعد", - "statuses": "المنشورات" - }, - "user_profile": { - "timeline_title": "الخيط الزمني للمستخدم" - }, - "who_to_follow": { - "more": "المزيد", - "who_to_follow": "للمتابعة" + "content_warning": "الموضوع (اختياري)", + "default": "وصلت للتوّ إلى لوس أنجلس.", + "direct_warning": "", + "posting": "النشر", + "scope": { + "direct": "", + "private": "", + "public": "علني - يُنشر على الخيوط الزمنية العمومية", + "unlisted": "غير مُدرَج - لا يُنشَر على الخيوط الزمنية العمومية" + } + }, + "registration": { + "bio": "السيرة الذاتية", + "email": "عنوان البريد الإلكتروني", + "fullname": "الإسم المعروض", + "password_confirm": "تأكيد الكلمة السرية", + "registration": "التسجيل", + "token": "رمز الدعوة" + }, + "settings": { + "attachmentRadius": "المُرفَقات", + "attachments": "المُرفَقات", + "avatar": "الصورة الرمزية", + "avatarAltRadius": "الصور الرمزية (الإشعارات)", + "avatarRadius": "الصور الرمزية", + "background": "الخلفية", + "bio": "السيرة الذاتية", + "btnRadius": "الأزرار", + "cBlue": "أزرق (الرد، المتابَعة)", + "cGreen": "أخضر (إعادة النشر)", + "cOrange": "برتقالي (مفضلة)", + "cRed": "أحمر (إلغاء)", + "change_password": "تغيير كلمة السر", + "change_password_error": "وقع هناك خلل أثناء تعديل كلمتك السرية.", + "changed_password": "تم تغيير كلمة المرور بنجاح!", + "collapse_subject": "", + "confirm_new_password": "تأكيد كلمة السر الجديدة", + "current_avatar": "صورتك الرمزية الحالية", + "current_password": "كلمة السر الحالية", + "current_profile_banner": "الرأسية الحالية لصفحتك الشخصية", + "data_import_export_tab": "تصدير واستيراد البيانات", + "default_vis": "أسلوب العرض الافتراضي", + "delete_account": "حذف الحساب", + "delete_account_description": "حذف حسابك و كافة منشوراتك نهائيًا.", + "delete_account_error": "", + "delete_account_instructions": "يُرجى إدخال كلمتك السرية أدناه لتأكيد عملية حذف الحساب.", + "export_theme": "حفظ النموذج", + "filtering": "التصفية", + "filtering_explanation": "سيتم إخفاء كافة المنشورات التي تحتوي على هذه الكلمات، كلمة واحدة في كل سطر", + "follow_export": "تصدير الاشتراكات", + "follow_export_button": "تصدير الاشتراكات كملف csv", + "follow_export_processing": "التصدير جارٍ، سوف يُطلَب منك تنزيل ملفك بعد حين", + "follow_import": "استيراد الاشتراكات", + "follow_import_error": "خطأ أثناء استيراد المتابِعين", + "follows_imported": "", + "foreground": "الأمامية", + "general": "الإعدادات العامة", + "hide_attachments_in_convo": "إخفاء المرفقات على المحادثات", + "hide_attachments_in_tl": "إخفاء المرفقات على الخيط الزمني", + "hide_post_stats": "", + "hide_user_stats": "", + "import_followers_from_a_csv_file": "", + "import_theme": "تحميل نموذج", + "inputRadius": "", + "instance_default": "", + "interfaceLanguage": "لغة الواجهة", + "invalid_theme_imported": "", + "limited_availability": "غير متوفر على متصفحك", + "links": "الروابط", + "lock_account_description": "", + "loop_video": "", + "loop_video_silent_only": "", + "name": "الاسم", + "name_bio": "الاسم والسيرة الذاتية", + "new_password": "كلمة السر الجديدة", + "no_rich_text_description": "", + "notification_visibility": "نوع الإشعارات التي تريد عرضها", + "notification_visibility_follows": "يتابع", + "notification_visibility_likes": "الإعجابات", + "notification_visibility_mentions": "الإشارات", + "notification_visibility_repeats": "", + "nsfw_clickthrough": "", + "oauth_tokens": "رموز OAuth", + "token": "رمز", + "refresh_token": "رمز التحديث", + "valid_until": "صالح حتى", + "revoke_token": "سحب", + "panelRadius": "", + "pause_on_unfocused": "", + "presets": "النماذج", + "profile_background": "خلفية الصفحة الشخصية", + "profile_banner": "رأسية الصفحة الشخصية", + "profile_tab": "الملف الشخصي", + "radii_help": "", + "replies_in_timeline": "الردود على الخيط الزمني", + "reply_visibility_all": "عرض كافة الردود", + "reply_visibility_following": "", + "reply_visibility_self": "", + "saving_err": "خطأ أثناء حفظ الإعدادات", + "saving_ok": "تم حفظ الإعدادات", + "security_tab": "الأمان", + "set_new_avatar": "اختيار صورة رمزية جديدة", + "set_new_profile_background": "اختيار خلفية جديدة للملف الشخصي", + "set_new_profile_banner": "اختيار رأسية جديدة للصفحة الشخصية", + "settings": "الإعدادات", + "stop_gifs": "", + "streaming": "", + "text": "النص", + "theme": "المظهر", + "theme_help": "", + "tooltipRadius": "", + "user_settings": "إعدادات المستخدم", + "values": { + "false": "لا", + "true": "نعم" } -} -\ No newline at end of file + }, + "timeline": { + "collapse": "", + "conversation": "محادثة", + "error_fetching": "خطأ أثناء جلب التحديثات", + "load_older": "تحميل المنشورات القديمة", + "no_retweet_hint": "", + "repeated": "", + "show_new": "عرض الجديد", + "up_to_date": "تم تحديثه" + }, + "user_card": { + "approve": "قبول", + "block": "حظر", + "blocked": "تم حظره!", + "deny": "رفض", + "follow": "اتبع", + "followees": "", + "followers": "مُتابِعون", + "following": "", + "follows_you": "يتابعك!", + "mute": "كتم", + "muted": "تم كتمه", + "per_day": "في اليوم", + "remote_follow": "مُتابَعة عن بُعد", + "statuses": "المنشورات" + }, + "user_profile": { + "timeline_title": "الخيط الزمني للمستخدم" + }, + "who_to_follow": { + "more": "المزيد", + "who_to_follow": "للمتابعة" + } +} diff --git a/src/i18n/ca.json b/src/i18n/ca.json @@ -73,7 +73,6 @@ "settings": { "attachmentRadius": "Adjunts", "attachments": "Adjunts", - "autoload": "Recarrega automàticament en arribar a sota de tot.", "avatar": "Avatar", "avatarAltRadius": "Avatars en les notificacions", "avatarRadius": "Avatars", @@ -96,8 +95,8 @@ "default_vis": "Abast per defecte de les entrades", "delete_account": "Esborra el compte", "delete_account_description": "Esborra permanentment el teu compte i tots els missatges", - "delete_account_error": "No s'ha pogut esborrar el compte. Si continua el problema, contacta amb l'administració del node", - "delete_account_instructions": "Confirma que vols esborrar el compte escrivint la teva contrasenya aquí sota", + "delete_account_error": "No s'ha pogut esborrar el compte. Si continua el problema, contacta amb l'administració del node.", + "delete_account_instructions": "Confirma que vols esborrar el compte escrivint la teva contrasenya aquí sota.", "export_theme": "Desa el tema", "filtering": "Filtres", "filtering_explanation": "Es silenciaran totes les entrades que continguin aquestes paraules. Separa-les per línies", @@ -119,7 +118,7 @@ "invalid_theme_imported": "No s'ha entès l'arxiu carregat perquè no és un tema vàlid de Pleroma. No s'ha fet cap canvi als temes actuals.", "limited_availability": "No està disponible en aquest navegador", "links": "Enllaços", - "lock_account_description": "Restringeix el teu compte només a seguidores aprovades.", + "lock_account_description": "Restringeix el teu compte només a seguidores aprovades", "loop_video": "Reprodueix els vídeos en bucle", "loop_video_silent_only": "Reprodueix en bucles només els vídeos sense so (com els \"GIF\" de Mastodon)", "name": "Nom", @@ -145,7 +144,6 @@ "profile_tab": "Perfil", "radii_help": "Configura l'arrodoniment de les vores (en píxels)", "replies_in_timeline": "Replies in timeline", - "reply_link_preview": "Mostra el missatge citat en passar el ratolí per sobre de l'enllaç de resposta", "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_self": "Mostra només les respostes a entrades meves", @@ -160,7 +158,7 @@ "streaming": "Carrega automàticament entrades noves quan estigui a dalt de tot", "text": "Text", "theme": "Tema", - "theme_help": "Personalitza els colors del tema. Escriu-los en format RGB hexadecimal (#rrggbb)", + "theme_help": "Personalitza els colors del tema. Escriu-los en format RGB hexadecimal (#rrggbb).", "tooltipRadius": "Missatges sobreposats", "user_settings": "Configuració personal", "values": { diff --git a/src/i18n/cs.json b/src/i18n/cs.json @@ -112,7 +112,6 @@ "app_name": "Název aplikace", "attachmentRadius": "Přílohy", "attachments": "Přílohy", - "autoload": "Povolit automatické načítání při rolování dolů", "avatar": "Avatar", "avatarAltRadius": "Avatary (oznámení)", "avatarRadius": "Avatary", @@ -206,7 +205,6 @@ "profile_tab": "Profil", "radii_help": "Nastavit zakulacení rohů rozhraní (v pixelech)", "replies_in_timeline": "Odpovědi v časové ose", - "reply_link_preview": "Povolit náhledy odkazu pro odpověď při přejetí myši", "reply_visibility_all": "Zobrazit všechny odpovědi", "reply_visibility_following": "Zobrazit pouze odpovědi směřované na mě nebo uživatele, které sleduji", "reply_visibility_self": "Zobrazit pouze odpovědi směřované na mě", @@ -258,7 +256,7 @@ "contrast": { "hint": "Poměr kontrastu je {ratio}, {level} {context}", "level": { - "aa": "splňuje směrnici úrovně AA (minimální)", + "aa": "splňuje směrnici úrovně AA (minimální)", "aaa": "splňuje směrnici úrovně AAA (doporučováno)", "bad": "nesplňuje žádné směrnice přístupnosti" }, @@ -400,7 +398,6 @@ "reply_to": "Odpověď uživateli", "replies_list": "Odpovědi:" }, - "user_card": { "approve": "Schválit", "block": "Blokovat", @@ -446,7 +443,7 @@ "favorite": "Oblíbit", "user_settings": "Uživatelské nastavení" }, - "upload":{ + "upload": { "error": { "base": "Nahrávání selhalo.", "file_too_big": "Soubor je příliš velký [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", diff --git a/src/i18n/de.json b/src/i18n/de.json @@ -7,8 +7,8 @@ "gopher": "Gopher", "media_proxy": "Medienproxy", "scope_options": "Reichweitenoptionen", - "text_limit": "Textlimit", - "title": "Features", + "text_limit": "Zeichenlimit", + "title": "Funktionen", "who_to_follow": "Wem folgen?" }, "finder": { @@ -17,7 +17,18 @@ }, "general": { "apply": "Anwenden", - "submit": "Absenden" + "submit": "Absenden", + "more": "Mehr", + "generic_error": "Ein Fehler ist aufgetreten", + "optional": "Optional", + "show_more": "Zeige mehr", + "show_less": "Zeige weniger", + "dismiss": "Ablehnen", + "cancel": "Abbrechen", + "disable": "Deaktivieren", + "enable": "Aktivieren", + "confirm": "Bestätigen", + "verify": "Verifizieren" }, "login": { "login": "Anmelden", @@ -26,7 +37,16 @@ "password": "Passwort", "placeholder": "z.B. lain", "register": "Registrieren", - "username": "Benutzername" + "username": "Benutzername", + "authentication_code": "Authentifizierungscode", + "enter_recovery_code": "Gebe einen Wiederherstellungscode ein", + "recovery_code": "Wiederherstellungscode", + "heading": { + "totp": "Zwei-Faktor Authentifizierung", + "recovery": "Zwei-Faktor Wiederherstellung" + }, + "hint": "Anmelden um an der Diskussion teilzunehmen", + "enter_two_factor_code": "Gebe einen Zwei-Faktor-Code ein" }, "nav": { "about": "Über", @@ -38,19 +58,25 @@ "dms": "Direktnachrichten", "public_tl": "Öffentliche Zeitleiste", "timeline": "Zeitleiste", - "twkn": "Das gesamte bekannte Netzwerk", + "twkn": "Bekannte Netzwerk", "user_search": "Benutzersuche", "search": "Suche", - "preferences": "Voreinstellungen" + "preferences": "Voreinstellungen", + "administration": "Administration", + "who_to_follow": "Wem folgen" }, "notifications": { - "broken_favorite": "Unbekannte Nachricht, suche danach...", + "broken_favorite": "Unbekannte Nachricht, suche danach…", "favorited_you": "favorisierte deine Nachricht", "followed_you": "folgt dir", "load_older": "Ältere Benachrichtigungen laden", "notifications": "Benachrichtigungen", "read": "Gelesen!", - "repeated_you": "wiederholte deine Nachricht" + "repeated_you": "wiederholte deine Nachricht", + "follow_request": "möchte dir folgen", + "migrated_to": "migrierte zu", + "reacted_with": "reagierte mit {0}", + "no_more_notifications": "Keine Benachrichtigungen mehr" }, "post_status": { "new_status": "Neuen Status veröffentlichen", @@ -58,7 +84,10 @@ "account_not_locked_warning_link": "gesperrt", "attachments_sensitive": "Anhänge als heikel markieren", "content_type": { - "text/plain": "Nur Text" + "text/plain": "Nur Text", + "text/bbcode": "BBCode", + "text/markdown": "Markdown", + "text/html": "HTML" }, "content_warning": "Betreff (optional)", "default": "Sitze gerade im Hofbräuhaus.", @@ -69,6 +98,13 @@ "private": "Nur Follower - Beitrag nur für Follower sichtbar", "public": "Öffentlich - Beitrag an öffentliche Zeitleisten", "unlisted": "Nicht gelistet - Nicht in öffentlichen Zeitleisten anzeigen" + }, + "direct_warning_to_all": "Dieser Beitrag wird für alle erwähnten Benutzer sichtbar sein.", + "direct_warning_to_first_only": "Dieser Beitrag wird für alle Benutzer, die am Anfang der Nachricht erwähnt wurden, sichtbar sein.", + "scope_notice": { + "public": "Dieser Beitrag wird für alle sichtbar sein", + "private": "Dieser Beitrag wird nur für deine Follower sichtbar sein", + "unlisted": "Dieser Beitrag wird weder in der öffentlichen Zeitleiste noch im gesamten bekannten Netzwerk sichtbar sein" } }, "registration": { @@ -79,27 +115,29 @@ "registration": "Registrierung", "token": "Einladungsschlüssel", "captcha": "CAPTCHA", - "new_captcha": "Zum Erstellen eines neuen Captcha auf das Bild klicken.", + "new_captcha": "Zum Erstellen eines neuen Captcha auf das Bild klicken", "validations": { "username_required": "darf nicht leer sein", "fullname_required": "darf nicht leer sein", "email_required": "darf nicht leer sein", "password_required": "darf nicht leer sein", "password_confirmation_required": "darf nicht leer sein", - "password_confirmation_match": "sollte mit dem Passwort identisch sein." - } + "password_confirmation_match": "sollte mit dem Passwort identisch sein" + }, + "bio_placeholder": "z.B.\nHallo, ich bin Lain.\nIch bin ein Anime Mödchen aus dem vorstädtischen Japan. Du kennst mich vielleicht vom Wired.", + "fullname_placeholder": "z.B. Lain Iwakura", + "username_placeholder": "z.B. lain" }, "settings": { "attachmentRadius": "Anhänge", "attachments": "Anhänge", - "autoload": "Aktiviere automatisches Laden von älteren Beiträgen beim scrollen", "avatar": "Avatar", "avatarAltRadius": "Avatare (Benachrichtigungen)", "avatarRadius": "Avatare", "background": "Hintergrund", "bio": "Bio", "btnRadius": "Buttons", - "cBlue": "Blau (Antworten, Folgt dir)", + "cBlue": "Blau (Antworten, folgt dir)", "cGreen": "Grün (Retweet)", "cOrange": "Orange (Favorisieren)", "cRed": "Rot (Abbrechen)", @@ -115,21 +153,21 @@ "data_import_export_tab": "Datenimport/-export", "default_vis": "Standard-Sichtbarkeitsumfang", "delete_account": "Account löschen", - "delete_account_description": "Lösche deinen Account und alle deine Nachrichten unwiderruflich.", + "delete_account_description": "Lösche deine Daten und deaktiviere deinen Account unwiderruflich.", "delete_account_error": "Es ist ein Fehler beim Löschen deines Accounts aufgetreten. Tritt dies weiterhin auf, wende dich an den Administrator der Instanz.", "delete_account_instructions": "Tippe dein Passwort unten in das Feld ein, um die Löschung deines Accounts zu bestätigen.", - "discoverable": "Erlaubnis für automatisches Suchen nach diesem Account", + "discoverable": "Erlaube, dass dieser Account in Suchergebnissen auftaucht", "avatar_size_instruction": "Die empfohlene minimale Größe für Avatare ist 150x150 Pixel.", "pad_emoji": "Emojis mit Leerzeichen umrahmen", "export_theme": "Farbschema speichern", "filtering": "Filtern", - "filtering_explanation": "Alle Beiträge die diese Wörter enthalten werden ausgeblendet. Ein Wort pro Zeile.", + "filtering_explanation": "Alle Beiträge, welche diese Wörter enthalten, werden ausgeblendet. Ein Wort pro Zeile", "follow_export": "Follower exportieren", "follow_export_button": "Exportiere deine Follows in eine csv-Datei", "follow_export_processing": "In Bearbeitung. Die Liste steht gleich zum herunterladen bereit.", - "follow_import": "Followers importieren", - "follow_import_error": "Fehler beim importieren der Follower", - "follows_imported": "Followers importiert! Die Bearbeitung kann eine Zeit lang dauern.", + "follow_import": "Follower importieren", + "follow_import_error": "Fehler beim Importieren der Follower", + "follows_imported": "Follower importiert! Die Bearbeitung kann einen Moment dauern.", "foreground": "Vordergrund", "general": "Allgemein", "hide_attachments_in_convo": "Anhänge in Unterhaltungen ausblenden", @@ -142,7 +180,7 @@ "hide_post_stats": "Beitragsstatistiken verbergen (z.B. die Anzahl der Favoriten)", "hide_user_stats": "Benutzerstatistiken verbergen (z.B. die Anzahl der Follower)", "hide_filtered_statuses": "Gefilterte Beiträge verbergen", - "import_followers_from_a_csv_file": "Importiere Follower, denen du folgen möchtest, aus einer CSV-Datei", + "import_followers_from_a_csv_file": "Importiere Follower aus einer CSV-Datei", "import_theme": "Farbschema laden", "inputRadius": "Eingabefelder", "checkboxRadius": "Auswahlfelder", @@ -156,7 +194,7 @@ "lock_account_description": "Sperre deinen Account, um neue Follower zu genehmigen oder abzulehnen", "loop_video": "Videos wiederholen", "loop_video_silent_only": "Nur Videos ohne Ton wiederholen (z.B. Mastodons \"gifs\")", - "mutes_tab": "Mutes", + "mutes_tab": "Stummschaltungen", "play_videos_in_modal": "Videos in größerem Medienfenster abspielen", "use_contain_fit": "Vorschaubilder nicht zuschneiden", "name": "Name", @@ -186,7 +224,6 @@ "profile_tab": "Profil", "radii_help": "Kantenrundung (in Pixel) der Oberfläche anpassen", "replies_in_timeline": "Antworten in der Zeitleiste", - "reply_link_preview": "Antwortlink-Vorschau beim Überfahren mit der Maus aktivieren", "reply_visibility_all": "Alle Antworten zeigen", "reply_visibility_following": "Zeige nur Antworten an mich oder an Benutzer, denen ich folge", "reply_visibility_self": "Nur Antworten an mich anzeigen", @@ -210,7 +247,7 @@ "streaming": "Aktiviere automatisches Laden (Streaming) von neuen Beiträgen", "text": "Text", "theme": "Farbschema", - "theme_help": "Benutze HTML-Farbcodes (#rrggbb) um dein Farbschema anzupassen", + "theme_help": "Benutze HTML-Farbcodes (#rrggbb) um dein Farbschema anzupassen.", "theme_help_v2_1": "Du kannst auch die Farben und die Deckkraft bestimmter Komponenten überschreiben, indem du das Kontrollkästchen umschaltest. Verwende die Schaltfläche \"Alle löschen\", um alle Überschreibungen zurückzusetzen.", "theme_help_v2_2": "Unter einigen Einträgen befinden sich Symbole für Hintergrund-/Textkontrastindikatoren, für detaillierte Informationen fahre mit der Maus darüber. Bitte beachte, dass bei der Verwendung von Transparenz Kontrastindikatoren den schlechtest möglichen Fall darstellen.", "tooltipRadius": "Tooltips/Warnungen", @@ -221,7 +258,7 @@ }, "notifications": "Benachrichtigungen", "enable_web_push_notifications": "Web-Pushbenachrichtigungen aktivieren", - "style": { + "style": { "switcher": { "keep_color": "Farben beibehalten", "keep_shadows": "Schatten beibehalten", @@ -284,7 +321,7 @@ "always_drop_shadow": "Achtung, dieser Schatten verwendet immer {0}, wenn der Browser dies unterstützt.", "drop_shadow_syntax": "{0} unterstützt Parameter {1} und Schlüsselwort {2} nicht.", "avatar_inset": "Bitte beachte, dass die Kombination von eingesetzten und nicht eingesetzten Schatten auf Avataren zu unerwarteten Ergebnissen bei transparenten Avataren führen kann.", - "spread_zero": "Schatten mit einer Streuung > 0 erscheinen so, als ob sie auf Null gesetzt wären.", + "spread_zero": "Schatten mit einer Streuung > 0 erscheinen so, als ob sie auf Null gesetzt wären", "inset_classic": "Eingesetzte Schatten werden mit {0} verwendet" }, "components": { @@ -329,14 +366,48 @@ "checkbox": "Ich habe die Allgemeinen Geschäftsbedingungen überflogen", "link": "ein netter kleiner Link" } - } + }, + "app_name": "Anwendungsname", + "mfa": { + "otp": "OTP", + "recovery_codes_warning": "Schreibe dir die Codes auf oder speichere sie an einem sicheren Ort - ansonsten wirst du sie nicht wiederfinden. Wenn du den Zugriff zu deiner 2FA App und die Wiederherstellungs-Codes verlierst, wirst du aus deinem Account ausgeschlossen sein.", + "recovery_codes": "Wiederherstellungs-Codes.", + "warning_of_generate_new_codes": "Wenn du neue Wiederherstellungs-Codes generierst, werden die alten Codes nicht mehr funktionieren.", + "generate_new_recovery_codes": "Generiere neue Wiederherstellungs-Codes", + "title": "Zwei-Faktor Authentifizierung", + "waiting_a_recovery_codes": "Erhalte Wiederherstellungscodes…", + "authentication_methods": "Authentifizierungsmethoden", + "scan": { + "title": "Scan", + "secret_code": "Schlüssel", + "desc": "Wenn du deine 2FA App verwendest, scanne diesen QR Code oder gebe den Schlüssel ein:" + }, + "verify": { + "desc": "Um 2FA zu aktivieren, gib den Code von deiner 2FA-App ein:" + } + }, + "enter_current_password_to_confirm": "Gib dein aktuelles Passwort ein, um deine Identität zu bestätigen", + "security": "Sicherheit", + "allow_following_move": "Erlaube automatisches Folgen, sobald ein gefolgter Nutzer umzieht", + "blocks_imported": "Blocks importiert! Die Verarbeitung wird einen Moment brauchen.", + "block_import_error": "Fehler beim Importieren der Blocks", + "block_import": "Block Import", + "block_export_button": "Exportiere deine Blocks in eine csv Datei", + "block_export": "Block Export", + "emoji_reactions_on_timeline": "Zeige Emoji-Reaktionen auf der Zeitleiste", + "domain_mutes": "Domains", + "changed_email": "Email Adresse erfolgreich geändert!", + "change_email_error": "Es trat ein Problem auf beim Versuch, deine Email Adresse zu ändern.", + "change_email": "Ändere Email", + "import_blocks_from_a_csv_file": "Importiere Blocks von einer CSV Datei", + "accent": "Akzent" }, "timeline": { "collapse": "Einklappen", "conversation": "Unterhaltung", "error_fetching": "Fehler beim Laden", "load_older": "Lade ältere Beiträge", - "no_retweet_hint": "Der Beitrag ist als nur-für-Follower oder als Direktnachricht markiert und kann nicht wiederholt werden.", + "no_retweet_hint": "Der Beitrag ist als nur-für-Follower oder als Direktnachricht markiert und kann nicht wiederholt werden", "repeated": "wiederholte", "show_new": "Zeige Neuere", "up_to_date": "Aktuell" @@ -352,7 +423,7 @@ "follow_again": "Anfrage erneut senden?", "follow_unfollow": "Folgen beenden", "followees": "Folgt", - "followers": "Followers", + "followers": "Folgende", "following": "Folgst du!", "follows_you": "Folgt dir!", "its_you": "Das bist du!", @@ -360,7 +431,10 @@ "muted": "Stummgeschaltet", "per_day": "pro Tag", "remote_follow": "Folgen", - "statuses": "Beiträge" + "statuses": "Beiträge", + "admin_menu": { + "sandbox": "Erzwinge Beiträge nur für Follower sichtbar zu sein" + } }, "user_profile": { "timeline_title": "Beiträge" @@ -376,11 +450,11 @@ "favorite": "Favorisieren", "user_settings": "Benutzereinstellungen" }, - "upload":{ + "upload": { "error": { - "base": "Hochladen fehlgeschlagen.", - "file_too_big": "Datei ist zu groß [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", - "default": "Bitte versuche es später erneut" + "base": "Hochladen fehlgeschlagen.", + "file_too_big": "Datei ist zu groß [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "Bitte versuche es später erneut" }, "file_size_units": { "B": "B", @@ -407,7 +481,100 @@ "not_found": "Benutzername/E-Mail-Adresse nicht gefunden. Vertippt?", "too_many_requests": "Kurze Pause. Zu viele Versuche. Bitte, später nochmal probieren.", "password_reset_disabled": "Passwortzurücksetzen deaktiviert. Bitte Administrator kontaktieren.", - "password_reset_required": "Passwortzurücksetzen erforderlich", + "password_reset_required": "Passwortzurücksetzen erforderlich.", "password_reset_required_but_mailer_is_disabled": "Passwortzurücksetzen wäre erforderlich, ist aber deaktiviert. Bitte Administrator kontaktieren." + }, + "about": { + "mrf": { + "federation": "Föderation", + "mrf_policies": "Aktivierte MRF Richtlinien", + "simple": { + "simple_policies": "Instanzspezifische Richtlinien", + "accept": "Akzeptieren", + "reject": "Ablehnen", + "reject_desc": "Diese Instanz akzeptiert keine Nachrichten der folgenden Instanzen:", + "quarantine": "Quarantäne", + "ftl_removal": "Von der Zeitleiste \"Das gesamte bekannte Netzwerk\" entfernen", + "media_removal": "Medienentfernung", + "media_removal_desc": "Diese Instanz entfernt Medien von den Beiträgen der folgenden Instanzen:", + "media_nsfw": "Erzwingen Medien als heikel zu makieren", + "media_nsfw_desc": "Diese Instanz makiert die Medien in Beiträgen der folgenden Instanzen als heikel:", + "accept_desc": "Diese Instanz akzeptiert nur Nachrichten von den folgenden Instanzen:", + "quarantine_desc": "Diese Instanz sendet nur öffentliche Beiträge zu den folgenden Instanzen:", + "ftl_removal_desc": "Dieser Instanz entfernt folgende Instanzen von der \"Das gesamte bekannte Netzwerk\" Zeitleiste:" + }, + "keyword": { + "keyword_policies": "Keyword Richtlinien", + "reject": "Ablehnen", + "replace": "Ersetzen", + "is_replaced_by": "→", + "ftl_removal": "Von der Zeitleiste \"Das gesamte bekannte Netzwerk\" entfernen" + }, + "mrf_policies_desc": "MRF Richtlinien manipulieren das Föderationsverhalten dieser Instanz. Die folgenden Richtlinien sind aktiv:" + }, + "staff": "Mitarbeiter" + }, + "domain_mute_card": { + "mute": "Stummschalten", + "mute_progress": "Wird stummgeschaltet…", + "unmute": "Stummschaltung aufheben", + "unmute_progress": "Stummschaltung wird aufgehoben…" + }, + "exporter": { + "export": "Exportieren", + "processing": "Verarbeitung läuft, bald wird Du dazu aufgefordert, deine Datei herunterzuladen" + }, + "image_cropper": { + "crop_picture": "Bild zuschneiden", + "save": "Speichern", + "cancel": "Abbrechen", + "save_without_cropping": "Ohne Zuschneiden speichern" + }, + "importer": { + "submit": "Absenden", + "success": "Erfolgreich importiert.", + "error": "Ein Fehler ist beim Verabeiten der Datei aufgetreten." + }, + "media_modal": { + "previous": "Zurück", + "next": "Weiter" + }, + "polls": { + "add_poll": "Umfrage hinzufügen", + "add_option": "Option hinzufügen", + "option": "Option", + "votes": "Stimmen", + "vote": "Abstimmen", + "type": "Umfragetyp", + "multiple_choices": "Mehrere Auswahlmöglichkeiten", + "single_choice": "Eine Auswahlmöglichkeit", + "expiry": "Alter der Umfrage", + "expired": "Die Umfrage endete vor {0}", + "not_enough_options": "Zu wenig einzigartige Auswahlmöglichkeiten in der Umfrage", + "expires_in": "Die Umfrage endet in {0}" + }, + "emoji": { + "stickers": "Sticker", + "emoji": "Emoji", + "search_emoji": "Nach einem Emoji suchen", + "custom": "Benutzerdefinierter Emoji", + "keep_open": "Auswahlfenster offen halten", + "add_emoji": "Emoji einfügen", + "load_all": "Lade alle {emojiAmount} Emoji", + "load_all_hint": "Erfolgreich erste {saneAmount} Emoji geladen, alle Emojis zu laden würde Leistungsprobleme hervorrufen.", + "unicode": "Unicode Emoji" + }, + "interactions": { + "load_older": "Lade ältere Interaktionen", + "follows": "Neue Follows", + "favs_repeats": "Wiederholungen und Favoriten", + "moves": "Benutzer migriert zu" + }, + "selectable_list": { + "select_all": "Wähle alle" + }, + "remote_user_resolver": { + "searching_for": "Suche nach", + "error": "Nicht gefunden." } } diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -29,14 +29,14 @@ }, "staff": "Staff" }, - "chat": { - "title": "Chat" + "shoutbox": { + "title": "Shoutbox" }, "domain_mute_card": { "mute": "Mute", - "mute_progress": "Muting...", + "mute_progress": "Muting…", "unmute": "Unmute", - "unmute_progress": "Unmuting..." + "unmute_progress": "Unmuting…" }, "exporter": { "export": "Export", @@ -44,6 +44,7 @@ }, "features_panel": { "chat": "Chat", + "pleroma_chat_messages": "Pleroma Chat", "gopher": "Gopher", "media_proxy": "Media proxy", "scope_options": "Scope options", @@ -59,7 +60,10 @@ "apply": "Apply", "submit": "Submit", "more": "More", + "loading": "Loading…", "generic_error": "An error occured", + "error_retry": "Please try again", + "retry": "Try again", "optional": "optional", "show_more": "Show more", "show_less": "Show less", @@ -68,7 +72,9 @@ "disable": "Disable", "enable": "Enable", "confirm": "Confirm", - "verify": "Verify" + "verify": "Verify", + "close": "Close", + "peek": "Peek" }, "image_cropper": { "crop_picture": "Crop picture", @@ -94,9 +100,9 @@ "enter_recovery_code": "Enter a recovery code", "enter_two_factor_code": "Enter a two-factor code", "recovery_code": "Recovery code", - "heading" : { - "totp" : "Two-factor authentication", - "recovery" : "Two-factor recovery" + "heading": { + "totp": "Two-factor authentication", + "recovery": "Two-factor recovery" } }, "media_modal": { @@ -114,14 +120,17 @@ "dms": "Direct Messages", "public_tl": "Public Timeline", "timeline": "Timeline", - "twkn": "The Whole Known Network", + "twkn": "Known Network", + "bookmarks": "Bookmarks", "user_search": "User Search", "search": "Search", "who_to_follow": "Who to follow", - "preferences": "Preferences" + "preferences": "Preferences", + "timelines": "Timelines", + "chats": "Chats" }, "notifications": { - "broken_favorite": "Unknown status, searching for it...", + "broken_favorite": "Unknown status, searching for it…", "favorited_you": "favorited your status", "followed_you": "followed you", "follow_request": "wants to follow you", @@ -158,6 +167,9 @@ "load_all_hint": "Loaded first {saneAmount} emoji, loading all emoji may cause performance issues.", "load_all": "Loading all {emojiAmount} emoji" }, + "errors": { + "storage_unavailable": "Pleroma could not access browser storage. Your login or your local settings won't be saved and you might encounter unexpected issues. Try enabling cookies." + }, "interactions": { "favs_repeats": "Repeats and Favorites", "follows": "New follows", @@ -169,6 +181,7 @@ "account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.", "account_not_locked_warning_link": "locked", "attachments_sensitive": "Mark attachments as sensitive", + "media_description": "Media description", "content_type": { "text/plain": "Plain text", "text/html": "HTML", @@ -180,6 +193,10 @@ "direct_warning_to_all": "This post will be visible to all the mentioned users.", "direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.", "posting": "Posting", + "preview": "Preview", + "preview_empty": "Empty", + "empty_status_error": "Can't post an empty status with no files", + "media_description_error": "Failed to update media, try again", "scope_notice": { "public": "This post will be visible to everyone", "private": "This post will be visible to your followers only", @@ -226,17 +243,17 @@ "security": "Security", "enter_current_password_to_confirm": "Enter your current password to confirm your identity", "mfa": { - "otp" : "OTP", - "setup_otp" : "Setup OTP", - "wait_pre_setup_otp" : "presetting OTP", - "confirm_and_enable" : "Confirm & enable OTP", + "otp": "OTP", + "setup_otp": "Setup OTP", + "wait_pre_setup_otp": "presetting OTP", + "confirm_and_enable": "Confirm & enable OTP", "title": "Two-factor Authentication", - "generate_new_recovery_codes" : "Generate new recovery codes", - "warning_of_generate_new_codes" : "When you generate new recovery codes, your old codes won’t work anymore.", - "recovery_codes" : "Recovery codes.", - "waiting_a_recovery_codes": "Receiving backup codes...", - "recovery_codes_warning" : "Write the codes down or save them somewhere secure - otherwise you won't see them again. If you lose access to your 2FA app and recovery codes you'll be locked out of your account.", - "authentication_methods" : "Authentication methods", + "generate_new_recovery_codes": "Generate new recovery codes", + "warning_of_generate_new_codes": "When you generate new recovery codes, your old codes won’t work anymore.", + "recovery_codes": "Recovery codes.", + "waiting_a_recovery_codes": "Receiving backup codes…", + "recovery_codes_warning": "Write the codes down or save them somewhere secure - otherwise you won't see them again. If you lose access to your 2FA app and recovery codes you'll be locked out of your account.", + "authentication_methods": "Authentication methods", "scan": { "title": "Scan", "desc": "Using your two-factor app, scan this QR code or enter text key:", @@ -249,7 +266,6 @@ "allow_following_move": "Allow auto-follow when following account moves", "attachmentRadius": "Attachments", "attachments": "Attachments", - "autoload": "Enable automatic loading when scrolled to the bottom", "avatar": "Avatar", "avatarAltRadius": "Avatars (Notifications)", "avatarRadius": "Avatars", @@ -261,6 +277,7 @@ "block_import_error": "Error importing blocks", "blocks_imported": "Blocks imported! Processing them will take a while.", "blocks_tab": "Blocks", + "bot": "This is a bot account", "btnRadius": "Buttons", "cBlue": "Blue (Reply, follow)", "cGreen": "Green (Retweet)", @@ -272,16 +289,16 @@ "change_password": "Change Password", "change_password_error": "There was an issue changing your password.", "changed_password": "Password changed successfully!", + "chatMessageRadius": "Chat message", "collapse_subject": "Collapse posts with subjects", "composing": "Composing", "confirm_new_password": "Confirm new password", - "current_avatar": "Your current avatar", "current_password": "Current password", - "current_profile_banner": "Your current profile banner", + "mutes_and_blocks": "Mutes and Blocks", "data_import_export_tab": "Data Import / Export", "default_vis": "Default visibility scope", "delete_account": "Delete Account", - "delete_account_description": "Permanently delete your account and all your messages.", + "delete_account_description": "Permanently delete your data and deactivate your account.", "delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.", "delete_account_instructions": "Type your password in the input below to confirm account deletion.", "discoverable": "Allow discovery of this account in search results and other services", @@ -327,6 +344,12 @@ "loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")", "mutes_tab": "Mutes", "play_videos_in_modal": "Play videos in a popup frame", + "profile_fields": { + "label": "Profile metadata", + "add_field": "Add Field", + "name": "Label", + "value": "Content" + }, "use_contain_fit": "Don't crop the attachment in thumbnails", "name": "Name", "name_bio": "Name & Bio", @@ -362,7 +385,6 @@ "profile_tab": "Profile", "radii_help": "Set up interface edge rounding (in pixels)", "replies_in_timeline": "Replies in timeline", - "reply_link_preview": "Enable reply-link preview on mouse hover", "reply_visibility_all": "Show all replies", "reply_visibility_following": "Only show replies directed at me or users I'm following", "reply_visibility_self": "Only show replies directed at me", @@ -377,6 +399,12 @@ "set_new_avatar": "Set new avatar", "set_new_profile_background": "Set new profile background", "set_new_profile_banner": "Set new profile banner", + "reset_avatar": "Reset avatar", + "reset_profile_background": "Reset profile background", + "reset_profile_banner": "Reset profile banner", + "reset_avatar_confirm": "Do you really want to reset the avatar?", + "reset_banner_confirm": "Do you really want to reset the banner?", + "reset_background_confirm": "Do you really want to reset the background?", "settings": "Settings", "subject_input_always_show": "Always show subject field", "subject_line_behavior": "Copy subject when replying", @@ -395,7 +423,7 @@ "theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.", "theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.", "tooltipRadius": "Tooltips/alerts", - "type_domains_to_mute": "Type in domains to mute", + "type_domains_to_mute": "Search domains to mute", "upload_a_photo": "Upload a photo", "user_settings": "User Settings", "values": { @@ -406,13 +434,9 @@ "greentext": "Meme arrows", "notifications": "Notifications", "notification_setting_filters": "Filters", - "notification_setting": "Receive notifications from:", - "notification_setting_follows": "Users you follow", - "notification_setting_non_follows": "Users you do not follow", - "notification_setting_followers": "Users who follow you", - "notification_setting_non_followers": "Users who do not follow you", + "notification_setting_block_from_strangers": "Block notifications from users who you do not follow", "notification_setting_privacy": "Privacy", - "notification_setting_privacy_option": "Hide the sender and contents of push notifications", + "notification_setting_hide_notification_contents": "Hide the sender and contents of push notifications", "notification_mutes": "To stop receiving notifications from a specific user, use a mute.", "notification_blocks": "Blocking a user stops all notifications as well as unsubscribes them.", "enable_web_push_notifications": "Enable web push notifications", @@ -433,7 +457,7 @@ "use_source": "New version", "help": { "upgraded_from_v2": "PleromaFE has been upgraded, theme could look a little bit different than you remember.", - "v2_imported": "File you imported was made for older FE. We try to maximize compatibility but there still could be inconsitencies.", + "v2_imported": "File you imported was made for older FE. We try to maximize compatibility but there still could be inconsistencies.", "future_version_imported": "File you imported was made in newer version of FE.", "older_version_imported": "File you imported was made in older version of FE.", "snapshot_present": "Theme snapshot is loaded, so all values are overriden. You can load theme's actual data instead.", @@ -492,7 +516,12 @@ "selectedMenu": "Selected menu item", "disabled": "Disabled", "toggled": "Toggled", - "tabs": "Tabs" + "tabs": "Tabs", + "chat": { + "incoming": "Incoming", + "outgoing": "Outgoing", + "border": "Border" + } }, "radii": { "_tab_label": "Roundness" @@ -604,6 +633,7 @@ "no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated", "repeated": "repeated", "show_new": "Show new", + "reload": "Reload", "up_to_date": "Up-to-date", "no_more_statuses": "No more statuses", "no_statuses": "No statuses" @@ -615,13 +645,21 @@ "pin": "Pin on profile", "unpin": "Unpin from profile", "pinned": "Pinned", + "bookmark": "Bookmark", + "unbookmark": "Unbookmark", "delete_confirm": "Do you really want to delete this status?", "reply_to": "Reply to", "replies_list": "Replies:", "mute_conversation": "Mute conversation", "unmute_conversation": "Unmute conversation", "status_unavailable": "Status unavailable", - "copy_link": "Copy link to status" + "copy_link": "Copy link to status", + "thread_muted": "Thread muted", + "thread_muted_and_words": ", has words:", + "show_full_subject": "Show full subject", + "hide_full_subject": "Hide full subject", + "show_content": "Show content", + "hide_content": "Hide content" }, "user_card": { "approve": "Approve", @@ -642,6 +680,7 @@ "its_you": "It's you!", "media": "Media", "mention": "Mention", + "message": "Message", "mute": "Mute", "muted": "Muted", "per_day": "per day", @@ -651,11 +690,11 @@ "subscribe": "Subscribe", "unsubscribe": "Unsubscribe", "unblock": "Unblock", - "unblock_progress": "Unblocking...", - "block_progress": "Blocking...", + "unblock_progress": "Unblocking…", + "block_progress": "Blocking…", "unmute": "Unmute", - "unmute_progress": "Unmuting...", - "mute_progress": "Muting...", + "unmute_progress": "Unmuting…", + "mute_progress": "Muting…", "hide_repeats": "Hide repeats", "show_repeats": "Show repeats", "admin_menu": { @@ -704,9 +743,10 @@ "add_reaction": "Add Reaction", "user_settings": "User Settings", "accept_follow_request": "Accept follow request", - "reject_follow_request": "Reject follow request" + "reject_follow_request": "Reject follow request", + "bookmark": "Bookmark" }, - "upload":{ + "upload": { "error": { "base": "Upload failed.", "file_too_big": "File too big [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", @@ -739,5 +779,27 @@ "password_reset_disabled": "Password reset is disabled. Please contact your instance administrator.", "password_reset_required": "You must reset your password to log in.", "password_reset_required_but_mailer_is_disabled": "You must reset your password, but password reset is disabled. Please contact your instance administrator." + }, + "chats": { + "you": "You:", + "message_user": "Message {nickname}", + "delete": "Delete", + "chats": "Chats", + "new": "New Chat", + "empty_message_error": "Cannot post empty message", + "more": "More", + "delete_confirm": "Do you really want to delete this message?", + "error_loading_chat": "Something went wrong when loading the chat.", + "error_sending_message": "Something went wrong when sending the message.", + "empty_chat_list_placeholder": "You don't have any chats yet. Start a new chat!" + }, + "file_type": { + "audio": "Audio", + "video": "Video", + "image": "Image", + "file": "File" + }, + "display_date": { + "today": "Today" } } diff --git a/src/i18n/eo.json b/src/i18n/eo.json @@ -5,14 +5,15 @@ "features_panel": { "chat": "Babilejo", "gopher": "Gopher", - "media_proxy": "Aŭdvidaĵa prokurilo", + "media_proxy": "Vidaŭdaĵa prokurilo", "scope_options": "Agordoj de amplekso", - "text_limit": "Teksta limo", + "text_limit": "Limo de teksto", "title": "Funkcioj", - "who_to_follow": "Kiun aboni" + "who_to_follow": "Kiun aboni", + "pleroma_chat_messages": "Babilejo de Pleroma" }, "finder": { - "error_fetching_user": "Eraro alportante uzanton", + "error_fetching_user": "Eraris alporto de uzanto", "find_user": "Trovi uzanton" }, "general": { @@ -20,12 +21,25 @@ "submit": "Sendi", "more": "Pli", "generic_error": "Eraro okazis", - "optional": "Malnepra" + "optional": "malnepra", + "close": "Fermi", + "verify": "Kontroli", + "confirm": "Konfirmi", + "enable": "Ŝalti", + "disable": "Malŝalti", + "cancel": "Nuligi", + "dismiss": "Forlasi", + "show_less": "Montri malplion", + "show_more": "Montri plion", + "retry": "Reprovi", + "error_retry": "Bonvolu reprovi", + "loading": "Enlegante…" }, "image_cropper": { "crop_picture": "Tondi bildon", "save": "Konservi", - "cancel": "Nuligi" + "cancel": "Nuligi", + "save_without_cropping": "Konservi sen tondado" }, "login": { "login": "Saluti", @@ -34,8 +48,16 @@ "password": "Pasvorto", "placeholder": "ekz. lain", "register": "Registriĝi", - "username": "Salutnomo", - "hint": "Salutu por partopreni la diskutadon" + "username": "Uzantonomo", + "hint": "Salutu por partopreni la diskutadon", + "heading": { + "recovery": "Rehavo de duobla aŭtentikigo", + "totp": "Duobla aŭtentikigo" + }, + "recovery_code": "Rehava kodo", + "enter_two_factor_code": "Enigu kodon de duobla aŭtentikigo", + "enter_recovery_code": "Enigu rehavan kodon", + "authentication_code": "Aŭtentikiga kodo" }, "media_modal": { "previous": "Antaŭa", @@ -45,7 +67,7 @@ "about": "Pri", "back": "Reen", "chat": "Loka babilejo", - "friend_requests": "Abonaj petoj", + "friend_requests": "Petoj pri abono", "mentions": "Mencioj", "dms": "Rektaj mesaĝoj", "public_tl": "Publika tempolinio", @@ -53,7 +75,12 @@ "twkn": "La tuta konata reto", "user_search": "Serĉi uzantojn", "who_to_follow": "Kiun aboni", - "preferences": "Agordoj" + "preferences": "Agordoj", + "chats": "Babiloj", + "search": "Serĉi", + "interactions": "Interagoj", + "administration": "Administrado", + "bookmarks": "Legosignoj" }, "notifications": { "broken_favorite": "Nekonata stato, serĉante ĝin…", @@ -63,15 +90,21 @@ "notifications": "Sciigoj", "read": "Legite!", "repeated_you": "ripetis vian staton", - "no_more_notifications": "Neniuj pliaj sciigoj" + "no_more_notifications": "Neniuj pliaj sciigoj", + "reacted_with": "reagis per {0}", + "migrated_to": "migris al", + "follow_request": "volas vin aboni" }, "post_status": { "new_status": "Afiŝi novan staton", - "account_not_locked_warning": "Via konto ne estas {0}. Iu ajn povas vin aboni por vidi viajn afiŝoj nur por abonantoj.", + "account_not_locked_warning": "Via konto ne estas {0}. Iu ajn povas vin aboni por vidi eĉ viajn afiŝoj nur por abonantoj.", "account_not_locked_warning_link": "ŝlosita", - "attachments_sensitive": "Marki kunsendaĵojn kiel konsternajn", + "attachments_sensitive": "Marki kunsendaĵojn konsternaj", "content_type": { - "text/plain": "Plata teksto" + "text/plain": "Plata teksto", + "text/bbcode": "BBCode", + "text/markdown": "Markdown", + "text/html": "HTML" }, "content_warning": "Temo (malnepra)", "default": "Ĵus alvenis al la Universala Kongreso!", @@ -82,7 +115,19 @@ "private": "Nur abonantoj – Afiŝi nur al abonantoj", "public": "Publika – Afiŝi al publikaj tempolinioj", "unlisted": "Nelistigita – Ne afiŝi al publikaj tempolinioj" - } + }, + "scope_notice": { + "unlisted": "Ĉi tiu afiŝo ne estos videbla en la Publika historio kaj La tuta konata reto", + "private": "Ĉi tiu afiŝo estos videbla nur al viaj abonantoj", + "public": "Ĉi tiu afiŝo estos videbla al ĉiuj" + }, + "media_description_error": "Malsukcesis afiŝo de vidaŭdaĵoj; reprovu", + "empty_status_error": "Ne povas afiŝi malplenan staton sen dosieroj", + "preview_empty": "Malplena", + "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_all": "Ĉi tiu afiŝo estos videbla al ĉiuj menciitaj uzantoj.", + "media_description": "Priskribo de vidaŭdaĵo" }, "registration": { "bio": "Priskribo", @@ -92,10 +137,10 @@ "registration": "Registriĝo", "token": "Invita ĵetono", "captcha": "TESTO DE HOMECO", - "new_captcha": "Alklaku la bildon por akiri novan teston", + "new_captcha": "Klaku la bildon por akiri novan teston", "username_placeholder": "ekz. lain", "fullname_placeholder": "ekz. Lain Iwakura", - "bio_placeholder": "ekz.\nSaluton, mi estas Lain\nMi estas animea knabino vivante en Japanujo. Eble vi konas min de la retejo « Wired ».", + "bio_placeholder": "ekz.\nSaluton, mi estas Lain.\nMi estas animea knabino vivanta en Japanujo. Eble vi konas min pro la retejo « Wired ».", "validations": { "username_required": "ne povas resti malplena", "fullname_required": "ne povas resti malplena", @@ -109,53 +154,52 @@ "app_name": "Nomo de aplikaĵo", "attachmentRadius": "Kunsendaĵoj", "attachments": "Kunsendaĵoj", - "autoload": "Ŝalti memfaran enlegadon ĉe subo de paĝo", "avatar": "Profilbildo", "avatarAltRadius": "Profilbildoj (sciigoj)", "avatarRadius": "Profilbildoj", "background": "Fono", "bio": "Priskribo", - "blocks_tab": "Baroj", + "blocks_tab": "Blokitoj", "btnRadius": "Butonoj", - "cBlue": "Blua (Respondo, abono)", - "cGreen": "Verda (Kunhavigo)", - "cOrange": "Oranĝa (Ŝato)", - "cRed": "Ruĝa (Nuligo)", + "cBlue": "Blua (respondi, aboni)", + "cGreen": "Verda (kunhavigi)", + "cOrange": "Oranĝa (ŝati)", + "cRed": "Ruĝa (nuligi)", "change_password": "Ŝanĝi pasvorton", - "change_password_error": "Okazis eraro dum ŝanĝo de via pasvorto.", + "change_password_error": "Eraris ŝanĝo de via pasvorto.", "changed_password": "Pasvorto sukcese ŝanĝiĝis!", "collapse_subject": "Maletendi afiŝojn kun temoj", - "composing": "Verkante", + "composing": "Verkado", "confirm_new_password": "Konfirmu novan pasvorton", "current_avatar": "Via nuna profilbildo", "current_password": "Nuna pasvorto", "current_profile_banner": "Via nuna profila rubando", - "data_import_export_tab": "Enporto / Elporto de datenoj", - "default_vis": "Implicita videbleca amplekso", + "data_import_export_tab": "Enporto / Elporto de datumoj", + "default_vis": "Implicita amplekso de vidibleco", "delete_account": "Forigi konton", - "delete_account_description": "Por ĉiam forigi vian konton kaj ĉiujn viajn mesaĝojn", - "delete_account_error": "Okazis eraro dum forigo de via kanto. Se tio daŭre okazados, bonvolu kontakti la administranton de via nodo.", + "delete_account_description": "Por ĉiam forigi viajn datumojn kaj malaktivigi vian konton.", + "delete_account_error": "Eraris forigo de via kanto. Se tio daŭre ripetiĝos, bonvolu kontakti la administranton de via nodo.", "delete_account_instructions": "Entajpu sube vian pasvorton por konfirmi forigon de konto.", - "avatar_size_instruction": "La rekomendata malpleja grando de profilbildoj estas 150×150 bilderoj.", + "avatar_size_instruction": "La rekomendata minimuma grando de profilbildoj estas 150×150 bilderoj.", "export_theme": "Konservi antaŭagordon", "filtering": "Filtrado", - "filtering_explanation": "Ĉiuj statoj kun tiuj ĉi vortoj silentiĝos, po unu linio", - "follow_export": "Abona elporto", + "filtering_explanation": "Ĉiuj statoj kun tiuj ĉi vortoj silentiĝos; skribu po unu linie", + "follow_export": "Elporto de abonoj", "follow_export_button": "Elporti viajn abonojn al CSV-dosiero", "follow_export_processing": "Traktante; baldaŭ vi ricevos peton elŝuti la dosieron", - "follow_import": "Abona enporto", + "follow_import": "Enporto de abonoj", "follow_import_error": "Eraro enportante abonojn", - "follows_imported": "Abonoj enportiĝis! Traktado daŭros iom.", + "follows_imported": "Abonoj enportiĝis! Traktado daŭros iom da tempo.", "foreground": "Malfono", "general": "Ĝenerala", "hide_attachments_in_convo": "Kaŝi kunsendaĵojn en interparoloj", "hide_attachments_in_tl": "Kaŝi kunsendaĵojn en tempolinio", - "max_thumbnails": "Plej multa nombro da bildetoj po afiŝo", - "hide_isp": "Kaŝi nodo-propran breton", + "max_thumbnails": "Maksimuma nombro da bildetoj en afiŝo", + "hide_isp": "Kaŝi breton propran al nodo", "preload_images": "Antaŭ-enlegi bildojn", "use_one_click_nsfw": "Malfermi konsternajn kunsendaĵojn per nur unu klako", - "hide_post_stats": "Kaŝi statistikon de afiŝoj (ekz. nombron da ŝatoj)", - "hide_user_stats": "Kaŝi statistikon de uzantoj (ekz. nombron da abonantoj)", + "hide_post_stats": "Kaŝi statistikon de afiŝoj (ekz. nombron de ŝatoj)", + "hide_user_stats": "Kaŝi statistikon de uzantoj (ekz. nombron de abonantoj)", "hide_filtered_statuses": "Kaŝi filtritajn statojn", "import_followers_from_a_csv_file": "Enporti abonojn el CSV-dosiero", "import_theme": "Enlegi antaŭagordojn", @@ -170,9 +214,9 @@ "links": "Ligiloj", "lock_account_description": "Limigi vian konton al nur abonantoj aprobitaj", "loop_video": "Ripetadi filmojn", - "loop_video_silent_only": "Ripetadi nur filmojn sen sono (ekz. la \"GIF-ojn\" de Mastodon)", + "loop_video_silent_only": "Ripetadi nur filmojn sen sono (ekz. la «GIF-ojn» de Mastodon)", "mutes_tab": "Silentigoj", - "play_videos_in_modal": "Ludi filmojn rekte en la aŭdvidaĵa spektilo", + "play_videos_in_modal": "Ludi filmojn en ŝpruca kadro", "use_contain_fit": "Ne tondi la kunsendaĵon en bildetoj", "name": "Nomo", "name_bio": "Nomo kaj priskribo", @@ -183,51 +227,50 @@ "notification_visibility_mentions": "Mencioj", "notification_visibility_repeats": "Ripetoj", "no_rich_text_description": "Forigi riĉtekstajn formojn de ĉiuj afiŝoj", - "no_blocks": "Neniuj baroj", + "no_blocks": "Neniuj blokitoj", "no_mutes": "Neniuj silentigoj", "hide_follows_description": "Ne montri kiun mi sekvas", "hide_followers_description": "Ne montri kiu min sekvas", "show_admin_badge": "Montri la insignon de administranto en mia profilo", - "show_moderator_badge": "Montri la insignon de kontrolanto en mia profilo", - "nsfw_clickthrough": "Ŝalti traklakan kaŝon de konsternaj kunsendaĵoj", + "show_moderator_badge": "Montri la insignon de reguligisto en mia profilo", + "nsfw_clickthrough": "Ŝalti traklakan kaŝadon de konsternaj kunsendaĵoj", "oauth_tokens": "Ĵetonoj de OAuth", "token": "Ĵetono", - "refresh_token": "Ĵetono de novigo", + "refresh_token": "Ĵetono de aktualigo", "valid_until": "Valida ĝis", "revoke_token": "Senvalidigi", "panelRadius": "Bretoj", "pause_on_unfocused": "Paŭzigi elsendfluon kiam langeto ne estas fokusata", "presets": "Antaŭagordoj", - "profile_background": "Profila fono", - "profile_banner": "Profila rubando", + "profile_background": "Fono de profilo", + "profile_banner": "Rubando de profilo", "profile_tab": "Profilo", "radii_help": "Agordi fasadan rondigon de randoj (bildere)", "replies_in_timeline": "Respondoj en tempolinio", - "reply_link_preview": "Ŝalti respond-ligilan antaŭvidon dum musa ŝvebo", "reply_visibility_all": "Montri ĉiujn respondojn", "reply_visibility_following": "Montri nur respondojn por mi aŭ miaj abonatoj", "reply_visibility_self": "Montri nur respondojn por mi", - "saving_err": "Eraro dum konservo de agordoj", + "saving_err": "Eraris konservado de agordoj", "saving_ok": "Agordoj konserviĝis", "security_tab": "Sekureco", "scope_copy": "Kopii amplekson por respondo (rektaj mesaĝoj ĉiam kopiiĝas)", "set_new_avatar": "Agordi novan profilbildon", - "set_new_profile_background": "Agordi novan profilan fonon", - "set_new_profile_banner": "Agordi novan profilan rubandon", + "set_new_profile_background": "Agordi novan fonon de profilo", + "set_new_profile_banner": "Agordi novan rubandon de profilo", "settings": "Agordoj", - "subject_input_always_show": "Ĉiam montri teman kampon", - "subject_line_behavior": "Kopii temon por respondo", - "subject_line_email": "Kiel retpoŝto: \"re: temo\"", + "subject_input_always_show": "Ĉiam montri kampon de temo", + "subject_line_behavior": "Kopii temon dum respondado", + "subject_line_email": "Kiel retpoŝto: «re: temo»", "subject_line_mastodon": "Kiel Mastodon: kopii senŝanĝe", "subject_line_noop": "Ne kopii", "post_status_content_type": "Afiŝi specon de la enhavo de la stato", - "stop_gifs": "Movi GIF-bildojn dum musa ŝvebo", - "streaming": "Ŝalti memfaran fluigon de novaj afiŝoj ĉe la supro de la paĝo", + "stop_gifs": "Movi GIF-bildojn dum ŝvebo de muso", + "streaming": "Ŝalti memagan fluigon de novaj afiŝoj kiam vi vidas la supron de la paĝo", "text": "Teksto", "theme": "Haŭto", "theme_help": "Uzu deksesumajn kolorkodojn (#rrvvbb) por adapti vian koloran haŭton.", - "theme_help_v2_1": "Vi ankaŭ povas superagordi la kolorojn kaj travideblecon de kelkaj eroj per marko de la markbutono; uzu la butonon \"Vakigi ĉion\" por forigi ĉîujn superagordojn.", - "theme_help_v2_2": "Bildsimboloj sub kelkaj eroj estas indikiloj de kontrasto inter fono kaj teksto; muse ŝvebu por detalaj informoj. Bonvolu memori, ke la indikilo montras la plej malbonan okazeblon dum sia uzo.", + "theme_help_v2_1": "Vi ankaŭ povas superagordi la kolorojn kaj travideblecon de kelkaj eroj per marko de la markbutono; uzu la butonon «Vakigi ĉion» por forigi ĉîujn superagordojn.", + "theme_help_v2_2": "Bildsimboloj sub kelkaj eroj estas indikiloj de kontrasto inter fono kaj teksto; ŝvebigu muson por detalaj informoj. Bonvolu memori, ke la indikilo montras la plej malbonan okazeblon dum sia uzo.", "tooltipRadius": "Ŝpruchelpiloj/avertoj", "upload_a_photo": "Alŝuti foton", "user_settings": "Agordoj de uzanto", @@ -236,7 +279,7 @@ "true": "jes" }, "notifications": "Sciigoj", - "enable_web_push_notifications": "Ŝalti retajn puŝajn sciigojn", + "enable_web_push_notifications": "Ŝalti retajn pasivajn sciigojn", "style": { "switcher": { "keep_color": "Konservi kolorojn", @@ -244,10 +287,22 @@ "keep_opacity": "Konservi maltravideblecon", "keep_roundness": "Konservi rondecon", "keep_fonts": "Konservi tiparojn", - "save_load_hint": "Elektebloj de \"konservi\" konservas la nuntempajn agordojn dum elektado aŭ enlegado de haŭtoj. Ĝi ankaŭ konservas tiujn agordojn dum elportado de haŭto. Kun ĉiuj markbutonoj nemarkitaj, elporto de la haŭto ĉion konservos.", + "save_load_hint": "Elektebloj de «konservi» konservas la nuntempajn agordojn dum elektado aŭ enlegado de haŭtoj. Ĝi ankaŭ konservas tiujn agordojn dum elportado de haŭto. Kun ĉiuj markbutonoj nemarkitaj, elporto de la haŭto ĉion konservos.", "reset": "Restarigi", "clear_all": "Vakigi ĉion", - "clear_opacity": "Vakigi maltravideblecon" + "clear_opacity": "Vakigi maltravideblecon", + "help": { + "fe_downgraded": "Versio de PleromaFE reen iris.", + "fe_upgraded": "La motoro de haŭtoj de PleromaFE ĝisdatiĝis post ĝisdatigo de la versio.", + "older_version_imported": "La enportita dosiero estis farita per pli malnova versio de PleromaFE.", + "future_version_imported": "La enportita dosiero estis farita per pli nova versio de PleromaFE.", + "v2_imported": "La dosiero, kiun vi enportis, estis farita por malnova versio de PleromaFE. Ni provas maksimumigi interkonformecon, sed tamen eble montriĝos misoj.", + "upgraded_from_v2": "PleromaFE estis ĝisdatigita; la haŭto eble aspektos malsame ol kiel vi ĝin memoras." + }, + "use_source": "Nova versio", + "use_snapshot": "Malnova versio", + "keep_as_is": "Teni senŝanĝa", + "load_theme": "Enlegi haŭton" }, "common": { "color": "Koloro", @@ -255,7 +310,7 @@ "contrast": { "hint": "Proporcio de kontrasto estas {ratio}, ĝi {level} {context}", "level": { - "aa": "plenumas la gvidilon je nivelo AA (malpleja)", + "aa": "plenumas la gvidilon je nivelo AA (minimuma)", "aaa": "plenumas la gvidilon je nivela AAA (rekomendita)", "bad": "plenumas neniujn faciluzajn gvidilojn" }, @@ -268,21 +323,39 @@ "common_colors": { "_tab_label": "Komunaj", "main": "Komunaj koloroj", - "foreground_hint": "Vidu langeton \"Specialaj\" por pli detalaj agordoj", + "foreground_hint": "Vidu langeton «Specialaj» por pli detalaj agordoj", "rgbo": "Bildsimboloj, emfazoj, insignoj" }, "advanced_colors": { "_tab_label": "Specialaj", - "alert": "Averta fono", + "alert": "Fono de averto", "alert_error": "Eraro", - "badge": "Insigna fono", + "badge": "Fono de insigno", "badge_notification": "Sciigo", "panel_header": "Kapo de breto", "top_bar": "Supra breto", "borders": "Limoj", "buttons": "Butonoj", "inputs": "Enigaj kampoj", - "faint_text": "Malvigla teksto" + "faint_text": "Malvigla teksto", + "chat": { + "border": "Limo", + "outgoing": "Eliraj", + "incoming": "Envenaj" + }, + "tabs": "Langetoj", + "disabled": "Malŝaltita", + "selectedMenu": "Elektita menuero", + "selectedPost": "Elektita afiŝo", + "pressed": "Premita", + "highlight": "Emfazitaj eroj", + "icons": "Bildsimboloj", + "poll": "Grafo de enketo", + "underlay": "Subtavolo", + "popover": "Ŝpruchelpiloj, menuoj", + "post": "Afiŝoj/Priskriboj de uzantoj", + "alert_neutral": "Neŭtrala", + "alert_warning": "Averto" }, "radii": { "_tab_label": "Rondeco" @@ -297,7 +370,7 @@ "inset": "Internigo", "hint": "Por ombroj vi ankaŭ povas uzi --variable kiel koloran valoron, por uzi variantojn de CSS3. Bonvolu rimarki, ke tiuokaze agordoj de maltravidebleco ne funkcios.", "filter_hint": { - "always_drop_shadow": "Averto: ĉi tiu ombro ĉiam uzas {0} kiam la foliumilo ĝin subtenas.", + "always_drop_shadow": "Averto: ĉi tiu ombro ĉiam uzas {0} kiam la foliumilo tion subtenas.", "drop_shadow_syntax": "{0} ne subtenas parametron {1} kaj ŝlosilvorton {2}.", "avatar_inset": "Bonvolu rimarki, ke agordi ambaŭ internajn kaj eksterajn ombrojn por profilbildoj povas redoni neatenditajn rezultojn ĉe profilbildoj travideblaj.", "spread_zero": "Ombroj kun vastigo > 0 aperos kvazaŭ ĝi estus fakte nulo", @@ -313,13 +386,13 @@ "button": "Butono", "buttonHover": "Butono (je ŝvebo)", "buttonPressed": "Butono (premita)", - "buttonPressedHover": "Butono (premita je ŝvebo)", + "buttonPressedHover": "Butono (premita kaj je ŝvebo)", "input": "Eniga kampo" } }, "fonts": { "_tab_label": "Tiparoj", - "help": "Elektu tiparon uzotan por eroj de la fasado. Por \"propra\" vi devas enigi la precizan nomon de tiparo tiel, kiel ĝi aperas en la sistemo", + "help": "Elektu tiparon uzotan por eroj de la fasado. Por «propra» vi devas enigi la precizan nomon de tiparo tiel, kiel ĝi aperas en la sistemo.", "components": { "interface": "Fasado", "input": "Enigaj kampoj", @@ -345,66 +418,175 @@ "checkbox": "Mi legetis la kondiĉojn de uzado", "link": "bela eta ligil’" } - } + }, + "discoverable": "Permesi trovon de ĉi tiu konto en serĉrezultoj kaj aliaj servoj", + "mutes_and_blocks": "Silentigitoj kaj blokitoj", + "chatMessageRadius": "Babileja mesaĝo", + "changed_email": "Retpoŝtadreso sukcese ŝanĝiĝis!", + "change_email_error": "Eraris ŝanĝo de via retpoŝtadreso.", + "change_email": "Ŝanĝi retpoŝtadreson", + "bot": "Ĉi tio estas robota konto", + "blocks_imported": "Blokitoj enportiĝis! Traktado daŭros iom da tempo.", + "block_import_error": "Eraris enporto de blokitoj", + "block_export": "Elporto de blokitoj", + "block_import": "Enporto de blokitoj", + "block_export_button": "Elporti viajn blokitojn al CSV-dosiero", + "allow_following_move": "Permesi memagan abonadon kiam abonata konto migras", + "mfa": { + "verify": { + "desc": "Por ŝalti duoblan aŭtentikigon, enigu la kodon el via aplikaĵo por duobla aŭtentikigo:" + }, + "scan": { + "secret_code": "Ŝlosilo", + "desc": "Uzante vian aplikaĵon por duobla aŭtentikigo, skanu ĉi tiun rapidrespondan kodon aŭ enigu tekstan ŝlosilon:", + "title": "Skani" + }, + "authentication_methods": "Metodoj de aŭtentikigo", + "recovery_codes_warning": "Notu la kodojn aŭ konservu ilin en sekura loko – alie vi ne revidos ilin. Se vi perdos aliron al via aplikaĵo por duobla aŭtentikigo kaj al la rehavaj kodoj, vi ne povos aliri vian konton.", + "waiting_a_recovery_codes": "Ricevante savkopiajn kodojn…", + "recovery_codes": "Rehavaj kodoj.", + "warning_of_generate_new_codes": "Kiam vi estigos novajn rehavajn kodojn, viaj malnovaj ne plu funkcios.", + "generate_new_recovery_codes": "Estigi novajn rehavajn kodojn", + "title": "Duobla aŭtentikigo", + "otp": "OTP" + }, + "enter_current_password_to_confirm": "Enigu vian pasvorton por konfirmi vian identecon", + "security": "Sekureco", + "fun": "Amuzo", + "type_domains_to_mute": "Serĉu silentigotajn retnomojn", + "useStreamingApiWarning": "(Nerekomendate, eksperimente, povas preterpasi afiŝojn)", + "useStreamingApi": "Ricevi afiŝojn kaj sciigojn realtempe", + "user_mutes": "Uzantoj", + "reset_background_confirm": "Ĉu vi certe volas restarigi la fonon?", + "reset_banner_confirm": "Ĉu vi certe volas restarigi la rubandon?", + "reset_avatar_confirm": "Ĉu vi certe volas restarigi la profilbildon?", + "reset_profile_banner": "Restarigi rubandon de profilo", + "reset_profile_background": "Restarigi fonon de profilo", + "reset_avatar": "Restarigi profilbildon", + "minimal_scopes_mode": "Minimumigi elekteblojn pri amplekso de afiŝoj", + "search_user_to_block": "Serĉu, kiun vi volas bloki", + "search_user_to_mute": "Serĉu, kiun vi volas silentigi", + "autohide_floating_post_button": "Memage kaŝi la butonon por Nova afiŝo (poŝtelefone)", + "hide_followers_count_description": "Ne montri nombron de abonantoj", + "hide_follows_count_description": "Ne montri nombron de abonoj", + "notification_visibility_emoji_reactions": "Reagoj", + "notification_visibility_moves": "Migroj", + "new_email": "Nova retpoŝtadreso", + "profile_fields": { + "value": "Enhavo", + "name": "Etikedo", + "add_field": "Aldoni kampon", + "label": "Pridatumoj de profilo" + }, + "import_blocks_from_a_csv_file": "Enporti blokitojn el CSV-dosiero", + "hide_muted_posts": "Kaŝi afiŝojn de silentigitaj uzantoj", + "emoji_reactions_on_timeline": "Montri bildosignajn reagojn en la tempolinio", + "pad_emoji": "Meti spacetojn ĉirkaŭ bildosigno post ties elekto", + "domain_mutes": "Retnomoj", + "notification_blocks": "Blokinte uzanton vi malabonos ĝin kaj haltigos ĉiujn sciigojn.", + "notification_mutes": "Por ne plu ricevi sciigojn de certa uzanto, silentigu.", + "notification_setting_hide_notification_contents": "Kaŝi la sendinton kaj la enhavojn de pasivaj sciigoj", + "notification_setting_privacy": "Privateco", + "notification_setting_block_from_strangers": "Bloki sciigojn de uzantoj, kiujn vi ne abonas", + "notification_setting_filters": "Filtriloj", + "greentext": "Memecitaĵoj", + "version": { + "frontend_version": "Versio de fasado", + "backend_version": "Versio de internaĵo", + "title": "Versio" + }, + "accent": "Emfazo" }, "timeline": { "collapse": "Maletendi", "conversation": "Interparolo", - "error_fetching": "Eraro dum ĝisdatigo", + "error_fetching": "Eraris ĝisdatigo", "load_older": "Montri pli malnovajn statojn", "no_retweet_hint": "Afiŝo estas markita kiel rekta aŭ nur por abonantoj, kaj ne eblas ĝin ripeti", "repeated": "ripetita", "show_new": "Montri novajn", "up_to_date": "Ĝisdata", "no_more_statuses": "Neniuj pliaj statoj", - "no_statuses": "Neniuj statoj" + "no_statuses": "Neniuj statoj", + "reload": "Enlegi ree" }, "user_card": { "approve": "Aprobi", - "block": "Bari", - "blocked": "Barita!", + "block": "Bloki", + "blocked": "Blokita!", "deny": "Rifuzi", "favorites": "Ŝatataj", "follow": "Aboni", "follow_sent": "Peto sendiĝis!", - "follow_progress": "Petanta…", - "follow_again": "Ĉu sendi peton denove?", + "follow_progress": "Petante…", + "follow_again": "Ĉu sendi peton ree?", "follow_unfollow": "Malaboni", "followees": "Abonatoj", "followers": "Abonantoj", - "following": "Abonanta!", + "following": "Abonata!", "follows_you": "Abonas vin!", "its_you": "Tio estas vi!", - "media": "Aŭdvidaĵoj", + "media": "Vidaŭdaĵoj", "mute": "Silentigi", - "muted": "Silentigitaj", + "muted": "Silentigita", "per_day": "tage", "remote_follow": "Fore aboni", "statuses": "Statoj", - "unblock": "Malbari", - "unblock_progress": "Malbaranta…", - "block_progress": "Baranta…", + "unblock": "Malbloki", + "unblock_progress": "Malblokante…", + "block_progress": "Blokante…", "unmute": "Malsilentigi", - "unmute_progress": "Malsilentiganta…", - "mute_progress": "Silentiganta…" + "unmute_progress": "Malsilentigante…", + "mute_progress": "Silentigante…", + "report": "Raporti", + "message": "Mesaĝo", + "mention": "Mencio", + "hidden": "Kaŝita", + "admin_menu": { + "delete_user_confirmation": "Ĉu vi tute certas? Ĉi tiu ago ne estas malfarebla.", + "delete_user": "Forigi uzanton", + "quarantine": "Malpermesi federadon de afiŝoj de uzanto", + "disable_any_subscription": "Malpermesi ĉian abonadon al uzanto", + "disable_remote_subscription": "Malpermesi abonadon al uzanto el foraj nodoj", + "sandbox": "Devigi afiŝojn esti nur por abonantoj", + "force_unlisted": "Devigi afiŝojn nelistiĝi", + "strip_media": "Forigi vidaŭdaĵojn de afiŝoj", + "force_nsfw": "Marki ĉiujn afiŝojn konsternaj", + "delete_account": "Forigi konton", + "deactivate_account": "Malaktivigi konton", + "activate_account": "Aktivigi konton", + "revoke_moderator": "Malnomumi reguligiston", + "grant_moderator": "Nomumi reguligiston", + "revoke_admin": "Malnomumi administranton", + "grant_admin": "Nomumi administranton", + "moderation": "Reguligado" + }, + "show_repeats": "Montri ripetojn", + "hide_repeats": "Kaŝi ripetojn", + "unsubscribe": "Ne ricevi sciigojn", + "subscribe": "Ricevi sciigojn" }, "user_profile": { - "timeline_title": "Uzanta tempolinio", + "timeline_title": "Historio de uzanto", "profile_does_not_exist": "Pardonu, ĉi tiu profilo ne ekzistas.", - "profile_loading_error": "Pardonu, eraro okazis dum enlegado de ĉi tiu profilo." + "profile_loading_error": "Pardonu, eraris enlego de ĉi tiu profilo." }, "who_to_follow": { "more": "Pli", "who_to_follow": "Kiun aboni" }, "tool_tip": { - "media_upload": "Alŝuti aŭdvidaĵon", + "media_upload": "Alŝuti vidaŭdaĵon", "repeat": "Ripeti", "reply": "Respondi", "favorite": "Ŝati", - "user_settings": "Agordoj de uzanto" + "user_settings": "Agordoj de uzanto", + "bookmark": "Legosigno", + "reject_follow_request": "Rifuzi abonpeton", + "accept_follow_request": "Akcepti abonpeton", + "add_reaction": "Aldoni reagon" }, - "upload":{ + "upload": { "error": { "base": "Alŝuto malsukcesis.", "file_too_big": "Dosiero estas tro granda [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", @@ -417,5 +599,198 @@ "GiB": "GiB", "TiB": "TiB" } + }, + "emoji": { + "search_emoji": "Serĉi bildosignon", + "keep_open": "Teni elektilon malfermita", + "emoji": "Bildsignoj", + "stickers": "Glumarkoj", + "add_emoji": "Enigi bildosignon", + "load_all": "Enlegante ĉiujn {emojiAmount} bildosignojn", + "load_all_hint": "Enlegis la {saneAmount} unuajn bildosignojn; enlego de ĉiuj povus kaŭzi problemojn pri efikeco.", + "unicode": "Unikoda bildosigno", + "custom": "Propra bildosigno" + }, + "polls": { + "not_enough_options": "Tro malmultaj unikaj elektebloj en la enketo", + "expired": "Enketo finiĝis antaŭ {0}", + "expires_in": "Enketo finiĝas je {0}", + "expiry": "Aĝo de enketo", + "multiple_choices": "Pluraj elektoj", + "single_choice": "Unu elekto", + "type": "Speco de enketo", + "vote": "Voĉi", + "votes": "voĉoj", + "option": "Elekteblo", + "add_option": "Aldoni elekteblon", + "add_poll": "Aldoni enketon" + }, + "importer": { + "error": "Eraris enporto de ĉi tiu dosiero.", + "success": "Enportita sukcese.", + "submit": "Sendi" + }, + "exporter": { + "processing": "Traktante; baldaŭ vi ricevos peton elŝuti vian dosieron", + "export": "Elporti" + }, + "domain_mute_card": { + "unmute_progress": "Malsilentigante…", + "unmute": "Malsilentigi", + "mute_progress": "Silentigante…", + "mute": "Silentigi" + }, + "about": { + "staff": "Skipo", + "mrf": { + "simple": { + "media_nsfw_desc": "Ĉi tiu nodo devigas vidaŭdaĵojn esti markitaj kiel konsternaj en afiŝoj el la jenaj nodoj:", + "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": "Forigo de vidaŭdaĵoj", + "ftl_removal": "Forigo de la historio de «La tuta konata reto»", + "quarantine_desc": "Ĉi tiu nodo sendos nur publikajn afiŝojn al la jenaj nodoj:", + "quarantine": "Kvaranteno", + "reject_desc": "Ĉi tiu nodo ne akceptos mesaĝojn de la jenaj nodoj:", + "reject": "Rifuzi", + "accept_desc": "Ĉi tiu nodo nur akceptas mesaĝojn de la jenaj nodoj:", + "accept": "Akcepti", + "simple_policies": "Specialaj politikoj de la nodo" + }, + "mrf_policies": "Ŝaltis politikon de Mesaĝa ŝanĝilaro (MRF)", + "keyword": { + "is_replaced_by": "→", + "replace": "Anstataŭigi", + "reject": "Rifuzi", + "ftl_removal": "Forigo de la historio de «La tuta konata reto»", + "keyword_policies": "Politiko pri ŝlosilvortoj" + }, + "federation": "Federado", + "mrf_policies_desc": "Politikoj de Mesaĝa ŝanĝilaro (MRF) efikas sur federa konduto de la nodo. La sekvaj politikoj estas ŝaltitaj:" + } + }, + "selectable_list": { + "select_all": "Elekti ĉion" + }, + "remote_user_resolver": { + "error": "Netrovinte.", + "searching_for": "Serĉante", + "remote_user_resolver": "Trovilo de foraj uzantoj" + }, + "interactions": { + "load_older": "Enlegi pli malnovajn interagojn", + "moves": "Migrado de uzantoj", + "follows": "Novaj abonoj", + "favs_repeats": "Ripetoj kaj ŝatoj" + }, + "errors": { + "storage_unavailable": "Pleroma ne povis aliri deponejon de la foliumilo. Via saluto kaj viaj lokaj agordoj ne estos konservitaj, kaj vi eble renkontos neatenditajn problemojn. Provu permesi kuketojn." + }, + "status": { + "hide_content": "Kaŝi enhavon", + "show_content": "Montri enhavon", + "hide_full_subject": "Kaŝi plenan temon", + "show_full_subject": "Montri plenan temon", + "thread_muted_and_words": ", enhavas vortojn:", + "thread_muted": "Fadeno silentigita", + "copy_link": "Kopii ligilon al stato", + "status_unavailable": "Stato ne estas disponebla", + "unmute_conversation": "Malsilentigi interparolon", + "mute_conversation": "Silentigi interparolon", + "replies_list": "Respondoj:", + "reply_to": "Responde al", + "delete_confirm": "Ĉu vi certe volas forigi ĉi tiun staton?", + "unbookmark": "Senlegosigni", + "bookmark": "Legosigni", + "pinned": "Fiksita", + "unpin": "Malfiksi de profilo", + "pin": "Fiksi al profilo", + "delete": "Forigi staton", + "repeats": "Ripetoj", + "favorites": "Ŝatataj" + }, + "time": { + "years_short": "{0}j", + "year_short": "{0}j", + "years": "{0} jaroj", + "year": "{0} jaro", + "weeks_short": "{0}s", + "week_short": "{0}s", + "weeks": "{0} semajnoj", + "week": "{0} semajno", + "seconds_short": "{0}s", + "second_short": "{0}s", + "seconds": "{0} sekundoj", + "second": "{0} sekundo", + "now_short": "nun", + "now": "ĵus", + "months_short": "{0}m", + "month_short": "{0}m", + "months": "{0} monatoj", + "month": "{0} monato", + "minutes_short": "{0}m", + "minute_short": "{0}m", + "minutes": "{0} minutoj", + "minute": "{0} minuto", + "in_past": "antaŭ {0}", + "in_future": "post {0}", + "hours_short": "{0}h", + "hour_short": "{0}h", + "hours": "{0} horoj", + "hour": "{0} horo", + "days_short": "{0}t", + "day_short": "{0}t", + "days": "{0} tagoj", + "day": "{0} tago" + }, + "search": { + "people": "Personoj", + "no_results": "Neniuj rezultoj", + "people_talking": "{count} personoj parolas", + "person_talking": "{count} persono parolas", + "hashtags": "Kradvortoj" + }, + "display_date": { + "today": "Hodiaŭ" + }, + "file_type": { + "file": "Dosiero", + "image": "Bildo", + "video": "Filmo", + "audio": "Sono" + }, + "chats": { + "empty_chat_list_placeholder": "Vi ankoraŭ havas neniun babilon. Komencu novan babilon!", + "error_sending_message": "Io misokazis dum sendado de la mesaĝo.", + "error_loading_chat": "Io misokazis dum enlego de la babilo.", + "delete_confirm": "Ĉu vi certe volas forigi ĉi tiun mesaĝon?", + "more": "Pli", + "empty_message_error": "Ne povas sendi malplenan mesaĝon", + "new": "Nova babilo", + "chats": "Babiloj", + "delete": "Forigi", + "you": "Vi:" + }, + "password_reset": { + "password_reset_required_but_mailer_is_disabled": "Vi devas restarigi vian pasvorton, sed restarigado de pasvortoj estas malŝaltita. Bonvolu kontakti la administranton de via nodo.", + "password_reset_required": "Vi devas restarigi vian pasvorton por saluti.", + "password_reset_disabled": "Restarigado de pasvortoj estas malŝaltita. Bonvolu kontakti la administranton de via nodo.", + "too_many_requests": "Vi atingis la limon de provoj, reprovu pli poste.", + "not_found": "Ni ne trovis tiun retpoŝtadreson aŭ uzantonomon.", + "return_home": "Reiri al la hejmpaĝo", + "check_email": "Kontrolu vian retpoŝton pro ligilo por restarigi vian pasvorton.", + "placeholder": "Via retpoŝtadreso aŭ uzantonomo", + "instruction": "Enigu vian retpoŝtadreson aŭ uzantonomon. Ni sendos al vi ligilon por restarigi vian pasvorton.", + "password_reset": "Restarigi pasvorton", + "forgot_password": "Ĉu vi forgesis pasvorton?" + }, + "user_reporting": { + "generic_error": "Eraris traktado de via peto.", + "submit": "Sendi", + "forward_to": "Plusendi al {0}", + "forward_description": "La konto venas de alia servilo. Ĉu kopio de la raporto sendiĝu ankaŭ tien?", + "additional_comments": "Aldonaj komentoj", + "add_comment_description": "Ĉi tiu raporto sendiĝos al reguligistoj de via nodo. Vi povas komprenigi kial vi raportas ĉi tiun konton sube:", + "title": "Raportante {0}" } } diff --git a/src/i18n/es.json b/src/i18n/es.json @@ -57,12 +57,12 @@ "enter_recovery_code": "Inserta el código de recuperación", "enter_two_factor_code": "Inserta el código de dos factores", "recovery_code": "Código de recuperación", - "heading" : { - "totp" : "Autenticación de dos factores", - "recovery" : "Recuperación de dos factores" + "heading": { + "totp": "Autenticación de dos factores", + "recovery": "Recuperación de dos factores" } }, - "media_modal": { + "media_modal": { "previous": "Anterior", "next": "Siguiente" }, @@ -103,7 +103,7 @@ "single_choice": "Elección única", "multiple_choices": "Elección múltiple", "expiry": "Tiempo de vida de la encuesta", - "expires_in": "La encuensta termina en {0}", + "expires_in": "La encuesta termina en {0}", "expired": "La encuesta terminó hace {0}", "not_enough_options": "Muy pocas opciones únicas en la encuesta" }, @@ -137,7 +137,7 @@ }, "content_warning": "Tema (opcional)", "default": "Acabo de aterrizar en L.A.", - "direct_warning_to_all": "Esta publicación será visible para todos los usarios mencionados.", + "direct_warning_to_all": "Esta publicación será visible para todos los usuarios mencionados.", "direct_warning_to_first_only": "Esta publicación solo será visible para los usuarios mencionados al comienzo del mensaje.", "posting": "Publicando", "scope_notice": { @@ -146,7 +146,7 @@ "unlisted": "Esta publicación no será visible en la Línea Temporal Pública ni en Toda La Red Conocida" }, "scope": { - "direct": "Directo - Solo para los usuarios mencionados.", + "direct": "Directo - Solo para los usuarios mencionados", "private": "Solo-seguidores - Solo tus seguidores leerán la publicación", "public": "Público - Entradas visibles en las Líneas Temporales Públicas", "unlisted": "Sin listar - Entradas no visibles en las Líneas Temporales Públicas" @@ -173,7 +173,7 @@ "password_confirmation_match": "la contraseña no coincide" } }, - "selectable_list": { + "selectable_list": { "select_all": "Seleccionar todo" }, "settings": { @@ -181,17 +181,17 @@ "security": "Seguridad", "enter_current_password_to_confirm": "Introduce la contraseña actual para confirmar tu identidad", "mfa": { - "otp" : "OTP", - "setup_otp" : "Configurar OTP", - "wait_pre_setup_otp" : "preconfiguración OTP", - "confirm_and_enable" : "Confirmar y habilitar OTP", + "otp": "OTP", + "setup_otp": "Configurar OTP", + "wait_pre_setup_otp": "preconfiguración OTP", + "confirm_and_enable": "Confirmar y habilitar OTP", "title": "Autentificación de dos factores", - "generate_new_recovery_codes" : "Generar códigos de recuperación nuevos", - "warning_of_generate_new_codes" : "Cuando generas nuevos códigos de recuperación, los antiguos dejarán de funcionar.", - "recovery_codes" : "Códigos de recuperación.", + "generate_new_recovery_codes": "Generar códigos de recuperación nuevos", + "warning_of_generate_new_codes": "Cuando generas nuevos códigos de recuperación, los antiguos dejarán de funcionar.", + "recovery_codes": "Códigos de recuperación.", "waiting_a_recovery_codes": "Recibiendo códigos de respaldo", - "recovery_codes_warning" : "Anote los códigos o guárdelos en un lugar seguro, de lo contrario no los volverá a ver. Si pierde el acceso a su aplicación 2FA y los códigos de recuperación, su cuenta quedará bloqueada.", - "authentication_methods" : "Métodos de autentificación", + "recovery_codes_warning": "Anote los códigos o guárdelos en un lugar seguro, de lo contrario no los volverá a ver. Si pierde el acceso a su aplicación 2FA y los códigos de recuperación, su cuenta quedará bloqueada.", + "authentication_methods": "Métodos de autentificación", "scan": { "title": "Escanear", "desc": "Usando su aplicación de dos factores, escanee este código QR o ingrese la clave de texto:", @@ -203,14 +203,13 @@ }, "attachmentRadius": "Adjuntos", "attachments": "Adjuntos", - "autoload": "Habilitar carga automática al llegar al final de la página", "avatar": "Avatar", "avatarAltRadius": "Avatares (Notificaciones)", "avatarRadius": "Avatares", "background": "Fondo", "bio": "Biografía", "block_export": "Exportar usuarios bloqueados", - "block_export_button": "Exporta la lista de tus usarios bloqueados a un archivo csv", + "block_export_button": "Exporta la lista de tus usuarios bloqueados a un archivo csv", "block_import": "Importar usuarios bloqueados", "block_import_error": "Error importando la lista de usuarios bloqueados", "blocks_imported": "¡Lista de usuarios bloqueados importada! El procesado puede tardar un poco.", @@ -222,7 +221,7 @@ "cRed": "Rojo (Cancelar)", "change_password": "Cambiar contraseña", "change_password_error": "Hubo un problema cambiando la contraseña.", - "changed_password": "Contraseña cambiada correctamente!", + "changed_password": "¡Contraseña cambiada correctamente!", "collapse_subject": "Colapsar entradas con tema", "composing": "Redactando", "confirm_new_password": "Confirmar la nueva contraseña", @@ -286,7 +285,7 @@ "notification_visibility_repeats": "Repeticiones (Repeats)", "no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las entradas", "no_blocks": "No hay usuarios bloqueados", - "no_mutes": "No hay usuarios sinlenciados", + "no_mutes": "No hay usuarios silenciados", "hide_follows_description": "No mostrar a quién sigo", "hide_followers_description": "No mostrar quién me sigue", "hide_follows_count_description": "No mostrar el número de cuentas que sigo", @@ -305,9 +304,8 @@ "profile_background": "Fondo del Perfil", "profile_banner": "Cabecera del Perfil", "profile_tab": "Perfil", - "radii_help": "Estable el redondeo de las esquinas de la interfaz (en píxeles)", + "radii_help": "Establezca el redondeo de las esquinas de la interfaz (en píxeles)", "replies_in_timeline": "Réplicas en la línea temporal", - "reply_link_preview": "Activar la previsualización del enlace de responder al pasar el ratón por encima", "reply_visibility_all": "Mostrar todas las réplicas", "reply_visibility_following": "Solo mostrar réplicas para mí o usuarios a los que sigo", "reply_visibility_self": "Solo mostrar réplicas para mí", @@ -337,18 +335,13 @@ "theme_help_v2_1": "También puede invalidar los colores y la opacidad de ciertos componentes si activa la casilla de verificación. Use el botón \"Borrar todo\" para deshacer los cambios.", "theme_help_v2_2": "Los iconos debajo de algunas entradas son indicadores de contraste de fondo/texto, desplace el ratón por encima para obtener información más detallada. Tenga en cuenta que cuando se utilizan indicadores de contraste de transparencia se muestra el peor caso posible.", "tooltipRadius": "Información/alertas", - "upload_a_photo": "Subir una foto", + "upload_a_photo": "Subir una foto", "user_settings": "Ajustes del Usuario", "values": { "false": "no", "true": "sí" }, "notifications": "Notificaciones", - "notification_setting": "Recibir notificaciones de:", - "notification_setting_follows": "Usuarios que sigues", - "notification_setting_non_follows": "Usuarios que no sigues", - "notification_setting_followers": "Usuarios que te siguen", - "notification_setting_non_followers": "Usuarios que no te siguen", "notification_mutes": "Para dejar de recibir notificaciones de un usuario específico, siléncialo.", "notification_blocks": "El bloqueo de un usuario detiene todas las notificaciones y también las cancela.", "enable_web_push_notifications": "Habilitar las notificiaciones en el navegador", @@ -583,7 +576,7 @@ "profile_does_not_exist": "Lo sentimos, este perfil no existe.", "profile_loading_error": "Lo sentimos, hubo un error al cargar este perfil." }, - "user_reporting": { + "user_reporting": { "title": "Reportando a {0}", "add_comment_description": "El informe será enviado a los moderadores de su instancia. Puedes proporcionar una explicación de por qué estás reportando esta cuenta a continuación:", "additional_comments": "Comentarios adicionales", @@ -603,7 +596,7 @@ "favorite": "Favorito", "user_settings": "Ajustes de usuario" }, - "upload":{ + "upload": { "error": { "base": "Subida fallida.", "file_too_big": "Archivo demasiado grande [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", @@ -635,4 +628,4 @@ "too_many_requests": "Has alcanzado el límite de intentos, vuelve a intentarlo más tarde.", "password_reset_disabled": "El restablecimiento de contraseñas está deshabilitado. Póngase en contacto con el administrador de su instancia." } -} -\ No newline at end of file +} diff --git a/src/i18n/et.json b/src/i18n/et.json @@ -4,7 +4,19 @@ "find_user": "Otsi kasutajaid" }, "general": { - "submit": "Postita" + "submit": "Postita", + "verify": "Kinnita", + "confirm": "Kinnita", + "enable": "Luba", + "disable": "Keela", + "cancel": "Tühista", + "dismiss": "Olgu", + "show_less": "Kuva vähem", + "show_more": "Kuva rohkem", + "optional": "valikuline", + "generic_error": "Esines viga", + "more": "Rohkem", + "apply": "Rakenda" }, "login": { "login": "Logi sisse", @@ -12,39 +24,104 @@ "password": "Parool", "placeholder": "nt lain", "register": "Registreeru", - "username": "Kasutajanimi" + "username": "Kasutajanimi", + "heading": { + "recovery": "Kaheastmelise autentimise taaste", + "totp": "Kaheastmeline autentimine" + }, + "recovery_code": "Taastekood", + "enter_two_factor_code": "Sisesta kaheastmelise autentimise kood", + "enter_recovery_code": "Sisesta taastekood", + "authentication_code": "Autentimiskood", + "hint": "Logi sisse, et liituda vestlusega", + "description": "Logi sisse OAuthiga" }, "nav": { "mentions": "Mainimised", "public_tl": "Avalik Ajajoon", "timeline": "Ajajoon", - "twkn": "Kogu Teadaolev Võrgustik" + "twkn": "Kogu Teadaolev Võrgustik", + "preferences": "Eelistused", + "who_to_follow": "Keda jälgida", + "search": "Otsing", + "user_search": "Kasutajaotsing", + "dms": "Privaatsõnumid", + "interactions": "Interaktsioonid", + "friend_requests": "Jägimistaotlused", + "chat": "Kohalik vestlus", + "back": "Tagasi", + "administration": "Administreerimine", + "about": "Meist" }, "notifications": { "followed_you": "alustas sinu jälgimist", - "notifications": "Teavitused", - "read": "Loe!" + "notifications": "Teated", + "read": "Loe!", + "reacted_with": "reageeris {0}", + "migrated_to": "kolis", + "no_more_notifications": "Rohkem teateid ei ole", + "repeated_you": "taaspostitas su staatuse", + "load_older": "Laadi vanemad teated", + "follow_request": "soovib Teid jälgida", + "favorited_you": "lisas su staatuse lemmikuks", + "broken_favorite": "Tundmatu staatus, otsin…" }, "post_status": { "default": "Just sõitsin elektrirongiga Tallinnast Pääskülla.", - "posting": "Postitan" + "posting": "Postitan", + "scope": { + "unlisted": "Peidetud - Ära postita avalikele ajajoontele", + "public": "Avalil - Postita avalikele ajajoontele", + "private": "Jälgijatele - Postita ainult jälgijatele", + "direct": "Privaatne - Postita ainult mainitud kasutajatele" + }, + "scope_notice": { + "unlisted": "See postitus ei ole nähtav avalikul ega kogu võrgu ajajoonel", + "private": "See postitus on nähtav ainult Teie jälgijatele", + "public": "See postitus on nähtav kõigile" + }, + "direct_warning_to_first_only": "See postitus on nähtav ainult kirja alguses mainitud kasutajatele.", + "direct_warning_to_all": "See postitus on nähtav kõikidele mainitud kasutajatele.", + "content_warning": "Pealkiri (valikuline)", + "content_type": { + "text/bbcode": "BBCode", + "text/markdown": "Markdown", + "text/html": "HTML", + "text/plain": "Lihttekst" + }, + "attachments_sensitive": "Märgi manused sensitiivseks", + "account_not_locked_warning_link": "lukus", + "account_not_locked_warning": "Teie konto ei ole {0}. Kõik võivad Teid jälgida, et näha Teie ainult-jälgijatele postitusi.", + "new_status": "Postita uus staatus" }, "registration": { "bio": "Bio", "email": "E-post", "fullname": "Kuvatav nimi", "password_confirm": "Parooli kinnitamine", - "registration": "Registreerimine" + "registration": "Registreerimine", + "validations": { + "password_confirmation_match": "peaks olema sama kui salasõna", + "password_confirmation_required": "ei saa jätta tühjaks", + "password_required": "ei saa jätta tühjaks", + "email_required": "ei saa jätta tühjaks", + "fullname_required": "ei saa jätta tühjaks", + "username_required": "ei saa jätta tühjaks" + }, + "fullname_placeholder": "Näiteks Lain Iwakura", + "username_placeholder": "Näiteks lain", + "new_captcha": "Vajuta pildile, et saada uus captcha", + "captcha": "CAPTCHA", + "token": "Kutse võti" }, "settings": { "attachments": "Manused", - "autoload": "Luba ajajoone automaatne uuendamine kui ajajoon on põhja keritud", "avatar": "Profiilipilt", "bio": "Bio", "current_avatar": "Sinu praegune profiilipilt", "current_profile_banner": "Praegune profiilibänner", "filtering": "Sisu filtreerimine", - "filtering_explanation": "Kõiki staatuseid, mis sisaldavad neid sõnu, ei kuvata. Üks sõna reale.", + "filtering_explanation": "Kõiki staatuseid, mis sisaldavad neid sõnu, ei kuvata. Üks sõna reale", "hide_attachments_in_convo": "Peida manused vastlustes", "hide_attachments_in_tl": "Peida manused ajajoonel", "name": "Nimi", @@ -52,13 +129,201 @@ "nsfw_clickthrough": "Peida tööks-mittesobivad(NSFW) manuste hiireklõpsu taha", "profile_background": "Profiilitaust", "profile_banner": "Profiilibänner", - "reply_link_preview": "Luba algpostituse kuvamine vastustes", "set_new_avatar": "Vali uus profiilipilt", "set_new_profile_background": "Vali uus profiilitaust", "set_new_profile_banner": "Vali uus profiilibänner", "settings": "Sätted", "theme": "Teema", - "user_settings": "Kasutaja sätted" + "user_settings": "Kasutaja sätted", + "subject_line_noop": "Ära kopeeri", + "subject_line_mastodon": "Nagu mastodon: kopeeri nagu on", + "subject_line_email": "Nagu e-post: \"vs: pealkiri\"", + "subject_line_behavior": "Kopeeri pealkiri vastamisel", + "subject_input_always_show": "Alati kuva pealkirja välja", + "minimal_scopes_mode": "Peida postituse nähtavussätted", + "scope_copy": "Kopeeri nähtavussätted vastamisel (Privaatsed on alati kopeeritud)", + "security_tab": "Turvalisus", + "search_user_to_mute": "Otsi, keda soovid vaigistada", + "search_user_to_block": "Otsi, keda soovid blokeerida", + "saving_ok": "Sätted salvestatud", + "saving_err": "Sätete salvestamine ebaõnnestus", + "autohide_floating_post_button": "Automaatselt peida uue postituse nupp (mobiilil)", + "reply_visibility_self": "Näita ainult vastuseid, mis on suunatud mulle", + "reply_visibility_following": "Näita ainult vastuseid, mis on suunatud mulle või kasutajatele, keda jälgin", + "reply_visibility_all": "Näita kõiki vastuseid", + "replies_in_timeline": "Vastused ajajoonel", + "radii_help": "Liidese ümardamine (pikslites)", + "profile_tab": "Profiil", + "presets": "Salvestatud sätted", + "pause_on_unfocused": "Peata reaalajas voog kui leht pole fookuses", + "panelRadius": "Paneelid", + "revoke_token": "Keela", + "valid_until": "Kehtiv kuni", + "refresh_token": "Värskendustoken", + "token": "Token", + "oauth_tokens": "OAuth tokenid", + "show_moderator_badge": "Näita Moderaator silti mu profiilil", + "show_admin_badge": "Näita Admin silti mu profiilil", + "hide_followers_count_description": "Ära näita minu jälgijate arvu", + "hide_follows_count_description": "Ära näita minu jälgimiste arvu", + "hide_followers_description": "Ära näita minu jälgijaid", + "hide_follows_description": "Ära näita minu jälgimisi", + "no_mutes": "Vaigistusi pole", + "no_blocks": "Blokeeringuid pole", + "no_rich_text_description": "Muuda kõik postitused lihttekstiks", + "notification_visibility_emoji_reactions": "Reaktsioonid", + "notification_visibility_moves": "Kasutaja kolimised", + "notification_visibility_repeats": "Taaspostitused", + "notification_visibility_mentions": "Mainimised", + "notification_visibility_likes": "Lemmikud", + "notification_visibility_follows": "Jälgimised", + "notification_visibility": "Milliseid teateid kuvatakse", + "new_password": "Uus salasõna", + "new_email": "Uus e-post", + "use_contain_fit": "Näita eelvaadetes täis suuruses pilte", + "play_videos_in_modal": "Näita videoid eraldi raamis", + "mutes_tab": "Vaigistused", + "loop_video_silent_only": "Loop videod, millel pole heli (nt. Mastodoni \"gifid\")", + "loop_video": "Loop videod", + "lock_account_description": "Piira oma konto ainult lubatud jälgijatele", + "links": "Lingid", + "limited_availability": "Pole Teie veebilehitsejas saadaval", + "invalid_theme_imported": "Valitud fail ei ole Pleroma kujundus. Kujundusele muudatusi ei tehtud.", + "interfaceLanguage": "Liidese keel", + "interface": "Liides", + "instance_default_simple": "(vaikimisi)", + "instance_default": "(vaikimisi: {value})", + "checkboxRadius": "Märkeruudud", + "inputRadius": "Sisestuskastid", + "import_theme": "Lae sätted", + "import_followers_from_a_csv_file": "Impordi jälgimised csv failist", + "import_blocks_from_a_csv_file": "Impordi blokeeringud csv failist", + "hide_filtered_statuses": "Peida filtreeritud staatused", + "hide_user_stats": "Peida kasutaja statistika (nt. jälgijate arv)", + "hide_post_stats": "Peida postituse statistika (nt. lemmikute arv)", + "use_one_click_nsfw": "Ava NSFW manused ühe klikiga", + "preload_images": "Piltide eellaadimine", + "hide_isp": "Peida instantsipõhine paneel", + "max_thumbnails": "Maksimaalne lubatud eelvaadete arv postituste kohta", + "hide_muted_posts": "Peida vaigistatud kasutajate postitused", + "general": "Üldine", + "foreground": "Esiplaan", + "accent": "Rõhk", + "follows_imported": "Jälgimised imporditud! Nende töötlemine võtab natuke aega.", + "follow_import_error": "Jälgimiste importimisel tekkis viga", + "follow_import": "Impordi jälgimised", + "follow_export_button": "Ekspordi oma jälgimised csv failiks", + "follow_export": "Ekspordi jälgimised", + "export_theme": "Salvesta sätted", + "emoji_reactions_on_timeline": "Näita reaktsioone ajajoonel", + "pad_emoji": "Lisa emotikonidele tühikud ette ja järgi neid menüüst valides", + "avatar_size_instruction": "Profiilipildi soovitatud minimaalne suurus on 150x150 pikslit.", + "domain_mutes": "Domeenid", + "discoverable": "Luba selle konto ilmumine otsingutulemustes ning muudes teenustes", + "delete_account_instructions": "Konto kustutamise kinnitamiseks sisestage oma salasõna.", + "delete_account_error": "Teie konto kustutamisel tekkis viga. Kui see jätkub, palun võtke kontakti administraatoriga.", + "delete_account_description": "Jäädavalt kustuta oma andmed ja konto.", + "delete_account": "Kustuta konto", + "default_vis": "Vaikimisi nähtavus", + "data_import_export_tab": "Andmete import / eksport", + "current_password": "Praegune salasõna", + "confirm_new_password": "Kinnita uus salasõna", + "composing": "Koostamine", + "collapse_subject": "Peida postituste pealkirjad", + "changed_password": "Salasõna edukalt muudetud!", + "change_password_error": "Esines viga salasõna muutmisel.", + "change_password": "Muuda salasõna", + "changed_email": "E-post edukalt muudetud!", + "change_email_error": "Esines viga e-posti muutmisel.", + "change_email": "Muuda e-posti", + "cRed": "Punane (Tühista)", + "cOrange": "Oranž (Lisa lemmikuks)", + "cGreen": "Roheline (Taaspostita)", + "cBlue": "Sinine (Vasta, jälgi)", + "btnRadius": "Nupud", + "blocks_tab": "Blokeeringud", + "blocks_imported": "Blokeeringud imporditud! Nende töötlemine võtab natuke aega.", + "block_import_error": "Blokeeringute importimisel esines viga", + "block_import": "Blokeeringute import", + "block_export_button": "Ekspordi oma blokeeringud csv failiks", + "block_export": "Blokeeringute eksport", + "background": "Taust", + "avatarRadius": "Profiilipildid", + "avatarAltRadius": "Profiilipildid (Teated)", + "attachmentRadius": "Manused", + "allow_following_move": "Luba automaatjälgimine kui jälgitav konto kolib", + "mfa": { + "verify": { + "desc": "Et lubada kaheastmelist autentimist, sisestage kood oma äpist:" + }, + "scan": { + "desc": "Kasutades oma kaheastmelise autentimise äppi, skännige see QR kood või sisestage tekstiline võti:", + "secret_code": "Võti", + "title": "Skänni" + }, + "authentication_methods": "Autentimismeetodid", + "recovery_codes_warning": "Kirjutage need koodid üles ning hoidke need kindlas kohas. Kui Te kaotate ligipääsu oma kaheastmelise autentimise äppile ning nendele koodidele, ei ole Teil võimalik oma kontosse sisse logida.", + "waiting_a_recovery_codes": "Laen taastekoode…", + "recovery_codes": "Taastekoodid.", + "warning_of_generate_new_codes": "Kui Te loote uued taastekoodid, Teie vanad koodid ei tööta enam.", + "generate_new_recovery_codes": "Loo uued taastekoodid", + "title": "Kaheastmeline autentimine", + "confirm_and_enable": "Kinnita & luba OTP", + "wait_pre_setup_otp": "sean üles OTP", + "setup_otp": "Sea üles OTP", + "otp": "OTP" + }, + "enter_current_password_to_confirm": "Sisetage isiku tõestamiseks oma salasõna", + "security": "Turvalisus", + "app_name": "Rakenduse nimi", + "style": { + "switcher": { + "help": { + "snapshot_present": "Kujunduse eelvaade on laetud, nii et kõik väärtused on üle kirjutatud. Te saate laadida ka kujunduse päris sisu.", + "older_version_imported": "Teie imporditud fail oli loodud vanemas versioonis.", + "future_version_imported": "Teie imporditud fail oli loodud uuemas versioonis.", + "v2_imported": "Teie imporditud fail oli vanema versiooni jaoks. Me üritame hoida ühilduvust, kuid ikkagi võib esineda erinevusi.", + "upgraded_from_v2": "PleromaFE-d uuendati, teie kujundus võib välja näha natuke erinev, kui mäletate." + }, + "use_source": "Uus versioon", + "use_snapshot": "Vana versioon", + "keep_as_is": "Jäta nii, nagu on", + "load_theme": "Lae kujundus", + "clear_opacity": "Tühista läbipaistvus", + "clear_all": "Tühista kõik", + "reset": "Taasta algne", + "keep_fonts": "Jäta fondid", + "keep_roundness": "Jäta ümarus", + "keep_opacity": "Jäta läbipaistvus", + "keep_shadows": "Jäta varjud", + "keep_color": "Jäta värvid" + } + }, + "enable_web_push_notifications": "Luba veebipõhised push-teated", + "notification_blocks": "Kasutaja blokeerimisel ei tule neilt enam teateid ning nendele teilt ka mitte.", + "notification_setting_privacy_option": "Peida saatja ning sisu push-teadetelt", + "notifications": "Teated", + "notification_mutes": "Kui soovid mõnelt kasutajalt mitte teateid saada, kasuta vaigistust.", + "notification_setting_privacy": "Privaatsus", + "notification_setting_filters": "Filtrid", + "greentext": "Meemi nooled", + "fun": "Naljad", + "values": { + "true": "jah", + "false": "ei" + }, + "upload_a_photo": "Lae üles foto", + "type_domains_to_mute": "Trüki siia domeene, mida vaigistada", + "tooltipRadius": "Vihjed/hoiatused", + "theme_help_v2_1": "Te saate ka mõndade komponentide värvust ning läbipaistvust üle kirjutada vajutades ruudule. Kasuta \"Tühista kõik\" nuppu, et need tühistada.", + "theme_help": "Kasuta hex värvikoode (#rrggbb) oma kujunduse isikupärastamiseks.", + "text": "Tekst", + "useStreamingApiWarning": "(Pole soovituslik, eksperimentaalne, on teada, et jätab postitusi vahele)", + "useStreamingApi": "Saa postitusi ning teateid reaalajas", + "user_mutes": "Kasutajad", + "streaming": "Luba uute postituste automaatvoog kui oled lehekülje alguses", + "stop_gifs": "Mängi GIFid hiirega ületades", + "post_status_content_type": "Postituse sisutüüp" }, "timeline": { "conversation": "Vestlus", @@ -79,5 +344,111 @@ "muted": "Vaigistatud", "per_day": "päevas", "statuses": "Staatuseid" + }, + "about": { + "mrf": { + "mrf_policies_desc": "MRF poliitikad mõjutavad selle instansi föderatsiooni käitumist. Järgmised poliitikad on lubatud:", + "simple": { + "media_nsfw_desc": "See instants määrab nendest instantsidest postituste meedia sensitiivseks:", + "media_nsfw": "Meedia määratakse sensitiivseks", + "media_removal_desc": "See instants eemaldab meedia postitustelt nendest instantsidest:", + "media_removal": "Meedia eemaldamine", + "ftl_removal_desc": "See instants eemaldab postitused nendelt instantsidest \"Kogu teatud võrgu\" ajajoonelt:", + "ftl_removal": "\"Kogu teatud võrgu\" ajajoonelt eemaldamine", + "quarantine_desc": "See instants saadab ainult avalikke postitusi järgmistele instantsidele:", + "quarantine": "Karantiini", + "reject_desc": "See instants ei luba sõnumeid nendest instantsidest:", + "reject": "Keela", + "accept_desc": "See instants lubab sõnumeid ainult nendest instantsidest:", + "accept": "Luba", + "simple_policies": "Instansi-omased poliitikad" + }, + "mrf_policies": "Lubatud MRF poliitikad", + "keyword": { + "is_replaced_by": "→", + "replace": "Vaheta", + "reject": "Lükka tagasi", + "ftl_removal": "\"Kogu teatud võrgu\" ajajoonelt eemaldamine", + "keyword_policies": "Võtmesõna poliitikad" + }, + "federation": "Föderatsioon" + }, + "staff": "Personal" + }, + "selectable_list": { + "select_all": "Vali kõik" + }, + "remote_user_resolver": { + "error": "Ei leitud.", + "searching_for": "Otsin", + "remote_user_resolver": "Kaugkasutaja leidja" + }, + "interactions": { + "load_older": "Laadi vanemad interaktsioonid", + "moves": "Kasutaja kolimised", + "follows": "Uued jälgimised", + "favs_repeats": "Taaspostitused ja lemmikud" + }, + "emoji": { + "load_all": "Laen kõik {emojiAmount} emotikoni", + "load_all_hint": "Laadisin esimesed {saneAmount} emotikoni, kõike laadides võib esineda probleeme jõudlusega.", + "unicode": "Unicode emotikonid", + "custom": "Kohandatud emotikonid", + "add_emoji": "Lisa emotikon", + "search_emoji": "Otsi emotikone", + "keep_open": "Hoia valija lahti", + "emoji": "Emotikonid", + "stickers": "Kleepsud" + }, + "polls": { + "not_enough_options": "Liiga vähe unikaalseid valikuid hääletuses", + "expired": "Hääletus lõppes {0} tagasi", + "expires_in": "Hääletus lõppeb {0}", + "expiry": "Hääletuse vanus", + "multiple_choices": "Mitu vastust", + "single_choice": "Üks vastus", + "type": "Hääletuse tüüp", + "vote": "Hääleta", + "votes": "häält", + "option": "Valik", + "add_option": "Lisa valik", + "add_poll": "Lisa küsitlus" + }, + "media_modal": { + "next": "Järgmine", + "previous": "Eelmine" + }, + "importer": { + "error": "Faili importimisel tekkis viga.", + "success": "Import õnnestus.", + "submit": "Esita" + }, + "image_cropper": { + "cancel": "Tühista", + "save_without_cropping": "Salvesta muudatusteta", + "save": "Salvesta", + "crop_picture": "Modifitseeri pilti" + }, + "features_panel": { + "who_to_follow": "Keda jälgida", + "title": "Featuurid", + "text_limit": "Tekstilimiit", + "scope_options": "Ulatuse valikud", + "media_proxy": "Meedia proksi", + "gopher": "Gopher", + "chat": "Vestlus" + }, + "exporter": { + "processing": "Töötlemine, Teilt küsitakse varsti faili allalaadimist", + "export": "Ekspordi" + }, + "domain_mute_card": { + "unmute_progress": "Eemaldan vaigistuse…", + "unmute": "Ära vaigista", + "mute_progress": "Vaigistan…", + "mute": "Vaigista" + }, + "chat": { + "title": "Vestlus" } } diff --git a/src/i18n/eu.json b/src/i18n/eu.json @@ -84,7 +84,7 @@ "preferences": "Hobespenak" }, "notifications": { - "broken_favorite": "Egoera ezezaguna, bilatzen...", + "broken_favorite": "Egoera ezezaguna, bilatzen…", "favorited_you": "zure mezua gogoko du", "followed_you": "Zu jarraitzen zaitu", "load_older": "Kargatu jakinarazpen zaharragoak", @@ -128,7 +128,7 @@ "new_status": "Mezu berri bat idatzi", "account_not_locked_warning": "Zure kontua ez dago {0}. Edozeinek jarraitzen hastearekin, zure mezuak irakur ditzake.", "account_not_locked_warning_link": "Blokeatuta", - "attachments_sensitive": "Nabarmendu eranskinak hunkigarri gisa ", + "attachments_sensitive": "Nabarmendu eranskinak hunkigarri gisa", "content_type": { "text/plain": "Testu arrunta", "text/html": "HTML", @@ -187,9 +187,9 @@ "confirm_and_enable": "Baieztatu eta gaitu OTP", "title": "Bi-faktore autentifikazioa", "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", - "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.", "authentication_methods": "Autentifikazio metodoa", "scan": { @@ -198,12 +198,11 @@ "secret_code": "Giltza" }, "verify": { - "desc": "Bi-faktore autentifikazioa gaitzeko, sar ezazu bi-faktore kodea zure app-tik" + "desc": "Bi-faktore autentifikazioa gaitzeko, sar ezazu bi-faktore kodea zure app-tik:" } }, "attachmentRadius": "Eranskinak", "attachments": "Eranskinak", - "autoload": "Gaitu karga automatikoa beheraino mugitzean", "avatar": "Avatarra", "avatarAltRadius": "Avatarra (Aipamenak)", "avatarRadius": "Avatarrak", @@ -221,7 +220,7 @@ "cOrange": "Laranja (Gogokoa)", "cRed": "Gorria (ezeztatu)", "change_password": "Pasahitza aldatu", - "change_password_error": "Arazao bat egon da zure pasahitza aldatzean", + "change_password_error": "Arazao bat egon da zure pasahitza aldatzean.", "changed_password": "Pasahitza ondo aldatu da!", "collapse_subject": "Bildu gaia daukaten mezuak", "composing": "Idazten", @@ -248,7 +247,7 @@ "follows_imported": "Jarraitzaileak inportatuta! Prozesatzeak denbora pixka bat iraungo du.", "foreground": "Aurreko planoa", "general": "Orokorra", - "hide_attachments_in_convo": "Ezkutatu eranskinak elkarrizketatan ", + "hide_attachments_in_convo": "Ezkutatu eranskinak elkarrizketatan", "hide_attachments_in_tl": "Ezkutatu eranskinak donbora-lerroan", "hide_muted_posts": "Ezkutatu mutututako erabiltzaileen mezuak", "max_thumbnails": "Mezu bakoitzeko argazki-miniatura kopuru maximoa", @@ -307,7 +306,6 @@ "profile_tab": "Profila", "radii_help": "Konfiguratu interfazearen ertzen biribiltzea (pixeletan)", "replies_in_timeline": "Denbora-lerroko erantzunak", - "reply_link_preview": "Gaitu erantzun-estekaren aurrebista arratoiarekin", "reply_visibility_all": "Erakutsi erantzun guztiak", "reply_visibility_following": "Erakutsi bakarrik niri zuzendutako edo nik jarraitutako erabiltzaileen erantzunak", "reply_visibility_self": "Erakutsi bakarrik niri zuzendutako erantzunak", @@ -344,11 +342,6 @@ "true": "bai" }, "notifications": "Jakinarazpenak", - "notification_setting": "Jaso pertsona honen jakinarazpenak:", - "notification_setting_follows": "Jarraitutako erabiltzaileak", - "notification_setting_non_follows": "Jarraitzen ez dituzun erabiltzaileak", - "notification_setting_followers": "Zu jarraitzen zaituzten erabiltzaileak", - "notification_setting_non_followers": "Zu jarraitzen ez zaituzten erabiltzaileak", "notification_mutes": "Erabiltzaile jakin baten jakinarazpenak jasotzeari uzteko, isilarazi ezazu.", "notification_blocks": "Erabiltzaile bat blokeatzeak jakinarazpen guztiak gelditzen ditu eta harpidetza ezeztatu.", "enable_web_push_notifications": "Gaitu web jakinarazpenak", @@ -434,7 +427,7 @@ }, "fonts": { "_tab_label": "Letra-tipoak", - "help": "Aukeratu letra-tipoak erabiltzailearen interfazean erabiltzeko. \"Pertsonalizatua\" letra-tipoan, sisteman agertzen den izen berdinarekin idatzi behar duzu.", + "help": "Aukeratu letra-tipoak erabiltzailearen interfazean erabiltzeko. \"Pertsonalizatua\" letra-tipoan, sisteman agertzen den izen berdinarekin idatzi behar duzu.", "components": { "interface": "Interfazea", "input": "Sarrera eremuak", @@ -534,7 +527,7 @@ "favorites": "Gogokoak", "follow": "Jarraitu", "follow_sent": "Eskaera bidalita!", - "follow_progress": "Eskatzen...", + "follow_progress": "Eskatzen…", "follow_again": "Eskaera berriro bidali?", "follow_unfollow": "Jarraitzeari utzi", "followees": "Jarraitzen", @@ -553,11 +546,11 @@ "subscribe": "Harpidetu", "unsubscribe": "Harpidetza ezeztatu", "unblock": "Blokeoa kendu", - "unblock_progress": "Blokeoa ezeztatzen...", - "block_progress": "Blokeatzen...", + "unblock_progress": "Blokeoa ezeztatzen…", + "block_progress": "Blokeatzen…", "unmute": "Isiltasuna kendu", - "unmute_progress": "Isiltasuna kentzen...", - "mute_progress": "Isiltzen...", + "unmute_progress": "Isiltasuna kentzen…", + "mute_progress": "Isiltzen…", "hide_repeats": "Ezkutatu errepikapenak", "show_repeats": "Erakutsi errpekiapenak", "admin_menu": { @@ -639,4 +632,4 @@ "password_reset_required": "Pasahitza berrezarri behar duzu saioa hasteko.", "password_reset_required_but_mailer_is_disabled": "Pasahitza berrezarri behar duzu, baina pasahitza berrezartzeko aukera desgaituta dago. Mesedez, jarri harremanetan instantziaren administratzailearekin." } -} -\ No newline at end of file +} diff --git a/src/i18n/fi.json b/src/i18n/fi.json @@ -19,7 +19,21 @@ "apply": "Aseta", "submit": "Lähetä", "more": "Lisää", - "generic_error": "Virhe tapahtui" + "generic_error": "Virhe tapahtui", + "optional": "valinnainen", + "show_more": "Näytä lisää", + "show_less": "Näytä vähemmän", + "dismiss": "Sulje", + "cancel": "Peruuta", + "disable": "Poista käytöstä", + "confirm": "Hyväksy", + "verify": "Varmenna", + "enable": "Ota käyttöön", + "loading": "Ladataan…", + "error_retry": "Yritä uudelleen", + "retry": "Yritä uudelleen", + "close": "Sulje", + "peek": "Kurkkaa" }, "login": { "login": "Kirjaudu sisään", @@ -28,7 +42,16 @@ "password": "Salasana", "placeholder": "esim. Seppo", "register": "Rekisteröidy", - "username": "Käyttäjänimi" + "username": "Käyttäjänimi", + "hint": "Kirjaudu sisään liittyäksesi keskusteluun", + "authentication_code": "Todennuskoodi", + "enter_recovery_code": "Syötä palautuskoodi", + "recovery_code": "Palautuskoodi", + "heading": { + "totp": "Monivaihetodennus", + "recovery": "Monivaihepalautus" + }, + "enter_two_factor_code": "Syötä monivaihetodennuskoodi" }, "nav": { "about": "Tietoja", @@ -40,13 +63,16 @@ "dms": "Yksityisviestit", "public_tl": "Julkinen Aikajana", "timeline": "Aikajana", - "twkn": "Koko Tunnettu Verkosto", + "twkn": "Tunnettu Verkosto", "user_search": "Käyttäjähaku", "who_to_follow": "Seurausehdotukset", - "preferences": "Asetukset" + "preferences": "Asetukset", + "administration": "Ylläpito", + "search": "Haku", + "bookmarks": "Kirjanmerkit" }, "notifications": { - "broken_favorite": "Viestiä ei löydetty...", + "broken_favorite": "Viestiä ei löydetty…", "favorited_you": "tykkäsi viestistäsi", "followed_you": "seuraa sinua", "load_older": "Lataa vanhempia ilmoituksia", @@ -54,7 +80,9 @@ "read": "Lue!", "repeated_you": "toisti viestisi", "no_more_notifications": "Ei enempää ilmoituksia", - "reacted_with": "lisäsi reaktion {0}" + "reacted_with": "lisäsi reaktion {0}", + "migrated_to": "siirtyi sivulle", + "follow_request": "haluaa seurata sinua" }, "polls": { "add_poll": "Lisää äänestys", @@ -68,20 +96,25 @@ "expiry": "Äänestyksen kesto", "expires_in": "Päättyy {0} päästä", "expired": "Päättyi {0} sitten", - "not_enough_option": "Liian vähän uniikkeja vaihtoehtoja äänestyksessä" + "not_enough_option": "Liian vähän uniikkeja vaihtoehtoja äänestyksessä", + "not_enough_options": "Liian vähän ainutkertaisia vaihtoehtoja" }, "interactions": { "favs_repeats": "Toistot ja tykkäykset", "follows": "Uudet seuraukset", - "load_older": "Lataa vanhempia interaktioita" + "load_older": "Lataa vanhempia interaktioita", + "moves": "Käyttäjien siirtymiset" }, "post_status": { "new_status": "Uusi viesti", - "account_not_locked_warning": "Tilisi ei ole {0}. Kuka vain voi seurata sinua nähdäksesi 'vain-seuraajille' -viestisi", + "account_not_locked_warning": "Tilisi ei ole {0}. Kuka vain voi seurata sinua nähdäksesi 'vain-seuraajille' -viestisi.", "account_not_locked_warning_link": "lukittu", "attachments_sensitive": "Merkkaa liitteet arkaluonteisiksi", "content_type": { - "text/plain": "Tavallinen teksti" + "text/plain": "Tavallinen teksti", + "text/html": "HTML", + "text/markdown": "Markdown", + "text/bbcode": "BBCode" }, "content_warning": "Aihe (valinnainen)", "default": "Tulin juuri saunasta.", @@ -92,7 +125,19 @@ "private": "Vain-seuraajille - Näkyy vain seuraajillesi", "public": "Julkinen - Näkyy julkisilla aikajanoilla", "unlisted": "Listaamaton - Ei näy julkisilla aikajanoilla" - } + }, + "direct_warning_to_all": "Tämä viesti näkyy vain viestissä mainituille käyttäjille.", + "direct_warning_to_first_only": "Tämä viesti näkyy vain viestin alussa mainituille käyttäjille.", + "scope_notice": { + "public": "Tämä viesti näkyy kaikille", + "private": "Tämä viesti näkyy vain sinun seuraajillesi", + "unlisted": "Tämä viesti ei näy Julkisella Aikajanalla tai Koko Tunnettu Verkosto -aikajanalla" + }, + "preview": "Esikatselu", + "preview_empty": "Tyhjä", + "empty_status_error": "Tyhjää viestiä ilman tiedostoja ei voi lähettää", + "media_description": "Tiedoston kuvaus", + "media_description_error": "Tiedostojen päivitys epäonnistui, yritä uudelleen" }, "registration": { "bio": "Kuvaus", @@ -110,12 +155,14 @@ "password_required": "ei voi olla tyhjä", "password_confirmation_required": "ei voi olla tyhjä", "password_confirmation_match": "pitää vastata salasanaa" - } + }, + "username_placeholder": "esim. peke", + "fullname_placeholder": "esim. Pekka Postaaja", + "bio_placeholder": "esim.\nHei, olen Pekka.\nOlen esimerkkikäyttäjä tässä verkostossa." }, "settings": { "attachmentRadius": "Liitteet", "attachments": "Liitteet", - "autoload": "Lataa vanhempia viestejä automaattisesti ruudun pohjalla", "avatar": "Profiilikuva", "avatarAltRadius": "Profiilikuvat (ilmoitukset)", "avatarRadius": "Profiilikuvat", @@ -138,7 +185,7 @@ "data_import_export_tab": "Tietojen tuonti / vienti", "default_vis": "Oletusnäkyvyysrajaus", "delete_account": "Poista tili", - "delete_account_description": "Poista tilisi ja viestisi pysyvästi.", + "delete_account_description": "Poista tietosi ja lukitse tili pysyvästi.", "delete_account_error": "Virhe poistaessa tiliäsi. Jos virhe jatkuu, ota yhteyttä palvelimesi ylläpitoon.", "delete_account_instructions": "Syötä salasanasi vahvistaaksesi tilin poiston.", "emoji_reactions_on_timeline": "Näytä emojireaktiot aikajanalla", @@ -151,7 +198,7 @@ "follow_import": "Seurausten tuonti", "follow_import_error": "Virhe tuodessa seuraksia", "follows_imported": "Seuraukset tuotu! Niiden käsittely vie hetken.", - "foreground": "Korostus", + "foreground": "Etuala", "general": "Yleinen", "hide_attachments_in_convo": "Piilota liitteet keskusteluissa", "hide_attachments_in_tl": "Piilota liitteet aikajanalla", @@ -186,14 +233,14 @@ "notification_visibility_mentions": "Maininnat", "notification_visibility_repeats": "Toistot", "notification_visibility_emoji_reactions": "Reaktiot", - "no_rich_text_description": "Älä näytä tekstin muotoilua.", + "no_rich_text_description": "Älä näytä tekstin muotoilua", "hide_network_description": "Älä näytä seurauksiani tai seuraajiani", "nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse", "oauth_tokens": "OAuth-merkit", "token": "Token", "refresh_token": "Päivitä token", "valid_until": "Voimassa asti", - "revoke_token": "Peruuttaa", + "revoke_token": "Peruuta", "panelRadius": "Ruudut", "pause_on_unfocused": "Pysäytä automaattinen viestien näyttö välilehden ollessa pois fokuksesta", "presets": "Valmiit teemat", @@ -202,7 +249,6 @@ "profile_tab": "Profiili", "radii_help": "Aseta reunojen pyöristys (pikseleinä)", "replies_in_timeline": "Keskustelut aikajanalla", - "reply_link_preview": "Keskusteluiden vastauslinkkien esikatselu", "reply_visibility_all": "Näytä kaikki vastaukset", "reply_visibility_following": "Näytä vain vastaukset minulle tai seuraamilleni käyttäjille", "reply_visibility_self": "Näytä vain vastaukset minulle", @@ -231,7 +277,238 @@ "values": { "false": "pois päältä", "true": "päällä" - } + }, + "hide_follows_description": "Älä näytä ketä seuraan", + "show_moderator_badge": "Näytä Moderaattori-merkki profiilissani", + "useStreamingApi": "Vastaanota viestiejä ja ilmoituksia reaaliajassa", + "notification_setting_filters": "Suodattimet", + "notification_setting_privacy_option": "Piilota lähettäjä ja sisältö sovelluksen ulkopuolisista ilmoituksista", + "enable_web_push_notifications": "Ota käyttöön sovelluksen ulkopuoliset ilmoitukset", + "app_name": "Sovelluksen nimi", + "security": "Turvallisuus", + "mfa": { + "otp": "OTP", + "setup_otp": "OTP-asetukset", + "wait_pre_setup_otp": "esiasetetaan OTP:ta", + "confirm_and_enable": "Hyväksy ja käytä OTP", + "title": "Monivaihetodennus", + "generate_new_recovery_codes": "Luo uudet palautuskoodit", + "authentication_methods": "Todennus", + "warning_of_generate_new_codes": "Luodessasi uudet palautuskoodit, vanhat koodisi lakkaavat toimimasta.", + "recovery_codes": "Palautuskoodit.", + "waiting_a_recovery_codes": "Odotetaan palautuskoodeja…", + "recovery_codes_warning": "Kirjoita koodit ylös tai tallenna ne turvallisesti, muuten et näe niitä uudestaan. Jos et voi käyttää monivaihetodennusta ja sinulla ei ole palautuskoodeja, et voi enää kirjautua sisään tilillesi.", + "scan": { + "title": "Skannaa", + "secret_code": "Avain", + "desc": "Käytä monivaihetodennus-sovellusta skannakksesi tämän QR-kooding, tai syötä avain:" + }, + "verify": { + "desc": "Kytkeäksesi päälle monivaihetodennuksen, syötä koodi monivaihetodennussovellksesta:" + } + }, + "allow_following_move": "Salli automaattinen seuraaminen kun käyttäjä siirtää tilinsä", + "block_export": "Estojen vienti", + "block_export_button": "Vie estosi CSV-tiedostoon", + "block_import": "Estojen tuonti", + "block_import_error": "Virhe tuodessa estoja", + "blocks_imported": "Estot tuotu! Käsittely vie hetken.", + "blocks_tab": "Estot", + "change_email": "Vaihda sähköpostiosoite", + "change_email_error": "Virhe vaihtaessa sähköpostiosoitetta.", + "changed_email": "Sähköpostiosoite vaihdettu!", + "domain_mutes": "Sivut", + "avatar_size_instruction": "Suositeltu vähimmäiskoko profiilikuville on 150x150 pikseliä.", + "accent": "Korostus", + "hide_muted_posts": "Piilota mykistettyjen käyttäjien viestit", + "hide_filtered_statuses": "Piilota mykistetyt viestit", + "import_blocks_from_a_csv_file": "Tuo estot CSV-tiedostosta", + "no_blocks": "Ei estoja", + "no_mutes": "Ei mykistyksiä", + "notification_visibility_moves": "Käyttäjien siirtymiset", + "hide_followers_description": "Älä näytä ketkä seuraavat minua", + "hide_follows_count_description": "Älä näytä seurauksien määrää", + "hide_followers_count_description": "Älä näytä seuraajien määrää", + "show_admin_badge": "Näytä Ylläpitäjä-merkki proofilissani", + "autohide_floating_post_button": "Piilota Uusi Viesti -nappi automaattisesti (mobiili)", + "search_user_to_block": "Hae estettäviä käyttäjiä", + "search_user_to_mute": "Hae mykistettäviä käyttäjiä", + "minimal_scopes_mode": "Yksinkertaista näkyvyydenrajauksen vaihtoehdot", + "post_status_content_type": "Uuden viestin sisällön muoto", + "user_mutes": "Käyttäjät", + "useStreamingApiWarning": "(Kokeellinen)", + "type_domains_to_mute": "Etsi mykistettäviä sivustoja", + "upload_a_photo": "Lataa kuva", + "fun": "Hupi", + "greentext": "Meeminuolet", + "notifications": "Ilmoitukset", + "style": { + "switcher": { + "save_load_hint": "\"Säilytä\" asetukset säilyttävät tällä hetkellä asetetut asetukset valittaessa tai ladatessa teemaa, se myös tallentaa kyseiset asetukset viedessä teemaa. Kun kaikki laatikot ovat tyhjänä, viety teema tallentaa kaiken.", + "help": { + "older_version_imported": "Tuomasi tiedosto on luotu vanhemmalla versiolla.", + "fe_upgraded": "PleromaFE:n teemaus päivitetty versiopäivityksen yhteydessä.", + "migration_snapshot_ok": "Varmuuden vuoksi teeman kaappaus ladattu. Voit koittaa ladata teeman sisällön.", + "migration_napshot_gone": "Jostain syystä teeman kaappaus puuttuu, kaikki asiat eivät välttämättä näytä oikealta.", + "snapshot_source_mismatch": "Versiot eivät täsmää: todennäköisesti versio vaihdettu vanhempaan ja päivitetty uudestaan, jos vaihdoit teemaa vanhalla versiolla, sinun tulisi käyttää vanhaa versiota, muutoin uutta.", + "upgraded_from_v2": "PleromaFE on päivitetty, teemasi saattaa näyttää erilaiselta kuin muistat.", + "v2_imported": "Tuomasi tiedosto on luotu vanhemmalla versiolla. Yhteensopivuus ei välttämättä ole täydellinen.", + "future_version_imported": "Tuomasi tiedosto on luotu uudemmalla versiolla.", + "snapshot_present": "Teeman kaappaus ladattu, joten kaikki arvot ovat ylikirjoitettu. Voit sen sijaan ladata teeman sisällön.", + "snapshot_missing": "Teeman kaappausta ei tiedostossa, joten se voi näyttää erilaiselta kuin suunniteltu.", + "fe_downgraded": "PleromaFE:n versio vaihtunut vanhempaan." + }, + "keep_color": "Säilytä värit", + "keep_shadows": "Säilytä varjot", + "keep_opacity": "Säilytä läpinäkyvyys", + "keep_roundness": "Säilytä pyöristys", + "keep_fonts": "Säilytä fontit", + "reset": "Palauta", + "clear_all": "Tyhjennä kaikki", + "clear_opacity": "Tyhjennä läpinäkyvyys", + "load_theme": "Lataa teema", + "keep_as_is": "Pidä sellaisenaan", + "use_snapshot": "Vanha", + "use_source": "Uusi" + }, + "advanced_colors": { + "selectedPost": "Valittu viesti", + "_tab_label": "Edistynyt", + "alert": "Varoituksen tausta", + "alert_error": "Virhe", + "alert_warning": "Varoitus", + "alert_neutral": "Neutraali", + "post": "Viestit/Käyttäjien kuvaukset", + "badge": "Merkin tausta", + "badge_notification": "Ilmoitus", + "panel_header": "Ruudun otsikko", + "top_bar": "Yläpalkki", + "borders": "Reunat", + "buttons": "Napit", + "inputs": "Syöttökentät", + "faint_text": "Häivytetty teksti", + "underlay": "Taustapeite", + "poll": "Äänestyksen kuvaaja", + "icons": "Ikonit", + "highlight": "Korostetut elementit", + "pressed": "Painettu", + "selectedMenu": "Valikon valinta", + "disabled": "Pois käytöstä", + "toggled": "Kytketty", + "tabs": "Välilehdet", + "popover": "Työkaluvinkit, valikot, ponnahdusviestit" + }, + "common": { + "color": "Väri", + "opacity": "Läpinäkyvyys", + "contrast": { + "level": { + "aaa": "saavuttaa AAA-tason (suositeltu)", + "aa": "saavuttaa AA-tason (minimi)", + "bad": "ei saavuta mitään helppokäyttöisyyssuosituksia" + }, + "hint": "Kontrastisuhde on {ratio}, se {level} {context}", + "context": { + "18pt": "suurella (18pt+) tekstillä", + "text": "tekstillä" + } + } + }, + "common_colors": { + "_tab_label": "Yleinen", + "main": "Yleiset värit", + "foreground_hint": "Löydät \"Edistynyt\"-välilehdeltä tarkemmat asetukset", + "rgbo": "Ikonit, korostukset, merkit" + }, + "shadows": { + "filter_hint": { + "always_drop_shadow": "Varoitus, tämä varjo käyttää aina {0} kun selain tukee sitä.", + "avatar_inset": "Huom. sisennettyjen ja ei-sisennettyjen varjojen yhdistelmät saattavat luoda ei-odotettuja lopputuloksia läpinäkyvillä profiilikuvilla.", + "drop_shadow_syntax": "{0} ei tue {1} parametria ja {2} avainsanaa.", + "spread_zero": "Varjot joiden levitys > 0 näyttävät samalta kuin se olisi nolla", + "inset_classic": "Sisennetyt varjot käyttävät {0}" + }, + "components": { + "buttonPressedHover": "Nappi (painettu ja kohdistettu)", + "panel": "Ruutu", + "panelHeader": "Ruudun otsikko", + "topBar": "Yläpalkki", + "avatar": "Profiilikuva (profiilinäkymässä)", + "avatarStatus": "Profiilikuva (viestin yhtyedessä)", + "popup": "Ponnahdusviestit ja työkaluvinkit", + "button": "Nappi", + "buttonHover": "Nappi (kohdistus)", + "buttonPressed": "Nappi (painettu)", + "input": "Syöttökenttä" + }, + "hintV3": "Voit käyttää {0} merkintää varjoille käyttääksesi väriä toisesta asetuksesta.", + "_tab_label": "Valo ja varjostus", + "component": "Komponentti", + "override": "Ylikirjoita", + "shadow_id": "Varjo #{value}", + "blur": "Sumennus", + "spread": "Levitys", + "inset": "Sisennys" + }, + "fonts": { + "help": "Valitse fontti käyttöliittymälle. \"Oma\"-vaihtohdolle on syötettävä fontin nimi tarkalleen samana kuin se on järjestelmässäsi.", + "_tab_label": "Fontit", + "components": { + "interface": "Käyttöliittymä", + "input": "Syöttökentät", + "post": "Viestin teksti", + "postCode": "Tasavälistetty teksti viestissä" + }, + "family": "Fontin nimi", + "size": "Koko (pikseleissä)", + "weight": "Painostus (paksuus)", + "custom": "Oma" + }, + "preview": { + "input": "Tulin juuri saunasta.", + "header": "Esikatselu", + "content": "Sisältö", + "error": "Esimerkkivirhe", + "button": "Nappi", + "text": "Vähän lisää {0} ja {1}", + "mono": "sisältöä", + "faint_link": "manuaali", + "fine_print": "Lue meidän {0} vaikka huvin vuoksi!", + "header_faint": "Tämä on OK", + "checkbox": "Olen silmäillyt käyttöehdot", + "link": "kiva linkki" + }, + "radii": { + "_tab_label": "Pyöristys" + } + }, + "enter_current_password_to_confirm": "Syötä nykyinen salasanasi todentaaksesi henkilöllisyytesi", + "discoverable": "Salli tilisi näkyvyys hakukoneisiin ja muihin palveluihin", + "pad_emoji": "Välistä emojit välilyönneillä lisätessäsi niitä valitsimesta", + "mutes_tab": "Mykistykset", + "new_email": "Uusi sähköpostiosoite", + "notification_setting_privacy": "Yksityisyys", + "notification_mutes": "Jos et halua ilmoituksia joltain käyttäjältä, käytä mykistystä.", + "notification_blocks": "Estäminen pysäyttää kaikki ilmoitukset käyttäjältä ja poistaa seurauksen.", + "version": { + "title": "Versio", + "backend_version": "Palvelimen versio", + "frontend_version": "Käyttöliittymän versio" + }, + "reset_profile_background": "Nollaa taustakuva", + "reset_background_confirm": "Haluatko todella nollata taustakuvan?", + "mutes_and_blocks": "Mykistykset ja Estot", + "bot": "Tämä on bottitili", + "profile_fields": { + "label": "Profiilin metatiedot", + "add_field": "Lisää kenttä", + "name": "Nimi", + "value": "Sisältö" + }, + "reset_avatar": "Nollaa profiilikuva", + "reset_profile_banner": "Nollaa profiilin tausta", + "reset_avatar_confirm": "Haluatko todella nollata profiilikuvan?", + "reset_banner_confirm": "Haluatko todella nollata profiilin taustan?" }, "time": { "day": "{0} päivä", @@ -252,8 +529,8 @@ "months": "{0} kuukautta", "month_short": "{0}kk", "months_short": "{0}kk", - "now": "nyt", - "now_short": "juuri nyt", + "now": "juuri nyt", + "now_short": "nyt", "second": "{0} sekunti", "seconds": "{0} sekuntia", "second_short": "{0}s", @@ -276,7 +553,9 @@ "repeated": "toisti", "show_new": "Näytä uudet", "up_to_date": "Ajantasalla", - "no_more_statuses": "Ei enempää viestejä" + "no_more_statuses": "Ei enempää viestejä", + "no_statuses": "Ei viestejä", + "reload": "Päivitä" }, "status": { "favorites": "Tykkäykset", @@ -288,9 +567,18 @@ "delete_confirm": "Haluatko varmasti postaa viestin?", "reply_to": "Vastaus", "replies_list": "Vastaukset:", - "mute_conversation": "Hiljennä keskustelu", - "unmute_conversation": "Poista hiljennys", - "status_unavailable": "Viesti ei saatavissa" + "mute_conversation": "Mykistä keskustelu", + "unmute_conversation": "Poista mykistys", + "status_unavailable": "Viesti ei saatavissa", + "copy_link": "Kopioi linkki", + "bookmark": "Lisää kirjanmerkkeihin", + "unbookmark": "Poista kirjanmerkeistä", + "thread_muted": "Keskustelu mykistetty", + "thread_muted_and_words": ", sisältää sanat:", + "show_full_subject": "Näytä koko otsikko", + "hide_full_subject": "Piilota koko otsikko", + "show_content": "Näytä sisältö", + "hide_content": "Piilota sisältö" }, "user_card": { "approve": "Hyväksy", @@ -299,22 +587,58 @@ "deny": "Älä hyväksy", "follow": "Seuraa", "follow_sent": "Pyyntö lähetetty!", - "follow_progress": "Pyydetään...", - "follow_again": "Lähetä pyyntö uudestaan", + "follow_progress": "Pyydetään…", + "follow_again": "Lähetä pyyntö uudestaan?", "follow_unfollow": "Älä seuraa", "followees": "Seuraa", "followers": "Seuraajat", "following": "Seuraat!", "follows_you": "Seuraa sinua!", "its_you": "Sinun tili!", - "mute": "Hiljennä", - "muted": "Hiljennetty", + "mute": "Mykistä", + "muted": "Mykistetty", "per_day": "päivässä", "remote_follow": "Seuraa muualta", - "statuses": "Viestit" + "statuses": "Viestit", + "hidden": "Piilotettu", + "media": "Media", + "block_progress": "Estetään…", + "admin_menu": { + "grant_admin": "Anna Ylläpitöoikeudet", + "force_nsfw": "Merkitse kaikki viestit NSFW:nä", + "disable_any_subscription": "Estä käyttäjän seuraaminen", + "moderation": "Moderaatio", + "revoke_admin": "Poista Ylläpitöoikeudet", + "grant_moderator": "Anna Moderaattorioikeudet", + "revoke_moderator": "Poista Moderaattorioikeudet", + "activate_account": "Aktivoi tili", + "deactivate_account": "Deaktivoi tili", + "delete_account": "Poista tili", + "strip_media": "Poista media viesteistä", + "force_unlisted": "Pakota viestit listaamattomiksi", + "sandbox": "Pakota viestit vain seuraajille", + "disable_remote_subscription": "Estä seuraaminen ulkopuolisilta sivuilta", + "quarantine": "Estä käyttäjän viestin federoituminen", + "delete_user": "Poista käyttäjä", + "delete_user_confirmation": "Oletko aivan varma? Tätä ei voi kumota." + }, + "favorites": "Tykkäykset", + "mention": "Mainitse", + "report": "Ilmianna", + "subscribe": "Tilaa", + "unsubscribe": "Poista tilaus", + "unblock": "Poista esto", + "unblock_progress": "Poistetaan estoa…", + "unmute": "Poista mykistys", + "unmute_progress": "Poistetaan mykistystä…", + "mute_progress": "Mykistetään…", + "hide_repeats": "Piilota toistot", + "show_repeats": "Näytä toistot" }, "user_profile": { - "timeline_title": "Käyttäjän aikajana" + "timeline_title": "Käyttäjän aikajana", + "profile_does_not_exist": "Tätä profiilia ei ole.", + "profile_loading_error": "Virhe ladatessa profiilia." }, "who_to_follow": { "more": "Lisää", @@ -325,9 +649,13 @@ "repeat": "Toista", "reply": "Vastaa", "favorite": "Tykkää", - "user_settings": "Käyttäjäasetukset" + "user_settings": "Käyttäjäasetukset", + "add_reaction": "Lisää Reaktio", + "accept_follow_request": "Hyväksy seurauspyyntö", + "reject_follow_request": "Hylkää seurauspyyntö", + "bookmark": "Kirjanmerkki" }, - "upload":{ + "upload": { "error": { "base": "Lataus epäonnistui.", "file_too_big": "Tiedosto liian suuri [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", @@ -340,5 +668,111 @@ "GiB": "Gt", "TiB": "Tt" } + }, + "about": { + "mrf": { + "keyword": { + "keyword_policies": "Avainsanasäännöt", + "ftl_removal": "Poistettu \"Koko Tunnettu Verkosto\" -aikajanalta", + "reject": "Hylkää", + "replace": "Korvaa", + "is_replaced_by": "→" + }, + "simple": { + "accept": "Hyväksy", + "reject": "Hylkää", + "quarantine": "Karanteeni", + "ftl_removal": "Poisto \"Koko Tunnettu Verkosto\" -aikajanalta", + "media_removal": "Media-tiedostojen poisto", + "simple_policies": "Palvelinkohtaiset Säännöt", + "accept_desc": "Tämä palvelin hyväksyy viestit vain seuraavilta palvelimilta:", + "reject_desc": "Tämä palvelin ei hyväksy viestejä seuraavilta palvelimilta:", + "quarantine_desc": "Tämä palvelin lähettää vain julkisia viestejä seuraaville palvelimille:", + "ftl_removal_desc": "Tämä palvelin poistaa nämä palvelimet \"Koko Tunnettu Verkosto\"-aikajanalta:", + "media_removal_desc": "Tämä palvelin postaa mediatiedostot viesteistä seuraavilta palvelimilta:", + "media_nsfw": "Pakota Media Arkaluontoiseksi", + "media_nsfw_desc": "Tämä palvelin pakottaa mediatiedostot arkaluonteisiksi seuraavilta palvelimilta:" + }, + "federation": "Federaatio", + "mrf_policies": "Aktivoidut MRF-säännöt", + "mrf_policies_desc": "MRF-säännöt muuttavat federaation toimintaa sivulla. Seuraavat säännöt ovat kytketty päälle:" + }, + "staff": "Henkilökunta" + }, + "domain_mute_card": { + "mute": "Mykistä", + "unmute": "Poista mykistys", + "mute_progress": "Mykistetään…", + "unmute_progress": "Poistetaan mykistystä…" + }, + "exporter": { + "export": "Vie", + "processing": "Käsitellään, hetken päästä voit tallentaa tiedoston" + }, + "image_cropper": { + "crop_picture": "Rajaa kuva", + "save": "Tallenna", + "save_without_cropping": "Tallenna rajaamatta", + "cancel": "Peruuta" + }, + "importer": { + "submit": "Hyväksy", + "error": "Virhe tapahtui tietoja tuodessa.", + "success": "Tuonti onnistui." + }, + "media_modal": { + "previous": "Edellinen", + "next": "Seuraava" + }, + "emoji": { + "stickers": "Tarrat", + "emoji": "Emoji", + "keep_open": "Pidä valitsin auki", + "search_emoji": "Hae emojia", + "add_emoji": "Lisää emoji", + "custom": "Custom-emoji", + "load_all": "Ladataan kaikkia {emojiAmount} emojia", + "unicode": "Unicode-emoji", + "load_all_hint": "Ensimmäiset {saneAmount} emojia ladattu, kaikkien emojien lataaminen voi aiheuttaa hidastelua." + }, + "remote_user_resolver": { + "remote_user_resolver": "Ulkopuolinen käyttäjä", + "searching_for": "Etsitään käyttäjää", + "error": "Ei löytynyt." + }, + "selectable_list": { + "select_all": "Valitse kaikki" + }, + "password_reset": { + "check_email": "Tarkista sähköpostisi salasanannollausta varten.", + "instruction": "Syötä sähköpostiosoite tai käyttäjänimi. Lähetämme linkin salasanan nollausta varten.", + "password_reset_disabled": "Salasanan nollaus ei käytössä. Ota yhteyttä sivun ylläpitäjään.", + "password_reset_required_but_mailer_is_disabled": "Sinun täytyy vaihtaa salasana, mutta salasanan nollaus on pois käytöstä. Ota yhteyttä sivun ylläpitäjään.", + "forgot_password": "Unohditko salasanan?", + "password_reset": "Salasanan nollaus", + "placeholder": "Sähköpostiosoite tai käyttäjänimi", + "return_home": "Palaa etusivulle", + "not_found": "Sähköpostiosoitetta tai käyttäjänimeä ei löytynyt.", + "too_many_requests": "Olet käyttänyt kaikki yritykset, yritä uudelleen myöhemmin.", + "password_reset_required": "Sinun täytyy vaihtaa salasana kirjautuaksesi." + }, + "user_reporting": { + "add_comment_description": "Tämä raportti lähetetään sivun moderaattoreille. Voit antaa selityksen miksi ilmiannoit tilin:", + "title": "Ilmiannetaan {0}", + "additional_comments": "Lisäkommentit", + "forward_description": "Tämä tili on toiselta palvelimelta. Lähetä kopio ilmiannosta sinnekin?", + "forward_to": "Lähetä eteenpäin: {0}", + "submit": "Lähetä", + "generic_error": "Virhe käsitellessä pyyntöä." + }, + "search": { + "people": "Käyttäjät", + "hashtags": "Aihetunnisteet", + "people_talking": "{0} käyttäjää puhuvat", + "person_talking": "{0} käyttäjä puhuu", + "no_results": "Ei tuloksia" + }, + "errors": { + "storage_unavailable": "Pleroma ei voinut käyttää selaimen muistia. Kirjautumisesi ja paikalliset asetukset eivät tallennu ja saatat kohdata odottamattomia ongelmia. Yritä sallia evästeet." } } diff --git a/src/i18n/fr.json b/src/i18n/fr.json @@ -1,549 +1,737 @@ { - "chat": { - "title": "Chat" - }, - "exporter": { - "export": "Exporter", - "processing": "En cours de traitement, vous pourrez bientôt télécharger votre fichier" - }, - "features_panel": { - "chat": "Chat", - "gopher": "Gopher", - "media_proxy": "Proxy média", - "scope_options": "Options de visibilité", - "text_limit": "Limite de texte", - "title": "Caractéristiques", - "who_to_follow": "Personnes à suivre" - }, - "finder": { - "error_fetching_user": "Erreur lors de la recherche de l'utilisateur·ice", - "find_user": "Chercher un-e utilisateur·ice" - }, - "general": { - "apply": "Appliquer", - "submit": "Envoyer", - "more": "Plus", - "generic_error": "Une erreur s'est produite", - "optional": "optionnel", - "show_more": "Montrer plus", - "show_less": "Montrer moins", - "cancel": "Annuler", - "disable": "Désactiver", - "enable": "Activer", - "confirm": "Confirmer", - "verify": "Vérifier" - }, - "image_cropper": { - "crop_picture": "Rogner l'image", - "save": "Sauvegarder", - "save_without_cropping": "Sauvegarder sans rogner", - "cancel": "Annuler" - }, - "importer": { - "submit": "Soumettre", - "success": "Importé avec succès.", - "error": "Une erreur est survenue pendant l'import de ce fichier." - }, - "login": { - "login": "Connexion", - "description": "Connexion avec OAuth", - "logout": "Déconnexion", - "password": "Mot de passe", - "placeholder": "p.e. lain", - "register": "S'inscrire", - "username": "Identifiant", - "hint": "Connectez-vous pour rejoindre la discussion", - "authentication_code": "Code d'authentification", - "enter_recovery_code": "Entrez un code de récupération", - "enter_two_factor_code": "Entrez un code à double authentification", - "recovery_code": "Code de récupération", - "heading": { - "totp": "Authentification à double authentification", - "recovery": "Récuperation de la double authentification" - } - }, - "media_modal": { - "previous": "Précédent", - "next": "Suivant" + "chat": { + "title": "Chat" + }, + "exporter": { + "export": "Exporter", + "processing": "En cours de traitement, vous pourrez bientôt télécharger votre fichier" + }, + "features_panel": { + "chat": "Chat", + "gopher": "Gopher", + "media_proxy": "Proxy média", + "scope_options": "Options de visibilité", + "text_limit": "Limite de texte", + "title": "Caractéristiques", + "who_to_follow": "Personnes à suivre" + }, + "finder": { + "error_fetching_user": "Erreur lors de la recherche de l'utilisateur·ice", + "find_user": "Chercher un-e utilisateur·ice" + }, + "general": { + "apply": "Appliquer", + "submit": "Envoyer", + "more": "Plus", + "generic_error": "Une erreur s'est produite", + "optional": "optionnel", + "show_more": "Montrer plus", + "show_less": "Montrer moins", + "cancel": "Annuler", + "disable": "Désactiver", + "enable": "Activer", + "confirm": "Confirmer", + "verify": "Vérifier", + "dismiss": "Rejeter" + }, + "image_cropper": { + "crop_picture": "Rogner l'image", + "save": "Sauvegarder", + "save_without_cropping": "Sauvegarder sans rogner", + "cancel": "Annuler" + }, + "importer": { + "submit": "Soumettre", + "success": "Importé avec succès.", + "error": "Une erreur est survenue pendant l'import de ce fichier." + }, + "login": { + "login": "Connexion", + "description": "Connexion avec OAuth", + "logout": "Déconnexion", + "password": "Mot de passe", + "placeholder": "p.e. lain", + "register": "S'inscrire", + "username": "Identifiant", + "hint": "Connectez-vous pour rejoindre la discussion", + "authentication_code": "Code d'authentification", + "enter_recovery_code": "Entrez un code de récupération", + "enter_two_factor_code": "Entrez un code à double authentification", + "recovery_code": "Code de récupération", + "heading": { + "totp": "Authentification à double authentification", + "recovery": "Récuperation de la double authentification" + } + }, + "media_modal": { + "previous": "Précédent", + "next": "Suivant" + }, + "nav": { + "about": "À propos", + "back": "Retour", + "chat": "Chat local", + "friend_requests": "Demandes de suivi", + "mentions": "Notifications", + "interactions": "Interactions", + "dms": "Messages directs", + "public_tl": "Fil d'actualité public", + "timeline": "Fil d'actualité", + "twkn": "Ensemble du réseau connu", + "user_search": "Recherche d'utilisateur·ice", + "who_to_follow": "Qui suivre", + "preferences": "Préférences", + "search": "Recherche", + "administration": "Administration" + }, + "notifications": { + "broken_favorite": "Chargement d'un message inconnu…", + "favorited_you": "a aimé votre statut", + "followed_you": "a commencé à vous suivre", + "load_older": "Charger les notifications précédentes", + "notifications": "Notifications", + "read": "Lu !", + "repeated_you": "a partagé votre statut", + "no_more_notifications": "Aucune notification supplémentaire", + "migrated_to": "a migré à", + "reacted_with": "a réagi avec {0}", + "follow_request": "veut vous suivre" + }, + "interactions": { + "favs_repeats": "Partages et favoris", + "follows": "Nouveaux suivis", + "load_older": "Chargez d'anciennes interactions", + "moves": "Migrations de comptes" + }, + "post_status": { + "new_status": "Poster un nouveau statut", + "account_not_locked_warning": "Votre compte n'est pas {0}. N'importe qui peut vous suivre pour voir vos billets en Abonné·e·s uniquement.", + "account_not_locked_warning_link": "verrouillé", + "attachments_sensitive": "Marquer le média comme sensible", + "content_type": { + "text/plain": "Texte brut", + "text/html": "HTML", + "text/markdown": "Markdown", + "text/bbcode": "BBCode" }, - "nav": { - "about": "À propos", - "back": "Retour", - "chat": "Chat local", - "friend_requests": "Demandes de suivi", - "mentions": "Notifications", - "interactions": "Interactions", - "dms": "Messages directs", - "public_tl": "Fil d'actualité public", - "timeline": "Fil d'actualité", - "twkn": "Ensemble du réseau connu", - "user_search": "Recherche d'utilisateur·ice", - "who_to_follow": "Qui suivre", - "preferences": "Préférences" + "content_warning": "Sujet (optionnel)", + "default": "Écrivez ici votre prochain statut.", + "direct_warning_to_all": "Ce message sera visible pour toutes les personnes mentionnées.", + "direct_warning_to_first_only": "Ce message sera visible uniquement pour personnes mentionnées au début du message.", + "posting": "Envoi en cours", + "scope_notice": { + "public": "Ce statut sera visible par tout le monde", + "private": "Ce statut sera visible par seulement vos abonné⋅e⋅s", + "unlisted": "Ce statut ne sera pas visible dans le Fil d'actualité public et l'Ensemble du réseau connu" }, - "notifications": { - "broken_favorite": "Chargement d'un message inconnu…", - "favorited_you": "a aimé votre statut", - "followed_you": "a commencé à vous suivre", - "load_older": "Charger les notifications précédentes", - "notifications": "Notifications", - "read": "Lu !", - "repeated_you": "a partagé votre statut", - "no_more_notifications": "Aucune notification supplémentaire" + "scope": { + "direct": "Direct - N'envoyer qu'aux personnes mentionnées", + "private": "Abonné·e·s uniquement - Seul·e·s vos abonné·e·s verront vos billets", + "public": "Publique - Afficher dans les fils publics", + "unlisted": "Non-Listé - Ne pas afficher dans les fils publics" + } + }, + "registration": { + "bio": "Biographie", + "email": "Adresse mail", + "fullname": "Pseudonyme", + "password_confirm": "Confirmation du mot de passe", + "registration": "Inscription", + "token": "Jeton d'invitation", + "captcha": "CAPTCHA", + "new_captcha": "Cliquez sur l'image pour avoir un nouveau captcha", + "username_placeholder": "p.e. lain", + "fullname_placeholder": "p.e. Lain Iwakura", + "bio_placeholder": "p.e.\nSalut, je suis Lain\nJe suis une héroïne d'animé qui vit dans une banlieue japonaise. Vous me connaissez peut-être du Wired.", + "validations": { + "username_required": "ne peut pas être laissé vide", + "fullname_required": "ne peut pas être laissé vide", + "email_required": "ne peut pas être laissé vide", + "password_required": "ne peut pas être laissé vide", + "password_confirmation_required": "ne peut pas être laissé vide", + "password_confirmation_match": "doit être identique au mot de passe" + } + }, + "selectable_list": { + "select_all": "Tout selectionner" + }, + "settings": { + "app_name": "Nom de l'application", + "security": "Sécurité", + "enter_current_password_to_confirm": "Entrez votre mot de passe actuel pour confirmer votre identité", + "mfa": { + "otp": "OTP", + "setup_otp": "Configurer OTP", + "wait_pre_setup_otp": "préconfiguration OTP", + "confirm_and_enable": "Confirmer & activer OTP", + "title": "Double authentification", + "generate_new_recovery_codes": "Générer de nouveaux codes de récupération", + "warning_of_generate_new_codes": "Quand vous générez de nouveauc codes de récupération, vos anciens codes ne fonctionnerons plus.", + "recovery_codes": "Codes de récupération.", + "waiting_a_recovery_codes": "Réception des codes de récupération…", + "recovery_codes_warning": "Écrivez les codes ou sauvez les quelquepart sécurisé - sinon vous ne les verrez plus jamais. Si vous perdez l'accès à votre application de double authentification et codes de récupération vous serez vérouillé en dehors de votre compte.", + "authentication_methods": "Methodes d'authentification", + "scan": { + "title": "Scanner", + "desc": "En utilisant votre application de double authentification, scannez ce QR code ou entrez la clé textuelle :", + "secret_code": "Clé" + }, + "verify": { + "desc": "Pour activer la double authentification, entrez le code depuis votre application :" + } }, - "interactions": { - "favs_repeats": "Partages et favoris", - "follows": "Nouveaux⋅elles abonné⋅e⋅s ?", - "load_older": "Chargez d'anciennes interactions" + "attachmentRadius": "Pièces jointes", + "attachments": "Pièces jointes", + "avatar": "Avatar", + "avatarAltRadius": "Avatars (Notifications)", + "avatarRadius": "Avatars", + "background": "Arrière-plan", + "bio": "Biographie", + "block_export": "Export des comptes bloqués", + "block_export_button": "Export des comptes bloqués vers un fichier csv", + "block_import": "Import des comptes bloqués", + "block_import_error": "Erreur lors de l'import des comptes bloqués", + "blocks_imported": "Blocks importés ! Le traitement va prendre un moment.", + "blocks_tab": "Bloqué·e·s", + "btnRadius": "Boutons", + "cBlue": "Bleu (répondre, suivre)", + "cGreen": "Vert (partager)", + "cOrange": "Orange (aimer)", + "cRed": "Rouge (annuler)", + "change_password": "Changez votre mot de passe", + "change_password_error": "Il y a eu un problème pour changer votre mot de passe.", + "changed_password": "Mot de passe modifié avec succès !", + "collapse_subject": "Réduire les messages avec des sujets", + "composing": "Composition", + "confirm_new_password": "Confirmation du nouveau mot de passe", + "current_avatar": "Avatar actuel", + "current_password": "Mot de passe actuel", + "current_profile_banner": "Bannière de profil actuelle", + "data_import_export_tab": "Import / Export des Données", + "default_vis": "Visibilité par défaut", + "delete_account": "Supprimer le compte", + "delete_account_description": "Supprimer définitivement vos données et désactiver votre compte.", + "delete_account_error": "Il y a eu un problème lors de la tentative de suppression de votre compte. Si le problème persiste, contactez l'administrateur⋅ice de cette instance.", + "delete_account_instructions": "Indiquez votre mot de passe ci-dessous pour confirmer la suppression de votre compte.", + "avatar_size_instruction": "La taille minimale recommandée pour l'image de l'avatar est de 150x150 pixels.", + "export_theme": "Enregistrer le thème", + "filtering": "Filtre", + "filtering_explanation": "Tous les statuts contenant ces mots seront masqués. Un mot par ligne", + "follow_export": "Exporter les abonnements", + "follow_export_button": "Exporter les abonnements en csv", + "follow_import": "Importer des abonnements", + "follow_import_error": "Erreur lors de l'importation des abonnements", + "follows_imported": "Abonnements importés ! Le traitement peut prendre un moment.", + "foreground": "Premier plan", + "general": "Général", + "hide_attachments_in_convo": "Masquer les pièces jointes dans les conversations", + "hide_attachments_in_tl": "Masquer les pièces jointes dans le journal", + "hide_muted_posts": "Masquer les statuts des utilisateurs masqués", + "max_thumbnails": "Nombre maximum de miniatures par statuts", + "hide_isp": "Masquer le panneau spécifique a l'instance", + "preload_images": "Précharger les images", + "use_one_click_nsfw": "Ouvrir les pièces-jointes NSFW avec un seul clic", + "hide_post_stats": "Masquer les statistiques de publication (le nombre de favoris)", + "hide_user_stats": "Masquer les statistiques de profil (le nombre d'amis)", + "hide_filtered_statuses": "Masquer les statuts filtrés", + "import_blocks_from_a_csv_file": "Importer les blocages depuis un fichier csv", + "import_followers_from_a_csv_file": "Importer des abonnements depuis un fichier csv", + "import_theme": "Charger le thème", + "inputRadius": "Champs de texte", + "checkboxRadius": "Cases à cocher", + "instance_default": "(default : {value})", + "instance_default_simple": "(default)", + "interface": "Interface", + "interfaceLanguage": "Langue de l'interface", + "invalid_theme_imported": "Le fichier sélectionné n'est pas un thème Pleroma pris en charge. Aucun changement n'a été apporté à votre thème.", + "limited_availability": "Non disponible dans votre navigateur", + "links": "Liens", + "lock_account_description": "Limitez votre compte aux abonnés acceptés uniquement", + "loop_video": "Vidéos en boucle", + "loop_video_silent_only": "Boucle uniquement les vidéos sans le son (les « gifs » de Mastodon)", + "mutes_tab": "Comptes silenciés", + "play_videos_in_modal": "Jouer les vidéos directement dans le visionneur de médias", + "use_contain_fit": "Ne pas rogner les miniatures des pièces-jointes", + "name": "Nom", + "name_bio": "Nom & Bio", + "new_password": "Nouveau mot de passe", + "notification_visibility": "Types de notifications à afficher", + "notification_visibility_follows": "Abonnements", + "notification_visibility_likes": "J'aime", + "notification_visibility_mentions": "Mentionnés", + "notification_visibility_repeats": "Partages", + "no_rich_text_description": "Ne formatez pas le texte", + "no_blocks": "Aucun bloqués", + "no_mutes": "Aucun masqués", + "hide_follows_description": "Ne pas afficher à qui je suis abonné", + "hide_followers_description": "Ne pas afficher qui est abonné à moi", + "show_admin_badge": "Afficher le badge d'Administrateur⋅ice sur mon profil", + "show_moderator_badge": "Afficher le badge de Modérateur⋅ice sur mon profil", + "nsfw_clickthrough": "Masquer les images marquées comme contenu adulte ou sensible", + "oauth_tokens": "Jetons OAuth", + "token": "Jeton", + "refresh_token": "Rafraichir le jeton", + "valid_until": "Valable jusque", + "revoke_token": "Révoquer", + "panelRadius": "Fenêtres", + "pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas actif", + "presets": "Thèmes prédéfinis", + "profile_background": "Image de fond", + "profile_banner": "Bannière de profil", + "profile_tab": "Profil", + "radii_help": "Vous pouvez ici choisir le niveau d'arrondi des angles de l'interface (en pixels)", + "replies_in_timeline": "Réponses au journal", + "reply_visibility_all": "Montrer toutes les réponses", + "reply_visibility_following": "Afficher uniquement les réponses adressées à moi ou aux personnes que je suis", + "reply_visibility_self": "Afficher uniquement les réponses adressées à moi", + "autohide_floating_post_button": "Automatiquement cacher le bouton de Nouveau Statut (sur mobile)", + "saving_err": "Erreur lors de l'enregistrement des paramètres", + "saving_ok": "Paramètres enregistrés", + "search_user_to_block": "Rechercher qui vous voulez bloquer", + "search_user_to_mute": "Rechercher qui vous voulez masquer", + "security_tab": "Sécurité", + "scope_copy": "Garder la même visibilité en répondant (les DMs restent toujours des DMs)", + "minimal_scopes_mode": "Rétrécir les options de séléction de la portée", + "set_new_avatar": "Changer d'avatar", + "set_new_profile_background": "Changer d'image de fond", + "set_new_profile_banner": "Changer de bannière", + "settings": "Paramètres", + "subject_input_always_show": "Toujours copier le champ de sujet", + "subject_line_behavior": "Copier le sujet en répondant", + "subject_line_email": "Similaire au courriel : « re : sujet »", + "subject_line_mastodon": "Comme mastodon : copier tel quel", + "subject_line_noop": "Ne pas copier", + "post_status_content_type": "Type de contenu du statuts", + "stop_gifs": "N'animer les GIFS que lors du survol du curseur de la souris", + "streaming": "Charger automatiquement les nouveaux statuts lorsque vous êtes au haut de la page", + "text": "Texte", + "theme": "Thème", + "theme_help": "Spécifiez des codes couleur hexadécimaux (#rrvvbb) pour personnaliser les couleurs du thème.", + "theme_help_v2_1": "Vous pouvez aussi surcharger certaines couleurs de composants et transparence via la case à cocher, utilisez le bouton « Vider tout » pour effacer toutes les surcharges.", + "theme_help_v2_2": "Les icônes sous certaines des entrées ont un indicateur de contraste du fond/texte, survolez les pour plus d'informations détailles. Veuillez garder a l'esprit que lors de l'utilisation de transparence l'indicateur de contraste indique le pire des cas.", + "tooltipRadius": "Info-bulles/alertes", + "upload_a_photo": "Envoyer une photo", + "user_settings": "Paramètres utilisateur", + "values": { + "false": "non", + "true": "oui" }, - "post_status": { - "new_status": "Poster un nouveau statut", - "account_not_locked_warning": "Votre compte n'est pas {0}. N'importe qui peut vous suivre pour voir vos billets en Abonné·e·s uniquement.", - "account_not_locked_warning_link": "verrouillé", - "attachments_sensitive": "Marquer le média comme sensible", - "content_type": { - "text/plain": "Texte brut", - "text/html": "HTML", - "text/markdown": "Markdown", - "text/bbcode": "BBCode" - }, - "content_warning": "Sujet (optionnel)", - "default": "Écrivez ici votre prochain statut.", - "direct_warning_to_all": "Ce message sera visible pour toutes les personnes mentionnées.", - "direct_warning_to_first_only": "Ce message sera visible uniquement pour personnes mentionnées au début du message.", - "posting": "Envoi en cours", - "scope_notice": { - "public": "Ce statut sera visible par tout le monde", - "private": "Ce statut sera visible par seulement vos abonné⋅e⋅s", - "unlisted": "Ce statut ne sera pas visible dans le Fil d'actualité public et l'Ensemble du réseau connu" + "notifications": "Notifications", + "notification_mutes": "Pour stopper la récéption de notifications d'un utilisateur particulier, utilisez un masquage.", + "notification_blocks": "Bloquer un utilisateur stoppe toute notification et se désabonne de lui.", + "enable_web_push_notifications": "Activer les notifications de push web", + "style": { + "switcher": { + "keep_color": "Garder les couleurs", + "keep_shadows": "Garder les ombres", + "keep_opacity": "Garder la transparence", + "keep_roundness": "Garder la rondeur", + "keep_fonts": "Garder les polices", + "save_load_hint": "L'option « Garder » préserve les options activés en cours lors de la séléction ou chargement des thèmes, il sauve aussi les dites options lors de l'export d'un thème. Quand toutes les cases sont décochés, exporter un thème sauvera tout.", + "reset": "Remise à zéro", + "clear_all": "Tout vider", + "clear_opacity": "Vider la transparence", + "load_theme": "Charger le thème", + "use_snapshot": "Ancienne version", + "help": { + "upgraded_from_v2": "PleromaFE à été mis à jour, le thème peut être un peu différent que dans vos souvenirs.", + "v2_imported": "Le fichier que vous avez importé vient d'un version antérieure. Nous essayons de maximizer la compatibilité mais il peu y avoir quelques incohérences.", + "future_version_imported": "Le fichier importé viens d'une version postérieure de PleromaFE.", + "older_version_imported": "Le fichier importé viens d'une version antérieure de PleromaFE.", + "snapshot_source_mismatch": "Conflict de version : Probablement due à un retour arrière puis remise à jour de la version de PleromaFE, si vous avez charger le thème en utilisant une version antérieure vous voulez probablement utiliser la version antérieure, autrement utiliser la version postérieure.", + "migration_napshot_gone": "Pour une raison inconnue l'instantané est manquant, des parties peuvent rendre différentes que dans vos souvenirs.", + "migration_snapshot_ok": "Pour être sûr un instantanée du thème à été chargé. Vos pouvez essayer de charger ses données.", + "fe_downgraded": "Retour en arrière de la version de PleromaFE.", + "fe_upgraded": "Le moteur de thème PleromaFE à été mis à jour après un changement de version.", + "snapshot_missing": "Aucun instantané du thème à été trouvé dans le fichier, il peut y avoir un rendu différent à la vision originelle.", + "snapshot_present": "Un instantané du thème à été chargé, toutes les valeurs sont dont écrasées. Vous pouvez autrement charger le thème complètement." }, - "scope": { - "direct": "Direct - N'envoyer qu'aux personnes mentionnées", - "private": "Abonné·e·s uniquement - Seul·e·s vos abonné·e·s verront vos billets", - "public": "Publique - Afficher dans les fils publics", - "unlisted": "Non-Listé - Ne pas afficher dans les fils publics" + "keep_as_is": "Garder tel-quel", + "use_source": "Nouvelle version" + }, + "common": { + "color": "Couleur", + "opacity": "Transparence", + "contrast": { + "hint": "Le ratio de contraste est {ratio}, il {level} {context}", + "level": { + "aa": "répond aux directives de niveau AA (minimum)", + "aaa": "répond aux directives de niveau AAA (recommandé)", + "bad": "ne réponds à aucune directive d'accessibilité" + }, + "context": { + "18pt": "pour texte large (19pt+)", + "text": "pour texte" + } } - }, - "registration": { - "bio": "Biographie", - "email": "Adresse mail", - "fullname": "Pseudonyme", - "password_confirm": "Confirmation du mot de passe", - "registration": "Inscription", - "token": "Jeton d'invitation", - "captcha": "CAPTCHA", - "new_captcha": "Cliquez sur l'image pour avoir un nouveau captcha", - "username_placeholder": "p.e. lain", - "fullname_placeholder": "p.e. Lain Iwakura", - "bio_placeholder": "p.e.\nSalut, je suis Lain\nJe suis une héroïne d'animé qui vit dans une banlieue japonaise. Vous me connaissez peut-être du Wired.", - "validations": { - "username_required": "ne peut pas être laissé vide", - "fullname_required": "ne peut pas être laissé vide", - "email_required": "ne peut pas être laissé vide", - "password_required": "ne peut pas être laissé vide", - "password_confirmation_required": "ne peut pas être laissé vide", - "password_confirmation_match": "doit être identique au mot de passe" - } - }, - "selectable_list": { - "select_all": "Tout selectionner" - }, - "settings": { - "app_name": "Nom de l'application", - "security": "Sécurité", - "enter_current_password_to_confirm": "Entrez votre mot de passe actuel pour confirmer votre identité", - "mfa": { - "otp": "OTP", - "setup_otp": "Configurer OTP", - "wait_pre_setup_otp": "préconfiguration OTP", - "confirm_and_enable": "Confirmer & activer OTP", - "title": "Double authentification", - "generate_new_recovery_codes": "Générer de nouveaux codes de récupération", - "warning_of_generate_new_codes": "Quand vous générez de nouveauc codes de récupération, vos anciens codes ne fonctionnerons plus.", - "recovery_codes": "Codes de récupération.", - "waiting_a_recovery_codes": "Récéption des codes de récupération…", - "recovery_codes_warning": "Écrivez les codes ou sauvez les quelquepart sécurisé - sinon vous ne les verrez plus jamais. Si vous perdez l'accès à votre application de double authentification et codes de récupération vous serez vérouillé en dehors de votre compte.", - "authentication_methods": "Methodes d'authentification", - "scan": { - "title": "Scanner", - "desc": "En utilisant votre application de double authentification, scannez ce QR code ou entrez la clé textuelle :", - "secret_code": "Clé" - }, - "verify": { - "desc": "Pour activer la double authentification, entrez le code depuis votre application:" - } + }, + "common_colors": { + "_tab_label": "Commun", + "main": "Couleurs communes", + "foreground_hint": "Voir l'onglet « Avancé » pour plus de contrôle détaillé", + "rgbo": "Icônes, accents, badges" + }, + "advanced_colors": { + "_tab_label": "Avancé", + "alert": "Fond d'alerte", + "alert_error": "Erreur", + "badge": "Fond de badge", + "badge_notification": "Notification", + "panel_header": "Entête de panneau", + "top_bar": "Barre du haut", + "borders": "Bordures", + "buttons": "Boutons", + "inputs": "Champs de saisie", + "faint_text": "Texte en fondu", + "underlay": "sous-calque", + "pressed": "Appuyé", + "alert_warning": "Avertissement", + "alert_neutral": "Neutre", + "post": "Messages/Bios des comptes", + "poll": "Graphique de Sondage", + "icons": "Icônes", + "selectedPost": "Message sélectionné", + "selectedMenu": "Objet sélectionné du menu", + "disabled": "Désactivé", + "tabs": "Onglets", + "toggled": "(Dés)activé", + "highlight": "Éléments mis en valeur", + "popover": "Infobulles, menus" + }, + "radii": { + "_tab_label": "Rondeur" + }, + "shadows": { + "_tab_label": "Ombres et éclairage", + "component": "Composant", + "override": "Surcharger", + "shadow_id": "Ombre #{value}", + "blur": "Flou", + "spread": "Dispersion", + "inset": "Interne", + "hint": "Pour les ombres, vous pouvez aussi utiliser --variable comme valeur de couleur en CSS3. Veuillez noter que spécifier la transparence ne fonctionnera pas dans ce cas.", + "filter_hint": { + "always_drop_shadow": "Attention, cette ombre utilise toujours {0} quand le navigateur le supporte.", + "drop_shadow_syntax": "{0} ne supporte pas le paramètre {1} et mot-clé {2}.", + "avatar_inset": "Veuillez noter que combiner a la fois les ombres internes et non-internes sur les avatars peut fournir des résultats innatendus avec la transparence des avatars.", + "spread_zero": "Les ombres avec une dispersion > 0 apparaitrons comme si ils étaient à zéro", + "inset_classic": "L'ombre interne utilisera toujours {0}" }, - "attachmentRadius": "Pièces jointes", - "attachments": "Pièces jointes", - "autoload": "Charger la suite automatiquement une fois le bas de la page atteint", - "avatar": "Avatar", - "avatarAltRadius": "Avatars (Notifications)", - "avatarRadius": "Avatars", - "background": "Arrière-plan", - "bio": "Biographie", - "block_export": "Export des comptes bloqués", - "block_export_button": "Export des comptes bloqués vers un fichier csv", - "block_import": "Import des comptes bloqués", - "block_import_error": "Erreur lors de l'import des comptes bloqués", - "blocks_imported": "Blocks importés! Le traitement va prendre un moment.", - "blocks_tab": "Bloqué·e·s", - "btnRadius": "Boutons", - "cBlue": "Bleu (répondre, suivre)", - "cGreen": "Vert (partager)", - "cOrange": "Orange (aimer)", - "cRed": "Rouge (annuler)", - "change_password": "Changez votre mot de passe", - "change_password_error": "Il y a eu un problème pour changer votre mot de passe.", - "changed_password": "Mot de passe modifié avec succès !", - "collapse_subject": "Réduire les messages avec des sujets", - "composing": "Composition", - "confirm_new_password": "Confirmation du nouveau mot de passe", - "current_avatar": "Avatar actuel", - "current_password": "Mot de passe actuel", - "current_profile_banner": "Bannière de profil actuelle", - "data_import_export_tab": "Import / Export des Données", - "default_vis": "Visibilité par défaut", - "delete_account": "Supprimer le compte", - "delete_account_description": "Supprimer définitivement votre compte et tous vos statuts.", - "delete_account_error": "Il y a eu un problème lors de la tentative de suppression de votre compte. Si le problème persiste, contactez l'administrateur⋅ice de cette instance.", - "delete_account_instructions": "Indiquez votre mot de passe ci-dessous pour confirmer la suppression de votre compte.", - "avatar_size_instruction": "La taille minimale recommandée pour l'image de l'avatar est de 150x150 pixels.", - "export_theme": "Enregistrer le thème", - "filtering": "Filtre", - "filtering_explanation": "Tous les statuts contenant ces mots seront masqués. Un mot par ligne", - "follow_export": "Exporter les abonnements", - "follow_export_button": "Exporter les abonnements en csv", - "follow_import": "Importer des abonnements", - "follow_import_error": "Erreur lors de l'importation des abonnements", - "follows_imported": "Abonnements importés ! Le traitement peut prendre un moment.", - "foreground": "Premier plan", - "general": "Général", - "hide_attachments_in_convo": "Masquer les pièces jointes dans les conversations", - "hide_attachments_in_tl": "Masquer les pièces jointes dans le journal", - "hide_muted_posts": "Masquer les statuts des utilisateurs masqués", - "max_thumbnails": "Nombre maximum de miniatures par statuts", - "hide_isp": "Masquer le panneau spécifique a l'instance", - "preload_images": "Précharger les images", - "use_one_click_nsfw": "Ouvrir les pièces-jointes NSFW avec un seul clic", - "hide_post_stats": "Masquer les statistiques de publication (le nombre de favoris)", - "hide_user_stats": "Masquer les statistiques de profil (le nombre d'amis)", - "hide_filtered_statuses": "Masquer les statuts filtrés", - "import_blocks_from_a_csv_file": "Importer les blocages depuis un fichier csv", - "import_followers_from_a_csv_file": "Importer des abonnements depuis un fichier csv", - "import_theme": "Charger le thème", - "inputRadius": "Champs de texte", - "checkboxRadius": "Cases à cocher", - "instance_default": "(default: {value})", - "instance_default_simple": "(default)", - "interface": "Interface", - "interfaceLanguage": "Langue de l'interface", - "invalid_theme_imported": "Le fichier sélectionné n'est pas un thème Pleroma pris en charge. Aucun changement n'a été apporté à votre thème.", - "limited_availability": "Non disponible dans votre navigateur", - "links": "Liens", - "lock_account_description": "Limitez votre compte aux abonnés acceptés uniquement", - "loop_video": "Vidéos en boucle", - "loop_video_silent_only": "Boucle uniquement les vidéos sans le son (les « gifs » de Mastodon)", - "mutes_tab": "Comptes silenciés", - "play_videos_in_modal": "Jouer les vidéos directement dans le visionneur de médias", - "use_contain_fit": "Ne pas rogner les miniatures des pièces-jointes", - "name": "Nom", - "name_bio": "Nom & Bio", - "new_password": "Nouveau mot de passe", - "notification_visibility": "Types de notifications à afficher", - "notification_visibility_follows": "Abonnements", - "notification_visibility_likes": "J'aime", - "notification_visibility_mentions": "Mentionnés", - "notification_visibility_repeats": "Partages", - "no_rich_text_description": "Ne formatez pas le texte", - "no_blocks": "Aucun bloqués", - "no_mutes": "Aucun masqués", - "hide_follows_description": "Ne pas afficher à qui je suis abonné", - "hide_followers_description": "Ne pas afficher qui est abonné à moi", - "show_admin_badge": "Afficher le badge d'Administrateur⋅ice sur mon profil", - "show_moderator_badge": "Afficher le badge de Modérateur⋅ice sur mon profil", - "nsfw_clickthrough": "Masquer les images marquées comme contenu adulte ou sensible", - "oauth_tokens": "Jetons OAuth", - "token": "Jeton", - "refresh_token": "Refresh Token", - "valid_until": "Valable jusque", - "revoke_token": "Révoquer", - "panelRadius": "Fenêtres", - "pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas actif", - "presets": "Thèmes prédéfinis", - "profile_background": "Image de fond", - "profile_banner": "Bannière de profil", - "profile_tab": "Profil", - "radii_help": "Vous pouvez ici choisir le niveau d'arrondi des angles de l'interface (en pixels)", - "replies_in_timeline": "Réponses au journal", - "reply_link_preview": "Afficher un aperçu lors du survol de liens vers une réponse", - "reply_visibility_all": "Montrer toutes les réponses", - "reply_visibility_following": "Afficher uniquement les réponses adressées à moi ou aux personnes que je suis", - "reply_visibility_self": "Afficher uniquement les réponses adressées à moi", - "autohide_floating_post_button": "Automatiquement cacher le bouton de Nouveau Statut (sur mobile)", - "saving_err": "Erreur lors de l'enregistrement des paramètres", - "saving_ok": "Paramètres enregistrés", - "search_user_to_block": "Rechercher qui vous voulez bloquer", - "search_user_to_mute": "Rechercher qui vous voulez masquer", - "security_tab": "Sécurité", - "scope_copy": "Garder la même visibilité en répondant (les DMs restent toujours des DMs)", - "minimal_scopes_mode": "Rétrécir les options de séléction de la portée", - "set_new_avatar": "Changer d'avatar", - "set_new_profile_background": "Changer d'image de fond", - "set_new_profile_banner": "Changer de bannière", - "settings": "Paramètres", - "subject_input_always_show": "Toujours copier le champ de sujet", - "subject_line_behavior": "Copier le sujet en répondant", - "subject_line_email": "Comme les mails: « re: sujet »", - "subject_line_mastodon": "Comme mastodon: copier tel quel", - "subject_line_noop": "Ne pas copier", - "post_status_content_type": "Type de contenu du statuts", - "stop_gifs": "N'animer les GIFS que lors du survol du curseur de la souris", - "streaming": "Charger automatiquement les nouveaux statuts lorsque vous êtes au haut de la page", - "text": "Texte", - "theme": "Thème", - "theme_help": "Spécifiez des codes couleur hexadécimaux (#rrvvbb) pour personnaliser les couleurs du thème.", - "theme_help_v2_1": "Vous pouvez aussi surcharger certaines couleurs de composants et transparence via la case à cocher, utilisez le bouton « Vider tout » pour effacer toutes les surcharges.", - "theme_help_v2_2": "Les icônes sous certaines des entrées ont un indicateur de contraste du fond/texte, survolez les pour plus d'informations détailles. Veuillez garder a l'esprit que lors de l'utilisation de transparence l'indicateur de contraste indique le pire des cas.", - "tooltipRadius": "Info-bulles/alertes", - "upload_a_photo": "Envoyer une photo", - "user_settings": "Paramètres utilisateur", - "values": { - "false": "non", - "true": "oui" + "components": { + "panel": "Panneau", + "panelHeader": "En-tête de panneau", + "topBar": "Barre du haut", + "avatar": "Avatar utilisateur⋅ice (dans la vue de profil)", + "avatarStatus": "Avatar utilisateur⋅ice (dans la vue de statuts)", + "popup": "Popups et infobulles", + "button": "Bouton", + "buttonHover": "Bouton (survol)", + "buttonPressed": "Bouton (cliqué)", + "buttonPressedHover": "Bouton (cliqué+survol)", + "input": "Champ de saisie" }, - "notifications": "Notifications", - "notification_setting": "Reçevoir les notifications de:", - "notification_setting_follows": "Utilisateurs que vous suivez", - "notification_setting_non_follows": "Utilisateurs que vous ne suivez pas", - "notification_setting_followers": "Utilisateurs qui vous suivent", - "notification_setting_non_followers": "Utilisateurs qui ne vous suivent pas", - "notification_mutes": "Pour stopper la récéption de notifications d'un utilisateur particulier, utilisez un masquage.", - "notification_blocks": "Bloquer un utilisateur stoppe toute notification et se désabonne de lui.", - "enable_web_push_notifications": "Activer les notifications de push web", - "style": { - "switcher": { - "keep_color": "Garder les couleurs", - "keep_shadows": "Garder les ombres", - "keep_opacity": "Garder la transparence", - "keep_roundness": "Garder la rondeur", - "keep_fonts": "Garder les polices", - "save_load_hint": "L'option « Garder » préserve les options activés en cours lors de la séléction ou chargement des thèmes, il sauve aussi les dites options lors de l'export d'un thème. Quand toutes les cases sont décochés, exporter un thème sauvera tout.", - "reset": "Remise à zéro", - "clear_all": "Tout vider", - "clear_opacity": "Vider la transparence" - }, - "common": { - "color": "Couleur", - "opacity": "Transparence", - "contrast": { - "hint": "Le ratio de contraste est {ratio}, il {level} {context}", - "level": { - "aa": "répond aux directives de niveau AA (minimum)", - "aaa": "répond aux directives de niveau AAA (recommandé)", - "bad": "ne réponds à aucune directive d'accessibilité" - }, - "context": { - "18pt": "pour texte large (19pt+)", - "text": "pour texte" - } - } - }, - "common_colors": { - "_tab_label": "Commun", - "main": "Couleurs communes", - "foreground_hint": "Voir l'onglet « Avancé » pour plus de contrôle détaillé", - "rgbo": "Icônes, accents, badges" - }, - "advanced_colors": { - "_tab_label": "Avancé", - "alert": "Fond d'alerte", - "alert_error": "Erreur", - "badge": "Fond de badge", - "badge_notification": "Notification", - "panel_header": "Entête de panneau", - "top_bar": "Barre du haut", - "borders": "Bordures", - "buttons": "Boutons", - "inputs": "Champs de saisie", - "faint_text": "Texte en fondu" - }, - "radii": { - "_tab_label": "Rondeur" - }, - "shadows": { - "_tab_label": "Ombres et éclairage", - "component": "Composant", - "override": "Surcharger", - "shadow_id": "Ombre #{value}", - "blur": "Flou", - "spread": "Dispersion", - "inset": "Interne", - "hint": "Pour les ombres, vous pouvez aussi utiliser --variable comme valeur de couleur en CSS3. Veuillez noter que spécifier la transparence ne fonctionnera pas dans ce cas.", - "filter_hint": { - "always_drop_shadow": "Attention, cette ombre utilise toujours {0} quand le navigateur le supporte.", - "drop_shadow_syntax": "{0} ne supporte pas le paramètre {1} et mot-clé {2}.", - "avatar_inset": "Veuillez noter que combiner a la fois les ombres internes et non-internes sur les avatars peut fournir des résultats innatendus avec la transparence des avatars.", - "spread_zero": "Les ombres avec une dispersion > 0 apparaitrons comme si ils étaient à zéro", - "inset_classic": "L'ombre interne utilisera toujours {0}" - }, - "components": { - "panel": "Panneau", - "panelHeader": "En-tête de panneau", - "topBar": "Barre du haut", - "avatar": "Avatar utilisateur⋅ice (dans la vue de profil)", - "avatarStatus": "Avatar utilisateur⋅ice (dans la vue de statuts)", - "popup": "Popups et infobulles", - "button": "Bouton", - "buttonHover": "Bouton (survol)", - "buttonPressed": "Bouton (cliqué)", - "buttonPressedHover": "Bouton (cliqué+survol)", - "input": "Champ de saisie" - } - }, - "fonts": { - "_tab_label": "Polices", - "help": "Sélectionnez la police à utiliser pour les éléments de l'UI. Pour « personnalisé » vous avez à entrer le nom exact de la police comme il apparaît dans le système.", - "components": { - "interface": "Interface", - "input": "Champs de saisie", - "post": "Post text", - "postCode": "Texte à taille fixe dans un article (texte enrichi)" - }, - "family": "Nom de la police", - "size": "Taille (en px)", - "weight": "Poid (gras)", - "custom": "Personnalisé" - }, - "preview": { - "header": "Prévisualisation", - "content": "Contenu", - "error": "Exemple d'erreur", - "button": "Bouton", - "text": "Un certain nombre de {0} et {1}", - "mono": "contenu", - "input": "Je viens juste d’atterrir à L.A.", - "faint_link": "manuel utile", - "fine_print": "Lisez notre {0} pour n'apprendre rien d'utile !", - "header_faint": "Tout va bien", - "checkbox": "J'ai survolé les conditions d'utilisation", - "link": "un petit lien sympa" - } + "hintV3": "Pour les ombres vous pouvez aussi utiliser la notation {0} pour utiliser un autre emplacement de couleur." + }, + "fonts": { + "_tab_label": "Polices", + "help": "Sélectionnez la police à utiliser pour les éléments de l'UI. Pour « personnalisé » vous avez à entrer le nom exact de la police comme il apparaît dans le système.", + "components": { + "interface": "Interface", + "input": "Champs de saisie", + "post": "Post text", + "postCode": "Texte à taille fixe dans un article (texte enrichi)" }, - "version": { - "title": "Version", - "backend_version": "Version du Backend", - "frontend_version": "Version du Frontend" - } - }, - "timeline": { - "collapse": "Fermer", - "conversation": "Conversation", - "error_fetching": "Erreur en cherchant les mises à jour", - "load_older": "Afficher plus", - "no_retweet_hint": "Le message est marqué en abonnés-seulement ou direct et ne peut pas être partagé", - "repeated": "a partagé", - "show_new": "Afficher plus", - "up_to_date": "À jour", - "no_more_statuses": "Pas plus de statuts", - "no_statuses": "Aucun statuts" + "family": "Nom de la police", + "size": "Taille (en px)", + "weight": "Poid (gras)", + "custom": "Personnalisé" + }, + "preview": { + "header": "Prévisualisation", + "content": "Contenu", + "error": "Exemple d'erreur", + "button": "Bouton", + "text": "Un certain nombre de {0} et {1}", + "mono": "contenu", + "input": "Je viens juste d’atterrir à L.A.", + "faint_link": "manuel utile", + "fine_print": "Lisez notre {0} pour n'apprendre rien d'utile !", + "header_faint": "Tout va bien", + "checkbox": "J'ai survolé les conditions d'utilisation", + "link": "un petit lien sympa" + } }, - "status": { - "favorites": "Favoris", - "repeats": "Partages", - "delete": "Supprimer statuts", - "pin": "Agraffer sur le profil", - "unpin": "Dégraffer du profil", - "pinned": "Agraffé", - "delete_confirm": "Voulez-vous vraiment supprimer ce statuts ?", - "reply_to": "Réponse à", - "replies_list": "Réponses:" + "version": { + "title": "Version", + "backend_version": "Version du Backend", + "frontend_version": "Version du Frontend" }, - "user_card": { - "approve": "Accepter", - "block": "Bloquer", - "blocked": "Bloqué !", - "deny": "Rejeter", - "favorites": "Favoris", - "follow": "Suivre", - "follow_sent": "Demande envoyée !", - "follow_progress": "Demande en cours…", - "follow_again": "Renvoyer la demande ?", - "follow_unfollow": "Désabonner", - "followees": "Suivis", - "followers": "Vous suivent", - "following": "Suivi !", - "follows_you": "Vous suit !", - "its_you": "C'est vous !", - "media": "Media", - "mute": "Masquer", - "muted": "Masqué", - "per_day": "par jour", - "remote_follow": "Suivre d'une autre instance", - "report": "Signalement", - "statuses": "Statuts", - "unblock": "Débloquer", - "unblock_progress": "Déblocage…", - "block_progress": "Blocage…", - "unmute": "Démasquer", - "unmute_progress": "Démasquage…", - "mute_progress": "Masquage…", - "admin_menu": { - "moderation": "Moderation", - "grant_admin": "Promouvoir Administrateur⋅ice", - "revoke_admin": "Dégrader Administrateur⋅ice", - "grant_moderator": "Promouvoir Modérateur⋅ice", - "revoke_moderator": "Dégrader Modérateur⋅ice", - "activate_account": "Activer le compte", - "deactivate_account": "Désactiver le compte", - "delete_account": "Supprimer le compte", - "force_nsfw": "Marquer tous les statuts comme NSFW", - "strip_media": "Supprimer les medias des statuts", - "force_unlisted": "Forcer les statuts à être délistés", - "sandbox": "Forcer les statuts à être visibles seuleument pour les abonné⋅e⋅s", - "disable_remote_subscription": "Interdir de s'abonner a l'utilisateur depuis l'instance distante", - "disable_any_subscription": "Interdir de s'abonner à l'utilisateur tout court", - "quarantine": "Interdir les statuts de l'utilisateur à fédérer", - "delete_user": "Supprimer l'utilisateur", - "delete_user_confirmation": "Êtes-vous absolument-sûr⋅e ? Cette action ne peut être annulée." - } + "change_email": "Changer de courriel", + "domain_mutes": "Domaines", + "pad_emoji": "Rajouter un espace autour de l'émoji après l’avoir choisit", + "notification_visibility_emoji_reactions": "Réactions", + "hide_follows_count_description": "Masquer le nombre de suivis", + "useStreamingApiWarning": "(Non recommandé, expérimental, connu pour rater des messages)", + "type_domains_to_mute": "Écrire les domaines à masquer", + "fun": "Rigolo", + "greentext": "greentexting", + "allow_following_move": "Suivre automatiquement quand ce compte migre", + "change_email_error": "Il y a eu un problème pour charger votre courriel.", + "changed_email": "Courriel changé avec succès !", + "discoverable": "Permettre de découvrir ce compte dans les résultats de recherche web et autres services", + "emoji_reactions_on_timeline": "Montrer les émojis-réactions dans le flux", + "new_email": "Nouveau courriel", + "notification_visibility_moves": "Migrations de compte", + "user_mutes": "Comptes", + "useStreamingApi": "Recevoir les messages et notifications en temps réel", + "notification_setting_filters": "Filtres", + "notification_setting_privacy_option": "Masquer l'expéditeur et le contenu des notifications push", + "notification_setting_privacy": "Intimité", + "hide_followers_count_description": "Masquer le nombre d'abonnés", + "accent": "Accent" + }, + "timeline": { + "collapse": "Fermer", + "conversation": "Conversation", + "error_fetching": "Erreur en cherchant les mises à jour", + "load_older": "Afficher plus", + "no_retweet_hint": "Le message est marqué en abonnés-seulement ou direct et ne peut pas être partagé", + "repeated": "a partagé", + "show_new": "Afficher plus", + "up_to_date": "À jour", + "no_more_statuses": "Pas plus de statuts", + "no_statuses": "Aucun statuts" + }, + "status": { + "favorites": "Favoris", + "repeats": "Partages", + "delete": "Supprimer statuts", + "pin": "Agraffer sur le profil", + "unpin": "Dégraffer du profil", + "pinned": "Agraffé", + "delete_confirm": "Voulez-vous vraiment supprimer ce statuts ?", + "reply_to": "Réponse à", + "replies_list": "Réponses :", + "mute_conversation": "Masquer la conversation", + "unmute_conversation": "Démasquer la conversation", + "status_unavailable": "Status indisponible", + "copy_link": "Copier le lien au status" + }, + "user_card": { + "approve": "Accepter", + "block": "Bloquer", + "blocked": "Bloqué !", + "deny": "Rejeter", + "favorites": "Favoris", + "follow": "Suivre", + "follow_sent": "Demande envoyée !", + "follow_progress": "Demande en cours…", + "follow_again": "Renvoyer la demande ?", + "follow_unfollow": "Désabonner", + "followees": "Suivis", + "followers": "Vous suivent", + "following": "Suivi !", + "follows_you": "Vous suit !", + "its_you": "C'est vous !", + "media": "Media", + "mute": "Masquer", + "muted": "Masqué", + "per_day": "par jour", + "remote_follow": "Suivre d'une autre instance", + "report": "Signalement", + "statuses": "Statuts", + "unblock": "Débloquer", + "unblock_progress": "Déblocage…", + "block_progress": "Blocage…", + "unmute": "Démasquer", + "unmute_progress": "Démasquage…", + "mute_progress": "Masquage…", + "admin_menu": { + "moderation": "Moderation", + "grant_admin": "Promouvoir Administrateur⋅ice", + "revoke_admin": "Dégrader Administrateur⋅ice", + "grant_moderator": "Promouvoir Modérateur⋅ice", + "revoke_moderator": "Dégrader Modérateur⋅ice", + "activate_account": "Activer le compte", + "deactivate_account": "Désactiver le compte", + "delete_account": "Supprimer le compte", + "force_nsfw": "Marquer tous les statuts comme NSFW", + "strip_media": "Supprimer les medias des statuts", + "force_unlisted": "Forcer les statuts à être délistés", + "sandbox": "Forcer les statuts à être visibles seuleument pour les abonné⋅e⋅s", + "disable_remote_subscription": "Interdir de s'abonner a l'utilisateur depuis l'instance distante", + "disable_any_subscription": "Interdir de s'abonner à l'utilisateur tout court", + "quarantine": "Interdir les statuts de l'utilisateur à fédérer", + "delete_user": "Supprimer l'utilisateur", + "delete_user_confirmation": "Êtes-vous absolument-sûr⋅e ? Cette action ne peut être annulée." }, - "user_profile": { - "timeline_title": "Journal de l'utilisateur⋅ice", - "profile_does_not_exist": "Désolé, ce profil n'existe pas.", - "profile_loading_error": "Désolé, il y a eu une erreur au chargement du profil." + "mention": "Mention", + "hidden": "Caché", + "subscribe": "Abonner", + "unsubscribe": "Désabonner", + "hide_repeats": "Cacher les partages", + "show_repeats": "Montrer les partages" + }, + "user_profile": { + "timeline_title": "Journal de l'utilisateur⋅ice", + "profile_does_not_exist": "Désolé, ce profil n'existe pas.", + "profile_loading_error": "Désolé, il y a eu une erreur au chargement du profil." + }, + "user_reporting": { + "title": "Signaler {0}", + "add_comment_description": "Ce signalement sera envoyé aux modérateur⋅ice⋅s de votre instance. Vous pouvez fournir une explication de pourquoi vous signalez ce compte ci-dessous :", + "additional_comments": "Commentaires additionnels", + "forward_description": "Le compte vient d'un autre serveur. Envoyer une copie du signalement à celui-ci aussi ?", + "forward_to": "Transmettre à {0}", + "submit": "Envoyer", + "generic_error": "Une erreur est survenue lors du traitement de votre requête." + }, + "who_to_follow": { + "more": "Plus", + "who_to_follow": "À qui s'abonner" + }, + "tool_tip": { + "media_upload": "Envoyer un media", + "repeat": "Répéter", + "reply": "Répondre", + "favorite": "Favoriser", + "user_settings": "Paramètres utilisateur", + "add_reaction": "Ajouter une réaction", + "accept_follow_request": "Accepter la demande de suivit", + "reject_follow_request": "Rejeter la demande de suivit" + }, + "upload": { + "error": { + "base": "L'envoi a échoué.", + "file_too_big": "Fichier trop gros [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "Réessayez plus tard" }, - "user_reporting": { - "title": "Signaler {0}", - "add_comment_description": "Ce signalement sera envoyé aux modérateur⋅ice⋅s de votre instance. Vous pouvez fournir une explication de pourquoi vous signalez ce compte ci-dessous :", - "additional_comments": "Commentaires additionnels", - "forward_description": "Le compte vient d'un autre serveur. Envoyer une copie du signalement à celui-ci aussi ?", - "forward_to": "Transmettre à {0}", - "submit": "Envoyer", - "generic_error": "Une erreur est survenue lors du traitement de votre requête." - }, - "who_to_follow": { - "more": "Plus", - "who_to_follow": "À qui s'abonner" - }, - "tool_tip": { - "media_upload": "Envoyer un media", - "repeat": "Répéter", - "reply": "Répondre", - "favorite": "Favoriser", - "user_settings": "Paramètres utilisateur" - }, - "upload": { - "error": { - "base": "L'envoi a échoué.", - "file_too_big": "Fichier trop gros [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", - "default": "Réessayez plus tard" - }, - "file_size_units": { - "B": "O", - "KiB": "KiO", - "MiB": "MiO", - "GiB": "GiO", - "TiB": "TiO" - } + "file_size_units": { + "B": "O", + "KiB": "KiO", + "MiB": "MiO", + "GiB": "GiO", + "TiB": "TiO" } + }, + "about": { + "mrf": { + "keyword": { + "reject": "Rejeté", + "replace": "Remplacer", + "keyword_policies": "Politiques par mot-clés", + "ftl_removal": "Suppression du flux fédéré", + "is_replaced_by": "→" + }, + "simple": { + "simple_policies": "Politiques par instances", + "accept": "Accepter", + "accept_desc": "Cette instance accepte des messages seulement depuis ces instances :", + "reject": "Rejeter", + "reject_desc": "Cette instance n'acceptera pas de message de ces instances :", + "quarantine": "Quarantaine", + "quarantine_desc": "Cette instance enverras seulement des messages publics à ces instances :", + "ftl_removal_desc": "Cette instance supprime ces instance du flux fédéré :", + "media_removal": "Suppression multimédia", + "media_removal_desc": "Cette instance supprime le contenu multimédia des instances suivantes :", + "media_nsfw": "Force le contenu multimédia comme sensible", + "ftl_removal": "Suppression du flux fédéré", + "media_nsfw_desc": "Cette instance force le contenu multimédia comme sensible pour les messages des instances suivantes :" + }, + "federation": "Fédération", + "mrf_policies": "Politiques MRF activées", + "mrf_policies_desc": "Les politiques MRF modifient la fédération entre les instances. Les politiques suivantes sont activées :" + }, + "staff": "Staff" + }, + "domain_mute_card": { + "mute": "Muet", + "mute_progress": "Masquage…", + "unmute": "Démasquer", + "unmute_progress": "Démasquage…" + }, + "polls": { + "add_poll": "Ajouter un Sondage", + "add_option": "Ajouter une option", + "option": "Option", + "votes": "votes", + "type": "Type de Sondage", + "single_choice": "Choix unique", + "multiple_choices": "Choix multiples", + "expiry": "Age du sondage", + "expires_in": "Fin du sondage dans {0}", + "not_enough_options": "Trop peu d'options unique au sondage", + "vote": "Voter", + "expired": "Sondage terminé il y a {0}" + }, + "emoji": { + "emoji": "Émoji", + "search_emoji": "Rechercher un émoji", + "add_emoji": "Insérer un émoji", + "custom": "émoji personnalisé", + "unicode": "émoji unicode", + "load_all": "Charger tout les {emojiAmount} émojis", + "load_all_hint": "{saneAmount} émojis chargé, charger tout les émojis peuvent causer des problèmes de performances.", + "stickers": "Stickers", + "keep_open": "Garder le sélecteur ouvert" + }, + "remote_user_resolver": { + "error": "Non trouvé.", + "searching_for": "Rechercher", + "remote_user_resolver": "Résolution de compte distant" + }, + "time": { + "minutes_short": "{0}min", + "second_short": "{0}s", + "day": "{0} jour", + "days": "{0} jours", + "months": "{0} mois", + "month_short": "{0}m", + "months_short": "{0}m", + "now": "tout de suite", + "now_short": "maintenant", + "second": "{0} seconde", + "seconds": "{0} secondes", + "seconds_short": "{0}s", + "day_short": "{0}j", + "days_short": "{0}j", + "hour": "{0} heure", + "hours": "{0} heures", + "hour_short": "{0}h", + "hours_short": "{0}h", + "in_future": "dans {0}", + "in_past": "il y a {0}", + "minute": "{0} minute", + "minutes": "{0} minutes", + "minute_short": "{0}min", + "month": "{0} mois", + "week": "{0} semaine", + "weeks": "{0} semaines", + "week_short": "{0}s", + "weeks_short": "{0}s", + "year": "{0} année", + "years": "{0} années", + "year_short": "{0}a", + "years_short": "{0}a" + }, + "search": { + "people": "Comptes", + "person_talking": "{count} personnes discutant", + "hashtags": "Mot-dièses", + "people_talking": "{count} personnes discutant", + "no_results": "Aucun résultats" + }, + "password_reset": { + "forgot_password": "Mot de passe oublié ?", + "check_email": "Vérifiez vos courriels pour le lien permettant de changer votre mot de passe.", + "password_reset_disabled": "Le changement de mot de passe est désactivé. Veuillez contacter l'administration de votre instance.", + "password_reset_required_but_mailer_is_disabled": "Vous devez changer votre mot de passe mais sont changement est désactivé. Veuillez contacter l’administration de votre instance.", + "password_reset": "Nouveau mot de passe", + "instruction": "Entrer votre address de courriel ou votre nom utilisateur. Nous enverrons un lien pour changer votre mot de passe.", + "placeholder": "Votre email ou nom d'utilisateur", + "return_home": "Retourner à la page d'accueil", + "not_found": "Email ou nom d'utilisateur inconnu.", + "too_many_requests": "Vos avez atteint la limite d'essais, essayez plus tard.", + "password_reset_required": "Vous devez changer votre mot de passe pour vous authentifier." + } } diff --git a/src/i18n/ga.json b/src/i18n/ga.json @@ -36,7 +36,7 @@ "twkn": "An Líonra Iomlán" }, "notifications": { - "broken_favorite": "Post anaithnid. Cuardach dó...", + "broken_favorite": "Post anaithnid. Cuardach dó…", "favorited_you": "toghadh le do phost", "followed_you": "lean tú", "load_older": "Luchtaigh fógraí aosta", @@ -73,7 +73,6 @@ "settings": { "attachmentRadius": "Ceangaltáin", "attachments": "Ceangaltáin", - "autoload": "Cumasaigh luchtú uathoibríoch nuair a scrollaítear go bun", "avatar": "Phictúir phrófíle", "avatarAltRadius": "Phictúirí phrófíle (Fograí)", "avatarRadius": "Phictúirí phrófíle", @@ -85,7 +84,7 @@ "cOrange": "Oráiste (Cosúil)", "cRed": "Dearg (Cealaigh)", "change_password": "Athraigh do pasfhocal", - "change_password_error": "Bhí fadhb ann ag athrú do pasfhocail", + "change_password_error": "Bhí fadhb ann ag athrú do pasfhocail.", "changed_password": "Athraigh an pasfhocal go rathúil!", "collapse_subject": "Poist a chosc le teidil", "confirm_new_password": "Deimhnigh do pasfhocal nua", @@ -147,7 +146,6 @@ "profile_tab": "Próifíl", "radii_help": "Cruinniú imeall comhéadan a chumrú (i bpicteilíní)", "replies_in_timeline": "Freagraí sa amlíne", - "reply_link_preview": "Cumasaigh réamhamharc nasc freagartha ar chlár na luiche", "reply_visibility_all": "Taispeáin gach freagra", "reply_visibility_following": "Taispeáin freagraí amháin atá dírithe ar mise nó ar úsáideoirí atá mé ag leanúint", "reply_visibility_self": "Taispeáin freagraí amháin atá dírithe ar mise", @@ -162,7 +160,7 @@ "streaming": "Cumasaigh post nua a shruthú uathoibríoch nuair a scrollaítear go barr an leathanaigh", "text": "Téacs", "theme": "Téama", - "theme_help": "Úsáid cód daith hex (#rrggbb) chun do schéim a saincheapadh", + "theme_help": "Úsáid cód daith hex (#rrggbb) chun do schéim a saincheapadh.", "tooltipRadius": "Bileoga eolais", "user_settings": "Socruithe úsáideora", "values": { diff --git a/src/i18n/he.json b/src/i18n/he.json @@ -70,9 +70,9 @@ "preferences": "העדפות" }, "notifications": { - "broken_favorite": "סטאטוס לא ידוע, מחפש...", + "broken_favorite": "סטאטוס לא ידוע, מחפש…", "favorited_you": "אהב את הסטטוס שלך", - "followed_you": "עקב אחריך!", + "followed_you": "עקב אחריך", "load_older": "טען התראות ישנות", "notifications": "התראות", "read": "קרא!", @@ -140,7 +140,6 @@ "app_name": "שם האפליקציה", "attachmentRadius": "צירופים", "attachments": "צירופים", - "autoload": "החל טעינה אוטומטית בגלילה לתחתית הדף", "avatar": "תמונת פרופיל", "avatarAltRadius": "תמונות פרופיל (התראות)", "avatarRadius": "תמונות פרופיל", @@ -179,7 +178,7 @@ "follow_export": "יצוא עקיבות", "follow_export_button": "ייצא את הנעקבים שלך לקובץ csv", "follow_import": "יבוא עקיבות", - "follow_import_error": "שגיאה בייבוא נעקבים.", + "follow_import_error": "שגיאה בייבוא נעקבים", "follows_imported": "נעקבים יובאו! ייקח זמן מה לעבד אותם.", "foreground": "חזית", "general": "כללי", @@ -240,7 +239,6 @@ "profile_tab": "פרופיל", "radii_help": "קבע מראש עיגול פינות לממשק (בפיקסלים)", "replies_in_timeline": "תגובות בציר הזמן", - "reply_link_preview": "החל תצוגה מקדימה של לינק-תגובה בעת ריחוף עם העכבר", "reply_visibility_all": "הראה את כל התגובות", "reply_visibility_following": "הראה תגובות שמופנות אליי או לעקובים שלי בלבד", "reply_visibility_self": "הראה תגובות שמופנות אליי בלבד", @@ -313,7 +311,7 @@ "favorites": "מועדפים", "follow": "עקוב", "follow_sent": "בקשה נשלחה!", - "follow_progress": "מבקש...", + "follow_progress": "מבקש…", "follow_again": "שלח בקשה שוב?", "follow_unfollow": "בטל עקיבה", "followees": "נעקבים", @@ -329,11 +327,11 @@ "report": "דווח", "statuses": "סטטוסים", "unblock": "הסר חסימה", - "unblock_progress": "מסיר חסימה...", - "block_progress": "חוסם...", + "unblock_progress": "מסיר חסימה…", + "block_progress": "חוסם…", "unmute": "הסר השתקה", - "unmute_progress": "מסיר השתקה...", - "mute_progress": "משתיק...", + "unmute_progress": "מסיר השתקה…", + "mute_progress": "משתיק…", "admin_menu": { "moderation": "ניהול (צוות)", "grant_admin": "הפוך למנהל", @@ -379,7 +377,7 @@ "favorite": "מועדף", "user_settings": "הגדרות משתמש" }, - "upload":{ + "upload": { "error": { "base": "העלאה נכשלה.", "file_too_big": "קובץ גדול מדי [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", diff --git a/src/i18n/hu.json b/src/i18n/hu.json @@ -38,7 +38,6 @@ }, "settings": { "attachments": "Csatolmányok", - "autoload": "Autoatikus betöltés engedélyezése lap aljára görgetéskor", "avatar": "Avatár", "bio": "Bio", "current_avatar": "Jelenlegi avatár", @@ -52,7 +51,6 @@ "nsfw_clickthrough": "NSFW átkattintási tartalom elrejtésének engedélyezése", "profile_background": "Profil háttérkép", "profile_banner": "Profil Banner", - "reply_link_preview": "Válasz-link előzetes mutatása egér rátételkor", "set_new_avatar": "Új avatár", "set_new_profile_background": "Új profil háttér beállítása", "set_new_profile_banner": "Új profil banner", diff --git a/src/i18n/it.json b/src/i18n/it.json @@ -1,140 +1,432 @@ { "general": { "submit": "Invia", - "apply": "Applica" + "apply": "Applica", + "more": "Altro", + "generic_error": "Errore", + "optional": "facoltativo", + "show_more": "Mostra tutto", + "show_less": "Ripiega", + "dismiss": "Chiudi", + "cancel": "Annulla", + "disable": "Disabilita", + "enable": "Abilita", + "confirm": "Conferma", + "verify": "Verifica", + "peek": "Anteprima", + "close": "Chiudi", + "retry": "Riprova", + "error_retry": "Per favore, riprova", + "loading": "Carico…" }, "nav": { "mentions": "Menzioni", - "public_tl": "Sequenza temporale pubblica", - "timeline": "Sequenza temporale", - "twkn": "L'intera rete conosciuta", - "chat": "Chat Locale", - "friend_requests": "Richieste di Seguirti" + "public_tl": "Sequenza pubblica", + "timeline": "Sequenza personale", + "twkn": "Sequenza globale", + "chat": "Chat della stanza", + "friend_requests": "Vogliono seguirti", + "about": "Informazioni", + "administration": "Amministrazione", + "back": "Indietro", + "interactions": "Interazioni", + "dms": "Messaggi diretti", + "user_search": "Ricerca utenti", + "search": "Ricerca", + "who_to_follow": "Chi seguire", + "preferences": "Preferenze", + "bookmarks": "Segnalibri", + "chats": "Conversazioni", + "timelines": "Sequenze" }, "notifications": { "followed_you": "ti segue", "notifications": "Notifiche", - "read": "Leggi!", - "broken_favorite": "Stato sconosciuto, lo sto cercando...", - "favorited_you": "ha messo mi piace al tuo stato", - "load_older": "Carica notifiche più vecchie", - "repeated_you": "ha condiviso il tuo stato" + "read": "Letto!", + "broken_favorite": "Stato sconosciuto, lo sto cercando…", + "favorited_you": "ha gradito il tuo messaggio", + "load_older": "Carica notifiche precedenti", + "repeated_you": "ha condiviso il tuo messaggio", + "follow_request": "vuole seguirti", + "no_more_notifications": "Fine delle notifiche", + "migrated_to": "è migrato verso", + "reacted_with": "ha reagito con {0}" }, "settings": { "attachments": "Allegati", - "autoload": "Abilita caricamento automatico quando si raggiunge fondo pagina", - "avatar": "Avatar", + "avatar": "Icona utente", "bio": "Introduzione", - "current_avatar": "Il tuo avatar attuale", - "current_profile_banner": "Il tuo banner attuale", + "current_avatar": "La tua icona attuale", + "current_profile_banner": "Il tuo stendardo attuale", "filtering": "Filtri", - "filtering_explanation": "Tutti i post contenenti queste parole saranno silenziati, uno per linea", + "filtering_explanation": "Tutti i post contenenti queste parole saranno silenziati, una per riga", "hide_attachments_in_convo": "Nascondi gli allegati presenti nelle conversazioni", - "hide_attachments_in_tl": "Nascondi gli allegati presenti nella sequenza temporale", + "hide_attachments_in_tl": "Nascondi gli allegati presenti nelle sequenze", "name": "Nome", - "name_bio": "Nome & Introduzione", - "nsfw_clickthrough": "Abilita il click per visualizzare gli allegati segnati come NSFW", + "name_bio": "Nome ed introduzione", + "nsfw_clickthrough": "Fai click per visualizzare gli allegati offuscati", "profile_background": "Sfondo della tua pagina", - "profile_banner": "Banner del tuo profilo", - "reply_link_preview": "Abilita il link per la risposta al passaggio del mouse", - "set_new_avatar": "Scegli un nuovo avatar", + "profile_banner": "Stendardo del tuo profilo", + "set_new_avatar": "Scegli una nuova icona", "set_new_profile_background": "Scegli un nuovo sfondo per la tua pagina", - "set_new_profile_banner": "Scegli un nuovo banner per il tuo profilo", + "set_new_profile_banner": "Scegli un nuovo stendardo per il tuo profilo", "settings": "Impostazioni", "theme": "Tema", "user_settings": "Impostazioni Utente", "attachmentRadius": "Allegati", - "avatarAltRadius": "Avatar (Notifiche)", - "avatarRadius": "Avatar", + "avatarAltRadius": "Icone utente (Notifiche)", + "avatarRadius": "Icone utente", "background": "Sfondo", "btnRadius": "Pulsanti", - "cBlue": "Blu (Rispondere, seguire)", - "cGreen": "Verde (Condividi)", - "cOrange": "Arancio (Mi piace)", - "cRed": "Rosso (Annulla)", - "change_password": "Cambia Password", + "cBlue": "Blu (risposte, seguire)", + "cGreen": "Verde (ripeti)", + "cOrange": "Arancione (gradire)", + "cRed": "Rosso (annulla)", + "change_password": "Cambia password", "change_password_error": "C'è stato un problema durante il cambiamento della password.", "changed_password": "Password cambiata correttamente!", - "collapse_subject": "Riduci post che hanno un oggetto", + "collapse_subject": "Ripiega messaggi con oggetto", "confirm_new_password": "Conferma la nuova password", - "current_password": "Password attuale", - "data_import_export_tab": "Importa / Esporta Dati", - "default_vis": "Visibilità predefinita dei post", - "delete_account": "Elimina Account", - "delete_account_description": "Elimina definitivamente il tuo account e tutti i tuoi messaggi.", - "delete_account_error": "C'è stato un problema durante l'eliminazione del tuo account. Se il problema persiste contatta l'amministratore della tua istanza.", - "delete_account_instructions": "Digita la tua password nel campo sottostante per confermare l'eliminazione dell'account.", - "export_theme": "Salva settaggi", + "current_password": "La tua password attuale", + "data_import_export_tab": "Importa o esporta dati", + "default_vis": "Visibilità predefinita dei messaggi", + "delete_account": "Elimina profilo", + "delete_account_description": "Elimina definitivamente i tuoi dati e disattiva il tuo profilo.", + "delete_account_error": "C'è stato un problema durante l'eliminazione del tuo profilo. Se il problema persiste contatta l'amministratore della tua stanza.", + "delete_account_instructions": "Digita la tua password nel campo sottostante per confermare l'eliminazione del tuo profilo.", + "export_theme": "Salva impostazioni", "follow_export": "Esporta la lista di chi segui", - "follow_export_button": "Esporta la lista di chi segui in un file csv", + "follow_export_button": "Esporta la lista di chi segui in un file CSV", "follow_export_processing": "Sto elaborando, presto ti sarà chiesto di scaricare il tuo file", "follow_import": "Importa la lista di chi segui", "follow_import_error": "Errore nell'importazione della lista di chi segui", "follows_imported": "Importazione riuscita! L'elaborazione richiederà un po' di tempo.", - "foreground": "In primo piano", + "foreground": "Primo piano", "general": "Generale", - "hide_post_stats": "Nascondi statistiche dei post (es. il numero di mi piace)", - "hide_user_stats": "Nascondi statistiche dell'utente (es. il numero di chi ti segue)", - "import_followers_from_a_csv_file": "Importa una lista di chi segui da un file csv", - "import_theme": "Carica settaggi", + "hide_post_stats": "Nascondi statistiche dei messaggi (es. il numero di preferenze)", + "hide_user_stats": "Nascondi statistiche dell'utente (es. il numero dei tuoi seguaci)", + "import_followers_from_a_csv_file": "Importa una lista di chi segui da un file CSV", + "import_theme": "Carica impostazioni", "inputRadius": "Campi di testo", "instance_default": "(predefinito: {value})", - "interfaceLanguage": "Linguaggio dell'interfaccia", - "invalid_theme_imported": "Il file selezionato non è un file di tema per Pleroma supportato. Il tuo tema non è stato modificato.", + "interfaceLanguage": "Lingua dell'interfaccia", + "invalid_theme_imported": "Il file selezionato non è un tema supportato da Pleroma. Il tuo tema non è stato modificato.", "limited_availability": "Non disponibile nel tuo browser", "links": "Collegamenti", - "lock_account_description": "Limita il tuo account solo per contatti approvati", + "lock_account_description": "Limita il tuo account solo a seguaci approvati", "loop_video": "Riproduci video in ciclo continuo", - "loop_video_silent_only": "Riproduci solo video senza audio in ciclo continuo (es. le gif di Mastodon)", + "loop_video_silent_only": "Riproduci solo video senza audio in ciclo continuo (es. le \"gif\" di Mastodon)", "new_password": "Nuova password", "notification_visibility": "Tipi di notifiche da mostrare", "notification_visibility_follows": "Nuove persone ti seguono", - "notification_visibility_likes": "Mi piace", + "notification_visibility_likes": "Preferiti", "notification_visibility_mentions": "Menzioni", "notification_visibility_repeats": "Condivisioni", - "no_rich_text_description": "Togli la formattazione del testo da tutti i post", + "no_rich_text_description": "Togli la formattazione del testo da tutti i messaggi", "oauth_tokens": "Token OAuth", "token": "Token", "refresh_token": "Aggiorna token", "valid_until": "Valido fino a", - "revoke_token": "Revocare", + "revoke_token": "Revoca", "panelRadius": "Pannelli", - "pause_on_unfocused": "Metti in pausa l'aggiornamento continuo quando la scheda non è in primo piano", + "pause_on_unfocused": "Interrompi l'aggiornamento continuo mentre la scheda è in secondo piano", "presets": "Valori predefiniti", "profile_tab": "Profilo", - "radii_help": "Imposta l'arrotondamento dei bordi (in pixel)", - "replies_in_timeline": "Risposte nella sequenza temporale", + "radii_help": "Imposta il raggio degli angoli (in pixel)", + "replies_in_timeline": "Risposte nella sequenza personale", "reply_visibility_all": "Mostra tutte le risposte", - "reply_visibility_following": "Mostra solo le risposte dirette a me o agli utenti che seguo", - "reply_visibility_self": "Mostra solo risposte dirette a me", + "reply_visibility_following": "Mostra solo le risposte rivolte a me o agli utenti che seguo", + "reply_visibility_self": "Mostra solo risposte rivolte a me", "saving_err": "Errore nel salvataggio delle impostazioni", "saving_ok": "Impostazioni salvate", "security_tab": "Sicurezza", - "stop_gifs": "Riproduci GIF al passaggio del cursore del mouse", - "streaming": "Abilita aggiornamento automatico dei nuovi post quando si è in alto alla pagina", + "stop_gifs": "Riproduci GIF al passaggio del cursore", + "streaming": "Mostra automaticamente i nuovi messaggi quando sei in cima alla pagina", "text": "Testo", "theme_help": "Usa codici colore esadecimali (#rrggbb) per personalizzare il tuo schema di colori.", - "tooltipRadius": "Descrizioni/avvisi", + "tooltipRadius": "Suggerimenti/avvisi", "values": { "false": "no", - "true": "si" - } + "true": "sì" + }, + "avatar_size_instruction": "La taglia minima per l'icona personale è 150x150 pixel.", + "domain_mutes": "Domini", + "discoverable": "Permetti la scoperta di questo profilo da servizi di ricerca ed altro", + "composing": "Composizione", + "changed_email": "Email cambiata con successo!", + "change_email_error": "C'è stato un problema nel cambiare la tua email.", + "change_email": "Cambia email", + "blocks_tab": "Bloccati", + "blocks_imported": "Blocchi importati! Saranno elaborati a breve.", + "block_import_error": "Errore nell'importazione", + "block_import": "Importa blocchi", + "block_export_button": "Esporta i tuoi blocchi in un file CSV", + "block_export": "Esporta blocchi", + "allow_following_move": "Consenti", + "mfa": { + "verify": { + "desc": "Per abilitare l'autenticazione bifattoriale, inserisci il codice fornito dalla tua applicazione:" + }, + "scan": { + "secret_code": "Codice", + "desc": "Con la tua applicazione bifattoriale, acquisisci questo QR o inserisci il codice manualmente:", + "title": "Acquisisci" + }, + "authentication_methods": "Metodi di accesso", + "recovery_codes_warning": "Appuntati i codici o salvali in un posto sicuro, altrimenti rischi di non rivederli mai più. Se perderai l'accesso sia alla tua applicazione bifattoriale che ai codici di recupero non potrai più accedere al tuo profilo.", + "waiting_a_recovery_codes": "Ricevo codici di recupero…", + "recovery_codes": "Codici di recupero.", + "warning_of_generate_new_codes": "Alla generazione di nuovi codici di recupero, quelli vecchi saranno disattivati.", + "generate_new_recovery_codes": "Genera nuovi codici di recupero", + "title": "Accesso bifattoriale", + "confirm_and_enable": "Conferma ed abilita OTP", + "wait_pre_setup_otp": "preimposto OTP", + "setup_otp": "Imposta OTP", + "otp": "OTP" + }, + "enter_current_password_to_confirm": "Inserisci la tua password per identificarti", + "security": "Sicurezza", + "app_name": "Nome applicazione", + "style": { + "switcher": { + "help": { + "older_version_imported": "Il tema importato è stato creato per una versione precedente dell'interfaccia.", + "future_version_imported": "Il tema importato è stato creato per una versione più recente dell'interfaccia.", + "v2_imported": "Il tema importato è stato creato per una vecchia interfaccia. Non tutto potrebbe essere come prima.", + "upgraded_from_v2": "L'interfaccia è stata aggiornata, il tema potrebbe essere diverso da come lo intendevi.", + "migration_snapshot_ok": "Ho caricato l'anteprima del tema. Puoi provare a caricarne i contenuti.", + "fe_downgraded": "L'interfaccia è stata portata ad una versione precedente.", + "fe_upgraded": "Lo schema dei temi è stato aggiornato insieme all'interfaccia.", + "snapshot_missing": "Il tema non è provvisto di anteprima, quindi potrebbe essere diverso da come appare.", + "snapshot_present": "Tutti i valori sono sostituiti dall'anteprima del tema. Puoi invece caricare i suoi contenuti.", + "snapshot_source_mismatch": "Conflitto di versione: probabilmente l'interfaccia è stata portata ad una versione precedente e poi aggiornata di nuovo. Se hai modificato il tema con una versione precedente dell'interfaccia, usa la vecchia versione del tema, altrimenti puoi usare la nuova.", + "migration_napshot_gone": "Anteprima del tema non trovata, non tutto potrebbe essere come ricordi." + }, + "use_source": "Nuova versione", + "use_snapshot": "Versione precedente", + "keep_as_is": "Mantieni tal quale", + "load_theme": "Carica tema", + "clear_opacity": "Rimuovi opacità", + "clear_all": "Azzera tutto", + "reset": "Reimposta", + "save_load_hint": "Le opzioni \"mantieni\" conservano le impostazioni correnti quando selezioni o carichi un tema, e le salvano quando ne esporti uno. Quando nessuna casella è selezionata, tutte le impostazioni correnti saranno salvate nel tema.", + "keep_fonts": "Mantieni font", + "keep_roundness": "Mantieni vertici", + "keep_opacity": "Mantieni opacità", + "keep_shadows": "Mantieni ombre", + "keep_color": "Mantieni colori" + }, + "common": { + "opacity": "Opacità", + "color": "Colore", + "contrast": { + "context": { + "text": "per il testo", + "18pt": "per il testo grande (oltre 17pt)" + }, + "level": { + "bad": "non soddisfa le linee guida di alcun livello", + "aaa": "soddisfa le linee guida di livello AAA (ottimo)", + "aa": "soddisfa le linee guida di livello AA (sufficiente)" + }, + "hint": "Il rapporto di contrasto è {ratio}, e {level} {context}" + } + }, + "advanced_colors": { + "badge": "Sfondo medaglie", + "post": "Messaggi / Biografie", + "alert_neutral": "Neutro", + "alert_warning": "Attenzione", + "alert_error": "Errore", + "alert": "Sfondo degli avvertimenti", + "_tab_label": "Avanzate", + "tabs": "Etichette", + "disabled": "Disabilitato", + "selectedMenu": "Voce menù selezionata", + "selectedPost": "Messaggio selezionato", + "pressed": "Premuto", + "highlight": "Elementi evidenziati", + "icons": "Icone", + "poll": "Grafico sondaggi", + "underlay": "Sottostante", + "faint_text": "Testo sbiadito", + "inputs": "Campi d'immissione", + "buttons": "Pulsanti", + "borders": "Bordi", + "top_bar": "Barra superiore", + "panel_header": "Titolo pannello", + "badge_notification": "Notifica", + "popover": "Suggerimenti, menù, sbalzi", + "toggled": "Scambiato", + "chat": { + "border": "Bordo", + "outgoing": "Inviati", + "incoming": "Ricevuti" + } + }, + "common_colors": { + "rgbo": "Icone, accenti, medaglie", + "foreground_hint": "Seleziona l'etichetta \"Avanzate\" per controlli più fini", + "main": "Colori comuni", + "_tab_label": "Comuni" + }, + "shadows": { + "inset": "Includi", + "spread": "Spandi", + "blur": "Sfoca", + "shadow_id": "Ombra numero {value}", + "override": "Sostituisci", + "component": "Componente", + "_tab_label": "Luci ed ombre", + "components": { + "avatarStatus": "Icona utente (vista messaggio)", + "avatar": "Icona utente (vista profilo)", + "topBar": "Barra superiore", + "panelHeader": "Intestazione pannello", + "panel": "Pannello", + "input": "Campo d'immissione", + "buttonPressedHover": "Pulsante (puntato e premuto)", + "buttonPressed": "Pulsante (premuto)", + "buttonHover": "Pulsante (puntato)", + "button": "Pulsante", + "popup": "Sbalzi e suggerimenti" + }, + "filter_hint": { + "inset_classic": "Le ombre incluse usano {0}", + "spread_zero": "Lo spandimento maggiore di zero si azzera sulle ombre", + "avatar_inset": "Tieni presente che combinare ombre (sia incluse che non) sulle icone utente potrebbe dare risultati strani con quelle trasparenti.", + "drop_shadow_syntax": "{0} non supporta il parametro {1} né la keyword {2}.", + "always_drop_shadow": "Attenzione: quest'ombra usa sempre {0} se il tuo browser lo supporta." + }, + "hintV3": "Per le ombre puoi anche usare la sintassi {0} per sfruttare il secondo colore." + }, + "radii": { + "_tab_label": "Raggio" + }, + "fonts": { + "_tab_label": "Font", + "custom": "Personalizzato", + "weight": "Peso (grassettatura)", + "size": "Dimensione (in pixel)", + "family": "Nome font", + "components": { + "postCode": "Font a spaziatura fissa incluso in un messaggio", + "post": "Testo del messaggio", + "input": "Campi d'immissione", + "interface": "Interfaccia" + }, + "help": "Seleziona il font da usare per gli elementi dell'interfaccia. Se scegli \"personalizzato\" devi inserire il suo nome di sistema." + }, + "preview": { + "link": "un bel collegamentino", + "checkbox": "Ho dato uno sguardo a termini e condizioni", + "header_faint": "Tutto bene", + "fine_print": "Leggi il nostro {0} per imparare un bel niente!", + "faint_link": "utilissimo manuale", + "input": "Sono appena atterrato a Fiumicino.", + "mono": "contenuto", + "text": "Altro {0} e {1}", + "content": "Contenuto", + "button": "Pulsante", + "error": "Errore d'esempio", + "header": "Anteprima" + } + }, + "enable_web_push_notifications": "Abilita notifiche web push", + "fun": "Divertimento", + "notification_mutes": "Per non ricevere notifiche da uno specifico utente, zittiscilo.", + "notification_setting_privacy_option": "Nascondi mittente e contenuti delle notifiche push", + "notification_setting_privacy": "Privacy", + "notification_setting_filters": "Filtri", + "notifications": "Notifiche", + "greentext": "Frecce da meme", + "upload_a_photo": "Carica un'immagine", + "type_domains_to_mute": "Cerca domini da zittire", + "theme_help_v2_2": "Le icone dietro alcuni elementi sono indicatori del contrasto fra testo e sfondo, passaci sopra col puntatore per ulteriori informazioni. Se si usano delle trasparenze, questi indicatori mostrano il peggior caso possibile.", + "theme_help_v2_1": "Puoi anche forzare colore ed opacità di alcuni elementi selezionando la casella. Usa il pulsante \"Azzera\" per azzerare tutte le forzature.", + "useStreamingApiWarning": "(Sconsigliato, sperimentale, può saltare messaggi)", + "useStreamingApi": "Ricevi messaggi e notifiche in tempo reale", + "user_mutes": "Utenti", + "post_status_content_type": "Tipo di contenuto dei messaggi", + "subject_line_noop": "Non copiare", + "subject_line_mastodon": "Come in Mastodon: copia tal quale", + "subject_line_email": "Come nelle email: \"re: oggetto\"", + "subject_line_behavior": "Copia oggetto quando rispondi", + "subject_input_always_show": "Mostra sempre il campo Oggetto", + "minimal_scopes_mode": "Riduci opzioni di visibilità", + "scope_copy": "Risposte ereditano la visibilità (messaggi privati lo fanno sempre)", + "search_user_to_mute": "Cerca utente da zittire", + "search_user_to_block": "Cerca utente da bloccare", + "autohide_floating_post_button": "Nascondi automaticamente il pulsante di composizione (mobile)", + "show_moderator_badge": "Mostra l'insegna di moderatore sulla mia pagina", + "show_admin_badge": "Mostra l'insegna di amministratore sulla mia pagina", + "hide_followers_count_description": "Non mostrare quanti seguaci ho", + "hide_follows_count_description": "Non mostrare quanti utenti seguo", + "hide_followers_description": "Non mostrare i miei seguaci", + "hide_follows_description": "Non mostrare chi seguo", + "no_mutes": "Nessun utente zittito", + "no_blocks": "Nessun utente bloccato", + "notification_visibility_emoji_reactions": "Reazioni", + "notification_visibility_moves": "Migrazioni utenti", + "new_email": "Nuova email", + "use_contain_fit": "Non ritagliare le anteprime degli allegati", + "play_videos_in_modal": "Riproduci video in un riquadro a sbalzo", + "mutes_tab": "Zittiti", + "interface": "Interfaccia", + "instance_default_simple": "(predefinito)", + "checkboxRadius": "Caselle di selezione", + "import_blocks_from_a_csv_file": "Importa blocchi da un file CSV", + "hide_filtered_statuses": "Nascondi messaggi filtrati", + "use_one_click_nsfw": "Apri media offuscati con un solo click", + "preload_images": "Precarica immagini", + "hide_isp": "Nascondi pannello della stanza", + "max_thumbnails": "Numero massimo di anteprime per messaggio", + "hide_muted_posts": "Nascondi messaggi degli utenti zittiti", + "accent": "Accento", + "emoji_reactions_on_timeline": "Mostra emoji di reazione sulle sequenze", + "pad_emoji": "Affianca spazi agli emoji inseriti tramite selettore", + "notification_blocks": "Bloccando un utente non riceverai più le sue notifiche né lo seguirai più.", + "mutes_and_blocks": "Zittiti e bloccati", + "profile_fields": { + "value": "Contenuto", + "name": "Etichetta", + "add_field": "Aggiungi campo", + "label": "Metadati profilo" + }, + "bot": "Questo profilo è di un robot", + "version": { + "frontend_version": "Versione interfaccia", + "backend_version": "Versione backend", + "title": "Versione" + }, + "reset_avatar": "Azzera icona", + "reset_profile_background": "Azzera sfondo profilo", + "reset_profile_banner": "Azzera stendardo profilo", + "reset_avatar_confirm": "Vuoi veramente azzerare l'icona?", + "reset_banner_confirm": "Vuoi veramente azzerare lo stendardo?", + "reset_background_confirm": "Vuoi veramente azzerare lo sfondo?", + "chatMessageRadius": "Messaggi istantanei", + "notification_setting_hide_notification_contents": "Nascondi mittente e contenuti delle notifiche push", + "notification_setting_block_from_strangers": "Blocca notifiche da utenti che non segui" }, "timeline": { - "error_fetching": "Errore nel prelievo aggiornamenti", + "error_fetching": "Errore nell'aggiornamento", "load_older": "Carica messaggi più vecchi", "show_new": "Mostra nuovi", "up_to_date": "Aggiornato", "collapse": "Riduci", "conversation": "Conversazione", - "no_retweet_hint": "La visibilità del post è impostata solo per chi ti segue o messaggio diretto e non può essere condiviso", - "repeated": "condiviso" + "no_retweet_hint": "Il messaggio è diretto o solo per seguaci e non può essere condiviso", + "repeated": "condiviso", + "no_statuses": "Nessun messaggio", + "no_more_statuses": "Fine dei messaggi", + "reload": "Ricarica" }, "user_card": { "follow": "Segui", "followees": "Chi stai seguendo", - "followers": "Chi ti segue", - "following": "Lo stai seguendo!", + "followers": "Seguaci", + "following": "Seguìto!", "follows_you": "Ti segue!", "mute": "Silenzia", "muted": "Silenziato", @@ -144,7 +436,47 @@ "block": "Blocca", "blocked": "Bloccato!", "deny": "Nega", - "remote_follow": "Segui da remoto" + "remote_follow": "Segui da remoto", + "admin_menu": { + "delete_user_confirmation": "Ne sei completamente sicuro? Quest'azione non può essere annullata.", + "delete_user": "Elimina utente", + "quarantine": "I messaggi non arriveranno alle altre stanze", + "disable_any_subscription": "Rendi utente non seguibile", + "disable_remote_subscription": "Blocca i tentativi di seguirlo da altre stanze", + "sandbox": "Rendi tutti i messaggi solo per seguaci", + "force_unlisted": "Rendi tutti i messaggi invisibili", + "strip_media": "Rimuovi ogni allegato ai messaggi", + "force_nsfw": "Oscura tutti i messaggi", + "delete_account": "Elimina profilo", + "deactivate_account": "Disattiva profilo", + "activate_account": "Attiva profilo", + "revoke_moderator": "Divesti Moderatore", + "grant_moderator": "Crea Moderatore", + "revoke_admin": "Divesti Amministratore", + "grant_admin": "Crea Amministratore", + "moderation": "Moderazione" + }, + "show_repeats": "Mostra condivisioni", + "hide_repeats": "Nascondi condivisioni", + "mute_progress": "Zittisco…", + "unmute_progress": "Riabilito…", + "unmute": "Riabilita", + "block_progress": "Blocco…", + "unblock_progress": "Sblocco…", + "unblock": "Sblocca", + "unsubscribe": "Disdici", + "subscribe": "Abbònati", + "report": "Segnala", + "mention": "Menzioni", + "media": "Media", + "its_you": "Sei tu!", + "hidden": "Nascosto", + "follow_unfollow": "Disconosci", + "follow_again": "Reinvio richiesta?", + "follow_progress": "Richiedo…", + "follow_sent": "Richiesta inviata!", + "favorites": "Preferiti", + "message": "Contatta" }, "chat": { "title": "Chat" @@ -152,11 +484,12 @@ "features_panel": { "chat": "Chat", "gopher": "Gopher", - "media_proxy": "Media proxy", - "scope_options": "Opzioni di visibilità", - "text_limit": "Lunghezza limite", + "media_proxy": "Proxy multimedia", + "scope_options": "Opzioni visibilità", + "text_limit": "Lunghezza massima", "title": "Caratteristiche", - "who_to_follow": "Chi seguire" + "who_to_follow": "Chi seguire", + "pleroma_chat_messages": "Chiacchiere" }, "finder": { "error_fetching_user": "Errore nel recupero dell'utente", @@ -166,27 +499,53 @@ "login": "Accedi", "logout": "Disconnettiti", "password": "Password", - "placeholder": "es. lain", + "placeholder": "es. Lupo Lucio", "register": "Registrati", - "username": "Nome utente" + "username": "Nome utente", + "description": "Accedi con OAuth", + "hint": "Accedi per partecipare alla discussione", + "authentication_code": "Codice di autenticazione", + "enter_recovery_code": "Inserisci un codice di recupero", + "enter_two_factor_code": "Inserisci un codice two-factor", + "recovery_code": "Codice di recupero", + "heading": { + "totp": "Autenticazione two-factor", + "recovery": "Recupero two-factor" + } }, "post_status": { - "account_not_locked_warning": "Il tuo account non è {0}. Chiunque può seguirti e vedere i tuoi post riservati a chi ti segue.", - "account_not_locked_warning_link": "bloccato", - "attachments_sensitive": "Segna allegati come sensibili", + "account_not_locked_warning": "Il tuo profilo non è {0}. Chiunque può seguirti e vedere i tuoi messaggi riservati ai tuoi seguaci.", + "account_not_locked_warning_link": "protetto", + "attachments_sensitive": "Nascondi gli allegati", "content_type": { - "text/plain": "Testo normale" + "text/plain": "Testo normale", + "text/bbcode": "BBCode", + "text/markdown": "Markdown", + "text/html": "HTML" }, "content_warning": "Oggetto (facoltativo)", - "default": "Appena atterrato in L.A.", + "default": "Sono appena atterrato a Fiumicino.", "direct_warning": "Questo post sarà visibile solo dagli utenti menzionati.", - "posting": "Pubblica", + "posting": "Sto pubblicando", "scope": { - "direct": "Diretto - Pubblicato solo per gli utenti menzionati", - "private": "Solo per chi ti segue - Visibile solo da chi ti segue", - "public": "Pubblico - Visibile sulla sequenza temporale pubblica", - "unlisted": "Non elencato - Non visibile sulla sequenza temporale pubblica" - } + "direct": "Diretto - Visibile solo agli utenti menzionati", + "private": "Solo per seguaci - Visibile solo dai tuoi seguaci", + "public": "Pubblico - Visibile sulla sequenza pubblica", + "unlisted": "Non elencato - Non visibile sulla sequenza pubblica" + }, + "scope_notice": { + "unlisted": "Questo messaggio non sarà visibile sulla sequenza locale né su quella pubblica", + "private": "Questo messaggio sarà visibile solo ai tuoi seguaci", + "public": "Questo messaggio sarà visibile a tutti" + }, + "direct_warning_to_first_only": "Questo messaggio sarà visibile solo agli utenti menzionati all'inizio.", + "direct_warning_to_all": "Questo messaggio sarà visibile a tutti i menzionati.", + "new_status": "Nuovo messaggio", + "empty_status_error": "Non puoi pubblicare messaggi vuoti senza allegati", + "preview_empty": "Vuoto", + "preview": "Anteprima", + "media_description_error": "Allegati non caricati, riprova", + "media_description": "Descrizione allegati" }, "registration": { "bio": "Introduzione", @@ -194,13 +553,261 @@ "fullname": "Nome visualizzato", "password_confirm": "Conferma password", "registration": "Registrazione", - "token": "Codice d'invito" + "token": "Codice d'invito", + "validations": { + "password_confirmation_match": "dovrebbe essere uguale alla password", + "password_confirmation_required": "non può essere vuoto", + "password_required": "non può essere vuoto", + "email_required": "non può essere vuoto", + "fullname_required": "non può essere vuoto", + "username_required": "non può essere vuoto" + }, + "bio_placeholder": "es.\nCiao, sono Lupo Lucio.\nSono un lupo fantastico che vive nel Fantabosco. Forse mi hai visto alla Melevisione.", + "fullname_placeholder": "es. Lupo Lucio", + "username_placeholder": "es. mister_wolf", + "new_captcha": "Clicca l'immagine per avere un altro captcha", + "captcha": "CAPTCHA" }, "user_profile": { - "timeline_title": "Sequenza Temporale dell'Utente" + "timeline_title": "Sequenza dell'Utente", + "profile_loading_error": "Spiacente, c'è stato un errore nel caricamento del profilo.", + "profile_does_not_exist": "Spiacente, questo profilo non esiste." }, "who_to_follow": { - "more": "Più", + "more": "Altro", "who_to_follow": "Chi seguire" + }, + "about": { + "mrf": { + "federation": "Federazione", + "keyword": { + "reject": "Rifiuta", + "replace": "Sostituisci", + "is_replaced_by": "→", + "keyword_policies": "Regole per parole chiave", + "ftl_removal": "Rimozione dalla sequenza globale" + }, + "simple": { + "reject": "Rifiuta", + "accept": "Accetta", + "simple_policies": "Regole specifiche alla stanza", + "accept_desc": "Questa stanza accetta messaggi solo dalle seguenti stanze:", + "reject_desc": "Questa stanza non accetterà messaggi dalle stanze seguenti:", + "quarantine": "Quarantena", + "quarantine_desc": "Questa stanza inoltrerà solo messaggi pubblici alle seguenti stanze:", + "ftl_removal": "Rimozione dalla sequenza globale", + "ftl_removal_desc": "Questa stanza rimuove le seguenti stanze dalla sequenza globale:", + "media_removal": "Rimozione multimedia", + "media_removal_desc": "Questa istanza rimuove gli allegati dalle seguenti stanze:", + "media_nsfw": "Allegati oscurati forzatamente", + "media_nsfw_desc": "Questa stanza oscura gli allegati dei messaggi provenienti da queste stanze:" + }, + "mrf_policies": "Regole RM abilitate", + "mrf_policies_desc": "Le regole RM cambiano il comportamento federativo della stanza. Vigono le seguenti regole:" + }, + "staff": "Equipaggio" + }, + "domain_mute_card": { + "mute": "Zittisci", + "mute_progress": "Zittisco…", + "unmute": "Ascolta", + "unmute_progress": "Procedo…" + }, + "exporter": { + "export": "Esporta", + "processing": "In elaborazione, il tuo file sarà scaricabile a breve" + }, + "image_cropper": { + "crop_picture": "Ritaglia immagine", + "save": "Salva", + "save_without_cropping": "Salva senza ritagliare", + "cancel": "Annulla" + }, + "importer": { + "submit": "Invia", + "success": "Importato.", + "error": "L'importazione non è andata a buon fine." + }, + "media_modal": { + "previous": "Precedente", + "next": "Prossimo" + }, + "polls": { + "add_poll": "Sondaggio", + "add_option": "Alternativa", + "option": "Opzione", + "votes": "voti", + "vote": "Vota", + "type": "Tipo di sondaggio", + "single_choice": "Scelta singola", + "multiple_choices": "Scelta multipla", + "expiry": "Scadenza", + "expires_in": "Scade fra {0}", + "expired": "Scaduto {0} fa", + "not_enough_options": "Aggiungi altre risposte" + }, + "interactions": { + "favs_repeats": "Condivisi e preferiti", + "load_older": "Carica vecchie interazioni", + "moves": "Utenti migrati", + "follows": "Nuovi seguìti" + }, + "emoji": { + "load_all": "Carico tutti i {emojiAmount} emoji", + "load_all_hint": "Primi {saneAmount} emoji caricati, caricarli tutti potrebbe causare rallentamenti.", + "unicode": "Emoji Unicode", + "custom": "Emoji personale", + "add_emoji": "Inserisci Emoji", + "search_emoji": "Cerca un emoji", + "keep_open": "Tieni aperto il menù", + "emoji": "Emoji", + "stickers": "Adesivi" + }, + "selectable_list": { + "select_all": "Seleziona tutto" + }, + "remote_user_resolver": { + "error": "Non trovato.", + "searching_for": "Cerco", + "remote_user_resolver": "Cerca utenti remoti" + }, + "errors": { + "storage_unavailable": "Pleroma non ha potuto accedere ai dati del tuo browser. Le tue credenziali o le tue impostazioni locali non potranno essere salvate e potresti incontrare strani errori. Prova ad abilitare i cookie." + }, + "status": { + "pinned": "Intestato", + "unpin": "De-intesta", + "pin": "Intesta al profilo", + "delete": "Elimina messaggio", + "repeats": "Condivisi", + "favorites": "Preferiti", + "hide_content": "Nascondi contenuti", + "show_content": "Mostra contenuti", + "hide_full_subject": "Nascondi intero oggetto", + "show_full_subject": "Mostra intero oggetto", + "thread_muted_and_words": ", contiene:", + "thread_muted": "Discussione zittita", + "copy_link": "Copia collegamento", + "status_unavailable": "Messaggio non disponibile", + "unmute_conversation": "Riabilita conversazione", + "mute_conversation": "Zittisci conversazione", + "replies_list": "Risposte:", + "reply_to": "Rispondi a", + "delete_confirm": "Vuoi veramente eliminare questo messaggio?", + "unbookmark": "Rimuovi segnalibro", + "bookmark": "Aggiungi segnalibro" + }, + "time": { + "years_short": "{0}a", + "year_short": "{0}a", + "years": "{0} anni", + "year": "{0} anno", + "weeks_short": "{0}set", + "week_short": "{0}set", + "seconds_short": "{0}sec", + "second_short": "{0}sec", + "weeks": "{0} settimane", + "week": "{0} settimana", + "seconds": "{0} secondi", + "second": "{0} secondo", + "now_short": "ora", + "now": "adesso", + "months_short": "{0}me", + "month_short": "{0}me", + "months": "{0} mesi", + "month": "{0} mese", + "minutes_short": "{0}min", + "minute_short": "{0}min", + "minutes": "{0} minuti", + "minute": "{0} minuto", + "in_past": "{0} fa", + "in_future": "fra {0}", + "hours_short": "{0}h", + "days_short": "{0}g", + "hour_short": "{0}h", + "hours": "{0} ore", + "hour": "{0} ora", + "day_short": "{0}g", + "days": "{0} giorni", + "day": "{0} giorno" + }, + "user_reporting": { + "title": "Segnalo {0}", + "additional_comments": "Osservazioni accessorie", + "generic_error": "C'è stato un errore nell'elaborazione della tua richiesta.", + "submit": "Invia", + "forward_to": "Inoltra a {0}", + "forward_description": "Il profilo appartiene ad un'altra stanza. Inviare la segnalazione anche a quella?", + "add_comment_description": "La segnalazione sarà inviata ai moderatori della tua stanza. Puoi motivarla qui sotto:" + }, + "password_reset": { + "password_reset_required_but_mailer_is_disabled": "Devi reimpostare la tua password, ma non puoi farlo. Contatta il tuo amministratore.", + "password_reset_required": "Devi reimpostare la tua password per poter continuare.", + "password_reset_disabled": "Non puoi azzerare la tua password. Contatta il tuo amministratore.", + "too_many_requests": "Hai raggiunto il numero massimo di tentativi, riprova più tardi.", + "not_found": "Non ho trovato questa email o nome utente.", + "return_home": "Torna alla pagina principale", + "check_email": "Controlla la tua posta elettronica.", + "placeholder": "La tua email o nome utente", + "instruction": "Inserisci il tuo indirizzo email o il tuo nome utente. Ti invieremo un collegamento per reimpostare la tua password.", + "password_reset": "Azzera password", + "forgot_password": "Password dimenticata?" + }, + "search": { + "no_results": "Nessun risultato", + "people_talking": "{count} partecipanti", + "person_talking": "{count} partecipante", + "hashtags": "Etichette", + "people": "Utenti" + }, + "upload": { + "file_size_units": { + "TiB": "TiB", + "GiB": "GiB", + "MiB": "MiB", + "KiB": "KiB", + "B": "B" + }, + "error": { + "default": "Riprova in seguito", + "file_too_big": "File troppo pesante [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "base": "Caricamento fallito." + } + }, + "tool_tip": { + "bookmark": "Aggiungi segnalibro", + "reject_follow_request": "Rifiuta seguace", + "accept_follow_request": "Accetta seguace", + "user_settings": "Impostazioni utente", + "add_reaction": "Reagisci", + "favorite": "Gradisci", + "reply": "Rispondi", + "repeat": "Ripeti", + "media_upload": "Carica allegati" + }, + "display_date": { + "today": "Oggi" + }, + "file_type": { + "file": "File", + "image": "Immagine", + "video": "Video", + "audio": "Audio" + }, + "chats": { + "empty_chat_list_placeholder": "Non hai conversazioni. Contatta qualcuno!", + "error_sending_message": "Errore. Il messaggio non è stato inviato.", + "error_loading_chat": "Errore. La conversazione non è stata caricata.", + "delete_confirm": "Vuoi veramente eliminare questo messaggio?", + "more": "Altro", + "empty_message_error": "Non puoi inviare messaggi vuoti", + "new": "Nuova conversazione", + "chats": "Conversazioni", + "delete": "Elimina", + "message_user": "Contatta {nickname}", + "you": "Tu:" + }, + "shoutbox": { + "title": "Graffiti" } } diff --git a/src/i18n/ja_easy.json b/src/i18n/ja_easy.json @@ -27,7 +27,7 @@ }, "exporter": { "export": "エクスポート", - "processing": "おまちください。しばらくすると、あなたのファイルをダウンロードするように、メッセージがでます。" + "processing": "おまちください。しばらくすると、あなたのファイルをダウンロードするように、メッセージがでます" }, "features_panel": { "chat": "チャット", @@ -39,7 +39,7 @@ "who_to_follow": "おすすめユーザー" }, "finder": { - "error_fetching_user": "ユーザーけんさくがエラーになりました。", + "error_fetching_user": "ユーザーけんさくがエラーになりました", "find_user": "ユーザーをさがす" }, "general": { @@ -80,9 +80,9 @@ "enter_recovery_code": "リカバリーコードをいれてください", "enter_two_factor_code": "2-ファクターコードをいれてください", "recovery_code": "リカバリーコード", - "heading" : { - "totp" : "2-ファクターにんしょう", - "recovery" : "2-ファクターリカバリー" + "heading": { + "totp": "2-ファクターにんしょう", + "recovery": "2-ファクターリカバリー" } }, "media_modal": { @@ -107,7 +107,7 @@ "preferences": "せってい" }, "notifications": { - "broken_favorite": "ステータスがみつかりません。さがしています...", + "broken_favorite": "ステータスがみつかりません。さがしています…", "favorited_you": "あなたのステータスがおきにいりされました", "followed_you": "フォローされました", "load_older": "ふるいつうちをみる", @@ -172,10 +172,10 @@ "unlisted": "このとうこうは、パブリックタイムラインと、つながっているすべてのネットワークでは、みることができません" }, "scope": { - "direct": "ダイレクト: メンションされたユーザーのみにとどきます。", - "private": "フォロワーげんてい: フォロワーのみにとどきます。", - "public": "パブリック: パブリックタイムラインにとどきます。", - "unlisted": "アンリステッド: パブリックタイムラインにとどきません。" + "direct": "ダイレクト: メンションされたユーザーのみにとどきます", + "private": "フォロワーげんてい: フォロワーのみにとどきます", + "public": "パブリック: パブリックタイムラインにとどきます", + "unlisted": "アンリステッド: パブリックタイムラインにとどきません" } }, "registration": { @@ -212,17 +212,17 @@ "security": "セキュリティ", "enter_current_password_to_confirm": "あなたのアイデンティティをたしかめるため、あなたのいまのパスワードをかいてください", "mfa": { - "otp" : "OTP", - "setup_otp" : "OTPをつくる", - "wait_pre_setup_otp" : "OTPをよういしています", - "confirm_and_enable" : "OTPをたしかめて、ゆうこうにする", + "otp": "OTP", + "setup_otp": "OTPをつくる", + "wait_pre_setup_otp": "OTPをよういしています", + "confirm_and_enable": "OTPをたしかめて、ゆうこうにする", "title": "2-ファクターにんしょう", - "generate_new_recovery_codes" : "あたらしいリカバリーコードをつくる", - "warning_of_generate_new_codes" : "あたらしいリカバリーコードをつくったら、ふるいコードはつかえなくなります。", - "recovery_codes" : "リカバリーコード。", - "waiting_a_recovery_codes": "バックアップコードをうけとっています...", - "recovery_codes_warning" : "コードをかきうつすか、ひとにみられないところにセーブしてください。そうでなければ、あなたはこのコードをふたたびみることはできません。もしあなたが、2FAアプリのアクセスをうしなって、なおかつ、リカバリーコードもおもいだせないならば、あなたはあなたのアカウントから、しめだされます。", - "authentication_methods" : "にんしょうメソッド", + "generate_new_recovery_codes": "あたらしいリカバリーコードをつくる", + "warning_of_generate_new_codes": "あたらしいリカバリーコードをつくったら、ふるいコードはつかえなくなります。", + "recovery_codes": "リカバリーコード。", + "waiting_a_recovery_codes": "バックアップコードをうけとっています…", + "recovery_codes_warning": "コードをかきうつすか、ひとにみられないところにセーブしてください。そうでなければ、あなたはこのコードをふたたびみることはできません。もしあなたが、2FAアプリのアクセスをうしなって、なおかつ、リカバリーコードもおもいだせないならば、あなたはあなたのアカウントから、しめだされます。", + "authentication_methods": "にんしょうメソッド", "scan": { "title": "スキャン", "desc": "あなたの2-ファクターアプリをつかって、このQRコードをスキャンするか、テキストキーをうちこんでください:", @@ -234,7 +234,6 @@ }, "attachmentRadius": "ファイル", "attachments": "ファイル", - "autoload": "したにスクロールしたとき、じどうてきによみこむ。", "avatar": "アバター", "avatarAltRadius": "つうちのアバター", "avatarRadius": "アバター", @@ -274,12 +273,12 @@ "pad_emoji": "えもじをピッカーでえらんだとき、えもじのまわりにスペースをいれる", "export_theme": "セーブ", "filtering": "フィルタリング", - "filtering_explanation": "これらのことばをふくむすべてのものがミュートされます。1ぎょうに1つのことばをかいてください。", + "filtering_explanation": "これらのことばをふくむすべてのものがミュートされます。1ぎょうに1つのことばをかいてください", "follow_export": "フォローのエクスポート", "follow_export_button": "エクスポート", "follow_export_processing": "おまちください。まもなくファイルをダウンロードできます。", "follow_import": "フォローインポート", - "follow_import_error": "フォローのインポートがエラーになりました。", + "follow_import_error": "フォローのインポートがエラーになりました", "follows_imported": "フォローがインポートされました! すこしじかんがかかるかもしれません。", "foreground": "フォアグラウンド", "general": "ぜんぱん", @@ -341,9 +340,8 @@ "profile_background": "プロフィールのバックグラウンド", "profile_banner": "プロフィールバナー", "profile_tab": "プロフィール", - "radii_help": "インターフェースのまるさをせっていする。", + "radii_help": "インターフェースのまるさをせっていする", "replies_in_timeline": "タイムラインのリプライ", - "reply_link_preview": "カーソルをかさねたとき、リプライのプレビューをみる", "reply_visibility_all": "すべてのリプライをみる", "reply_visibility_following": "わたしにあてられたリプライと、フォローしているひとからのリプライをみる", "reply_visibility_self": "わたしにあてられたリプライをみる", @@ -369,7 +367,7 @@ "streaming": "うえまでスクロールしたとき、じどうてきにストリーミングする", "text": "もじ", "theme": "テーマ", - "theme_help": "カラーテーマをカスタマイズできます", + "theme_help": "カラーテーマをカスタマイズできます。", "theme_help_v2_1": "チェックボックスをONにすると、コンポーネントごとに、いろと、とうめいどを、オーバーライドできます。「すべてクリア」ボタンをおすと、すべてのオーバーライドを、やめます。", "theme_help_v2_2": "バックグラウンドとテキストのコントラストをあらわすアイコンがあります。マウスをホバーすると、くわしいせつめいがでます。とうめいないろをつかっているときは、もっともわるいばあいのコントラストがしめされます。", "upload_a_photo": "がぞうをアップロード", @@ -382,11 +380,6 @@ "fun": "おたのしみ", "greentext": "ミームやじるし", "notifications": "つうち", - "notification_setting": "つうちをうけとる:", - "notification_setting_follows": "あなたがフォローしているひとから", - "notification_setting_non_follows": "あなたがフォローしていないひとから", - "notification_setting_followers": "あなたをフォローしているひとから", - "notification_setting_non_followers": "あなたをフォローしていないひとから", "notification_mutes": "あるユーザーからのつうちをとめるには、ミュートしてください。", "notification_blocks": "ブロックしているユーザーからのつうちは、すべてとまります。", "enable_web_push_notifications": "ウェブプッシュつうちをゆるす", @@ -409,8 +402,8 @@ "hint": "コントラストは {ratio} です。{level}。({context})", "level": { "aa": "AAレベルガイドライン (ミニマル) をみたします", - "aaa": "AAAレベルガイドライン (レコメンデッド) をみたします。", - "bad": "ガイドラインをみたしません。" + "aaa": "AAAレベルガイドライン (レコメンデッド) をみたします", + "bad": "ガイドラインをみたしません" }, "context": { "18pt": "おおきい (18ポイントいじょう) テキスト", @@ -454,8 +447,8 @@ "always_drop_shadow": "ブラウザーがサポートしていれば、つねに {0} がつかわれます。", "drop_shadow_syntax": "{0} は、{1} パラメーターと {2} キーワードをサポートしていません。", "avatar_inset": "うちがわのかげと、そとがわのかげを、いっしょにつかうと、とうめいなアバターが、へんなみためになります。", - "spread_zero": "ひろがりが 0 よりもおおきなかげは、0 とおなじです。", - "inset_classic": "うちがわのかげは {0} をつかいます。" + "spread_zero": "ひろがりが 0 よりもおおきなかげは、0 とおなじです", + "inset_classic": "うちがわのかげは {0} をつかいます" }, "components": { "panel": "パネル", @@ -490,7 +483,7 @@ "content": "ほんぶん", "error": "エラーのれい", "button": "ボタン", - "text": "これは{0}と{1}のれいです。", + "text": "これは{0}と{1}のれいです", "mono": "monospace", "input": "はねだくうこうに、つきました。", "faint_link": "とてもたすけになるマニュアル", @@ -593,11 +586,11 @@ "subscribe": "サブスクライブ", "unsubscribe": "サブスクライブをやめる", "unblock": "ブロックをやめる", - "unblock_progress": "ブロックをとりけしています...", - "block_progress": "ブロックしています...", + "unblock_progress": "ブロックをとりけしています…", + "block_progress": "ブロックしています…", "unmute": "ミュートをやめる", - "unmute_progress": "ミュートをとりけしています...", - "mute_progress": "ミュートしています...", + "unmute_progress": "ミュートをとりけしています…", + "mute_progress": "ミュートしています…", "hide_repeats": "リピートをかくす", "show_repeats": "リピートをみる", "admin_menu": { @@ -645,11 +638,11 @@ "favorite": "おきにいり", "user_settings": "ユーザーせってい" }, - "upload":{ + "upload": { "error": { - "base": "アップロードにしっぱいしました。", - "file_too_big": "ファイルがおおきすぎます [{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]", - "default": "しばらくしてから、ためしてください" + "base": "アップロードにしっぱいしました。", + "file_too_big": "ファイルがおおきすぎます [{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]", + "default": "しばらくしてから、ためしてください" }, "file_size_units": { "B": "B", diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json @@ -203,7 +203,6 @@ }, "attachmentRadius": "ファイル", "attachments": "ファイル", - "autoload": "下にスクロールしたとき、自動的に読み込む。", "avatar": "アバター", "avatarAltRadius": "通知のアバター", "avatarRadius": "アバター", @@ -308,7 +307,6 @@ "profile_tab": "プロフィール", "radii_help": "インターフェースの丸さを設定する。", "replies_in_timeline": "タイムラインのリプライ", - "reply_link_preview": "カーソルを重ねたとき、リプライのプレビューを見る", "reply_visibility_all": "すべてのリプライを見る", "reply_visibility_following": "私に宛てられたリプライと、フォローしている人からのリプライを見る", "reply_visibility_self": "私に宛てられたリプライを見る", @@ -345,11 +343,6 @@ "true": "はい" }, "notifications": "通知", - "notification_setting": "通知を受け取る:", - "notification_setting_follows": "あなたがフォローしているユーザーから", - "notification_setting_non_follows": "あなたがフォローしていないユーザーから", - "notification_setting_followers": "あなたをフォローしているユーザーから", - "notification_setting_non_followers": "あなたをフォローしていないユーザーから", "notification_mutes": "特定のユーザーからの通知を止めるには、ミュートしてください。", "notification_blocks": "ブロックしているユーザーからの通知は、すべて止まります。", "enable_web_push_notifications": "ウェブプッシュ通知を許可する", diff --git a/src/i18n/ko.json b/src/i18n/ko.json @@ -90,7 +90,6 @@ "settings": { "attachmentRadius": "첨부물", "attachments": "첨부물", - "autoload": "최하단에 도착하면 자동으로 로드 활성화", "avatar": "아바타", "avatarAltRadius": "아바타 (알림)", "avatarRadius": "아바타", @@ -172,7 +171,6 @@ "profile_tab": "프로필", "radii_help": "인터페이스 모서리 둥글기 (픽셀 단위)", "replies_in_timeline": "답글을 타임라인에", - "reply_link_preview": "마우스를 올려서 답글 링크 미리보기 활성화", "reply_visibility_all": "모든 답글 보기", "reply_visibility_following": "나에게 직접 오는 답글이나 내가 팔로우 중인 사람에게서 오는 답글만 표시", "reply_visibility_self": "나에게 직접 전송 된 답글만 보이기", diff --git a/src/i18n/messages.js b/src/i18n/messages.js @@ -7,34 +7,47 @@ // sed -i -e "s/'//gm" -e 's/"/\\"/gm' -re 's/^( +)(.+?): ((.+?))?(,?)(\{?)$/\1"\2": "\4"/gm' -e 's/\"\{\"/{/g' -e 's/,"$/",/g' file.json // There's only problem that apostrophe character ' gets replaced by \\ so you have to fix it manually, sorry. +const loaders = { + ar: () => import('./ar.json'), + ca: () => import('./ca.json'), + cs: () => import('./cs.json'), + de: () => import('./de.json'), + eo: () => import('./eo.json'), + es: () => import('./es.json'), + et: () => import('./et.json'), + eu: () => import('./eu.json'), + fi: () => import('./fi.json'), + fr: () => import('./fr.json'), + ga: () => import('./ga.json'), + he: () => import('./he.json'), + hu: () => import('./hu.json'), + it: () => import('./it.json'), + ja: () => import('./ja_pedantic.json'), + ja_easy: () => import('./ja_easy.json'), + ko: () => import('./ko.json'), + nb: () => import('./nb.json'), + nl: () => import('./nl.json'), + oc: () => import('./oc.json'), + pl: () => import('./pl.json'), + pt: () => import('./pt.json'), + ro: () => import('./ro.json'), + ru: () => import('./ru.json'), + te: () => import('./te.json'), + zh: () => import('./zh.json') +} + const messages = { - ar: require('./ar.json'), - ca: require('./ca.json'), - cs: require('./cs.json'), - de: require('./de.json'), - en: require('./en.json'), - eo: require('./eo.json'), - es: require('./es.json'), - et: require('./et.json'), - eu: require('./eu.json'), - fi: require('./fi.json'), - fr: require('./fr.json'), - ga: require('./ga.json'), - he: require('./he.json'), - hu: require('./hu.json'), - it: require('./it.json'), - ja: require('./ja_pedantic.json'), - ja_easy: require('./ja_easy.json'), - ko: require('./ko.json'), - nb: require('./nb.json'), - nl: require('./nl.json'), - oc: require('./oc.json'), - pl: require('./pl.json'), - pt: require('./pt.json'), - ro: require('./ro.json'), - ru: require('./ru.json'), - te: require('./te.json'), - zh: require('./zh.json') + languages: ['en', ...Object.keys(loaders)], + default: { + en: require('./en.json') + }, + setLanguage: async (i18n, language) => { + if (loaders[language]) { + let messages = await loaders[language]() + i18n.setLocaleMessage(language, messages) + } + i18n.locale = language + } } export default messages diff --git a/src/i18n/nb.json b/src/i18n/nb.json @@ -193,7 +193,6 @@ }, "attachmentRadius": "Vedlegg", "attachments": "Vedlegg", - "autoload": "Automatisk lasting når du blar ned til bunnen", "avatar": "Profilbilde", "avatarAltRadius": "Profilbilde (Varslinger)", "avatarRadius": "Profilbilde", @@ -293,7 +292,6 @@ "profile_tab": "Profil", "radii_help": "Bestem hvor runde hjørnene i brukergrensesnittet skal være (i piksler)", "replies_in_timeline": "Svar på tidslinje", - "reply_link_preview": "Vis en forhåndsvisning når du holder musen over svar til en status", "reply_visibility_all": "Vis alle svar", "reply_visibility_following": "Vis bare svar som er til meg eller folk jeg følger", "reply_visibility_self": "Vis bare svar som er til meg", @@ -330,11 +328,6 @@ "true": "ja" }, "notifications": "Varsler", - "notification_setting": "Motta varsler i fra:", - "notification_setting_follows": "Brukere du følger", - "notification_setting_non_follows": "Brukere du ikke følger", - "notification_setting_followers": "Brukere som følger deg", - "notification_setting_non_followers": "Brukere som ikke følger deg", "notification_mutes": "For å stoppe å motta varsler i fra en spesifikk bruker, kan du dempe dem.", "notification_blocks": "Hvis du blokkerer en bruker vil det stoppe alle varsler og i tilleg få dem til å slutte å følge deg", "enable_web_push_notifications": "Skru på pushnotifikasjoner i nettlesere", diff --git a/src/i18n/nl.json b/src/i18n/nl.json @@ -8,7 +8,7 @@ "media_proxy": "Media proxy", "scope_options": "Zichtbaarheidsopties", "text_limit": "Tekst limiet", - "title": "Features", + "title": "Kenmerken", "who_to_follow": "Wie te volgen" }, "finder": { @@ -16,58 +16,100 @@ "find_user": "Gebruiker zoeken" }, "general": { - "apply": "toepassen", - "submit": "Verzend" + "apply": "Toepassen", + "submit": "Verzend", + "more": "Meer", + "optional": "optioneel", + "show_more": "Bekijk meer", + "show_less": "Bekijk minder", + "dismiss": "Opheffen", + "cancel": "Annuleren", + "disable": "Uitschakelen", + "enable": "Inschakelen", + "confirm": "Bevestigen", + "verify": "Verifiëren", + "generic_error": "Er is een fout opgetreden", + "peek": "Spiek", + "close": "Sluiten", + "retry": "Opnieuw proberen", + "error_retry": "Probeer het opnieuw", + "loading": "Laden…" }, "login": { "login": "Log in", "description": "Log in met OAuth", - "logout": "Log uit", + "logout": "Uitloggen", "password": "Wachtwoord", - "placeholder": "bv. lain", - "register": "Registreer", - "username": "Gebruikersnaam" + "placeholder": "bijv. lain", + "register": "Registreren", + "username": "Gebruikersnaam", + "hint": "Log in om deel te nemen aan de discussie", + "authentication_code": "Authenticatie code", + "enter_recovery_code": "Voer een herstelcode in", + "enter_two_factor_code": "Voer een twee-factor code in", + "recovery_code": "Herstelcode", + "heading": { + "totp": "Twee-factor authenticatie", + "recovery": "Twee-factor herstelling" + } }, "nav": { "about": "Over", "back": "Terug", - "chat": "Locale Chat", - "friend_requests": "Volgverzoek", + "chat": "Lokale Chat", + "friend_requests": "Volgverzoeken", "mentions": "Vermeldingen", "dms": "Directe Berichten", "public_tl": "Publieke Tijdlijn", "timeline": "Tijdlijn", - "twkn": "Het Geheel Gekende Netwerk", - "user_search": "Zoek Gebruiker", + "twkn": "Het Geheel Bekende Netwerk", + "user_search": "Gebruiker Zoeken", "who_to_follow": "Wie te volgen", - "preferences": "Voorkeuren" + "preferences": "Voorkeuren", + "administration": "Administratie", + "search": "Zoeken", + "interactions": "Interacties" }, "notifications": { - "broken_favorite": "Onbekende status, aan het zoeken...", + "broken_favorite": "Onbekende status, aan het zoeken…", "favorited_you": "vond je status leuk", "followed_you": "volgt jou", "load_older": "Laad oudere meldingen", "notifications": "Meldingen", "read": "Gelezen!", - "repeated_you": "Herhaalde je status" + "repeated_you": "Herhaalde je status", + "no_more_notifications": "Geen meldingen meer", + "migrated_to": "is gemigreerd naar", + "follow_request": "wil je volgen", + "reacted_with": "reageerde met {0}" }, "post_status": { - "new_status": "Post nieuwe status", - "account_not_locked_warning": "Je account is niet {0}. Iedereen die je volgt kan enkel-volgers posts lezen.", + "new_status": "Nieuwe status plaatsen", + "account_not_locked_warning": "Je account is niet {0}. Iedereen kan je volgen om je alleen-volgers berichten te lezen.", "account_not_locked_warning_link": "gesloten", - "attachments_sensitive": "Markeer bijlage als gevoelig", + "attachments_sensitive": "Markeer bijlagen als gevoelig", "content_type": { - "text/plain": "Gewone tekst" + "text/plain": "Platte tekst", + "text/html": "HTML", + "text/markdown": "Markdown", + "text/bbcode": "BBCode" }, "content_warning": "Onderwerp (optioneel)", - "default": "Tijd voor een pauze!", + "default": "Tijd voor anime!", "direct_warning": "Deze post zal enkel zichtbaar zijn voor de personen die genoemd zijn.", "posting": "Plaatsen", "scope": { - "direct": "Direct - Post enkel naar genoemde gebruikers", + "direct": "Direct - Post enkel naar vermelde gebruikers", "private": "Enkel volgers - Post enkel naar volgers", "public": "Publiek - Post op publieke tijdlijnen", - "unlisted": "Unlisted - Toon niet op publieke tijdlijnen" + "unlisted": "Niet Vermelden - Niet tonen op publieke tijdlijnen" + }, + "direct_warning_to_all": "Dit bericht zal zichtbaar zijn voor alle vermelde gebruikers.", + "direct_warning_to_first_only": "Dit bericht zal alleen zichtbaar zijn voor de vermelde gebruikers aan het begin van het bericht.", + "scope_notice": { + "public": "Dit bericht zal voor iedereen zichtbaar zijn", + "unlisted": "Dit bericht zal niet zichtbaar zijn in de Publieke Tijdlijn en Het Geheel Bekende Netwerk", + "private": "Dit bericht zal voor alleen je volgers zichtbaar zijn" } }, "registration": { @@ -76,7 +118,7 @@ "fullname": "Weergave naam", "password_confirm": "Wachtwoord bevestiging", "registration": "Registratie", - "token": "Uitnodigingstoken", + "token": "Uitnodigings-token", "captcha": "CAPTCHA", "new_captcha": "Klik op de afbeelding voor een nieuwe captcha", "validations": { @@ -86,141 +128,159 @@ "password_required": "moet ingevuld zijn", "password_confirmation_required": "moet ingevuld zijn", "password_confirmation_match": "komt niet overeen met het wachtwoord" - } + }, + "username_placeholder": "bijv. lain", + "fullname_placeholder": "bijv. Lain Iwakura", + "bio_placeholder": "bijv.\nHallo, ik ben Lain.\nIk ben een anime meisje woonachtig in een buitenwijk in Japan. Je kent me misschien van the Wired." }, "settings": { "attachmentRadius": "Bijlages", "attachments": "Bijlages", - "autoload": "Automatisch laden wanneer tot de bodem gescrold inschakelen", "avatar": "Avatar", "avatarAltRadius": "Avatars (Meldingen)", "avatarRadius": "Avatars", "background": "Achtergrond", "bio": "Bio", "btnRadius": "Knoppen", - "cBlue": "Blauw (Antwoord, volgen)", - "cGreen": "Groen (Herhaal)", - "cOrange": "Oranje (Vind ik leuk)", - "cRed": "Rood (Annuleer)", - "change_password": "Verander Wachtwoord", - "change_password_error": "Er was een probleem bij het aanpassen van je wachtwoord.", - "changed_password": "Wachtwoord succesvol aangepast!", - "collapse_subject": "Klap posts met onderwerp in", - "composing": "Samenstellen", - "confirm_new_password": "Bevestig nieuw wachtwoord", + "cBlue": "Blauw (Beantwoorden, volgen)", + "cGreen": "Groen (Herhalen)", + "cOrange": "Oranje (Favoriet)", + "cRed": "Rood (Annuleren)", + "change_password": "Wachtwoord Wijzigen", + "change_password_error": "Er is een fout opgetreden bij het wijzigen van je wachtwoord.", + "changed_password": "Wachtwoord succesvol gewijzigd!", + "collapse_subject": "Klap berichten met een onderwerp in", + "composing": "Opstellen", + "confirm_new_password": "Nieuw wachtwoord bevestigen", "current_avatar": "Je huidige avatar", "current_password": "Huidig wachtwoord", "current_profile_banner": "Je huidige profiel banner", "data_import_export_tab": "Data Import / Export", - "default_vis": "Standaard zichtbaarheidsscope", - "delete_account": "Verwijder Account", - "delete_account_description": "Verwijder je account en berichten permanent.", - "delete_account_error": "Er was een probleem bij het verwijderen van je account. Indien dit probleem blijft, gelieve de administratie van deze instantie te verwittigen.", - "delete_account_instructions": "Typ je wachtwoord in de input hieronder om het verwijderen van je account te bevestigen.", - "export_theme": "Sla preset op", + "default_vis": "Standaard zichtbaarheidsbereik", + "delete_account": "Account Verwijderen", + "delete_account_description": "Permanent je gegevens verwijderen en account deactiveren.", + "delete_account_error": "Er is een fout opgetreden bij het verwijderen van je account. Indien dit probleem zich voor blijft doen, neem dan contact op met de beheerder van deze instantie.", + "delete_account_instructions": "Voer je wachtwoord in het onderstaande invoerveld in om het verwijderen van je account te bevestigen.", + "export_theme": "Preset opslaan", "filtering": "Filtering", - "filtering_explanation": "Alle statussen die deze woorden bevatten worden genegeerd, één filter per lijn.", - "follow_export": "Volgers export", - "follow_export_button": "Exporteer je volgers naar een csv file", + "filtering_explanation": "Alle statussen die deze woorden bevatten worden genegeerd, één filter per lijn", + "follow_export": "Volgers exporteren", + "follow_export_button": "Exporteer je volgers naar een csv bestand", "follow_export_processing": "Aan het verwerken, binnen enkele ogenblikken wordt je gevraagd je bestand te downloaden", - "follow_import": "Volgers import", + "follow_import": "Volgers importeren", "follow_import_error": "Fout bij importeren volgers", - "follows_imported": "Volgers geïmporteerd! Het kan even duren om ze allemaal te verwerken.", + "follows_imported": "Volgers geïmporteerd! Het kan even duren voordat deze verwerkt zijn.", "foreground": "Voorgrond", "general": "Algemeen", "hide_attachments_in_convo": "Verberg bijlages in conversaties", "hide_attachments_in_tl": "Verberg bijlages in de tijdlijn", "hide_isp": "Verberg instantie-specifiek paneel", - "preload_images": "Afbeeldingen voorladen", - "hide_post_stats": "Verberg post statistieken (bv. het aantal vind-ik-leuks)", - "hide_user_stats": "Verberg post statistieken (bv. het aantal volgers)", - "import_followers_from_a_csv_file": "Importeer volgers uit een csv file", - "import_theme": "Laad preset", - "inputRadius": "Invoer velden", + "preload_images": "Afbeeldingen vooraf laden", + "hide_post_stats": "Verberg bericht statistieken (bijv. het aantal favorieten)", + "hide_user_stats": "Verberg bericht statistieken (bijv. het aantal volgers)", + "import_followers_from_a_csv_file": "Importeer volgers uit een csv bestand", + "import_theme": "Preset laden", + "inputRadius": "Invoervelden", "checkboxRadius": "Checkboxen", "instance_default": "(standaard: {value})", "instance_default_simple": "(standaard)", "interface": "Interface", "interfaceLanguage": "Interface taal", - "invalid_theme_imported": "Het geselecteerde thema is geen door Pleroma ondersteund thema. Er zijn geen aanpassingen gedaan.", - "limited_availability": "Onbeschikbaar in je browser", + "invalid_theme_imported": "Het geselecteerde bestand is geen door Pleroma ondersteund thema. Er zijn geen aanpassingen gedaan.", + "limited_availability": "Niet beschikbaar in je browser", "links": "Links", "lock_account_description": "Laat volgers enkel toe na expliciete toestemming", - "loop_video": "Speel videos af in een lus", - "loop_video_silent_only": "Speel enkel videos zonder geluid af in een lus (bv. Mastodon's \"gifs\")", + "loop_video": "Herhaal video's", + "loop_video_silent_only": "Herhaal enkel video's zonder geluid (bijv. Mastodon's \"gifs\")", "name": "Naam", "name_bio": "Naam & Bio", "new_password": "Nieuw wachtwoord", "notification_visibility": "Type meldingen die getoond worden", - "notification_visibility_follows": "Volgers", + "notification_visibility_follows": "Volgingen", "notification_visibility_likes": "Vind-ik-leuks", "notification_visibility_mentions": "Vermeldingen", "notification_visibility_repeats": "Herhalingen", - "no_rich_text_description": "Strip rich text formattering van alle posts", + "no_rich_text_description": "Verwijder rich text formattering van alle berichten", "hide_network_description": "Toon niet wie mij volgt en wie ik volg.", - "nsfw_clickthrough": "Schakel doorklikbaar verbergen van NSFW bijlages in", + "nsfw_clickthrough": "Doorklikbaar verbergen van gevoelige bijlages inschakelen", "oauth_tokens": "OAuth-tokens", "token": "Token", - "refresh_token": "Token vernieuwen", + "refresh_token": "Token Vernieuwen", "valid_until": "Geldig tot", "revoke_token": "Intrekken", "panelRadius": "Panelen", - "pause_on_unfocused": "Pauzeer streamen wanneer de tab niet gefocused is", + "pause_on_unfocused": "Streamen pauzeren wanneer de tab niet in focus is", "presets": "Presets", "profile_background": "Profiel Achtergrond", "profile_banner": "Profiel Banner", "profile_tab": "Profiel", "radii_help": "Stel afronding van hoeken in de interface in (in pixels)", "replies_in_timeline": "Antwoorden in tijdlijn", - "reply_link_preview": "Schakel antwoordlink preview in bij over zweven met muisaanwijzer", - "reply_visibility_all": "Toon alle antwoorden", - "reply_visibility_following": "Toon enkel antwoorden naar mij of andere gebruikers gericht", - "reply_visibility_self": "Toon enkel antwoorden naar mij gericht", + "reply_visibility_all": "Alle antwoorden tonen", + "reply_visibility_following": "Enkel antwoorden tonen die aan mij of gevolgde gebruikers gericht zijn", + "reply_visibility_self": "Enkel antwoorden tonen die aan mij gericht zijn", "saving_err": "Fout tijdens opslaan van instellingen", "saving_ok": "Instellingen opgeslagen", - "security_tab": "Veiligheid", - "scope_copy": "Neem scope over bij antwoorden (Directe Berichten blijven altijd Direct)", - "set_new_avatar": "Zet nieuwe avatar", - "set_new_profile_background": "Zet nieuwe profiel achtergrond", - "set_new_profile_banner": "Zet nieuwe profiel banner", + "security_tab": "Beveiliging", + "scope_copy": "Neem bereik over bij beantwoorden (Directe Berichten blijven altijd Direct)", + "set_new_avatar": "Nieuwe avatar instellen", + "set_new_profile_background": "Nieuwe profiel achtergrond instellen", + "set_new_profile_banner": "Nieuwe profiel banner instellen", "settings": "Instellingen", - "subject_input_always_show": "Maak onderwerpveld altijd zichtbaar", - "subject_line_behavior": "Kopieer onderwerp bij antwoorden", + "subject_input_always_show": "Altijd onderwerpveld tonen", + "subject_line_behavior": "Onderwerp kopiëren bij antwoorden", "subject_line_email": "Zoals email: \"re: onderwerp\"", - "subject_line_mastodon": "Zoals Mastodon: kopieer zoals het is", - "subject_line_noop": "Kopieer niet", - "stop_gifs": "Speel GIFs af bij zweven", - "streaming": "Schakel automatisch streamen van posts in wanneer tot boven gescrold.", + "subject_line_mastodon": "Zoals mastodon: kopieer zoals het is", + "subject_line_noop": "Niet kopiëren", + "stop_gifs": "GIFs afspelen bij zweven", + "streaming": "Automatisch streamen van nieuwe berichten inschakelen wanneer tot boven gescrold is", "text": "Tekst", "theme": "Thema", "theme_help": "Gebruik hex color codes (#rrggbb) om je kleurschema te wijzigen.", - "theme_help_v2_1": "Je kan ook de kleur en transparantie van bepaalde componenten overschrijven door de checkbox aan te vinken, gebruik de \"Wis alles\" knop om alle overschrijvingen te annuleren.", - "theme_help_v2_2": "Iconen onder sommige items zijn achtergrond/tekst contrast indicators, zweef er over voor gedetailleerde info. Hou er rekening mee dat bij doorzichtigheid de ergst mogelijke situatie wordt weer gegeven.", - "tooltipRadius": "Gereedschapstips/alarmen", - "user_settings": "Gebruikers Instellingen", + "theme_help_v2_1": "Je kan ook de kleur en transparantie van bepaalde componenten overschrijven door de checkbox aan te vinken, gebruik de \"Alles wissen\" knop om alle overschrijvingen te annuleren.", + "theme_help_v2_2": "Iconen onder sommige onderdelen zijn achtergrond/tekst contrast indicatoren, zweef er over voor gedetailleerde info. Hou er rekening mee dat bij doorzichtigheid de ergst mogelijke situatie wordt weer gegeven.", + "tooltipRadius": "Tooltips/alarmen", + "user_settings": "Gebruikersinstellingen", "values": { "false": "nee", "true": "ja" }, "notifications": "Meldingen", - "enable_web_push_notifications": "Schakel web push meldingen in", + "enable_web_push_notifications": "Web push meldingen inschakelen", "style": { "switcher": { - "keep_color": "Behoud kleuren", - "keep_shadows": "Behoud schaduwen", - "keep_opacity": "Behoud transparantie", - "keep_roundness": "Behoud afrondingen", - "keep_fonts": "Behoud lettertypes", + "keep_color": "Kleuren behouden", + "keep_shadows": "Schaduwen behouden", + "keep_opacity": "Transparantie behouden", + "keep_roundness": "Rondingen behouden", + "keep_fonts": "Lettertypes behouden", "save_load_hint": "\"Behoud\" opties behouden de momenteel ingestelde opties bij het selecteren of laden van thema's, maar slaan ook de genoemde opties op bij het exporteren van een thema. Wanneer alle selectievakjes zijn uitgeschakeld, zal het exporteren van thema's alles opslaan.", "reset": "Reset", - "clear_all": "Wis alles", - "clear_opacity": "Wis transparantie" + "clear_all": "Alles wissen", + "clear_opacity": "Transparantie wissen", + "keep_as_is": "Hou zoals het is", + "use_snapshot": "Oude versie", + "use_source": "Nieuwe versie", + "help": { + "future_version_imported": "Het geïmporteerde bestand is gemaakt voor een nieuwere versie van FE.", + "older_version_imported": "Het geïmporteerde bestand is gemaakt voor een oudere versie van FE.", + "upgraded_from_v2": "PleromaFE is bijgewerkt, het thema kan iets anders uitzien dan dat je gewend bent.", + "v2_imported": "Het geïmporteerde bestand is gemaakt voor een oudere FE. We proberen compatibiliteit te maximaliseren, maar het kan toch voorkomen dat er inconsistenties zijn.", + "snapshot_source_mismatch": "Versie conflict: waarschijnlijk was FE terug gerold en opnieuw bijgewerkt, indien je het thema aangepast hebt met de oudere versie van FE wil je waarschijnlijk de oude versie gebruiken, gebruik anders de nieuwe versie.", + "migration_napshot_gone": "Voor een onduidelijke reden mist de momentopname, dus sommige dingen kunnen anders uitzien dan je gewend bent.", + "migration_snapshot_ok": "Voor de zekerheid is een momentopname van het thema geladen. Je kunt proberen om de thema gegevens te laden.", + "fe_downgraded": "PleromaFE's versie is terug gerold.", + "fe_upgraded": "De thema-engine van PleromaFE is bijgewerkt na de versie update.", + "snapshot_missing": "Het bestand bevat geen thema momentopname, dus het thema kan anders uitzien dan je oorspronkelijk bedacht had.", + "snapshot_present": "Thema momentopname is geladen, alle waarden zijn overschreven. Je kunt in plaats daarvan ook de daadwerkelijke data van het thema laden." + }, + "load_theme": "Thema laden" }, "common": { "color": "Kleur", "opacity": "Transparantie", "contrast": { - "hint": "Contrast ratio is {ratio}, {level} {context}", + "hint": "Contrast verhouding is {ratio}, {level} {context}", "level": { "aa": "voldoet aan de richtlijn van niveau AA (minimum)", "aaa": "voldoet aan de richtlijn van niveau AAA (aangeraden)", @@ -233,8 +293,8 @@ } }, "common_colors": { - "_tab_label": "Gemeenschappelijk", - "main": "Gemeenschappelijke kleuren", + "_tab_label": "Algemeen", + "main": "Algemene kleuren", "foreground_hint": "Zie \"Geavanceerd\" tab voor meer gedetailleerde controle", "rgbo": "Iconen, accenten, badges" }, @@ -244,58 +304,73 @@ "alert_error": "Fout", "badge": "Badge achtergrond", "badge_notification": "Meldingen", - "panel_header": "Paneel hoofding", - "top_bar": "Top bar", + "panel_header": "Paneel koptekst", + "top_bar": "Top balk", "borders": "Randen", "buttons": "Knoppen", "inputs": "Invoervelden", - "faint_text": "Vervaagde tekst" + "faint_text": "Vervaagde tekst", + "tabs": "Tabbladen", + "toggled": "Geschakeld", + "disabled": "Uitgeschakeld", + "selectedMenu": "Geselecteerd menu item", + "selectedPost": "Geselecteerd bericht", + "pressed": "Ingedrukt", + "highlight": "Gemarkeerde elementen", + "icons": "Iconen", + "poll": "Poll grafiek", + "underlay": "Onderlaag", + "popover": "Tooltips, menu's, popovers", + "post": "Berichten / Gebruiker bios", + "alert_neutral": "Neutraal", + "alert_warning": "Waarschuwing" }, "radii": { "_tab_label": "Rondheid" }, "shadows": { "_tab_label": "Schaduw en belichting", - "component": "Component", + "component": "Onderdeel", "override": "Overschrijven", "shadow_id": "Schaduw #{value}", "blur": "Vervagen", - "spread": "Spreid", + "spread": "Spreiding", "inset": "Inzet", "hint": "Voor schaduw kan je ook --variable gebruiken als een kleur waarde om CSS3 variabelen te gebruiken. Houd er rekening mee dat het instellen van opaciteit in dit geval niet werkt.", "filter_hint": { "always_drop_shadow": "Waarschuwing, deze schaduw gebruikt altijd {0} als de browser dit ondersteund.", "drop_shadow_syntax": "{0} ondersteund niet de {1} parameter en {2} sleutelwoord.", - "avatar_inset": "Houd er rekening mee dat het combineren van zowel inzet and niet-inzet schaduwen op transparante avatars onverwachte resultaten kan opleveren.", + "avatar_inset": "Houdt er rekening mee dat het combineren van zowel inzet and niet-inzet schaduwen op transparante avatars onverwachte resultaten kan opleveren.", "spread_zero": "Schaduw met spreiding > 0 worden weergegeven alsof ze op nul staan", "inset_classic": "Inzet schaduw zal {0} gebruiken" }, "components": { "panel": "Paneel", - "panelHeader": "Paneel hoofding", - "topBar": "Top bar", - "avatar": "Gebruiker avatar (in profiel weergave)", - "avatarStatus": "Gebruiker avatar (in post weergave)", - "popup": "Popups en gereedschapstips", + "panelHeader": "Paneel koptekst", + "topBar": "Top balk", + "avatar": "Gebruikers avatar (in profiel weergave)", + "avatarStatus": "Gebruikers avatar (in bericht weergave)", + "popup": "Popups en tooltips", "button": "Knop", "buttonHover": "Knop (zweven)", "buttonPressed": "Knop (ingedrukt)", "buttonPressedHover": "Knop (ingedrukt+zweven)", "input": "Invoerveld" - } + }, + "hintV3": "Voor schaduwen kun je ook de {0} notatie gebruiken om de andere kleur invoer te gebruiken." }, "fonts": { "_tab_label": "Lettertypes", - "help": "Selecteer het lettertype om te gebruiken voor elementen van de UI.Voor \"aangepast\" moet je de exacte naam van het lettertype invoeren zoals die in het systeem wordt weergegeven.", + "help": "Selecteer het lettertype om te gebruiken voor elementen van de UI. Voor \"aangepast\" dien je de exacte naam van het lettertype in te voeren zoals die in het systeem wordt weergegeven.", "components": { "interface": "Interface", "input": "Invoervelden", - "post": "Post tekst", - "postCode": "Monospaced tekst in een post (rich text)" + "post": "Bericht tekst", + "postCode": "Monospaced tekst in een bericht (rich text)" }, - "family": "Naam lettertype", + "family": "Lettertype naam", "size": "Grootte (in px)", - "weight": "Gewicht (vetheid)", + "weight": "Gewicht (dikgedruktheid)", "custom": "Aangepast" }, "preview": { @@ -305,31 +380,122 @@ "button": "Knop", "text": "Nog een boel andere {0} en {1}", "mono": "inhoud", - "input": "Tijd voor een pauze!", + "input": "Tijd voor anime!", "faint_link": "handige gebruikershandleiding", "fine_print": "Lees onze {0} om niets nuttig te leren!", "header_faint": "Alles komt goed", - "checkbox": "Ik heb de gebruikersvoorwaarden eens van ver bekeken", - "link": "een link" + "checkbox": "Ik heb de gebruikersvoorwaarden gelezen", + "link": "een leuke kleine link" } - } + }, + "notification_setting_privacy": "Privacy", + "notification_setting_privacy_option": "Verberg de afzender en inhoud van push meldingen", + "notification_mutes": "Om niet langer meldingen te ontvangen van een specifieke gebruiker, kun je deze negeren.", + "app_name": "App naam", + "security": "Beveiliging", + "enter_current_password_to_confirm": "Voer je huidige wachtwoord in om je identiteit te bevestigen", + "mfa": { + "otp": "OTP", + "setup_otp": "OTP instellen", + "wait_pre_setup_otp": "OTP voorinstellen", + "confirm_and_enable": "Bevestig en schakel OTP in", + "title": "Twee-factor Authenticatie", + "generate_new_recovery_codes": "Genereer nieuwe herstelcodes", + "recovery_codes": "Herstelcodes.", + "waiting_a_recovery_codes": "Backup codes ontvangen…", + "authentication_methods": "Authenticatie methodes", + "scan": { + "title": "Scannen", + "desc": "Scan de QR code of voer een sleutel in met je twee-factor applicatie:", + "secret_code": "Sleutel" + }, + "verify": { + "desc": "Voer de code van je twee-factor applicatie in om twee-factor authenticatie in te schakelen:" + }, + "warning_of_generate_new_codes": "Wanneer je nieuwe herstelcodes genereert, zullen je oude code niet langer werken.", + "recovery_codes_warning": "Schrijf de codes op of sla ze op een veilige locatie op - anders kun je ze niet meer inzien. Als je toegang tot je 2FA app en herstelcodes verliest, zal je buitengesloten zijn uit je account." + }, + "allow_following_move": "Automatisch volgen toestaan wanneer een gevolgd account migreert", + "block_export": "Blokkades exporteren", + "block_import": "Blokkades importeren", + "blocks_imported": "Blokkades geïmporteerd! Het kan even duren voordat deze verwerkt zijn.", + "blocks_tab": "Blokkades", + "change_email": "Email wijzigen", + "change_email_error": "Er is een fout opgetreden tijdens het wijzigen van je email.", + "changed_email": "Email succesvol gewijzigd!", + "domain_mutes": "Domeinen", + "avatar_size_instruction": "De aangeraden minimale afmeting voor avatar afbeeldingen is 150x150 pixels.", + "pad_emoji": "Vul emoji aan met spaties wanneer deze met de picker ingevoegd worden", + "emoji_reactions_on_timeline": "Toon emoji reacties op de tijdlijn", + "accent": "Accent", + "hide_muted_posts": "Verberg berichten van genegeerde gebruikers", + "max_thumbnails": "Maximaal aantal miniaturen per bericht", + "use_one_click_nsfw": "Open gevoelige bijlagen met slechts één klik", + "hide_filtered_statuses": "Gefilterde statussen verbergen", + "import_blocks_from_a_csv_file": "Importeer blokkades van een csv bestand", + "mutes_tab": "Negeringen", + "play_videos_in_modal": "Speel video's af in een popup frame", + "new_email": "Nieuwe Email", + "notification_visibility_emoji_reactions": "Reacties", + "no_blocks": "Geen blokkades", + "no_mutes": "Geen negeringen", + "hide_followers_description": "Niet tonen wie mij volgt", + "hide_followers_count_description": "Niet mijn volgers aantal tonen", + "hide_follows_count_description": "Niet mijn gevolgde aantal tonen", + "show_admin_badge": "Beheerders badge tonen in mijn profiel", + "autohide_floating_post_button": "Nieuw Bericht knop automatisch verbergen (mobiel)", + "search_user_to_block": "Zoek wie je wilt blokkeren", + "search_user_to_mute": "Zoek wie je wilt negeren", + "minimal_scopes_mode": "Bericht bereik-opties minimaliseren", + "post_status_content_type": "Bericht status content type", + "user_mutes": "Gebruikers", + "useStreamingApi": "Berichten en meldingen in real-time ontvangen", + "useStreamingApiWarning": "(Afgeraden, experimenteel, kan berichten overslaan)", + "type_domains_to_mute": "Zoek domeinen om te negeren", + "upload_a_photo": "Upload een foto", + "fun": "Plezier", + "greentext": "Meme pijlen", + "block_export_button": "Exporteer je geblokkeerde gebruikers naar een csv bestand", + "block_import_error": "Fout bij importeren blokkades", + "discoverable": "Sta toe dat dit account ontdekt kan worden in zoekresultaten en andere diensten", + "use_contain_fit": "Snij bijlage in miniaturen niet bij", + "notification_visibility_moves": "Gebruiker Migraties", + "hide_follows_description": "Niet tonen wie ik volg", + "show_moderator_badge": "Moderators badge tonen in mijn profiel", + "notification_setting_filters": "Filters", + "notification_blocks": "Door een gebruiker te blokkeren, ontvang je geen meldingen meer van de gebruiker en wordt je abonnement op de gebruiker opgeheven.", + "version": { + "frontend_version": "Frontend Versie", + "backend_version": "Backend Versie", + "title": "Versie" + }, + "mutes_and_blocks": "Negeringen en Blokkades", + "profile_fields": { + "value": "Inhoud", + "name": "Label", + "add_field": "Veld Toevoegen", + "label": "Profiel metadata" + }, + "bot": "Dit is een bot account" }, "timeline": { "collapse": "Inklappen", "conversation": "Conversatie", "error_fetching": "Fout bij ophalen van updates", - "load_older": "Laad oudere Statussen", - "no_retweet_hint": "Post is gemarkeerd als enkel volgers of direct en kan niet worden herhaald", + "load_older": "Oudere statussen laden", + "no_retweet_hint": "Bericht is gemarkeerd als enkel volgers of direct en kan niet worden herhaald", "repeated": "herhaalde", - "show_new": "Toon nieuwe", - "up_to_date": "Up-to-date" + "show_new": "Nieuwe tonen", + "up_to_date": "Up-to-date", + "no_statuses": "Geen statussen", + "no_more_statuses": "Geen statussen meer" }, "user_card": { "approve": "Goedkeuren", "block": "Blokkeren", "blocked": "Geblokkeerd!", - "deny": "Ontzeggen", - "favorites": "Vind-ik-leuks", + "deny": "Weigeren", + "favorites": "Favorieten", "follow": "Volgen", "follow_sent": "Aanvraag verzonden!", "follow_progress": "Aanvragen…", @@ -340,31 +506,69 @@ "following": "Aan het volgen!", "follows_you": "Volgt jou!", "its_you": "'t is jij!", - "mute": "Dempen", - "muted": "Gedempt", + "mute": "Negeren", + "muted": "Genegeerd", "per_day": "per dag", "remote_follow": "Volg vanop afstand", - "statuses": "Statussen" + "statuses": "Statussen", + "admin_menu": { + "delete_user_confirmation": "Weet je het heel zeker? Deze uitvoering kan niet ongedaan worden gemaakt.", + "delete_user": "Gebruiker verwijderen", + "quarantine": "Federeren van gebruikers berichten verbieden", + "disable_any_subscription": "Volgen van gebruiker in zijn geheel verbieden", + "disable_remote_subscription": "Volgen van gebruiker vanaf andere instanties verbieden", + "sandbox": "Berichten forceren om alleen voor volgers zichtbaar te zijn", + "force_unlisted": "Berichten forceren om niet publiekelijk getoond te worden", + "strip_media": "Media van berichten verwijderen", + "force_nsfw": "Alle berichten als gevoelig markeren", + "delete_account": "Account verwijderen", + "deactivate_account": "Account deactiveren", + "activate_account": "Account activeren", + "revoke_moderator": "Moderatorsrechten intrekken", + "grant_moderator": "Moderatorsrechten toekennen", + "revoke_admin": "Beheerdersrechten intrekken", + "grant_admin": "Beheerdersrechten toekennen", + "moderation": "Moderatie" + }, + "show_repeats": "Herhalingen tonen", + "hide_repeats": "Herhalingen verbergen", + "mute_progress": "Negeren…", + "unmute_progress": "Negering opheffen…", + "unmute": "Negering opheffen", + "block_progress": "Blokkeren…", + "unblock_progress": "Blokkade opheffen…", + "unblock": "Blokkade opheffen", + "unsubscribe": "Abonnement opzeggen", + "subscribe": "Abonneren", + "report": "Aangeven", + "mention": "Vermelding", + "media": "Media", + "hidden": "Verborgen" }, "user_profile": { - "timeline_title": "Gebruikers Tijdlijn" + "timeline_title": "Gebruikers Tijdlijn", + "profile_loading_error": "Sorry, er is een fout opgetreden bij het laden van dit profiel.", + "profile_does_not_exist": "Sorry, dit profiel bestaat niet." }, "who_to_follow": { "more": "Meer", "who_to_follow": "Wie te volgen" }, "tool_tip": { - "media_upload": "Upload Media", - "repeat": "Herhaal", - "reply": "Antwoord", - "favorite": "Vind-ik-leuk", - "user_settings": "Gebruikers Instellingen" + "media_upload": "Media Uploaden", + "repeat": "Herhalen", + "reply": "Beantwoorden", + "favorite": "Favoriet maken", + "user_settings": "Gebruikers Instellingen", + "reject_follow_request": "Volg-verzoek afwijzen", + "accept_follow_request": "Volg-aanvraag accepteren", + "add_reaction": "Reactie toevoegen" }, - "upload":{ + "upload": { "error": { - "base": "Upload gefaald.", - "file_too_big": "Bestand is te groot [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", - "default": "Probeer later opnieuw" + "base": "Upload mislukt.", + "file_too_big": "Bestand is te groot [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "Probeer het later opnieuw" }, "file_size_units": { "B": "B", @@ -373,5 +577,179 @@ "GiB": "GiB", "TiB": "TiB" } + }, + "about": { + "mrf": { + "federation": "Federatie", + "keyword": { + "reject": "Afwijzen", + "replace": "Vervangen", + "is_replaced_by": "→", + "keyword_policies": "Zoekwoord Beleid", + "ftl_removal": "Verwijdering van \"Het Geheel Bekende Netwerk\" Tijdlijn" + }, + "mrf_policies_desc": "MRF regels beïnvloeden het federatie gedrag van de instantie. De volgende regels zijn ingeschakeld:", + "mrf_policies": "Ingeschakelde MRF Regels", + "simple": { + "simple_policies": "Instantie-specifieke Regels", + "accept": "Accepteren", + "accept_desc": "Deze instantie accepteert alleen berichten van de volgende instanties:", + "reject": "Afwijzen", + "reject_desc": "Deze instantie zal geen berichten accepteren van de volgende instanties:", + "quarantine": "Quarantaine", + "quarantine_desc": "Deze instantie zal alleen publieke berichten sturen naar de volgende instanties:", + "ftl_removal_desc": "Deze instantie verwijdert de volgende instanties van \"Het Geheel Bekende Netwerk\" tijdlijn:", + "media_removal_desc": "Deze instantie verwijdert media van berichten van de volgende instanties:", + "media_nsfw_desc": "Deze instantie stelt media in als gevoelig in berichten van de volgende instanties:", + "ftl_removal": "Verwijderen van \"Het Geheel Bekende Netwerk\" Tijdlijn", + "media_removal": "Media Verwijdering", + "media_nsfw": "Forceer Media als Gevoelig" + } + }, + "staff": "Personeel" + }, + "domain_mute_card": { + "mute": "Negeren", + "mute_progress": "Negeren…", + "unmute": "Negering opheffen", + "unmute_progress": "Negering wordt opgeheven…" + }, + "exporter": { + "export": "Exporteren", + "processing": "Verwerken, er wordt zo gevraagd om je bestand te downloaden" + }, + "image_cropper": { + "save": "Opslaan", + "save_without_cropping": "Opslaan zonder bijsnijden", + "cancel": "Annuleren", + "crop_picture": "Afbeelding bijsnijden" + }, + "importer": { + "submit": "Verzenden", + "success": "Succesvol geïmporteerd.", + "error": "Er is een fout opgetreden bij het importeren van dit bestand." + }, + "media_modal": { + "previous": "Vorige", + "next": "Volgende" + }, + "polls": { + "add_poll": "Poll Toevoegen", + "add_option": "Optie Toevoegen", + "option": "Optie", + "votes": "stemmen", + "vote": "Stem", + "single_choice": "Enkele keuze", + "multiple_choices": "Meerkeuze", + "expiry": "Poll leeftijd", + "expires_in": "Poll eindigt in {0}", + "expired": "Poll is {0} geleden beëindigd", + "not_enough_options": "Te weinig opties in poll", + "type": "Poll type" + }, + "emoji": { + "emoji": "Emoji", + "keep_open": "Picker openhouden", + "search_emoji": "Zoek voor een emoji", + "add_emoji": "Emoji invoegen", + "unicode": "Unicode emoji", + "load_all": "Alle {emojiAmount} emoji worden geladen", + "stickers": "Stickers", + "load_all_hint": "Eerste {saneAmount} emoji geladen, alle emoji tegelijk laden kan problemen veroorzaken met prestaties.", + "custom": "Gepersonaliseerde emoji" + }, + "interactions": { + "favs_repeats": "Herhalingen en Favorieten", + "follows": "Nieuwe volgingen", + "moves": "Gebruiker migreert", + "load_older": "Oudere interacties laden" + }, + "remote_user_resolver": { + "searching_for": "Zoeken naar", + "error": "Niet gevonden.", + "remote_user_resolver": "Externe gebruikers zoeker" + }, + "selectable_list": { + "select_all": "Alles selecteren" + }, + "password_reset": { + "password_reset_required_but_mailer_is_disabled": "Je dient je wachtwoord opnieuw in te stellen, maar wachtwoord reset is uitgeschakeld. Neem contact op met de beheerder van deze instantie.", + "password_reset_required": "Je dient je wachtwoord opnieuw in te stellen om in te kunnen loggen.", + "password_reset_disabled": "Wachtwoord reset is uitgeschakeld. Neem contact op met de beheerder van deze instantie.", + "too_many_requests": "Je hebt het maximaal aantal pogingen bereikt, probeer het later opnieuw.", + "not_found": "We kunnen die email of gebruikersnaam niet vinden.", + "return_home": "Terugkeren naar de home pagina", + "check_email": "Controleer je email inbox voor een link om je wachtwoord opnieuw in te stellen.", + "placeholder": "Je email of gebruikersnaam", + "instruction": "Voer je email adres of gebruikersnaam in. We sturen je een link om je wachtwoord opnieuw in te stellen.", + "password_reset": "Wachtwoord opnieuw instellen", + "forgot_password": "Wachtwoord vergeten?" + }, + "search": { + "no_results": "Geen resultaten", + "people_talking": "{count} personen aan het praten", + "person_talking": "{count} persoon aan het praten", + "hashtags": "Hashtags", + "people": "Personen" + }, + "user_reporting": { + "generic_error": "Er is een fout opgetreden tijdens het verwerken van je verzoek.", + "submit": "Verzenden", + "forward_to": "Doorsturen naar {0}", + "forward_description": "Dit account hoort bij een andere server. Wil je een kopie van het rapport ook daarheen sturen?", + "additional_comments": "Aanvullende opmerkingen", + "add_comment_description": "Het rapport zal naar de moderators van de instantie worden verstuurd. Je kunt hieronder uitleg bijvoegen waarom je dit account wilt aangeven:", + "title": "{0} aangeven" + }, + "status": { + "copy_link": "Link naar status kopiëren", + "status_unavailable": "Status niet beschikbaar", + "unmute_conversation": "Conversatie niet meer negeren", + "mute_conversation": "Conversatie negeren", + "replies_list": "Antwoorden:", + "reply_to": "Antwoorden aan", + "delete_confirm": "Wil je echt deze status verwijderen?", + "pin": "Aan profiel vastmaken", + "pinned": "Vastgezet", + "unpin": "Van profiel losmaken", + "delete": "Status verwijderen", + "repeats": "Herhalingen", + "favorites": "Favorieten", + "thread_muted_and_words": ", heeft woorden:", + "thread_muted": "Thread genegeerd" + }, + "time": { + "years_short": "{0}j", + "year_short": "{0}j", + "years": "{0} jaren", + "year": "{0} jaar", + "weeks_short": "{0}w", + "week_short": "{0}w", + "weeks": "{0} weken", + "week": "{0} week", + "seconds_short": "{0}s", + "second_short": "{0}s", + "seconds": "{0} seconden", + "second": "{0} seconde", + "now_short": "nu", + "now": "zojuist", + "months_short": "{0}ma", + "month_short": "{0}ma", + "months": "{0} maanden", + "month": "{0} maand", + "minutes_short": "{0}min", + "minute_short": "{0}min", + "minutes": "{0} minuten", + "minute": "{0} minuut", + "in_past": "{0} geleden", + "in_future": "over {0}", + "hours_short": "{0}u", + "hour_short": "{0}u", + "hours": "{0} uren", + "hour": "{0} uur", + "days_short": "{0}d", + "day_short": "{0}d", + "days": "{0} dagen", + "day": "{0} dag" } } diff --git a/src/i18n/oc.json b/src/i18n/oc.json @@ -152,7 +152,6 @@ "app_name": "Nom de l’aplicacion", "attachmentRadius": "Pèças juntas", "attachments": "Pèças juntas", - "autoload": "Activar lo cargament automatic un còp arribat al cap de la pagina", "avatar": "Avatar", "avatarAltRadius": "Avatars (Notificacions)", "avatarRadius": "Avatars", @@ -252,7 +251,6 @@ "profile_tab": "Perfil", "radii_help": "Configurar los caires arredondits de l’interfàcia (en pixèls)", "replies_in_timeline": "Responsas del flux", - "reply_link_preview": "Activar l’apercebut en passar la mirga", "reply_visibility_all": "Mostrar totas las responsas", "reply_visibility_following": "Mostrar pas que las responsas que me son destinada a ieu o un utilizaire que seguissi", "reply_visibility_self": "Mostrar pas que las responsas que me son destinadas", @@ -288,11 +286,6 @@ "true": "òc" }, "notifications": "Notificacions", - "notification_setting": "Recebre las notificacions de :", - "notification_setting_follows": "Utilizaires que seguissètz", - "notification_setting_non_follows": "Utilizaires que seguissètz pas", - "notification_setting_followers": "Utilizaires que vos seguisson", - "notification_setting_non_followers": "Utilizaires que vos seguisson pas", "notification_mutes": "Per recebre pas mai d’un utilizaire en particular, botatz-lo en silenci.", "notification_blocks": "Blocar un utilizaire arrèsta totas las notificacions tan coma quitar de los seguir.", "enable_web_push_notifications": "Activar las notificacions web push", @@ -550,4 +543,4 @@ "people_talking": "{count} personas ne parlan", "no_results": "Cap de resultats" } -} -\ No newline at end of file +} diff --git a/src/i18n/pl.json b/src/i18n/pl.json @@ -20,27 +20,27 @@ "quarantine": "Kwarantanna", "quarantine_desc": "Ta instancja wysyła tylko publiczne posty do wymienionych instancji:", "ftl_removal": "Usunięcie z \"Całej znanej sieci\"", - "ftl_removal_desc": "Ta instancja usuwa te 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_desc": "Ta instancja usuwa multimedia z postów od wymienionych instancji:", "media_nsfw": "Multimedia ustawione jako wrażliwe", "media_nsfw_desc": "Ta instancja wymusza, by multimedia z wymienionych instancji były ustawione jako wrażliwe:" } }, - "staff": "Obsługa" + "staff": "Administracja" }, "chat": { "title": "Czat" }, "domain_mute_card": { "mute": "Wycisz", - "mute_progress": "Wyciszam...", + "mute_progress": "Wyciszam…", "unmute": "Odcisz", - "unmute_progress": "Odciszam..." + "unmute_progress": "Odciszam…" }, "exporter": { "export": "Eksportuj", - "processing": "Przetwarzam, za chwilę zostaniesz zapytany o ściągnięcie pliku" + "processing": "Przetwarzam, za chwilę zostaniesz zapytany(-na) o ściągnięcie pliku" }, "features_panel": { "chat": "Czat", @@ -68,7 +68,10 @@ "disable": "Wyłącz", "enable": "Włącz", "confirm": "Potwierdź", - "verify": "Zweryfikuj" + "verify": "Zweryfikuj", + "close": "Zamknij", + "loading": "Ładowanie…", + "retry": "Spróbuj ponownie" }, "image_cropper": { "crop_picture": "Przytnij obrazek", @@ -78,7 +81,7 @@ }, "importer": { "submit": "Wyślij", - "success": "Zaimportowano pomyślnie", + "success": "Zaimportowano pomyślnie.", "error": "Wystąpił błąd podczas importowania pliku." }, "login": { @@ -94,9 +97,9 @@ "enter_recovery_code": "Wprowadź kod zapasowy", "enter_two_factor_code": "Wprowadź kod weryfikacyjny", "recovery_code": "Kod zapasowy", - "heading" : { - "totp" : "Weryfikacja dwuetapowa", - "recovery" : "Zapasowa weryfikacja dwuetapowa" + "heading": { + "totp": "Weryfikacja dwuetapowa", + "recovery": "Zapasowa weryfikacja dwuetapowa" } }, "media_modal": { @@ -118,7 +121,8 @@ "user_search": "Wyszukiwanie użytkowników", "search": "Wyszukiwanie", "who_to_follow": "Sugestie obserwacji", - "preferences": "Preferencje" + "preferences": "Preferencje", + "bookmarks": "Zakładki" }, "notifications": { "broken_favorite": "Nieznany status, szukam go…", @@ -130,7 +134,8 @@ "repeated_you": "powtórzył(-a) twój status", "no_more_notifications": "Nie masz więcej powiadomień", "migrated_to": "wyemigrował do", - "reacted_with": "zareagował z {0}" + "reacted_with": "zareagował z {0}", + "follow_request": "chce ciebie obserwować" }, "polls": { "add_poll": "Dodaj ankietę", @@ -142,7 +147,7 @@ "single_choice": "jednokrotnego wyboru", "multiple_choices": "wielokrotnego wyboru", "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", "not_enough_options": "Zbyt mało unikalnych opcji w ankiecie" }, @@ -189,7 +194,10 @@ "private": "Tylko dla obserwujących – Umieść dla osób, które cię obserwują", "public": "Publiczny – Umieść na publicznych osiach czasu", "unlisted": "Niewidoczny – Nie umieszczaj na publicznych osiach czasu" - } + }, + "preview_empty": "Pusty", + "preview": "Podgląd", + "empty_status_error": "Nie można wysłać pustego wpisu bez plików" }, "registration": { "bio": "Bio", @@ -225,17 +233,17 @@ "security": "Bezpieczeństwo", "enter_current_password_to_confirm": "Wprowadź obecne hasło, by potwierdzić twoją tożsamość", "mfa": { - "otp" : "OTP", - "setup_otp" : "Ustaw OTP", - "wait_pre_setup_otp" : "początkowe ustawianie OTP", - "confirm_and_enable" : "Potwierdź i włącz OTP", + "otp": "OTP", + "setup_otp": "Ustaw OTP", + "wait_pre_setup_otp": "początkowe ustawianie OTP", + "confirm_and_enable": "Potwierdź i włącz OTP", "title": "Weryfikacja dwuetapowa", - "generate_new_recovery_codes" : "Wygeneruj nowe kody zapasowe", - "warning_of_generate_new_codes" : "Po tym gdy generujesz nowe kody zapasowe, stare przestaną działać.", - "recovery_codes" : "Kody zapasowe.", - "waiting_a_recovery_codes": "Otrzymuję kody zapasowe...", - "recovery_codes_warning" : "Spisz kody na kartce papieru, albo zapisz je w bezpiecznym miejscu - inaczej nie zobaczysz ich już nigdy. Jeśli stracisz dostęp do twojej aplikacji 2FA i kodów zapasowych, nie będziesz miał dostępu do swojego konta.", - "authentication_methods" : "Metody weryfikacji", + "generate_new_recovery_codes": "Wygeneruj nowe kody zapasowe", + "warning_of_generate_new_codes": "Po tym gdy wygenerujesz nowe kody zapasowe, stare przestaną działać.", + "recovery_codes": "Kody zapasowe.", + "waiting_a_recovery_codes": "Otrzymuję kody zapasowe…", + "recovery_codes_warning": "Spisz kody na kartce papieru, albo zapisz je w bezpiecznym miejscu - inaczej nie zobaczysz ich już nigdy. Jeśli stracisz dostęp do twojej aplikacji 2FA i kodów zapasowych, nie będziesz miał(-a) dostępu do swojego konta.", + "authentication_methods": "Metody weryfikacji", "scan": { "title": "Skanuj", "desc": "Zeskanuj ten kod QR używając twojej aplikacji 2FA albo wpisz ten klucz:", @@ -248,7 +256,6 @@ "allow_following_move": "Zezwalaj na automatyczną obserwację gdy obserwowane konto migruje", "attachmentRadius": "Załączniki", "attachments": "Załączniki", - "autoload": "Włącz automatyczne ładowanie po przewinięciu do końca strony", "avatar": "Awatar", "avatarAltRadius": "Awatary (powiadomienia)", "avatarRadius": "Awatary", @@ -280,10 +287,10 @@ "data_import_export_tab": "Import/eksport danych", "default_vis": "Domyślny zakres widoczności", "delete_account": "Usuń konto", - "delete_account_description": "Trwale usuń konto i wszystkie posty.", + "delete_account_description": "Trwale usuń dane i zdezaktywuj konto.", "delete_account_error": "Wystąpił problem z usuwaniem twojego konta. Jeżeli problem powtarza się, poinformuj administratora swojej instancji.", "delete_account_instructions": "Wprowadź swoje hasło w poniższe pole aby potwierdzić usunięcie konta.", - "discoverable": "Zezwól na odkrywanie tego konta w wynikach wyszukiwania i innych usługa.", + "discoverable": "Zezwól na odkrywanie tego konta w wynikach wyszukiwania i innych usługach", "domain_mutes": "Domeny", "avatar_size_instruction": "Zalecany minimalny rozmiar awatarów to 150x150 pikseli.", "pad_emoji": "Dodaj odstęp z obu stron emoji podczas dodawania selektorem", @@ -314,14 +321,14 @@ "import_theme": "Załaduj motyw", "inputRadius": "Pola tekstowe", "checkboxRadius": "Pola wyboru", - "instance_default": "(domyślny: {value})", - "instance_default_simple": "(domyślny)", + "instance_default": "(domyślnie: {value})", + "instance_default_simple": "(domyślne)", "interface": "Interfejs", "interfaceLanguage": "Język interfejsu", "invalid_theme_imported": "Wybrany plik nie jest obsługiwanym motywem Pleromy. Nie dokonano zmian w twoim motywie.", "limited_availability": "Niedostępne w twojej przeglądarce", "links": "Łącza", - "lock_account_description": "Ogranicz swoje konto dla zatwierdzonych obserwowanych", + "lock_account_description": "Spraw, by konto mogli wyświetlać tylko zatwierdzeni obserwujący", "loop_video": "Zapętlaj filmy", "loop_video_silent_only": "Zapętlaj tylko filmy bez dźwięku (np. mastodonowe „gify”)", "mutes_tab": "Wyciszenia", @@ -339,8 +346,8 @@ "notification_visibility_moves": "Użytkownik migruje", "notification_visibility_emoji_reactions": "Reakcje", "no_rich_text_description": "Usuwaj formatowanie ze wszystkich postów", - "no_blocks": "Bez blokad", - "no_mutes": "Bez wyciszeń", + "no_blocks": "Brak blokad", + "no_mutes": "Brak wyciszeń", "hide_follows_description": "Nie pokazuj kogo obserwuję", "hide_followers_description": "Nie pokazuj kto mnie obserwuje", "hide_follows_count_description": "Nie pokazuj licznika obserwowanych", @@ -361,7 +368,6 @@ "profile_tab": "Profil", "radii_help": "Ustaw zaokrąglenie krawędzi interfejsu (w pikselach)", "replies_in_timeline": "Odpowiedzi na osi czasu", - "reply_link_preview": "Włącz dymek z podglądem postu po najechaniu na znak odpowiedzi", "reply_visibility_all": "Pokazuj wszystkie odpowiedzi", "reply_visibility_following": "Pokazuj tylko odpowiedzi skierowane do mnie i osób które obserwuję", "reply_visibility_self": "Pokazuj tylko odpowiedzi skierowane do mnie", @@ -385,7 +391,7 @@ "post_status_content_type": "Post status content type", "stop_gifs": "Odtwarzaj GIFy po najechaniu kursorem", "streaming": "Włącz automatycznie strumieniowanie nowych postów gdy jesteś na początku strony", - "user_mutes": "Users", + "user_mutes": "Użytkownicy", "useStreamingApi": "Otrzymuj posty i powiadomienia w czasie rzeczywistym", "useStreamingApiWarning": "(Niezalecane, eksperymentalne, pomija posty)", "text": "Tekst", @@ -404,12 +410,7 @@ "fun": "Zabawa", "greentext": "Memiczne strzałki", "notifications": "Powiadomienia", - "notification_setting": "Otrzymuj powiadomienia od:", - "notification_setting_follows": "Ludzi których obserwujesz", - "notification_setting_non_follows": "Ludzi których nie obserwujesz", - "notification_setting_followers": "Ludzi którzy obserwują ciebie", - "notification_setting_non_followers": "Ludzi którzy nie obserwują ciebie", - "notification_mutes": "By przestać otrzymywać powiadomienia od jednego użytkownika, wycisz go", + "notification_mutes": "By przestać otrzymywać powiadomienia od jednego użytkownika, wycisz go.", "notification_blocks": "Blokowanie uzytkownika zatrzymuje wszystkie powiadomienia i odsubskrybowuje go.", "enable_web_push_notifications": "Włącz powiadomienia push", "style": { @@ -419,7 +420,7 @@ "keep_opacity": "Zachowaj widoczność", "keep_roundness": "Zachowaj zaokrąglenie", "keep_fonts": "Zachowaj czcionki", - "save_load_hint": "Opcje „zachowaj” pozwalają na pozostanie przy obecnych opcjach po wybraniu lub załadowaniu motywu, jak i przechowywanie ich podczas eksportowania motywu. Jeżeli wszystkie są odznaczone, eksportowanie motywu spowoduje zapisanie wszystkiego.", + "save_load_hint": "Opcje „zachowaj” pozwalają na pozostanie przy obecnych opcjach po wybraniu lub załadowaniu motywu, jak i przechowywanie ich podczas eksportowania motywu. Jeżeli wszystkie opcje są odznaczone, eksportowanie motywu spowoduje zapisanie wszystkiego.", "reset": "Wyzeruj", "clear_all": "Wyczyść wszystko", "clear_opacity": "Wyczyść widoczność", @@ -428,17 +429,17 @@ "use_snapshot": "Stara wersja", "use_source": "Nowa wersja", "help": { - "upgraded_from_v2": "PleromaFE zostało zaaktualizowane, motyw może wyglądać nieco inaczej niż sobie zapamiętałeś.", - "v2_imported": "Plik który zaimportowałeś został stworzony dla starszego FE. Próbujemy zwiększyć kompatybiliność, lecz wciąż mogą występować rozbieżności.", - "future_version_imported": "Plik który zaimportowałeś został stworzony w nowszej wersji FE.", - "older_version_imported": "Plik który zaimportowałeś został stworzony w starszej wersji FE.", - "snapshot_present": "Migawka motywu jest załadowana, więc wszystkie wartości zostały nadpisane. Zamiast tego, możesz załadować właściwe dane motywu", + "upgraded_from_v2": "PleromaFE zostało zaaktualizowane, motyw może wyglądać nieco inaczej niż zapamiętałeś(-aś).", + "v2_imported": "Plik który zaimportowałeś(-aś) został stworzony dla starszego FE. Próbujemy zwiększyć kompatybilność, lecz wciąż mogą występować rozbieżności.", + "future_version_imported": "Plik który zaimportowałeś(-aś) został stworzony w nowszej wersji FE.", + "older_version_imported": "Plik który zaimportowałeś(-aś) został stworzony w starszej wersji FE.", + "snapshot_present": "Migawka motywu jest załadowana, więc wszystkie wartości zostały nadpisane. Zamiast tego możesz załadować właściwe dane motywu.", "snapshot_missing": "Nie znaleziono migawki motywu w pliku, więc motyw może wyglądać inaczej niż pierwotnie zaplanowano.", "fe_upgraded": "Silnik motywów PleromaFE został zaaktualizowany.", "fe_downgraded": "Wersja PleromaFE została cofnięta.", "migration_snapshot_ok": "Żeby być bezpiecznym, migawka motywu została załadowana. Możesz spróbować załadować dane motywu.", - "migration_napshot_gone": "Z jakiegoś powodu migawka zniknęła, niektóre rzeczy mogą wyglądać inaczej niż sobie zapamiętałeś.", - "snapshot_source_mismatch": "Konflikt wersji: najprawdopodobniej FE zostało cofnięte do poprzedniej wersji i zaaktualizowane ponownie, jeśli zmieniłeś motyw używając starszej wersji FE, najprawdopodobniej chcesz używać starszej wersji, w przeciwnym razie użyj nowej wersji." + "migration_napshot_gone": "Z jakiegoś powodu migawka zniknęła, niektóre rzeczy mogą wyglądać inaczej niż zapamiętałeś(-aś).", + "snapshot_source_mismatch": "Konflikt wersji: najprawdopodobniej FE zostało cofnięte do poprzedniej wersji i zaktualizowane ponownie, jeśli zmieniłeś(-aś) motyw używając starszej wersji FE, najprawdopodobniej chcesz używać starszej wersji, w przeciwnym razie użyj nowej wersji." } }, "common": { @@ -488,7 +489,11 @@ "selectedMenu": "Wybrany element menu", "disabled": "Wyłączone", "toggled": "Przełączone", - "tabs": "Karty" + "tabs": "Karty", + "chat": { + "outgoing": "Wiadomości wychodzące", + "incoming": "Wiadomości przychodzące" + } }, "radii": { "_tab_label": "Zaokrąglenie" @@ -505,7 +510,7 @@ "filter_hint": { "always_drop_shadow": "Ostrzeżenie, ten cień zawsze używa {0} jeżeli to obsługiwane przez przeglądarkę.", "drop_shadow_syntax": "{0} nie obsługuje parametru {1} i słowa kluczowego {2}.", - "avatar_inset": "Pamiętaj że użycie jednocześnie cieni inset i nie inset na awatarach może daćnieoczekiwane wyniki z przezroczystymi awatarami.", + "avatar_inset": "Pamiętaj że użycie jednocześnie cieni inset i nie inset na awatarach może dać nieoczekiwane wyniki z przezroczystymi awatarami.", "spread_zero": "Cienie o ujemnej szerokości będą widoczne tak, jakby wynosiła ona zero", "inset_classic": "Cienie inset będą używały {0}" }, @@ -548,7 +553,7 @@ "faint_link": "pomocny podręcznik", "fine_print": "Przeczytaj nasz {0}, aby nie nauczyć się niczego przydatnego!", "header_faint": "W porządku", - "checkbox": "Przeleciałem przez zasady użytkowania", + "checkbox": "Przeleciałem(-am) przez zasady użytkowania", "link": "i fajny mały odnośnik" } }, @@ -556,13 +561,25 @@ "title": "Wersja", "backend_version": "Wersja back-endu", "frontend_version": "Wersja front-endu" - } + }, + "notification_setting_privacy": "Prywatność", + "notification_setting_filters": "Filtry", + "notification_setting_privacy_option": "Ukryj nadawcę i zawartość powiadomień push", + "reset_avatar": "Zresetuj awatar", + "profile_fields": { + "value": "Zawartość", + "label": "Metadane profilu", + "name": "Nazwa", + "add_field": "Dodaj pole" + }, + "bot": "To konto jest prowadzone przez bota", + "notification_setting_hide_notification_contents": "Ukryj nadawcę i zawartość powiadomień push" }, "time": { "day": "{0} dzień", "days": "{0} dni", - "day_short": "{0}d", - "days_short": "{0}d", + "day_short": "{0} d", + "days_short": "{0} d", "hour": "{0} godzina", "hours": "{0} godzin", "hour_short": "{0} godz.", @@ -571,8 +588,8 @@ "in_past": "{0} temu", "minute": "{0} minuta", "minutes": "{0} minut", - "minute_short": "{0}min", - "minutes_short": "{0}min", + "minute_short": "{0} min", + "minutes_short": "{0} min", "month": "{0} miesiąc", "months": "{0} miesięcy", "month_short": "{0} mies.", @@ -581,8 +598,8 @@ "now_short": "teraz", "second": "{0} sekunda", "seconds": "{0} sekund", - "second_short": "{0}s", - "seconds_short": "{0}s", + "second_short": "{0} s", + "seconds_short": "{0} s", "week": "{0} tydzień", "weeks": "{0} tygodni", "week_short": "{0} tydz.", @@ -602,7 +619,8 @@ "show_new": "Pokaż nowe", "up_to_date": "Na bieżąco", "no_more_statuses": "Brak kolejnych statusów", - "no_statuses": "Brak statusów" + "no_statuses": "Brak statusów", + "reload": "Odśwież" }, "status": { "favorites": "Ulubione", @@ -615,7 +633,13 @@ "reply_to": "Odpowiedź dla", "replies_list": "Odpowiedzi:", "mute_conversation": "Wycisz konwersację", - "unmute_conversation": "Odcisz konwersację" + "unmute_conversation": "Odcisz konwersację", + "status_unavailable": "Status niedostępny", + "copy_link": "Kopiuj link do statusu", + "unbookmark": "Usuń z zakładek", + "bookmark": "Dodaj do zakładek", + "hide_content": "Ukryj zawartość", + "show_content": "Pokaż zawartość" }, "user_card": { "approve": "Przyjmij", @@ -640,7 +664,7 @@ "muted": "Wyciszony(-a)", "per_day": "dziennie", "remote_follow": "Zdalna obserwacja", - "report": "Raportuj", + "report": "Zgłoś", "statuses": "Statusy", "subscribe": "Subskrybuj", "unsubscribe": "Odsubskrybuj", @@ -669,8 +693,9 @@ "disable_any_subscription": "Zakaż całkowicie obserwowania użytkownika", "quarantine": "Zakaż federowania postów od tego użytkownika", "delete_user": "Usuń użytkownika", - "delete_user_confirmation": "Czy jesteś absolutnie pewny? 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" }, "user_profile": { "timeline_title": "Oś czasu użytkownika", @@ -679,10 +704,10 @@ }, "user_reporting": { "title": "Raportowanie {0}", - "add_comment_description": "Raport zostanie wysłany do moderatorów instancji. Możesz dodać powód dlaczego raportujesz to konto poniżej:", + "add_comment_description": "Zgłoszenie zostanie wysłane do moderatorów instancji. Możesz dodać powód dlaczego zgłaszasz owe konto poniżej:", "additional_comments": "Dodatkowe komentarze", - "forward_description": "To konto jest z innego serwera. Wysłać również tam kopię raportu?", - "forward_to": "Przekaż do{0}", + "forward_description": "To konto jest z innego serwera. Wysłać również tam kopię zgłoszenia?", + "forward_to": "Przekaż do {0}", "submit": "Wyślij", "generic_error": "Wystąpił błąd podczas przetwarzania twojej prośby." }, @@ -696,9 +721,11 @@ "reply": "Odpowiedz", "favorite": "Dodaj do ulubionych", "add_reaction": "Dodaj reakcję", - "user_settings": "Ustawienia użytkownika" + "user_settings": "Ustawienia użytkownika", + "accept_follow_request": "Akceptuj prośbę o możliwość obserwacji", + "reject_follow_request": "Odrzuć prośbę o możliwość obserwacji" }, - "upload":{ + "upload": { "error": { "base": "Wysyłanie nie powiodło się.", "file_too_big": "Zbyt duży plik [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", @@ -720,16 +747,36 @@ "no_results": "Brak wyników" }, "password_reset": { - "forgot_password": "Zapomniałeś hasła?", + "forgot_password": "Zapomniałeś(-aś) hasła?", "password_reset": "Reset hasła", "instruction": "Wprowadź swój adres email lub nazwę użytkownika. Wyślemy ci link z którym możesz zresetować hasło.", "placeholder": "Twój email lub nazwa użytkownika", "check_email": "Sprawdź pocztę, aby uzyskać link do zresetowania hasła.", "return_home": "Wróć do strony głównej", "not_found": "Nie mogliśmy znaleźć tego emaila lub nazwy użytkownika.", - "too_many_requests": "Przekroczyłeś limit prób, spróbuj ponownie później.", + "too_many_requests": "Przekroczyłeś(-aś) limit prób, spróbuj ponownie później.", "password_reset_disabled": "Resetowanie hasła jest wyłączone. Proszę skontaktuj się z administratorem tej instancji.", "password_reset_required": "Musisz zresetować hasło, by się zalogować.", "password_reset_required_but_mailer_is_disabled": "Musisz zresetować hasło, ale resetowanie hasła jest wyłączone. Proszę skontaktuj się z administratorem tej instancji." + }, + "file_type": { + "file": "Plik", + "image": "Zdjęcie", + "video": "Wideo", + "audio": "Audio" + }, + "chats": { + "more": "Więcej", + "delete": "Usuń", + "you": "Ty:", + "delete_confirm": "Czy na pewno chcesz usunąć tą wiadomość?", + "message_user": "Napisz do {nickname}", + "error_sending_message": "Coś poszło nie tak podczas wysyłania wiadomości.", + "error_loading_chat": "Coś poszło nie tak podczas ładowania czatu.", + "empty_message_error": "Nie można wysłać pustej wiadomości", + "new": "Nowy czat" + }, + "display_date": { + "today": "Dzisiaj" } } diff --git a/src/i18n/pt.json b/src/i18n/pt.json @@ -109,7 +109,6 @@ "app_name": "Nome do aplicativo", "attachmentRadius": "Anexos", "attachments": "Anexos", - "autoload": "Habilitar carregamento automático quando a rolagem chegar ao fim.", "avatar": "Avatar", "avatarAltRadius": "Avatares (Notificações)", "avatarRadius": "Avatares", @@ -203,7 +202,6 @@ "profile_tab": "Perfil", "radii_help": "Arredondar arestas da interface (em pixel)", "replies_in_timeline": "Respostas na linha do tempo", - "reply_link_preview": "Habilitar a pré-visualização de de respostas ao passar o mouse.", "reply_visibility_all": "Mostrar todas as respostas", "reply_visibility_following": "Só mostrar respostas direcionadas a mim ou a usuários que sigo", "reply_visibility_self": "Só mostrar respostas direcionadas a mim", diff --git a/src/i18n/ro.json b/src/i18n/ro.json @@ -38,7 +38,6 @@ }, "settings": { "attachments": "Atașamente", - "autoload": "Permite încărcarea automată când scrolat la capăt", "avatar": "Avatar", "bio": "Bio", "current_avatar": "Avatarul curent", @@ -52,7 +51,6 @@ "nsfw_clickthrough": "Permite ascunderea al atașamentelor NSFW", "profile_background": "Fundalul de profil", "profile_banner": "Banner de profil", - "reply_link_preview": "Permite previzualizarea linkului de răspuns la planarea de mouse", "set_new_avatar": "Setează avatar nou", "set_new_profile_background": "Setează fundal nou", "set_new_profile_banner": "Setează banner nou la profil", diff --git a/src/i18n/ru.json b/src/i18n/ru.json @@ -13,7 +13,12 @@ "disable": "Оключить", "enable": "Включить", "confirm": "Подтвердить", - "verify": "Проверить" + "verify": "Проверить", + "more": "Больше", + "generic_error": "Произошла ошибка", + "optional": "не обязательно", + "show_less": "Показать меньше", + "show_more": "Показать больше" }, "login": { "login": "Войти", @@ -26,9 +31,9 @@ "enter_recovery_code": "Ввести код восстановления", "enter_two_factor_code": "Ввести код аутентификации", "recovery_code": "Код восстановления", - "heading" : { - "TotpForm" : "Двухфакторная аутентификация", - "RecoveryForm" : "Two-factor recovery" + "heading": { + "TotpForm": "Двухфакторная аутентификация", + "RecoveryForm": "Two-factor recovery" } }, "nav": { @@ -39,7 +44,9 @@ "public_tl": "Публичная лента", "timeline": "Лента", "twkn": "Федеративная лента", - "search": "Поиск" + "search": "Поиск", + "friend_requests": "Запросы на чтение", + "bookmarks": "Закладки" }, "notifications": { "broken_favorite": "Неизвестный статус, ищем...", @@ -48,7 +55,8 @@ "load_older": "Загрузить старые уведомления", "notifications": "Уведомления", "read": "Прочесть", - "repeated_you": "повторил(а) ваш статус" + "repeated_you": "повторил(а) ваш статус", + "follow_request": "хочет читать вас" }, "interactions": { "favs_repeats": "Повторы и фавориты", @@ -56,7 +64,7 @@ "load_older": "Загрузить старые взаимодействия" }, "post_status": { - "account_not_locked_warning": "Ваш аккаунт не {0}. Кто угодно может зафоловить вас чтобы прочитать посты только для подписчиков", + "account_not_locked_warning": "Ваш аккаунт не {0}. Кто угодно может начать читать вас чтобы видеть посты только для подписчиков.", "account_not_locked_warning_link": "залочен", "attachments_sensitive": "Вложения содержат чувствительный контент", "content_warning": "Тема (не обязательно)", @@ -94,17 +102,17 @@ "settings": { "enter_current_password_to_confirm": "Введите свой текущий пароль", "mfa": { - "otp" : "OTP", - "setup_otp" : "Настройка OTP", - "wait_pre_setup_otp" : "предварительная настройка OTP", - "confirm_and_enable" : "Подтвердить и включить OTP", + "otp": "OTP", + "setup_otp": "Настройка OTP", + "wait_pre_setup_otp": "предварительная настройка OTP", + "confirm_and_enable": "Подтвердить и включить OTP", "title": "Двухфакторная аутентификация", - "generate_new_recovery_codes" : "Получить новые коды востановления", - "warning_of_generate_new_codes" : "После получения новых кодов восстановления, старые больше не будут работать.", - "recovery_codes" : "Коды восстановления.", + "generate_new_recovery_codes": "Получить новые коды востановления", + "warning_of_generate_new_codes": "После получения новых кодов восстановления, старые больше не будут работать.", + "recovery_codes": "Коды восстановления.", "waiting_a_recovery_codes": "Получение кодов восстановления ...", - "recovery_codes_warning" : "Запишите эти коды и держите в безопасном месте - иначе вы их больше не увидите. Если вы потеряете доступ к OTP приложению - без резервных кодов вы больше не сможете залогиниться.", - "authentication_methods" : "Методы аутентификации", + "recovery_codes_warning": "Запишите эти коды и держите в безопасном месте - иначе вы их больше не увидите. Если вы потеряете доступ к OTP приложению - без резервных кодов вы больше не сможете залогиниться.", + "authentication_methods": "Методы аутентификации", "scan": { "title": "Сканирование", "desc": "Используйте приложение для двухэтапной аутентификации для сканирования этого QR-код или введите текстовый ключ:", @@ -116,23 +124,23 @@ }, "attachmentRadius": "Прикреплённые файлы", "attachments": "Вложения", - "autoload": "Включить автоматическую загрузку при прокрутке вниз", "avatar": "Аватар", "avatarAltRadius": "Аватары в уведомлениях", "avatarRadius": "Аватары", "background": "Фон", "bio": "Описание", "btnRadius": "Кнопки", + "bot": "Это аккаунт бота", "cBlue": "Ответить, читать", "cGreen": "Повторить", "cOrange": "Нравится", "cRed": "Отменить", "change_email": "Сменить email", "change_email_error": "Произошла ошибка при попытке изменить email.", - "changed_email": "Email изменён успешно.", + "changed_email": "Email изменён успешно!", "change_password": "Сменить пароль", "change_password_error": "Произошла ошибка при попытке изменить пароль.", - "changed_password": "Пароль изменён успешно.", + "changed_password": "Пароль изменён успешно!", "collapse_subject": "Сворачивать посты с темой", "confirm_new_password": "Подтверждение нового пароля", "current_avatar": "Текущий аватар", @@ -150,7 +158,7 @@ "follow_export_button": "Экспортировать читаемых в файл .csv", "follow_export_processing": "Ведётся обработка, скоро вам будет предложено загрузить файл", "follow_import": "Импортировать читаемых", - "follow_import_error": "Ошибка при импортировании читаемых.", + "follow_import_error": "Ошибка при импортировании читаемых", "follows_imported": "Список читаемых импортирован. Обработка займёт некоторое время..", "foreground": "Передний план", "general": "Общие", @@ -202,9 +210,8 @@ "profile_tab": "Профиль", "radii_help": "Скругление углов элементов интерфейса (в пикселях)", "replies_in_timeline": "Ответы в ленте", - "reply_link_preview": "Включить предварительный просмотр ответа при наведении мыши", "reply_visibility_all": "Показывать все ответы", - "reply_visibility_following": "Показывать только ответы мне и тех на кого я подписан", + "reply_visibility_following": "Показывать только ответы мне или тех на кого я подписан", "reply_visibility_self": "Показывать только ответы мне", "autohide_floating_post_button": "Автоматически скрывать кнопку постинга (в мобильной версии)", "saving_err": "Не удалось сохранить настройки", @@ -224,7 +231,7 @@ "text": "Текст", "theme": "Тема", "theme_help": "Используйте шестнадцатеричные коды цветов (#rrggbb) для настройки темы.", - "theme_help_v2_1": "Вы так же можете перепоределить цвета определенных компонентов нажав соотв. галочку. Используйте кнопку \"Очистить всё\" чтобы снять все переопределения", + "theme_help_v2_1": "Вы так же можете перепоределить цвета определенных компонентов нажав соотв. галочку. Используйте кнопку \"Очистить всё\" чтобы снять все переопределения.", "theme_help_v2_2": "Под некоторыми полями ввода это идикаторы контрастности, наведите на них мышью чтобы узнать больше. Приспользовании прозрачности контраст расчитывается для наихудшего варианта.", "tooltipRadius": "Всплывающие подсказки/уведомления", "user_settings": "Настройки пользователя", @@ -292,9 +299,9 @@ "inset": "Внутренняя", "hint": "Для теней вы так же можете использовать --variable в качестве цвета чтобы использовать CSS3-переменные. В таком случае прозрачность работать не будет.", "filter_hint": { - "always_drop_shadow": "Внимание, эта тень всегда использует {0} когда браузер поддерживает это", - "drop_shadow_syntax": "{0} не поддерживает параметр {1} и ключевое слово {2}", - "avatar_inset": "Одновременное использование внутренних и внешних теней на (прозрачных) аватарках может дать не те результаты что вы ожидаете", + "always_drop_shadow": "Внимание, эта тень всегда использует {0} когда браузер поддерживает это.", + "drop_shadow_syntax": "{0} не поддерживает параметр {1} и ключевое слово {2}.", + "avatar_inset": "Одновременное использование внутренних и внешних теней на (прозрачных) аватарках может дать не те результаты что вы ожидаете.", "spread_zero": "Тени с разбросом > 0 будут выглядеть как если бы разброс установлен в 0", "inset_classic": "Внутренние тени будут использовать {0}" }, @@ -340,7 +347,9 @@ "checkbox": "Я подтверждаю что не было ни единого разрыва", "link": "ссылка" } - } + }, + "allow_following_move": "Разрешить автоматически читать новый аккаунт при перемещении на другой сервер", + "hide_user_stats": "Не показывать статистику пользователей (например количество читателей)" }, "timeline": { "collapse": "Свернуть", @@ -352,6 +361,10 @@ "show_new": "Показать новые", "up_to_date": "Обновлено" }, + "status": { + "bookmark": "В закладки", + "unbookmark": "Удалить из закладок" + }, "user_card": { "block": "Заблокировать", "blocked": "Заблокирован", @@ -359,12 +372,12 @@ "follow": "Читать", "follow_sent": "Запрос отправлен!", "follow_progress": "Запрашиваем…", - "follow_again": "Запросить еще заново?", + "follow_again": "Запросить еще раз?", "follow_unfollow": "Перестать читать", "followees": "Читаемые", "followers": "Читатели", - "following": "Читаю", - "follows_you": "Читает вас", + "following": "Читаю!", + "follows_you": "Читает вас!", "mute": "Игнорировать", "muted": "Игнорирую", "per_day": "в день", @@ -382,9 +395,9 @@ "force_nsfw": "Отмечать посты пользователя как NSFW", "strip_media": "Убирать вложения из постов пользователя", "force_unlisted": "Не добавлять посты в публичные ленты", - "sandbox": "Посты доступны только для подписчиков", - "disable_remote_subscription": "Запретить подписываться с удаленных серверов", - "disable_any_subscription": "Запретить подписываться на пользователя", + "sandbox": "Принудить видимость постов только читателям", + "disable_remote_subscription": "Запретить читать с удаленных серверов", + "disable_any_subscription": "Запретить читать пользователя", "quarantine": "Не федерировать посты пользователя", "delete_user": "Удалить пользователя", "delete_user_confirmation": "Вы уверены? Это действие нельзя отменить." @@ -410,5 +423,56 @@ "not_found": "Мы не смогли найти аккаунт с таким email-ом или именем пользователя.", "too_many_requests": "Вы исчерпали допустимое количество попыток, попробуйте позже.", "password_reset_disabled": "Сброс пароля отключен. Cвяжитесь с администратором вашего сервера." + }, + "about": { + "mrf": { + "federation": "Федерация", + "simple": { + "accept_desc": "Данный сервер принимает сообщения только со следующих серверов:", + "ftl_removal_desc": "Данный сервер скрывает следующие сервера с федеративной ленты:", + "media_nsfw_desc": "Данный сервер принужденно помечает вложения со следущих серверов как NSFW:", + "simple_policies": "Правила для определенных серверов", + "accept": "Принимаемые сообщения", + "reject": "Отклоняемые сообщения", + "reject_desc": "Данный сервер не принимает сообщения со следующих серверов:", + "quarantine": "Зона карантина", + "quarantine_desc": "Данный сервер отправляет только публичные посты следующим серверам:", + "ftl_removal": "Скрытие с федеративной ленты", + "media_removal": "Удаление вложений", + "media_removal_desc": "Данный сервер удаляет вложения со следующих серверов:", + "media_nsfw": "Принужденно помеченно как NSFW" + }, + "keyword": { + "ftl_removal": "Убрать из федеративной ленты", + "reject": "Отклонить", + "keyword_policies": "Действия на ключевые слова", + "replace": "Заменить", + "is_replaced_by": "→" + }, + "mrf_policies": "Активные правила MRF (модуль переписывания сообщений)", + "mrf_policies_desc": "Правила MRF (модуль переписывания сообщений) влияют на федерацию данного сервера. Следующие правила активны:" + }, + "staff": "Администрация" + }, + "domain_mute_card": { + "mute": "Игнорировать", + "mute_progress": "В процессе…", + "unmute": "Прекратить игнорирование", + "unmute_progress": "В процессе…" + }, + "exporter": { + "export": "Экспорт", + "processing": "Запрос в обработке, вам скоро будет предложено загрузить файл" + }, + "features_panel": { + "chat": "Чат", + "media_proxy": "Прокси для внешних вложений", + "text_limit": "Лимит символов", + "title": "Особенности", + "gopher": "Gopher" + }, + "tool_tip": { + "accept_follow_request": "Принять запрос на чтение", + "reject_follow_request": "Отклонить запрос на чтение" } } diff --git a/src/i18n/service_worker_messages.js b/src/i18n/service_worker_messages.js @@ -0,0 +1,35 @@ +/* eslint-disable import/no-webpack-loader-syntax */ +// This module exports only the notification part of the i18n, +// which is useful for the service worker + +const messages = { + ar: require('../lib/notification-i18n-loader.js!./ar.json'), + ca: require('../lib/notification-i18n-loader.js!./ca.json'), + cs: require('../lib/notification-i18n-loader.js!./cs.json'), + de: require('../lib/notification-i18n-loader.js!./de.json'), + eo: require('../lib/notification-i18n-loader.js!./eo.json'), + es: require('../lib/notification-i18n-loader.js!./es.json'), + et: require('../lib/notification-i18n-loader.js!./et.json'), + eu: require('../lib/notification-i18n-loader.js!./eu.json'), + fi: require('../lib/notification-i18n-loader.js!./fi.json'), + fr: require('../lib/notification-i18n-loader.js!./fr.json'), + ga: require('../lib/notification-i18n-loader.js!./ga.json'), + he: require('../lib/notification-i18n-loader.js!./he.json'), + hu: require('../lib/notification-i18n-loader.js!./hu.json'), + it: require('../lib/notification-i18n-loader.js!./it.json'), + ja: require('../lib/notification-i18n-loader.js!./ja_pedantic.json'), + ja_easy: require('../lib/notification-i18n-loader.js!./ja_easy.json'), + ko: require('../lib/notification-i18n-loader.js!./ko.json'), + nb: require('../lib/notification-i18n-loader.js!./nb.json'), + nl: require('../lib/notification-i18n-loader.js!./nl.json'), + oc: require('../lib/notification-i18n-loader.js!./oc.json'), + pl: require('../lib/notification-i18n-loader.js!./pl.json'), + pt: require('../lib/notification-i18n-loader.js!./pt.json'), + ro: require('../lib/notification-i18n-loader.js!./ro.json'), + ru: require('../lib/notification-i18n-loader.js!./ru.json'), + te: require('../lib/notification-i18n-loader.js!./te.json'), + zh: require('../lib/notification-i18n-loader.js!./zh.json'), + en: require('../lib/notification-i18n-loader.js!./en.json') +} + +export default messages diff --git a/src/i18n/te.json b/src/i18n/te.json @@ -1,352 +1,350 @@ { - "chat.title": "చాట్", - "features_panel.chat": "చాట్", - "features_panel.gopher": "గోఫర్", - "features_panel.media_proxy": "మీడియా ప్రాక్సీ", - "features_panel.scope_options": "స్కోప్ ఎంపికలు", - "features_panel.text_limit": "వచన పరిమితి", - "features_panel.title": "లక్షణాలు", - "features_panel.who_to_follow": "ఎవరిని అనుసరించాలి", - "finder.error_fetching_user": "వినియోగదారుని పొందడంలో లోపం", - "finder.find_user": "వినియోగదారుని కనుగొనండి", - "general.apply": "వర్తించు", - "general.submit": "సమర్పించు", - "general.more": "మరిన్ని", - "general.generic_error": "ఒక తప్పిదం సంభవించినది", - "general.optional": "ఐచ్చికం", - "image_cropper.crop_picture": "చిత్రాన్ని కత్తిరించండి", - "image_cropper.save": "దాచు", - "image_cropper.save_without_cropping": "కత్తిరించకుండా సేవ్ చేయి", - "image_cropper.cancel": "రద్దుచేయి", - "login.login": "లాగిన్", - "login.description": "OAuth తో లాగిన్ అవ్వండి", - "login.logout": "లాగౌట్", - "login.password": "సంకేతపదము", - "login.placeholder": "ఉదా. lain", - "login.register": "నమోదు చేసుకోండి", - "login.username": "వాడుకరి పేరు", - "login.hint": "చర్చలో చేరడానికి లాగిన్ అవ్వండి", - "media_modal.previous": "ముందరి పుట", - "media_modal.next": "తరువాత", - "nav.about": "గురించి", - "nav.back": "వెనక్కి", - "nav.chat": "స్థానిక చాట్", - "nav.friend_requests": "అనుసరించడానికి అభ్యర్థనలు", - "nav.mentions": "ప్రస్తావనలు", - "nav.dms": "నేరుగా పంపిన సందేశాలు", - "nav.public_tl": "ప్రజా కాలక్రమం", - "nav.timeline": "కాలక్రమం", - "nav.twkn": "మొత్తం తెలిసిన నెట్వర్క్", - "nav.user_search": "వాడుకరి శోధన", - "nav.who_to_follow": "ఎవరిని అనుసరించాలి", - "nav.preferences": "ప్రాధాన్యతలు", - "notifications.broken_favorite": "తెలియని స్థితి, దాని కోసం శోధిస్తోంది...", - "notifications.favorited_you": "మీ స్థితిని ఇష్టపడ్డారు", - "notifications.followed_you": "మిమ్మల్ని అనుసరించారు", - "notifications.load_older": "పాత నోటిఫికేషన్లను లోడ్ చేయండి", - "notifications.notifications": "ప్రకటనలు", - "notifications.read": "చదివాను!", - "notifications.repeated_you": "మీ స్థితిని పునరావృతం చేసారు", - "notifications.no_more_notifications": "ఇక నోటిఫికేషన్లు లేవు", - "post_status.new_status": "క్రొత్త స్థితిని పోస్ట్ చేయండి", - "post_status.account_not_locked_warning": "మీ ఖాతా {౦} కాదు. ఎవరైనా మిమ్మల్ని అనుసరించి అనుచరులకు మాత్రమే ఉద్దేశించిన పోస్టులను చూడవచ్చు.", - "post_status.account_not_locked_warning_link": "తాళం వేయబడినది", - "post_status.attachments_sensitive": "జోడింపులను సున్నితమైనవిగా గుర్తించండి", - "post_status.content_type.text/plain": "సాధారణ అక్షరాలు", - "post_status.content_type.text/html": "హెచ్‌టిఎమ్ఎల్", - "post_status.content_type.text/markdown": "మార్క్డౌన్", - "post_status.content_warning": "విషయం (ఐచ్ఛికం)", - "post_status.default": "ఇప్పుడే విజయవాడలో దిగాను.", - "post_status.direct_warning": "ఈ పోస్ట్ మాత్రమే పేర్కొన్న వినియోగదారులకు మాత్రమే కనిపిస్తుంది.", - "post_status.posting": "పోస్ట్ చేస్తున్నా", - "post_status.scope.direct": "ప్రత్యక్ష - పేర్కొన్న వినియోగదారులకు మాత్రమే పోస్ట్ చేయబడుతుంది", - "post_status.scope.private": "అనుచరులకు మాత్రమే - అనుచరులకు మాత్రమే పోస్ట్ చేయబడుతుంది", - "post_status.scope.public": "పబ్లిక్ - ప్రజా కాలక్రమాలకు పోస్ట్ చేయబడుతుంది", - "post_status.scope.unlisted": "జాబితా చేయబడనిది - ప్రజా కాలక్రమాలకు పోస్ట్ చేయవద్దు", - "registration.bio": "బయో", - "registration.email": "ఈ మెయిల్", - "registration.fullname": "ప్రదర్శన పేరు", - "registration.password_confirm": "పాస్వర్డ్ నిర్ధారణ", - "registration.registration": "నమోదు", - "registration.token": "ఆహ్వాన టోకెన్", - "registration.captcha": "కాప్చా", - "registration.new_captcha": "కొత్త కాప్చా పొందుటకు చిత్రం మీద క్లిక్ చేయండి", - "registration.username_placeholder": "ఉదా. lain", - "registration.fullname_placeholder": "ఉదా. Lain Iwakura", - "registration.bio_placeholder": "e.g.\nHi, I'm Lain.\nI’m an anime girl living in suburban Japan. You may know me from the Wired.", - "registration.validations.username_required": "ఖాళీగా విడిచిపెట్టరాదు", - "registration.validations.fullname_required": "ఖాళీగా విడిచిపెట్టరాదు", - "registration.validations.email_required": "ఖాళీగా విడిచిపెట్టరాదు", - "registration.validations.password_required": "ఖాళీగా విడిచిపెట్టరాదు", - "registration.validations.password_confirmation_required": "ఖాళీగా విడిచిపెట్టరాదు", - "registration.validations.password_confirmation_match": "సంకేతపదం వలె ఉండాలి", - "settings.app_name": "అనువర్తన పేరు", - "settings.attachmentRadius": "జోడింపులు", - "settings.attachments": "జోడింపులు", - "settings.autoload": "క్రిందికి స్క్రోల్ చేయబడినప్పుడు స్వయంచాలక లోడింగ్ని ప్రారంభించు", - "settings.avatar": "అవతారం", - "settings.avatarAltRadius": "అవతారాలు (ప్రకటనలు)", - "settings.avatarRadius": "అవతారాలు", - "settings.background": "బ్యాక్‌గ్రౌండు", - "settings.bio": "బయో", - "settings.blocks_tab": "బ్లాక్‌లు", - "settings.btnRadius": "బటన్లు", - "settings.cBlue": "నీలం (ప్రత్యుత్తరం, అనుసరించండి)", - "settings.cGreen": "Green (Retweet)", - "settings.cOrange": "ఆరెంజ్ (ఇష్టపడు)", - "settings.cRed": "Red (Cancel)", - "settings.change_password": "పాస్‌వర్డ్ మార్చండి", - "settings.change_password_error": "మీ పాస్వర్డ్ను మార్చడంలో సమస్య ఉంది.", - "settings.changed_password": "పాస్వర్డ్ విజయవంతంగా మార్చబడింది!", - "settings.collapse_subject": "Collapse posts with subjects", - "settings.composing": "Composing", - "settings.confirm_new_password": "కొత్త పాస్వర్డ్ను నిర్ధారించండి", - "settings.current_avatar": "మీ ప్రస్తుత అవతారం", - "settings.current_password": "ప్రస్తుత పాస్వర్డ్", - "settings.current_profile_banner": "మీ ప్రస్తుత ప్రొఫైల్ బ్యానర్", - "settings.data_import_export_tab": "Data Import / Export", - "settings.default_vis": "Default visibility scope", - "settings.delete_account": "Delete Account", - "settings.delete_account_description": "మీ ఖాతా మరియు మీ అన్ని సందేశాలను శాశ్వతంగా తొలగించండి.", - "settings.delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.", - "settings.delete_account_instructions": "ఖాతా తొలగింపును నిర్ధారించడానికి దిగువ ఇన్పుట్లో మీ పాస్వర్డ్ను టైప్ చేయండి.", - "settings.avatar_size_instruction": "అవతార్ చిత్రాలకు సిఫార్సు చేసిన కనీస పరిమాణం 150x150 పిక్సెల్స్.", - "settings.export_theme": "Save preset", - "settings.filtering": "వడపోత", - "settings.filtering_explanation": "All statuses containing these words will be muted, one per line", - "settings.follow_export": "Follow export", - "settings.follow_export_button": "Export your follows to a csv file", - "settings.follow_export_processing": "Processing, you'll soon be asked to download your file", - "settings.follow_import": "Follow import", - "settings.follow_import_error": "అనుచరులను దిగుమతి చేయడంలో లోపం", - "settings.follows_imported": "Follows imported! Processing them will take a while.", - "settings.foreground": "Foreground", - "settings.general": "General", - "settings.hide_attachments_in_convo": "సంభాషణలలో జోడింపులను దాచు", - "settings.hide_attachments_in_tl": "కాలక్రమంలో జోడింపులను దాచు", - "settings.hide_muted_posts": "మ్యూట్ చేసిన వినియోగదారుల యొక్క పోస్ట్లను దాచిపెట్టు", - "settings.max_thumbnails": "Maximum amount of thumbnails per post", - "settings.hide_isp": "Hide instance-specific panel", - "settings.preload_images": "Preload images", - "settings.use_one_click_nsfw": "కేవలం ఒక క్లిక్ తో NSFW జోడింపులను తెరవండి", - "settings.hide_post_stats": "Hide post statistics (e.g. the number of favorites)", - "settings.hide_user_stats": "Hide user statistics (e.g. the number of followers)", - "settings.hide_filtered_statuses": "Hide filtered statuses", - "settings.import_followers_from_a_csv_file": "Import follows from a csv file", - "settings.import_theme": "Load preset", - "settings.inputRadius": "Input fields", - "settings.checkboxRadius": "Checkboxes", - "settings.instance_default": "(default: {value})", - "settings.instance_default_simple": "(default)", - "settings.interface": "Interface", - "settings.interfaceLanguage": "Interface language", - "settings.invalid_theme_imported": "The selected file is not a supported Pleroma theme. No changes to your theme were made.", - "settings.limited_availability": "మీ బ్రౌజర్లో అందుబాటులో లేదు", - "settings.links": "Links", - "settings.lock_account_description": "మీ ఖాతాను ఆమోదించిన అనుచరులకు మాత్రమే పరిమితం చేయండి", - "settings.loop_video": "Loop videos", - "settings.loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")", - "settings.mutes_tab": "మ్యూట్ చేయబడినవి", - "settings.play_videos_in_modal": "మీడియా వీక్షికలో నేరుగా వీడియోలను ప్లే చేయి", - "settings.use_contain_fit": "అటాచ్మెంట్ సూక్ష్మచిత్రాలను కత్తిరించవద్దు", - "settings.name": "Name", - "settings.name_bio": "పేరు & బయో", - "settings.new_password": "కొత్త సంకేతపదం", - "settings.notification_visibility": "చూపించవలసిన నోటిఫికేషన్ రకాలు", - "settings.notification_visibility_follows": "Follows", - "settings.notification_visibility_likes": "ఇష్టాలు", - "settings.notification_visibility_mentions": "ప్రస్తావనలు", - "settings.notification_visibility_repeats": "పునఃప్రసారాలు", - "settings.no_rich_text_description": "అన్ని పోస్ట్ల నుండి రిచ్ టెక్స్ట్ ఫార్మాటింగ్ను స్ట్రిప్ చేయండి", - "settings.no_blocks": "బ్లాక్స్ లేవు", - "settings.no_mutes": "మ్యూట్లు లేవు", - "settings.hide_follows_description": "నేను ఎవరిని అనుసరిస్తున్నానో చూపించవద్దు", - "settings.hide_followers_description": "నన్ను ఎవరు అనుసరిస్తున్నారో చూపవద్దు", - "settings.show_admin_badge": "నా ప్రొఫైల్ లో అడ్మిన్ బ్యాడ్జ్ చూపించు", - "settings.show_moderator_badge": "నా ప్రొఫైల్లో మోడరేటర్ బ్యాడ్జ్ని చూపించు", - "settings.nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding", - "settings.oauth_tokens": "OAuth tokens", - "settings.token": "Token", - "settings.refresh_token": "Refresh Token", - "settings.valid_until": "Valid Until", - "settings.revoke_token": "Revoke", - "settings.panelRadius": "Panels", - "settings.pause_on_unfocused": "Pause streaming when tab is not focused", - "settings.presets": "Presets", - "settings.profile_background": "Profile Background", - "settings.profile_banner": "Profile Banner", - "settings.profile_tab": "Profile", - "settings.radii_help": "Set up interface edge rounding (in pixels)", - "settings.replies_in_timeline": "Replies in timeline", - "settings.reply_link_preview": "Enable reply-link preview on mouse hover", - "settings.reply_visibility_all": "Show all replies", - "settings.reply_visibility_following": "Only show replies directed at me or users I'm following", - "settings.reply_visibility_self": "Only show replies directed at me", - "settings.saving_err": "Error saving settings", - "settings.saving_ok": "Settings saved", - "settings.security_tab": "Security", - "settings.scope_copy": "Copy scope when replying (DMs are always copied)", - "settings.set_new_avatar": "Set new avatar", - "settings.set_new_profile_background": "Set new profile background", - "settings.set_new_profile_banner": "Set new profile banner", - "settings.settings": "Settings", - "settings.subject_input_always_show": "Always show subject field", - "settings.subject_line_behavior": "Copy subject when replying", - "settings.subject_line_email": "Like email: \"re: subject\"", - "settings.subject_line_mastodon": "Like mastodon: copy as is", - "settings.subject_line_noop": "Do not copy", - "settings.post_status_content_type": "Post status content type", - "settings.stop_gifs": "Play-on-hover GIFs", - "settings.streaming": "Enable automatic streaming of new posts when scrolled to the top", - "settings.text": "Text", - "settings.theme": "Theme", - "settings.theme_help": "Use hex color codes (#rrggbb) to customize your color theme.", - "settings.theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.", - "settings.theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.", - "settings.tooltipRadius": "Tooltips/alerts", - "settings.upload_a_photo": "Upload a photo", - "settings.user_settings": "User Settings", - "settings.values.false": "no", - "settings.values.true": "yes", - "settings.notifications": "Notifications", - "settings.enable_web_push_notifications": "Enable web push notifications", - "settings.style.switcher.keep_color": "Keep colors", - "settings.style.switcher.keep_shadows": "Keep shadows", - "settings.style.switcher.keep_opacity": "Keep opacity", - "settings.style.switcher.keep_roundness": "Keep roundness", - "settings.style.switcher.keep_fonts": "Keep fonts", - "settings.style.switcher.save_load_hint": "\"Keep\" options preserve currently set options when selecting or loading themes, it also stores said options when exporting a theme. When all checkboxes unset, exporting theme will save everything.", - "settings.style.switcher.reset": "Reset", - "settings.style.switcher.clear_all": "Clear all", - "settings.style.switcher.clear_opacity": "Clear opacity", - "settings.style.common.color": "Color", - "settings.style.common.opacity": "Opacity", - "settings.style.common.contrast.hint": "Contrast ratio is {ratio}, it {level} {context}", - "settings.style.common.contrast.level.aa": "meets Level AA guideline (minimal)", - "settings.style.common.contrast.level.aaa": "meets Level AAA guideline (recommended)", - "settings.style.common.contrast.level.bad": "doesn't meet any accessibility guidelines", - "settings.style.common.contrast.context.18pt": "for large (18pt+) text", - "settings.style.common.contrast.context.text": "for text", - "settings.style.common_colors._tab_label": "Common", - "settings.style.common_colors.main": "Common colors", - "settings.style.common_colors.foreground_hint": "See \"Advanced\" tab for more detailed control", - "settings.style.common_colors.rgbo": "Icons, accents, badges", - "settings.style.advanced_colors._tab_label": "Advanced", - "settings.style.advanced_colors.alert": "Alert background", - "settings.style.advanced_colors.alert_error": "Error", - "settings.style.advanced_colors.badge": "Badge background", - "settings.style.advanced_colors.badge_notification": "Notification", - "settings.style.advanced_colors.panel_header": "Panel header", - "settings.style.advanced_colors.top_bar": "Top bar", - "settings.style.advanced_colors.borders": "Borders", - "settings.style.advanced_colors.buttons": "Buttons", - "settings.style.advanced_colors.inputs": "Input fields", - "settings.style.advanced_colors.faint_text": "Faded text", - "settings.style.radii._tab_label": "Roundness", - "settings.style.shadows._tab_label": "Shadow and lighting", - "settings.style.shadows.component": "Component", - "settings.style.shadows.override": "Override", - "settings.style.shadows.shadow_id": "Shadow #{value}", - "settings.style.shadows.blur": "Blur", - "settings.style.shadows.spread": "Spread", - "settings.style.shadows.inset": "Inset", - "settings.style.shadows.hint": "For shadows you can also use --variable as a color value to use CSS3 variables. Please note that setting opacity won't work in this case.", - "settings.style.shadows.filter_hint.always_drop_shadow": "Warning, this shadow always uses {0} when browser supports it.", - "settings.style.shadows.filter_hint.drop_shadow_syntax": "{0} does not support {1} parameter and {2} keyword.", - "settings.style.shadows.filter_hint.avatar_inset": "Please note that combining both inset and non-inset shadows on avatars might give unexpected results with transparent avatars.", - "settings.style.shadows.filter_hint.spread_zero": "Shadows with spread > 0 will appear as if it was set to zero", - "settings.style.shadows.filter_hint.inset_classic": "Inset shadows will be using {0}", - "settings.style.shadows.components.panel": "Panel", - "settings.style.shadows.components.panelHeader": "Panel header", - "settings.style.shadows.components.topBar": "Top bar", - "settings.style.shadows.components.avatar": "User avatar (in profile view)", - "settings.style.shadows.components.avatarStatus": "User avatar (in post display)", - "settings.style.shadows.components.popup": "Popups and tooltips", - "settings.style.shadows.components.button": "Button", - "settings.style.shadows.components.buttonHover": "Button (hover)", - "settings.style.shadows.components.buttonPressed": "Button (pressed)", - "settings.style.shadows.components.buttonPressedHover": "Button (pressed+hover)", - "settings.style.shadows.components.input": "Input field", - "settings.style.fonts._tab_label": "Fonts", - "settings.style.fonts.help": "Select font to use for elements of UI. For \"custom\" you have to enter exact font name as it appears in system.", - "settings.style.fonts.components.interface": "Interface", - "settings.style.fonts.components.input": "Input fields", - "settings.style.fonts.components.post": "Post text", - "settings.style.fonts.components.postCode": "Monospaced text in a post (rich text)", - "settings.style.fonts.family": "Font name", - "settings.style.fonts.size": "Size (in px)", - "settings.style.fonts.weight": "Weight (boldness)", - "settings.style.fonts.custom": "Custom", - "settings.style.preview.header": "Preview", - "settings.style.preview.content": "Content", - "settings.style.preview.error": "Example error", - "settings.style.preview.button": "Button", - "settings.style.preview.text": "A bunch of more {0} and {1}", - "settings.style.preview.mono": "content", - "settings.style.preview.input": "Just landed in L.A.", - "settings.style.preview.faint_link": "helpful manual", - "settings.style.preview.fine_print": "Read our {0} to learn nothing useful!", - "settings.style.preview.header_faint": "This is fine", - "settings.style.preview.checkbox": "I have skimmed over terms and conditions", - "settings.style.preview.link": "a nice lil' link", - "settings.version.title": "Version", - "settings.version.backend_version": "Backend Version", - "settings.version.frontend_version": "Frontend Version", - "timeline.collapse": "Collapse", - "timeline.conversation": "Conversation", - "timeline.error_fetching": "Error fetching updates", - "timeline.load_older": "Load older statuses", - "timeline.no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated", - "timeline.repeated": "repeated", - "timeline.show_new": "Show new", - "timeline.up_to_date": "Up-to-date", - "timeline.no_more_statuses": "No more statuses", - "timeline.no_statuses": "No statuses", - "status.reply_to": "Reply to", - "status.replies_list": "Replies:", - "user_card.approve": "Approve", - "user_card.block": "Block", - "user_card.blocked": "Blocked!", - "user_card.deny": "Deny", - "user_card.favorites": "Favorites", - "user_card.follow": "Follow", - "user_card.follow_sent": "Request sent!", - "user_card.follow_progress": "Requesting…", - "user_card.follow_again": "Send request again?", - "user_card.follow_unfollow": "Unfollow", - "user_card.followees": "Following", - "user_card.followers": "Followers", - "user_card.following": "Following!", - "user_card.follows_you": "Follows you!", - "user_card.its_you": "It's you!", - "user_card.media": "Media", - "user_card.mute": "Mute", - "user_card.muted": "Muted", - "user_card.per_day": "per day", - "user_card.remote_follow": "Remote follow", - "user_card.statuses": "Statuses", - "user_card.unblock": "Unblock", - "user_card.unblock_progress": "Unblocking...", - "user_card.block_progress": "Blocking...", - "user_card.unmute": "Unmute", - "user_card.unmute_progress": "Unmuting...", - "user_card.mute_progress": "Muting...", - "user_profile.timeline_title": "User Timeline", - "user_profile.profile_does_not_exist": "Sorry, this profile does not exist.", - "user_profile.profile_loading_error": "Sorry, there was an error loading this profile.", - "who_to_follow.more": "More", - "who_to_follow.who_to_follow": "Who to follow", - "tool_tip.media_upload": "Upload Media", - "tool_tip.repeat": "Repeat", - "tool_tip.reply": "Reply", - "tool_tip.favorite": "Favorite", - "tool_tip.user_settings": "User Settings", - "upload.error.base": "Upload failed.", - "upload.error.file_too_big": "File too big [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", - "upload.error.default": "Try again later", - "upload.file_size_units.B": "B", - "upload.file_size_units.KiB": "KiB", - "upload.file_size_units.MiB": "MiB", - "upload.file_size_units.GiB": "GiB", - "upload.file_size_units.TiB": "TiB" + "chat.title": "చాట్", + "features_panel.chat": "చాట్", + "features_panel.gopher": "గోఫర్", + "features_panel.media_proxy": "మీడియా ప్రాక్సీ", + "features_panel.scope_options": "స్కోప్ ఎంపికలు", + "features_panel.text_limit": "వచన పరిమితి", + "features_panel.title": "లక్షణాలు", + "features_panel.who_to_follow": "ఎవరిని అనుసరించాలి", + "finder.error_fetching_user": "వినియోగదారుని పొందడంలో లోపం", + "finder.find_user": "వినియోగదారుని కనుగొనండి", + "general.apply": "వర్తించు", + "general.submit": "సమర్పించు", + "general.more": "మరిన్ని", + "general.generic_error": "ఒక తప్పిదం సంభవించినది", + "general.optional": "ఐచ్చికం", + "image_cropper.crop_picture": "చిత్రాన్ని కత్తిరించండి", + "image_cropper.save": "దాచు", + "image_cropper.save_without_cropping": "కత్తిరించకుండా సేవ్ చేయి", + "image_cropper.cancel": "రద్దుచేయి", + "login.login": "లాగిన్", + "login.description": "OAuth తో లాగిన్ అవ్వండి", + "login.logout": "లాగౌట్", + "login.password": "సంకేతపదము", + "login.placeholder": "ఉదా. lain", + "login.register": "నమోదు చేసుకోండి", + "login.username": "వాడుకరి పేరు", + "login.hint": "చర్చలో చేరడానికి లాగిన్ అవ్వండి", + "media_modal.previous": "ముందరి పుట", + "media_modal.next": "తరువాత", + "nav.about": "గురించి", + "nav.back": "వెనక్కి", + "nav.chat": "స్థానిక చాట్", + "nav.friend_requests": "అనుసరించడానికి అభ్యర్థనలు", + "nav.mentions": "ప్రస్తావనలు", + "nav.dms": "నేరుగా పంపిన సందేశాలు", + "nav.public_tl": "ప్రజా కాలక్రమం", + "nav.timeline": "కాలక్రమం", + "nav.twkn": "మొత్తం తెలిసిన నెట్వర్క్", + "nav.user_search": "వాడుకరి శోధన", + "nav.who_to_follow": "ఎవరిని అనుసరించాలి", + "nav.preferences": "ప్రాధాన్యతలు", + "notifications.broken_favorite": "తెలియని స్థితి, దాని కోసం శోధిస్తోంది...", + "notifications.favorited_you": "మీ స్థితిని ఇష్టపడ్డారు", + "notifications.followed_you": "మిమ్మల్ని అనుసరించారు", + "notifications.load_older": "పాత నోటిఫికేషన్లను లోడ్ చేయండి", + "notifications.notifications": "ప్రకటనలు", + "notifications.read": "చదివాను!", + "notifications.repeated_you": "మీ స్థితిని పునరావృతం చేసారు", + "notifications.no_more_notifications": "ఇక నోటిఫికేషన్లు లేవు", + "post_status.new_status": "క్రొత్త స్థితిని పోస్ట్ చేయండి", + "post_status.account_not_locked_warning": "మీ ఖాతా {౦} కాదు. ఎవరైనా మిమ్మల్ని అనుసరించి అనుచరులకు మాత్రమే ఉద్దేశించిన పోస్టులను చూడవచ్చు.", + "post_status.account_not_locked_warning_link": "తాళం వేయబడినది", + "post_status.attachments_sensitive": "జోడింపులను సున్నితమైనవిగా గుర్తించండి", + "post_status.content_type.text/plain": "సాధారణ అక్షరాలు", + "post_status.content_type.text/html": "హెచ్‌టిఎమ్ఎల్", + "post_status.content_type.text/markdown": "మార్క్డౌన్", + "post_status.content_warning": "విషయం (ఐచ్ఛికం)", + "post_status.default": "ఇప్పుడే విజయవాడలో దిగాను.", + "post_status.direct_warning": "ఈ పోస్ట్ మాత్రమే పేర్కొన్న వినియోగదారులకు మాత్రమే కనిపిస్తుంది.", + "post_status.posting": "పోస్ట్ చేస్తున్నా", + "post_status.scope.direct": "ప్రత్యక్ష - పేర్కొన్న వినియోగదారులకు మాత్రమే పోస్ట్ చేయబడుతుంది", + "post_status.scope.private": "అనుచరులకు మాత్రమే - అనుచరులకు మాత్రమే పోస్ట్ చేయబడుతుంది", + "post_status.scope.public": "పబ్లిక్ - ప్రజా కాలక్రమాలకు పోస్ట్ చేయబడుతుంది", + "post_status.scope.unlisted": "జాబితా చేయబడనిది - ప్రజా కాలక్రమాలకు పోస్ట్ చేయవద్దు", + "registration.bio": "బయో", + "registration.email": "ఈ మెయిల్", + "registration.fullname": "ప్రదర్శన పేరు", + "registration.password_confirm": "పాస్వర్డ్ నిర్ధారణ", + "registration.registration": "నమోదు", + "registration.token": "ఆహ్వాన టోకెన్", + "registration.captcha": "కాప్చా", + "registration.new_captcha": "కొత్త కాప్చా పొందుటకు చిత్రం మీద క్లిక్ చేయండి", + "registration.username_placeholder": "ఉదా. lain", + "registration.fullname_placeholder": "ఉదా. Lain Iwakura", + "registration.bio_placeholder": "e.g.\nHi, I'm Lain.\nI’m an anime girl living in suburban Japan. You may know me from the Wired.", + "registration.validations.username_required": "ఖాళీగా విడిచిపెట్టరాదు", + "registration.validations.fullname_required": "ఖాళీగా విడిచిపెట్టరాదు", + "registration.validations.email_required": "ఖాళీగా విడిచిపెట్టరాదు", + "registration.validations.password_required": "ఖాళీగా విడిచిపెట్టరాదు", + "registration.validations.password_confirmation_required": "ఖాళీగా విడిచిపెట్టరాదు", + "registration.validations.password_confirmation_match": "సంకేతపదం వలె ఉండాలి", + "settings.app_name": "అనువర్తన పేరు", + "settings.attachmentRadius": "జోడింపులు", + "settings.attachments": "జోడింపులు", + "settings.avatar": "అవతారం", + "settings.avatarAltRadius": "అవతారాలు (ప్రకటనలు)", + "settings.avatarRadius": "అవతారాలు", + "settings.background": "బ్యాక్‌గ్రౌండు", + "settings.bio": "బయో", + "settings.blocks_tab": "బ్లాక్‌లు", + "settings.btnRadius": "బటన్లు", + "settings.cBlue": "నీలం (ప్రత్యుత్తరం, అనుసరించండి)", + "settings.cGreen": "Green (Retweet)", + "settings.cOrange": "ఆరెంజ్ (ఇష్టపడు)", + "settings.cRed": "Red (Cancel)", + "settings.change_password": "పాస్‌వర్డ్ మార్చండి", + "settings.change_password_error": "మీ పాస్వర్డ్ను మార్చడంలో సమస్య ఉంది.", + "settings.changed_password": "పాస్వర్డ్ విజయవంతంగా మార్చబడింది!", + "settings.collapse_subject": "Collapse posts with subjects", + "settings.composing": "Composing", + "settings.confirm_new_password": "కొత్త పాస్వర్డ్ను నిర్ధారించండి", + "settings.current_avatar": "మీ ప్రస్తుత అవతారం", + "settings.current_password": "ప్రస్తుత పాస్వర్డ్", + "settings.current_profile_banner": "మీ ప్రస్తుత ప్రొఫైల్ బ్యానర్", + "settings.data_import_export_tab": "Data Import / Export", + "settings.default_vis": "Default visibility scope", + "settings.delete_account": "Delete Account", + "settings.delete_account_description": "మీ ఖాతా మరియు మీ అన్ని సందేశాలను శాశ్వతంగా తొలగించండి.", + "settings.delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.", + "settings.delete_account_instructions": "ఖాతా తొలగింపును నిర్ధారించడానికి దిగువ ఇన్పుట్లో మీ పాస్వర్డ్ను టైప్ చేయండి.", + "settings.avatar_size_instruction": "అవతార్ చిత్రాలకు సిఫార్సు చేసిన కనీస పరిమాణం 150x150 పిక్సెల్స్.", + "settings.export_theme": "Save preset", + "settings.filtering": "వడపోత", + "settings.filtering_explanation": "All statuses containing these words will be muted, one per line", + "settings.follow_export": "Follow export", + "settings.follow_export_button": "Export your follows to a csv file", + "settings.follow_export_processing": "Processing, you'll soon be asked to download your file", + "settings.follow_import": "Follow import", + "settings.follow_import_error": "అనుచరులను దిగుమతి చేయడంలో లోపం", + "settings.follows_imported": "Follows imported! Processing them will take a while.", + "settings.foreground": "Foreground", + "settings.general": "General", + "settings.hide_attachments_in_convo": "సంభాషణలలో జోడింపులను దాచు", + "settings.hide_attachments_in_tl": "కాలక్రమంలో జోడింపులను దాచు", + "settings.hide_muted_posts": "మ్యూట్ చేసిన వినియోగదారుల యొక్క పోస్ట్లను దాచిపెట్టు", + "settings.max_thumbnails": "Maximum amount of thumbnails per post", + "settings.hide_isp": "Hide instance-specific panel", + "settings.preload_images": "Preload images", + "settings.use_one_click_nsfw": "కేవలం ఒక క్లిక్ తో NSFW జోడింపులను తెరవండి", + "settings.hide_post_stats": "Hide post statistics (e.g. the number of favorites)", + "settings.hide_user_stats": "Hide user statistics (e.g. the number of followers)", + "settings.hide_filtered_statuses": "Hide filtered statuses", + "settings.import_followers_from_a_csv_file": "Import follows from a csv file", + "settings.import_theme": "Load preset", + "settings.inputRadius": "Input fields", + "settings.checkboxRadius": "Checkboxes", + "settings.instance_default": "(default: {value})", + "settings.instance_default_simple": "(default)", + "settings.interface": "Interface", + "settings.interfaceLanguage": "Interface language", + "settings.invalid_theme_imported": "The selected file is not a supported Pleroma theme. No changes to your theme were made.", + "settings.limited_availability": "మీ బ్రౌజర్లో అందుబాటులో లేదు", + "settings.links": "Links", + "settings.lock_account_description": "మీ ఖాతాను ఆమోదించిన అనుచరులకు మాత్రమే పరిమితం చేయండి", + "settings.loop_video": "Loop videos", + "settings.loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")", + "settings.mutes_tab": "మ్యూట్ చేయబడినవి", + "settings.play_videos_in_modal": "మీడియా వీక్షికలో నేరుగా వీడియోలను ప్లే చేయి", + "settings.use_contain_fit": "అటాచ్మెంట్ సూక్ష్మచిత్రాలను కత్తిరించవద్దు", + "settings.name": "Name", + "settings.name_bio": "పేరు & బయో", + "settings.new_password": "కొత్త సంకేతపదం", + "settings.notification_visibility": "చూపించవలసిన నోటిఫికేషన్ రకాలు", + "settings.notification_visibility_follows": "Follows", + "settings.notification_visibility_likes": "ఇష్టాలు", + "settings.notification_visibility_mentions": "ప్రస్తావనలు", + "settings.notification_visibility_repeats": "పునఃప్రసారాలు", + "settings.no_rich_text_description": "అన్ని పోస్ట్ల నుండి రిచ్ టెక్స్ట్ ఫార్మాటింగ్ను స్ట్రిప్ చేయండి", + "settings.no_blocks": "బ్లాక్స్ లేవు", + "settings.no_mutes": "మ్యూట్లు లేవు", + "settings.hide_follows_description": "నేను ఎవరిని అనుసరిస్తున్నానో చూపించవద్దు", + "settings.hide_followers_description": "నన్ను ఎవరు అనుసరిస్తున్నారో చూపవద్దు", + "settings.show_admin_badge": "నా ప్రొఫైల్ లో అడ్మిన్ బ్యాడ్జ్ చూపించు", + "settings.show_moderator_badge": "నా ప్రొఫైల్లో మోడరేటర్ బ్యాడ్జ్ని చూపించు", + "settings.nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding", + "settings.oauth_tokens": "OAuth tokens", + "settings.token": "Token", + "settings.refresh_token": "Refresh Token", + "settings.valid_until": "Valid Until", + "settings.revoke_token": "Revoke", + "settings.panelRadius": "Panels", + "settings.pause_on_unfocused": "Pause streaming when tab is not focused", + "settings.presets": "Presets", + "settings.profile_background": "Profile Background", + "settings.profile_banner": "Profile Banner", + "settings.profile_tab": "Profile", + "settings.radii_help": "Set up interface edge rounding (in pixels)", + "settings.replies_in_timeline": "Replies in timeline", + "settings.reply_visibility_all": "Show all replies", + "settings.reply_visibility_following": "Only show replies directed at me or users I'm following", + "settings.reply_visibility_self": "Only show replies directed at me", + "settings.saving_err": "Error saving settings", + "settings.saving_ok": "Settings saved", + "settings.security_tab": "Security", + "settings.scope_copy": "Copy scope when replying (DMs are always copied)", + "settings.set_new_avatar": "Set new avatar", + "settings.set_new_profile_background": "Set new profile background", + "settings.set_new_profile_banner": "Set new profile banner", + "settings.settings": "Settings", + "settings.subject_input_always_show": "Always show subject field", + "settings.subject_line_behavior": "Copy subject when replying", + "settings.subject_line_email": "Like email: \"re: subject\"", + "settings.subject_line_mastodon": "Like mastodon: copy as is", + "settings.subject_line_noop": "Do not copy", + "settings.post_status_content_type": "Post status content type", + "settings.stop_gifs": "Play-on-hover GIFs", + "settings.streaming": "Enable automatic streaming of new posts when scrolled to the top", + "settings.text": "Text", + "settings.theme": "Theme", + "settings.theme_help": "Use hex color codes (#rrggbb) to customize your color theme.", + "settings.theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.", + "settings.theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.", + "settings.tooltipRadius": "Tooltips/alerts", + "settings.upload_a_photo": "Upload a photo", + "settings.user_settings": "User Settings", + "settings.values.false": "no", + "settings.values.true": "yes", + "settings.notifications": "Notifications", + "settings.enable_web_push_notifications": "Enable web push notifications", + "settings.style.switcher.keep_color": "Keep colors", + "settings.style.switcher.keep_shadows": "Keep shadows", + "settings.style.switcher.keep_opacity": "Keep opacity", + "settings.style.switcher.keep_roundness": "Keep roundness", + "settings.style.switcher.keep_fonts": "Keep fonts", + "settings.style.switcher.save_load_hint": "\"Keep\" options preserve currently set options when selecting or loading themes, it also stores said options when exporting a theme. When all checkboxes unset, exporting theme will save everything.", + "settings.style.switcher.reset": "Reset", + "settings.style.switcher.clear_all": "Clear all", + "settings.style.switcher.clear_opacity": "Clear opacity", + "settings.style.common.color": "Color", + "settings.style.common.opacity": "Opacity", + "settings.style.common.contrast.hint": "Contrast ratio is {ratio}, it {level} {context}", + "settings.style.common.contrast.level.aa": "meets Level AA guideline (minimal)", + "settings.style.common.contrast.level.aaa": "meets Level AAA guideline (recommended)", + "settings.style.common.contrast.level.bad": "doesn't meet any accessibility guidelines", + "settings.style.common.contrast.context.18pt": "for large (18pt+) text", + "settings.style.common.contrast.context.text": "for text", + "settings.style.common_colors._tab_label": "Common", + "settings.style.common_colors.main": "Common colors", + "settings.style.common_colors.foreground_hint": "See \"Advanced\" tab for more detailed control", + "settings.style.common_colors.rgbo": "Icons, accents, badges", + "settings.style.advanced_colors._tab_label": "Advanced", + "settings.style.advanced_colors.alert": "Alert background", + "settings.style.advanced_colors.alert_error": "Error", + "settings.style.advanced_colors.badge": "Badge background", + "settings.style.advanced_colors.badge_notification": "Notification", + "settings.style.advanced_colors.panel_header": "Panel header", + "settings.style.advanced_colors.top_bar": "Top bar", + "settings.style.advanced_colors.borders": "Borders", + "settings.style.advanced_colors.buttons": "Buttons", + "settings.style.advanced_colors.inputs": "Input fields", + "settings.style.advanced_colors.faint_text": "Faded text", + "settings.style.radii._tab_label": "Roundness", + "settings.style.shadows._tab_label": "Shadow and lighting", + "settings.style.shadows.component": "Component", + "settings.style.shadows.override": "Override", + "settings.style.shadows.shadow_id": "Shadow #{value}", + "settings.style.shadows.blur": "Blur", + "settings.style.shadows.spread": "Spread", + "settings.style.shadows.inset": "Inset", + "settings.style.shadows.hint": "For shadows you can also use --variable as a color value to use CSS3 variables. Please note that setting opacity won't work in this case.", + "settings.style.shadows.filter_hint.always_drop_shadow": "Warning, this shadow always uses {0} when browser supports it.", + "settings.style.shadows.filter_hint.drop_shadow_syntax": "{0} does not support {1} parameter and {2} keyword.", + "settings.style.shadows.filter_hint.avatar_inset": "Please note that combining both inset and non-inset shadows on avatars might give unexpected results with transparent avatars.", + "settings.style.shadows.filter_hint.spread_zero": "Shadows with spread > 0 will appear as if it was set to zero", + "settings.style.shadows.filter_hint.inset_classic": "Inset shadows will be using {0}", + "settings.style.shadows.components.panel": "Panel", + "settings.style.shadows.components.panelHeader": "Panel header", + "settings.style.shadows.components.topBar": "Top bar", + "settings.style.shadows.components.avatar": "User avatar (in profile view)", + "settings.style.shadows.components.avatarStatus": "User avatar (in post display)", + "settings.style.shadows.components.popup": "Popups and tooltips", + "settings.style.shadows.components.button": "Button", + "settings.style.shadows.components.buttonHover": "Button (hover)", + "settings.style.shadows.components.buttonPressed": "Button (pressed)", + "settings.style.shadows.components.buttonPressedHover": "Button (pressed+hover)", + "settings.style.shadows.components.input": "Input field", + "settings.style.fonts._tab_label": "Fonts", + "settings.style.fonts.help": "Select font to use for elements of UI. For \"custom\" you have to enter exact font name as it appears in system.", + "settings.style.fonts.components.interface": "Interface", + "settings.style.fonts.components.input": "Input fields", + "settings.style.fonts.components.post": "Post text", + "settings.style.fonts.components.postCode": "Monospaced text in a post (rich text)", + "settings.style.fonts.family": "Font name", + "settings.style.fonts.size": "Size (in px)", + "settings.style.fonts.weight": "Weight (boldness)", + "settings.style.fonts.custom": "Custom", + "settings.style.preview.header": "Preview", + "settings.style.preview.content": "Content", + "settings.style.preview.error": "Example error", + "settings.style.preview.button": "Button", + "settings.style.preview.text": "A bunch of more {0} and {1}", + "settings.style.preview.mono": "content", + "settings.style.preview.input": "Just landed in L.A.", + "settings.style.preview.faint_link": "helpful manual", + "settings.style.preview.fine_print": "Read our {0} to learn nothing useful!", + "settings.style.preview.header_faint": "This is fine", + "settings.style.preview.checkbox": "I have skimmed over terms and conditions", + "settings.style.preview.link": "a nice lil' link", + "settings.version.title": "Version", + "settings.version.backend_version": "Backend Version", + "settings.version.frontend_version": "Frontend Version", + "timeline.collapse": "Collapse", + "timeline.conversation": "Conversation", + "timeline.error_fetching": "Error fetching updates", + "timeline.load_older": "Load older statuses", + "timeline.no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated", + "timeline.repeated": "repeated", + "timeline.show_new": "Show new", + "timeline.up_to_date": "Up-to-date", + "timeline.no_more_statuses": "No more statuses", + "timeline.no_statuses": "No statuses", + "status.reply_to": "Reply to", + "status.replies_list": "Replies:", + "user_card.approve": "Approve", + "user_card.block": "Block", + "user_card.blocked": "Blocked!", + "user_card.deny": "Deny", + "user_card.favorites": "Favorites", + "user_card.follow": "Follow", + "user_card.follow_sent": "Request sent!", + "user_card.follow_progress": "Requesting…", + "user_card.follow_again": "Send request again?", + "user_card.follow_unfollow": "Unfollow", + "user_card.followees": "Following", + "user_card.followers": "Followers", + "user_card.following": "Following!", + "user_card.follows_you": "Follows you!", + "user_card.its_you": "It's you!", + "user_card.media": "Media", + "user_card.mute": "Mute", + "user_card.muted": "Muted", + "user_card.per_day": "per day", + "user_card.remote_follow": "Remote follow", + "user_card.statuses": "Statuses", + "user_card.unblock": "Unblock", + "user_card.unblock_progress": "Unblocking...", + "user_card.block_progress": "Blocking...", + "user_card.unmute": "Unmute", + "user_card.unmute_progress": "Unmuting...", + "user_card.mute_progress": "Muting...", + "user_profile.timeline_title": "User Timeline", + "user_profile.profile_does_not_exist": "Sorry, this profile does not exist.", + "user_profile.profile_loading_error": "Sorry, there was an error loading this profile.", + "who_to_follow.more": "More", + "who_to_follow.who_to_follow": "Who to follow", + "tool_tip.media_upload": "Upload Media", + "tool_tip.repeat": "Repeat", + "tool_tip.reply": "Reply", + "tool_tip.favorite": "Favorite", + "tool_tip.user_settings": "User Settings", + "upload.error.base": "Upload failed.", + "upload.error.file_too_big": "File too big [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "upload.error.default": "Try again later", + "upload.file_size_units.B": "B", + "upload.file_size_units.KiB": "KiB", + "upload.file_size_units.MiB": "MiB", + "upload.file_size_units.GiB": "GiB", + "upload.file_size_units.TiB": "TiB" } diff --git a/src/i18n/zh.json b/src/i18n/zh.json @@ -25,13 +25,14 @@ "more": "更多", "generic_error": "发生一个错误", "optional": "可选项", - "show_more": "显示更多", - "show_less": "显示更少", + "show_more": "展开", + "show_less": "收起", "cancel": "取消", "disable": "禁用", "enable": "启用", "confirm": "确认", - "verify": "验证" + "verify": "验证", + "dismiss": "忽略" }, "image_cropper": { "crop_picture": "裁剪图片", @@ -57,9 +58,9 @@ "enter_recovery_code": "输入一个恢复码", "enter_two_factor_code": "输入一个双重因素验证码", "recovery_code": "恢复码", - "heading" : { - "totp" : "双重因素验证", - "recovery" : "双重因素恢复" + "heading": { + "totp": "双重因素验证", + "recovery": "双重因素恢复" } }, "media_modal": { @@ -68,8 +69,8 @@ }, "nav": { "about": "关于", - "back": "Back", - "chat": "本地聊天", + "back": "后退", + "chat": "本站聊天", "friend_requests": "关注请求", "mentions": "提及", "interactions": "互动", @@ -80,17 +81,21 @@ "user_search": "用户搜索", "search": "搜索", "who_to_follow": "推荐关注", - "preferences": "偏好设置" + "preferences": "偏好设置", + "administration": "管理员" }, "notifications": { - "broken_favorite": "未知的状态,正在搜索中...", + "broken_favorite": "未知的状态,正在搜索中…", "favorited_you": "收藏了你的状态", "followed_you": "关注了你", "load_older": "加载更早的通知", "notifications": "通知", "read": "阅读!", "repeated_you": "转发了你的状态", - "no_more_notifications": "没有更多的通知" + "no_more_notifications": "没有更多的通知", + "reacted_with": "和 {0} 互动过", + "migrated_to": "迁移到", + "follow_request": "想要关注你" }, "polls": { "add_poll": "增加问卷调查", @@ -112,7 +117,8 @@ "interactions": { "favs_repeats": "转发和收藏", "follows": "新的关注者", - "load_older": "加载更早的互动" + "load_older": "加载更早的互动", + "moves": "用户迁移" }, "post_status": { "new_status": "发布新状态", @@ -151,9 +157,9 @@ "token": "邀请码", "captcha": "CAPTCHA", "new_captcha": "点击图片获取新的验证码", - "username_placeholder": "例如: lain", - "fullname_placeholder": "例如: Lain Iwakura", - "bio_placeholder": "例如:\n你好, 我是 Lain.\n我是一个住在上海的宅男。你可能在某处见过我。", + "username_placeholder": "例如:lain", + "fullname_placeholder": "例如:岩仓玲音", + "bio_placeholder": "例如:\n你好,我是玲音。\n我是一个住在日本郊区的动画少女。你可能在 Wired 见过我。", "validations": { "username_required": "不能留空", "fullname_required": "不能留空", @@ -171,17 +177,17 @@ "security": "安全", "enter_current_password_to_confirm": "输入你当前密码来确认你的身份", "mfa": { - "otp" : "OTP", - "setup_otp" : "设置 OTP", - "wait_pre_setup_otp" : "预设 OTP", - "confirm_and_enable" : "确认并启用 OTP", + "otp": "OTP", + "setup_otp": "设置 OTP", + "wait_pre_setup_otp": "预设 OTP", + "confirm_and_enable": "确认并启用 OTP", "title": "双因素验证", - "generate_new_recovery_codes" : "生成新的恢复码", - "warning_of_generate_new_codes" : "当你生成新的恢复码时,你的就恢复码就失效了。", - "recovery_codes" : "恢复码。", - "waiting_a_recovery_codes": "接受备份码。。。", - "recovery_codes_warning" : "抄写这些号码,或者保存在安全的地方。这些号码不会再次显示。如果你无法访问你的 2FA app,也丢失了你的恢复码,你的账号就再也无法登录了。", - "authentication_methods" : "身份验证方法", + "generate_new_recovery_codes": "生成新的恢复码", + "warning_of_generate_new_codes": "当你生成新的恢复码时,你的旧恢复码就失效了。", + "recovery_codes": "恢复码。", + "waiting_a_recovery_codes": "正在接收备份码…", + "recovery_codes_warning": "抄写这些号码,或者保存在安全的地方。这些号码不会再次显示。如果你无法访问你的 2FA app,也丢失了你的恢复码,你的账号就再也无法登录了。", + "authentication_methods": "身份验证方法", "scan": { "title": "扫一下", "desc": "使用你的双因素验证 app,扫描这个二维码,或者输入这些文字密钥:", @@ -193,7 +199,6 @@ }, "attachmentRadius": "附件", "attachments": "附件", - "autoload": "启用滚动到底部时的自动加载", "avatar": "头像", "avatarAltRadius": "头像(通知)", "avatarRadius": "头像", @@ -222,7 +227,7 @@ "data_import_export_tab": "数据导入/导出", "default_vis": "默认可见范围", "delete_account": "删除账户", - "delete_account_description": "永久删除你的帐号和所有消息。", + "delete_account_description": "永久删除你的帐号和所有数据。", "delete_account_error": "删除账户时发生错误,如果一直删除不了,请联系实例管理员。", "delete_account_instructions": "在下面输入你的密码来确认删除账户", "avatar_size_instruction": "推荐的头像图片最小的尺寸是 150x150 像素。", @@ -263,7 +268,7 @@ "loop_video_silent_only": "只循环没有声音的视频(例如:Mastodon 里的“GIF”)", "mutes_tab": "隐藏", "play_videos_in_modal": "在弹出框内播放视频", - "use_contain_fit": "生成缩略图时不要裁剪附件。", + "use_contain_fit": "生成缩略图时不要裁剪附件", "name": "名字", "name_bio": "名字及简介", "new_password": "新密码", @@ -293,7 +298,6 @@ "profile_tab": "个人资料", "radii_help": "设置界面边缘的圆角 (单位:像素)", "replies_in_timeline": "时间线中的回复", - "reply_link_preview": "启用鼠标悬停时预览回复链接", "reply_visibility_all": "显示所有回复", "reply_visibility_following": "只显示发送给我的回复/发送给我关注的用户的回复", "reply_visibility_self": "只显示发送给我的回复", @@ -330,11 +334,6 @@ "true": "是" }, "notifications": "通知", - "notification_setting": "通知来源:", - "notification_setting_follows": "你所关注的用户", - "notification_setting_non_follows": "你没有关注的用户", - "notification_setting_followers": "关注你的用户", - "notification_setting_non_followers": "没有关注你的用户", "notification_mutes": "要停止收到某个指定的用户的通知,请使用隐藏功能。", "notification_blocks": "拉黑一个用户会停掉所有他的通知,等同于取消关注。", "enable_web_push_notifications": "启用 web 推送通知", @@ -348,7 +347,14 @@ "save_load_hint": "\"保留\" 选项在选择或加载主题时保留当前设置的选项,在导出主题时还会存储上述选项。当所有复选框未设置时,导出主题将保存所有内容。", "reset": "重置", "clear_all": "清除全部", - "clear_opacity": "清除透明度" + "clear_opacity": "清除透明度", + "load_theme": "加载主题", + "help": { + "upgraded_from_v2": "PleromaFE 已升级,主题会和你记忆中的不太一样。" + }, + "use_source": "新版本", + "use_snapshot": "老版本", + "keep_as_is": "保持原状" }, "common": { "color": "颜色", @@ -441,7 +447,7 @@ "mono": "内容", "input": "刚刚抵达上海", "faint_link": "帮助菜单", - "fine_print": "阅读我们的 {0} 学不到什么东东!", + "fine_print": "阅读我们的 {0} ,然而什么也学不到!", "header_faint": "这很正常", "checkbox": "我已经浏览了 TOC", "link": "一个很棒的摇滚链接" @@ -451,7 +457,20 @@ "title": "版本", "backend_version": "后端版本", "frontend_version": "前端版本" - } + }, + "notification_setting_filters": "过滤器", + "domain_mutes": "域名", + "changed_email": "邮箱修改成功!", + "change_email_error": "修改你的电子邮箱时发生错误", + "change_email": "修改电子邮箱", + "allow_following_move": "正在关注的账号迁移时自动重新关注", + "notification_setting_privacy_option": "在通知推送中隐藏发送者和内容", + "notification_setting_privacy": "隐私", + "hide_follows_count_description": "不显示关注数", + "notification_visibility_emoji_reactions": "互动", + "notification_visibility_moves": "用户迁移", + "new_email": "新邮箱", + "emoji_reactions_on_timeline": "在时间线上显示表情符号互动" }, "time": { "day": "{0} 天", @@ -533,16 +552,16 @@ "muted": "已隐藏", "per_day": "每天", "remote_follow": "跨站关注", - "report": "报告", + "report": "报告", "statuses": "状态", "subscribe": "订阅", "unsubscribe": "退订", "unblock": "取消拉黑", - "unblock_progress": "取消拉黑中...", - "block_progress": "拉黑中...", + "unblock_progress": "取消拉黑中…", + "block_progress": "拉黑中…", "unmute": "取消隐藏", - "unmute_progress": "取消隐藏中...", - "mute_progress": "隐藏中...", + "unmute_progress": "取消隐藏中…", + "mute_progress": "隐藏中…", "admin_menu": { "moderation": "权限", "grant_admin": "赋予管理权限", @@ -561,7 +580,10 @@ "quarantine": "从联合实例中禁止用户帖子", "delete_user": "删除用户", "delete_user_confirmation": "你确认吗?此操作无法撤销。" - } + }, + "hidden": "已隐藏", + "show_repeats": "显示转发", + "hide_repeats": "隐藏转发" }, "user_profile": { "timeline_title": "用户时间线", @@ -586,9 +608,11 @@ "repeat": "转发", "reply": "回复", "favorite": "收藏", - "user_settings": "用户设置" + "user_settings": "用户设置", + "reject_follow_request": "拒绝关注请求", + "add_reaction": "添加互动" }, - "upload":{ + "upload": { "error": { "base": "上传不成功。", "file_too_big": "文件太大了 [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", @@ -605,8 +629,8 @@ "search": { "people": "人", "hashtags": "Hashtags", - "person_talking": "{count} 人谈论", - "people_talking": "{count} 人谈论", + "person_talking": "{count} 人正在讨论", + "people_talking": "{count} 人正在讨论", "no_results": "没有搜索结果" }, "password_reset": { @@ -619,5 +643,49 @@ "not_found": "我们无法找到匹配的邮箱地址或者用户名。", "too_many_requests": "你触发了尝试的限制,请稍后再试。", "password_reset_disabled": "密码重置已经被禁用。请联系你的实例管理员。" + }, + "remote_user_resolver": { + "error": "未找到。", + "searching_for": "搜索", + "remote_user_resolver": "远程用户解析器" + }, + "emoji": { + "keep_open": "选择器保持打开", + "stickers": "贴图", + "unicode": "Unicode 表情符号", + "custom": "自定义表情符号", + "add_emoji": "插入表情符号", + "search_emoji": "搜索表情符号", + "emoji": "表情符号" + }, + "about": { + "mrf": { + "simple": { + "quarantine_desc": "本实例只会把公开状态发送非下列实例:", + "quarantine": "隔离", + "reject_desc": "本实例不会接收来自下列实例的消息:", + "reject": "拒绝", + "accept_desc": "本实例只接收来自下列实例的消息:", + "simple_policies": "站规", + "accept": "接受", + "media_removal": "移除媒体" + }, + "mrf_policies_desc": "MRF 策略会影响本实例的互通行为。以下策略已启用:", + "mrf_policies": "已启动 MRF 策略", + "keyword": { + "ftl_removal": "从“全部已知网络”时间线上移除", + "keyword_policies": "关键词策略", + "is_replaced_by": "→", + "replace": "替换", + "reject": "拒绝" + }, + "federation": "联邦" + } + }, + "domain_mute_card": { + "unmute_progress": "正在取消隐藏…", + "unmute": "取消隐藏", + "mute_progress": "隐藏中…", + "mute": "隐藏" } } diff --git a/src/lib/notification-i18n-loader.js b/src/lib/notification-i18n-loader.js @@ -0,0 +1,12 @@ +// This somewhat mysterious module will load a json string +// and then extract only the 'notifications' part. This is +// meant to be used to load the partial i18n we need for +// the service worker. +module.exports = function (source) { + var object = JSON.parse(source) + var smol = { + notifications: object.notifications || {} + } + + return JSON.stringify(smol) +} diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js @@ -1,13 +1,12 @@ import merge from 'lodash.merge' -import objectPath from 'object-path' import localforage from 'localforage' -import { each } from 'lodash' +import { each, get, set } from 'lodash' let loaded = false const defaultReducer = (state, paths) => ( paths.length === 0 ? state : paths.reduce((substate, path) => { - objectPath.set(substate, path, objectPath.get(state, path)) + set(substate, path, get(state, path)) return substate }, {}) ) diff --git a/src/main.js b/src/main.js @@ -19,6 +19,7 @@ import oauthTokensModule from './modules/oauth_tokens.js' import reportsModule from './modules/reports.js' import pollsModule from './modules/polls.js' import postStatusModule from './modules/postStatus.js' +import chatsModule from './modules/chats.js' import VueI18n from 'vue-i18n' @@ -46,11 +47,13 @@ Vue.use(VBodyScrollLock) const i18n = new VueI18n({ // By default, use the browser locale, we will update it if neccessary - locale: currentLocale, + locale: 'en', fallbackLocale: 'en', - messages + messages: messages.default }) +messages.setLanguage(i18n, currentLocale) + const persistedStateOptions = { paths: [ 'config', @@ -60,7 +63,15 @@ const persistedStateOptions = { }; (async () => { - const persistedState = await createPersistedState(persistedStateOptions) + let storageError = false + const plugins = [pushNotifications] + try { + const persistedState = await createPersistedState(persistedStateOptions) + plugins.push(persistedState) + } catch (e) { + console.error(e) + storageError = true + } const store = new Vuex.Store({ modules: { i18n: { @@ -81,13 +92,16 @@ const persistedStateOptions = { oauthTokens: oauthTokensModule, reports: reportsModule, polls: pollsModule, - postStatus: postStatusModule + postStatus: postStatusModule, + chats: chatsModule }, - plugins: [persistedState, pushNotifications], + plugins, strict: false // Socket modifies itself, let's ignore this for now. // strict: process.env.NODE_ENV !== 'production' }) - + if (storageError) { + store.dispatch('pushGlobalNotice', { messageKey: 'errors.storage_unavailable', level: 'error' }) + } afterStoreSetup({ store, i18n }) })() diff --git a/src/modules/api.js b/src/modules/api.js @@ -1,4 +1,6 @@ import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' +import { WSConnectionStatus } from '../services/api/api.service.js' +import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js' import { Socket } from 'phoenix' const api = { @@ -7,6 +9,7 @@ const api = { fetchers: {}, socket: null, mastoUserSocket: null, + mastoUserSocketStatus: null, followRequests: [] }, mutations: { @@ -28,6 +31,9 @@ const api = { }, setFollowRequests (state, value) { state.followRequests = value + }, + setMastoUserSocketStatus (state, value) { + state.mastoUserSocketStatus = value } }, actions: { @@ -47,7 +53,7 @@ const api = { startMastoUserSocket (store) { return new Promise((resolve, reject) => { try { - const { state, dispatch, rootState } = store + const { state, commit, dispatch, rootState } = store const timelineData = rootState.statuses.timelines.friends state.mastoUserSocket = state.backendInteractor.startUserSocket({ store }) state.mastoUserSocket.addEventListener( @@ -66,11 +72,23 @@ const api = { showImmediately: timelineData.visibleStatuses.length === 0, timeline: 'friends' }) + } else if (message.event === 'pleroma:chat_update') { + dispatch('addChatMessages', { + chatId: message.chatUpdate.id, + messages: [message.chatUpdate.lastMessage] + }) + dispatch('updateChat', { chat: message.chatUpdate }) + maybeShowChatNotification(store, message.chatUpdate) } } ) + state.mastoUserSocket.addEventListener('open', () => { + commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED) + }) state.mastoUserSocket.addEventListener('error', ({ detail: error }) => { console.error('Error in MastoAPI websocket:', error) + commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR) + dispatch('clearOpenedChats') }) state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => { const ignoreCodes = new Set([ @@ -84,8 +102,11 @@ const api = { console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`) dispatch('startFetchingTimeline', { timeline: 'friends' }) dispatch('startFetchingNotifications') + dispatch('startFetchingChats') dispatch('restartMastoUserSocket') } + commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED) + dispatch('clearOpenedChats') }) resolve() } catch (e) { @@ -99,12 +120,13 @@ const api = { return dispatch('startMastoUserSocket').then(() => { dispatch('stopFetchingTimeline', { timeline: 'friends' }) dispatch('stopFetchingNotifications') + dispatch('stopFetchingChats') }) }, stopMastoUserSocket ({ state, dispatch }) { dispatch('startFetchingTimeline', { timeline: 'friends' }) dispatch('startFetchingNotifications') - console.log(state.mastoUserSocket) + dispatch('startFetchingChats') state.mastoUserSocket.close() }, @@ -138,9 +160,6 @@ const api = { if (!fetcher) return store.commit('removeFetcher', { fetcherName: 'notifications', fetcher }) }, - fetchAndUpdateNotifications (store) { - store.state.backendInteractor.fetchAndUpdateNotifications({ store }) - }, // Follow requests startFetchingFollowRequests (store) { diff --git a/src/modules/chats.js b/src/modules/chats.js @@ -0,0 +1,234 @@ +import Vue from 'vue' +import { find, omitBy, orderBy, sumBy } from 'lodash' +import chatService from '../services/chat_service/chat_service.js' +import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js' +import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js' + +const emptyChatList = () => ({ + data: [], + idStore: {} +}) + +const defaultState = { + chatList: emptyChatList(), + chatListFetcher: null, + openedChats: {}, + openedChatMessageServices: {}, + fetcher: undefined, + currentChatId: null +} + +const getChatById = (state, id) => { + return find(state.chatList.data, { id }) +} + +const sortedChatList = (state) => { + return orderBy(state.chatList.data, ['updated_at'], ['desc']) +} + +const unreadChatCount = (state) => { + return sumBy(state.chatList.data, 'unread') +} + +const chats = { + state: { ...defaultState }, + getters: { + currentChat: state => state.openedChats[state.currentChatId], + currentChatMessageService: state => state.openedChatMessageServices[state.currentChatId], + findOpenedChatByRecipientId: state => recipientId => find(state.openedChats, c => c.account.id === recipientId), + sortedChatList, + unreadChatCount + }, + actions: { + // Chat list + startFetchingChats ({ dispatch, commit }) { + const fetcher = () => { + dispatch('fetchChats', { latest: true }) + } + fetcher() + commit('setChatListFetcher', { + fetcher: () => setInterval(() => { fetcher() }, 5000) + }) + }, + stopFetchingChats ({ commit }) { + commit('setChatListFetcher', { fetcher: undefined }) + }, + fetchChats ({ dispatch, rootState, commit }, params = {}) { + return rootState.api.backendInteractor.chats() + .then(({ chats }) => { + dispatch('addNewChats', { chats }) + return chats + }) + }, + addNewChats (store, { chats }) { + const { commit, dispatch, rootGetters } = store + const newChatMessageSideEffects = (chat) => { + maybeShowChatNotification(store, chat) + } + commit('addNewChats', { dispatch, chats, rootGetters, newChatMessageSideEffects }) + }, + updateChat ({ commit }, { chat }) { + commit('updateChat', { chat }) + }, + + // Opened Chats + startFetchingCurrentChat ({ commit, dispatch }, { fetcher }) { + dispatch('setCurrentChatFetcher', { fetcher }) + }, + setCurrentChatFetcher ({ rootState, commit }, { fetcher }) { + commit('setCurrentChatFetcher', { fetcher }) + }, + addOpenedChat ({ rootState, commit, dispatch }, { chat }) { + commit('addOpenedChat', { dispatch, chat: parseChat(chat) }) + dispatch('addNewUsers', [chat.account]) + }, + addChatMessages ({ commit }, value) { + commit('addChatMessages', { commit, ...value }) + }, + resetChatNewMessageCount ({ commit }, value) { + commit('resetChatNewMessageCount', value) + }, + clearCurrentChat ({ rootState, commit, dispatch }, value) { + commit('setCurrentChatId', { chatId: undefined }) + commit('setCurrentChatFetcher', { fetcher: undefined }) + }, + readChat ({ rootState, commit, dispatch }, { id, lastReadId }) { + dispatch('resetChatNewMessageCount') + commit('readChat', { id }) + rootState.api.backendInteractor.readChat({ id, lastReadId }) + }, + deleteChatMessage ({ rootState, commit }, value) { + rootState.api.backendInteractor.deleteChatMessage(value) + commit('deleteChatMessage', { commit, ...value }) + }, + resetChats ({ commit, dispatch }) { + dispatch('clearCurrentChat') + commit('resetChats', { commit }) + }, + clearOpenedChats ({ rootState, commit, dispatch, rootGetters }) { + commit('clearOpenedChats', { commit }) + } + }, + mutations: { + setChatListFetcher (state, { commit, fetcher }) { + const prevFetcher = state.chatListFetcher + if (prevFetcher) { + clearInterval(prevFetcher) + } + state.chatListFetcher = fetcher && fetcher() + }, + setCurrentChatFetcher (state, { fetcher }) { + const prevFetcher = state.fetcher + if (prevFetcher) { + clearInterval(prevFetcher) + } + state.fetcher = fetcher && fetcher() + }, + addOpenedChat (state, { _dispatch, chat }) { + state.currentChatId = chat.id + Vue.set(state.openedChats, chat.id, chat) + + if (!state.openedChatMessageServices[chat.id]) { + Vue.set(state.openedChatMessageServices, chat.id, chatService.empty(chat.id)) + } + }, + setCurrentChatId (state, { chatId }) { + state.currentChatId = chatId + }, + addNewChats (state, { chats, newChatMessageSideEffects }) { + chats.forEach((updatedChat) => { + const chat = getChatById(state, updatedChat.id) + + if (chat) { + const isNewMessage = (chat.lastMessage && chat.lastMessage.id) !== (updatedChat.lastMessage && updatedChat.lastMessage.id) + chat.lastMessage = updatedChat.lastMessage + chat.unread = updatedChat.unread + if (isNewMessage && chat.unread) { + newChatMessageSideEffects(updatedChat) + } + } else { + state.chatList.data.push(updatedChat) + Vue.set(state.chatList.idStore, updatedChat.id, updatedChat) + } + }) + }, + updateChat (state, { _dispatch, chat: updatedChat, _rootGetters }) { + const chat = getChatById(state, updatedChat.id) + if (chat) { + chat.lastMessage = updatedChat.lastMessage + chat.unread = updatedChat.unread + chat.updated_at = updatedChat.updated_at + } + if (!chat) { state.chatList.data.unshift(updatedChat) } + Vue.set(state.chatList.idStore, updatedChat.id, updatedChat) + }, + deleteChat (state, { _dispatch, id, _rootGetters }) { + state.chats.data = state.chats.data.filter(conversation => + conversation.last_status.id !== id + ) + state.chats.idStore = omitBy(state.chats.idStore, conversation => conversation.last_status.id === id) + }, + resetChats (state, { commit }) { + state.chatList = emptyChatList() + state.currentChatId = null + commit('setChatListFetcher', { fetcher: undefined }) + for (const chatId in state.openedChats) { + chatService.clear(state.openedChatMessageServices[chatId]) + Vue.delete(state.openedChats, chatId) + Vue.delete(state.openedChatMessageServices, chatId) + } + }, + setChatsLoading (state, { value }) { + state.chats.loading = value + }, + addChatMessages (state, { commit, chatId, messages }) { + const chatMessageService = state.openedChatMessageServices[chatId] + if (chatMessageService) { + chatService.add(chatMessageService, { messages: messages.map(parseChatMessage) }) + commit('refreshLastMessage', { chatId }) + } + }, + refreshLastMessage (state, { chatId }) { + const chatMessageService = state.openedChatMessageServices[chatId] + if (chatMessageService) { + const chat = getChatById(state, chatId) + if (chat) { + chat.lastMessage = chatMessageService.lastMessage + if (chatMessageService.lastMessage) { + chat.updated_at = chatMessageService.lastMessage.created_at + } + } + } + }, + deleteChatMessage (state, { commit, chatId, messageId }) { + const chatMessageService = state.openedChatMessageServices[chatId] + if (chatMessageService) { + chatService.deleteMessage(chatMessageService, messageId) + commit('refreshLastMessage', { chatId }) + } + }, + resetChatNewMessageCount (state, _value) { + const chatMessageService = state.openedChatMessageServices[state.currentChatId] + chatService.resetNewMessageCount(chatMessageService) + }, + // Used when a connection loss occurs + clearOpenedChats (state) { + const currentChatId = state.currentChatId + for (const chatId in state.openedChats) { + if (currentChatId !== chatId) { + chatService.clear(state.openedChatMessageServices[chatId]) + Vue.delete(state.openedChats, chatId) + Vue.delete(state.openedChatMessageServices, chatId) + } + } + }, + readChat (state, { id }) { + const chat = getChatById(state, id) + if (chat) { + chat.unread = 0 + } + } + } +} + +export default chats diff --git a/src/modules/config.js b/src/modules/config.js @@ -1,8 +1,19 @@ import { set, delete as del } from 'vue' import { setPreset, applyTheme } from '../services/style_setter/style_setter.js' +import messages from '../i18n/messages' const browserLocale = (window.navigator.language || 'en').split('-')[0] +/* TODO this is a bit messy. + * We need to declare settings with their types and also deal with + * instance-default settings in some way, hopefully try to avoid copy-pasta + * in general. + */ +export const multiChoiceProperties = [ + 'postContentType', + 'subjectLineBehavior' +] + export const defaultState = { colors: {}, theme: undefined, @@ -20,9 +31,7 @@ export const defaultState = { preloadImage: true, loopVideo: true, loopVideoSilentOnly: true, - autoLoad: true, streaming: false, - hoverPreview: true, emojiReactionsOnTimeline: true, autohideFloatingPostButton: false, pauseOnUnfocused: true, @@ -35,7 +44,8 @@ export const defaultState = { repeats: true, moves: true, emojiReactions: false, - followRequest: true + followRequest: true, + chatMention: true }, webPushNotifications: false, muteWords: [], @@ -105,6 +115,10 @@ const config = { case 'customTheme': case 'customThemeSource': applyTheme(value) + break + case 'interfaceLanguage': + messages.setLanguage(this.getters.i18n, value) + break } } } diff --git a/src/modules/instance.js b/src/modules/instance.js @@ -1,55 +1,63 @@ import { set } from 'vue' import { getPreset, applyTheme } from '../services/style_setter/style_setter.js' import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' +import apiService from '../services/api/api.service.js' import { instanceDefaultProperties } from './config.js' const defaultState = { - // Stuff from static/config.json and apiConfig + // Stuff from apiConfig name: 'Pleroma FE', registrationOpen: true, - safeDM: true, - textlimit: 5000, server: 'http://localhost:4040/', - theme: 'pleroma-dark', + textlimit: 5000, themeData: undefined, - background: '/static/aurora_borealis.jpg', - logo: '/static/logo.png', - logoMask: true, - logoMargin: '.2em', - redirectRootNoLogin: '/main/all', - redirectRootLogin: '/main/friends', - showInstanceSpecificPanel: false, + vapidPublicKey: undefined, + + // Stuff from static/config.json alwaysShowSubjectInput: true, - hideMutedPosts: false, + defaultAvatar: '/images/avi.png', + defaultBanner: '/images/banner.png', + background: '/static/aurora_borealis.jpg', collapseMessageWithSubject: false, - hidePostStats: false, - hideUserStats: false, - hideFilteredStatuses: false, disableChat: false, - scopeCopy: true, - subjectLineBehavior: 'email', - postContentType: 'text/plain', + greentext: false, + hideFilteredStatuses: false, + hideMutedPosts: false, + hidePostStats: false, hideSitename: false, + hideUserStats: false, + loginMethod: 'password', + logo: '/static/logo.png', + logoMargin: '.2em', + logoMask: true, + minimalScopesMode: false, nsfwCensorImage: undefined, - vapidPublicKey: undefined, - noAttachmentLinks: false, + postContentType: 'text/plain', + redirectRootLogin: '/main/friends', + redirectRootNoLogin: '/main/all', + scopeCopy: true, showFeaturesPanel: true, - minimalScopesMode: false, - greentext: false, + showInstanceSpecificPanel: false, + sidebarRight: false, + subjectLineBehavior: 'email', + theme: 'pleroma-dark', // Nasty stuff - pleromaBackend: true, - emoji: [], - emojiFetched: false, customEmoji: [], customEmojiFetched: false, - restrictedNicknames: [], + emoji: [], + emojiFetched: false, + pleromaBackend: true, postFormats: [], + restrictedNicknames: [], + safeDM: true, + knownDomains: [], // Feature-set, apparently, not everything here is reported... - mediaProxyAvailable: false, chatAvailable: false, + pleromaChatMessagesAvailable: false, gopherAvailable: false, + mediaProxyAvailable: false, suggestionsEnabled: false, suggestionsWeb: '', @@ -77,6 +85,9 @@ const instance = { if (typeof value !== 'undefined') { set(state, name, value) } + }, + setKnownDomains (state, domains) { + state.knownDomains = domains } }, getters: { @@ -179,6 +190,18 @@ const instance = { state.emojiFetched = true dispatch('getStaticEmoji') } + }, + + async getKnownDomains ({ commit, rootState }) { + try { + const result = await apiService.fetchKnownDomains({ + credentials: rootState.users.currentUser.credentials + }) + commit('setKnownDomains', result) + } catch (e) { + console.warn("Can't load known domains") + console.warn(e) + } } } } diff --git a/src/modules/interface.js b/src/modules/interface.js @@ -1,6 +1,8 @@ import { set, delete as del } from 'vue' const defaultState = { + settingsModalState: 'hidden', + settingsModalLoaded: false, settings: { currentSaveStateNotice: null, noticeClearTimeout: null, @@ -12,7 +14,10 @@ const defaultState = { window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)') ) }, - mobileLayout: false + mobileLayout: false, + globalNotices: [], + layoutHeight: 0, + lastTimeline: null } const interfaceMod = { @@ -35,6 +40,39 @@ const interfaceMod = { }, setMobileLayout (state, value) { state.mobileLayout = value + }, + closeSettingsModal (state) { + state.settingsModalState = 'hidden' + }, + togglePeekSettingsModal (state) { + switch (state.settingsModalState) { + case 'minimized': + state.settingsModalState = 'visible' + return + case 'visible': + state.settingsModalState = 'minimized' + return + default: + throw new Error('Illegal minimization state of settings modal') + } + }, + openSettingsModal (state) { + state.settingsModalState = 'visible' + if (!state.settingsModalLoaded) { + state.settingsModalLoaded = true + } + }, + pushGlobalNotice (state, notice) { + state.globalNotices.push(notice) + }, + removeGlobalNotice (state, notice) { + state.globalNotices = state.globalNotices.filter(n => n !== notice) + }, + setLayoutHeight (state, value) { + state.layoutHeight = value + }, + setLastTimeline (state, value) { + state.lastTimeline = value } }, actions: { @@ -49,6 +87,43 @@ const interfaceMod = { }, setMobileLayout ({ commit }, value) { commit('setMobileLayout', value) + }, + closeSettingsModal ({ commit }) { + commit('closeSettingsModal') + }, + openSettingsModal ({ commit }) { + commit('openSettingsModal') + }, + togglePeekSettingsModal ({ commit }) { + commit('togglePeekSettingsModal') + }, + pushGlobalNotice ( + { commit, dispatch }, + { + messageKey, + messageArgs = {}, + level = 'error', + timeout = 0 + }) { + const notice = { + messageKey, + messageArgs, + level + } + if (timeout) { + setTimeout(() => dispatch('removeGlobalNotice', notice), timeout) + } + commit('pushGlobalNotice', notice) + return notice + }, + removeGlobalNotice ({ commit }, notice) { + commit('removeGlobalNotice', notice) + }, + setLayoutHeight ({ commit }, value) { + commit('setLayoutHeight', value) + }, + setLastTimeline ({ commit }, value) { + commit('setLastTimeline', value) } } } diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js @@ -22,7 +22,7 @@ const mediaViewer = { setMedia ({ commit }, attachments) { const media = attachments.filter(attachment => { const type = fileTypeService.fileType(attachment.mimetype) - return type === 'image' || type === 'video' + return type === 'image' || type === 'video' || type === 'audio' }) commit('setMedia', media) }, diff --git a/src/modules/statuses.js b/src/modules/statuses.js @@ -13,9 +13,8 @@ import { omitBy } from 'lodash' import { set } from 'vue' -import { isStatusNotification } from '../services/notification_utils/notification_utils.js' +import { isStatusNotification, maybeShowNotification } from '../services/notification_utils/notification_utils.js' import apiService from '../services/api/api.service.js' -// import parse from '../services/status_parser/status_parser.js' const emptyTl = (userId = 0) => ({ statuses: [], @@ -62,7 +61,8 @@ export const defaultState = () => ({ publicAndExternal: emptyTl(), friends: emptyTl(), tag: emptyTl(), - dms: emptyTl() + dms: emptyTl(), + bookmarks: emptyTl() } }) @@ -76,17 +76,6 @@ export const prepareStatus = (status) => { return status } -const visibleNotificationTypes = (rootState) => { - return [ - rootState.config.notificationVisibility.likes && 'like', - rootState.config.notificationVisibility.mentions && 'mention', - rootState.config.notificationVisibility.repeats && 'repeat', - rootState.config.notificationVisibility.follows && 'follow', - rootState.config.notificationVisibility.moves && 'move', - rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reactions' - ].filter(_ => _) -} - const mergeOrAdd = (arr, obj, item) => { const oldItem = obj[item.id] @@ -163,8 +152,7 @@ const removeStatusFromGlobalStorage = (state, status) => { } } -const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, - noIdUpdate = false, userId }) => { +const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId, pagination = {} }) => { // Sanity check if (!isArray(statuses)) { return false @@ -173,8 +161,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us const allStatuses = state.allStatuses const timelineObject = state.timelines[timeline] - const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0 - const minNew = statuses.length > 0 ? minBy(statuses, 'id').id : 0 + // Mismatch between API pagination and our internal minId/maxId tracking systems: + // pagination.maxId is the oldest of the returned statuses when fetching older, + // and pagination.minId is the newest when fetching newer. The names come directly + // from the arguments they're supposed to be passed as for the next fetch. + const minNew = pagination.maxId || (statuses.length > 0 ? minBy(statuses, 'id').id : 0) + const maxNew = pagination.minId || (statuses.length > 0 ? maxBy(statuses, 'id').id : 0) + const newer = timeline && (maxNew > timelineObject.maxId || timelineObject.maxId === 0) && statuses.length > 0 const older = timeline && (minNew < timelineObject.minId || timelineObject.minId === 0) && statuses.length > 0 @@ -315,12 +308,12 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us }) // Keep the visible statuses sorted - if (timeline) { + if (timeline && !(timeline === 'bookmarks')) { sortTimeline(timelineObject) } } -const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => { +const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters, newNotificationSideEffects }) => { each(notifications, (notification) => { if (isStatusNotification(notification.type)) { notification.action = addStatusToGlobalStorage(state, notification.action).item @@ -343,51 +336,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot state.notifications.data.push(notification) state.notifications.idStore[notification.id] = notification - if ('Notification' in window && window.Notification.permission === 'granted') { - const notifObj = {} - const status = notification.status - const title = notification.from_profile.name - notifObj.icon = notification.from_profile.profile_image_url - let i18nString - switch (notification.type) { - case 'like': - i18nString = 'favorited_you' - break - case 'repeat': - i18nString = 'repeated_you' - break - case 'follow': - i18nString = 'followed_you' - break - case 'move': - i18nString = 'migrated_to' - break - case 'follow_request': - i18nString = 'follow_request' - break - } - - if (notification.type === 'pleroma:emoji_reaction') { - notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji]) - } else if (i18nString) { - notifObj.body = rootGetters.i18n.t('notifications.' + i18nString) - } else if (isStatusNotification(notification.type)) { - notifObj.body = notification.status.text - } - - // Shows first attached non-nsfw image, if any. Should add configuration for this somehow... - if (status && status.attachments && status.attachments.length > 0 && !status.nsfw && - status.attachments[0].mimetype.startsWith('image/')) { - notifObj.image = status.attachments[0].url - } - - if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) { - let desktopNotification = new window.Notification(title, notifObj) - // Chrome is known for not closing notifications automatically - // according to MDN, anyway. - setTimeout(desktopNotification.close.bind(desktopNotification), 5000) - } - } + newNotificationSideEffects(notification) } else if (notification.seen) { state.notifications.idStore[notification.id].seen = true } @@ -487,9 +436,17 @@ export const mutations = { newStatus.rebloggedBy.push(user) } }, + setBookmarked (state, { status, value }) { + const newStatus = state.allStatusesObject[status.id] + newStatus.bookmarked = value + }, + setBookmarkedConfirm (state, { status }) { + const newStatus = state.allStatusesObject[status.id] + newStatus.bookmarked = status.bookmarked + }, setDeleted (state, { status }) { const newStatus = state.allStatusesObject[status.id] - newStatus.deleted = true + if (newStatus) newStatus.deleted = true }, setManyDeleted (state, condition) { Object.values(state.allStatusesObject).forEach(status => { @@ -532,6 +489,9 @@ export const mutations = { dismissNotification (state, { id }) { state.notifications.data = state.notifications.data.filter(n => n.id !== id) }, + dismissNotifications (state, { finder }) { + state.notifications.data = state.notifications.data.filter(n => finder) + }, updateNotification (state, { id, updater }) { const notification = find(state.notifications.data, n => n.id === id) notification && updater(notification) @@ -539,6 +499,11 @@ export const mutations = { queueFlush (state, { timeline, id }) { state.timelines[timeline].flushMarker = id }, + queueFlushAll (state) { + Object.keys(state.timelines).forEach((timeline) => { + state.timelines[timeline].flushMarker = state.timelines[timeline].maxId + }) + }, addRepeats (state, { id, rebloggedByUsers, currentUser }) { const newStatus = state.allStatusesObject[id] newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _) @@ -609,11 +574,16 @@ export const mutations = { const statuses = { state: defaultState(), actions: { - addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId }) { - commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId }) + addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) { + commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination }) }, - addNewNotifications ({ rootState, commit, dispatch, rootGetters }, { notifications, older }) { - commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters }) + addNewNotifications (store, { notifications, older }) { + const { commit, dispatch, rootGetters } = store + + const newNotificationSideEffects = (notification) => { + maybeShowNotification(store, notification) + } + commit('addNewNotifications', { dispatch, notifications, older, rootGetters, newNotificationSideEffects }) }, setError ({ rootState, commit }, { value }) { commit('setError', { value }) @@ -685,9 +655,26 @@ const statuses = { rootState.api.backendInteractor.unretweet({ id: status.id }) .then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser })) }, + bookmark ({ rootState, commit }, status) { + commit('setBookmarked', { status, value: true }) + rootState.api.backendInteractor.bookmarkStatus({ id: status.id }) + .then(status => { + commit('setBookmarkedConfirm', { status }) + }) + }, + unbookmark ({ rootState, commit }, status) { + commit('setBookmarked', { status, value: false }) + rootState.api.backendInteractor.unbookmarkStatus({ id: status.id }) + .then(status => { + commit('setBookmarkedConfirm', { status }) + }) + }, queueFlush ({ rootState, commit }, { timeline, id }) { commit('queueFlush', { timeline, id }) }, + queueFlushAll ({ rootState, commit }) { + commit('queueFlushAll') + }, markNotificationsAsSeen ({ rootState, commit }) { commit('markNotificationsAsSeen') apiService.markNotificationsAsSeen({ diff --git a/src/modules/users.js b/src/modules/users.js @@ -1,6 +1,6 @@ import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import oauthApi from '../services/new_api/oauth.js' -import { compact, map, each, merge, last, concat, uniq } from 'lodash' +import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash' import { set } from 'vue' import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js' @@ -10,7 +10,7 @@ export const mergeOrAdd = (arr, obj, item) => { const oldItem = obj[item.id] if (oldItem) { // We already have this, so only merge the new info. - merge(oldItem, item) + mergeWith(oldItem, item, mergeArrayLength) return { item: oldItem, new: false } } else { // This is a new item, prepare it @@ -23,6 +23,13 @@ export const mergeOrAdd = (arr, obj, item) => { } } +const mergeArrayLength = (oldValue, newValue) => { + if (isArray(oldValue) && isArray(newValue)) { + oldValue.length = newValue.length + return mergeWith(oldValue, newValue, mergeArrayLength) + } +} + const getNotificationPermission = () => { const Notification = window.Notification @@ -116,7 +123,7 @@ export const mutations = { }, setCurrentUser (state, user) { state.lastLoginName = user.screen_name - state.currentUser = merge(state.currentUser || {}, user) + state.currentUser = mergeWith(state.currentUser || {}, user, mergeArrayLength) }, clearCurrentUser (state) { state.currentUser = false @@ -259,6 +266,11 @@ const users = { mutations, getters, actions: { + fetchUserIfMissing (store, id) { + if (!store.getters.findUser(id)) { + store.dispatch('fetchUser', id) + } + }, fetchUser (store, id) { return store.rootState.api.backendInteractor.fetchUser({ id }) .then((user) => { @@ -428,10 +440,10 @@ const users = { store.commit('setUserForNotification', notification) }) }, - searchUsers (store, query) { - return store.rootState.api.backendInteractor.searchUsers({ query }) + searchUsers ({ rootState, commit }, { query }) { + return rootState.api.backendInteractor.searchUsers({ query }) .then((users) => { - store.commit('addNewUsers', users) + commit('addNewUsers', users) return users }) }, @@ -486,6 +498,8 @@ const users = { store.dispatch('stopFetchingFollowRequests') store.commit('clearNotifications') store.commit('resetStatuses') + store.dispatch('resetChats') + store.dispatch('setLastTimeline', 'public-timeline') }) }, loginUser (store, accessToken) { @@ -525,6 +539,9 @@ const users = { // Start fetching notifications store.dispatch('startFetchingNotifications') + + // Start fetching chats + store.dispatch('startFetchingChats') } if (store.getters.mergedConfig.useStreamingApi) { @@ -532,6 +549,7 @@ const users = { console.error('Failed initializing MastoAPI Streaming socket', error) startPolling() }).then(() => { + store.dispatch('fetchChats', { latest: true }) setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000) }) } else { diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js @@ -1,6 +1,5 @@ import { each, map, concat, last, get } from 'lodash' -import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js' -import 'whatwg-fetch' +import { parseStatus, parseUser, parseNotification, parseAttachment, parseChat, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js' import { RegistrationError, StatusCodeError } from '../errors/errors' /* eslint-env browser */ @@ -51,6 +50,7 @@ const MASTODON_USER_URL = '/api/v1/accounts' const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships' const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses` const MASTODON_TAG_TIMELINE_URL = tag => `/api/v1/timelines/tag/${tag}` +const MASTODON_BOOKMARK_TIMELINE_URL = '/api/v1/bookmarks' const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/' const MASTODON_USER_MUTES_URL = '/api/v1/mutes/' const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block` @@ -59,6 +59,8 @@ const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute` const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute` const MASTODON_SUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/subscribe` const MASTODON_UNSUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/unsubscribe` +const MASTODON_BOOKMARK_STATUS_URL = id => `/api/v1/statuses/${id}/bookmark` +const MASTODON_UNBOOKMARK_STATUS_URL = id => `/api/v1/statuses/${id}/unbookmark` const MASTODON_POST_STATUS_URL = '/api/v1/statuses' const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media' const MASTODON_VOTE_URL = id => `/api/v1/polls/${id}/votes` @@ -75,9 +77,15 @@ const MASTODON_SEARCH_2 = `/api/v2/search` const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search' const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks' const MASTODON_STREAMING = '/api/v1/streaming' +const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers' const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions` const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` +const PLEROMA_CHATS_URL = `/api/v1/pleroma/chats` +const PLEROMA_CHAT_URL = id => `/api/v1/pleroma/chats/by-account-id/${id}` +const PLEROMA_CHAT_MESSAGES_URL = id => `/api/v1/pleroma/chats/${id}/messages` +const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read` +const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}` const oldfetch = window.fetch @@ -138,20 +146,11 @@ const updateNotificationSettings = ({ credentials, settings }) => { }).then((data) => data.json()) } -const updateAvatar = ({ credentials, avatar }) => { +const updateProfileImages = ({ credentials, avatar = null, banner = null, background = null }) => { const form = new FormData() - form.append('avatar', avatar) - return fetch(MASTODON_PROFILE_UPDATE_URL, { - headers: authHeaders(credentials), - method: 'PATCH', - body: form - }).then((data) => data.json()) - .then((data) => parseUser(data)) -} - -const updateBg = ({ credentials, background }) => { - const form = new FormData() - form.append('pleroma_background_image', background) + if (avatar !== null) form.append('avatar', avatar) + if (banner !== null) form.append('header', banner) + if (background !== null) form.append('pleroma_background_image', background) return fetch(MASTODON_PROFILE_UPDATE_URL, { headers: authHeaders(credentials), method: 'PATCH', @@ -161,17 +160,6 @@ const updateBg = ({ credentials, background }) => { .then((data) => parseUser(data)) } -const updateBanner = ({ credentials, banner }) => { - const form = new FormData() - form.append('header', banner) - return fetch(MASTODON_PROFILE_UPDATE_URL, { - headers: authHeaders(credentials), - method: 'PATCH', - body: form - }).then((data) => data.json()) - .then((data) => parseUser(data)) -} - const updateProfile = ({ credentials, params }) => { return promisedRequest({ url: MASTODON_PROFILE_UPDATE_URL, @@ -324,7 +312,8 @@ const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) => { const args = [ maxId && `max_id=${maxId}`, sinceId && `since_id=${sinceId}`, - limit && `limit=${limit}` + limit && `limit=${limit}`, + `with_relationships=true` ].filter(_ => _).join('&') url = url + (args ? '?' + args : '') @@ -358,7 +347,8 @@ const fetchFollowers = ({ id, maxId, sinceId, limit = 20, credentials }) => { const args = [ maxId && `max_id=${maxId}`, sinceId && `since_id=${sinceId}`, - limit && `limit=${limit}` + limit && `limit=${limit}`, + `with_relationships=true` ].filter(_ => _).join('&') url += args ? '?' + args : '' @@ -497,7 +487,7 @@ const fetchTimeline = ({ userId = false, tag = false, withMuted = false, - withMove = false + replyVisibility = 'all' }) => { const timelineUrls = { public: MASTODON_PUBLIC_TIMELINE, @@ -508,7 +498,8 @@ const fetchTimeline = ({ user: MASTODON_USER_TIMELINE_URL, media: MASTODON_USER_TIMELINE_URL, favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, - tag: MASTODON_TAG_TIMELINE_URL + tag: MASTODON_TAG_TIMELINE_URL, + bookmarks: MASTODON_BOOKMARK_TIMELINE_URL } const isNotifications = timeline === 'notifications' const params = [] @@ -537,27 +528,33 @@ const fetchTimeline = ({ if (timeline === 'public' || timeline === 'publicAndExternal') { params.push(['only_media', false]) } - if (timeline === 'notifications') { - params.push(['with_move', withMove]) + if (timeline !== 'favorites' && timeline !== 'bookmarks') { + params.push(['with_muted', withMuted]) + } + if (replyVisibility !== 'all') { + params.push(['reply_visibility', replyVisibility]) } params.push(['limit', 20]) - params.push(['with_muted', withMuted]) const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&') url += `?${queryString}` let status = '' let statusText = '' + let pagination = {} return fetch(url, { headers: authHeaders(credentials) }) .then((data) => { status = data.status statusText = data.statusText + pagination = parseLinkHeaderPagination(data.headers.get('Link'), { + flakeId: timeline !== 'bookmarks' && timeline !== 'notifications' + }) return data }) .then((data) => data.json()) .then((data) => { if (!data.error) { - return data.map(isNotifications ? parseNotification : parseStatus) + return { data: data.map(isNotifications ? parseNotification : parseStatus), pagination } } else { data.status = status data.statusText = statusText @@ -608,6 +605,22 @@ const unretweet = ({ id, credentials }) => { .then((data) => parseStatus(data)) } +const bookmarkStatus = ({ id, credentials }) => { + return promisedRequest({ + url: MASTODON_BOOKMARK_STATUS_URL(id), + headers: authHeaders(credentials), + method: 'POST' + }) +} + +const unbookmarkStatus = ({ id, credentials }) => { + return promisedRequest({ + url: MASTODON_UNBOOKMARK_STATUS_URL(id), + headers: authHeaders(credentials), + method: 'POST' + }) +} + const postStatus = ({ credentials, status, @@ -617,7 +630,9 @@ const postStatus = ({ poll, mediaIds = [], inReplyToStatusId, - contentType + contentType, + preview, + idempotencyKey }) => { const form = new FormData() const pollOptions = poll.options || [] @@ -647,20 +662,22 @@ const postStatus = ({ if (inReplyToStatusId) { form.append('in_reply_to_id', inReplyToStatusId) } + if (preview) { + form.append('preview', 'true') + } + + let postHeaders = authHeaders(credentials) + if (idempotencyKey) { + postHeaders['idempotency-key'] = idempotencyKey + } return fetch(MASTODON_POST_STATUS_URL, { body: form, method: 'POST', - headers: authHeaders(credentials) + headers: postHeaders }) .then((response) => { - if (response.ok) { - return response.json() - } else { - return { - error: response - } - } + return response.json() }) .then((data) => data.error ? data : parseStatus(data)) } @@ -682,6 +699,17 @@ const uploadMedia = ({ formData, credentials }) => { .then((data) => parseAttachment(data)) } +const setMediaDescription = ({ id, description, credentials }) => { + return promisedRequest({ + url: `${MASTODON_MEDIA_UPLOAD_URL}/${id}`, + method: 'PUT', + headers: authHeaders(credentials), + payload: { + description + } + }).then((data) => parseAttachment(data)) +} + const importBlocks = ({ file, credentials }) => { const formData = new FormData() formData.append('list', file) @@ -975,6 +1003,8 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => { params.push(['following', true]) } + params.push(['with_relationships', true]) + let queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&') url += `?${queryString}` @@ -993,6 +1023,10 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => { }) } +const fetchKnownDomains = ({ credentials }) => { + return promisedRequest({ url: MASTODON_KNOWN_DOMAIN_LIST_URL, credentials }) +} + const fetchDomainMutes = ({ credentials }) => { return promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials }) } @@ -1044,6 +1078,10 @@ const MASTODON_STREAMING_EVENTS = new Set([ 'filters_changed' ]) +const PLEROMA_STREAMING_EVENTS = new Set([ + 'pleroma:chat_update' +]) + // A thin wrapper around WebSocket API that allows adding a pre-processor to it // Uses EventTarget and a CustomEvent to proxy events export const ProcessedWS = ({ @@ -1100,7 +1138,7 @@ export const handleMastoWS = (wsEvent) => { if (!data) return const parsedEvent = JSON.parse(data) const { event, payload } = parsedEvent - if (MASTODON_STREAMING_EVENTS.has(event)) { + if (MASTODON_STREAMING_EVENTS.has(event) || PLEROMA_STREAMING_EVENTS.has(event)) { // MastoBE and PleromaBE both send payload for delete as a PLAIN string if (event === 'delete') { return { event, id: payload } @@ -1110,6 +1148,8 @@ export const handleMastoWS = (wsEvent) => { return { event, status: parseStatus(data) } } else if (event === 'notification') { return { event, notification: parseNotification(data) } + } else if (event === 'pleroma:chat_update') { + return { event, chatUpdate: parseChat(data) } } } else { console.warn('Unknown event', wsEvent) @@ -1117,6 +1157,81 @@ export const handleMastoWS = (wsEvent) => { } } +export const WSConnectionStatus = Object.freeze({ + 'JOINED': 1, + 'CLOSED': 2, + 'ERROR': 3 +}) + +const chats = ({ credentials }) => { + return fetch(PLEROMA_CHATS_URL, { headers: authHeaders(credentials) }) + .then((data) => data.json()) + .then((data) => { + return { chats: data.map(parseChat).filter(c => c) } + }) +} + +const getOrCreateChat = ({ accountId, credentials }) => { + return promisedRequest({ + url: PLEROMA_CHAT_URL(accountId), + method: 'POST', + credentials + }) +} + +const chatMessages = ({ id, credentials, maxId, sinceId, limit = 20 }) => { + let url = PLEROMA_CHAT_MESSAGES_URL(id) + const args = [ + maxId && `max_id=${maxId}`, + sinceId && `since_id=${sinceId}`, + limit && `limit=${limit}` + ].filter(_ => _).join('&') + + url = url + (args ? '?' + args : '') + + return promisedRequest({ + url, + method: 'GET', + credentials + }) +} + +const sendChatMessage = ({ id, content, mediaId = null, credentials }) => { + const payload = { + 'content': content + } + + if (mediaId) { + payload['media_id'] = mediaId + } + + return promisedRequest({ + url: PLEROMA_CHAT_MESSAGES_URL(id), + method: 'POST', + payload: payload, + credentials + }) +} + +const readChat = ({ id, lastReadId, credentials }) => { + return promisedRequest({ + url: PLEROMA_CHAT_READ_URL(id), + method: 'POST', + payload: { + 'last_read_id': lastReadId + }, + credentials + }) +} + +const deleteChatMessage = ({ chatId, messageId, credentials }) => { + return promisedRequest({ + url: PLEROMA_DELETE_CHAT_MESSAGE_URL(chatId, messageId), + method: 'DELETE', + credentials + }) +} + const apiService = { verifyCredentials, fetchTimeline, @@ -1140,9 +1255,12 @@ const apiService = { unfavorite, retweet, unretweet, + bookmarkStatus, + unbookmarkStatus, postStatus, deleteStatus, uploadMedia, + setMediaDescription, fetchMutes, muteUser, unmuteUser, @@ -1160,10 +1278,8 @@ const apiService = { deactivateUser, register, getCaptcha, - updateAvatar, - updateBg, + updateProfileImages, updateProfile, - updateBanner, importBlocks, importFollows, deleteAccount, @@ -1191,9 +1307,16 @@ const apiService = { updateNotificationSettings, search2, searchUsers, + fetchKnownDomains, fetchDomainMutes, muteDomain, - unmuteDomain + unmuteDomain, + chats, + getOrCreateChat, + chatMessages, + sendChatMessage, + readChat, + deleteChatMessage } export default apiService diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js @@ -12,10 +12,6 @@ const backendInteractorService = credentials => ({ return notificationsFetcher.startFetching({ store, credentials }) }, - fetchAndUpdateNotifications ({ store }) { - return notificationsFetcher.fetchAndUpdate({ store, credentials }) - }, - startFetchingFollowRequests ({ store }) { return followRequestFetcher.startFetching({ store, credentials }) }, diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js @@ -0,0 +1,151 @@ +import _ from 'lodash' + +const empty = (chatId) => { + return { + idIndex: {}, + messages: [], + newMessageCount: 0, + lastSeenTimestamp: 0, + chatId: chatId, + minId: undefined, + lastMessage: undefined + } +} + +const clear = (storage) => { + storage.idIndex = {} + storage.messages.splice(0, storage.messages.length) + storage.newMessageCount = 0 + storage.lastSeenTimestamp = 0 + storage.minId = undefined + storage.lastMessage = undefined +} + +const deleteMessage = (storage, messageId) => { + if (!storage) { return } + storage.messages = storage.messages.filter(m => m.id !== messageId) + delete storage.idIndex[messageId] + + if (storage.lastMessage && (storage.lastMessage.id === messageId)) { + storage.lastMessage = _.maxBy(storage.messages, 'id') + } + + if (storage.minId === messageId) { + const firstMessage = _.minBy(storage.messages, 'id') + storage.minId = firstMessage.id + } +} + +const add = (storage, { messages: newMessages }) => { + if (!storage) { return } + for (let i = 0; i < newMessages.length; i++) { + const message = newMessages[i] + + // sanity check + if (message.chat_id !== storage.chatId) { return } + + if (!storage.minId || message.id < storage.minId) { + storage.minId = message.id + } + + if (!storage.lastMessage || message.id > storage.lastMessage.id) { + storage.lastMessage = message + } + + if (!storage.idIndex[message.id]) { + if (storage.lastSeenTimestamp < message.created_at) { + storage.newMessageCount++ + } + storage.messages.push(message) + storage.idIndex[message.id] = message + } + } +} + +const resetNewMessageCount = (storage) => { + if (!storage) { return } + storage.newMessageCount = 0 + storage.lastSeenTimestamp = new Date() +} + +// Inserts date separators and marks the head and tail if it's the chain of messages made by the same user +const getView = (storage) => { + if (!storage) { return [] } + + const result = [] + const messages = _.sortBy(storage.messages, ['id', 'desc']) + const firstMessage = messages[0] + let previousMessage = messages[messages.length - 1] + let currentMessageChainId + + if (firstMessage) { + const date = new Date(firstMessage.created_at) + date.setHours(0, 0, 0, 0) + result.push({ + type: 'date', + date, + id: date.getTime().toString() + }) + } + + let afterDate = false + + for (let i = 0; i < messages.length; i++) { + const message = messages[i] + const nextMessage = messages[i + 1] + + const date = new Date(message.created_at) + date.setHours(0, 0, 0, 0) + + // insert date separator and start a new message chain + if (previousMessage && previousMessage.date < date) { + result.push({ + type: 'date', + date, + id: date.getTime().toString() + }) + + previousMessage['isTail'] = true + currentMessageChainId = undefined + afterDate = true + } + + const object = { + type: 'message', + data: message, + date, + id: message.id, + messageChainId: currentMessageChainId + } + + // end a message chian + if ((nextMessage && nextMessage.account_id) !== message.account_id) { + object['isTail'] = true + currentMessageChainId = undefined + } + + // start a new message chain + if ((previousMessage && previousMessage.data && previousMessage.data.account_id) !== message.account_id || afterDate) { + currentMessageChainId = _.uniqueId() + object['isHead'] = true + object['messageChainId'] = currentMessageChainId + } + + result.push(object) + previousMessage = object + afterDate = false + } + + return result +} + +const ChatService = { + add, + empty, + getView, + deleteMessage, + resetNewMessageCount, + clear +} + +export default ChatService diff --git a/src/services/chat_utils/chat_utils.js b/src/services/chat_utils/chat_utils.js @@ -0,0 +1,19 @@ +import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js' + +export const maybeShowChatNotification = (store, chat) => { + if (!chat.lastMessage) return + if (store.rootState.chats.currentChatId === chat.id && !document.hidden) return + + const opts = { + tag: chat.lastMessage.id, + title: chat.account.name, + icon: chat.account.profile_image_url, + body: chat.lastMessage.content + } + + if (chat.lastMessage.attachment && chat.lastMessage.attachment.type === 'image') { + opts.image = chat.lastMessage.attachment.preview_url + } + + showDesktopNotification(store.rootState, opts) +} diff --git a/src/services/desktop_notification_utils/desktop_notification_utils.js b/src/services/desktop_notification_utils/desktop_notification_utils.js @@ -0,0 +1,9 @@ +export const showDesktopNotification = (rootState, desktopNotificationOpts) => { + if (!('Notification' in window && window.Notification.permission === 'granted')) return + if (rootState.statuses.notifications.desktopNotificationSilence) { return } + + const desktopNotification = new window.Notification(desktopNotificationOpts.title, desktopNotificationOpts) + // Chrome is known for not closing notifications automatically + // according to MDN, anyway. + setTimeout(desktopNotification.close.bind(desktopNotification), 5000) +} diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js @@ -1,4 +1,5 @@ import escape from 'escape-html' +import parseLinkHeader from 'parse-link-header' import { isStatusNotification } from '../notification_utils/notification_utils.js' const qvitterStatusType = (status) => { @@ -56,6 +57,12 @@ export const parseUser = (data) => { value: addEmojis(field.value, data.emojis) } }) + output.fields_text = data.fields.map(field => { + return { + name: unescape(field.name.replace(/<[^>]*>/g, '')), + value: unescape(field.value.replace(/<[^>]*>/g, '')) + } + }) // Utilize avatar_static for gif avatars? output.profile_image_url = data.avatar @@ -72,6 +79,7 @@ export const parseUser = (data) => { const relationship = data.pleroma.relationship output.background_image = data.pleroma.background_image + output.favicon = data.pleroma.favicon output.token = data.pleroma.chat_token if (relationship) { @@ -176,6 +184,7 @@ export const parseUser = (data) => { output.deactivated = data.pleroma.deactivated output.notification_settings = data.pleroma.notification_settings + output.unread_chat_count = data.pleroma.unread_chat_count } output.tags = output.tags || [] @@ -200,6 +209,7 @@ export const parseAttachment = (data) => { } output.url = data.url + output.large_thumb_url = data.preview_url output.description = data.description return output @@ -210,7 +220,7 @@ export const addEmojis = (string, emojis) => { 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' />` + `<img src='${emoji.url}' alt=':${emoji.shortcode}:' title=':${emoji.shortcode}:' class='emoji' />` ) }, string) } @@ -226,6 +236,8 @@ export const parseStatus = (data) => { output.repeated = data.reblogged output.repeat_num = data.reblogs_count + output.bookmarked = data.bookmarked + output.type = data.reblog ? 'retweet' : 'status' output.nsfw = data.sensitive @@ -242,6 +254,7 @@ export const parseStatus = (data) => { output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct output.thread_muted = pleroma.thread_muted output.emoji_reactions = pleroma.emoji_reactions + output.parent_visible = pleroma.parent_visible === undefined ? true : pleroma.parent_visible } else { output.text = data.content output.summary = data.spoiler_text @@ -258,6 +271,12 @@ export const parseStatus = (data) => { output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis) output.external_url = data.url output.poll = data.poll + if (output.poll) { + output.poll.options = (output.poll.options || []).map(field => ({ + ...field, + title_html: addEmojis(field.title, data.emojis) + })) + } output.pinned = data.pinned output.muted = data.muted } else { @@ -356,7 +375,7 @@ export const parseNotification = (data) => { ? parseStatus(data.notice.favorited_status) : parsedNotice output.action = parsedNotice - output.from_profile = parseUser(data.from_profile) + output.from_profile = output.type === 'pleroma:chat_mention' ? parseUser(data.account) : parseUser(data.from_profile) } output.created_at = new Date(data.created_at) @@ -369,3 +388,47 @@ const isNsfw = (status) => { const nsfwRegex = /#nsfw/i return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex) } + +export const parseLinkHeaderPagination = (linkHeader, opts = {}) => { + const flakeId = opts.flakeId + const parsedLinkHeader = parseLinkHeader(linkHeader) + if (!parsedLinkHeader) return + const maxId = parsedLinkHeader.next.max_id + const minId = parsedLinkHeader.prev.min_id + + return { + maxId: flakeId ? maxId : parseInt(maxId, 10), + minId: flakeId ? minId : parseInt(minId, 10) + } +} + +export const parseChat = (chat) => { + const output = {} + output.id = chat.id + output.account = parseUser(chat.account) + output.unread = chat.unread + output.lastMessage = parseChatMessage(chat.last_message) + output.updated_at = new Date(chat.updated_at) + return output +} + +export const parseChatMessage = (message) => { + if (!message) { return } + if (message.isNormalized) { return message } + const output = message + output.id = message.id + output.created_at = new Date(message.created_at) + output.chat_id = message.chat_id + if (message.content) { + output.content = addEmojis(message.content, message.emojis) + } else { + output.content = '' + } + if (message.attachment) { + output.attachments = [parseAttachment(message.attachment)] + } else { + output.attachments = [] + } + output.isNormalized = true + return output +} diff --git a/src/services/follow_request_fetcher/follow_request_fetcher.service.js b/src/services/follow_request_fetcher/follow_request_fetcher.service.js @@ -4,6 +4,7 @@ const fetchAndUpdate = ({ store, credentials }) => { return apiService.fetchFollowRequests({ credentials }) .then((requests) => { store.commit('setFollowRequests', requests) + store.commit('addNewUsers', requests) }, () => {}) .catch(() => {}) } diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js @@ -1,16 +1,22 @@ import { filter, sortBy, includes } from 'lodash' +import { muteWordHits } from '../status_parser/status_parser.js' +import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js' export const notificationsFromStore = store => store.state.statuses.notifications.data -export const visibleTypes = store => ([ - store.state.config.notificationVisibility.likes && 'like', - store.state.config.notificationVisibility.mentions && 'mention', - store.state.config.notificationVisibility.repeats && 'repeat', - store.state.config.notificationVisibility.follows && 'follow', - store.state.config.notificationVisibility.followRequest && 'follow_request', - store.state.config.notificationVisibility.moves && 'move', - store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction' -].filter(_ => _)) +export const visibleTypes = store => { + const rootState = store.rootState || store.state + + return ([ + rootState.config.notificationVisibility.likes && 'like', + rootState.config.notificationVisibility.mentions && 'mention', + rootState.config.notificationVisibility.repeats && 'repeat', + rootState.config.notificationVisibility.follows && 'follow', + rootState.config.notificationVisibility.followRequest && 'follow_request', + rootState.config.notificationVisibility.moves && 'move', + rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction' + ].filter(_ => _)) +} const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction'] @@ -32,6 +38,22 @@ const sortById = (a, b) => { } } +const isMutedNotification = (store, notification) => { + if (!notification.status) return + return notification.status.muted || muteWordHits(notification.status, store.rootGetters.mergedConfig.muteWords).length > 0 +} + +export const maybeShowNotification = (store, notification) => { + const rootState = store.rootState || store.state + + if (notification.seen) return + if (!visibleTypes(store).includes(notification.type)) return + if (notification.type === 'mention' && isMutedNotification(store, notification)) return + + const notificationObject = prepareNotificationObject(notification, store.rootGetters.i18n) + showDesktopNotification(rootState, notificationObject) +} + export const filteredNotificationsFromStore = (store, types) => { // map is just to clone the array since sort mutates it and it causes some issues let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById) @@ -43,3 +65,47 @@ export const filteredNotificationsFromStore = (store, types) => { export const unseenNotificationsFromStore = store => filter(filteredNotificationsFromStore(store), ({ seen }) => !seen) + +export const prepareNotificationObject = (notification, i18n) => { + const notifObj = { + tag: notification.id + } + const status = notification.status + const title = notification.from_profile.name + notifObj.title = title + notifObj.icon = notification.from_profile.profile_image_url + let i18nString + switch (notification.type) { + case 'like': + i18nString = 'favorited_you' + break + case 'repeat': + i18nString = 'repeated_you' + break + case 'follow': + i18nString = 'followed_you' + break + case 'move': + i18nString = 'migrated_to' + break + case 'follow_request': + i18nString = 'follow_request' + break + } + + if (notification.type === 'pleroma:emoji_reaction') { + notifObj.body = i18n.t('notifications.reacted_with', [notification.emoji]) + } else if (i18nString) { + notifObj.body = i18n.t('notifications.' + i18nString) + } else if (isStatusNotification(notification.type)) { + notifObj.body = notification.status.text + } + + // Shows first attached non-nsfw image, if any. Should add configuration for this somehow... + if (status && status.attachments && status.attachments.length > 0 && !status.nsfw && + status.attachments[0].mimetype.startsWith('image/')) { + notifObj.image = status.attachments[0].url + } + + return notifObj +} diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -30,21 +30,25 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => { } const result = fetchNotifications({ store, args, older }) - // load unread notifications repeatedly to provide consistency between browser tabs + // If there's any unread notifications, try fetch notifications since + // the newest read notification to check if any of the unread notifs + // have changed their 'seen' state (marked as read in another session), so + // we can update the state in this session to mark them as read as well. + // The normal maxId-check does not tell if older notifications have changed const notifications = timelineData.data const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id) - if (readNotifsIds.length) { + const numUnseenNotifs = notifications.length - readNotifsIds.length + if (numUnseenNotifs > 0 && readNotifsIds.length > 0) { args['since'] = Math.max(...readNotifsIds) fetchNotifications({ store, args, older }) } - return result } } const fetchNotifications = ({ store, args, older }) => { return apiService.fetchTimeline(args) - .then((notifications) => { + .then(({ data: notifications }) => { update({ store, notifications, older }) return notifications }, () => store.dispatch('setNotificationsError', { value: true })) diff --git a/src/services/resettable_async_component.js b/src/services/resettable_async_component.js @@ -0,0 +1,32 @@ +import Vue from 'vue' + +/* By default async components don't have any way to recover, if component is + * failed, it is failed forever. This helper tries to remedy that by recreating + * async component when retry is requested (by user). You need to emit the + * `resetAsyncComponent` event from child to reset the component. Generally, + * this should be done from error component but could be done from loading or + * actual target component itself if needs to be. + */ +function getResettableAsyncComponent (asyncComponent, options) { + const asyncComponentFactory = () => () => ({ + component: asyncComponent(), + ...options + }) + + const observe = Vue.observable({ c: asyncComponentFactory() }) + + return { + functional: true, + render (createElement, { data, children }) { + // emit event resetAsyncComponent to reloading + data.on = {} + data.on.resetAsyncComponent = () => { + observe.c = asyncComponentFactory() + // parent.$forceUpdate() + } + return createElement(observe.c, data, children) + } + } +} + +export default getResettableAsyncComponent diff --git a/src/services/status_parser/status_parser.js b/src/services/status_parser/status_parser.js @@ -1,15 +1,11 @@ -import sanitize from 'sanitize-html' +import { filter } from 'lodash' -export const removeAttachmentLinks = (html) => { - return sanitize(html, { - allowedTags: false, - allowedAttributes: false, - exclusiveFilter: ({ tag, attribs }) => tag === 'a' && typeof attribs.class === 'string' && attribs.class.match(/attachment/) +export const muteWordHits = (status, muteWords) => { + const statusText = status.text.toLowerCase() + const statusSummary = status.summary.toLowerCase() + const hits = filter(muteWords, (muteWord) => { + return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase()) }) -} -export const parse = (html) => { - return removeAttachmentLinks(html) + return hits } - -export default parse diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js @@ -1,7 +1,19 @@ import { map } from 'lodash' import apiService from '../api/api.service.js' -const postStatus = ({ store, status, spoilerText, visibility, sensitive, poll, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => { +const postStatus = ({ + store, + status, + spoilerText, + visibility, + sensitive, + poll, + media = [], + inReplyToStatusId = undefined, + contentType = 'text/plain', + preview = false, + idempotencyKey = '' +}) => { const mediaIds = map(media, 'id') return apiService.postStatus({ @@ -13,9 +25,12 @@ const postStatus = ({ store, status, spoilerText, visibility, sensitive, poll, m mediaIds, inReplyToStatusId, contentType, - poll }) + poll, + preview, + idempotencyKey + }) .then((data) => { - if (!data.error) { + if (!data.error && !preview) { store.dispatch('addNewStatuses', { statuses: [data], timeline: 'friends', @@ -34,13 +49,18 @@ const postStatus = ({ store, status, spoilerText, visibility, sensitive, poll, m const uploadMedia = ({ store, formData }) => { const credentials = store.state.users.currentUser.credentials - return apiService.uploadMedia({ credentials, formData }) } +const setMediaDescription = ({ store, id, description }) => { + const credentials = store.state.users.currentUser.credentials + return apiService.setMediaDescription({ credentials, id, description }) +} + const statusPosterService = { postStatus, - uploadMedia + uploadMedia, + setMediaDescription } export default statusPosterService diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js @@ -106,7 +106,8 @@ export const generateRadii = (input) => { avatar: 5, avatarAlt: 50, tooltip: 2, - attachment: 5 + attachment: 5, + chatMessage: inputRadii.panel }) return { diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js @@ -23,7 +23,9 @@ export const LAYERS = { inputTopBar: 'topBar', alert: 'bg', alertPanel: 'panel', - poll: 'bg' + poll: 'bg', + chatBg: 'underlay', + chatMessage: 'chatBg' } /* By default opacity slots have 1 as default opacity @@ -34,7 +36,8 @@ export const DEFAULT_OPACITY = { alert: 0.5, input: 0.5, faint: 0.5, - underlay: 0.15 + underlay: 0.15, + alertPopup: 0.95 } /** SUBJECT TO CHANGE IN THE FUTURE, this is all beta @@ -356,6 +359,12 @@ export const SLOT_INHERITANCE = { textColor: 'preserve' }, + postGreentext: { + depends: ['cGreen'], + layer: 'bg', + textColor: 'preserve' + }, + border: { depends: ['fg'], opacity: 'border', @@ -621,11 +630,97 @@ export const SLOT_INHERITANCE = { textColor: true }, + alertPopupError: { + depends: ['alertError'], + opacity: 'alertPopup' + }, + alertPopupErrorText: { + depends: ['alertErrorText'], + layer: 'popover', + variant: 'alertPopupError', + textColor: true + }, + + alertPopupWarning: { + depends: ['alertWarning'], + opacity: 'alertPopup' + }, + alertPopupWarningText: { + depends: ['alertWarningText'], + layer: 'popover', + variant: 'alertPopupWarning', + textColor: true + }, + + alertPopupNeutral: { + depends: ['alertNeutral'], + opacity: 'alertPopup' + }, + alertPopupNeutralText: { + depends: ['alertNeutralText'], + layer: 'popover', + variant: 'alertPopupNeutral', + textColor: true + }, + badgeNotification: '--cRed', badgeNotificationText: { depends: ['text', 'badgeNotification'], layer: 'badge', variant: 'badgeNotification', textColor: 'bw' + }, + + chatBg: { + depends: ['bg'] + }, + + chatMessageIncomingBg: { + depends: ['chatBg'] + }, + + chatMessageIncomingText: { + depends: ['text'], + layer: 'chatMessage', + variant: 'chatMessageIncomingBg', + textColor: true + }, + + chatMessageIncomingLink: { + depends: ['link'], + layer: 'chatMessage', + variant: 'chatMessageIncomingBg', + textColor: 'preserve' + }, + + chatMessageIncomingBorder: { + depends: ['border'], + opacity: 'border', + color: (mod, border) => brightness(2 * mod, border).rgb + }, + + chatMessageOutgoingBg: { + depends: ['chatMessageIncomingBg'], + color: (mod, chatMessage) => brightness(5 * mod, chatMessage).rgb + }, + + chatMessageOutgoingText: { + depends: ['text'], + layer: 'chatMessage', + variant: 'chatMessageOutgoingBg', + textColor: true + }, + + chatMessageOutgoingLink: { + depends: ['link'], + layer: 'chatMessage', + variant: 'chatMessageOutgoingBg', + textColor: 'preserve' + }, + + chatMessageOutgoingBorder: { + depends: ['chatMessageOutgoingBg'], + opacity: 'border', + color: (mod, border) => brightness(2 * mod, border).rgb } } diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js @@ -128,14 +128,17 @@ export const topoSort = ( while (unprocessed.length > 0) { step(unprocessed.pop()) } - return output.sort((a, b) => { + + // The index thing is to make sorting stable on browsers + // where Array.sort() isn't stable + return output.map((data, index) => ({ data, index })).sort(({ data: a, index: ai }, { data: b, index: bi }) => { const depsA = getDeps(a, inheritance).length const depsB = getDeps(b, inheritance).length - if (depsA === depsB || (depsB !== 0 && depsA !== 0)) return 0 + if (depsA === depsB || (depsB !== 0 && depsA !== 0)) return ai - bi if (depsA === 0 && depsB !== 0) return -1 if (depsB === 0 && depsA !== 0) return 1 - }) + }).map(({ data }) => data) } const expandSlotValue = (value) => { diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -2,7 +2,7 @@ import { camelCase } from 'lodash' import apiService from '../api/api.service.js' -const update = ({ store, statuses, timeline, showImmediately, userId }) => { +const update = ({ store, statuses, timeline, showImmediately, userId, pagination }) => { const ccTimeline = camelCase(timeline) store.dispatch('setError', { value: false }) @@ -12,7 +12,8 @@ const update = ({ store, statuses, timeline, showImmediately, userId }) => { timeline: ccTimeline, userId, statuses, - showImmediately + showImmediately, + pagination }) } @@ -30,7 +31,8 @@ const fetchAndUpdate = ({ const rootState = store.rootState || store.state const { getters } = store const timelineData = rootState.statuses.timelines[camelCase(timeline)] - const hideMutedPosts = getters.mergedConfig.hideMutedPosts + const { hideMutedPosts, replyVisibility } = getters.mergedConfig + const loggedIn = !!rootState.users.currentUser if (older) { args['until'] = until || timelineData.minId @@ -41,20 +43,25 @@ const fetchAndUpdate = ({ args['userId'] = userId args['tag'] = tag args['withMuted'] = !hideMutedPosts + if (loggedIn && ['friends', 'public', 'publicAndExternal'].includes(timeline)) { + args['replyVisibility'] = replyVisibility + } const numStatusesBeforeFetch = timelineData.statuses.length return apiService.fetchTimeline(args) - .then((statuses) => { - if (statuses.error) { - store.dispatch('setErrorData', { value: statuses }) + .then(response => { + if (response.error) { + store.dispatch('setErrorData', { value: response }) return } + + const { data: statuses, pagination } = response if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) { store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId }) } - update({ store, statuses, timeline, showImmediately, userId }) - return statuses + update({ store, statuses, timeline, showImmediately, userId, pagination }) + return { statuses, pagination } }, () => store.dispatch('setError', { value: true })) } diff --git a/src/services/window_utils/window_utils.js b/src/services/window_utils/window_utils.js @@ -3,3 +3,8 @@ export const windowWidth = () => window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth + +export const windowHeight = () => + window.innerHeight || + document.documentElement.clientHeight || + document.body.clientHeight diff --git a/src/sw.js b/src/sw.js @@ -1,6 +1,19 @@ /* eslint-env serviceworker */ import localForage from 'localforage' +import { parseNotification } from './services/entity_normalizer/entity_normalizer.service.js' +import { prepareNotificationObject } from './services/notification_utils/notification_utils.js' +import Vue from 'vue' +import VueI18n from 'vue-i18n' +import messages from './i18n/service_worker_messages.js' + +Vue.use(VueI18n) +const i18n = new VueI18n({ + // By default, use the browser locale, we will update it if neccessary + locale: 'en', + fallbackLocale: 'en', + messages +}) function isEnabled () { return localForage.getItem('vuex-lz') @@ -12,15 +25,33 @@ function getWindowClients () { .then((clientList) => clientList.filter(({ type }) => type === 'window')) } -self.addEventListener('push', (event) => { - if (event.data) { - event.waitUntil(isEnabled().then((isEnabled) => { - return isEnabled && getWindowClients().then((list) => { - const data = event.data.json() +const setLocale = async () => { + const state = await localForage.getItem('vuex-lz') + const locale = state.config.interfaceLanguage || 'en' + i18n.locale = locale +} + +const maybeShowNotification = async (event) => { + const enabled = await isEnabled() + const activeClients = await getWindowClients() + await setLocale() + if (enabled && (activeClients.length === 0)) { + const data = event.data.json() + + const url = `${self.registration.scope}api/v1/notifications/${data.notification_id}` + const notification = await fetch(url, { headers: { Authorization: 'Bearer ' + data.access_token } }) + const notificationJson = await notification.json() + const parsedNotification = parseNotification(notificationJson) - if (list.length === 0) return self.registration.showNotification(data.title, data) - }) - })) + const res = prepareNotificationObject(parsedNotification, i18n) + + self.registration.showNotification(res.title, res) + } +} + +self.addEventListener('push', async (event) => { + if (event.data) { + event.waitUntil(maybeShowNotification(event)) } }) diff --git a/static/config.json b/static/config.json @@ -1,23 +1,28 @@ { - "theme": "pleroma-dark", + "alwaysShowSubjectInput": true, "background": "/static/aurora_borealis.jpg", - "logo": "/static/logo.png", - "logoMask": true, - "logoMargin": ".1em", - "redirectRootNoLogin": "/main/all", - "redirectRootLogin": "/main/friends", - "showInstanceSpecificPanel": false, "collapseMessageWithSubject": false, - "scopeCopy": true, - "subjectLineBehavior": "email", - "postContentType": "text/plain", - "alwaysShowSubjectInput": true, + "disableChat": false, + "greentext": false, + "hideFilteredStatuses": false, + "hideMutedPosts": false, "hidePostStats": false, + "hideSitename": false, "hideUserStats": false, "loginMethod": "password", - "webPushNotifications": false, - "noAttachmentLinks": false, + "logo": "/static/logo.png", + "logoMargin": ".1em", + "logoMask": true, + "minimalScopesMode": false, "nsfwCensorImage": "", + "postContentType": "text/plain", + "redirectRootLogin": "/main/friends", + "redirectRootNoLogin": "/main/all", + "scopeCopy": true, "showFeaturesPanel": true, - "minimalScopesMode": false + "showInstanceSpecificPanel": false, + "sidebarRight": false, + "subjectLineBehavior": "email", + "theme": "pleroma-dark", + "webPushNotifications": false } diff --git a/static/fontello.json b/static/fontello.json @@ -363,6 +363,48 @@ "css": "ok", "code": 59431, "src": "fontawesome" + }, + { + "uid": "4109c474ff99cad28fd5a2c38af2ec6f", + "css": "filter", + "code": 61616, + "src": "fontawesome" + }, + { + "uid": "9a76bc135eac17d2c8b8ad4a5774fc87", + "css": "download", + "code": 59429, + "src": "fontawesome" + }, + { + "uid": "f04a5d24e9e659145b966739c4fde82a", + "css": "bookmark", + "code": 59430, + "src": "fontawesome" + }, + { + "uid": "2f5ef6f6b7aaebc56458ab4e865beff5", + "css": "bookmark-empty", + "code": 61591, + "src": "fontawesome" + }, + { + "uid": "9ea0a737ccc45d6c510dcbae56058849", + "css": "music", + "code": 59432, + "src": "fontawesome" + }, + { + "uid": "1b5a5d7b7e3c71437f5a26befdd045ed", + "css": "doc", + "code": 59433, + "src": "fontawesome" + }, + { + "uid": "98d9c83c1ee7c2c25af784b518c522c5", + "css": "block", + "code": 59434, + "src": "fontawesome" } ] } \ No newline at end of file diff --git a/static/terms-of-service.html b/static/terms-of-service.html @@ -1,4 +1,9 @@ <h4>Terms of Service</h4> -<p>This is a placeholder ToS. Edit <code>"/static/terms-of-service.html"</code> to make it fit the needs of your instance.</p> +<p>This is the default placeholder ToS. You should copy it over to your static folder and edit it to fit the needs of your instance.</p> + +<p>To do so, place a file at <code>"/instance/static/static/terms-of-service.html"</code> in your + Pleroma install containing the real ToS for your instance.</p> +<p>See the <a href='https://docs.pleroma.social/backend/configuration/static_dir/'>Pleroma documentation</a> for more information.</p> +<br> <img src="/static/logo.png" style="display: block; margin: auto; max-width: 100%; height: 50px; object-fit: contain;" /> diff --git a/static/themes/redmond-xx-se.json b/static/themes/redmond-xx-se.json @@ -286,7 +286,9 @@ "cGreen": "#008000", "cOrange": "#808000", "highlight": "--accent", - "selectedPost": "--bg,-10" + "selectedPost": "--bg,-10", + "selectedMenu": "--accent", + "selectedMenuPopover": "--accent" }, "radii": { "btn": "0", diff --git a/static/themes/redmond-xx.json b/static/themes/redmond-xx.json @@ -277,7 +277,9 @@ "cGreen": "#008000", "cOrange": "#808000", "highlight": "--accent", - "selectedPost": "--bg,-10" + "selectedPost": "--bg,-10", + "selectedMenu": "--accent", + "selectedMenuPopover": "--accent" }, "radii": { "btn": "0", diff --git a/static/themes/redmond-xxi.json b/static/themes/redmond-xxi.json @@ -259,7 +259,9 @@ "cGreen": "#669966", "cOrange": "#cc6633", "highlight": "--accent", - "selectedPost": "--bg,-10" + "selectedPost": "--bg,-10", + "selectedMenu": "--accent", + "selectedMenuPopover": "--accent" }, "radii": { "btn": "0", diff --git a/test/unit/specs/boot/routes.spec.js b/test/unit/specs/boot/routes.spec.js @@ -1,14 +1,22 @@ +import Vuex from 'vuex' import routes from 'src/boot/routes' import { createLocalVue } from '@vue/test-utils' import VueRouter from 'vue-router' const localVue = createLocalVue() +localVue.use(Vuex) localVue.use(VueRouter) +const store = new Vuex.Store({ + state: { + instance: {} + } +}) + describe('routes', () => { const router = new VueRouter({ mode: 'abstract', - routes: routes({}) + routes: routes(store) }) it('root path', () => { diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js @@ -330,7 +330,7 @@ describe('Statuses module', () => { const deletion = makeMockStatus({ id: '4', type: 'deletion' }) deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.' deletion.uri = 'xxx' - + const newNotificationSideEffects = () => {} mutations.addNewStatuses(state, { statuses: [status, otherStatus], user }) mutations.addNewNotifications( state, @@ -342,7 +342,8 @@ describe('Statuses module', () => { status: otherStatus, action: otherStatus, seen: false - }] + }], + newNotificationSideEffects }) expect(state.notifications.data.length).to.eql(1) @@ -356,7 +357,8 @@ describe('Statuses module', () => { status: mentionedStatus, action: mentionedStatus, seen: false - }] + }], + newNotificationSideEffects }) mutations.addNewStatuses(state, { statuses: [mentionedStatus], user }) diff --git a/test/unit/specs/modules/users.spec.js b/test/unit/specs/modules/users.spec.js @@ -18,6 +18,42 @@ describe('The users module', () => { expect(state.users).to.eql([user]) expect(state.users[0].name).to.eql('Dude') }) + + it('merging array field in new information for old users', () => { + const state = cloneDeep(defaultState) + const user = { + id: '1', + fields: [ + { name: 'Label 1', value: 'Content 1' } + ] + } + const firstModUser = { + id: '1', + fields: [ + { name: 'Label 2', value: 'Content 2' }, + { name: 'Label 3', value: 'Content 3' } + ] + } + const secondModUser = { + id: '1', + fields: [ + { name: 'Label 4', value: 'Content 4' } + ] + } + + mutations.addNewUsers(state, [user]) + expect(state.users[0].fields).to.have.length(1) + expect(state.users[0].fields[0].name).to.eql('Label 1') + + mutations.addNewUsers(state, [firstModUser]) + expect(state.users[0].fields).to.have.length(2) + expect(state.users[0].fields[0].name).to.eql('Label 2') + expect(state.users[0].fields[1].name).to.eql('Label 3') + + mutations.addNewUsers(state, [secondModUser]) + expect(state.users[0].fields).to.have.length(1) + expect(state.users[0].fields[0].name).to.eql('Label 4') + }) }) describe('findUser', () => { diff --git a/test/unit/specs/services/chat_service/chat_service.spec.js b/test/unit/specs/services/chat_service/chat_service.spec.js @@ -0,0 +1,88 @@ +import chatService from '../../../../../src/services/chat_service/chat_service.js' + +const message1 = { + id: '9wLkdcmQXD21Oy8lEX', + created_at: (new Date('2020-06-22T18:45:53.000Z')) +} + +const message2 = { + id: '9wLkdp6ihaOVdNj8Wu', + account_id: '9vmRb29zLQReckr5ay', + created_at: (new Date('2020-06-22T18:45:56.000Z')) +} + +const message3 = { + id: '9wLke9zL4Dy4OZR2RM', + account_id: '9vmRb29zLQReckr5ay', + created_at: (new Date('2020-07-22T18:45:59.000Z')) +} + +describe('chatService', () => { + describe('.add', () => { + it("Doesn't add duplicates", () => { + const chat = chatService.empty() + chatService.add(chat, { messages: [ message1 ] }) + chatService.add(chat, { messages: [ message1 ] }) + expect(chat.messages.length).to.eql(1) + + chatService.add(chat, { messages: [ message2 ] }) + expect(chat.messages.length).to.eql(2) + }) + + it('Updates minId and lastMessage and newMessageCount', () => { + const chat = chatService.empty() + + chatService.add(chat, { messages: [ message1 ] }) + expect(chat.lastMessage.id).to.eql(message1.id) + expect(chat.minId).to.eql(message1.id) + expect(chat.newMessageCount).to.eql(1) + + chatService.add(chat, { messages: [ message2 ] }) + expect(chat.lastMessage.id).to.eql(message2.id) + expect(chat.minId).to.eql(message1.id) + expect(chat.newMessageCount).to.eql(2) + + chatService.resetNewMessageCount(chat) + expect(chat.newMessageCount).to.eql(0) + + const createdAt = new Date() + createdAt.setSeconds(createdAt.getSeconds() + 10) + chatService.add(chat, { messages: [ { message3, created_at: createdAt } ] }) + expect(chat.newMessageCount).to.eql(1) + }) + }) + + describe('.delete', () => { + it('Updates minId and lastMessage', () => { + const chat = chatService.empty() + + chatService.add(chat, { messages: [ message1 ] }) + chatService.add(chat, { messages: [ message2 ] }) + chatService.add(chat, { messages: [ message3 ] }) + + expect(chat.lastMessage.id).to.eql(message3.id) + expect(chat.minId).to.eql(message1.id) + + chatService.deleteMessage(chat, message3.id) + expect(chat.lastMessage.id).to.eql(message2.id) + expect(chat.minId).to.eql(message1.id) + + chatService.deleteMessage(chat, message1.id) + expect(chat.lastMessage.id).to.eql(message2.id) + expect(chat.minId).to.eql(message2.id) + }) + }) + + describe('.getView', () => { + it('Inserts date separators', () => { + const chat = chatService.empty() + + chatService.add(chat, { messages: [ message1 ] }) + chatService.add(chat, { messages: [ message2 ] }) + chatService.add(chat, { messages: [ message3 ] }) + + const view = chatService.getView(chat) + expect(view.map(i => i.type)).to.eql(['date', 'message', 'message', 'date', 'message']) + }) + }) +}) diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js @@ -1,4 +1,4 @@ -import { parseStatus, parseUser, parseNotification, addEmojis } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' +import { parseStatus, parseUser, parseNotification, addEmojis, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' import mastoapidata from '../../../../fixtures/mastoapi.json' import qvitterapidata from '../../../../fixtures/statuses.json' @@ -290,6 +290,19 @@ describe('API Entities normalizer', () => { expect(field).to.have.property('value').that.contains('<img') }) + 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 parsedUser = parseUser(user) + + expect(parsedUser).to.have.property('fields_text').to.be.an('array') + + const field = parsedUser.fields_text[0] + + expect(field).to.have.property('name').that.equal('user') + expect(field).to.have.property('value').that.equal('@user') + }) + it('adds hide_follows and hide_followers user settings', () => { const user = makeMockUserMasto({ pleroma: { hide_followers: true, hide_follows: false, hide_followers_count: false, hide_follows_count: true } }) @@ -338,9 +351,9 @@ 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" />' + 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" />' + const thinkHtml = '<img src="https://example.com/think.png" alt=":thinking:" title=":thinking:" class="emoji" />' .replace(/"/g, '\'') it('correctly replaces shortcodes in supplied string', () => { @@ -366,8 +379,28 @@ describe('API Entities normalizer', () => { 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] {|}*\'') + expect(result).to.include('title=\':c++:\'') + expect(result).to.include('title=\':[a-z] {|}*:\'') + }) + }) + + describe('Link header pagination', () => { + 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 result = parseLinkHeaderPagination(linkHeader) + expect(result).to.eql({ + 'maxId': 861676, + 'minId': 861741 + }) + }) + + it('Parses min and max ids as flakes', () => { + const linkHeader = '<http://example.com/api/v1/timelines/home?max_id=9waQx5IIS48qVue2Ai>; rel="next", <http://example.com/api/v1/timelines/home?min_id=9wi61nIPnfn674xgie>; rel="prev"' + const result = parseLinkHeaderPagination(linkHeader, { flakeId: true }) + expect(result).to.eql({ + 'maxId': '9waQx5IIS48qVue2Ai', + 'minId': '9wi61nIPnfn674xgie' + }) }) }) }) diff --git a/test/unit/specs/services/status_parser/status_parses.spec.js b/test/unit/specs/services/status_parser/status_parses.spec.js @@ -1,17 +0,0 @@ -import { removeAttachmentLinks } from '../../../../../src/services/status_parser/status_parser.js' - -const example = '<div class="status-content">@<a href="https://sealion.club/user/4" class="h-card mention" title="dewoo">dwmatiz</a> <a href="https://social.heldscal.la/file/3deb764ada10ce64a61b7a070b75dac45f86d2d5bf213bf18873da71d8714d86.png" title="https://social.heldscal.la/file/3deb764ada10ce64a61b7a070b75dac45f86d2d5bf213bf18873da71d8714d86.png" class="attachment" id="attachment-159853" rel="nofollow external">https://social.heldscal.la/attachment/159853</a></div>' - -describe('statusParser.removeAttachmentLinks', () => { - const exampleWithoutAttachmentLinks = '<div class="status-content">@<a href="https://sealion.club/user/4" class="h-card mention" title="dewoo">dwmatiz</a> </div>' - - it('removes attachment links', () => { - const parsed = removeAttachmentLinks(example) - expect(parsed).to.eql(exampleWithoutAttachmentLinks) - }) - - it('works when the class is empty', () => { - const parsed = removeAttachmentLinks('<a></a>') - expect(parsed).to.eql('<a></a>') - }) -}) diff --git a/yarn.lock b/yarn.lock @@ -8,6 +8,13 @@ dependencies: "@babel/highlight" "^7.0.0" +"@babel/code-frame@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + "@babel/code-frame@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" @@ -15,6 +22,28 @@ dependencies: "@babel/highlight" "^7.0.0" +"@babel/core@>=7.9.0": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.5.tgz#1f15e2cca8ad9a1d78a38ddba612f5e7cdbbd330" + integrity sha512-O34LQooYVDXPl7QWCdW9p4NR+QlzOr7xShPPJz8GsuCU3/8ua/wqTr7gmnxXv+WBESiGU/G5s16i6tUvHkNb+w== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.10.5" + "@babel/helper-module-transforms" "^7.10.5" + "@babel/helpers" "^7.10.4" + "@babel/parser" "^7.10.5" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.5" + "@babel/types" "^7.10.5" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + "@babel/core@^7.7.5": version "7.7.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.5.tgz#ae1323cd035b5160293307f50647e83f8ba62f7e" @@ -35,6 +64,15 @@ semver "^5.4.1" source-map "^0.5.0" +"@babel/generator@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.5.tgz#1b903554bc8c583ee8d25f1e8969732e6b829a69" + integrity sha512-3vXxr3FEW7E7lJZiWQ3bM4+v/Vyr9C+hpolQ8BGFr9Y8Ri2tFLWTixmwKBafDujO1WVah4fhZBeU1bieKdghig== + dependencies: + "@babel/types" "^7.10.5" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/generator@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.4.tgz#db651e2840ca9aa66f327dcec1dc5f5fa9611369" @@ -94,6 +132,15 @@ "@babel/traverse" "^7.7.4" "@babel/types" "^7.7.4" +"@babel/helper-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" + integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== + dependencies: + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/helper-function-name@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz#ab6e041e7135d436d8f0a3eca15de5b67a341a2e" @@ -103,6 +150,13 @@ "@babel/template" "^7.7.4" "@babel/types" "^7.7.4" +"@babel/helper-get-function-arity@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" + integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== + dependencies: + "@babel/types" "^7.10.4" + "@babel/helper-get-function-arity@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz#cb46348d2f8808e632f0ab048172130e636005f0" @@ -117,6 +171,13 @@ dependencies: "@babel/types" "^7.7.4" +"@babel/helper-member-expression-to-functions@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.5.tgz#172f56e7a63e78112f3a04055f24365af702e7ee" + integrity sha512-HiqJpYD5+WopCXIAbQDG0zye5XYVvcO9w/DHp5GsaGkRUaamLj2bEtu6i8rnGGprAhHM3qidCMgp71HF4endhA== + dependencies: + "@babel/types" "^7.10.5" + "@babel/helper-member-expression-to-functions@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.7.4.tgz#356438e2569df7321a8326644d4b790d2122cb74" @@ -137,6 +198,26 @@ dependencies: "@babel/types" "^7.0.0" +"@babel/helper-module-imports@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" + integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-module-transforms@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.5.tgz#120c271c0b3353673fcdfd8c053db3c544a260d6" + integrity sha512-4P+CWMJ6/j1W915ITJaUkadLObmCRRSC234uctJfn/vHrsLNxsR8dwlcXv9ZhJWzl77awf+mWXSZEKt5t0OnlA== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-simple-access" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.5" + lodash "^4.17.19" + "@babel/helper-module-transforms@^7.7.4", "@babel/helper-module-transforms@^7.7.5": version "7.7.5" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.7.5.tgz#d044da7ffd91ec967db25cd6748f704b6b244835" @@ -149,6 +230,13 @@ "@babel/types" "^7.7.4" lodash "^4.17.13" +"@babel/helper-optimise-call-expression@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" + integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== + dependencies: + "@babel/types" "^7.10.4" + "@babel/helper-optimise-call-expression@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.7.4.tgz#034af31370d2995242aa4df402c3b7794b2dcdf2" @@ -179,6 +267,16 @@ "@babel/traverse" "^7.7.4" "@babel/types" "^7.7.4" +"@babel/helper-replace-supers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" + integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/helper-replace-supers@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.7.4.tgz#3c881a6a6a7571275a72d82e6107126ec9e2cdd2" @@ -189,6 +287,14 @@ "@babel/traverse" "^7.7.4" "@babel/types" "^7.7.4" +"@babel/helper-simple-access@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" + integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw== + dependencies: + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/helper-simple-access@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.7.4.tgz#a169a0adb1b5f418cfc19f22586b2ebf58a9a294" @@ -197,6 +303,13 @@ "@babel/template" "^7.7.4" "@babel/types" "^7.7.4" +"@babel/helper-split-export-declaration@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz#2c70576eaa3b5609b24cb99db2888cc3fc4251d1" + integrity sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg== + dependencies: + "@babel/types" "^7.10.4" + "@babel/helper-split-export-declaration@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz#57292af60443c4a3622cf74040ddc28e68336fd8" @@ -204,6 +317,11 @@ dependencies: "@babel/types" "^7.7.4" +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + "@babel/helper-wrap-function@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.7.4.tgz#37ab7fed5150e22d9d7266e830072c0cdd8baace" @@ -214,6 +332,15 @@ "@babel/traverse" "^7.7.4" "@babel/types" "^7.7.4" +"@babel/helpers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" + integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA== + dependencies: + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/helpers@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.7.4.tgz#62c215b9e6c712dadc15a9a0dcab76c92a940302" @@ -231,6 +358,20 @@ esutils "^2.0.2" js-tokens "^4.0.0" +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.10.4", "@babel/parser@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.5.tgz#e7c6bf5a7deff957cec9f04b551e2762909d826b" + integrity sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ== + "@babel/parser@^7.7.4", "@babel/parser@^7.7.5": version "7.7.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.5.tgz#cbf45321619ac12d83363fcf9c94bb67fa646d71" @@ -663,6 +804,15 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/template@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" + integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/template@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.7.4.tgz#428a7d9eecffe27deac0a98e23bf8e3675d2a77b" @@ -672,6 +822,21 @@ "@babel/parser" "^7.7.4" "@babel/types" "^7.7.4" +"@babel/traverse@^7.10.4", "@babel/traverse@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.5.tgz#77ce464f5b258be265af618d8fddf0536f20b564" + integrity sha512-yc/fyv2gUjPqzTz0WHeRJH2pv7jA9kA7mBX2tXl/x5iOE81uaVPuGPtaYk7wmkx4b67mQ7NqI8rmT2pF47KYKQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.10.5" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" + "@babel/parser" "^7.10.5" + "@babel/types" "^7.10.5" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + "@babel/traverse@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.7.4.tgz#9c1e7c60fb679fe4fcfaa42500833333c2058558" @@ -695,6 +860,15 @@ lodash "^4.17.10" to-fast-properties "^2.0.0" +"@babel/types@^7.10.4", "@babel/types@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.5.tgz#d88ae7e2fde86bfbfe851d4d81afa70a997b5d15" + integrity sha512-ixV66KWfCI6GKoA/2H9v6bQdbfXEwwpOdQ8cRvb4F+eyvhlaHxWFMQB4+3d9QFJXZsiiiqVrewNV0DFEQpyT4Q== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@babel/types@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.7.4.tgz#516570d539e44ddf308c07569c258ff94fde9193" @@ -710,6 +884,67 @@ dependencies: qrcode "^1.3.0" +"@nodelib/fs.scandir@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" + integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== + dependencies: + "@nodelib/fs.stat" "2.0.3" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" + integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" + integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + dependencies: + "@nodelib/fs.scandir" "2.1.3" + fastq "^1.6.0" + +"@stylelint/postcss-css-in-js@^0.37.1": + version "0.37.2" + resolved "https://registry.yarnpkg.com/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz#7e5a84ad181f4234a2480803422a47b8749af3d2" + integrity sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA== + dependencies: + "@babel/core" ">=7.9.0" + +"@stylelint/postcss-markdown@^0.36.1": + version "0.36.1" + resolved "https://registry.yarnpkg.com/@stylelint/postcss-markdown/-/postcss-markdown-0.36.1.tgz#829b87e6c0f108014533d9d7b987dc9efb6632e8" + integrity sha512-iDxMBWk9nB2BPi1VFQ+Dc5+XpvODBHw2n3tYpaBZuEAFQlbtF9If0Qh5LTTwSi/XwdbJ2jt+0dis3i8omyggpw== + dependencies: + remark "^12.0.0" + unist-util-find-all-after "^3.0.1" + +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + +"@types/minimist@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" + integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= + +"@types/normalize-package-data@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" + integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/unist@^2.0.0", "@types/unist@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" + integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ== + "@ungap/event-target@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.1.0.tgz#88d527d40de86c4b0c99a060ca241d755999915b" @@ -940,6 +1175,16 @@ ajv@^6.1.0, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.10.2: + version "6.12.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" + integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ajv@^6.5.5: version "6.6.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.6.2.tgz#caceccf474bf3fc3ce3b147443711a24063cc30d" @@ -981,6 +1226,11 @@ ansi-regex@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -991,6 +1241,14 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + ansi-styles@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" @@ -1062,7 +1320,12 @@ array-union@^1.0.1: dependencies: array-uniq "^1.0.1" -array-uniq@^1.0.1, array-uniq@^1.0.2: +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array-uniq@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" @@ -1078,6 +1341,16 @@ arraybuffer.slice@~0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" +arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + +arrify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + asn1.js@^4.0.0: version "4.10.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" @@ -1165,6 +1438,19 @@ autoprefixer@^6.3.1, autoprefixer@^6.4.0: postcss "^5.2.16" postcss-value-parser "^3.2.3" +autoprefixer@^9.8.0: + version "9.8.5" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.5.tgz#2c225de229ddafe1d1424c02791d0c3e10ccccaa" + integrity sha512-C2p5KkumJlsTHoNv9w31NrBRgXhf6eCMteJuHZi2xhkgC+5Vm40MEtCKPhc0qdgAOhox0YPy1SQHTAky05UoKg== + dependencies: + browserslist "^4.12.0" + caniuse-lite "^1.0.30001097" + colorette "^1.2.0" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^7.0.32" + postcss-value-parser "^4.1.0" + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -1328,6 +1614,11 @@ backo2@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" +bail@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" + integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ== + balanced-match@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" @@ -1478,6 +1769,13 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" +braces@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -1546,6 +1844,16 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: caniuse-db "^1.0.30000639" electron-to-chromium "^1.2.7" +browserslist@^4.12.0: + version "4.13.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.13.0.tgz#42556cba011e1b0a2775b611cba6a8eca18e940d" + integrity sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ== + dependencies: + caniuse-lite "^1.0.30001093" + electron-to-chromium "^1.3.488" + escalade "^3.0.1" + node-releases "^1.1.58" + browserslist@^4.6.0, browserslist@^4.8.2: version "4.8.2" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.2.tgz#b45720ad5fbc8713b7253c20766f701c9a694289" @@ -1682,6 +1990,15 @@ camelcase-keys@^2.0.0: camelcase "^2.0.0" map-obj "^1.0.0" +camelcase-keys@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" + integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== + dependencies: + camelcase "^5.3.1" + map-obj "^4.0.0" + quick-lru "^4.0.1" + camelcase@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" @@ -1690,6 +2007,16 @@ camelcase@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e" + integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w== + can-promise@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/can-promise/-/can-promise-0.0.1.tgz#7a7597ad801fb14c8b22341dfec314b6bd6ad8d3" @@ -1714,10 +2041,20 @@ caniuse-lite@^1.0.30001015: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001015.tgz#15a7ddf66aba786a71d99626bc8f2b91c6f0f5f0" integrity sha512-/xL2AbW/XWHNu1gnIrO8UitBGoFthcsDgU9VLK1/dpsoxbaD5LscHozKze05R6WLsBvLhqv78dAPozMFQBYLbQ== +caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001097: + version "1.0.30001107" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001107.tgz#809360df7a5b3458f627aa46b0f6ed6d5239da9a" + integrity sha512-86rCH+G8onCmdN4VZzJet5uPELII59cUzDphko3thQFgAQG1RNa+sVLDoALIhRYmflo5iSIzWY3vu1XTWtNMQQ== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" +ccount@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17" + integrity sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw== + chai-nightwatch@~0.1.x: version "0.1.1" resolved "https://registry.yarnpkg.com/chai-nightwatch/-/chai-nightwatch-0.1.1.tgz#1ca56de768d3c0868fe7fc2f4d32c2fe894e6be9" @@ -1758,6 +2095,14 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^4.0.0, chalk@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" @@ -1766,6 +2111,26 @@ chalk@~0.4.0: has-color "~0.1.0" strip-ansi "~0.1.0" +character-entities-html4@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.4.tgz#0e64b0a3753ddbf1fdc044c5fd01d0199a02e125" + integrity sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g== + +character-entities-legacy@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" + integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== + +character-entities@^1.0.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" + integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== + +character-reference-invalid@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" + integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -1880,6 +2245,13 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +clone-regexp@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f" + integrity sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q== + dependencies: + is-regexp "^2.0.0" + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -1898,6 +2270,11 @@ code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" +collapse-white-space@^1.0.2: + version "1.0.6" + resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" + integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ== + collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -1911,10 +2288,22 @@ color-convert@^1.3.0, color-convert@^1.9.0: dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-name@1.1.3, color-name@^1.0.0: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + color-string@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" @@ -1929,6 +2318,11 @@ color@^0.11.0: color-convert "^1.3.0" color-string "^0.3.0" +colorette@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" + integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== + colormin@^1.0.5: version "1.1.2" resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133" @@ -2123,6 +2517,17 @@ cosmiconfig@^5.0.0: js-yaml "^3.13.1" parse-json "^4.0.0" +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.7.2" + create-ecdh@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" @@ -2236,6 +2641,11 @@ cssesc@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + cssnano@^3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38" @@ -2327,6 +2737,7 @@ dateformat@^1.0.6: de-indent@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" + integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0= debug@2, debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: version "2.6.9" @@ -2364,13 +2775,21 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -debug@^4.0.1, debug@^4.1.0: +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" dependencies: ms "^2.1.1" -decamelize@^1.1.2, decamelize@^1.2.0: +decamelize-keys@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" + integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= + dependencies: + decamelize "^1.1.0" + map-obj "^1.0.0" + +decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -2501,6 +2920,13 @@ dijkstrajs@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.1.tgz#d3cd81221e3ea40742cfcde556d4e99e98ddc71b" +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + doctrine@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" @@ -2544,7 +2970,7 @@ domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" -domelementtype@1, domelementtype@^1.3.0: +domelementtype@1, domelementtype@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" @@ -2561,6 +2987,7 @@ domhandler@2.1: domhandler@^2.3.0: version "2.4.2" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== dependencies: domelementtype "1" @@ -2580,6 +3007,7 @@ domutils@1.5.1: domutils@^1.5.1: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== dependencies: dom-serializer "0" domelementtype "1" @@ -2624,6 +3052,11 @@ electron-to-chromium@^1.3.322: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.322.tgz#a6f7e1c79025c2b05838e8e344f6e89eb83213a8" integrity sha512-Tc8JQEfGQ1MzfSzI/bTlSr7btJv/FFO7Yh6tanqVmIWOuNCu6/D1MilIEgLtmWqIrsv+o4IjpLAhgMBr/ncNAA== +electron-to-chromium@^1.3.488: + version "1.3.509" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.509.tgz#830fcb89cd66dc2984d18d794973b99e3f00584c" + integrity sha512-cN4lkjNRuTG8rtAqTOVgwpecEC2kbKA04PG6YijcKGHK/kD0xLjiqExcAOmLUwtXZRF8cBeam2I0VZcih919Ug== + elliptic@^6.0.0: version "6.4.1" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.1.tgz#c2d0b7776911b86722c632c3c06c60f2f819939a" @@ -2640,6 +3073,11 @@ emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" @@ -2757,6 +3195,11 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" +escalade@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4" + integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ== + escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -3016,6 +3459,13 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execall@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/execall/-/execall-2.0.0.tgz#16a06b5fe5099df7d00be5d9c06eecded1663b45" + integrity sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow== + dependencies: + clone-regexp "^2.1.0" + exit-hook@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" @@ -3155,6 +3605,23 @@ fast-deep-equal@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.1.1: + version "3.2.4" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" + integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" @@ -3167,6 +3634,13 @@ fastparse@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" +fastq@^1.6.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" + integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== + dependencies: + reusify "^1.0.4" + fd-slicer@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" @@ -3223,6 +3697,13 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + finalhandler@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" @@ -3282,6 +3763,14 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + flat-cache@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" @@ -3445,6 +3934,11 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" +gensync@^1.0.0-beta.1: + version "1.0.0-beta.1" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" + integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== + get-caller-file@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" @@ -3453,6 +3947,11 @@ get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" +get-stdin@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" + integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== + get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -3500,6 +3999,13 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" +glob-parent@^5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + glob@7.0.5: version "7.0.5" resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.5.tgz#b4202a69099bbb4d292a7c1b95b6682b67ebdc95" @@ -3554,6 +4060,22 @@ glob@^7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" +global-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + globals@^11.1.0, globals@^11.7.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -3562,6 +4084,18 @@ globals@^9.18.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" +globby@^11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" + integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -3572,6 +4106,18 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" +globjoin@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" + integrity sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM= + +gonzales-pe@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3" + integrity sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ== + dependencies: + minimist "^1.2.5" + graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: version "4.1.15" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" @@ -3610,6 +4156,11 @@ har-validator@~5.1.0: ajv "^6.5.5" har-schema "^2.0.0" +hard-rejection@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" + integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -3638,6 +4189,11 @@ has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + has-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" @@ -3749,6 +4305,11 @@ html-tags@^2.0.0: resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-2.0.0.tgz#10b30a386085f43cede353cc8fa7cb0deeea668b" integrity sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos= +html-tags@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" + integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg== + html-webpack-plugin@^3.0.0, html-webpack-plugin@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b" @@ -3762,15 +4323,16 @@ html-webpack-plugin@^3.0.0, html-webpack-plugin@^3.2.0: util.promisify "1.0.0" htmlparser2@^3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464" + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== dependencies: - domelementtype "^1.3.0" + domelementtype "^1.3.1" domhandler "^2.3.0" domutils "^1.5.1" entities "^1.1.1" inherits "^2.0.1" - readable-stream "^3.0.6" + readable-stream "^3.1.1" htmlparser2@~3.3.0: version "3.3.0" @@ -3883,6 +4445,11 @@ ignore@^4.0.2, ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" +ignore@^5.1.4, ignore@^5.1.8: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -3909,6 +4476,14 @@ import-fresh@^3.0.0: parent-module "^1.0.0" resolve-from "^4.0.0" +import-fresh@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" + integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + import-from@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" @@ -3916,6 +4491,11 @@ import-from@^2.1.0: dependencies: resolve-from "^3.0.0" +import-lazy@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" + integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -3926,6 +4506,11 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" @@ -3949,12 +4534,12 @@ inherits@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" -inherits@~2.0.0: +inherits@^2.0.0, inherits@~2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@~1.3.0: +ini@^1.3.5, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" @@ -4028,6 +4613,24 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" +is-alphabetical@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" + integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== + +is-alphanumeric@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz#4a9cef71daf4c001c1d81d63d140cf53fd6889f4" + integrity sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ= + +is-alphanumerical@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf" + integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A== + dependencies: + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -4042,6 +4645,11 @@ is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" +is-buffer@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" + integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== + is-builtin-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" @@ -4068,6 +4676,11 @@ is-date-object@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" +is-decimal@^1.0.0, is-decimal@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" + integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== + is-descriptor@^0.1.0: version "0.1.6" resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" @@ -4132,6 +4745,11 @@ is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-glob@^2.0.0, is-glob@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" @@ -4144,12 +4762,17 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0: +is-glob@^4.0.0, is-glob@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" dependencies: is-extglob "^2.1.1" +is-hexadecimal@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" + integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== + is-number@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-number/-/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806" @@ -4170,6 +4793,11 @@ is-number@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + is-path-cwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" @@ -4186,10 +4814,15 @@ is-path-inside@^1.0.0: dependencies: path-is-inside "^1.0.1" -is-plain-obj@^1.0.0: +is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" +is-plain-obj@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -4214,6 +4847,11 @@ is-regex@^1.0.4: dependencies: has "^1.0.1" +is-regexp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d" + integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA== + is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -4230,7 +4868,7 @@ is-symbol@^1.0.2: dependencies: has-symbols "^1.0.0" -is-typedarray@~1.0.0: +is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -4242,10 +4880,20 @@ is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" +is-whitespace-character@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7" + integrity sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w== + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" +is-word-character@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230" + integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA== + is-wsl@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" @@ -4441,6 +5089,13 @@ json5@^2.1.0: dependencies: minimist "^1.2.0" +json5@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + dependencies: + minimist "^1.2.5" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -4556,12 +5211,27 @@ kind-of@^6.0.0, kind-of@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" +kind-of@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +known-css-properties@^0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.19.0.tgz#5d92b7fa16c72d971bda9b7fe295bdf61836ee5b" + integrity sha512-eYboRV94Vco725nKMlpkn3nV2+96p9c3gKXRsYqAJSswSENvBhN7n5L+uDhY58xQa0UukWsDMTGELzmD8Q+wTA== + lcid@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" dependencies: invert-kv "^2.0.0" +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + levn@^0.3.0, levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" @@ -4575,6 +5245,11 @@ lie@3.1.1: dependencies: immediate "~3.0.5" +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + listenercount@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" @@ -4647,6 +5322,13 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + lodash._arraycopy@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1" @@ -4756,10 +5438,6 @@ lodash.clone@3.0.3: lodash._bindcallback "^3.0.0" lodash._isiterateecall "^3.0.0" -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - lodash.create@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" @@ -4779,10 +5457,6 @@ lodash.defaultsdeep@4.3.2: lodash.mergewith "^4.0.0" lodash.rest "^4.0.0" -lodash.escaperegexp@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" - lodash.find@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-3.2.1.tgz#046e319f3ace912ac6c9246c7f683c5ec07b36ad" @@ -4814,14 +5488,10 @@ lodash.isplainobject@^3.0.0, lodash.isplainobject@^3.2.0: lodash.isarguments "^3.0.0" lodash.keysin "^3.0.0" -lodash.isplainobject@^4.0.0, lodash.isplainobject@^4.0.6: +lodash.isplainobject@^4.0.0: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - lodash.istypedarray@^3.0.0: version "3.0.6" resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" @@ -4870,7 +5540,7 @@ lodash.merge@^3.3.2: lodash.keysin "^3.0.0" lodash.toplainobject "^3.0.0" -lodash.mergewith@^4.0.0, lodash.mergewith@^4.6.1: +lodash.mergewith@^4.0.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" @@ -4912,18 +5582,30 @@ lodash@^4.17.13: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + log-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" dependencies: chalk "^1.0.0" -log-symbols@^2.1.0: +log-symbols@^2.1.0, log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" dependencies: chalk "^2.0.1" +log-symbols@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== + dependencies: + chalk "^4.0.0" + log4js@^3.0.0: version "3.0.6" resolved "https://registry.yarnpkg.com/log4js/-/log4js-3.0.6.tgz#e6caced94967eeeb9ce399f9f8682a4b2b28c8ff" @@ -4938,6 +5620,11 @@ lolex@^1.4.0, lolex@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6" +longest-streak@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" + integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== + loose-envify@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -4997,12 +5684,29 @@ map-obj@^1.0.0, map-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" +map-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5" + integrity sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g== + map-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" dependencies: object-visit "^1.0.0" +markdown-escapes@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" + integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== + +markdown-table@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b" + integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A== + dependencies: + repeat-string "^1.0.0" + math-expression-evaluator@^1.2.14: version "1.2.17" resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" @@ -5011,6 +5715,11 @@ math-random@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac" +mathml-tag-names@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" + integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -5019,6 +5728,13 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +mdast-util-compact@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-compact/-/mdast-util-compact-2.0.1.tgz#cabc69a2f43103628326f35b1acf735d55c99490" + integrity sha512-7GlnT24gEwDrdAwEHrU4Vv5lLWrEer4KOkAiKT9nYstsTad7Oc1TwqT2zIMKRdZF7cTuaf+GA1E4Kv7jJh8mPA== + dependencies: + unist-util-visit "^2.0.0" + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -5053,10 +5769,34 @@ meow@^3.3.0: redent "^1.0.0" trim-newlines "^1.0.0" +meow@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/meow/-/meow-7.0.1.tgz#1ed4a0a50b3844b451369c48362eb0515f04c1dc" + integrity sha512-tBKIQqVrAHqwit0vfuFPY3LlzJYkEOFyKa3bPgxzNl6q/RtN8KQ+ALYEASYuFayzSAsjlhXj/JZ10rH85Q6TUw== + dependencies: + "@types/minimist" "^1.2.0" + arrify "^2.0.1" + camelcase "^6.0.0" + camelcase-keys "^6.2.2" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "^4.0.2" + normalize-package-data "^2.5.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.13.1" + yargs-parser "^18.1.3" + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -5097,6 +5837,14 @@ micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8: snapdragon "^0.8.1" to-regex "^3.0.2" +micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -5136,6 +5884,11 @@ mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + mini-css-extract-plugin@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz#ac0059b02b9692515a637115b0cc9fed3a35c7b0" @@ -5164,6 +5917,15 @@ minimatch@3.0.3: dependencies: brace-expansion "^1.0.0" +minimist-options@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" + integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + kind-of "^6.0.3" + minimist@0.0.8, minimist@~0.0.1: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" @@ -5172,6 +5934,11 @@ minimist@1.2.0, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + minipass@^2.2.1, minipass@^2.3.4: version "2.3.5" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" @@ -5417,6 +6184,11 @@ node-releases@^1.1.42: dependencies: semver "^6.3.0" +node-releases@^1.1.58: + version "1.1.60" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" + integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA== + nomnomnomnom@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/nomnomnomnom/-/nomnomnomnom-2.0.1.tgz#b2239f031c8d04da67e32836e1e3199e12f7a8e2" @@ -5446,6 +6218,16 @@ normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" +normalize-package-data@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + normalize-path@^2.0.1, normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" @@ -5460,6 +6242,11 @@ normalize-range@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" +normalize-selector@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03" + integrity sha1-0LFF62kRicY6eNIB3E/bEpPvDAM= + normalize-url@^1.4.0: version "1.9.1" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" @@ -5537,10 +6324,6 @@ object-keys@^1.0.11, object-keys@^1.0.12: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" -object-path@^0.11.3: - version "0.11.4" - resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.4.tgz#370ae752fbf37de3ea70a861c23bba8915691949" - object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" @@ -5684,6 +6467,13 @@ p-limit@^2.0.0: dependencies: p-try "^2.0.0" +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -5696,6 +6486,13 @@ p-locate@^3.0.0: dependencies: p-limit "^2.0.0" +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + p-map@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" @@ -5767,6 +6564,18 @@ parse-asn1@^5.0.0: pbkdf2 "^3.0.3" safe-buffer "^5.1.1" +parse-entities@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" + integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ== + dependencies: + character-entities "^1.0.0" + character-entities-legacy "^1.0.0" + character-reference-invalid "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.0" + is-hexadecimal "^1.0.0" + parse-glob@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" @@ -5790,6 +6599,23 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" +parse-json@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.1.tgz#7cfe35c1ccd641bce3981467e6c2ece61b3b3878" + integrity sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + lines-and-columns "^1.1.6" + +parse-link-header@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-link-header/-/parse-link-header-1.0.1.tgz#bedfe0d2118aeb84be75e7b025419ec8a61140a7" + integrity sha1-vt/g0hGK64S+deewJUGeyKYRQKc= + dependencies: + xtend "~4.0.1" + parseqs@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" @@ -5828,6 +6654,11 @@ path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -5868,6 +6699,11 @@ path-type@^2.0.0: dependencies: pify "^2.0.0" +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + pbkdf2@^3.0.3: version "3.0.17" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" @@ -5890,6 +6726,11 @@ phoenix@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.4.0.tgz#9cec8dbd8cbc59ecd2147bc09ca8ceb56b860d75" +picomatch@^2.0.5, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -6009,6 +6850,20 @@ postcss-filter-plugins@^2.0.0: dependencies: postcss "^5.0.4" +postcss-html@^0.36.0: + version "0.36.0" + resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.36.0.tgz#b40913f94eaacc2453fd30a1327ad6ee1f88b204" + integrity sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw== + dependencies: + htmlparser2 "^3.10.0" + +postcss-less@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-3.1.4.tgz#369f58642b5928ef898ffbc1a6e93c958304c5ad" + integrity sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA== + dependencies: + postcss "^7.0.14" + postcss-load-config@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" @@ -6050,6 +6905,11 @@ postcss-loader@^3.0.0: postcss-load-config "^2.0.0" schema-utils "^1.0.0" +postcss-media-query-parser@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" + integrity sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ= + postcss-merge-idents@^2.1.5: version "2.1.7" resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270" @@ -6181,6 +7041,52 @@ postcss-reduce-transforms@^1.0.3: postcss "^5.0.8" postcss-value-parser "^3.0.1" +postcss-reporter@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-6.0.1.tgz#7c055120060a97c8837b4e48215661aafb74245f" + integrity sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw== + dependencies: + chalk "^2.4.1" + lodash "^4.17.11" + log-symbols "^2.2.0" + postcss "^7.0.7" + +postcss-resolve-nested-selector@0.1.1, postcss-resolve-nested-selector@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e" + integrity sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4= + +postcss-safe-parser@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz#a6d4e48f0f37d9f7c11b2a581bf00f8ba4870b96" + integrity sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g== + dependencies: + postcss "^7.0.26" + +postcss-sass@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.4.4.tgz#91f0f3447b45ce373227a98b61f8d8f0785285a3" + integrity sha512-BYxnVYx4mQooOhr+zer0qWbSPYnarAy8ZT7hAQtbxtgVf8gy+LSLT/hHGe35h14/pZDTw1DsxdbrwxBN++H+fg== + dependencies: + gonzales-pe "^4.3.0" + postcss "^7.0.21" + +postcss-scss@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-2.1.1.tgz#ec3a75fa29a55e016b90bf3269026c53c1d2b383" + integrity sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA== + dependencies: + postcss "^7.0.6" + +postcss-selector-parser@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.1.tgz#fdbf696103b12b0a64060e5610507f410491f7c8" + integrity sha1-/b9pYQOxKwpkBg5WEFB/QQSR98g= + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2: version "2.2.3" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90" @@ -6189,6 +7095,15 @@ postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2: indexes-of "^1.0.1" uniq "^1.0.1" +postcss-selector-parser@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c" + integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg== + dependencies: + cssesc "^3.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + postcss-svgo@^2.1.1: version "2.1.6" resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" @@ -6198,6 +7113,11 @@ postcss-svgo@^2.1.1: postcss-value-parser "^3.2.3" svgo "^0.7.0" +postcss-syntax@^0.36.2: + version "0.36.2" + resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.36.2.tgz#f08578c7d95834574e5593a82dfbfa8afae3b51c" + integrity sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w== + postcss-unique-selectors@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d" @@ -6210,6 +7130,11 @@ postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^ version "3.3.1" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" +postcss-value-parser@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" + integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== + postcss-zindex@^2.0.1: version "2.2.0" resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22" @@ -6244,13 +7169,14 @@ postcss@^7.0.0: source-map "^0.6.1" supports-color "^6.1.0" -postcss@^7.0.5: - version "7.0.8" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.8.tgz#2a3c5f2bdd00240cd0d0901fd998347c93d36696" +postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.6, postcss@^7.0.7: + version "7.0.32" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" + integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== dependencies: chalk "^2.4.2" source-map "^0.6.1" - supports-color "^6.0.0" + supports-color "^6.1.0" prelude-ls@~1.1.2: version "1.1.2" @@ -6409,6 +7335,11 @@ querystring@0.2.0, querystring@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" +quick-lru@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" + integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== + randomatic@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" @@ -6474,6 +7405,15 @@ read-pkg-up@^2.0.0: find-up "^2.0.0" read-pkg "^2.0.0" +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + read-pkg@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" @@ -6490,6 +7430,16 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" + "readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" @@ -6520,9 +7470,10 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^3.0.6: - version "3.1.1" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.1.1.tgz#ed6bbc6c5ba58b090039ff18ce670515795aeb06" +readable-stream@^3.1.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" @@ -6549,6 +7500,14 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + reduce-css-calc@^1.2.6: version "1.3.0" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" @@ -6653,6 +7612,57 @@ relateurl@0.2.x: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" +remark-parse@^8.0.0: + version "8.0.3" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-8.0.3.tgz#9c62aa3b35b79a486454c690472906075f40c7e1" + integrity sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q== + dependencies: + ccount "^1.0.0" + collapse-white-space "^1.0.2" + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + is-whitespace-character "^1.0.0" + is-word-character "^1.0.0" + markdown-escapes "^1.0.0" + parse-entities "^2.0.0" + repeat-string "^1.5.4" + state-toggle "^1.0.0" + trim "0.0.1" + trim-trailing-lines "^1.0.0" + unherit "^1.0.4" + unist-util-remove-position "^2.0.0" + vfile-location "^3.0.0" + xtend "^4.0.1" + +remark-stringify@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-8.1.1.tgz#e2a9dc7a7bf44e46a155ec78996db896780d8ce5" + integrity sha512-q4EyPZT3PcA3Eq7vPpT6bIdokXzFGp9i85igjmhRyXWmPs0Y6/d2FYwUNotKAWyLch7g0ASZJn/KHHcHZQ163A== + dependencies: + ccount "^1.0.0" + is-alphanumeric "^1.0.0" + is-decimal "^1.0.0" + is-whitespace-character "^1.0.0" + longest-streak "^2.0.1" + markdown-escapes "^1.0.0" + markdown-table "^2.0.0" + mdast-util-compact "^2.0.0" + parse-entities "^2.0.0" + repeat-string "^1.5.4" + state-toggle "^1.0.0" + stringify-entities "^3.0.0" + unherit "^1.0.4" + xtend "^4.0.1" + +remark@^12.0.0: + version "12.0.1" + resolved "https://registry.yarnpkg.com/remark/-/remark-12.0.1.tgz#f1ddf68db7be71ca2bad0a33cd3678b86b9c709f" + integrity sha512-gS7HDonkdIaHmmP/+shCPejCEEW+liMp/t/QwmF0Xt47Rpuhl32lLtDV1uKWvGoq+kxr5jSgg5oAIpGuyULjUw== + dependencies: + remark-parse "^8.0.0" + remark-stringify "^8.0.0" + unified "^9.0.0" + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -6675,7 +7685,7 @@ repeat-string@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-0.2.2.tgz#c7a8d3236068362059a7e4651fc6884e8b1fb4ae" -repeat-string@^1.5.2, repeat-string@^1.6.1: +repeat-string@^1.0.0, repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" @@ -6685,6 +7695,11 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" +replace-ext@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" + integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs= + request@^2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" @@ -6739,6 +7754,11 @@ resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -6778,6 +7798,11 @@ ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + rfdc@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" @@ -6808,6 +7833,11 @@ run-async@^2.2.0: dependencies: is-promise "^2.1.0" +run-parallel@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" + integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== + run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" @@ -6824,6 +7854,11 @@ safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, s version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -6838,21 +7873,6 @@ samsam@1.x, samsam@^1.1.3: version "1.3.0" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" -sanitize-html@^1.13.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.20.0.tgz#9a602beb1c9faf960fb31f9890f61911cc4d9156" - dependencies: - chalk "^2.4.1" - htmlparser2 "^3.10.0" - lodash.clonedeep "^4.5.0" - lodash.escaperegexp "^4.1.2" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.mergewith "^4.6.1" - postcss "^7.0.5" - srcset "^1.0.0" - xtend "^4.0.1" - "sass-loader@git://github.com/webpack-contrib/sass-loader": version "7.1.0" resolved "git://github.com/webpack-contrib/sass-loader#e279f2a129eee0bd0b624b5acd498f23a81ee35e" @@ -7032,6 +8052,11 @@ slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + slice-ansi@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" @@ -7214,6 +8239,11 @@ spdx-license-ids@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz#81c0ce8f21474756148bbb5f3bfc0f36bf15d76e" +specificity@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.1.tgz#aab5e645012db08ba182e151165738d00887b019" + integrity sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg== + split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" @@ -7224,13 +8254,6 @@ sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" -srcset@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/srcset/-/srcset-1.0.0.tgz#a5669de12b42f3b1d5e83ed03c71046fc48f41ef" - dependencies: - array-uniq "^1.0.2" - number-is-nan "^1.0.0" - sshpk@^1.7.0: version "1.16.0" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.0.tgz#1d4963a2fbffe58050aa9084ca20be81741c07de" @@ -7251,6 +8274,11 @@ ssri@^6.0.1: dependencies: figgy-pudding "^3.5.1" +state-toggle@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" + integrity sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ== + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -7330,12 +8358,28 @@ string-width@^3.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string_decoder@^1.0.0, string_decoder@^1.1.1: +string-width@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string_decoder@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" dependencies: safe-buffer "~5.1.0" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -7346,6 +8390,17 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +stringify-entities@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-3.0.1.tgz#32154b91286ab0869ab2c07696223bd23b6dbfc0" + integrity sha512-Lsk3ISA2++eJYqBMPKcr/8eby1I6L0gP0NlxF8Zja6c05yr/yCYyb2c9PwXjd08Ib3If1vn1rbs1H5ZtVuOfvQ== + dependencies: + character-entities-html4 "^1.0.0" + character-entities-legacy "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.2" + is-hexadecimal "^1.0.0" + strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -7364,6 +8419,13 @@ strip-ansi@^5.1.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + strip-ansi@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" @@ -7388,10 +8450,103 @@ strip-indent@^1.0.1: dependencies: get-stdin "^4.0.1" +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" +style-search@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" + integrity sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI= + +stylelint-config-recommended@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-3.0.0.tgz#e0e547434016c5539fe2650afd58049a2fd1d657" + integrity sha512-F6yTRuc06xr1h5Qw/ykb2LuFynJ2IxkKfCMf+1xqPffkxh0S09Zc902XCffcsw/XMFq/OzQ1w54fLIDtmRNHnQ== + +stylelint-config-standard@^20.0.0: + version "20.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-20.0.0.tgz#06135090c9e064befee3d594289f50e295b5e20d" + integrity sha512-IB2iFdzOTA/zS4jSVav6z+wGtin08qfj+YyExHB3LF9lnouQht//YyB0KZq9gGz5HNPkddHOzcY8HsUey6ZUlA== + dependencies: + stylelint-config-recommended "^3.0.0" + +stylelint-rscss@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/stylelint-rscss/-/stylelint-rscss-0.4.0.tgz#ea77c478e1c703dbda878c00f53bbc002b2d07b7" + integrity sha1-6nfEeOHHA9vah4wA9Tu8ACstB7c= + dependencies: + postcss-resolve-nested-selector "0.1.1" + postcss-selector-parser "2.2.1" + +stylelint@^13.6.1: + version "13.6.1" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.6.1.tgz#cc1d76338116d55e8ff2be94c4a4386c1239b878" + integrity sha512-XyvKyNE7eyrqkuZ85Citd/Uv3ljGiuYHC6UiztTR6sWS9rza8j3UeQv/eGcQS9NZz/imiC4GKdk1EVL3wst5vw== + dependencies: + "@stylelint/postcss-css-in-js" "^0.37.1" + "@stylelint/postcss-markdown" "^0.36.1" + autoprefixer "^9.8.0" + balanced-match "^1.0.0" + chalk "^4.1.0" + cosmiconfig "^6.0.0" + debug "^4.1.1" + execall "^2.0.0" + file-entry-cache "^5.0.1" + get-stdin "^8.0.0" + global-modules "^2.0.0" + globby "^11.0.1" + globjoin "^0.1.4" + html-tags "^3.1.0" + ignore "^5.1.8" + import-lazy "^4.0.0" + imurmurhash "^0.1.4" + known-css-properties "^0.19.0" + leven "^3.1.0" + lodash "^4.17.15" + log-symbols "^4.0.0" + mathml-tag-names "^2.1.3" + meow "^7.0.1" + micromatch "^4.0.2" + normalize-selector "^0.2.0" + postcss "^7.0.32" + postcss-html "^0.36.0" + postcss-less "^3.1.4" + postcss-media-query-parser "^0.2.3" + postcss-reporter "^6.0.1" + postcss-resolve-nested-selector "^0.1.1" + postcss-safe-parser "^4.0.2" + postcss-sass "^0.4.4" + postcss-scss "^2.1.1" + postcss-selector-parser "^6.0.2" + postcss-syntax "^0.36.2" + postcss-value-parser "^4.1.0" + resolve-from "^5.0.0" + slash "^3.0.0" + specificity "^0.4.1" + string-width "^4.2.0" + strip-ansi "^6.0.0" + style-search "^0.1.0" + sugarss "^2.0.0" + svg-tags "^1.0.0" + table "^5.4.6" + v8-compile-cache "^2.1.1" + write-file-atomic "^3.0.3" + +sugarss@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-2.0.0.tgz#ddd76e0124b297d40bf3cca31c8b22ecb43bc61d" + integrity sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ== + dependencies: + postcss "^7.0.2" + supports-color@3.1.2, supports-color@^3.1.0: version "3.1.2" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" @@ -7414,12 +8569,19 @@ supports-color@^5.3.0, supports-color@^5.4.0: dependencies: has-flag "^3.0.0" -supports-color@^6.0.0, supports-color@^6.1.0: +supports-color@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + svg-tags@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" @@ -7446,6 +8608,16 @@ table@^5.2.3: slice-ansi "^2.1.0" string-width "^3.0.0" +table@^5.4.6: + version "5.4.6" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== + dependencies: + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" + tapable@^1.0.0, tapable@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" @@ -7561,6 +8733,13 @@ to-regex-range@^2.1.0: is-number "^3.0.0" repeat-string "^1.6.1" +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + to-regex@^3.0.1, to-regex@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" @@ -7590,10 +8769,30 @@ trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" +trim-newlines@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" + integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA== + trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" +trim-trailing-lines@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz#7f0739881ff76657b7776e10874128004b625a94" + integrity sha512-4ku0mmjXifQcTVfYDfR5lpgV7zVqPg6zV9rdZmwOPqq0+Zq19xDqEgagqVbc4pOOShbncuAOIs59R3+3gcF3ZA== + +trim@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" + integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0= + +trough@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" + integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== + tslib@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" @@ -7630,6 +8829,21 @@ type-detect@^4.0.0: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" +type-fest@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" + integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== + +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + type-is@~1.6.16: version "1.6.16" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" @@ -7637,6 +8851,13 @@ type-is@~1.6.16: media-typer "0.3.0" mime-types "~2.1.18" +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -7656,6 +8877,14 @@ underscore@~1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" +unherit@^1.0.4: + version "1.1.3" + resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22" + integrity sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ== + dependencies: + inherits "^2.0.0" + xtend "^4.0.0" + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -7679,6 +8908,18 @@ unicode-property-aliases-ecmascript@^1.0.4: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57" integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw== +unified@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/unified/-/unified-9.1.0.tgz#7ba82e5db4740c47a04e688a9ca8335980547410" + integrity sha512-VXOv7Ic6twsKGJDeZQ2wwPqXs2hM0KNu5Hkg9WgAZbSD1pxhZ7p8swqg583nw1Je2fhwHy6U8aEjiI79x1gvag== + dependencies: + bail "^1.0.0" + extend "^3.0.0" + is-buffer "^2.0.0" + is-plain-obj "^2.0.0" + trough "^1.0.0" + vfile "^4.0.0" + union-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" @@ -7708,6 +8949,49 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" +unist-util-find-all-after@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/unist-util-find-all-after/-/unist-util-find-all-after-3.0.1.tgz#95cc62f48812d879b4685a0512bf1b838da50e9a" + integrity sha512-0GICgc++sRJesLwEYDjFVJPJttBpVQaTNgc6Jw0Jhzvfs+jtKePEMu+uD+PqkRUrAvGQqwhpDwLGWo1PK8PDEw== + dependencies: + unist-util-is "^4.0.0" + +unist-util-is@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.0.2.tgz#c7d1341188aa9ce5b3cff538958de9895f14a5de" + integrity sha512-Ofx8uf6haexJwI1gxWMGg6I/dLnF2yE+KibhD3/diOqY2TinLcqHXCV6OI5gFVn3xQqDH+u0M625pfKwIwgBKQ== + +unist-util-remove-position@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz#5d19ca79fdba712301999b2b73553ca8f3b352cc" + integrity sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA== + dependencies: + unist-util-visit "^2.0.0" + +unist-util-stringify-position@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da" + integrity sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g== + dependencies: + "@types/unist" "^2.0.2" + +unist-util-visit-parents@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.0.tgz#4dd262fb9dcfe44f297d53e882fc6ff3421173d5" + integrity sha512-0g4wbluTF93npyPrp/ymd3tCDTMnP0yo2akFD2FIBAYXq/Sga3lwaU1D8OYKbtpioaI6CkDcQ6fsMnmtzt7htw== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^4.0.0" + +unist-util-visit@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c" + integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^4.0.0" + unist-util-visit-parents "^3.0.0" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -7818,6 +9102,11 @@ v-click-outside@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/v-click-outside/-/v-click-outside-2.1.3.tgz#b7297abe833a439dc0895e6418a494381e64b5e7" +v8-compile-cache@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" + integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -7841,6 +9130,30 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vfile-location@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-3.0.1.tgz#d78677c3546de0f7cd977544c367266764d31bb3" + integrity sha512-yYBO06eeN/Ki6Kh1QAkgzYpWT1d3Qln+ZCtSbJqFExPl1S3y2qqotJQXoh6qEvl/jDlgpUJolBn3PItVnnZRqQ== + +vfile-message@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a" + integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ== + dependencies: + "@types/unist" "^2.0.0" + unist-util-stringify-position "^2.0.0" + +vfile@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.1.1.tgz#282d28cebb609183ac51703001bc18b3e3f17de9" + integrity sha512-lRjkpyDGjVlBA7cDQhQ+gNcvB1BGaTHYuSOcY3S7OhDmBtnzX95FhtZZDecSTDm6aajFymyve6S5DN4ZHGezdQ== + dependencies: + "@types/unist" "^2.0.0" + is-buffer "^2.0.0" + replace-ext "1.0.0" + unist-util-stringify-position "^2.0.0" + vfile-message "^2.0.0" + vm-browserify@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" @@ -7903,9 +9216,10 @@ vue-style-loader@^4.0.0, vue-style-loader@^4.0.1: hash-sum "^1.0.2" loader-utils "^1.0.2" -vue-template-compiler@^2.3.4: - version "2.5.21" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.21.tgz#a57ceb903177e8f643560a8d639a0f8db647054a" +vue-template-compiler@^2.6.11: + version "2.6.11" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz#c04704ef8f498b153130018993e56309d4698080" + integrity sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA== dependencies: de-indent "^1.0.2" he "^1.1.0" @@ -7914,9 +9228,10 @@ vue-template-es2015-compiler@^1.6.0: version "1.9.1" resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" -vue@^2.5.13: - version "2.5.21" - resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.21.tgz#3d33dcd03bb813912ce894a8303ab553699c4a85" +vue@^2.6.11: + version "2.6.11" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5" + integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ== vuelidate@^0.7.4: version "0.7.4" @@ -8012,10 +9327,6 @@ webpack@^4.0.0: watchpack "^1.5.0" webpack-sources "^1.3.0" -whatwg-fetch@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" - whet.extend@~0.9.9: version "0.9.9" resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" @@ -8024,7 +9335,7 @@ which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" -which@^1.0.9, which@^1.1.1, which@^1.2.9: +which@^1.0.9, which@^1.1.1, which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" dependencies: @@ -8065,6 +9376,16 @@ wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" +write-file-atomic@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + write@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" @@ -8087,10 +9408,15 @@ xregexp@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" -xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: +xtend@^4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" +xtend@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + "y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" @@ -8103,6 +9429,11 @@ yallist@^3.0.0, yallist@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" +yaml@^1.7.2: + version "1.10.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" + integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== + yargs-parser@^11.1.1: version "11.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" @@ -8110,6 +9441,14 @@ yargs-parser@^11.1.1: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^18.1.3: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs@^12.0.5: version "12.0.5" resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"