logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: d0c4ad22cd5a93f69c689f3c8c75546c35861740
parent 819b76026101ddc0363118f240049a0019ebb4d6
Author: Ilja <ilja@ilja.space>
Date:   Sat, 26 Feb 2022 02:08:13 +0100

Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma-fe into feat/report-notification

Diffstat:

M.babelrc4++--
A.mailmap2++
MCHANGELOG.md85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
MCONTRIBUTORS.md1+
Mbuild/dev-server.js1+
Mbuild/webpack.base.conf.js14++++++++++++++
Mconfig/index.js5+++++
Mpackage.json11++++++-----
Msrc/App.js14++++++++++----
Msrc/App.scss90++++++++++++++++++++++++++++++++-----------------------------------------------
Msrc/App.vue7++++---
Msrc/boot/after_store.js3++-
Msrc/boot/routes.js4++--
Msrc/components/account_actions/account_actions.vue25++++++++++---------------
Msrc/components/attachment/attachment.js114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Asrc/components/attachment/attachment.scss268+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/attachment/attachment.vue524+++++++++++++++++++++++++++++++++++--------------------------------------------
Msrc/components/basic_user_card/basic_user_card.js4+++-
Msrc/components/basic_user_card/basic_user_card.vue14++++----------
Msrc/components/chat/chat.js10+++++++++-
Msrc/components/chat/chat.scss6+++---
Msrc/components/chat_list/chat_list.vue5+----
Msrc/components/chat_list_item/chat_list_item.js8+++++---
Msrc/components/chat_list_item/chat_list_item.scss9+++------
Msrc/components/chat_list_item/chat_list_item.vue3++-
Msrc/components/chat_message/chat_message.js5+++--
Msrc/components/chat_message/chat_message.scss17+++++++++++------
Msrc/components/chat_message/chat_message.vue33++++++++++++++++++---------------
Msrc/components/chat_message_date/chat_message_date.vue4+++-
Dsrc/components/chat_panel/chat_panel.js41-----------------------------------------
Dsrc/components/chat_panel/chat_panel.vue143-------------------------------------------------------------------------------
Msrc/components/chat_title/chat_title.js2+-
Msrc/components/chat_title/chat_title.vue18++++++------------
Msrc/components/conversation/conversation.vue1-
Msrc/components/desktop_nav/desktop_nav.vue1+
Msrc/components/domain_mute_card/domain_mute_card.vue4++--
Msrc/components/emoji_input/emoji_input.js68++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/components/emoji_input/emoji_input.vue2++
Msrc/components/emoji_input/suggestor.js4++--
Dsrc/components/export_import/export_import.vue102-------------------------------------------------------------------------------
Msrc/components/extra_buttons/extra_buttons.vue29+++++++++++++++--------------
Msrc/components/features_panel/features_panel.js2+-
Msrc/components/features_panel/features_panel.vue4++--
Asrc/components/flash/flash.js53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/flash/flash.vue84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/follow_button/follow_button.js9++++++---
Msrc/components/follow_button/follow_button.vue2+-
Msrc/components/follow_card/follow_card.vue1+
Msrc/components/font_control/font_control.js12++++--------
Msrc/components/font_control/font_control.vue35+++++++++++++----------------------
Msrc/components/gallery/gallery.js97++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/components/gallery/gallery.vue202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/components/global_notice_list/global_notice_list.vue8++++++++
Asrc/components/hashtag_link/hashtag_link.js36++++++++++++++++++++++++++++++++++++
Asrc/components/hashtag_link/hashtag_link.scss6++++++
Asrc/components/hashtag_link/hashtag_link.vue19+++++++++++++++++++
Msrc/components/interface_language_switcher/interface_language_switcher.vue41++++++++++++++---------------------------
Msrc/components/media_modal/media_modal.js38++++++++++++++++++++++++++++++++------
Msrc/components/media_modal/media_modal.vue160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Asrc/components/mention_link/mention_link.js134+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/mention_link/mention_link.scss116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/mention_link/mention_link.vue75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/mentions_line/mentions_line.js37+++++++++++++++++++++++++++++++++++++
Asrc/components/mentions_line/mentions_line.scss13+++++++++++++
Asrc/components/mentions_line/mentions_line.vue43+++++++++++++++++++++++++++++++++++++++++++
Msrc/components/mfa_form/recovery_form.vue2++
Msrc/components/mfa_form/totp_form.vue2++
Msrc/components/mobile_post_status_button/mobile_post_status_button.js3+++
Msrc/components/mobile_post_status_button/mobile_post_status_button.vue4++--
Msrc/components/moderation_tools/moderation_tools.js5+++++
Msrc/components/moderation_tools/moderation_tools.vue63++++++++++++++++++++++++++-------------------------------------
Msrc/components/mrf_transparency_panel/mrf_transparency_panel.js51+++++++++++++++++++++++++++++++++++++++++++++------
Asrc/components/mrf_transparency_panel/mrf_transparency_panel.scss21+++++++++++++++++++++
Msrc/components/mrf_transparency_panel/mrf_transparency_panel.vue155++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/components/nav_panel/nav_panel.js36++++++++++++++++++++++--------------
Msrc/components/nav_panel/nav_panel.vue70+++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/components/notification/notification.js4+++-
Msrc/components/notification/notification.scss13+++++++++++++
Msrc/components/notification/notification.vue26+++++++++++++++-----------
Asrc/components/notifications/notification_filters.vue122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/notifications/notifications.js13+++++--------
Msrc/components/notifications/notifications.scss19+++++--------------
Msrc/components/notifications/notifications.vue11++++++-----
Msrc/components/password_reset/password_reset.vue2+-
Msrc/components/poll/poll.js10+++++++---
Msrc/components/poll/poll.vue21++++++++++++++++-----
Msrc/components/poll/poll_form.js6++++--
Msrc/components/poll/poll_form.vue71++++++++++++++++++++++++++++-------------------------------------------
Msrc/components/popover/popover.js23++++++++++++++++++-----
Msrc/components/popover/popover.vue38++++++++++++++++++++++++++++++++------
Msrc/components/post_status_form/post_status_form.js27++++++++++++++++++++++-----
Msrc/components/post_status_form/post_status_form.vue114+++++++++++++++++++++++--------------------------------------------------------
Msrc/components/react_button/react_button.js6++++++
Msrc/components/react_button/react_button.vue129++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/components/registration/registration.js17+++++++++++++----
Msrc/components/registration/registration.vue19++++++++++++++++++-
Asrc/components/rich_content/rich_content.jsx328+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/rich_content/rich_content.scss64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/scope_selector/scope_selector.vue4++++
Msrc/components/search/search.vue1+
Msrc/components/search_bar/search_bar.vue3+++
Asrc/components/select/select.js21+++++++++++++++++++++
Asrc/components/select/select.vue62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/selectable_list/selectable_list.vue7++-----
Asrc/components/settings_modal/helpers/boolean_setting.js38++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/helpers/boolean_setting.vue21+++++++++++++++++++++
Asrc/components/settings_modal/helpers/choice_setting.js39+++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/helpers/choice_setting.vue29+++++++++++++++++++++++++++++
Asrc/components/settings_modal/helpers/modified_indicator.vue51+++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/settings_modal/helpers/shared_computed_object.js22++++------------------
Msrc/components/settings_modal/settings_modal.js124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/settings_modal/settings_modal.vue68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/components/settings_modal/settings_modal_content.scss13++++++++++++-
Msrc/components/settings_modal/tabs/filtering_tab.js21++++++++++-----------
Msrc/components/settings_modal/tabs/filtering_tab.vue207+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/components/settings_modal/tabs/general_tab.js26++++++++++++++++++++++----
Msrc/components/settings_modal/tabs/general_tab.vue324+++++++++++++++++++++++++++++++++++++++----------------------------------------
Msrc/components/settings_modal/tabs/mutes_and_blocks_tab.vue73+++++++++++++++++++++++++++++--------------------------------------------
Msrc/components/settings_modal/tabs/notifications_tab.vue2+-
Msrc/components/settings_modal/tabs/profile_tab.js2+-
Msrc/components/settings_modal/tabs/profile_tab.vue6+++---
Msrc/components/settings_modal/tabs/security_tab/security_tab.js3++-
Msrc/components/settings_modal/tabs/security_tab/security_tab.vue6+++---
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.js67++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.scss3+++
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.vue108+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/components/shadow_control/shadow_control.js4+++-
Msrc/components/shadow_control/shadow_control.vue46++++++++++++++++++----------------------------
Asrc/components/shout_panel/shout_panel.js53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/shout_panel/shout_panel.vue155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/side_drawer/side_drawer.js2+-
Msrc/components/side_drawer/side_drawer.vue8+++-----
Msrc/components/staff_panel/staff_panel.js20+++++++++++++++++---
Msrc/components/staff_panel/staff_panel.vue27++++++++++++++++++++++-----
Msrc/components/status/status.js88++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/components/status/status.scss58++++++++++++++++++++++++++++------------------------------
Msrc/components/status/status.vue120+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Asrc/components/status_body/status_body.js129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/status_body/status_body.scss174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/status_body/status_body.vue100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/status_content/status_content.js153++++---------------------------------------------------------------------------
Msrc/components/status_content/status_content.vue301++++++++++---------------------------------------------------------------------
Msrc/components/status_popover/status_popover.vue8+++-----
Msrc/components/still-image/still-image.js11++++++++++-
Msrc/components/still-image/still-image.vue6++++--
Msrc/components/tab_switcher/tab_switcher.js4+++-
Msrc/components/timeago/timeago.vue6++++--
Msrc/components/timeline/timeline.js27++++++---------------------
Asrc/components/timeline/timeline.scss31+++++++++++++++++++++++++++++++
Msrc/components/timeline/timeline.vue38+++++++-------------------------------
Asrc/components/timeline/timeline_quick_settings.js60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/timeline/timeline_quick_settings.vue102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/timeline_menu/timeline_menu.js31+++++--------------------------
Msrc/components/timeline_menu/timeline_menu.vue86++++++++++++++++++-------------------------------------------------------------
Asrc/components/timeline_menu/timeline_menu_content.js29+++++++++++++++++++++++++++++
Asrc/components/timeline_menu/timeline_menu_content.vue66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/user_avatar/user_avatar.vue4++--
Msrc/components/user_card/user_card.js19+++++++++++++------
Msrc/components/user_card/user_card.vue147+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/components/user_list_popover/user_list_popover.vue66++++++++++++++++++++++++++++++++++++------------------------------
Msrc/components/user_profile/user_profile.js4+++-
Msrc/components/user_profile/user_profile.vue30++++++++++++++----------------
Msrc/components/user_reporting_modal/user_reporting_modal.vue7++-----
Msrc/i18n/ca.json568++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/i18n/cs.json1-
Msrc/i18n/de.json361+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/i18n/en.json173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/i18n/eo.json101++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/i18n/es.json152++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/i18n/eu.json91++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/i18n/fi.json4++--
Msrc/i18n/fr.json331+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/i18n/he.json1-
Asrc/i18n/id.json631+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/i18n/it.json248+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Msrc/i18n/ja_easy.json1-
Msrc/i18n/ja_pedantic.json387+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msrc/i18n/ko.json267++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/i18n/nb.json64++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/i18n/nl.json357+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/i18n/oc.json1-
Msrc/i18n/pl.json79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/i18n/pt.json14+++++++-------
Msrc/i18n/ru.json279+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/i18n/te.json1-
Msrc/i18n/uk.json70++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Asrc/i18n/vi.json872+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/i18n/zh.json76+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/i18n/zh_Hant.json103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/main.js6++----
Msrc/modules/api.js106++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Dsrc/modules/chat.js33---------------------------------
Msrc/modules/chats.js6++++++
Msrc/modules/config.js55++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/modules/instance.js19++++++++++++++++---
Msrc/modules/media_viewer.js9+++++----
Asrc/modules/shout.js33+++++++++++++++++++++++++++++++++
Msrc/modules/statuses.js29++++++++++++++++++++++-------
Msrc/modules/users.js14++++++++++----
Msrc/services/api/api.service.js6+++++-
Msrc/services/backend_interactor_service/backend_interactor_service.js12++++++++++--
Msrc/services/chat_service/chat_service.js17+++++++++++++++++
Msrc/services/entity_normalizer/entity_normalizer.service.js46+++++++++++++++++++---------------------------
Asrc/services/export_import/export_import.js55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/services/favicon_service/favicon_service.js70++++++++++++++++++++++++++++++++++++++--------------------------------
Msrc/services/file_type/file_type.service.js4++++
Asrc/services/html_converter/html_line_converter.service.js136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/services/html_converter/html_tree_converter.service.js98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/services/html_converter/utility.service.js73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/services/notification_utils/notification_utils.js7+++++++
Msrc/services/notifications_fetcher/notifications_fetcher.service.js6++++--
Asrc/services/ruffle_service/ruffle_service.js40++++++++++++++++++++++++++++++++++++++++
Msrc/services/style_setter/style_setter.js4++--
Msrc/services/theme_data/pleromafe.js34++++++++++++++++++++++++++++++++++
Msrc/services/timeline_fetcher/timeline_fetcher.service.js9+++++++--
Dsrc/services/tiny_post_html_processor/tiny_post_html_processor.service.js94-------------------------------------------------------------------------------
Msrc/services/user_highlighter/user_highlighter.js14+++++++++++---
Mstatic/config.json1-
Atest/unit/specs/components/rich_content.spec.js557+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtest/unit/specs/components/timeline.spec.js27---------------------------
Mtest/unit/specs/components/user_profile.spec.js6++++--
Mtest/unit/specs/services/chat_service/chat_service.spec.js17+++++++++++++++++
Mtest/unit/specs/services/entity_normalizer/entity_normalizer.spec.js85++-----------------------------------------------------------------------------
Atest/unit/specs/services/html_converter/html_line_converter.spec.js171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/unit/specs/services/html_converter/html_tree_converter.spec.js132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/unit/specs/services/html_converter/utility.spec.js37+++++++++++++++++++++++++++++++++++++
Dtest/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js96-------------------------------------------------------------------------------
Myarn.lock933+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
228 files changed, 12161 insertions(+), 4167 deletions(-)

diff --git a/.babelrc b/.babelrc @@ -1,5 +1,5 @@ { - "presets": ["@babel/preset-env"], - "plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-transform-vue-jsx"], + "presets": ["@babel/preset-env", "@vue/babel-preset-jsx"], + "plugins": ["@babel/plugin-transform-runtime", "lodash"], "comments": false } diff --git a/.mailmap b/.mailmap @@ -0,0 +1 @@ +rinpatch <rin@patch.cx> <rinpatch@sdf.org> +\ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -3,10 +3,88 @@ 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 +### Fixed +- AdminFE button no longer scrolls page to top when clicked +- Pinned statuses no longer appear at bottom of user timeline (still appear as part of the timeline when fetched deep enough) +- Fixed many many bugs related to new mentions, including spacing and alignment issues +- Links in profile bios now properly open in new tabs +- Inline images now respect their intended width/height attributes +- Links with `&` in them work properly now +- Interaction list popovers now properly emojify names +- Completely hidden posts still had 1px border +- Attachments are ALWAYS in same order as user uploaded, no more "videos first" +- Attachment description is prefilled with backend-provided default when uploading +- Proper visual feedback that next image is loading when browsing + +### Changed +- (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out) +- User highlight background now also covers the `@` +- Reverted back to textual `@`, svg version is opt-in. +- Settings window has been throughly rearranged to make make more sense and make navication settings easier. +- Uploaded attachments are uniform with displayed attachments +- Flash is watchable in media-modal (takes up nearly full screen though due to sizing issues) +- Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post. + +### Added +- Options to show domains in mentions +- Option to show user avatars in mention links (opt-in) +- Option to disable the tooltip for mentions +- Option to completely hide muted threads +- Ability to open videos in modal even if you disabled that feature, via an icon button +- New button on attachment that indicates that attachment has a description and shows a bar filled with description +- Attachments are truncated just like post contents +- Media modal now also displays description and counter position in gallery (i.e. 1/5) +- Ability to rearrange order of attachments when uploading + +## [2.4.2] - 2022-01-09 +### Added +- Added Apply and Reset buttons to the bottom of theme tab to minimize UI travel +- Implemented user option to always show floating New Post button (normally mobile-only) +- Display reasons for instance specific policies +- Added functionality to cancel follow request + +### Fixed +- Fixed link to external profile not working on user profiles +- Fixed mobile shoutbox display +- Fixed favicon badge not working in Chrome +- Escape html more properly in subject/display name + + +## [2.4.0] - 2021-08-08 +### Added +- Added a quick settings to timeline header for easier access +- Added option to mark posts as sensitive by default +- Added quick filters for notifications +- Implemented user option to change sidebar position to the right side +- Implemented user option to hide floating shout panel +- Implemented "edit profile" button if viewing own profile which opens profile settings + +### Fixed +- Fixed follow request count showing in the wrong location in mobile view + + +## [2.3.0] - 2021-03-01 ### Fixed - Button to remove uploaded media in post status form is now properly placed and sized. - Fixed shoutbox not working in mobile layout +- Fixed missing highlighted border in expanded conversations again +- Fixed some UI jumpiness when opening images particularly in chat view +- Fixed chat unread badge looking weird +- Fixed punycode names not working properly +- Fixed notifications crashing on an invalid notification + +### Changed +- Display 'people voted' instead of 'votes' for multi-choice polls +- Changed the "Timelines" link in side panel to toggle show all timeline options inside the panel +- Renamed "Timeline" to "Home Timeline" to be more clear +- Optimized chat to not get horrible performance after keeping the same chat open for a long time +- When opening emoji picker or react picker, it automatically focuses the search field +- Language picker now uses native language names + +### Added +- Added reason field for registration when approval is required +- Group staff members by role in the About page ## [2.2.3] - 2021-01-18 @@ -16,10 +94,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Follows/Followers tabs on user profiles now display the content properly. - Handle punycode in screen names +- Fixed local dev mode having non-functional websockets in some cases +- Show notices for websocket events (errors, abnormal closures, reconnections) +- Fix not being able to re-enable websocket until page refresh +- Fix annoying issue where timeline might have few posts when streaming is enabled ### Changed - Don't filter own posts when they hit your wordfilter -- Language picker now uses native language names ## [2.2.2] - 2020-12-22 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md @@ -3,6 +3,7 @@ Contributors of this project. - Constance Variable (lambadalambda@social.heldscal.la): Code - Coco Snuss (cocosnuss@social.heldscal.la): Code - wakarimasen (wakarimasen@shitposter.club): NSFW hiding image +- eris (eris@disqordia.space): Code - dtluna (dtluna@social.heldscal.la): Code - sonyam (sonyam@social.heldscal.la): Background images - hakui (hakui@freezepeach.xyz): CSS and styling diff --git a/build/dev-server.js b/build/dev-server.js @@ -21,6 +21,7 @@ var compiler = webpack(webpackConfig) var devMiddleware = require('webpack-dev-middleware')(compiler, { publicPath: webpackConfig.output.publicPath, + writeToDisk: true, stats: { colors: true, chunks: false diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js @@ -3,6 +3,7 @@ var config = require('../config') var utils = require('./utils') var projectRoot = path.resolve(__dirname, '../') var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin') +var CopyPlugin = require('copy-webpack-plugin'); var env = process.env.NODE_ENV // check env & config/index.js to decide weither to enable CSS Sourcemaps for the @@ -93,6 +94,19 @@ module.exports = { new ServiceWorkerWebpackPlugin({ entry: path.join(__dirname, '..', 'src/sw.js'), filename: 'sw-pleroma.js' + }), + // This copies Ruffle's WASM to a directory so that JS side can access it + new CopyPlugin({ + patterns: [ + { + from: "node_modules/ruffle-mirror/*", + to: "static/ruffle", + flatten: true + }, + ], + options: { + concurrency: 100, + }, }) ] } diff --git a/config/index.js b/config/index.js @@ -3,6 +3,11 @@ const path = require('path') let settings = {} try { settings = require('./local.json') + if (settings.target && settings.target.endsWith('/')) { + // replacing trailing slash since it can conflict with some apis + // and that's how actual BE reports its url + settings.target = settings.target.replace(/\/$/, '') + } console.log('Using local dev server settings (/config/local.json):') console.log(JSON.stringify(settings, null, 2)) } catch (e) { diff --git a/package.json b/package.json @@ -32,9 +32,9 @@ "phoenix": "^1.3.0", "portal-vue": "^2.1.4", "punycode.js": "^2.1.0", + "ruffle-mirror": "^2021.4.10", "v-click-outside": "^2.1.1", "vue": "^2.6.11", - "vue-chat-scroll": "^1.2.1", "vue-i18n": "^7.3.2", "vue-router": "^3.0.1", "vue-template-compiler": "^2.6.11", @@ -47,8 +47,8 @@ "@babel/preset-env": "^7.7.6", "@babel/register": "^7.7.4", "@ungap/event-target": "^0.1.0", - "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.1.2", + "@vue/babel-helper-vue-jsx-merge-props": "^1.2.1", + "@vue/babel-preset-jsx": "^1.2.4", "@vue/test-utils": "^1.0.0-beta.26", "autoprefixer": "^6.4.0", "babel-eslint": "^7.0.0", @@ -58,6 +58,7 @@ "chalk": "^1.1.3", "chromedriver": "^87.0.1", "connect-history-api-fallback": "^1.1.0", + "copy-webpack-plugin": "^6.4.1", "cross-spawn": "^4.0.2", "css-loader": "^0.28.0", "custom-event-polyfill": "^1.0.7", @@ -103,7 +104,7 @@ "selenium-server": "2.53.1", "semver": "^5.3.0", "serviceworker-webpack-plugin": "^1.0.0", - "shelljs": "^0.7.4", + "shelljs": "^0.8.4", "sinon": "^2.1.0", "sinon-chai": "^2.8.0", "stylelint": "^13.6.1", @@ -112,7 +113,7 @@ "url-loader": "^1.1.2", "vue-loader": "^14.0.0", "vue-style-loader": "^4.0.0", - "webpack": "^4.0.0", + "webpack": "^4.44.0", "webpack-dev-middleware": "^3.6.0", "webpack-hot-middleware": "^2.12.2", "webpack-merge": "^0.14.1" diff --git a/src/App.js b/src/App.js @@ -4,7 +4,7 @@ import Notifications from './components/notifications/notifications.vue' import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue' 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 ShoutPanel from './components/shout_panel/shout_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' @@ -26,7 +26,7 @@ export default { InstanceSpecificPanel, FeaturesPanel, WhoToFollowPanel, - ChatPanel, + ShoutPanel, MediaModal, SideDrawer, MobilePostStatusButton, @@ -65,7 +65,7 @@ export default { } } }, - chat () { return this.$store.state.chat.channel.state === 'joined' }, + shout () { return this.$store.state.shout.channel.state === 'joined' }, suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, showInstanceSpecificPanel () { return this.$store.state.instance.showInstanceSpecificPanel && @@ -73,11 +73,17 @@ export default { this.$store.state.instance.instanceSpecificPanelContent }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, + shoutboxPosition () { + return this.$store.getters.mergedConfig.showNewPostButton || false + }, + hideShoutbox () { + return this.$store.getters.mergedConfig.hideShoutbox + }, isMobileLayout () { return this.$store.state.interface.mobileLayout }, privateMode () { return this.$store.state.instance.private }, sidebarAlign () { return { - 'order': this.$store.state.instance.sidebarRight ? 99 : 0 + 'order': this.$store.getters.mergedConfig.sidebarRight ? 99 : 0 } }, ...mapGetters(['mergedConfig']) diff --git a/src/App.scss b/src/App.scss @@ -88,6 +88,10 @@ a { font-family: sans-serif; font-family: var(--interfaceFont, sans-serif); + &.-sublime { + background: transparent; + } + i[class*=icon-], .svg-inline--fa { color: $fallback--text; @@ -187,7 +191,7 @@ a { } } -input, textarea, .select, .input { +input, textarea, .input { &.unstyled { border-radius: 0; @@ -217,47 +221,11 @@ input, textarea, .select, .input { hyphens: none; padding: 8px .5em; - &.select { - padding: 0; - } - - &:disabled, &[disabled=disabled] { + &:disabled, &[disabled=disabled], &.disabled { cursor: not-allowed; opacity: 0.5; } - .select-down-icon { - position: absolute; - top: 0; - bottom: 0; - right: 5px; - height: 100%; - color: $fallback--text; - color: var(--inputText, $fallback--text); - line-height: 28px; - z-index: 0; - pointer-events: none; - } - - select { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background: transparent; - border: none; - color: $fallback--text; - color: var(--inputText, --text, $fallback--text); - margin: 0; - padding: 0 2em 0 .2em; - font-family: sans-serif; - font-family: var(--inputFont, sans-serif); - font-size: 14px; - width: 100%; - z-index: 1; - height: 28px; - line-height: 16px; - } - &[type=range] { background: none; border: none; @@ -547,9 +515,21 @@ main-router { border-radius: var(--panelRadius, $fallback--panelRadius); } -.panel-footer { +/* TODO Should remove timeline-footer from here when we refactor panels into + * separate component and utilize slots + */ +.panel-footer, .timeline-footer { + display: flex; border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); + flex: none; + padding: 0.6em 0.6em; + text-align: left; + line-height: 28px; + align-items: baseline; + border-width: 1px 0 0 0; + border-style: solid; + border-color: var(--border, $fallback--border); .faint { color: $fallback--faint; @@ -586,6 +566,7 @@ nav { color: var(--faint, $fallback--faint); box-shadow: 0px 0px 4px rgba(0,0,0,.6); box-shadow: var(--topBarShadow); + box-sizing: border-box; } .fade-enter-active, .fade-leave-active { @@ -705,6 +686,15 @@ nav { color: var(--alertWarningPanelText, $fallback--text); } } + + &.success { + background-color: var(--alertSuccess, $fallback--alertWarning); + color: var(--alertSuccessText, $fallback--text); + + .panel-heading & { + color: var(--alertSuccessPanelText, $fallback--text); + } + } } .faint { @@ -808,13 +798,6 @@ nav { } } -.select-multiple { - display: flex; - .option-list { - margin: 0; - padding-left: .5em; - } -} .setting-list, .option-list{ list-style-type: none; @@ -861,16 +844,10 @@ nav { } .new-status-notification { - position:relative; - margin-top: -1px; + position: relative; font-size: 1.1em; - border-width: 1px 0 0 0; - border-style: solid; - border-color: var(--border, $fallback--border); - padding: 10px; z-index: 1; - background-color: $fallback--fg; - background-color: var(--panel, $fallback--fg); + flex: 1; } .chat-layout { @@ -878,6 +855,11 @@ nav { overflow: hidden; height: 100%; + // Get rid of scrollbar on body as scrolling happens on different element + body { + overflow: hidden; + } + // 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) { diff --git a/src/App.vue b/src/App.vue @@ -49,10 +49,11 @@ </div> <media-modal /> </div> - <chat-panel - v-if="currentUser && chat" + <shout-panel + v-if="currentUser && shout && !hideShoutbox" :floating="true" - class="floating-chat mobile-hidden" + class="floating-shout mobile-hidden" + :class="{ 'left': shoutboxPosition }" /> <MobilePostStatusButton /> <UserReportingModal /> diff --git a/src/boot/after_store.js b/src/boot/after_store.js @@ -51,6 +51,7 @@ const getInstanceConfig = async ({ store }) => { const vapidPublicKey = data.pleroma.vapid_public_key store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit }) + store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required }) if (vapidPublicKey) { store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) @@ -239,7 +240,7 @@ const getNodeInfo = async ({ store }) => { 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: 'shoutAvailable', 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') }) diff --git a/src/boot/routes.js b/src/boot/routes.js @@ -16,7 +16,7 @@ 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' import AuthForm from 'components/auth_form/auth_form.js' -import ChatPanel from 'components/chat_panel/chat_panel.vue' +import ShoutPanel from 'components/shout_panel/shout_panel.vue' import WhoToFollow from 'components/who_to_follow/who_to_follow.vue' import About from 'components/about/about.vue' import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue' @@ -64,7 +64,7 @@ export default (store) => { { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, { name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute }, { name: 'login', path: '/login', component: AuthForm }, - { name: 'chat-panel', path: '/chat-panel', component: ChatPanel, props: () => ({ floating: false }) }, + { name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, 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 }, diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue @@ -6,10 +6,7 @@ :bound-to="{ x: 'container' }" remove-padding > - <div - slot="content" - class="account-tools-popover" - > + <template v-slot:content> <div class="dropdown-menu"> <template v-if="relationship.following"> <button @@ -59,16 +56,15 @@ {{ $t('user_card.message') }} </button> </div> - </div> - <div - slot="trigger" - class="ellipsis-button" - > - <FAIcon - class="icon" - icon="ellipsis-v" - /> - </div> + </template> + <template v-slot:trigger> + <button class="button-unstyled ellipsis-button"> + <FAIcon + class="icon" + icon="ellipsis-v" + /> + </button> + </template> </Popover> </div> </template> @@ -83,7 +79,6 @@ } .ellipsis-button { - cursor: pointer; width: 2.5em; margin: -0.5em 0; padding: 0.5em 0; diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js @@ -1,4 +1,5 @@ import StillImage from '../still-image/still-image.vue' +import Flash from '../flash/flash.vue' import VideoAttachment from '../video_attachment/video_attachment.vue' import nsfwImage from '../../assets/nsfw.png' import fileTypeService from '../../services/file_type/file_type.service.js' @@ -10,7 +11,12 @@ import { faImage, faVideo, faPlayCircle, - faTimes + faTimes, + faStop, + faSearchPlus, + faTrashAlt, + faPencilAlt, + faAlignRight } from '@fortawesome/free-solid-svg-icons' library.add( @@ -19,36 +25,64 @@ library.add( faImage, faVideo, faPlayCircle, - faTimes + faTimes, + faStop, + faSearchPlus, + faTrashAlt, + faPencilAlt, + faAlignRight ) const Attachment = { props: [ 'attachment', + 'description', + 'hideDescription', 'nsfw', 'size', - 'allowPlay', 'setMedia', - 'naturalSizeLoad' + 'remove', + 'shiftUp', + 'shiftDn', + 'edit' ], data () { return { + localDescription: this.description || this.attachment.description, nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage, hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw, preloadImage: this.$store.getters.mergedConfig.preloadImage, loading: false, img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'), modalOpen: false, - showHidden: false + showHidden: false, + flashLoaded: false, + showDescription: false } }, components: { + Flash, StillImage, VideoAttachment }, computed: { + classNames () { + return [ + { + '-loading': this.loading, + '-nsfw-placeholder': this.hidden, + '-editable': this.edit !== undefined + }, + '-type-' + this.type, + this.size && '-size-' + this.size, + `-${this.useContainFit ? 'contain' : 'cover'}-fit` + ] + }, usePlaceholder () { - return this.size === 'hide' || this.type === 'unknown' + return this.size === 'hide' + }, + useContainFit () { + return this.$store.getters.mergedConfig.useContainFit }, placeholderName () { if (this.attachment.description === '' || !this.attachment.description) { @@ -72,24 +106,33 @@ const Attachment = { return this.nsfw && this.hideNsfwLocal && !this.showHidden }, isEmpty () { - return (this.type === 'html' && !this.attachment.oembed) || this.type === 'unknown' - }, - isSmall () { - return this.size === 'small' - }, - fullwidth () { - if (this.size === 'hide') return false - return this.type === 'html' || this.type === 'audio' || this.type === 'unknown' + return (this.type === 'html' && !this.attachment.oembed) }, useModal () { - const modalTypes = this.size === 'hide' ? ['image', 'video', 'audio'] - : this.mergedConfig.playVideosInModal - ? ['image', 'video'] - : ['image'] + let modalTypes = [] + switch (this.size) { + case 'hide': + case 'small': + modalTypes = ['image', 'video', 'audio', 'flash'] + break + default: + modalTypes = this.mergedConfig.playVideosInModal + ? ['image', 'video', 'flash'] + : ['image'] + break + } return modalTypes.includes(this.type) }, + videoTag () { + return this.useModal ? 'button' : 'span' + }, ...mapGetters(['mergedConfig']) }, + watch: { + localDescription (newVal) { + this.onEdit(newVal) + } + }, methods: { linkClicked ({ target }) { if (target.tagName === 'A') { @@ -98,12 +141,37 @@ const Attachment = { }, openModal (event) { if (this.useModal) { - event.stopPropagation() - event.preventDefault() - this.setMedia() - this.$store.dispatch('setCurrent', this.attachment) + this.$emit('setMedia') + this.$store.dispatch('setCurrentMedia', this.attachment) + } else if (this.type === 'unknown') { + window.open(this.attachment.url) } }, + openModalForce (event) { + this.$emit('setMedia') + this.$store.dispatch('setCurrentMedia', this.attachment) + }, + onEdit (event) { + this.edit && this.edit(this.attachment, event) + }, + onRemove () { + this.remove && this.remove(this.attachment) + }, + onShiftUp () { + this.shiftUp && this.shiftUp(this.attachment) + }, + onShiftDn () { + this.shiftDn && this.shiftDn(this.attachment) + }, + stopFlash () { + this.$refs.flash.closePlayer() + }, + setFlashLoaded (event) { + this.flashLoaded = event + }, + toggleDescription () { + this.showDescription = !this.showDescription + }, toggleHidden (event) { if ( (this.mergedConfig.useOneClickNsfw && !this.showHidden) && @@ -130,7 +198,7 @@ const Attachment = { onImageLoad (image) { const width = image.naturalWidth const height = image.naturalHeight - this.naturalSizeLoad && this.naturalSizeLoad({ width, height }) + this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height }) } } } diff --git a/src/components/attachment/attachment.scss b/src/components/attachment/attachment.scss @@ -0,0 +1,268 @@ +@import '../../_variables.scss'; + +.Attachment { + display: inline-flex; + flex-direction: column; + position: relative; + align-self: flex-start; + line-height: 0; + height: 100%; + border-style: solid; + border-width: 1px; + border-radius: $fallback--attachmentRadius; + border-radius: var(--attachmentRadius, $fallback--attachmentRadius); + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + + .attachment-wrapper { + flex: 1 1 auto; + height: 100%; + position: relative; + overflow: hidden; + } + + .description-container { + flex: 0 1 0; + display: flex; + padding-top: 0.5em; + z-index: 1; + + p { + flex: 1; + text-align: center; + line-height: 1.5; + padding: 0.5em; + margin: 0; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + + &.-static { + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding-top: 0; + background: var(--popover); + box-shadow: var(--popupShadow); + } + } + + .description-field { + flex: 1; + min-width: 0; + } + + & .placeholder-container, + & .image-container, + & .audio-container, + & .video-container, + & .flash-container, + & .oembed-container { + display: flex; + justify-content: center; + width: 100%; + height: 100%; + } + + .image-container { + .image { + width: 100%; + height: 100%; + } + } + + & .flash-container, + & .video-container { + & .flash, + & video { + width: 100%; + height: 100%; + object-fit: contain; + align-self: center; + } + } + + .audio-container { + display: flex; + align-items: flex-end; + + audio { + width: 100%; + height: 100%; + } + } + + .placeholder-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 0.5em; + } + + + .play-icon { + position: absolute; + font-size: 64px; + top: calc(50% - 32px); + left: calc(50% - 32px); + color: rgba(255, 255, 255, 0.75); + text-shadow: 0 0 2px rgba(0, 0, 0, 0.4); + + &::before { + margin: 0; + } + } + + .attachment-buttons { + display: flex; + position: absolute; + right: 0; + top: 0; + margin-top: 0.5em; + margin-right: 0.5em; + z-index: 1; + + .attachment-button { + padding: 0; + border-radius: $fallback--tooltipRadius; + border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + text-align: center; + width: 2em; + height: 2em; + margin-left: 0.5em; + font-size: 1.25em; + // TODO: theming? hard to theme with unknown background image color + background: rgba(230, 230, 230, 0.7); + + .svg-inline--fa { + color: rgba(0, 0, 0, 0.6); + } + + &:hover .svg-inline--fa { + color: rgba(0, 0, 0, 0.9); + } + } + } + + .oembed-container { + line-height: 1.2em; + flex: 1 0 100%; + width: 100%; + margin-right: 15px; + display: flex; + + img { + width: 100%; + } + + .image { + flex: 1; + img { + border: 0px; + border-radius: 5px; + height: 100%; + object-fit: cover; + } + } + + .text { + flex: 2; + margin: 8px; + word-break: break-all; + h1 { + font-size: 14px; + margin: 0px; + } + } + } + + &.-size-small { + .play-icon { + zoom: 0.5; + opacity: 0.7; + } + + .attachment-buttons { + zoom: 0.7; + opacity: 0.5; + } + } + + &.-editable { + padding: 0.5em; + + & .description-container, + & .attachment-buttons { + margin: 0; + } + } + + &.-placeholder { + display: inline-block; + color: $fallback--link; + color: var(--postLink, $fallback--link); + overflow: hidden; + white-space: nowrap; + height: auto; + line-height: 1.5; + + &:not(.-editable) { + border: none; + } + + &.-editable { + display: flex; + flex-direction: row; + align-items: baseline; + + & .description-container, + & .attachment-buttons { + margin: 0; + padding: 0; + position: relative; + } + + .description-container { + flex: 1; + padding-left: 0.5em; + } + + .attachment-buttons { + order: 99; + align-self: center; + } + } + + a { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + } + + svg { + color: inherit; + } + } + + &.-loading { + cursor: progress; + } + + &.-contain-fit { + img, + canvas { + object-fit: contain; + } + } + + &.-cover-fit { + img, + canvas { + object-fit: cover; + } + } +} diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue @@ -1,7 +1,8 @@ <template> - <div + <button v-if="usePlaceholder" - :class="{ 'fullwidth': fullwidth }" + class="Attachment -placeholder button-unstyled" + :class="classNames" @click="openModal" > <a @@ -13,310 +14,251 @@ :title="attachment.description" > <FAIcon :icon="placeholderIconClass" /> - <b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }} + <b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }} </a> - </div> - <div - v-else - v-show="!isEmpty" - class="attachment" - :class="{[type]: true, loading, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}" - > - <a - v-if="hidden" - class="image-attachment" - :href="attachment.url" - :alt="attachment.description" - :title="attachment.description" - @click.prevent.stop="toggleHidden" + <div + v-if="edit || remove" + class="attachment-buttons" > - <img - :key="nsfwImage" - class="nsfw" - :src="nsfwImage" - :class="{'small': isSmall}" + <button + v-if="remove" + class="button-unstyled attachment-button" + @click.prevent="onRemove" > - <FAIcon - v-if="type === 'video'" - class="play-icon" - icon="play-circle" - /> - </a> - <button - v-if="nsfw && hideNsfwLocal && !hidden" - class="button-unstyled hider" - @click.prevent="toggleHidden" + <FAIcon icon="trash-alt" /> + </button> + </div> + <div + v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)" + class="description-container" + :class="{ '-static': !edit }" > - <FAIcon icon="times" /> - </button> - - <a - v-if="type === 'image' && (!hidden || preloadImage)" - class="image-attachment" - :class="{'hidden': hidden && preloadImage }" - :href="attachment.url" - target="_blank" - @click="openModal" + <input + v-if="edit" + v-model="localDescription" + type="text" + class="description-field" + :placeholder="$t('post_status.media_description')" + @keydown.enter.prevent="" + > + <p v-else> + {{ localDescription }} + </p> + </div> + </button> + <div + v-else + class="Attachment" + :class="classNames" + > + <div + v-show="!isEmpty" + class="attachment-wrapper" > - <StillImage - class="image" - :referrerpolicy="referrerpolicy" - :mimetype="attachment.mimetype" - :src="attachment.large_thumb_url || attachment.url" - :image-load-handler="onImageLoad" + <a + v-if="hidden" + class="image-container" + :href="attachment.url" :alt="attachment.description" - /> - </a> + :title="attachment.description" + @click.prevent.stop="toggleHidden" + > + <img + :key="nsfwImage" + class="nsfw" + :src="nsfwImage" + > + <FAIcon + v-if="type === 'video'" + class="play-icon" + icon="play-circle" + /> + </a> + <div + v-if="!hidden" + class="attachment-buttons" + > + <button + v-if="type === 'flash' && flashLoaded" + class="button-unstyled attachment-button" + @click.prevent="stopFlash" + :title="$t('status.attachment_stop_flash')" + > + <FAIcon icon="stop" /> + </button> + <button + v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'" + class="button-unstyled attachment-button" + @click.prevent="toggleDescription" + :title="$t('status.show_attachment_description')" + > + <FAIcon icon="align-right" /> + </button> + <button + v-if="!useModal && type !== 'unknown'" + class="button-unstyled attachment-button" + @click.prevent="openModalForce" + :title="$t('status.show_attachment_in_modal')" + > + <FAIcon icon="search-plus" /> + </button> + <button + v-if="nsfw && hideNsfwLocal" + class="button-unstyled attachment-button" + @click.prevent="toggleHidden" + :title="$t('status.hide_attachment')" + > + <FAIcon icon="times" /> + </button> + <button + v-if="shiftUp" + class="button-unstyled attachment-button" + @click.prevent="onShiftUp" + :title="$t('status.move_up')" + > + <FAIcon icon="chevron-left" /> + </button> + <button + v-if="shiftDn" + class="button-unstyled attachment-button" + @click.prevent="onShiftDn" + :title="$t('status.move_down')" + > + <FAIcon icon="chevron-right" /> + </button> + <button + v-if="remove" + class="button-unstyled attachment-button" + @click.prevent="onRemove" + :title="$t('status.remove_attachment')" + > + <FAIcon icon="trash-alt" /> + </button> + </div> - <a - v-if="type === 'video' && !hidden" - class="video-container" - :class="{'small': isSmall}" - :href="allowPlay ? undefined : attachment.url" - @click="openModal" - > - <VideoAttachment - class="video" - :attachment="attachment" - :controls="allowPlay" - @play="$emit('play')" - @pause="$emit('pause')" - /> - <FAIcon - v-if="!allowPlay" - class="play-icon" - icon="play-circle" - /> - </a> + <a + v-if="type === 'image' && (!hidden || preloadImage)" + class="image-container" + :class="{'-hidden': hidden && preloadImage }" + :href="attachment.url" + target="_blank" + @click.stop.prevent="openModal" + > + <StillImage + class="image" + :referrerpolicy="referrerpolicy" + :mimetype="attachment.mimetype" + :src="attachment.large_thumb_url || attachment.url" + :image-load-handler="onImageLoad" + :alt="attachment.description" + /> + </a> + + <a + v-if="type === 'unknown' && !hidden" + class="placeholder-container" + :href="attachment.url" + target="_blank" + > + <FAIcon size="5x" :icon="placeholderIconClass" /> + <p> + {{ localDescription }} + </p> + </a> + + <component + :is="videoTag" + v-if="type === 'video' && !hidden" + class="video-container" + :class="{ 'button-unstyled': 'isModal' }" + :href="attachment.url" + @click.stop.prevent="openModal" + > + <VideoAttachment + class="video" + :attachment="attachment" + :controls="!useModal" + @play="$emit('play')" + @pause="$emit('pause')" + /> + <FAIcon + v-if="useModal" + class="play-icon" + icon="play-circle" + /> + </component> + + <span + v-if="type === 'audio' && !hidden" + class="audio-container" + :href="attachment.url" + @click.stop.prevent="openModal" + > + <audio + v-if="type === 'audio'" + :src="attachment.url" + :alt="attachment.description" + :title="attachment.description" + controls + @play="$emit('play')" + @pause="$emit('pause')" + /> + </span> - <audio - v-if="type === 'audio'" - :src="attachment.url" - :alt="attachment.description" - :title="attachment.description" - controls - @play="$emit('play')" - @pause="$emit('pause')" - /> + <div + v-if="type === 'html' && attachment.oembed" + class="oembed-container" + @click.prevent="linkClicked" + > + <div + v-if="attachment.thumb_url" + class="image" + > + <img :src="attachment.thumb_url"> + </div> + <div class="text"> + <!-- eslint-disable vue/no-v-html --> + <h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1> + <div v-html="attachment.oembed.oembedHTML" /> + <!-- eslint-enable vue/no-v-html --> + </div> + </div> + <span + v-if="type === 'flash' && !hidden" + class="flash-container" + :href="attachment.url" + @click.stop.prevent="openModal" + > + <Flash + ref="flash" + class="flash" + :src="attachment.large_thumb_url || attachment.url" + @playerOpened="setFlashLoaded(true)" + @playerClosed="setFlashLoaded(false)" + /> + </span> + </div> <div - v-if="type === 'html' && attachment.oembed" - class="oembed" - @click.prevent="linkClicked" + v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))" + class="description-container" + :class="{ '-static': !edit }" > - <div - v-if="attachment.thumb_url" - class="image" + <input + v-if="edit" + v-model="localDescription" + type="text" + class="description-field" + :placeholder="$t('post_status.media_description')" + @keydown.enter.prevent="" > - <img :src="attachment.thumb_url"> - </div> - <div class="text"> - <!-- eslint-disable vue/no-v-html --> - <h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1> - <div v-html="attachment.oembed.oembedHTML" /> - <!-- eslint-enable vue/no-v-html --> - </div> + <p v-else> + {{ localDescription }} + </p> </div> </div> </template> <script src="./attachment.js"></script> -<style lang="scss"> -@import '../../_variables.scss'; - -.attachments { - display: flex; - flex-wrap: wrap; - - .non-gallery { - max-width: 100%; - } - - .placeholder { - 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%; - - svg { - color: inherit; - } - } - - .nsfw-placeholder { - cursor: pointer; - - &.loading { - cursor: progress; - } - } - - .attachment { - position: relative; - margin-top: 0.5em; - align-self: flex-start; - line-height: 0; - - border-style: solid; - border-width: 1px; - border-radius: $fallback--attachmentRadius; - border-radius: var(--attachmentRadius, $fallback--attachmentRadius); - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - overflow: hidden; - } - - .non-gallery.attachment { - &.video { - flex: 1 0 40%; - } - .nsfw { - height: 260px; - } - .small { - height: 120px; - flex-grow: 0; - } - .video { - height: 260px; - display: flex; - } - video { - max-height: 100%; - object-fit: contain; - } - } - - .fullwidth { - flex-basis: 100%; - } - // fixes small gap below video - &.video { - line-height: 0; - } - - .video-container { - display: flex; - max-height: 100%; - } - - .video { - width: 100%; - height: 100%; - } - - .play-icon { - position: absolute; - font-size: 64px; - top: calc(50% - 32px); - left: calc(50% - 32px); - color: rgba(255, 255, 255, 0.75); - text-shadow: 0 0 2px rgba(0, 0, 0, 0.4); - } - - .play-icon::before { - margin: 0; - } - - &.html { - flex-basis: 90%; - width: 100%; - display: flex; - } - - .hider { - position: absolute; - right: 0; - margin: 10px; - padding: 0; - z-index: 4; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - text-align: center; - width: 2em; - height: 2em; - font-size: 1.25em; - // TODO: theming? hard to theme with unknown background image color - background: rgba(230, 230, 230, 0.7); - .svg-inline--fa { - color: rgba(0, 0, 0, 0.6); - } - &:hover .svg-inline--fa { - color: rgba(0, 0, 0, 0.9); - } - } - - video { - z-index: 0; - } - - audio { - width: 100%; - } - - img.media-upload { - line-height: 0; - max-height: 200px; - max-width: 100%; - } - - .oembed { - line-height: 1.2em; - flex: 1 0 100%; - width: 100%; - margin-right: 15px; - display: flex; - - img { - width: 100%; - } - - .image { - flex: 1; - img { - border: 0px; - border-radius: 5px; - height: 100%; - object-fit: cover; - } - } - - .text { - flex: 2; - margin: 8px; - word-break: break-all; - h1 { - font-size: 14px; - margin: 0px; - } - } - } - - .image-attachment { - &, - & .image { - width: 100%; - height: 100%; - } - - &.hidden { - display: none; - } - - .nsfw { - object-fit: cover; - width: 100%; - height: 100%; - } - - img { - image-orientation: from-image; // NOTE: only FF supports this - } - } -} -</style> +<style src="./attachment.scss" lang="scss"></style> diff --git a/src/components/basic_user_card/basic_user_card.js b/src/components/basic_user_card/basic_user_card.js @@ -1,5 +1,6 @@ import UserCard from '../user_card/user_card.vue' import UserAvatar from '../user_avatar/user_avatar.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' const BasicUserCard = { @@ -13,7 +14,8 @@ const BasicUserCard = { }, components: { UserCard, - UserAvatar + UserAvatar, + RichContent }, methods: { toggleUserExpanded () { diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue @@ -25,24 +25,18 @@ :title="user.name" class="basic-user-card-user-name" > - <!-- eslint-disable vue/no-v-html --> - <span - v-if="user.name_html" + <RichContent class="basic-user-card-user-name-value" - v-html="user.name_html" + :html="user.name" + :emoji="user.emoji" /> - <!-- eslint-enable vue/no-v-html --> - <span - v-else - class="basic-user-card-user-name-value" - >{{ user.name }}</span> </div> <div> <router-link class="basic-user-card-screen-name" :to="userProfileLink(user)" > - @{{ user.screen_name }} + @{{ user.screen_name_ui }} </router-link> </div> <slot /> diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js @@ -73,7 +73,7 @@ const Chat = { }, formPlaceholder () { if (this.recipient) { - return this.$t('chats.message_user', { nickname: this.recipient.screen_name }) + return this.$t('chats.message_user', { nickname: this.recipient.screen_name_ui }) } else { return '' } @@ -234,6 +234,13 @@ const Chat = { const scrollable = this.$refs.scrollable return scrollable && scrollable.scrollTop <= 0 }, + cullOlderCheck () { + window.setTimeout(() => { + if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) { + this.$store.dispatch('cullOlderMessages', this.currentChatMessageService.chatId) + } + }, 5000) + }, handleScroll: _.throttle(function () { if (!this.currentChat) { return } @@ -241,6 +248,7 @@ const Chat = { this.fetchChat({ maxId: this.currentChatMessageService.minId }) } else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) { this.jumpToBottomButtonVisible = false + this.cullOlderCheck() if (this.newMessageCount > 0) { // Use a delay before marking as read to prevent situation where new messages // arrive just as you're leaving the view and messages that you didn't actually diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss @@ -98,10 +98,10 @@ .unread-message-count { font-size: 0.8em; left: 50%; - transform: translate(-50%, 0); - border-radius: 100%; margin-top: -1rem; - padding: 0; + padding: 0.1em; + border-radius: 50px; + position: absolute; } .chat-loading-error { diff --git a/src/components/chat_list/chat_list.vue b/src/components/chat_list/chat_list.vue @@ -23,10 +23,7 @@ class="timeline" > <List :items="sortedChatList"> - <template - slot="item" - slot-scope="{item}" - > + <template v-slot:item="{item}"> <ChatListItem :key="item.id" :compact="false" diff --git a/src/components/chat_list_item/chat_list_item.js b/src/components/chat_list_item/chat_list_item.js @@ -1,5 +1,5 @@ import { mapState } from 'vuex' -import StatusContent from '../status_content/status_content.vue' +import StatusBody from '../status_content/status_content.vue' import fileType from 'src/services/file_type/file_type.service' import UserAvatar from '../user_avatar/user_avatar.vue' import AvatarList from '../avatar_list/avatar_list.vue' @@ -16,7 +16,7 @@ const ChatListItem = { AvatarList, Timeago, ChatTitle, - StatusContent + StatusBody }, computed: { ...mapState({ @@ -38,12 +38,14 @@ const ChatListItem = { }, messageForStatusContent () { const message = this.chat.lastMessage + const messageEmojis = message ? message.emojis : [] 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, + emojis: messageEmojis, + raw_html: messagePreview, text: messagePreview, attachments: [] } diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss @@ -77,18 +77,15 @@ border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); } - .StatusContent { - img.emoji { - width: 1.4em; - height: 1.4em; - } + .chat-preview-body { + --emoji-size: 1.4em; } .time-wrapper { line-height: 1.4em; } - .single-line { + .chat-preview-body { 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 @@ -29,7 +29,8 @@ </div> </div> <div class="chat-preview"> - <StatusContent + <StatusBody + class="chat-preview-body" :status="messageForStatusContent" :single-line="true" /> diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js @@ -57,8 +57,9 @@ const ChatMessage = { messageForStatusContent () { return { summary: '', - statusnet_html: this.message.content, - text: this.message.content, + emojis: this.message.emojis, + raw_html: this.message.content || '', + text: this.message.content || '', attachments: this.message.attachments } }, diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss @@ -1,6 +1,7 @@ @import '../../_variables.scss'; .chat-message-wrapper { + &.hovered-message-chain { .animated.Avatar { canvas { @@ -40,6 +41,12 @@ .chat-message { display: flex; padding-bottom: 0.5em; + + .status-body:hover { + --_still-image-img-visibility: visible; + --_still-image-canvas-visibility: hidden; + --_still-image-label-visibility: hidden; + } } .avatar-wrapper { @@ -62,10 +69,6 @@ &.with-media { width: 100%; - .gallery-row { - overflow: hidden; - } - .status { width: 100%; } @@ -89,8 +92,9 @@ } .without-attachment { - .status-content { - &::after { + .message-content { + // TODO figure out how to do it properly + .RichContent::after { margin-right: 5.4em; content: " "; display: inline-block; @@ -162,6 +166,7 @@ .visible { opacity: 1; } + } .chat-message-date-separator { diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue @@ -50,7 +50,7 @@ @show="menuOpened = true" @close="menuOpened = false" > - <div slot="content"> + <template v-slot:content> <div class="dropdown-menu"> <button class="button-default dropdown-item dropdown-item-icon" @@ -59,26 +59,29 @@ <FAIcon icon="times" /> {{ $t("chats.delete") }} </button> </div> - </div> - <button - slot="trigger" - class="button-default menu-icon" - :title="$t('chats.more')" - > - <FAIcon icon="ellipsis-h" /> - </button> + </template> + <template v-slot:trigger> + <button + class="button-default menu-icon" + :title="$t('chats.more')" + > + <FAIcon icon="ellipsis-h" /> + </button> + </template> </Popover> </div> <StatusContent + class="message-content" :status="messageForStatusContent" :full-content="true" > - <span - slot="footer" - class="created-at" - > - {{ createdAt }} - </span> + <template v-slot:footer> + <span + class="created-at" + > + {{ createdAt }} + </span> + </template> </StatusContent> </div> </div> diff --git a/src/components/chat_message_date/chat_message_date.vue b/src/components/chat_message_date/chat_message_date.vue @@ -5,6 +5,8 @@ </template> <script> +import localeService from 'src/services/locale/locale.service.js' + export default { name: 'Timeago', props: ['date'], @@ -16,7 +18,7 @@ export default { if (this.date.getTime() === today.getTime()) { return this.$t('display_date.today') } else { - return this.date.toLocaleDateString('en', { day: 'numeric', month: 'long' }) + return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' }) } } } diff --git a/src/components/chat_panel/chat_panel.js b/src/components/chat_panel/chat_panel.js @@ -1,41 +0,0 @@ -import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faBullhorn, - faTimes -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faBullhorn, - faTimes -) - -const chatPanel = { - props: [ 'floating' ], - data () { - return { - currentMessage: '', - channel: null, - collapsed: true - } - }, - computed: { - messages () { - return this.$store.state.chat.messages - } - }, - methods: { - submit (message) { - this.$store.state.chat.channel.push('new_msg', { text: message }, 10000) - this.currentMessage = '' - }, - togglePanel () { - this.collapsed = !this.collapsed - }, - userProfileLink (user) { - return generateProfileLink(user.id, user.username, this.$store.state.instance.restrictedNicknames) - } - } -} - -export default chatPanel diff --git a/src/components/chat_panel/chat_panel.vue b/src/components/chat_panel/chat_panel.vue @@ -1,143 +0,0 @@ -<template> - <div - v-if="!collapsed || !floating" - class="chat-panel" - > - <div class="panel panel-default"> - <div - class="panel-heading timeline-heading" - :class="{ 'chat-heading': floating }" - @click.stop.prevent="togglePanel" - > - <div class="title"> - <span>{{ $t('shoutbox.title') }}</span> - <FAIcon - v-if="floating" - icon="times" - /> - </div> - </div> - <div - v-chat-scroll - class="chat-window" - > - <div - v-for="message in messages" - :key="message.id" - class="chat-message" - > - <span class="chat-avatar"> - <img :src="message.author.avatar"> - </span> - <div class="chat-content"> - <router-link - class="chat-name" - :to="userProfileLink(message.author)" - > - {{ message.author.username }} - </router-link> - <br> - <span class="chat-text"> - {{ message.text }} - </span> - </div> - </div> - </div> - <div class="chat-input"> - <textarea - v-model="currentMessage" - class="chat-input-textarea" - rows="1" - @keyup.enter="submit(currentMessage)" - /> - </div> - </div> - </div> - <div - v-else - class="chat-panel" - > - <div class="panel panel-default"> - <div - class="panel-heading stub timeline-heading chat-heading" - @click.stop.prevent="togglePanel" - > - <div class="title"> - <FAIcon - class="icon" - icon="bullhorn" - /> - {{ $t('shoutbox.title') }} - </div> - </div> - </div> - </div> -</template> - -<script src="./chat_panel.js"></script> - -<style lang="scss"> -@import '../../_variables.scss'; - -.floating-chat { - position: fixed; - right: 0px; - bottom: 0px; - z-index: 1000; - max-width: 25em; -} - -.chat-panel { - .chat-heading { - cursor: pointer; - - .icon { - 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-message { - display: flex; - padding: 0.2em 0.5em - } - - .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-input { - display: flex; - 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 @@ -12,7 +12,7 @@ export default Vue.component('chat-title', { ], computed: { title () { - return this.user ? this.user.screen_name : '' + return this.user ? this.user.screen_name_ui : '' }, htmlTitle () { return this.user ? this.user.name_html : '' diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue @@ -1,5 +1,4 @@ <template> - <!-- eslint-disable vue/no-v-html --> <div class="chat-title" :title="title" @@ -14,12 +13,13 @@ height="23px" /> </router-link> - <span + <RichContent class="username" - v-html="htmlTitle" + :title="'@'+user.screen_name_ui" + :html="htmlTitle" + :emoji="user.emoji" /> </div> - <!-- eslint-enable vue/no-v-html --> </template> <script src="./chat_title.js"></script> @@ -34,6 +34,8 @@ white-space: nowrap; align-items: center; + --emoji-size: 14px; + .username { max-width: 100%; text-overflow: ellipsis; @@ -41,14 +43,6 @@ display: inline; word-wrap: break-word; overflow: hidden; - text-overflow: ellipsis; - - .emoji { - width: 14px; - height: 14px; - vertical-align: middle; - object-fit: contain - } } .Avatar { diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue @@ -50,7 +50,6 @@ .Conversation { .conversation-status { - border-left: none; border-bottom-width: 1px; border-bottom-style: solid; border-bottom-color: var(--border, $fallback--border); diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue @@ -52,6 +52,7 @@ href="/pleroma/admin/#/login-pleroma" class="nav-icon" target="_blank" + @click.stop > <FAIcon fixed-width diff --git a/src/components/domain_mute_card/domain_mute_card.vue b/src/components/domain_mute_card/domain_mute_card.vue @@ -9,7 +9,7 @@ class="btn button-default" > {{ $t('domain_mute_card.unmute') }} - <template slot="progress"> + <template v-slot:progress> {{ $t('domain_mute_card.unmute_progress') }} </template> </ProgressButton> @@ -19,7 +19,7 @@ class="btn button-default" > {{ $t('domain_mute_card.mute') }} - <template slot="progress"> + <template v-slot:progress> {{ $t('domain_mute_card.mute_progress') }} </template> </ProgressButton> diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js @@ -57,6 +57,7 @@ const EmojiInput = { required: true, type: Function }, + // TODO VUE3: change to modelValue, change 'input' event to 'input' value: { /** * Used for v-model @@ -143,32 +144,31 @@ const EmojiInput = { } }, mounted () { - const slots = this.$slots.default - if (!slots || slots.length === 0) return - const input = slots.find(slot => ['input', 'textarea'].includes(slot.tag)) + const { root } = this.$refs + const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea') if (!input) return this.input = input this.resize() - input.elm.addEventListener('blur', this.onBlur) - input.elm.addEventListener('focus', this.onFocus) - input.elm.addEventListener('paste', this.onPaste) - input.elm.addEventListener('keyup', this.onKeyUp) - input.elm.addEventListener('keydown', this.onKeyDown) - input.elm.addEventListener('click', this.onClickInput) - input.elm.addEventListener('transitionend', this.onTransition) - input.elm.addEventListener('input', this.onInput) + input.addEventListener('blur', this.onBlur) + input.addEventListener('focus', this.onFocus) + input.addEventListener('paste', this.onPaste) + input.addEventListener('keyup', this.onKeyUp) + input.addEventListener('keydown', this.onKeyDown) + input.addEventListener('click', this.onClickInput) + input.addEventListener('transitionend', this.onTransition) + input.addEventListener('input', this.onInput) }, unmounted () { const { input } = this if (input) { - input.elm.removeEventListener('blur', this.onBlur) - input.elm.removeEventListener('focus', this.onFocus) - input.elm.removeEventListener('paste', this.onPaste) - input.elm.removeEventListener('keyup', this.onKeyUp) - input.elm.removeEventListener('keydown', this.onKeyDown) - input.elm.removeEventListener('click', this.onClickInput) - input.elm.removeEventListener('transitionend', this.onTransition) - input.elm.removeEventListener('input', this.onInput) + input.removeEventListener('blur', this.onBlur) + input.removeEventListener('focus', this.onFocus) + input.removeEventListener('paste', this.onPaste) + input.removeEventListener('keyup', this.onKeyUp) + input.removeEventListener('keydown', this.onKeyDown) + input.removeEventListener('click', this.onClickInput) + input.removeEventListener('transitionend', this.onTransition) + input.removeEventListener('input', this.onInput) } }, watch: { @@ -194,11 +194,18 @@ const EmojiInput = { } }, methods: { + focusPickerInput () { + const pickerEl = this.$refs.picker.$el + if (!pickerEl) return + const pickerInput = pickerEl.querySelector('input') + if (pickerInput) pickerInput.focus() + }, triggerShowPicker () { this.showPicker = true this.$refs.picker.startEmojiLoad() this.$nextTick(() => { this.scrollIntoView() + this.focusPickerInput() }) // This temporarily disables "click outside" handler // since external trigger also means click originates @@ -209,11 +216,12 @@ const EmojiInput = { }, 0) }, togglePicker () { - this.input.elm.focus() + this.input.focus() this.showPicker = !this.showPicker if (this.showPicker) { this.scrollIntoView() this.$refs.picker.startEmojiLoad() + this.$nextTick(this.focusPickerInput) } }, replace (replacement) { @@ -254,13 +262,13 @@ const EmojiInput = { this.$emit('input', newValue) const position = this.caret + (insertion + spaceAfter + spaceBefore).length if (!keepOpen) { - this.input.elm.focus() + this.input.focus() } this.$nextTick(function () { // Re-focus inputbox after clicking suggestion // Set selection right after the replacement instead of the very end - this.input.elm.setSelectionRange(position, position) + this.input.setSelectionRange(position, position) this.caret = position }) }, @@ -277,9 +285,9 @@ const EmojiInput = { this.$nextTick(function () { // Re-focus inputbox after clicking suggestion - this.input.elm.focus() + this.input.focus() // Set selection right after the replacement instead of the very end - this.input.elm.setSelectionRange(position, position) + this.input.setSelectionRange(position, position) this.caret = position }) e.preventDefault() @@ -341,7 +349,7 @@ const EmojiInput = { } this.$nextTick(() => { - const { offsetHeight } = this.input.elm + const { offsetHeight } = this.input const { picker } = this.$refs const pickerBottom = picker.$el.getBoundingClientRect().bottom if (pickerBottom > window.innerHeight) { @@ -406,8 +414,8 @@ const EmojiInput = { // Scroll the input element to the position of the cursor this.$nextTick(() => { - this.input.elm.blur() - this.input.elm.focus() + this.input.blur() + this.input.focus() }) } // Disable suggestions hotkeys if suggestions are hidden @@ -436,7 +444,7 @@ const EmojiInput = { // de-focuses the element (i.e. default browser behavior) if (key === 'Escape') { if (!this.temporarilyHideSuggestions) { - this.input.elm.focus() + this.input.focus() } } @@ -472,7 +480,7 @@ const EmojiInput = { if (!panel) return const picker = this.$refs.picker.$el const panelBody = this.$refs['panel-body'] - const { offsetHeight, offsetTop } = this.input.elm + const { offsetHeight, offsetTop } = this.input const offsetBottom = offsetTop + offsetHeight this.setPlacement(panelBody, panel, offsetBottom) @@ -486,7 +494,7 @@ const EmojiInput = { if (this.placement === 'top' || (this.placement === 'auto' && this.overflowsBottom(container))) { target.style.top = 'auto' - target.style.bottom = this.input.elm.offsetHeight + 'px' + target.style.bottom = this.input.offsetHeight + 'px' } }, overflowsBottom (el) { diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue @@ -1,5 +1,6 @@ <template> <div + ref="root" v-click-outside="onClickOutside" class="emoji-input" :class="{ 'with-picker': !hideEmojiButton }" @@ -9,6 +10,7 @@ <button v-if="!hideEmojiButton" class="button-unstyled emoji-picker-icon" + type="button" @click.prevent="togglePicker" > <FAIcon :icon="['far', 'smile-beam']" /> diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js @@ -116,8 +116,8 @@ export const suggestUsers = ({ dispatch, state }) => { return diff + nameAlphabetically + screenNameAlphabetically /* eslint-disable camelcase */ - }).map(({ screen_name, name, profile_image_url_original }) => ({ - displayText: screen_name, + }).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({ + displayText: screen_name_ui, detailText: name, imageUrl: profile_image_url_original, replacement: '@' + screen_name + ' ' diff --git a/src/components/export_import/export_import.vue b/src/components/export_import/export_import.vue @@ -1,102 +0,0 @@ -<template> - <div class="import-export-container"> - <slot name="before" /> - <button - class="btn button-default" - @click="exportData" - > - {{ exportLabel }} - </button> - <button - class="btn button-default" - @click="importData" - > - {{ importLabel }} - </button> - <slot name="afterButtons" /> - <p - v-if="importFailed" - class="alert error" - > - {{ importFailedText }} - </p> - <slot name="afterError" /> - </div> -</template> - -<script> -export default { - props: [ - 'exportObject', - 'importLabel', - 'exportLabel', - 'importFailedText', - 'validator', - 'onImport', - 'onImportFailure' - ], - data () { - return { - importFailed: false - } - }, - methods: { - exportData () { - const stringified = JSON.stringify(this.exportObject, null, 2) // Pretty-print and indent with 2 spaces - - // Create an invisible link with a data url and simulate a click - const e = document.createElement('a') - e.setAttribute('download', 'pleroma_theme.json') - e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified)) - e.style.display = 'none' - - document.body.appendChild(e) - e.click() - document.body.removeChild(e) - }, - importData () { - this.importFailed = false - const filePicker = document.createElement('input') - filePicker.setAttribute('type', 'file') - filePicker.setAttribute('accept', '.json') - - filePicker.addEventListener('change', event => { - if (event.target.files[0]) { - // eslint-disable-next-line no-undef - const reader = new FileReader() - reader.onload = ({ target }) => { - try { - const parsed = JSON.parse(target.result) - const valid = this.validator(parsed) - if (valid) { - this.onImport(parsed) - } else { - this.importFailed = true - // this.onImportFailure(valid) - } - } catch (e) { - // This will happen both if there is a JSON syntax error or the theme is missing components - this.importFailed = true - // this.onImportFailure(e) - } - } - reader.readAsText(event.target.files[0]) - } - }) - - document.body.appendChild(filePicker) - filePicker.click() - document.body.removeChild(filePicker) - } - } -} -</script> - -<style lang="scss"> -.import-export-container { - display: flex; - flex-wrap: wrap; - align-items: baseline; - justify-content: center; -} -</style> diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue @@ -7,10 +7,7 @@ :bound-to="{ x: 'container' }" remove-padding > - <div - slot="content" - slot-scope="{close}" - > + <template v-slot:content="{close}"> <div class="dropdown-menu"> <button v-if="canMute && !status.thread_muted" @@ -120,16 +117,15 @@ /><span>{{ $t("user_card.report") }}</span> </button> </div> - </div> - <span - slot="trigger" - class="popover-trigger" - > - <FAIcon - class="fa-scale-110 fa-old-padding" - icon="ellipsis-h" - /> - </span> + </template> + <template v-slot:trigger> + <button class="button-unstyled popover-trigger"> + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="ellipsis-h" + /> + </button> + </template> </Popover> </template> @@ -139,6 +135,11 @@ @import '../../_variables.scss'; .ExtraButtons { + /* override of popover internal stuff */ + .popover-trigger-button { + width: auto; + } + .popover-trigger { position: static; padding: 10px; diff --git a/src/components/features_panel/features_panel.js b/src/components/features_panel/features_panel.js @@ -2,7 +2,7 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for const FeaturesPanel = { computed: { - chat: function () { return this.$store.state.instance.chatAvailable }, + shout: function () { return this.$store.state.instance.shoutAvailable }, pleromaChatMessages: function () { return this.$store.state.instance.pleromaChatMessagesAvailable }, gopher: function () { return this.$store.state.instance.gopherAvailable }, whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled }, diff --git a/src/components/features_panel/features_panel.vue b/src/components/features_panel/features_panel.vue @@ -8,8 +8,8 @@ </div> <div class="panel-body features-panel"> <ul> - <li v-if="chat"> - {{ $t('features_panel.chat') }} + <li v-if="shout"> + {{ $t('features_panel.shout') }} </li> <li v-if="pleromaChatMessages"> {{ $t('features_panel.pleroma_chat_messages') }} diff --git a/src/components/flash/flash.js b/src/components/flash/flash.js @@ -0,0 +1,53 @@ +import RuffleService from '../../services/ruffle_service/ruffle_service.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faStop, + faExclamationTriangle +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faStop, + faExclamationTriangle +) + +const Flash = { + props: [ 'src' ], + data () { + return { + player: false, // can be true, "hidden", false. hidden = element exists + loaded: false, + ruffleInstance: null + } + }, + methods: { + openPlayer () { + if (this.player) return // prevent double-loading, or re-loading on failure + this.player = 'hidden' + RuffleService.getRuffle().then((ruffle) => { + const player = ruffle.newest().createPlayer() + player.config = { + letterbox: 'on' + } + const container = this.$refs.container + container.appendChild(player) + player.style.width = '100%' + player.style.height = '100%' + player.load(this.src).then(() => { + this.player = true + }).catch((e) => { + console.error('Error loading ruffle', e) + this.player = 'error' + }) + this.ruffleInstance = player + this.$emit('playerOpened') + }) + }, + closePlayer () { + this.ruffleInstance && this.ruffleInstance.remove() + this.player = false + this.$emit('playerClosed') + } + } +} + +export default Flash diff --git a/src/components/flash/flash.vue b/src/components/flash/flash.vue @@ -0,0 +1,84 @@ +<template> + <div class="Flash"> + <div + v-if="player === true || player === 'hidden'" + ref="container" + class="player" + :class="{ hidden: player === 'hidden' }" + /> + <button + v-if="player !== true" + class="button-unstyled placeholder" + @click="openPlayer" + > + <span + v-if="player === 'hidden'" + class="label" + > + {{ $t('general.loading') }} + </span> + <span + v-if="player === 'error'" + class="label" + > + {{ $t('general.flash_fail') }} + </span> + <span + v-else + class="label" + > + <p> + {{ $t('general.flash_content') }} + </p> + <p> + <FAIcon icon="exclamation-triangle" /> + {{ $t('general.flash_security') }} + </p> + </span> + </button> + </div> +</template> + +<script src="./flash.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; +.Flash { + display: inline-block; + width: 100%; + height: 100%; + position: relative; + + .player { + height: 100%; + width: 100%; + } + + .placeholder { + height: 100%; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg); + color: var(--link); + } + + .hider { + top: 0; + } + + .label { + text-align: center; + flex: 1 1 0; + line-height: 1.2; + white-space: normal; + word-wrap: normal; + } + + .hidden { + display: none; + visibility: 'hidden'; + } +} +</style> diff --git a/src/components/follow_button/follow_button.js b/src/components/follow_button/follow_button.js @@ -1,6 +1,6 @@ import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' export default { - props: ['relationship', 'labelFollowing', 'buttonClass'], + props: ['relationship', 'user', 'labelFollowing', 'buttonClass'], data () { return { inProgress: false @@ -14,7 +14,7 @@ export default { if (this.inProgress || this.relationship.following) { return this.$t('user_card.follow_unfollow') } else if (this.relationship.requested) { - return this.$t('user_card.follow_again') + return this.$t('user_card.follow_cancel') } else { return this.$t('user_card.follow') } @@ -29,11 +29,14 @@ export default { } else { return this.$t('user_card.follow') } + }, + disabled () { + return this.inProgress || this.user.deactivated } }, methods: { onClick () { - this.relationship.following ? this.unfollow() : this.follow() + this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow() }, follow () { this.inProgress = true diff --git a/src/components/follow_button/follow_button.vue b/src/components/follow_button/follow_button.vue @@ -2,7 +2,7 @@ <button class="btn button-default follow-button" :class="{ toggled: isPressed }" - :disabled="inProgress" + :disabled="disabled" :title="title" @click="onClick" > diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue @@ -20,6 +20,7 @@ :relationship="relationship" :label-following="$t('user_card.follow_unfollow')" class="follow-card-follow-button" + :user="user" /> </template> </div> diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js @@ -1,14 +1,10 @@ import { set } from 'vue' -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faChevronDown -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faChevronDown -) +import Select from '../select/select.vue' export default { + components: { + Select + }, props: [ 'name', 'label', 'value', 'fallback', 'options', 'no-inherit' ], diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue @@ -22,30 +22,20 @@ class="opt-l" :for="name + '-o'" /> - <label - :for="name + '-font-switcher'" - class="select" + <Select + :id="name + '-font-switcher'" + v-model="preset" :disabled="!present" + class="font-switcher" > - <select - :id="name + '-font-switcher'" - v-model="preset" - :disabled="!present" - class="font-switcher" + <option + v-for="option in availableOptions" + :key="option" + :value="option" > - <option - v-for="option in availableOptions" - :key="option" - :value="option" - > - {{ option === 'custom' ? $t('settings.style.fonts.custom') : option }} - </option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> + {{ option === 'custom' ? $t('settings.style.fonts.custom') : option }} + </option> + </Select> <input v-if="isCustom" :id="name" @@ -65,7 +55,8 @@ min-width: 10em; } &.custom { - .select { + /* TODO Should make proper joiners... */ + .font-switcher { border-top-right-radius: 0; border-bottom-right-radius: 0; } diff --git a/src/components/gallery/gallery.js b/src/components/gallery/gallery.js @@ -1,15 +1,26 @@ import Attachment from '../attachment/attachment.vue' -import { chunk, last, dropRight, sumBy } from 'lodash' +import { sumBy } from 'lodash' const Gallery = { props: [ 'attachments', + 'limitRows', + 'descriptions', + 'limit', 'nsfw', - 'setMedia' + 'setMedia', + 'size', + 'editable', + 'removeAttachment', + 'shiftUpAttachment', + 'shiftDnAttachment', + 'editAttachment', + 'grid' ], data () { return { - sizes: {} + sizes: {}, + hidingLong: true } }, components: { Attachment }, @@ -18,26 +29,70 @@ const Gallery = { if (!this.attachments) { return [] } - const rows = chunk(this.attachments, 3) - if (last(rows).length === 1 && rows.length > 1) { - // if 1 attachment on last row -> add it to the previous row instead - const lastAttachment = last(rows)[0] - const allButLastRow = dropRight(rows) - last(allButLastRow).push(lastAttachment) - return allButLastRow + const attachments = this.limit > 0 + ? this.attachments.slice(0, this.limit) + : this.attachments + if (this.size === 'hide') { + return attachments.map(item => ({ minimal: true, items: [item] })) } + const rows = this.grid + ? [{ grid: true, items: attachments }] + : attachments.reduce((acc, attachment, i) => { + if (attachment.mimetype.includes('audio')) { + return [...acc, { audio: true, items: [attachment] }, { items: [] }] + } + if (!( + attachment.mimetype.includes('image') || + attachment.mimetype.includes('video') || + attachment.mimetype.includes('flash') + )) { + return [...acc, { minimal: true, items: [attachment] }, { items: [] }] + } + const maxPerRow = 3 + const attachmentsRemaining = this.attachments.length - i + 1 + const currentRow = acc[acc.length - 1].items + currentRow.push(attachment) + if (currentRow.length >= maxPerRow && attachmentsRemaining > maxPerRow) { + return [...acc, { items: [] }] + } else { + return acc + } + }, [{ items: [] }]).filter(_ => _.items.length > 0) return rows }, - useContainFit () { - return this.$store.getters.mergedConfig.useContainFit + attachmentsDimensionalScore () { + return this.rows.reduce((acc, row) => { + let size = 0 + if (row.minimal) { + size += 1 / 8 + } else if (row.audio) { + size += 1 / 4 + } else { + size += 1 / (row.items.length + 0.6) + } + return acc + size + }, 0) + }, + tooManyAttachments () { + if (this.editable || this.size === 'small') { + return false + } else if (this.size === 'hide') { + return this.attachments.length > 8 + } else { + return this.attachmentsDimensionalScore > 1 + } } }, methods: { - onNaturalSizeLoad (id, size) { - this.$set(this.sizes, id, size) + onNaturalSizeLoad ({ id, width, height }) { + this.$set(this.sizes, id, { width, height }) }, - rowStyle (itemsPerRow) { - return { 'padding-bottom': `${(100 / (itemsPerRow + 0.6))}%` } + rowStyle (row) { + if (row.audio) { + return { 'padding-bottom': '25%' } // fixed reduced height for audio + } else if (!row.minimal && !row.grid) { + return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` } + } }, itemStyle (id, row) { const total = sumBy(row, item => this.getAspectRatio(item.id)) @@ -46,6 +101,16 @@ const Gallery = { getAspectRatio (id) { const size = this.sizes[id] return size ? size.width / size.height : 1 + }, + toggleHidingLong (event) { + this.hidingLong = event + }, + openGallery () { + this.$store.dispatch('setMedia', this.attachments) + this.$store.dispatch('setCurrentMedia', this.attachments[0]) + }, + onMedia () { + this.$store.dispatch('setMedia', this.attachments) } } } diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue @@ -1,26 +1,84 @@ <template> <div ref="galleryContainer" - style="width: 100%;" + class="Gallery" + :class="{ '-long': tooManyAttachments && hidingLong }" > + <div class="gallery-rows"> + <div + v-for="(row, rowIndex) in rows" + :key="rowIndex" + class="gallery-row" + :style="rowStyle(row)" + :class="{ '-audio': row.audio, '-minimal': row.minimal, '-grid': grid }" + > + <div + class="gallery-row-inner" + :class="{ '-grid': grid }" + > + <Attachment + v-for="(attachment, attachmentIndex) in row.items" + :key="attachment.id" + class="gallery-item" + :nsfw="nsfw" + :attachment="attachment" + :allow-play="false" + :size="size" + :editable="editable" + :remove="removeAttachment" + :shiftUp="!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment" + :shiftDn="!(attachmentIndex === row.items.length - 1 && rowIndex === rows.length - 1) && shiftDnAttachment" + :edit="editAttachment" + :description="descriptions && descriptions[attachment.id]" + :hide-description="size === 'small' || tooManyAttachments && hidingLong" + :style="itemStyle(attachment.id, row.items)" + @setMedia="onMedia" + @naturalSizeLoad="onNaturalSizeLoad" + /> + </div> + </div> + </div> <div - v-for="(row, index) in rows" - :key="index" - class="gallery-row" - :style="rowStyle(row.length)" - :class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }" + v-if="tooManyAttachments" + class="many-attachments" > - <div class="gallery-row-inner"> - <attachment - v-for="attachment in row" - :key="attachment.id" - :set-media="setMedia" - :nsfw="nsfw" - :attachment="attachment" - :allow-play="false" - :natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)" - :style="itemStyle(attachment.id, row)" - /> + <div class="many-attachments-text"> + {{ $t("status.many_attachments", { number: attachments.length }) }} + </div> + <div class="many-attachments-buttons"> + <span + v-if="!hidingLong" + class="many-attachments-button" + > + <button + class="button-unstyled -link" + @click="toggleHidingLong(true)" + > + {{ $t("status.collapse_attachments") }} + </button> + </span> + <span + v-if="hidingLong" + class="many-attachments-button" + > + <button + class="button-unstyled -link" + @click="toggleHidingLong(false)" + > + {{ $t("status.show_all_attachments") }} + </button> + </span> + <span + v-if="hidingLong" + class="many-attachments-button" + > + <button + class="button-unstyled -link" + @click="openGallery" + > + {{ $t("status.open_gallery") }} + </button> + </span> </div> </div> </div> @@ -31,12 +89,66 @@ <style lang="scss"> @import '../../_variables.scss'; -.gallery-row { - position: relative; - height: 0; - width: 100%; - flex-grow: 1; - margin-top: 0.5em; +.Gallery { + .gallery-rows { + display: flex; + flex-direction: column; + } + + .gallery-row { + position: relative; + height: 0; + width: 100%; + flex-grow: 1; + + &:not(:first-child) { + margin-top: 0.5em; + } + } + + &.-long { + .gallery-rows { + max-height: 25em; + overflow: hidden; + mask: + linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat, + linear-gradient(to top, white, white); + + /* Autoprefixed seem to ignore this one, and also syntax is different */ + -webkit-mask-composite: xor; + mask-composite: exclude; + } + } + + .many-attachments-text { + text-align: center; + line-height: 2; + } + + .many-attachments-buttons { + display: flex; + } + + .many-attachments-button { + display: flex; + flex: 1; + justify-content: center; + line-height: 2; + + button { + padding: 0 2em; + } + } + + .gallery-row { + &.-grid, + &.-minimal { + height: auto; + .gallery-row-inner { + position: relative; + } + } + } .gallery-row-inner { position: absolute; @@ -48,9 +160,24 @@ flex-direction: row; flex-wrap: nowrap; align-content: stretch; + + &.-grid { + width: 100%; + height: auto; + position: relative; + display: grid; + grid-column-gap: 0.5em; + grid-row-gap: 0.5em; + grid-template-columns: repeat(auto-fill, minmax(15em, 1fr)); + + .gallery-item { + margin: 0; + height: 200px; + } + } } - .gallery-row-inner .attachment { + .gallery-item { margin: 0 0.5em 0 0; flex-grow: 1; height: 100%; @@ -61,32 +188,5 @@ margin: 0; } } - - .image-attachment { - width: 100%; - height: 100%; - } - - .video-container { - height: 100%; - } - - &.contain-fit { - img, - video, - canvas { - object-fit: contain; - height: 100%; - } - } - - &.cover-fit { - img, - video, - canvas { - object-fit: cover; - } - } } - </style> diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue @@ -71,6 +71,14 @@ } } + .global-success { + background-color: var(--alertPopupSuccess, $fallback--cGreen); + color: var(--alertPopupSuccessText, $fallback--text); + .svg-inline--fa { + color: var(--alertPopupSuccessText, $fallback--text); + } + } + .global-info { background-color: var(--alertPopupNeutral, $fallback--fg); color: var(--alertPopupNeutralText, $fallback--text); diff --git a/src/components/hashtag_link/hashtag_link.js b/src/components/hashtag_link/hashtag_link.js @@ -0,0 +1,36 @@ +import { extractTagFromUrl } from 'src/services/matcher/matcher.service.js' + +const HashtagLink = { + name: 'HashtagLink', + props: { + url: { + required: true, + type: String + }, + content: { + required: true, + type: String + }, + tag: { + required: false, + type: String, + default: '' + } + }, + methods: { + onClick () { + const tag = this.tag || extractTagFromUrl(this.url) + if (tag) { + const link = this.generateTagLink(tag) + this.$router.push(link) + } else { + window.open(this.url, '_blank') + } + }, + generateTagLink (tag) { + return `/tag/${tag}` + } + } +} + +export default HashtagLink diff --git a/src/components/hashtag_link/hashtag_link.scss b/src/components/hashtag_link/hashtag_link.scss @@ -0,0 +1,6 @@ +.HashtagLink { + position: relative; + white-space: normal; + display: inline-block; + color: var(--link); +} diff --git a/src/components/hashtag_link/hashtag_link.vue b/src/components/hashtag_link/hashtag_link.vue @@ -0,0 +1,19 @@ +<template> + <span + class="HashtagLink" + > + <!-- eslint-disable vue/no-v-html --> + <a + :href="url" + class="original" + target="_blank" + @click.prevent="onClick" + v-html="content" + /> + <!-- eslint-enable vue/no-v-html --> + </span> +</template> + +<script src="./hashtag_link.js"/> + +<style lang="scss" src="./hashtag_link.scss"/> diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -3,27 +3,18 @@ <label for="interface-language-switcher"> {{ $t('settings.interfaceLanguage') }} </label> - <label - for="interface-language-switcher" - class="select" + <Select + id="interface-language-switcher" + v-model="language" > - <select - id="interface-language-switcher" - v-model="language" + <option + v-for="lang in languages" + :key="lang.code" + :value="lang.code" > - <option - v-for="lang in languages" - :key="lang.code" - :value="lang.code" - > - {{ lang.name }} - </option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> + {{ lang.name }} + </option> + </Select> </div> </template> @@ -32,16 +23,12 @@ import languagesObject from '../../i18n/messages' import localeService from '../../services/locale/locale.service.js' import ISO6391 from 'iso-639-1' import _ from 'lodash' -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faChevronDown -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faChevronDown -) +import Select from '../select/select.vue' export default { + components: { + Select + }, computed: { languages () { return _.map(languagesObject.languages, (code) => ({ code: code, name: this.getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name)) diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js @@ -3,22 +3,31 @@ import VideoAttachment from '../video_attachment/video_attachment.vue' import Modal from '../modal/modal.vue' import fileTypeService from '../../services/file_type/file_type.service.js' import GestureService from '../../services/gesture_service/gesture_service' +import Flash from 'src/components/flash/flash.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronLeft, - faChevronRight + faChevronRight, + faCircleNotch } from '@fortawesome/free-solid-svg-icons' library.add( faChevronLeft, - faChevronRight + faChevronRight, + faCircleNotch ) const MediaModal = { components: { StillImage, VideoAttachment, - Modal + Modal, + Flash + }, + data () { + return { + loading: false + } }, computed: { showing () { @@ -27,6 +36,9 @@ const MediaModal = { media () { return this.$store.state.mediaViewer.media }, + description () { + return this.currentMedia.description + }, currentIndex () { return this.$store.state.mediaViewer.currentIndex }, @@ -37,7 +49,7 @@ const MediaModal = { return this.media.length > 1 }, type () { - return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null + return this.currentMedia ? this.getType(this.currentMedia) : null } }, created () { @@ -53,6 +65,9 @@ const MediaModal = { ) }, methods: { + getType (media) { + return fileTypeService.fileType(media.mimetype) + }, mediaTouchStart (e) { GestureService.beginSwipe(e, this.mediaSwipeGestureRight) GestureService.beginSwipe(e, this.mediaSwipeGestureLeft) @@ -67,15 +82,26 @@ const MediaModal = { goPrev () { if (this.canNavigate) { const prevIndex = this.currentIndex === 0 ? this.media.length - 1 : (this.currentIndex - 1) - this.$store.dispatch('setCurrent', this.media[prevIndex]) + const newMedia = this.media[prevIndex] + if (this.getType(newMedia) === 'image') { + this.loading = true + } + this.$store.dispatch('setCurrentMedia', newMedia) } }, goNext () { if (this.canNavigate) { const nextIndex = this.currentIndex === this.media.length - 1 ? 0 : (this.currentIndex + 1) - this.$store.dispatch('setCurrent', this.media[nextIndex]) + const newMedia = this.media[nextIndex] + if (this.getType(newMedia) === 'image') { + this.loading = true + } + this.$store.dispatch('setCurrentMedia', newMedia) } }, + onImageLoaded () { + this.loading = false + }, handleKeyupEvent (e) { if (this.showing && e.keyCode === 27) { // escape this.hide() diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue @@ -6,6 +6,7 @@ > <img v-if="type === 'image'" + :class="{ loading }" class="modal-image" :src="currentMedia.url" :alt="currentMedia.description" @@ -13,6 +14,7 @@ @touchstart.stop="mediaTouchStart" @touchmove.stop="mediaTouchMove" @click="hide" + @load="onImageLoaded" > <VideoAttachment v-if="type === 'video'" @@ -28,6 +30,13 @@ :title="currentMedia.description" controls /> + <Flash + v-if="type === 'flash'" + class="modal-image" + :src="currentMedia.url" + :alt="currentMedia.description" + :title="currentMedia.description" + /> <button v-if="canNavigate" :title="$t('media_modal.previous')" @@ -50,6 +59,27 @@ icon="chevron-right" /> </button> + <span + v-if="description" + class="description" + > + {{ description }} + </span> + <span + class="counter" + > + {{ $tc('media_modal.counter', currentIndex + 1, { current: currentIndex + 1, total: media.length }) }} + </span> + <span + v-if="loading" + class="loading-spinner" + > + <FAIcon + spin + icon="circle-notch" + size="5x" + /> + </span> </Modal> </template> @@ -58,6 +88,7 @@ <style lang="scss"> .modal-view.media-modal-view { z-index: 1001; + flex-direction: column; .modal-view-button-arrow { opacity: 0.75; @@ -67,59 +98,108 @@ outline: none; box-shadow: none; } + &:hover { opacity: 1; } } } -.modal-image { - max-width: 90%; - max-height: 90%; - box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5); - image-orientation: from-image; // NOTE: only FF supports this -} +.media-modal-view { + @keyframes media-fadein { + from { + opacity: 0; + } + to { + opacity: 1; + } + } -.modal-view-button-arrow { - position: absolute; - display: block; - top: 50%; - margin-top: -50px; - width: 70px; - height: 100px; - border: 0; - padding: 0; - opacity: 0; - box-shadow: none; - background: none; - appearance: none; - overflow: visible; - cursor: pointer; - transition: opacity 333ms cubic-bezier(.4,0,.22,1); - - .arrow-icon { - position: absolute; - top: 35px; - height: 30px; - width: 32px; - font-size: 14px; - line-height: 30px; - color: #FFF; - text-align: center; - background-color: rgba(0,0,0,.3); + .description, + .counter { + /* Hardcoded since background is also hardcoded */ + color: white; + margin-top: 1em; + text-shadow: 0 0 10px black, 0 0 10px black; + padding: 0.2em 2em; } - &--prev { - left: 0; - .arrow-icon { - left: 6px; + .description { + flex: 0 0 auto; + overflow-y: auto; + min-height: 1em; + max-width: 500px; + max-height: 9.5em; + word-break: break-all; + } + + .modal-image { + max-width: 90%; + max-height: 90%; + box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5); + image-orientation: from-image; // NOTE: only FF supports this + animation: 0.1s cubic-bezier(0.7, 0, 1, 0.6) media-fadein; + + &.loading { + opacity: 0.5; + } + } + + .loading-spinner { + width: 100%; + height: 100%; + position: absolute; + pointer-events: none; + display: flex; + justify-content: center; + align-items: center; + + svg { + color: white; } } - &--next { - right: 0; + .modal-view-button-arrow { + position: absolute; + display: block; + top: 50%; + margin-top: -50px; + width: 70px; + height: 100px; + border: 0; + padding: 0; + opacity: 0; + box-shadow: none; + background: none; + appearance: none; + overflow: visible; + cursor: pointer; + transition: opacity 333ms cubic-bezier(.4,0,.22,1); + .arrow-icon { - right: 6px; + position: absolute; + top: 35px; + height: 30px; + width: 32px; + font-size: 14px; + line-height: 30px; + color: #FFF; + text-align: center; + background-color: rgba(0,0,0,.3); + } + + &--prev { + left: 0; + .arrow-icon { + left: 6px; + } + } + + &--next { + right: 0; + .arrow-icon { + right: 6px; + } } } } diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js @@ -0,0 +1,134 @@ +import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +import { mapGetters, mapState } from 'vuex' +import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' +import UserAvatar from '../user_avatar/user_avatar.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faAt +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faAt +) + +const MentionLink = { + name: 'MentionLink', + components: { + UserAvatar + }, + props: { + url: { + required: true, + type: String + }, + content: { + required: true, + type: String + }, + userId: { + required: false, + type: String + }, + userScreenName: { + required: false, + type: String + } + }, + methods: { + onClick () { + const link = generateProfileLink( + this.userId || this.user.id, + this.userScreenName || this.user.screen_name + ) + this.$router.push(link) + } + }, + computed: { + user () { + return this.url && this.$store && this.$store.getters.findUserByUrl(this.url) + }, + isYou () { + // FIXME why user !== currentUser??? + return this.user && this.user.id === this.currentUser.id + }, + userName () { + return this.user && this.userNameFullUi.split('@')[0] + }, + serverName () { + // XXX assumed that domain does not contain @ + return this.user && (this.userNameFullUi.split('@')[1] || this.$store.getters.instanceDomain) + }, + userNameFull () { + return this.user && this.user.screen_name + }, + userNameFullUi () { + return this.user && this.user.screen_name_ui + }, + highlight () { + return this.user && this.mergedConfig.highlight[this.user.screen_name] + }, + highlightType () { + return this.highlight && ('-' + this.highlight.type) + }, + highlightClass () { + if (this.highlight) return highlightClass(this.user) + }, + style () { + if (this.highlight) { + const { + backgroundColor, + backgroundPosition, + backgroundImage, + ...rest + } = highlightStyle(this.highlight) + return rest + } + }, + classnames () { + return [ + { + '-you': this.isYou && this.shouldBoldenYou, + '-highlighted': this.highlight + }, + this.highlightType + ] + }, + useAtIcon () { + return this.mergedConfig.useAtIcon + }, + isRemote () { + return this.userName !== this.userNameFull + }, + shouldShowFullUserName () { + const conf = this.mergedConfig.mentionLinkDisplay + if (conf === 'short') { + return false + } else if (conf === 'full') { + return true + } else { // full_for_remote + return this.isRemote + } + }, + shouldShowTooltip () { + return this.mergedConfig.mentionLinkShowTooltip && this.mergedConfig.mentionLinkDisplay === 'short' && this.isRemote + }, + shouldShowAvatar () { + return this.mergedConfig.mentionLinkShowAvatar + }, + shouldShowYous () { + return this.mergedConfig.mentionLinkShowYous + }, + shouldBoldenYou () { + return this.mergedConfig.mentionLinkBoldenYou + }, + shouldFadeDomain () { + return this.mergedConfig.mentionLinkFadeDomain + }, + ...mapGetters(['mergedConfig']), + ...mapState({ + currentUser: state => state.users.currentUser + }) + } +} + +export default MentionLink diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss @@ -0,0 +1,116 @@ +@import '../../_variables.scss'; + +.MentionLink { + position: relative; + white-space: normal; + display: inline; + color: var(--link); + word-break: normal; + + & .new, + & .original { + display: inline; + border-radius: 2px; + } + + .mention-avatar { + border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + width: 1.5em; + height: 1.5em; + vertical-align: middle; + user-select: none; + margin-right: 0.2em; + } + + .full { + position: absolute; + display: inline-block; + pointer-events: none; + opacity: 0; + top: 100%; + left: 0; + height: 100%; + word-wrap: normal; + white-space: nowrap; + transition: opacity 0.2s ease; + z-index: 1; + margin-top: 0.25em; + padding: 0.5em; + user-select: all; + } + + & .short.-with-tooltip, + & .you { + user-select: none; + } + + & .short, + & .full { + white-space: nowrap; + } + + .shortName { + white-space: normal; + } + + .new { + &.-you { + & .shortName, + & .full { + font-weight: 600; + } + } + + .at { + color: var(--link); + opacity: 0.8; + display: inline-block; + height: 50%; + line-height: 1; + padding: 0 0.1em; + vertical-align: -25%; + margin: 0; + } + + &.-striped { + & .shortName, + & .full { + background-image: + repeating-linear-gradient( + 135deg, + var(--____highlight-tintColor), + var(--____highlight-tintColor) 5px, + var(--____highlight-tintColor2) 5px, + var(--____highlight-tintColor2) 10px + ); + } + } + + &.-solid { + & .shortName, + & .full { + background-image: linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2)); + } + } + + &.-side { + & .shortName, + & .userNameFull { + box-shadow: 0 -5px 3px -4px inset var(--____highlight-solidColor); + } + } + } + + &:hover .new .full { + opacity: 1; + pointer-events: initial; + } + + .serverName.-faded { + color: var(--faintLink, $fallback--link); + } + + .full .-faded { + color: var(--faint, $fallback--faint); + } +} diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue @@ -0,0 +1,75 @@ +<template> + <span + class="MentionLink" + > + <!-- eslint-disable vue/no-v-html --> + <a + v-if="!user" + :href="url" + class="original" + target="_blank" + v-html="content" + /><!-- eslint-enable vue/no-v-html --><span + v-if="user" + class="new" + :style="style" + :class="classnames" + > + <a + class="short button-unstyled" + :class="{ '-with-tooltip': shouldShowTooltip }" + :href="url" + @click.prevent="onClick" + > + <!-- eslint-disable vue/no-v-html --> + <UserAvatar + v-if="shouldShowAvatar" + class="mention-avatar" + :user="user" + /><span + class="shortName" + ><FAIcon + v-if="useAtIcon" + size="sm" + icon="at" + class="at" + />{{ !useAtIcon ? '@' : '' }}<span + class="userName" + v-html="userName" + /><span + v-if="shouldShowFullUserName" + class="serverName" + :class="{ '-faded': shouldFadeDomain }" + v-html="'@' + serverName" + /></span><span + v-if="isYou && shouldShowYous" + :class="{ '-you': shouldBoldenYou }" + > {{ $t('status.you') }}</span> + <!-- eslint-enable vue/no-v-html --> + </a><span + v-if="shouldShowTooltip" + class="full popover-default" + :class="[highlightType]" + > + <span + class="userNameFull" + > + <!-- eslint-disable vue/no-v-html --> + @<span + class="userName" + v-html="userName" + /><span + class="serverName" + :class="{ '-faded': shouldFadeDomain }" + v-html="'@' + serverName" + /> + <!-- eslint-enable vue/no-v-html --> + </span> + </span> + </span> + </span> +</template> + +<script src="./mention_link.js"/> + +<style lang="scss" src="./mention_link.scss"/> diff --git a/src/components/mentions_line/mentions_line.js b/src/components/mentions_line/mentions_line.js @@ -0,0 +1,37 @@ +import MentionLink from 'src/components/mention_link/mention_link.vue' +import { mapGetters } from 'vuex' + +export const MENTIONS_LIMIT = 5 + +const MentionsLine = { + name: 'MentionsLine', + props: { + mentions: { + required: true, + type: Array + } + }, + data: () => ({ expanded: false }), + components: { + MentionLink + }, + computed: { + mentionsComputed () { + return this.mentions.slice(0, MENTIONS_LIMIT) + }, + extraMentions () { + return this.mentions.slice(MENTIONS_LIMIT) + }, + manyMentions () { + return this.extraMentions.length > 0 + }, + ...mapGetters(['mergedConfig']) + }, + methods: { + toggleShowMore () { + this.expanded = !this.expanded + } + } +} + +export default MentionsLine diff --git a/src/components/mentions_line/mentions_line.scss b/src/components/mentions_line/mentions_line.scss @@ -0,0 +1,13 @@ +.MentionsLine { + word-break: break-all; + + .mention-link:not(:first-child)::before { + content: ' '; + } + + .showMoreLess { + margin-left: 0.5em; + white-space: normal; + color: var(--link); + } +} diff --git a/src/components/mentions_line/mentions_line.vue b/src/components/mentions_line/mentions_line.vue @@ -0,0 +1,43 @@ +<template> + <span class="MentionsLine"> + <MentionLink + v-for="mention in mentionsComputed" + :key="mention.index" + class="mention-link" + :content="mention.content" + :url="mention.url" + :first-mention="false" + /><span + v-if="manyMentions" + class="extraMentions" + > + <span + v-if="expanded" + class="fullExtraMentions" + > + <MentionLink + v-for="mention in extraMentions" + :key="mention.index" + class="mention-link" + :content="mention.content" + :url="mention.url" + :first-mention="false" + /> + </span><button + v-if="!expanded" + class="button-unstyled showMoreLess" + @click="toggleShowMore" + > + {{ $t('status.plus_more', { number: extraMentions.length }) }} + </button><button + v-if="expanded" + class="button-unstyled showMoreLess" + @click="toggleShowMore" + > + {{ $t('general.show_less') }} + </button> + </span> + </span> +</template> +<script src="./mentions_line.js" ></script> +<style lang="scss" src="./mentions_line.scss" /> diff --git a/src/components/mfa_form/recovery_form.vue b/src/components/mfa_form/recovery_form.vue @@ -25,6 +25,7 @@ <div> <button class="button-unstyled -link" + type="button" @click.prevent="requireTOTP" > {{ $t('login.enter_two_factor_code') }} @@ -32,6 +33,7 @@ <br> <button class="button-unstyled -link" + type="button" @click.prevent="abortMFA" > {{ $t('general.cancel') }} diff --git a/src/components/mfa_form/totp_form.vue b/src/components/mfa_form/totp_form.vue @@ -27,6 +27,7 @@ <div> <button class="button-unstyled -link" + type="button" @click.prevent="requireRecovery" > {{ $t('login.enter_recovery_code') }} @@ -34,6 +35,7 @@ <br> <button class="button-unstyled -link" + type="button" @click.prevent="abortMFA" > {{ $t('general.cancel') }} 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 @@ -44,6 +44,9 @@ const MobilePostStatusButton = { return this.autohideFloatingPostButton && (this.hidden || this.inputActive) }, + isPersistent () { + return !!this.$store.getters.mergedConfig.showNewPostButton + }, autohideFloatingPostButton () { return !!this.$store.getters.mergedConfig.autohideFloatingPostButton } diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.vue b/src/components/mobile_post_status_button/mobile_post_status_button.vue @@ -2,7 +2,7 @@ <div v-if="isLoggedIn"> <button class="button-default new-status-button" - :class="{ 'hidden': isHidden }" + :class="{ 'hidden': isHidden, 'always-show': isPersistent }" @click="openPostForm" > <FAIcon icon="pen" /> @@ -47,7 +47,7 @@ } @media all and (min-width: 801px) { - .new-status-button { + .new-status-button:not(.always-show) { display: none; } } diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js @@ -1,6 +1,11 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { faChevronDown } from '@fortawesome/free-solid-svg-icons' + import DialogModal from '../dialog_modal/dialog_modal.vue' import Popover from '../popover/popover.vue' +library.add(faChevronDown) + const FORCE_NSFW = 'mrf_tag:media-force-nsfw' const STRIP_MEDIA = 'mrf_tag:media-strip' const FORCE_UNLISTED = 'mrf_tag:force-unlisted' diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue @@ -8,7 +8,7 @@ @show="setToggled(true)" @close="setToggled(false)" > - <div slot="content"> + <template v-slot:content> <div class="dropdown-menu"> <span v-if="user.is_local"> <button @@ -50,96 +50,98 @@ class="button-default dropdown-item" @click="toggleTag(tags.FORCE_NSFW)" > - {{ $t('user_card.admin_menu.force_nsfw') }} <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }" /> + {{ $t('user_card.admin_menu.force_nsfw') }} </button> <button class="button-default dropdown-item" @click="toggleTag(tags.STRIP_MEDIA)" > - {{ $t('user_card.admin_menu.strip_media') }} <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }" /> + {{ $t('user_card.admin_menu.strip_media') }} </button> <button class="button-default dropdown-item" @click="toggleTag(tags.FORCE_UNLISTED)" > - {{ $t('user_card.admin_menu.force_unlisted') }} <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }" /> + {{ $t('user_card.admin_menu.force_unlisted') }} </button> <button class="button-default dropdown-item" @click="toggleTag(tags.SANDBOX)" > - {{ $t('user_card.admin_menu.sandbox') }} <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }" /> + {{ $t('user_card.admin_menu.sandbox') }} </button> <button v-if="user.is_local" class="button-default dropdown-item" @click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)" > - {{ $t('user_card.admin_menu.disable_remote_subscription') }} <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }" /> + {{ $t('user_card.admin_menu.disable_remote_subscription') }} </button> <button v-if="user.is_local" class="button-default dropdown-item" @click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)" > - {{ $t('user_card.admin_menu.disable_any_subscription') }} <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }" /> + {{ $t('user_card.admin_menu.disable_any_subscription') }} </button> <button v-if="user.is_local" class="button-default dropdown-item" @click="toggleTag(tags.QUARANTINE)" > - {{ $t('user_card.admin_menu.quarantine') }} <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }" /> + {{ $t('user_card.admin_menu.quarantine') }} </button> </span> </div> - </div> - <button - slot="trigger" - class="btn button-default btn-block" - :class="{ toggled }" - > - {{ $t('user_card.admin_menu.moderation') }} - </button> + </template> + <template v-slot:trigger> + <button + class="btn button-default btn-block moderation-tools-button" + :class="{ toggled }" + > + {{ $t('user_card.admin_menu.moderation') }} + <FAIcon icon="chevron-down" /> + </button> + </template> </Popover> <portal to="modal"> <DialogModal v-if="showDeleteUserDialog" :on-cancel="deleteUserDialog.bind(this, false)" > - <template slot="header"> + <template v-slot:header> {{ $t('user_card.admin_menu.delete_user') }} </template> <p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p> - <template slot="footer"> + <template v-slot:footer> <button class="btn button-default" @click="deleteUserDialog(false)" @@ -163,25 +165,6 @@ <style lang="scss"> @import '../../_variables.scss'; -.menu-checkbox { - float: right; - min-width: 22px; - max-width: 22px; - min-height: 22px; - max-height: 22px; - line-height: 22px; - text-align: center; - border-radius: 0px; - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); - box-shadow: 0px 0px 2px black inset; - box-shadow: var(--inputShadow); - - &.menu-checkbox-checked::after { - content: '✓'; - } -} - .moderation-tools-popover { height: 100%; .trigger { @@ -189,4 +172,10 @@ height: 100%; } } + +.moderation-tools-button { + svg,i { + font-size: 0.8em; + } +} </style> diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.js b/src/components/mrf_transparency_panel/mrf_transparency_panel.js @@ -1,17 +1,56 @@ import { mapState } from 'vuex' import { get } from 'lodash' +/** + * This is for backwards compatibility. We originally didn't recieve + * extra info like a reason why an instance was rejected/quarantined/etc. + * Because we didn't want to break backwards compatibility it was decided + * to add an extra "info" key. + */ +const toInstanceReasonObject = (instances, info, key) => { + return instances.map(instance => { + if (info[key] && info[key][instance] && info[key][instance]['reason']) { + return { instance: instance, reason: info[key][instance]['reason'] } + } + return { instance: instance, reason: '' } + }) +} + const MRFTransparencyPanel = { computed: { ...mapState({ federationPolicy: state => get(state, 'instance.federationPolicy'), mrfPolicies: state => get(state, 'instance.federationPolicy.mrf_policies', []), - quarantineInstances: state => get(state, 'instance.federationPolicy.quarantined_instances', []), - acceptInstances: state => get(state, 'instance.federationPolicy.mrf_simple.accept', []), - rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []), - ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []), - mediaNsfwInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []), - mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', []), + quarantineInstances: state => toInstanceReasonObject( + get(state, 'instance.federationPolicy.quarantined_instances', []), + get(state, 'instance.federationPolicy.quarantined_instances_info', []), + 'quarantined_instances' + ), + acceptInstances: state => toInstanceReasonObject( + get(state, 'instance.federationPolicy.mrf_simple.accept', []), + get(state, 'instance.federationPolicy.mrf_simple_info', []), + 'accept' + ), + rejectInstances: state => toInstanceReasonObject( + get(state, 'instance.federationPolicy.mrf_simple.reject', []), + get(state, 'instance.federationPolicy.mrf_simple_info', []), + 'reject' + ), + ftlRemovalInstances: state => toInstanceReasonObject( + get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []), + get(state, 'instance.federationPolicy.mrf_simple_info', []), + 'federated_timeline_removal' + ), + mediaNsfwInstances: state => toInstanceReasonObject( + get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []), + get(state, 'instance.federationPolicy.mrf_simple_info', []), + 'media_nsfw' + ), + mediaRemovalInstances: state => toInstanceReasonObject( + get(state, 'instance.federationPolicy.mrf_simple.media_removal', []), + get(state, 'instance.federationPolicy.mrf_simple_info', []), + 'media_removal' + ), keywordsFtlRemoval: state => get(state, 'instance.federationPolicy.mrf_keyword.federated_timeline_removal', []), keywordsReject: state => get(state, 'instance.federationPolicy.mrf_keyword.reject', []), keywordsReplace: state => get(state, 'instance.federationPolicy.mrf_keyword.replace', []) diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.scss b/src/components/mrf_transparency_panel/mrf_transparency_panel.scss @@ -0,0 +1,21 @@ +.mrf-section { + margin: 1em; + + table { + width:100%; + text-align: left; + padding-left:10px; + padding-bottom:20px; + + th, td { + width: 180px; + max-width: 360px; + overflow: hidden; + vertical-align: text-top; + } + + th+th, td+td { + width: auto; + } + } +} diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue @@ -31,13 +31,24 @@ <p>{{ $t("about.mrf.simple.accept_desc") }}</p> - <ul> - <li - v-for="instance in acceptInstances" - :key="instance" - v-text="instance" - /> - </ul> + <table> + <tr> + <th>{{ $t("about.mrf.simple.instance") }}</th> + <th>{{ $t("about.mrf.simple.reason") }}</th> + </tr> + <tr + v-for="entry in acceptInstances" + :key="entry.instance + '_accept'" + > + <td>{{ entry.instance }}</td> + <td v-if="entry.reason === ''"> + {{ $t("about.mrf.simple.not_applicable") }} + </td> + <td v-else> + {{ entry.reason }} + </td> + </tr> + </table> </div> <div v-if="rejectInstances.length"> @@ -45,13 +56,24 @@ <p>{{ $t("about.mrf.simple.reject_desc") }}</p> - <ul> - <li - v-for="instance in rejectInstances" - :key="instance" - v-text="instance" - /> - </ul> + <table> + <tr> + <th>{{ $t("about.mrf.simple.instance") }}</th> + <th>{{ $t("about.mrf.simple.reason") }}</th> + </tr> + <tr + v-for="entry in rejectInstances" + :key="entry.instance + '_reject'" + > + <td>{{ entry.instance }}</td> + <td v-if="entry.reason === ''"> + {{ $t("about.mrf.simple.not_applicable") }} + </td> + <td v-else> + {{ entry.reason }} + </td> + </tr> + </table> </div> <div v-if="quarantineInstances.length"> @@ -59,13 +81,24 @@ <p>{{ $t("about.mrf.simple.quarantine_desc") }}</p> - <ul> - <li - v-for="instance in quarantineInstances" - :key="instance" - v-text="instance" - /> - </ul> + <table> + <tr> + <th>{{ $t("about.mrf.simple.instance") }}</th> + <th>{{ $t("about.mrf.simple.reason") }}</th> + </tr> + <tr + v-for="entry in quarantineInstances" + :key="entry.instance + '_quarantine'" + > + <td>{{ entry.instance }}</td> + <td v-if="entry.reason === ''"> + {{ $t("about.mrf.simple.not_applicable") }} + </td> + <td v-else> + {{ entry.reason }} + </td> + </tr> + </table> </div> <div v-if="ftlRemovalInstances.length"> @@ -73,13 +106,24 @@ <p>{{ $t("about.mrf.simple.ftl_removal_desc") }}</p> - <ul> - <li - v-for="instance in ftlRemovalInstances" - :key="instance" - v-text="instance" - /> - </ul> + <table> + <tr> + <th>{{ $t("about.mrf.simple.instance") }}</th> + <th>{{ $t("about.mrf.simple.reason") }}</th> + </tr> + <tr + v-for="entry in ftlRemovalInstances" + :key="entry.instance + '_ftl_removal'" + > + <td>{{ entry.instance }}</td> + <td v-if="entry.reason === ''"> + {{ $t("about.mrf.simple.not_applicable") }} + </td> + <td v-else> + {{ entry.reason }} + </td> + </tr> + </table> </div> <div v-if="mediaNsfwInstances.length"> @@ -87,13 +131,24 @@ <p>{{ $t("about.mrf.simple.media_nsfw_desc") }}</p> - <ul> - <li - v-for="instance in mediaNsfwInstances" - :key="instance" - v-text="instance" - /> - </ul> + <table> + <tr> + <th>{{ $t("about.mrf.simple.instance") }}</th> + <th>{{ $t("about.mrf.simple.reason") }}</th> + </tr> + <tr + v-for="entry in mediaNsfwInstances" + :key="entry.instance + '_media_nsfw'" + > + <td>{{ entry.instance }}</td> + <td v-if="entry.reason === ''"> + {{ $t("about.mrf.simple.not_applicable") }} + </td> + <td v-else> + {{ entry.reason }} + </td> + </tr> + </table> </div> <div v-if="mediaRemovalInstances.length"> @@ -101,13 +156,24 @@ <p>{{ $t("about.mrf.simple.media_removal_desc") }}</p> - <ul> - <li - v-for="instance in mediaRemovalInstances" - :key="instance" - v-text="instance" - /> - </ul> + <table> + <tr> + <th>{{ $t("about.mrf.simple.instance") }}</th> + <th>{{ $t("about.mrf.simple.reason") }}</th> + </tr> + <tr + v-for="entry in mediaRemovalInstances" + :key="entry.instance + '_media_removal'" + > + <td>{{ entry.instance }}</td> + <td v-if="entry.reason === ''"> + {{ $t("about.mrf.simple.not_applicable") }} + </td> + <td v-else> + {{ entry.reason }} + </td> + </tr> + </table> </div> <h2 v-if="hasKeywordPolicies"> @@ -161,7 +227,6 @@ <script src="./mrf_transparency_panel.js"></script> <style lang="scss"> -.mrf-section { - margin: 1em; -} +@import '../../_variables.scss'; +@import './mrf_transparency_panel.scss'; </style> diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js @@ -1,4 +1,4 @@ -import { timelineNames } from '../timeline_menu/timeline_menu.js' +import TimelineMenuContent from '../timeline_menu/timeline_menu_content.vue' import { mapState, mapGetters } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' @@ -7,10 +7,12 @@ import { faGlobe, faBookmark, faEnvelope, - faHome, + faChevronDown, + faChevronUp, faComments, faBell, - faInfoCircle + faInfoCircle, + faStream } from '@fortawesome/free-solid-svg-icons' library.add( @@ -18,10 +20,12 @@ library.add( faGlobe, faBookmark, faEnvelope, - faHome, + faChevronDown, + faChevronUp, faComments, faBell, - faInfoCircle + faInfoCircle, + faStream ) const NavPanel = { @@ -30,16 +34,20 @@ const NavPanel = { this.$store.dispatch('startFetchingFollowRequests') } }, + components: { + TimelineMenuContent + }, + data () { + return { + showTimelines: false + } + }, + methods: { + toggleTimelines () { + this.showTimelines = !this.showTimelines + } + }, 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, diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue @@ -3,19 +3,33 @@ <div class="panel panel-default"> <ul> <li v-if="currentUser || !privateMode"> - <router-link - :to="{ name: timelinesRoute }" - :class="onTimelineRoute && 'router-link-active'" + <button + class="button-unstyled menu-item" + @click="toggleTimelines" > <FAIcon fixed-width class="fa-scale-110" - icon="home" + icon="stream" />{{ $t("nav.timelines") }} - </router-link> + <FAIcon + class="timelines-chevron" + fixed-width + :icon="showTimelines ? 'chevron-up' : 'chevron-down'" + /> + </button> + <div + v-show="showTimelines" + class="timelines-background" + > + <TimelineMenuContent class="timelines" /> + </div> </li> <li v-if="currentUser"> - <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> + <router-link + class="menu-item" + :to="{ name: 'interactions', params: { username: currentUser.screen_name } }" + > <FAIcon fixed-width class="fa-scale-110" @@ -24,7 +38,10 @@ </router-link> </li> <li v-if="currentUser && pleromaChatMessagesAvailable"> - <router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }"> + <router-link + class="menu-item" + :to="{ name: 'chats', params: { username: currentUser.screen_name } }" + > <div v-if="unreadChatCount" class="badge badge-notification" @@ -39,7 +56,10 @@ </router-link> </li> <li v-if="currentUser && currentUser.locked"> - <router-link :to="{ name: 'friend-requests' }"> + <router-link + class="menu-item" + :to="{ name: 'friend-requests' }" + > <FAIcon fixed-width class="fa-scale-110" @@ -54,7 +74,10 @@ </router-link> </li> <li> - <router-link :to="{ name: 'about' }"> + <router-link + class="menu-item" + :to="{ name: 'about' }" + > <FAIcon fixed-width class="fa-scale-110" @@ -91,14 +114,14 @@ border-color: var(--border, $fallback--border); padding: 0; - &:first-child a { + &:first-child .menu-item { border-top-right-radius: $fallback--panelRadius; border-top-right-radius: var(--panelRadius, $fallback--panelRadius); border-top-left-radius: $fallback--panelRadius; border-top-left-radius: var(--panelRadius, $fallback--panelRadius); } - &:last-child a { + &:last-child .menu-item { border-bottom-right-radius: $fallback--panelRadius; border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius); border-bottom-left-radius: $fallback--panelRadius; @@ -110,13 +133,15 @@ border: none; } - a { + .menu-item { display: block; box-sizing: border-box; - align-items: stretch; height: 3.5em; line-height: 3.5em; padding: 0 1em; + width: 100%; + color: $fallback--link; + color: var(--link, $fallback--link); &:hover { background-color: $fallback--lightBg; @@ -146,6 +171,25 @@ } } + .timelines-chevron { + margin-left: 0.8em; + font-size: 1.1em; + } + + .timelines-background { + padding: 0 0 0 0.6em; + background-color: $fallback--lightBg; + background-color: var(--selectedMenu, $fallback--lightBg); + border-top: 1px solid; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + } + + .timelines { + background-color: $fallback--bg; + background-color: var(--bg, $fallback--bg); + } + .fa-scale-110 { margin-right: 0.8em; } diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js @@ -5,6 +5,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue' import UserCard from '../user_card/user_card.vue' import Timeago from '../timeago/timeago.vue' import Report from '../report/report.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -46,7 +47,8 @@ const Notification = { UserCard, Timeago, Status, - Report + Report, + RichContent }, methods: { toggleUserExpanded () { diff --git a/src/components/notification/notification.scss b/src/components/notification/notification.scss @@ -2,6 +2,19 @@ // TODO Copypaste from Status, should unify it somehow .Notification { + border-bottom: 1px solid; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + word-wrap: break-word; + word-break: break-word; + --emoji-size: 14px; + + &:hover { + --_still-image-img-visibility: visible; + --_still-image-canvas-visibility: hidden; + --_still-image-label-visibility: hidden; + } + &.-muted { padding: 0.25em 0.6em; height: 1.2em; diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue @@ -1,6 +1,7 @@ <template> <Status v-if="notification.type === 'mention'" + class="Notification" :compact="true" :statusoid="notification.status" /> @@ -11,7 +12,7 @@ > <small> <router-link :to="userProfileLink"> - {{ notification.from_profile.screen_name }} + {{ notification.from_profile.screen_name_ui }} </router-link> </small> <button @@ -51,17 +52,19 @@ <span class="notification-details"> <div class="name-and-action"> <!-- eslint-disable vue/no-v-html --> - <bdi - v-if="!!notification.from_profile.name_html" - class="username" - :title="'@'+notification.from_profile.screen_name" - v-html="notification.from_profile.name_html" - /> + <bdi v-if="!!notification.from_profile.name_html"> + <RichContent + class="username" + :title="'@'+notification.from_profile.screen_name_ui" + :html="notification.from_profile.name_html" + :emoji="notification.from_profile.emoji" + /> + </bdi> <!-- eslint-enable vue/no-v-html --> <span v-else class="username" - :title="'@'+notification.from_profile.screen_name" + :title="'@'+notification.from_profile.screen_name_ui" >{{ notification.from_profile.name }}</span> <span v-if="notification.type === 'like'"> <FAIcon @@ -155,7 +158,7 @@ :to="userProfileLink" class="follow-name" > - @{{ notification.from_profile.screen_name }} + @{{ notification.from_profile.screen_name_ui }} </router-link> <div v-if="notification.type === 'follow_request'" @@ -180,7 +183,7 @@ class="move-text" > <router-link :to="targetUserProfileLink"> - @{{ notification.target.screen_name }} + @{{ notification.target.screen_name_ui }} </router-link> </div> <Report @@ -188,8 +191,9 @@ :report-id="notification.report.id" /> <template v-else> - <status-content + <StatusContent class="faint" + :compact="true" :status="notification.action" /> </template> diff --git a/src/components/notifications/notification_filters.vue b/src/components/notifications/notification_filters.vue @@ -0,0 +1,122 @@ +<template> + <Popover + trigger="click" + class="NotificationFilters" + placement="bottom" + :bound-to="{ x: 'container' }" + > + <template v-slot:content> + <div class="dropdown-menu"> + <button + class="button-default dropdown-item" + @click="toggleNotificationFilter('likes')" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': filters.likes }" + />{{ $t('settings.notification_visibility_likes') }} + </button> + <button + class="button-default dropdown-item" + @click="toggleNotificationFilter('repeats')" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': filters.repeats }" + />{{ $t('settings.notification_visibility_repeats') }} + </button> + <button + class="button-default dropdown-item" + @click="toggleNotificationFilter('follows')" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': filters.follows }" + />{{ $t('settings.notification_visibility_follows') }} + </button> + <button + class="button-default dropdown-item" + @click="toggleNotificationFilter('mentions')" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': filters.mentions }" + />{{ $t('settings.notification_visibility_mentions') }} + </button> + <button + class="button-default dropdown-item" + @click="toggleNotificationFilter('emojiReactions')" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': filters.emojiReactions }" + />{{ $t('settings.notification_visibility_emoji_reactions') }} + </button> + <button + class="button-default dropdown-item" + @click="toggleNotificationFilter('moves')" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': filters.moves }" + />{{ $t('settings.notification_visibility_moves') }} + </button> + </div> + </template> + <template v-slot:trigger> + <button class="button-unstyled"> + <FAIcon icon="filter" /> + </button> + </template> + </Popover> +</template> + +<script> +import Popover from '../popover/popover.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faFilter } from '@fortawesome/free-solid-svg-icons' + +library.add( + faFilter +) + +export default { + components: { Popover }, + computed: { + filters () { + return this.$store.getters.mergedConfig.notificationVisibility + } + }, + methods: { + toggleNotificationFilter (type) { + this.$store.dispatch('setOption', { + name: 'notificationVisibility', + value: { + ...this.filters, + [type]: !this.filters[type] + } + }) + } + } +} +</script> + +<style lang="scss"> + +.NotificationFilters { + align-self: stretch; + + > button { + font-size: 1.2em; + padding-left: 0.7em; + padding-right: 0.2em; + line-height: 100%; + height: 100%; + } + + .dropdown-item { + margin: 0; + } +} + +</style> diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js @@ -1,5 +1,6 @@ import { mapGetters } from 'vuex' import Notification from '../notification/notification.vue' +import NotificationFilters from './notification_filters.vue' import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js' import { notificationsFromStore, @@ -17,6 +18,10 @@ library.add( const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30 const Notifications = { + components: { + Notification, + NotificationFilters + }, props: { // Disables display of panel header noHeading: Boolean, @@ -35,11 +40,6 @@ 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' @@ -70,9 +70,6 @@ const Notifications = { }, ...mapGetters(['unreadChatCount']) }, - components: { - Notification - }, watch: { unseenCountTitle (count) { if (count > 0) { diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss @@ -1,6 +1,6 @@ @import '../../_variables.scss'; -.notifications { +.Notifications { &:not(.minimal) { // a bit of a hack to allow scrolling below notifications padding-bottom: 15em; @@ -11,6 +11,10 @@ color: var(--text, $fallback--text); } + .notifications-footer { + border: none; + } + .notification { position: relative; @@ -33,11 +37,6 @@ .notification { box-sizing: border-box; - 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 { canvas { @@ -84,7 +83,6 @@ } } - .follow-text, .move-text { padding: 0.5em 0; overflow-wrap: break-word; @@ -147,13 +145,6 @@ max-width: 100%; text-overflow: ellipsis; white-space: nowrap; - - img { - width: 14px; - height: 14px; - vertical-align: middle; - object-fit: contain - } } .timeago { diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue @@ -1,7 +1,7 @@ <template> <div :class="{ minimal: minimalMode }" - class="notifications" + class="Notifications" > <div :class="mainClass"> <div @@ -22,6 +22,7 @@ > {{ $t('notifications.read') }} </button> + <NotificationFilters /> </div> <div class="panel-body"> <div @@ -34,10 +35,10 @@ <notification :notification="notification" /> </div> </div> - <div class="panel-footer"> + <div class="panel-footer notifications-footer"> <div v-if="bottomedOut" - class="new-status-notification text-center panel-footer faint" + class="new-status-notification text-center faint" > {{ $t('notifications.no_more_notifications') }} </div> @@ -46,13 +47,13 @@ class="button-unstyled -link -fullwidth" @click.prevent="fetchOlderNotifications()" > - <div class="new-status-notification text-center panel-footer"> + <div class="new-status-notification text-center"> {{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }} </div> </button> <div v-else - class="new-status-notification text-center panel-footer" + class="new-status-notification text-center" > <FAIcon icon="circle-notch" diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue @@ -53,7 +53,7 @@ type="submit" class="btn button-default btn-block" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} </button> </div> </div> diff --git a/src/components/poll/poll.js b/src/components/poll/poll.js @@ -1,10 +1,14 @@ -import Timeago from '../timeago/timeago.vue' +import Timeago from 'components/timeago/timeago.vue' +import RichContent from 'components/rich_content/rich_content.jsx' import { forEach, map } from 'lodash' export default { name: 'Poll', - props: ['basePoll'], - components: { Timeago }, + props: ['basePoll', 'emoji'], + components: { + Timeago, + RichContent + }, data () { return { loading: false, diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue @@ -17,8 +17,11 @@ <span class="result-percentage"> {{ percentageForOption(option.votes_count) }}% </span> - <!-- eslint-disable-next-line vue/no-v-html --> - <span v-html="option.title_html" /> + <RichContent + :html="option.title_html" + :handle-links="false" + :emoji="emoji" + /> </div> <div class="result-fill" @@ -42,8 +45,11 @@ :value="index" > <label class="option-vote"> - <!-- eslint-disable-next-line vue/no-v-html --> - <div v-html="option.title_html" /> + <RichContent + :html="option.title_html" + :handle-links="false" + :emoji="emoji" + /> </label> </div> </div> @@ -58,7 +64,12 @@ {{ $t('polls.vote') }} </button> <div class="total"> - {{ totalVotesCount }} {{ $t("polls.votes") }}&nbsp;·&nbsp; + <template v-if="typeof poll.voters_count === 'number'"> + {{ $tc("polls.people_voted_count", poll.voters_count, { count: poll.voters_count }) }}&nbsp;·&nbsp; + </template> + <template v-else> + {{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }}&nbsp;·&nbsp; + </template> </div> <i18n :path="expired ? 'polls.expired' : 'polls.expires_in'"> <Timeago diff --git a/src/components/poll/poll_form.js b/src/components/poll/poll_form.js @@ -1,19 +1,21 @@ import * as DateUtils from 'src/services/date_utils/date_utils.js' import { uniq } from 'lodash' import { library } from '@fortawesome/fontawesome-svg-core' +import Select from '../select/select.vue' import { faTimes, - faChevronDown, faPlus } from '@fortawesome/free-solid-svg-icons' library.add( faTimes, - faChevronDown, faPlus ) export default { + components: { + Select + }, name: 'PollForm', props: ['visible'], data: () => ({ diff --git a/src/components/poll/poll_form.vue b/src/components/poll/poll_form.vue @@ -46,23 +46,19 @@ class="poll-type" :title="$t('polls.type')" > - <label - for="poll-type-selector" - class="select" + <Select + v-model="pollType" + class="poll-type-select" + unstyled="true" + @change="updatePollToParent" > - <select - v-model="pollType" - class="select" - @change="updatePollToParent" - > - <option value="single">{{ $t('polls.single_choice') }}</option> - <option value="multiple">{{ $t('polls.multiple_choices') }}</option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> + <option value="single"> + {{ $t('polls.single_choice') }} + </option> + <option value="multiple"> + {{ $t('polls.multiple_choices') }} + </option> + </Select> </div> <div class="poll-expiry" @@ -76,24 +72,20 @@ :max="maxExpirationInCurrentUnit" @change="expiryAmountChange" > - <label class="expiry-unit select"> - <select - v-model="expiryUnit" - @change="expiryAmountChange" + <Select + v-model="expiryUnit" + unstyled="true" + class="expiry-unit" + @change="expiryAmountChange" + > + <option + v-for="unit in expiryUnits" + :key="unit" + :value="unit" > - <option - v-for="unit in expiryUnits" - :key="unit" - :value="unit" - > - {{ $t(`time.${unit}_short`, ['']) }} - </option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> + {{ $t(`time.${unit}_short`, ['']) }} + </option> + </Select> </div> </div> </div> @@ -147,10 +139,9 @@ .poll-type { margin-right: 0.75em; flex: 1 1 60%; - .select { - border: none; - box-shadow: none; - background-color: transparent; + + .poll-type-select { + padding-right: 0.75em; } } @@ -161,12 +152,6 @@ width: 3em; text-align: right; } - - .expiry-unit { - border: none; - box-shadow: none; - background-color: transparent; - } } } </style> diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js @@ -3,25 +3,32 @@ const Popover = { props: { // Action to trigger popover: either 'hover' or 'click' trigger: String, + // Either 'top' or 'bottom' placement: String, + // Takes object with properties 'x' and 'y', values of these can be // '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, + // 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, + // If true, subtract padding when calculating position for the popover, // use it when popover offset looks to be different on top vs bottom. removePadding: Boolean @@ -47,8 +54,11 @@ const Popover = { } // Popover will be anchored around this element, trigger ref is the container, so - // its children are what are inside the slot. Expect only one slot="trigger". + // its children are what are inside the slot. Expect only one v-slot:trigger. const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el + // SVGs don't have offsetWidth/Height, use fallback + const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth + const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight const screenBox = anchorEl.getBoundingClientRect() // Screen position of the origin point for popover const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top } @@ -107,11 +117,11 @@ const Popover = { const yOffset = (this.offset && this.offset.y) || 0 const translateY = usingTop - ? -anchorEl.offsetHeight + vPadding - yOffset - content.offsetHeight + ? -anchorHeight + vPadding - yOffset - content.offsetHeight : yOffset const xOffset = (this.offset && this.offset.x) || 0 - const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset + const translateX = anchorWidth * 0.5 - content.offsetWidth * 0.5 + horizOffset + xOffset // Note, separate translateX and translateY avoids blurry text on chromium, // single translate or translate3d resulted in blurry text. @@ -121,9 +131,12 @@ const Popover = { } }, showPopover () { - if (this.hidden) this.$emit('show') + const wasHidden = this.hidden this.hidden = false - this.$nextTick(this.updateStyles) + this.$nextTick(() => { + if (wasHidden) this.$emit('show') + this.updateStyles() + }) }, hidePopover () { if (!this.hidden) this.$emit('close') diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue @@ -6,6 +6,7 @@ <button ref="trigger" class="button-unstyled -fullwidth popover-trigger-button" + type="button" @click="onClick" > <slot name="trigger" /> @@ -32,7 +33,7 @@ @import '../../_variables.scss'; .popover-trigger-button { - display: block; + display: inline-block; } .popover { @@ -81,10 +82,9 @@ .dropdown-item { line-height: 21px; - margin-right: 5px; overflow: auto; display: block; - padding: .25rem 1.0rem .25rem 1.5rem; + padding: .5em 0.75em; clear: both; font-weight: 400; text-align: inherit; @@ -100,10 +100,9 @@ --btnText: var(--popoverText, $fallback--text); &-icon { - padding-left: 0.5rem; - svg { - margin-right: 0.25rem; + width: 22px; + margin-right: 0.75rem; color: var(--menuPopoverIcon, $fallback--icon) } } @@ -122,6 +121,33 @@ } } + .menu-checkbox { + display: inline-block; + vertical-align: middle; + min-width: 22px; + max-width: 22px; + min-height: 22px; + max-height: 22px; + line-height: 22px; + text-align: center; + border-radius: 0px; + background-color: $fallback--fg; + background-color: var(--input, $fallback--fg); + box-shadow: 0px 0px 2px black inset; + box-shadow: var(--inputShadow); + margin-right: 0.75em; + + &.menu-checkbox-checked::after { + font-size: 1.25em; + content: '✓'; + } + + &.menu-checkbox-radio::after { + font-size: 2em; + content: '•'; + } + } + } } </style> diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js @@ -4,6 +4,7 @@ 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 Gallery from 'src/components/gallery/gallery.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' @@ -11,10 +12,10 @@ import { reject, map, uniqBy, debounce } from 'lodash' import suggestor from '../emoji_input/suggestor.js' import { mapGetters, mapState } from 'vuex' import Checkbox from '../checkbox/checkbox.vue' +import Select from '../select/select.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { - faChevronDown, faSmileBeam, faPollH, faUpload, @@ -24,7 +25,6 @@ import { } from '@fortawesome/free-solid-svg-icons' library.add( - faChevronDown, faSmileBeam, faPollH, faUpload, @@ -84,8 +84,10 @@ const PostStatusForm = { PollForm, ScopeSelector, Checkbox, + Select, Attachment, - StatusContent + StatusContent, + Gallery }, mounted () { this.updateIdempotencyKey() @@ -115,7 +117,7 @@ const PostStatusForm = { ? this.copyMessageScope : this.$store.state.users.currentUser.default_scope - const { postContentType: contentType } = this.$store.getters.mergedConfig + const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig return { dropFiles: [], @@ -126,7 +128,7 @@ const PostStatusForm = { newStatus: { spoilerText: this.subject || '', status: statusText, - nsfw: false, + nsfw: !!sensitiveByDefault, files: [], poll: {}, mediaDescriptions: {}, @@ -388,6 +390,21 @@ const PostStatusForm = { this.newStatus.files.splice(index, 1) this.$emit('resize') }, + editAttachment (fileInfo, newText) { + this.newStatus.mediaDescriptions[fileInfo.id] = newText + }, + shiftUpMediaFile (fileInfo) { + const { files } = this.newStatus + const index = this.newStatus.files.indexOf(fileInfo) + files.splice(index, 1) + files.splice(index - 1, 0, fileInfo) + }, + shiftDnMediaFile (fileInfo) { + const { files } = this.newStatus + const index = this.newStatus.files.indexOf(fileInfo) + files.splice(index, 1) + files.splice(index + 1, 0, fileInfo) + }, uploadFailed (errString, templateArgs) { templateArgs = templateArgs || {} this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs) diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue @@ -189,28 +189,19 @@ v-if="postFormats.length > 1" class="text-format" > - <label - for="post-content-type" - class="select" + <Select + id="post-content-type" + v-model="newStatus.contentType" + class="form-control" > - <select - id="post-content-type" - v-model="newStatus.contentType" - class="form-control" + <option + v-for="postFormat in postFormats" + :key="postFormat" + :value="postFormat" > - <option - v-for="postFormat in postFormats" - :key="postFormat" - :value="postFormat" - > - {{ $t(`post_status.content_type["${postFormat}"]`) }} - </option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> + {{ $t(`post_status.content_type["${postFormat}"]`) }} + </option> + </Select> </div> <div v-if="postFormats.length === 1 && postFormats[0] !== 'text/plain'" @@ -272,7 +263,7 @@ disabled class="btn button-default" > - {{ $t('general.submit') }} + {{ $t('post_status.post') }} </button> <!-- touchstart is used to keep the OSK at the same position after a message send --> <button @@ -282,7 +273,7 @@ @touchstart.stop.prevent="postStatus($event, newStatus)" @click.stop.prevent="postStatus($event, newStatus)" > - {{ $t('general.submit') }} + {{ $t('post_status.post') }} </button> </div> <div @@ -296,32 +287,22 @@ @click="clearError" /> </div> - <div class="attachments"> - <div - v-for="file in newStatus.files" - :key="file.url" - class="media-upload-wrapper" - > - <button - class="button-unstyled hider" - @click="removeMediaFile(file)" - > - <FAIcon icon="times" /> - </button> - <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> + <gallery + v-if="newStatus.files && newStatus.files.length > 0" + class="attachments" + :grid="true" + :nsfw="false" + :attachments="newStatus.files" + :descriptions="newStatus.mediaDescriptions" + :set-media="() => $store.dispatch('setMedia', newStatus.files)" + :editable="true" + :edit-attachment="editAttachment" + :remove-attachment="removeMediaFile" + :shift-up-attachment="newStatus.files.length > 1 && shiftUpMediaFile" + :shift-dn-attachment="newStatus.files.length > 1 && shiftDnMediaFile" + @play="$emit('mediaplay', attachment.id)" + @pause="$emit('mediapause', attachment.id)" + /> <div v-if="newStatus.files.length > 0 && !disableSensitivityCheckbox" class="upload_settings" @@ -339,26 +320,13 @@ <style lang="scss"> @import '../../_variables.scss'; -.tribute-container { - ul { - padding: 0px; - li { - display: flex; - align-items: center; - } - } - img { - padding: 3px; - width: 16px; - height: 16px; - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); - } -} - .post-status-form { position: relative; + .attachments { + margin-bottom: 0.5em; + } + .form-bottom { display: flex; justify-content: space-between; @@ -516,15 +484,6 @@ flex-direction: column; } - .attachments .media-upload-wrapper { - position: relative; - - .attachment { - margin: 0; - padding: 0; - } - } - .btn { cursor: pointer; } @@ -625,11 +584,4 @@ 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 @@ -23,6 +23,12 @@ const ReactButton = { this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) } close() + }, + focusInput () { + this.$nextTick(() => { + const input = this.$el.querySelector('input') + if (input) input.focus() + }) } }, computed: { diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue @@ -1,15 +1,14 @@ <template> <Popover trigger="click" + class="ReactButton" placement="top" :offset="{ y: 5 }" :bound-to="{ x: 'container' }" remove-padding + @show="focusInput" > - <div - slot="content" - slot-scope="{close}" - > + <template v-slot:content="{close}"> <div class="reaction-picker-filter"> <input v-model="filterWord" @@ -39,17 +38,18 @@ </span> <div class="reaction-bottom-fader" /> </div> - </div> - <span - slot="trigger" - class="ReactButton" - :title="$t('tool_tip.add_reaction')" - > - <FAIcon - class="fa-scale-110 fa-old-padding" - :icon="['far', 'smile-beam']" - /> - </span> + </template> + <template v-slot:trigger> + <button + class="button-unstyled popover-trigger" + :title="$t('tool_tip.add_reaction')" + > + <FAIcon + class="fa-scale-110 fa-old-padding" + :icon="['far', 'smile-beam']" + /> + </button> + </template> </Popover> </template> @@ -58,62 +58,71 @@ <style lang="scss"> @import '../../_variables.scss'; -.reaction-picker-filter { - padding: 0.5em; - display: flex; - input { - flex: 1; +.ReactButton { + .reaction-picker-filter { + padding: 0.5em; + display: flex; + + input { + flex: 1; + } } -} -.reaction-picker-divider { - height: 1px; - width: 100%; - margin: 0.5em; - background-color: var(--border, $fallback--border); -} + .reaction-picker-divider { + height: 1px; + width: 100%; + margin: 0.5em; + background-color: var(--border, $fallback--border); + } -.reaction-picker { - width: 10em; - height: 9em; - font-size: 1.5em; - overflow-y: scroll; - display: flex; - flex-wrap: wrap; - padding: 0.5em; - text-align: center; - align-content: flex-start; - user-select: none; + .reaction-picker { + width: 10em; + height: 9em; + font-size: 1.5em; + overflow-y: scroll; + display: flex; + flex-wrap: wrap; + padding: 0.5em; + text-align: center; + align-content: flex-start; + user-select: none; - mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat, - linear-gradient(to bottom, white 0, transparent 100%) top no-repeat, - linear-gradient(to top, white, white); - transition: mask-size 150ms; - mask-size: 100% 20px, 100% 20px, auto; - // Autoprefixed seem to ignore this one, and also syntax is different - -webkit-mask-composite: xor; - mask-composite: exclude; + mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat, + linear-gradient(to bottom, white 0, transparent 100%) top no-repeat, + linear-gradient(to top, white, white); + transition: mask-size 150ms; + mask-size: 100% 20px, 100% 20px, auto; - .emoji-button { - cursor: pointer; + /* Autoprefixed seem to ignore this one, and also syntax is different */ + -webkit-mask-composite: xor; + mask-composite: exclude; - flex-basis: 20%; - line-height: 1.5em; - align-content: center; + .emoji-button { + cursor: pointer; - &:hover { - transform: scale(1.25); + flex-basis: 20%; + line-height: 1.5em; + align-content: center; + + &:hover { + transform: scale(1.25); + } } } -} -.ReactButton { - padding: 10px; - margin: -10px; + /* override of popover internal stuff */ + .popover-trigger-button { + width: auto; + } + + .popover-trigger { + padding: 10px; + margin: -10px; - &:hover .svg-inline--fa { - color: $fallback--text; - color: var(--text, $fallback--text); + &:hover .svg-inline--fa { + color: $fallback--text; + color: var(--text, $fallback--text); + } } } diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js @@ -10,7 +10,8 @@ const registration = { fullname: '', username: '', password: '', - confirm: '' + confirm: '', + reason: '' }, captcha: {} }), @@ -24,7 +25,8 @@ const registration = { confirm: { required, sameAsPassword: sameAs('password') - } + }, + reason: { required: requiredIf(() => this.accountApprovalRequired) } } } }, @@ -38,7 +40,10 @@ const registration = { computed: { token () { return this.$route.params.token }, bioPlaceholder () { - return this.$t('registration.bio_placeholder').replace(/\s*\n\s*/g, ' \n') + return this.replaceNewlines(this.$t('registration.bio_placeholder')) + }, + reasonPlaceholder () { + return this.replaceNewlines(this.$t('registration.reason_placeholder')) }, ...mapState({ registrationOpen: (state) => state.instance.registrationOpen, @@ -46,7 +51,8 @@ const registration = { isPending: (state) => state.users.signUpPending, serverValidationErrors: (state) => state.users.signUpErrors, termsOfService: (state) => state.instance.tos, - accountActivationRequired: (state) => state.instance.accountActivationRequired + accountActivationRequired: (state) => state.instance.accountActivationRequired, + accountApprovalRequired: (state) => state.instance.accountApprovalRequired }) }, methods: { @@ -73,6 +79,9 @@ const registration = { }, setCaptcha () { this.getCaptcha().then(cpt => { this.captcha = cpt }) + }, + replaceNewlines (str) { + return str.replace(/\s*\n\s*/g, ' \n') } } } diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue @@ -163,6 +163,23 @@ </div> <div + v-if="accountApprovalRequired" + class="form-group" + > + <label + class="form--label" + for="reason" + >{{ $t('registration.reason') }}</label> + <textarea + id="reason" + v-model="user.reason" + :disabled="isPending" + class="form-control" + :placeholder="reasonPlaceholder" + /> + </div> + + <div v-if="captcha.type != 'none'" id="captcha-group" class="form-group" @@ -213,7 +230,7 @@ type="submit" class="btn button-default" > - {{ $t('general.submit') }} + {{ $t('registration.register') }} </button> </div> </div> diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx @@ -0,0 +1,328 @@ +import Vue from 'vue' +import { unescape, flattenDeep } from 'lodash' +import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_converter/utility.service.js' +import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js' +import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js' +import StillImage from 'src/components/still-image/still-image.vue' +import MentionsLine, { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.vue' +import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue' + +import './rich_content.scss' + +/** + * RichContent, The Über-powered component for rendering Post HTML. + * + * This takes post HTML and does multiple things to it: + * - Groups all mentions into <MentionsLine>, this affects all mentions regardles + * of where they are (beginning/middle/end), even single mentions are converted + * to a <MentionsLine> containing single <MentionLink>. + * - Replaces emoji shortcodes with <StillImage>'d images. + * + * There are two problems with this component's architecture: + * 1. Parsing HTML and rendering are inseparable. Attempts to separate the two + * proven to be a massive overcomplication due to amount of things done here. + * 2. We need to output both render and some extra data, which seems to be imp- + * possible in vue. Current solution is to emit 'parseReady' event when parsing + * is done within render() function. + * + * Apart from that one small hiccup with emit in render this _should_ be vue3-ready + */ +export default Vue.component('RichContent', { + name: 'RichContent', + props: { + // Original html content + html: { + required: true, + type: String + }, + attentions: { + required: false, + default: () => [] + }, + // Emoji object, as in status.emojis, note the "s" at the end... + emoji: { + required: true, + type: Array + }, + // Whether to handle links or not (posts: yes, everything else: no) + handleLinks: { + required: false, + type: Boolean, + default: false + }, + // Meme arrows + greentext: { + required: false, + type: Boolean, + default: false + } + }, + // NEVER EVER TOUCH DATA INSIDE RENDER + render (h) { + // Pre-process HTML + const { newHtml: html } = preProcessPerLine(this.html, this.greentext) + let currentMentions = null // Current chain of mentions, we group all mentions together + // This is used to recover spacing removed when parsing mentions + let lastSpacing = '' + + const lastTags = [] // Tags that appear at the end of post body + const writtenMentions = [] // All mentions that appear in post body + const invisibleMentions = [] // All mentions that go beyond the limiter (see MentionsLine) + // to collapse too many mentions in a row + const writtenTags = [] // All tags that appear in post body + // unique index for vue "tag" property + let mentionIndex = 0 + let tagsIndex = 0 + + const renderImage = (tag) => { + return <StillImage + {...{ attrs: getAttrs(tag) }} + class="img" + /> + } + + const renderHashtag = (attrs, children, encounteredTextReverse) => { + const linkData = getLinkData(attrs, children, tagsIndex++) + writtenTags.push(linkData) + if (!encounteredTextReverse) { + lastTags.push(linkData) + } + return <HashtagLink {...{ props: linkData }}/> + } + + const renderMention = (attrs, children) => { + const linkData = getLinkData(attrs, children, mentionIndex++) + linkData.notifying = this.attentions.some(a => a.statusnet_profile_url === linkData.url) + writtenMentions.push(linkData) + if (currentMentions === null) { + currentMentions = [] + } + currentMentions.push(linkData) + if (currentMentions.length > MENTIONS_LIMIT) { + invisibleMentions.push(linkData) + } + if (currentMentions.length === 1) { + return <MentionsLine mentions={ currentMentions } /> + } else { + return '' + } + } + + // Processor to use with html_tree_converter + const processItem = (item, index, array, what) => { + // Handle text nodes - just add emoji + if (typeof item === 'string') { + const emptyText = item.trim() === '' + if (item.includes('\n')) { + currentMentions = null + } + if (emptyText) { + // don't include spaces when processing mentions - we'll include them + // in MentionsLine + lastSpacing = item + // Don't remove last space in a container (fixes poast mentions) + return (index !== array.length - 1) && (currentMentions !== null) ? item.trim() : item + } + + currentMentions = null + if (item.includes(':')) { + item = ['', processTextForEmoji( + item, + this.emoji, + ({ shortcode, url }) => { + return <StillImage + class="emoji img" + src={url} + title={`:${shortcode}:`} + alt={`:${shortcode}:`} + /> + } + )] + } + return item + } + + // Handle tag nodes + if (Array.isArray(item)) { + const [opener, children, closer] = item + const Tag = getTagName(opener) + const attrs = getAttrs(opener) + const previouslyMentions = currentMentions !== null + /* During grouping of mentions we trim all the empty text elements + * This padding is added to recover last space removed in case + * we have a tag right next to mentions + */ + const mentionsLinePadding = + // Padding is only needed if we just finished parsing mentions + previouslyMentions && + // Don't add padding if content is string and has padding already + !(children && typeof children[0] === 'string' && children[0].match(/^\s/)) + ? lastSpacing + : '' + switch (Tag) { + case 'br': + currentMentions = null + break + case 'img': // replace images with StillImage + return ['', [mentionsLinePadding, renderImage(opener)], ''] + case 'a': // replace mentions with MentionLink + if (!this.handleLinks) break + if (attrs['class'] && attrs['class'].includes('mention')) { + // Handling mentions here + return renderMention(attrs, children) + } else { + currentMentions = null + break + } + case 'span': + if (this.handleLinks && attrs['class'] && attrs['class'].includes('h-card')) { + return ['', children.map(processItem), ''] + } + } + + if (children !== undefined) { + return [ + '', + [ + mentionsLinePadding, + [opener, children.map(processItem), closer] + ], + '' + ] + } else { + return ['', [mentionsLinePadding, item], ''] + } + } + } + + // Processor for back direction (for finding "last" stuff, just easier this way) + let encounteredTextReverse = false + const processItemReverse = (item, index, array, what) => { + // Handle text nodes - just add emoji + if (typeof item === 'string') { + const emptyText = item.trim() === '' + if (emptyText) return item + if (!encounteredTextReverse) encounteredTextReverse = true + return unescape(item) + } else if (Array.isArray(item)) { + // Handle tag nodes + const [opener, children] = item + const Tag = opener === '' ? '' : getTagName(opener) + switch (Tag) { + case 'a': // replace mentions with MentionLink + if (!this.handleLinks) break + const attrs = getAttrs(opener) + // should only be this + if ( + (attrs['class'] && attrs['class'].includes('hashtag')) || // Pleroma style + (attrs['rel'] === 'tag') // Mastodon style + ) { + return renderHashtag(attrs, children, encounteredTextReverse) + } else { + attrs.target = '_blank' + const newChildren = [...children].reverse().map(processItemReverse).reverse() + + return <a {...{ attrs }}> + { newChildren } + </a> + } + case '': + return [...children].reverse().map(processItemReverse).reverse() + } + + // Render tag as is + if (children !== undefined) { + const newChildren = Array.isArray(children) + ? [...children].reverse().map(processItemReverse).reverse() + : children + return <Tag {...{ attrs: getAttrs(opener) }}> + { newChildren } + </Tag> + } else { + return <Tag/> + } + } + return item + } + + const pass1 = convertHtmlToTree(html).map(processItem) + const pass2 = [...pass1].reverse().map(processItemReverse).reverse() + // DO NOT USE SLOTS they cause a re-render feedback loop here. + // slots updated -> rerender -> emit -> update up the tree -> rerender -> ... + // at least until vue3? + const result = <span class="RichContent"> + { pass2 } + </span> + + const event = { + lastTags, + writtenMentions, + writtenTags, + invisibleMentions + } + + // DO NOT MOVE TO UPDATE. BAD IDEA. + this.$emit('parseReady', event) + + return result + } +}) + +const getLinkData = (attrs, children, index) => { + const stripTags = (item) => { + if (typeof item === 'string') { + return item + } else { + return item[1].map(stripTags).join('') + } + } + const textContent = children.map(stripTags).join('') + return { + index, + url: attrs.href, + tag: attrs['data-tag'], + content: flattenDeep(children).join(''), + textContent + } +} + +/** Pre-processing HTML + * + * Currently this does one thing: + * - add green/cyantexting + * + * @param {String} html - raw HTML to process + * @param {Boolean} greentext - whether to enable greentexting or not + */ +export const preProcessPerLine = (html, greentext) => { + const greentextHandle = new Set(['p', 'div']) + + const lines = convertHtmlToLines(html) + const newHtml = lines.reverse().map((item, index, array) => { + if (!item.text) return item + const string = item.text + + // Greentext stuff + if ( + // Only if greentext is engaged + greentext && + // Only handle p's and divs. Don't want to affect blockquotes, code etc + item.level.every(l => greentextHandle.has(l)) && + // Only if line begins with '>' or '<' + (string.includes('&gt;') || string.includes('&lt;')) + ) { + const cleanedString = string.replace(/<[^>]+?>/gi, '') // remove all tags + .replace(/@\w+/gi, '') // remove mentions (even failed ones) + .trim() + if (cleanedString.startsWith('&gt;')) { + return `<span class='greentext'>${string}</span>` + } else if (cleanedString.startsWith('&lt;')) { + return `<span class='cyantext'>${string}</span>` + } + } + + return string + }).reverse().join('') + + return { newHtml } +} diff --git a/src/components/rich_content/rich_content.scss b/src/components/rich_content/rich_content.scss @@ -0,0 +1,64 @@ +.RichContent { + blockquote { + margin: 0.2em 0 0.2em 2em; + font-style: italic; + } + + pre { + overflow: auto; + } + + code, + samp, + kbd, + var, + pre { + font-family: var(--postCodeFont, monospace); + } + + p { + margin: 0 0 1em 0; + } + + p:last-child { + margin: 0 0 0 0; + } + + h1 { + font-size: 1.1em; + line-height: 1.2em; + margin: 1.4em 0; + } + + h2 { + font-size: 1.1em; + margin: 1em 0; + } + + h3 { + font-size: 1em; + margin: 1.2em 0; + } + + h4 { + margin: 1.1em 0; + } + + .img { + display: inline-block; + } + + .emoji { + display: inline-block; + width: var(--emoji-size, 32px); + height: var(--emoji-size, 32px); + } + + .img, + video { + max-width: 100%; + max-height: 400px; + vertical-align: middle; + object-fit: contain; + } +} diff --git a/src/components/scope_selector/scope_selector.vue b/src/components/scope_selector/scope_selector.vue @@ -8,6 +8,7 @@ class="button-unstyled scope" :class="css.direct" :title="$t('post_status.scope.direct')" + type="button" @click="changeVis('direct')" > <FAIcon @@ -20,6 +21,7 @@ class="button-unstyled scope" :class="css.private" :title="$t('post_status.scope.private')" + type="button" @click="changeVis('private')" > <FAIcon @@ -32,6 +34,7 @@ class="button-unstyled scope" :class="css.unlisted" :title="$t('post_status.scope.unlisted')" + type="button" @click="changeVis('unlisted')" > <FAIcon @@ -44,6 +47,7 @@ class="button-unstyled scope" :class="css.public" :title="$t('post_status.scope.public')" + type="button" @click="changeVis('public')" > <FAIcon diff --git a/src/components/search/search.vue b/src/components/search/search.vue @@ -15,6 +15,7 @@ > <button class="btn button-default search-button" + type="submit" @click="newQuery(searchTerm)" > <FAIcon icon="search" /> diff --git a/src/components/search_bar/search_bar.vue b/src/components/search_bar/search_bar.vue @@ -7,6 +7,7 @@ v-if="hidden" class="button-unstyled nav-icon" :title="$t('nav.search')" + type="button" @click.prevent.stop="toggleHidden" > <FAIcon @@ -27,6 +28,7 @@ > <button class="button-default search-button" + type="submit" @click="find(searchTerm)" > <FAIcon @@ -36,6 +38,7 @@ </button> <button class="button-unstyled cancel-search" + type="button" @click.prevent.stop="toggleHidden" > <FAIcon diff --git a/src/components/select/select.js b/src/components/select/select.js @@ -0,0 +1,21 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faChevronDown +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faChevronDown +) + +export default { + model: { + prop: 'value', + event: 'change' + }, + props: [ + 'value', + 'disabled', + 'unstyled', + 'kind' + ] +} diff --git a/src/components/select/select.vue b/src/components/select/select.vue @@ -0,0 +1,62 @@ + +<template> + <label + class="Select input" + :class="{ disabled, unstyled }" + > + <select + :disabled="disabled" + :value="value" + @change="$emit('change', $event.target.value)" + > + <slot /> + </select> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> + </label> +</template> + +<script src="./select.js"> </script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.Select { + padding: 0; + + select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: transparent; + border: none; + color: $fallback--text; + color: var(--inputText, --text, $fallback--text); + margin: 0; + padding: 0 2em 0 .2em; + font-family: sans-serif; + font-family: var(--inputFont, sans-serif); + font-size: 14px; + width: 100%; + z-index: 1; + height: 28px; + line-height: 16px; + } + + .select-down-icon { + position: absolute; + top: 0; + bottom: 0; + right: 5px; + height: 100%; + color: $fallback--text; + color: var(--inputText, $fallback--text); + line-height: 28px; + z-index: 0; + pointer-events: none; + } + +} +</style> diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue @@ -24,10 +24,7 @@ :items="items" :get-key="getKey" > - <template - slot="item" - slot-scope="{item}" - > + <template v-slot:item="{item}"> <div class="selectable-list-item-inner" :class="{ 'selectable-list-item-selected-inner': isSelected(item) }" @@ -44,7 +41,7 @@ /> </div> </template> - <template slot="empty"> + <template v-slot:empty> <slot name="empty" /> </template> </List> diff --git a/src/components/settings_modal/helpers/boolean_setting.js b/src/components/settings_modal/helpers/boolean_setting.js @@ -0,0 +1,38 @@ +import { get, set } from 'lodash' +import Checkbox from 'src/components/checkbox/checkbox.vue' +import ModifiedIndicator from './modified_indicator.vue' +export default { + components: { + Checkbox, + ModifiedIndicator + }, + props: [ + 'path', + 'disabled' + ], + computed: { + pathDefault () { + const [firstSegment, ...rest] = this.path.split('.') + return [firstSegment + 'DefaultValue', ...rest].join('.') + }, + state () { + const value = get(this.$parent, this.path) + if (value === undefined) { + return this.defaultState + } else { + return value + } + }, + defaultState () { + return get(this.$parent, this.pathDefault) + }, + isChanged () { + return this.state !== this.defaultState + } + }, + methods: { + update (e) { + set(this.$parent, this.path, e) + } + } +} diff --git a/src/components/settings_modal/helpers/boolean_setting.vue b/src/components/settings_modal/helpers/boolean_setting.vue @@ -0,0 +1,21 @@ +<template> + <label + class="BooleanSetting" + > + <Checkbox + :checked="state" + :disabled="disabled" + @change="update" + > + <span + v-if="!!$slots.default" + class="label" + > + <slot /> + </span> + <ModifiedIndicator :changed="isChanged" /> + </Checkbox> + </label> +</template> + +<script src="./boolean_setting.js"></script> diff --git a/src/components/settings_modal/helpers/choice_setting.js b/src/components/settings_modal/helpers/choice_setting.js @@ -0,0 +1,39 @@ +import { get, set } from 'lodash' +import Select from 'src/components/select/select.vue' +import ModifiedIndicator from './modified_indicator.vue' +export default { + components: { + Select, + ModifiedIndicator + }, + props: [ + 'path', + 'disabled', + 'options' + ], + computed: { + pathDefault () { + const [firstSegment, ...rest] = this.path.split('.') + return [firstSegment + 'DefaultValue', ...rest].join('.') + }, + state () { + const value = get(this.$parent, this.path) + if (value === undefined) { + return this.defaultState + } else { + return value + } + }, + defaultState () { + return get(this.$parent, this.pathDefault) + }, + isChanged () { + return this.state !== this.defaultState + } + }, + methods: { + update (e) { + set(this.$parent, this.path, e) + } + } +} diff --git a/src/components/settings_modal/helpers/choice_setting.vue b/src/components/settings_modal/helpers/choice_setting.vue @@ -0,0 +1,29 @@ +<template> + <label + class="ChoiceSetting" + > + <slot /> + <Select + :value="state" + :disabled="disabled" + @change="update" + > + <option + v-for="option in options" + :key="option.key" + :value="option.value" + > + {{ option.label }} + {{ option.value === defaultState ? $t('settings.instance_default_simple') : '' }} + </option> + </Select> + <ModifiedIndicator :changed="isChanged" /> + </label> +</template> + +<script src="./choice_setting.js"></script> + +<style lang="scss"> +.ChoiceSetting { +} +</style> diff --git a/src/components/settings_modal/helpers/modified_indicator.vue b/src/components/settings_modal/helpers/modified_indicator.vue @@ -0,0 +1,51 @@ +<template> + <span + v-if="changed" + class="ModifiedIndicator" + > + <Popover + trigger="hover" + > + <template v-slot:trigger> + &nbsp; + <FAIcon + icon="wrench" + :aria-label="$t('settings.setting_changed')" + /> + </template> + <template v-slot:content> + <div class="modified-tooltip"> + {{ $t('settings.setting_changed') }} + </div> + </template> + </Popover> + </span> +</template> + +<script> +import Popover from 'src/components/popover/popover.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faWrench } from '@fortawesome/free-solid-svg-icons' + +library.add( + faWrench +) + +export default { + components: { Popover }, + props: ['changed'] +} +</script> + +<style lang="scss"> +.ModifiedIndicator { + display: inline-block; + position: relative; + + .modified-tooltip { + margin: 0.5em 1em; + min-width: 10em; + text-align: center; + } +} +</style> diff --git a/src/components/settings_modal/helpers/shared_computed_object.js b/src/components/settings_modal/helpers/shared_computed_object.js @@ -1,29 +1,15 @@ -import { - instanceDefaultProperties, - multiChoiceProperties, - defaultState as configDefaultState -} from 'src/modules/config.js' +import { 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)) + // Getting values for default properties + ...Object.keys(configDefaultState) .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]) + return this.$store.getters.defaultConfig[key] } ]) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js @@ -2,10 +2,55 @@ 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' +import Popover from '../popover/popover.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { cloneDeep } from 'lodash' +import { + newImporter, + newExporter +} from 'src/services/export_import/export_import.js' +import { + faTimes, + faFileUpload, + faFileDownload, + faChevronDown +} from '@fortawesome/free-solid-svg-icons' +import { + faWindowMinimize +} from '@fortawesome/free-regular-svg-icons' + +const PLEROMAFE_SETTINGS_MAJOR_VERSION = 1 +const PLEROMAFE_SETTINGS_MINOR_VERSION = 0 + +library.add( + faTimes, + faWindowMinimize, + faFileUpload, + faFileDownload, + faChevronDown +) const SettingsModal = { + data () { + return { + dataImporter: newImporter({ + validator: this.importValidator, + onImport: this.onImport, + onImportFailure: this.onImportFailure + }), + dataThemeExporter: newExporter({ + filename: 'pleromafe_settings.full', + getExportedObject: () => this.generateExport(true) + }), + dataExporter: newExporter({ + filename: 'pleromafe_settings', + getExportedObject: () => this.generateExport() + }) + } + }, components: { Modal, + Popover, SettingsModalContent: getResettableAsyncComponent( () => import('./settings_modal_content.vue'), { @@ -21,6 +66,85 @@ const SettingsModal = { }, peekModal () { this.$store.dispatch('togglePeekSettingsModal') + }, + importValidator (data) { + if (!Array.isArray(data._pleroma_settings_version)) { + return { + messageKey: 'settings.file_import_export.invalid_file' + } + } + + const [major, minor] = data._pleroma_settings_version + + if (major > PLEROMAFE_SETTINGS_MAJOR_VERSION) { + return { + messageKey: 'settings.file_export_import.errors.file_too_new', + messageArgs: { + fileMajor: major, + feMajor: PLEROMAFE_SETTINGS_MAJOR_VERSION + } + } + } + + if (major < PLEROMAFE_SETTINGS_MAJOR_VERSION) { + return { + messageKey: 'settings.file_export_import.errors.file_too_old', + messageArgs: { + fileMajor: major, + feMajor: PLEROMAFE_SETTINGS_MAJOR_VERSION + } + } + } + + if (minor > PLEROMAFE_SETTINGS_MINOR_VERSION) { + this.$store.dispatch('pushGlobalNotice', { + level: 'warning', + messageKey: 'settings.file_export_import.errors.file_slightly_new' + }) + } + + return true + }, + onImportFailure (result) { + if (result.error) { + this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_settings_imported', level: 'error' }) + } else { + this.$store.dispatch('pushGlobalNotice', { ...result.validationResult, level: 'error' }) + } + }, + onImport (data) { + if (data) { this.$store.dispatch('loadSettings', data) } + }, + restore () { + this.dataImporter.importData() + }, + backup () { + this.dataExporter.exportData() + }, + backupWithTheme () { + this.dataThemeExporter.exportData() + }, + generateExport (theme = false) { + const { config } = this.$store.state + let sample = config + if (!theme) { + const ignoreList = new Set([ + 'customTheme', + 'customThemeSource', + 'colors' + ]) + sample = Object.fromEntries( + Object + .entries(sample) + .filter(([key]) => !ignoreList.has(key)) + ) + } + const clone = cloneDeep(sample) + clone._pleroma_settings_version = [ + PLEROMAFE_SETTINGS_MAJOR_VERSION, + PLEROMAFE_SETTINGS_MINOR_VERSION + ] + return clone } }, computed: { diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue @@ -31,20 +31,84 @@ </transition> <button class="btn button-default" + :title="$t('general.peek')" @click="peekModal" > - {{ $t('general.peek') }} + <FAIcon + :icon="['far', 'window-minimize']" + fixed-width + /> </button> <button class="btn button-default" + :title="$t('general.close')" @click="closeModal" > - {{ $t('general.close') }} + <FAIcon + icon="times" + fixed-width + /> </button> </div> <div class="panel-body"> <SettingsModalContent v-if="modalOpenedOnce" /> </div> + <div class="panel-footer"> + <Popover + class="export" + trigger="click" + placement="top" + :offset="{ y: 5, x: 5 }" + :bound-to="{ x: 'container' }" + remove-padding + > + <template v-slot:trigger> + <button + class="btn button-default" + :title="$t('general.close')" + > + <span>{{ $t("settings.file_export_import.backup_restore") }}</span> + <FAIcon + icon="chevron-down" + /> + </button> + </template> + <template v-slot:content="{close}"> + <div class="dropdown-menu"> + <button + class="button-default dropdown-item dropdown-item-icon" + @click.prevent="backup" + @click="close" + > + <FAIcon + icon="file-download" + fixed-width + /><span>{{ $t("settings.file_export_import.backup_settings") }}</span> + </button> + <button + class="button-default dropdown-item dropdown-item-icon" + @click.prevent="backupWithTheme" + @click="close" + > + <FAIcon + icon="file-download" + fixed-width + /><span>{{ $t("settings.file_export_import.backup_settings_theme") }}</span> + </button> + <button + class="button-default dropdown-item dropdown-item-icon" + @click.prevent="restore" + @click="close" + > + <FAIcon + icon="file-upload" + fixed-width + /><span>{{ $t("settings.file_export_import.restore_settings") }}</span> + </button> + </div> + </template> + </Popover> + </div> </div> </Modal> </template> diff --git a/src/components/settings_modal/settings_modal_content.scss b/src/components/settings_modal/settings_modal_content.scss @@ -7,13 +7,24 @@ margin: 1em 1em 1.4em; padding-bottom: 1.4em; - > div { + > div, + > label { + display: block; margin-bottom: .5em; &:last-child { margin-bottom: 0; } } + .select-multiple { + display: flex; + + .option-list { + margin: 0; + padding-left: .5em; + } + } + &:last-child { border-bottom: none; padding-bottom: 0; diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js @@ -1,24 +1,23 @@ import { filter, trim } from 'lodash' -import Checkbox from 'src/components/checkbox/checkbox.vue' +import BooleanSetting from '../helpers/boolean_setting.vue' +import ChoiceSetting from '../helpers/choice_setting.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faChevronDown -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faChevronDown -) const FilteringTab = { data () { return { - muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n') + muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n'), + replyVisibilityOptions: ['all', 'following', 'self'].map(mode => ({ + key: mode, + value: mode, + label: this.$t(`settings.reply_visibility_${mode}`) + })) } }, components: { - Checkbox + BooleanSetting, + ChoiceSetting }, computed: { ...SharedComputedObject(), diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue @@ -1,89 +1,138 @@ <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" + <h2>{{ $t('settings.posts') }}</h2> + <ul class="setting-list"> + <li> + <BooleanSetting path="hideFilteredStatuses"> + {{ $t('settings.hide_filtered_statuses') }} + </BooleanSetting> + <ul + class="setting-list suboptions" + :class="[{disabled: !streaming}]" > - <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> - <FAIcon - class="select-down-icon" - icon="chevron-down" + <li> + <BooleanSetting + :disabled="hideFilteredStatuses" + path="hideWordFilteredPosts" + > + {{ $t('settings.hide_wordfiltered_statuses') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting + :disabled="hideFilteredStatuses" + path="hideMutedThreads" + > + {{ $t('settings.hide_muted_threads') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting + :disabled="hideFilteredStatuses" + path="hideMutedPosts" + > + {{ $t('settings.hide_muted_posts') }} + </BooleanSetting> + </li> + </ul> + </li> + <li> + <BooleanSetting path="hidePostStats"> + {{ $t('settings.hide_post_stats') }} + </BooleanSetting> + </li> + <ChoiceSetting + id="replyVisibility" + path="replyVisibility" + :options="replyVisibilityOptions" + > + {{ $t('settings.replies_in_timeline') }} + </ChoiceSetting> + <li> + <h3>{{ $t('settings.wordfilter') }}</h3> + <textarea + id="muteWords" + v-model="muteWordsString" + class="resize-height" /> - </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>{{ $t('settings.filtering_explanation') }}</div> + </li> + <h3>{{ $t('settings.attachments') }}</h3> + <li> + <label for="maxThumbnails"> + {{ $t('settings.max_thumbnails') }} + </label> + <input + id="maxThumbnails" + path.number="maxThumbnails" + class="number-input" + type="number" + min="0" + step="1" + > + </li> + <li> + <BooleanSetting path="hideAttachments"> + {{ $t('settings.hide_attachments_in_tl') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="hideAttachmentsInConv"> + {{ $t('settings.hide_attachments_in_convo') }} + </BooleanSetting> + </li> + </ul> + </div> + <div class="setting-item"> + <h2>{{ $t('settings.user_profiles') }}</h2> + <ul class="setting-list"> + <li> + <BooleanSetting path="hideUserStats"> + {{ $t('settings.hide_user_stats') }} + </BooleanSetting> + </li> + </ul> </div> <div class="setting-item"> - <div> - <p>{{ $t('settings.filtering_explanation') }}</p> - <textarea - id="muteWords" - class="resize-height" - v-model="muteWordsString" - /> - </div> - <div> - <Checkbox v-model="hideFilteredStatuses"> - {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }} - </Checkbox> - </div> + <h2>{{ $t('settings.notifications') }}</h2> + <ul class="setting-list"> + <li class="select-multiple"> + <span class="label">{{ $t('settings.notification_visibility') }}</span> + <ul class="option-list"> + <li> + <BooleanSetting path="notificationVisibility.likes"> + {{ $t('settings.notification_visibility_likes') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationVisibility.repeats"> + {{ $t('settings.notification_visibility_repeats') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationVisibility.follows"> + {{ $t('settings.notification_visibility_follows') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationVisibility.mentions"> + {{ $t('settings.notification_visibility_mentions') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationVisibility.moves"> + {{ $t('settings.notification_visibility_moves') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationVisibility.emojiReactions"> + {{ $t('settings.notification_visibility_emoji_reactions') }} + </BooleanSetting> + </li> + </ul> + </li> + </ul> </div> </div> </template> diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js @@ -1,21 +1,30 @@ -import Checkbox from 'src/components/checkbox/checkbox.vue' +import BooleanSetting from '../helpers/boolean_setting.vue' +import ChoiceSetting from '../helpers/choice_setting.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' import { library } from '@fortawesome/fontawesome-svg-core' import { - faChevronDown, faGlobe } from '@fortawesome/free-solid-svg-icons' library.add( - faChevronDown, faGlobe ) const GeneralTab = { data () { return { + subjectLineOptions: ['email', 'noop', 'masto'].map(mode => ({ + key: mode, + value: mode, + label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`) + })), + mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({ + key: mode, + value: mode, + label: this.$t(`settings.mention_link_display_${mode}`) + })), loopSilentAvailable: // Firefox Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || @@ -26,18 +35,27 @@ const GeneralTab = { } }, components: { - Checkbox, + BooleanSetting, + ChoiceSetting, InterfaceLanguageSwitcher }, computed: { postFormats () { return this.$store.state.instance.postFormats || [] }, + postContentOptions () { + return this.postFormats.map(format => ({ + key: format, + value: format, + label: this.$t(`post_status.content_type["${format}"]`) + })) + }, instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }, instanceWallpaperUsed () { return this.$store.state.instance.background && !this.$store.state.users.currentUser.background_image }, + instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable }, ...SharedComputedObject() } } diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue @@ -7,228 +7,126 @@ <interface-language-switcher /> </li> <li v-if="instanceSpecificPanelPresent"> - <Checkbox v-model="hideISP"> + <BooleanSetting path="hideISP"> {{ $t('settings.hide_isp') }} - </Checkbox> + </BooleanSetting> + </li> + <li> + <BooleanSetting path="sidebarRight"> + {{ $t('settings.right_sidebar') }} + </BooleanSetting> </li> <li v-if="instanceWallpaperUsed"> - <Checkbox v-model="hideInstanceWallpaper"> + <BooleanSetting path="hideInstanceWallpaper"> {{ $t('settings.hide_wallpaper') }} - </Checkbox> + </BooleanSetting> </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> + <BooleanSetting path="stopGifs"> + {{ $t('settings.stop_gifs') }} + </BooleanSetting> </li> <li> - <Checkbox v-model="streaming"> + <BooleanSetting path="streaming"> {{ $t('settings.streaming') }} - </Checkbox> + </BooleanSetting> <ul class="setting-list suboptions" :class="[{disabled: !streaming}]" > <li> - <Checkbox - v-model="pauseOnUnfocused" + <BooleanSetting + path="pauseOnUnfocused" :disabled="!streaming" > {{ $t('settings.pause_on_unfocused') }} - </Checkbox> + </BooleanSetting> </li> </ul> </li> <li> - <Checkbox v-model="useStreamingApi"> + <BooleanSetting path="useStreamingApi"> {{ $t('settings.useStreamingApi') }} <br> <small> {{ $t('settings.useStreamingApiWarning') }} </small> - </Checkbox> - </li> - <li> - <Checkbox v-model="emojiReactionsOnTimeline"> - {{ $t('settings.emoji_reactions_on_timeline') }} - </Checkbox> + </BooleanSetting> </li> <li> - <Checkbox v-model="virtualScrolling"> + <BooleanSetting path="virtualScrolling"> {{ $t('settings.virtual_scrolling') }} - </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> + </BooleanSetting> </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> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </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> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </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"> + <BooleanSetting path="autohideFloatingPostButton"> {{ $t('settings.autohide_floating_post_button') }} - </Checkbox> + </BooleanSetting> </li> - <li> - <Checkbox v-model="padEmoji"> - {{ $t('settings.pad_emoji') }} - </Checkbox> + <li v-if="instanceShoutboxPresent"> + <BooleanSetting path="hideShoutbox"> + {{ $t('settings.hide_shoutbox') }} + </BooleanSetting> </li> </ul> </div> - <div class="setting-item"> - <h2>{{ $t('settings.attachments') }}</h2> + <h2>{{ $t('settings.post_look_feel') }}</h2> <ul class="setting-list"> <li> - <Checkbox v-model="hideAttachments"> - {{ $t('settings.hide_attachments_in_tl') }} - </Checkbox> + <BooleanSetting path="collapseMessageWithSubject"> + {{ $t('settings.collapse_subject') }} + </BooleanSetting> </li> <li> - <Checkbox v-model="hideAttachmentsInConv"> - {{ $t('settings.hide_attachments_in_convo') }} - </Checkbox> + <BooleanSetting path="emojiReactionsOnTimeline"> + {{ $t('settings.emoji_reactions_on_timeline') }} + </BooleanSetting> </li> + <h3>{{ $t('settings.attachments') }}</h3> <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" - > + <BooleanSetting path="useContainFit"> + {{ $t('settings.use_contain_fit') }} + </BooleanSetting> </li> <li> - <Checkbox v-model="hideNsfw"> + <BooleanSetting path="hideNsfw"> {{ $t('settings.nsfw_clickthrough') }} - </Checkbox> + </BooleanSetting> </li> <ul class="setting-list suboptions"> <li> - <Checkbox - v-model="preloadImage" + <BooleanSetting + path="preloadImage" :disabled="!hideNsfw" > {{ $t('settings.preload_images') }} - </Checkbox> + </BooleanSetting> </li> <li> - <Checkbox - v-model="useOneClickNsfw" + <BooleanSetting + path="useOneClickNsfw" :disabled="!hideNsfw" > {{ $t('settings.use_one_click_nsfw') }} - </Checkbox> + </BooleanSetting> </li> </ul> <li> - <Checkbox v-model="stopGifs"> - {{ $t('settings.stop_gifs') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="loopVideo"> + <BooleanSetting path="loopVideo"> {{ $t('settings.loop_video') }} - </Checkbox> + </BooleanSetting> <ul class="setting-list suboptions" :class="[{disabled: !streaming}]" > <li> - <Checkbox - v-model="loopVideoSilentOnly" + <BooleanSetting + path="loopVideoSilentOnly" :disabled="!loopVideo || !loopSilentAvailable" > {{ $t('settings.loop_video_silent_only') }} - </Checkbox> + </BooleanSetting> <div v-if="!loopSilentAvailable" class="unavailable" @@ -239,36 +137,130 @@ </ul> </li> <li> - <Checkbox v-model="playVideosInModal"> + <BooleanSetting path="playVideosInModal"> {{ $t('settings.play_videos_in_modal') }} - </Checkbox> + </BooleanSetting> </li> + <h3>{{ $t('settings.fun') }}</h3> <li> - <Checkbox v-model="useContainFit"> - {{ $t('settings.use_contain_fit') }} - </Checkbox> + <BooleanSetting path="greentext"> + {{ $t('settings.greentext') }} + </BooleanSetting> </li> + <li> + <BooleanSetting path="mentionLinkShowYous"> + {{ $t('settings.show_yous') }} + </BooleanSetting> + </li> + <li> + <ChoiceSetting + id="mentionLinkDisplay" + path="mentionLinkDisplay" + :options="mentionLinkDisplayOptions" + > + {{ $t('settings.mention_link_display') }} + </ChoiceSetting> + </li> + <ul + class="setting-list suboptions" + > + <li + v-if="mentionLinkDisplay === 'short'" + > + <BooleanSetting path="mentionLinkShowTooltip"> + {{ $t('settings.mention_link_show_tooltip') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="useAtIcon"> + {{ $t('settings.use_at_icon') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="mentionLinkShowAvatar"> + {{ $t('settings.mention_link_show_avatar') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="mentionLinkFadeDomain"> + {{ $t('settings.mention_link_fade_domain') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="mentionLinkBoldenYou"> + {{ $t('settings.mention_link_bolden_you') }} + </BooleanSetting> + </li> + </ul> </ul> </div> <div class="setting-item"> - <h2>{{ $t('settings.notifications') }}</h2> + <h2>{{ $t('settings.composing') }}</h2> <ul class="setting-list"> <li> - <Checkbox v-model="webPushNotifications"> - {{ $t('settings.enable_web_push_notifications') }} - </Checkbox> + <BooleanSetting path="scopeCopy"> + {{ $t('settings.scope_copy') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="alwaysShowSubjectInput"> + {{ $t('settings.subject_input_always_show') }} + </BooleanSetting> + </li> + <li> + <ChoiceSetting + id="subjectLineBehavior" + path="subjectLineBehavior" + :options="subjectLineOptions" + > + {{ $t('settings.subject_line_behavior') }} + </ChoiceSetting> + </li> + <li v-if="postFormats.length > 0"> + <ChoiceSetting + id="postContentType" + path="postContentType" + :options="postContentOptions" + > + {{ $t('settings.post_status_content_type') }} + </ChoiceSetting> + </li> + <li> + <BooleanSetting path="minimalScopesMode"> + {{ $t('settings.minimal_scopes_mode') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="sensitiveByDefault"> + {{ $t('settings.sensitive_by_default') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="alwaysShowNewPostButton"> + {{ $t('settings.always_show_post_button') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="autohideFloatingPostButton"> + {{ $t('settings.autohide_floating_post_button') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="padEmoji"> + {{ $t('settings.pad_emoji') }} + </BooleanSetting> </li> </ul> </div> <div class="setting-item"> - <h2>{{ $t('settings.fun') }}</h2> + <h2>{{ $t('settings.notifications') }}</h2> <ul class="setting-list"> <li> - <Checkbox v-model="greentext"> - {{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }} - </Checkbox> + <BooleanSetting path="webPushNotifications"> + {{ $t('settings.enable_web_push_notifications') }} + </BooleanSetting> </li> </ul> </div> diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue @@ -10,20 +10,18 @@ :query="queryUserIds" :placeholder="$t('settings.search_user_to_block')" > - <BlockCard - slot-scope="row" - :user-id="row.item" - /> + <template v-slot="row"> + <BlockCard + :user-id="row.item" + /> + </template> </Autosuggest> </div> <BlockList :refresh="true" :get-key="i => i" > - <template - slot="header" - slot-scope="{selected}" - > + <template v-slot:header="{selected}"> <div class="bulk-actions"> <ProgressButton v-if="selected.length > 0" @@ -31,7 +29,7 @@ :click="() => blockUsers(selected)" > {{ $t('user_card.block') }} - <template slot="progress"> + <template v-slot:progress> {{ $t('user_card.block_progress') }} </template> </ProgressButton> @@ -41,19 +39,16 @@ :click="() => unblockUsers(selected)" > {{ $t('user_card.unblock') }} - <template slot="progress"> + <template v-slot:progress> {{ $t('user_card.unblock_progress') }} </template> </ProgressButton> </div> </template> - <template - slot="item" - slot-scope="{item}" - > + <template v-slot:item="{item}"> <BlockCard :user-id="item" /> </template> - <template slot="empty"> + <template v-slot:empty> {{ $t('settings.no_blocks') }} </template> </BlockList> @@ -68,20 +63,18 @@ :query="queryUserIds" :placeholder="$t('settings.search_user_to_mute')" > - <MuteCard - slot-scope="row" - :user-id="row.item" - /> + <template v-slot="row"> + <MuteCard + :user-id="row.item" + /> + </template> </Autosuggest> </div> <MuteList :refresh="true" :get-key="i => i" > - <template - slot="header" - slot-scope="{selected}" - > + <template v-slot:header="{selected}"> <div class="bulk-actions"> <ProgressButton v-if="selected.length > 0" @@ -89,7 +82,7 @@ :click="() => muteUsers(selected)" > {{ $t('user_card.mute') }} - <template slot="progress"> + <template v-slot:progress> {{ $t('user_card.mute_progress') }} </template> </ProgressButton> @@ -99,19 +92,16 @@ :click="() => unmuteUsers(selected)" > {{ $t('user_card.unmute') }} - <template slot="progress"> + <template v-slot:progress> {{ $t('user_card.unmute_progress') }} </template> </ProgressButton> </div> </template> - <template - slot="item" - slot-scope="{item}" - > + <template v-slot:item="{item}"> <MuteCard :user-id="item" /> </template> - <template slot="empty"> + <template v-slot:empty> {{ $t('settings.no_mutes') }} </template> </MuteList> @@ -124,20 +114,18 @@ :query="queryKnownDomains" :placeholder="$t('settings.type_domains_to_mute')" > - <DomainMuteCard - slot-scope="row" - :domain="row.item" - /> + <template v-slot="row"> + <DomainMuteCard + :domain="row.item" + /> + </template> </Autosuggest> </div> <DomainMuteList :refresh="true" :get-key="i => i" > - <template - slot="header" - slot-scope="{selected}" - > + <template v-slot:header="{selected}"> <div class="bulk-actions"> <ProgressButton v-if="selected.length > 0" @@ -145,19 +133,16 @@ :click="() => unmuteDomains(selected)" > {{ $t('domain_mute_card.unmute') }} - <template slot="progress"> + <template v-slot:progress> {{ $t('domain_mute_card.unmute_progress') }} </template> </ProgressButton> </div> </template> - <template - slot="item" - slot-scope="{item}" - > + <template v-slot:item="{item}"> <DomainMuteCard :domain="item" /> </template> - <template slot="empty"> + <template v-slot:empty> {{ $t('settings.no_mutes') }} </template> </DomainMuteList> diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue @@ -24,7 +24,7 @@ class="btn button-default" @click="updateNotificationSettings" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} </button> </div> </div> diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js @@ -24,7 +24,7 @@ library.add( const ProfileTab = { data () { return { - newName: this.$store.state.users.currentUser.name, + newName: this.$store.state.users.currentUser.name_unescaped, newBio: unescape(this.$store.state.users.currentUser.description), newLocked: this.$store.state.users.currentUser.locked, newNoRichText: this.$store.state.users.currentUser.no_rich_text, diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue @@ -153,7 +153,7 @@ class="btn button-default" @click="updateProfile" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} </button> </div> <div class="setting-item"> @@ -227,7 +227,7 @@ class="btn button-default" @click="submitBanner(banner)" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} </button> </div> <div class="setting-item"> @@ -266,7 +266,7 @@ class="btn button-default" @click="submitBackground(background)" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} </button> </div> </div> diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.js b/src/components/settings_modal/tabs/security_tab/security_tab.js @@ -1,6 +1,7 @@ import ProgressButton from 'src/components/progress_button/progress_button.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' import Mfa from './mfa.vue' +import localeService from 'src/services/locale/locale.service.js' const SecurityTab = { data () { @@ -37,7 +38,7 @@ const SecurityTab = { return { id: oauthToken.id, appName: oauthToken.app_name, - validUntil: new Date(oauthToken.valid_until).toLocaleDateString() + validUntil: new Date(oauthToken.valid_until).toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale)) } }) } diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue @@ -22,7 +22,7 @@ class="btn button-default" @click="changeEmail" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} </button> <p v-if="changedEmail"> {{ $t('settings.changed_email') }} @@ -60,7 +60,7 @@ class="btn button-default" @click="changePassword" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} </button> <p v-if="changedPassword"> {{ $t('settings.changed_password') }} @@ -133,7 +133,7 @@ class="btn button-default" @click="confirmDelete" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} </button> </div> </div> diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -16,6 +16,10 @@ import { colors2to3 } from 'src/services/style_setter/style_setter.js' import { + newImporter, + newExporter +} from 'src/services/export_import/export_import.js' +import { SLOT_INHERITANCE } from 'src/services/theme_data/pleromafe.js' import { @@ -31,18 +35,10 @@ 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 Select from 'src/components/select/select.vue' import Preview from './preview.vue' -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faChevronDown -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faChevronDown -) // List of color values used in v1 const v1OnlyNames = [ @@ -67,8 +63,18 @@ const colorConvert = (color) => { export default { data () { return { + themeImporter: newImporter({ + validator: this.importValidator, + onImport: this.onImport, + onImportFailure: this.onImportFailure + }), + themeExporter: newExporter({ + filename: 'pleroma_theme', + getExportedObject: () => this.exportedTheme + }), availableStyles: [], - selected: this.$store.getters.mergedConfig.theme, + selected: '', + selectedTheme: this.$store.getters.mergedConfig.theme, themeWarning: undefined, tempImportFile: undefined, engineVersion: 0, @@ -202,7 +208,7 @@ export default { } }, selectedVersion () { - return Array.isArray(this.selected) ? 1 : 2 + return Array.isArray(this.selectedTheme) ? 1 : 2 }, currentColors () { return Object.keys(SLOT_INHERITANCE) @@ -383,8 +389,8 @@ export default { FontControl, TabSwitcher, Preview, - ExportImport, - Checkbox + Checkbox, + Select }, methods: { loadTheme ( @@ -469,7 +475,7 @@ export default { this.loadThemeFromLocalStorage(false, true) break case 'file': - console.err('Forcing snapshout from file is not supported yet') + console.error('Forcing snapshot from file is not supported yet') break } this.dismissWarning() @@ -528,10 +534,15 @@ export default { this.previewColors.mod ) }, + importTheme () { this.themeImporter.importData() }, + exportTheme () { this.themeExporter.exportData() }, onImport (parsed, forceSource = false) { this.tempImportFile = parsed this.loadTheme(parsed, 'file', forceSource) }, + onImportFailure (result) { + this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' }) + }, importValidator (parsed) { const version = parsed._pleroma_theme_version return version >= 1 || version <= 2 @@ -735,6 +746,16 @@ export default { } }, selected () { + this.selectedTheme = Object.entries(this.availableStyles).find(([k, s]) => { + if (Array.isArray(s)) { + console.log(s[0] === this.selected, this.selected) + return s[0] === this.selected + } else { + return s.name === this.selected + } + })[1] + }, + selectedTheme () { this.dismissWarning() if (this.selectedVersion === 1) { if (!this.keepRoundness) { @@ -752,17 +773,17 @@ export default { 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] + this.bgColorLocal = this.selectedTheme[1] + this.fgColorLocal = this.selectedTheme[2] + this.textColorLocal = this.selectedTheme[3] + this.linkColorLocal = this.selectedTheme[4] + this.cRedColorLocal = this.selectedTheme[5] + this.cGreenColorLocal = this.selectedTheme[6] + this.cBlueColorLocal = this.selectedTheme[7] + this.cOrangeColorLocal = this.selectedTheme[8] } } else if (this.selectedVersion >= 2) { - this.normalizeLocalState(this.selected.theme, 2, this.selected.source) + this.normalizeLocalState(this.selectedTheme.theme, 2, this.selectedTheme.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 @@ -270,6 +270,9 @@ .apply-container { justify-content: center; + position: absolute; + bottom: 8px; + right: 5px; } .radius-item, diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue @@ -48,46 +48,47 @@ </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" + <div class="top"> + <div class="presets"> + {{ $t('settings.presets') }} + <label + for="preset-switcher" + class="select" + > + <Select + id="preset-switcher" + v-model="selected" + class="preset-switcher" > - <select - id="preset-switcher" - v-model="selected" - class="preset-switcher" + <option + v-for="style in availableStyles" + :key="style.name" + :value="style.name || style[0]" + :style="{ + backgroundColor: style[1] || (style.theme || style.source).colors.bg, + color: style[3] || (style.theme || style.source).colors.text + }" > - <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> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> - </div> - </template> - </ExportImport> + {{ style[0] || style.name }} + </option> + </Select> + </label> + </div> + <div class="export-import"> + <button + class="btn button-default" + @click="importTheme" + > + {{ $t(&quot;settings.import_theme&quot;) }} + </button> + <button + class="btn button-default" + @click="exportTheme" + > + {{ $t(&quot;settings.export_theme&quot;) }} + </button> + </div> + </div> </div> <div class="save-load-options"> <span class="keep-option"> @@ -902,28 +903,19 @@ <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" > - <select - id="shadow-switcher" - v-model="shadowSelected" - class="shadow-switcher" + <option + v-for="shadow in shadowsAvailable" + :key="shadow" + :value="shadow" > - <option - v-for="shadow in shadowsAvailable" - :key="shadow" - :value="shadow" - > - {{ $t('settings.style.shadows.components.' + shadow) }} - </option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> + {{ $t('settings.style.shadows.components.' + shadow) }} + </option> + </Select> </div> <div class="override"> <label diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js @@ -1,5 +1,6 @@ import ColorInput from '../color_input/color_input.vue' import OpacityInput from '../opacity_input/opacity_input.vue' +import Select from '../select/select.vue' import { getCssShadow } from '../../services/style_setter/style_setter.js' import { hex2rgb } from '../../services/color_convert/color_convert.js' import { library } from '@fortawesome/fontawesome-svg-core' @@ -45,7 +46,8 @@ export default { }, components: { ColorInput, - OpacityInput + OpacityInput, + Select }, methods: { add () { diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue @@ -59,30 +59,20 @@ :disabled="usingFallback" class="id-control style-control" > - <label - for="shadow-switcher" - class="select" + <Select + id="shadow-switcher" + v-model="selectedId" + class="shadow-switcher" :disabled="!ready || usingFallback" > - <select - id="shadow-switcher" - v-model="selectedId" - class="shadow-switcher" - :disabled="!ready || usingFallback" + <option + v-for="(shadow, index) in cValue" + :key="index" + :value="index" > - <option - v-for="(shadow, index) in cValue" - :key="index" - :value="index" - > - {{ $t('settings.style.shadows.shadow_id', { value: index }) }} - </option> - </select> - <FAIcon - icon="chevron-down" - class="select-down-icon" - /> - </label> + {{ $t('settings.style.shadows.shadow_id', { value: index }) }} + </option> + </Select> <button class="btn button-default" :disabled="!ready || !present" @@ -316,20 +306,20 @@ .id-control { align-items: stretch; - .select, .btn { + + .shadow-switcher { + flex: 1; + } + + .shadow-switcher, .btn { min-width: 1px; margin-right: 5px; } + .btn { padding: 0 .4em; margin: 0 .1em; } - .select { - flex: 1; - select { - align-self: initial; - } - } } } } diff --git a/src/components/shout_panel/shout_panel.js b/src/components/shout_panel/shout_panel.js @@ -0,0 +1,53 @@ +import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faBullhorn, + faTimes +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faBullhorn, + faTimes +) + +const shoutPanel = { + props: [ 'floating' ], + data () { + return { + currentMessage: '', + channel: null, + collapsed: true + } + }, + computed: { + messages () { + return this.$store.state.shout.messages + } + }, + methods: { + submit (message) { + this.$store.state.shout.channel.push('new_msg', { text: message }, 10000) + this.currentMessage = '' + }, + togglePanel () { + this.collapsed = !this.collapsed + }, + userProfileLink (user) { + return generateProfileLink(user.id, user.username, this.$store.state.instance.restrictedNicknames) + } + }, + watch: { + messages (newVal) { + const scrollEl = this.$el.querySelector('.chat-window') + if (!scrollEl) return + if (scrollEl.scrollTop + scrollEl.offsetHeight + 20 > scrollEl.scrollHeight) { + this.$nextTick(() => { + if (!scrollEl) return + scrollEl.scrollTop = scrollEl.scrollHeight - scrollEl.offsetHeight + }) + } + } + } +} + +export default shoutPanel diff --git a/src/components/shout_panel/shout_panel.vue b/src/components/shout_panel/shout_panel.vue @@ -0,0 +1,155 @@ +<template> + <div + v-if="!collapsed || !floating" + class="shout-panel" + > + <div class="panel panel-default"> + <div + class="panel-heading timeline-heading" + :class="{ 'shout-heading': floating }" + @click.stop.prevent="togglePanel" + > + <div class="title"> + {{ $t('shoutbox.title') }} + <FAIcon + v-if="floating" + icon="times" + class="close-icon" + /> + </div> + </div> + <div class="shout-window"> + <div + v-for="message in messages" + :key="message.id" + class="shout-message" + > + <span class="shout-avatar"> + <img :src="message.author.avatar"> + </span> + <div class="shout-content"> + <router-link + class="shout-name" + :to="userProfileLink(message.author)" + > + {{ message.author.username }} + </router-link> + <br> + <span class="shout-text"> + {{ message.text }} + </span> + </div> + </div> + </div> + <div class="shout-input"> + <textarea + v-model="currentMessage" + class="shout-input-textarea" + rows="1" + @keyup.enter="submit(currentMessage)" + /> + </div> + </div> + </div> + <div + v-else + class="shout-panel" + > + <div class="panel panel-default"> + <div + class="panel-heading stub timeline-heading shout-heading" + @click.stop.prevent="togglePanel" + > + <div class="title"> + <FAIcon + class="icon" + icon="bullhorn" + /> + {{ $t('shoutbox.title') }} + </div> + </div> + </div> + </div> +</template> + +<script src="./shout_panel.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.floating-shout { + position: fixed; + bottom: 0px; + z-index: 1000; + max-width: 25em; +} + +.floating-shout.left { + left: 0px; +} + +.floating-shout:not(.left) { + right: 0px; +} + +.shout-panel { + .shout-heading { + cursor: pointer; + + .icon { + color: $fallback--text; + color: var(--text, $fallback--text); + margin-right: 0.5em; + } + + .title { + display: flex; + justify-content: space-between; + align-items: center; + } + } + + .shout-window { + overflow-y: auto; + overflow-x: hidden; + max-height: 20em; + } + + .shout-window-container { + height: 100%; + } + + .shout-message { + display: flex; + padding: 0.2em 0.5em + } + + .shout-avatar { + img { + height: 24px; + width: 24px; + border-radius: $fallback--avatarRadius; + border-radius: var(--avatarRadius, $fallback--avatarRadius); + margin-right: 0.5em; + margin-top: 0.25em; + } + } + + .shout-input { + display: flex; + textarea { + flex: 1; + margin: 0.6em; + min-height: 3.5em; + resize: none; + } + } + + .shout-panel { + .title { + display: flex; + justify-content: space-between; + } + } +} +</style> diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js @@ -49,7 +49,7 @@ const SideDrawer = { currentUser () { return this.$store.state.users.currentUser }, - chat () { return this.$store.state.chat.channel.state === 'joined' }, + shout () { return this.$store.state.shout.channel.state === 'joined' }, unseenNotifications () { return unseenNotificationsFromStore(this.$store) }, diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue @@ -106,10 +106,10 @@ </router-link> </li> <li - v-if="chat" + v-if="shout" @click="toggleDrawer" > - <router-link :to="{ name: 'chat-panel' }"> + <router-link :to="{ name: 'shout-panel' }"> <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -273,9 +273,7 @@ --icon: var(--popoverIcon, $fallback--icon); .badge { - position: absolute; - right: 0.7rem; - top: 1em; + margin-left: 10px; } } diff --git a/src/components/staff_panel/staff_panel.js b/src/components/staff_panel/staff_panel.js @@ -1,4 +1,6 @@ import map from 'lodash/map' +import groupBy from 'lodash/groupBy' +import { mapGetters, mapState } from 'vuex' import BasicUserCard from '../basic_user_card/basic_user_card.vue' const StaffPanel = { @@ -10,9 +12,21 @@ const StaffPanel = { BasicUserCard }, computed: { - staffAccounts () { - return map(this.$store.state.instance.staffAccounts, nickname => this.$store.getters.findUser(nickname)).filter(_ => _) - } + groupedStaffAccounts () { + const staffAccounts = map(this.staffAccounts, this.findUser).filter(_ => _) + const groupedStaffAccounts = groupBy(staffAccounts, 'role') + + return [ + { role: 'admin', users: groupedStaffAccounts['admin'] }, + { role: 'moderator', users: groupedStaffAccounts['moderator'] } + ].filter(group => group.users) + }, + ...mapGetters([ + 'findUser' + ]), + ...mapState({ + staffAccounts: state => state.instance.staffAccounts + }) } } diff --git a/src/components/staff_panel/staff_panel.vue b/src/components/staff_panel/staff_panel.vue @@ -7,11 +7,18 @@ </div> </div> <div class="panel-body"> - <basic-user-card - v-for="user in staffAccounts" - :key="user.screen_name" - :user="user" - /> + <div + v-for="group in groupedStaffAccounts" + :key="group.role" + class="staff-group" + > + <h4>{{ $t('general.role.' + group.role) }}</h4> + <basic-user-card + v-for="user in group.users" + :key="user.screen_name" + :user="user" + /> + </div> </div> </div> </div> @@ -20,4 +27,14 @@ <script src="./staff_panel.js" ></script> <style lang="scss"> + +.staff-group { + padding-left: 1em; + padding-top: 1em; + + .basic-user-card { + padding-left: 0; + } +} + </style> diff --git a/src/components/status/status.js b/src/components/status/status.js @@ -9,9 +9,12 @@ import UserAvatar from '../user_avatar/user_avatar.vue' import AvatarList from '../avatar_list/avatar_list.vue' import Timeago from '../timeago/timeago.vue' import StatusContent from '../status_content/status_content.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import StatusPopover from '../status_popover/status_popover.vue' import UserListPopover from '../user_list_popover/user_list_popover.vue' import EmojiReactions from '../emoji_reactions/emoji_reactions.vue' +import MentionsLine from 'src/components/mentions_line/mentions_line.vue' +import MentionLink from 'src/components/mention_link/mention_link.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { muteWordHits } from '../../services/status_parser/status_parser.js' @@ -68,7 +71,10 @@ const Status = { StatusPopover, UserListPopover, EmojiReactions, - StatusContent + StatusContent, + RichContent, + MentionLink, + MentionsLine }, props: [ 'statusoid', @@ -92,7 +98,8 @@ const Status = { userExpanded: false, mediaPlaying: [], suspendable: true, - error: null + error: null, + headTailLinks: null } }, computed: { @@ -132,12 +139,15 @@ const Status = { }, replyProfileLink () { if (this.isReply) { - return this.generateUserProfileLink(this.status.in_reply_to_user_id, this.replyToName) + const user = this.$store.getters.findUser(this.status.in_reply_to_user_id) + // FIXME Why user not found sometimes??? + return user ? user.statusnet_profile_url : 'NOT_FOUND' } }, retweet () { return !!this.statusoid.retweeted_status }, - retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name }, - retweeterHtml () { return this.statusoid.user.name_html }, + retweeterUser () { return this.statusoid.user }, + retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name_ui }, + retweeterHtml () { return this.statusoid.user.name }, retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) }, status () { if (this.retweet) { @@ -156,27 +166,52 @@ const Status = { muteWordHits () { return muteWordHits(this.status, this.muteWords) }, + mentionsLine () { + if (!this.headTailLinks) return [] + const writtenSet = new Set(this.headTailLinks.writtenMentions.map(_ => _.url)) + return this.status.attentions.filter(attn => { + // no reply user + return attn.id !== this.status.in_reply_to_user_id && + // no self-replies + attn.statusnet_profile_url !== this.status.user.statusnet_profile_url && + // don't include if mentions is written + !writtenSet.has(attn.statusnet_profile_url) + }).map(attn => ({ + url: attn.statusnet_profile_url, + content: attn.screen_name, + userId: attn.id + })) + }, + hasMentionsLine () { + return this.mentionsLine.length > 0 + }, muted () { if (this.statusoid.user.id === this.currentUser.id) return false + const reasonsToMute = this.userIsMuted || + // Thread is muted + status.thread_muted || + // Wordfiltered + this.muteWordHits.length > 0 + return !this.unmuted && !this.shouldNotMute && reasonsToMute + }, + userIsMuted () { + if (this.statusoid.user.id === this.currentUser.id) return false 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 || + return 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 = ( + (relationshipReblog && relationshipReblog.muting) + }, + shouldNotMute () { + const { status } = this + const { reblog } = status + return ( ( this.inProfile && ( // Don't mute user's posts on user timeline (except reblogs) @@ -189,14 +224,26 @@ const Status = { (this.inConversation && status.thread_muted) // No excuses if post has muted words ) && !this.muteWordHits.length > 0 - - return !this.unmuted && !excusesNotToMute && reasonsToMute + }, + hideMutedUsers () { + return this.mergedConfig.hideMutedPosts + }, + hideMutedThreads () { + return this.mergedConfig.hideMutedThreads }, hideFilteredStatuses () { return this.mergedConfig.hideFilteredStatuses }, + hideWordFilteredPosts () { + return this.mergedConfig.hideWordFilteredPosts + }, hideStatus () { - return (this.muted && this.hideFilteredStatuses) || this.virtualHidden + return (this.virtualHidden || !this.shouldNotMute) && ( + (this.muted && this.hideFilteredStatuses) || + (this.userIsMuted && this.hideMutedUsers) || + (this.status.thread_muted && this.hideMutedThreads) || + (this.muteWordHits.length > 0 && this.hideWordFilteredPosts) + ) }, isFocused () { // retweet or root of an expanded conversation @@ -216,7 +263,7 @@ const Status = { return this.status.in_reply_to_screen_name } else { const user = this.$store.getters.findUser(this.status.in_reply_to_user_id) - return user && user.screen_name + return user && user.screen_name_ui } }, replySubject () { @@ -303,6 +350,9 @@ const Status = { }, removeMediaPlaying (id) { this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id) + }, + setHeadTailLinks (headTailLinks) { + this.headTailLinks = headTailLinks } }, watch: { diff --git a/src/components/status/status.scss b/src/components/status/status.scss @@ -1,10 +1,12 @@ - @import '../../_variables.scss'; $status-margin: 0.75em; .Status { min-width: 0; + white-space: normal; + word-wrap: break-word; + word-break: break-word; &:hover { --_still-image-img-visibility: visible; @@ -93,12 +95,8 @@ $status-margin: 0.75em; margin-right: 0.4em; text-overflow: ellipsis; - .emoji { - width: 14px; - height: 14px; - vertical-align: middle; - object-fit: contain; - } + --_still_image-label-scale: 0.25; + --emoji-size: 14px; } .status-favicon { @@ -155,42 +153,37 @@ $status-margin: 0.75em; } } + .glued-label { + display: inline-flex; + white-space: nowrap; + } + .timeago { margin-right: 0.2em; } - .heading-reply-row { + & .heading-reply-row { position: relative; align-content: baseline; font-size: 12px; - line-height: 18px; + margin-top: 0.2em; + line-height: 130%; 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; - } - } - & .reply-to-popover, - & .reply-to-no-popover { + & .reply-to-no-popover, + & .mentions { min-width: 0; margin-right: 0.4em; flex-shrink: 0; } + .reply-glued-label { + margin-right: 0.5em; + } + .reply-to-popover { .reply-to:hover::before { content: ''; @@ -220,21 +213,26 @@ $status-margin: 0.75em; } } - .reply-to { + & .mentions, + & .reply-to { + white-space: nowrap; position: relative; } - .reply-to-text { + & .mentions-text, + & .reply-to-text { + color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .replies-separator { - margin-left: 0.4em; + .mentions-line { + display: inline; } .replies { + margin-top: 0.25em; line-height: 18px; font-size: 12px; display: flex; diff --git a/src/components/status/status.vue b/src/components/status/status.vue @@ -1,5 +1,4 @@ <template> - <!-- eslint-disable vue/no-v-html --> <div v-if="!hideStatus" class="Status" @@ -26,7 +25,7 @@ icon="retweet" /> <router-link :to="userProfileLink"> - {{ status.user.screen_name }} + {{ status.user.screen_name_ui }} </router-link> </small> <small @@ -89,8 +88,12 @@ <router-link v-if="retweeterHtml" :to="retweeterProfileLink" - v-html="retweeterHtml" - /> + > + <RichContent + :html="retweeterHtml" + :emoji="retweeterUser.emoji" + /> + </router-link> <router-link v-else :to="retweeterProfileLink" @@ -145,8 +148,12 @@ v-if="status.user.name_html" class="status-username" :title="status.user.name" - v-html="status.user.name_html" - /> + > + <RichContent + :html="status.user.name" + :emoji="status.user.emoji" + /> + </h4> <h4 v-else class="status-username" @@ -156,10 +163,10 @@ </h4> <router-link class="account-name" - :title="status.user.screen_name" + :title="status.user.screen_name_ui" :to="userProfileLink" > - {{ status.user.screen_name }} + {{ status.user.screen_name_ui }} </router-link> <img v-if="!!(status.user && status.user.favicon)" @@ -214,11 +221,13 @@ </button> </span> </div> - - <div class="heading-reply-row"> - <div + <div + v-if="isReply || hasMentionsLine" + class="heading-reply-row" + > + <span v-if="isReply" - class="reply-to-and-accountname" + class="glued-label reply-glued-label" > <StatusPopover v-if="!isPreview" @@ -238,7 +247,7 @@ flip="horizontal" /> <span - class="faint-link reply-to-text" + class="reply-to-text" > {{ $t('status.reply_to') }} </span> @@ -251,50 +260,76 @@ > <span class="reply-to-text">{{ $t('status.reply_to') }}</span> </span> - <router-link - class="reply-to-link" - :title="replyToName" - :to="replyProfileLink" - > - {{ replyToName }} - </router-link> - <span - v-if="replies && replies.length" - class="faint replies-separator" - > - - - </span> - </div> - <div - v-if="inConversation && !isPreview && replies && replies.length" - class="replies" + <MentionLink + :content="replyToName" + :url="replyProfileLink" + :user-id="status.in_reply_to_user_id" + :user-screen-name="status.in_reply_to_screen_name" + :first-mention="false" + /> + </span> + + <!-- This little wrapper is made for sole purpose of "gluing" --> + <!-- "Mentions" label to the first mention --> + <span + v-if="hasMentionsLine" + class="glued-label" > - <span class="faint">{{ $t('status.replies_list') }}</span> - <StatusPopover - v-for="reply in replies" - :key="reply.id" - :status-id="reply.id" + <span + class="mentions" + :aria-label="$t('tool_tip.mentions')" + @click.prevent="gotoOriginal(status.in_reply_to_status_id)" > - <button - class="button-unstyled -link reply-link" - @click.prevent="gotoOriginal(reply.id)" + <span + class="mentions-text" > - {{ reply.name }} - </button> - </StatusPopover> - </div> + {{ $t('status.mentions') }} + </span> + </span> + <MentionsLine + v-if="hasMentionsLine" + :mentions="mentionsLine.slice(0, 1)" + class="mentions-line-first" + /> + </span> + <MentionsLine + v-if="hasMentionsLine" + :mentions="mentionsLine.slice(1)" + class="mentions-line" + /> </div> </div> <StatusContent + ref="content" :status="status" :no-heading="noHeading" :highlight="highlight" :focused="isFocused" @mediaplay="addMediaPlaying($event)" @mediapause="removeMediaPlaying($event)" + @parseReady="setHeadTailLinks" /> + <div + v-if="inConversation && !isPreview && replies && replies.length" + class="replies" + > + <span class="faint">{{ $t('status.replies_list') }}</span> + <StatusPopover + v-for="reply in replies" + :key="reply.id" + :status-id="reply.id" + > + <button + class="button-unstyled -link reply-link" + @click.prevent="gotoOriginal(reply.id)" + > + {{ reply.name }} + </button> + </StatusPopover> + </div> + <transition name="fade"> <div v-if="!hidePostStats && isFocused && combinedFavsAndRepeatsUsers.length > 0" @@ -402,7 +437,6 @@ </div> </template> </div> -<!-- eslint-enable vue/no-v-html --> </template> <script src="./status.js" ></script> diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js @@ -0,0 +1,129 @@ +import fileType from 'src/services/file_type/file_type.service' +import RichContent from 'src/components/rich_content/rich_content.jsx' +import { mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faFile, + faMusic, + faImage, + faLink, + faPollH +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faFile, + faMusic, + faImage, + faLink, + faPollH +) + +const StatusContent = { + name: 'StatusContent', + props: [ + 'compact', + 'status', + 'focused', + 'noHeading', + 'fullContent', + 'singleLine' + ], + data () { + return { + showingTall: this.fullContent || (this.inConversation && this.focused), + showingLongSubject: false, + // not as computed because it sets the initial state which will be changed later + expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject, + postLength: this.status.text.length, + parseReadyDone: false + } + }, + computed: { + localCollapseSubjectDefault () { + return this.mergedConfig.collapseMessageWithSubject + }, + // This is a bit hacky, but we want to approximate post height before rendering + // so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them) + // as well as approximate line count by counting characters and approximating ~80 + // per line. + // + // Using max-height + overflow: auto for status components resulted in false positives + // very often with japanese characters, and it was very annoying. + tallStatus () { + if (this.singleLine || this.compact) return false + const lengthScore = this.status.raw_html.split(/<p|<br/).length + this.postLength / 80 + return lengthScore > 20 + }, + longSubject () { + return this.status.summary.length > 240 + }, + // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status. + mightHideBecauseSubject () { + return !!this.status.summary && this.localCollapseSubjectDefault + }, + mightHideBecauseTall () { + return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault) + }, + hideSubjectStatus () { + return this.mightHideBecauseSubject && !this.expandingSubject + }, + hideTallStatus () { + return this.mightHideBecauseTall && !this.showingTall + }, + showingMore () { + return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject) + }, + attachmentTypes () { + return this.status.attachments.map(file => fileType.fileType(file.mimetype)) + }, + ...mapGetters(['mergedConfig']) + }, + components: { + RichContent + }, + mounted () { + this.status.attentions && this.status.attentions.forEach(attn => { + const { id } = attn + this.$store.dispatch('fetchUserIfMissing', id) + }) + }, + methods: { + onParseReady (event) { + if (this.parseReadyDone) return + this.parseReadyDone = true + this.$emit('parseReady', event) + const { writtenMentions, invisibleMentions } = event + writtenMentions + .filter(mention => !mention.notifying) + .forEach(mention => { + const { content, url } = mention + const cleanedString = content.replace(/<[^>]+?>/gi, '') // remove all tags + if (!cleanedString.startsWith('@')) return + const handle = cleanedString.slice(1) + const host = url.replace(/^https?:\/\//, '').replace(/\/.+?$/, '') + this.$store.dispatch('fetchUserIfMissing', `${handle}@${host}`) + }) + /* This is a bit of a hack to make current tall status detector work + * with rich mentions. Invisible mentions are detected at RichContent level + * and also we generate plaintext version of mentions by stripping tags + * so here we subtract from post length by each mention that became invisible + * via MentionsLine + */ + this.postLength = invisibleMentions.reduce((acc, mention) => { + return acc - mention.textContent.length - 1 + }, this.postLength) + }, + toggleShowMore () { + if (this.mightHideBecauseTall) { + this.showingTall = !this.showingTall + } else if (this.mightHideBecauseSubject) { + this.expandingSubject = !this.expandingSubject + } + }, + generateTagLink (tag) { + return `/tag/${tag}` + } + } +} + +export default StatusContent diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss @@ -0,0 +1,174 @@ +@import '../../_variables.scss'; + +.StatusBody { + display: flex; + flex-direction: column; + + .emoji { + --_still_image-label-scale: 0.5; + } + + .attachments { + margin-top: 0.5em; + } + + & .text, + & .summary { + font-family: var(--postFont, sans-serif); + white-space: pre-wrap; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; + line-height: 1.4em; + } + + .summary { + display: block; + font-style: italic; + padding-bottom: 0.5em; + } + + .text { + &.-single-line { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + height: 1.4em; + } + } + + .summary-wrapper { + margin-bottom: 0.5em; + border-style: solid; + border-width: 0 0 1px 0; + border-color: var(--border, $fallback--border); + flex-grow: 0; + + &.-tall { + position: relative; + + .summary { + max-height: 2em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + } + + .text-wrapper { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + + &.-tall-status { + position: relative; + height: 220px; + overflow-x: hidden; + overflow-y: hidden; + z-index: 1; + + .media-body { + min-height: 0; + mask: + linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat, + linear-gradient(to top, white, white); + + /* Autoprefixed seem to ignore this one, and also syntax is different */ + -webkit-mask-composite: xor; + mask-composite: exclude; + } + } + } + + & .tall-status-hider, + & .tall-subject-hider, + & .status-unhider, + & .cw-status-hider { + display: inline-block; + word-break: break-all; + width: 100%; + text-align: center; + } + + .tall-status-hider { + position: absolute; + height: 70px; + margin-top: 150px; + line-height: 110px; + z-index: 2; + } + + .tall-subject-hider { + // position: absolute; + padding-bottom: 0.5em; + } + + & .status-unhider, + & .cw-status-hider { + word-break: break-all; + + svg { + color: inherit; + } + } + + .greentext { + color: $fallback--cGreen; + color: var(--postGreentext, $fallback--cGreen); + } + + .cyantext { + color: var(--postCyantext, $fallback--cBlue); + } + + &.-compact { + align-items: top; + flex-direction: row; + + --emoji-size: 16px; + + & .body, + & .attachments { + max-height: 3.25em; + } + + .body { + overflow: hidden; + white-space: normal; + min-width: 5em; + flex: 5 1 auto; + mask-size: auto 3.5em, auto auto; + mask-position: 0 0, 0 0; + mask-repeat: repeat-x, repeat; + mask-image: linear-gradient(to bottom, white 2em, transparent 3em); + + /* Autoprefixed seem to ignore this one, and also syntax is different */ + -webkit-mask-composite: xor; + mask-composite: exclude; + } + + .attachments { + margin-top: 0; + flex: 1 1 0; + min-width: 5em; + height: 100%; + margin-left: 0.5em; + } + + .summary-wrapper { + .summary::after { + content: ': '; + } + + line-height: inherit; + margin: 0; + border: none; + display: inline-block; + } + + .text-wrapper { + display: inline-block; + } + } +} diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue @@ -0,0 +1,100 @@ +<template> + <div + class="StatusBody" + :class="{ '-compact': compact }" + > + <div class="body"> + <div + v-if="status.summary_raw_html" + class="summary-wrapper" + :class="{ '-tall': (longSubject && !showingLongSubject) }" + > + <RichContent + class="media-body summary" + :html="status.summary_raw_html" + :emoji="status.emojis" + /> + <button + v-if="longSubject && showingLongSubject" + class="button-unstyled -link tall-subject-hider" + @click.prevent="showingLongSubject=false" + > + {{ $t("status.hide_full_subject") }} + </button> + <button + v-else-if="longSubject" + class="button-unstyled -link tall-subject-hider" + @click.prevent="showingLongSubject=true" + > + {{ $t("status.show_full_subject") }} + </button> + </div> + <div + :class="{'-tall-status': hideTallStatus}" + class="text-wrapper" + > + <button + v-if="hideTallStatus" + class="button-unstyled -link tall-status-hider" + :class="{ '-focused': focused }" + @click.prevent="toggleShowMore" + > + {{ $t("general.show_more") }} + </button> + <RichContent + v-if="!hideSubjectStatus && !(singleLine && status.summary_raw_html)" + :class="{ '-single-line': singleLine }" + class="text media-body" + :html="status.raw_html" + :emoji="status.emojis" + :handle-links="true" + :greentext="mergedConfig.greentext" + :attentions="status.attentions" + @parseReady="onParseReady" + /> + + <button + v-if="hideSubjectStatus" + class="button-unstyled -link cw-status-hider" + @click.prevent="toggleShowMore" + > + {{ $t("status.show_content") }} + <FAIcon + v-if="attachmentTypes.includes('image')" + icon="image" + /> + <FAIcon + v-if="attachmentTypes.includes('video')" + icon="video" + /> + <FAIcon + v-if="attachmentTypes.includes('audio')" + icon="music" + /> + <FAIcon + v-if="attachmentTypes.includes('unknown')" + icon="file" + /> + <FAIcon + v-if="status.poll && status.poll.options" + icon="poll-h" + /> + <FAIcon + v-if="status.card" + icon="link" + /> + </button> + <button + v-if="showingMore && !fullContent" + class="button-unstyled -link status-unhider" + @click.prevent="toggleShowMore" + > + {{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }} + </button> + </div> + </div> + <slot v-if="!hideSubjectStatus" /> + </div> +</template> +<script src="./status_body.js" ></script> +<style lang="scss" src="./status_body.scss" /> diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js @@ -1,11 +1,8 @@ import Attachment from '../attachment/attachment.vue' import Poll from '../poll/poll.vue' import Gallery from '../gallery/gallery.vue' +import StatusBody from 'src/components/status_body/status_body.vue' import LinkPreview from '../link-preview/link-preview.vue' -import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' -import fileType from 'src/services/file_type/file_type.service' -import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js' -import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js' import { mapGetters, mapState } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -30,57 +27,17 @@ const StatusContent = { name: 'StatusContent', props: [ 'status', + 'compact', 'focused', 'noHeading', 'fullContent', 'singleLine' ], - data () { - return { - showingTall: this.fullContent || (this.inConversation && this.focused), - showingLongSubject: false, - // not as computed because it sets the initial state which will be changed later - expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject - } - }, computed: { - localCollapseSubjectDefault () { - return this.mergedConfig.collapseMessageWithSubject - }, hideAttachments () { return (this.mergedConfig.hideAttachments && !this.inConversation) || (this.mergedConfig.hideAttachmentsInConv && this.inConversation) }, - // This is a bit hacky, but we want to approximate post height before rendering - // so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them) - // as well as approximate line count by counting characters and approximating ~80 - // per line. - // - // Using max-height + overflow: auto for status components resulted in false positives - // very often with japanese characters, and it was very annoying. - tallStatus () { - const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80 - return lengthScore > 20 - }, - longSubject () { - return this.status.summary.length > 240 - }, - // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status. - mightHideBecauseSubject () { - return !!this.status.summary && this.localCollapseSubjectDefault - }, - mightHideBecauseTall () { - return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault) - }, - hideSubjectStatus () { - return this.mightHideBecauseSubject && !this.expandingSubject - }, - hideTallStatus () { - return this.mightHideBecauseTall && !this.showingTall - }, - showingMore () { - return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject) - }, nsfwClickthrough () { if (!this.status.nsfw) { return false @@ -91,72 +48,20 @@ const StatusContent = { return true }, attachmentSize () { - if ((this.mergedConfig.hideAttachments && !this.inConversation) || + if (this.compact) { + return 'small' + } else if ((this.mergedConfig.hideAttachments && !this.inConversation) || (this.mergedConfig.hideAttachmentsInConv && this.inConversation) || (this.status.attachments.length > this.maxThumbnails)) { return 'hide' - } else if (this.compact) { - return 'small' } return 'normal' }, - galleryTypes () { - if (this.attachmentSize === 'hide') { - return [] - } - return this.mergedConfig.playVideosInModal - ? ['image', 'video'] - : ['image'] - }, - galleryAttachments () { - return this.status.attachments.filter( - file => fileType.fileMatchesSomeType(this.galleryTypes, file) - ) - }, - nonGalleryAttachments () { - return this.status.attachments.filter( - file => !fileType.fileMatchesSomeType(this.galleryTypes, file) - ) - }, - attachmentTypes () { - return this.status.attachments.map(file => fileType.fileType(file.mimetype)) - }, maxThumbnails () { return this.mergedConfig.maxThumbnails }, - postBodyHtml () { - const html = this.status.statusnet_html - - if (this.mergedConfig.greentext) { - try { - if (html.includes('&gt;')) { - // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works - return processHtml(html, (string) => { - if (string.includes('&gt;') && - string - .replace(/<[^>]+?>/gi, '') // remove all tags - .replace(/@\w+/gi, '') // remove mentions (even failed ones) - .trim() - .startsWith('&gt;')) { - return `<span class='greentext'>${string}</span>` - } else { - return string - } - }) - } else { - return html - } - } catch (e) { - console.err('Failed to process status html', e) - return html - } - } else { - return html - } - }, ...mapGetters(['mergedConfig']), ...mapState({ - betterShadow: state => state.interface.browserSupport.cssFilter, currentUser: state => state.users.currentUser }) }, @@ -164,52 +69,8 @@ const StatusContent = { Attachment, Poll, Gallery, - LinkPreview - }, - methods: { - linkClicked (event) { - const target = event.target.closest('.status-content a') - if (target) { - if (target.className.match(/mention/)) { - const href = target.href - const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href)) - if (attn) { - event.stopPropagation() - event.preventDefault() - const link = this.generateUserProfileLink(attn.id, attn.screen_name) - this.$router.push(link) - return - } - } - if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) { - // Extract tag name from dataset or link url - const tag = target.dataset.tag || extractTagFromUrl(target.href) - if (tag) { - const link = this.generateTagLink(tag) - this.$router.push(link) - return - } - } - window.open(target.href, '_blank') - } - }, - toggleShowMore () { - if (this.mightHideBecauseTall) { - this.showingTall = !this.showingTall - } else if (this.mightHideBecauseSubject) { - this.expandingSubject = !this.expandingSubject - } - }, - generateUserProfileLink (id, name) { - return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) - }, - generateTagLink (tag) { - return `/tag/${tag}` - }, - setMedia () { - const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments - return () => this.$store.dispatch('setMedia', attachments) - } + LinkPreview, + StatusBody } } diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue @@ -1,133 +1,53 @@ <template> - <!-- eslint-disable vue/no-v-html --> - <div class="StatusContent"> + <div + class="StatusContent" + :class="{ '-compact': compact }" + > <slot name="header" /> - <div - v-if="status.summary_html" - class="summary-wrapper" - :class="{ 'tall-subject': (longSubject && !showingLongSubject) }" + <StatusBody + :status="status" + :compact="compact" + :single-line="singleLine" + @parseReady="$emit('parseReady', $event)" > - <div - class="media-body summary" - @click.prevent="linkClicked" - v-html="status.summary_html" - /> - <button - v-if="longSubject && showingLongSubject" - class="button-unstyled -link tall-subject-hider" - @click.prevent="showingLongSubject=false" - > - {{ $t("status.hide_full_subject") }} - </button> - <button - v-else-if="longSubject" - class="button-unstyled -link tall-subject-hider" - :class="{ 'tall-subject-hider_focused': focused }" - @click.prevent="showingLongSubject=true" - > - {{ $t("status.show_full_subject") }} - </button> - </div> - <div - :class="{'tall-status': hideTallStatus}" - class="status-content-wrapper" - > - <button - v-if="hideTallStatus" - class="button-unstyled -link tall-status-hider" - :class="{ 'tall-status-hider_focused': focused }" - @click.prevent="toggleShowMore" - > - {{ $t("general.show_more") }} - </button> - <div - v-if="!hideSubjectStatus" - :class="{ 'single-line': singleLine }" - class="status-content media-body" - @click.prevent="linkClicked" - v-html="postBodyHtml" - /> - <button - v-if="hideSubjectStatus" - class="button-unstyled -link cw-status-hider" - @click.prevent="toggleShowMore" - > - {{ $t("status.show_content") }} - <FAIcon - v-if="attachmentTypes.includes('image')" - icon="image" - /> - <FAIcon - v-if="attachmentTypes.includes('video')" - icon="video" - /> - <FAIcon - v-if="attachmentTypes.includes('audio')" - icon="music" - /> - <FAIcon - v-if="attachmentTypes.includes('unknown')" - icon="file" + <div v-if="status.poll && status.poll.options && !compact"> + <Poll + :base-poll="status.poll" + :emoji="status.emojis" /> + </div> + + <div v-else-if="status.poll && status.poll.options && compact"> <FAIcon - v-if="status.poll && status.poll.options" icon="poll-h" + size="2x" /> - <FAIcon - v-if="status.card" - icon="link" - /> - </button> - <button - v-if="showingMore && !fullContent" - class="button-unstyled -link status-unhider" - @click.prevent="toggleShowMore" - > - {{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }} - </button> - </div> - - <div v-if="status.poll && status.poll.options && !hideSubjectStatus"> - <poll :base-poll="status.poll" /> - </div> + </div> - <div - v-if="status.attachments.length !== 0 && (!hideSubjectStatus || showingLongSubject)" - class="attachments media-body" - > - <attachment - v-for="attachment in nonGalleryAttachments" - :key="attachment.id" - class="non-gallery" - :size="attachmentSize" + <gallery + v-if="status.attachments.length !== 0" + class="attachments media-body" :nsfw="nsfwClickthrough" - :attachment="attachment" - :allow-play="true" - :set-media="setMedia()" + :attachments="status.attachments" + :limit="compact ? 1 : 0" + :size="attachmentSize" @play="$emit('mediaplay', attachment.id)" @pause="$emit('mediapause', attachment.id)" /> - <gallery - v-if="galleryAttachments.length > 0" - :nsfw="nsfwClickthrough" - :attachments="galleryAttachments" - :set-media="setMedia()" - /> - </div> - <div - v-if="status.card && !hideSubjectStatus && !noHeading" - class="link-preview media-body" - > - <link-preview - :card="status.card" - :size="attachmentSize" - :nsfw="nsfwClickthrough" - /> - </div> + <div + v-if="status.card && !noHeading && !compact" + class="link-preview media-body" + > + <link-preview + :card="status.card" + :size="attachmentSize" + :nsfw="nsfwClickthrough" + /> + </div> + </StatusBody> <slot name="footer" /> </div> - <!-- eslint-enable vue/no-v-html --> </template> <script src="./status_content.js" ></script> @@ -139,156 +59,5 @@ $status-margin: 0.75em; .StatusContent { flex: 1; min-width: 0; - - .status-content-wrapper { - display: flex; - flex-direction: column; - flex-wrap: nowrap; - } - - .tall-status { - position: relative; - height: 220px; - overflow-x: hidden; - overflow-y: hidden; - z-index: 1; - .status-content { - min-height: 0; - mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat, - linear-gradient(to top, white, white); - /* Autoprefixed seem to ignore this one, and also syntax is different */ - -webkit-mask-composite: xor; - mask-composite: exclude; - } - } - - .tall-status-hider { - display: inline-block; - word-break: break-all; - position: absolute; - height: 70px; - margin-top: 150px; - width: 100%; - text-align: center; - line-height: 110px; - z-index: 2; - } - - .status-unhider, .cw-status-hider { - width: 100%; - text-align: center; - display: inline-block; - word-break: break-all; - - svg { - color: inherit; - } - } - - img, video { - max-width: 100%; - max-height: 400px; - vertical-align: middle; - object-fit: contain; - - &.emoji { - width: 32px; - height: 32px; - } - } - - .summary-wrapper { - margin-bottom: 0.5em; - border-style: solid; - border-width: 0 0 1px 0; - border-color: var(--border, $fallback--border); - flex-grow: 0; - } - - .summary { - font-style: italic; - padding-bottom: 0.5em; - } - - .tall-subject { - position: relative; - .summary { - max-height: 2em; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - } - - .tall-subject-hider { - display: inline-block; - word-break: break-all; - // position: absolute; - width: 100%; - text-align: center; - padding-bottom: 0.5em; - } - - .status-content { - font-family: var(--postFont, sans-serif); - line-height: 1.4em; - white-space: pre-wrap; - overflow-wrap: break-word; - word-wrap: break-word; - word-break: break-word; - - blockquote { - margin: 0.2em 0 0.2em 2em; - font-style: italic; - } - - pre { - overflow: auto; - } - - code, samp, kbd, var, pre { - font-family: var(--postCodeFont, monospace); - } - - p { - margin: 0 0 1em 0; - } - - p:last-child { - margin: 0 0 0 0; - } - - h1 { - font-size: 1.1em; - line-height: 1.2em; - margin: 1.4em 0; - } - - h2 { - font-size: 1.1em; - margin: 1.0em 0; - } - - h3 { - font-size: 1em; - margin: 1.2em 0; - } - - h4 { - margin: 1.1em 0; - } - - &.single-line { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - height: 1.4em; - } - } -} - -.greentext { - color: $fallback--cGreen; - color: var(--postGreentext, $fallback--cGreen); } </style> diff --git a/src/components/status_popover/status_popover.vue b/src/components/status_popover/status_popover.vue @@ -5,12 +5,10 @@ :bound-to="{ x: 'container' }" @show="enter" > - <template slot="trigger"> + <template v-slot:trigger> <slot /> </template> - <div - slot="content" - > + <template v-slot:content> <Status v-if="status" :is-preview="true" @@ -33,7 +31,7 @@ size="2x" /> </div> - </div> + </template> </Popover> </template> diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js @@ -5,7 +5,9 @@ const StillImage = { 'mimetype', 'imageLoadError', 'imageLoadHandler', - 'alt' + 'alt', + 'height', + 'width' ], data () { return { @@ -15,6 +17,13 @@ const StillImage = { computed: { animated () { return this.stopGifs && (this.mimetype === 'image/gif' || this.src.endsWith('.gif')) + }, + style () { + const appendPx = (str) => /\d$/.test(str) ? str + 'px' : str + return { + height: this.height ? appendPx(this.height) : null, + width: this.width ? appendPx(this.width) : null + } } }, methods: { diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue @@ -2,6 +2,7 @@ <div class="still-image" :class="{ animated: animated }" + :style="style" > <canvas v-if="animated" @@ -30,7 +31,7 @@ position: relative; line-height: 0; overflow: hidden; - display: flex; + display: inline-flex; align-items: center; canvas { @@ -47,12 +48,13 @@ img { width: 100%; - min-height: 100%; + height: 100%; object-fit: contain; } &.animated { &::before { + zoom: var(--_still_image-label-scale, 1); content: 'gif'; position: absolute; line-height: 10px; diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js @@ -93,7 +93,9 @@ export default Vue.component('tab-switcher', { <button disabled={slot.data.attrs.disabled} onClick={this.clickTab(index)} - class={classesTab.join(' ')}> + class={classesTab.join(' ')} + type="button" + > <img src={slot.data.attrs.image} title={slot.data.attrs['image-tooltip']}/> {slot.data.attrs.label ? '' : slot.data.attrs.label} </button> diff --git a/src/components/timeago/timeago.vue b/src/components/timeago/timeago.vue @@ -9,6 +9,7 @@ <script> import * as DateUtils from 'src/services/date_utils/date_utils.js' +import localeService from 'src/services/locale/locale.service.js' export default { name: 'Timeago', @@ -21,9 +22,10 @@ export default { }, computed: { localeDateString () { + const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale) return typeof this.time === 'string' - ? new Date(Date.parse(this.time)).toLocaleString() - : this.time.toLocaleString() + ? new Date(Date.parse(this.time)).toLocaleString(browserLocale) + : this.time.toLocaleString(browserLocale) } }, created () { diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js @@ -2,27 +2,16 @@ 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 TimelineQuickSettings from './timeline_quick_settings.vue' import { debounce, throttle, keyBy } from 'lodash' import { library } from '@fortawesome/fontawesome-svg-core' -import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' +import { faCircleNotch, faCog } from '@fortawesome/free-solid-svg-icons' library.add( - faCircleNotch + faCircleNotch, + faCog ) -export const getExcludedStatusIdsByPinning = (statuses, pinnedStatusIds) => { - const ids = [] - if (pinnedStatusIds && pinnedStatusIds.length > 0) { - for (let status of statuses) { - if (!pinnedStatusIds.includes(status.id)) { - break - } - ids.push(status.id) - } - } - return ids -} - const Timeline = { props: [ 'timeline', @@ -47,7 +36,8 @@ const Timeline = { components: { Status, Conversation, - TimelineMenu + TimelineMenu, + TimelineQuickSettings }, computed: { newStatusCount () { @@ -74,11 +64,6 @@ const Timeline = { } }, // id map of statuses which need to be hidden in the main list due to pinning logic - excludedStatusIdsObject () { - const ids = getExcludedStatusIdsByPinning(this.timeline.visibleStatuses, this.pinnedStatusIds) - // Convert id array to object - return keyBy(ids) - }, pinnedStatusIdsObject () { return keyBy(this.pinnedStatusIds) }, diff --git a/src/components/timeline/timeline.scss b/src/components/timeline/timeline.scss @@ -0,0 +1,31 @@ +@import '../../_variables.scss'; + +.Timeline { + .loadmore-text { + opacity: 1; + } + + &.-blocked { + cursor: progress; + } + + .timeline-heading { + max-width: 100%; + flex-wrap: nowrap; + align-items: center; + position: relative; + + .loadmore-button { + flex-shrink: 0; + } + + .loadmore-text { + flex-shrink: 0; + line-height: 1em; + } + } + + .timeline-footer { + border: none; + } +} diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue @@ -16,6 +16,7 @@ > {{ $t('timeline.up_to_date') }} </div> + <TimelineQuickSettings v-if="!embedded" /> </div> <div :class="classes.body"> <div @@ -36,7 +37,7 @@ </template> <template v-for="status in timeline.visibleStatuses"> <conversation - v-if="!excludedStatusIdsObject[status.id]" + v-if="timelineName !== 'user' || (status.id >= timeline.minId && status.id <= timeline.maxId)" :key="status.id" class="status-fadein" :status-id="status.id" @@ -51,13 +52,13 @@ <div :class="classes.footer"> <div v-if="count===0" - class="new-status-notification text-center panel-footer faint" + class="new-status-notification text-center faint" > {{ $t('timeline.no_statuses') }} </div> <div v-else-if="bottomedOut" - class="new-status-notification text-center panel-footer faint" + class="new-status-notification text-center faint" > {{ $t('timeline.no_more_statuses') }} </div> @@ -66,13 +67,13 @@ class="button-unstyled -link -fullwidth" @click.prevent="fetchOlderStatuses()" > - <div class="new-status-notification text-center panel-footer"> + <div class="new-status-notification text-center"> {{ $t('timeline.load_older') }} </div> </button> <div v-else - class="new-status-notification text-center panel-footer" + class="new-status-notification text-center" > <FAIcon icon="circle-notch" @@ -86,29 +87,4 @@ <script src="./timeline.js"></script> -<style lang="scss"> -@import '../../_variables.scss'; - -.Timeline { - .loadmore-text { - opacity: 1; - } - - &.-blocked { - cursor: progress; - } -} - -.timeline-heading { - max-width: 100%; - flex-wrap: nowrap; - align-items: center; - .loadmore-button { - flex-shrink: 0; - } - .loadmore-text { - flex-shrink: 0; - line-height: 1em; - } -} -</style> +<style src="./timeline.scss" lang="scss"> </style> diff --git a/src/components/timeline/timeline_quick_settings.js b/src/components/timeline/timeline_quick_settings.js @@ -0,0 +1,60 @@ +import Popover from '../popover/popover.vue' +import { mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faFilter, faFont, faWrench } from '@fortawesome/free-solid-svg-icons' + +library.add( + faFilter, + faFont, + faWrench +) + +const TimelineQuickSettings = { + components: { + Popover + }, + methods: { + setReplyVisibility (visibility) { + this.$store.dispatch('setOption', { name: 'replyVisibility', value: visibility }) + this.$store.dispatch('queueFlushAll') + }, + openTab (tab) { + this.$store.dispatch('openSettingsModalTab', tab) + } + }, + computed: { + ...mapGetters(['mergedConfig']), + loggedIn () { + return !!this.$store.state.users.currentUser + }, + replyVisibilitySelf: { + get () { return this.mergedConfig.replyVisibility === 'self' }, + set () { this.setReplyVisibility('self') } + }, + replyVisibilityFollowing: { + get () { return this.mergedConfig.replyVisibility === 'following' }, + set () { this.setReplyVisibility('following') } + }, + replyVisibilityAll: { + get () { return this.mergedConfig.replyVisibility === 'all' }, + set () { this.setReplyVisibility('all') } + }, + hideMedia: { + get () { return this.mergedConfig.hideAttachments || this.mergedConfig.hideAttachmentsInConv }, + set () { + const value = !this.hideMedia + this.$store.dispatch('setOption', { name: 'hideAttachments', value }) + this.$store.dispatch('setOption', { name: 'hideAttachmentsInConv', value }) + } + }, + hideMutedPosts: { + get () { return this.mergedConfig.hideFilteredStatuses }, + set () { + const value = !this.hideMutedPosts + this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value }) + } + } + } +} + +export default TimelineQuickSettings diff --git a/src/components/timeline/timeline_quick_settings.vue b/src/components/timeline/timeline_quick_settings.vue @@ -0,0 +1,102 @@ +<template> + <Popover + trigger="click" + class="TimelineQuickSettings" + :bound-to="{ x: 'container' }" + > + <template v-slot:content> + <div class="dropdown-menu"> + <div v-if="loggedIn"> + <button + class="button-default dropdown-item" + @click="replyVisibilityAll = true" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-radio': replyVisibilityAll }" + />{{ $t('settings.reply_visibility_all') }} + </button> + <button + class="button-default dropdown-item" + @click="replyVisibilityFollowing = true" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-radio': replyVisibilityFollowing }" + />{{ $t('settings.reply_visibility_following_short') }} + </button> + <button + class="button-default dropdown-item" + @click="replyVisibilitySelf = true" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-radio': replyVisibilitySelf }" + />{{ $t('settings.reply_visibility_self_short') }} + </button> + <div + role="separator" + class="dropdown-divider" + /> + </div> + <button + class="button-default dropdown-item" + @click="hideMedia = !hideMedia" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': hideMedia }" + />{{ $t('settings.hide_media_previews') }} + </button> + <button + class="button-default dropdown-item" + @click="hideMutedPosts = !hideMutedPosts" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': hideMutedPosts }" + />{{ $t('settings.hide_all_muted_posts') }} + </button> + <button + class="button-default dropdown-item dropdown-item-icon" + @click="openTab('filtering')" + > + <FAIcon icon="font" />{{ $t('settings.word_filter') }} + </button> + <button + class="button-default dropdown-item dropdown-item-icon" + @click="openTab('general')" + > + <FAIcon icon="wrench" />{{ $t('settings.more_settings') }} + </button> + </div> + </template> + <template v-slot:trigger> + <button class="button-unstyled"> + <FAIcon icon="filter" /> + </button> + </template> + </Popover> +</template> + +<script src="./timeline_quick_settings.js"></script> + +<style lang="scss"> + +.TimelineQuickSettings { + align-self: stretch; + + > button { + font-size: 1.2em; + padding-left: 0.7em; + padding-right: 0.2em; + line-height: 100%; + height: 100%; + } + + .dropdown-item { + margin: 0; + } +} + +</style> diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js @@ -1,29 +1,17 @@ import Popover from '../popover/popover.vue' -import { mapState } from 'vuex' +import TimelineMenuContent from './timeline_menu_content.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { - faUsers, - faGlobe, - faBookmark, - faEnvelope, - faHome, faChevronDown } from '@fortawesome/free-solid-svg-icons' -library.add( - faUsers, - faGlobe, - faBookmark, - faEnvelope, - faHome, - faChevronDown -) +library.add(faChevronDown) // Route -> i18n key mapping, exported and not in the computed // because nav panel benefits from the same information. export const timelineNames = () => { return { - 'friends': 'nav.timeline', + 'friends': 'nav.home_timeline', 'bookmarks': 'nav.bookmarks', 'dms': 'nav.dms', 'public-timeline': 'nav.public_tl', @@ -33,7 +21,8 @@ export const timelineNames = () => { const TimelineMenu = { components: { - Popover + Popover, + TimelineMenuContent }, data () { return { @@ -41,9 +30,6 @@ const TimelineMenu = { } }, created () { - if (this.currentUser && this.currentUser.locked) { - this.$store.dispatch('startFetchingFollowRequests') - } if (timelineNames()[this.$route.name]) { this.$store.dispatch('setLastTimeline', this.$route.name) } @@ -75,13 +61,6 @@ const TimelineMenu = { 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 - }) } } diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue @@ -9,74 +9,26 @@ @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' }"> - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="home" - />{{ $t("nav.timeline") }} - </router-link> - </li> - <li v-if="currentUser"> - <router-link :to="{ name: 'bookmarks'}"> - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="bookmark" - />{{ $t("nav.bookmarks") }} - </router-link> - </li> - <li v-if="currentUser"> - <router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }"> - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="envelope" - />{{ $t("nav.dms") }} - </router-link> - </li> - <li v-if="currentUser || !privateMode"> - <router-link :to="{ name: 'public-timeline' }"> - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="users" - />{{ $t("nav.public_tl") }} - </router-link> - </li> - <li v-if="federating && (currentUser || !privateMode)"> - <router-link :to="{ name: 'public-external-timeline' }"> - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="globe" - />{{ $t("nav.twkn") }} - </router-link> - </li> - </ul> - </div> - <div - slot="trigger" - class="title timeline-menu-title" - > - <span class="timeline-title">{{ timelineName() }}</span> - <span> - <FAIcon - size="sm" - icon="chevron-down" + <template v-slot:content> + <div class="timeline-menu-popover popover-default"> + <TimelineMenuContent /> + </div> + </template> + <template v-slot:trigger> + <button class="button-unstyled title timeline-menu-title"> + <span class="timeline-title">{{ timelineName() }}</span> + <span> + <FAIcon + size="sm" + icon="chevron-down" + /> + </span> + <span + class="click-blocker" + @click="blockOpen" /> - </span> - <span - class="click-blocker" - @click="blockOpen" - /> - </div> + </button> + </template> </Popover> </template> diff --git a/src/components/timeline_menu/timeline_menu_content.js b/src/components/timeline_menu/timeline_menu_content.js @@ -0,0 +1,29 @@ +import { mapState } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faHome +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faHome +) + +const TimelineMenuContent = { + computed: { + ...mapState({ + currentUser: state => state.users.currentUser, + privateMode: state => state.instance.private, + federating: state => state.instance.federating + }) + } +} + +export default TimelineMenuContent diff --git a/src/components/timeline_menu/timeline_menu_content.vue b/src/components/timeline_menu/timeline_menu_content.vue @@ -0,0 +1,66 @@ +<template> + <ul> + <li v-if="currentUser"> + <router-link + class="menu-item" + :to="{ name: 'friends' }" + > + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="home" + />{{ $t("nav.home_timeline") }} + </router-link> + </li> + <li v-if="currentUser || !privateMode"> + <router-link + class="menu-item" + :to="{ name: 'public-timeline' }" + > + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="users" + />{{ $t("nav.public_tl") }} + </router-link> + </li> + <li v-if="federating && (currentUser || !privateMode)"> + <router-link + class="menu-item" + :to="{ name: 'public-external-timeline' }" + > + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="globe" + />{{ $t("nav.twkn") }} + </router-link> + </li> + <li v-if="currentUser"> + <router-link + class="menu-item" + :to="{ name: 'bookmarks'}" + > + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="bookmark" + />{{ $t("nav.bookmarks") }} + </router-link> + </li> + <li v-if="currentUser"> + <router-link + class="menu-item" + :to="{ name: 'dms', params: { username: currentUser.screen_name } }" + > + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="envelope" + />{{ $t("nav.dms") }} + </router-link> + </li> + </ul> +</template> + +<script src="./timeline_menu_content.js" ></script> diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue @@ -2,8 +2,8 @@ <StillImage v-if="user" class="Avatar" - :alt="user.screen_name" - :title="user.screen_name" + :alt="user.screen_name_ui" + :title="user.screen_name_ui" :src="imgSrc(user.profile_image_url_original)" :class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }" :image-load-error="imageLoadError" diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js @@ -4,23 +4,25 @@ import ProgressButton from '../progress_button/progress_button.vue' import FollowButton from '../follow_button/follow_button.vue' import ModerationTools from '../moderation_tools/moderation_tools.vue' import AccountActions from '../account_actions/account_actions.vue' +import Select from '../select/select.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import { mapGetters } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' import { faBell, faRss, - faChevronDown, faSearchPlus, - faExternalLinkAlt + faExternalLinkAlt, + faEdit } from '@fortawesome/free-solid-svg-icons' library.add( faRss, faBell, - faChevronDown, faSearchPlus, - faExternalLinkAlt + faExternalLinkAlt, + faEdit ) export default { @@ -118,7 +120,9 @@ export default { ModerationTools, AccountActions, ProgressButton, - FollowButton + FollowButton, + Select, + RichContent }, methods: { muteUser () { @@ -153,13 +157,16 @@ export default { this.$store.state.instance.restrictedNicknames ) }, + openProfileTab () { + this.$store.dispatch('openSettingsModalTab', 'profile') + }, zoomAvatar () { const attachment = { url: this.user.profile_image_url_original, mimetype: 'image' } this.$store.dispatch('setMedia', [attachment]) - this.$store.dispatch('setCurrent', attachment) + this.$store.dispatch('setCurrentMedia', attachment) }, mentionUser () { this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user }) diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue @@ -38,26 +38,29 @@ </router-link> <div class="user-summary"> <div class="top-line"> - <!-- eslint-disable vue/no-v-html --> - <div - v-if="user.name_html" + <RichContent :title="user.name" class="user-name" - v-html="user.name_html" + :html="user.name" + :emoji="user.emoji" /> - <!-- eslint-enable vue/no-v-html --> - <div - v-else - :title="user.name" - class="user-name" + <button + v-if="!isOtherUser && user.is_local" + class="button-unstyled edit-profile-button" + @click.stop="openProfileTab" > - {{ user.name }} - </div> + <FAIcon + fixed-width + class="icon" + icon="edit" + :title="$t('user_card.edit_profile')" + /> + </button> <a v-if="isOtherUser && !user.is_local" :href="user.statusnet_profile_url" target="_blank" - class="external-link-button" + class="button-unstyled external-link-button" > <FAIcon class="icon" @@ -73,23 +76,29 @@ <div class="bottom-line"> <router-link class="user-screen-name" - :title="user.screen_name" + :title="user.screen_name_ui" :to="userProfileLink(user)" > - @{{ user.screen_name }} + @{{ user.screen_name_ui }} </router-link> <template v-if="!hideBio"> <span + v-if="user.deactivated" + class="alert user-role" + > + {{ $t('user_card.deactivated') }} + </span> + <span v-if="!!visibleRole" class="alert user-role" > - {{ $t(`user_card.roles.${visibleRole}`) }} + {{ $t(`general.role.${visibleRole}`) }} </span> <span v-if="user.bot" class="alert user-role" > - bot + {{ $t('user_card.bot') }} </span> </template> <span v-if="user.locked"> @@ -132,25 +141,24 @@ class="userHighlightCl" type="color" > - <label - for="theme_tab" - class="userHighlightSel select" + <Select + :id="'userHighlightSel'+user.id" + v-model="userHighlightType" + class="userHighlightSel" > - <select - :id="'userHighlightSel'+user.id" - v-model="userHighlightType" - class="userHighlightSel" - > - <option value="disabled">No highlight</option> - <option value="solid">Solid bg</option> - <option value="striped">Striped bg</option> - <option value="side">Side stripe</option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> + <option value="disabled"> + {{ $t('user_card.highlight.disabled') }} + </option> + <option value="solid"> + {{ $t('user_card.highlight.solid') }} + </option> + <option value="striped"> + {{ $t('user_card.highlight.striped') }} + </option> + <option value="side"> + {{ $t('user_card.highlight.side') }} + </option> + </Select> </div> </div> <div @@ -158,7 +166,10 @@ class="user-interactions" > <div class="btn-group"> - <FollowButton :relationship="relationship" /> + <FollowButton + :relationship="relationship" + :user="user" + /> <template v-if="relationship.following"> <ProgressButton v-if="!relationship.subscribing" @@ -193,6 +204,7 @@ <button v-if="relationship.muting" class="btn button-default btn-block toggled" + :disabled="user.deactivated" @click="unmuteUser" > {{ $t('user_card.muted') }} @@ -200,6 +212,7 @@ <button v-else class="btn button-default btn-block" + :disabled="user.deactivated" @click="muteUser" > {{ $t('user_card.mute') }} @@ -208,6 +221,7 @@ <div> <button class="btn button-default btn-block" + :disabled="user.deactivated" @click="mentionUser" > {{ $t('user_card.mention') }} @@ -256,20 +270,13 @@ <span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span> </div> </div> - <!-- eslint-disable vue/no-v-html --> - <p - v-if="!hideBio && user.description_html" + <RichContent + v-if="!hideBio" class="user-card-bio" - @click.prevent="linkClicked" - v-html="user.description_html" + :html="user.description_html" + :emoji="user.emoji" + :handle-links="true" /> - <!-- eslint-enable vue/no-v-html --> - <p - v-else-if="!hideBio" - class="user-card-bio" - > - {{ user.description }} - </p> </div> </div> </template> @@ -282,9 +289,10 @@ .user-card { position: relative; - &:hover .Avatar { + &:hover { --_still-image-img-visibility: visible; --_still-image-canvas-visibility: hidden; + --_still-image-label-visibility: hidden; } .panel-heading { @@ -328,12 +336,12 @@ } } - p { - margin-bottom: 0; - } - &-bio { text-align: center; + display: block; + line-height: 18px; + padding: 1em; + margin: 0; a { color: $fallback--link; @@ -345,11 +353,6 @@ vertical-align: middle; max-width: 100%; max-height: 400px; - - &.emoji { - width: 32px; - height: 32px; - } } } @@ -427,7 +430,7 @@ } } - .external-link-button { + .external-link-button, .edit-profile-button { cursor: pointer; width: 2.5em; text-align: center; @@ -451,13 +454,6 @@ // big one z-index: 1; - img { - width: 26px; - height: 26px; - vertical-align: middle; - object-fit: contain - } - .top-line { display: flex; } @@ -470,12 +466,7 @@ margin-right: 1em; font-size: 15px; - img { - object-fit: contain; - height: 16px; - width: 16px; - vertical-align: middle; - } + --emoji-size: 14px; } .bottom-line { @@ -541,15 +532,11 @@ flex: 1 0 auto; } - .userHighlightSel, - .userHighlightSel.select { + .userHighlightSel { padding-top: 0; padding-bottom: 0; flex: 1 0 auto; } - .userHighlightSel.select svg { - line-height: 22px; - } .userHighlightText { width: 70px; @@ -558,9 +545,7 @@ .userHighlightCl, .userHighlightText, - .userHighlightSel, - .userHighlightSel.select { - height: 22px; + .userHighlightSel { vertical-align: top; margin-right: .5em; margin-bottom: .25em; @@ -585,6 +570,10 @@ } } +.sidebar .edit-profile-button { + display: none; +} + .user-counts { display: flex; line-height:16px; diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue @@ -4,40 +4,44 @@ placement="top" :offset="{ y: 5 }" > - <template slot="trigger"> + <template v-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> + <template v-slot:content> + <div class="user-list-popover"> + <template 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 --> + <RichContent + class="username" + :title="'@'+user.screen_name_ui" + :html="user.name_html" + :emoji="user.emoji" + /> + <!-- eslint-enable vue/no-v-html --> + <span class="user-list-screen-name">{{ user.screen_name_ui }}</span> + </div> </div> - </div> - </div> - <div v-else> - <FAIcon - icon="circle-notch" - spin - size="3x" - /> + </template> + <template v-else> + <FAIcon + icon="circle-notch" + spin + size="3x" + /> + </template> </div> - </div> + </template> </Popover> </template> @@ -49,6 +53,8 @@ .user-list-popover { padding: 0.5em; + --emoji-size: 16px; + .user-list-row { padding: 0.25em; display: flex; diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js @@ -4,6 +4,7 @@ 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 RichContent from 'src/components/rich_content/rich_content.jsx' import List from '../list/list.vue' import withLoadMore from '../../hocs/with_load_more/with_load_more' import { library } from '@fortawesome/fontawesome-svg-core' @@ -164,7 +165,8 @@ const UserProfile = { FriendList, FollowCard, TabSwitcher, - Conversation + Conversation, + RichContent } } diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue @@ -20,20 +20,24 @@ :key="index" class="user-profile-field" > - <!-- eslint-disable vue/no-v-html --> <dt :title="user.fields_text[index].name" class="user-profile-field-name" - @click.prevent="linkClicked" - v-html="field.name" - /> + > + <RichContent + :html="field.name" + :emoji="user.emoji" + /> + </dt> <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 --> + > + <RichContent + :html="field.value" + :emoji="user.emoji" + /> + </dd> </dl> </div> <tab-switcher @@ -60,10 +64,7 @@ :disabled="!user.friends_count" > <FriendList :user-id="userId"> - <template - slot="item" - slot-scope="{item}" - > + <template v-slot:item="{item}"> <FollowCard :user="item" /> </template> </FriendList> @@ -75,10 +76,7 @@ :disabled="!user.followers_count" > <FollowerList :user-id="userId"> - <template - slot="item" - slot-scope="{item}" - > + <template v-slot:item="{item}"> <FollowCard :user="item" :no-follows-you="isUs" diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue @@ -6,7 +6,7 @@ <div class="user-reporting-panel panel"> <div class="panel-heading"> <div class="title"> - {{ $t('user_reporting.title', [user.screen_name]) }} + {{ $t('user_reporting.title', [user.screen_name_ui]) }} </div> </div> <div class="panel-body"> @@ -45,10 +45,7 @@ </div> <div class="user-reporting-panel-right"> <List :items="statuses"> - <template - slot="item" - slot-scope="{item}" - > + <template v-slot:item="{item}"> <div class="status-fadein user-reporting-panel-sitem"> <Status :in-conversation="false" diff --git a/src/i18n/ca.json b/src/i18n/ca.json @@ -10,11 +10,12 @@ "text_limit": "Límit de text", "title": "Funcionalitats", "who_to_follow": "A qui seguir", - "pleroma_chat_messages": "Xat de Pleroma" + "pleroma_chat_messages": "Xat de Pleroma", + "upload_limit": "Límit de càrrega" }, "finder": { "error_fetching_user": "No s'ha pogut carregar l'usuari/a", - "find_user": "Find user" + "find_user": "Trobar usuari" }, "general": { "apply": "Aplica", @@ -32,7 +33,16 @@ "error_retry": "Si us plau, prova de nou", "generic_error": "Hi ha hagut un error", "loading": "Carregant…", - "more": "Més" + "more": "Més", + "flash_content": "Fes clic per mostrar el contingut Flash utilitzant Ruffle (experimental, pot no funcionar).", + "flash_security": "Tingues en compte que això pot ser potencialment perillós, ja que el contingut Flash encara és un codi arbitrari.", + "flash_fail": "No s'ha pogut carregar el contingut del flaix, consulta la consola per als detalls.", + "role": { + "moderator": "Moderador/a", + "admin": "Administrador/a" + }, + "dismiss": "Descartar", + "peek": "Donar un cop d'ull" }, "login": { "login": "Inicia sessió", @@ -45,15 +55,20 @@ "enter_recovery_code": "Posa un codi de recuperació", "authentication_code": "Codi d'autenticació", "hint": "Entra per participar a la conversa", - "description": "Entra amb OAuth" + "description": "Entra amb OAuth", + "heading": { + "totp": "Autenticació de dos factors", + "recovery": "Recuperació de dos factors" + }, + "enter_two_factor_code": "Introdueix un codi de dos factors" }, "nav": { "chat": "Xat local públic", - "friend_requests": "Soŀlicituds de connexió", + "friend_requests": "Sol·licituds de seguiment", "mentions": "Mencions", - "public_tl": "Flux públic del node", + "public_tl": "Línia temporal pública", "timeline": "Flux personal", - "twkn": "Flux de la xarxa coneguda", + "twkn": "Xarxa coneguda", "chats": "Xats", "timelines": "Línies de temps", "preferences": "Preferències", @@ -62,19 +77,25 @@ "dms": "Missatges directes", "interactions": "Interaccions", "back": "Enrere", - "administration": "Administració" + "administration": "Administració", + "about": "Quant a", + "bookmarks": "Marcadors", + "user_search": "Cerca d'usuaris", + "home_timeline": "Línea temporal personal" }, "notifications": { - "broken_favorite": "No es coneix aquest estat. S'està cercant.", + "broken_favorite": "Publicació desconeguda, s'està cercant…", "favorited_you": "ha marcat un estat teu", "followed_you": "ha començat a seguir-te", "load_older": "Carrega més notificacions", "notifications": "Notificacions", - "read": "Read!", + "read": "Llegit!", "repeated_you": "ha repetit el teu estat", "migrated_to": "migrat a", "no_more_notifications": "No més notificacions", - "follow_request": "et vol seguir" + "follow_request": "et vol seguir", + "reacted_with": "ha reaccionat amb {0}", + "error": "Error obtenint notificacions: {0}" }, "post_status": { "account_not_locked_warning": "El teu compte no està {0}. Qualsevol persona pot seguir-te per llegir les teves entrades reservades només a seguidores.", @@ -83,24 +104,33 @@ "content_type": { "text/plain": "Text pla", "text/markdown": "Markdown", - "text/html": "HTML" + "text/html": "HTML", + "text/bbcode": "BBCode" }, "content_warning": "Assumpte (opcional)", - "default": "Em sento…", + "default": "Acabe d'aterrar a L.A.", "direct_warning": "Aquesta entrada només serà visible per les usuràries que etiquetis", "posting": "Publicació", "scope": { - "direct": "Directa - Publica només per les usuàries etiquetades", - "private": "Només seguidors/es - Publica només per comptes que et segueixin", - "public": "Pública - Publica als fluxos públics", - "unlisted": "Silenciosa - No la mostris en fluxos públics" + "direct": "Directa - publica només per als usuaris etiquetats", + "private": "Només seguidors/es - publica només per comptes que et segueixin", + "public": "Pública - publica als fluxos públics", + "unlisted": "Silenciosa - no la mostris en fluxos públics" }, "scope_notice": { "private": "Aquesta entrada serà visible només per a qui et segueixi", - "public": "Aquesta entrada serà visible per a tothom" + "public": "Aquesta entrada serà visible per a tothom", + "unlisted": "Aquesta entrada no es veurà ni a la Línia de temps local ni a la Línia de temps federada" }, "preview_empty": "Buida", - "preview": "Vista prèvia" + "preview": "Vista prèvia", + "direct_warning_to_first_only": "Aquesta publicació només serà visible per als usuaris mencionats al principi del missatge.", + "empty_status_error": "No es pot publicar un estat buit sense fitxers adjunts", + "media_description": "Descripció multimèdia", + "direct_warning_to_all": "Aquesta publicació serà visible per a tots els usuaris mencionats.", + "new_status": "Publicar un nou estat", + "post": "Publicació", + "media_description_error": "Ha fallat la pujada del contingut. Prova de nou" }, "registration": { "bio": "Presentació", @@ -118,13 +148,19 @@ "username_required": "no es pot deixar en blanc" }, "fullname_placeholder": "p. ex. Lain Iwakura", - "username_placeholder": "p. ex. lain" + "username_placeholder": "p. ex. lain", + "captcha": "CAPTCHA", + "register": "Registrar-se", + "reason": "Raó per a registrar-se", + "bio_placeholder": "p.e.\nHola, sóc la Lain.\nSóc una noia anime que viu a un suburbi de Japó. Potser em coneixes per Wired.", + "reason_placeholder": "Aquesta instància aprova els registres manualment.\nExplica a l'administració per què vols registrar-te.", + "new_captcha": "Clica a la imatge per obtenir un nou captcha" }, "settings": { "attachmentRadius": "Adjunts", "attachments": "Adjunts", "avatar": "Avatar", - "avatarAltRadius": "Avatars en les notificacions", + "avatarAltRadius": "Avatars (notificacions)", "avatarRadius": "Avatars", "background": "Fons de pantalla", "bio": "Presentació", @@ -134,8 +170,8 @@ "cOrange": "Taronja (marca com a preferit)", "cRed": "Vermell (canceŀla)", "change_password": "Canvia la contrasenya", - "change_password_error": "No s'ha pogut canviar la contrasenya", - "changed_password": "S'ha canviat la contrasenya", + "change_password_error": "No s'ha pogut canviar la contrasenya.", + "changed_password": "S'ha canviat la contrasenya correctament!", "collapse_subject": "Replega les entrades amb títol", "confirm_new_password": "Confirma la nova contrasenya", "current_avatar": "L'avatar actual", @@ -176,7 +212,7 @@ "new_password": "Contrasenya nova", "notification_visibility": "Notifica'm quan algú", "notification_visibility_follows": "Comença a seguir-me", - "notification_visibility_likes": "Marca com a preferida una entrada meva", + "notification_visibility_likes": "Favorits", "notification_visibility_mentions": "Em menciona", "notification_visibility_repeats": "Republica una entrada meva", "no_rich_text_description": "Neteja el formatat de text de totes les entrades", @@ -193,7 +229,7 @@ "profile_banner": "Fons de perfil", "profile_tab": "Perfil", "radii_help": "Configura l'arrodoniment de les vores (en píxels)", - "replies_in_timeline": "Replies in timeline", + "replies_in_timeline": "Respostes al flux", "reply_visibility_all": "Mostra totes les respostes", "reply_visibility_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", @@ -216,7 +252,7 @@ "true": "sí" }, "show_moderator_badge": "Mostra una insígnia de Moderació en el meu perfil", - "show_admin_badge": "Mostra una insígnia d'Administració en el meu perfil", + "show_admin_badge": "Mostra una insígnia \"d'Administració\" en el meu perfil", "hide_followers_description": "No mostris qui m'està seguint", "hide_follows_description": "No mostris a qui segueixo", "notification_visibility_emoji_reactions": "Reaccions", @@ -254,25 +290,270 @@ "allow_following_move": "Permet el seguiment automàtic quan un compte a qui seguim es mou", "mfa": { "scan": { - "secret_code": "Clau" + "secret_code": "Clau", + "title": "Escanejar", + "desc": "S'està usant l'aplicació two-factor, escaneja aquest codi QR o introdueix la clau de text:" }, "authentication_methods": "Mètodes d'autenticació", "waiting_a_recovery_codes": "Rebent còpies de seguretat dels codis…", "recovery_codes": "Codis de recuperació.", "warning_of_generate_new_codes": "Quan generes nous codis de recuperació, els antics ja no funcionaran més.", - "generate_new_recovery_codes": "Genera nous codis de recuperació" + "generate_new_recovery_codes": "Genera nous codis de recuperació", + "otp": "OTP", + "confirm_and_enable": "Confirmar i habilitar OTP", + "recovery_codes_warning": "Anote els codis o guarda'ls en un lloc segur, o no els veuràs una altra volta. Si perds l'accés a la teua aplicació 2FA i els codis de recuperació, no podràs accedir al compte.", + "title": "Autenticació de dos factors", + "setup_otp": "Configurar OTP", + "wait_pre_setup_otp": "preconfiguració OTP", + "verify": { + "desc": "Per habilitar l'autenticació two-factor, introdueix el codi des de la teva aplicació two-factor:" + } }, "enter_current_password_to_confirm": "Posar la contrasenya actual per confirmar la teva identitat", "security": "Seguretat", - "app_name": "Nom de l'aplicació" + "app_name": "Nom de l'aplicació", + "subject_line_mastodon": "Com a mastodon: copiar com és", + "mute_export_button": "Exportar silenciats a un fitxer csv", + "mute_import_error": "Error al importar silenciats", + "mutes_imported": "Silenciats importats! Processar-los portarà una estona.", + "import_mutes_from_a_csv_file": "Importar silenciats des d'un fitxer csv", + "word_filter": "Filtre de paraules", + "hide_media_previews": "Ocultar les vistes prèvies multimèdia", + "hide_filtered_statuses": "Amagar estats filtrats", + "play_videos_in_modal": "Reproduir vídeos en un marc emergent", + "file_export_import": { + "errors": { + "invalid_file": "El fitxer seleccionat no és vàlid com a còpia de seguretat de la configuració. No s'ha realitzat cap canvi.", + "file_too_new": "Versió important incompatible: {fileMajor}, aquest PleromaFE (configuració versió {feMajor}) és massa antiga per gestionar-lo", + "file_too_old": "Versió important incompatible: {fileMajor}, la versió del fitxer és massa antiga i no està implementada (s'ha establert un mínim ver. {feMajor})", + "file_slightly_new": "La versió menor del fitxer és diferent, alguns paràmetres podrien no carregar-se" + }, + "backup_settings": "Còpia de seguretat de la configuració a un fitxer", + "backup_settings_theme": "Còpia de seguretat de la configuració i tema a un fitxer", + "restore_settings": "Restaurar configuració des d'un fitxer", + "backup_restore": "Còpia de seguretat de la configuració" + }, + "user_mutes": "Usuaris", + "subject_line_email": "Com a l'email: \"re: tema\"", + "search_user_to_block": "Busca a qui vols bloquejar", + "save": "Guardar els canvis", + "use_contain_fit": "No retallar els adjunts en miniatures", + "reset_profile_background": "Restablir fons del perfil", + "reset_profile_banner": "Restablir banner del perfil", + "emoji_reactions_on_timeline": "Mostrar reaccions emoji al flux", + "max_thumbnails": "Quantitat màxima de miniatures per publicació", + "hide_user_stats": "Amagar les estadístiques de l'usuari (p. ex. el nombre de seguidors)", + "reset_banner_confirm": "Realment vols restablir el banner?", + "reset_background_confirm": "Realment vols restablir el fons del perfil?", + "subject_input_always_show": "Sempre mostrar el camp del tema", + "subject_line_noop": "No copiar", + "subject_line_behavior": "Copiar el tema a les respostes", + "search_user_to_mute": "Busca a qui vols silenciar", + "mute_export": "Exportar silenciats", + "scope_copy": "Copiar visibilitat quan contestes (En els missatges directes sempre es copia)", + "reset_avatar": "Restablir avatar", + "right_sidebar": "Mostrar barra lateral a la dreta", + "no_blocks": "No hi han bloquejats", + "no_mutes": "No hi han silenciats", + "hide_follows_count_description": "No mostrar el nombre de comptes que segueixo", + "mute_import": "Importar silenciats", + "hide_all_muted_posts": "Ocultar publicacions silenciades", + "hide_wallpaper": "Amagar el fons de la instància", + "notification_visibility_moves": "Usuari Migrat", + "reply_visibility_following_short": "Mostrar respostes als meus seguidors", + "reply_visibility_self_short": "Mostrar respostes només a un mateix", + "autohide_floating_post_button": "Ocultar automàticament el botó 'Nova Publicació' (mòbil)", + "minimal_scopes_mode": "Minimitzar les opcions de visibilitat de la publicació", + "sensitive_by_default": "Marcar publicacions com a sensibles per defecte", + "useStreamingApi": "Rebre publicacions i notificacions en temps real", + "hide_isp": "Ocultar el panell especific de la instància", + "preload_images": "Precarregar les imatges", + "setting_changed": "La configuració és diferent a la predeterminada", + "hide_followers_count_description": "No mostrar el nombre de seguidors", + "reset_avatar_confirm": "Realment vols restablir l'avatar?", + "accent": "Accent", + "useStreamingApiWarning": "(No recomanat, experimental, pot ometre publicacions)", + "style": { + "fonts": { + "family": "Nom de la font", + "size": "Mida (en píxels)", + "custom": "Personalitza", + "_tab_label": "Fonts", + "help": "Selecciona la font per als elements de la interfície. Per a \"personalitzat\" deus escriure el nom de la font exactament com apareix al sistema.", + "components": { + "post": "Text de les publicacions", + "postCode": "Text monoespai en publicació (text enriquit)", + "input": "Camps d'entrada", + "interface": "Interfície" + }, + "weight": "Pes (negreta)" + }, + "preview": { + "input": "Acabo d'aterrar a Los Angeles.", + "button": "Botó", + "mono": "contingut", + "content": "Contingut", + "header": "Previsualització", + "header_faint": "Això està bé", + "error": "Exemple d'error", + "faint_link": "Manual d'ajuda", + "checkbox": "He llegit els termes i condicions", + "link": "un bonic enllaç", + "fine_print": "Llegiu el nostre {0} per no aprendre res útil!", + "text": "Un grapat més de {0} i {1}" + }, + "shadows": { + "spread": "Difon", + "filter_hint": { + "drop_shadow_syntax": "{0} no suporta el paràmetre {1} i la paraula clau {2}.", + "avatar_inset": "Tingues en compte que combinar ombres interiors i no interiors als avatars podria donar resultats inesperats amb avatars transparents.", + "inset_classic": "Les ombres interiors estaran usant {0}", + "always_drop_shadow": "Advertència, aquesta ombra sempre utilitza {0} quan el navegador ho suporta.", + "spread_zero": "Ombres amb propagació > 0 apareixeran com si estigueren posades a zero" + }, + "components": { + "popup": "Texts i finestres emergents (popups & tooltips)", + "panel": "Panell", + "panelHeader": "Capçalera del panell", + "avatar": "Avatar de l'usuari (en vista de perfil)", + "input": "Camp d'entrada", + "buttonHover": "Botó (surant)", + "buttonPressed": "Botó (pressionat)", + "topBar": "Barra superior", + "buttonPressedHover": "Botó (surant i pressionat)", + "avatarStatus": "Avatar de l'usuari (en vista de publicació)", + "button": "Botó" + }, + "hintV3": "per a les ombres també pots usar la notació {0} per a utilitzar un altre espai de color.", + "blur": "Difuminat", + "component": "Component", + "override": "Sobreescriure", + "shadow_id": "Ombra #{value}", + "_tab_label": "Ombra i il·luminació", + "inset": "Ombra interior" + }, + "switcher": { + "use_snapshot": "Versió antiga", + "help": { + "future_version_imported": "El fitxer importat es va crear per a una versió del front-end més recent.", + "migration_snapshot_ok": "Per a estar segurs, s'ha carregat la instantània del tema. Pots intentar carregar les dades del tema.", + "migration_napshot_gone": "Per alguna raó, faltava la instantània, algunes coses podrien veure's diferents del que recordes.", + "snapshot_source_mismatch": "Conflicte de versions: probablement el front-end s'ha revertit i actualitzat una altra volta, si has canviat el tema en una versió anterior, segurament vols utilitzar la versió antiga; d'altra banda utilitza la nova versió.", + "v2_imported": "El fitxer que has importat va ser creat per a un front-end més antic. Intentem maximitzar la compatibilitat, però podrien haver inconsistències.", + "fe_upgraded": "El motor de temes de PleromaFE es va actualitzar després de l'actualització de la versió.", + "snapshot_missing": "No hi havia cap instantània del tema al fitxer, per tant podria veure's diferent del previst originalment.", + "upgraded_from_v2": "PleromaFE s'ha actualitzat, el tema pot veure's un poc diferent de com recordes.", + "fe_downgraded": "Versió de PleromaFE revertida.", + "older_version_imported": "El fitxer que has importat va ser creat en una versió del front-end més antiga.", + "snapshot_present": "S'ha carregat la instantània del tema, de manera que tots els valors estan sobreescrits. En canvi, podeu carregar les dades reals del tema." + }, + "keep_as_is": "Mantindre com està", + "save_load_hint": "Les opcions \"Mantindre\" conserven les opcions configurades actualment al seleccionar o carregar temes, també emmagatzema aquestes opcions quan s'exporta un tema. Quan es desactiven totes les caselles de verificació, el tema exportat ho guardarà tot.", + "keep_color": "Mantindre colors", + "keep_opacity": "Mantindre opacitat", + "keep_shadows": "Mantindre ombres", + "keep_fonts": "Mantindre fonts", + "keep_roundness": "Mantindre rodoneses", + "clear_all": "Netejar tot", + "reset": "Reinciar", + "load_theme": "Carregar tema", + "use_source": "Nova versió", + "clear_opacity": "Netejar opacitat" + }, + "common": { + "contrast": { + "hint": "El ràtio de contrast és {ratio}. {level} {context}", + "level": { + "bad": "no compleix amb cap pauta d'accecibilitat", + "aaa": "Compleix amb el nivell AA (recomanat)", + "aa": "Compleix amb el nivell AA (mínim)" + }, + "context": { + "18pt": "per a textos grans (+18pt)", + "text": "per a textos" + } + }, + "opacity": "Opacitat", + "color": "Color" + }, + "advanced_colors": { + "badge": "Fons de insígnies", + "inputs": "Camps d'entrada", + "wallpaper": "Fons de pantalla", + "pressed": "Pressionat", + "chat": { + "outgoing": "Eixint", + "border": "Borde", + "incoming": "Entrants" + }, + "borders": "Bordes", + "panel_header": "Capçalera del panell", + "buttons": "Botons", + "faint_text": "Text esvaït", + "poll": "Gràfica de l'enquesta", + "toggled": "Commutat", + "alert": "Fons d'alertes", + "alert_error": "Error", + "alert_warning": "Precaució", + "post": "Publicacions/Biografies d'usuaris", + "badge_notification": "Notificacions", + "selectedMenu": "Element del menú seleccionat", + "tabs": "Pestanyes", + "_tab_label": "Avançat", + "alert_neutral": "Neutral", + "popover": "Suggeriments, menús, superposicions", + "top_bar": "Barra superior", + "highlight": "Elements destacats", + "disabled": "Deshabilitat", + "icons": "Icones", + "selectedPost": "Publicació seleccionada", + "underlay": "Subratllat" + }, + "common_colors": { + "main": "Colors comuns", + "rgbo": "Icones, accents, insígnies", + "foreground_hint": "mira la pestanya \"Avançat\" per a un control més detallat", + "_tab_label": "Comú" + }, + "radii": { + "_tab_label": "Rodonesa" + } + }, + "version": { + "frontend_version": "Versió \"Frontend\"", + "backend_version": "Versió \"backend\"", + "title": "Versió" + }, + "theme_help_v2_1": "També pots anular alguns components de color i opacitat activant la casella. Usa el botó \"Esborrar tot\" per esborrar totes les anulacions.", + "type_domains_to_mute": "Buscar dominis per a silenciar", + "greentext": "Text verd (meme arrows)", + "fun": "Divertit", + "notification_setting_filters": "Filtres", + "virtual_scrolling": "Optimitzar la representació del flux", + "notification_setting_block_from_strangers": "Bloqueja les notificacions dels usuaris que no segueixes", + "enable_web_push_notifications": "Habilitar notificacions del navegador", + "notification_blocks": "Bloquejar a un usuari para totes les notificacions i també les cancel·la.", + "more_settings": "Més opcions", + "notification_setting_privacy": "Privacitat", + "upload_a_photo": "Pujar una foto", + "notification_setting_hide_notification_contents": "Amagar el remitent i els continguts de les notificacions push", + "notifications": "Notificacions", + "notification_mutes": "Per a deixar de rebre notificacions d'un usuari en concret, silencia'l-ho.", + "theme_help_v2_2": "Les icones per baix d'algunes entrades són indicadors del contrast del fons/text, desplaça el ratolí per a més informació. Tingues en compte que quan s'utilitzen indicadors de contrast de transparència es mostra el pitjor cas possible.", + "hide_shoutbox": "Oculta la casella de gàbia de grills", + "always_show_post_button": "Mostra sempre el botó flotant de publicació nova", + "pad_emoji": "Acompanya els emojis amb espais en afegir des del selector", + "mentions_new_style": "Enllaços d'esment més elegants", + "mentions_new_place": "Posa les mencions en una línia separada", + "post_status_content_type": "Format de publicació" }, "time": { "day": "{0} dia", "days": "{0} dies", "day_short": "{0} dia", "days_short": "{0} dies", - "hour": "{0} hour", - "hours": "{0} hours", + "hour": "{0} hora", + "hours": "{0} hores", "hour_short": "{0}h", "hours_short": "{0}h", "in_future": "in {0}", @@ -287,12 +568,12 @@ "months_short": "{0} mesos", "now": "ara mateix", "now_short": "ara mateix", - "second": "{0} second", - "seconds": "{0} seconds", + "second": "{0} segon", + "seconds": "{0} segons", "second_short": "{0}s", "seconds_short": "{0}s", - "week": "{0} setm.", - "weeks": "{0} setm.", + "week": "{0} setmana", + "weeks": "{0} setmanes", "week_short": "{0} setm.", "weeks_short": "{0} setm.", "year": "{0} any", @@ -308,7 +589,13 @@ "no_retweet_hint": "L'entrada és només per a seguidores o és \"directa\", i per tant no es pot republicar", "repeated": "republicat", "show_new": "Mostra els nous", - "up_to_date": "Actualitzat" + "up_to_date": "Actualitzat", + "socket_reconnected": "Connexió a temps real establerta", + "socket_broke": "Connexió a temps real perduda: codi CloseEvent {0}", + "error": "Error de càrrega de la línia de temps: {0}", + "no_statuses": "No hi ha entrades", + "reload": "Recarrega", + "no_more_statuses": "No hi ha més entrades" }, "user_card": { "approve": "Aprova", @@ -324,13 +611,62 @@ "muted": "Silenciat", "per_day": "per dia", "remote_follow": "Seguiment remot", - "statuses": "Estats" + "statuses": "Estats", + "unblock_progress": "Desbloquejant…", + "unmute": "Deixa de silenciar", + "follow_progress": "Sol·licitant…", + "admin_menu": { + "force_nsfw": "Marca totes les entrades amb \"No segur per a entorns laborals\"", + "strip_media": "Esborra els audiovisuals de les entrades", + "disable_any_subscription": "Deshabilita completament seguir algú", + "quarantine": "Deshabilita la federació a les entrades de les usuàries", + "moderation": "Moderació", + "delete_user_confirmation": "Estàs completament segur/a? Aquesta acció no es pot desfer.", + "revoke_admin": "Revoca l'Admin", + "activate_account": "Activa el compte", + "deactivate_account": "Desactiva el compte", + "revoke_moderator": "Revoca Moderació", + "delete_account": "Esborra el compte", + "disable_remote_subscription": "Deshabilita seguir algú des d'una instància remota", + "delete_user": "Esborra la usuària", + "grant_admin": "Concedir permisos d'Administració", + "grant_moderator": "Concedir permisos de Moderació", + "force_unlisted": "Força que les publicacions no estiguin llistades", + "sandbox": "Força que els missatges siguin només seguidors" + }, + "edit_profile": "Edita el perfil", + "hidden": "Amagat", + "follow_sent": "Petició enviada!", + "unmute_progress": "Deixant de silenciar…", + "bot": "Bot", + "mute_progress": "Silenciant…", + "favorites": "Favorits", + "mention": "Menció", + "follow_unfollow": "Deixa de seguir", + "subscribe": "Subscriu-te", + "show_repeats": "Mostra les repeticions", + "report": "Report", + "its_you": "Ets tu!", + "unblock": "Desbloqueja", + "block_progress": "Bloquejant…", + "message": "Missatge", + "unsubscribe": "Anul·la la subscripció", + "hide_repeats": "Amaga les repeticions", + "highlight": { + "disabled": "Sense ressaltat", + "solid": "Fons sòlid", + "striped": "Fons a ratlles", + "side": "Ratlla lateral" + }, + "media": "Media" }, "user_profile": { - "timeline_title": "Flux personal" + "timeline_title": "Flux personal", + "profile_loading_error": "Disculpes, hi ha hagut un error carregant aquest perfil.", + "profile_does_not_exist": "Disculpes, aquest perfil no existeix." }, "who_to_follow": { - "more": "More", + "more": "Més", "who_to_follow": "A qui seguir" }, "selectable_list": { @@ -338,14 +674,25 @@ }, "remote_user_resolver": { "error": "No trobat.", - "searching_for": "Cercant per" + "searching_for": "Cercant per", + "remote_user_resolver": "Resolució d'usuari remot" }, "interactions": { "load_older": "Carrega antigues interaccions", - "favs_repeats": "Repeticions i favorits" + "favs_repeats": "Repeticions i favorits", + "follows": "Nous seguidors", + "moves": "Migració d'usuaris" }, "emoji": { - "stickers": "Adhesius" + "stickers": "Adhesius", + "keep_open": "Mantindre el selector obert", + "custom": "Emojis personalitzats", + "unicode": "Emojis unicode", + "load_all_hint": "Carregat el primer emoji {saneAmount}, carregar tots els emoji pot causar problemes de rendiment.", + "emoji": "Emoji", + "search_emoji": "Buscar un emoji", + "add_emoji": "Inserir un emoji", + "load_all": "Carregant tots els {emojiAmount} emoji" }, "polls": { "expired": "L'enquesta va acabar fa {0}", @@ -357,7 +704,11 @@ "votes": "vots", "option": "Opció", "add_option": "Afegeix opció", - "add_poll": "Afegeix enquesta" + "add_poll": "Afegeix enquesta", + "expiry": "Temps de vida de l'enquesta", + "people_voted_count": "{count} persona ha votat | {count} persones han votat", + "votes_count": "{count} vot | {count} vots", + "not_enough_options": "L'enquesta no té suficients opcions úniques" }, "media_modal": { "next": "Següent", @@ -365,7 +716,8 @@ }, "importer": { "error": "Ha succeït un error mentre s'importava aquest arxiu.", - "success": "Importat amb èxit." + "success": "Importat amb èxit.", + "submit": "Enviar" }, "image_cropper": { "cancel": "Cancel·la", @@ -379,7 +731,9 @@ }, "domain_mute_card": { "mute_progress": "Silenciant…", - "mute": "Silencia" + "mute": "Silencia", + "unmute": "Deixar de silenciar", + "unmute_progress": "Deixant de silenciar…" }, "about": { "staff": "Equip responsable", @@ -391,16 +745,136 @@ "reject": "Rebutja", "accept_desc": "Aquesta instància només accepta missatges de les següents instàncies:", "accept": "Accepta", - "simple_policies": "Polítiques específiques de la instància" + "simple_policies": "Polítiques específiques de la instància", + "ftl_removal_desc": "Aquesta instància elimina les següents instàncies del flux de la xarxa coneguda:", + "ftl_removal": "Eliminació de la línia de temps coneguda", + "media_nsfw_desc": "Aquesta instància obliga el contingut multimèdia a establir-se com a sensible dins de les publicacions en les següents instàncies:", + "media_removal": "Eliminació de la multimèdia", + "media_removal_desc": "Aquesta instància elimina els suports multimèdia de les publicacions en les següents instàncies:", + "media_nsfw": "Forçar contingut multimèdia com a sensible" }, "mrf_policies_desc": "Les polítiques MRF controlen el comportament federat de la instància. Les següents polítiques estan habilitades:", "mrf_policies": "Polítiques MRF habilitades", "keyword": { "replace": "Reemplaça", "reject": "Rebutja", - "keyword_policies": "Polítiques de paraules clau" + "keyword_policies": "Filtratge per paraules clau", + "is_replaced_by": "→", + "ftl_removal": "Eliminació de la línia de temps federada" }, "federation": "Federació" } + }, + "shoutbox": { + "title": "Gàbia de Grills" + }, + "status": { + "delete": "Esborra l'entrada", + "delete_confirm": "Segur que vols esborrar aquesta entrada?", + "thread_muted_and_words": ", té les paraules:", + "show_full_subject": "Mostra tot el tema", + "show_content": "Mostra el contingut", + "repeats": "Repeticions", + "bookmark": "Marcadors", + "status_unavailable": "Entrada no disponible", + "expand": "Expandeix", + "copy_link": "Copia l'enllaç a l'entrada", + "hide_full_subject": "Amaga tot el tema", + "favorites": "Favorits", + "replies_list": "Contestacions:", + "mute_conversation": "Silencia la conversa", + "thread_muted": "Fil silenciat", + "hide_content": "Amaga el contingut", + "status_deleted": "S'ha esborrat aquesta entrada", + "nsfw": "No segur per a entorns laborals", + "unbookmark": "Desmarca", + "external_source": "Font externa", + "unpin": "Deixa de destacar al perfil", + "pinned": "Destacat", + "reply_to": "Contesta a", + "pin": "Destaca al perfil", + "unmute_conversation": "Deixa de silenciar la conversa", + "mentions": "Mencions", + "you": "(Tu)", + "plus_more": "+{number} més" + }, + "user_reporting": { + "additional_comments": "Comentaris addicionals", + "forward_description": "Aquest compte és d'un altre servidor. Vols enviar una còpia del report allà també?", + "forward_to": "Endavant a {0}", + "generic_error": "Hi ha hagut un error mentre s'estava processant la teva sol·licitud.", + "title": "Reportant {0}", + "add_comment_description": "Aquest report serà enviat a la moderació a la instància. Pots donar una explicació de per què estàs reportant aquest compte:", + "submit": "Envia" + }, + "tool_tip": { + "add_reaction": "Afegeix una Reacció", + "accept_follow_request": "Accepta la sol·licitud de seguir", + "repeat": "Repeteix", + "reply": "Respon", + "favorite": "Favorit", + "user_settings": "Configuració d'usuària", + "reject_follow_request": "Rebutja la sol·licitud de seguir", + "bookmark": "Marcador", + "media_upload": "Pujar multimèdia" + }, + "search": { + "no_results": "No hi ha resultats", + "people": "Persones", + "hashtags": "Etiquetes", + "people_talking": "{count} persones parlant", + "person_talking": "{count} persones parlant" + }, + "upload": { + "file_size_units": { + "B": "B", + "KiB": "KiB", + "GiB": "GiB", + "TiB": "TiB", + "MiB": "MiB" + }, + "error": { + "base": "La pujada ha fallat.", + "file_too_big": "Fitxer massa gran [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "Prova de nou d'aquí una estona", + "message": "La pujada ha fallat: {0}" + } + }, + "errors": { + "storage_unavailable": "Pleroma no ha pogut accedir a l'emmagatzematge del navegador. El teu inici de sessió o configuració no es desaran i et pots trobar algun altre problema. Prova a habilitar les galetes." + }, + "password_reset": { + "password_reset": "Reinicia la contrasenya", + "forgot_password": "Has oblidat la contrasenya?", + "too_many_requests": "Has arribat al límit d'intents. Prova de nou d'aquí una estona.", + "password_reset_required_but_mailer_is_disabled": "Has de reiniciar la teva contrasenya però el reinici de la contrasenya està deshabilitat. Si us plau, contacta l'administració de la teva instància.", + "placeholder": "El teu correu electrònic o nom d'usuària", + "instruction": "Introdueix la teva adreça de correu electrònic o nom d'usuària. T'enviarem un enllaç per reiniciar la teva contrasenya.", + "return_home": "Torna a la pàgina principal", + "password_reset_required": "Has de reiniciar la teva contrasenya per iniciar la sessió.", + "password_reset_disabled": "El reinici de la contrasenya està deshabilitat. Si us plau, contacta l'administració de la teva instància.", + "check_email": "Comprova que has rebut al correu electrònic un enllaç per reiniciar la teva contrasenya." + }, + "file_type": { + "image": "Imatge", + "file": "Fitxer", + "video": "Vídeo", + "audio": "Àudio" + }, + "chats": { + "chats": "Xats", + "new": "Nou xat", + "delete_confirm": "Realment vols esborrar aquest missatge?", + "error_sending_message": "Alguna cosa ha fallat quan s'enviava el missatge.", + "more": "Més", + "delete": "Esborra", + "empty_message_error": "No es pot publicar un missatge buit", + "you": "Tu:", + "message_user": "Missatge {nickname}", + "error_loading_chat": "Alguna cosa ha fallat quan es carregava el xat.", + "empty_chat_list_placeholder": "Encara no tens cap xat. Crea un nou xat!" + }, + "display_date": { + "today": "Avui" } } diff --git a/src/i18n/cs.json b/src/i18n/cs.json @@ -407,7 +407,6 @@ "follow": "Sledovat", "follow_sent": "Požadavek odeslán!", "follow_progress": "Odeslílám požadavek…", - "follow_again": "Odeslat požadavek znovu?", "follow_unfollow": "Přestat sledovat", "followees": "Sledovaní", "followers": "Sledující", diff --git a/src/i18n/de.json b/src/i18n/de.json @@ -9,7 +9,9 @@ "scope_options": "Reichweitenoptionen", "text_limit": "Zeichenlimit", "title": "Funktionen", - "who_to_follow": "Wem folgen?" + "who_to_follow": "Vorschläge", + "upload_limit": "Maximale Upload Größe", + "pleroma_chat_messages": "Pleroma Chat" }, "finder": { "error_fetching_user": "Fehler beim Suchen des Benutzers", @@ -28,7 +30,19 @@ "disable": "Deaktivieren", "enable": "Aktivieren", "confirm": "Bestätigen", - "verify": "Verifizieren" + "verify": "Verifizieren", + "role": { + "moderator": "Moderator", + "admin": "Admin" + }, + "peek": "Schau rein", + "close": "Schliessen", + "retry": "Versuche es erneut", + "error_retry": "Bitte versuche es erneut", + "loading": "Lade…", + "flash_content": "Klicken, um den Flash-Inhalt mit Ruffle anzuzeigen (Die Funktion ist experimentell und funktioniert daher möglicherweise nicht).", + "flash_security": "Diese Funktion stellt möglicherweise eine Risiko dar, weil Flash-Inhalte weiterhin potentiell gefährlich sind.", + "flash_fail": "Falsh-Inhalt konnte nicht geladen werden, Details werden in der Konsole angezeigt." }, "login": { "login": "Anmelden", @@ -63,7 +77,11 @@ "search": "Suche", "preferences": "Voreinstellungen", "administration": "Administration", - "who_to_follow": "Wem folgen" + "who_to_follow": "Wem folgen", + "chats": "Chats", + "timelines": "Zeitlinie", + "bookmarks": "Lesezeichen", + "home_timeline": "Heim Zeitlinie" }, "notifications": { "broken_favorite": "Unbekannte Nachricht, suche danach…", @@ -76,7 +94,8 @@ "follow_request": "möchte dir folgen", "migrated_to": "migrierte zu", "reacted_with": "reagierte mit {0}", - "no_more_notifications": "Keine Benachrichtigungen mehr" + "no_more_notifications": "Keine Benachrichtigungen mehr", + "error": "Error beim laden von Neuigkeiten" }, "post_status": { "new_status": "Neuen Status veröffentlichen", @@ -105,7 +124,13 @@ "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" - } + }, + "media_description_error": "Medien konnten nicht neu geladen werden, versuche es erneut", + "empty_status_error": "Eine leere Nachricht ohne Anhänge kann nicht gesendet werden", + "preview_empty": "Leer", + "preview": "Vorschau", + "post": "Post", + "media_description": "Medienbeschreibung" }, "registration": { "bio": "Bio", @@ -124,9 +149,12 @@ "password_confirmation_required": "darf nicht leer 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.", + "bio_placeholder": "z.B.\nHallo, ich bin Lain.\nIch bin ein super süßes blushy-crushy Anime Girl aus dem vorstädtischen Japan. Du kennst mich vielleicht von Wired.", "fullname_placeholder": "z.B. Lain Iwakura", - "username_placeholder": "z.B. lain" + "username_placeholder": "z.B. lain", + "register": "Registrierung", + "reason_placeholder": "Diese Instanz bestätigt Registrierungen manuell. \nLass die Admins wissen warum du dich registrieren willst.", + "reason": "Grund zur Anmeldung" }, "settings": { "attachmentRadius": "Anhänge", @@ -136,7 +164,7 @@ "avatarRadius": "Avatare", "background": "Hintergrund", "bio": "Bio", - "btnRadius": "Buttons", + "btnRadius": "Knöpfe", "cBlue": "Blau (Antworten, folgt dir)", "cGreen": "Grün (Retweet)", "cOrange": "Orange (Favorisieren)", @@ -201,7 +229,7 @@ "name_bio": "Name & Bio", "new_password": "Neues Passwort", "notification_visibility": "Benachrichtigungstypen, die angezeigt werden sollen", - "notification_visibility_follows": "Follows", + "notification_visibility_follows": "Folgt", "notification_visibility_likes": "Favoriten", "notification_visibility_mentions": "Erwähnungen", "notification_visibility_repeats": "Wiederholungen", @@ -268,7 +296,24 @@ "save_load_hint": "Die \"Beibehalten\"-Optionen behalten die aktuell eingestellten Optionen beim Auswählen oder Laden von Designs bei, sie speichern diese Optionen auch beim Exportieren eines Designs. Wenn alle Kontrollkästchen deaktiviert sind, wird beim Exportieren des Designs alles gespeichert.", "reset": "Zurücksetzen", "clear_all": "Alles leeren", - "clear_opacity": "Deckkraft leeren" + "clear_opacity": "Deckkraft leeren", + "help": { + "fe_downgraded": "PleromaFE Version wurde zurückgerollt.", + "older_version_imported": "Die Datei, die du importiert hast, wurde für eine ältere Version vom FE gemacht.", + "future_version_imported": "Die Datei, die du importiert hast, wurde für eine neuere Version vom FE gemacht.", + "v2_imported": "Die Datei, die du importiert hast, war für eine ältere Version des FEs. Wir versuchen, die Kompatibilität zu maximieren, aber es könnte trotzdem Inkonsistenz auftreten.", + "upgraded_from_v2": "PleromaFE wurde modernisiert, dein Theme könnte etwas anders aussehen als vorher.", + "snapshot_source_mismatch": "Versionskonflikt: vermutlich wurde das FE zurückgesetzt und dann ein Update durchgeführt. Falls das Theme mit einer alten FE-Version erstellt wurde, sollte vermutlich die alte Version verwendet werden, andernfalls die neue.", + "migration_napshot_gone": "Snapshot konnte nicht gefunden werden, die Anzeige könnte daher teilweise möglicherweise nicht den Erwartungen entsprechen.", + "migration_snapshot_ok": "Vorsichtshalber wurde ein Snapshot des Themes geladen. Alternativ kann versucht werden, die Daten des Themes selbst zu laden.", + "snapshot_present": "Snapshot des Themes wurde geladen, alle entsprechenden Einstellungen wurden überschrieben. Alternativ können die tatsächlichen Daten des Themes geladen werden.", + "fe_upgraded": "Mit dem Upgrade wurde auch eine neue Version von Pleromas Theme Engine installiert.", + "snapshot_missing": "Die Datei enthält keinen Theme-Snapshot, die Darstellung kann daher möglicherweise abweichend sein." + }, + "use_source": "Neue Version", + "use_snapshot": "Alte Version", + "keep_as_is": "Lass es so, wie es ist", + "load_theme": "Lade Theme" }, "common": { "color": "Farbe", @@ -303,7 +348,27 @@ "borders": "Rahmen", "buttons": "Schaltflächen", "inputs": "Eingabefelder", - "faint_text": "Verblasster Text" + "faint_text": "Verblasster Text", + "disabled": "aus", + "selectedMenu": "Ausgewähltes Menüelement", + "selectedPost": "Ausgewählter Post", + "pressed": "Gedrückt", + "highlight": "Hervorgehobene Elemente", + "icons": "Icons", + "poll": "Umfragegraph", + "post": "Posts/Benutzerinfo", + "alert_neutral": "Neutral", + "alert_warning": "Warnung", + "wallpaper": "Hintergrund", + "popover": "Kurzinfo, Menüs, Popover-Fenster", + "chat": { + "border": "Ränder", + "outgoing": "Ausgehend", + "incoming": "Eingehend" + }, + "toggled": "Umgeschaltet", + "underlay": "Halbtransparenter Hintergrund", + "tabs": "Reiter" }, "radii": { "_tab_label": "Abrundungen" @@ -325,7 +390,7 @@ "inset_classic": "Eingesetzte Schatten werden mit {0} verwendet" }, "components": { - "panel": "Panel", + "panel": "Bedienfeld", "panelHeader": "Panel-Kopf", "topBar": "Obere Leiste", "avatar": "Benutzer-Avatar (in der Profilansicht)", @@ -335,8 +400,9 @@ "buttonHover": "Schaltfläche (hover)", "buttonPressed": "Schaltfläche (gedrückt)", "buttonPressedHover": "Schaltfläche (gedrückt+hover)", - "input": "Input field" - } + "input": "Eingabefeld" + }, + "hintV3": "Um die Farbe der Schatten zu bestimmen, kann auch die Auszeichnung {0} verwendet werden, um einen anderen Fabbereich zu nutzen." }, "fonts": { "_tab_label": "Schriften", @@ -384,11 +450,14 @@ }, "verify": { "desc": "Um 2FA zu aktivieren, gib den Code von deiner 2FA-App ein:" - } + }, + "confirm_and_enable": "Bestätige und aktiviere OTP", + "setup_otp": "Richte OTP ein", + "wait_pre_setup_otp": "OTP voreinstellen" }, "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", + "allow_following_move": "Erlaube auto-follow, wenn von dir verfolgte Accounts umziehen", "blocks_imported": "Blocks importiert! Die Verarbeitung wird einen Moment brauchen.", "block_import_error": "Fehler beim Importieren der Blocks", "block_import": "Block Import", @@ -400,7 +469,81 @@ "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" + "accent": "Akzent", + "no_blocks": "Keine Blocks", + "notification_visibility_emoji_reactions": "Reaktionen", + "new_email": "Neue Email", + "profile_fields": { + "value": "Inhalt", + "name": "Label", + "add_field": "Feld hinzufügen", + "label": "Profil Metadaten" + }, + "bot": "Dies ist ein Bot Account", + "blocks_tab": "Blocks", + "save": "Änderungen speichern", + "show_moderator_badge": "Zeige Moderator-Abzeichen auf meinem Profil", + "show_admin_badge": "Zeige Admin-Abzeichen auf meinem Profil", + "no_mutes": "Keine Stummschaltungen", + "reset_profile_background": "Profilhintergrund zurücksetzen", + "reset_avatar": "Avatar zurücksetzten", + "search_user_to_mute": "Suche, wen du stummschalten willst", + "search_user_to_block": "Suche, wen du blocken willst", + "reply_visibility_self_short": "Zeige antworten nur einem selbst", + "reply_visibility_following_short": "Zeige Antworten an meine Follower", + "notification_visibility_moves": "Nutzer zieht um", + "file_export_import": { + "errors": { + "file_too_new": "Inkompatible Major Version: {fileMajor}, dieses PleromaFE Version (settings ver {feMajor}) ist zu alt", + "invalid_file": "Die ausgewählte Datei kann nicht zur Wiederherstellung verwendet werden. Keine Änderungen wurden umgesetzt.", + "file_too_old": "Inkompatible Major Version: {fileMajor}, die Dateiversion ist zu alt und wird nicht mehr unterstützt (min. set. ver. {feMajor})", + "file_slightly_new": "Geringfügige Abweichung in der Dateiversion, einige Einstellungen konnten möglicherweise nicht geladen werden" + }, + "restore_settings": "Einstellungen von einer Datei wiederherstellen", + "backup_settings_theme": "Einstellungen und Theme in eine Datei speichern", + "backup_settings": "Einstellungen in Datei speichern", + "backup_restore": "Einstellungen backuppen" + }, + "hide_wallpaper": "Verstecke Instanzhintergrundbild", + "hide_all_muted_posts": "Verstecke stummgeschaltete Posts", + "hide_media_previews": "Verstecke Vorschau von Medien", + "word_filter": "Wort Filter", + "mutes_and_blocks": "Stummgeschaltete und Geblockte", + "chatMessageRadius": "Chat Nachricht", + "import_mutes_from_a_csv_file": "Importiere stummgeschaltete User von einer cvs Datei", + "mutes_imported": "Stummgeschaltete User wurden importiert! Verarbeitung dauert eine Weile.", + "mute_import_error": "Fehler beim Importieren von stummgeschalteten Usern", + "mute_import": "Stumm geschaltete User importieren", + "mute_export_button": "Stumm geschaltete User in eine cvs Datei exportieren", + "mute_export": "Stumm geschaltete User exportieren", + "setting_changed": "Einstellungen weichen von den Standardeinstellungen ab", + "notification_blocks": "Einen User zu blocken stoppt alle Benachrichtigungen von ihm und deabonniert ihn.", + "version": { + "frontend_version": "Frontend Version", + "backend_version": "Backend Version", + "title": "Version" + }, + "notification_mutes": "Um nicht mehr die Benachrichtigungen von einem bestimmten User zu bekommen, verwende eine Stummschaltung.", + "user_mutes": "User", + "notification_setting_privacy": "Privatsphäre", + "notification_setting_filters": "Filter", + "greentext": "Meme Pfeile", + "fun": "Spaß", + "upload_a_photo": "Lade ein Foto hoch", + "type_domains_to_mute": "Tippe die Domains ein, die du stummschalten willst", + "useStreamingApiWarning": "(Nicht empfohlen, experimentell, bekannt dafür, Posts zu überspringen)", + "useStreamingApi": "Empfange Posts und Benachrichtigungen in Echtzeit", + "more_settings": "Weitere Einstellungen", + "notification_setting_hide_notification_contents": "Absender und Inhalte von Push-Nachrichten verbergen", + "notification_setting_block_from_strangers": "Benachrichtigungen von Nutzern blockieren, denen Du nicht folgst", + "virtual_scrolling": "Rendering der Timeline optimieren", + "sensitive_by_default": "Alle Beiträge standardmäßig als heikel markieren", + "reset_background_confirm": "Hintergrund wirklich zurücksetzen?", + "reset_banner_confirm": "Banner wirklich zurücksetzen?", + "reset_avatar_confirm": "Avatar wirklich zurücksetzen?", + "reset_profile_banner": "Profilbanner zurücksetzen", + "hide_shoutbox": "Shoutbox der Instanz verbergen", + "right_sidebar": "Seitenleiste rechts anzeigen" }, "timeline": { "collapse": "Einklappen", @@ -410,7 +553,13 @@ "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" + "up_to_date": "Aktuell", + "no_statuses": "Keine Beiträge", + "no_more_statuses": "Keine weiteren Beiträge", + "reload": "Neu laden", + "error": "Fehler beim Lesen der Timeline: {0}", + "socket_broke": "Netzverbindung verloren: CloseEvent code {0}", + "socket_reconnected": "Netzverbindung hergestellt" }, "user_card": { "approve": "Genehmigen", @@ -420,7 +569,6 @@ "follow": "Folgen", "follow_sent": "Anfrage gesendet!", "follow_progress": "Anfragen…", - "follow_again": "Anfrage erneut senden?", "follow_unfollow": "Folgen beenden", "followees": "Folgt", "followers": "Folgende", @@ -433,11 +581,52 @@ "remote_follow": "Folgen", "statuses": "Beiträge", "admin_menu": { - "sandbox": "Erzwinge Beiträge nur für Follower sichtbar zu sein" + "sandbox": "Erzwinge Beiträge nur für Follower sichtbar zu sein", + "delete_user_confirmation": "Achtung! Diese Entscheidung kann nicht rückgängig gemacht werden! Trotzdem durchführen?", + "grant_admin": "Administratorprivilegien gewähren", + "delete_user": "Nutzer löschen", + "strip_media": "Medien von Beiträgen entfernen", + "force_nsfw": "Alle Beiträge als pervers markieren", + "activate_account": "Aktiviere Account", + "revoke_moderator": "Administratorstatuß wiederrufen", + "grant_moderator": "Moderatorstatuß gewähren", + "revoke_admin": "Administratorstatuß wiederrufen", + "moderation": "Moderation", + "delete_account": "Konto löschen", + "deactivate_account": "Konto deaktivieren", + "quarantine": "Beiträge des Nutzers können nur auf der eigenen Instanz gesehen werden", + "disable_any_subscription": "Alle Folgeanfragen für diesen Nutzer grundsätzlich ablehnen", + "disable_remote_subscription": "Nutzer anderer Instanzen vom Folgen dieses Nutzers ausschließen", + "force_unlisted": "Beiträge von der öffentlichen Zeitleiste ausschliessen" + }, + "block_progress": "Blocken…", + "unblock_progress": "Entblocken…", + "unblock": "Entblocken", + "report": "Melden", + "mention": "Erwähnungen", + "media": "Medien", + "hidden": "Versteckt", + "favorites": "Favoriten", + "bot": "Bot", + "show_repeats": "Geteilte Beiträge anzeigen", + "hide_repeats": "Geteilte Beiträge nicht anzeigen", + "mute_progress": "Stummschalten erfolgt…", + "unmute_progress": "Aufhebung erfolgt…", + "unmute": "Stummschalten aufheben", + "unsubscribe": "Entfolgen", + "subscribe": "Folgen", + "message": "Nachricht", + "highlight": { + "side": "Randmarkierung", + "striped": "gestreifter Hintergrund", + "solid": "kein Muster verwenden", + "disabled": "Nicht hervorheben" } }, "user_profile": { - "timeline_title": "Beiträge" + "timeline_title": "Beiträge", + "profile_loading_error": "Beim Laden dieses Profils ist ein Fehler aufgetreten.", + "profile_does_not_exist": "Profil nicht vorhanden." }, "who_to_follow": { "more": "Mehr", @@ -448,13 +637,18 @@ "repeat": "Wiederholen", "reply": "Antworten", "favorite": "Favorisieren", - "user_settings": "Benutzereinstellungen" + "user_settings": "Benutzereinstellungen", + "bookmark": "Lesezeichen", + "reject_follow_request": "Folgeanfrage ablehnen", + "accept_follow_request": "Folgeanfrage annehmen", + "add_reaction": "Emoji-Reaktion hinzufügen" }, "upload": { "error": { "base": "Hochladen fehlgeschlagen.", "file_too_big": "Datei ist zu groß [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", - "default": "Bitte versuche es später erneut" + "default": "Bitte versuche es später erneut", + "message": "Hochladen fehlgeschlagen" }, "file_size_units": { "B": "B", @@ -478,7 +672,7 @@ "placeholder": "Dein Benutzername oder die zugehörige E-Mail-Adresse", "check_email": "Im E-Mail-Posteingang des angebenen Kontos müsste sich jetzt (oder zumindest in Kürze) die E-Mail mit dem Link zum Passwortzurücksetzen befinden.", "return_home": "Zurück zur Heimseite", - "too_many_requests": "Kurze Pause. Zu viele Versuche. Bitte, später nochmal probieren.", + "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_but_mailer_is_disabled": "Passwortzurücksetzen wäre erforderlich, ist aber deaktiviert. Bitte Administrator kontaktieren." @@ -486,21 +680,21 @@ "about": { "mrf": { "federation": "Föderation", - "mrf_policies": "Aktivierte MRF Richtlinien", + "mrf_policies": "Aktive 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", + "ftl_removal": "Von der Zeitleiste \"Das 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:" + "ftl_removal_desc": "Dieser Instanz entfernt folgende Instanzen von der \"Das bekannte Netzwerk\" Zeitleiste:" }, "keyword": { "keyword_policies": "Keyword Richtlinien", @@ -509,7 +703,7 @@ "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:" + "mrf_policies_desc": "MRF Richtlinien beeinflussen das Föderationsverhalten dieser Instanz. Die folgenden Richtlinien sind aktiv:" }, "staff": "Mitarbeiter" }, @@ -550,7 +744,9 @@ "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}" + "expires_in": "Die Umfrage endet in {0}", + "votes_count": "{count} Stimme | {count} Stimmen", + "people_voted_count": "{count} Person hat gewählt | {count} Personen haben gewählt" }, "emoji": { "stickers": "Sticker", @@ -560,12 +756,12 @@ "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.", + "load_all_hint": "Erste {saneAmount} Emoji geladen, alle Emoji zu laden könnte Leistungsprobleme verursachen.", "unicode": "Unicode Emoji" }, "interactions": { "load_older": "Lade ältere Interaktionen", - "follows": "Neue Follows", + "follows": "Neue Follower", "favs_repeats": "Wiederholungen und Favoriten", "moves": "Benutzer migriert zu" }, @@ -573,7 +769,106 @@ "select_all": "Wähle alle" }, "remote_user_resolver": { - "searching_for": "Suche nach", - "error": "Nicht gefunden." + "searching_for": "Suche für", + "error": "Nicht gefunden.", + "remote_user_resolver": "Resolver für Nutzer auf anderen Instanzen" + }, + "errors": { + "storage_unavailable": "Pleroma konnte nicht auf den Browser Speicher zugreifen. Deine Anmeldung und deine Einstellungen werden nicht gespeichert. Es kann unvorhersehbare Probleme geben. Versuche ansonsten Cookies zu erlauben." + }, + "shoutbox": { + "title": "Shoutbox" + }, + "chats": { + "error_sending_message": "Beim Senden der Nachricht ist ein Fehler aufgetreten.", + "error_loading_chat": "Beim Laden des Chats ist ein Fehler aufgetreten.", + "delete_confirm": "Soll diese Nachricht wirklich gelöscht werden?", + "empty_message_error": "Die Nachricht darf nicht leer sein", + "delete": "Löschen", + "message_user": "Nachricht an {nickname} senden", + "empty_chat_list_placeholder": "Es sind noch keine Chats vorhanden. Jetzt einen Chat starten!", + "more": "Mehr", + "you": "Du:", + "new": "Neuer Chat", + "chats": "Chats" + }, + "user_reporting": { + "generic_error": "Beim Verarbeiten der Anfrage ist ein Fehler aufgetreten.", + "submit": "Senden", + "forward_to": "Weiterleiten an {0}", + "forward_description": "Das fragliche Konto befindet sich auf einem anderen Server. Soll eine Kopie der Beschwerde an den dortigen Verantwortlichen gesendet werden?", + "additional_comments": "Weitere Anmerkungen", + "add_comment_description": "Die Beschwerde wird an die Moderatoren dieser Instanz gesendet. Die Gründe für die Beschwerde können hier angegeben werden:", + "title": "{0} melddn" + }, + "status": { + "copy_link": "Beitragslink kopieren", + "status_unavailable": "Beitrag nicht verfügbar", + "unmute_conversation": "Konversation nicht mehr stummstellen", + "mute_conversation": "Konversation stummstellen", + "replies_list": "Antworten:", + "reply_to": "Antworten auf", + "delete_confirm": "Möchtest du diese Beitrag wirklich löschen?", + "pinned": "Angeheftet", + "unpin": "Nicht mehr an Profil anheften", + "pin": "An Profil anheften", + "delete": "Lösche Beitrag", + "favorites": "Favoriten", + "expand": "Ausklappen", + "nsfw": "NSFW", + "status_deleted": "Dieser Beitrag wurde gelöscht", + "hide_content": "Inhalt verbergen", + "show_content": "Inhalt anzeigen", + "hide_full_subject": "Vollständiges Thema verbergen", + "show_full_subject": "Vollständiges Thema anzeigen", + "thread_muted": "Thread stummgeschaltet", + "external_source": "Externe Quelle", + "unbookmark": "Lesezeichen entfernen", + "bookmark": "Lesezeichen setzen", + "repeats": "Geteilte Beiträge", + "thread_muted_and_words": ", enthält folgende Wörter:" + }, + "time": { + "seconds_short": "{0}s", + "second_short": "{0}s", + "seconds": "{0} Sekunden", + "second": "{0} Sekunde", + "now_short": "jetzt", + "years_short": "{0}Jhr", + "year_short": "{0}Jhr", + "years": "{0} Jahren", + "year": "{0} Jahr", + "weeks_short": "{0}W", + "week_short": "{0}W", + "weeks": "{0} Wochen", + "week": "{0} Woche", + "now": "gerade eben", + "months_short": "{0}Mo", + "month_short": "{0}Mo", + "months": "{0} Monaten", + "month": "{0} Monat", + "minutes_short": "{0}Min", + "minute_short": "{0}Min", + "minutes": "{0} Minuten", + "minute": "{0} Minute", + "in_past": "vor {0}", + "in_future": "in {0}", + "hours_short": "{0}Std", + "hour_short": "{0}Std", + "hours": "{0} Stunden", + "hour": "{0} Stunde", + "days_short": "{0}T", + "day_short": "{0}T", + "days": "{0} Tage", + "day": "{0} Tag" + }, + "display_date": { + "today": "Heute" + }, + "file_type": { + "file": "Datei", + "image": "Bild", + "video": "Video", + "audio": "Audio" } } diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -3,27 +3,30 @@ "mrf": { "federation": "Federation", "keyword": { - "keyword_policies": "Keyword Policies", + "keyword_policies": "Keyword policies", "ftl_removal": "Removal from \"The Whole Known Network\" Timeline", "reject": "Reject", "replace": "Replace", "is_replaced_by": "→" }, - "mrf_policies": "Enabled MRF Policies", + "mrf_policies": "Enabled MRF policies", "mrf_policies_desc": "MRF policies manipulate the federation behaviour of the instance. The following policies are enabled:", "simple": { - "simple_policies": "Instance-specific Policies", + "simple_policies": "Instance-specific policies", + "instance": "Instance", + "reason": "Reason", + "not_applicable": "N/A", "accept": "Accept", "accept_desc": "This instance only accepts messages from the following instances:", "reject": "Reject", "reject_desc": "This instance will not accept messages from the following instances:", "quarantine": "Quarantine", "quarantine_desc": "This instance will send only public posts to the following instances:", - "ftl_removal": "Removal from \"The Whole Known Network\" Timeline", - "ftl_removal_desc": "This instance removes these instances from \"The Whole Known Network\" timeline:", + "ftl_removal": "Removal from \"Known Network\" Timeline", + "ftl_removal_desc": "This instance removes these instances from \"Known Network\" timeline:", "media_removal": "Media Removal", "media_removal_desc": "This instance removes media from posts on the following instances:", - "media_nsfw": "Media Force-set As Sensitive", + "media_nsfw": "Media force-set as sensitive", "media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:" } }, @@ -76,7 +79,14 @@ "confirm": "Confirm", "verify": "Verify", "close": "Close", - "peek": "Peek" + "peek": "Peek", + "role": { + "admin": "Admin", + "moderator": "Moderator" + }, + "flash_content": "Click to show Flash content using Ruffle (Experimental, may not work).", + "flash_security": "Note that this can be potentially dangerous since Flash content is still arbitrary code.", + "flash_fail": "Failed to load flash content, see console for details." }, "image_cropper": { "crop_picture": "Crop picture", @@ -109,18 +119,20 @@ }, "media_modal": { "previous": "Previous", - "next": "Next" + "next": "Next", + "counter": "{current} / {total}" }, "nav": { "about": "About", "administration": "Administration", "back": "Back", - "friend_requests": "Follow Requests", + "friend_requests": "Follow requests", "mentions": "Mentions", "interactions": "Interactions", - "dms": "Direct Messages", - "public_tl": "Public Timeline", + "dms": "Direct messages", + "public_tl": "Public timeline", "timeline": "Timeline", + "home_timeline": "Home timeline", "twkn": "Known Network", "bookmarks": "Bookmarks", "user_search": "User Search", @@ -146,10 +158,12 @@ "submitted_report": "submitted a report" }, "polls": { - "add_poll": "Add Poll", - "add_option": "Add Option", + "add_poll": "Add poll", + "add_option": "Add option", "option": "Option", "votes": "votes", + "people_voted_count": "{count} person voted | {count} people voted", + "votes_count": "{count} vote | {count} votes", "vote": "Vote", "type": "Poll type", "single_choice": "Single choice", @@ -174,7 +188,7 @@ "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", + "favs_repeats": "Repeats and favorites", "follows": "New follows", "emoji_reactions": "Emoji Reactions", "reports": "Reports", @@ -198,6 +212,7 @@ "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", + "post": "Post", "preview": "Preview", "preview_empty": "Empty", "empty_status_error": "Can't post an empty status with no files", @@ -208,10 +223,10 @@ "unlisted": "This post will not be visible in Public Timeline and The Whole Known Network" }, "scope": { - "direct": "Direct - Post to mentioned users only", - "private": "Followers-only - Post to followers only", - "public": "Public - Post to public timelines", - "unlisted": "Unlisted - Do not post to public timelines" + "direct": "Direct - post to mentioned users only", + "private": "Followers-only - post to followers only", + "public": "Public - post to public timelines", + "unlisted": "Unlisted - do not post to public timelines" } }, "registration": { @@ -226,6 +241,9 @@ "username_placeholder": "e.g. lain", "fullname_placeholder": "e.g. Lain Iwakura", "bio_placeholder": "e.g.\nHi, I'm Lain.\nI’m an anime girl living in suburban Japan. You may know me from the Wired.", + "reason": "Reason to register", + "reason_placeholder": "This instance approves registrations manually.\nLet the administration know why you want to register.", + "register": "Register", "validations": { "username_required": "cannot be left blank", "fullname_required": "cannot be left blank", @@ -255,8 +273,11 @@ }, "settings": { "app_name": "App name", + "save": "Save changes", "security": "Security", + "setting_changed": "Setting is different from default", "enter_current_password_to_confirm": "Enter your current password to confirm your identity", + "post_look_feel": "Posts Look & Feel", "mfa": { "otp": "OTP", "setup_otp": "Setup OTP", @@ -282,7 +303,7 @@ "attachmentRadius": "Attachments", "attachments": "Attachments", "avatar": "Avatar", - "avatarAltRadius": "Avatars (Notifications)", + "avatarAltRadius": "Avatars (notifications)", "avatarRadius": "Avatars", "background": "Background", "bio": "Bio", @@ -304,10 +325,10 @@ "cGreen": "Green (Retweet)", "cOrange": "Orange (Favorite)", "cRed": "Red (Cancel)", - "change_email": "Change Email", + "change_email": "Change email", "change_email_error": "There was an issue changing your email.", "changed_email": "Email changed successfully!", - "change_password": "Change Password", + "change_password": "Change password", "change_password_error": "There was an issue changing your password.", "changed_password": "Password changed successfully!", "chatMessageRadius": "Chat message", @@ -316,9 +337,9 @@ "confirm_new_password": "Confirm new password", "current_password": "Current password", "mutes_and_blocks": "Mutes and Blocks", - "data_import_export_tab": "Data Import / Export", + "data_import_export_tab": "Data import / export", "default_vis": "Default visibility scope", - "delete_account": "Delete Account", + "delete_account": "Delete account", "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.", @@ -329,7 +350,9 @@ "emoji_reactions_on_timeline": "Show emoji reactions on timeline", "export_theme": "Save preset", "filtering": "Filtering", + "wordfilter": "Wordfilter", "filtering_explanation": "All statuses containing these words will be muted, one per line", + "word_filter": "Word filter", "follow_export": "Follow export", "follow_export_button": "Export your follows to a csv file", "follow_import": "Follow import", @@ -340,15 +363,22 @@ "general": "General", "hide_attachments_in_convo": "Hide attachments in conversations", "hide_attachments_in_tl": "Hide attachments in timeline", + "hide_media_previews": "Hide media previews", "hide_muted_posts": "Hide posts of muted users", - "max_thumbnails": "Maximum amount of thumbnails per post", + "hide_all_muted_posts": "Hide muted posts", + "max_thumbnails": "Maximum amount of thumbnails per post (empty = no limit)", "hide_isp": "Hide instance-specific panel", + "hide_shoutbox": "Hide instance shoutbox", + "right_sidebar": "Show sidebar on the right side", + "always_show_post_button": "Always show floating New Post button", "hide_wallpaper": "Hide instance wallpaper", "preload_images": "Preload images", "use_one_click_nsfw": "Open NSFW attachments with just one click", "hide_post_stats": "Hide post statistics (e.g. the number of favorites)", "hide_user_stats": "Hide user statistics (e.g. the number of followers)", - "hide_filtered_statuses": "Hide filtered statuses", + "hide_filtered_statuses": "Hide all filtered posts", + "hide_wordfiltered_statuses": "Hide word-filtered statuses", + "hide_muted_threads": "Hide muted threads", "import_blocks_from_a_csv_file": "Import blocks from a csv file", "import_followers_from_a_csv_file": "Import follows from a csv file", "import_theme": "Load preset", @@ -366,20 +396,34 @@ "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", + "file_export_import": { + "backup_restore": "Settings backup", + "backup_settings": "Backup settings to file", + "backup_settings_theme": "Backup settings and theme to file", + "restore_settings": "Restore settings from file", + "errors": { + "invalid_file": "The selected file is not a supported Pleroma settings backup. No changes were made.", + "file_too_new": "Incompatile major version: {fileMajor}, this PleromaFE (settings ver {feMajor}) is too old to handle it", + "file_too_old": "Incompatile major version: {fileMajor}, file version is too old and not supported (min. set. ver. {feMajor})", + "file_slightly_new": "File minor version is different, some settings might not load" + } + }, "profile_fields": { "label": "Profile metadata", - "add_field": "Add Field", + "add_field": "Add field", "name": "Label", "value": "Content" }, "use_contain_fit": "Don't crop the attachment in thumbnails", "name": "Name", - "name_bio": "Name & Bio", - "new_email": "New Email", + "name_bio": "Name & bio", + "new_email": "New email", "new_password": "New password", + "posts": "Posts", + "user_profiles": "User Profiles", "notification_visibility": "Types of notifications to show", "notification_visibility_follows": "Follows", - "notification_visibility_likes": "Likes", + "notification_visibility_likes": "Favorites", "notification_visibility_mentions": "Mentions", "notification_visibility_repeats": "Repeats", "notification_visibility_moves": "User Migrates", @@ -391,25 +435,27 @@ "hide_followers_description": "Don't show who's following me", "hide_follows_count_description": "Don't show follow count", "hide_followers_count_description": "Don't show follower count", - "show_admin_badge": "Show Admin badge in my profile", - "show_moderator_badge": "Show Moderator badge in my profile", - "nsfw_clickthrough": "Enable clickthrough attachment and link preview image hiding for NSFW statuses", + "show_admin_badge": "Show \"Admin\" badge in my profile", + "show_moderator_badge": "Show \"Moderator\" badge in my profile", + "nsfw_clickthrough": "Hide sensitive/NSFW media", "oauth_tokens": "OAuth tokens", "token": "Token", - "refresh_token": "Refresh Token", - "valid_until": "Valid Until", + "refresh_token": "Refresh token", + "valid_until": "Valid until", "revoke_token": "Revoke", "panelRadius": "Panels", "pause_on_unfocused": "Pause streaming when tab is not focused", "presets": "Presets", - "profile_background": "Profile Background", - "profile_banner": "Profile Banner", + "profile_background": "Profile background", + "profile_banner": "Profile banner", "profile_tab": "Profile", "radii_help": "Set up interface edge rounding (in pixels)", "replies_in_timeline": "Replies in timeline", "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", + "reply_visibility_following_short": "Show replies to my follows", + "reply_visibility_self_short": "Show replies to self only", "autohide_floating_post_button": "Automatically hide New Post button (mobile)", "saving_err": "Error saving settings", "saving_ok": "Settings saved", @@ -434,7 +480,8 @@ "subject_line_mastodon": "Like mastodon: copy as is", "subject_line_noop": "Do not copy", "post_status_content_type": "Post status content type", - "stop_gifs": "Play-on-hover GIFs", + "sensitive_by_default": "Mark posts as sensitive by default", + "stop_gifs": "Pause animated images until you hover on them", "streaming": "Enable automatic streaming of new posts when scrolled to the top", "user_mutes": "Users", "useStreamingApi": "Receive posts and notifications real-time", @@ -453,8 +500,18 @@ "true": "yes" }, "virtual_scrolling": "Optimize timeline rendering", + "use_at_icon": "Display @ symbol as an icon instead of text", + "mention_link_display": "Display mention links", + "mention_link_display_short": "always as short names (e.g. @foo)", + "mention_link_display_full_for_remote": "as full names only for remote users (e.g. @foo@example.org)", + "mention_link_display_full": "always as full names (e.g. @foo@example.org)", + "mention_link_show_tooltip": "Show full user names as tooltip for remote users", + "mention_link_show_avatar": "Show user avatar beside the link", + "mention_link_fade_domain": "Fade domains (e.g. @example.org in @foo@example.org)", + "mention_link_bolden_you": "Highlight mention of you when you are mentioned", "fun": "Fun", "greentext": "Meme arrows", + "show_yous": "Show (You)s", "notifications": "Notifications", "notification_setting_filters": "Filters", "notification_setting_block_from_strangers": "Block notifications from users who you do not follow", @@ -463,6 +520,7 @@ "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", + "more_settings": "More settings", "style": { "switcher": { "keep_color": "Keep colors", @@ -611,8 +669,8 @@ }, "version": { "title": "Version", - "backend_version": "Backend Version", - "frontend_version": "Frontend Version" + "backend_version": "Backend version", + "frontend_version": "Frontend version" } }, "time": { @@ -660,7 +718,9 @@ "reload": "Reload", "up_to_date": "Up-to-date", "no_more_statuses": "No more statuses", - "no_statuses": "No statuses" + "no_statuses": "No statuses", + "socket_reconnected": "Realtime connection established", + "socket_broke": "Realtime connection lost: CloseEvent code {0}" }, "status": { "favorites": "Favorites", @@ -673,6 +733,7 @@ "unbookmark": "Unbookmark", "delete_confirm": "Do you really want to delete this status?", "reply_to": "Reply to", + "mentions": "Mentions", "replies_list": "Replies:", "mute_conversation": "Mute conversation", "unmute_conversation": "Unmute conversation", @@ -687,18 +748,33 @@ "hide_content": "Hide content", "status_deleted": "This post was deleted", "nsfw": "NSFW", - "expand": "Expand" + "expand": "Expand", + "you": "(You)", + "plus_more": "+{number} more", + "many_attachments": "Post has {number} attachment(s)", + "collapse_attachments": "Collapse attachments", + "show_all_attachments": "Show all attachments", + "show_attachment_in_modal": "Show in media modal", + "show_attachment_description": "Preview description (open attachment for full description)", + "hide_attachment": "Hide attachment", + "remove_attachment": "Remove attachment", + "attachment_stop_flash": "Stop Flash player", + "move_up": "Shift attachment left", + "move_down": "Shift attachment right", + "open_gallery": "Open gallery" }, "user_card": { "approve": "Approve", "block": "Block", "blocked": "Blocked!", + "deactivated": "Deactivated", "deny": "Deny", + "edit_profile": "Edit profile", "favorites": "Favorites", "follow": "Follow", + "follow_cancel": "Cancel request", "follow_sent": "Request sent!", "follow_progress": "Requesting…", - "follow_again": "Send request again?", "follow_unfollow": "Unfollow", "followees": "Following", "followers": "Followers", @@ -725,6 +801,7 @@ "mute_progress": "Muting…", "hide_repeats": "Hide repeats", "show_repeats": "Show repeats", + "bot": "Bot", "admin_menu": { "moderation": "Moderation", "grant_admin": "Grant Admin", @@ -744,13 +821,15 @@ "delete_user": "Delete user", "delete_user_confirmation": "Are you absolutely sure? This action cannot be undone." }, - "roles": { - "admin": "Admin", - "moderator": "Moderator" + "highlight": { + "disabled": "No highlight", + "solid": "Solid bg", + "striped": "Striped bg", + "side": "Side stripe" } }, "user_profile": { - "timeline_title": "User Timeline", + "timeline_title": "User timeline", "profile_does_not_exist": "Sorry, this profile does not exist.", "profile_loading_error": "Sorry, there was an error loading this profile." }, @@ -768,7 +847,7 @@ "who_to_follow": "Who to follow" }, "tool_tip": { - "media_upload": "Upload Media", + "media_upload": "Upload media", "repeat": "Repeat", "reply": "Reply", "favorite": "Favorite", diff --git a/src/i18n/eo.json b/src/i18n/eo.json @@ -35,7 +35,14 @@ "retry": "Reprovi", "error_retry": "Bonvolu reprovi", "loading": "Enlegante…", - "peek": "Antaŭmontri" + "peek": "Antaŭmontri", + "role": { + "moderator": "Reguligisto", + "admin": "Administranto" + }, + "flash_content": "Klaku por montri enhavon de Flash per Ruffle. (Eksperimente, eble ne funkcios.)", + "flash_security": "Sciu, ke tio povas esti danĝera, ĉar la enhavo de Flash ja estas arbitra programo.", + "flash_fail": "Malsukcesis enlegi enhavon de Flash; vidu detalojn en konzolo." }, "image_cropper": { "crop_picture": "Tondi bildon", @@ -83,7 +90,8 @@ "interactions": "Interagoj", "administration": "Administrado", "bookmarks": "Legosignoj", - "timelines": "Historioj" + "timelines": "Historioj", + "home_timeline": "Hejma historio" }, "notifications": { "broken_favorite": "Nekonata stato, serĉante ĝin…", @@ -115,10 +123,10 @@ "direct_warning": "Ĉi tiu afiŝo estos videbla nur por ĉiuj menciitaj uzantoj.", "posting": "Afiŝante", "scope": { - "direct": "Rekta – Afiŝi nur al menciitaj uzantoj", - "private": "Nur abonantoj – Afiŝi nur al abonantoj", - "public": "Publika – Afiŝi al publikaj historioj", - "unlisted": "Nelistigita – Ne afiŝi al publikaj historioj" + "direct": "Rekta – afiŝi nur al menciitaj uzantoj", + "private": "Nur abonantoj – afiŝi nur al abonantoj", + "public": "Publika – afiŝi al publikaj historioj", + "unlisted": "Nelistigita – ne afiŝi al publikaj historioj" }, "scope_notice": { "unlisted": "Ĉi tiu afiŝo ne estos videbla en la Publika historio kaj La tuta konata reto", @@ -131,7 +139,8 @@ "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" + "media_description": "Priskribo de vidaŭdaĵo", + "post": "Afiŝo" }, "registration": { "bio": "Priskribo", @@ -139,7 +148,7 @@ "fullname": "Prezenta nomo", "password_confirm": "Konfirmo de pasvorto", "registration": "Registriĝo", - "token": "Invita ĵetono", + "token": "Invita peco", "captcha": "TESTO DE HOMECO", "new_captcha": "Klaku la bildon por akiri novan teston", "username_placeholder": "ekz. lain", @@ -152,7 +161,10 @@ "password_required": "ne povas resti malplena", "password_confirmation_required": "ne povas resti malplena", "password_confirmation_match": "samu la pasvorton" - } + }, + "reason_placeholder": "Ĉi-node oni aprobas registriĝojn permane.\nSciigu la administrantojn kial vi volas registriĝi.", + "reason": "Kialo registriĝi", + "register": "Registriĝi" }, "settings": { "app_name": "Nomo de aplikaĵo", @@ -238,9 +250,9 @@ "show_admin_badge": "Montri la insignon de administranto en mia profilo", "show_moderator_badge": "Montri la insignon de reguligisto en mia profilo", "nsfw_clickthrough": "Ŝalti traklakan kaŝadon de kunsendaĵoj kaj antaŭmontroj de ligiloj por konsternaj statoj", - "oauth_tokens": "Ĵetonoj de OAuth", - "token": "Ĵetono", - "refresh_token": "Ĵetono de aktualigo", + "oauth_tokens": "Pecoj de OAuth", + "token": "Peco", + "refresh_token": "Aktualiga peco", "valid_until": "Valida ĝis", "revoke_token": "Senvalidigi", "panelRadius": "Bretoj", @@ -365,7 +377,8 @@ "post": "Afiŝoj/Priskriboj de uzantoj", "alert_neutral": "Neŭtrala", "alert_warning": "Averto", - "toggled": "Ŝaltita" + "toggled": "Ŝaltita", + "wallpaper": "Fonbildo" }, "radii": { "_tab_label": "Rondeco" @@ -516,7 +529,34 @@ "mute_import_error": "Eraris enporto de silentigoj", "mute_import": "Enporto de silentigoj", "mute_export_button": "Elportu viajn silentigojn al CSV-dosiero", - "mute_export": "Elporto de silentigoj" + "mute_export": "Elporto de silentigoj", + "hide_wallpaper": "Kaŝi fonbildon de nodo", + "setting_changed": "Agordo malsamas de la implicita", + "more_settings": "Pliaj agordoj", + "sensitive_by_default": "Implicite marki afiŝojn konsternaj", + "reply_visibility_following_short": "Montri respondojn por miaj abonatoj", + "hide_all_muted_posts": "Kaŝi silentigitajn afiŝojn", + "hide_media_previews": "Kaŝi antaŭrigardojn al vidaŭdaĵoj", + "word_filter": "Vortofiltro", + "reply_visibility_self_short": "Montri nur respondojn por mi", + "file_export_import": { + "errors": { + "file_slightly_new": "Etversio de dosiero malsamas, iuj agordoj eble ne funkcios", + "file_too_old": "Nekonforma ĉefa versio: {fileMajor}, versio de dosiero estas tro malnova kaj nesubtenata (minimuma estas {feMajor})", + "file_too_new": "Nekonforma ĉefa versio: {fileMajor}, ĉi tiu PleromaFE (agordoj je versio {feMajor}) tro malnovas por tio", + "invalid_file": "La elektita dosiero ne estas subtenata savkopio de agordoj de Pleroma. Nenio ŝanĝiĝis." + }, + "restore_settings": "Rehavi agordojn el dosiero", + "backup_settings_theme": "Savkopii agordojn kaj haŭton al dosiero", + "backup_settings": "Savkopii agordojn al dosiero", + "backup_restore": "Savkopio de agordoj" + }, + "right_sidebar": "Montri flankan breton dekstre", + "save": "Konservi ŝanĝojn", + "hide_shoutbox": "Kaŝi kriujon de nodo", + "always_show_post_button": "Ĉiam montri ŝvebantan butonon por nova afiŝo", + "mentions_new_style": "Pli mojosaj menciligiloj", + "mentions_new_place": "Meti menciojn sur apartan linion" }, "timeline": { "collapse": "Maletendi", @@ -530,7 +570,9 @@ "no_more_statuses": "Neniuj pliaj statoj", "no_statuses": "Neniuj statoj", "reload": "Enlegi ree", - "error": "Eraris akirado de historio: {0}" + "error": "Eraris akirado de historio: {0}", + "socket_reconnected": "Realtempa konekto fariĝis", + "socket_broke": "Realtempa konekto perdiĝis: CloseEvent code {0}" }, "user_card": { "approve": "Aprobi", @@ -541,7 +583,6 @@ "follow": "Aboni", "follow_sent": "Peto sendiĝis!", "follow_progress": "Petante…", - "follow_again": "Ĉu sendi peton ree?", "follow_unfollow": "Malaboni", "followees": "Abonatoj", "followers": "Abonantoj", @@ -586,7 +627,15 @@ "show_repeats": "Montri ripetojn", "hide_repeats": "Kaŝi ripetojn", "unsubscribe": "Ne ricevi sciigojn", - "subscribe": "Ricevi sciigojn" + "subscribe": "Ricevi sciigojn", + "bot": "Roboto", + "highlight": { + "side": "Flanka strio", + "striped": "Stria fono", + "solid": "Unueca fono", + "disabled": "Senemfaze" + }, + "edit_profile": "Redakti profilon" }, "user_profile": { "timeline_title": "Historio de uzanto", @@ -612,7 +661,8 @@ "error": { "base": "Alŝuto malsukcesis.", "file_too_big": "Dosiero estas tro granda [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", - "default": "Reprovu pli poste" + "default": "Reprovu pli poste", + "message": "Malsukcesis alŝuto: {0}" }, "file_size_units": { "B": "B", @@ -645,7 +695,9 @@ "votes": "voĉoj", "option": "Elekteblo", "add_option": "Aldoni elekteblon", - "add_poll": "Aldoni enketon" + "add_poll": "Aldoni enketon", + "votes_count": "{count} voĉdono | {count} voĉdonoj", + "people_voted_count": "{count} persono voĉdonis | {count} personoj voĉdonis" }, "importer": { "error": "Eraris enporto de ĉi tiu dosiero.", @@ -670,7 +722,7 @@ "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 el la historio de «La tuta konata reto»", + "ftl_removal": "Forigo el la historio de «Konata reto»", "quarantine_desc": "Ĉi tiu nodo sendos nur publikajn afiŝojn al la jenaj nodoj:", "quarantine": "Kvaranteno", "reject_desc": "Ĉi tiu nodo ne akceptos mesaĝojn de la jenaj nodoj:", @@ -678,7 +730,7 @@ "accept_desc": "Ĉi tiu nodo nur akceptas mesaĝojn de la jenaj nodoj:", "accept": "Akcepti", "simple_policies": "Specialaj politikoj de la nodo", - "ftl_removal_desc": "Ĉi tiu nodo forigas la jenajn nodojn el la historio de «La tuta konata reto»:" + "ftl_removal_desc": "Ĉi tiu nodo forigas la jenajn nodojn el la historio de «Konata reto»:" }, "mrf_policies": "Ŝaltis politikon de Mesaĝa ŝanĝilaro (MRF)", "keyword": { @@ -732,7 +784,12 @@ "repeats": "Ripetoj", "favorites": "Ŝatoj", "status_deleted": "Ĉi tiu afiŝo foriĝis", - "nsfw": "Konsterna" + "nsfw": "Konsterna", + "expand": "Etendi", + "external_source": "Ekstera fonto", + "mentions": "Mencioj", + "you": "(Vi)", + "plus_more": "+{number} pli" }, "time": { "years_short": "{0}j", diff --git a/src/i18n/es.json b/src/i18n/es.json @@ -34,12 +34,19 @@ "enable": "Habilitar", "confirm": "Confirmar", "verify": "Verificar", - "peek": "Ojear", + "peek": "Previsualizar", "close": "Cerrar", "dismiss": "Descartar", "retry": "Inténtalo de nuevo", "error_retry": "Por favor, inténtalo de nuevo", - "loading": "Cargando…" + "loading": "Cargando…", + "role": { + "admin": "Administrador/a", + "moderator": "Moderador/a" + }, + "flash_content": "Haga clic para mostrar contenido Flash usando Ruffle (experimental, puede que no funcione).", + "flash_security": "Tenga en cuenta que esto puede ser potencialmente peligroso ya que el contenido Flash sigue siendo código arbitrario.", + "flash_fail": "No se pudo cargar el contenido flash, consulte la consola para obtener más detalles." }, "image_cropper": { "crop_picture": "Recortar la foto", @@ -82,8 +89,8 @@ "friend_requests": "Solicitudes de seguimiento", "mentions": "Menciones", "interactions": "Interacciones", - "dms": "Mensajes Directos", - "public_tl": "Línea Temporal Pública", + "dms": "Mensajes directos", + "public_tl": "Línea temporal pública", "timeline": "Línea Temporal", "twkn": "Red Conocida", "user_search": "Búsqueda de Usuarios", @@ -92,7 +99,8 @@ "preferences": "Preferencias", "chats": "Chats", "timelines": "Líneas de Tiempo", - "bookmarks": "Marcadores" + "bookmarks": "Marcadores", + "home_timeline": "Línea temporal personal" }, "notifications": { "broken_favorite": "Estado desconocido, buscándolo…", @@ -120,7 +128,9 @@ "expiry": "Tiempo de vida de la encuesta", "expires_in": "La encuesta termina en {0}", "expired": "La encuesta terminó hace {0}", - "not_enough_options": "Muy pocas opciones únicas en la encuesta" + "not_enough_options": "Muy pocas opciones únicas en la encuesta", + "people_voted_count": "{count} persona votó | {count} personas votaron", + "votes_count": "{count} voto | {count} votos" }, "emoji": { "stickers": "Pegatinas", @@ -137,14 +147,14 @@ "add_sticker": "Añadir Pegatina" }, "interactions": { - "favs_repeats": "Favoritos y Repetidos", + "favs_repeats": "Favoritos y repetidos", "follows": "Nuevos seguidores", "load_older": "Cargar interacciones más antiguas", - "moves": "Usuario Migrado" + "moves": "Usuario migrado" }, "post_status": { "new_status": "Publicar un nuevo estado", - "account_not_locked_warning": "Tu cuenta no está {0}. Cualquiera puede seguirte y leer las entradas para Solo-Seguidores.", + "account_not_locked_warning": "Tu cuenta no está {0}. Cualquiera puede seguirte y leer las publicaciones para Solo-Seguidores.", "account_not_locked_warning_link": "bloqueada", "attachments_sensitive": "Contenido sensible", "content_type": { @@ -164,16 +174,17 @@ "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", - "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" + "direct": "Directo - solo para los usuarios mencionados", + "private": "Solo-seguidores - solo tus seguidores leerán la publicación", + "public": "Público - publicaciones visibles en las líneas temporales públicas", + "unlisted": "Sin listar -publicaciones no visibles en las líneas temporales públicas" }, "media_description_error": "Error al actualizar el archivo, inténtalo de nuevo", "empty_status_error": "No se puede publicar un estado vacío y sin archivos adjuntos", "preview_empty": "Vacío", "preview": "Vista previa", - "media_description": "Descripción multimedia" + "media_description": "Descripción multimedia", + "post": "Publicar" }, "registration": { "bio": "Biografía", @@ -194,7 +205,10 @@ "password_required": "no puede estar vacío", "password_confirmation_required": "no puede estar vacío", "password_confirmation_match": "la contraseña no coincide" - } + }, + "reason_placeholder": "Los registros de esta instancia son aprobados manualmente.\nComéntanos por qué quieres registrarte aquí.", + "reason": "Razón para registrarse", + "register": "Registrarse" }, "selectable_list": { "select_all": "Seleccionar todo" @@ -227,7 +241,7 @@ "attachmentRadius": "Adjuntos", "attachments": "Adjuntos", "avatar": "Avatar", - "avatarAltRadius": "Avatares (Notificaciones)", + "avatarAltRadius": "Avatares (notificaciones)", "avatarRadius": "Avatares", "background": "Fondo", "bio": "Biografía", @@ -245,19 +259,19 @@ "change_password": "Cambiar contraseña", "change_password_error": "Hubo un problema cambiando la contraseña.", "changed_password": "¡Contraseña cambiada correctamente!", - "collapse_subject": "Colapsar entradas con tema", + "collapse_subject": "Colapsar publicaciones con tema", "composing": "Redactando", "confirm_new_password": "Confirmar la nueva contraseña", "current_avatar": "Tu avatar actual", "current_password": "Contraseña actual", "current_profile_banner": "Tu cabecera actual", - "data_import_export_tab": "Importar / Exportar Datos", + "data_import_export_tab": "Importar / Exportar datos", "default_vis": "Alcance de visibilidad por defecto", "delete_account": "Eliminar la cuenta", "discoverable": "Permitir la aparición de esta cuenta en los resultados de búsqueda y otros servicios", "delete_account_description": "Eliminar para siempre los datos y desactivar la cuenta.", "pad_emoji": "Rellenar con espacios al agregar emojis desde el selector", - "delete_account_error": "Hubo un error al eliminar tu cuenta. Si el fallo persiste, ponte en contacto con el administrador de tu instancia.", + "delete_account_error": "Hubo un error al eliminar tu cuenta. Si el fallo persiste, ponte en contacto con el/la administrador/a de tu instancia.", "delete_account_instructions": "Escribe tu contraseña para confirmar la eliminación de tu cuenta.", "avatar_size_instruction": "El tamaño mínimo recomendado para el avatar es de 150X150 píxeles.", "export_theme": "Exportar tema", @@ -277,7 +291,7 @@ "hide_isp": "Ocultar el panel específico de la instancia", "preload_images": "Precargar las imágenes", "use_one_click_nsfw": "Abrir los adjuntos NSFW con un solo click", - "hide_post_stats": "Ocultar las estadísticas de las entradas (p.ej. el número de favoritos)", + "hide_post_stats": "Ocultar las estadísticas de las publicaciones (p.ej. el número de favoritos)", "hide_user_stats": "Ocultar las estadísticas del usuario (p.ej. el número de seguidores)", "hide_filtered_statuses": "Ocultar estados filtrados", "import_blocks_from_a_csv_file": "Importar lista de usuarios bloqueados dese un archivo csv", @@ -299,22 +313,22 @@ "play_videos_in_modal": "Reproducir los vídeos en un marco emergente", "use_contain_fit": "No recortar los adjuntos en miniaturas", "name": "Nombre", - "name_bio": "Nombre y Biografía", + "name_bio": "Nombre y biografía", "new_password": "Nueva contraseña", "notification_visibility": "Tipos de notificaciones a mostrar", "notification_visibility_follows": "Nuevos seguidores", - "notification_visibility_likes": "Me gustan (Likes)", + "notification_visibility_likes": "Favoritos", "notification_visibility_mentions": "Menciones", "notification_visibility_repeats": "Repeticiones (Repeats)", - "no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las entradas", + "no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las publicaciones", "no_blocks": "No hay usuarios bloqueados", "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", "hide_followers_count_description": "No mostrar el número de cuentas que me siguen", - "show_admin_badge": "Mostrar la insignia de Administrador en mi perfil", - "show_moderator_badge": "Mostrar la insignia de Moderador en mi perfil", + "show_admin_badge": "Mostrar la insignia de \"Administrador/a\" en mi perfil", + "show_moderator_badge": "Mostrar la insignia de \"Moderador/a\" en mi perfil", "nsfw_clickthrough": "Habilitar la ocultación de la imagen de vista previa del enlace y el adjunto para los estados NSFW por defecto", "oauth_tokens": "Tokens de OAuth", "token": "Token", @@ -324,8 +338,8 @@ "panelRadius": "Paneles", "pause_on_unfocused": "Parar la transmisión cuando no estés en foco", "presets": "Por defecto", - "profile_background": "Fondo del Perfil", - "profile_banner": "Cabecera del Perfil", + "profile_background": "Imagen de fondo del perfil", + "profile_banner": "Imagen de cabecera del perfil", "profile_tab": "Perfil", "radii_help": "Establezca el redondeo de las esquinas de la interfaz (en píxeles)", "replies_in_timeline": "Réplicas en la línea temporal", @@ -356,7 +370,7 @@ "theme": "Tema", "theme_help": "Use códigos de color hexadecimales (#rrggbb) para personalizar su tema de colores.", "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.", + "theme_help_v2_2": "Los iconos debajo de algunas publicaciones 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", "user_settings": "Ajustes del Usuario", @@ -476,7 +490,7 @@ "panelHeader": "Cabecera del panel", "topBar": "Barra superior", "avatar": "Avatar del usuario (en la vista del perfil)", - "avatarStatus": "Avatar del usuario (en la vista de la entrada)", + "avatarStatus": "Avatar del usuario (en la vista de la publicación)", "popup": "Ventanas y textos emergentes (popups & tooltips)", "button": "Botones", "buttonHover": "Botón (encima)", @@ -517,8 +531,8 @@ }, "version": { "title": "Versión", - "backend_version": "Versión del Backend", - "frontend_version": "Versión del Frontend" + "backend_version": "Versión del backend", + "frontend_version": "Versión del frontend" }, "notification_visibility_moves": "Usuario Migrado", "greentext": "Texto verde (meme arrows)", @@ -529,7 +543,7 @@ "fun": "Divertido", "type_domains_to_mute": "Buscar dominios para silenciar", "useStreamingApiWarning": "(no recomendado, experimental, puede omitir publicaciones)", - "useStreamingApi": "Recibir entradas y notificaciones en tiempo real", + "useStreamingApi": "Recibir publicaciones y notificaciones en tiempo real", "user_mutes": "Usuarios", "reset_profile_background": "Restablecer el fondo de pantalla", "reset_background_confirm": "¿Estás seguro de restablecer el fondo de pantalla?", @@ -562,7 +576,33 @@ "mute_import": "Importar silenciados", "mute_export_button": "Exportar los silenciados a un archivo csv", "mute_export": "Exportar silenciados", - "hide_wallpaper": "Ocultar el fondo de pantalla de la instancia" + "hide_wallpaper": "Ocultar el fondo de pantalla de la instancia", + "setting_changed": "La configuración es diferente a la predeterminada", + "hide_all_muted_posts": "Ocultar las publicaciones silenciadas", + "more_settings": "Más opciones", + "sensitive_by_default": "Identificar las publicaciones como sensibles de forma predeterminada", + "reply_visibility_self_short": "Mostrar respuestas solo a uno mismo", + "reply_visibility_following_short": "Mostrar las réplicas a mis seguidores", + "hide_media_previews": "Ocultar la vista previa multimedia", + "word_filter": "Filtro de palabras", + "save": "Guardar los cambios", + "file_export_import": { + "errors": { + "invalid_file": "El archivo seleccionado no es válido como copia de seguridad de Pleroma. No se han realizado cambios.", + "file_too_new": "Versión principal incompatible: {fileMajor}, este \"FrontEnd\" de Pleroma (versión de configuración {feMajor}) es demasiado antiguo para manejarlo", + "file_too_old": "Versión principal incompatible: {fileMajor}, la versión del archivo es demasiado antigua y no es compatible (versión mínima {FeMajor})", + "file_slightly_new": "La versión secundaria del archivo es diferente, es posible que algunas configuraciones no se carguen" + }, + "restore_settings": "Restaurar ajustes desde archivo", + "backup_settings_theme": "Descargar la copia de seguridad de la configuración y del tema", + "backup_settings": "Descargar la copia de seguridad de la configuración", + "backup_restore": "Copia de seguridad de la configuración" + }, + "hide_shoutbox": "Ocultar cuadro de diálogo de la instancia", + "right_sidebar": "Mostrar la barra lateral a la derecha", + "always_show_post_button": "Muestra siempre el botón flotante de Nueva Plubicación", + "mentions_new_style": "Enlaces de menciones más elegantes", + "mentions_new_place": "Situa las menciones en una línea separada" }, "time": { "day": "{0} día", @@ -610,7 +650,9 @@ "no_more_statuses": "No hay más estados", "no_statuses": "Sin estados", "reload": "Recargar", - "error": "Error obteniendo la linea de tiempo:{0}" + "error": "Error obteniendo la linea de tiempo:{0}", + "socket_broke": "Conexión en timpo real perdida: código del motivo {0}", + "socket_reconnected": "Establecida la conexión en tiempo real" }, "status": { "favorites": "Favoritos", @@ -634,10 +676,13 @@ "status_unavailable": "Estado no disponible", "bookmark": "Marcar", "unbookmark": "Desmarcar", - "status_deleted": "Esta entrada ha sido eliminada", + "status_deleted": "Esta publicación ha sido eliminada", "nsfw": "NSFW (No apropiado para el trabajo)", "expand": "Expandir", - "external_source": "Fuente externa" + "external_source": "Fuente externa", + "mentions": "Menciones", + "you": "(Tú)", + "plus_more": "+{number} más" }, "user_card": { "approve": "Aprobar", @@ -648,7 +693,6 @@ "follow": "Seguir", "follow_sent": "¡Solicitud enviada!", "follow_progress": "Solicitando…", - "follow_again": "¿Enviar solicitud de nuevo?", "follow_unfollow": "Dejar de seguir", "followees": "Siguiendo", "followers": "Seguidores", @@ -673,10 +717,10 @@ "mute_progress": "Silenciando…", "admin_menu": { "moderation": "Moderación", - "grant_admin": "Conceder permisos de Administrador", - "revoke_admin": "Revocar permisos de Administrador", - "grant_moderator": "Conceder permisos de Moderador", - "revoke_moderator": "Revocar permisos de Moderador", + "grant_admin": "Conceder permisos de Administrador/a", + "revoke_admin": "Revocar permisos de Administrador/a", + "grant_moderator": "Conceder permisos de Moderador/a", + "revoke_moderator": "Revocar permisos de Moderador/a", "activate_account": "Activar cuenta", "deactivate_account": "Desactivar cuenta", "delete_account": "Eliminar cuenta", @@ -693,16 +737,28 @@ "show_repeats": "Mostrar repetidos", "hide_repeats": "Ocultar repetidos", "message": "Mensaje", - "hidden": "Oculto" + "hidden": "Oculto", + "roles": { + "moderator": "Moderador", + "admin": "Administrador" + }, + "highlight": { + "striped": "Fondo rayado", + "side": "Raya lateral", + "solid": "Fondo sólido", + "disabled": "Sin resaltado" + }, + "bot": "Bot", + "edit_profile": "Edita el perfil" }, "user_profile": { - "timeline_title": "Linea Temporal del Usuario", + "timeline_title": "Línea temporal del usuario", "profile_does_not_exist": "Lo sentimos, este perfil no existe.", "profile_loading_error": "Lo sentimos, hubo un error al cargar este perfil." }, "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:", + "add_comment_description": "El informe será enviado a los/las moderadores/as de su instancia. Puedes proporcionar una explicación de por qué estás reportando esta cuenta a continuación:", "additional_comments": "Comentarios adicionales", "forward_description": "La cuenta es de otro servidor. ¿Enviar una copia del informe allí también?", "forward_to": "Reenviar a {0}", @@ -714,7 +770,7 @@ "who_to_follow": "A quién seguir" }, "tool_tip": { - "media_upload": "Subir Medios", + "media_upload": "Subir multimedia", "repeat": "Repetir", "reply": "Contestar", "favorite": "Favorito", @@ -772,12 +828,12 @@ "simple": { "accept_desc": "Esta instancia solo acepta mensajes de las siguientes instancias:", "media_nsfw_desc": "Esta instancia obliga a que los archivos multimedia se establezcan como sensibles en las publicaciones de las siguientes instancias:", - "media_nsfw": "Forzar Multimedia Como Sensible", + "media_nsfw": "Forzar contenido multimedia como sensible", "media_removal_desc": "Esta instancia elimina los archivos multimedia de las publicaciones de las siguientes instancias:", "media_removal": "Eliminar Multimedia", "quarantine": "Cuarentena", - "ftl_removal_desc": "Esta instancia elimina las siguientes instancias de la línea de tiempo \"Toda la red conocida\":", - "ftl_removal": "Eliminar de la línea de tiempo \"Toda La Red Conocida\"", + "ftl_removal_desc": "Esta instancia elimina las siguientes instancias de la línea de tiempo \"Red Conocida\":", + "ftl_removal": "Eliminar de la línea de tiempo \"Red Conocida\"", "quarantine_desc": "Esta instancia enviará solo publicaciones públicas a las siguientes instancias:", "simple_policies": "Políticas específicas de la instancia", "reject_desc": "Esta instancia no aceptará mensajes de las siguientes instancias:", diff --git a/src/i18n/eu.json b/src/i18n/eu.json @@ -14,7 +14,8 @@ "text_limit": "Testu limitea", "title": "Ezaugarriak", "who_to_follow": "Nori jarraitu", - "pleroma_chat_messages": "Pleroma Txata" + "pleroma_chat_messages": "Pleroma Txata", + "upload_limit": "Kargatzeko muga" }, "finder": { "error_fetching_user": "Errorea erabiltzailea eskuratzen", @@ -38,7 +39,14 @@ "dismiss": "Baztertu", "retry": "Saiatu berriro", "error_retry": "Saiatu berriro mesedez", - "loading": "Kargatzen…" + "loading": "Kargatzen…", + "role": { + "moderator": "Moderatzailea", + "admin": "Administratzailea" + }, + "flash_content": "Klik egin Flash edukia erakusteko Ruffle erabilita (esperimentala, baliteke ez ibiltzea).", + "flash_security": "Kontuan izan arriskutsua izan daitekeela, Flash edukia kode arbitrarioa baita.", + "flash_fail": "Ezin izan da Flash edukia kargatu. Ikusi kontsola xehetasunetarako." }, "image_cropper": { "crop_picture": "Moztu argazkia", @@ -81,8 +89,8 @@ "friend_requests": "Jarraitzeko eskaerak", "mentions": "Aipamenak", "interactions": "Interakzioak", - "dms": "Zuzeneko Mezuak", - "public_tl": "Denbora-lerro Publikoa", + "dms": "Zuzeneko mezuak", + "public_tl": "Denbora-lerro publikoa", "timeline": "Denbora-lerroa", "twkn": "Ezagutzen den Sarea", "user_search": "Erabiltzailea Bilatu", @@ -91,7 +99,8 @@ "preferences": "Hobespenak", "chats": "Txatak", "timelines": "Denbora-lerroak", - "bookmarks": "Laster-markak" + "bookmarks": "Laster-markak", + "home_timeline": "Denbora-lerro pertsonala" }, "notifications": { "broken_favorite": "Egoera ezezaguna, bilatzen…", @@ -104,7 +113,8 @@ "no_more_notifications": "Ez dago jakinarazpen gehiago", "reacted_with": "{0}kin erreakzionatu zuen", "migrated_to": "hona migratua:", - "follow_request": "jarraitu nahi zaitu" + "follow_request": "jarraitu nahi zaitu", + "error": "Errorea jakinarazpenak eskuratzean: {0}" }, "polls": { "add_poll": "Inkesta gehitu", @@ -118,7 +128,9 @@ "expiry": "Inkestaren iraupena", "expires_in": "Inkesta {0} bukatzen da", "expired": "Inkesta {0} bukatu zen", - "not_enough_options": "Aukera gutxiegi inkestan" + "not_enough_options": "Aukera gutxiegi inkestan", + "votes_count": "{count} boto| {count} boto", + "people_voted_count": "Pertsona batek bozkatu du | {count} pertsonak bozkatu dute" }, "emoji": { "stickers": "Pegatinak", @@ -128,7 +140,8 @@ "add_emoji": "Emoji bat gehitu", "custom": "Ohiko emojiak", "unicode": "Unicode emojiak", - "load_all": "{emojiAmount} emoji guztiak kargatzen" + "load_all": "{emojiAmount} emoji guztiak kargatzen", + "load_all_hint": "Lehenengo {saneAmount} emojia kargatuta, emoji guztiak kargatzeak errendimendu arazoak sor ditzake." }, "stickers": { "add_sticker": "Pegatina gehitu" @@ -136,7 +149,8 @@ "interactions": { "favs_repeats": "Errepikapen eta gogokoak", "follows": "Jarraitzaile berriak", - "load_older": "Kargatu elkarrekintza zaharragoak" + "load_older": "Kargatu elkarrekintza zaharragoak", + "moves": "Erabiltzailea migratuta" }, "post_status": { "new_status": "Mezu berri bat idatzi", @@ -160,18 +174,24 @@ "unlisted": "Mezu hau ez da argitaratuko Denbora-lerro Publikoan ezta Ezagutzen den Sarean" }, "scope": { - "direct": "Zuzena: Bidali aipatutako erabiltzaileei besterik ez", - "private": "Jarraitzaileentzako bakarrik: Bidali jarraitzaileentzat bakarrik", - "public": "Publikoa: Bistaratu denbora-lerro publikoetan", + "direct": "Zuzena: bidali aipatutako erabiltzaileei besterik ez", + "private": "Jarraitzaileentzako bakarrik: bidali jarraitzaileentzat bakarrik", + "public": "Publikoa: bistaratu denbora-lerro publikoetan", "unlisted": "Zerrendatu gabea: ez bidali denbora-lerro publikoetara" - } + }, + "media_description_error": "Ezin izan da artxiboa eguneratu, saiatu berriro", + "preview": "Aurrebista", + "media_description": "Media deskribapena", + "preview_empty": "Hutsik", + "post": "Bidali", + "empty_status_error": "Ezin da argitaratu ezer idatzi gabe edo eranskinik gabe" }, "registration": { "bio": "Biografia", "email": "E-posta", "fullname": "Erakutsi izena", "password_confirm": "Pasahitza berretsi", - "registration": "Izena ematea", + "registration": "Sortu kontua", "token": "Gonbidapen txartela", "captcha": "CAPTCHA", "new_captcha": "Klikatu irudia captcha berri bat lortzeko", @@ -185,7 +205,10 @@ "password_required": "Ezin da hutsik utzi", "password_confirmation_required": "Ezin da hutsik utzi", "password_confirmation_match": "Pasahitzaren berdina izan behar du" - } + }, + "reason": "Kontua sortzeko arrazoia", + "reason_placeholder": "Instantzia honek kontu berriak eskuz onartzen ditu.\nJakinarazi administrazioari zergatik erregistratu nahi duzun.", + "register": "Erregistratu" }, "selectable_list": { "select_all": "Hautatu denak" @@ -202,7 +225,7 @@ "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.", - "recovery_codes": "Berreskuratze kodea", + "recovery_codes": "Berreskuratze kodea.", "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", @@ -218,7 +241,7 @@ "attachmentRadius": "Eranskinak", "attachments": "Eranskinak", "avatar": "Avatarra", - "avatarAltRadius": "Avatarra (Aipamenak)", + "avatarAltRadius": "Abatarra (aipamenak)", "avatarRadius": "Avatarrak", "background": "Atzeko planoa", "bio": "Biografia", @@ -242,7 +265,7 @@ "current_avatar": "Zure uneko avatarra", "current_password": "Indarrean dagoen pasahitza", "current_profile_banner": "Zure profilaren banner-a", - "data_import_export_tab": "Datuak Inportatu / Esportatu", + "data_import_export_tab": "Datuak inportatu / esportatu", "default_vis": "Lehenetsitako ikusgaitasunak", "delete_account": "Ezabatu kontua", "discoverable": "Baimendu zure kontua kanpo bilaketa-emaitzetan eta bestelako zerbitzuetan agertzea", @@ -304,19 +327,19 @@ "hide_followers_description": "Ez erakutsi nor ari den ni jarraitzen", "hide_follows_count_description": "Ez erakutsi jarraitzen ari naizen kontuen kopurua", "hide_followers_count_description": "Ez erakutsi nire jarraitzaileen kontuen kopurua", - "show_admin_badge": "Erakutsi Administratzaile etiketa nire profilan", - "show_moderator_badge": "Erakutsi Moderatzaile etiketa nire profilan", + "show_admin_badge": "Erakutsi \"Administratzaile\" etiketa nire profilan", + "show_moderator_badge": "Erakutsi \"Moderatzaile\" etiketa nire profilan", "nsfw_clickthrough": "Gaitu klika hunkigarri eranskinak ezkutatzeko", "oauth_tokens": "OAuth tokenak", "token": "Tokena", - "refresh_token": "Berrgin Tokena", - "valid_until": "Baliozkoa Arte", + "refresh_token": "Berrgin tokena", + "valid_until": "Baliozkoa arte", "revoke_token": "Ezeztatu", "panelRadius": "Panelak", "pause_on_unfocused": "Eguneraketa automatikoa gelditu fitxatik kanpo", "presets": "Aurrezarpenak", "profile_background": "Profilaren atzeko planoa", - "profile_banner": "Profilaren Banner-a", + "profile_banner": "Profilaren banner-a", "profile_tab": "Profila", "radii_help": "Konfiguratu interfazearen ertzen biribiltzea (pixeletan)", "replies_in_timeline": "Denbora-lerroko erantzunak", @@ -460,7 +483,7 @@ "button": "Botoia", "text": "Hamaika {0} eta {1}", "mono": "edukia", - "input": "Jadanik Los Angeles-en", + "input": "Jadanik Los Angeles-en.", "faint_link": "laguntza", "fine_print": "Irakurri gure {0} ezer erabilgarria ikasteko!", "header_faint": "Ondo dago", @@ -470,9 +493,13 @@ }, "version": { "title": "Bertsioa", - "backend_version": "Backend Bertsioa", - "frontend_version": "Frontend Bertsioa" - } + "backend_version": "Backend bertsioa", + "frontend_version": "Frontend bertsioa" + }, + "save": "Aldaketak gorde", + "setting_changed": "Ezarpena lehenetsitakoaren desberdina da", + "allow_following_move": "Baimendu jarraipen automatikoa, jarraitzen duzun kontua beste instantzia batera eramaten denean", + "new_email": "E-posta berria" }, "time": { "day": "{0} egun", @@ -542,7 +569,6 @@ "follow": "Jarraitu", "follow_sent": "Eskaera bidalita!", "follow_progress": "Eskatzen…", - "follow_again": "Eskaera berriro bidali?", "follow_unfollow": "Jarraitzeari utzi", "followees": "Jarraitzen", "followers": "Jarraitzaileak", @@ -657,7 +683,7 @@ "federation": "Federazioa", "simple": { "media_nsfw_desc": "Instantzia honek hurrengo instantzien multimediak sentikorrak izatera behartzen ditu:", - "media_nsfw": "Behartu Multimedia Sentikor", + "media_nsfw": "Behartu multimedia sentikor moduan", "media_removal_desc": "Instantzia honek atxikitutako multimedia hurrengo instantzietatik ezabatzen ditu:", "media_removal": "Multimedia Ezabatu", "ftl_removal_desc": "Instantzia honek hurrengo instantziak ezabatzen ditu \"Ezagutzen den Sarea\" denbora-lerrotik:", @@ -683,5 +709,12 @@ }, "shoutbox": { "title": "Oihu-kutxa" + }, + "errors": { + "storage_unavailable": "Pleromak ezin izan du nabigatzailearen biltegira sartu. Hasiera-saioa edo tokiko ezarpenak ez dira gordeko eta ustekabeko arazoak sor ditzake. Saiatu cookie-ak gaitzen." + }, + "remote_user_resolver": { + "searching_for": "Bilatzen", + "error": "Ez da aurkitu." } } diff --git a/src/i18n/fi.json b/src/i18n/fi.json @@ -579,7 +579,8 @@ "hide_full_subject": "Piilota koko otsikko", "show_content": "Näytä sisältö", "hide_content": "Piilota sisältö", - "status_deleted": "Poistettu viesti" + "status_deleted": "Poistettu viesti", + "you": "(sinä)" }, "user_card": { "approve": "Hyväksy", @@ -589,7 +590,6 @@ "follow": "Seuraa", "follow_sent": "Pyyntö lähetetty!", "follow_progress": "Pyydetään…", - "follow_again": "Lähetä pyyntö uudestaan?", "follow_unfollow": "Älä seuraa", "followees": "Seuraa", "followers": "Seuraajat", diff --git a/src/i18n/fr.json b/src/i18n/fr.json @@ -9,16 +9,17 @@ "features_panel": { "chat": "Chat", "gopher": "Gopher", - "media_proxy": "Proxy média", + "media_proxy": "Proxy pièce-jointes", "scope_options": "Options de visibilité", - "text_limit": "Limite de texte", - "title": "Caractéristiques", - "who_to_follow": "Personnes à suivre", - "pleroma_chat_messages": "Chat Pleroma" + "text_limit": "Limite du texte", + "title": "Fonctionnalités", + "who_to_follow": "Suggestions de suivis", + "pleroma_chat_messages": "Chat Pleroma", + "upload_limit": "Limite de téléversement" }, "finder": { - "error_fetching_user": "Erreur lors de la recherche de l'utilisateur·ice", - "find_user": "Chercher un-e utilisateur·ice" + "error_fetching_user": "Erreur lors de la recherche du compte", + "find_user": "Rechercher un compte" }, "general": { "apply": "Appliquer", @@ -26,19 +27,26 @@ "more": "Plus", "generic_error": "Une erreur s'est produite", "optional": "optionnel", - "show_more": "Montrer plus", - "show_less": "Montrer moins", + "show_more": "Afficher plus", + "show_less": "Afficher moins", "cancel": "Annuler", "disable": "Désactiver", "enable": "Activer", "confirm": "Confirmer", "verify": "Vérifier", - "dismiss": "Rejeter", + "dismiss": "Ignorer", "peek": "Jeter un coup d'œil", "close": "Fermer", "retry": "Réessayez", "error_retry": "Veuillez réessayer", - "loading": "Chargement…" + "loading": "Chargement…", + "role": { + "moderator": "Modo'", + "admin": "Admin" + }, + "flash_content": "Clique pour afficher le contenu Flash avec Ruffle (Expérimental, peut ne pas fonctionner).", + "flash_security": "Cela reste potentiellement dangereux, Flash restant du code arbitraire.", + "flash_fail": "Échec de chargement du contenu Flash, voir la console pour les détails." }, "image_cropper": { "crop_picture": "Rogner l'image", @@ -47,7 +55,7 @@ "cancel": "Annuler" }, "importer": { - "submit": "Soumettre", + "submit": "Envoyer", "success": "Importé avec succès.", "error": "Une erreur est survenue pendant l'import de ce fichier." }, @@ -56,17 +64,17 @@ "description": "Connexion avec OAuth", "logout": "Déconnexion", "password": "Mot de passe", - "placeholder": "p.e. lain", + "placeholder": "ex. 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", + "enter_two_factor_code": "Entrez un code double-facteur", "recovery_code": "Code de récupération", "heading": { - "totp": "Authentification à double authentification", - "recovery": "Récuperation de la double authentification" + "totp": "Authentification à double-facteur", + "recovery": "Récupération de l'authentification à double-facteur" } }, "media_modal": { @@ -78,24 +86,26 @@ "back": "Retour", "chat": "Chat local", "friend_requests": "Demandes de suivi", - "mentions": "Notifications", + "mentions": "Mentions", "interactions": "Interactions", "dms": "Messages directs", - "public_tl": "Fil d'actualité public", - "timeline": "Fil d'actualité", + "public_tl": "Flux publique", + "timeline": "Flux personnel", "twkn": "Réseau connu", - "user_search": "Recherche d'utilisateur·ice", - "who_to_follow": "Qui suivre", + "user_search": "Recherche de comptes", + "who_to_follow": "Suggestion de suivit", "preferences": "Préférences", "search": "Recherche", "administration": "Administration", "chats": "Chats", - "bookmarks": "Marques-Pages" + "bookmarks": "Marques-Pages", + "timelines": "Flux", + "home_timeline": "Flux personnel" }, "notifications": { - "broken_favorite": "Message inconnu, chargement…", + "broken_favorite": "Message inconnu, recherche en cours…", "favorited_you": "a aimé votre statut", - "followed_you": "a commencé à vous suivre", + "followed_you": "vous suit", "load_older": "Charger les notifications précédentes", "notifications": "Notifications", "read": "Lu !", @@ -103,7 +113,8 @@ "no_more_notifications": "Aucune notification supplémentaire", "migrated_to": "a migré à", "reacted_with": "a réagi avec {0}", - "follow_request": "veut vous suivre" + "follow_request": "veut vous suivre", + "error": "Erreur de chargement des notifications : {0}" }, "interactions": { "favs_repeats": "Partages et favoris", @@ -115,7 +126,7 @@ "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", + "attachments_sensitive": "Marquer les pièce-jointes comme sensible", "content_type": { "text/plain": "Texte brut", "text/html": "HTML", @@ -130,32 +141,33 @@ "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" + "unlisted": "Ce statut ne sera pas visible dans le Flux Public et le Flux Fédéré" }, "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" + "private": "Abonné·e·s uniquement - Seul·e·s vos abonné·e·s verront vos status", + "public": "Publique - Afficher dans les flux publics", + "unlisted": "Non-Listé - Ne pas afficher dans les flux publics" }, "media_description_error": "Échec de téléversement du media, essayez encore", - "empty_status_error": "Impossible de poster un statut vide sans attachements", + "empty_status_error": "Impossible de poster un statut vide sans pièces-jointes", "preview_empty": "Vide", "preview": "Prévisualisation", - "media_description": "Description de l'attachement" + "media_description": "Description de la pièce-jointe", + "post": "Post" }, "registration": { "bio": "Biographie", - "email": "Adresse mail", + "email": "Courriel", "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.", + "username_placeholder": "ex. lain", + "fullname_placeholder": "ex. Lain Iwakura", + "bio_placeholder": "ex.\nSalut, je suis Lain\nJe suis une héroïne d'animation 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", @@ -163,7 +175,10 @@ "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" - } + }, + "reason_placeholder": "Cette instance modère les inscriptions manuellement.\nExpliquer ce qui motive votre inscription à l'administration.", + "reason": "Motivation d'inscription", + "register": "Enregistrer" }, "selectable_list": { "select_all": "Tout selectionner" @@ -177,20 +192,20 @@ "setup_otp": "Configurer OTP", "wait_pre_setup_otp": "préconfiguration OTP", "confirm_and_enable": "Confirmer & activer OTP", - "title": "Double authentification", + "title": "Authentification double-facteur", "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.", + "warning_of_generate_new_codes": "Quand vous générez de nouveaux 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", + "recovery_codes_warning": "Écrivez ces codes ou sauvegardez les dans un endroit 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 verrouillé en dehors de votre compte.", + "authentication_methods": "Méthodes d'authentification", "scan": { "title": "Scanner", - "desc": "En utilisant votre application de double authentification, scannez ce QR code ou entrez la clé textuelle :", + "desc": "En utilisant votre application d'authentification à double-facteur, 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 :" + "desc": "Pour activer l'authentification à double-facteur, entrez le code donné par votre application :" } }, "attachmentRadius": "Pièces jointes", @@ -201,10 +216,10 @@ "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_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_imported": "Blocages importés ! Le traitement va prendre un moment.", "blocks_tab": "Bloqué·e·s", "btnRadius": "Boutons", "cBlue": "Bleu (répondre, suivre)", @@ -224,31 +239,31 @@ "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_error": "Il y a eu un problème lors de la tentative de suppression de votre compte. Si le problème persiste, contactez l'administration 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": "Filtrage", "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.", + "follow_export": "Exporter les suivis", + "follow_export_button": "Exporter les suivis dans un fichier CSV", + "follow_import": "Import des suivis", + "follow_import_error": "Erreur lors de l'importation des suivis", + "follows_imported": "Suivis 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", + "hide_attachments_in_tl": "Masquer les pièces jointes dans le flux", + "hide_muted_posts": "Masquer les statuts des comptes masqués", "max_thumbnails": "Nombre maximum de miniatures par statuts", - "hide_isp": "Masquer le panneau spécifique a l'instance", + "hide_isp": "Masquer le panneau de 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)", + "use_one_click_nsfw": "Ouvrir les pièces-jointes sensibles avec un seul clic", + "hide_post_stats": "Masquer les statistiques des messages (ex. le nombre de favoris)", + "hide_user_stats": "Masquer les statistiques de compte (ex. le nombre de suivis)", "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_blocks_from_a_csv_file": "Import de blocages depuis un fichier CSV", + "import_followers_from_a_csv_file": "Import de suivis depuis un fichier CSV", "import_theme": "Charger le thème", "inputRadius": "Champs de texte", "checkboxRadius": "Cases à cocher", @@ -269,8 +284,8 @@ "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_follows": "Suivis", + "notification_visibility_likes": "Favoris", "notification_visibility_mentions": "Mentionnés", "notification_visibility_repeats": "Partages", "no_rich_text_description": "Ne formatez pas le texte", @@ -278,9 +293,9 @@ "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", + "show_admin_badge": "Afficher le badge d'Admin sur mon profil", + "show_moderator_badge": "Afficher le badge de Modo' sur mon profil", + "nsfw_clickthrough": "Activer le clic pour dévoiler les pièces jointes et cacher l'aperçu des liens pour les statuts marqués comme sensibles", "oauth_tokens": "Jetons OAuth", "token": "Jeton", "refresh_token": "Rafraichir le jeton", @@ -289,11 +304,11 @@ "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_background": "Image de fond de profil", "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", + "replies_in_timeline": "Réponses dans le flux", "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", @@ -309,7 +324,7 @@ "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_input_always_show": "Toujours afficher le champ 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", @@ -348,7 +363,7 @@ "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.", + "v2_imported": "Le fichier que vous avez importé vient d'une version antérieure. Nous essayons de maximizer la compatibilité mais il peut 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.", @@ -409,7 +424,13 @@ "tabs": "Onglets", "toggled": "(Dés)activé", "highlight": "Éléments mis en valeur", - "popover": "Infobulles, menus" + "popover": "Infobulles, menus", + "chat": { + "border": "Bordure", + "outgoing": "Sortant(s)", + "incoming": "Entrant(s)" + }, + "wallpaper": "Fond d'écran" }, "radii": { "_tab_label": "Rondeur" @@ -426,7 +447,7 @@ "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.", + "avatar_inset": "Veuillez noter que combiner à la fois les ombres internes et non-internes sur les avatars peut fournir des résultats inattendus 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}" }, @@ -481,15 +502,15 @@ }, "change_email": "Changer de courriel", "domain_mutes": "Domaines", - "pad_emoji": "Rajouter un espace autour de l'émoji après l’avoir choisit", + "pad_emoji": "Entourer les émoji d'espaces après leur sélections", "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", + "type_domains_to_mute": "Chercher 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.", + "allow_following_move": "Activer le suivit automatique à la migration des comptes", + "change_email_error": "Il y a eu un problème pour changer 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", @@ -504,31 +525,75 @@ "accent": "Accent", "chatMessageRadius": "Message de chat", "bot": "Ce compte est un robot", - "import_mutes_from_a_csv_file": "Importer les masquages depuis un fichier CSV", + "import_mutes_from_a_csv_file": "Import de masquages depuis un fichier CSV", "mutes_imported": "Masquages importés ! Leur application peut prendre du temps.", "mute_import_error": "Erreur à l'import des masquages", "mute_import": "Import des masquages", "mute_export_button": "Exporter vos masquages dans un fichier CSV", - "mute_export": "Export des masquages" + "mute_export": "Export des masquages", + "notification_setting_hide_notification_contents": "Cacher l'expéditeur et le contenu des notifications push", + "notification_setting_block_from_strangers": "Bloquer les notifications des utilisateur⋅ice⋅s que vous ne suivez pas", + "virtual_scrolling": "Optimiser le rendu des flux", + "reset_background_confirm": "Voulez-vraiment réinitialiser l'arrière-plan ?", + "reset_banner_confirm": "Voulez-vraiment réinitialiser la bannière ?", + "reset_avatar_confirm": "Voulez-vraiment réinitialiser l'avatar ?", + "reset_profile_banner": "Réinitialiser la bannière du profil", + "reset_profile_background": "Réinitialiser le fond du profil", + "reset_avatar": "Réinitialiser l'avatar", + "profile_fields": { + "value": "Contenu", + "name": "Nom du champ", + "add_field": "Ajouter un champ", + "label": "Champs du profil" + }, + "hide_media_previews": "Cacher la prévisualisation des pièces jointes", + "mutes_and_blocks": "Masquage et Blocages", + "setting_changed": "Préférence modifiée", + "more_settings": "Plus de préférences", + "sensitive_by_default": "Marquer les messages comme sensible par défaut", + "reply_visibility_self_short": "Uniquement les réponses à moi", + "reply_visibility_following_short": "Montrer les réponses à mes suivis", + "hide_wallpaper": "Cacher le fond d'écran", + "hide_all_muted_posts": "Cacher les messages masqués", + "word_filter": "Filtrage par mots", + "save": "Enregistrer les changements", + "file_export_import": { + "backup_settings_theme": "Sauvegarder les paramètres et le thème dans un fichier", + "errors": { + "invalid_file": "Le fichier sélectionné n'est pas un format supporté pour les sauvegarde Pleroma. Aucun changement n'a été fait.", + "file_too_new": "Version majeure incompatible. {fileMajor}, ce PleromaFE ({feMajor}) est trop ancien", + "file_too_old": "Version majeure incompatible : {fileMajor}, la version du fichier est trop vielle et n'est plus supportée (vers. min. {feMajor})", + "file_slightly_new": "La version mineure du fichier est différente, quelques paramètres on pût ne pas chargés" + }, + "backup_restore": "Sauvegarde des Paramètres", + "backup_settings": "Sauvegarder les paramètres dans un fichier", + "restore_settings": "Restaurer les paramètres depuis un fichier" + }, + "hide_shoutbox": "Cacher la shoutbox de l'instance", + "right_sidebar": "Afficher le paneau latéral à droite" }, "timeline": { "collapse": "Fermer", "conversation": "Conversation", "error_fetching": "Erreur en cherchant les mises à jour", - "load_older": "Afficher plus", + "load_older": "Afficher des status plus ancien", "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" + "no_statuses": "Aucun statuts", + "reload": "Recharger", + "error": "Erreur lors de l'affichage du flux : {0}", + "socket_broke": "Connexion temps-réel perdue : CloseEvent code {0}", + "socket_reconnected": "Connexion temps-réel établie" }, "status": { "favorites": "Favoris", "repeats": "Partages", "delete": "Supprimer statuts", - "pin": "Agraffer sur le profil", - "unpin": "Dégraffer du profil", + "pin": "Agrafer sur le profil", + "unpin": "Dégrafer du profil", "pinned": "Agraffé", "delete_confirm": "Voulez-vous vraiment supprimer ce statuts ?", "reply_to": "Réponse à", @@ -536,7 +601,19 @@ "mute_conversation": "Masquer la conversation", "unmute_conversation": "Démasquer la conversation", "status_unavailable": "Status indisponible", - "copy_link": "Copier le lien au status" + "copy_link": "Copier le lien au status", + "expand": "Développer", + "nsfw": "Contenu sensible", + "status_deleted": "Ce post a été effacé", + "hide_content": "Cacher le contenu", + "show_content": "Montrer le contenu", + "hide_full_subject": "Cacher le sujet", + "show_full_subject": "Montrer le sujet en entier", + "thread_muted_and_words": ", contient les mots :", + "thread_muted": "Fil de discussion masqué", + "external_source": "Source externe", + "unbookmark": "Supprimer des favoris", + "bookmark": "Ajouter aux favoris" }, "user_card": { "approve": "Accepter", @@ -547,7 +624,6 @@ "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", @@ -591,10 +667,23 @@ "subscribe": "Abonner", "unsubscribe": "Désabonner", "hide_repeats": "Cacher les partages", - "show_repeats": "Montrer les partages" + "show_repeats": "Montrer les partages", + "roles": { + "moderator": "Modérateur⋅ice", + "admin": "Administrateur⋅ice" + }, + "message": "Message", + "highlight": { + "disabled": "Sans mise-en-valeur", + "solid": "Fond uni", + "side": "Coté rayé", + "striped": "Fond rayé" + }, + "bot": "Robot", + "edit_profile": "Éditer le profil" }, "user_profile": { - "timeline_title": "Journal de l'utilisateur⋅ice", + "timeline_title": "Flux du compte", "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." }, @@ -619,54 +708,56 @@ "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" + "reject_follow_request": "Rejeter la demande de suivit", + "bookmark": "Favori" }, "upload": { "error": { "base": "L'envoi a échoué.", "file_too_big": "Fichier trop gros [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", - "default": "Réessayez plus tard" + "default": "Réessayez plus tard", + "message": "Envoi échoué : {0}" }, "file_size_units": { - "B": "O", - "KiB": "KiO", - "MiB": "MiO", - "GiB": "GiO", - "TiB": "TiO" + "B": "o", + "KiB": "Ko", + "MiB": "Mo", + "GiB": "Go", + "TiB": "To" } }, "about": { "mrf": { "keyword": { - "reject": "Rejeté", - "replace": "Remplacer", - "keyword_policies": "Politiques par mot-clés", + "reject": "Rejette", + "replace": "Remplace", + "keyword_policies": "Filtrage par mots-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", + "accept": "Acceptées", + "accept_desc": "Cette instance accepte les messages seulement depuis ces instances :", + "reject": "Rejetées", "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", + "quarantine_desc": "Cette instance enverra seulement des messages publics à ces instances :", + "ftl_removal_desc": "Cette instance supprime les instance suivantes du flux fédéré :", + "media_removal": "Suppression des pièce-jointes", "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 :" + "ftl_removal": "Supprimées du flux fédéré", + "media_nsfw_desc": "Cette instance force les pièce-jointes comme sensible pour les messages des instances suivantes :" }, "federation": "Fédération", - "mrf_policies": "Politiques MRF activées", + "mrf_policies": "Politiques MRF actives", "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": "Masqué", "mute_progress": "Masquage…", "unmute": "Démasquer", "unmute_progress": "Démasquage…" @@ -683,7 +774,9 @@ "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}" + "expired": "Sondage terminé il y a {0}", + "people_voted_count": "{count} voteur | {count} voteurs", + "votes_count": "{count} vote | {count} votes" }, "emoji": { "emoji": "Émoji", @@ -694,11 +787,11 @@ "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" + "keep_open": "Garder ouvert" }, "remote_user_resolver": { "error": "Non trouvé.", - "searching_for": "Rechercher", + "searching_for": "Recherche pour", "remote_user_resolver": "Résolution de compte distant" }, "time": { @@ -759,5 +852,27 @@ }, "shoutbox": { "title": "Shoutbox" + }, + "display_date": { + "today": "Aujourd'hui" + }, + "file_type": { + "file": "Fichier", + "image": "Image", + "video": "Vidéo", + "audio": "Audio" + }, + "chats": { + "empty_chat_list_placeholder": "Vous n'avez pas encore de discussions. Démarrez-en une nouvelle !", + "error_sending_message": "Quelque chose s'est mal passé pendant l'envoi du message.", + "error_loading_chat": "Quelque chose s'est mal passé au chargement de la discussion.", + "delete_confirm": "Voulez-vous vraiment effacer ce message ?", + "more": "Plus", + "empty_message_error": "Impossible d'envoyer un message vide", + "new": "Nouvelle discussion", + "chats": "Discussions", + "delete": "Effacer", + "message_user": "Message à {nickname}", + "you": "Vous :" } } diff --git a/src/i18n/he.json b/src/i18n/he.json @@ -312,7 +312,6 @@ "follow": "עקוב", "follow_sent": "בקשה נשלחה!", "follow_progress": "מבקש…", - "follow_again": "שלח בקשה שוב?", "follow_unfollow": "בטל עקיבה", "followees": "נעקבים", "followers": "עוקבים", diff --git a/src/i18n/id.json b/src/i18n/id.json @@ -0,0 +1,631 @@ +{ + "settings": { + "style": { + "preview": { + "link": "sebuah tautan yang kecil nan bagus", + "header": "Pratinjau", + "error": "Contoh kesalahan", + "button": "Tombol", + "input": "Baru saja mendarat di L.A.", + "faint_link": "manual berguna", + "fine_print": "Baca {0} kami untuk belajar sesuatu yang tak ada gunanya!", + "header_faint": "Ini baik-baik saja", + "checkbox": "Saya telah membaca sekilas syarat dan ketentuan" + }, + "advanced_colors": { + "alert_neutral": "Neutral", + "alert_warning": "Peringatan", + "alert_error": "Kesalahan", + "_tab_label": "Lanjutan", + "post": "Postingan/Bio pengguna", + "popover": "Tooltip, menu, popover", + "badge_notification": "Notifikasi", + "top_bar": "Bar atas", + "borders": "", + "buttons": "Tombol", + "wallpaper": "Latar belakang", + "panel_header": "Header panel", + "icons": "Ikon-ikon", + "disabled": "Dinonaktifkan" + }, + "common_colors": { + "main": "Warna umum", + "_tab_label": "Umum" + }, + "common": { + "contrast": { + "context": { + "text": "untuk teks", + "18pt": "Untuk teks besar (18pt+)" + } + }, + "color": "Warna" + }, + "switcher": { + "help": { + "upgraded_from_v2": "PleromaFE telah diperbarui, tema dapat terlihat sedikit berbeda dari apa yang Anda ingat.", + "future_version_imported": "Berkas yang Anda impor dibuat pada versi FE yang lebih baru.", + "older_version_imported": "Berkas yang Anda impor dibuat pada versi FE yang lebih lama.", + "fe_upgraded": "Mesin tema PleromaFE diperbarui setelah pembaruan versi." + }, + "use_source": "Versi baru", + "use_snapshot": "Versi lama", + "load_theme": "Muat tema" + }, + "fonts": { + "_tab_label": "Font", + "components": { + "interface": "Antarmuka", + "post": "Teks postingan" + }, + "family": "Nama font", + "size": "Ukuran (dalam px)", + "weight": "Berat (ketebalan)" + }, + "shadows": { + "components": { + "panel": "Panel", + "panelHeader": "Header panel" + } + } + }, + "notification_setting_privacy": "Privasi", + "notifications": "Notifikasi", + "values": { + "true": "ya", + "false": "tidak" + }, + "user_settings": "Pengaturan Pengguna", + "upload_a_photo": "Unggah foto", + "theme": "Tema", + "text": "Teks", + "settings": "Pengaturan", + "security_tab": "Keamanan", + "saving_ok": "Pengaturan disimpan", + "profile_tab": "Profil", + "profile_background": "Latar belakang profil", + "token": "Token", + "oauth_tokens": "Token OAuth", + "show_moderator_badge": "Tampilkan lencana \"Moderator\" di profil saya", + "show_admin_badge": "Tampilkan lencana \"Admin\" di profil saya", + "new_password": "Kata sandi baru", + "new_email": "Surel baru", + "name_bio": "Nama & bio", + "name": "Nama", + "profile_fields": { + "value": "Isi", + "name": "Label", + "label": "Metadata profil" + }, + "limited_availability": "Tidak tersedia di browser Anda", + "invalid_theme_imported": "Berkas yang dipilih bukan sebuah tema yang didukung Pleroma. Tidak ada perbuahan yang dibuat pada tema Anda.", + "interfaceLanguage": "Bahasa antarmuka", + "interface": "Antarmuka", + "instance_default_simple": "(bawaan)", + "instance_default": "(bawaan: {value})", + "general": "Umum", + "delete_account_error": "Ada masalah ketika menghapus akun Anda. Jika ini terus terjadi harap hubungi adminstrator instansi Anda.", + "delete_account_description": "Hapus data Anda secara permanen dan menonaktifkan akun Anda.", + "delete_account": "Hapus akun", + "data_import_export_tab": "Impor / ekspor data", + "current_password": "Kata sandi saat ini", + "confirm_new_password": "Konfirmasi kata sandi baru", + "version": { + "title": "Versi", + "backend_version": "Versi backend", + "frontend_version": "Versi frontend" + }, + "security": "Keamanan", + "changed_password": "Kata sandi berhasil diubah!", + "change_password_error": "Ada masalah ketika mengubah kata sandi Anda.", + "change_password": "Ubah kata sandi", + "changed_email": "Surel berhasil diubah!", + "change_email_error": "Ada masalah ketika mengubah surel Anda.", + "change_email": "Ubah surel", + "cRed": "Merah (Batal)", + "cBlue": "Biru (Balas, ikuti)", + "btnRadius": "Tombol", + "bot": "Ini adalah akun bot", + "block_export": "Ekspor blokiran", + "bio": "Bio", + "background": "Latar belakang", + "avatarRadius": "Avatar", + "avatar": "Avatar", + "attachments": "Lampiran", + "mfa": { + "scan": { + "title": "Pindai" + }, + "confirm_and_enable": "Konfirmasi & aktifkan OTP", + "setup_otp": "Siapkan OTP", + "otp": "OTP", + "recovery_codes_warning": "Tulis kode-kode nya atau simpan mereka di tempat yang aman - jika tidak Anda tidak akan melihat mereka lagi. Jika Anda tidak dapat mengakses aplikasi 2FA Anda dan kode pemulihan Anda hilang Anda tidak akan bisa mengakses akun Anda.", + "authentication_methods": "Metode otentikasi", + "recovery_codes": "Kode pemulihan.", + "warning_of_generate_new_codes": "Ketika Anda menghasilkan kode pemulihan baru, kode lama Anda berhenti bekerja.", + "generate_new_recovery_codes": "Hasilkan kode pemulihan baru", + "title": "Otentikasi Dua-faktor", + "waiting_a_recovery_codes": "Menerima kode cadangan…", + "verify": { + "desc": "Untuk mengaktifkan otentikasi dua-faktor, masukkan kode dari aplikasi dua-faktor Anda:" + } + }, + "app_name": "Nama aplikasi", + "save": "Simpan perubahan", + "valid_until": "Valid hingga", + "follow_import_error": "Terjadi kesalahan ketika mengimpor pengikut", + "emoji_reactions_on_timeline": "Tampilkan reaksi emoji pada linimasa", + "chatMessageRadius": "Pesan obrolan", + "cOrange": "Jingga (Favorit)", + "avatarAltRadius": "Avatar (notifikasi)", + "hide_shoutbox": "Sembunyikan kotak suara instansi", + "hide_followers_count_description": "Jangan tampilkan jumlah pengikut", + "hide_follows_count_description": "Jangan tampilkan jumlah mengikuti", + "hide_followers_description": "Jangan tampilkan siapa yang mengikuti saya", + "hide_follows_description": "Jangan tampilkan siapa yang saya ikuti", + "notification_visibility_emoji_reactions": "Reaksi", + "notification_visibility_follows": "Diikuti", + "notification_visibility_moves": "Pengguna Bermigrasi", + "notification_visibility_repeats": "Ulangan", + "notification_visibility_mentions": "Sebutan", + "notification_visibility_likes": "Favorit", + "notification_visibility": "Jenis notifikasi yang perlu ditampilkan", + "links": "Tautan", + "hide_user_stats": "Sembunyikan statistik pengguna (contoh. jumlah pengikut)", + "hide_post_stats": "Sembunyikan statistik postingan (contoh. jumlah favorit)", + "use_one_click_nsfw": "Buka lampiran NSFW hanya dengan satu klik", + "hide_wallpaper": "Sembunyikan latar belakang instansi", + "blocks_imported": "Blokiran diimpor! Pemrosesannya mungkin memakan sedikit waktu.", + "block_import_error": "Terjadi kesalahan ketika mengimpor blokiran", + "block_import": "Impor blokiran", + "block_export_button": "Ekspor blokiran Anda menjadi berkas csv", + "blocks_tab": "Blokiran", + "delete_account_instructions": "Ketik kata sandi Anda pada input di bawah untuk mengkonfirmasi penghapusan akun.", + "mutes_and_blocks": "Bisuan dan Blokiran", + "enter_current_password_to_confirm": "Masukkan kata sandi Anda saat ini untuk mengkonfirmasi identitas Anda", + "filtering": "Penyaringan", + "word_filter": "Penyaring kata", + "avatar_size_instruction": "Ukuran minimum gambar avatar yang disarankan adalah 150x150 piksel.", + "attachmentRadius": "Lampiran", + "cGreen": "Hijau (Retweet)", + "max_thumbnails": "Jumlah thumbnail maksimum per postingan", + "loop_video": "Ulang-ulang video", + "loop_video_silent_only": "Ulang-ulang video tanpa suara (seperti \"gif\" Mastodon)", + "pause_on_unfocused": "Jeda aliran ketika tab di dalam fokus", + "reply_visibility_following": "Hanya tampilkan balasan yang ditujukan kepada saya atau orang yang saya ikuti", + "reply_visibility_following_short": "Tampilkan balasan ke orang yang saya ikuti", + "saving_err": "Terjadi kesalahan ketika menyimpan pengaturan", + "search_user_to_block": "Cari siapa yang Anda ingin blokir", + "search_user_to_mute": "Cari siapa yang ingin Anda bisukan", + "set_new_avatar": "Tetapkan avatar baru", + "set_new_profile_background": "Tetapkan latar belakang profil baru", + "subject_line_behavior": "Salin subyek ketika membalas", + "subject_line_email": "Seperti surel: \"re: subyek\"", + "subject_line_mastodon": "Seperti mastodon: salin saja", + "subject_line_noop": "Jangan salin", + "useStreamingApiWarning": "(Tidak disarankan, eksperimental, diketahui dapat melewati postingan-postingan)", + "fun": "Seru", + "enable_web_push_notifications": "Aktifkan notifikasi push web", + "more_settings": "Lebih banyak pengaturan", + "reply_visibility_all": "Tampilkan semua balasan", + "reply_visibility_self": "Hanya tampilkan balasan yang ditujukan kepada saya", + "hide_muted_posts": "Sembunyikan postingan-postingan dari pengguna yang dibisukan", + "import_blocks_from_a_csv_file": "Impor blokiran dari berkas csv", + "domain_mutes": "Domain", + "composing": "Menulis", + "no_blocks": "Tidak ada yang diblokir", + "no_mutes": "Tidak ada yang dibisukan" + }, + "about": { + "mrf": { + "keyword": { + "reject": "Tolak", + "is_replaced_by": "→" + }, + "simple": { + "quarantine_desc": "Instansi ini hanya akan mengirim postingan publik ke instansi-instansi berikut:", + "quarantine": "Karantina", + "reject_desc": "Instansi ini tidak akan menerima pesan dari instansi-instansi berikut:", + "reject": "Tolak", + "accept_desc": "Instansi ini hanya menerima pesan dari instansi-instansi berikut:", + "accept": "Terima", + "media_removal": "Penghapusan Media", + "media_removal_desc": "Instansi ini menghapus media dari postingan yang berasal dari instansi-instansi berikut:" + }, + "federation": "Federasi", + "mrf_policies": "Kebijakan MRF yang diaktifkan" + }, + "staff": "Staf" + }, + "time": { + "day": "{0} hari", + "days": "{0} hari", + "day_short": "{0}h", + "days_short": "{0}h", + "hour": "{0} jam", + "hours": "{0} jam", + "hour_short": "{0}j", + "hours_short": "{0}j", + "in_future": "dalam {0}", + "in_past": "{0} yang lalu", + "minute": "{0} menit", + "minutes": "{0} menit", + "minute_short": "{0}m", + "minutes_short": "{0}m", + "month": "{0} bulan", + "months": "{0} bulan", + "month_short": "{0}b", + "months_short": "{0}b", + "now": "baru saja", + "now_short": "sekarang", + "second": "{0} detik", + "seconds": "{0} detik", + "second_short": "{0}d", + "seconds_short": "{0}d", + "week": "{0} pekan", + "weeks": "{0} pekan", + "week_short": "{0}p", + "weeks_short": "{0}p", + "year": "{0} tahun", + "years": "{0} tahun", + "year_short": "{0}t", + "years_short": "{0}t" + }, + "timeline": { + "conversation": "Percakapan", + "error": "Terjadi kesalahan memuat linimasa: {0}", + "no_retweet_hint": "Postingan ditandai sebagai hanya-pengikut atau langsung dan tidak dapat diulang", + "repeated": "diulangi", + "reload": "Muat ulang", + "no_more_statuses": "Tidak ada status lagi", + "no_statuses": "Tidak ada status" + }, + "status": { + "favorites": "Favorit", + "repeats": "Ulangan", + "delete": "Hapus status", + "pin": "Sematkan di profil", + "unpin": "Berhenti menyematkan dari profil", + "pinned": "Disematkan", + "delete_confirm": "Apakah Anda benar-benar ingin menghapus status ini?", + "reply_to": "Balas ke", + "replies_list": "Balasan:", + "mute_conversation": "Bisukan percakapan", + "unmute_conversation": "Berhenti membisikan percakapan", + "status_unavailable": "Status tidak tersedia", + "thread_muted_and_words": ", memiliki kata:", + "hide_content": "", + "show_content": "", + "status_deleted": "Postingan ini telah dihapus", + "nsfw": "NSFW" + }, + "user_card": { + "block": "Blokir", + "blocked": "Diblokir!", + "deny": "Tolak", + "edit_profile": "Sunting profil", + "favorites": "Favorit", + "follow": "Ikuti", + "follow_sent": "Permintaan dikirim!", + "follow_progress": "Meminta…", + "mute": "Bisukan", + "muted": "Dibisukan", + "per_day": "per hari", + "report": "Laporkan", + "statuses": "Status", + "unblock": "Berhenti memblokir", + "block_progress": "Memblokir…", + "unmute": "Berhenti membisukan", + "mute_progress": "Membisukan…", + "hide_repeats": "Sembunyikan ulangan", + "show_repeats": "Tampilkan ulangan", + "bot": "Bot", + "admin_menu": { + "moderation": "Moderasi", + "activate_account": "Aktifkan akun", + "deactivate_account": "Nonaktifkan akun", + "delete_account": "Hapus akun", + "force_nsfw": "Tandai semua postingan sebagai NSFW", + "strip_media": "Hapus media dari postingan-postingan", + "delete_user": "Hapus pengguna", + "delete_user_confirmation": "Apakah Anda benar-benar yakin? Tindakan ini tidak dapat dibatalkan." + }, + "follow_unfollow": "Berhenti mengikuti", + "followees": "Mengikuti", + "followers": "Pengikut", + "following": "Diikuti!", + "follows_you": "Mengikuti Anda!", + "hidden": "Disembunyikan", + "its_you": "Ini Anda!", + "media": "Media", + "mention": "Sebut", + "message": "Kirimkan pesan" + }, + "user_profile": { + "timeline_title": "Linimasa pengguna", + "profile_does_not_exist": "Maaf, profil ini tidak ada.", + "profile_loading_error": "Maaf, terjadi kesalahan ketika memuat profil ini." + }, + "user_reporting": { + "title": "Melaporkan {0}", + "add_comment_description": "Laporan ini akan dikirim ke moderator instansi Anda. Anda dapat menyediakan penjelasan mengapa Anda melaporkan akun ini di bawah:", + "additional_comments": "Komentar tambahan", + "forward_description": "Akun ini berada di server lain. Kirim salinan dari laporannya juga?", + "submit": "Kirim", + "generic_error": "Sebuah kesalahan terjadi ketika memproses permintaan Anda." + }, + "notifications": { + "favorited_you": "memfavoritkan status Anda", + "reacted_with": "bereaksi dengan {0}", + "no_more_notifications": "Tidak ada notifikasi lagi", + "repeated_you": "mengulangi status Anda", + "read": "Dibaca!", + "notifications": "Notifikasi", + "follow_request": "ingin mengikuti Anda", + "followed_you": "mengikuti Anda", + "error": "Terjadi kesalahan ketika memuat notifikasi: {0}", + "migrated_to": "bermigrasi ke", + "load_older": "Muat notifikasi yang lebih lama", + "broken_favorite": "Status tak diketahui, mencarinya…" + }, + "who_to_follow": { + "more": "Lebih banyak" + }, + "tool_tip": { + "media_upload": "Unggah media", + "repeat": "Ulangi", + "reply": "Balas", + "favorite": "Favorit", + "add_reaction": "Tambahkan Reaksi", + "user_settings": "Pengaturan Pengguna" + }, + "upload": { + "error": { + "base": "Pengunggahan gagal.", + "message": "Pengunggahan gagal: {0}", + "file_too_big": "Berkas terlalu besar [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "Coba lagi nanti" + }, + "file_size_units": { + "B": "B", + "KiB": "KiB", + "MiB": "MiB", + "GiB": "GiB", + "TiB": "TiB" + } + }, + "search": { + "people": "Orang", + "hashtags": "Tagar", + "person_talking": "{count} orang berbicara", + "people_talking": "{count} orang berbicara", + "no_results": "Tidak ada hasil" + }, + "password_reset": { + "forgot_password": "Lupa kata sandi?", + "placeholder": "Surel atau nama pengguna Anda", + "return_home": "Kembali ke halaman beranda", + "too_many_requests": "Anda telah mencapai batas percobaan, coba lagi nanti.", + "instruction": "Masukkan surel atau nama pengguna Anda. Kami akan mengirimkan Anda tautan untuk mengatur ulang kata sandi.", + "password_reset": "Pengatur-ulangan kata sandi", + "password_reset_disabled": "Pengatur-ulangan kata sandi dinonaktifkan. Hubungi administrator instansi Anda.", + "password_reset_required": "Anda harus mengatur ulang kata sandi Anda untuk masuk.", + "password_reset_required_but_mailer_is_disabled": "Anda harus mengatur ulang kata sandi, tetapi pengatur-ulangan kata sandi dinonaktifkan. Silakan hubungi administrator instansi Anda." + }, + "chats": { + "you": "Anda:", + "message_user": "Kirim Pesan ke {nickname}", + "delete": "Hapus", + "chats": "Obrolan", + "new": "Obrolan Baru", + "empty_message_error": "Tidak dapat memposting pesan yang kosong", + "more": "Lebih banyak", + "delete_confirm": "Apakah Anda benar-benar ingin menghapus pesan ini?", + "error_loading_chat": "Sesuatu yang salah terjadi ketika memuat obrolan.", + "error_sending_message": "Sesuatu yang salah terjadi ketika mengirim pesan.", + "empty_chat_list_placeholder": "Anda belum memiliki obrolan. Buat sbeuah obrolan baru!" + }, + "file_type": { + "audio": "Audio", + "video": "Video", + "image": "Gambar", + "file": "Berkas" + }, + "registration": { + "bio_placeholder": "contoh.\nHai, aku Lain.\nAku seorang putri anime yang tinggal di pinggiran kota Jepang. Kamu mungkin mengenal aku dari Wired.", + "validations": { + "password_confirmation_required": "tidak boleh kosong", + "password_required": "tidak boleh kosong", + "email_required": "tidak boleh kosong", + "fullname_required": "tidak boleh kosong", + "username_required": "tidak boleh kosong" + }, + "register": "Daftar", + "fullname_placeholder": "contoh. Lain Iwakura", + "username_placeholder": "contoh. lain", + "new_captcha": "Klik gambarnya untuk mendapatkan captcha baru", + "captcha": "CAPTCHA", + "token": "Token undangan", + "password_confirm": "Konfirmasi kata sandi", + "email": "Surel", + "bio": "Bio", + "reason_placeholder": "Instansi ini menerima pendaftaran secara manual.\nBeritahu administrasinya mengapa Anda ingin mendaftar.", + "reason": "Alasan mendaftar", + "registration": "Pendaftaran" + }, + "post_status": { + "preview_empty": "Kosong", + "default": "Baru saja mendarat di L.A.", + "content_warning": "Subyek (opsional)", + "content_type": { + "text/bbcode": "BBCode", + "text/markdown": "Markdown", + "text/html": "HTML", + "text/plain": "Teks biasa" + }, + "media_description": "Keterangan media", + "attachments_sensitive": "Tandai lampiran sebagai sensitif", + "scope": { + "public": "Publik - posting ke linimasa publik", + "private": "Hanya-pengikut - posting hanya kepada pengikut", + "direct": "Langsung - posting hanya kepada pengguna yang disebut" + }, + "preview": "Pratinjau", + "post": "Posting", + "posting": "Memposting", + "direct_warning_to_first_only": "Postingan ini akan terlihat oleh pengguna yang disebutkan di awal pesan.", + "direct_warning_to_all": "Postingan ini akan terlihat oleh pengguna yang disebutkan.", + "scope_notice": { + "private": "Postingan ini akan terlihat hanya oleh pengikut Anda", + "public": "Postingan ini akan terlihat oleh siapa saja" + }, + "media_description_error": "Gagal memperbarui media, coba lagi", + "empty_status_error": "Tidak dapat memposting status kosong tanpa berkas", + "account_not_locked_warning_link": "terkunci", + "account_not_locked_warning": "Akun Anda tidak {0}. Siapapun dapat mengikuti Anda untuk melihat postingan hanya-pengikut Anda.", + "new_status": "Posting status baru" + }, + "general": { + "apply": "Terapkan", + "flash_fail": "Gagal memuat konten flash, lihat console untuk keterangan.", + "flash_security": "Harap ingat ini dapat menjadi berbahaya karena konten Flash masih termasuk arbitrary code.", + "flash_content": "Klik untuk menampilkan konten Flash menggunakan Ruffle (Eksperimental, mungkin tidak bekerja).", + "role": { + "moderator": "Moderator", + "admin": "Admin" + }, + "peek": "Intip", + "close": "Tutup", + "verify": "Verifikasi", + "confirm": "Konfirmasi", + "enable": "Aktifkan", + "disable": "Nonaktifkan", + "cancel": "Batal", + "show_less": "Tampilkan lebih sedikit", + "show_more": "Tampilkan lebih banyak", + "optional": "opsional", + "retry": "Coba lagi", + "error_retry": "Harap coba lagi", + "generic_error": "Terjadi kesalahan", + "loading": "Memuat…", + "more": "Lebih banyak", + "submit": "Kirim" + }, + "remote_user_resolver": { + "error": "Tidak ditemukan." + }, + "emoji": { + "load_all": "Memuat semua {emojiAmount} emoji", + "load_all_hint": "Memuat {saneAmount} emoji pertama, memuat semua emoji dapat menyebabkan masalah performa.", + "unicode": "Emoji unicode", + "add_emoji": "Sisipkan emoji", + "search_emoji": "Cari emoji", + "emoji": "Emoji", + "stickers": "Stiker", + "keep_open": "Tetap buka pemilih", + "custom": "Emoji kustom" + }, + "polls": { + "expired": "Japat berakhir {0} yang lalu", + "expires_in": "Japat berakhir dalam {0}", + "expiry": "Usia japat", + "type": "Jenis japat", + "vote": "Pilih", + "votes_count": "{count} suara | {count} suara", + "people_voted_count": "{count} orang memilih | {count} orang memilih", + "votes": "suara", + "option": "Opsi", + "add_option": "Tambahkan opsi", + "add_poll": "Tambahkan japat", + "not_enough_options": "Terlalu sedikit opsi yang unik pada japat" + }, + "nav": { + "preferences": "Preferensi", + "search": "Cari", + "user_search": "Pencarian Pengguna", + "home_timeline": "Linimasa beranda", + "timeline": "Linimasa", + "public_tl": "Linimasa publik", + "interactions": "Interaksi", + "mentions": "Sebutan", + "back": "Kembali", + "administration": "Administrasi", + "about": "Tentang", + "timelines": "Linimasa", + "chats": "Obrolan", + "dms": "Pesan langsung", + "friend_requests": "Ingin mengikuti" + }, + "media_modal": { + "next": "Selanjutnya", + "previous": "Sebelum" + }, + "login": { + "recovery_code": "Kode pemulihan", + "enter_recovery_code": "Masukkan kode pemulihan", + "authentication_code": "Kode otentikasi", + "hint": "Masuk untuk ikut berdiskusi", + "username": "Nama pengguna", + "register": "Daftar", + "placeholder": "contoh: lain", + "password": "Kata sandi", + "logout": "Keluar", + "description": "Masuk dengan OAuth", + "login": "Masuk", + "heading": { + "totp": "Otentikasi dua-faktor" + }, + "enter_two_factor_code": "Masukkan kode dua-faktor" + }, + "importer": { + "error": "Terjadi kesalahan ketika mnengimpor berkas ini.", + "success": "Berhasil mengimpor.", + "submit": "Kirim" + }, + "image_cropper": { + "cancel": "Batal", + "save_without_cropping": "Simpan tanpa memotong", + "save": "Simpan", + "crop_picture": "Potong gambar" + }, + "finder": { + "find_user": "Cari pengguna", + "error_fetching_user": "Terjadi kesalahan ketika memuat pengguna" + }, + "features_panel": { + "title": "Fitur-fitur", + "text_limit": "Batas teks", + "gopher": "Gopher", + "pleroma_chat_messages": "Pleroma Obrolan", + "chat": "Obrolan", + "upload_limit": "Batas unggahan" + }, + "exporter": { + "processing": "Memproses, Anda akan segera diminta untuk mengunduh berkas Anda", + "export": "Ekspor" + }, + "domain_mute_card": { + "unmute": "Berhenti membisukan", + "mute_progress": "Membisukan…", + "mute": "Bisukan", + "unmute_progress": "Memberhentikan pembisuan…" + }, + "display_date": { + "today": "Hari Ini" + }, + "selectable_list": { + "select_all": "Pilih semua" + }, + "interactions": { + "moves": "Pengguna yang bermigrasi", + "follows": "Pengikut baru", + "favs_repeats": "Ulangan dan favorit", + "load_older": "Muat interaksi yang lebih tua" + }, + "errors": { + "storage_unavailable": "Pleroma tidak dapat mengakses penyimpanan browser. Login Anda atau pengaturan lokal Anda tidak akan tersimpan dan masalah yang tidak terduga dapat terjadi. Coba mengaktifkan kuki." + }, + "shoutbox": { + "title": "Kotak Suara" + } +} diff --git a/src/i18n/it.json b/src/i18n/it.json @@ -17,13 +17,20 @@ "close": "Chiudi", "retry": "Riprova", "error_retry": "Per favore, riprova", - "loading": "Carico…" + "loading": "Carico…", + "role": { + "moderator": "Moderatore", + "admin": "Amministratore" + }, + "flash_fail": "Contenuto Flash non caricato, vedi console del browser.", + "flash_content": "Mostra contenuto Flash tramite Ruffle (funzione in prova).", + "flash_security": "Può essere pericoloso perché i contenuti in Flash sono eseguibili." }, "nav": { "mentions": "Menzioni", "public_tl": "Sequenza pubblica", "timeline": "Sequenza personale", - "twkn": "Sequenza globale", + "twkn": "Sequenza federale", "chat": "Chat della stanza", "friend_requests": "Vogliono seguirti", "about": "Informazioni", @@ -37,14 +44,15 @@ "preferences": "Preferenze", "bookmarks": "Segnalibri", "chats": "Conversazioni", - "timelines": "Sequenze" + "timelines": "Sequenze", + "home_timeline": "Sequenza personale" }, "notifications": { "followed_you": "ti segue", "notifications": "Notifiche", "read": "Letto!", "broken_favorite": "Stato sconosciuto, lo sto cercando…", - "favorited_you": "gradisce il tuo messaggio", + "favorited_you": "ha gradito", "load_older": "Carica notifiche precedenti", "repeated_you": "ha condiviso il tuo messaggio", "follow_request": "vuole seguirti", @@ -60,17 +68,17 @@ "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, una per riga", + "filtering_explanation": "Tutti i messaggi contenenti queste parole saranno silenziati, una per riga", "hide_attachments_in_convo": "Nascondi gli allegati presenti nelle conversazioni", "hide_attachments_in_tl": "Nascondi gli allegati presenti nelle sequenze", "name": "Nome", "name_bio": "Nome ed introduzione", "nsfw_clickthrough": "Fai click per visualizzare gli allegati offuscati", - "profile_background": "Sfondo della tua pagina", - "profile_banner": "Stendardo del tuo profilo", + "profile_background": "Sfondo del tuo profilo", + "profile_banner": "Gonfalone 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 stendardo per il tuo profilo", + "set_new_profile_background": "Scegli un nuovo sfondo", + "set_new_profile_banner": "Scegli un nuovo gonfalone", "settings": "Impostazioni", "theme": "Tema", "user_settings": "Impostazioni Utente", @@ -79,9 +87,9 @@ "avatarRadius": "Icone utente", "background": "Sfondo", "btnRadius": "Pulsanti", - "cBlue": "Blu (risposte, seguire)", + "cBlue": "Blu (rispondi, segui)", "cGreen": "Verde (ripeti)", - "cOrange": "Arancione (gradire)", + "cOrange": "Arancione (gradisci)", "cRed": "Rosso (annulla)", "change_password": "Cambia password", "change_password_error": "C'è stato un problema durante il cambiamento della password.", @@ -94,7 +102,7 @@ "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.", + "delete_account_instructions": "Digita la tua password nel campo sottostante per eliminare il 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", @@ -105,7 +113,7 @@ "foreground": "Primo piano", "general": "Generale", "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)", + "hide_user_stats": "Nascondi statistiche dell'utente (es. il numero di 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", @@ -114,12 +122,12 @@ "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 a seguaci approvati", + "lock_account_description": "Vaglia manualmente i nuovi seguaci", "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 muti 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_follows": "Nuovi seguaci", "notification_visibility_likes": "Preferiti", "notification_visibility_mentions": "Menzioni", "notification_visibility_repeats": "Condivisioni", @@ -134,7 +142,7 @@ "presets": "Valori predefiniti", "profile_tab": "Profilo", "radii_help": "Imposta il raggio degli angoli (in pixel)", - "replies_in_timeline": "Risposte nella sequenza personale", + "replies_in_timeline": "Risposte nelle sequenze", "reply_visibility_all": "Mostra tutte le risposte", "reply_visibility_following": "Mostra solo le risposte rivolte a me o agli utenti che seguo", "reply_visibility_self": "Mostra solo risposte rivolte a me", @@ -144,7 +152,7 @@ "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.", + "theme_help": "Usa colori esadecimali (#rrggbb) per personalizzare il tuo schema di colori.", "tooltipRadius": "Suggerimenti/avvisi", "values": { "false": "no", @@ -152,7 +160,7 @@ }, "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", + "discoverable": "Permetti la scoperta di questo profilo a 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.", @@ -163,18 +171,18 @@ "block_import": "Importa blocchi", "block_export_button": "Esporta i tuoi blocchi in un file CSV", "block_export": "Esporta blocchi", - "allow_following_move": "Consenti", + "allow_following_move": "Consenti l'iscrizione automatica ai profili traslocati", "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:", + "desc": "Con la tua applicazione bifattoriale, acquisisci il QR o inserisci il codice:", "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.", + "recovery_codes_warning": "Metti i codici al sicuro, perché non potrai più visualizzarli. 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.", @@ -193,14 +201,14 @@ "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.", + "v2_imported": "Il tema importato è stato creato per una vecchia interfaccia. Non tutto potrebbe essere come inteso.", + "upgraded_from_v2": "L'interfaccia è stata aggiornata, il tema potrebbe essere diverso da come lo ricordi.", "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.", + "snapshot_source_mismatch": "Conflitto di versione: probabilmente l'interfaccia è stata portata indietro e poi aggiornata di nuovo. Se hai modificato il tema con una vecchia versione usa il tema precedente, altrimenti puoi usare il nuovo.", "migration_napshot_gone": "Anteprima del tema non trovata, non tutto potrebbe essere come ricordi." }, "use_source": "Nuova versione", @@ -223,7 +231,7 @@ "contrast": { "context": { "text": "per il testo", - "18pt": "per il testo grande (oltre 17pt)" + "18pt": "per il testo oltre 17pt" }, "level": { "bad": "non soddisfa le linee guida di alcun livello", @@ -246,7 +254,7 @@ "selectedMenu": "Voce menù selezionata", "selectedPost": "Messaggio selezionato", "pressed": "Premuto", - "highlight": "Elementi evidenziati", + "highlight": "Elementi in risalto", "icons": "Icone", "poll": "Grafico sondaggi", "underlay": "Sottostante", @@ -308,8 +316,8 @@ "fonts": { "_tab_label": "Font", "custom": "Personalizzato", - "weight": "Peso (grassettatura)", - "size": "Dimensione (in pixel)", + "weight": "Grassettatura", + "size": "Dimensione in pixel", "family": "Nome font", "components": { "postCode": "Font a spaziatura fissa incluso in un messaggio", @@ -336,15 +344,15 @@ }, "enable_web_push_notifications": "Abilita notifiche web push", "fun": "Divertimento", - "notification_mutes": "Per non ricevere notifiche da uno specifico utente, zittiscilo.", + "notification_mutes": "Per non ricevere notifiche da uno specifico utente, silenzialo.", "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.", + "type_domains_to_mute": "Cerca domini da silenziare", + "theme_help_v2_2": "Le icone vicino alcuni elementi sono indicatori del contrasto fra testo e sfondo, passaci sopra col puntatore per ulteriori informazioni. Se usani 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", @@ -357,23 +365,23 @@ "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_mute": "Cerca utente da silenziare", "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", + "show_moderator_badge": "Mostra l'insegna di moderatore sul mio profilo", + "show_admin_badge": "Mostra l'insegna di amministratore sul mio profilo", "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_mutes": "Nessun utente silenziato", "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", + "mutes_tab": "Silenziati", "interface": "Interfaccia", "instance_default_simple": "(predefinito)", "checkboxRadius": "Caselle di selezione", @@ -383,59 +391,87 @@ "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 zilenziati", + "hide_muted_posts": "Nascondi messaggi degli utenti silenziati", "accent": "Accento", - "emoji_reactions_on_timeline": "Mostra emoji di reazione sulle sequenze", + "emoji_reactions_on_timeline": "Mostra reazioni nelle 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", + "mutes_and_blocks": "Silenziati e bloccati", "profile_fields": { "value": "Contenuto", - "name": "Etichetta", + "name": "Descrizione", "add_field": "Aggiungi campo", "label": "Metadati profilo" }, - "bot": "Questo profilo è di un robot", + "bot": "Questo è 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_profile_background": "Azzera sfondo", + "reset_profile_banner": "Azzera gonfalone", "reset_avatar_confirm": "Vuoi veramente azzerare l'icona?", - "reset_banner_confirm": "Vuoi veramente azzerare lo stendardo?", + "reset_banner_confirm": "Vuoi veramente azzerare il gonfalone?", "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", "virtual_scrolling": "Velocizza l'elaborazione delle sequenze", "import_mutes_from_a_csv_file": "Importa silenziati da un file CSV", - "mutes_imported": "Silenziati importati! Saranno elaborati a breve.", + "mutes_imported": "Silenziati importati! Elaborazione in corso.", "mute_import_error": "Errore nell'importazione", - "mute_import": "Importa silenziati", - "mute_export_button": "Esporta la tua lista di silenziati in un file CSV", + "mute_import": "Carica silenziati", + "mute_export_button": "Esporta i silenziati in un file CSV", "mute_export": "Esporta silenziati", - "hide_wallpaper": "Nascondi sfondo della stanza" + "hide_wallpaper": "Nascondi sfondo della stanza", + "setting_changed": "Valore personalizzato", + "more_settings": "Altre impostazioni", + "sensitive_by_default": "Tutti i miei messaggi sono scabrosi", + "reply_visibility_self_short": "Vedi solo risposte a te", + "reply_visibility_following_short": "Vedi risposte a messaggi di altri", + "hide_all_muted_posts": "Nascondi messaggi silenziati", + "hide_media_previews": "Nascondi anteprime", + "word_filter": "Parole filtrate", + "save": "Salva modifiche", + "file_export_import": { + "errors": { + "file_slightly_new": "Versione minore diversa, qualcosa potrebbe non combaciare.", + "file_too_old": "Versione troppo vecchia: {fileMajor}. Questa versione dell'interfaccia ({feMajor}) non supporta il file.", + "file_too_new": "Versione troppo recente: {fileMajor}. Questa versione dell'interfaccia ({feMajor}) non supporta il file.", + "invalid_file": "Il file selezionato non è un archivio supportato. Nessuna modifica è stata apportata." + }, + "restore_settings": "Carica impostazioni sul server", + "backup_settings_theme": "Archivia impostazioni e tema localmente", + "backup_settings": "Archivia impostazioni localmente", + "backup_restore": "Archiviazione impostazioni" + }, + "right_sidebar": "Mostra barra laterale a destra", + "hide_shoutbox": "Nascondi muro dei graffiti", + "mentions_new_style": "Menzioni abbreviate", + "mentions_new_place": "Segrega le menzioni", + "always_show_post_button": "Non nascondere il pulsante di composizione" }, "timeline": { "error_fetching": "Errore nell'aggiornamento", - "load_older": "Carica messaggi più vecchi", + "load_older": "Carica messaggi precedenti", "show_new": "Mostra nuovi", "up_to_date": "Aggiornato", - "collapse": "Riduci", + "collapse": "Ripiega", "conversation": "Conversazione", "no_retweet_hint": "Il messaggio è diretto o solo per seguaci e non può essere condiviso", - "repeated": "condiviso", + "repeated": "ha condiviso", "no_statuses": "Nessun messaggio", "no_more_statuses": "Fine dei messaggi", "reload": "Ricarica", - "error": "Errore nel caricare la sequenza: {0}" + "error": "Errore nel caricare la sequenza: {0}", + "socket_broke": "Connessione tempo reale interrotta: codice {0}", + "socket_reconnected": "Connesso in tempo reale" }, "user_card": { "follow": "Segui", - "followees": "Chi stai seguendo", + "followees": "Segue", "followers": "Seguaci", "following": "Seguìto!", "follows_you": "Ti segue!", @@ -449,13 +485,13 @@ "deny": "Nega", "remote_follow": "Segui da remoto", "admin_menu": { - "delete_user_confirmation": "Ne sei completamente sicuro? Quest'azione non può essere annullata.", + "delete_user_confirmation": "Ne sei completamente sicuro? Non potrai tornare indietro.", "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", + "force_unlisted": "Nascondi tutti i messaggi", "strip_media": "Rimuovi ogni allegato ai messaggi", "force_nsfw": "Oscura tutti i messaggi", "delete_account": "Elimina profilo", @@ -469,7 +505,7 @@ }, "show_repeats": "Mostra condivisioni", "hide_repeats": "Nascondi condivisioni", - "mute_progress": "Zittisco…", + "mute_progress": "Silenzio…", "unmute_progress": "Riabilito…", "unmute": "Riabilita", "block_progress": "Blocco…", @@ -478,20 +514,23 @@ "unsubscribe": "Disdici", "subscribe": "Abbònati", "report": "Segnala", - "mention": "Menzioni", + "mention": "Menziona", "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", - "roles": { - "moderator": "Moderatore", - "admin": "Amministratore" - } + "bot": "Bot", + "highlight": { + "side": "Nastro a lato", + "striped": "A righe", + "solid": "Un colore", + "disabled": "Nessun risalto" + }, + "edit_profile": "Modifica profilo" }, "chat": { "title": "Chat" @@ -547,21 +586,22 @@ "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" + "unlisted": "Nascosto - 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_first_only": "Questo messaggio sarà visibile solo agli utenti menzionati in testa.", "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", + "empty_status_error": "Aggiungi del testo o degli allegati", "preview_empty": "Vuoto", "preview": "Anteprima", "media_description_error": "Allegati non caricati, riprova", - "media_description": "Descrizione allegati" + "media_description": "Descrizione allegati", + "post": "Pubblica" }, "registration": { "bio": "Introduzione", @@ -581,11 +621,14 @@ "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" + "new_captcha": "Clicca il captcha per averne uno nuovo", + "captcha": "CAPTCHA", + "reason_placeholder": "L'amministratore esamina ciascuna richiesta.\nFornisci il motivo della tua iscrizione.", + "reason": "Motivo dell'iscrizione", + "register": "Registrati" }, "user_profile": { - "timeline_title": "Sequenza 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." }, @@ -601,7 +644,7 @@ "replace": "Sostituisci", "is_replaced_by": "→", "keyword_policies": "Regole per parole chiave", - "ftl_removal": "Rimozione dalla sequenza globale" + "ftl_removal": "Rimozione dalla sequenza federale" }, "simple": { "reject": "Rifiuta", @@ -611,8 +654,8 @@ "reject_desc": "Questa stanza rifiuterà i messaggi provenienti dalle seguenti:", "quarantine": "Quarantena", "quarantine_desc": "Questa stanza inoltrerà solo messaggi pubblici alle seguenti:", - "ftl_removal": "Rimozione dalla sequenza globale", - "ftl_removal_desc": "Questa stanza rimuove le seguenti dalla sequenza globale:", + "ftl_removal": "Rimozione dalla sequenza federale", + "ftl_removal_desc": "Questa stanza rimuove le seguenti dalla sequenza federale:", "media_removal": "Rimozione multimedia", "media_removal_desc": "Questa istanza rimuove gli allegati dalle seguenti stanze:", "media_nsfw": "Allegati oscurati d'ufficio", @@ -624,8 +667,8 @@ "staff": "Responsabili" }, "domain_mute_card": { - "mute": "Zittisci", - "mute_progress": "Zittisco…", + "mute": "Silenzia", + "mute_progress": "Procedo…", "unmute": "Ascolta", "unmute_progress": "Procedo…" }, @@ -660,11 +703,13 @@ "expiry": "Età", "expires_in": "Chiude fra {0}", "expired": "Chiuso {0} fa", - "not_enough_options": "Aggiungi altre risposte" + "not_enough_options": "Aggiungi altre risposte", + "votes_count": "{count} voto | {count} voti", + "people_voted_count": "{count} votante | {count} votanti" }, "interactions": { "favs_repeats": "Condivisi e Graditi", - "load_older": "Carica vecchie interazioni", + "load_older": "Carica interazioni precedenti", "moves": "Utenti migrati", "follows": "Nuovi seguìti" }, @@ -699,8 +744,8 @@ "favorites": "Preferiti", "hide_content": "Nascondi contenuti", "show_content": "Mostra contenuti", - "hide_full_subject": "Nascondi intero oggetto", - "show_full_subject": "Mostra intero oggetto", + "hide_full_subject": "Nascondi oggetto intero", + "show_full_subject": "Mostra oggetto intero", "thread_muted_and_words": ", contiene:", "thread_muted": "Discussione silenziata", "copy_link": "Copia collegamento", @@ -708,46 +753,49 @@ "unmute_conversation": "Riabilita conversazione", "mute_conversation": "Silenzia conversazione", "replies_list": "Risposte:", - "reply_to": "Rispondi a", + "reply_to": "In risposta a", "delete_confirm": "Vuoi veramente eliminare questo messaggio?", "unbookmark": "Rimuovi segnalibro", "bookmark": "Aggiungi segnalibro", "status_deleted": "Questo messagio è stato cancellato", - "nsfw": "Pruriginoso", - "external_source": "Vai al sito", - "expand": "Espandi" + "nsfw": "DISDICEVOLE", + "external_source": "Vai all'origine", + "expand": "Espandi", + "mentions": "Menzioni", + "you": "(Tu)", + "plus_more": "+{number} altri" }, "time": { - "years_short": "{0}a", - "year_short": "{0}a", + "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_short": "{0} stm", + "week_short": "{0} stm", + "seconds_short": "{0} sec", + "second_short": "{0} sec", "weeks": "{0} settimane", "week": "{0} settimana", "seconds": "{0} secondi", "second": "{0} secondo", - "now_short": "ora", + "now_short": "adesso", "now": "adesso", - "months_short": "{0}me", - "month_short": "{0}me", + "months_short": "{0} mes", + "month_short": "{0} mes", "months": "{0} mesi", "month": "{0} mese", - "minutes_short": "{0}min", - "minute_short": "{0}min", + "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_short": "{0} h", + "days_short": "{0} g", + "hour_short": "{0} h", "hours": "{0} ore", "hour": "{0} ora", - "day_short": "{0}g", + "day_short": "{0} g", "days": "{0} giorni", "day": "{0} giorno" }, @@ -761,7 +809,7 @@ "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_but_mailer_is_disabled": "Devi reimpostare la tua password, ma non puoi farlo. Contatta l'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.", @@ -802,7 +850,7 @@ "add_reaction": "Reagisci", "favorite": "Gradisci", "reply": "Rispondi", - "repeat": "Ripeti", + "repeat": "Condividi", "media_upload": "Carica allegati" }, "display_date": { diff --git a/src/i18n/ja_easy.json b/src/i18n/ja_easy.json @@ -567,7 +567,6 @@ "follow": "フォロー", "follow_sent": "リクエストを、おくりました!", "follow_progress": "リクエストしています…", - "follow_again": "ふたたびリクエストをおくりますか?", "follow_unfollow": "フォローをやめる", "followees": "フォロー", "followers": "フォロワー", diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json @@ -4,7 +4,7 @@ }, "exporter": { "export": "エクスポート", - "processing": "処理中です。処理が完了すると、ファイルをダウンロードするよう指示があります。" + "processing": "処理中です。処理が完了すると、ファイルをダウンロードするよう指示があります" }, "features_panel": { "chat": "チャット", @@ -13,10 +13,12 @@ "scope_options": "公開範囲選択", "text_limit": "文字の数", "title": "有効な機能", - "who_to_follow": "おすすめユーザー" + "who_to_follow": "おすすめユーザー", + "upload_limit": "ファイルサイズの上限", + "pleroma_chat_messages": "Pleroma チャット" }, "finder": { - "error_fetching_user": "ユーザー検索がエラーになりました。", + "error_fetching_user": "ユーザー検索がエラーになりました", "find_user": "ユーザーを探す" }, "general": { @@ -31,7 +33,20 @@ "disable": "無効", "enable": "有効", "confirm": "確認", - "verify": "検査" + "verify": "検査", + "peek": "隠す", + "close": "閉じる", + "dismiss": "無視", + "retry": "もう一度お試し下さい", + "error_retry": "もう一度お試し下さい", + "loading": "読み込み中…", + "role": { + "moderator": "モデレーター", + "admin": "管理者" + }, + "flash_security": "Flashコンテンツが任意の命令を実行させることにより、コンピューターが危険にさらされることがあります。", + "flash_fail": "Flashコンテンツの読み込みに失敗しました。コンソールで詳細を確認できます。", + "flash_content": "(試験的機能)クリックしてFlashコンテンツを再生します。" }, "image_cropper": { "crop_picture": "画像を切り抜く", @@ -57,9 +72,9 @@ "enter_recovery_code": "リカバリーコードを入力してください", "enter_two_factor_code": "2段階認証コードを入力してください", "recovery_code": "リカバリーコード", - "heading" : { - "totp" : "2段階認証", - "recovery" : "2段階リカバリー" + "heading": { + "totp": "2段階認証", + "recovery": "2段階リカバリー" } }, "media_modal": { @@ -74,23 +89,32 @@ "mentions": "通知", "interactions": "インタラクション", "dms": "ダイレクトメッセージ", - "public_tl": "パブリックタイムライン", + "public_tl": "公開タイムライン", "timeline": "タイムライン", - "twkn": "接続しているすべてのネットワーク", + "twkn": "すべてのネットワーク", "user_search": "ユーザーを探す", "search": "検索", "who_to_follow": "おすすめユーザー", - "preferences": "設定" + "preferences": "設定", + "administration": "管理", + "bookmarks": "ブックマーク", + "timelines": "タイムライン", + "chats": "チャット", + "home_timeline": "ホームタイムライン" }, "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": "あなたをフォローしたいです", + "error": "通知の取得に失敗しました: {0}" }, "polls": { "add_poll": "投票を追加", @@ -104,7 +128,9 @@ "expiry": "投票期間", "expires_in": "投票は {0} で終了します", "expired": "投票は {0} 前に終了しました", - "not_enough_options": "相異なる選択肢が不足しています" + "not_enough_options": "相異なる選択肢が不足しています", + "votes_count": "{count} 票 | {count} 票", + "people_voted_count": "{count} 人投票 | {count} 人投票" }, "emoji": { "stickers": "ステッカー", @@ -113,7 +139,9 @@ "search_emoji": "絵文字を検索", "add_emoji": "絵文字を挿入", "custom": "カスタム絵文字", - "unicode": "Unicode絵文字" + "unicode": "Unicode絵文字", + "load_all": "全 {emojiAmount} 絵文字を読み込む", + "load_all_hint": "最初の {saneAmount} 絵文字を読み込みました、全て読み込むと重くなる可能性があります。" }, "stickers": { "add_sticker": "ステッカーを追加" @@ -121,7 +149,8 @@ "interactions": { "favs_repeats": "リピートとお気に入り", "follows": "新しいフォロワー", - "load_older": "古いインタラクションを見る" + "load_older": "古いインタラクションを見る", + "moves": "ユーザーの引っ越し" }, "post_status": { "new_status": "投稿する", @@ -142,15 +171,21 @@ "posting": "投稿", "scope_notice": { "public": "この投稿は、誰でも見ることができます", - "private": "この投稿は、あなたのフォロワーだけが、見ることができます。", - "unlisted": "この投稿は、パブリックタイムラインと、接続しているすべてのネットワークには、表示されません。" + "private": "この投稿は、あなたのフォロワーだけが、見ることができます", + "unlisted": "この投稿は、パブリックタイムラインと、接続しているすべてのネットワークには、表示されません" }, "scope": { - "direct": "ダイレクト: メンションされたユーザーのみに届きます。", - "private": "フォロワーげんてい: フォロワーのみに届きます。", - "public": "パブリック: パブリックタイムラインに届きます。", - "unlisted": "アンリステッド: パブリックタイムラインに届きません。" - } + "direct": "ダイレクト: メンションされたユーザーのみに届きます", + "private": "フォロワー限定: フォロワーのみに届きます", + "public": "パブリック: 公開タイムラインに届きます", + "unlisted": "アンリステッド: 公開タイムラインに届きません" + }, + "media_description_error": "メディアのアップロードに失敗しました。もう一度お試しください", + "empty_status_error": "投稿内容を入力してください", + "preview_empty": "何もありません", + "preview": "プレビュー", + "media_description": "メディアの説明", + "post": "投稿" }, "registration": { "bio": "プロフィール", @@ -171,7 +206,10 @@ "password_required": "必須", "password_confirmation_required": "必須", "password_confirmation_match": "パスワードが違います" - } + }, + "reason_placeholder": "このインスタンスは、新規登録を手動で受け付けています。\n登録したい理由を、インスタンスの管理者に教えてください。", + "reason": "登録するための目的", + "register": "登録" }, "selectable_list": { "select_all": "すべて選択" @@ -181,17 +219,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" : "コードを紙に書くか、安全な場所に保存してください。そうでなければ、あなたはコードを再び見ることはできません。もし2段階認証アプリのアクセスを喪失し、なおかつ、リカバリーコードもないならば、あなたは自分のアカウントから閉め出されます。", - "authentication_methods" : "認証方法", + "generate_new_recovery_codes": "新しいリカバリーコードを生成", + "warning_of_generate_new_codes": "新しいリカバリーコードを生成すると、古いコードは使用できなくなります。", + "recovery_codes": "リカバリーコード。", + "waiting_a_recovery_codes": "バックアップコードを受信しています…", + "recovery_codes_warning": "コードを紙に書くか、安全な場所に保存してください。そうでなければ、あなたはコードを再び見ることはできません。もし2段階認証アプリのアクセスを喪失し、なおかつ、リカバリーコードもないならば、あなたは自分のアカウントから閉め出されます。", + "authentication_methods": "認証方法", "scan": { "title": "スキャン", "desc": "あなたの2段階認証アプリを使って、このQRコードをスキャンするか、テキストキーを入力してください:", @@ -231,7 +269,7 @@ "data_import_export_tab": "インポートとエクスポート", "default_vis": "デフォルトの公開範囲", "delete_account": "アカウントを消す", - "delete_account_description": "あなたのアカウントとメッセージが、消えます。", + "delete_account_description": "あなたのデータが消えて、アカウントが使えなくなります。", "delete_account_error": "アカウントを消すことが、できなかったかもしれません。インスタンスの管理者に、連絡してください。", "delete_account_instructions": "本当にアカウントを消してもいいなら、パスワードを入力してください。", "discoverable": "検索などのサービスでこのアカウントを見つけることを許可する", @@ -239,12 +277,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": "全般", @@ -291,8 +329,8 @@ "hide_followers_description": "フォロワーを見せない", "hide_follows_count_description": "フォローしている人の数を見せない", "hide_followers_count_description": "フォロワーの数を見せない", - "show_admin_badge": "管理者のバッジを見せる", - "show_moderator_badge": "モデレーターのバッジを見せる", + "show_admin_badge": "\"管理者\"のバッジを見せる", + "show_moderator_badge": "\"モデレーター\"のバッジを見せる", "nsfw_clickthrough": "NSFWなファイルを隠す", "oauth_tokens": "OAuthトークン", "token": "トークン", @@ -302,10 +340,10 @@ "panelRadius": "パネル", "pause_on_unfocused": "タブにフォーカスがないときストリーミングを止める", "presets": "プリセット", - "profile_background": "プロフィールのバックグラウンド", - "profile_banner": "プロフィールバナー", + "profile_background": "プロフィールの背景", + "profile_banner": "プロフィールのバナー", "profile_tab": "プロフィール", - "radii_help": "インターフェースの丸さを設定する。", + "radii_help": "インターフェースの丸さを設定する", "replies_in_timeline": "タイムラインのリプライ", "reply_visibility_all": "すべてのリプライを見る", "reply_visibility_following": "私に宛てられたリプライと、フォローしている人からのリプライを見る", @@ -332,7 +370,7 @@ "streaming": "上までスクロールしたとき、自動的にストリーミングする", "text": "文字", "theme": "テーマ", - "theme_help": "カラーテーマをカスタマイズできます", + "theme_help": "カラーテーマをカスタマイズできます。", "theme_help_v2_1": "チェックボックスをONにすると、コンポーネントごとに、色と透明度をオーバーライドできます。「すべてクリア」ボタンを押すと、すべてのオーバーライドをやめます。", "theme_help_v2_2": "バックグラウンドとテキストのコントラストを表すアイコンがあります。マウスをホバーすると、詳しい説明が出ます。透明な色を使っているときは、最悪の場合のコントラストが示されます。", "tooltipRadius": "ツールチップとアラート", @@ -356,7 +394,24 @@ "save_load_hint": "「残す」オプションをONにすると、テーマを選んだときとロードしたとき、現在の設定を残します。また、テーマをエクスポートするとき、これらのオプションを維持します。すべてのチェックボックスをOFFにすると、テーマをエクスポートしたとき、すべての設定を保存します。", "reset": "リセット", "clear_all": "すべてクリア", - "clear_opacity": "透明度をクリア" + "clear_opacity": "透明度をクリア", + "help": { + "snapshot_missing": "テーマのスナップショットがありません。思っていた見た目と違うかもしれません。", + "migration_snapshot_ok": "念のために、テーマのスナップショットが読み込まれました。テーマのデータを読み込むことができます。", + "fe_downgraded": "フロントエンドが前のバージョンに戻りました。", + "fe_upgraded": "フロントエンドと一緒に、テーマエンジンが新しくなりました。", + "older_version_imported": "古いフロントエンドで作られたファイルをインポートしました。", + "future_version_imported": "新しいフロントエンドで作られたファイルをインポートしました。", + "v2_imported": "古いフロントエンドのためのファイルをインポートしました。設定した通りにならないかもしれません。", + "upgraded_from_v2": "フロントエンドが新しくなったので、今までの見た目と少し違うかもしれません。", + "snapshot_source_mismatch": "フロントエンドがロールバックと更新を繰り返したため、バージョンが競合しています。", + "migration_napshot_gone": "スナップショットがありません、覚えているものと見た目が違うかもしれません。", + "snapshot_present": "テーマのスナップショットが読み込まれました。設定は上書きされました。代わりとして実データを読み込むことができます。" + }, + "use_source": "新しいバージョン", + "use_snapshot": "古いバージョン", + "load_theme": "テーマの読み込み", + "keep_as_is": "変更しない" }, "common": { "color": "色", @@ -364,9 +419,9 @@ "contrast": { "hint": "コントラストは {ratio} です。{level}。({context})", "level": { - "aa": "AAレベルガイドライン (ミニマル) を満たします", - "aaa": "AAAレベルガイドライン (レコメンデッド) を満たします。", - "bad": "ガイドラインを満たしません。" + "aa": "AAレベルガイドライン (最低限) を満たします", + "aaa": "AAAレベルガイドライン (推奨) を満たします", + "bad": "ガイドラインを満たしません" }, "context": { "18pt": "大きい (18ポイント以上) テキスト", @@ -391,7 +446,27 @@ "borders": "境界", "buttons": "ボタン", "inputs": "インプットフィールド", - "faint_text": "薄いテキスト" + "faint_text": "薄いテキスト", + "alert_neutral": "それ以外", + "chat": { + "border": "境界線", + "outgoing": "送信", + "incoming": "受信" + }, + "tabs": "タブ", + "toggled": "切り替えたとき", + "disabled": "無効なとき", + "selectedMenu": "選択されたメニューアイテム", + "selectedPost": "選択された投稿", + "pressed": "押したとき", + "highlight": "強調された要素", + "icons": "アイコン", + "poll": "投票グラフ", + "wallpaper": "壁紙", + "underlay": "アンダーレイ", + "popover": "ツールチップ、メニュー、ポップオーバー", + "post": "投稿/プロフィール", + "alert_warning": "警告" }, "radii": { "_tab_label": "丸さ" @@ -409,8 +484,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": "パネル", @@ -424,7 +499,8 @@ "buttonPressed": "ボタン (押されているとき)", "buttonPressedHover": "ボタン (ホバー、かつ、押されているとき)", "input": "インプットフィールド" - } + }, + "hintV3": "影の場合は、 {0} 表記を使って他の色スロットを使うこともできます。" }, "fonts": { "_tab_label": "フォント", @@ -445,7 +521,7 @@ "content": "本文", "error": "エラーの例", "button": "ボタン", - "text": "これは{0}と{1}の例です。", + "text": "これは{0}と{1}の例です", "mono": "monospace", "input": "羽田空港に着きました。", "faint_link": "とても助けになるマニュアル", @@ -459,7 +535,72 @@ "title": "バージョン", "backend_version": "バックエンドのバージョン", "frontend_version": "フロントエンドのバージョン" - } + }, + "notification_setting_hide_notification_contents": "送った人と内容を、プッシュ通知に表示しない", + "notification_setting_privacy": "プライバシー", + "notification_setting_block_from_strangers": "フォローしていないユーザーからの通知を拒否する", + "notification_setting_filters": "フィルター", + "fun": "お楽しみ", + "virtual_scrolling": "タイムラインの描画を最適化する", + "type_domains_to_mute": "ミュートしたいドメインを検索", + "useStreamingApiWarning": "(実験中で、投稿を取りこぼすかもしれないので、おすすめしません)", + "useStreamingApi": "投稿と通知を、すぐに受け取る", + "user_mutes": "ユーザー", + "reset_background_confirm": "本当にバックグラウンドを初期化しますか?", + "reset_banner_confirm": "本当にバナーを初期化しますか?", + "reset_avatar_confirm": "本当にアバターを初期化しますか?", + "hide_wallpaper": "インスタンスのバックグラウンドを隠す", + "reset_profile_background": "プロフィールのバックグラウンドを初期化", + "reset_profile_banner": "プロフィールのバナーを初期化", + "reset_avatar": "アバターを初期化", + "notification_visibility_emoji_reactions": "リアクション", + "notification_visibility_moves": "ユーザーの引っ越し", + "new_email": "新しいメールアドレス", + "profile_fields": { + "value": "内容", + "name": "ラベル", + "add_field": "枠を追加", + "label": "プロフィール補足情報" + }, + "accent": "アクセント", + "mutes_imported": "ミュートをインポートしました!少し時間がかかるかもしれません。", + "emoji_reactions_on_timeline": "絵文字リアクションをタイムラインに表示", + "domain_mutes": "ドメイン", + "mutes_and_blocks": "ミュートとブロック", + "chatMessageRadius": "チャットメッセージ", + "change_email_error": "メールアドレスを変えることが、できなかったかもしれません。", + "changed_email": "メールアドレスが、変わりました!", + "change_email": "メールアドレスを変える", + "bot": "これは bot アカウントです", + "mute_export_button": "ミュートをCSVファイルにエクスポートする", + "import_mutes_from_a_csv_file": "CSVファイルからミュートをインポートする", + "mute_import_error": "ミュートのインポートに失敗しました", + "mute_import": "ミュートのインポート", + "mute_export": "ミュートのエクスポート", + "allow_following_move": "フォロー中のアカウントが引っ越したとき、自動フォローを許可する", + "setting_changed": "規定の設定と異なっています", + "greentext": "引用を緑色で表示", + "sensitive_by_default": "はじめから投稿をセンシティブとして設定", + "more_settings": "その他の設定", + "reply_visibility_self_short": "自分宛のリプライを見る", + "reply_visibility_following_short": "フォローしている人に宛てられたリプライを見る", + "hide_all_muted_posts": "ミュートした投稿を隠す", + "hide_media_previews": "メディアのプレビューを隠す", + "word_filter": "単語フィルタ", + "file_export_import": { + "errors": { + "invalid_file": "これはPleromaの設定をバックアップしたファイルではありません。", + "file_slightly_new": "ファイルのマイナーバージョンが異なり、一部の設定が読み込まれないことがあります" + }, + "restore_settings": "設定をファイルから復元する", + "backup_settings_theme": "テーマを含む設定をファイルにバックアップする", + "backup_settings": "設定をファイルにバックアップする", + "backup_restore": "設定をバックアップ" + }, + "save": "変更を保存", + "hide_shoutbox": "Shoutboxを表示しない", + "always_show_post_button": "投稿ボタンを常に表示", + "right_sidebar": "サイドバーを右に表示" }, "time": { "day": "{0}日", @@ -505,7 +646,11 @@ "show_new": "読み込み", "up_to_date": "最新", "no_more_statuses": "これで終わりです", - "no_statuses": "ステータスはありません" + "no_statuses": "ステータスはありません", + "reload": "再読み込み", + "error": "タイムラインの読み込みに失敗しました: {0}", + "socket_reconnected": "リアルタイム接続が確立されました", + "socket_broke": "コード{0}によりリアルタイム接続が切断されました" }, "status": { "favorites": "お気に入り", @@ -518,7 +663,24 @@ "reply_to": "返信", "replies_list": "返信:", "mute_conversation": "スレッドをミュート", - "unmute_conversation": "スレッドのミュートを解除" + "unmute_conversation": "スレッドのミュートを解除", + "nsfw": "閲覧注意", + "expand": "広げる", + "status_deleted": "この投稿は削除されました", + "hide_content": "隠す", + "show_content": "見る", + "hide_full_subject": "隠す", + "show_full_subject": "全部見る", + "thread_muted_and_words": "以下の単語を含むため:", + "thread_muted": "ミュートされたスレッド", + "external_source": "外部ソース", + "copy_link": "リンクをコピー", + "status_unavailable": "利用できません", + "unbookmark": "ブックマーク解除", + "bookmark": "ブックマーク", + "mentions": "メンション", + "you": "(あなた)", + "plus_more": "ほか{number}件" }, "user_card": { "approve": "受け入れ", @@ -529,7 +691,6 @@ "follow": "フォロー", "follow_sent": "リクエストを送りました!", "follow_progress": "リクエストしています…", - "follow_again": "再びリクエストを送りますか?", "follow_unfollow": "フォローをやめる", "followees": "フォロー", "followers": "フォロワー", @@ -539,7 +700,7 @@ "media": "メディア", "mention": "メンション", "mute": "ミュート", - "muted": "ミュートしています!", + "muted": "ミュートしています", "per_day": "/日", "remote_follow": "リモートフォロー", "report": "通報", @@ -547,11 +708,11 @@ "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": "管理者権限を付与", @@ -570,7 +731,23 @@ "quarantine": "他のインスタンスからの投稿を止める", "delete_user": "ユーザーを削除", "delete_user_confirmation": "あなたの精神状態に何か問題はございませんか? この操作を取り消すことはできません。" - } + }, + "roles": { + "moderator": "モデレーター", + "admin": "管理者" + }, + "show_repeats": "リピートを見る", + "hide_repeats": "リピートを隠す", + "message": "メッセージ", + "hidden": "隠す", + "bot": "bot", + "highlight": { + "solid": "背景を単色にする", + "striped": "背景を縞模様にする", + "side": "端に線を付ける", + "disabled": "強調しない" + }, + "edit_profile": "プロフィールを編集" }, "user_profile": { "timeline_title": "ユーザータイムライン", @@ -595,13 +772,18 @@ "repeat": "リピート", "reply": "返信", "favorite": "お気に入り", - "user_settings": "ユーザー設定" + "user_settings": "ユーザー設定", + "bookmark": "ブックマーク", + "reject_follow_request": "フォローリクエストを拒否", + "accept_follow_request": "フォローリクエストを許可", + "add_reaction": "リアクションを追加" }, - "upload":{ + "upload": { "error": { - "base": "アップロードに失敗しました。", - "file_too_big": "ファイルが大きすぎます [{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]", - "default": "しばらくしてから試してください" + "base": "アップロードに失敗しました。", + "file_too_big": "ファイルが大きすぎます [{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]", + "default": "しばらくしてから試してください", + "message": "アップロードに失敗: {0}" }, "file_size_units": { "B": "B", @@ -626,6 +808,77 @@ "check_email": "パスワードをリセットするためのリンクが記載されたメールが届いているか確認してください。", "return_home": "ホームページに戻る", "too_many_requests": "試行回数の制限に達しました。しばらく時間を置いてから再試行してください。", - "password_reset_disabled": "このインスタンスではパスワードリセットは無効になっています。インスタンスの管理者に連絡してください。" + "password_reset_disabled": "このインスタンスではパスワードリセットは無効になっています。インスタンスの管理者に連絡してください。", + "password_reset_required_but_mailer_is_disabled": "パスワードの初期化が必要ですが、初期化は使えません。インスタンスの管理者に連絡してください。", + "password_reset_required": "ログインするためにパスワードを初期化してください。" + }, + "about": { + "mrf": { + "mrf_policies_desc": "MRFポリシーは、インスタンスの振る舞いを操作します。以下のポリシーが有効になっています:", + "federation": "連合", + "simple": { + "media_nsfw_desc": "このインスタンスでは、以下のインスタンスからの投稿に対して、メディアを閲覧注意に設定します:", + "media_nsfw": "メディアを閲覧注意に設定", + "media_removal_desc": "このインスタンスでは、以下のインスタンスからの投稿に対して、メディアを除去します:", + "media_removal": "メディア除去", + "ftl_removal": "「既知のネットワーク」タイムラインから除外", + "ftl_removal_desc": "このインスタンスでは、以下のインスタンスを「既知のネットワーク」タイムラインから除外します:", + "quarantine_desc": "このインスタンスでは、以下のインスタンスに対して公開投稿のみを送信します:", + "quarantine": "検疫", + "reject_desc": "このインスタンスでは、以下のインスタンスからのメッセージを受け付けません:", + "accept_desc": "このインスタンスでは、以下のインスタンスからのメッセージのみを受け付けます:", + "accept": "許可", + "simple_policies": "インスタンス固有のポリシー", + "reject": "拒否" + }, + "mrf_policies": "有効なMRFポリシー", + "keyword": { + "replace": "置き換え", + "ftl_removal": "「接続しているすべてのネットワーク」タイムラインから除外", + "keyword_policies": "キーワードポリシー", + "is_replaced_by": "→", + "reject": "拒否" + } + }, + "staff": "スタッフ" + }, + "display_date": { + "today": "今日" + }, + "file_type": { + "file": "ファイル", + "image": "画像", + "video": "ビデオ", + "audio": "オーディオ" + }, + "remote_user_resolver": { + "error": "見つかりませんでした。", + "searching_for": "検索中", + "remote_user_resolver": "リモートユーザーリゾルバ" + }, + "errors": { + "storage_unavailable": "ブラウザのストレージに接続できなかったため、ログインや設定情報は保存されません。Cookieを有効にしてください。" + }, + "shoutbox": { + "title": "Shoutbox" + }, + "chats": { + "empty_chat_list_placeholder": "チャットはありません。新規チャットのボタンを押して始めましょう!", + "error_sending_message": "メッセージの送信に失敗しました。", + "error_loading_chat": "チャットの読み込みに失敗しました。", + "delete_confirm": "このメッセージを本当に消してもいいですか?", + "more": "もっと見る", + "empty_message_error": "メッセージを入力して下さい", + "new": "新規チャット", + "chats": "チャット一覧", + "delete": "削除", + "message_user": "{nickname} にメッセージ", + "you": "あなた:" + }, + "domain_mute_card": { + "unmute_progress": "ミュート解除中…", + "unmute": "ミュート解除", + "mute_progress": "ミュート中…", + "mute": "ミュート" } } diff --git a/src/i18n/ko.json b/src/i18n/ko.json @@ -9,7 +9,9 @@ "scope_options": "범위 옵션", "text_limit": "텍스트 제한", "title": "기능", - "who_to_follow": "팔로우 추천" + "who_to_follow": "팔로우 추천", + "upload_limit": "최대 파일용량", + "pleroma_chat_messages": "Pleroma 채트" }, "finder": { "error_fetching_user": "사용자 정보 불러오기 실패", @@ -17,7 +19,27 @@ }, "general": { "apply": "적용", - "submit": "보내기" + "submit": "보내기", + "loading": "로딩중…", + "peek": "숨기기", + "close": "닫기", + "verify": "검사", + "confirm": "확인", + "enable": "유효", + "disable": "무효", + "cancel": "취소", + "dismiss": "무시", + "show_less": "접기", + "show_more": "더 보기", + "optional": "필수 아님", + "retry": "다시 시도하십시오", + "error_retry": "다시 시도하십시오", + "generic_error": "잘못되었습니다", + "more": "더 보기", + "role": { + "moderator": "중재자", + "admin": "관리자" + } }, "login": { "login": "로그인", @@ -26,10 +48,19 @@ "password": "암호", "placeholder": "예시: lain", "register": "가입", - "username": "사용자 이름" + "username": "사용자 이름", + "heading": { + "recovery": "2단계 복구", + "totp": "2단계인증" + }, + "recovery_code": "복구 코드", + "enter_two_factor_code": "2단계인증 코드를 입력하십시오", + "enter_recovery_code": "복구 코드를 입력하십시오", + "authentication_code": "인증 코드", + "hint": "로그인하여 대화에 참가합시다" }, "nav": { - "about": "About", + "about": "인스턴스 소개", "back": "뒤로", "chat": "로컬 챗", "friend_requests": "팔로우 요청", @@ -37,18 +68,31 @@ "dms": "다이렉트 메시지", "public_tl": "공개 타임라인", "timeline": "타임라인", - "twkn": "모든 알려진 네트워크", + "twkn": "알려진 네트워크", "user_search": "사용자 검색", - "preferences": "환경설정" + "preferences": "환경설정", + "chats": "채트", + "timelines": "타임라인", + "who_to_follow": "추천된 사용자", + "search": "검색", + "bookmarks": "북마크", + "interactions": "대화", + "administration": "관리", + "home_timeline": "홈 타임라인" }, "notifications": { - "broken_favorite": "알 수 없는 게시물입니다, 검색 합니다...", + "broken_favorite": "알 수 없는 게시물입니다, 검색합니다…", "favorited_you": "당신의 게시물을 즐겨찾기", "followed_you": "당신을 팔로우", "load_older": "오래 된 알림 불러오기", "notifications": "알림", "read": "읽음!", - "repeated_you": "당신의 게시물을 리핏" + "repeated_you": "당신의 게시물을 리핏", + "no_more_notifications": "알림이 없습니다", + "migrated_to": "이사했습니다", + "reacted_with": "{0} 로 반응했습니다", + "error": "알림 불러오기 실패: {0}", + "follow_request": "당신에게 팔로우 신청" }, "post_status": { "new_status": "새 게시물 게시", @@ -56,10 +100,13 @@ "account_not_locked_warning_link": "잠김", "attachments_sensitive": "첨부물을 민감함으로 설정", "content_type": { - "text/plain": "평문" + "text/plain": "평문", + "text/bbcode": "BBCode", + "text/markdown": "Markdown", + "text/html": "HTML" }, "content_warning": "주제 (필수 아님)", - "default": "LA에 도착!", + "default": "인천공항에 도착했습니다.", "direct_warning": "이 게시물을 멘션 된 사용자들에게만 보여집니다", "posting": "게시", "scope": { @@ -67,7 +114,15 @@ "private": "팔로워 전용 - 팔로워들에게만", "public": "공개 - 공개 타임라인으로", "unlisted": "비공개 - 공개 타임라인에 게시 안 함" - } + }, + "preview_empty": "아무것도 없습니다", + "preview": "미리보기", + "scope_notice": { + "public": "이 글은 누구나 볼 수 있습니다" + }, + "media_description_error": "파일을 올리지 못하였습니다. 다시한번 시도하여 주십시오", + "empty_status_error": "글을 입력하십시오", + "media_description": "첨부파일 설명" }, "registration": { "bio": "소개", @@ -85,7 +140,9 @@ "password_required": "공백으로 둘 수 없습니다", "password_confirmation_required": "공백으로 둘 수 없습니다", "password_confirmation_match": "패스워드와 일치해야 합니다" - } + }, + "fullname_placeholder": "예: 김례인", + "username_placeholder": "예: lain" }, "settings": { "attachmentRadius": "첨부물", @@ -112,7 +169,7 @@ "data_import_export_tab": "데이터 불러오기 / 내보내기", "default_vis": "기본 공개 범위", "delete_account": "계정 삭제", - "delete_account_description": "계정과 메시지를 영구히 삭제.", + "delete_account_description": "데이터가 영구히 삭제되고 계정이 불활성화됩니다.", "delete_account_error": "계정을 삭제하는데 문제가 있습니다. 계속 발생한다면 인스턴스 관리자에게 문의하세요.", "delete_account_instructions": "계정 삭제를 확인하기 위해 아래에 패스워드 입력.", "export_theme": "프리셋 저장", @@ -156,7 +213,7 @@ "notification_visibility_repeats": "반복", "no_rich_text_description": "모든 게시물의 서식을 지우기", "hide_follows_description": "내가 팔로우하는 사람을 표시하지 않음", - "hide_followers_description": "나를 따르는 사람을 보여주지 마라.", + "hide_followers_description": "나를 따르는 사람을 숨기기", "nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화", "oauth_tokens": "OAuth 토큰", "token": "토큰", @@ -247,7 +304,16 @@ "borders": "테두리", "buttons": "버튼", "inputs": "입력칸", - "faint_text": "흐려진 텍스트" + "faint_text": "흐려진 텍스트", + "chat": { + "border": "경계선", + "outgoing": "송신", + "incoming": "수신" + }, + "selectedMenu": "선택된 메뉴 요소", + "selectedPost": "선택된 글", + "icons": "아이콘", + "alert_warning": "경고" }, "radii": { "_tab_label": "둥글기" @@ -303,14 +369,46 @@ "button": "버튼", "text": "더 많은 {0} 그리고 {1}", "mono": "내용", - "input": "LA에 막 도착!", + "input": "인천공항에 도착했습니다.", "faint_link": "도움 되는 설명서", "fine_print": "우리의 {0} 를 읽고 도움 되지 않는 것들을 배우자!", "header_faint": "이건 괜찮아", "checkbox": "나는 약관을 대충 훑어보았습니다", "link": "작고 귀여운 링크" } - } + }, + "block_export": "차단 목록 내보내기", + "mfa": { + "scan": { + "secret_code": "키", + "title": "스캔" + }, + "authentication_methods": "인증 방법", + "waiting_a_recovery_codes": "예비 코드를 수신하고 있습니다…", + "recovery_codes": "복구 코드.", + "generate_new_recovery_codes": "새로운 복구 코드를 작성", + "title": "2단계인증", + "confirm_and_enable": "OTP 확인과 활성화", + "setup_otp": "OTP 설치", + "otp": "OTP" + }, + "security": "보안", + "emoji_reactions_on_timeline": "이모지 반응을 타임라인으로 표시", + "avatar_size_instruction": "크기를 150x150 이상으로 설정할 것을 추장합니다.", + "blocks_tab": "차단", + "notification_setting_privacy": "보안", + "user_mutes": "사용자", + "notification_visibility_emoji_reactions": "반응", + "profile_fields": { + "value": "내용" + }, + "mutes_and_blocks": "침묵과 차단", + "chatMessageRadius": "챗 메시지", + "change_email": "메일주소 바꾸기", + "changed_email": "메일주소가 갱신되었습니다!", + "bot": "이 계정은 bot입니다", + "mutes_tab": "침묵", + "app_name": "앱 이름" }, "timeline": { "collapse": "접기", @@ -330,7 +428,6 @@ "follow": "팔로우", "follow_sent": "요청 보내짐!", "follow_progress": "요청 중…", - "follow_again": "요청을 다시 보낼까요?", "follow_unfollow": "팔로우 중지", "followees": "팔로우 중", "followers": "팔로워", @@ -339,7 +436,7 @@ "its_you": "당신입니다!", "mute": "침묵", "muted": "침묵 됨", - "per_day": " / 하루", + "per_day": "/ 하루", "remote_follow": "원격 팔로우", "statuses": "게시물" }, @@ -357,11 +454,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": "바이트", @@ -370,5 +467,129 @@ "GiB": "기비바이트", "TiB": "테비바이트" } + }, + "interactions": { + "follows": "새 팔로워", + "favs_repeats": "반복과 즐겨찾기", + "moves": "계정 통합" + }, + "emoji": { + "load_all": "전체 {emojiAmount} 이모지 불러오기", + "unicode": "Unicode 이모지", + "custom": "전용 이모지", + "add_emoji": "이모지 넣기", + "search_emoji": "이모지 검색", + "emoji": "이모지", + "stickers": "스티커" + }, + "polls": { + "add_poll": "투표를 추가", + "votes": "표", + "vote": "투표", + "type": "투표 형식", + "expiry": "투표 기간", + "votes_count": "{count} 표 | {count} 표", + "people_voted_count": "{count} 명 투표 | {count} 명 투표", + "option": "선택지", + "add_option": "선택지 추가", + "expired": "투표는 {0} 전에 마감되었습니다", + "expires_in": "투표는 {0}에 마감됩니다" + }, + "media_modal": { + "next": "다음", + "previous": "이전" + }, + "importer": { + "error": "이 파일을 가져올 때 오류가 발생하였습니다.", + "success": "정상히 불러왔습니다.", + "submit": "보내기" + }, + "image_cropper": { + "cancel": "취소", + "save_without_cropping": "그대로 저장", + "save": "저장", + "crop_picture": "사진 자르기" + }, + "exporter": { + "processing": "처리중입니다, 처리가 끝나면 파일을 다운로드하라는 지시가 있겠습니다", + "export": "내보내기" + }, + "domain_mute_card": { + "unmute_progress": "침묵을 해제중…", + "unmute": "침묵 해제", + "mute_progress": "침묵으로 설정중…", + "mute": "침묵" + }, + "about": { + "staff": "운영자", + "mrf": { + "simple": { + "media_nsfw_desc": "이 인스턴스에서는 아래의 인스턴스로부터 보내온 투고에 붙혀 있는 매체는 민감함으로 설정됩니다:", + "media_nsfw": "매체를 민감함으로 설정", + "media_removal_desc": "이 인스턴스에서는 아래의 인스턴스로부터 보내온 투고에 붙혀 있는 매체는 제거됩니다:", + "media_removal": "매체 제거", + "ftl_removal_desc": "이 인스턴스에서 아래의 인스턴스들은 \"알려진 네트워크\" 타임라인에서 제외됩니다:", + "ftl_removal": "\"알려진 네트워크\" 타임라인에서 제외", + "quarantine_desc": "이 인스턴스는 아래의 인스턴스에게 공개투고만을 보냅니다:", + "quarantine": "검역", + "reject_desc": "이 인스턴스에서는 아래의 인스턴스로부터 보내온 투고를 받아들이지 않습니다:", + "accept_desc": "이 인스턴스에서는 아래의 인스턴스로부터 보내온 투고만이 접수됩니다:", + "reject": "거부", + "accept": "허가", + "simple_policies": "인스턴스 특유의 폴리시" + }, + "mrf_policies": "사용되는 MRF 폴리시", + "keyword": { + "is_replaced_by": "→", + "replace": "바꾸기", + "reject": "거부", + "ftl_removal": "\"알려진 모든 네트워크\" 타임라인에서 제외", + "keyword_policies": "단어 폴리시" + }, + "federation": "연합" + } + }, + "shoutbox": { + "title": "Shoutbox" + }, + "time": { + "years_short": "{0} 년", + "year_short": "{0} 년", + "years": "{0} 년", + "year": "{0} 년", + "weeks_short": "{0} 주일", + "week_short": "{0} 주일", + "weeks": "{0} 주일", + "week": "{0} 주일", + "seconds_short": "{0} 초", + "second_short": "{0} 초", + "seconds": "{0} 초", + "second": "{0} 초", + "now_short": "방금", + "now": "방끔", + "months_short": "{0} 달 전", + "month_short": "{0} 달 전", + "months": "{0} 달 전", + "month": "{0} 달 전", + "minutes_short": "{0} 분", + "minute_short": "{0} 분", + "minutes": "{0} 분", + "minute": "{0} 분", + "in_past": "{0} 전", + "hours_short": "{0} 시간", + "hour_short": "{0} 시간", + "hours": "{0} 시간", + "hour": "{0} 시간", + "days_short": "{0} 일", + "day_short": "{0} 일", + "days": "{0} 일", + "day": "{0} 일" + }, + "remote_user_resolver": { + "error": "찾을 수 없습니다.", + "searching_for": "검색중" + }, + "selectable_list": { + "select_all": "모두 선택" } } diff --git a/src/i18n/nb.json b/src/i18n/nb.json @@ -41,8 +41,8 @@ }, "importer": { "submit": "Send", - "success": "Importering fullført", - "error": "Det oppsto en feil under importering av denne filen" + "success": "Importering fullført.", + "error": "Det oppsto en feil under importering av denne filen." }, "login": { "login": "Logg inn", @@ -57,9 +57,9 @@ "enter_recovery_code": "Skriv inn en gjenopprettingskode", "enter_two_factor_code": "Skriv inn en to-faktors kode", "recovery_code": "Gjenopprettingskode", - "heading" : { - "totp" : "To-faktors autentisering", - "recovery" : "To-faktors gjenoppretting" + "heading": { + "totp": "To-faktors autentisering", + "recovery": "To-faktors gjenoppretting" } }, "media_modal": { @@ -72,7 +72,7 @@ "chat": "Lokal nettprat", "friend_requests": "Følgeforespørsler", "mentions": "Nevnt", - "interactions": "Interaksjooner", + "interactions": "Interaksjoner", "dms": "Direktemeldinger", "public_tl": "Offentlig Tidslinje", "timeline": "Tidslinje", @@ -80,17 +80,20 @@ "user_search": "Søk etter brukere", "search": "Søk", "who_to_follow": "Kontoer å følge", - "preferences": "Innstillinger" + "preferences": "Innstillinger", + "timelines": "Tidslinjer", + "bookmarks": "Bokmerker" }, "notifications": { - "broken_favorite": "Ukjent status, leter etter den...", + "broken_favorite": "Ukjent status, leter etter den…", "favorited_you": "likte din status", "followed_you": "fulgte deg", "load_older": "Last eldre varsler", "notifications": "Varslinger", "read": "Les!", "repeated_you": "Gjentok din status", - "no_more_notifications": "Ingen gjenstående varsler" + "no_more_notifications": "Ingen gjenstående varsler", + "follow_request": "ønsker å følge deg" }, "polls": { "add_poll": "Legg til undersøkelse", @@ -134,7 +137,7 @@ "public": "Denne statusen vil være synlig for alle", "private": "Denne statusen vil være synlig for dine følgere", "unlisted": "Denne statusen vil ikke være synlig i Offentlig Tidslinje eller Det Hele Kjente Nettverket" - }, + }, "scope": { "direct": "Direkte, publiser bare til nevnte brukere", "private": "Bare følgere, publiser bare til brukere som følger deg", @@ -171,17 +174,17 @@ "security": "Sikkerhet", "enter_current_password_to_confirm": "Skriv inn ditt nåverende passord for å bekrefte din identitet", "mfa": { - "otp" : "OTP", - "setup_otp" : "Set opp OTP", - "wait_pre_setup_otp" : "forhåndsstiller OTP", - "confirm_and_enable" : "Bekreft og slå på OTP", + "otp": "OTP", + "setup_otp": "Set opp OTP", + "wait_pre_setup_otp": "forhåndsstiller OTP", + "confirm_and_enable": "Bekreft og slå på OTP", "title": "To-faktors autentisering", - "generate_new_recovery_codes" : "Generer nye gjenopprettingskoder", - "warning_of_generate_new_codes" : "Når du genererer nye gjenopprettingskoder, vil de gamle slutte å fungere.", - "recovery_codes" : "Gjenopprettingskoder.", + "generate_new_recovery_codes": "Generer nye gjenopprettingskoder", + "warning_of_generate_new_codes": "Når du genererer nye gjenopprettingskoder, vil de gamle slutte å fungere.", + "recovery_codes": "Gjenopprettingskoder.", "waiting_a_recovery_codes": "Mottar gjenopprettingskoder...", - "recovery_codes_warning" : "Skriv disse kodene ned eller plasser dem ett sikkert sted - ellers så vil du ikke se dem igjen. Dersom du mister tilgang til din to-faktors app og dine gjenopprettingskoder, vil du bli stengt ute av kontoen din.", - "authentication_methods" : "Autentiseringsmetoder", + "recovery_codes_warning": "Skriv disse kodene ned eller plasser dem ett sikkert sted - ellers så vil du ikke se dem igjen. Dersom du mister tilgang til din to-faktors app og dine gjenopprettingskoder, vil du bli stengt ute av kontoen din.", + "authentication_methods": "Autentiseringsmetoder", "scan": { "title": "Skann", "desc": "Ved hjelp av din to-faktors applikasjon, skann denne QR-koden eller skriv inn tekstnøkkelen", @@ -444,7 +447,8 @@ "title": "Versjon", "backend_version": "Backend Versjon", "frontend_version": "Frontend Versjon" - } + }, + "hide_wallpaper": "Skjul instansens bakgrunnsbilde" }, "time": { "day": "{0} dag", @@ -512,7 +516,6 @@ "follow": "Følg", "follow_sent": "Forespørsel sendt!", "follow_progress": "Forespør…", - "follow_again": "Gjenta forespørsel?", "follow_unfollow": "Avfølg", "followees": "Følger", "followers": "Følgere", @@ -579,7 +582,7 @@ "favorite": "Lik", "user_settings": "Brukerinnstillinger" }, - "upload":{ + "upload": { "error": { "base": "Det oppsto en feil under opplastning.", "file_too_big": "Fil for stor [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", @@ -599,5 +602,22 @@ "person_talking": "{count} person snakker om dette", "people_talking": "{count} personer snakker om dette", "no_results": "Ingen resultater" + }, + "about": { + "mrf": { + "simple": { + "quarantine": "Karantene", + "reject_desc": "Denne instansen vil ikke godta meldinger fra følgende instanser:", + "reject": "Avvis", + "accept_desc": "Denne instansen godtar kun meldinger fra følgende instanser:", + "accept": "Aksepter" + }, + "keyword": { + "is_replaced_by": "→", + "replace": "Erstatt", + "reject": "Avvis", + "ftl_removal": "Fjerning fra \"Det hele kjente nettverket\" Tidslinjen" + } + } } } diff --git a/src/i18n/nl.json b/src/i18n/nl.json @@ -5,11 +5,13 @@ "features_panel": { "chat": "Chat", "gopher": "Gopher", - "media_proxy": "Media proxy", + "media_proxy": "Mediaproxy", "scope_options": "Zichtbaarheidsopties", - "text_limit": "Tekst limiet", + "text_limit": "Tekstlimiet", "title": "Kenmerken", - "who_to_follow": "Wie te volgen" + "who_to_follow": "Wie te volgen", + "upload_limit": "Upload limiet", + "pleroma_chat_messages": "Pleroma Chat" }, "finder": { "error_fetching_user": "Fout tijdens ophalen gebruiker", @@ -17,11 +19,11 @@ }, "general": { "apply": "Toepassen", - "submit": "Verzend", + "submit": "Verzenden", "more": "Meer", "optional": "optioneel", - "show_more": "Bekijk meer", - "show_less": "Bekijk minder", + "show_more": "Meer tonen", + "show_less": "Minder tonen", "dismiss": "Opheffen", "cancel": "Annuleren", "disable": "Uitschakelen", @@ -29,28 +31,32 @@ "confirm": "Bevestigen", "verify": "Verifiëren", "generic_error": "Er is een fout opgetreden", - "peek": "Spiek", + "peek": "Spieken", "close": "Sluiten", "retry": "Opnieuw proberen", "error_retry": "Probeer het opnieuw", - "loading": "Laden…" + "loading": "Laden…", + "role": { + "moderator": "Moderator", + "admin": "Beheerder" + } }, "login": { - "login": "Log in", - "description": "Log in met OAuth", + "login": "Inloggen", + "description": "Inloggen met OAuth", "logout": "Uitloggen", "password": "Wachtwoord", - "placeholder": "bijv. lain", + "placeholder": "bijv. barbapapa", "register": "Registreren", "username": "Gebruikersnaam", "hint": "Log in om deel te nemen aan de discussie", - "authentication_code": "Authenticatie code", + "authentication_code": "Authenticatiecode", "enter_recovery_code": "Voer een herstelcode in", - "enter_two_factor_code": "Voer een twee-factor code in", + "enter_two_factor_code": "Voer een twee-factorcode in", "recovery_code": "Herstelcode", "heading": { - "totp": "Twee-factor authenticatie", - "recovery": "Twee-factor herstelling" + "totp": "Twee-factorauthenticatie", + "recovery": "Twee-factorherstelling" } }, "nav": { @@ -59,35 +65,40 @@ "chat": "Lokale Chat", "friend_requests": "Volgverzoeken", "mentions": "Vermeldingen", - "dms": "Directe Berichten", - "public_tl": "Publieke Tijdlijn", + "dms": "Privéberichten", + "public_tl": "Openbare tijdlijn", "timeline": "Tijdlijn", - "twkn": "Het Geheel Bekende Netwerk", + "twkn": "Bekende Netwerk", "user_search": "Gebruiker Zoeken", "who_to_follow": "Wie te volgen", "preferences": "Voorkeuren", - "administration": "Administratie", + "administration": "Beheer", "search": "Zoeken", - "interactions": "Interacties" + "interactions": "Interacties", + "chats": "Chats", + "home_timeline": "Thuis tijdlijn", + "timelines": "Tijdlijnen", + "bookmarks": "Bladwijzers" }, "notifications": { "broken_favorite": "Onbekende status, aan het zoeken…", "favorited_you": "vond je status leuk", "followed_you": "volgt jou", - "load_older": "Laad oudere meldingen", + "load_older": "Oudere meldingen laden", "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}" + "reacted_with": "reageerde met {0}", + "error": "Fout bij ophalen van meldingen: {0}" }, "post_status": { "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": "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 bijlagen als gevoelig", + "attachments_sensitive": "Bijlagen als gevoelig markeren", "content_type": { "text/plain": "Platte tekst", "text/html": "HTML", @@ -99,26 +110,32 @@ "direct_warning": "Deze post zal enkel zichtbaar zijn voor de personen die genoemd zijn.", "posting": "Plaatsen", "scope": { - "direct": "Direct - Post enkel naar vermelde gebruikers", - "private": "Enkel volgers - Post enkel naar volgers", - "public": "Publiek - Post op publieke tijdlijnen", - "unlisted": "Niet Vermelden - Niet tonen op publieke tijdlijnen" + "direct": "Privé - bericht enkel naar vermelde gebruikers sturen", + "private": "Enkel volgers - bericht enkel naar volgers sturen", + "public": "Openbaar - bericht op openbare tijdlijnen plaatsen", + "unlisted": "Niet vermelden - niet tonen op openbare 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", + "unlisted": "Dit bericht zal niet zichtbaar zijn in de Openbare Tijdlijn en Het Geheel Bekende Netwerk", "private": "Dit bericht zal voor alleen je volgers zichtbaar zijn" - } + }, + "post": "Bericht", + "empty_status_error": "Kan geen lege status zonder bijlagen plaatsen", + "preview_empty": "Leeg", + "preview": "Voorbeeld", + "media_description": "Mediaomschrijving", + "media_description_error": "Kon media niet ophalen, probeer het opnieuw" }, "registration": { "bio": "Bio", - "email": "Email", - "fullname": "Weergave naam", + "email": "E-mail", + "fullname": "Weergavenaam", "password_confirm": "Wachtwoord bevestiging", "registration": "Registratie", - "token": "Uitnodigings-token", + "token": "Uitnodigingstoken", "captcha": "CAPTCHA", "new_captcha": "Klik op de afbeelding voor een nieuwe captcha", "validations": { @@ -131,13 +148,16 @@ }, "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." + "bio_placeholder": "bijv.\nHallo, ik ben Lain.\nIk ben een animemeisje woonachtig in een buitenwijk in Japan. Je kent me misschien van the Wired.", + "reason_placeholder": "Deze instantie keurt registraties handmatig goed.\nLaat de beheerder weten waarom je wilt registreren.", + "reason": "Reden voor registratie", + "register": "Registreren" }, "settings": { "attachmentRadius": "Bijlages", "attachments": "Bijlages", "avatar": "Avatar", - "avatarAltRadius": "Avatars (Meldingen)", + "avatarAltRadius": "Avatars (meldingen)", "avatarRadius": "Avatars", "background": "Achtergrond", "bio": "Bio", @@ -146,7 +166,7 @@ "cGreen": "Groen (Herhalen)", "cOrange": "Oranje (Favoriet)", "cRed": "Rood (Annuleren)", - "change_password": "Wachtwoord Wijzigen", + "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", @@ -155,30 +175,30 @@ "current_avatar": "Je huidige avatar", "current_password": "Huidig wachtwoord", "current_profile_banner": "Je huidige profiel banner", - "data_import_export_tab": "Data Import / Export", + "data_import_export_tab": "Data-import / export", "default_vis": "Standaard zichtbaarheidsbereik", - "delete_account": "Account Verwijderen", + "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", + "export_theme": "Voorinstelling opslaan", "filtering": "Filtering", - "filtering_explanation": "Alle statussen die deze woorden bevatten worden genegeerd, één filter per lijn", + "filtering_explanation": "Alle statussen die deze woorden bevatten worden genegeerd, één filter per regel", "follow_export": "Volgers exporteren", - "follow_export_button": "Exporteer je volgers naar een csv bestand", + "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 importeren", "follow_import_error": "Fout bij importeren volgers", "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", + "hide_attachments_in_convo": "Bijlagen in conversaties verbergen", + "hide_attachments_in_tl": "Bijlagen in tijdlijn verbergen", + "hide_isp": "Instantie-specifiek paneel verbergen", "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", + "hide_post_stats": "Bericht statistieken verbergen (bijv. het aantal favorieten)", + "hide_user_stats": "Gebruikers-statistieken verbergen (bijv. het aantal volgers)", + "import_followers_from_a_csv_file": "Gevolgden uit een csv bestand importeren", "import_theme": "Preset laden", "inputRadius": "Invoervelden", "checkboxRadius": "Checkboxen", @@ -186,35 +206,35 @@ "instance_default_simple": "(standaard)", "interface": "Interface", "interfaceLanguage": "Interface taal", - "invalid_theme_imported": "Het geselecteerde bestand is geen door Pleroma ondersteund thema. Er zijn geen aanpassingen gedaan.", + "invalid_theme_imported": "Het geselecteerde bestand is niet een 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": "Herhaal video's", - "loop_video_silent_only": "Herhaal enkel video's zonder geluid (bijv. Mastodon's \"gifs\")", + "lock_account_description": "Volgers enkel na expliciete toestemming toelaten", + "loop_video": "Video's herhalen", + "loop_video_silent_only": "Enkel video's zonder geluid herhalen (bijv. Mastodon's \"gifs\")", "name": "Naam", - "name_bio": "Naam & Bio", + "name_bio": "Naam & bio", "new_password": "Nieuw wachtwoord", "notification_visibility": "Type meldingen die getoond worden", - "notification_visibility_follows": "Volgingen", - "notification_visibility_likes": "Vind-ik-leuks", + "notification_visibility_follows": "Gevolgden", + "notification_visibility_likes": "Favorieten", "notification_visibility_mentions": "Vermeldingen", "notification_visibility_repeats": "Herhalingen", "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": "Doorklikbaar verbergen van gevoelige bijlages inschakelen", + "nsfw_clickthrough": "Doorklikbaar verbergen van gevoelige bijlages en link voorbeelden 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": "Streamen pauzeren wanneer de tab niet in focus is", "presets": "Presets", - "profile_background": "Profiel Achtergrond", - "profile_banner": "Profiel Banner", + "profile_background": "Profiel achtergrond", + "profile_banner": "Profiel banner", "profile_tab": "Profiel", - "radii_help": "Stel afronding van hoeken in de interface in (in pixels)", + "radii_help": "Afronding van hoeken in de interface instellen (in pixels)", "replies_in_timeline": "Antwoorden in tijdlijn", "reply_visibility_all": "Alle antwoorden tonen", "reply_visibility_following": "Enkel antwoorden tonen die aan mij of gevolgde gebruikers gericht zijn", @@ -222,13 +242,13 @@ "saving_err": "Fout tijdens opslaan van instellingen", "saving_ok": "Instellingen opgeslagen", "security_tab": "Beveiliging", - "scope_copy": "Neem bereik over bij beantwoorden (Directe Berichten blijven altijd Direct)", + "scope_copy": "Bereik overnemen bij beantwoorden (Privéberichten blijven altijd privé)", "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": "Altijd onderwerpveld tonen", - "subject_line_behavior": "Onderwerp kopiëren bij antwoorden", + "subject_line_behavior": "Onderwerp kopiëren bij beantwoorden", "subject_line_email": "Zoals email: \"re: onderwerp\"", "subject_line_mastodon": "Zoals mastodon: kopieer zoals het is", "subject_line_noop": "Niet kopiëren", @@ -236,7 +256,7 @@ "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": "Hex kleur codes (#rrggbb) gebruiken om je kleur thema 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 \"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", @@ -323,7 +343,13 @@ "popover": "Tooltips, menu's, popovers", "post": "Berichten / Gebruiker bios", "alert_neutral": "Neutraal", - "alert_warning": "Waarschuwing" + "alert_warning": "Waarschuwing", + "chat": { + "border": "Rand", + "outgoing": "Uitgaand", + "incoming": "Binnenkomend" + }, + "wallpaper": "Achtergrond" }, "radii": { "_tab_label": "Rondheid" @@ -399,50 +425,50 @@ "setup_otp": "OTP instellen", "wait_pre_setup_otp": "OTP voorinstellen", "confirm_and_enable": "Bevestig en schakel OTP in", - "title": "Twee-factor Authenticatie", + "title": "Twee-factorauthenticatie", "generate_new_recovery_codes": "Genereer nieuwe herstelcodes", "recovery_codes": "Herstelcodes.", - "waiting_a_recovery_codes": "Backup codes ontvangen…", - "authentication_methods": "Authenticatie methodes", + "waiting_a_recovery_codes": "Back-upcodes ontvangen…", + "authentication_methods": "Authenticatiemethodes", "scan": { "title": "Scannen", - "desc": "Scan de QR code of voer een sleutel in met je twee-factor applicatie:", + "desc": "Scan de QR-code of voer een sleutel in met je twee-factorapplicatie:", "secret_code": "Sleutel" }, "verify": { - "desc": "Voer de code van je twee-factor applicatie in om twee-factor authenticatie in te schakelen:" + "desc": "Voer de code van je twee-factorapplicatie in om twee-factorauthenticatie 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." + "warning_of_generate_new_codes": "Wanneer je nieuwe herstelcodes genereert, zullen je oude codes 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 van 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!", + "change_email": "E-mail wijzigen", + "change_email_error": "Er is een fout opgetreden tijdens het wijzigen van je e-mailadres.", + "changed_email": "E-mailadres succesvol gewijzigd!", "domain_mutes": "Domeinen", - "avatar_size_instruction": "De aangeraden minimale afmeting voor avatar afbeeldingen is 150x150 pixels.", + "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", + "emoji_reactions_on_timeline": "Toon emoji-reacties op de tijdlijn", "accent": "Accent", - "hide_muted_posts": "Verberg berichten van genegeerde gebruikers", + "hide_muted_posts": "Berichten van genegeerde gebruikers verbergen", "max_thumbnails": "Maximaal aantal miniaturen per bericht", - "use_one_click_nsfw": "Open gevoelige bijlagen met slechts één klik", + "use_one_click_nsfw": "Gevoelige bijlagen met slechts één klik openen", "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", + "import_blocks_from_a_csv_file": "Blokkades van een csv bestand importeren", + "mutes_tab": "Genegeerden", + "play_videos_in_modal": "Video's in een popup frame afspelen", + "new_email": "Nieuwe e-mail", "notification_visibility_emoji_reactions": "Reacties", "no_blocks": "Geen blokkades", - "no_mutes": "Geen negeringen", + "no_mutes": "Geen genegeerden", "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", + "show_admin_badge": "\"Beheerder\" badge in mijn profiel tonen", "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", @@ -452,31 +478,69 @@ "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", + "upload_a_photo": "Foto uploaden", "fun": "Plezier", "greentext": "Meme pijlen", - "block_export_button": "Exporteer je geblokkeerde gebruikers naar een csv bestand", + "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", + "use_contain_fit": "Bijlage in miniaturen niet bijsnijden", "notification_visibility_moves": "Gebruiker Migraties", "hide_follows_description": "Niet tonen wie ik volg", - "show_moderator_badge": "Moderators badge tonen in mijn profiel", + "show_moderator_badge": "\"Moderator\" badge in mijn profiel tonen", "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", + "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", + "add_field": "Veld toevoegen", "label": "Profiel metadata" }, - "bot": "Dit is een bot account" + "bot": "Dit is een bot-account", + "setting_changed": "Instelling verschilt van standaard waarde", + "save": "Wijzigingen opslaan", + "hide_media_previews": "Media voorbeelden verbergen", + "word_filter": "Woord filter", + "chatMessageRadius": "Chatbericht", + "mute_export": "Genegeerden export", + "mute_export_button": "Exporteer je genegeerden naar een csv-bestand", + "mute_import_error": "Fout tijdens het importeren van genegeerden", + "mute_import": "Genegeerden import", + "mutes_imported": "Genegeerden geïmporteerd! Het kan even duren voordat deze verwerkt zijn.", + "more_settings": "Meer instellingen", + "notification_setting_hide_notification_contents": "Afzender en inhoud van push meldingen verbergen", + "notification_setting_block_from_strangers": "Meldingen van gebruikers die je niet volgt blokkeren", + "virtual_scrolling": "Tijdlijn rendering optimaliseren", + "sensitive_by_default": "Berichten standaard als gevoelig markeren", + "reset_avatar_confirm": "Wil je echt de avatar herstellen?", + "reset_banner_confirm": "Wil je echt de banner herstellen?", + "reset_background_confirm": "Wil je echt de achtergrond herstellen?", + "reset_profile_banner": "Profiel banner herstellen", + "reset_profile_background": "Profiel achtergrond herstellen", + "reset_avatar": "Avatar herstellen", + "reply_visibility_self_short": "Alleen antwoorden aan mijzelf tonen", + "reply_visibility_following_short": "Antwoorden naar mijn gevolgden tonen", + "file_export_import": { + "errors": { + "file_slightly_new": "Bestand minor versie is verschillend, sommige instellingen kunnen mogelijk niet worden geladen", + "file_too_old": "Incompatibele hoofdversie: {fileMajor}, bestandsversie is te oud en wordt niet ondersteund (minimale versie {feMajor})", + "file_too_new": "Incompatibele hoofdversie: {fileMajor}, deze PleromaFE (instellingen versie {feMajor}) is te oud om deze te ondersteunen", + "invalid_file": "Het geselecteerde bestand is niet een door Pleroma ondersteunde instellingen back-up. Er zijn geen wijzigingen gemaakt." + }, + "restore_settings": "Instellingen uit bestand herstellen", + "backup_settings_theme": "Instellingen en thema naar bestand back-uppen", + "backup_settings": "Instellingen naar bestand back-uppen", + "backup_restore": "Instellingen backup" + }, + "hide_wallpaper": "Instantie achtergrond verbergen", + "hide_all_muted_posts": "Genegeerde berichten verbergen", + "import_mutes_from_a_csv_file": "Importeer genegeerden van een csv bestand" }, "timeline": { "collapse": "Inklappen", @@ -488,7 +552,11 @@ "show_new": "Nieuwe tonen", "up_to_date": "Up-to-date", "no_statuses": "Geen statussen", - "no_more_statuses": "Geen statussen meer" + "no_more_statuses": "Geen statussen meer", + "socket_broke": "Realtime verbinding verloren: CloseEvent code {0}", + "socket_reconnected": "Realtime verbinding opgezet", + "reload": "Verversen", + "error": "Fout tijdens het ophalen van tijdlijn: {0}" }, "user_card": { "approve": "Goedkeuren", @@ -497,9 +565,9 @@ "deny": "Weigeren", "favorites": "Favorieten", "follow": "Volgen", + "follow_cancel": "Aanvraag annuleren", "follow_sent": "Aanvraag verzonden!", "follow_progress": "Aanvragen…", - "follow_again": "Aanvraag opnieuw zenden?", "follow_unfollow": "Stop volgen", "followees": "Aan het volgen", "followers": "Volgers", @@ -543,10 +611,18 @@ "report": "Aangeven", "mention": "Vermelding", "media": "Media", - "hidden": "Verborgen" + "hidden": "Verborgen", + "highlight": { + "side": "Zijstreep", + "striped": "Gestreepte achtergrond", + "solid": "Effen achtergrond", + "disabled": "Geen highlight" + }, + "bot": "Bot", + "message": "Bericht" }, "user_profile": { - "timeline_title": "Gebruikers Tijdlijn", + "timeline_title": "Gebruikerstijdlijn", "profile_loading_error": "Sorry, er is een fout opgetreden bij het laden van dit profiel.", "profile_does_not_exist": "Sorry, dit profiel bestaat niet." }, @@ -555,20 +631,22 @@ "who_to_follow": "Wie te volgen" }, "tool_tip": { - "media_upload": "Media Uploaden", + "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" + "add_reaction": "Reactie toevoegen", + "bookmark": "Bladwijzer" }, "upload": { "error": { "base": "Upload mislukt.", "file_too_big": "Bestand is te groot [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", - "default": "Probeer het later opnieuw" + "default": "Probeer het later opnieuw", + "message": "Upload is mislukt: {0}" }, "file_size_units": { "B": "B", @@ -585,25 +663,28 @@ "reject": "Afwijzen", "replace": "Vervangen", "is_replaced_by": "→", - "keyword_policies": "Zoekwoord Beleid", + "keyword_policies": "Zoekwoordbeleid", "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", + "mrf_policies_desc": "MRF-regels beïnvloeden het federatiegedrag van de instantie. De volgende regels zijn ingeschakeld:", + "mrf_policies": "Ingeschakelde MRF-regels", "simple": { - "simple_policies": "Instantie-specifieke Regels", + "simple_policies": "Instantiespecifieke regels", + "instance": "Instantie", + "reason": "Reden", + "not_applicable": "n.v.t.", "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:", + "quarantine_desc": "Deze instantie zal alleen openbare berichten sturen naar de volgende instanties:", + "ftl_removal_desc": "Deze instantie verwijdert de volgende instanties van \"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" + "ftl_removal": "Verwijderen van \"Bekende Netwerk\" Tijdlijn", + "media_removal": "Mediaverwijdering", + "media_nsfw": "Forceer media als gevoelig" } }, "staff": "Personeel" @@ -634,8 +715,8 @@ "next": "Volgende" }, "polls": { - "add_poll": "Poll Toevoegen", - "add_option": "Optie Toevoegen", + "add_poll": "Poll toevoegen", + "add_option": "Optie toevoegen", "option": "Optie", "votes": "stemmen", "vote": "Stem", @@ -645,31 +726,33 @@ "expires_in": "Poll eindigt in {0}", "expired": "Poll is {0} geleden beëindigd", "not_enough_options": "Te weinig opties in poll", - "type": "Poll type" + "type": "Poll-type", + "votes_count": "{count} stem | {count} stemmen", + "people_voted_count": "{count} persoon heeft gestemd | {count} personen hebben gestemd" }, "emoji": { "emoji": "Emoji", "keep_open": "Picker openhouden", - "search_emoji": "Zoek voor een emoji", + "search_emoji": "Emoji zoeken", "add_emoji": "Emoji invoegen", - "unicode": "Unicode emoji", + "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", + "favs_repeats": "Herhalingen en favorieten", + "follows": "Nieuwe gevolgden", + "moves": "Gebruikermigraties", "emoji_reactions": "Emoji Reacties", "reports": "Rapportages", - "moves": "Gebruiker migreert", "load_older": "Oudere interacties laden" }, "remote_user_resolver": { "searching_for": "Zoeken naar", "error": "Niet gevonden.", - "remote_user_resolver": "Externe gebruikers zoeker" + "remote_user_resolver": "Externe gebruikers-zoeker" }, "report": { "reporter": "Reporteerder:", @@ -728,7 +811,17 @@ "repeats": "Herhalingen", "favorites": "Favorieten", "thread_muted_and_words": ", heeft woorden:", - "thread_muted": "Thread genegeerd" + "thread_muted": "Thread genegeerd", + "expand": "Uitklappen", + "nsfw": "Gevoelig", + "status_deleted": "Dit bericht is verwijderd", + "hide_content": "Inhoud verbergen", + "show_content": "Inhoud tonen", + "hide_full_subject": "Volledig onderwerp verbergen", + "show_full_subject": "Volledig onderwerp tonen", + "external_source": "Externe bron", + "unbookmark": "Bladwijzer verwijderen", + "bookmark": "Bladwijzer toevoegen" }, "time": { "years_short": "{0}j", @@ -763,5 +856,33 @@ "day_short": "{0}d", "days": "{0} dagen", "day": "{0} dag" + }, + "shoutbox": { + "title": "Shoutbox" + }, + "errors": { + "storage_unavailable": "Pleroma kon browseropslag niet benaderen. Je login of lokale instellingen worden niet opgeslagen en je kunt onverwachte problemen ondervinden. Probeer cookies te accepteren." + }, + "display_date": { + "today": "Vandaag" + }, + "file_type": { + "file": "Bestand", + "image": "Afbeelding", + "video": "Video", + "audio": "Audio" + }, + "chats": { + "empty_chat_list_placeholder": "Je hebt nog geen chats. Start een nieuwe chat!", + "error_sending_message": "Er is iets fout gegaan tijdens het verzenden van het bericht.", + "error_loading_chat": "Er is iets fout gegaan tijdens het laden van de chat.", + "delete_confirm": "Wil je echt dit bericht verwijderen?", + "more": "Meer", + "empty_message_error": "Kan niet een leeg bericht plaatsen", + "new": "Nieuwe Chat", + "chats": "Chats", + "delete": "Verwijderen", + "message_user": "Spreek met {nickname}", + "you": "Jij:" } } diff --git a/src/i18n/oc.json b/src/i18n/oc.json @@ -465,7 +465,6 @@ "follow": "Seguir", "follow_sent": "Demanda enviada !", "follow_progress": "Demanda…", - "follow_again": "Tornar enviar la demanda ?", "follow_unfollow": "Quitar de seguir", "followees": "Abonaments", "followers": "Seguidors", diff --git a/src/i18n/pl.json b/src/i18n/pl.json @@ -19,8 +19,8 @@ "reject_desc": "Ta instancja odrzuca posty z wymienionych instancji:", "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 wymienionych instancje z \"Całej znanej sieci\":", + "ftl_removal": "Usunięcie z „Całej znanej sieci”", + "ftl_removal_desc": "Ta instancja usuwa wymienionych instancje z „Całej znanej sieci”:", "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", @@ -75,7 +75,13 @@ "loading": "Ładowanie…", "retry": "Spróbuj ponownie", "peek": "Spójrz", - "error_retry": "Spróbuj ponownie" + "error_retry": "Spróbuj ponownie", + "flash_content": "Naciśnij, aby wyświetlić zawartości Flash z użyciem Ruffle (eksperymentalnie, może nie działać).", + "flash_fail": "Nie udało się załadować treści flash, zajrzyj do konsoli, aby odnaleźć szczegóły.", + "role": { + "moderator": "Moderator", + "admin": "Administrator" + } }, "image_cropper": { "crop_picture": "Przytnij obrazek", @@ -118,7 +124,7 @@ "friend_requests": "Prośby o możliwość obserwacji", "mentions": "Wzmianki", "interactions": "Interakcje", - "dms": "Wiadomości prywatne", + "dms": "Wiadomości bezpośrednie", "public_tl": "Publiczna oś czasu", "timeline": "Oś czasu", "twkn": "Znana sieć", @@ -128,7 +134,8 @@ "preferences": "Preferencje", "bookmarks": "Zakładki", "chats": "Czaty", - "timelines": "Osie czasu" + "timelines": "Osie czasu", + "home_timeline": "Główna oś czasu" }, "notifications": { "broken_favorite": "Nieznany status, szukam go…", @@ -156,7 +163,9 @@ "expiry": "Czas trwania ankiety", "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" + "not_enough_options": "Zbyt mało unikalnych opcji w ankiecie", + "people_voted_count": "{count} osoba zagłosowała | {count} osoby zagłosowały | {count} osób zagłosowało", + "votes_count": "{count} głos | {count} głosy | {count} głosów" }, "emoji": { "stickers": "Naklejki", @@ -197,16 +206,17 @@ "unlisted": "Ten post nie będzie widoczny na publicznej osi czasu i całej znanej sieci" }, "scope": { - "direct": "Bezpośredni – Tylko dla wspomnianych użytkowników", - "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" + "direct": "Bezpośredni – tylko dla wspomnianych użytkowników", + "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", "media_description_error": "Nie udało się zaktualizować mediów, spróbuj ponownie", - "media_description": "Opis mediów" + "media_description": "Opis mediów", + "post": "Opublikuj" }, "registration": { "bio": "Bio", @@ -227,7 +237,10 @@ "password_required": "nie może być puste", "password_confirmation_required": "nie może być puste", "password_confirmation_match": "musi być takie jak hasło" - } + }, + "reason": "Powód rejestracji", + "reason_placeholder": "Ta instancja ręcznie zatwierdza rejestracje.\nPoinformuj administratora, dlaczego chcesz się zarejestrować.", + "register": "Zarejestruj się" }, "remote_user_resolver": { "remote_user_resolver": "Wyszukiwarka użytkowników nietutejszych", @@ -281,7 +294,7 @@ "cGreen": "Zielony (powtórzenia)", "cOrange": "Pomarańczowy (ulubione)", "cRed": "Czerwony (anuluj)", - "change_email": "Zmień email", + "change_email": "Zmień e-mail", "change_email_error": "Wystąpił problem podczas zmiany emaila.", "changed_email": "Pomyślnie zmieniono email!", "change_password": "Zmień hasło", @@ -345,7 +358,7 @@ "use_contain_fit": "Nie przycinaj załączników na miniaturach", "name": "Imię", "name_bio": "Imię i bio", - "new_email": "Nowy email", + "new_email": "Nowy e-mail", "new_password": "Nowe hasło", "notification_visibility": "Rodzaje powiadomień do wyświetlania", "notification_visibility_follows": "Obserwacje", @@ -361,8 +374,8 @@ "hide_followers_description": "Nie pokazuj kto mnie obserwuje", "hide_follows_count_description": "Nie pokazuj licznika obserwowanych", "hide_followers_count_description": "Nie pokazuj licznika obserwujących", - "show_admin_badge": "Pokazuj odznakę Administrator na moim profilu", - "show_moderator_badge": "Pokazuj odznakę Moderator na moim profilu", + "show_admin_badge": "Pokazuj odznakę „Administrator” na moim profilu", + "show_moderator_badge": "Pokazuj odznakę „Moderator” na moim profilu", "nsfw_clickthrough": "Włącz domyślne ukrywanie załączników o treści nieprzyzwoitej (NSFW)", "oauth_tokens": "Tokeny OAuth", "token": "Token", @@ -600,7 +613,27 @@ "mute_import": "Import wyciszeń", "mute_export_button": "Wyeksportuj swoje wyciszenia do pliku .csv", "mute_export": "Eksport wyciszeń", - "hide_wallpaper": "Ukryj tło instancji" + "hide_wallpaper": "Ukryj tło instancji", + "save": "Zapisz zmiany", + "setting_changed": "Opcja różni się od domyślnej", + "right_sidebar": "Pokaż pasek boczny po prawej", + "file_export_import": { + "errors": { + "invalid_file": "Wybrany plik nie jest obsługiwaną kopią zapasową ustawień Pleromy. Nie dokonano żadnych zmian." + }, + "backup_restore": "Kopia zapasowa ustawień", + "backup_settings": "Kopia zapasowa ustawień do pliku", + "backup_settings_theme": "Kopia zapasowa ustawień i motywu do pliku", + "restore_settings": "Przywróć ustawienia z pliku" + }, + "more_settings": "Więcej ustawień", + "word_filter": "Filtr słów", + "hide_media_previews": "Ukryj podgląd mediów", + "hide_all_muted_posts": "Ukryj wyciszone słowa", + "reply_visibility_following_short": "Pokazuj odpowiedzi obserwującym", + "reply_visibility_self_short": "Pokazuj odpowiedzi tylko do mnie", + "sensitive_by_default": "Domyślnie oznaczaj wpisy jako wrażliwe", + "hide_shoutbox": "Ukryj shoutbox instancji" }, "time": { "day": "{0} dzień", @@ -648,7 +681,9 @@ "no_more_statuses": "Brak kolejnych statusów", "no_statuses": "Brak statusów", "reload": "Odśwież", - "error": "Błąd pobierania osi czasu: {0}" + "error": "Błąd pobierania osi czasu: {0}", + "socket_broke": "Utracono połączenie w czasie rzeczywistym: kod CloseEvent {0}", + "socket_reconnected": "Osiągnięto połączenie w czasie rzeczywistym" }, "status": { "favorites": "Ulubione", @@ -686,7 +721,6 @@ "follow": "Obserwuj", "follow_sent": "Wysłano prośbę!", "follow_progress": "Wysyłam prośbę…", - "follow_again": "Wysłać prośbę ponownie?", "follow_unfollow": "Przestań obserwować", "followees": "Obserwowani", "followers": "Obserwujący", @@ -731,7 +765,12 @@ "delete_user": "Usuń użytkownika", "delete_user_confirmation": "Czy jesteś absolutnie pewny(-a)? Ta operacja nie może być cofnięta." }, - "message": "Napisz" + "message": "Napisz", + "edit_profile": "Edytuj profil", + "highlight": { + "disabled": "Bez wyróżnienia" + }, + "bot": "Bot" }, "user_profile": { "timeline_title": "Oś czasu użytkownika", diff --git a/src/i18n/pt.json b/src/i18n/pt.json @@ -35,7 +35,12 @@ "retry": "Tenta novamente", "error_retry": "Por favor, tenta novamente", "loading": "A carregar…", - "dismiss": "Ignorar" + "dismiss": "Ignorar", + "role": + { + "moderator": "Moderador", + "admin": "Admin" + } }, "image_cropper": { "crop_picture": "Cortar imagem", @@ -570,7 +575,6 @@ "follow": "Seguir", "follow_sent": "Pedido enviado!", "follow_progress": "Enviando…", - "follow_again": "Enviar solicitação novamente?", "follow_unfollow": "Deixar de seguir", "followees": "Seguindo", "followers": "Seguidores", @@ -615,11 +619,7 @@ "report": "Denunciar", "message": "Mensagem", "mention": "Mencionar", - "hidden": "Ocultar", - "roles": { - "moderator": "Moderador", - "admin": "Admin" - } + "hidden": "Ocultar" }, "user_profile": { "timeline_title": "Cronologia do Utilizador", diff --git a/src/i18n/ru.json b/src/i18n/ru.json @@ -13,7 +13,7 @@ "disable": "Оключить", "enable": "Включить", "confirm": "Подтвердить", - "verify": "Проверить", + "verify": "Подтверждение", "more": "Больше", "generic_error": "Произошла ошибка", "optional": "не обязательно", @@ -24,7 +24,11 @@ "retry": "Попробуйте еще раз", "error_retry": "Пожалуйста попробуйте еще раз", "close": "Закрыть", - "loading": "Загрузка…" + "loading": "Загрузка…", + "role": { + "moderator": "Модератор", + "admin": "Администратор" + } }, "login": { "login": "Войти", @@ -40,8 +44,8 @@ "heading": { "TotpForm": "Двухфакторная аутентификация", "RecoveryForm": "Two-factor recovery", - "totp": "Двухфакторная аутентификация", - "recovery": "Двухфакторное возвращение аккаунта" + "totp": "Двухэтапная аутентификация", + "recovery": "Восстановление двухэтапной аутентификации" }, "hint": "Войдите чтобы присоединиться к дискуссии", "description": "Войти с помощью OAuth" @@ -51,8 +55,8 @@ "chat": "Локальный чат", "mentions": "Упоминания", "interactions": "Взаимодействия", - "public_tl": "Публичная лента", - "timeline": "Лента", + "public_tl": "Локальная лента", + "timeline": "Главная", "twkn": "Федеративная лента", "search": "Поиск", "friend_requests": "Запросы на чтение", @@ -61,10 +65,11 @@ "timelines": "Ленты", "preferences": "Настройки", "who_to_follow": "Кого читать", - "dms": "Личные Сообщения", + "dms": "Личные сообщения", "administration": "Панель администратора", - "about": "О сервере", - "user_search": "Поиск пользователей" + "about": "Об узле", + "user_search": "Поиск пользователей", + "home_timeline": "Главная" }, "notifications": { "broken_favorite": "Неизвестный статус, ищем…", @@ -75,35 +80,35 @@ "read": "Прочесть", "repeated_you": "повторил(а) ваш статус", "follow_request": "хочет читать вас", - "reacted_with": "добавил реакцию: {0}", - "migrated_to": "мигрировал на", + "reacted_with": "добавил(а) реакцию: {0}", + "migrated_to": "перехал на", "no_more_notifications": "Нет дальнейших уведомлений", "error": "Ошибка при обновлении уведомлений: {0}" }, "interactions": { - "favs_repeats": "Повторы и фавориты", + "favs_repeats": "Повторы и отметки «Нравится»", "follows": "Новые читатели", "load_older": "Загрузить старые взаимодействия", - "moves": "Миграции пользователей" + "moves": "Переезды" }, "post_status": { - "account_not_locked_warning": "Ваш аккаунт не {0}. Кто угодно может начать читать вас чтобы видеть посты только для подписчиков.", - "account_not_locked_warning_link": "залочен", - "attachments_sensitive": "Вложения содержат чувствительный контент", + "account_not_locked_warning": "Ваша учетная запись не {0}. Кто угодно может начать читать вас чтобы видеть статусы только для читателей.", + "account_not_locked_warning_link": "закрыт", + "attachments_sensitive": "Вложения имеют щекотливый характер", "content_warning": "Тема (не обязательно)", "default": "Что нового?", "direct_warning": "Этот пост будет виден только упомянутым пользователям", "posting": "Отправляется", "scope_notice": { - "public": "Этот пост будет виден всем", - "private": "Этот пост будет виден только вашим подписчикам", - "unlisted": "Этот пост не будет виден в публичной и федеративной ленте" + "public": "Этот статус будет виден всем", + "private": "Этот статус будет виден только вашим читателям", + "unlisted": "Этот статус не будет виден в локальной и федеративной ленте" }, "scope": { - "direct": "Личное - этот пост видят только те кто в нём упомянут", - "private": "Для подписчиков - этот пост видят только подписчики", - "public": "Публичный - этот пост виден всем", - "unlisted": "Непубличный - этот пост не виден на публичных лентах" + "direct": "Личное сообщение - этот статус видят только те, кто в нём упомянут", + "private": "Для читателей - этот статус видят только ваши читатели", + "public": "Публичный - этот статус виден всем", + "unlisted": "Тихий - этот пост виден всем, но не отображается в публичных лентах" }, "preview_empty": "Пустой предпросмотр", "media_description_error": "Не удалось обновить вложение, попробуйте еще раз", @@ -118,11 +123,12 @@ "text/plain": "Простой текст" }, "media_description": "Описание вложения", - "new_status": "Написать новый статус" + "new_status": "Написать новый статус", + "post": "Опубликовать" }, "registration": { - "bio": "Описание", - "email": "Email", + "bio": "О себе", + "email": "Электронная почта", "fullname": "Отображаемое имя", "password_confirm": "Подтверждение пароля", "registration": "Регистрация", @@ -139,7 +145,10 @@ "fullname_placeholder": "например: Почтальон Печкин", "username_placeholder": "например: pechkin", "captcha": "Код подтверждения", - "new_captcha": "Нажмите на изображение чтобы получить новый код" + "new_captcha": "Нажмите на изображение чтобы получить новый код", + "reason_placeholder": "Данный узел обрабатывает запросы на регистрацию вручную.\nРасскажите администрации почему вы хотите зарегистрироваться.", + "reason": "Причина регистрации", + "register": "Зарегистрироваться" }, "settings": { "enter_current_password_to_confirm": "Введите свой текущий пароль", @@ -148,7 +157,7 @@ "setup_otp": "Настройка OTP", "wait_pre_setup_otp": "предварительная настройка OTP", "confirm_and_enable": "Подтвердить и включить OTP", - "title": "Двухфакторная аутентификация", + "title": "Двухэтапная аутентификация", "generate_new_recovery_codes": "Получить новые коды востановления", "warning_of_generate_new_codes": "После получения новых кодов восстановления, старые больше не будут работать.", "recovery_codes": "Коды восстановления.", @@ -157,11 +166,11 @@ "authentication_methods": "Методы аутентификации", "scan": { "title": "Сканирование", - "desc": "Используйте приложение для двухэтапной аутентификации для сканирования этого QR-код или введите текстовый ключ:", + "desc": "Отсканируйте QR-код приложением для двухэтапной аутентификации или введите текстовый ключ:", "secret_code": "Ключ" }, "verify": { - "desc": "Чтобы включить двухэтапную аутентификации, введите код из вашего приложение для двухэтапной аутентификации:" + "desc": "Чтобы включить двухэтапную аутентификацию, введите код из приложения-аутентификатора:" } }, "attachmentRadius": "Прикреплённые файлы", @@ -170,16 +179,16 @@ "avatarAltRadius": "Аватары в уведомлениях", "avatarRadius": "Аватары", "background": "Фон", - "bio": "Описание", + "bio": "О себе", "btnRadius": "Кнопки", - "bot": "Это аккаунт бота", + "bot": "Это учётная запись бота", "cBlue": "Ответить, читать", "cGreen": "Повторить", "cOrange": "Нравится", "cRed": "Отменить", - "change_email": "Сменить email", - "change_email_error": "Произошла ошибка при попытке изменить email.", - "changed_email": "Email изменён успешно!", + "change_email": "Сменить адрес электронной почты", + "change_email_error": "Произошла ошибка при попытке изменить электронную почту.", + "changed_email": "Электронная почта изменена успешно!", "change_password": "Сменить пароль", "change_password_error": "Произошла ошибка при попытке изменить пароль.", "changed_password": "Пароль изменён успешно!", @@ -189,9 +198,9 @@ "current_password": "Текущий пароль", "current_profile_banner": "Текущий баннер профиля", "data_import_export_tab": "Импорт / Экспорт данных", - "delete_account": "Удалить аккаунт", - "delete_account_description": "Удалить вашу учётную запись и все ваши сообщения.", - "delete_account_error": "Возникла ошибка в процессе удаления вашего аккаунта. Если это повторяется, свяжитесь с администратором вашего сервера.", + "delete_account": "Удалить учетную запись", + "delete_account_description": "Навсегда удалить вашу учётную запись и ваши статусы.", + "delete_account_error": "Возникла ошибка в процессе удаления вашей учетной записи. Если это повторяется, свяжитесь с администратором данного узла.", "delete_account_instructions": "Введите ваш пароль в поле ниже для подтверждения удаления.", "export_theme": "Сохранить Тему", "filtering": "Фильтрация", @@ -217,28 +226,28 @@ "interfaceLanguage": "Язык интерфейса", "limited_availability": "Не доступно в вашем браузере", "links": "Ссылки", - "lock_account_description": "Аккаунт доступен только подтверждённым подписчикам", + "lock_account_description": "Сделать учетную запись закрытой — подтверждать читателей вручную", "loop_video": "Зациливать видео", "loop_video_silent_only": "Зацикливать только беззвучные видео (т.е. \"гифки\" с Mastodon)", "name": "Имя", - "name_bio": "Имя и описание", - "new_email": "Новый email", + "name_bio": "Личные данные", + "new_email": "Новый адрес электронной почты", "new_password": "Новый пароль", "fun": "Потешное", "greentext": "Мемные стрелочки", "notification_visibility": "Показывать уведомления", - "notification_visibility_follows": "Подписки", + "notification_visibility_follows": "Новые читатели", "notification_visibility_likes": "Лайки", "notification_visibility_mentions": "Упоминания", "notification_visibility_repeats": "Повторы", - "no_rich_text_description": "Убрать форматирование из всех постов", + "no_rich_text_description": "Убрать форматирование из всех статусов", "hide_follows_description": "Не показывать кого я читаю", "hide_followers_description": "Не показывать кто читает меня", "hide_follows_count_description": "Не показывать число читаемых пользователей", - "hide_followers_count_description": "Не показывать число моих подписчиков", + "hide_followers_count_description": "Не показывать число моих читателей", "show_admin_badge": "Показывать значок администратора в моем профиле", "show_moderator_badge": "Показывать значок модератора в моем профиле", - "nsfw_clickthrough": "Включить скрытие вложений и предпросмотра ссылок для NSFW статусов", + "nsfw_clickthrough": "Включить скрытие вложений и предпросмотра ссылок для статусов щекотливого характера", "oauth_tokens": "OAuth токены", "token": "Токен", "refresh_token": "Рефреш токен", @@ -253,14 +262,14 @@ "radii_help": "Скругление углов элементов интерфейса (в пикселях)", "replies_in_timeline": "Ответы в ленте", "reply_visibility_all": "Показывать все ответы", - "reply_visibility_following": "Показывать только ответы мне или тех на кого я подписан", + "reply_visibility_following": "Показывать только ответы мне или тем кого я читаю", "reply_visibility_self": "Показывать только ответы мне", - "autohide_floating_post_button": "Автоматически скрывать кнопку постинга (в мобильной версии)", + "autohide_floating_post_button": "Автоматически скрывать кнопку \"Написать новый статус\" (в мобильной версии)", "saving_err": "Не удалось сохранить настройки", "saving_ok": "Сохранено", "security_tab": "Безопасность", - "scope_copy": "Копировать видимость поста при ответе (всегда включено для Личных Сообщений)", - "minimal_scopes_mode": "Минимизировать набор опций видимости поста", + "scope_copy": "Копировать видимость поста при ответе (всегда включено для личных сообщений)", + "minimal_scopes_mode": "Показывать только личное сообщение и публичный статус в опциях видимости", "set_new_avatar": "Загрузить новый аватар", "set_new_profile_background": "Загрузить новый фон профиля", "set_new_profile_banner": "Загрузить новый баннер профиля", @@ -269,7 +278,7 @@ "stop_gifs": "Проигрывать GIF анимации только при наведении", "streaming": "Включить автоматическую загрузку новых сообщений при прокрутке вверх", "useStreamingApi": "Получать сообщения и уведомления в реальном времени", - "useStreamingApiWarning": "(Не рекомендуется, экспериментально, сообщения могут пропадать)", + "useStreamingApiWarning": "(Не рекомендуется, экспериментально, статусы могут пропадать)", "text": "Текст", "theme": "Тема", "theme_help": "Используйте шестнадцатеричные коды цветов (#rrggbb) для настройки темы.", @@ -301,7 +310,8 @@ "older_version_imported": "Файл, который вы импортировали, был сделан в старой версии фронт-энда.", "future_version_imported": "Файл, который вы импортировали, был сделан в новой версии фронт-энда.", "v2_imported": "Файл, который вы импортировали, был сделан под старый фронт-энд. Мы стараемся улучшить совместимость, но все еще возможны несостыковки.", - "upgraded_from_v2": "Фронт-энд Pleroma был изменен. Выбранная тема может выглядеть слегка по-другому." + "upgraded_from_v2": "Фронт-энд Pleroma был изменен. Выбранная тема может выглядеть слегка по-другому.", + "fe_downgraded": "Версия фронт-энда Pleroma была откачена." } }, "common": { @@ -333,13 +343,29 @@ "badge": "Фон значков", "badge_notification": "Уведомления", "panel_header": "Заголовок панели", - "top_bar": "Верняя полоска", + "top_bar": "Верхняя полоска", "borders": "Границы", "buttons": "Кнопки", "inputs": "Поля ввода", "faint_text": "Маловажный текст", - "post": "Сообщения и описание пользователя", - "alert_neutral": "Нейтральный" + "post": "Статусы и раздел \"О себе\"", + "alert_neutral": "Нейтральный", + "alert_warning": "Предупреждение", + "selectedPost": "Выбранный статус", + "pressed": "Нажатие", + "highlight": "Выделенные элементы", + "icons": "Иконки", + "poll": "График результатов опроса", + "wallpaper": "Фон", + "chat": { + "border": "Границы", + "outgoing": "Исходящие", + "incoming": "Входящие" + }, + "tabs": "Вкладки", + "toggled": "Включено", + "disabled": "Отключено", + "selectedMenu": "Выбранный пункт меню" }, "radii": { "_tab_label": "Скругление" @@ -364,8 +390,8 @@ "panel": "Панель", "panelHeader": "Заголовок панели", "topBar": "Верхняя полоска", - "avatar": "Аватарка (профиль)", - "avatarStatus": "Аватарка (в ленте)", + "avatar": "Аватар (профиль)", + "avatarStatus": "Аватар (в ленте)", "popup": "Всплывающие подсказки", "button": "Кнопки", "buttonHover": "Кнопки (наведен курсор)", @@ -381,7 +407,7 @@ "interface": "Интерфейс", "input": "Поля ввода", "post": "Текст постов", - "postCode": "Моноширинный текст в посте (форматирование)" + "postCode": "Моноширинный текст в статусе (форматирование)" }, "family": "Шрифт", "size": "Размер (в пикселях)", @@ -403,12 +429,12 @@ "link": "ссылка" } }, - "allow_following_move": "Разрешить автоматически читать новый аккаунт при перемещении на другой сервер", + "allow_following_move": "Автоматически начать читать новый профиль при переезде", "hide_user_stats": "Не показывать статистику пользователей (например количество читателей)", - "discoverable": "Разрешить показ аккаунта в поисковиках и других сервисах", - "default_vis": "Видимость постов по умолчанию", + "discoverable": "Разрешить показывать учетную запись в поисковых системах и прочих сервисах", + "default_vis": "Видимость статусов по умолчанию", "mutes_and_blocks": "Блокировки и игнорируемые", - "composing": "Составление постов", + "composing": "Составление статусов", "chatMessageRadius": "Сообщения в беседе", "blocks_tab": "Блокировки", "import_mutes_from_a_csv_file": "Импортировать игнорируемых из CSV файла", @@ -428,12 +454,12 @@ "post_status_content_type": "Формат составляемых статусов по умолчанию", "subject_line_noop": "Не копировать", "subject_line_mastodon": "Как в Mastodon: скопировать как есть", - "subject_line_email": "Как в e-mail: \"re: тема\"", + "subject_line_email": "Как в электронной почте: \"re: тема\"", "subject_line_behavior": "Копировать тему в ответах", "no_mutes": "Нет игнорируемых", "no_blocks": "Нет блокировок", "notification_visibility_emoji_reactions": "Реакции", - "notification_visibility_moves": "Миграции пользователей", + "notification_visibility_moves": "Переезды", "use_contain_fit": "Не обрезать вложения в миниатюрах", "profile_fields": { "value": "Значение", @@ -448,7 +474,7 @@ "hide_filtered_statuses": "Не показывать отфильтрованные статусы", "hide_muted_posts": "Не показывать статусы игнорируемых пользователей", "hide_post_stats": "Не показывать статистику статусов (например количество отметок «Нравится»)", - "use_one_click_nsfw": "Открывать NSFW вложения одним кликом", + "use_one_click_nsfw": "Открывать вложения имеющие щекотливый характер одним кликом", "preload_images": "Предварительно загружать изображения", "max_thumbnails": "Максимальное число миниатюр показываемых в статусе", "emoji_reactions_on_timeline": "Показывать эмодзи реакции в ленте", @@ -460,26 +486,43 @@ "virtual_scrolling": "Оптимизировать рендеринг ленты", "hide_wallpaper": "Скрыть обои узла", "accent": "Акцент", - "upload_a_photo": "Загрузить фото", - "notification_mutes": "Чтобы не получать уведомления от определённого пользователя, заглушите его.", - "reset_avatar_confirm": "Вы действительно хотите сбросить личный образ?", - "reset_profile_banner": "Сбросить личный баннер", - "reset_profile_background": "Сбросить личные обои", - "reset_avatar": "Сбросить личный образ", - "search_user_to_mute": "Искать, кого вы хотите заглушить", - "search_user_to_block": "Искать, кого вы хотите заблокировать", - "pad_emoji": "Выделять эмодзи пробелами при добавлении из панели", - "avatar_size_instruction": "Желательный наименьший размер личного образа 150 на 150 пикселей.", + "upload_a_photo": "Загрузить изображение", + "notification_mutes": "Чтобы не получать уведомления от конкретного пользователя, заглушите его.", + "reset_avatar_confirm": "Вы точно хотите сбросить аватар?", + "reset_profile_banner": "Сбросить баннер профиля", + "reset_profile_background": "Сбросить фон профиля", + "reset_avatar": "Сбросить аватар", + "search_user_to_mute": "Поиск того, кого вы хотите заглушить", + "search_user_to_block": "Поиск того, кого вы хотите заблокировать", + "pad_emoji": "Разделять эмодзи пробелами, когда они добавляются из меню", + "avatar_size_instruction": "Рекомендуется использовать изображение больше чем 150 на 150 пикселей в качестве аватара.", "enable_web_push_notifications": "Включить web push-уведомления", "notification_blocks": "Блокировка пользователя выключает все уведомления от него, а также отписывает вас от него.", - "notification_setting_hide_notification_contents": "Скрыть отправителя и содержимое push-уведомлений" + "notification_setting_hide_notification_contents": "Скрыть отправителя и содержимое push-уведомлений", + "version": { + "title": "Версия", + "frontend_version": "Версия фронт-энда", + "backend_version": "Версия бэк-энда" + }, + "word_filter": "Фильтр слов", + "sensitive_by_default": "Помечать статусы как имеющие щекотливый характер по умолчанию", + "reply_visibility_self_short": "Показывать ответы только вам", + "reply_visibility_following_short": "Показывать ответы тем кого вы читаете", + "hide_all_muted_posts": "Не показывать игнорируемые статусы", + "hide_media_previews": "Не показывать вложения в ленте", + "setting_changed": "Отличается от значения по умолчанию", + "reset_background_confirm": "Вы точно хотите сбросить фон?", + "reset_banner_confirm": "Вы точно хотите сбросить баннер?", + "type_domains_to_mute": "Поиск узлов, которые вы хотите заглушить", + "more_settings": "Остальные настройки", + "save": "Сохранить изменения" }, "timeline": { "collapse": "Свернуть", "conversation": "Разговор", "error_fetching": "Ошибка при обновлении", "load_older": "Загрузить старые статусы", - "no_retweet_hint": "Пост помечен как \"только для подписчиков\" или \"личное\" и поэтому не может быть повторён", + "no_retweet_hint": "Статус помечен как \"только для читателей\" или \"личное сообщение\" и потому не может быть повторён", "repeated": "повторил(а)", "show_new": "Показать новые", "up_to_date": "Обновлено", @@ -488,7 +531,7 @@ "status": { "bookmark": "Добавить в закладки", "unbookmark": "Удалить из закладок", - "status_deleted": "Пост удален", + "status_deleted": "Статус удален", "reply_to": "Ответ", "repeats": "Повторы", "favorites": "Понравилось", @@ -507,7 +550,6 @@ "follow": "Читать", "follow_sent": "Запрос отправлен!", "follow_progress": "Запрашиваем…", - "follow_again": "Запросить еще раз?", "follow_unfollow": "Перестать читать", "followees": "Читаемые", "followers": "Читатели", @@ -524,16 +566,16 @@ "revoke_admin": "Забрать права администратора", "grant_moderator": "Сделать модератором", "revoke_moderator": "Забрать права модератора", - "activate_account": "Активировать аккаунт", - "deactivate_account": "Деактивировать аккаунт", - "delete_account": "Удалить аккаунт", - "force_nsfw": "Отмечать посты пользователя как NSFW", - "strip_media": "Убирать вложения из постов пользователя", - "force_unlisted": "Не добавлять посты в публичные ленты", + "activate_account": "Активировать учетную запись", + "deactivate_account": "Деактивировать учетную запись", + "delete_account": "Удалить учетную запись", + "force_nsfw": "Отмечать статусы пользователя как имеющие щекотливый характер", + "strip_media": "Убирать вложения из статусов пользователя", + "force_unlisted": "Не показывать статусы в публичных лентах", "sandbox": "Принудить видимость постов только читателям", - "disable_remote_subscription": "Запретить читать с удаленных серверов", + "disable_remote_subscription": "Запретить читать с других узлов", "disable_any_subscription": "Запретить читать пользователя", - "quarantine": "Не федерировать посты пользователя", + "quarantine": "Не федерировать статусы пользователя", "delete_user": "Удалить пользователя", "delete_user_confirmation": "Вы уверены? Это действие нельзя отменить." }, @@ -542,9 +584,12 @@ "show_repeats": "Показывать повторы", "hide_repeats": "Скрыть повторы", "report": "Пожаловаться", - "roles": { - "moderator": "Модератор", - "admin": "Администратор" + "message": "Написать сообщение", + "highlight": { + "side": "Полоска сбоку", + "striped": "Фон в полоску", + "solid": "Сплошной фон", + "disabled": "Нет выделения" } }, "user_profile": { @@ -560,30 +605,31 @@ "password_reset": { "forgot_password": "Забыли пароль?", "password_reset": "Сброс пароля", - "instruction": "Введите ваш email или имя пользователя, и мы отправим вам ссылку для сброса пароля.", - "placeholder": "Ваш email или имя пользователя", - "check_email": "Проверьте ваш email и перейдите по ссылке для сброса пароля.", + "instruction": "Введите ваш адрес электронной почты или имя пользователя: на вашу электронную почту будет отправлена ссылка для сброса пароля.", + "placeholder": "Ваш адрес электронной почты или имя пользователя", + "check_email": "Проверьте вашу электронную почту и перейдите по ссылке для сброса пароля.", "return_home": "Вернуться на главную страницу", "too_many_requests": "Вы исчерпали допустимое количество попыток, попробуйте позже.", - "password_reset_disabled": "Сброс пароля отключен. Cвяжитесь с администратором вашего сервера." + "password_reset_disabled": "Автоматический сброс пароля отключен. Свяжитесь с администратором данного узла для сброса пароля.", + "password_reset_required_but_mailer_is_disabled": "Вы должны сбросить свой пароль, однако автоматический сброс пароля отключен. Пожалуйста свяжитесь с администратором данного узла." }, "about": { "mrf": { "federation": "Федерация", "simple": { - "accept_desc": "Данный сервер принимает сообщения только со следующих серверов:", - "ftl_removal_desc": "Данный сервер скрывает следующие сервера с федеративной ленты:", - "media_nsfw_desc": "Данный сервер принужденно помечает вложения со следущих серверов как NSFW:", - "simple_policies": "Правила для определенных серверов", - "accept": "Принимаемые сообщения", - "reject": "Отклоняемые сообщения", - "reject_desc": "Данный сервер не принимает сообщения со следующих серверов:", + "accept_desc": "Данный узел принимает сообщения только со следующих узлов:", + "ftl_removal_desc": "Данный узел скрывает следующие узлы с федеративной ленты:", + "media_nsfw_desc": "Данный узел принужденно помечает вложения со следующих узлов как имеющие щекотливый характер:", + "simple_policies": "Правила для определенных узлов", + "accept": "Белый список", + "reject": "Черный список", + "reject_desc": "Данный узел не принимает сообщения со следующих узлов:", "quarantine": "Зона карантина", - "quarantine_desc": "Данный сервер отправляет только публичные посты следующим серверам:", + "quarantine_desc": "Данный узел отправляет только публичные статусы следующим узлам:", "ftl_removal": "Скрытие с федеративной ленты", "media_removal": "Удаление вложений", - "media_removal_desc": "Данный сервер удаляет вложения со следующих серверов:", - "media_nsfw": "Принужденно помеченно как NSFW" + "media_removal_desc": "Данный узел удаляет вложения со следующих узлов:", + "media_nsfw": "Принужденно помеченно как имеющее щекотливый характер" }, "keyword": { "ftl_removal": "Убрать из федеративной ленты", @@ -593,7 +639,7 @@ "is_replaced_by": "→" }, "mrf_policies": "Активные правила MRF (модуль переписывания сообщений)", - "mrf_policies_desc": "Правила MRF (модуль переписывания сообщений) влияют на федерацию данного сервера. Следующие правила активны:" + "mrf_policies_desc": "Правила MRF (модуль переписывания сообщений) влияют на федерацию данного узла. Следующие правила активны:" }, "staff": "Администрация" }, @@ -615,7 +661,8 @@ "gopher": "Gopher", "who_to_follow": "Предложения кого читать", "pleroma_chat_messages": "Pleroma Чат", - "upload_limit": "Наибольший размер загружаемого файла" + "upload_limit": "Наибольший размер загружаемого файла", + "scope_options": "Настраиваемая видимость статусов" }, "tool_tip": { "accept_follow_request": "Принять запрос на чтение", @@ -643,7 +690,9 @@ "votes": "голосов", "option": "Вариант", "add_option": "Добавить вариант", - "add_poll": "Прикрепить опрос" + "add_poll": "Прикрепить опрос", + "votes_count": "{count} голос | {count} голосов", + "people_voted_count": "{count} человек проголосовал | {count} человек проголосовали" }, "media_modal": { "next": "Следующая", @@ -701,10 +750,26 @@ "chats": "Беседы", "delete": "Удалить", "message_user": "Напишите {nickname}", - "you": "Вы:" + "you": "Вы:", + "error_sending_message": "Произошла ошибка при отправке сообщения." }, "remote_user_resolver": { "error": "Не найдено.", "searching_for": "Ищем" + }, + "upload": { + "error": { + "message": "Произошла ошибка при загрузке: {0}" + } + }, + "user_reporting": { + "add_comment_description": "Жалоба будет направлена модераторам вашего узла. Вы можете указать причину жалобы ниже:", + "forward_description": "Данный пользователь находится на другом узле. Отослать туда копию вашей жалобы?" + }, + "file_type": { + "file": "Файл", + "video": "Видеозапись", + "audio": "Аудиозапись", + "image": "Изображение" } } diff --git a/src/i18n/te.json b/src/i18n/te.json @@ -310,7 +310,6 @@ "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", diff --git a/src/i18n/uk.json b/src/i18n/uk.json @@ -17,7 +17,14 @@ "more": "Більше", "submit": "Відправити", "apply": "Застосувати", - "peek": "Глянути" + "peek": "Глянути", + "role": { + "moderator": "Модератор", + "admin": "Адміністратор" + }, + "flash_content": "Натисніть для перегляду змісту Flash за допомогою Ruffle (експериментально, може не працювати).", + "flash_security": "Ця функція може становити ризик, оскільки Flash-вміст все ще є потенційно небезпечним.", + "flash_fail": "Не вдалося завантажити Flash-вміст, докладнішу інформацію дивись у консолі." }, "finder": { "error_fetching_user": "Користувача не знайдено", @@ -26,7 +33,7 @@ "features_panel": { "gopher": "Gopher", "pleroma_chat_messages": "Чати", - "chat": "Міні-чат", + "chat": "Оголошення", "who_to_follow": "Кого відстежувати", "title": "Особливості", "scope_options": "Параметри обсягу", @@ -45,7 +52,7 @@ "mute": "Ігнорувати" }, "shoutbox": { - "title": "Міні-чат" + "title": "Оголошення" }, "about": { "staff": "Адміністрація", @@ -118,7 +125,9 @@ "votes": "голосів", "option": "Відповідь", "add_poll": "Додати опитування", - "not_enough_options": "Замало унікальних варіантів в опитуванні" + "not_enough_options": "Замало унікальних варіантів в опитуванні", + "people_voted_count": "{count} особа проголосувала | {count} осіб проголосувало", + "votes_count": "{count} голос | {count} голосів" }, "notifications": { "reacted_with": "додав реакцію: {0}", @@ -151,7 +160,8 @@ "interactions": "Взаємодії", "mentions": "Згадування", "back": "Назад", - "administration": "Адміністрування" + "administration": "Адміністрування", + "home_timeline": "Домашня стрічка" }, "media_modal": { "next": "Наступна", @@ -242,7 +252,8 @@ }, "preview_empty": "Пустий", "media_description_error": "Не вдалось оновити медіа, спробуйте ще раз", - "media_description": "Опис медіа" + "media_description": "Опис медіа", + "post": "Опублікувати" }, "settings": { "blocks_imported": "Блокування імпортовані! Їх обробка триватиме певний час.", @@ -604,7 +615,30 @@ "backend_version": "Версія бекенду", "title": "Версія" }, - "hide_wallpaper": "Сховати шпалери екземпляру" + "hide_wallpaper": "Сховати шпалери екземпляру", + "more_settings": "Більше налаштувань", + "sensitive_by_default": "Визначати допис як дратівливий за замовчуванням", + "reply_visibility_self_short": "Показувати відповіді лише мені", + "reply_visibility_following_short": "Показувати відповіді тим, на кого я підписаний", + "hide_all_muted_posts": "Приховати приглушені повідомлення", + "hide_media_previews": "Приховати попередній перегляд медіа", + "word_filter": "Фільтр слів", + "setting_changed": "Конфігурація відрізняється від типової", + "save": "Зберегти зміни", + "file_export_import": { + "errors": { + "file_slightly_new": "Другорядна версія файлу відрізняється, деякі налаштування можуть бути не прийняті", + "file_too_old": "Несумісна основна версія: {fileMajor}, версія файлу занадто стара і не підтримується (мінімальна версія налаштувань {feMajor})", + "file_too_new": "Несумісна основна версія: {fileMajor}, ця версія PleromaFE ({feMajor}) занадто стара для його обробки", + "invalid_file": "Вибраний файл не є резервною копією налаштувань Pleroma. Ніяких змін не було зроблено." + }, + "restore_settings": "Відновити налаштування з файлу", + "backup_settings_theme": "Резервне копіювання налаштувань та теми у файл", + "backup_settings": "Резервне копіювання налаштувань у файл", + "backup_restore": "Резервне копіювання налаштувань" + }, + "right_sidebar": "Показувати бокову панель справа", + "hide_shoutbox": "Приховати оголошення інстансу" }, "selectable_list": { "select_all": "Вибрати все" @@ -633,7 +667,10 @@ "fullname": "Відображене ім'я", "email": "Ел. пошта", "bio": "Про себе", - "captcha": "CAPTCHA" + "captcha": "CAPTCHA", + "register": "Зареєструватися", + "reason_placeholder": "Цей інстанс обробляє запити на реєстрацію вручну.\nРозкажіть адміністрації чому ви хочете зареєструватися.", + "reason": "Причина реєстрації" }, "who_to_follow": { "who_to_follow": "На кого підписатися", @@ -711,7 +748,6 @@ "message": "Повідомлення", "follow": "Підписатись", "follow_unfollow": "Відписатись", - "follow_again": "Відправити запит знову?", "follow_sent": "Запит відправлено!", "blocked": "Заблоковано!", "admin_menu": { @@ -761,10 +797,14 @@ "remote_follow": "Підписатись", "muted": "Заглушений", "mute": "Заглушити", - "roles": { - "moderator": "Модератор", - "admin": "Адміністратор" - } + "highlight": { + "side": "Смужка ліворуч", + "striped": "Смугастий фон", + "solid": "Суцільний фон", + "disabled": "Не виділяти" + }, + "bot": "Бот", + "edit_profile": "Редагувати профіль" }, "status": { "copy_link": "Скопіювати посилання на допис", @@ -804,7 +844,9 @@ "conversation": "Розмова", "no_statuses": "Ніяких статусів", "repeated": "поширив(-ла)", - "no_retweet_hint": "Запис, позначено як \"тільки для підписників\" або \"особисте\" і тому не може бути поширений" + "no_retweet_hint": "Запис, позначено як \"тільки для підписників\" або \"особисте\" і тому не може бути поширений", + "socket_broke": "Втрачено з'єднання у реальному часі: код {0}", + "socket_reconnected": "Встановлено з'єднання у реальному часі" }, "user_reporting": { "submit": "Відправити", diff --git a/src/i18n/vi.json b/src/i18n/vi.json @@ -0,0 +1,872 @@ +{ + "about": { + "mrf": { + "federation": "Liên hợp", + "keyword": { + "keyword_policies": "Chính sách quan trọng", + "reject": "Từ chối", + "replace": "Thay thế", + "is_replaced_by": "→", + "ftl_removal": "Giới hạn chung" + }, + "mrf_policies": "Kích hoạt chính sách MRF", + "simple": { + "simple_policies": "Quy tắc máy chủ", + "accept": "Đồng ý", + "accept_desc": "Máy chủ này chỉ chấp nhận tin nhắn từ những máy chủ:", + "reject": "Từ chối", + "quarantine": "Bảo hành", + "quarantine_desc": "Máy chủ này sẽ gửi tút công khai đến những máy chủ:", + "ftl_removal": "Giới hạn chung", + "media_removal": "Ẩn Media", + "media_removal_desc": "Media từ những máy chủ sau sẽ bị ẩn:", + "media_nsfw": "Áp đặt nhạy cảm", + "media_nsfw_desc": "Nội dung từ những máy chủ sau sẽ bị tự động gắn nhãn nhạy cảm:", + "reject_desc": "Máy chủ này không chấp nhận tin nhắn từ những máy chủ:", + "ftl_removal_desc": "Nội dung từ những máy chủ sau sẽ bị ẩn:" + }, + "mrf_policies_desc": "Các chính sách MRF kiểm soát sự liên hợp của máy chủ. Các chính sách sau được bật:" + }, + "staff": "Nhân viên" + }, + "domain_mute_card": { + "mute": "Ẩn", + "mute_progress": "Đang ẩn…", + "unmute": "Ngưng ẩn", + "unmute_progress": "Đang ngưng ẩn…" + }, + "exporter": { + "export": "Xuất dữ liệu", + "processing": "Đang chuẩn bị tập tin cho bạn tải về" + }, + "features_panel": { + "chat": "Chat", + "pleroma_chat_messages": "Pleroma Chat", + "gopher": "Gopher", + "media_proxy": "Proxy media", + "text_limit": "Giới hạn ký tự", + "title": "Tính năng", + "who_to_follow": "Đề xuất theo dõi", + "upload_limit": "Giới hạn tải lên", + "scope_options": "Đa dạng kiểu đăng" + }, + "finder": { + "error_fetching_user": "Lỗi khi nạp người dùng", + "find_user": "Tìm người dùng" + }, + "shoutbox": { + "title": "Chat cùng nhau" + }, + "general": { + "apply": "Áp dụng", + "submit": "Gửi tặng", + "more": "Nhiều hơn", + "loading": "Đang tải…", + "generic_error": "Đã có lỗi xảy ra", + "error_retry": "Xin hãy thử lại", + "retry": "Thử lại", + "optional": "tùy chọn", + "show_more": "Xem thêm", + "show_less": "Thu gọn", + "dismiss": "Bỏ qua", + "cancel": "Hủy bỏ", + "disable": "Tắt", + "enable": "Bật", + "confirm": "Xác nhận", + "verify": "Xác thực", + "close": "Đóng", + "peek": "Thu gọn", + "role": { + "admin": "Quản trị viên", + "moderator": "Kiểm duyệt viên" + }, + "flash_security": "Lưu ý rằng điều này có thể tiềm ẩn nguy hiểm vì nội dung Flash là mã lập trình tùy ý.", + "flash_fail": "Tải nội dung Flash thất bại, tham khảo chi tiết trong console.", + "flash_content": "Nhấn để hiện nội dung Flash bằng Ruffle (Thử nghiệm, có thể không dùng được)." + }, + "image_cropper": { + "crop_picture": "Cắt hình ảnh", + "save": "Lưu", + "save_without_cropping": "Bỏ qua cắt", + "cancel": "Hủy bỏ" + }, + "importer": { + "submit": "Gửi đi", + "success": "Đã nhập dữ liệu thành công.", + "error": "Có lỗi xảy ra khi nhập dữ liệu từ tập tin này." + }, + "login": { + "login": "Đăng nhập", + "description": "Đăng nhập bằng OAuth", + "logout": "Đăng xuất", + "password": "Mật khẩu", + "placeholder": "vd: cobetronxinh", + "register": "Đăng ký", + "username": "Tên người dùng", + "hint": "Đăng nhập để cùng trò chuyện", + "authentication_code": "Mã truy cập", + "enter_recovery_code": "Nhập mã khôi phục", + "recovery_code": "Mã khôi phục", + "heading": { + "totp": "Xác thực hai bước", + "recovery": "Khôi phục hai bước" + }, + "enter_two_factor_code": "Nhập mã xác thực hai bước" + }, + "media_modal": { + "previous": "Trước đó", + "next": "Kế tiếp" + }, + "nav": { + "about": "Về máy chủ này", + "administration": "Vận hành bởi", + "back": "Quay lại", + "friend_requests": "Yêu cầu theo dõi", + "mentions": "Lượt nhắc đến", + "interactions": "Giao tiếp", + "dms": "Nhắn tin", + "public_tl": "Bảng tin máy chủ", + "timeline": "Bảng tin", + "home_timeline": "Bảng tin của bạn", + "twkn": "Thế giới", + "bookmarks": "Đã lưu", + "user_search": "Tìm kiếm người dùng", + "search": "Tìm kiếm", + "who_to_follow": "Đề xuất theo dõi", + "preferences": "Thiết lập", + "timelines": "Bảng tin", + "chats": "Chat" + }, + "notifications": { + "broken_favorite": "Trạng thái chưa rõ, đang tìm kiếm…", + "favorited_you": "thích tút của bạn", + "followed_you": "theo dõi bạn", + "follow_request": "yêu cầu theo dõi bạn", + "load_older": "Xem những thông báo cũ hơn", + "notifications": "Thông báo", + "read": "Đọc!", + "repeated_you": "chia sẻ tút của bạn", + "no_more_notifications": "Không còn thông báo nào", + "migrated_to": "chuyển sang", + "reacted_with": "chạm tới {0}", + "error": "Lỗi khi nạp thông báo {0}" + }, + "polls": { + "add_poll": "Tạo bình chọn", + "option": "Lựa chọn", + "votes": "người bình chọn", + "people_voted_count": "{count} người bình chọn | {count} người bình chọn", + "vote": "Bình chọn", + "type": "Kiểu bình chọn", + "single_choice": "Chỉ được chọn một lựa chọn", + "multiple_choices": "Cho phép chọn nhiều lựa chọn", + "expiry": "Thời hạn bình chọn", + "expires_in": "Bình chọn kết thúc sau {0}", + "not_enough_options": "Không đủ lựa chọn tối thiểu", + "add_option": "Thêm lựa chọn", + "votes_count": "{count} bình chọn | {count} bình chọn", + "expired": "Bình chọn đã kết thúc {0} trước" + }, + "emoji": { + "stickers": "Sticker", + "emoji": "Emoji", + "keep_open": "Mở khung lựa chọn", + "search_emoji": "Tìm emoji", + "add_emoji": "Nhập emoji", + "custom": "Tùy chỉnh emoji", + "unicode": "Unicode emoji", + "load_all_hint": "Tải trước {saneAmount} emoji, tải toàn bộ emoji có thể gây xử lí chậm.", + "load_all": "Đang tải {emojiAmount} emoji" + }, + "interactions": { + "favs_repeats": "Tương tác", + "follows": "Lượt theo dõi mới", + "moves": "Người dùng chuyển đi", + "load_older": "Xem tương tác cũ hơn" + }, + "post_status": { + "new_status": "Đăng tút", + "account_not_locked_warning": "Tài khoản của bạn chưa {0}. Bất kỳ ai cũng có thể xem những tút dành cho người theo dõi của bạn.", + "account_not_locked_warning_link": "đã khóa", + "attachments_sensitive": "Đánh dấu media là nhạy cảm", + "media_description": "Mô tả media", + "content_type": { + "text/plain": "Văn bản", + "text/html": "HTML", + "text/markdown": "Markdown", + "text/bbcode": "BBCode" + }, + "content_warning": "Tiêu đề (tùy chọn)", + "default": "Đời người con gái không muốn yêu ai được không?", + "direct_warning_to_first_only": "Người đầu tiên được nhắc đến mới có thể thấy tút này.", + "posting": "Đang đăng tút", + "post": "Đăng", + "preview": "Xem trước", + "preview_empty": "Trống", + "empty_status_error": "Không thể đăng một tút trống và không có media", + "media_description_error": "Cập nhật media thất bại, thử lại sau", + "scope_notice": { + "private": "Chỉ những người theo dõi bạn mới thấy tút này", + "unlisted": "Tút này sẽ không hiện trong bảng tin máy chủ và thế giới", + "public": "Mọi người đều có thể thấy tút này" + }, + "scope": { + "public": "Công khai - hiện trên bảng tin máy chủ", + "private": "Riêng tư - Chỉ dành cho người theo dõi", + "unlisted": "Hạn chế - không hiện trên bảng tin", + "direct": "Tin nhắn - chỉ người được nhắc đến mới thấy" + }, + "direct_warning_to_all": "Những ai được nhắc đến sẽ đều thấy tút này." + }, + "registration": { + "bio": "Tiểu sử", + "email": "Email", + "fullname": "Tên hiển thị", + "password_confirm": "Xác nhận mật khẩu", + "registration": "Đăng ký", + "token": "Lời mời", + "captcha": "CAPTCHA", + "new_captcha": "Nhấn vào hình ảnh để đổi captcha mới", + "username_placeholder": "vd: cobetronxinh", + "fullname_placeholder": "vd: Cô Bé Tròn Xinh", + "bio_placeholder": "vd:\nHi, I'm Cô Bé Tròn Xinh.\nI’m an anime girl living in suburban Vietnam. You may know me from the school.", + "reason": "Lý do đăng ký", + "reason_placeholder": "Máy chủ này phê duyệt đăng ký thủ công.\nHãy cho quản trị viên biết lý do bạn muốn đăng ký.", + "register": "Đăng ký", + "validations": { + "username_required": "không được để trống", + "fullname_required": "không được để trống", + "email_required": "không được để trống", + "password_confirmation_required": "không được để trống", + "password_confirmation_match": "phải trùng khớp với mật khẩu", + "password_required": "không được để trống" + } + }, + "remote_user_resolver": { + "remote_user_resolver": "Giải quyết người dùng từ xa", + "searching_for": "Tìm kiếm", + "error": "Không tìm thấy." + }, + "selectable_list": { + "select_all": "Chọn tất cả" + }, + "settings": { + "app_name": "Tên app", + "save": "Lưu thay đổi", + "security": "Bảo mật", + "enter_current_password_to_confirm": "Nhập mật khẩu để xác thực", + "mfa": { + "otp": "OTP", + "setup_otp": "Thiết lập OTP", + "wait_pre_setup_otp": "hậu thiết lập OTP", + "confirm_and_enable": "Xác nhận và kích hoạt OTP", + "title": "Xác thực hai bước", + "recovery_codes": "Những mã khôi phục.", + "waiting_a_recovery_codes": "Đang nhận mã khôi phục…", + "authentication_methods": "Phương pháp xác thực", + "scan": { + "title": "Quét", + "desc": "Sử dụng app xác thực hai bước để quét mã QR hoặc nhập mã khôi phục:", + "secret_code": "Mã" + }, + "verify": { + "desc": "Để bật xác thực hai bước, nhập mã từ app của bạn:" + }, + "generate_new_recovery_codes": "Tạo mã khôi phục mới", + "warning_of_generate_new_codes": "Khi tạo mã khôi phục mới, những mã khôi phục cũ sẽ không sử dụng được nữa.", + "recovery_codes_warning": "Hãy viết lại mã và cất ở một nơi an toàn - những mã này sẽ không xuất hiện lại nữa. Nếu mất quyền sử dụng app 2FA app và mã khôi phục, tài khoản của bạn sẽ không thể truy cập." + }, + "allow_following_move": "Cho phép tự động theo dõi lại khi tài khoản đang theo dõi chuyển sang máy chủ khác", + "attachmentRadius": "Tập tin tải lên", + "attachments": "Tập tin tải lên", + "avatar": "Ảnh đại diện", + "avatarAltRadius": "Ảnh đại diện (thông báo)", + "avatarRadius": "Ảnh đại diện", + "background": "Ảnh nền", + "bio": "Tiểu sử", + "block_export": "Xuất danh sách chặn", + "block_import": "Nhập danh sách chặn", + "block_import_error": "Lỗi khi nhập danh sách chặn", + "mute_export": "Xuất danh sách ẩn", + "mute_export_button": "Xuất danh sách ẩn ra tập tin CSV", + "mute_import": "Nhập danh sách ẩn", + "mute_import_error": "Lỗi khi nhập danh sách ẩn", + "mutes_imported": "Đã nhập danh sách ẩn! Sẽ mất một lúc nữa để hoàn thành.", + "import_mutes_from_a_csv_file": "Nhập danh sách ẩn từ tập tin CSV", + "blocks_tab": "Danh sách chặn", + "bot": "Đây là tài khoản Bot", + "btnRadius": "Nút", + "cBlue": "Xanh (Trả lời, theo dõi)", + "cOrange": "Cam (Thích)", + "cRed": "Đỏ (Hủy bỏ)", + "change_email": "Đổi email", + "change_email_error": "Có lỗi xảy ra khi đổi email.", + "changed_email": "Đã đổi email thành công!", + "change_password": "Đổi mật khẩu", + "changed_password": "Đổi mật khẩu thành công!", + "chatMessageRadius": "Tin nhắn chat", + "follows_imported": "Đã nhập danh sách theo dõi! Sẽ mất một lúc nữa để hoàn thành.", + "collapse_subject": "Thu gọn những tút có tựa đề", + "composing": "Thu gọn", + "current_password": "Mật khẩu cũ", + "mutes_and_blocks": "Ẩn và Chặn", + "data_import_export_tab": "Nhập / Xuất dữ liệu", + "default_vis": "Kiểu đăng tút mặc định", + "delete_account": "Xóa tài khoản", + "delete_account_error": "Có lỗi khi xóa tài khoản. Xin liên hệ quản trị viên máy chủ để tìm hiểu.", + "delete_account_instructions": "Nhập mật khẩu bên dưới để xác nhận.", + "domain_mutes": "Máy chủ", + "avatar_size_instruction": "Kích cỡ tối thiểu 150x150 pixels.", + "pad_emoji": "Nhớ chừa khoảng cách khi chèn emoji", + "emoji_reactions_on_timeline": "Hiện tương tác emoji trên bảng tin", + "export_theme": "Lưu mẫu", + "filtering": "Bộ lọc", + "filtering_explanation": "Những tút chứa từ sau sẽ bị ẩn, mỗi chữ một hàng", + "word_filter": "Bộ lọc từ ngữ", + "follow_export": "Xuất danh sách theo dõi", + "follow_import": "Nhập danh sách theo dõi", + "follow_import_error": "Lỗi khi nhập danh sách theo dõi", + "accent": "Màu chủ đạo", + "foreground": "Màu phối", + "general": "Chung", + "hide_attachments_in_convo": "Ẩn tập tin đính kèm trong thảo luận", + "hide_media_previews": "Ẩn xem trước media", + "hide_all_muted_posts": "Ẩn những tút đã ẩn", + "hide_muted_posts": "Ẩn tút từ các người dùng đã ẩn", + "max_thumbnails": "Số ảnh xem trước tối đa cho mỗi tút", + "hide_isp": "Ẩn thanh bên của máy chủ", + "hide_shoutbox": "Ẩn thanh chat máy chủ", + "hide_wallpaper": "Ẩn ảnh nền máy chủ", + "preload_images": "Tải trước hình ảnh", + "use_one_click_nsfw": "Xem nội dung nhạy cảm bằng cách nhấn vào", + "hide_user_stats": "Ẩn số liệu người dùng (vd: số người theo dõi)", + "hide_filtered_statuses": "Ẩn những tút đã lọc", + "import_followers_from_a_csv_file": "Nhập danh sách theo dõi từ tập tin CSV", + "import_theme": "Tải mẫu có sẵn", + "inputRadius": "Chỗ nhập vào", + "checkboxRadius": "Hộp kiểm", + "instance_default": "(mặc định: {value})", + "instance_default_simple": "(mặc định)", + "interface": "Giao diện", + "interfaceLanguage": "Ngôn ngữ", + "limited_availability": "Trình duyệt không hỗ trợ", + "links": "Liên kết", + "lock_account_description": "Tự phê duyệt yêu cầu theo dõi", + "loop_video": "Lặp lại video", + "loop_video_silent_only": "Chỉ lặp lại những video không có âm thanh", + "mutes_tab": "Ẩn", + "play_videos_in_modal": "Phát video trong khung hình riêng", + "file_export_import": { + "backup_restore": "Sao lưu", + "backup_settings": "Thiết lập sao lưu", + "restore_settings": "Khôi phục thiết lập từ tập tin", + "errors": { + "invalid_file": "Tập tin đã chọn không hỗ trợ bởi Pleroma. Giữ nguyên mọi thay đổi.", + "file_too_old": "Phiên bản không tương thích: {fileMajor}, phiên bản tập tin quá cũ và không được hỗ trợ (min. set. ver. {feMajor})", + "file_slightly_new": "Phiên bản tập tin khác biệt, không thể áp dụng một vài thay đổi", + "file_too_new": "Phiên bản không tương thích: {fileMajor}, phiên bản PleromaFE(settings ver {feMajor}) của máy chủ này quá cũ để sử dụng" + }, + "backup_settings_theme": "Thiết lập sao lưu dữ liệu và giao diện" + }, + "profile_fields": { + "label": "Metadata", + "add_field": "Thêm mục", + "name": "Nhãn", + "value": "Nội dung" + }, + "use_contain_fit": "Không cắt ảnh đính kèm trong bản xem trước", + "name": "Tên", + "name_bio": "Tên & tiểu sử", + "new_email": "Email mới", + "new_password": "Mật khẩu mới", + "notification_visibility_follows": "Theo dõi", + "notification_visibility_mentions": "Lượt nhắc", + "notification_visibility_repeats": "Chia sẻ", + "notification_visibility_moves": "Chuyển máy chủ", + "notification_visibility_emoji_reactions": "Tương tác", + "no_blocks": "Không có chặn", + "no_mutes": "Không có ẩn", + "hide_follows_description": "Ẩn danh sách những người tôi theo dõi", + "hide_followers_description": "Ẩn danh sách những người theo dõi tôi", + "hide_followers_count_description": "Ẩn số lượng người theo dõi tôi", + "show_admin_badge": "Hiện huy hiệu \"Quản trị viên\" trên trang của tôi", + "show_moderator_badge": "Hiện huy hiệu \"Kiểm duyệt viên\" trên trang của tôi", + "oauth_tokens": "OAuth tokens", + "token": "Token", + "refresh_token": "Làm tươi token", + "valid_until": "Có giá trị tới", + "revoke_token": "Gỡ", + "panelRadius": "Panels", + "pause_on_unfocused": "Dừng phát khi đang lướt các tút khác", + "presets": "Mẫu có sẵn", + "profile_background": "Ảnh nền trang cá nhân", + "profile_banner": "Ảnh bìa trang cá nhân", + "profile_tab": "Trang cá nhân", + "radii_help": "Thiết lập góc bo tròn (bằng pixels)", + "replies_in_timeline": "Trả lời trong bảng tin", + "reply_visibility_all": "Hiện toàn bộ trả lời", + "reply_visibility_self": "Chỉ hiện những trả lời có nhắc tới tôi", + "reply_visibility_following_short": "Hiện trả lời có những người tôi theo dõi", + "reply_visibility_self_short": "Hiện trả lời của bản thân", + "setting_changed": "Thiết lập khác với mặc định", + "block_export_button": "Xuất danh sách chặn ra tập tin CSV", + "blocks_imported": "Đã nhập danh sách chặn! Sẽ mất một lúc nữa để hoàn thành.", + "cGreen": "Green (Chia sẻ)", + "change_password_error": "Có lỗi xảy ra khi đổi mật khẩu.", + "confirm_new_password": "Xác nhận mật khẩu mới", + "delete_account_description": "Xóa vĩnh viễn mọi dữ liệu và vô hiệu hóa tài khoản của bạn.", + "discoverable": "Hiện tài khoản trong công cụ tìm kiếm và những tính năng khác", + "follow_export_button": "Xuất danh sách theo dõi ra tập tin CSV", + "hide_attachments_in_tl": "Ẩn tập tin đính kèm trong bảng tin", + "right_sidebar": "Hiện thanh bên bên phải", + "hide_post_stats": "Ẩn tương tác của tút (vd: số lượt thích)", + "import_blocks_from_a_csv_file": "Nhập danh sách chặn từ tập tin CSV", + "invalid_theme_imported": "Tập tin đã chọn không hỗ trợ bởi Pleroma. Giao diện của bạn sẽ giữ nguyên.", + "notification_visibility": "Những loại thông báo sẽ hiện", + "notification_visibility_likes": "Thích", + "no_rich_text_description": "Không hiện rich text trong các tút", + "hide_follows_count_description": "Ẩn số lượng người tôi theo dõi", + "nsfw_clickthrough": "Cho phép nhấn vào xem các tút nhạy cảm", + "reply_visibility_following": "Chỉ hiện những trả lời có nhắc tới tôi hoặc từ những người mà tôi theo dõi", + "autohide_floating_post_button": "Ẩn nút viết tút khi xem bảng tin (di động)", + "saving_err": "Thiết lập lỗi lưu", + "saving_ok": "Đã lưu các thay đổi", + "search_user_to_block": "Tìm người bạn muốn chặn", + "search_user_to_mute": "Tìm người bạn muốn ẩn", + "security_tab": "Bảo mật", + "scope_copy": "Chép phạm vi khi trả lời (tin nhắn luôn được chép sẵn)", + "minimal_scopes_mode": "Tùy chọn thu nhỏ phạm vi tút", + "set_new_avatar": "Đổi ảnh đại diện", + "set_new_profile_background": "Đổi ảnh nền", + "set_new_profile_banner": "Đổi ảnh bìa", + "reset_profile_background": "Đặt lại ảnh nền", + "reset_profile_banner": "Đặt lại ảnh bìa", + "reset_banner_confirm": "Bạn có chắc chắn muốn đặt lại ảnh bìa?", + "reset_background_confirm": "Bạn có chắc chắn muốn đặt lại ảnh nền?", + "settings": "Cài đặt", + "subject_input_always_show": "Luôn hiện vùng tiêu đề", + "subject_line_behavior": "Chép tiêu đề khi trả lời", + "subject_line_email": "Giống email: \"re: subject\"", + "subject_line_mastodon": "Giống Mastodon: copy as is", + "subject_line_noop": "Đừng chép", + "sensitive_by_default": "Mặc định tút là nhạy cảm", + "stop_gifs": "Chỉ phát GIF khi chạm vào", + "streaming": "Tự động tải tút mới khi cuộn lên trên", + "user_mutes": "Người dùng", + "useStreamingApiWarning": "(Tính năng thử nghiệm, không đề xuất sử dụng)", + "text": "Văn bản", + "theme": "Theme", + "theme_help": "Dùng mã màu hex (#rrggbb) để tự chế theme.", + "tooltipRadius": "Tooltips/alerts", + "type_domains_to_mute": "Tìm máy chủ để ẩn", + "upload_a_photo": "Tải ảnh lên", + "user_settings": "Thiết lập người dùng", + "values": { + "false": "không", + "true": "có" + }, + "virtual_scrolling": "Render bảng tin", + "fun": "Vui nhộn", + "greentext": "Mũi tên meme", + "notifications": "Thông báo", + "notification_setting_filters": "Bộ lọc", + "notification_setting_block_from_strangers": "Chặn thông báo từ những người bạn không theo dõi", + "notification_setting_privacy": "Riêng tư", + "notification_setting_hide_notification_contents": "Ẩn người gửi và nội dung thông báo đẩy", + "notification_mutes": "Sử dụng ẩn nếu muốn dừng nhận thông báo từ một người cụ thể.", + "notification_blocks": "Chặn một người ngừng toàn bộ thông báo cũng giống như hủy đăng ký họ.", + "more_settings": "Cài đặt khác", + "style": { + "switcher": { + "keep_shadows": "Giữ bóng đổ", + "keep_color": "Giữ màu", + "keep_opacity": "Giữ trong suốt", + "keep_roundness": "Giữ bo tròn góc", + "reset": "Đặt lại", + "clear_all": "Xóa hết", + "clear_opacity": "Xóa trong suốt", + "load_theme": "Tải theme", + "keep_as_is": "Giữ như là", + "use_snapshot": "Bản cũ", + "use_source": "Bản mới", + "help": { + "upgraded_from_v2": "PleromaFE đã được nâng cấp, theme có thể khác hơn một chút so với bản cũ.", + "v2_imported": "Tập tin bạn nhập là từ phiên bản PleromaFE cũ. Chúng tôi sẽ cố làm nó tương thích nhưng có thể sẽ có xung đột.", + "older_version_imported": "Tập tin bạn vừa nhập được tạo ra từ phiên bản PleromaFE cũ.", + "snapshot_present": "Đã tải theme snapshot, mọi giá trị sẽ bị chép đè. Thay vào đó, bạn có thể tải dữ liệu chắc chắn của theme.", + "fe_upgraded": "Theme của PleromaFE được nâng cấp sau mỗi phiên bản.", + "fe_downgraded": "Theme của phiên bản PleromaFE đã được hạ cấp.", + "migration_snapshot_ok": "Theme snapshot đã tải xong. Bạn có thể thử tải dữ liệu theme.", + "migration_napshot_gone": "Nếu thiếu snapshot, một số thứ sẽ khác với ban đầu.", + "future_version_imported": "Tập tin bạn vừa nhập được tạo ra từ phiên bản PleromaFE mới.", + "snapshot_missing": "Không có theme snapshot trong tập tin cho nên có thể nó sẽ khác với bản gốc đôi chút.", + "snapshot_source_mismatch": "Xung đột phiên bản: hầu hết Pleroma FE đã hạ cấp và cập nhật lại, nếu bạn đổi theme sử dụng phiên bản cũ hơn của FE, bạn gần như muốn sử dụng phiên bản cũ, thay vào đó sử dụng phiên bản mới." + }, + "keep_fonts": "Giữ phông chữ", + "save_load_hint": "Giúp giữ nguyên các tùy chọn hiện tại khi chọn hoặc tải theme khác, nó cũng lưu trữ các tùy chọn đã nói khi xuất một theme. Khi tất cả các hộp kiểm bị bỏ trống, việc xuất theme sẽ lưu mọi thứ." + }, + "common": { + "color": "Màu sắc", + "opacity": "Trong suốt", + "contrast": { + "hint": "Tỉ lệ tương phản là {ratio}, nó {level} {context}", + "level": { + "aa": "đạt mức AA (tối thiểu)", + "aaa": "đạt mức AAA (đề xuất)", + "bad": "không đạt yêu cầu" + }, + "context": { + "18pt": "cỡ chữ lớn (18pt+)", + "text": "cho chữ" + } + } + }, + "common_colors": { + "_tab_label": "Chung", + "main": "Màu sắc chung", + "foreground_hint": "Mở tab \"Nâng cao\" để có nhiều tùy chọn hơn", + "rgbo": "Icons, accents, badges" + }, + "advanced_colors": { + "_tab_label": "Nâng cao", + "alert": "Nền cảnh báo", + "alert_error": "Lỗi", + "alert_warning": "Cảnh báo", + "alert_neutral": "Neutral", + "post": "Tút/Tiểu sử", + "badge": "Nền huy hiệu", + "popover": "Tooltips, menus, popovers", + "badge_notification": "Thông báo", + "panel_header": "Tiêu đề panel", + "top_bar": "Thanh trên cùng", + "borders": "Đường biên", + "buttons": "Nút bấm", + "faint_text": "Chữ mờ", + "underlay": "Lớp dưới", + "wallpaper": "Wallpaper", + "poll": "Biểu đồ cuộc bình chọn", + "icons": "Biểu tượng", + "highlight": "Những thành phần nổi bật", + "pressed": "Khi nhấn xuống", + "selectedPost": "Chọn tút", + "selectedMenu": "Chọn menu", + "toggled": "Toggled", + "tabs": "Tab", + "chat": { + "incoming": "Tin nhắn đến", + "outgoing": "Tin nhắn đi", + "border": "Đường biên" + }, + "inputs": "Khung soạn thảo", + "disabled": "Vô hiệu hóa" + }, + "radii": { + "_tab_label": "Góc bo tròn" + }, + "shadows": { + "component": "Thành phần", + "shadow_id": "Đổ bóng #{value}", + "blur": "Làm mờ", + "spread": "Mở rộng", + "inset": "Thu vào", + "filter_hint": { + "always_drop_shadow": "Chú ý, màu bóng đổ này luôn sử dụng {0} nếu trình duyệt hỗ trợ.", + "drop_shadow_syntax": "{0} không hỗ trợ {1} phần và từ khóa {2}.", + "spread_zero": "Bóng đổ > 0 sẽ xuất hiện nếu chọn nó thành không", + "inset_classic": "Bóng đổ inset sẽ sử dụng {0}", + "avatar_inset": "Nếu trộn lẫn bóng đổ inset và non-inset trên ảnh đại diện có thể khiến ảnh đại diện biến thành trong suốt." + }, + "components": { + "panel": "Panel", + "panelHeader": "Panel ảnh bìa", + "topBar": "Thanh trên cùng", + "avatar": "Ảnh đại diện (ở trang cá nhân)", + "avatarStatus": "Ảnh đại diện (ở tút)", + "popup": "Popups và tooltips", + "button": "Nút bấm", + "buttonHover": "Nút bấm (khi rê chuột)", + "buttonPressed": "Nút bấm (khi nhấn chuột)", + "buttonPressedHover": "Nút bấm (khi nhấn+giữ)", + "input": "Khung soạn thảo" + }, + "_tab_label": "Đổ bóng và tô sáng", + "override": "Chép đè", + "hintV3": "Với bóng đổ, bạn có thể sử dụng ký hiệu {0} để dùng slot màu khác." + }, + "fonts": { + "_tab_label": "Phông chữ", + "components": { + "interface": "Giao diện chung", + "input": "Khung soạn thảo", + "post": "Tút", + "postCode": "Chữ monospaced (rich text)" + }, + "family": "Tên phông", + "size": "Kích cỡ (px)", + "weight": "Độ đậm", + "custom": "Tùy chỉnh", + "help": "Chọn phông chữ hiển thị. Để \"tùy chọn\", bạn phải nhập chính xác tên phông chữ trên hệ thống." + }, + "preview": { + "header": "Xem trước", + "content": "Nội dung", + "error": "Lỗi mẫu ví dụ", + "button": "Nút bấm", + "text": "Một đống {0} và {1}", + "mono": "nội dung", + "input": "Đời người con gái không muốn yêu ai được không?", + "faint_link": "tài liệu hướng dẫn", + "checkbox": "Tôi đã đọc lướt qua quy tắc và chính sách bảo mật", + "link": "Link đẹp đó em yêu", + "fine_print": "Đọc {0} để tìm hiểu thêm!", + "header_faint": "OK nè" + } + }, + "version": { + "title": "Phiên bản", + "frontend_version": "Frontend", + "backend_version": "Backend" + }, + "reset_avatar": "Đặt lại ảnh đại diện", + "reset_avatar_confirm": "Bạn có chắc chắn muốn đặt lại ảnh đại diện?", + "post_status_content_type": "Loại tút đăng", + "useStreamingApi": "Nhận tút và thông báo theo thời gian thực", + "theme_help_v2_1": "Bạn cũng có thể xóa hết màu thành phần và làm theme trong suốt, chọn nút \"Xóa hết\".", + "theme_help_v2_2": "Các biểu tượng bên dưới các mục có độ tương phản nền/văn bản, hãy rê chuột qua để biết thông tin chi tiết. Xin lưu ý rằng, khi sử dụng các độ tương phản trong suốt có thể khiến đọc chữ không ra.", + "enable_web_push_notifications": "Cho phép thông báo đẩy trên web", + "mentions_new_style": "Lượt nhắc màu mè", + "mentions_new_place": "Đặt lượt nhắc ở dòng riêng", + "always_show_post_button": "Luôn hiện nút viết tút mới" + }, + "errors": { + "storage_unavailable": "Pleroma không thể truy cập lưu trữ trình duyệt. Thông tin đăng nhập và những thiết lập tạm thời sẽ bị mất. Hãy cho phép cookies." + }, + "time": { + "day": "{0} ngày", + "days": "{0} ngày", + "day_short": "{0} ngày", + "days_short": "{0} ngày", + "hour": "{0} giờ", + "hours": "{0} giờ", + "hour_short": "{0} giờ", + "hours_short": "{0} giờ", + "in_future": "lúc {0}", + "in_past": "{0} trước", + "minute": "{0} phút", + "minutes": "{0} phút", + "minute_short": "{0} phút", + "minutes_short": "{0} phút", + "month": "{0} tháng", + "months": "{0} tháng", + "month_short": "{0} tháng", + "months_short": "{0} tháng", + "now": "vừa xong", + "second": "{0} giây", + "seconds": "{0} giây", + "second_short": "{0}s", + "seconds_short": "{0}s", + "week": "{0} tuần", + "weeks": "{0} tuần", + "week_short": "{0} tuần", + "weeks_short": "{0} tuần", + "year": "{0} năm", + "years": "{0} năm", + "year_short": "{0} năm", + "years_short": "{0} năm", + "now_short": "vừa xong" + }, + "timeline": { + "collapse": "Thu gọn", + "error": "Lỗi khi nạp bảng tin {0}", + "load_older": "Xem tút cũ hơn", + "repeated": "chia sẻ", + "show_new": "Hiện mới", + "reload": "Tải lại", + "up_to_date": "Đã tải những tút mới nhất", + "no_more_statuses": "Không còn tút nào", + "no_statuses": "Trống trơn!", + "socket_reconnected": "Thiết lập kết nối thời gian thực", + "conversation": "Thảo luận", + "no_retweet_hint": "Không thể chia sẻ tin nhắn và những tút riêng tư", + "socket_broke": "Mất kết nối thời gian thực: CloseEvent {0}" + }, + "status": { + "repeats": "Chia sẻ", + "delete": "Xóa tút", + "unpin": "Bỏ ghim trên trang cá nhân", + "pin": "Ghim trên trang cá nhân", + "pinned": "Tút được ghim", + "bookmark": "Lưu", + "unbookmark": "Bỏ lưu", + "reply_to": "Trả lời", + "replies_list": "Những trả lời:", + "mute_conversation": "Không quan tâm nữa", + "unmute_conversation": "Quan tâm", + "status_unavailable": "Không tìm thấy tút", + "copy_link": "Sao chép URL", + "external_source": "Nguồn bên ngoài", + "thread_muted": "Đã ẩn chủ đề", + "thread_muted_and_words": ", có từ:", + "hide_full_subject": "Ẩn tiêu đề", + "show_content": "Hiện nội dung", + "hide_content": "Ẩn nội dung", + "status_deleted": "Tút này đã bị xóa", + "nsfw": "Nhạy cảm", + "expand": "Xem nguyên văn", + "favorites": "Thích", + "delete_confirm": "Bạn có chắc chắn muốn xóa tút này?", + "show_full_subject": "Hiện đầy đủ tiêu đề", + "you": "(Bạn)", + "mentions": "Lượt nhắc", + "plus_more": "+{number} nhiều hơn" + }, + "user_card": { + "approve": "Chấp nhận", + "block": "Chặn", + "blocked": "Đã chặn!", + "deny": "Từ chối", + "edit_profile": "Chỉnh sửa trang cá nhân", + "favorites": "Thích", + "follow": "Theo dõi", + "follow_progress": "Đang yêu cầu…", + "follow_again": "Gửi lại yêu cầu?", + "follow_unfollow": "Ngưng theo dõi", + "followees": "Đang theo dõi", + "followers": "Người theo dõi", + "following": "Đang theo dõi!", + "follows_you": "Theo dõi bạn!", + "hidden": "Ẩn", + "media": "Media", + "mention": "Lượt nhắc", + "message": "Tin nhắn", + "mute": "Ẩn", + "muted": "Đã ẩn", + "per_day": "tút mỗi ngày", + "remote_follow": "Theo dõi từ xa", + "report": "Báo cáo", + "statuses": "Tút", + "subscribe": "Đăng ký", + "unsubscribe": "Hủy đăng ký", + "unblock": "Bỏ chặn", + "unblock_progress": "Đang bỏ chặn…", + "block_progress": "Đang chặn…", + "unmute": "Bỏ ẩn", + "unmute_progress": "Đang bỏ ẩn…", + "mute_progress": "Đang ẩn…", + "hide_repeats": "Ẩn lượt chia sẻ", + "show_repeats": "Hiện lượt chia sẻ", + "bot": "Bot", + "admin_menu": { + "moderation": "Kiểm duyệt", + "grant_admin": "Chỉ định Quản trị viên", + "revoke_admin": "Gỡ bỏ Quản trị viên", + "grant_moderator": "Chỉ định Kiểm duyệt viên", + "activate_account": "Xác thực người dùng", + "deactivate_account": "Vô hiệu hóa người dùng", + "delete_account": "Xóa người dùng", + "force_nsfw": "Đánh dấu tất cả tút là nhạy cảm", + "strip_media": "Gỡ bỏ media trong tút", + "sandbox": "Đánh dấu tất cả tút là riêng tư", + "disable_remote_subscription": "Không cho phép theo dõi từ máy chủ khác", + "disable_any_subscription": "Không cho phép theo dõi bất cứ ai", + "quarantine": "Không cho phép tút liên hợp", + "delete_user": "Xóa người dùng", + "revoke_moderator": "Gỡ bỏ Quản trị viên", + "force_unlisted": "Đánh dấu tất cả tút là hạn chế", + "delete_user_confirmation": "Bạn chắc chắn chưa? Hành động này không thể phục hồi." + }, + "highlight": { + "disabled": "Không nổi bật", + "solid": "Nền 1 màu", + "striped": "Nền 2 màu", + "side": "Sọc bên" + }, + "follow_sent": "Đã gửi yêu cầu!", + "its_you": "Đó là bạn!" + }, + "user_profile": { + "timeline_title": "Bảng tin người dùng", + "profile_does_not_exist": "Xin lỗi, tài khoản này không tồn tại.", + "profile_loading_error": "Xin lỗi, có lỗi xảy ra khi xem trang cá nhân này." + }, + "user_reporting": { + "title": "Báo cáo {0}", + "additional_comments": "Ghi chú", + "forward_description": "Người này thuộc máy chủ khác. Gửi một báo cáo ẩn danh tới máy chủ đó?", + "forward_to": "Chuyển cho {0}", + "submit": "Gửi", + "generic_error": "Có lỗi xảy ra khi xử lý yêu cầu của bạn.", + "add_comment_description": "Hãy cho quản trị viên biết lý do vì sao bạn báo cáo người này:" + }, + "who_to_follow": { + "more": "Nhiều hơn nữa", + "who_to_follow": "Những người dùng nổi bật" + }, + "tool_tip": { + "media_upload": "Tải lên media", + "repeat": "Chia sẻ", + "reply": "Trả lời", + "favorite": "Thích", + "add_reaction": "Thêm tương tác", + "accept_follow_request": "Phê duyệt yêu cầu theo dõi", + "reject_follow_request": "Từ chối yêu cầu theo dõi", + "bookmark": "Lưu", + "user_settings": "Thiết lập người dùng" + }, + "upload": { + "error": { + "base": "Tải lên thất bại.", + "message": "Tải lên thất bại: {0}", + "file_too_big": "Tập tin quá lớn [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "Hãy thử lại sau" + }, + "file_size_units": { + "KiB": "KB", + "MiB": "MB", + "GiB": "GB", + "B": "byte", + "TiB": "TB" + } + }, + "search": { + "people": "Người", + "hashtags": "Hashtag", + "person_talking": "{count} người đang trò chuyện", + "people_talking": "{count} người đang trò chuyện", + "no_results": "Không tìm thấy" + }, + "password_reset": { + "forgot_password": "Quên mật khẩu", + "password_reset": "Đổi mật khẩu", + "placeholder": "Email hoặc tên người dùng", + "check_email": "Kiểm tra email của bạn.", + "return_home": "Quay lại Pleroma", + "too_many_requests": "Bạn đã vượt giới hạn cho phép, hãy thử lại sau.", + "password_reset_disabled": "Reset mật khẩu bị tắt. Hãy liên hệ quản trị viên máy chủ.", + "password_reset_required": "Bạn phải đổi mật khẩu để đăng nhập.", + "instruction": "Nhập email hoặc tên người dùng. Chúng tôi sẽ gửi email reset mật khẩu cho bạn.", + "password_reset_required_but_mailer_is_disabled": "Bạn cần phải đổi mật khẩu, nhưng tính năng bị tắt. Hãy liên hệ quản trị viên máy chủ." + }, + "chats": { + "you": "Bạn:", + "message_user": "Nhắn tin {nickname}", + "delete": "Xóa", + "chats": "Chat", + "new": "Chat mới", + "empty_message_error": "Không thể gửi tin nhắn trống", + "more": "Nhiều hơn", + "delete_confirm": "Bạn có chắc chắn muốn xóa tin nhắn này?", + "error_loading_chat": "Có vấn đề khi tải giao diện chat.", + "error_sending_message": "Có vấn đề khi gửi tin nhắn.", + "empty_chat_list_placeholder": "Bạn không có tin nhắn. Hãy bắt đầu nhắn cho ai đó!" + }, + "file_type": { + "audio": "Âm thanh", + "video": "Video", + "image": "Hình ảnh", + "file": "Tập tin" + }, + "display_date": { + "today": "Hôm nay" + } +} diff --git a/src/i18n/zh.json b/src/i18n/zh.json @@ -39,7 +39,14 @@ "close": "关闭", "retry": "重试", "error_retry": "请重试", - "loading": "载入中…" + "loading": "载入中…", + "role": { + "moderator": "监察员", + "admin": "管理员" + }, + "flash_content": "点击以使用 Ruffle 显示 Flash 内容(实验性,可能无效)。", + "flash_security": "注意这可能有潜在的危险,因为 Flash 内容仍然是任意的代码。", + "flash_fail": "Flash 内容加载失败,请在控制台查看详情。" }, "image_cropper": { "crop_picture": "裁剪图片", @@ -92,7 +99,8 @@ "administration": "管理员", "chats": "聊天", "timelines": "时间线", - "bookmarks": "书签" + "bookmarks": "书签", + "home_timeline": "主页时间线" }, "notifications": { "broken_favorite": "未知的状态,正在搜索中…", @@ -120,7 +128,9 @@ "expiry": "投票期限", "expires_in": "投票于 {0} 后结束", "expired": "投票 {0} 前已结束", - "not_enough_options": "投票的选项太少" + "not_enough_options": "投票的选项太少", + "votes_count": "{count} 票 | {count} 票", + "people_voted_count": "{count} 人已投票 | {count} 人已投票" }, "stickers": { "add_sticker": "添加贴纸" @@ -162,7 +172,8 @@ "preview": "预览", "media_description": "媒体描述", "media_description_error": "更新媒体失败,请重试", - "empty_status_error": "不能发布没有内容、没有附件的发文" + "empty_status_error": "不能发布没有内容、没有附件的发文", + "post": "发送" }, "registration": { "bio": "简介", @@ -183,7 +194,10 @@ "password_required": "不能留空", "password_confirmation_required": "不能留空", "password_confirmation_match": "密码不一致" - } + }, + "reason_placeholder": "此实例的注册需要手动批准。\n请让管理员知道您为什么想要注册。", + "reason": "注册理由", + "register": "注册" }, "selectable_list": { "select_all": "选择全部" @@ -290,7 +304,7 @@ "new_password": "新密码", "notification_visibility": "要显示的通知类型", "notification_visibility_follows": "关注", - "notification_visibility_likes": "点赞", + "notification_visibility_likes": "喜欢", "notification_visibility_mentions": "提及", "notification_visibility_repeats": "转发", "no_rich_text_description": "不显示富文本格式", @@ -298,8 +312,8 @@ "no_mutes": "没有隐藏", "hide_follows_description": "不要显示我所关注的人", "hide_followers_description": "不要显示关注我的人", - "show_admin_badge": "在我的个人资料中显示管理员徽章", - "show_moderator_badge": "在我的个人资料中显示监察员徽章", + "show_admin_badge": "在我的个人资料中显示“管理员”徽章", + "show_moderator_badge": "在我的个人资料中显示“监察员”徽章", "nsfw_clickthrough": "将不和谐附件和链接预览隐藏,点击才会显示", "oauth_tokens": "OAuth令牌", "token": "令牌", @@ -552,7 +566,30 @@ "mute_import": "隐藏名单导入", "mute_export_button": "导出你的隐藏名单到一个 csv 文件", "mute_export": "隐藏名单导出", - "hide_wallpaper": "隐藏实例壁纸" + "hide_wallpaper": "隐藏实例壁纸", + "setting_changed": "与默认设置不同", + "more_settings": "更多设置", + "sensitive_by_default": "默认标记发文为敏感内容", + "reply_visibility_self_short": "只显示对我本人的回复", + "reply_visibility_following_short": "显示对我关注的人的回复", + "hide_all_muted_posts": "不显示已隐藏的发文", + "hide_media_previews": "隐藏媒体预览", + "word_filter": "词语过滤", + "save": "保存更改", + "file_export_import": { + "errors": { + "file_slightly_new": "文件的小版本不同,有些设置可能无法加载", + "file_too_old": "不兼容的主版本:{fileMajor},文件版本过旧,不受支持(最小设置版本 {feMajor})", + "file_too_new": "不兼容的主版本:{fileMajor},此 PleromaFE(设置版本 {feMajor})过旧,无法处理", + "invalid_file": "所选文件不是受支持的 Pleroma 设置备份。没有进行任何更改。" + }, + "restore_settings": "从文件恢复设置", + "backup_settings_theme": "备份设置和主题到文件", + "backup_settings": "备份设置到文件", + "backup_restore": "设置备份" + }, + "right_sidebar": "在右侧显示侧边栏", + "hide_shoutbox": "隐藏实例留言板" }, "time": { "day": "{0} 天", @@ -600,7 +637,9 @@ "no_more_statuses": "没有更多的状态", "no_statuses": "没有状态更新", "reload": "重新载入", - "error": "取得时间轴时发生错误:{0}" + "error": "取得时间轴时发生错误:{0}", + "socket_broke": "丢失实时连接:CloseEvent code {0}", + "socket_reconnected": "已建立实时连接" }, "status": { "favorites": "喜欢", @@ -638,7 +677,6 @@ "follow": "关注", "follow_sent": "请求已发送!", "follow_progress": "请求中…", - "follow_again": "再次发送请求?", "follow_unfollow": "取消关注", "followees": "正在关注", "followers": "关注者", @@ -683,7 +721,15 @@ "show_repeats": "显示转发", "hide_repeats": "隐藏转发", "message": "消息", - "mention": "提及" + "mention": "提及", + "bot": "机器人", + "highlight": { + "side": "侧边条纹", + "striped": "条纹背景", + "solid": "单一颜色背景", + "disabled": "不突出显示" + }, + "edit_profile": "编辑个人资料" }, "user_profile": { "timeline_title": "用户时间线", @@ -778,8 +824,8 @@ "media_nsfw_desc": "本实例将来自以下实例的媒体内容强制设置为敏感内容:", "media_nsfw": "强制设置媒体为敏感内容", "media_removal_desc": "本实例移除来自以下实例的媒体内容:", - "ftl_removal_desc": "该实例在从“全部已知网络”时间线上移除了下列实例:", - "ftl_removal": "从“全部已知网络”时间线上移除" + "ftl_removal_desc": "该实例在从“已知网络”时间线上移除了下列实例:", + "ftl_removal": "从“已知网络”时间线上移除" }, "mrf_policies_desc": "MRF 策略会影响本实例的互通行为。以下策略已启用:", "mrf_policies": "已启用的 MRF 策略", @@ -801,7 +847,7 @@ "mute": "隐藏" }, "errors": { - "storage_unavailable": "Pleroma 无法访问浏览器储存。您的登陆名以及本地设置将不会被保存,您可能遇到意外问题。请尝试启用 cookies。" + "storage_unavailable": "Pleroma 无法访问浏览器储存。您的登陆以及本地设置将不会被保存,您也可能遇到未知问题。请尝试启用 cookies。" }, "shoutbox": { "title": "留言板" diff --git a/src/i18n/zh_Hant.json b/src/i18n/zh_Hant.json @@ -22,10 +22,12 @@ "votes": "票", "option": "選項", "add_option": "增加選項", - "add_poll": "增加投票" + "add_poll": "增加投票", + "votes_count": "{count} 票 | {count} 票", + "people_voted_count": "{count} 人已投票 | {count} 人已投票" }, "notifications": { - "reacted_with": "和 {0} 互動過", + "reacted_with": "作出了 {0} 的反應", "migrated_to": "遷移到", "no_more_notifications": "沒有更多的通知", "repeated_you": "轉發了你的發文", @@ -54,8 +56,9 @@ "mentions": "提及", "friend_requests": "關注請求", "back": "後退", - "administration": "管理", - "about": "關於" + "administration": "管理員", + "about": "關於", + "home_timeline": "家時間線" }, "media_modal": { "next": "往後", @@ -108,7 +111,14 @@ "loading": "載入中…", "more": "更多", "submit": "提交", - "apply": "應用" + "apply": "應用", + "role": { + "moderator": "主持人", + "admin": "管理員" + }, + "flash_content": "點擊以使用 Ruffle 顯示 Flash 內容(實驗性,可能無效)。", + "flash_security": "請注意,這可能有潜在的危險,因為Flash內容仍然是武斷的程式碼。", + "flash_fail": "無法加載flash內容,請參閱控制台瞭解詳細資訊。" }, "finder": { "find_user": "尋找用戶", @@ -216,7 +226,8 @@ "incoming": "收到", "outgoing": "發出", "border": "邊框" - } + }, + "wallpaper": "桌布" }, "preview": { "header_faint": "這很正常", @@ -321,7 +332,7 @@ "notification_visibility_moves": "用戶遷移", "notification_visibility_repeats": "轉發", "notification_visibility_mentions": "提及", - "notification_visibility_likes": "點贊", + "notification_visibility_likes": "喜歡", "interfaceLanguage": "界面語言", "instance_default": "(默認:{value})", "inputRadius": "輸入框", @@ -412,7 +423,7 @@ "hide_follows_description": "不要顯示我所關注的人", "hide_followers_description": "不要顯示關注我的人", "hide_follows_count_description": "不顯示關注數", - "nsfw_clickthrough": "將敏感附件隱藏,點擊才能打開", + "nsfw_clickthrough": "將敏感附件和鏈接隱藏,點擊才能打開", "valid_until": "有效期至", "panelRadius": "面板", "pause_on_unfocused": "在離開頁面時暫停時間線推送", @@ -423,7 +434,7 @@ "notification_blocks": "封鎖一個用戶會停掉所有他的通知,等同於取消關注。", "enable_web_push_notifications": "啟用 web 推送通知", "presets": "預置", - "profile_background": "個人背景圖", + "profile_background": "配置文件背景圖", "profile_banner": "橫幅圖片", "profile_tab": "個人資料", "radii_help": "設置界面邊緣的圓角 (單位:像素)", @@ -511,7 +522,7 @@ "show_moderator_badge": "顯示主持人徽章", "oauth_tokens": "OAuth代幣", "token": "代幣", - "refresh_token": "刷新代幣", + "refresh_token": "刷新token", "useStreamingApiWarning": "(不推薦使用,實驗性的,已知跳過文章)", "fun": "有趣", "notification_setting_hide_notification_contents": "隱藏推送通知中的發送者與內容信息", @@ -527,7 +538,30 @@ "mute_import_error": "導入靜音時出錯", "mute_export_button": "將靜音導出到csv文件", "mute_export": "靜音導出", - "hide_wallpaper": "隱藏實例桌布" + "hide_wallpaper": "隱藏實例桌布", + "reply_visibility_self_short": "只顯示對我本人的回复", + "reply_visibility_following_short": "顯示對我關注的人的回复", + "hide_all_muted_posts": "不顯示已隱藏的帖子", + "hide_media_previews": "隱藏媒體預覽", + "word_filter": "詞過濾", + "setting_changed": "與默認設置不同", + "more_settings": "更多設置", + "save": "保存更改", + "file_export_import": { + "errors": { + "invalid_file": "所選文件不是受支持的Pleroma設置備份。 沒有進行任何更改。", + "file_too_new": "不兼容的主版本:{fileMajor},此 PleromaFE(設置版本 {feMajor})過舊,無法處理", + "file_too_old": "不兼容的主版本:{fileMajor},文件版本過舊,不受支持(最小設置版本 {feMajor})", + "file_slightly_new": "檔案的小版本不同,有些設置可能無法載入" + }, + "restore_settings": "從文件還原設置", + "backup_settings_theme": "備份設置和主題到文件", + "backup_settings": "備份設置到文件", + "backup_restore": "設定備份" + }, + "sensitive_by_default": "默認標記發文為敏感內容", + "right_sidebar": "在右側顯示側邊欄", + "hide_shoutbox": "隱藏實例留言框" }, "chats": { "more": "更多", @@ -572,16 +606,20 @@ "thread_muted_and_words": ",有这些字:", "hide_full_subject": "隱藏完整標題", "show_content": "顯示內容", - "hide_content": "隱藏內容" + "hide_content": "隱藏內容", + "status_deleted": "該帖已被刪除", + "expand": "展开", + "external_source": "外部來源", + "nsfw": "工作不安全" }, "time": { - "hours": "{0} 小時", + "hours": "{0} 時", "days_short": "{0}天", "day_short": "{0}天", "days": "{0} 天", - "hour": "{0} 小时", - "hour_short": "{0}h", - "hours_short": "{0}h", + "hour": "{0} 時", + "hour_short": "{0}時", + "hours_short": "{0}時", "years_short": "{0} y", "now": "剛剛", "day": "{0} 天", @@ -639,7 +677,8 @@ "attachments_sensitive": "標記附件為敏感內容", "account_not_locked_warning_link": "上鎖", "default": "剛剛抵達洛杉磯。", - "empty_status_error": "無法發佈沒有附件的空發文" + "empty_status_error": "不能發布沒有內容,沒有附件的發文", + "post": "發送" }, "errors": { "storage_unavailable": "Pleroma無法訪問瀏覽器存儲。您的登錄名或本地設置將不會保存,您可能會遇到意外問題。嘗試啟用Cookie。" @@ -655,13 +694,16 @@ "reload": "重新載入", "up_to_date": "已是最新", "no_more_statuses": "没有更多發文", - "no_statuses": "没有發文" + "no_statuses": "没有發文", + "error": "取得時間線時發生錯誤:{0}", + "socket_reconnected": "已建立實時連接", + "socket_broke": "丟失實時連接:CloseEvent代碼{0}" }, "interactions": { "load_older": "載入更早的互動", "moves": "用戶遷移", "follows": "新的關注者", - "favs_repeats": "轉發和收藏" + "favs_repeats": "轉發和喜歡" }, "selectable_list": { "select_all": "選擇全部" @@ -690,7 +732,10 @@ "registration": "註冊", "password_confirm": "確認密碼", "email": "電子郵箱", - "bio": "簡介" + "bio": "簡介", + "reason_placeholder": "此實例的註冊需要手動批准。\n請讓管理知道您為什麼想要註冊。", + "reason": "註冊理由", + "register": "註冊" }, "user_card": { "its_you": "就是你!!", @@ -726,7 +771,6 @@ "follow": "關注", "follow_sent": "請求已發送!", "follow_progress": "請求中…", - "follow_again": "再次發送請求?", "follow_unfollow": "取消關注", "followees": "正在關注", "followers": "關注者", @@ -746,7 +790,19 @@ "unmute": "取消靜音", "unmute_progress": "取消靜音中…", "hide_repeats": "隱藏轉發", - "show_repeats": "顯示轉發" + "show_repeats": "顯示轉發", + "roles": { + "moderator": "主持人", + "admin": "管理員" + }, + "highlight": { + "disabled": "無突出顯示", + "solid": "單色背景", + "striped": "條紋背景", + "side": "彩條" + }, + "bot": "機器人", + "edit_profile": "編輯個人資料" }, "user_profile": { "timeline_title": "用戶時間線", @@ -788,7 +844,8 @@ "error": { "base": "上傳失敗。", "file_too_big": "文件太大[{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]", - "default": "稍後再試" + "default": "稍後再試", + "message": "上傳錯誤:{0}" } }, "search": { diff --git a/src/main.js b/src/main.js @@ -11,7 +11,7 @@ import statusesModule from './modules/statuses.js' import usersModule from './modules/users.js' import apiModule from './modules/api.js' import configModule from './modules/config.js' -import chatModule from './modules/chat.js' +import shoutModule from './modules/shout.js' import oauthModule from './modules/oauth.js' import authFlowModule from './modules/auth_flow.js' import mediaViewerModule from './modules/media_viewer.js' @@ -28,7 +28,6 @@ import pushNotifications from './lib/push_notifications_plugin.js' import messages from './i18n/messages.js' -import VueChatScroll from 'vue-chat-scroll' import VueClickOutside from 'v-click-outside' import PortalVue from 'portal-vue' import VBodyScrollLock from './directives/body_scroll_lock' @@ -42,7 +41,6 @@ const currentLocale = (window.navigator.language || 'en').split('-')[0] Vue.use(Vuex) Vue.use(VueRouter) Vue.use(VueI18n) -Vue.use(VueChatScroll) Vue.use(VueClickOutside) Vue.use(PortalVue) Vue.use(VBodyScrollLock) @@ -90,7 +88,7 @@ const persistedStateOptions = { users: usersModule, api: apiModule, config: configModule, - chat: chatModule, + shout: shoutModule, oauth: oauthModule, authFlow: authFlowModule, mediaViewer: mediaViewerModule, diff --git a/src/modules/api.js b/src/modules/api.js @@ -3,8 +3,11 @@ import { WSConnectionStatus } from '../services/api/api.service.js' import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js' import { Socket } from 'phoenix' +const retryTimeout = (multiplier) => 1000 * multiplier + const api = { state: { + retryMultiplier: 1, backendInteractor: backendInteractorService(), fetchers: {}, socket: null, @@ -34,18 +37,43 @@ const api = { }, setMastoUserSocketStatus (state, value) { state.mastoUserSocketStatus = value + }, + incrementRetryMultiplier (state) { + state.retryMultiplier = Math.max(++state.retryMultiplier, 3) + }, + resetRetryMultiplier (state) { + state.retryMultiplier = 1 } }, actions: { - // Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets - enableMastoSockets (store) { - const { state, dispatch } = store - if (state.mastoUserSocket) return + /** + * Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets + * + * @param {Boolean} [initial] - whether this enabling happened at boot time or not + */ + enableMastoSockets (store, initial) { + const { state, dispatch, commit } = store + // Do not initialize unless nonexistent or closed + if ( + state.mastoUserSocket && + ![ + WebSocket.CLOSED, + WebSocket.CLOSING + ].includes(state.mastoUserSocket.getState()) + ) { + return + } + if (initial) { + commit('setMastoUserSocketStatus', WSConnectionStatus.STARTING_INITIAL) + } else { + commit('setMastoUserSocketStatus', WSConnectionStatus.STARTING) + } return dispatch('startMastoUserSocket') }, disableMastoSockets (store) { - const { state, dispatch } = store + const { state, dispatch, commit } = store if (!state.mastoUserSocket) return + commit('setMastoUserSocketStatus', WSConnectionStatus.DISABLED) return dispatch('stopMastoUserSocket') }, @@ -91,11 +119,29 @@ const api = { } ) state.mastoUserSocket.addEventListener('open', () => { + // Do not show notification when we just opened up the page + if (state.mastoUserSocketStatus !== WSConnectionStatus.STARTING_INITIAL) { + dispatch('pushGlobalNotice', { + level: 'success', + messageKey: 'timeline.socket_reconnected', + timeout: 5000 + }) + } + // Stop polling if we were errored or disabled + if (new Set([ + WSConnectionStatus.ERROR, + WSConnectionStatus.DISABLED + ]).has(state.mastoUserSocketStatus)) { + dispatch('stopFetchingTimeline', { timeline: 'friends' }) + dispatch('stopFetchingNotifications') + dispatch('stopFetchingChats') + } + commit('resetRetryMultiplier') commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED) }) state.mastoUserSocket.addEventListener('error', ({ detail: error }) => { console.error('Error in MastoAPI websocket:', error) - commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR) + // TODO is this needed? dispatch('clearOpenedChats') }) state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => { @@ -106,14 +152,26 @@ const api = { const { code } = closeEvent if (ignoreCodes.has(code)) { console.debug(`Not restarting socket becasue of closure code ${code} is in ignore list`) + commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED) } else { console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`) - dispatch('startFetchingTimeline', { timeline: 'friends' }) - dispatch('startFetchingNotifications') - dispatch('startFetchingChats') - dispatch('restartMastoUserSocket') + setTimeout(() => { + dispatch('startMastoUserSocket') + }, retryTimeout(state.retryMultiplier)) + commit('incrementRetryMultiplier') + if (state.mastoUserSocketStatus !== WSConnectionStatus.ERROR) { + dispatch('startFetchingTimeline', { timeline: 'friends' }) + dispatch('startFetchingNotifications') + dispatch('startFetchingChats') + dispatch('pushGlobalNotice', { + level: 'error', + messageKey: 'timeline.socket_broke', + messageArgs: [code], + timeout: 5000 + }) + } + commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR) } - commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED) dispatch('clearOpenedChats') }) resolve() @@ -122,15 +180,6 @@ const api = { } }) }, - restartMastoUserSocket ({ dispatch }) { - // This basically starts MastoAPI user socket and stops conventional - // fetchers when connection reestablished - return dispatch('startMastoUserSocket').then(() => { - dispatch('stopFetchingTimeline', { timeline: 'friends' }) - dispatch('stopFetchingNotifications') - dispatch('stopFetchingChats') - }) - }, stopMastoUserSocket ({ state, dispatch }) { dispatch('startFetchingTimeline', { timeline: 'friends' }) dispatch('startFetchingNotifications') @@ -156,6 +205,13 @@ const api = { if (!fetcher) return store.commit('removeFetcher', { fetcherName: timeline, fetcher }) }, + fetchTimeline (store, timeline, { ...rest }) { + store.state.backendInteractor.fetchTimeline({ + store, + timeline, + ...rest + }) + }, // Notifications startFetchingNotifications (store) { @@ -168,6 +224,12 @@ const api = { if (!fetcher) return store.commit('removeFetcher', { fetcherName: 'notifications', fetcher }) }, + fetchNotifications (store, { ...rest }) { + store.state.backendInteractor.fetchNotifications({ + store, + ...rest + }) + }, // Follow requests startFetchingFollowRequests (store) { @@ -193,12 +255,12 @@ const api = { initializeSocket ({ dispatch, commit, state, rootState }) { // Set up websocket connection const token = state.wsToken - if (rootState.instance.chatAvailable && typeof token !== 'undefined' && state.socket === null) { + if (rootState.instance.shoutAvailable && typeof token !== 'undefined' && state.socket === null) { const socket = new Socket('/socket', { params: { token } }) socket.connect() commit('setSocket', socket) - dispatch('initializeChat', socket) + dispatch('initializeShout', socket) } }, disconnectFromSocket ({ commit, state }) { diff --git a/src/modules/chat.js b/src/modules/chat.js @@ -1,33 +0,0 @@ -const chat = { - state: { - messages: [], - channel: { state: '' } - }, - mutations: { - setChannel (state, channel) { - state.channel = channel - }, - addMessage (state, message) { - state.messages.push(message) - state.messages = state.messages.slice(-19, 20) - }, - setMessages (state, messages) { - state.messages = messages.slice(-19, 20) - } - }, - actions: { - initializeChat (store, socket) { - const channel = socket.channel('chat:public') - channel.on('new_msg', (msg) => { - store.commit('addMessage', msg) - }) - channel.on('messages', ({ messages }) => { - store.commit('setMessages', messages) - }) - channel.join() - store.commit('setChannel', channel) - } - } -} - -export default chat diff --git a/src/modules/chats.js b/src/modules/chats.js @@ -115,6 +115,9 @@ const chats = { }, handleMessageError ({ commit }, value) { commit('handleMessageError', { commit, ...value }) + }, + cullOlderMessages ({ commit }, chatId) { + commit('cullOlderMessages', chatId) } }, mutations: { @@ -227,6 +230,9 @@ const chats = { handleMessageError (state, { chatId, fakeId, isRetry }) { const chatMessageService = state.openedChatMessageServices[chatId] chatService.handleMessageError(chatMessageService, fakeId, isRetry) + }, + cullOlderMessages (state, chatId) { + chatService.cullOlderMessages(state.openedChatMessageServices[chatId]) } } } diff --git a/src/modules/config.js b/src/modules/config.js @@ -11,7 +11,8 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0] */ export const multiChoiceProperties = [ 'postContentType', - 'subjectLineBehavior' + 'subjectLineBehavior', + 'mentionLinkDisplay' // short | full_for_remote | full ] export const defaultState = { @@ -21,8 +22,11 @@ export const defaultState = { customThemeSource: undefined, hideISP: false, hideInstanceWallpaper: false, + hideShoutbox: false, // bad name: actually hides posts of muted USERS hideMutedPosts: undefined, // instance default + hideMutedThreads: undefined, // instance default + hideWordFilteredPosts: undefined, // instance default collapseMessageWithSubject: undefined, // instance default padEmoji: true, hideAttachments: false, @@ -34,6 +38,7 @@ export const defaultState = { loopVideoSilentOnly: true, streaming: false, emojiReactionsOnTimeline: true, + alwaysShowNewPostButton: false, autohideFloatingPostButton: false, pauseOnUnfocused: true, stopGifs: false, @@ -55,6 +60,7 @@ export const defaultState = { interfaceLanguage: browserLocale, hideScopeNotice: false, useStreamingApi: false, + sidebarRight: undefined, // instance default scopeCopy: undefined, // instance default subjectLineBehavior: undefined, // instance default alwaysShowSubjectInput: undefined, // instance default @@ -66,9 +72,17 @@ export const defaultState = { useOneClickNsfw: false, useContainFit: false, greentext: undefined, // instance default + useAtIcon: undefined, // instance default + mentionLinkDisplay: undefined, // instance default + mentionLinkShowTooltip: undefined, // instance default + mentionLinkShowAvatar: undefined, // instance default + mentionLinkFadeDomain: undefined, // instance default + mentionLinkShowYous: undefined, // instance default + mentionLinkBoldenYou: undefined, // instance default hidePostStats: undefined, // instance default hideUserStats: undefined, // instance default - virtualScrolling: undefined // instance default + virtualScrolling: undefined, // instance default + sensitiveByDefault: undefined // instance default } // caching the instance default properties @@ -77,18 +91,23 @@ export const instanceDefaultProperties = Object.entries(defaultState) .map(([key, value]) => key) const config = { - state: defaultState, + state: { ...defaultState }, getters: { - mergedConfig (state, getters, rootState, rootGetters) { + defaultConfig (state, getters, rootState, rootGetters) { const { instance } = rootState return { - ...state, - ...instanceDefaultProperties - .map(key => [key, state[key] === undefined - ? instance[key] - : state[key] - ]) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) + ...defaultState, + ...Object.fromEntries( + instanceDefaultProperties.map(key => [key, instance[key]]) + ) + } + }, + mergedConfig (state, getters, rootState, rootGetters) { + const { defaultConfig } = rootGetters + return { + ...defaultConfig, + // Do not override with undefined + ...Object.fromEntries(Object.entries(state).filter(([k, v]) => v !== undefined)) } } }, @@ -106,6 +125,20 @@ const config = { } }, actions: { + loadSettings ({ dispatch }, data) { + const knownKeys = new Set(Object.keys(defaultState)) + const presentKeys = new Set(Object.keys(data)) + const intersection = new Set() + for (let elem of presentKeys) { + if (knownKeys.has(elem)) { + intersection.add(elem) + } + } + + intersection.forEach( + name => dispatch('setOption', { name, value: data[name] }) + ) + }, setHighlight ({ commit, dispatch }, { user, color, type }) { commit('setHighlight', { user, color, type }) }, diff --git a/src/modules/instance.js b/src/modules/instance.js @@ -19,10 +19,19 @@ const defaultState = { defaultBanner: '/images/banner.png', background: '/static/aurora_borealis.jpg', collapseMessageWithSubject: false, - disableChat: false, greentext: false, + useAtIcon: false, + mentionLinkDisplay: 'short', + mentionLinkShowTooltip: true, + mentionLinkShowAvatar: false, + mentionLinkFadeDomain: true, + mentionLinkShowYous: false, + mentionLinkBoldenYou: true, hideFilteredStatuses: false, + // bad name: actually hides posts of muted USERS hideMutedPosts: false, + hideMutedThreads: true, + hideWordFilteredPosts: false, hidePostStats: false, hideSitename: false, hideUserStats: false, @@ -43,6 +52,7 @@ const defaultState = { subjectLineBehavior: 'email', theme: 'pleroma-dark', virtualScrolling: true, + sensitiveByDefault: false, // Nasty stuff customEmoji: [], @@ -56,7 +66,7 @@ const defaultState = { knownDomains: [], // Feature-set, apparently, not everything here is reported... - chatAvailable: false, + shoutAvailable: false, pleromaChatMessagesAvailable: false, gopherAvailable: false, mediaProxyAvailable: false, @@ -97,6 +107,9 @@ const instance = { return instanceDefaultProperties .map(key => [key, state[key]]) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) + }, + instanceDomain (state) { + return new URL(state.server).hostname } }, actions: { @@ -106,7 +119,7 @@ const instance = { case 'name': dispatch('setPageTitle') break - case 'chatAvailable': + case 'shoutAvailable': if (value) { dispatch('initializeSocket') } diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js @@ -1,4 +1,5 @@ import fileTypeService from '../services/file_type/file_type.service.js' +const supportedTypes = new Set(['image', 'video', 'audio', 'flash']) const mediaViewer = { state: { @@ -10,7 +11,7 @@ const mediaViewer = { setMedia (state, media) { state.media = media }, - setCurrent (state, index) { + setCurrentMedia (state, index) { state.activated = true state.currentIndex = index }, @@ -22,13 +23,13 @@ const mediaViewer = { setMedia ({ commit }, attachments) { const media = attachments.filter(attachment => { const type = fileTypeService.fileType(attachment.mimetype) - return type === 'image' || type === 'video' || type === 'audio' + return supportedTypes.has(type) }) commit('setMedia', media) }, - setCurrent ({ commit, state }, current) { + setCurrentMedia ({ commit, state }, current) { const index = state.media.indexOf(current) - commit('setCurrent', index || 0) + commit('setCurrentMedia', index || 0) }, closeMediaViewer ({ commit }) { commit('close') diff --git a/src/modules/shout.js b/src/modules/shout.js @@ -0,0 +1,33 @@ +const shout = { + state: { + messages: [], + channel: { state: '' } + }, + mutations: { + setChannel (state, channel) { + state.channel = channel + }, + addMessage (state, message) { + state.messages.push(message) + state.messages = state.messages.slice(-19, 20) + }, + setMessages (state, messages) { + state.messages = messages.slice(-19, 20) + } + }, + actions: { + initializeShout (store, socket) { + const channel = socket.channel('chat:public') + channel.on('new_msg', (msg) => { + store.commit('addMessage', msg) + }) + channel.on('messages', ({ messages }) => { + store.commit('setMessages', messages) + }) + channel.join() + store.commit('setChannel', channel) + } + } +} + +export default shout diff --git a/src/modules/statuses.js b/src/modules/statuses.js @@ -13,7 +13,11 @@ import { omitBy } from 'lodash' import { set } from 'vue' -import { isStatusNotification, maybeShowNotification } from '../services/notification_utils/notification_utils.js' +import { + isStatusNotification, + isValidNotification, + maybeShowNotification +} from '../services/notification_utils/notification_utils.js' import apiService from '../services/api/api.service.js' const emptyTl = (userId = 0) => ({ @@ -310,8 +314,24 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us } } +const updateNotificationsMinMaxId = (state, notification) => { + state.notifications.maxId = notification.id > state.notifications.maxId + ? notification.id + : state.notifications.maxId + state.notifications.minId = notification.id < state.notifications.minId + ? notification.id + : state.notifications.minId +} + const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters, newNotificationSideEffects }) => { each(notifications, (notification) => { + // If invalid notification, update ids but don't add it to store + if (!isValidNotification(notification)) { + console.error('Invalid notification:', notification) + updateNotificationsMinMaxId(state, notification) + return + } + if (isStatusNotification(notification.type)) { notification.action = addStatusToGlobalStorage(state, notification.action).item notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item @@ -327,12 +347,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot // Only add a new notification if we don't have one for the same action if (!state.notifications.idStore.hasOwnProperty(notification.id)) { - state.notifications.maxId = notification.id > state.notifications.maxId - ? notification.id - : state.notifications.maxId - state.notifications.minId = notification.id < state.notifications.minId - ? notification.id - : state.notifications.minId + updateNotificationsMinMaxId(state, notification) state.notifications.data.push(notification) state.notifications.idStore[notification.id] = notification diff --git a/src/modules/users.js b/src/modules/users.js @@ -246,6 +246,11 @@ export const getters = { } return result }, + findUserByUrl: state => query => { + return state.users + .find(u => u.statusnet_profile_url && + u.statusnet_profile_url.toLowerCase() === query.toLowerCase()) + }, relationship: state => id => { const rel = id && state.relationships[id] return rel || { id, loading: true } @@ -388,7 +393,7 @@ const users = { toggleActivationStatus ({ rootState, commit }, { user }) { const api = user.deactivated ? rootState.api.backendInteractor.activateUser : rootState.api.backendInteractor.deactivateUser api({ user }) - .then(({ deactivated }) => commit('updateActivationStatus', { user, deactivated })) + .then((user) => { let deactivated = !user.is_active; commit('updateActivationStatus', { user, deactivated }) }) }, registerPushNotifications (store) { const token = store.state.currentUser.credentials @@ -531,7 +536,7 @@ const users = { if (user.token) { store.dispatch('setWsToken', user.token) - // Initialize the chat socket. + // Initialize the shout socket. store.dispatch('initializeSocket') } @@ -547,9 +552,10 @@ const users = { } if (store.getters.mergedConfig.useStreamingApi) { - store.dispatch('enableMastoSockets').catch((error) => { + store.dispatch('fetchTimeline', 'friends', { since: null }) + store.dispatch('fetchNotifications', { since: null }) + store.dispatch('enableMastoSockets', true).catch((error) => { console.error('Failed initializing MastoAPI Streaming socket', error) - startPolling() }).then(() => { store.dispatch('fetchChats', { latest: true }) setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js @@ -1159,6 +1159,7 @@ export const ProcessedWS = ({ // 1000 = Normal Closure eventTarget.close = () => { socket.close(1000, 'Shutting down socket') } + eventTarget.getState = () => socket.readyState return eventTarget } @@ -1190,7 +1191,10 @@ export const handleMastoWS = (wsEvent) => { export const WSConnectionStatus = Object.freeze({ 'JOINED': 1, 'CLOSED': 2, - 'ERROR': 3 + 'ERROR': 3, + 'DISABLED': 4, + 'STARTING': 5, + 'STARTING_INITIAL': 6 }) const chats = ({ credentials }) => { diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js @@ -1,17 +1,25 @@ import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js' -import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js' +import timelineFetcher from '../timeline_fetcher/timeline_fetcher.service.js' import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js' import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service' const backendInteractorService = credentials => ({ startFetchingTimeline ({ timeline, store, userId = false, tag }) { - return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag }) + return timelineFetcher.startFetching({ timeline, store, credentials, userId, tag }) + }, + + fetchTimeline (args) { + return timelineFetcher.fetchAndUpdate({ ...args, credentials }) }, startFetchingNotifications ({ store }) { return notificationsFetcher.startFetching({ store, credentials }) }, + fetchNotifications (args) { + return notificationsFetcher.fetchAndUpdate({ ...args, 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 @@ -48,6 +48,22 @@ const deleteMessage = (storage, messageId) => { } } +const cullOlderMessages = (storage) => { + const maxIndex = storage.messages.length + const minIndex = maxIndex - 50 + if (maxIndex <= 50) return + + storage.messages = _.sortBy(storage.messages, ['id']) + storage.minId = storage.messages[minIndex].id + for (const message of storage.messages) { + if (message.id < storage.minId) { + delete storage.idIndex[message.id] + delete storage.idempotencyKeyIndex[message.idempotency_key] + } + } + storage.messages = storage.messages.slice(minIndex, maxIndex) +} + const handleMessageError = (storage, fakeId, isRetry) => { if (!storage) { return } const fakeMessage = storage.idIndex[fakeId] @@ -201,6 +217,7 @@ const ChatService = { empty, getView, deleteMessage, + cullOlderMessages, resetNewMessageCount, clear, handleMessageError diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js @@ -54,17 +54,20 @@ export const parseUser = (data) => { return output } - output.name = data.display_name - output.name_html = addEmojis(escape(data.display_name), data.emojis) + output.emoji = data.emojis + output.name = escape(data.display_name) + output.name_html = output.name + output.name_unescaped = data.display_name output.description = data.note - output.description_html = addEmojis(data.note, data.emojis) + // TODO cleanup this shit, output.description is overriden with source data + output.description_html = data.note output.fields = data.fields output.fields_html = data.fields.map(field => { return { - name: addEmojis(escape(field.name), data.emojis), - value: addEmojis(field.value, data.emojis) + name: escape(field.name), + value: field.value } }) output.fields_text = data.fields.map(field => { @@ -203,15 +206,16 @@ export const parseUser = (data) => { output.rights = output.rights || {} output.notification_settings = output.notification_settings || {} - // Convert punycode to unicode - if (output.screen_name.includes('@')) { + // Convert punycode to unicode for UI + output.screen_name_ui = output.screen_name + if (output.screen_name && output.screen_name.includes('@')) { const parts = output.screen_name.split('@') let unicodeDomain = punycode.toUnicode(parts[1]) if (unicodeDomain !== parts[1]) { // Add some identifier so users can potentially spot spoofing attempts: // lain.com and xn--lin-6cd.com would appear identical otherwise. unicodeDomain = '🌏' + unicodeDomain - output.screen_name = [parts[0], unicodeDomain].join('@') + output.screen_name_ui = [parts[0], unicodeDomain].join('@') } } @@ -238,16 +242,6 @@ export const parseAttachment = (data) => { return output } -export const addEmojis = (string, emojis) => { - const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g - return emojis.reduce((acc, emoji) => { - const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&') - return acc.replace( - new RegExp(`:${regexSafeShortCode}:`, 'g'), - `<img src='${emoji.url}' alt=':${emoji.shortcode}:' title=':${emoji.shortcode}:' class='emoji' />` - ) - }, string) -} export const parseStatus = (data) => { const output = {} @@ -265,7 +259,8 @@ export const parseStatus = (data) => { output.type = data.reblog ? 'retweet' : 'status' output.nsfw = data.sensitive - output.statusnet_html = addEmojis(data.content, data.emojis) + output.raw_html = data.content + output.emojis = data.emojis output.tags = data.tags @@ -292,13 +287,13 @@ export const parseStatus = (data) => { output.retweeted_status = parseStatus(data.reblog) } - output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis) + output.summary_raw_html = escape(data.spoiler_text) output.external_url = data.url output.poll = data.poll if (output.poll) { output.poll.options = (output.poll.options || []).map(field => ({ ...field, - title_html: addEmojis(escape(field.title), data.emojis) + title_html: escape(field.title) })) } output.pinned = data.pinned @@ -324,7 +319,7 @@ export const parseStatus = (data) => { output.nsfw = data.nsfw } - output.statusnet_html = data.statusnet_html + output.raw_html = data.statusnet_html output.text = data.text output.in_reply_to_status_id = data.in_reply_to_status_id @@ -450,11 +445,8 @@ export const parseChatMessage = (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 = '' - } + output.emojis = message.emojis + output.content = message.content if (message.attachment) { output.attachments = [parseAttachment(message.attachment)] } else { diff --git a/src/services/export_import/export_import.js b/src/services/export_import/export_import.js @@ -0,0 +1,55 @@ +export const newExporter = ({ + filename = 'data', + getExportedObject +}) => ({ + exportData () { + const stringified = JSON.stringify(getExportedObject(), null, 2) // Pretty-print and indent with 2 spaces + + // Create an invisible link with a data url and simulate a click + const e = document.createElement('a') + e.setAttribute('download', `${filename}.json`) + e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified)) + e.style.display = 'none' + + document.body.appendChild(e) + e.click() + document.body.removeChild(e) + } +}) + +export const newImporter = ({ + onImport, + onImportFailure, + validator = () => true +}) => ({ + importData () { + const filePicker = document.createElement('input') + filePicker.setAttribute('type', 'file') + filePicker.setAttribute('accept', '.json') + + filePicker.addEventListener('change', event => { + if (event.target.files[0]) { + // eslint-disable-next-line no-undef + const reader = new FileReader() + reader.onload = ({ target }) => { + try { + const parsed = JSON.parse(target.result) + const validationResult = validator(parsed) + if (validationResult === true) { + onImport(parsed) + } else { + onImportFailure({ validationResult }) + } + } catch (error) { + onImportFailure({ error }) + } + } + reader.readAsText(event.target.files[0]) + } + }) + + document.body.appendChild(filePicker) + filePicker.click() + document.body.removeChild(filePicker) + } +}) diff --git a/src/services/favicon_service/favicon_service.js b/src/services/favicon_service/favicon_service.js @@ -1,52 +1,58 @@ -import { find } from 'lodash' - const createFaviconService = () => { - let favimg, favcanvas, favcontext, favicon + const favicons = [] const faviconWidth = 128 const faviconHeight = 128 const badgeRadius = 32 const initFaviconService = () => { - const nodes = document.getElementsByTagName('link') - favicon = find(nodes, node => node.rel === 'icon') - if (favicon) { - favcanvas = document.createElement('canvas') - favcanvas.width = faviconWidth - favcanvas.height = faviconHeight - favimg = new Image() - favimg.src = favicon.href - favcontext = favcanvas.getContext('2d') - } + const nodes = document.querySelectorAll('link[rel="icon"]') + nodes.forEach(favicon => { + if (favicon) { + const favcanvas = document.createElement('canvas') + favcanvas.width = faviconWidth + favcanvas.height = faviconHeight + const favimg = new Image() + favimg.crossOrigin = 'anonymous' + favimg.src = favicon.href + const favcontext = favcanvas.getContext('2d') + favicons.push({ favcanvas, favimg, favcontext, favicon }) + } + }) } const isImageLoaded = (img) => img.complete && img.naturalHeight !== 0 const clearFaviconBadge = () => { - if (!favimg || !favcontext || !favicon) return + if (favicons.length === 0) return + favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => { + if (!favimg || !favcontext || !favicon) return - favcontext.clearRect(0, 0, faviconWidth, faviconHeight) - if (isImageLoaded(favimg)) { - favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight) - } - favicon.href = favcanvas.toDataURL('image/png') + favcontext.clearRect(0, 0, faviconWidth, faviconHeight) + if (isImageLoaded(favimg)) { + favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight) + } + favicon.href = favcanvas.toDataURL('image/png') + }) } const drawFaviconBadge = () => { - if (!favimg || !favcontext || !favcontext) return - + if (favicons.length === 0) return clearFaviconBadge() + favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => { + if (!favimg || !favcontext || !favcontext) return + + const style = getComputedStyle(document.body) + const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}` - const style = getComputedStyle(document.body) - const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}` - - if (isImageLoaded(favimg)) { - favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight) - } - favcontext.fillStyle = badgeColor - favcontext.beginPath() - favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false) - favcontext.fill() - favicon.href = favcanvas.toDataURL('image/png') + if (isImageLoaded(favimg)) { + favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight) + } + favcontext.fillStyle = badgeColor + favcontext.beginPath() + favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false) + favcontext.fill() + favicon.href = favcanvas.toDataURL('image/png') + }) } return { diff --git a/src/services/file_type/file_type.service.js b/src/services/file_type/file_type.service.js @@ -2,6 +2,10 @@ // or the entire service could be just mimetype service that only operates // on mimetypes and not files. Currently the naming is confusing. const fileType = mimetype => { + if (mimetype.match(/flash/)) { + return 'flash' + } + if (mimetype.match(/text\/html/)) { return 'html' } diff --git a/src/services/html_converter/html_line_converter.service.js b/src/services/html_converter/html_line_converter.service.js @@ -0,0 +1,136 @@ +import { getTagName } from './utility.service.js' + +/** + * This is a tiny purpose-built HTML parser/processor. This basically detects + * any type of visual newline and converts entire HTML into a array structure. + * + * Text nodes are represented as object with single property - text - containing + * the visual line. Intended usage is to process the array with .map() in which + * map function returns a string and resulting array can be converted back to html + * with a .join(''). + * + * Generally this isn't very useful except for when you really need to either + * modify visual lines (greentext i.e. simple quoting) or do something with + * first/last line. + * + * known issue: doesn't handle CDATA so nested CDATA might not work well + * + * @param {Object} input - input data + * @return {(string|{ text: string })[]} processed html in form of a list. + */ +export const convertHtmlToLines = (html = '') => { + // Elements that are implicitly self-closing + // https://developer.mozilla.org/en-US/docs/Glossary/empty_element + const emptyElements = new Set([ + 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', + 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' + ]) + // Block-level element (they make a visual line) + // https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements + const blockElements = new Set([ + 'address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'dd', + 'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li', 'main', + 'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul' + ]) + // br is very weird in a way that it's technically not block-level, it's + // essentially converted to a \n (or \r\n). There's also wbr but it doesn't + // guarantee linebreak, only suggest it. + const linebreakElements = new Set(['br']) + + const visualLineElements = new Set([ + ...blockElements.values(), + ...linebreakElements.values() + ]) + + // All block-level elements that aren't empty elements, i.e. not <hr> + const nonEmptyElements = new Set(visualLineElements) + // Difference + for (let elem of emptyElements) { + nonEmptyElements.delete(elem) + } + + // All elements that we are recognizing + const allElements = new Set([ + ...nonEmptyElements.values(), + ...emptyElements.values() + ]) + + let buffer = [] // Current output buffer + const level = [] // How deep we are in tags and which tags were there + let textBuffer = '' // Current line content + let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag + + const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer + if (textBuffer.trim().length > 0) { + buffer.push({ level: [...level], text: textBuffer }) + } else { + buffer.push(textBuffer) + } + textBuffer = '' + } + + const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing + flush() + buffer.push(tag) + } + + const handleOpen = (tag) => { // handles opening tags + flush() + buffer.push(tag) + level.unshift(getTagName(tag)) + } + + const handleClose = (tag) => { // handles closing tags + if (level[0] === getTagName(tag)) { + flush() + buffer.push(tag) + level.shift() + } else { // Broken case + textBuffer += tag + } + } + + for (let i = 0; i < html.length; i++) { + const char = html[i] + if (char === '<' && tagBuffer === null) { + tagBuffer = char + } else if (char !== '>' && tagBuffer !== null) { + tagBuffer += char + } else if (char === '>' && tagBuffer !== null) { + tagBuffer += char + const tagFull = tagBuffer + tagBuffer = null + const tagName = getTagName(tagFull) + if (allElements.has(tagName)) { + if (linebreakElements.has(tagName)) { + handleBr(tagFull) + } else if (nonEmptyElements.has(tagName)) { + if (tagFull[1] === '/') { + handleClose(tagFull) + } else if (tagFull[tagFull.length - 2] === '/') { + // self-closing + handleBr(tagFull) + } else { + handleOpen(tagFull) + } + } else { + textBuffer += tagFull + } + } else { + textBuffer += tagFull + } + } else if (char === '\n') { + handleBr(char) + } else { + textBuffer += char + } + } + if (tagBuffer) { + textBuffer += tagBuffer + } + + flush() + + return buffer +} diff --git a/src/services/html_converter/html_tree_converter.service.js b/src/services/html_converter/html_tree_converter.service.js @@ -0,0 +1,98 @@ +import { getTagName } from './utility.service.js' +import { unescape } from 'lodash' + +/** + * This is a not-so-tiny purpose-built HTML parser/processor. This parses html + * and converts it into a tree structure representing tag openers/closers and + * children. + * + * Structure follows this pattern: [opener, [...children], closer] except root + * node which is just [...children]. Text nodes can only be within children and + * are represented as strings. + * + * Intended use is to convert HTML structure and then recursively iterate over it + * most likely using a map. Very useful for dynamically rendering html replacing + * tags with JSX elements in a render function. + * + * known issue: doesn't handle CDATA so CDATA might not work well + * known issue: doesn't handle HTML comments + * + * @param {Object} input - input data + * @return {string} processed html + */ +export const convertHtmlToTree = (html = '') => { + // Elements that are implicitly self-closing + // https://developer.mozilla.org/en-US/docs/Glossary/empty_element + const emptyElements = new Set([ + 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', + 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' + ]) + // TODO For future - also parse HTML5 multi-source components? + + const buffer = [] // Current output buffer + const levels = [['', buffer]] // How deep we are in tags and which tags were there + let textBuffer = '' // Current line content + let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag + + const getCurrentBuffer = () => { + return levels[levels.length - 1][1] + } + + const flushText = () => { // Processes current line buffer, adds it to output buffer and clears line buffer + if (textBuffer === '') return + getCurrentBuffer().push(textBuffer) + textBuffer = '' + } + + const handleSelfClosing = (tag) => { + getCurrentBuffer().push([tag]) + } + + const handleOpen = (tag) => { + const curBuf = getCurrentBuffer() + const newLevel = [unescape(tag), []] + levels.push(newLevel) + curBuf.push(newLevel) + } + + const handleClose = (tag) => { + const currentTag = levels[levels.length - 1] + if (getTagName(levels[levels.length - 1][0]) === getTagName(tag)) { + currentTag.push(tag) + levels.pop() + } else { + getCurrentBuffer().push(tag) + } + } + + for (let i = 0; i < html.length; i++) { + const char = html[i] + if (char === '<' && tagBuffer === null) { + flushText() + tagBuffer = char + } else if (char !== '>' && tagBuffer !== null) { + tagBuffer += char + } else if (char === '>' && tagBuffer !== null) { + tagBuffer += char + const tagFull = tagBuffer + tagBuffer = null + const tagName = getTagName(tagFull) + if (tagFull[1] === '/') { + handleClose(tagFull) + } else if (emptyElements.has(tagName) || tagFull[tagFull.length - 2] === '/') { + // self-closing + handleSelfClosing(tagFull) + } else { + handleOpen(tagFull) + } + } else { + textBuffer += char + } + } + if (tagBuffer) { + textBuffer += tagBuffer + } + + flushText() + return buffer +} diff --git a/src/services/html_converter/utility.service.js b/src/services/html_converter/utility.service.js @@ -0,0 +1,73 @@ +/** + * Extract tag name from tag opener/closer. + * + * @param {String} tag - tag string, i.e. '<a href="...">' + * @return {String} - tagname, i.e. "div" + */ +export const getTagName = (tag) => { + const result = /(?:<\/(\w+)>|<(\w+)\s?.*?\/?>)/gi.exec(tag) + return result && (result[1] || result[2]) +} + +/** + * Extract attributes from tag opener. + * + * @param {String} tag - tag string, i.e. '<a href="...">' + * @return {Object} - map of attributes key = attribute name, value = attribute value + * attributes without values represented as boolean true + */ +export const getAttrs = tag => { + const innertag = tag + .substring(1, tag.length - 1) + .replace(new RegExp('^' + getTagName(tag)), '') + .replace(/\/?$/, '') + .trim() + const attrs = Array.from(innertag.matchAll(/([a-z0-9-]+)(?:=("[^"]+?"|'[^']+?'))?/gi)) + .map(([trash, key, value]) => [key, value]) + .map(([k, v]) => { + if (!v) return [k, true] + return [k, v.substring(1, v.length - 1)] + }) + return Object.fromEntries(attrs) +} + +/** + * Finds shortcodes in text + * + * @param {String} text - original text to find emojis in + * @param {{ url: String, shortcode: Sring }[]} emoji - list of shortcodes to find + * @param {Function} processor - function to call on each encountered emoji, + * function is passed single object containing matching emoji ({ url, shortcode }) + * return value will be inserted into resulting array instead of :shortcode: + * @return {Array} resulting array with non-emoji parts of text and whatever {processor} + * returned for emoji + */ +export const processTextForEmoji = (text, emojis, processor) => { + const buffer = [] + let textBuffer = '' + for (let i = 0; i < text.length; i++) { + const char = text[i] + if (char === ':') { + const next = text.slice(i + 1) + let found = false + for (let emoji of emojis) { + if (next.slice(0, emoji.shortcode.length + 1) === (emoji.shortcode + ':')) { + found = emoji + break + } + } + if (found) { + buffer.push(textBuffer) + textBuffer = '' + buffer.push(processor(found)) + i += found.shortcode.length + 1 + } else { + textBuffer += char + } + } else { + textBuffer += char + } + } + if (textBuffer) buffer.push(textBuffer) + return buffer +} diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js @@ -23,6 +23,13 @@ const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reactio export const isStatusNotification = (type) => includes(statusNotifications, type) +export const isValidNotification = (notification) => { + if (isStatusNotification(notification.type) && !notification.status) { + return false + } + return true +} + const sortById = (a, b) => { const seqA = Number(a.id) const seqB = Number(b.id) diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -17,7 +17,7 @@ const update = ({ store, notifications, older }) => { store.dispatch('addNewNotifications', { notifications, older }) } -const fetchAndUpdate = ({ store, credentials, older = false }) => { +const fetchAndUpdate = ({ store, credentials, older = false, since }) => { const args = { credentials } const { getters } = store const rootState = store.rootState || store.state @@ -35,8 +35,10 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => { return fetchNotifications({ store, args, older }) } else { // fetch new notifications - if (timelineData.maxId !== Number.POSITIVE_INFINITY) { + if (since === undefined && timelineData.maxId !== Number.POSITIVE_INFINITY) { args['since'] = timelineData.maxId + } else if (since !== null) { + args['since'] = since } const result = fetchNotifications({ store, args, older }) diff --git a/src/services/ruffle_service/ruffle_service.js b/src/services/ruffle_service/ruffle_service.js @@ -0,0 +1,40 @@ +const createRuffleService = () => { + let ruffleInstance = null + + const getRuffle = () => new Promise((resolve, reject) => { + if (ruffleInstance) { + resolve(ruffleInstance) + return + } + // Ruffle needs these to be set before it's loaded + // https://github.com/ruffle-rs/ruffle/issues/3952 + window.RufflePlayer = {} + window.RufflePlayer.config = { + polyfills: false, + publicPath: '/static/ruffle' + } + + // Currently it's seems like a better way of loading ruffle + // because it needs the wasm publically accessible, but it needs path to it + // and filename of wasm seems to be pseudo-randomly generated (is it a hash?) + const script = document.createElement('script') + // see webpack config, using CopyPlugin to copy it from node_modules + // provided via ruffle-mirror + script.src = '/static/ruffle/ruffle.js' + script.type = 'text/javascript' + script.onerror = (e) => { reject(e) } + script.onabort = (e) => { reject(e) } + script.oncancel = (e) => { reject(e) } + script.onload = () => { + ruffleInstance = window.RufflePlayer + resolve(ruffleInstance) + } + document.body.appendChild(script) + }) + + return { getRuffle } +} + +const RuffleService = createRuffleService() + +export default RuffleService diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js @@ -244,7 +244,7 @@ export const generateShadows = (input, colors) => { } const cleanInputShadows = Object.fromEntries( - Object.entries(input.shadows) + Object.entries(input.shadows || {}) .map(([name, shadowSlot]) => [ name, // defaulting color to black to avoid potential problems @@ -380,7 +380,7 @@ export const colors2to3 = (colors) => { */ export const shadows2to3 = (shadows, opacity) => { return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => { - const isDynamic = ({ color }) => color.startsWith('--') + const isDynamic = ({ color = '#000000' }) => color.startsWith('--') const getOpacity = ({ color }) => opacity[getOpacitySlot(color.substring(2).split(',')[0])] const newShadow = shadowDefs.reduce((shadowAcc, def) => [ ...shadowAcc, diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js @@ -369,6 +369,12 @@ export const SLOT_INHERITANCE = { textColor: 'preserve' }, + postCyantext: { + depends: ['cBlue'], + layer: 'bg', + textColor: 'preserve' + }, + border: { depends: ['fg'], opacity: 'border', @@ -616,6 +622,23 @@ export const SLOT_INHERITANCE = { textColor: true }, + alertSuccess: { + depends: ['cGreen'], + opacity: 'alert' + }, + alertSuccessText: { + depends: ['text'], + layer: 'alert', + variant: 'alertSuccess', + textColor: true + }, + alertSuccessPanelText: { + depends: ['panelText'], + layer: 'alertPanel', + variant: 'alertSuccess', + textColor: true + }, + alertNeutral: { depends: ['text'], opacity: 'alert' @@ -656,6 +679,17 @@ export const SLOT_INHERITANCE = { textColor: true }, + alertPopupSuccess: { + depends: ['alertSuccess'], + opacity: 'alertPopup' + }, + alertPopupSuccessText: { + depends: ['alertSuccessText'], + layer: 'popover', + variant: 'alertPopupSuccess', + textColor: true + }, + alertPopupNeutral: { depends: ['alertNeutral'], opacity: 'alertPopup' diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -23,7 +23,8 @@ const fetchAndUpdate = ({ showImmediately = false, userId = false, tag = false, - until + until, + since }) => { const args = { timeline, credentials } const rootState = store.rootState || store.state @@ -35,7 +36,11 @@ const fetchAndUpdate = ({ if (older) { args['until'] = until || timelineData.minId } else { - args['since'] = timelineData.maxId + if (since === undefined) { + args['since'] = timelineData.maxId + } else if (since !== null) { + args['since'] = since + } } args['userId'] = userId diff --git a/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js b/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js @@ -1,94 +0,0 @@ -/** - * This is a tiny purpose-built HTML parser/processor. This basically detects any type of visual newline and - * allows it to be processed, useful for greentexting, mostly - * - * known issue: doesn't handle CDATA so nested CDATA might not work well - * - * @param {Object} input - input data - * @param {(string) => string} processor - function that will be called on every line - * @return {string} processed html - */ -export const processHtml = (html, processor) => { - const handledTags = new Set(['p', 'br', 'div']) - const openCloseTags = new Set(['p', 'div']) - - let buffer = '' // Current output buffer - const level = [] // How deep we are in tags and which tags were there - let textBuffer = '' // Current line content - let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag - - // Extracts tag name from tag, i.e. <span a="b"> => span - const getTagName = (tag) => { - const result = /(?:<\/(\w+)>|<(\w+)\s?[^/]*?\/?>)/gi.exec(tag) - return result && (result[1] || result[2]) - } - - const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer - if (textBuffer.trim().length > 0) { - buffer += processor(textBuffer) - } else { - buffer += textBuffer - } - textBuffer = '' - } - - const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing - flush() - buffer += tag - } - - const handleOpen = (tag) => { // handles opening tags - flush() - buffer += tag - level.push(tag) - } - - const handleClose = (tag) => { // handles closing tags - flush() - buffer += tag - if (level[level.length - 1] === tag) { - level.pop() - } - } - - for (let i = 0; i < html.length; i++) { - const char = html[i] - if (char === '<' && tagBuffer === null) { - tagBuffer = char - } else if (char !== '>' && tagBuffer !== null) { - tagBuffer += char - } else if (char === '>' && tagBuffer !== null) { - tagBuffer += char - const tagFull = tagBuffer - tagBuffer = null - const tagName = getTagName(tagFull) - if (handledTags.has(tagName)) { - if (tagName === 'br') { - handleBr(tagFull) - } else if (openCloseTags.has(tagName)) { - if (tagFull[1] === '/') { - handleClose(tagFull) - } else if (tagFull[tagFull.length - 2] === '/') { - // self-closing - handleBr(tagFull) - } else { - handleOpen(tagFull) - } - } - } else { - textBuffer += tagFull - } - } else if (char === '\n') { - handleBr(char) - } else { - textBuffer += char - } - } - if (tagBuffer) { - textBuffer += tagBuffer - } - - flush() - - return buffer -} diff --git a/src/services/user_highlighter/user_highlighter.js b/src/services/user_highlighter/user_highlighter.js @@ -8,6 +8,11 @@ const highlightStyle = (prefs) => { const solidColor = `rgb(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)})` const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .1)` const tintColor2 = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .2)` + const customProps = { + '--____highlight-solidColor': solidColor, + '--____highlight-tintColor': tintColor, + '--____highlight-tintColor2': tintColor2 + } if (type === 'striped') { return { backgroundImage: [ @@ -17,11 +22,13 @@ const highlightStyle = (prefs) => { `${tintColor2} 20px,`, `${tintColor2} 40px` ].join(' '), - backgroundPosition: '0 0' + backgroundPosition: '0 0', + ...customProps } } else if (type === 'solid') { return { - backgroundColor: tintColor2 + backgroundColor: tintColor2, + ...customProps } } else if (type === 'side') { return { @@ -31,7 +38,8 @@ const highlightStyle = (prefs) => { `${solidColor} 2px,`, `transparent 6px` ].join(' '), - backgroundPosition: '0 0' + backgroundPosition: '0 0', + ...customProps } } } diff --git a/static/config.json b/static/config.json @@ -2,7 +2,6 @@ "alwaysShowSubjectInput": true, "background": "/static/aurora_borealis.jpg", "collapseMessageWithSubject": false, - "disableChat": false, "greentext": false, "hideFilteredStatuses": false, "hideMutedPosts": false, diff --git a/test/unit/specs/components/rich_content.spec.js b/test/unit/specs/components/rich_content.spec.js @@ -0,0 +1,557 @@ +import { mount, shallowMount, createLocalVue } from '@vue/test-utils' +import RichContent from 'src/components/rich_content/rich_content.jsx' + +const localVue = createLocalVue() +const attentions = [] + +const makeMention = (who) => { + attentions.push({ statusnet_profile_url: `https://fake.tld/@${who}` }) + return `<span class="h-card"><a class="u-url mention" href="https://fake.tld/@${who}">@<span>${who}</span></a></span>` +} +const p = (...data) => `<p>${data.join('')}</p>` +const compwrap = (...data) => `<span class="RichContent">${data.join('')}</span>` +const mentionsLine = (times) => [ + '<mentionsline-stub mentions="', + new Array(times).fill('[object Object]').join(','), + '"></mentionsline-stub>' +].join('') + +describe('RichContent', () => { + it('renders simple post without exploding', () => { + const html = p('Hello world!') + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(html)) + }) + + it('unescapes everything as needed', () => { + const html = [ + p('Testing &#39;em all'), + 'Testing &#39;em all' + ].join('') + const expected = [ + p('Testing \'em all'), + 'Testing \'em all' + ].join('') + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('replaces mention with mentionsline', () => { + const html = p( + makeMention('John'), + ' how are you doing today?' + ) + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(p( + mentionsLine(1), + ' how are you doing today?' + ))) + }) + + it('replaces mentions at the end of the hellpost', () => { + const html = [ + p('How are you doing today, fine gentlemen?'), + p( + makeMention('John'), + makeMention('Josh'), + makeMention('Jeremy') + ) + ].join('') + const expected = [ + p( + 'How are you doing today, fine gentlemen?' + ), + // TODO fix this extra line somehow? + p( + '<mentionsline-stub mentions="', + '[object Object],', + '[object Object],', + '[object Object]', + '"></mentionsline-stub>' + ) + ].join('') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('Does not touch links if link handling is disabled', () => { + const html = [ + [ + makeMention('Jack'), + 'let\'s meet up with ', + makeMention('Janet') + ].join(''), + [ + makeMention('John'), + makeMention('Josh'), + makeMention('Jeremy') + ].join('') + ].join('\n') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: false, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(html)) + }) + + it('Adds greentext and cyantext to the post', () => { + const html = [ + '&gt;preordering videogames', + '&gt;any year' + ].join('\n') + const expected = [ + '<span class="greentext">&gt;preordering videogames</span>', + '<span class="greentext">&gt;any year</span>' + ].join('\n') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: false, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('Does not add greentext and cyantext if setting is set to false', () => { + const html = [ + '&gt;preordering videogames', + '&gt;any year' + ].join('\n') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: false, + greentext: false, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(html)) + }) + + it('Adds emoji to post', () => { + const html = p('Ebin :DDDD :spurdo:') + const expected = p( + 'Ebin :DDDD ', + '<anonymous-stub alt=":spurdo:" src="about:blank" title=":spurdo:" class="emoji img"></anonymous-stub>' + ) + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: false, + greentext: false, + emoji: [{ url: 'about:blank', shortcode: 'spurdo' }], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('Doesn\'t add nonexistent emoji to post', () => { + const html = p('Lol :lol:') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: false, + greentext: false, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(html)) + }) + + it('Greentext + last mentions', () => { + const html = [ + '&gt;quote', + makeMention('lol'), + '&gt;quote', + '&gt;quote' + ].join('\n') + const expected = [ + '<span class="greentext">&gt;quote</span>', + mentionsLine(1), + '<span class="greentext">&gt;quote</span>', + '<span class="greentext">&gt;quote</span>' + ].join('\n') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('One buggy example', () => { + const html = [ + 'Bruh', + 'Bruh', + [ + makeMention('foo'), + makeMention('bar'), + makeMention('baz') + ].join(''), + 'Bruh' + ].join('<br>') + const expected = [ + 'Bruh', + 'Bruh', + mentionsLine(3), + 'Bruh' + ].join('<br>') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('buggy example/hashtags', () => { + const html = [ + '<p>', + '<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg">', + 'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>', + ' <a class="hashtag" data-tag="nou" href="https://shitposter.club/tag/nou">', + '#nou</a>', + ' <a class="hashtag" data-tag="screencap" href="https://shitposter.club/tag/screencap">', + '#screencap</a>', + ' </p>' + ].join('') + const expected = [ + '<p>', + '<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg" target="_blank">', + 'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>', + ' <hashtaglink-stub url="https://shitposter.club/tag/nou" content="#nou" tag="nou">', + '</hashtaglink-stub>', + ' <hashtaglink-stub url="https://shitposter.club/tag/screencap" content="#screencap" tag="screencap">', + '</hashtaglink-stub>', + ' </p>' + ].join('') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('rich contents of a mention are handled properly', () => { + attentions.push({ statusnet_profile_url: 'lol' }) + const html = [ + p( + '<a href="lol" class="mention">', + '<span>', + 'https://</span>', + '<span>', + 'lol.tld/</span>', + '<span>', + '</span>', + '</a>' + ), + p( + 'Testing' + ) + ].join('') + const expected = [ + p( + '<span class="MentionsLine">', + '<span class="MentionLink mention-link">', + '<a href="lol" target="_blank" class="original">', + '<span>', + 'https://</span>', + '<span>', + 'lol.tld/</span>', + '<span>', + '</span>', + '</a>', + '<!---->', // v-if placeholder, mentionlink's "new" (i.e. rich) display + '</span>', + '<!---->', // v-if placeholder, mentionsline's extra mentions and stuff + '</span>' + ), + p( + 'Testing' + ) + ].join('') + + const wrapper = mount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('rich contents of nested mentions are handled properly', () => { + attentions.push({ statusnet_profile_url: 'lol' }) + const html = [ + p( + '<span class="poast-style">', + '<a href="lol" class="mention">', + '<span>', + 'https://</span>', + '<span>', + 'lol.tld/</span>', + '<span>', + '</span>', + '</a>', + ' ', + '<a href="lol" class="mention">', + '<span>', + 'https://</span>', + '<span>', + 'lol.tld/</span>', + '<span>', + '</span>', + '</a>', + '</span>' + ), + p( + 'Testing' + ) + ].join('') + const expected = [ + p( + '<span class="poast-style">', + '<span class="MentionsLine">', + '<span class="MentionLink mention-link">', + '<a href="lol" target="_blank" class="original">', + '<span>', + 'https://</span>', + '<span>', + 'lol.tld/</span>', + '<span>', + '</span>', + '</a>', + '<!---->', // v-if placeholder, mentionlink's "new" (i.e. rich) display + '</span>', + '<span class="MentionLink mention-link">', + '<a href="lol" target="_blank" class="original">', + '<span>', + 'https://</span>', + '<span>', + 'lol.tld/</span>', + '<span>', + '</span>', + '</a>', + '<!---->', // v-if placeholder, mentionlink's "new" (i.e. rich) display + '</span>', + '<!---->', // v-if placeholder, mentionsline's extra mentions and stuff + '</span>', + '</span>' + ), + ' ', + p( + 'Testing' + ) + ].join('') + + const wrapper = mount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('rich contents of a link are handled properly', () => { + const html = [ + '<p>', + 'Freenode is dead.</p>', + '<p>', + '<a href="https://isfreenodedeadyet.com/">', + '<span>', + 'https://</span>', + '<span>', + 'isfreenodedeadyet.com/</span>', + '<span>', + '</span>', + '</a>', + '</p>' + ].join('') + const expected = [ + '<p>', + 'Freenode is dead.</p>', + '<p>', + '<a href="https://isfreenodedeadyet.com/" target="_blank">', + '<span>', + 'https://</span>', + '<span>', + 'isfreenodedeadyet.com/</span>', + '<span>', + '</span>', + '</a>', + '</p>' + ].join('') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it.skip('[INFORMATIVE] Performance testing, 10 000 simple posts', () => { + const amount = 20 + + const onePost = p( + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + ' i just landed in l a where are you' + ) + + const TestComponent = { + template: ` + <div v-if="!vhtml"> + ${new Array(amount).fill(`<RichContent html="${onePost}" :greentext="true" :handleLinks="handeLinks" :emoji="[]" :attentions="attentions"/>`)} + </div> + <div v-else="vhtml"> + ${new Array(amount).fill(`<div v-html="${onePost}"/>`)} + </div> + `, + props: ['handleLinks', 'attentions', 'vhtml'] + } + console.log(1) + + const ptest = (handleLinks, vhtml) => { + const t0 = performance.now() + + const wrapper = mount(TestComponent, { + localVue, + propsData: { + attentions, + handleLinks, + vhtml + } + }) + + const t1 = performance.now() + + wrapper.destroy() + + const t2 = performance.now() + + return `Mount: ${t1 - t0}ms, destroy: ${t2 - t1}ms, avg ${(t1 - t0) / amount}ms - ${(t2 - t1) / amount}ms per item` + } + + console.log(`${amount} items with links handling:`) + console.log(ptest(true)) + console.log(`${amount} items without links handling:`) + console.log(ptest(false)) + console.log(`${amount} items plain v-html:`) + console.log(ptest(false, true)) + }) +}) diff --git a/test/unit/specs/components/timeline.spec.js b/test/unit/specs/components/timeline.spec.js @@ -1,27 +0,0 @@ -import { getExcludedStatusIdsByPinning } from 'src/components/timeline/timeline.js' - -describe('Timeline', () => { - describe('getExcludedStatusIdsByPinning', () => { - const mockStatuses = (ids) => ids.map(id => ({ id })) - - it('should return only members of both pinnedStatusIds and ids of the given statuses', () => { - const statusIds = [1, 2, 3, 4] - const statuses = mockStatuses(statusIds) - const pinnedStatusIds = [1, 3, 5] - const result = getExcludedStatusIdsByPinning(statuses, pinnedStatusIds) - result.forEach(item => { - expect(item).to.be.oneOf(statusIds) - expect(item).to.be.oneOf(pinnedStatusIds) - }) - }) - - it('should return ids of pinned statuses not posted before any unpinned status', () => { - const pinnedStatusIdSet1 = ['PINNED1', 'PINNED2'] - const pinnedStatusIdSet2 = ['PINNED3', 'PINNED4'] - const pinnedStatusIds = [...pinnedStatusIdSet1, ...pinnedStatusIdSet2] - const statusIds = [...pinnedStatusIdSet1, 'UNPINNED1', ...pinnedStatusIdSet2] - const statuses = mockStatuses(statusIds) - expect(getExcludedStatusIdsByPinning(statuses, pinnedStatusIds)).to.eql(pinnedStatusIdSet1) - }) - }) -}) diff --git a/test/unit/specs/components/user_profile.spec.js b/test/unit/specs/components/user_profile.spec.js @@ -31,13 +31,15 @@ const testGetters = { const localUser = { id: 100, is_local: true, - screen_name: 'testUser' + screen_name: 'testUser', + screen_name_ui: 'testUser' } const extUser = { id: 100, is_local: false, - screen_name: 'testUser@test.instance' + screen_name: 'testUser@test.instance', + screen_name_ui: 'testUser@test.instance' } const externalProfileStore = new Vuex.Store({ diff --git a/test/unit/specs/services/chat_service/chat_service.spec.js b/test/unit/specs/services/chat_service/chat_service.spec.js @@ -88,4 +88,21 @@ describe('chatService', () => { expect(view.map(i => i.type)).to.eql(['date', 'message', 'message', 'date', 'message']) }) }) + + describe('.cullOlderMessages', () => { + it('keeps 50 newest messages and idIndex matches', () => { + const chat = chatService.empty() + + for (let i = 100; i > 0; i--) { + // Use decimal values with toFixed to hack together constant length predictable strings + chatService.add(chat, { messages: [{ ...message1, id: 'a' + (i / 1000).toFixed(3), idempotency_key: i }] }) + } + chatService.cullOlderMessages(chat) + expect(chat.messages.length).to.eql(50) + expect(chat.messages[0].id).to.eql('a0.051') + expect(chat.minId).to.eql('a0.051') + expect(chat.messages[49].id).to.eql('a0.100') + expect(Object.keys(chat.idIndex).length).to.eql(50) + }) + }) }) 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, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' +import { parseStatus, parseUser, parseNotification, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' import mastoapidata from '../../../../fixtures/mastoapi.json' import qvitterapidata from '../../../../fixtures/statuses.json' @@ -23,7 +23,6 @@ const makeMockStatusQvitter = (overrides = {}) => { repeat_num: 0, repeated: false, statusnet_conversation_id: '16300488', - statusnet_html: '<p>haha benis</p>', summary: null, tags: [], text: 'haha benis', @@ -232,22 +231,6 @@ describe('API Entities normalizer', () => { expect(parsedRepeat).to.have.property('retweeted_status') expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef') }) - - it('adds emojis to post content', () => { - const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), content: 'Makes you think :thinking:' }) - - const parsedPost = parseStatus(post) - - expect(parsedPost).to.have.property('statusnet_html').that.contains('<img') - }) - - it('adds emojis to subject line', () => { - const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), spoiler_text: 'CW: 300 IQ :thinking:' }) - - const parsedPost = parseStatus(post) - - expect(parsedPost).to.have.property('summary_html').that.contains('<img') - }) }) }) @@ -261,35 +244,6 @@ describe('API Entities normalizer', () => { expect(parseUser(remote)).to.have.property('is_local', false) }) - it('adds emojis to user name', () => { - const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), display_name: 'The :thinking: thinker' }) - - const parsedUser = parseUser(user) - - expect(parsedUser).to.have.property('name_html').that.contains('<img') - }) - - it('adds emojis to user bio', () => { - const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), note: 'Hello i like to :thinking: a lot' }) - - const parsedUser = parseUser(user) - - expect(parsedUser).to.have.property('description_html').that.contains('<img') - }) - - it('adds emojis to user profile fields', () => { - const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: ':thinking:', value: ':image:' }] }) - - const parsedUser = parseUser(user) - - expect(parsedUser).to.have.property('fields_html').to.be.an('array') - - const field = parsedUser.fields_html[0] - - expect(field).to.have.property('name').that.contains('<img') - expect(field).to.have.property('value').that.contains('<img') - }) - it('removes html tags from user profile fields', () => { const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: 'user', value: '<a rel="me" href="https://example.com/@user">@user</a>' }] }) @@ -315,7 +269,7 @@ describe('API Entities normalizer', () => { it('converts IDN to unicode and marks it as internatonal', () => { const user = makeMockUserMasto({ acct: 'lain@xn--lin-6cd.com' }) - expect(parseUser(user)).to.have.property('screen_name').that.equal('lain@🌏lаin.com') + expect(parseUser(user)).to.have.property('screen_name_ui').that.equal('lain@🌏lаin.com') }) }) @@ -355,41 +309,6 @@ describe('API Entities normalizer', () => { }) }) - describe('MastoAPI emoji adder', () => { - const emojis = makeMockEmojiMasto() - const imageHtml = '<img src="https://example.com/image.png" alt=":image:" title=":image:" class="emoji" />' - .replace(/"/g, '\'') - const thinkHtml = '<img src="https://example.com/think.png" alt=":thinking:" title=":thinking:" class="emoji" />' - .replace(/"/g, '\'') - - it('correctly replaces shortcodes in supplied string', () => { - const result = addEmojis('This post has :image: emoji and :thinking: emoji', emojis) - expect(result).to.include(thinkHtml) - expect(result).to.include(imageHtml) - }) - - it('handles consecutive emojis correctly', () => { - const result = addEmojis('Lelel emoji spam :thinking::thinking::thinking::thinking:', emojis) - expect(result).to.include(thinkHtml + thinkHtml + thinkHtml + thinkHtml) - }) - - it('Doesn\'t replace nonexistent emojis', () => { - const result = addEmojis('Admin add the :tenshi: emoji', emojis) - expect(result).to.equal('Admin add the :tenshi: emoji') - }) - - it('Doesn\'t blow up on regex special characters', () => { - const emojis = makeMockEmojiMasto([{ - shortcode: 'c++' - }, { - shortcode: '[a-z] {|}*' - }]) - const result = addEmojis('This post has :c++: emoji and :[a-z] {|}*: emoji', emojis) - expect(result).to.include('title=\':c++:\'') - expect(result).to.include('title=\':[a-z] {|}*:\'') - }) - }) - describe('Link header pagination', () => { 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"' diff --git a/test/unit/specs/services/html_converter/html_line_converter.spec.js b/test/unit/specs/services/html_converter/html_line_converter.spec.js @@ -0,0 +1,171 @@ +import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js' + +const greentextHandle = new Set(['p', 'div']) +const mapOnlyText = (processor) => (input) => { + if (input.text && input.level.every(l => greentextHandle.has(l))) { + return processor(input.text) + } else if (input.text) { + return input.text + } else { + return input + } +} + +describe('html_line_converter', () => { + describe('with processor that keeps original line should not make any changes to HTML when', () => { + const processorKeep = (line) => line + it('fed with regular HTML with newlines', () => { + const inputOutput = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + + it('fed with possibly broken HTML with invalid tags/composition', () => { + const inputOutput = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + + it('fed with very broken HTML with broken composition', () => { + const inputOutput = '</p> lmao what </div> whats going on <div> wha <p>' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + + it('fed with sorta valid HTML but tags aren\'t closed', () => { + const inputOutput = 'just leaving a <div> hanging' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + + it('fed with not really HTML at this point... tags that aren\'t finished', () => { + const inputOutput = 'do you expect me to finish this <div class=' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + + it('fed with dubiously valid HTML (p within p and also div inside p)', () => { + const inputOutput = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + + it('fed with maybe valid HTML? self-closing divs and ps', () => { + const inputOutput = 'a <div class="what"/> what now <p aria-label="wtf"/> ?' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + + it('fed with valid XHTML containing a CDATA', () => { + const inputOutput = 'Yes, it is me, <![CDATA[DIO]]>' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + + it('fed with some recognized but not handled elements', () => { + const inputOutput = 'testing images\n\n<img src="benis.png">' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + }) + describe('with processor that replaces lines with word "_" should match expected line when', () => { + const processorReplace = (line) => '_' + it('fed with regular HTML with newlines', () => { + const input = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>' + const output = '_<br/>_<p class="lol">_</p>_\n_<p >_<br>_</p> <br>\n<br/>' + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + + it('fed with possibly broken HTML with invalid tags/composition', () => { + const input = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>' + const output = '_' + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + + it('fed with very broken HTML with broken composition', () => { + const input = '</p> lmao what </div> whats going on <div> wha <p>' + const output = '_<div>_<p>' + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + + it('fed with sorta valid HTML but tags aren\'t closed', () => { + const input = 'just leaving a <div> hanging' + const output = '_<div>_' + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + + it('fed with not really HTML at this point... tags that aren\'t finished', () => { + const input = 'do you expect me to finish this <div class=' + const output = '_' + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + + it('fed with dubiously valid HTML (p within p and also div inside p)', () => { + const input = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>' + const output = '_<p>_\n_<p>_</p>_<br/><div>_</div></p>' + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + + it('fed with maybe valid HTML? (XHTML) self-closing divs and ps', () => { + const input = 'a <div class="what"/> what now <p aria-label="wtf"/> ?' + const output = '_<div class="what"/>_<p aria-label="wtf"/>_' + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + + it('fed with valid XHTML containing a CDATA', () => { + const input = 'Yes, it is me, <![CDATA[DIO]]>' + const output = '_' + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + + it('Testing handling ignored blocks', () => { + const input = ` + <pre><code>&gt; rei = &quot;0&quot; + &#39;0&#39; + &gt; rei == 0 + true + &gt; rei == null + false</code></pre><blockquote>That, christian-like JS diagram but it’s evangelion instead.</blockquote> + ` + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(input) + }) + it('Testing handling ignored blocks 2', () => { + const input = ` + <blockquote>An SSL error has happened.</blockquote><p>Shakespeare</p> + ` + const output = ` + <blockquote>An SSL error has happened.</blockquote><p>_</p> + ` + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + }) +}) diff --git a/test/unit/specs/services/html_converter/html_tree_converter.spec.js b/test/unit/specs/services/html_converter/html_tree_converter.spec.js @@ -0,0 +1,132 @@ +import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js' + +describe('html_tree_converter', () => { + describe('convertHtmlToTree', () => { + it('converts html into a tree structure', () => { + const input = '1 <p>2</p> <b>3<img src="a">4</b>5' + expect(convertHtmlToTree(input)).to.eql([ + '1 ', + [ + '<p>', + ['2'], + '</p>' + ], + ' ', + [ + '<b>', + [ + '3', + ['<img src="a">'], + '4' + ], + '</b>' + ], + '5' + ]) + }) + it('converts html to tree while preserving tag formatting', () => { + const input = '1 <p >2</p><b >3<img src="a">4</b>5' + expect(convertHtmlToTree(input)).to.eql([ + '1 ', + [ + '<p >', + ['2'], + '</p>' + ], + [ + '<b >', + [ + '3', + ['<img src="a">'], + '4' + ], + '</b>' + ], + '5' + ]) + }) + it('converts semi-broken html', () => { + const input = '1 <br> 2 <p> 42' + expect(convertHtmlToTree(input)).to.eql([ + '1 ', + ['<br>'], + ' 2 ', + [ + '<p>', + [' 42'] + ] + ]) + }) + it('realistic case 1', () => { + const input = '<p><span class="h-card"><a class="u-url mention" data-user="9wRC6T2ZZiKWJ0vUi8" href="https://cawfee.club/users/benis" rel="ugc">@<span>benis</span></a></span> <span class="h-card"><a class="u-url mention" data-user="194" href="https://shigusegubu.club/users/hj" rel="ugc">@<span>hj</span></a></span> nice</p>' + expect(convertHtmlToTree(input)).to.eql([ + [ + '<p>', + [ + [ + '<span class="h-card">', + [ + [ + '<a class="u-url mention" data-user="9wRC6T2ZZiKWJ0vUi8" href="https://cawfee.club/users/benis" rel="ugc">', + [ + '@', + [ + '<span>', + [ + 'benis' + ], + '</span>' + ] + ], + '</a>' + ] + ], + '</span>' + ], + ' ', + [ + '<span class="h-card">', + [ + [ + '<a class="u-url mention" data-user="194" href="https://shigusegubu.club/users/hj" rel="ugc">', + [ + '@', + [ + '<span>', + [ + 'hj' + ], + '</span>' + ] + ], + '</a>' + ] + ], + '</span>' + ], + ' nice' + ], + '</p>' + ] + ]) + }) + it('realistic case 2', () => { + const inputOutput = 'Country improv: give me a city<br/>Audience: Memphis<br/>Improv troupe: come on, a better one<br/>Audience: el paso' + expect(convertHtmlToTree(inputOutput)).to.eql([ + 'Country improv: give me a city', + [ + '<br/>' + ], + 'Audience: Memphis', + [ + '<br/>' + ], + 'Improv troupe: come on, a better one', + [ + '<br/>' + ], + 'Audience: el paso' + ]) + }) + }) +}) diff --git a/test/unit/specs/services/html_converter/utility.spec.js b/test/unit/specs/services/html_converter/utility.spec.js @@ -0,0 +1,37 @@ +import { processTextForEmoji, getAttrs } from 'src/services/html_converter/utility.service.js' + +describe('html_converter utility', () => { + describe('processTextForEmoji', () => { + it('processes all emoji in text', () => { + const input = 'Hello from finland! :lol: We have best water! :lmao:' + const emojis = [ + { shortcode: 'lol', src: 'LOL' }, + { shortcode: 'lmao', src: 'LMAO' } + ] + const processor = ({ shortcode, src }) => ({ shortcode, src }) + expect(processTextForEmoji(input, emojis, processor)).to.eql([ + 'Hello from finland! ', + { shortcode: 'lol', src: 'LOL' }, + ' We have best water! ', + { shortcode: 'lmao', src: 'LMAO' } + ]) + }) + it('leaves text as is', () => { + const input = 'Number one: that\'s terror' + const emojis = [] + const processor = ({ shortcode, src }) => ({ shortcode, src }) + expect(processTextForEmoji(input, emojis, processor)).to.eql([ + 'Number one: that\'s terror' + ]) + }) + }) + + describe('getAttrs', () => { + it('extracts arguments from tag', () => { + const input = '<img src="boop" cool ebin=\'true\'>' + const output = { src: 'boop', cool: true, ebin: 'true' } + + expect(getAttrs(input)).to.eql(output) + }) + }) +}) diff --git a/test/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js b/test/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js @@ -1,96 +0,0 @@ -import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js' - -describe('TinyPostHTMLProcessor', () => { - describe('with processor that keeps original line should not make any changes to HTML when', () => { - const processorKeep = (line) => line - it('fed with regular HTML with newlines', () => { - const inputOutput = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>' - expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) - }) - - it('fed with possibly broken HTML with invalid tags/composition', () => { - const inputOutput = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>' - expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) - }) - - it('fed with very broken HTML with broken composition', () => { - const inputOutput = '</p> lmao what </div> whats going on <div> wha <p>' - expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) - }) - - it('fed with sorta valid HTML but tags aren\'t closed', () => { - const inputOutput = 'just leaving a <div> hanging' - expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) - }) - - it('fed with not really HTML at this point... tags that aren\'t finished', () => { - const inputOutput = 'do you expect me to finish this <div class=' - expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) - }) - - it('fed with dubiously valid HTML (p within p and also div inside p)', () => { - const inputOutput = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>' - expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) - }) - - it('fed with maybe valid HTML? self-closing divs and ps', () => { - const inputOutput = 'a <div class="what"/> what now <p aria-label="wtf"/> ?' - expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) - }) - - it('fed with valid XHTML containing a CDATA', () => { - const inputOutput = 'Yes, it is me, <![CDATA[DIO]]>' - expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) - }) - }) - describe('with processor that replaces lines with word "_" should match expected line when', () => { - const processorReplace = (line) => '_' - it('fed with regular HTML with newlines', () => { - const input = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>' - const output = '_<br/>_<p class="lol">_</p>_\n_<p >_<br>_</p> <br>\n<br/>' - expect(processHtml(input, processorReplace)).to.eql(output) - }) - - it('fed with possibly broken HTML with invalid tags/composition', () => { - const input = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>' - const output = '_' - expect(processHtml(input, processorReplace)).to.eql(output) - }) - - it('fed with very broken HTML with broken composition', () => { - const input = '</p> lmao what </div> whats going on <div> wha <p>' - const output = '</p>_</div>_<div>_<p>' - expect(processHtml(input, processorReplace)).to.eql(output) - }) - - it('fed with sorta valid HTML but tags aren\'t closed', () => { - const input = 'just leaving a <div> hanging' - const output = '_<div>_' - expect(processHtml(input, processorReplace)).to.eql(output) - }) - - it('fed with not really HTML at this point... tags that aren\'t finished', () => { - const input = 'do you expect me to finish this <div class=' - const output = '_' - expect(processHtml(input, processorReplace)).to.eql(output) - }) - - it('fed with dubiously valid HTML (p within p and also div inside p)', () => { - const input = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>' - const output = '_<p>_\n_<p>_</p>_<br/><div>_</div></p>' - expect(processHtml(input, processorReplace)).to.eql(output) - }) - - it('fed with maybe valid HTML? self-closing divs and ps', () => { - const input = 'a <div class="what"/> what now <p aria-label="wtf"/> ?' - const output = '_<div class="what"/>_<p aria-label="wtf"/>_' - expect(processHtml(input, processorReplace)).to.eql(output) - }) - - it('fed with valid XHTML containing a CDATA', () => { - const input = 'Yes, it is me, <![CDATA[DIO]]>' - const output = '_' - expect(processHtml(input, processorReplace)).to.eql(output) - }) - }) -}) diff --git a/yarn.lock b/yarn.lock @@ -936,6 +936,14 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" +"@npmcli/move-file@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + "@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" @@ -961,6 +969,11 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/json-schema@^7.0.6": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" + integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== + "@types/minimist@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" @@ -998,156 +1011,236 @@ resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.1.0.tgz#88d527d40de86c4b0c99a060ca241d755999915b" integrity sha512-W2oyj0Fe1w/XhPZjkI3oUcDUAmu5P4qsdT2/2S8aMhtAWM/CE/jYWtji0pKNPDfxLI75fa5gWSEmnynKMNP/oA== -"@vue/babel-helper-vue-jsx-merge-props@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz#048fe579958da408fb7a8b2a3ec050b50a661040" - integrity sha512-6tyf5Cqm4m6v7buITuwS+jHzPlIPxbFzEhXR5JGZpbrvOcp1hiQKckd305/3C7C36wFekNTQSxAtgeM0j0yoUw== +"@vue/babel-helper-vue-jsx-merge-props@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz#31624a7a505fb14da1d58023725a4c5f270e6a81" + integrity sha512-QOi5OW45e2R20VygMSNhyQHvpdUwQZqGPc748JLGCYEy+yp8fNFNdbNIGAgZmi9e+2JHPd6i6idRuqivyicIkA== -"@vue/babel-plugin-transform-vue-jsx@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.1.2.tgz#c0a3e6efc022e75e4247b448a8fc6b86f03e91c0" - integrity sha512-YfdaoSMvD1nj7+DsrwfTvTnhDXI7bsuh+Y5qWwvQXlD24uLgnsoww3qbiZvWf/EoviZMrvqkqN4CBw0W3BWUTQ== +"@vue/babel-plugin-transform-vue-jsx@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.2.1.tgz#646046c652c2f0242727f34519d917b064041ed7" + integrity sha512-HJuqwACYehQwh1fNT8f4kyzqlNMpBuUK4rSiSES5D4QsYncv5fxFsLyrxFPG2ksO7t5WP+Vgix6tt6yKClwPzA== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/plugin-syntax-jsx" "^7.2.0" - "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0" + "@vue/babel-helper-vue-jsx-merge-props" "^1.2.1" html-tags "^2.0.0" lodash.kebabcase "^4.1.1" svg-tags "^1.0.0" -"@vue/test-utils@^1.0.0-beta.26": - version "1.0.0-beta.28" - resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.0.0-beta.28.tgz#767c43413df8cde86128735e58923803e444b9a5" - dependencies: - dom-event-types "^1.0.0" - lodash "^4.17.4" - -"@webassemblyjs/ast@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" +"@vue/babel-preset-jsx@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@vue/babel-preset-jsx/-/babel-preset-jsx-1.2.4.tgz#92fea79db6f13b01e80d3a0099e2924bdcbe4e87" + integrity sha512-oRVnmN2a77bYDJzeGSt92AuHXbkIxbf/XXSE3klINnh9AXBmVS1DGa1f0d+dDYpLfsAKElMnqKTQfKn7obcL4w== + dependencies: + "@vue/babel-helper-vue-jsx-merge-props" "^1.2.1" + "@vue/babel-plugin-transform-vue-jsx" "^1.2.1" + "@vue/babel-sugar-composition-api-inject-h" "^1.2.1" + "@vue/babel-sugar-composition-api-render-instance" "^1.2.4" + "@vue/babel-sugar-functional-vue" "^1.2.2" + "@vue/babel-sugar-inject-h" "^1.2.2" + "@vue/babel-sugar-v-model" "^1.2.3" + "@vue/babel-sugar-v-on" "^1.2.3" + +"@vue/babel-sugar-composition-api-inject-h@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-composition-api-inject-h/-/babel-sugar-composition-api-inject-h-1.2.1.tgz#05d6e0c432710e37582b2be9a6049b689b6f03eb" + integrity sha512-4B3L5Z2G+7s+9Bwbf+zPIifkFNcKth7fQwekVbnOA3cr3Pq71q71goWr97sk4/yyzH8phfe5ODVzEjX7HU7ItQ== dependencies: - "@webassemblyjs/helper-module-context" "1.8.5" - "@webassemblyjs/helper-wasm-bytecode" "1.8.5" - "@webassemblyjs/wast-parser" "1.8.5" - -"@webassemblyjs/floating-point-hex-parser@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz#1ba926a2923613edce496fd5b02e8ce8a5f49721" - -"@webassemblyjs/helper-api-error@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz#c49dad22f645227c5edb610bdb9697f1aab721f7" - -"@webassemblyjs/helper-buffer@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz#fea93e429863dd5e4338555f42292385a653f204" + "@babel/plugin-syntax-jsx" "^7.2.0" -"@webassemblyjs/helper-code-frame@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz#9a740ff48e3faa3022b1dff54423df9aa293c25e" +"@vue/babel-sugar-composition-api-render-instance@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-composition-api-render-instance/-/babel-sugar-composition-api-render-instance-1.2.4.tgz#e4cbc6997c344fac271785ad7a29325c51d68d19" + integrity sha512-joha4PZznQMsxQYXtR3MnTgCASC9u3zt9KfBxIeuI5g2gscpTsSKRDzWQt4aqNIpx6cv8On7/m6zmmovlNsG7Q== dependencies: - "@webassemblyjs/wast-printer" "1.8.5" - -"@webassemblyjs/helper-fsm@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz#ba0b7d3b3f7e4733da6059c9332275d860702452" + "@babel/plugin-syntax-jsx" "^7.2.0" -"@webassemblyjs/helper-module-context@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz#def4b9927b0101dc8cbbd8d1edb5b7b9c82eb245" +"@vue/babel-sugar-functional-vue@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.2.2.tgz#267a9ac8d787c96edbf03ce3f392c49da9bd2658" + integrity sha512-JvbgGn1bjCLByIAU1VOoepHQ1vFsroSA/QkzdiSs657V79q6OwEWLCQtQnEXD/rLTA8rRit4rMOhFpbjRFm82w== dependencies: - "@webassemblyjs/ast" "1.8.5" - mamacro "^0.0.3" - -"@webassemblyjs/helper-wasm-bytecode@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz#537a750eddf5c1e932f3744206551c91c1b93e61" + "@babel/plugin-syntax-jsx" "^7.2.0" -"@webassemblyjs/helper-wasm-section@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz#74ca6a6bcbe19e50a3b6b462847e69503e6bfcbf" +"@vue/babel-sugar-inject-h@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.2.2.tgz#d738d3c893367ec8491dcbb669b000919293e3aa" + integrity sha512-y8vTo00oRkzQTgufeotjCLPAvlhnpSkcHFEp60+LJUwygGcd5Chrpn5480AQp/thrxVm8m2ifAk0LyFel9oCnw== dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-buffer" "1.8.5" - "@webassemblyjs/helper-wasm-bytecode" "1.8.5" - "@webassemblyjs/wasm-gen" "1.8.5" + "@babel/plugin-syntax-jsx" "^7.2.0" -"@webassemblyjs/ieee754@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz#712329dbef240f36bf57bd2f7b8fb9bf4154421e" +"@vue/babel-sugar-v-model@^1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.2.3.tgz#fa1f29ba51ebf0aa1a6c35fa66d539bc459a18f2" + integrity sha512-A2jxx87mySr/ulAsSSyYE8un6SIH0NWHiLaCWpodPCVOlQVODCaSpiR4+IMsmBr73haG+oeCuSvMOM+ttWUqRQ== dependencies: - "@xtuc/ieee754" "^1.2.0" + "@babel/plugin-syntax-jsx" "^7.2.0" + "@vue/babel-helper-vue-jsx-merge-props" "^1.2.1" + "@vue/babel-plugin-transform-vue-jsx" "^1.2.1" + camelcase "^5.0.0" + html-tags "^2.0.0" + svg-tags "^1.0.0" -"@webassemblyjs/leb128@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.8.5.tgz#044edeb34ea679f3e04cd4fd9824d5e35767ae10" +"@vue/babel-sugar-v-on@^1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.2.3.tgz#342367178586a69f392f04bfba32021d02913ada" + integrity sha512-kt12VJdz/37D3N3eglBywV8GStKNUhNrsxChXIV+o0MwVXORYuhDTHJRKPgLJRb/EY3vM2aRFQdxJBp9CLikjw== dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.8.5.tgz#a8bf3b5d8ffe986c7c1e373ccbdc2a0915f0cedc" + "@babel/plugin-syntax-jsx" "^7.2.0" + "@vue/babel-plugin-transform-vue-jsx" "^1.2.1" + camelcase "^5.0.0" -"@webassemblyjs/wasm-edit@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz#962da12aa5acc1c131c81c4232991c82ce56e01a" - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-buffer" "1.8.5" - "@webassemblyjs/helper-wasm-bytecode" "1.8.5" - "@webassemblyjs/helper-wasm-section" "1.8.5" - "@webassemblyjs/wasm-gen" "1.8.5" - "@webassemblyjs/wasm-opt" "1.8.5" - "@webassemblyjs/wasm-parser" "1.8.5" - "@webassemblyjs/wast-printer" "1.8.5" - -"@webassemblyjs/wasm-gen@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz#54840766c2c1002eb64ed1abe720aded714f98bc" +"@vue/test-utils@^1.0.0-beta.26": + version "1.0.0-beta.28" + resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.0.0-beta.28.tgz#767c43413df8cde86128735e58923803e444b9a5" dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-wasm-bytecode" "1.8.5" - "@webassemblyjs/ieee754" "1.8.5" - "@webassemblyjs/leb128" "1.8.5" - "@webassemblyjs/utf8" "1.8.5" + dom-event-types "^1.0.0" + lodash "^4.17.4" -"@webassemblyjs/wasm-opt@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz#b24d9f6ba50394af1349f510afa8ffcb8a63d264" +"@webassemblyjs/ast@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" + integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA== + dependencies: + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/wast-parser" "1.9.0" + +"@webassemblyjs/floating-point-hex-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" + integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA== + +"@webassemblyjs/helper-api-error@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" + integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw== + +"@webassemblyjs/helper-buffer@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" + integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== + +"@webassemblyjs/helper-code-frame@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27" + integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA== + dependencies: + "@webassemblyjs/wast-printer" "1.9.0" + +"@webassemblyjs/helper-fsm@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8" + integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw== + +"@webassemblyjs/helper-module-context@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07" + integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g== + dependencies: + "@webassemblyjs/ast" "1.9.0" + +"@webassemblyjs/helper-wasm-bytecode@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" + integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== + +"@webassemblyjs/helper-wasm-section@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" + integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + +"@webassemblyjs/ieee754@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" + integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg== dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-buffer" "1.8.5" - "@webassemblyjs/wasm-gen" "1.8.5" - "@webassemblyjs/wasm-parser" "1.8.5" + "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/wasm-parser@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz#21576f0ec88b91427357b8536383668ef7c66b8d" +"@webassemblyjs/leb128@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" + integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-api-error" "1.8.5" - "@webassemblyjs/helper-wasm-bytecode" "1.8.5" - "@webassemblyjs/ieee754" "1.8.5" - "@webassemblyjs/leb128" "1.8.5" - "@webassemblyjs/utf8" "1.8.5" + "@xtuc/long" "4.2.2" -"@webassemblyjs/wast-parser@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz#e10eecd542d0e7bd394f6827c49f3df6d4eefb8c" - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/floating-point-hex-parser" "1.8.5" - "@webassemblyjs/helper-api-error" "1.8.5" - "@webassemblyjs/helper-code-frame" "1.8.5" - "@webassemblyjs/helper-fsm" "1.8.5" +"@webassemblyjs/utf8@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" + integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w== + +"@webassemblyjs/wasm-edit@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf" + integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/helper-wasm-section" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + "@webassemblyjs/wasm-opt" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + "@webassemblyjs/wast-printer" "1.9.0" + +"@webassemblyjs/wasm-gen@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" + integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/ieee754" "1.9.0" + "@webassemblyjs/leb128" "1.9.0" + "@webassemblyjs/utf8" "1.9.0" + +"@webassemblyjs/wasm-opt@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" + integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + +"@webassemblyjs/wasm-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" + integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-api-error" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/ieee754" "1.9.0" + "@webassemblyjs/leb128" "1.9.0" + "@webassemblyjs/utf8" "1.9.0" + +"@webassemblyjs/wast-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914" + integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/floating-point-hex-parser" "1.9.0" + "@webassemblyjs/helper-api-error" "1.9.0" + "@webassemblyjs/helper-code-frame" "1.9.0" + "@webassemblyjs/helper-fsm" "1.9.0" "@xtuc/long" "4.2.2" -"@webassemblyjs/wast-printer@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz#114bbc481fd10ca0e23b3560fa812748b0bae5bc" +"@webassemblyjs/wast-printer@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" + integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA== dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/wast-parser" "1.8.5" + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/wast-parser" "1.9.0" "@xtuc/long" "4.2.2" "@xtuc/ieee754@^1.2.0": @@ -1176,18 +1269,19 @@ accepts@~1.3.5: mime-types "~2.1.18" negotiator "0.6.1" -acorn-dynamic-import@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" - acorn-jsx@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e" -acorn@^6.0.2, acorn@^6.0.5, acorn@^6.0.7: +acorn@^6.0.2, acorn@^6.0.7: version "6.1.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" +acorn@^6.4.1: + version "6.4.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" + integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== + after@0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" @@ -1222,6 +1316,11 @@ ajv-keywords@^3.1.0: version "3.4.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.0.tgz#4b831e7b531415a7cc518cd404e73f6193c6349d" +ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + ajv@^6.1.0, ajv@^6.9.1: version "6.10.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" @@ -1241,6 +1340,16 @@ ajv@^6.10.2: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + 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" + alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" @@ -1307,6 +1416,14 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +anymatch@~3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -1691,6 +1808,11 @@ binary-extensions@^1.0.0: version "1.12.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14" +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + blob@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" @@ -1699,9 +1821,10 @@ bluebird@^3.1.1, bluebird@^3.3.0: version "3.5.3" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" -bluebird@^3.5.3: - version "3.5.4" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.4.tgz#d6cc661595de30d5b3af5fcedd3c0b3ef6ec5714" +bluebird@^3.5.5: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" @@ -1767,7 +1890,7 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -1909,25 +2032,50 @@ bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" -cacache@^11.3.2: - version "11.3.2" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.2.tgz#2d81e308e3d258ca38125b676b98b2ac9ce69bfa" +cacache@^12.0.2: + version "12.0.4" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" + integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ== dependencies: - bluebird "^3.5.3" + bluebird "^3.5.5" chownr "^1.1.1" figgy-pudding "^3.5.1" - glob "^7.1.3" + glob "^7.1.4" graceful-fs "^4.1.15" + infer-owner "^1.0.3" lru-cache "^5.1.1" mississippi "^3.0.0" mkdirp "^0.5.1" move-concurrently "^1.0.1" promise-inflight "^1.0.1" - rimraf "^2.6.2" + rimraf "^2.6.3" ssri "^6.0.1" unique-filename "^1.1.1" y18n "^4.0.0" +cacache@^15.0.5: + version "15.0.6" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.6.tgz#65a8c580fda15b59150fb76bf3f3a8e45d583099" + integrity sha512-g1WYDMct/jzW+JdWEyjaX2zoBkZ6ZT9VpOyp2I/VMtDsNLffNat3kqPFfi1eDRSK9/SuKGyORDHcQMcPF8sQ/w== + dependencies: + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.0.2" + unique-filename "^1.1.1" + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -2117,7 +2265,7 @@ chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" -chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.3: +chokidar@^2.0.0, chokidar@^2.0.3: version "2.1.6" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.6.tgz#b6cad653a929e244ce8a834244164d241fa954c5" dependencies: @@ -2135,19 +2283,57 @@ chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.3: optionalDependencies: fsevents "^1.2.7" +chokidar@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" + integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + +chokidar@^3.4.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" + integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.5.0" + optionalDependencies: + fsevents "~2.3.1" + chownr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + chromatism@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chromatism/-/chromatism-3.0.0.tgz#a7249d353c1e4f3577e444ac41171c4e2e624b12" -chrome-trace-event@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz#45a91bd2c20c9411f0963b5aaeb9a1b95e09cc48" - dependencies: - tslib "^1.9.0" +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== chromedriver@^87.0.1: version "87.0.4" @@ -2350,9 +2536,10 @@ commander@2.9.0: dependencies: graceful-readlink ">= 1.0.0" -commander@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== commondir@^1.0.1: version "1.0.1" @@ -2464,6 +2651,23 @@ copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" +copy-webpack-plugin@^6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-6.4.1.tgz#138cd9b436dbca0a6d071720d5414848992ec47e" + integrity sha512-MXyPCjdPVx5iiWyl40Va3JGh27bKzOTNY3NjUTrosD2q7dR/cLD0013uqJ3BpFbUjyONINjb6qI7nDIJujrMbA== + dependencies: + cacache "^15.0.5" + fast-glob "^3.2.4" + find-cache-dir "^3.3.1" + glob-parent "^5.1.1" + globby "^11.0.1" + loader-utils "^2.0.0" + normalize-path "^3.0.0" + p-limit "^3.0.2" + schema-utils "^3.0.0" + serialize-javascript "^5.0.1" + webpack-sources "^1.4.3" + core-js-compat@^3.4.7: version "3.4.8" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.4.8.tgz#f72e6a4ed76437ea710928f44615f926a81607d5" @@ -3053,6 +3257,11 @@ emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + encodeurl@~1.0.1, encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -3100,12 +3309,13 @@ engine.io@~3.2.0: engine.io-parser "~2.1.0" ws "~3.3.1" -enhanced-resolve@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" +enhanced-resolve@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec" + integrity sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg== dependencies: graceful-fs "^4.1.2" - memory-fs "^0.4.0" + memory-fs "^0.5.0" tapable "^1.0.0" ent@~2.2.0: @@ -3572,6 +3782,18 @@ fast-glob@^3.1.1: micromatch "^4.0.2" picomatch "^2.2.1" +fast-glob@^3.2.4: + version "3.2.5" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" + integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== + 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" @@ -3687,7 +3909,7 @@ find-cache-dir@^0.1.1: mkdirp "^0.5.1" pkg-dir "^1.0.0" -find-cache-dir@^2.0.0: +find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" dependencies: @@ -3695,6 +3917,15 @@ find-cache-dir@^2.0.0: make-dir "^2.0.0" pkg-dir "^3.0.0" +find-cache-dir@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" + integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -3714,7 +3945,7 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" -find-up@^4.1.0: +find-up@^4.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== @@ -3799,6 +4030,13 @@ fs-minipass@^1.2.5: dependencies: minipass "^2.2.1" +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs-write-stream-atomic@^1.0.8: version "1.0.10" resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" @@ -3819,6 +4057,11 @@ fsevents@^1.2.7: nan "^2.12.1" node-pre-gyp "^0.12.0" +fsevents@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + ftp@~0.3.10: version "0.3.10" resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d" @@ -3920,6 +4163,13 @@ glob-parent@^5.1.0: dependencies: is-glob "^4.0.1" +glob-parent@^5.1.1, glob-parent@~5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + 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" @@ -3974,6 +4224,18 @@ glob@^7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.4: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + 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" @@ -4404,6 +4666,11 @@ indexof@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" +infer-owner@^1.0.3, infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -4526,6 +4793,13 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -4647,7 +4921,7 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0, is-glob@^4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" dependencies: @@ -5130,9 +5404,10 @@ loader-fs-cache@^1.0.0: find-cache-dir "^0.1.1" mkdirp "0.5.1" -loader-runner@^2.3.0: +loader-runner@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" + integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== loader-utils@^0.2.16, loader-utils@^0.2.3: version "0.2.17" @@ -5151,6 +5426,24 @@ loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: emojis-list "^2.0.0" json5 "^1.0.1" +loader-utils@^1.2.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" + integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" + +loader-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" + integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + localforage@^1.5.0: version "1.7.3" resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.7.3.tgz#0082b3ca9734679e1bd534995bdd3b24cf10f204" @@ -5504,6 +5797,13 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + lru-cache@~2.6.5: version "2.6.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.6.5.tgz#e56d6354148ede8d7707b58d143220fd08df0fd5" @@ -5515,9 +5815,12 @@ make-dir@^2.0.0, make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" -mamacro@^0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4" +make-dir@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" map-age-cleaner@^0.1.1: version "0.1.3" @@ -5596,13 +5899,21 @@ mem@^4.0.0: mimic-fn "^1.0.0" p-is-promise "^2.0.0" -memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: +memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" dependencies: errno "^0.1.3" readable-stream "^2.0.1" +memory-fs@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" + integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + meow@^3.3.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" @@ -5668,7 +5979,7 @@ micromatch@^2.3.11: parse-glob "^3.0.4" regex-cache "^0.4.2" -micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8: +micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" dependencies: @@ -5788,6 +6099,27 @@ minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.2: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + minipass@^2.2.1, minipass@^2.3.4: version "2.3.5" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" @@ -5795,12 +6127,27 @@ minipass@^2.2.1, minipass@^2.3.4: safe-buffer "^5.1.2" yallist "^3.0.0" +minipass@^3.0.0, minipass@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + minizlib@^1.1.1: version "1.2.1" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" dependencies: minipass "^2.2.1" +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + mississippi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" @@ -5823,13 +6170,20 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: +mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: minimist "0.0.8" -mkdirp@^1.0.4: +mkdirp@^0.5.3: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -5951,6 +6305,11 @@ neo-async@^2.5.0: version "2.6.1" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" +neo-async@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + netmask@~1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35" @@ -5980,9 +6339,10 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" -node-libs-browser@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.0.tgz#c72f60d9d46de08a940dedbb25f3ffa2f9bbaa77" +node-libs-browser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" + integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== dependencies: assert "^1.1.1" browserify-zlib "^0.2.0" @@ -5994,7 +6354,7 @@ node-libs-browser@^2.0.0: events "^3.0.0" https-browserify "^1.0.0" os-browserify "^0.3.0" - path-browserify "0.0.0" + path-browserify "0.0.1" process "^0.11.10" punycode "^1.2.4" querystring-es3 "^0.2.0" @@ -6006,7 +6366,7 @@ node-libs-browser@^2.0.0: tty-browserify "0.0.0" url "^0.11.0" util "^0.11.0" - vm-browserify "0.0.4" + vm-browserify "^1.0.1" node-modules-regexp@^1.0.0: version "1.0.0" @@ -6085,7 +6445,7 @@ normalize-path@^2.0.1, normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -6321,6 +6681,13 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -6486,9 +6853,10 @@ pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" -path-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" +path-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" + integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== path-dirname@^1.0.0: version "1.0.2" @@ -6572,6 +6940,11 @@ phoenix@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.4.0.tgz#9cec8dbd8cbc59ecd2147bc09ca8ceb56b860d75" +picomatch@^2.0.4: + version "2.2.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.3.tgz#465547f359ccc206d3c48e46a1bcb89bf7ee619d" + integrity sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg== + picomatch@^2.0.5, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" @@ -6620,6 +6993,13 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" +pkg-dir@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + pngjs@^3.3.0: version "3.3.3" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b" @@ -7196,9 +7576,10 @@ randomatic@^3.0.0: kind-of "^6.0.0" math-random "^1.0.1" -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" @@ -7335,6 +7716,13 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" +readdirp@~3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" + integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== + dependencies: + picomatch "^2.2.1" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -7630,12 +8018,19 @@ rfdc@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" -rimraf@2.6.3, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2: +rimraf@2.6.3, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" dependencies: glob "^7.1.3" +rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -7650,6 +8045,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +ruffle-mirror@^2021.4.10: + version "2021.4.11" + resolved "https://registry.yarnpkg.com/ruffle-mirror/-/ruffle-mirror-2021.4.11.tgz#039940e0a68e6849259dbef6b54fb877ac4373e7" + integrity sha512-a3N2OkPCJauiHBloHoZgCn/mSUlybyb9Ps4ikPGgHUy8iXPy6qMqh62imvNDU07tBJc5Y0c5mRHBFJRgpMgEpA== + run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" @@ -7725,6 +8125,15 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" +schema-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef" + integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA== + dependencies: + "@types/json-schema" "^7.0.6" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + selenium-server@2.53.1: version "2.53.1" resolved "https://registry.yarnpkg.com/selenium-server/-/selenium-server-2.53.1.tgz#d681528812f3c2e0531a6b7e613e23bb02cce8a6" @@ -7742,7 +8151,7 @@ semver@^5.5.1: version "5.7.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" -semver@^6.3.0: +semver@^6.0.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -7769,9 +8178,19 @@ send@0.16.2: range-parser "~1.2.0" statuses "~1.4.0" -serialize-javascript@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.7.0.tgz#d6e0dfb2a3832a8c94468e6eb1db97e55a192a65" +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +serialize-javascript@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" + integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== + dependencies: + randombytes "^2.1.0" serve-static@1.13.2: version "1.13.2" @@ -7842,9 +8261,10 @@ shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" -shelljs@^0.7.4: - version "0.7.8" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" +shelljs@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" + integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== dependencies: glob "^7.0.0" interpret "^1.0.0" @@ -8010,9 +8430,10 @@ source-map-support@^0.5.16: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-support@~0.5.10: - version "0.5.12" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" +source-map-support@~0.5.12: + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -8078,6 +8499,13 @@ ssri@^6.0.1: dependencies: figgy-pudding "^3.5.1" +ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + state-toggle@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" @@ -8422,7 +8850,7 @@ table@^5.4.6: slice-ansi "^2.1.0" string-width "^3.0.0" -tapable@^1.0.0, tapable@^1.1.0: +tapable@^1.0.0, tapable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" @@ -8438,6 +8866,18 @@ tar@^4: safe-buffer "^5.1.2" yallist "^3.0.2" +tar@^6.0.2: + version "6.1.0" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83" + integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + tcp-port-used@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tcp-port-used/-/tcp-port-used-1.0.1.tgz#46061078e2d38c73979a2c2c12b5a674e6689d70" @@ -8445,27 +8885,29 @@ tcp-port-used@^1.0.1: debug "4.1.0" is2 "2.0.1" -terser-webpack-plugin@^1.1.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.4.tgz#56f87540c28dd5265753431009388f473b5abba3" +terser-webpack-plugin@^1.4.3: + version "1.4.5" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b" + integrity sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw== dependencies: - cacache "^11.3.2" - find-cache-dir "^2.0.0" + cacache "^12.0.2" + find-cache-dir "^2.1.0" is-wsl "^1.1.0" schema-utils "^1.0.0" - serialize-javascript "^1.7.0" + serialize-javascript "^4.0.0" source-map "^0.6.1" - terser "^3.17.0" - webpack-sources "^1.3.0" + terser "^4.1.2" + webpack-sources "^1.4.0" worker-farm "^1.7.0" -terser@^3.17.0: - version "3.17.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2" +terser@^4.1.2: + version "4.8.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" + integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== dependencies: - commander "^2.19.0" + commander "^2.20.0" source-map "~0.6.1" - source-map-support "~0.5.10" + source-map-support "~0.5.12" text-encoding@0.6.4: version "0.6.4" @@ -8912,20 +9354,15 @@ vfile@^4.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" - dependencies: - indexof "0.0.1" +vm-browserify@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" + integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== void-elements@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" -vue-chat-scroll@^1.2.1: - version "1.3.5" - resolved "https://registry.yarnpkg.com/vue-chat-scroll/-/vue-chat-scroll-1.3.5.tgz#a5ee5bae5058f614818a96eac5ee3be4394a2f68" - vue-eslint-parser@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-5.0.0.tgz#00f4e4da94ec974b821a26ff0ed0f7a78402b8a1" @@ -8999,13 +9436,23 @@ vuex@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2" -watchpack@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" +watchpack-chokidar2@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957" + integrity sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww== + dependencies: + chokidar "^2.1.8" + +watchpack@^1.7.4: + version "1.7.5" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453" + integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ== dependencies: - chokidar "^2.0.2" graceful-fs "^4.1.2" neo-async "^2.5.0" + optionalDependencies: + chokidar "^3.4.1" + watchpack-chokidar2 "^2.0.1" webpack-dev-middleware@^3.2.0, webpack-dev-middleware@^3.6.0: version "3.7.0" @@ -9041,41 +9488,49 @@ webpack-merge@^0.14.1: lodash.isplainobject "^3.2.0" lodash.merge "^3.3.2" -webpack-sources@^1.1.0, webpack-sources@^1.3.0: +webpack-sources@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" dependencies: source-list-map "^2.0.0" source-map "~0.6.1" -webpack@^4.0.0: - version "4.32.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.32.1.tgz#afe0cc7dd2b196e5a58f8d1d385311cfbb5d68c0" +webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-module-context" "1.8.5" - "@webassemblyjs/wasm-edit" "1.8.5" - "@webassemblyjs/wasm-parser" "1.8.5" - acorn "^6.0.5" - acorn-dynamic-import "^4.0.0" - ajv "^6.1.0" - ajv-keywords "^3.1.0" - chrome-trace-event "^1.0.0" - enhanced-resolve "^4.1.0" - eslint-scope "^4.0.0" + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack@^4.44.0: + version "4.46.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.46.0.tgz#bf9b4404ea20a073605e0a011d188d77cb6ad542" + integrity sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/wasm-edit" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + acorn "^6.4.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^4.5.0" + eslint-scope "^4.0.3" json-parse-better-errors "^1.0.2" - loader-runner "^2.3.0" - loader-utils "^1.1.0" - memory-fs "~0.4.1" - micromatch "^3.1.8" - mkdirp "~0.5.0" - neo-async "^2.5.0" - node-libs-browser "^2.0.0" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.3" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" schema-utils "^1.0.0" - tapable "^1.1.0" - terser-webpack-plugin "^1.1.0" - watchpack "^1.5.0" - webpack-sources "^1.3.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.3" + watchpack "^1.7.4" + webpack-sources "^1.4.1" whet.extend@~0.9.9: version "0.9.9" @@ -9179,6 +9634,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" +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + yaml@^1.7.2: version "1.10.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" @@ -9227,3 +9687,8 @@ yauzl@^2.10.0: yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==