logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: cd4ad2df11369087b1f39bcaac1bbe258e00580d
parent 8a9115b58e020f750366e87bb4fd3483d6b62b03
Author: Henry Jameson <me@hjkos.com>
Date:   Wed, 16 Mar 2022 21:00:20 +0200

Merge remote-tracking branch 'origin/develop' into vue3-again

* origin/develop: (475 commits)
  Apply 1 suggestion(s) to 1 file(s)
  Update dependency @ungap/event-target to v0.2.3
  Update package.json
  fix broken icons after FA upgrade
  Update Font Awesome
  Update dependency webpack-dev-middleware to v3.7.3
  Update dependency vuelidate to v0.7.7
  Pin dependency @kazvmoe-infra/pinch-zoom-element to 1.2.0
  lint
  Make media modal buttons larger
  Add English translation for hide tooltip
  Add hide button to media modal
  Lint
  Prevent hiding media viewer if swiped over SwipeClick
  Fix webkit image blurs
  Fix video in media modal not displaying properly
  Add changelog for https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1403
  Remove image box-shadow in media modal
  Clean up debug code for image pinch zoom
  Bump @kazvmoe-infra/pinch-zoom-element to 1.2.0 on npm
  ...

Diffstat:

M.babelrc2+-
MCHANGELOG.md58+++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
MCONTRIBUTORS.md1+
Mbuild/dev-server.js1+
Mbuild/webpack.base.conf.js16+++++++++++++++-
Mpackage.json196++++++++++++++++++++++++++++++++++++++++---------------------------------------
Arenovate.json6++++++
Msrc/App.js14++++++++++----
Msrc/App.scss51++++++---------------------------------------------
Msrc/App.vue7++++---
Msrc/_variables.scss2++
Msrc/boot/after_store.js3++-
Msrc/boot/routes.js4++--
Msrc/components/attachment/attachment.js114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Asrc/components/attachment/attachment.scss268+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/attachment/attachment.vue528++++++++++++++++++++++++++++++++++++-------------------------------------------
Msrc/components/basic_user_card/basic_user_card.js4+++-
Msrc/components/basic_user_card/basic_user_card.vue12+++---------
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.vue1+
Dsrc/components/chat_panel/chat_panel.js53-----------------------------------------------------
Dsrc/components/chat_panel/chat_panel.vue148-------------------------------------------------------------------------------
Msrc/components/chat_title/chat_title.vue18++++++------------
Msrc/components/conversation/conversation.js374++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/components/conversation/conversation.vue247+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/components/desktop_nav/desktop_nav.vue1+
Msrc/components/emoji_input/emoji_input.js2+-
Msrc/components/emoji_input/emoji_input.vue2+-
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.js17+++++++----------
Msrc/components/font_control/font_control.vue35+++++++++++++----------------------
Msrc/components/gallery/gallery.js97++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/components/gallery/gallery.vue202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
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.js98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/components/media_modal/media_modal.vue269+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Asrc/components/mention_link/mention_link.js134+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/mention_link/mention_link.scss115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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/mobile_post_status_button/mobile_post_status_button.js3+++
Msrc/components/mobile_post_status_button/mobile_post_status_button.vue4++--
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/notification/notification.js4+++-
Msrc/components/notification/notification.scss13+++++++++++++
Msrc/components/notification/notification.vue18+++++++++++-------
Msrc/components/notifications/notifications.scss12------------
Asrc/components/pinch_zoom/pinch_zoom.js13+++++++++++++
Asrc/components/pinch_zoom/pinch_zoom.vue11+++++++++++
Msrc/components/poll/poll.js10+++++++---
Msrc/components/poll/poll.vue14++++++++++----
Msrc/components/poll/poll_form.js6++++--
Msrc/components/poll/poll_form.vue70+++++++++++++++++++++++++++-------------------------------------------
Msrc/components/popover/popover.vue2+-
Msrc/components/post_status_form/post_status_form.js23++++++++++++++++++++---
Msrc/components/post_status_form/post_status_form.vue110++++++++++++++++++++++---------------------------------------------------------
Asrc/components/rich_content/rich_content.jsx328+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/rich_content/rich_content.scss64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/select/select.js21+++++++++++++++++++++
Asrc/components/select/select.vue63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/helpers/boolean_setting.js47+++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/settings_modal/helpers/boolean_setting.vue42+++---------------------------------------
Asrc/components/settings_modal/helpers/choice_setting.js48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/helpers/choice_setting.vue31+++++++++++++++++++++++++++++++
Asrc/components/settings_modal/helpers/integer_setting.js41+++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/helpers/integer_setting.vue23+++++++++++++++++++++++
Asrc/components/settings_modal/helpers/server_side_indicator.vue51+++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/settings_modal/helpers/shared_computed_object.js9+++++++++
Msrc/components/settings_modal/settings_modal.js27++++++++++++++++++++-------
Msrc/components/settings_modal/settings_modal.scss7+++++++
Msrc/components/settings_modal/settings_modal.vue40+++++++++++++++++++++-------------------
Msrc/components/settings_modal/settings_modal_content.scss13++++++++++++-
Msrc/components/settings_modal/tabs/filtering_tab.js21+++++++++++----------
Msrc/components/settings_modal/tabs/filtering_tab.vue193++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/components/settings_modal/tabs/general_tab.js45++++++++++++++++++++++++++++++++++++++++++---
Msrc/components/settings_modal/tabs/general_tab.vue406+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Msrc/components/settings_modal/tabs/notifications_tab.js8+++++---
Msrc/components/settings_modal/tabs/notifications_tab.vue81++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/components/settings_modal/tabs/profile_tab.js25+++++++------------------
Msrc/components/settings_modal/tabs/profile_tab.vue121++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.js47++++++++++++++++++++++++++---------------------
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.scss3+++
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.vue41++++++++++++++---------------------------
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/status/status.js200+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msrc/components/status/status.scss85++++++++++++++++++++++++++++++++++---------------------------------------------
Msrc/components/status/status.vue165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Asrc/components/status_body/status_body.js131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/status_body/status_body.scss174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/status_body/status_body.vue100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/status_content/status_content.js178++++++++++++++++++++-----------------------------------------------------------
Msrc/components/status_content/status_content.vue311+++++++++++--------------------------------------------------------------------
Msrc/components/still-image/still-image.js11++++++++++-
Msrc/components/still-image/still-image.vue7+++++--
Asrc/components/swipe_click/swipe_click.js84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/swipe_click/swipe_click.vue14++++++++++++++
Asrc/components/thread_tree/thread_tree.js90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/thread_tree/thread_tree.vue127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/timeline/timeline.js18------------------
Msrc/components/timeline/timeline.vue2+-
Msrc/components/timeline/timeline_quick_settings.js10++++++++--
Msrc/components/timeline/timeline_quick_settings.vue9+++++++++
Msrc/components/timeline_menu/timeline_menu.vue5+----
Msrc/components/user_avatar/user_avatar.js13++++++++++++-
Msrc/components/user_avatar/user_avatar.vue14+++++++++++++-
Msrc/components/user_card/user_card.js19+++++++++++++------
Msrc/components/user_card/user_card.vue139++++++++++++++++++++++++++++++++++++-------------------------------------------
Msrc/components/user_list_popover/user_list_popover.vue9++++++++-
Msrc/components/user_profile/user_profile.js4+++-
Msrc/components/user_profile/user_profile.vue20++++++++++++--------
Msrc/i18n/ca.json568++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/i18n/cs.json1-
Msrc/i18n/de.json361+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/i18n/en.json92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/i18n/eo.json67++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/i18n/es.json39+++++++++++++++++++++++++++++++--------
Msrc/i18n/eu.json47++++++++++++++++++++++++++++++++++++-----------
Msrc/i18n/fi.json4++--
Msrc/i18n/fr.json27++++++++++++++++++++++-----
Msrc/i18n/he.json1-
Asrc/i18n/id.json631+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/i18n/it.json69++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/i18n/ja_easy.json1-
Msrc/i18n/ja_pedantic.json61+++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/i18n/ko.json17++++++++++-------
Msrc/i18n/nb.json1-
Msrc/i18n/nl.json357+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/i18n/oc.json1-
Msrc/i18n/pl.json79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/i18n/pt.json1-
Msrc/i18n/ru.json1-
Msrc/i18n/te.json1-
Msrc/i18n/uk.json64+++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Asrc/i18n/vi.json872+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/i18n/zh.json32+++++++++++++++++++++++++-------
Msrc/i18n/zh_Hant.json42+++++++++++++++++++++++++++++++++---------
Msrc/main.js20+++++++++++++++++---
Msrc/modules/api.js4++--
Dsrc/modules/chat.js34----------------------------------
Msrc/modules/config.js34+++++++++++++++++++++++++++++-----
Msrc/modules/instance.js25++++++++++++++++++++++---
Msrc/modules/media_viewer.js9+++++----
Asrc/modules/serverSideConfig.js137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/modules/shout.js33+++++++++++++++++++++++++++++++++
Msrc/modules/users.js9+++++++--
Msrc/services/entity_normalizer/entity_normalizer.service.js42+++++++++++++++++-------------------------
Msrc/services/favicon_service/favicon_service.js70++++++++++++++++++++++++++++++++++++++--------------------------------
Msrc/services/file_type/file_type.service.js4++++
Msrc/services/gesture_service/gesture_service.js137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
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+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/services/ruffle_service/ruffle_service.js40++++++++++++++++++++++++++++++++++++++++
Msrc/services/theme_data/pleromafe.js6++++++
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/services/entity_normalizer/entity_normalizer.spec.js83+------------------------------------------------------------------------------
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.lock2055++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
187 files changed, 11787 insertions(+), 3988 deletions(-)

diff --git a/.babelrc b/.babelrc @@ -1,5 +1,5 @@ { - "presets": ["@babel/preset-env"], + "presets": ["@babel/preset-env", "@vue/babel-preset-jsx"], "plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"], "comments": false } diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -3,11 +3,67 @@ 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 +- Enabled users to zoom and pan images in media viewer with mouse and touch + + +## [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 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 @@ -4,6 +4,7 @@ var utils = require('./utils') var projectRoot = path.resolve(__dirname, '../') var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin') var { VueLoaderPlugin } = require('vue-loader') +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 @@ -95,6 +96,19 @@ module.exports = { entry: path.join(__dirname, '..', 'src/sw.js'), filename: 'sw-pleroma.js' }), - new VueLoaderPlugin() + new VueLoaderPlugin(), + // 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/package.json b/package.json @@ -16,106 +16,108 @@ "lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs" }, "dependencies": { - "@babel/runtime": "^7.7.6", - "@chenfengyuan/vue-qrcode": "^2.0.0-beta", - "@fortawesome/fontawesome-svg-core": "^1.2.32", - "@fortawesome/free-regular-svg-icons": "^5.15.1", - "@fortawesome/free-solid-svg-icons": "^5.15.1", - "@fortawesome/vue-fontawesome": "^3.0.0-3", - "@vue/compiler-sfc": "^3.0.7", - "body-scroll-lock": "^2.6.4", - "chromatism": "^3.0.0", - "cropperjs": "^1.4.3", - "diff": "^3.0.1", - "escape-html": "^1.0.3", - "localforage": "^1.5.0", - "parse-link-header": "^1.0.1", - "phoenix": "^1.3.0", - "punycode.js": "^2.1.0", - "qrcode": "^1.4.4", - "v-click-outside": "^2.1.1", - "vue": "^3.0.7", - "vue-i18n": "^9.0.0-beta.18", - "vue-router": "^4.0.5", - "vuelidate": "^0.7.6", - "vuex": "^4.0.0" + "@babel/runtime": "7.7.6", + "@chenfengyuan/vue-qrcode": "1.0.2", + "@fortawesome/fontawesome-svg-core": "1.3.0", + "@fortawesome/free-regular-svg-icons": "5.15.4", + "@fortawesome/free-solid-svg-icons": "5.15.4", + "@fortawesome/vue-fontawesome": "2.0.6", + "@kazvmoe-infra/pinch-zoom-element": "1.2.0", + "body-scroll-lock": "2.6.4", + "chromatism": "3.0.0", + "cropperjs": "1.4.3", + "diff": "3.5.0", + "escape-html": "1.0.3", + "localforage": "1.7.3", + "parse-link-header": "1.0.1", + "phoenix": "1.4.0", + "portal-vue": "2.1.7", + "punycode.js": "2.1.0", + "ruffle-mirror": "2021.4.11", + "v-click-outside": "2.1.5", + "vue": "2.6.11", + "vue-i18n": "7.8.1", + "vue-router": "3.0.2", + "vue-template-compiler": "2.6.11", + "vuelidate": "0.7.7", + "vuex": "3.0.1" }, "devDependencies": { - "@babel/core": "^7.7.5", - "@babel/plugin-transform-runtime": "^7.7.6", - "@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-jsx": "^1.0.3", - "@vue/babel-plugin-transform-vue-jsx": "^1.1.2", - "@vue/test-utils": "^2.0.0-beta.8", - "autoprefixer": "^6.4.0", - "babel-eslint": "^7.0.0", - "babel-loader": "^8.0.6", - "babel-plugin-lodash": "^3.3.4", - "chai": "^3.5.0", - "chalk": "^1.1.3", - "chromedriver": "^87.0.1", - "connect-history-api-fallback": "^1.1.0", - "cross-spawn": "^4.0.2", - "css-loader": "^0.28.0", - "custom-event-polyfill": "^1.0.7", - "eslint": "^5.16.0", - "eslint-config-standard": "^12.0.0", - "eslint-friendly-formatter": "^2.0.5", - "eslint-loader": "^2.1.0", - "eslint-plugin-import": "^2.13.0", - "eslint-plugin-node": "^7.0.0", - "eslint-plugin-promise": "^4.0.0", - "eslint-plugin-standard": "^4.0.0", - "eslint-plugin-vue": "^5.2.2", - "eventsource-polyfill": "^0.9.6", - "express": "^4.13.3", - "file-loader": "^3.0.1", - "function-bind": "^1.0.2", - "html-webpack-plugin": "^3.0.0", - "http-proxy-middleware": "^0.17.2", - "inject-loader": "^2.0.1", - "iso-639-1": "^2.0.3", - "isparta-loader": "^2.0.0", - "json-loader": "^0.5.4", - "karma": "^3.0.0", - "karma-coverage": "^1.1.1", - "karma-firefox-launcher": "^1.1.0", - "karma-mocha": "^1.2.0", - "karma-mocha-reporter": "^2.2.1", - "karma-sinon-chai": "^2.0.2", - "karma-sourcemap-loader": "^0.3.7", - "karma-spec-reporter": "0.0.26", - "karma-webpack": "^4.0.0-rc.3", - "lodash": "^4.16.4", - "lolex": "^1.4.0", - "mini-css-extract-plugin": "^0.5.0", - "mocha": "^3.1.0", - "nightwatch": "^0.9.8", - "opn": "^4.0.2", - "ora": "^0.3.0", - "postcss-loader": "^3.0.0", - "raw-loader": "^0.5.1", - "sass": "^1.17.3", - "sass-loader": "git://github.com/webpack-contrib/sass-loader", + "@babel/core": "7.7.5", + "@babel/plugin-transform-runtime": "7.7.6", + "@babel/preset-env": "7.7.6", + "@babel/register": "7.7.4", + "@ungap/event-target": "0.2.3", + "@vue/babel-helper-vue-jsx-merge-props": "1.2.1", + "@vue/babel-preset-jsx": "1.2.4", + "@vue/test-utils": "1.0.0-beta.28", + "autoprefixer": "6.7.7", + "babel-eslint": "7.2.3", + "babel-loader": "8.0.6", + "babel-plugin-lodash": "3.3.4", + "chai": "3.5.0", + "chalk": "1.1.3", + "chromedriver": "87.0.7", + "connect-history-api-fallback": "1.6.0", + "copy-webpack-plugin": "6.4.1", + "cross-spawn": "4.0.2", + "css-loader": "0.28.11", + "custom-event-polyfill": "1.0.7", + "eslint": "5.16.0", + "eslint-config-standard": "12.0.0", + "eslint-friendly-formatter": "2.0.7", + "eslint-loader": "2.1.2", + "eslint-plugin-import": "2.17.2", + "eslint-plugin-node": "7.0.1", + "eslint-plugin-promise": "4.1.1", + "eslint-plugin-standard": "4.0.0", + "eslint-plugin-vue": "5.2.3", + "eventsource-polyfill": "0.9.6", + "express": "4.16.4", + "file-loader": "3.0.1", + "function-bind": "1.1.1", + "html-webpack-plugin": "3.2.0", + "http-proxy-middleware": "0.17.4", + "inject-loader": "2.0.1", + "iso-639-1": "2.0.3", + "isparta-loader": "2.0.0", + "json-loader": "0.5.7", + "karma": "3.1.4", + "karma-coverage": "1.1.2", + "karma-firefox-launcher": "1.1.0", + "karma-mocha": "1.3.0", + "karma-mocha-reporter": "2.2.5", + "karma-sinon-chai": "2.0.2", + "karma-sourcemap-loader": "0.3.8", + "karma-spec-reporter": "0.0.33", + "karma-webpack": "4.0.2", + "lodash": "4.17.21", + "lolex": "1.6.0", + "mini-css-extract-plugin": "0.5.0", + "mocha": "3.5.3", + "nightwatch": "0.9.21", + "opn": "4.0.2", + "ora": "0.3.0", + "postcss-loader": "3.0.0", + "raw-loader": "0.5.1", + "sass": "1.20.1", + "sass-loader": "7.2.0", "selenium-server": "2.53.1", - "semver": "^5.3.0", - "serviceworker-webpack-plugin": "^1.0.0", - "shelljs": "^0.8.4", - "sinon": "^2.1.0", - "sinon-chai": "^2.8.0", - "stylelint": "^13.6.1", - "stylelint-config-standard": "^20.0.0", - "stylelint-rscss": "^0.4.0", - "url-loader": "^1.1.2", - "vue-loader": "^16.1.2", - "vue-style-loader": "^4.0.0", - "webpack": "^4.0.0", - "webpack-dev-middleware": "^3.6.0", - "webpack-hot-middleware": "^2.12.2", - "webpack-merge": "^0.14.1" + "semver": "5.6.0", + "serviceworker-webpack-plugin": "1.0.1", + "shelljs": "0.8.5", + "sinon": "2.4.1", + "sinon-chai": "2.14.0", + "stylelint": "13.6.1", + "stylelint-config-standard": "20.0.0", + "stylelint-rscss": "0.4.0", + "url-loader": "1.1.2", + "vue-loader": "14.2.4", + "vue-style-loader": "4.1.2", + "webpack": "4.46.0", + "webpack-dev-middleware": "3.7.3", + "webpack-hot-middleware": "2.24.3", + "webpack-merge": "0.14.1" }, "engines": { "node": ">= 4.0.0", diff --git a/renovate.json b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ] +} 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; @@ -830,13 +798,6 @@ nav { } } -.select-multiple { - display: flex; - .option-list { - margin: 0; - padding-left: .5em; - } -} .setting-list, .option-list{ list-style-type: none; 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/_variables.scss b/src/_variables.scss @@ -30,3 +30,5 @@ $fallback--attachmentRadius: 10px; $fallback--chatMessageRadius: 10px; $fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; + +$status-margin: 0.75em; diff --git a/src/boot/after_store.js b/src/boot/after_store.js @@ -121,6 +121,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { copyInstanceOption('nsfwCensorImage') copyInstanceOption('background') copyInstanceOption('hidePostStats') + copyInstanceOption('hideBotIndication') copyInstanceOption('hideUserStats') copyInstanceOption('hideFilteredStatuses') copyInstanceOption('logo') @@ -246,7 +247,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/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 @@ -11,312 +12,257 @@ :href="attachment.url" :alt="attachment.description" :title="attachment.description" + @click.prevent > <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" + :title="$t('status.attachment_stop_flash')" + @click.prevent="stopFlash" + > + <FAIcon icon="stop" /> + </button> + <button + v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'" + class="button-unstyled attachment-button" + :title="$t('status.show_attachment_description')" + @click.prevent="toggleDescription" + > + <FAIcon icon="align-right" /> + </button> + <button + v-if="!useModal && type !== 'unknown'" + class="button-unstyled attachment-button" + :title="$t('status.show_attachment_in_modal')" + @click.prevent="openModalForce" + > + <FAIcon icon="search-plus" /> + </button> + <button + v-if="nsfw && hideNsfwLocal" + class="button-unstyled attachment-button" + :title="$t('status.hide_attachment')" + @click.prevent="toggleHidden" + > + <FAIcon icon="times" /> + </button> + <button + v-if="shiftUp" + class="button-unstyled attachment-button" + :title="$t('status.move_up')" + @click.prevent="onShiftUp" + > + <FAIcon icon="chevron-left" /> + </button> + <button + v-if="shiftDn" + class="button-unstyled attachment-button" + :title="$t('status.move_down')" + @click.prevent="onShiftDn" + > + <FAIcon icon="chevron-right" /> + </button> + <button + v-if="remove" + class="button-unstyled attachment-button" + :title="$t('status.remove_attachment')" + @click.prevent="onRemove" + > + <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,17 +25,11 @@ :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 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 @@ -71,6 +71,7 @@ </Popover> </div> <StatusContent + class="message-content" :status="messageForStatusContent" :full-content="true" > diff --git a/src/components/chat_panel/chat_panel.js b/src/components/chat_panel/chat_panel.js @@ -1,53 +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) - } - }, - 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 chatPanel diff --git a/src/components/chat_panel/chat_panel.vue b/src/components/chat_panel/chat_panel.vue @@ -1,148 +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"> - {{ $t('shoutbox.title') }} - <FAIcon - v-if="floating" - icon="times" - class="close-icon" - /> - </div> - </div> - <div 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); - margin-right: 0.5em; - } - - .title { - display: flex; - justify-content: space-between; - align-items: center; - } - } - - .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.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.js b/src/components/conversation/conversation.js @@ -1,5 +1,19 @@ import { reduce, filter, findIndex, clone, get } from 'lodash' import Status from '../status/status.vue' +import ThreadTree from '../thread_tree/thread_tree.vue' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faAngleDoubleDown, + faAngleDoubleLeft, + faChevronLeft +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faAngleDoubleDown, + faAngleDoubleLeft, + faChevronLeft +) const sortById = (a, b) => { const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id @@ -35,7 +49,10 @@ const conversation = { data () { return { highlight: null, - expanded: false + expanded: false, + threadDisplayStatusObject: {}, // id => 'showing' | 'hidden' + statusContentPropertiesObject: {}, + inlineDivePosition: null } }, props: [ @@ -53,13 +70,51 @@ const conversation = { } }, computed: { - hideStatus () { + maxDepthToShowByDefault () { + // maxDepthInThread = max number of depths that is *visible* + // since our depth starts with 0 and "showing" means "showing children" + // there is a -2 here + const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2 + return maxDepth >= 1 ? maxDepth : 1 + }, + displayStyle () { + return this.$store.getters.mergedConfig.conversationDisplay + }, + isTreeView () { + return !this.isLinearView + }, + treeViewIsSimple () { + return !this.$store.getters.mergedConfig.conversationTreeAdvanced + }, + isLinearView () { + return this.displayStyle === 'linear' + }, + shouldFadeAncestors () { + return this.$store.getters.mergedConfig.conversationTreeFadeAncestors + }, + otherRepliesButtonPosition () { + return this.$store.getters.mergedConfig.conversationOtherRepliesButton + }, + showOtherRepliesButtonBelowStatus () { + return this.otherRepliesButtonPosition === 'below' + }, + showOtherRepliesButtonInsideStatus () { + return this.otherRepliesButtonPosition === 'inside' + }, + suspendable () { + if (this.isTreeView) { + return Object.entries(this.statusContentProperties) + .every(([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0) + } if (this.$refs.statusComponent && this.$refs.statusComponent[0]) { - return this.virtualHidden && this.$refs.statusComponent[0].suspendable + return this.$refs.statusComponent.every(s => s.suspendable) } else { - return this.virtualHidden + return true } }, + hideStatus () { + return this.virtualHidden && this.suspendable + }, status () { return this.$store.state.statuses.allStatusesObject[this.statusId] }, @@ -90,6 +145,121 @@ const conversation = { return sortAndFilterConversation(conversation, this.status) }, + statusMap () { + return this.conversation.reduce((res, s) => { + res[s.id] = s + return res + }, {}) + }, + threadTree () { + const reverseLookupTable = this.conversation.reduce((table, status, index) => { + table[status.id] = index + return table + }, {}) + + const threads = this.conversation.reduce((a, cur) => { + const id = cur.id + a.forest[id] = this.getReplies(id) + .map(s => s.id) + + return a + }, { + forest: {} + }) + + const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => { + if (processed[id]) { + return [] + } + + processed[id] = true + return [{ + status: this.conversation[reverseLookupTable[id]], + id, + depth + }, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), []) + }).reduce((a, b) => a.concat(b), []) + + const linearized = walk(threads.forest, this.topLevel.map(k => k.id)) + + return linearized + }, + replyIds () { + return this.conversation.map(k => k.id) + .reduce((res, id) => { + res[id] = (this.replies[id] || []).map(k => k.id) + return res + }, {}) + }, + totalReplyCount () { + const sizes = {} + const subTreeSizeFor = (id) => { + if (sizes[id]) { + return sizes[id] + } + sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0) + return sizes[id] + } + this.conversation.map(k => k.id).map(subTreeSizeFor) + return Object.keys(sizes).reduce((res, id) => { + res[id] = sizes[id] - 1 // exclude itself + return res + }, {}) + }, + totalReplyDepth () { + const depths = {} + const subTreeDepthFor = (id) => { + if (depths[id]) { + return depths[id] + } + depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0) + return depths[id] + } + this.conversation.map(k => k.id).map(subTreeDepthFor) + return Object.keys(depths).reduce((res, id) => { + res[id] = depths[id] - 1 // exclude itself + return res + }, {}) + }, + depths () { + return this.threadTree.reduce((a, k) => { + a[k.id] = k.depth + return a + }, {}) + }, + topLevel () { + const topLevel = this.conversation.reduce((tl, cur) => + tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation) + return topLevel + }, + otherTopLevelCount () { + return this.topLevel.length - 1 + }, + showingTopLevel () { + if (this.canDive && this.diveRoot) { + return [this.statusMap[this.diveRoot]] + } + return this.topLevel + }, + diveRoot () { + const statusId = this.inlineDivePosition || this.statusId + const isTopLevel = !this.parentOf(statusId) + return isTopLevel ? null : statusId + }, + diveDepth () { + return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0 + }, + diveMode () { + return this.canDive && !!this.diveRoot + }, + shouldShowAllConversationButton () { + // The "show all conversation" button tells the user that there exist + // other toplevel statuses, so do not show it if there is only a single root + return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1 + }, + shouldShowAncestors () { + return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length + }, replies () { let i = 1 // eslint-disable-next-line camelcase @@ -109,15 +279,71 @@ const conversation = { }, {}) }, isExpanded () { - return this.expanded || this.isPage + return !!(this.expanded || this.isPage) }, hiddenStyle () { const height = (this.status && this.status.virtualHeight) || '120px' return this.virtualHidden ? { height } : {} + }, + threadDisplayStatus () { + return this.conversation.reduce((a, k) => { + const id = k.id + const depth = this.depths[id] + const status = (() => { + if (this.threadDisplayStatusObject[id]) { + return this.threadDisplayStatusObject[id] + } + if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) { + return 'showing' + } else { + return 'hidden' + } + })() + + a[id] = status + return a + }, {}) + }, + statusContentProperties () { + return this.conversation.reduce((a, k) => { + const id = k.id + const props = (() => { + const def = { + showingTall: false, + expandingSubject: false, + showingLongSubject: false, + isReplying: false, + mediaPlaying: [] + } + + if (this.statusContentPropertiesObject[id]) { + return { + ...def, + ...this.statusContentPropertiesObject[id] + } + } + return def + })() + + a[id] = props + return a + }, {}) + }, + canDive () { + return this.isTreeView && this.isExpanded + }, + focused () { + return (id) => { + return (this.isExpanded) && id === this.highlight + } + }, + maybeHighlight () { + return this.isExpanded ? this.highlight : null } }, components: { - Status + Status, + ThreadTree }, watch: { statusId (newVal, oldVal) { @@ -132,6 +358,8 @@ const conversation = { expanded (value) { if (value) { this.fetchConversation() + } else { + this.resetDisplayState() } }, virtualHidden (value) { @@ -161,8 +389,8 @@ const conversation = { getReplies (id) { return this.replies[id] || [] }, - focused (id) { - return (this.isExpanded) && id === this.statusId + getHighlight () { + return this.isExpanded ? this.highlight : null }, setHighlight (id) { if (!id) return @@ -170,15 +398,139 @@ const conversation = { this.$store.dispatch('fetchFavsAndRepeats', id) this.$store.dispatch('fetchEmojiReactionsBy', id) }, - getHighlight () { - return this.isExpanded ? this.highlight : null - }, toggleExpanded () { this.expanded = !this.expanded }, getConversationId (statusId) { const status = this.$store.state.statuses.allStatusesObject[statusId] return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id')) + }, + setThreadDisplay (id, nextStatus) { + this.threadDisplayStatusObject = { + ...this.threadDisplayStatusObject, + [id]: nextStatus + } + }, + toggleThreadDisplay (id) { + const curStatus = this.threadDisplayStatus[id] + const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing' + this.setThreadDisplay(id, nextStatus) + }, + setThreadDisplayRecursively (id, nextStatus) { + this.setThreadDisplay(id, nextStatus) + this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus)) + }, + showThreadRecursively (id) { + this.setThreadDisplayRecursively(id, 'showing') + }, + setStatusContentProperty (id, name, value) { + this.statusContentPropertiesObject = { + ...this.statusContentPropertiesObject, + [id]: { + ...this.statusContentPropertiesObject[id], + [name]: value + } + } + }, + toggleStatusContentProperty (id, name) { + this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name]) + }, + leastVisibleAncestor (id) { + let cur = id + let parent = this.parentOf(cur) + while (cur) { + // if the parent is showing it means cur is visible + if (this.threadDisplayStatus[parent] === 'showing') { + return cur + } + parent = this.parentOf(parent) + cur = this.parentOf(cur) + } + // nothing found, fall back to toplevel + return this.topLevel[0] ? this.topLevel[0].id : undefined + }, + diveIntoStatus (id, preventScroll) { + this.tryScrollTo(id) + }, + diveToTopLevel () { + this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id) + }, + // only used when we are not on a page + undive () { + this.inlineDivePosition = null + this.setHighlight(this.statusId) + }, + tryScrollTo (id) { + if (!id) { + return + } + if (this.isPage) { + // set statusId + this.$router.push({ name: 'conversation', params: { id } }) + } else { + this.inlineDivePosition = id + } + // Because the conversation can be unmounted when out of sight + // and mounted again when it comes into sight, + // the `mounted` or `created` function in `status` should not + // contain scrolling calls, as we do not want the page to jump + // when we scroll with an expanded conversation. + // + // Now the method is to rely solely on the `highlight` watcher + // in `status` components. + // In linear views, all statuses are rendered at all times, but + // in tree views, it is possible that a change in active status + // removes and adds status components (e.g. an originally child + // status becomes an ancestor status, and thus they will be + // different). + // Here, let the components be rendered first, in order to trigger + // the `highlight` watcher. + this.$nextTick(() => { + this.setHighlight(id) + }) + }, + goToCurrent () { + this.tryScrollTo(this.diveRoot || this.topLevel[0].id) + }, + statusById (id) { + return this.statusMap[id] + }, + parentOf (id) { + const status = this.statusById(id) + if (!status) { + return undefined + } + const { in_reply_to_status_id: parentId } = status + if (!this.statusMap[parentId]) { + return undefined + } + return parentId + }, + parentOrSelf (id) { + return this.parentOf(id) || id + }, + // Ancestors of some status, from top to bottom + ancestorsOf (id) { + const ancestors = [] + let cur = this.parentOf(id) + while (cur) { + ancestors.unshift(this.statusMap[cur]) + cur = this.parentOf(cur) + } + return ancestors + }, + topLevelAncestorOrSelfId (id) { + let cur = id + let parent = this.parentOf(id) + while (parent) { + cur = this.parentOf(cur) + parent = this.parentOf(parent) + } + return cur + }, + resetDisplayState () { + this.undive() + this.threadDisplayStatusObject = {} } } } diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue @@ -18,24 +18,168 @@ {{ $t('timeline.collapse') }} </button> </div> - <status - v-for="status in conversation" - :key="status.id" - ref="statusComponent" - :inline-expanded="collapsable && isExpanded" - :statusoid="status" - :expandable="!isExpanded" - :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]" - :focused="focused(status.id)" - :in-conversation="isExpanded" - :highlight="getHighlight()" - :replies="getReplies(status.id)" - :in-profile="inProfile" - :profile-user-id="profileUserId" - class="conversation-status status-fadein panel-body" - @goto="setHighlight" - @toggleExpanded="toggleExpanded" - /> + <div class="conversation-body panel-body"> + <div + v-if="isTreeView" + class="thread-body" + > + <div + v-if="shouldShowAllConversationButton" + class="conversation-dive-to-top-level-box" + > + <i18n + path="status.show_all_conversation_with_icon" + tag="button" + class="button-unstyled -link" + @click.prevent="diveToTopLevel" + > + <FAIcon + place="icon" + icon="angle-double-left" + /> + <span place="text"> + {{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }} + </span> + </i18n> + </div> + <div + v-if="shouldShowAncestors" + class="thread-ancestors" + > + <div + v-for="status in ancestorsOf(diveRoot)" + :key="status.id" + class="thread-ancestor" + :class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}" + > + <status + ref="statusComponent" + :inline-expanded="collapsable && isExpanded" + :statusoid="status" + :expandable="!isExpanded" + :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]" + :focused="focused(status.id)" + :in-conversation="isExpanded" + :highlight="getHighlight()" + :replies="getReplies(status.id)" + :in-profile="inProfile" + :profile-user-id="profileUserId" + class="conversation-status status-fadein panel-body" + + :simple-tree="treeViewIsSimple" + :toggle-thread-display="toggleThreadDisplay" + :thread-display-status="threadDisplayStatus" + :show-thread-recursively="showThreadRecursively" + :total-reply-count="totalReplyCount" + :total-reply-depth="totalReplyDepth" + :show-other-replies-as-button="showOtherRepliesButtonInsideStatus" + :dive="() => diveIntoStatus(status.id)" + + :controlled-showing-tall="statusContentProperties[status.id].showingTall" + :controlled-expanding-subject="statusContentProperties[status.id].expandingSubject" + :controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject" + :controlled-replying="statusContentProperties[status.id].replying" + :controlled-media-playing="statusContentProperties[status.id].mediaPlaying" + :controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')" + :controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')" + :controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')" + :controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')" + :controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)" + + @goto="setHighlight" + @toggleExpanded="toggleExpanded" + /> + <div + v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1" + class="thread-ancestor-dive-box" + > + <div + class="thread-ancestor-dive-box-inner" + > + <i18n + tag="button" + path="status.ancestor_follow_with_icon" + class="button-unstyled -link thread-tree-show-replies-button" + @click.prevent="diveIntoStatus(status.id)" + > + <FAIcon + place="icon" + icon="angle-double-right" + /> + <span place="text"> + {{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }} + </span> + </i18n> + </div> + </div> + </div> + </div> + <thread-tree + v-for="status in showingTopLevel" + :key="status.id" + ref="statusComponent" + :depth="0" + + :status="status" + :in-profile="inProfile" + :conversation="conversation" + :collapsable="collapsable" + :is-expanded="isExpanded" + :pinned-status-ids-object="pinnedStatusIdsObject" + :profile-user-id="profileUserId" + + :focused="focused" + :get-replies="getReplies" + :highlight="maybeHighlight" + :set-highlight="setHighlight" + :toggle-expanded="toggleExpanded" + + :simple="treeViewIsSimple" + :toggle-thread-display="toggleThreadDisplay" + :thread-display-status="threadDisplayStatus" + :show-thread-recursively="showThreadRecursively" + :total-reply-count="totalReplyCount" + :total-reply-depth="totalReplyDepth" + :status-content-properties="statusContentProperties" + :set-status-content-property="setStatusContentProperty" + :toggle-status-content-property="toggleStatusContentProperty" + :dive="canDive ? diveIntoStatus : undefined" + /> + </div> + <div + v-if="isLinearView" + class="thread-body" + > + <status + v-for="status in conversation" + :key="status.id" + ref="statusComponent" + :inline-expanded="collapsable && isExpanded" + :statusoid="status" + :expandable="!isExpanded" + :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]" + :focused="focused(status.id)" + :in-conversation="isExpanded" + :highlight="getHighlight()" + :replies="getReplies(status.id)" + :in-profile="inProfile" + :profile-user-id="profileUserId" + class="conversation-status status-fadein panel-body" + + :toggle-thread-display="toggleThreadDisplay" + :thread-display-status="threadDisplayStatus" + :show-thread-recursively="showThreadRecursively" + :total-reply-count="totalReplyCount" + :total-reply-depth="totalReplyDepth" + :status-content-properties="statusContentProperties" + :set-status-content-property="setStatusContentProperty" + :toggle-status-content-property="toggleStatusContentProperty" + + @goto="setHighlight" + @toggleExpanded="toggleExpanded" + /> + </div> + </div> </div> <div v-else @@ -49,19 +193,74 @@ @import '../../_variables.scss'; .Conversation { - .conversation-status { + .conversation-dive-to-top-level-box { + padding: var(--status-margin, $status-margin); border-bottom-width: 1px; border-bottom-style: solid; border-bottom-color: var(--border, $fallback--border); border-radius: 0; + /* Make the button stretch along the whole row */ + display: flex; + align-items: stretch; + flex-direction: column; + } + + .thread-ancestors { + margin-left: var(--status-margin, $status-margin); + border-left: 2px solid var(--border, $fallback--border); } - &.-expanded { - .conversation-status:last-child { - border-bottom: none; - border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; - border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); + .thread-ancestor.-faded .StatusContent { + --link: var(--faintLink); + --text: var(--faint); + color: var(--text); + } + .thread-ancestor-dive-box { + padding-left: var(--status-margin, $status-margin); + border-bottom-width: 1px; + border-bottom-style: solid; + border-bottom-color: var(--border, $fallback--border); + border-radius: 0; + /* Make the button stretch along the whole row */ + &, &-inner { + display: flex; + align-items: stretch; + flex-direction: column; } } + .thread-ancestor-dive-box-inner { + padding: var(--status-margin, $status-margin); + } + + .conversation-status { + border-bottom-width: 1px; + border-bottom-style: solid; + border-bottom-color: var(--border, $fallback--border); + border-radius: 0; + } + + .thread-ancestor-has-other-replies .conversation-status, + .thread-ancestor:last-child .conversation-status, + .thread-ancestor:last-child .thread-ancestor-dive-box, + &.-expanded .thread-tree .conversation-status { + border-bottom: none; + } + + .thread-ancestors + .thread-tree > .conversation-status { + border-top-width: 1px; + border-top-style: solid; + border-top-color: var(--border, $fallback--border); + } + + /* expanded conversation in timeline */ + &.status-fadein.-expanded .thread-body { + border-left-width: 4px; + border-left-style: solid; + border-left-color: $fallback--cRed; + border-left-color: var(--cRed, $fallback--cRed); + border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; + border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); + border-bottom: 1px solid var(--border, $fallback--border); + } } </style> 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/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js @@ -267,7 +267,7 @@ const EmojiInput = { this.$nextTick(function () { // Re-focus inputbox after clicking suggestion // Set selection right after the replacement instead of the very end - // this.input.setSelectionRange(position, position) + this.input.setSelectionRange(position, position) this.caret = position }) }, diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue @@ -1,9 +1,9 @@ <template> <div + ref="root" v-click-outside="onClickOutside" class="emoji-input" :class="{ 'with-picker': !hideEmojiButton }" - ref='root' > <slot /> <template v-if="enableEmojiPicker"> 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,13 +1,10 @@ -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faChevronDown -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faChevronDown -) +import { set } from 'vue' +import Select from '../select/select.vue' export default { + components: { + Select + }, props: [ 'name', 'label', 'value', 'fallback', 'options', 'no-inherit' ], @@ -39,8 +36,8 @@ export default { return this.dValue.family }, set (v) { - this.lValue.family = v - this.$emit('update:modelValue', this.lValue) + set(this.lValue, 'family', v) + this.$emit('input', this.lValue) } }, isCustom () { 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.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" + :shift-up="!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment" + :shift-dn="!(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/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 @@ -1,24 +1,46 @@ import StillImage from '../still-image/still-image.vue' 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 PinchZoom from '../pinch_zoom/pinch_zoom.vue' +import SwipeClick from '../swipe_click/swipe_click.vue' import GestureService from '../../services/gesture_service/gesture_service' +import Flash from 'src/components/flash/flash.vue' +import fileTypeService from '../../services/file_type/file_type.service.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronLeft, - faChevronRight + faChevronRight, + faCircleNotch, + faTimes } from '@fortawesome/free-solid-svg-icons' library.add( faChevronLeft, - faChevronRight + faChevronRight, + faCircleNotch, + faTimes ) const MediaModal = { components: { StillImage, VideoAttachment, - Modal + PinchZoom, + SwipeClick, + Modal, + Flash + }, + data () { + return { + loading: false, + swipeDirection: GestureService.DIRECTION_LEFT, + swipeThreshold: () => { + const considerableMoveRatio = 1 / 4 + return window.innerWidth * considerableMoveRatio + }, + pinchZoomMinScale: 1, + pinchZoomScaleResetLimit: 1.2 + } }, computed: { showing () { @@ -27,6 +49,9 @@ const MediaModal = { media () { return this.$store.state.mediaViewer.media }, + description () { + return this.currentMedia.description + }, currentIndex () { return this.$store.state.mediaViewer.currentIndex }, @@ -37,43 +62,62 @@ 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 () { - this.mediaSwipeGestureRight = GestureService.swipeGesture( - GestureService.DIRECTION_RIGHT, - this.goPrev, - 50 - ) - this.mediaSwipeGestureLeft = GestureService.swipeGesture( - GestureService.DIRECTION_LEFT, - this.goNext, - 50 - ) - }, methods: { - mediaTouchStart (e) { - GestureService.beginSwipe(e, this.mediaSwipeGestureRight) - GestureService.beginSwipe(e, this.mediaSwipeGestureLeft) - }, - mediaTouchMove (e) { - GestureService.updateSwipe(e, this.mediaSwipeGestureRight) - GestureService.updateSwipe(e, this.mediaSwipeGestureLeft) + getType (media) { + return fileTypeService.fileType(media.mimetype) }, hide () { - this.$store.dispatch('closeMediaViewer') + // HACK: Closing immediately via a touch will cause the click + // to be processed on the content below the overlay + const transitionTime = 100 // ms + setTimeout(() => { + this.$store.dispatch('closeMediaViewer') + }, transitionTime) + }, + hideIfNotSwiped (event) { + // If we have swiped over SwipeClick, do not trigger hide + const comp = this.$refs.swipeClick + if (!comp) { + this.hide() + } else { + comp.$gesture.click(event) + } }, 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 + }, + handleSwipePreview (offsets) { + this.$refs.pinchZoom.setTransform({ scale: 1, x: offsets[0], y: 0 }) + }, + handleSwipeEnd (sign) { + this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 }) + if (sign > 0) { + this.goNext() + } else if (sign < 0) { + this.goPrev() } }, handleKeyupEvent (e) { diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue @@ -2,18 +2,38 @@ <Modal v-if="showing" class="media-modal-view" - @backdropClicked="hide" + @backdropClicked="hideIfNotSwiped" > - <img + <SwipeClick v-if="type === 'image'" - class="modal-image" - :src="currentMedia.url" - :alt="currentMedia.description" - :title="currentMedia.description" - @touchstart.stop="mediaTouchStart" - @touchmove.stop="mediaTouchMove" - @click="hide" + ref="swipeClick" + class="modal-image-container" + :direction="swipeDirection" + :threshold="swipeThreshold" + @preview-requested="handleSwipePreview" + @swipe-finished="handleSwipeEnd" + @swipeless-clicked="hide" > + <PinchZoom + ref="pinchZoom" + class="modal-image-container-inner" + selector=".modal-image" + reach-min-scale-strategy="reset" + stop-propagate-handled="stop-propgate-handled" + :allow-pan-min-scale="pinchZoomMinScale" + :min-scale="pinchZoomMinScale" + :reset-to-min-scale-limit="pinchZoomScaleResetLimit" + > + <img + :class="{ loading }" + class="modal-image" + :src="currentMedia.url" + :alt="currentMedia.description" + :title="currentMedia.description" + @load="onImageLoaded" + > + </PinchZoom> + </SwipeClick> <VideoAttachment v-if="type === 'video'" class="modal-image" @@ -28,38 +48,84 @@ :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')" - class="modal-view-button-arrow modal-view-button-arrow--prev" + class="modal-view-button modal-view-button-arrow modal-view-button-arrow--prev" @click.stop.prevent="goPrev" > <FAIcon - class="arrow-icon" + class="button-icon arrow-icon" icon="chevron-left" /> </button> <button v-if="canNavigate" :title="$t('media_modal.next')" - class="modal-view-button-arrow modal-view-button-arrow--next" + class="modal-view-button modal-view-button-arrow modal-view-button-arrow--next" @click.stop.prevent="goNext" > <FAIcon - class="arrow-icon" + class="button-icon arrow-icon" icon="chevron-right" /> </button> + <button + class="modal-view-button modal-view-button-hide" + :title="$t('media_modal.hide')" + @click.stop.prevent="hide" + > + <FAIcon + class="button-icon" + icon="times" + /> + </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> <script src="./media_modal.js"></script> <style lang="scss"> +$modal-view-button-icon-height: 3em; +$modal-view-button-icon-half-height: calc(#{$modal-view-button-icon-height} / 2); +$modal-view-button-icon-width: 3em; +$modal-view-button-icon-margin: 0.5em; + .modal-view.media-modal-view { z-index: 1001; + flex-direction: column; - .modal-view-button-arrow { + .modal-view-button-arrow, + .modal-view-button-hide { opacity: 0.75; &:focus, @@ -67,69 +133,154 @@ outline: none; box-shadow: none; } + &:hover { opacity: 1; } } + overflow: hidden; } -@keyframes media-fadein { - from { - opacity: 0; +.media-modal-view { + @keyframes media-fadein { + from { + opacity: 0; + } + to { + opacity: 1; + } } - to { - opacity: 1; + + .modal-image-container { + display: flex; + overflow: hidden; + align-items: center; + flex-direction: column; + max-width: 100%; + max-height: 100%; + width: 100%; + height: 100%; + flex-grow: 1; + justify-content: center; + + &-inner { + width: 100%; + height: 100%; + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } } -} -.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; -} + .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; + } + + .description { + flex: 0 0 auto; + overflow-y: auto; + min-height: 1em; + max-width: 500px; + max-height: 9.5em; + word-break: break-all; + } -.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 { + .modal-image { + max-width: 100%; + max-height: 100%; + 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; - top: 35px; - height: 30px; - width: 32px; - font-size: 14px; - line-height: 30px; - color: #FFF; - text-align: center; - background-color: rgba(0,0,0,.3); + pointer-events: none; + display: flex; + justify-content: center; + align-items: center; + + svg { + color: white; + } } - &--prev { - left: 0; + .modal-view-button { + 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); + height: $modal-view-button-icon-height; + width: $modal-view-button-icon-width; + + .button-icon { + position: absolute; + height: $modal-view-button-icon-height; + width: $modal-view-button-icon-width; + font-size: 14px; + line-height: $modal-view-button-icon-height; + color: #FFF; + text-align: center; + background-color: rgba(0,0,0,.3); + } + } + + .modal-view-button-arrow { + position: absolute; + display: block; + top: 50%; + margin-top: $modal-view-button-icon-half-height; + width: $modal-view-button-icon-width; + height: $modal-view-button-icon-height; + .arrow-icon { - left: 6px; + position: absolute; + top: 0; + line-height: $modal-view-button-icon-height; + color: #FFF; + text-align: center; + background-color: rgba(0,0,0,.3); + } + + &--prev { + left: 0; + .arrow-icon { + left: $modal-view-button-icon-margin; + } + } + + &--next { + right: 0; + .arrow-icon { + right: $modal-view-button-icon-margin; + } } } - &--next { + .modal-view-button-hide { + position: absolute; + top: 0; right: 0; - .arrow-icon { - right: 6px; + .button-icon { + top: $modal-view-button-icon-margin; + right: $modal-view-button-icon-margin; } } } 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,115 @@ +@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; + 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/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/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/notification/notification.js b/src/components/notification/notification.js @@ -4,6 +4,7 @@ import Status from '../status/status.vue' import UserAvatar from '../user_avatar/user_avatar.vue' import UserCard from '../user_card/user_card.vue' import Timeago from '../timeago/timeago.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -44,7 +45,8 @@ const Notification = { UserAvatar, UserCard, Timeago, - Status + Status, + 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" /> @@ -51,12 +52,14 @@ <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_ui" - 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 @@ -181,8 +184,9 @@ </router-link> </div> <template v-else> - <status-content + <StatusContent class="faint" + :compact="true" :status="notification.action" /> </template> diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss @@ -37,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 { @@ -148,13 +143,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/pinch_zoom/pinch_zoom.js b/src/components/pinch_zoom/pinch_zoom.js @@ -0,0 +1,13 @@ +import PinchZoom from '@kazvmoe-infra/pinch-zoom-element' + +export default { + methods: { + setTransform ({ scale, x, y }) { + this.$el.setTransform({ scale, x, y }) + } + }, + created () { + // Make lint happy + (() => PinchZoom)() + } +} diff --git a/src/components/pinch_zoom/pinch_zoom.vue b/src/components/pinch_zoom/pinch_zoom.vue @@ -0,0 +1,11 @@ +<template> + <pinch-zoom + class="pinch-zoom-parent" + v-bind="$attrs" + v-on="$listeners" + > + <slot /> + </pinch-zoom> +</template> + +<script src="./pinch_zoom.js"></script> 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> 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,8 @@ .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; } } @@ -162,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.vue b/src/components/popover/popover.vue @@ -33,7 +33,7 @@ @import '../../_variables.scss'; .popover-trigger-button { - display: block; + display: inline-block; } .popover { 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() @@ -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'" @@ -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/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/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,63 @@ + +<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%; + width: 0.875em; + color: $fallback--text; + color: var(--inputText, $fallback--text); + line-height: 28px; + z-index: 0; + pointer-events: none; + } + +} +</style> diff --git a/src/components/settings_modal/helpers/boolean_setting.js b/src/components/settings_modal/helpers/boolean_setting.js @@ -0,0 +1,47 @@ +import { get, set } from 'lodash' +import Checkbox from 'src/components/checkbox/checkbox.vue' +import ModifiedIndicator from './modified_indicator.vue' +import ServerSideIndicator from './server_side_indicator.vue' +export default { + components: { + Checkbox, + ModifiedIndicator, + ServerSideIndicator + }, + props: [ + 'path', + 'disabled', + 'expert' + ], + 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) + }, + isServerSide () { + return this.path.startsWith('serverSide_') + }, + isChanged () { + return !this.path.startsWith('serverSide_') && this.state !== this.defaultState + }, + matchesExpertLevel () { + return (this.expert || 0) <= this.$parent.expertLevel + } + }, + 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 @@ -1,5 +1,6 @@ <template> <label + v-if="matchesExpertLevel" class="BooleanSetting" > <Checkbox @@ -13,45 +14,8 @@ > <slot /> </span> - <ModifiedIndicator :changed="isChanged" /> - </Checkbox> + <ModifiedIndicator :changed="isChanged" /><ServerSideIndicator :server-side="isServerSide" /> </Checkbox> </label> </template> -<script> -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 () { - return get(this.$parent, this.path) - }, - isChanged () { - return get(this.$parent, this.path) !== get(this.$parent, this.pathDefault) - } - }, - methods: { - update (e) { - set(this.$parent, this.path, e) - } - } -} -</script> - -<style lang="scss"> -.BooleanSetting { -} -</style> +<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,48 @@ +import { get, set } from 'lodash' +import Select from 'src/components/select/select.vue' +import ModifiedIndicator from './modified_indicator.vue' +import ServerSideIndicator from './server_side_indicator.vue' +export default { + components: { + Select, + ModifiedIndicator, + ServerSideIndicator + }, + props: [ + 'path', + 'disabled', + 'options', + 'expert' + ], + 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) + }, + isServerSide () { + return this.path.startsWith('serverSide_') + }, + isChanged () { + return !this.path.startsWith('serverSide_') && this.state !== this.defaultState + }, + matchesExpertLevel () { + return (this.expert || 0) <= this.$parent.expertLevel + } + }, + 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,31 @@ +<template> + <label + v-if="matchesExpertLevel" + 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" /> + <ServerSideIndicator :server-side="isServerSide" /> + </label> +</template> + +<script src="./choice_setting.js"></script> + +<style lang="scss"> +.ChoiceSetting { +} +</style> diff --git a/src/components/settings_modal/helpers/integer_setting.js b/src/components/settings_modal/helpers/integer_setting.js @@ -0,0 +1,41 @@ +import { get, set } from 'lodash' +import ModifiedIndicator from './modified_indicator.vue' +export default { + components: { + ModifiedIndicator + }, + props: { + path: String, + disabled: Boolean, + min: Number, + expert: Number + }, + 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 + }, + matchesExpertLevel () { + return (this.expert || 0) <= this.$parent.expertLevel + } + }, + methods: { + update (e) { + set(this.$parent, this.path, parseInt(e.target.value)) + } + } +} diff --git a/src/components/settings_modal/helpers/integer_setting.vue b/src/components/settings_modal/helpers/integer_setting.vue @@ -0,0 +1,23 @@ +<template> + <span + v-if="matchesExpertLevel" + class="IntegerSetting" + > + <label :for="path"> + <slot /> + </label> + <input + :id="path" + class="number-input" + type="number" + step="1" + :disabled="disabled" + :min="min || 0" + :value="state" + @change="update" + > + <ModifiedIndicator :changed="isChanged" /> + </span> +</template> + +<script src="./integer_setting.js"></script> diff --git a/src/components/settings_modal/helpers/server_side_indicator.vue b/src/components/settings_modal/helpers/server_side_indicator.vue @@ -0,0 +1,51 @@ +<template> + <span + v-if="serverSide" + class="ServerSideIndicator" + > + <Popover + trigger="hover" + > + <template v-slot:trigger> + &nbsp; + <FAIcon + icon="server" + :aria-label="$t('settings.setting_server_side')" + /> + </template> + <template v-slot:content> + <div class="serverside-tooltip"> + {{ $t('settings.setting_server_side') }} + </div> + </template> + </Popover> + </span> +</template> + +<script> +import Popover from 'src/components/popover/popover.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faServer } from '@fortawesome/free-solid-svg-icons' + +library.add( + faServer +) + +export default { + components: { Popover }, + props: ['serverSide'] +} +</script> + +<style lang="scss"> +.ServerSideIndicator { + display: inline-block; + position: relative; + + .serverside-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,4 +1,5 @@ import { defaultState as configDefaultState } from 'src/modules/config.js' +import { defaultState as serverSideConfigDefaultState } from 'src/modules/serverSideConfig.js' const SharedComputedObject = () => ({ user () { @@ -22,6 +23,14 @@ const SharedComputedObject = () => ({ } }]) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), + ...Object.keys(serverSideConfigDefaultState) + .map(key => ['serverSide_' + key, { + get () { return this.$store.state.serverSideConfig[key] }, + set (value) { + this.$store.dispatch('setServerSideOption', { name: key, value }) + } + }]) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), // Special cases (need to transform values or perform actions first) useStreamingApi: { get () { return this.$store.getters.mergedConfig.useStreamingApi }, diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js @@ -1,8 +1,9 @@ -import { defineAsyncComponent } from 'vue' 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 Checkbox from 'src/components/checkbox/checkbox.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { cloneDeep } from 'lodash' import { @@ -51,12 +52,15 @@ const SettingsModal = { components: { Modal, Popover, - SettingsModalContent: defineAsyncComponent({ - loader: () => import('./settings_modal_content.vue'), - loadingComponent: PanelLoading, - errorComponent: AsyncComponentError, - delay: 0 - }) + Checkbox, + SettingsModalContent: getResettableAsyncComponent( + () => import('./settings_modal_content.vue'), + { + loading: PanelLoading, + error: AsyncComponentError, + delay: 0 + } + ) }, methods: { closeModal () { @@ -157,6 +161,15 @@ const SettingsModal = { }, modalPeeked () { return this.$store.state.interface.settingsModalState === 'minimized' + }, + expertLevel: { + get () { + return this.$store.state.config.expertLevel > 0 + }, + set (value) { + console.log(value) + this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 }) + } } } } diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss @@ -48,4 +48,11 @@ } } } + + .settings-footer { + display: flex; + >* { + margin-right: 0.5em; + } + } } diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue @@ -11,24 +11,22 @@ {{ $t('settings.settings') }} </span> <transition name="fade"> - <template> - <template v-if="currentSaveStateNotice"> - <div - v-if="currentSaveStateNotice.error" - class="alert error" - @click.prevent - > - {{ $t('settings.saving_err') }} - </div> + <template v-if="currentSaveStateNotice"> + <div + v-if="currentSaveStateNotice.error" + class="alert error" + @click.prevent + > + {{ $t('settings.saving_err') }} + </div> - <div - v-if="!currentSaveStateNotice.error" - class="alert transparent" - @click.prevent - > - {{ $t('settings.saving_ok') }} - </div> - </template> + <div + v-if="!currentSaveStateNotice.error" + class="alert transparent" + @click.prevent + > + {{ $t('settings.saving_ok') }} + </div> </template> </transition> <button @@ -55,7 +53,7 @@ <div class="panel-body"> <SettingsModalContent v-if="modalOpenedOnce" /> </div> - <div class="panel-footer"> + <div class="panel-footer settings-footer"> <Popover class="export" trigger="click" @@ -75,7 +73,7 @@ /> </button> </template> - <template v-slot:content ="{close}"> + <template v-slot:content="{close}"> <div class="dropdown-menu"> <button class="button-default dropdown-item dropdown-item-icon" @@ -110,6 +108,10 @@ </div> </template> </Popover> + + <Checkbox v-model="expertLevel"> + {{ $t("settings.expert_mode") }} + </Checkbox> </div> </div> </Modal> 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,25 @@ import { filter, trim } from 'lodash' import BooleanSetting from '../helpers/boolean_setting.vue' +import ChoiceSetting from '../helpers/choice_setting.vue' +import IntegerSetting from '../helpers/integer_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: { - BooleanSetting + BooleanSetting, + ChoiceSetting, + IntegerSetting }, 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,122 @@ <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> - <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> - </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 + v-if="user" + :disabled="hideFilteredStatuses" + path="hideMutedThreads" + > + {{ $t('settings.hide_muted_threads') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting + v-if="user" + :disabled="hideFilteredStatuses" + path="hideMutedPosts" + > + {{ $t('settings.hide_muted_posts') }} + </BooleanSetting> + </li> + </ul> + </li> + <li> + <BooleanSetting path="muteBotStatuses"> + {{ $t('settings.mute_bot_posts') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="hidePostStats"> + {{ $t('settings.hide_post_stats') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="hideBotIndication"> + {{ $t('settings.hide_bot_indication') }} + </BooleanSetting> + </li> + <ChoiceSetting + v-if="user" + 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> - <BooleanSetting path="hidePostStats"> - {{ $t('settings.hide_post_stats') }} - </BooleanSetting> - </div> - <div> - <BooleanSetting path="hideUserStats"> - {{ $t('settings.hide_user_stats') }} - </BooleanSetting> - </div> + <div>{{ $t('settings.filtering_explanation') }}</div> + </li> + <h3>{{ $t('settings.attachments') }}</h3> + <li v-if="expertLevel > 0"> + <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> + <IntegerSetting + path="maxThumbnails" + :min="0" + > + {{ $t('settings.max_thumbnails') }} + </IntegerSetting> + </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"> - <div> - <p>{{ $t('settings.filtering_explanation') }}</p> - <textarea - id="muteWords" - v-model="muteWordsString" - class="resize-height" - /> - </div> - <div> - <BooleanSetting path="hideFilteredStatuses"> - {{ $t('settings.hide_filtered_statuses') }} - </BooleanSetting> - </div> + <div + v-if="expertLevel > 0" + 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> </template> diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js @@ -1,21 +1,43 @@ import BooleanSetting from '../helpers/boolean_setting.vue' +import ChoiceSetting from '../helpers/choice_setting.vue' +import ScopeSelector from 'src/components/scope_selector/scope_selector.vue' +import IntegerSetting from '../helpers/integer_setting.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' +import ServerSideIndicator from '../helpers/server_side_indicator.vue' 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}`) + })), + conversationDisplayOptions: ['tree', 'linear'].map(mode => ({ + key: mode, + value: mode, + label: this.$t(`settings.conversation_display_${mode}`) + })), + conversationOtherRepliesButtonOptions: ['below', 'inside'].map(mode => ({ + key: mode, + value: mode, + label: this.$t(`settings.conversation_other_replies_button_${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') || @@ -27,18 +49,35 @@ const GeneralTab = { }, components: { BooleanSetting, - InterfaceLanguageSwitcher + ChoiceSetting, + IntegerSetting, + InterfaceLanguageSwitcher, + ScopeSelector, + ServerSideIndicator }, 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() + }, + methods: { + changeDefaultScope (value) { + this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value }) + } } } diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue @@ -11,24 +11,19 @@ {{ $t('settings.hide_isp') }} </BooleanSetting> </li> + <li> + <BooleanSetting path="sidebarRight"> + {{ $t('settings.right_sidebar') }} + </BooleanSetting> + </li> <li v-if="instanceWallpaperUsed"> <BooleanSetting path="hideInstanceWallpaper"> {{ $t('settings.hide_wallpaper') }} </BooleanSetting> </li> - </ul> - </div> - <div class="setting-item"> - <h2>{{ $t('nav.timeline') }}</h2> - <ul class="setting-list"> <li> - <BooleanSetting path="hideMutedPosts"> - {{ $t('settings.hide_muted_posts') }} - </BooleanSetting> - </li> - <li> - <BooleanSetting path="collapseMessageWithSubject"> - {{ $t('settings.collapse_subject') }} + <BooleanSetting path="stopGifs"> + {{ $t('settings.stop_gifs') }} </BooleanSetting> </li> <li> @@ -50,146 +45,126 @@ </ul> </li> <li> - <BooleanSetting path="useStreamingApi"> + <BooleanSetting + path="useStreamingApi" + expert="1" + > {{ $t('settings.useStreamingApi') }} - <br> - <small> - {{ $t('settings.useStreamingApiWarning') }} - </small> - </BooleanSetting> - </li> - <li> - <BooleanSetting path="emojiReactionsOnTimeline"> - {{ $t('settings.emoji_reactions_on_timeline') }} </BooleanSetting> </li> <li> - <BooleanSetting path="virtualScrolling"> + <BooleanSetting + path="virtualScrolling" + expert="1" + > {{ $t('settings.virtual_scrolling') }} </BooleanSetting> </li> - </ul> - </div> - - <div class="setting-item"> - <h2>{{ $t('settings.composing') }}</h2> - <ul class="setting-list"> - <li> - <BooleanSetting path="scopeCopy"> - {{ $t('settings.scope_copy') }} - </BooleanSetting> - </li> - <li> - <BooleanSetting path="alwaysShowSubjectInput"> - {{ $t('settings.subject_input_always_show') }} - </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> - <BooleanSetting path="minimalScopesMode"> - {{ $t('settings.minimal_scopes_mode') }} - </BooleanSetting> - </li> <li> - <BooleanSetting path="sensitiveByDefault"> - {{ $t('settings.sensitive_by_default') }} + <BooleanSetting + path="alwaysShowNewPostButton" + expert="1" + > + {{ $t('settings.always_show_post_button') }} </BooleanSetting> </li> <li> - <BooleanSetting path="autohideFloatingPostButton"> + <BooleanSetting + path="autohideFloatingPostButton" + expert="1" + > {{ $t('settings.autohide_floating_post_button') }} </BooleanSetting> </li> - <li> - <BooleanSetting path="padEmoji"> - {{ $t('settings.pad_emoji') }} + <li v-if="instanceShoutboxPresent"> + <BooleanSetting + path="hideShoutbox" + expert="1" + > + {{ $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> - <BooleanSetting path="hideAttachments"> - {{ $t('settings.hide_attachments_in_tl') }} + <ChoiceSetting + id="conversationDisplay" + path="conversationDisplay" + :options="conversationDisplayOptions" + > + {{ $t('settings.conversation_display') }} + </ChoiceSetting> + </li> + <ul + v-if="conversationDisplay !== 'linear'" + class="setting-list suboptions" + > + <li> + <BooleanSetting path="conversationTreeAdvanced"> + {{ $t('settings.tree_advanced') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting + path="conversationTreeFadeAncestors" + :expert="1" + > + {{ $t('settings.tree_fade_ancestors') }} + </BooleanSetting> + </li> + <li> + <IntegerSetting + path="maxDepthInThread" + :min="3" + :expert="1" + > + {{ $t('settings.max_depth_in_thread') }} + </IntegerSetting> + </li> + <li> + <ChoiceSetting + id="conversationOtherRepliesButton" + path="conversationOtherRepliesButton" + :options="conversationOtherRepliesButtonOptions" + :expert="1" + > + {{ $t('settings.conversation_other_replies_button') }} + </ChoiceSetting> + </li> + </ul> + <li> + <BooleanSetting path="collapseMessageWithSubject"> + {{ $t('settings.collapse_subject') }} </BooleanSetting> </li> <li> - <BooleanSetting path="hideAttachmentsInConv"> - {{ $t('settings.hide_attachments_in_convo') }} + <BooleanSetting + path="emojiReactionsOnTimeline" + expert="1" + > + {{ $t('settings.emoji_reactions_on_timeline') }} </BooleanSetting> </li> <li> - <label for="maxThumbnails"> - {{ $t('settings.max_thumbnails') }} - </label> - <input - id="maxThumbnails" - path.number="maxThumbnails" - class="number-input" - type="number" - min="0" - step="1" + <BooleanSetting + v-if="user" + path="serverSide_stripRichContent" + expert="1" > + {{ $t('settings.no_rich_text_description') }} + </BooleanSetting> + </li> + <h3>{{ $t('settings.attachments') }}</h3> + <li> + <BooleanSetting + path="useContainFit" + expert="1" + > + {{ $t('settings.use_contain_fit') }} + </BooleanSetting> </li> <li> <BooleanSetting path="hideNsfw"> @@ -200,6 +175,7 @@ <li> <BooleanSetting path="preloadImage" + expert="1" :disabled="!hideNsfw" > {{ $t('settings.preload_images') }} @@ -208,6 +184,7 @@ <li> <BooleanSetting path="useOneClickNsfw" + expert="1" :disabled="!hideNsfw" > {{ $t('settings.use_one_click_nsfw') }} @@ -215,12 +192,10 @@ </li> </ul> <li> - <BooleanSetting path="stopGifs"> - {{ $t('settings.stop_gifs') }} - </BooleanSetting> - </li> - <li> - <BooleanSetting path="loopVideo"> + <BooleanSetting + path="loopVideo" + expert="1" + > {{ $t('settings.loop_video') }} </BooleanSetting> <ul @@ -230,6 +205,7 @@ <li> <BooleanSetting path="loopVideoSilentOnly" + expert="1" :disabled="!loopVideo || !loopSilentAvailable" > {{ $t('settings.loop_video_silent_only') }} @@ -244,35 +220,175 @@ </ul> </li> <li> - <BooleanSetting path="playVideosInModal"> + <BooleanSetting + path="playVideosInModal" + expert="1" + > {{ $t('settings.play_videos_in_modal') }} </BooleanSetting> </li> + <h3>{{ $t('settings.mention_links') }}</h3> <li> - <BooleanSetting path="useContainFit"> - {{ $t('settings.use_contain_fit') }} + <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" + expert="1" + > + {{ $t('settings.mention_link_show_tooltip') }} + </BooleanSetting> + </li> + </ul> + <li> + <BooleanSetting + path="useAtIcon" + expert="1" + > + {{ $t('settings.use_at_icon') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="mentionLinkShowAvatar"> + {{ $t('settings.mention_link_show_avatar') }} </BooleanSetting> </li> - </ul> - </div> - - <div class="setting-item"> - <h2>{{ $t('settings.notifications') }}</h2> - <ul class="setting-list"> <li> - <BooleanSetting path="webPushNotifications"> - {{ $t('settings.enable_web_push_notifications') }} + <BooleanSetting + path="mentionLinkFadeDomain" + expert="1" + > + {{ $t('settings.mention_link_fade_domain') }} + </BooleanSetting> + </li> + <li v-if="user"> + <BooleanSetting + path="mentionLinkBoldenYou" + expert="1" + > + {{ $t('settings.mention_link_bolden_you') }} + </BooleanSetting> + </li> + <h3 v-if="expertLevel > 0"> + {{ $t('settings.fun') }} + </h3> + <li> + <BooleanSetting + path="greentext" + expert="1" + > + {{ $t('settings.greentext') }} + </BooleanSetting> + </li> + <li v-if="user"> + <BooleanSetting + path="mentionLinkShowYous" + expert="1" + > + {{ $t('settings.show_yous') }} </BooleanSetting> </li> </ul> </div> - <div class="setting-item"> - <h2>{{ $t('settings.fun') }}</h2> + <div + v-if="user" + class="setting-item" + > + <h2>{{ $t('settings.composing') }}</h2> <ul class="setting-list"> <li> - <BooleanSetting path="greentext"> - {{ $t('settings.greentext') }} + <label for="default-vis"> + {{ $t('settings.default_vis') }} <ServerSideIndicator :server-side="true" /> + <ScopeSelector + class="scope-selector" + :show-all="true" + :user-default="serverSide_defaultScope" + :initial-scope="serverSide_defaultScope" + :on-scope-change="changeDefaultScope" + /> + </label> + </li> + <li> + <!-- <BooleanSetting path="serverSide_defaultNSFW"> --> + <BooleanSetting path="sensitiveByDefault"> + {{ $t('settings.sensitive_by_default') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting + path="scopeCopy" + expert="1" + > + {{ $t('settings.scope_copy') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting + path="alwaysShowSubjectInput" + expert="1" + > + {{ $t('settings.subject_input_always_show') }} + </BooleanSetting> + </li> + <li> + <ChoiceSetting + id="subjectLineBehavior" + path="subjectLineBehavior" + :options="subjectLineOptions" + expert="1" + > + {{ $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" + expert="1" + > + {{ $t('settings.minimal_scopes_mode') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting + path="alwaysShowNewPostButton" + expert="1" + > + {{ $t('settings.always_show_post_button') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting + path="autohideFloatingPostButton" + expert="1" + > + {{ $t('settings.autohide_floating_post_button') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting + path="padEmoji" + expert="1" + > + {{ $t('settings.pad_emoji') }} </BooleanSetting> </li> </ul> diff --git a/src/components/settings_modal/tabs/notifications_tab.js b/src/components/settings_modal/tabs/notifications_tab.js @@ -1,4 +1,5 @@ -import Checkbox from 'src/components/checkbox/checkbox.vue' +import BooleanSetting from '../helpers/boolean_setting.vue' +import SharedComputedObject from '../helpers/shared_computed_object.js' const NotificationsTab = { data () { @@ -9,12 +10,13 @@ const NotificationsTab = { } }, components: { - Checkbox + BooleanSetting }, computed: { user () { return this.$store.state.users.currentUser - } + }, + ...SharedComputedObject() }, methods: { updateNotificationSettings () { diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue @@ -2,30 +2,77 @@ <div :label="$t('settings.notifications')"> <div class="setting-item"> <h2>{{ $t('settings.notification_setting_filters') }}</h2> - <p> - <Checkbox v-model="notificationSettings.block_from_strangers"> - {{ $t('settings.notification_setting_block_from_strangers') }} - </Checkbox> - </p> + <ul class="setting-list"> + <li> + <BooleanSetting path="serverSide_blockNotificationsFromStrangers"> + {{ $t('settings.notification_setting_block_from_strangers') }} + </BooleanSetting> + </li> + <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 class="setting-item"> + <div + v-if="expertLevel > 0" + class="setting-item" + > <h2>{{ $t('settings.notification_setting_privacy') }}</h2> - <p> - <Checkbox v-model="notificationSettings.hide_notification_contents"> - {{ $t('settings.notification_setting_hide_notification_contents') }} - </Checkbox> - </p> + <ul class="setting-list"> + <li> + <BooleanSetting + path="webPushNotifications" + expert="1" + > + {{ $t('settings.enable_web_push_notifications') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting + path="serverSide_webPushHideContents" + expert="1" + > + {{ $t('settings.notification_setting_hide_notification_contents') }} + </BooleanSetting> + </li> + </ul> </div> <div class="setting-item"> <p>{{ $t('settings.notification_mutes') }}</p> <p>{{ $t('settings.notification_blocks') }}</p> - <button - class="btn button-default" - @click="updateNotificationSettings" - > - {{ $t('settings.save') }} - </button> </div> </div> </template> diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js @@ -8,6 +8,9 @@ import EmojiInput from 'src/components/emoji_input/emoji_input.vue' import suggestor from 'src/components/emoji_input/suggestor.js' import Autosuggest from 'src/components/autosuggest/autosuggest.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' +import BooleanSetting from '../helpers/boolean_setting.vue' +import SharedComputedObject from '../helpers/shared_computed_object.js' + import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes, @@ -24,21 +27,13 @@ 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, - newDefaultScope: this.$store.state.users.currentUser.default_scope, newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })), - hideFollows: this.$store.state.users.currentUser.hide_follows, - hideFollowers: this.$store.state.users.currentUser.hide_followers, - hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count, - hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count, showRole: this.$store.state.users.currentUser.show_role, role: this.$store.state.users.currentUser.role, - discoverable: this.$store.state.users.currentUser.discoverable, bot: this.$store.state.users.currentUser.bot, - allowFollowingMove: this.$store.state.users.currentUser.allow_following_move, pickAvatarBtnVisible: true, bannerUploading: false, backgroundUploading: false, @@ -54,12 +49,14 @@ const ProfileTab = { EmojiInput, Autosuggest, ProgressButton, - Checkbox + Checkbox, + BooleanSetting }, computed: { user () { return this.$store.state.users.currentUser }, + ...SharedComputedObject(), emojiUserSuggestor () { return suggestor({ emoji: [ @@ -123,15 +120,7 @@ const ProfileTab = { /* eslint-disable camelcase */ display_name: this.newName, fields_attributes: this.newFields.filter(el => el != null), - default_scope: this.newDefaultScope, - no_rich_text: this.newNoRichText, - hide_follows: this.hideFollows, - hide_followers: this.hideFollowers, - discoverable: this.discoverable, bot: this.bot, - allow_following_move: this.allowFollowingMove, - hide_follows_count: this.hideFollowsCount, - hide_followers_count: this.hideFollowersCount, show_role: this.showRole /* eslint-enable camelcase */ } }).then((user) => { diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue @@ -25,61 +25,6 @@ class="bio resize-height" /> </EmojiInput> - <p> - <Checkbox v-model="newLocked"> - {{ $t('settings.lock_account_description') }} - </Checkbox> - </p> - <div> - <label for="default-vis">{{ $t('settings.default_vis') }}</label> - <div - id="default-vis" - class="visibility-tray" - > - <scope-selector - :show-all="true" - :user-default="newDefaultScope" - :initial-scope="newDefaultScope" - :on-scope-change="changeVis" - /> - </div> - </div> - <p> - <Checkbox v-model="newNoRichText"> - {{ $t('settings.no_rich_text_description') }} - </Checkbox> - </p> - <p> - <Checkbox v-model="hideFollows"> - {{ $t('settings.hide_follows_description') }} - </Checkbox> - </p> - <p class="setting-subitem"> - <Checkbox - v-model="hideFollowsCount" - :disabled="!hideFollows" - > - {{ $t('settings.hide_follows_count_description') }} - </Checkbox> - </p> - <p> - <Checkbox v-model="hideFollowers"> - {{ $t('settings.hide_followers_description') }} - </Checkbox> - </p> - <p class="setting-subitem"> - <Checkbox - v-model="hideFollowersCount" - :disabled="!hideFollowers" - > - {{ $t('settings.hide_followers_count_description') }} - </Checkbox> - </p> - <p> - <Checkbox v-model="allowFollowingMove"> - {{ $t('settings.allow_following_move') }} - </Checkbox> - </p> <p v-if="role === 'admin' || role === 'moderator'"> <Checkbox v-model="showRole"> <template v-if="role === 'admin'"> @@ -90,11 +35,6 @@ </template> </Checkbox> </p> - <p> - <Checkbox v-model="discoverable"> - {{ $t('settings.discoverable') }} - </Checkbox> - </p> <div v-if="maxFields > 0"> <p>{{ $t('settings.profile_fields.label') }}</p> <div @@ -269,6 +209,67 @@ {{ $t('settings.save') }} </button> </div> + <div class="setting-item"> + <h2>{{ $t('settings.account_privacy') }}</h2> + <ul class="setting-list"> + <li> + <BooleanSetting path="serverSide_locked"> + {{ $t('settings.lock_account_description') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="serverSide_discoverable"> + {{ $t('settings.discoverable') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="serverSide_allowFollowingMove"> + {{ $t('settings.allow_following_move') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="serverSide_hideFavorites"> + {{ $t('settings.hide_favorites_description') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="serverSide_hideFollowers"> + {{ $t('settings.hide_followers_description') }} + </BooleanSetting> + <ul + class="setting-list suboptions" + :class="[{disabled: !serverSide_hideFollowers}]" + > + <li> + <BooleanSetting + path="serverSide_hideFollowersCount" + :disabled="!serverSide_hideFollowers" + > + {{ $t('settings.hide_followers_count_description') }} + </BooleanSetting> + </li> + </ul> + </li> + <li> + <BooleanSetting path="serverSide_hideFollows"> + {{ $t('settings.hide_follows_description') }} + </BooleanSetting> + <ul + class="setting-list suboptions" + :class="[{disabled: !serverSide_hideFollows}]" + > + <li> + <BooleanSetting + path="serverSide_hideFollowsCount" + :disabled="!serverSide_hideFollows" + > + {{ $t('settings.hide_follows_count_description') }} + </BooleanSetting> + </li> + </ul> + </li> + </ul> + </div> </div> </template> diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -35,16 +35,9 @@ 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.jsx' 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 = [ @@ -79,7 +72,8 @@ export default { getExportedObject: () => this.exportedTheme }), availableStyles: [], - selected: this.$store.getters.mergedConfig.theme, + selected: '', + selectedTheme: this.$store.getters.mergedConfig.theme, themeWarning: undefined, tempImportFile: undefined, engineVersion: 0, @@ -213,7 +207,7 @@ export default { } }, selectedVersion () { - return Array.isArray(this.selected) ? 1 : 2 + return Array.isArray(this.selectedTheme) ? 1 : 2 }, currentColors () { return Object.keys(SLOT_INHERITANCE) @@ -394,7 +388,8 @@ export default { FontControl, TabSwitcher, Preview, - Checkbox + Checkbox, + Select }, methods: { loadTheme ( @@ -479,7 +474,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() @@ -750,6 +745,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) { @@ -767,17 +772,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 @@ -55,7 +55,7 @@ for="preset-switcher" class="select" > - <select + <Select id="preset-switcher" v-model="selected" class="preset-switcher" @@ -63,7 +63,7 @@ <option v-for="style in availableStyles" :key="style.name" - :value="style" + :value="style.name || style[0]" :style="{ backgroundColor: style[1] || (style.theme || style.source).colors.bg, color: style[3] || (style.theme || style.source).colors.text @@ -71,11 +71,7 @@ > {{ style[0] || style.name }} </option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> + </Select> </label> </div> <div class="export-import"> @@ -907,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/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' @@ -32,7 +35,10 @@ import { faStar, faEyeSlash, faEye, - faThumbtack + faThumbtack, + faChevronUp, + faChevronDown, + faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons' library.add( @@ -49,9 +55,47 @@ library.add( faEllipsisH, faEyeSlash, faEye, - faThumbtack + faThumbtack, + faChevronUp, + faChevronDown, + faAngleDoubleRight ) +const camelCase = name => name.charAt(0).toUpperCase() + name.slice(1) + +const controlledOrUncontrolledGetters = list => list.reduce((res, name) => { + const camelized = camelCase(name) + const toggle = `controlledToggle${camelized}` + const controlledName = `controlled${camelized}` + const uncontrolledName = `uncontrolled${camelized}` + res[name] = function () { + return this[toggle] ? this[controlledName] : this[uncontrolledName] + } + return res +}, {}) + +const controlledOrUncontrolledToggle = (obj, name) => { + const camelized = camelCase(name) + const toggle = `controlledToggle${camelized}` + const uncontrolledName = `uncontrolled${camelized}` + if (obj[toggle]) { + obj[toggle]() + } else { + obj[uncontrolledName] = !obj[uncontrolledName] + } +} + +const controlledOrUncontrolledSet = (obj, name, val) => { + const camelized = camelCase(name) + const set = `controlledSet${camelized}` + const uncontrolledName = `uncontrolled${camelized}` + if (obj[set]) { + obj[set](val) + } else { + obj[uncontrolledName] = val + } +} + const Status = { name: 'Status', components: { @@ -68,7 +112,10 @@ const Status = { StatusPopover, UserListPopover, EmojiReactions, - StatusContent + StatusContent, + RichContent, + MentionLink, + MentionsLine }, props: [ 'statusoid', @@ -84,19 +131,37 @@ const Status = { 'showPinned', 'inProfile', 'profileUserId', - 'virtualHidden' + + 'simpleTree', + 'controlledThreadDisplayStatus', + 'controlledToggleThreadDisplay', + 'showOtherRepliesAsButton', + + 'controlledShowingTall', + 'controlledToggleShowingTall', + 'controlledExpandingSubject', + 'controlledToggleExpandingSubject', + 'controlledShowingLongSubject', + 'controlledToggleShowingLongSubject', + 'controlledReplying', + 'controlledToggleReplying', + 'controlledMediaPlaying', + 'controlledSetMediaPlaying', + 'dive' ], data () { return { - replying: false, + uncontrolledReplying: false, unmuted: false, userExpanded: false, - mediaPlaying: [], + uncontrolledMediaPlaying: [], suspendable: true, - error: null + error: null, + headTailLinks: null } }, computed: { + ...controlledOrUncontrolledGetters(['replying', 'mediaPlaying']), muteWords () { return this.mergedConfig.muteWords }, @@ -133,12 +198,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 }, + retweeterUser () { return this.statusoid.user }, retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name_ui }, - retweeterHtml () { return this.statusoid.user.name_html }, + retweeterHtml () { return this.statusoid.user.name }, retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) }, status () { if (this.retweet) { @@ -157,27 +225,60 @@ const Status = { muteWordHits () { return muteWordHits(this.status, this.muteWords) }, + botStatus () { + return this.status.user.bot + }, + botIndicator () { + return this.botStatus && !this.hideBotIndication + }, + 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 || + // bot status + (this.muteBotStatuses && this.botStatus && !this.compact) + 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) @@ -190,14 +291,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 @@ -247,6 +360,12 @@ const Status = { hidePostStats () { return this.mergedConfig.hidePostStats }, + muteBotStatuses () { + return this.mergedConfig.muteBotStatuses + }, + hideBotIndication () { + return this.mergedConfig.hideBotIndication + }, currentUser () { return this.$store.state.users.currentUser }, @@ -258,6 +377,12 @@ const Status = { }, isSuspendable () { return !this.replying && this.mediaPlaying.length === 0 + }, + inThreadForest () { + return !!this.controlledThreadDisplayStatus + }, + threadShowing () { + return this.controlledThreadDisplayStatus === 'showing' } }, methods: { @@ -280,7 +405,7 @@ const Status = { this.error = undefined }, toggleReplying () { - this.replying = !this.replying + controlledOrUncontrolledToggle(this, 'replying') }, gotoOriginal (id) { if (this.inConversation) { @@ -300,16 +425,21 @@ const Status = { return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) }, addMediaPlaying (id) { - this.mediaPlaying.push(id) + controlledOrUncontrolledSet(this, 'mediaPlaying', this.mediaPlaying.concat(id)) }, removeMediaPlaying (id) { - this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id) - } - }, - watch: { - 'highlight': function (id) { + controlledOrUncontrolledSet(this, 'mediaPlaying', this.mediaPlaying.filter(mediaId => mediaId !== id)) + }, + setHeadTailLinks (headTailLinks) { + this.headTailLinks = headTailLinks + }, + toggleThreadDisplay () { + this.controlledToggleThreadDisplay() + }, + scrollIfHighlighted (highlightId) { + const id = highlightId if (this.status.id === id) { - let rect = this.$refs.root.getBoundingClientRect() + let rect = this.$el.getBoundingClientRect() if (rect.top < 100) { // Post is above screen, match its top to screen top window.scrollBy(0, rect.top - 100) @@ -321,6 +451,11 @@ const Status = { window.scrollBy(0, rect.bottom - window.innerHeight + 50) } } + } + }, + watch: { + 'highlight': function (id) { + this.scrollIfHighlighted(id) }, 'status.repeat_num': function (num) { // refetch repeats when repeat_num is changed in any way @@ -337,6 +472,11 @@ const Status = { 'isSuspendable': function (val) { this.suspendable = val } + }, + filters: { + capitalize: function (str) { + return str.charAt(0).toUpperCase() + str.slice(1) + } } } diff --git a/src/components/status/status.scss b/src/components/status/status.scss @@ -1,10 +1,10 @@ - @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; @@ -26,15 +26,8 @@ $status-margin: 0.75em; --icon: var(--selectedPostIcon, $fallback--icon); } - &.-conversation { - border-left-width: 4px; - border-left-style: solid; - border-left-color: $fallback--cRed; - border-left-color: var(--cRed, $fallback--cRed); - } - .gravestone { - padding: $status-margin; + padding: var(--status-margin, $status-margin); color: $fallback--faint; color: var(--faint, $fallback--faint); display: flex; @@ -47,7 +40,7 @@ $status-margin: 0.75em; .status-container { display: flex; - padding: $status-margin; + padding: var(--status-margin, $status-margin); &.-repeat { padding-top: 0; @@ -55,7 +48,7 @@ $status-margin: 0.75em; } .pin { - padding: $status-margin $status-margin 0; + padding: var(--status-margin, $status-margin) var(--status-margin, $status-margin) 0; display: flex; align-items: center; justify-content: flex-end; @@ -71,7 +64,7 @@ $status-margin: 0.75em; } .left-side { - margin-right: $status-margin; + margin-right: var(--status-margin, $status-margin); } .right-side { @@ -80,7 +73,7 @@ $status-margin: 0.75em; } .usercard { - margin-bottom: $status-margin; + margin-bottom: var(--status-margin, $status-margin); } .status-username { @@ -93,12 +86,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 +144,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 +204,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; @@ -250,7 +239,7 @@ $status-margin: 0.75em; } .repeat-info { - padding: 0.4em $status-margin; + padding: 0.4em var(--status-margin, $status-margin); .repeat-icon { color: $fallback--cGreen; @@ -296,7 +285,7 @@ $status-margin: 0.75em; position: relative; width: 100%; display: flex; - margin-top: $status-margin; + margin-top: var(--status-margin, $status-margin); > * { max-width: 4em; @@ -364,7 +353,7 @@ $status-margin: 0.75em; } .favs-repeated-users { - margin-top: $status-margin; + margin-top: var(--status-margin, $status-margin); } .stats { @@ -391,7 +380,7 @@ $status-margin: 0.75em; } .stat-count { - margin-right: $status-margin; + margin-right: var(--status-margin, $status-margin); user-select: none; .stat-title { 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" ref="root" @@ -79,6 +78,7 @@ <UserAvatar v-if="retweet" class="left-side repeater-avatar" + :bot="botIndicator" :better-shadow="betterShadow" :user="statusoid.user" /> @@ -90,8 +90,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" @@ -122,6 +126,7 @@ @click.stop.prevent.capture.native="toggleUserExpanded" > <UserAvatar + :bot="botIndicator" :compact="compact" :better-shadow="betterShadow" :user="status.user" @@ -146,8 +151,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" @@ -213,13 +222,40 @@ class="fa-scale-110" /> </button> + <button + v-if="inThreadForest && replies && replies.length && !simpleTree" + class="button-unstyled" + :title="threadShowing ? $t('status.thread_hide') : $t('status.thread_show')" + :aria-expanded="threadShowing ? 'true' : 'false'" + @click.prevent="toggleThreadDisplay" + > + <FAIcon + fixed-width + class="fa-scale-110" + :icon="threadShowing ? 'chevron-up' : 'chevron-down'" + /> + </button> + <button + v-if="dive && !simpleTree" + class="button-unstyled" + :title="$t('status.show_only_conversation_under_this')" + @click.prevent="dive" + > + <FAIcon + fixed-width + class="fa-scale-110" + :icon="'angle-double-right'" + /> + </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" @@ -239,7 +275,7 @@ flip="horizontal" /> <span - class="faint-link reply-to-text" + class="reply-to-text" > {{ $t('status.reply_to') }} </span> @@ -252,50 +288,95 @@ > <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" + :controlled-showing-tall="controlledShowingTall" + :controlled-expanding-subject="controlledExpandingSubject" + :controlled-showing-long-subject="controlledShowingLongSubject" + :controlled-toggle-showing-tall="controlledToggleShowingTall" + :controlled-toggle-expanding-subject="controlledToggleExpandingSubject" + :controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject" @mediaplay="addMediaPlaying($event)" @mediapause="removeMediaPlaying($event)" + @parseReady="setHeadTailLinks" /> + <div + v-if="inConversation && !isPreview && replies && replies.length" + class="replies" + > + <button + v-if="showOtherRepliesAsButton && replies.length > 1" + class="button-unstyled -link faint" + :title="$tc('status.ancestor_follow', replies.length - 1, { numReplies: replies.length - 1 })" + @click.prevent="dive" + > + {{ $tc('status.replies_list_with_others', replies.length - 1, { numReplies: replies.length - 1 }) }} + </button> + <span + v-else + 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" @@ -373,7 +454,10 @@ class="gravestone" > <div class="left-side"> - <UserAvatar :compact="compact" /> + <UserAvatar + :compact="compact" + :bot="botIndicator" + /> </div> <div class="right-side"> <div class="deleted-text"> @@ -403,7 +487,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,131 @@ +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', + 'showingTall', + 'expandingSubject', + 'showingLongSubject', + 'toggleShowingTall', + 'toggleExpandingSubject', + 'toggleShowingLongSubject' + ], + data () { + return { + 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.toggleShowingTall() + } else if (this.mightHideBecauseSubject) { + this.toggleExpandingSubject() + } + }, + 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="toggleShowingLongSubject" + > + {{ $t("status.hide_full_subject") }} + </button> + <button + v-else-if="longSubject" + class="button-unstyled -link tall-subject-hider" + @click.prevent="toggleShowingLongSubject" + > + {{ $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 { @@ -26,62 +23,58 @@ library.add( faPollH ) +const camelCase = name => name.charAt(0).toUpperCase() + name.slice(1) + +const controlledOrUncontrolledGetters = list => list.reduce((res, name) => { + const camelized = camelCase(name) + const toggle = `controlledToggle${camelized}` + const controlledName = `controlled${camelized}` + const uncontrolledName = `uncontrolled${camelized}` + res[name] = function () { + return this[toggle] ? this[controlledName] : this[uncontrolledName] + } + return res +}, {}) + +const controlledOrUncontrolledToggle = (obj, name) => { + const camelized = camelCase(name) + const toggle = `controlledToggle${camelized}` + const uncontrolledName = `uncontrolled${camelized}` + if (obj[toggle]) { + obj[toggle]() + } else { + obj[uncontrolledName] = !obj[uncontrolledName] + } +} + const StatusContent = { name: 'StatusContent', props: [ 'status', + 'compact', 'focused', 'noHeading', 'fullContent', 'singleLine', - 'compact' + 'controlledShowingTall', + 'controlledExpandingSubject', + 'controlledToggleShowingTall', + 'controlledToggleExpandingSubject' ], data () { return { - showingTall: this.fullContent || (this.inConversation && this.focused), - showingLongSubject: false, + uncontrolledShowingTall: this.fullContent || (this.inConversation && this.focused), + uncontrolledShowingLongSubject: false, // not as computed because it sets the initial state which will be changed later - expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject + uncontrolledExpandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject } }, computed: { - localCollapseSubjectDefault () { - return this.mergedConfig.collapseMessageWithSubject - }, + ...controlledOrUncontrolledGetters(['showingTall', 'expandingSubject', 'showingLongSubject']), 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 @@ -92,72 +85,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 }) }, @@ -165,47 +106,18 @@ const StatusContent = { Attachment, Poll, Gallery, - LinkPreview + LinkPreview, + StatusBody }, 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 - } + toggleShowingTall () { + controlledOrUncontrolledToggle(this, 'showingTall') }, - generateUserProfileLink (id, name) { - return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) + toggleExpandingSubject () { + controlledOrUncontrolledToggle(this, 'expandingSubject') }, - generateTagLink (tag) { - return `/tag/${tag}` + toggleShowingLongSubject () { + controlledOrUncontrolledToggle(this, 'showingLongSubject') }, setMedia () { const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue @@ -1,294 +1,65 @@ <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" + :showing-tall="showingTall" + :expanding-subject="expandingSubject" + :showing-long-subject="showingLongSubject" + :toggle-showing-tall="toggleShowingTall" + :toggle-expanding-subject="toggleExpandingSubject" + :toggle-showing-long-subject="toggleShowingLongSubject" + @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> - <div v-if="status.poll && status.poll.options && !hideSubjectStatus"> - <poll :base-poll="status.poll" /> - </div> - - <div - v-if="status.attachments.length !== 0 && (!hideSubjectStatus || showingLongSubject)" - class="attachments media-body" - > - <attachment - v-for="attachment in nonGalleryAttachments" - :key="attachment.id" - class="non-gallery" - :size="attachmentSize" + <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> <style lang="scss"> -@import '../../_variables.scss'; - -$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/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" @@ -18,6 +19,7 @@ @load="onLoad" @error="onError" > + <slot /> </div> </template> @@ -30,7 +32,7 @@ position: relative; line-height: 0; overflow: hidden; - display: flex; + display: inline-flex; align-items: center; canvas { @@ -47,12 +49,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/swipe_click/swipe_click.js b/src/components/swipe_click/swipe_click.js @@ -0,0 +1,84 @@ +import GestureService from '../../services/gesture_service/gesture_service' + +/** + * props: + * direction: a vector that indicates the direction of the intended swipe + * threshold: the minimum distance in pixels the swipe has moved on `direction' + * for swipe-finished() to have a non-zero sign + * perpendicularTolerance: see gesture_service + * + * Events: + * preview-requested(offsets) + * Emitted when the pointer has moved. + * offsets: the offsets from the start of the swipe to the current cursor position + * + * swipe-canceled() + * Emitted when the swipe has been canceled due to a pointercancel event. + * + * swipe-finished(sign: 0|-1|1) + * Emitted when the swipe has finished. + * sign: if the swipe does not meet the threshold, 0 + * if the swipe meets the threshold in the positive direction, 1 + * if the swipe meets the threshold in the negative direction, -1 + * + * swipeless-clicked() + * Emitted when there is a click without swipe. + * This and swipe-finished() cannot be emitted for the same pointerup event. + */ +const SwipeClick = { + props: { + direction: { + type: Array + }, + threshold: { + type: Function, + default: () => 30 + }, + perpendicularTolerance: { + type: Number, + default: 1.0 + } + }, + methods: { + handlePointerDown (event) { + this.$gesture.start(event) + }, + handlePointerMove (event) { + this.$gesture.move(event) + }, + handlePointerUp (event) { + this.$gesture.end(event) + }, + handlePointerCancel (event) { + this.$gesture.cancel(event) + }, + handleNativeClick (event) { + this.$gesture.click(event) + }, + preview (offsets) { + this.$emit('preview-requested', offsets) + }, + end (sign) { + this.$emit('swipe-finished', sign) + }, + click () { + this.$emit('swipeless-clicked') + }, + cancel () { + this.$emit('swipe-canceled') + } + }, + created () { + this.$gesture = new GestureService.SwipeAndClickGesture({ + direction: this.direction, + threshold: this.threshold, + perpendicularTolerance: this.perpendicularTolerance, + swipePreviewCallback: this.preview, + swipeEndCallback: this.end, + swipeCancelCallback: this.cancel, + swipelessClickCallback: this.click + }) + } +} + +export default SwipeClick diff --git a/src/components/swipe_click/swipe_click.vue b/src/components/swipe_click/swipe_click.vue @@ -0,0 +1,14 @@ +<template> + <div + v-bind="$attrs" + @pointerdown="handlePointerDown" + @pointermove="handlePointerMove" + @pointerup="handlePointerUp" + @pointercancel="handlePointerCancel" + @click="handleNativeClick" + > + <slot /> + </div> +</template> + +<script src="./swipe_click.js"></script> diff --git a/src/components/thread_tree/thread_tree.js b/src/components/thread_tree/thread_tree.js @@ -0,0 +1,90 @@ +import Status from '../status/status.vue' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faAngleDoubleDown, + faAngleDoubleRight +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faAngleDoubleDown, + faAngleDoubleRight +) + +const ThreadTree = { + components: { + Status + }, + name: 'ThreadTree', + props: { + depth: Number, + status: Object, + inProfile: Boolean, + conversation: Array, + collapsable: Boolean, + isExpanded: Boolean, + pinnedStatusIdsObject: Object, + profileUserId: String, + + focused: Function, + highlight: String, + getReplies: Function, + setHighlight: Function, + toggleExpanded: Function, + + simple: Boolean, + // to control display of the whole thread forest + toggleThreadDisplay: Function, + threadDisplayStatus: Object, + showThreadRecursively: Function, + totalReplyCount: Object, + totalReplyDepth: Object, + statusContentProperties: Object, + setStatusContentProperty: Function, + toggleStatusContentProperty: Function, + dive: Function + }, + computed: { + suspendable () { + const selfSuspendable = this.$refs.statusComponent ? this.$refs.statusComponent.suspendable : true + if (this.$refs.childComponent) { + return selfSuspendable && this.$refs.childComponent.every(s => s.suspendable) + } + return selfSuspendable + }, + reverseLookupTable () { + return this.conversation.reduce((table, status, index) => { + table[status.id] = index + return table + }, {}) + }, + currentReplies () { + return this.getReplies(this.status.id).map(({ id }) => this.statusById(id)) + }, + threadShowing () { + return this.threadDisplayStatus[this.status.id] === 'showing' + }, + currentProp () { + return this.statusContentProperties[this.status.id] + } + }, + methods: { + statusById (id) { + return this.conversation[this.reverseLookupTable[id]] + }, + collapseThread () { + }, + showThread () { + }, + showAllSubthreads () { + }, + toggleCurrentProp (name) { + this.toggleStatusContentProperty(this.status.id, name) + }, + setCurrentProp (name, newVal) { + this.setStatusContentProperty(this.status.id, name) + } + } +} + +export default ThreadTree diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue @@ -0,0 +1,127 @@ +<template> + <div class="thread-tree panel-body"> + <status + :key="status.id" + ref="statusComponent" + :inline-expanded="collapsable && isExpanded" + :statusoid="status" + :expandable="!isExpanded" + :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]" + :focused="focused(status.id)" + :in-conversation="isExpanded" + :highlight="highlight" + :replies="getReplies(status.id)" + :in-profile="inProfile" + :profile-user-id="profileUserId" + class="conversation-status conversation-status-treeview status-fadein panel-body" + + :simple-tree="simple" + :controlled-thread-display-status="threadDisplayStatus[status.id]" + :controlled-toggle-thread-display="() => toggleThreadDisplay(status.id)" + + :controlled-showing-tall="currentProp.showingTall" + :controlled-expanding-subject="currentProp.expandingSubject" + :controlled-showing-long-subject="currentProp.showingLongSubject" + :controlled-replying="currentProp.replying" + :controlled-media-playing="currentProp.mediaPlaying" + :controlled-toggle-showing-tall="() => toggleCurrentProp('showingTall')" + :controlled-toggle-expanding-subject="() => toggleCurrentProp('expandingSubject')" + :controlled-toggle-showing-long-subject="() => toggleCurrentProp('showingLongSubject')" + :controlled-toggle-replying="() => toggleCurrentProp('replying')" + :controlled-set-media-playing="(newVal) => setCurrentProp('mediaPlaying', newVal)" + :dive="dive ? () => dive(status.id) : undefined" + + @goto="setHighlight" + @toggleExpanded="toggleExpanded" + /> + <div + v-if="currentReplies.length && threadShowing" + class="thread-tree-replies" + > + <thread-tree + v-for="replyStatus in currentReplies" + :key="replyStatus.id" + ref="childComponent" + :depth="depth + 1" + :status="replyStatus" + + :in-profile="inProfile" + :conversation="conversation" + :collapsable="collapsable" + :is-expanded="isExpanded" + :pinned-status-ids-object="pinnedStatusIdsObject" + :profile-user-id="profileUserId" + + :focused="focused" + :get-replies="getReplies" + :highlight="highlight" + :set-highlight="setHighlight" + :toggle-expanded="toggleExpanded" + + :simple="simple" + :toggle-thread-display="toggleThreadDisplay" + :thread-display-status="threadDisplayStatus" + :show-thread-recursively="showThreadRecursively" + :total-reply-count="totalReplyCount" + :total-reply-depth="totalReplyDepth" + :status-content-properties="statusContentProperties" + :set-status-content-property="setStatusContentProperty" + :toggle-status-content-property="toggleStatusContentProperty" + :dive="dive" + /> + </div> + <div + v-if="currentReplies.length && !threadShowing" + class="thread-tree-replies thread-tree-replies-hidden" + > + <i18n + v-if="simple" + tag="button" + path="status.thread_follow_with_icon" + class="button-unstyled -link thread-tree-show-replies-button" + @click.prevent="dive(status.id)" + > + <FAIcon + place="icon" + icon="angle-double-right" + /> + <span place="text"> + {{ $tc('status.thread_follow', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id] }) }} + </span> + </i18n> + <i18n + v-else + tag="button" + path="status.thread_show_full_with_icon" + class="button-unstyled -link thread-tree-show-replies-button" + @click.prevent="showThreadRecursively(status.id)" + > + <FAIcon + place="icon" + icon="angle-double-down" + /> + <span place="text"> + {{ $tc('status.thread_show_full', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id], depth: totalReplyDepth[status.id] }) }} + </span> + </i18n> + </div> + </div> +</template> + +<script src="./thread_tree.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; +.thread-tree-replies { + margin-left: var(--status-margin, $status-margin); + border-left: 2px solid var(--border, $fallback--border); +} + +.thread-tree-replies-hidden { + padding: var(--status-margin, $status-margin); + /* Make the button stretch along the whole row */ + display: flex; + align-items: stretch; + flex-direction: column; +} +</style> diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js @@ -12,19 +12,6 @@ library.add( 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', @@ -77,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.vue b/src/components/timeline/timeline.vue @@ -37,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" diff --git a/src/components/timeline/timeline_quick_settings.js b/src/components/timeline/timeline_quick_settings.js @@ -48,12 +48,18 @@ const TimelineQuickSettings = { } }, hideMutedPosts: { - get () { return this.mergedConfig.hideMutedPosts || this.mergedConfig.hideFilteredStatuses }, + get () { return this.mergedConfig.hideFilteredStatuses }, set () { const value = !this.hideMutedPosts - this.$store.dispatch('setOption', { name: 'hideMutedPosts', value }) this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value }) } + }, + muteBotStatuses: { + get () { return this.mergedConfig.muteBotStatuses }, + set () { + const value = !this.muteBotStatuses + this.$store.dispatch('setOption', { name: 'muteBotStatuses', value }) + } } } } diff --git a/src/components/timeline/timeline_quick_settings.vue b/src/components/timeline/timeline_quick_settings.vue @@ -41,6 +41,15 @@ </div> <button class="button-default dropdown-item" + @click="muteBotStatuses = !muteBotStatuses" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': muteBotStatuses }" + />{{ $t('settings.mute_bot_posts') }} + </button> + <button + class="button-default dropdown-item" @click="hideMedia = !hideMedia" > <span diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue @@ -10,10 +10,7 @@ @close="() => isOpen = false" > <template v-slot:content> - <div - slot="content" - class="timeline-menu-popover popover-default" - > + <div class="timeline-menu-popover popover-default"> <TimelineMenuContent /> </div> </template> diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js @@ -1,10 +1,21 @@ import StillImage from '../still-image/still-image.vue' +import { library } from '@fortawesome/fontawesome-svg-core' + +import { + faRobot +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faRobot +) + const UserAvatar = { props: [ 'user', 'betterShadow', - 'compact' + 'compact', + 'bot' ], data () { return { diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue @@ -7,7 +7,13 @@ :src="imgSrc(user.profile_image_url_original)" :class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }" :image-load-error="imageLoadError" - /> + > + <FAIcon + v-if="bot" + icon="robot" + class="bot-indicator" + /> + </StillImage> <div v-else class="Avatar -placeholder" @@ -36,6 +42,12 @@ height: 100%; } + & > .bot-indicator { + position: absolute; + bottom: 0; + right: 0; + } + &.better-shadow { box-shadow: var(--_avatarShadowInset); filter: var(--_avatarShadowFilter); 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,22 +38,25 @@ </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" - > - {{ user.name }} - </div> <button + v-if="!isOtherUser && user.is_local" + class="button-unstyled edit-profile-button" + @click.stop="openProfileTab" + > + <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" @@ -80,6 +83,12 @@ </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" > @@ -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">{{ $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> - <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 @@ -22,7 +22,12 @@ /> <div class="user-list-names"> <!-- eslint-disable vue/no-v-html --> - <span v-html="user.name_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> @@ -48,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.jsx' +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 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 @@ -13,6 +13,9 @@ "mrf_policies_desc": "MRF policies manipulate the federation behaviour of the instance. The following policies are enabled:", "simple": { "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", @@ -79,7 +82,10 @@ "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", @@ -112,7 +118,9 @@ }, "media_modal": { "previous": "Previous", - "next": "Next" + "next": "Next", + "counter": "{current} / {total}", + "hide": "Close media viewer" }, "nav": { "about": "About", @@ -252,10 +260,14 @@ }, "settings": { "app_name": "App name", + "expert_mode": "Show advanced", "save": "Save changes", "security": "Security", "setting_changed": "Setting is different from default", + "setting_server_side": "This setting is tied to your profile and affects all sessions and clients", "enter_current_password_to_confirm": "Enter your current password to confirm your identity", + "post_look_feel": "Posts Look & Feel", + "mention_links": "Mention links", "mfa": { "otp": "OTP", "setup_otp": "Setup OTP", @@ -328,6 +340,7 @@ "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", @@ -342,15 +355,22 @@ "hide_attachments_in_tl": "Hide attachments in timeline", "hide_media_previews": "Hide media previews", "hide_muted_posts": "Hide posts of muted users", + "mute_bot_posts": "Mute bot posts", + "hide_bot_indication": "Hide bot indication in posts", "hide_all_muted_posts": "Hide muted posts", - "max_thumbnails": "Maximum amount of thumbnails per post", + "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", @@ -386,11 +406,14 @@ "name": "Label", "value": "Content" }, + "account_privacy": "Privacy", "use_contain_fit": "Don't crop the attachment in thumbnails", "name": "Name", "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": "Favorites", @@ -401,20 +424,21 @@ "no_rich_text_description": "Strip rich text formatting from all posts", "no_blocks": "No blocks", "no_mutes": "No mutes", + "hide_favorites_description": "Don't show list of my favorites (people still get notified)", "hide_follows_description": "Don't show who I'm following", "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", + "nsfw_clickthrough": "Hide sensitive/NSFW media", "oauth_tokens": "OAuth tokens", "token": "Token", "refresh_token": "Refresh token", "valid_until": "Valid until", "revoke_token": "Revoke", "panelRadius": "Panels", - "pause_on_unfocused": "Pause streaming when tab is not focused", + "pause_on_unfocused": "Pause when tab is not focused", "presets": "Presets", "profile_background": "Profile background", "profile_banner": "Profile banner", @@ -449,13 +473,21 @@ "subject_line_email": "Like email: \"re: subject\"", "subject_line_mastodon": "Like mastodon: copy as is", "subject_line_noop": "Do not copy", + "conversation_display": "Conversation display style", + "conversation_display_tree": "Tree-style", + "tree_advanced": "Allow more flexible navigation in tree view", + "tree_fade_ancestors": "Display ancestors of the current status in faint text", + "conversation_display_linear": "Linear-style", + "conversation_other_replies_button": "Show the \"other replies\" button", + "conversation_other_replies_button_below": "Below statuses", + "conversation_other_replies_button_inside": "Inside statuses", + "max_depth_in_thread": "Maximum number of levels in thread to display by default", "post_status_content_type": "Post status content type", "sensitive_by_default": "Mark posts as sensitive by default", - "stop_gifs": "Play-on-hover GIFs", - "streaming": "Enable automatic streaming of new posts when scrolled to the top", + "stop_gifs": "Pause animated images until you hover on them", + "streaming": "Automatically show new posts when scrolled to the top", "user_mutes": "Users", "useStreamingApi": "Receive posts and notifications real-time", - "useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)", "text": "Text", "theme": "Theme", "theme_help": "Use hex color codes (#rrggbb) to customize your color theme.", @@ -470,8 +502,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", @@ -693,7 +735,9 @@ "unbookmark": "Unbookmark", "delete_confirm": "Do you really want to delete this status?", "reply_to": "Reply to", + "mentions": "Mentions", "replies_list": "Replies:", + "replies_list_with_others": "Replies (+{numReplies} other): | Replies (+{numReplies} others):", "mute_conversation": "Mute conversation", "unmute_conversation": "Unmute conversation", "status_unavailable": "Status unavailable", @@ -707,18 +751,44 @@ "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", + "thread_hide": "Hide this thread", + "thread_show": "Show this thread", + "thread_show_full": "Show everything under this thread ({numStatus} status in total, max depth {depth}) | Show everything under this thread ({numStatus} statuses in total, max depth {depth})", + "thread_show_full_with_icon": "{icon} {text}", + "thread_follow": "See the remaining part of this thread ({numStatus} status in total) | See the remaining part of this thread ({numStatus} statuses in total)", + "thread_follow_with_icon": "{icon} {text}", + "ancestor_follow": "See {numReplies} other reply under this status | See {numReplies} other replies under this status", + "ancestor_follow_with_icon": "{icon} {text}", + "show_all_conversation_with_icon": "{icon} {text}", + "show_all_conversation": "Show full conversation ({numStatus} other status) | Show full conversation ({numStatus} other statuses)", + "show_only_conversation_under_this": "Only show replies to this status" }, "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", diff --git a/src/i18n/eo.json b/src/i18n/eo.json @@ -39,7 +39,10 @@ "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", @@ -87,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…", @@ -119,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", @@ -135,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", @@ -143,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", @@ -158,7 +163,8 @@ "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" + "reason": "Kialo registriĝi", + "register": "Registriĝi" }, "settings": { "app_name": "Nomo de aplikaĵo", @@ -244,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", @@ -532,7 +538,25 @@ "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" + "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", @@ -546,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", @@ -557,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", @@ -609,7 +634,8 @@ "striped": "Stria fono", "solid": "Unueca fono", "disabled": "Senemfaze" - } + }, + "edit_profile": "Redakti profilon" }, "user_profile": { "timeline_title": "Historio de uzanto", @@ -696,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:", @@ -704,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": { @@ -760,7 +786,10 @@ "status_deleted": "Ĉi tiu afiŝo foriĝis", "nsfw": "Konsterna", "expand": "Etendi", - "external_source": "Ekstera fonto" + "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,7 +34,7 @@ "enable": "Habilitar", "confirm": "Confirmar", "verify": "Verificar", - "peek": "Ojear", + "peek": "Previsualizar", "close": "Cerrar", "dismiss": "Descartar", "retry": "Inténtalo de nuevo", @@ -43,7 +43,10 @@ "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", @@ -147,7 +150,7 @@ "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", @@ -181,7 +184,7 @@ "preview_empty": "Vacío", "preview": "Vista previa", "media_description": "Descripción multimedia", - "post": "Publicación" + "post": "Publicar" }, "registration": { "bio": "Biografía", @@ -582,7 +585,24 @@ "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" + "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", @@ -659,7 +679,10 @@ "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", @@ -670,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", @@ -726,7 +748,8 @@ "solid": "Fondo sólido", "disabled": "Sin resaltado" }, - "bot": "Bot" + "bot": "Bot", + "edit_profile": "Edita el perfil" }, "user_profile": { "timeline_title": "Línea temporal del usuario", diff --git a/src/i18n/eu.json b/src/i18n/eu.json @@ -43,7 +43,10 @@ "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", @@ -96,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…", @@ -136,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" @@ -144,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", @@ -172,14 +178,20 @@ "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", @@ -193,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" @@ -210,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", @@ -468,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", @@ -480,7 +495,11 @@ "title": "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", @@ -550,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", @@ -691,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 @@ -43,7 +43,10 @@ "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", @@ -282,7 +285,7 @@ "new_password": "Nouveau mot de passe", "notification_visibility": "Types de notifications à afficher", "notification_visibility_follows": "Suivis", - "notification_visibility_likes": "J'aime", + "notification_visibility_likes": "Favoris", "notification_visibility_mentions": "Mentionnés", "notification_visibility_repeats": "Partages", "no_rich_text_description": "Ne formatez pas le texte", @@ -553,7 +556,21 @@ "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" + "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", @@ -607,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", @@ -663,7 +679,8 @@ "side": "Coté rayé", "striped": "Fond rayé" }, - "bot": "Robot" + "bot": "Robot", + "edit_profile": "Éditer le profil" }, "user_profile": { "timeline_title": "Flux du compte", 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 @@ -21,7 +21,10 @@ "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", @@ -65,13 +68,13 @@ "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_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", @@ -341,14 +344,14 @@ }, "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", + "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)", @@ -362,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", @@ -431,11 +434,28 @@ "hide_all_muted_posts": "Nascondi messaggi silenziati", "hide_media_previews": "Nascondi anteprime", "word_filter": "Parole filtrate", - "save": "Salva modifiche" + "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": "Ripiega", @@ -499,7 +519,6 @@ "its_you": "Sei tu!", "hidden": "Nascosto", "follow_unfollow": "Disconosci", - "follow_again": "Reinvio richiesta?", "follow_progress": "Richiedo…", "follow_sent": "Richiesta inviata!", "favorites": "Preferiti", @@ -510,7 +529,8 @@ "striped": "A righe", "solid": "Un colore", "disabled": "Nessun risalto" - } + }, + "edit_profile": "Modifica profilo" }, "chat": { "title": "Chat" @@ -647,8 +667,8 @@ "staff": "Responsabili" }, "domain_mute_card": { - "mute": "Zittisci", - "mute_progress": "Zittisco…", + "mute": "Silenzia", + "mute_progress": "Procedo…", "unmute": "Ascolta", "unmute_progress": "Procedo…" }, @@ -689,7 +709,7 @@ }, "interactions": { "favs_repeats": "Condivisi e Graditi", - "load_older": "Carica vecchie interazioni", + "load_older": "Carica interazioni precedenti", "moves": "Utenti migrati", "follows": "Nuovi seguìti" }, @@ -739,16 +759,19 @@ "bookmark": "Aggiungi segnalibro", "status_deleted": "Questo messagio è stato cancellato", "nsfw": "DISDICEVOLE", - "external_source": "Vai al sito", - "expand": "Espandi" + "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": "{0} anni", "year": "{0} anno", - "weeks_short": "{0} set", - "week_short": "{0} set", + "weeks_short": "{0} stm", + "week_short": "{0} stm", "seconds_short": "{0} sec", "second_short": "{0} sec", "weeks": "{0} settimane", @@ -757,8 +780,8 @@ "second": "{0} secondo", "now_short": "adesso", "now": "adesso", - "months_short": "{0} ms", - "month_short": "{0} ms", + "months_short": "{0} mes", + "month_short": "{0} mes", "months": "{0} mesi", "month": "{0} mese", "minutes_short": "{0} min", 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 @@ -43,7 +43,10 @@ "role": { "moderator": "モデレーター", "admin": "管理者" - } + }, + "flash_security": "Flashコンテンツが任意の命令を実行させることにより、コンピューターが危険にさらされることがあります。", + "flash_fail": "Flashコンテンツの読み込みに失敗しました。コンソールで詳細を確認できます。", + "flash_content": "(試験的機能)クリックしてFlashコンテンツを再生します。" }, "image_cropper": { "crop_picture": "画像を切り抜く", @@ -86,7 +89,7 @@ "mentions": "通知", "interactions": "インタラクション", "dms": "ダイレクトメッセージ", - "public_tl": "パブリックタイムライン", + "public_tl": "公開タイムライン", "timeline": "タイムライン", "twkn": "すべてのネットワーク", "user_search": "ユーザーを探す", @@ -96,7 +99,8 @@ "administration": "管理", "bookmarks": "ブックマーク", "timelines": "タイムライン", - "chats": "チャット" + "chats": "チャット", + "home_timeline": "ホームタイムライン" }, "notifications": { "broken_favorite": "ステータスが見つかりません。探しています…", @@ -173,14 +177,15 @@ "scope": { "direct": "ダイレクト: メンションされたユーザーのみに届きます", "private": "フォロワー限定: フォロワーのみに届きます", - "public": "パブリック: パブリックタイムラインに届きます", - "unlisted": "アンリステッド: パブリックタイムラインに届きません" + "public": "パブリック: 公開タイムラインに届きます", + "unlisted": "アンリステッド: 公開タイムラインに届きません" }, "media_description_error": "メディアのアップロードに失敗しました。もう一度お試しください", "empty_status_error": "投稿内容を入力してください", "preview_empty": "何もありません", "preview": "プレビュー", - "media_description": "メディアの説明" + "media_description": "メディアの説明", + "post": "投稿" }, "registration": { "bio": "プロフィール", @@ -203,7 +208,8 @@ "password_confirmation_match": "パスワードが違います" }, "reason_placeholder": "このインスタンスは、新規登録を手動で受け付けています。\n登録したい理由を、インスタンスの管理者に教えてください。", - "reason": "登録するための目的" + "reason": "登録するための目的", + "register": "登録" }, "selectable_list": { "select_all": "すべて選択" @@ -323,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": "トークン", @@ -334,8 +340,8 @@ "panelRadius": "パネル", "pause_on_unfocused": "タブにフォーカスがないときストリーミングを止める", "presets": "プリセット", - "profile_background": "プロフィールのバックグラウンド", - "profile_banner": "プロフィールバナー", + "profile_background": "プロフィールの背景", + "profile_banner": "プロフィールのバナー", "profile_tab": "プロフィール", "radii_help": "インターフェースの丸さを設定する", "replies_in_timeline": "タイムラインのリプライ", @@ -580,7 +586,21 @@ "reply_visibility_following_short": "フォローしている人に宛てられたリプライを見る", "hide_all_muted_posts": "ミュートした投稿を隠す", "hide_media_previews": "メディアのプレビューを隠す", - "word_filter": "単語フィルタ" + "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}日", @@ -628,7 +648,9 @@ "no_more_statuses": "これで終わりです", "no_statuses": "ステータスはありません", "reload": "再読み込み", - "error": "タイムラインの読み込みに失敗しました: {0}" + "error": "タイムラインの読み込みに失敗しました: {0}", + "socket_reconnected": "リアルタイム接続が確立されました", + "socket_broke": "コード{0}によりリアルタイム接続が切断されました" }, "status": { "favorites": "お気に入り", @@ -655,7 +677,10 @@ "copy_link": "リンクをコピー", "status_unavailable": "利用できません", "unbookmark": "ブックマーク解除", - "bookmark": "ブックマーク" + "bookmark": "ブックマーク", + "mentions": "メンション", + "you": "(あなた)", + "plus_more": "ほか{number}件" }, "user_card": { "approve": "受け入れ", @@ -666,7 +691,6 @@ "follow": "フォロー", "follow_sent": "リクエストを送りました!", "follow_progress": "リクエストしています…", - "follow_again": "再びリクエストを送りますか?", "follow_unfollow": "フォローをやめる", "followees": "フォロー", "followers": "フォロワー", @@ -722,7 +746,8 @@ "striped": "背景を縞模様にする", "side": "端に線を付ける", "disabled": "強調しない" - } + }, + "edit_profile": "プロフィールを編集" }, "user_profile": { "timeline_title": "ユーザータイムライン", @@ -796,8 +821,8 @@ "media_nsfw": "メディアを閲覧注意に設定", "media_removal_desc": "このインスタンスでは、以下のインスタンスからの投稿に対して、メディアを除去します:", "media_removal": "メディア除去", - "ftl_removal": "「接続しているすべてのネットワーク」タイムラインから除外", - "ftl_removal_desc": "このインスタンスでは、以下のインスタンスを「接続しているすべてのネットワーク」タイムラインから除外します:", + "ftl_removal": "「既知のネットワーク」タイムラインから除外", + "ftl_removal_desc": "このインスタンスでは、以下のインスタンスを「既知のネットワーク」タイムラインから除外します:", "quarantine_desc": "このインスタンスでは、以下のインスタンスに対して公開投稿のみを送信します:", "quarantine": "検疫", "reject_desc": "このインスタンスでは、以下のインスタンスからのメッセージを受け付けません:", diff --git a/src/i18n/ko.json b/src/i18n/ko.json @@ -77,7 +77,8 @@ "search": "검색", "bookmarks": "북마크", "interactions": "대화", - "administration": "관리" + "administration": "관리", + "home_timeline": "홈 타임라인" }, "notifications": { "broken_favorite": "알 수 없는 게시물입니다, 검색합니다…", @@ -90,7 +91,8 @@ "no_more_notifications": "알림이 없습니다", "migrated_to": "이사했습니다", "reacted_with": "{0} 로 반응했습니다", - "error": "알림 불러오기 실패: {0}" + "error": "알림 불러오기 실패: {0}", + "follow_request": "당신에게 팔로우 신청" }, "post_status": { "new_status": "새 게시물 게시", @@ -402,7 +404,7 @@ }, "mutes_and_blocks": "침묵과 차단", "chatMessageRadius": "챗 메시지", - "change_email": "전자메일 주소 바꾸기", + "change_email": "메일주소 바꾸기", "changed_email": "메일주소가 갱신되었습니다!", "bot": "이 계정은 bot입니다", "mutes_tab": "침묵", @@ -426,7 +428,6 @@ "follow": "팔로우", "follow_sent": "요청 보내짐!", "follow_progress": "요청 중…", - "follow_again": "요청을 다시 보낼까요?", "follow_unfollow": "팔로우 중지", "followees": "팔로우 중", "followers": "팔로워", @@ -490,7 +491,9 @@ "votes_count": "{count} 표 | {count} 표", "people_voted_count": "{count} 명 투표 | {count} 명 투표", "option": "선택지", - "add_option": "선택지 추가" + "add_option": "선택지 추가", + "expired": "투표는 {0} 전에 마감되었습니다", + "expires_in": "투표는 {0}에 마감됩니다" }, "media_modal": { "next": "다음", @@ -525,8 +528,8 @@ "media_nsfw": "매체를 민감함으로 설정", "media_removal_desc": "이 인스턴스에서는 아래의 인스턴스로부터 보내온 투고에 붙혀 있는 매체는 제거됩니다:", "media_removal": "매체 제거", - "ftl_removal_desc": "이 인스턴스에서 아래의 인스턴스들은 \"알려진 모든 네트워크\" 타임라인에서 제외됩니다:", - "ftl_removal": "\"알려진 모든 네트워크\" 타임라인에서 제외", + "ftl_removal_desc": "이 인스턴스에서 아래의 인스턴스들은 \"알려진 네트워크\" 타임라인에서 제외됩니다:", + "ftl_removal": "\"알려진 네트워크\" 타임라인에서 제외", "quarantine_desc": "이 인스턴스는 아래의 인스턴스에게 공개투고만을 보냅니다:", "quarantine": "검역", "reject_desc": "이 인스턴스에서는 아래의 인스턴스로부터 보내온 투고를 받아들이지 않습니다:", diff --git a/src/i18n/nb.json b/src/i18n/nb.json @@ -516,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", 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,29 +726,31 @@ "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", - "moves": "Gebruiker migreert", + "favs_repeats": "Herhalingen en favorieten", + "follows": "Nieuwe gevolgden", + "moves": "Gebruikermigraties", "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" }, "selectable_list": { "select_all": "Alles selecteren" @@ -715,7 +798,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", @@ -750,5 +843,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 @@ -575,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", diff --git a/src/i18n/ru.json b/src/i18n/ru.json @@ -550,7 +550,6 @@ "follow": "Читать", "follow_sent": "Запрос отправлен!", "follow_progress": "Запрашиваем…", - "follow_again": "Запросить еще раз?", "follow_unfollow": "Перестать читать", "followees": "Читаемые", "followers": "Читатели", 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 @@ -21,7 +21,10 @@ "role": { "moderator": "Модератор", "admin": "Адміністратор" - } + }, + "flash_content": "Натисніть для перегляду змісту Flash за допомогою Ruffle (експериментально, може не працювати).", + "flash_security": "Ця функція може становити ризик, оскільки Flash-вміст все ще є потенційно небезпечним.", + "flash_fail": "Не вдалося завантажити Flash-вміст, докладнішу інформацію дивись у консолі." }, "finder": { "error_fetching_user": "Користувача не знайдено", @@ -30,7 +33,7 @@ "features_panel": { "gopher": "Gopher", "pleroma_chat_messages": "Чати", - "chat": "Міні-чат", + "chat": "Оголошення", "who_to_follow": "Кого відстежувати", "title": "Особливості", "scope_options": "Параметри обсягу", @@ -49,7 +52,7 @@ "mute": "Ігнорувати" }, "shoutbox": { - "title": "Міні-чат" + "title": "Оголошення" }, "about": { "staff": "Адміністрація", @@ -122,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}", @@ -155,7 +160,8 @@ "interactions": "Взаємодії", "mentions": "Згадування", "back": "Назад", - "administration": "Адміністрування" + "administration": "Адміністрування", + "home_timeline": "Домашня стрічка" }, "media_modal": { "next": "Наступна", @@ -246,7 +252,8 @@ }, "preview_empty": "Пустий", "media_description_error": "Не вдалось оновити медіа, спробуйте ще раз", - "media_description": "Опис медіа" + "media_description": "Опис медіа", + "post": "Опублікувати" }, "settings": { "blocks_imported": "Блокування імпортовані! Їх обробка триватиме певний час.", @@ -608,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": "Вибрати все" @@ -637,7 +667,10 @@ "fullname": "Відображене ім'я", "email": "Ел. пошта", "bio": "Про себе", - "captcha": "CAPTCHA" + "captcha": "CAPTCHA", + "register": "Зареєструватися", + "reason_placeholder": "Цей інстанс обробляє запити на реєстрацію вручну.\nРозкажіть адміністрації чому ви хочете зареєструватися.", + "reason": "Причина реєстрації" }, "who_to_follow": { "who_to_follow": "На кого підписатися", @@ -715,7 +748,6 @@ "message": "Повідомлення", "follow": "Підписатись", "follow_unfollow": "Відписатись", - "follow_again": "Відправити запит знову?", "follow_sent": "Запит відправлено!", "blocked": "Заблоковано!", "admin_menu": { @@ -764,7 +796,15 @@ "unblock": "Розблокувати", "remote_follow": "Підписатись", "muted": "Заглушений", - "mute": "Заглушити" + "mute": "Заглушити", + "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 @@ -43,7 +43,10 @@ "role": { "moderator": "监察员", "admin": "管理员" - } + }, + "flash_content": "点击以使用 Ruffle 显示 Flash 内容(实验性,可能无效)。", + "flash_security": "注意这可能有潜在的危险,因为 Flash 内容仍然是任意的代码。", + "flash_fail": "Flash 内容加载失败,请在控制台查看详情。" }, "image_cropper": { "crop_picture": "裁剪图片", @@ -96,7 +99,8 @@ "administration": "管理员", "chats": "聊天", "timelines": "时间线", - "bookmarks": "书签" + "bookmarks": "书签", + "home_timeline": "主页时间线" }, "notifications": { "broken_favorite": "未知的状态,正在搜索中…", @@ -300,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": "不显示富文本格式", @@ -571,7 +575,21 @@ "hide_all_muted_posts": "不显示已隐藏的发文", "hide_media_previews": "隐藏媒体预览", "word_filter": "词语过滤", - "save": "保存更改" + "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} 天", @@ -659,7 +677,6 @@ "follow": "关注", "follow_sent": "请求已发送!", "follow_progress": "请求中…", - "follow_again": "再次发送请求?", "follow_unfollow": "取消关注", "followees": "正在关注", "followers": "关注者", @@ -711,7 +728,8 @@ "striped": "条纹背景", "solid": "单一颜色背景", "disabled": "不突出显示" - } + }, + "edit_profile": "编辑个人资料" }, "user_profile": { "timeline_title": "用户时间线", @@ -829,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 @@ -57,7 +57,8 @@ "friend_requests": "關注請求", "back": "後退", "administration": "管理員", - "about": "關於" + "about": "關於", + "home_timeline": "家時間線" }, "media_modal": { "next": "往後", @@ -114,7 +115,10 @@ "role": { "moderator": "主持人", "admin": "管理員" - } + }, + "flash_content": "點擊以使用 Ruffle 顯示 Flash 內容(實驗性,可能無效)。", + "flash_security": "請注意,這可能有潜在的危險,因為Flash內容仍然是武斷的程式碼。", + "flash_fail": "無法加載flash內容,請參閱控制台瞭解詳細資訊。" }, "finder": { "find_user": "尋找用戶", @@ -328,7 +332,7 @@ "notification_visibility_moves": "用戶遷移", "notification_visibility_repeats": "轉發", "notification_visibility_mentions": "提及", - "notification_visibility_likes": "點贊", + "notification_visibility_likes": "喜歡", "interfaceLanguage": "界面語言", "instance_default": "(默認:{value})", "inputRadius": "輸入框", @@ -541,7 +545,23 @@ "hide_media_previews": "隱藏媒體預覽", "word_filter": "詞過濾", "setting_changed": "與默認設置不同", - "more_settings": "更多設置" + "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": "更多", @@ -657,7 +677,8 @@ "attachments_sensitive": "標記附件為敏感內容", "account_not_locked_warning_link": "上鎖", "default": "剛剛抵達洛杉磯。", - "empty_status_error": "無法發佈沒有附件的空發文" + "empty_status_error": "不能發布沒有內容,沒有附件的發文", + "post": "發送" }, "errors": { "storage_unavailable": "Pleroma無法訪問瀏覽器存儲。您的登錄名或本地設置將不會保存,您可能會遇到意外問題。嘗試啟用Cookie。" @@ -674,7 +695,9 @@ "up_to_date": "已是最新", "no_more_statuses": "没有更多發文", "no_statuses": "没有發文", - "error": "取得時間線時發生錯誤:{0}" + "error": "取得時間線時發生錯誤:{0}", + "socket_reconnected": "已建立實時連接", + "socket_broke": "丟失實時連接:CloseEvent代碼{0}" }, "interactions": { "load_older": "載入更早的互動", @@ -711,7 +734,8 @@ "email": "電子郵箱", "bio": "簡介", "reason_placeholder": "此實例的註冊需要手動批准。\n請讓管理知道您為什麼想要註冊。", - "reason": "註冊理由" + "reason": "註冊理由", + "register": "註冊" }, "user_card": { "its_you": "就是你!!", @@ -747,7 +771,6 @@ "follow": "關注", "follow_sent": "請求已發送!", "follow_progress": "請求中…", - "follow_again": "再次發送請求?", "follow_unfollow": "取消關注", "followees": "正在關注", "followers": "關注者", @@ -778,7 +801,8 @@ "striped": "條紋背景", "side": "彩條" }, - "bot": "機器人" + "bot": "機器人", + "edit_profile": "編輯個人資料" }, "user_profile": { "timeline_title": "用戶時間線", diff --git a/src/main.js b/src/main.js @@ -9,7 +9,8 @@ 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 serverSideConfigModule from './modules/serverSideConfig.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' @@ -30,7 +31,19 @@ import afterStoreSetup from './boot/after_store.js' const currentLocale = (window.navigator.language || 'en').split('-')[0] -const i18n = createI18n({ +Vue.use(Vuex) +Vue.use(VueRouter) +Vue.use(VueI18n) +Vue.use(VueClickOutside) +Vue.use(PortalVue) +Vue.use(VBodyScrollLock) + +Vue.config.ignoredElements = ['pinch-zoom'] + +Vue.component('FAIcon', FontAwesomeIcon) +Vue.component('FALayers', FontAwesomeLayers) + +const i18n = new VueI18n({ // By default, use the browser locale, we will update it if neccessary locale: 'en', fallbackLocale: 'en', @@ -71,7 +84,8 @@ const persistedStateOptions = { statuses: statusesModule, api: apiModule, config: configModule, - chat: chatModule, + serverSideConfig: serverSideConfigModule, + shout: shoutModule, oauth: oauthModule, authFlow: authFlowModule, mediaViewer: mediaViewerModule, diff --git a/src/modules/api.js b/src/modules/api.js @@ -255,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,34 +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/config.js b/src/modules/config.js @@ -10,18 +10,26 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0] */ export const multiChoiceProperties = [ 'postContentType', - 'subjectLineBehavior' + 'subjectLineBehavior', + 'conversationDisplay', // tree | linear + 'conversationOtherRepliesButton', // below | inside + 'mentionLinkDisplay' // short | full_for_remote | full ] export const defaultState = { + expertLevel: 0, // used to track which settings to show and hide colors: {}, theme: undefined, customTheme: undefined, 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 + muteBotStatuses: undefined, // instance default collapseMessageWithSubject: undefined, // instance default padEmoji: true, hideAttachments: false, @@ -33,9 +41,10 @@ export const defaultState = { loopVideoSilentOnly: true, streaming: false, emojiReactionsOnTimeline: true, + alwaysShowNewPostButton: false, autohideFloatingPostButton: false, pauseOnUnfocused: true, - stopGifs: false, + stopGifs: true, replyVisibility: 'all', notificationVisibility: { follows: true, @@ -53,6 +62,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 @@ -62,12 +72,25 @@ export const defaultState = { hideFilteredStatuses: undefined, // instance default playVideosInModal: false, useOneClickNsfw: false, - useContainFit: false, + useContainFit: true, 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 + hideBotIndication: undefined, // instance default hideUserStats: undefined, // instance default virtualScrolling: undefined, // instance default - sensitiveByDefault: undefined // instance default + sensitiveByDefault: undefined, // instance default + conversationDisplay: undefined, // instance default + conversationTreeAdvanced: undefined, // instance default + conversationOtherRepliesButton: undefined, // instance default + conversationTreeFadeAncestors: undefined, // instance default + maxDepthInThread: undefined // instance default } // caching the instance default properties @@ -91,7 +114,8 @@ const config = { const { defaultConfig } = rootGetters return { ...defaultConfig, - ...state + // Do not override with undefined + ...Object.fromEntries(Object.entries(state).filter(([k, v]) => v !== undefined)) } } }, diff --git a/src/modules/instance.js b/src/modules/instance.js @@ -18,13 +18,24 @@ 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, + hideBotIndication: false, hideSitename: false, hideUserStats: false, + muteBotStatuses: false, loginMethod: 'password', logo: '/static/logo.svg', logoMargin: '.2em', @@ -43,6 +54,11 @@ const defaultState = { theme: 'pleroma-dark', virtualScrolling: true, sensitiveByDefault: false, + conversationDisplay: 'linear', + conversationTreeAdvanced: false, + conversationOtherRepliesButton: 'below', + conversationTreeFadeAncestors: false, + maxDepthInThread: 6, // Nasty stuff customEmoji: [], @@ -56,7 +72,7 @@ const defaultState = { knownDomains: [], // Feature-set, apparently, not everything here is reported... - chatAvailable: false, + shoutAvailable: false, pleromaChatMessagesAvailable: false, gopherAvailable: false, mediaProxyAvailable: false, @@ -97,6 +113,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 +125,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/serverSideConfig.js b/src/modules/serverSideConfig.js @@ -0,0 +1,137 @@ +import { get, set } from 'lodash' + +const defaultApi = ({ rootState, commit }, { path, value }) => { + const params = {} + set(params, path, value) + return rootState + .api + .backendInteractor + .updateProfile({ params }) + .then(result => { + commit('addNewUsers', [result]) + commit('setCurrentUser', result) + }) +} + +const notificationsApi = ({ rootState, commit }, { path, value, oldValue }) => { + const settings = {} + set(settings, path, value) + return rootState + .api + .backendInteractor + .updateNotificationSettings({ settings }) + .then(result => { + if (result.status === 'success') { + commit('confirmServerSideOption', { name, value }) + } else { + commit('confirmServerSideOption', { name, value: oldValue }) + } + }) +} + +/** + * Map that stores relation between path for reading (from user profile), + * for writing (into API) an what API to use. + * + * Shorthand - instead of { get, set, api? } object it's possible to use string + * in case default api is used and get = set + * + * If no api is specified, defaultApi is used (see above) + */ +export const settingsMap = { + 'defaultScope': 'source.privacy', + 'defaultNSFW': 'source.sensitive', // BROKEN: pleroma/pleroma#2837 + 'stripRichContent': { + get: 'source.pleroma.no_rich_text', + set: 'no_rich_text' + }, + // Privacy + 'locked': 'locked', + 'acceptChatMessages': { + get: 'pleroma.accepts_chat_messages', + set: 'accepts_chat_messages' + }, + 'allowFollowingMove': { + get: 'pleroma.allow_following_move', + set: 'allow_following_move' + }, + 'discoverable': 'source.discoverable', + 'hideFavorites': { + get: 'pleroma.hide_favorites', + set: 'hide_favorites' + }, + 'hideFollowers': { + get: 'pleroma.hide_followers', + set: 'hide_followers' + }, + 'hideFollows': { + get: 'pleroma.hide_follows', + set: 'hide_follows' + }, + 'hideFollowersCount': { + get: 'pleroma.hide_followers_count', + set: 'hide_followers_count' + }, + 'hideFollowsCount': { + get: 'pleroma.hide_follows_count', + set: 'hide_follows_count' + }, + // NotificationSettingsAPIs + 'webPushHideContents': { + get: 'pleroma.notification_settings.hide_notification_contents', + set: 'hide_notification_contents', + api: notificationsApi + }, + 'blockNotificationsFromStrangers': { + get: 'pleroma.notification_settings.block_from_strangers', + set: 'block_from_strangers', + api: notificationsApi + } +} + +export const defaultState = Object.fromEntries(Object.keys(settingsMap).map(key => [key, null])) + +const serverSideConfig = { + state: { ...defaultState }, + mutations: { + confirmServerSideOption (state, { name, value }) { + set(state, name, value) + }, + wipeServerSideOption (state, { name }) { + set(state, name, null) + }, + wipeAllServerSideOptions (state) { + Object.keys(settingsMap).forEach(key => { + set(state, key, null) + }) + }, + // Set the settings based on their path location + setCurrentUser (state, user) { + Object.entries(settingsMap).forEach((map) => { + const [name, value] = map + const { get: path = value } = value + set(state, name, get(user._original, path)) + }) + } + }, + actions: { + setServerSideOption ({ rootState, state, commit, dispatch }, { name, value }) { + const oldValue = get(state, name) + const map = settingsMap[name] + if (!map) throw new Error('Invalid server-side setting') + const { set: path = map, api = defaultApi } = map + commit('wipeServerSideOption', { name }) + + api({ rootState, commit }, { path, value, oldValue }) + .catch((e) => { + console.warn('Error setting server-side option:', e) + commit('confirmServerSideOption', { name, value: oldValue }) + }) + }, + logout ({ commit }) { + commit('wipeAllServerSideOptions') + } + } +} + +export default serverSideConfig 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/users.js b/src/modules/users.js @@ -245,6 +245,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 } @@ -387,7 +392,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 @@ -530,7 +535,7 @@ const users = { if (user.token) { store.dispatch('setWsToken', user.token) - // Initialize the chat socket. + // Initialize the shout socket. store.dispatch('initializeSocket') } diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js @@ -44,6 +44,7 @@ export const parseUser = (data) => { const mastoShort = masto && !data.hasOwnProperty('avatar') output.id = String(data.id) + output._original = data // used for server-side settings if (masto) { output.screen_name = data.acct @@ -54,17 +55,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 => { @@ -205,7 +209,7 @@ export const parseUser = (data) => { // Convert punycode to unicode for UI output.screen_name_ui = output.screen_name - if (output.screen_name.includes('@')) { + if (output.screen_name && output.screen_name.includes('@')) { const parts = output.screen_name.split('@') let unicodeDomain = punycode.toUnicode(parts[1]) if (unicodeDomain !== parts[1]) { @@ -239,16 +243,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 = {} @@ -266,7 +260,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 @@ -293,13 +288,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 @@ -325,7 +320,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 @@ -444,11 +439,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/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/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js @@ -4,9 +4,15 @@ const DIRECTION_RIGHT = [1, 0] const DIRECTION_UP = [0, -1] const DIRECTION_DOWN = [0, 1] +const BUTTON_LEFT = 0 + const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]] -const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY]) +const touchCoord = touch => [touch.screenX, touch.screenY] + +const touchEventCoord = e => touchCoord(e.touches[0]) + +const pointerEventCoord = e => [e.clientX, e.clientY] const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1]) @@ -61,6 +67,132 @@ const updateSwipe = (event, gesture) => { gesture._swiping = false } +class SwipeAndClickGesture { + // swipePreviewCallback(offsets: Array[Number]) + // offsets: the offset vector which the underlying component should move, from the starting position + // swipeEndCallback(sign: 0|-1|1) + // sign: if the swipe does not meet the threshold, 0 + // if the swipe meets the threshold in the positive direction, 1 + // if the swipe meets the threshold in the negative direction, -1 + constructor ({ + direction, + // swipeStartCallback + swipePreviewCallback, + swipeEndCallback, + swipeCancelCallback, + swipelessClickCallback, + threshold = 30, + perpendicularTolerance = 1.0, + disableClickThreshold = 1 + }) { + const nop = () => {} + this.direction = direction + this.swipePreviewCallback = swipePreviewCallback || nop + this.swipeEndCallback = swipeEndCallback || nop + this.swipeCancelCallback = swipeCancelCallback || nop + this.swipelessClickCallback = swipelessClickCallback || nop + this.threshold = typeof threshold === 'function' ? threshold : () => threshold + this.disableClickThreshold = typeof disableClickThreshold === 'function' ? disableClickThreshold : () => disableClickThreshold + this.perpendicularTolerance = perpendicularTolerance + this._reset() + } + + _reset () { + this._startPos = [0, 0] + this._pointerId = -1 + this._swiping = false + this._swiped = false + this._preventNextClick = false + } + + start (event) { + // Only handle left click + if (event.button !== BUTTON_LEFT) { + return + } + + this._startPos = pointerEventCoord(event) + this._pointerId = event.pointerId + this._swiping = true + this._swiped = false + } + + move (event) { + if (this._swiping && this._pointerId === event.pointerId) { + this._swiped = true + + const coord = pointerEventCoord(event) + const delta = deltaCoord(this._startPos, coord) + + this.swipePreviewCallback(delta) + } + } + + cancel (event) { + if (!this._swiping || this._pointerId !== event.pointerId) { + return + } + + this.swipeCancelCallback() + } + + end (event) { + if (!this._swiping) { + return + } + + if (this._pointerId !== event.pointerId) { + return + } + + this._swiping = false + + // movement too small + const coord = pointerEventCoord(event) + const delta = deltaCoord(this._startPos, coord) + + const sign = (() => { + if (vectorLength(delta) < this.threshold()) { + return 0 + } + // movement is opposite from direction + const isPositive = dotProduct(delta, this.direction) > 0 + + // movement perpendicular to direction is too much + const towardsDir = project(delta, this.direction) + const perpendicularDir = perpendicular(this.direction) + const towardsPerpendicular = project(delta, perpendicularDir) + if ( + vectorLength(towardsDir) * this.perpendicularTolerance < + vectorLength(towardsPerpendicular) + ) { + return 0 + } + + return isPositive ? 1 : -1 + })() + + if (this._swiped) { + this.swipeEndCallback(sign) + } + this._reset() + // Only a mouse will fire click event when + // the end point is far from the starting point + // so for other kinds of pointers do not check + // whether we have swiped + if (vectorLength(delta) >= this.disableClickThreshold() && event.pointerType === 'mouse') { + this._preventNextClick = true + } + } + + click (event) { + if (!this._preventNextClick) { + this.swipelessClickCallback() + } + this._reset() + } +} + const GestureService = { DIRECTION_LEFT, DIRECTION_RIGHT, @@ -68,7 +200,8 @@ const GestureService = { DIRECTION_DOWN, swipeGesture, beginSwipe, - updateSwipe + updateSwipe, + SwipeAndClickGesture } export default GestureService 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/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/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', 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/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>' }] }) @@ -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 @@ -15,13 +15,6 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/code-frame@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" - integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g== - dependencies: - "@babel/highlight" "^7.12.13" - "@babel/code-frame@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" @@ -29,6 +22,26 @@ dependencies: "@babel/highlight" "^7.0.0" +"@babel/core@7.7.5": + version "7.7.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.5.tgz#ae1323cd035b5160293307f50647e83f8ba62f7e" + integrity sha512-M42+ScN4+1S9iB6f+TL7QBpoQETxbclx+KNoKJABghnKYE+fMzSGqst0BZJc8CpI625bwPwYgUyRvxZ+0mZzpw== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.7.4" + "@babel/helpers" "^7.7.4" + "@babel/parser" "^7.7.5" + "@babel/template" "^7.7.4" + "@babel/traverse" "^7.7.4" + "@babel/types" "^7.7.4" + convert-source-map "^1.7.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.13" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + "@babel/core@>=7.9.0": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.5.tgz#1f15e2cca8ad9a1d78a38ddba612f5e7cdbbd330" @@ -51,26 +64,6 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.7.5": - version "7.7.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.5.tgz#ae1323cd035b5160293307f50647e83f8ba62f7e" - integrity sha512-M42+ScN4+1S9iB6f+TL7QBpoQETxbclx+KNoKJABghnKYE+fMzSGqst0BZJc8CpI625bwPwYgUyRvxZ+0mZzpw== - dependencies: - "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.7.4" - "@babel/helpers" "^7.7.4" - "@babel/parser" "^7.7.5" - "@babel/template" "^7.7.4" - "@babel/traverse" "^7.7.4" - "@babel/types" "^7.7.4" - convert-source-map "^1.7.0" - debug "^4.1.0" - json5 "^2.1.0" - lodash "^4.17.13" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" - "@babel/generator@^7.10.5": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.5.tgz#1b903554bc8c583ee8d25f1e8969732e6b829a69" @@ -80,15 +73,6 @@ jsesc "^2.5.1" source-map "^0.5.0" -"@babel/generator@^7.13.16": - version "7.13.16" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.16.tgz#0befc287031a201d84cdfc173b46b320ae472d14" - integrity sha512-grBBR75UnKOcUWMp8WoDxNsWCFl//XCK6HWTrBQKTr5SV9f5g0pNOjdyzi/DTBv12S9GnYPInIXQBTky7OXEMg== - dependencies: - "@babel/types" "^7.13.16" - jsesc "^2.5.1" - source-map "^0.5.0" - "@babel/generator@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.4.tgz#db651e2840ca9aa66f327dcec1dc5f5fa9611369" @@ -157,15 +141,6 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-function-name@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a" - integrity sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA== - dependencies: - "@babel/helper-get-function-arity" "^7.12.13" - "@babel/template" "^7.12.13" - "@babel/types" "^7.12.13" - "@babel/helper-function-name@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz#ab6e041e7135d436d8f0a3eca15de5b67a341a2e" @@ -182,13 +157,6 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-get-function-arity@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz#bc63451d403a3b3082b97e1d8b3fe5bd4091e583" - integrity sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg== - dependencies: - "@babel/types" "^7.12.13" - "@babel/helper-get-function-arity@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz#cb46348d2f8808e632f0ab048172130e636005f0" @@ -281,11 +249,6 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== -"@babel/helper-plugin-utils@^7.12.13": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz#806526ce125aed03373bc416a828321e3a6a33af" - integrity sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ== - "@babel/helper-regex@^7.0.0", "@babel/helper-regex@^7.4.4": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.5.5.tgz#0aa6824f7100a2e0e89c1527c23936c152cab351" @@ -347,13 +310,6 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-split-export-declaration@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz#e9430be00baf3e88b0e13e6f9d4eaf2136372b05" - integrity sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg== - dependencies: - "@babel/types" "^7.12.13" - "@babel/helper-split-export-declaration@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz#57292af60443c4a3622cf74040ddc28e68336fd8" @@ -366,11 +322,6 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== -"@babel/helper-validator-identifier@^7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" - integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== - "@babel/helper-wrap-function@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.7.4.tgz#37ab7fed5150e22d9d7266e830072c0cdd8baace" @@ -416,25 +367,11 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/highlight@^7.12.13": - version "7.13.10" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1" - integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg== - dependencies: - "@babel/helper-validator-identifier" "^7.12.11" - chalk "^2.0.0" - js-tokens "^4.0.0" - "@babel/parser@^7.10.4", "@babel/parser@^7.10.5": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.5.tgz#e7c6bf5a7deff957cec9f04b551e2762909d826b" integrity sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ== -"@babel/parser@^7.12.0", "@babel/parser@^7.12.13", "@babel/parser@^7.13.16", "@babel/parser@^7.13.9": - version "7.13.16" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.16.tgz#0f18179b0448e6939b1f3f5c4c355a3a9bcdfd37" - integrity sha512-6bAg36mCwuqLO0hbR+z7PHuqWiCeP7Dzg73OpQwsAB1Eb8HnGEz5xYBzCfbu+YjoaJsJs+qheDxVAuqbt3ILEw== - "@babel/parser@^7.7.4", "@babel/parser@^7.7.5": version "7.7.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.5.tgz#cbf45321619ac12d83363fcf9c94bb67fa646d71" @@ -510,13 +447,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-syntax-jsx@^7.0.0": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.13.tgz#044fb81ebad6698fe62c478875575bcbb9b70f15" - integrity sha512-d4HM23Q1K7oq/SLNmG6mRt85l2csmQ0cHRaxRXjKW0YFdEXqlZ5kzFQKH5Uc3rDJECgu+yCRgPkG04Mm98R/1g== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - "@babel/plugin-syntax-jsx@^7.2.0": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.7.4.tgz#dab2b56a36fb6c3c222a1fbc71f7bf97f327a9ec" @@ -744,7 +674,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-runtime@^7.7.6": +"@babel/plugin-transform-runtime@7.7.6": version "7.7.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.7.6.tgz#4f2b548c88922fb98ec1c242afd4733ee3e12f61" integrity sha512-tajQY+YmXR7JjTwRvwL4HePqoL3DYxpYXIHKVvrOIvJmeHe2y1w4tz5qz9ObUDC9m76rCzIMPyn4eERuwA4a4A== @@ -799,7 +729,7 @@ "@babel/helper-create-regexp-features-plugin" "^7.7.4" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/preset-env@^7.7.6": +"@babel/preset-env@7.7.6": version "7.7.6" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.7.6.tgz#39ac600427bbb94eec6b27953f1dfa1d64d457b2" integrity sha512-k5hO17iF/Q7tR9Jv8PdNBZWYW6RofxhnxKjBMc0nG4JTaWvOTiPoO/RLFwAKcA4FpmuBFm6jkoqaRJLGi0zdaQ== @@ -856,7 +786,7 @@ js-levenshtein "^1.1.3" semver "^5.5.0" -"@babel/register@^7.7.4": +"@babel/register@7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.7.4.tgz#45a4956471a9df3b012b747f5781cc084ee8f128" integrity sha512-/fmONZqL6ZMl9KJUYajetCrID6m0xmL4odX7v+Xvoxcv0DdbP/oO0TWIeLUCHqczQ6L6njDMqmqHFy2cp3FFsA== @@ -867,22 +797,13 @@ pirates "^4.0.0" source-map-support "^0.5.16" -"@babel/runtime@^7.7.6": +"@babel/runtime@7.7.6": version "7.7.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.6.tgz#d18c511121aff1b4f2cd1d452f1bac9601dd830f" integrity sha512-BWAJxpNVa0QlE5gZdWjSxXtemZyZ9RmrmVozxt3NUXeZhVIJ5ANyqmMc0JDrivBZyxUuQvFxlvH4OWWOogGfUw== dependencies: regenerator-runtime "^0.13.2" -"@babel/template@^7.0.0", "@babel/template@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" - integrity sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA== - dependencies: - "@babel/code-frame" "^7.12.13" - "@babel/parser" "^7.12.13" - "@babel/types" "^7.12.13" - "@babel/template@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" @@ -901,20 +822,6 @@ "@babel/parser" "^7.7.4" "@babel/types" "^7.7.4" -"@babel/traverse@^7.0.0": - version "7.13.17" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.17.tgz#c85415e0c7d50ac053d758baec98b28b2ecfeea3" - integrity sha512-BMnZn0R+X6ayqm3C3To7o1j7Q020gWdqdyP50KEoVqaCO2c/Im7sYZSmVgvefp8TTMQ+9CtwuBp0Z1CZ8V3Pvg== - dependencies: - "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.13.16" - "@babel/helper-function-name" "^7.12.13" - "@babel/helper-split-export-declaration" "^7.12.13" - "@babel/parser" "^7.13.16" - "@babel/types" "^7.13.17" - debug "^4.1.0" - globals "^11.1.0" - "@babel/traverse@^7.10.4", "@babel/traverse@^7.10.5": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.5.tgz#77ce464f5b258be265af618d8fddf0536f20b564" @@ -962,14 +869,6 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" -"@babel/types@^7.12.0", "@babel/types@^7.12.13", "@babel/types@^7.13.0", "@babel/types@^7.13.16", "@babel/types@^7.13.17": - version "7.13.17" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.17.tgz#48010a115c9fba7588b4437dd68c9469012b38b4" - integrity sha512-RawydLgxbOPDlTLJNtoIypwdmAy//uQIzlKt2+iBiJaRlVuI6QLUxVAyWGNfOzp8Yu4L4lLIacoCyTNtpb4wiA== - dependencies: - "@babel/helper-validator-identifier" "^7.12.11" - to-fast-properties "^2.0.0" - "@babel/types@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.7.4.tgz#516570d539e44ddf308c07569c258ff94fde9193" @@ -979,97 +878,55 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@chenfengyuan/vue-qrcode@^2.0.0-beta": - version "2.0.0-beta" - resolved "https://registry.yarnpkg.com/@chenfengyuan/vue-qrcode/-/vue-qrcode-2.0.0-beta.tgz#eebd3bdcd057b1635ddef1839bfdb04f642d7e21" - integrity sha512-z3BYPdXT6pvha2aGhWQ5E/0PBEUVcxEfcG1S4VEIKNYTv1rrmbWD02RDOC0JnB1LrLd+cDt9J0MtVOJB0TVfvw== - -"@fortawesome/fontawesome-common-types@^0.2.32": - version "0.2.32" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.32.tgz#3436795d5684f22742989bfa08f46f50f516f259" - integrity sha512-ux2EDjKMpcdHBVLi/eWZynnPxs0BtFVXJkgHIxXRl+9ZFaHPvYamAfCzeeQFqHRjuJtX90wVnMRaMQAAlctz3w== - -"@fortawesome/fontawesome-svg-core@^1.2.32": - version "1.2.32" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.32.tgz#da092bfc7266aa274be8604de610d7115f9ba6cf" - integrity sha512-XjqyeLCsR/c/usUpdWcOdVtWFVjPbDFBTQkn2fQRrWhhUoxriQohO2RWDxLyUM8XpD+Zzg5xwJ8gqTYGDLeGaQ== - dependencies: - "@fortawesome/fontawesome-common-types" "^0.2.32" - -"@fortawesome/free-regular-svg-icons@^5.15.1": - version "5.15.1" - resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.1.tgz#a8897d0ce325352dbba0e943101323e0175ee2b2" - integrity sha512-eD9NWFy89e7SVVtrLedJUxIpCBGhd4x7s7dhesokjyo1Tw62daqN5UcuAGu1NrepLLq1IeAYUVfWwnOjZ/j3HA== - dependencies: - "@fortawesome/fontawesome-common-types" "^0.2.32" - -"@fortawesome/free-solid-svg-icons@^5.15.1": - version "5.15.1" - resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.1.tgz#e1432676ddd43108b41197fee9f86d910ad458ef" - integrity sha512-EFMuKtzRMNbvjab/SvJBaOOpaqJfdSap/Nl6hst7CgrJxwfORR1drdTV6q1Ib/JVzq4xObdTDcT6sqTaXMqfdg== - dependencies: - "@fortawesome/fontawesome-common-types" "^0.2.32" - -"@fortawesome/vue-fontawesome@^3.0.0-3": - version "3.0.0-3" - resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.0-3.tgz#b658a7c1f35d4eb04ac85749da693be483442b25" - integrity sha512-fCM7+R9M7Y/ipKC5n9hukGpJHhe53JOENGqtku/KWtpXsnbGik3AS5zfJYEupV2uXOw/5S0RSSfttQ2hNIrmFA== - -"@intlify/core-base@9.1.6": - version "9.1.6" - resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.1.6.tgz#887fbeafe37d955bac50318f30ac589839f0d9fb" - integrity sha512-d5GDPpsQbqPkisSJA5b6nJFEkalY/IHAd7vOLNd/Sj4YaNRzXtInu2FoqKiOv8e/lQnXGTpurdCZg5Jxq1Gsxw== - dependencies: - "@intlify/devtools-if" "9.1.6" - "@intlify/message-compiler" "9.1.6" - "@intlify/message-resolver" "9.1.6" - "@intlify/runtime" "9.1.6" - "@intlify/shared" "9.1.6" - "@intlify/vue-devtools" "9.1.6" - -"@intlify/devtools-if@9.1.6": - version "9.1.6" - resolved "https://registry.yarnpkg.com/@intlify/devtools-if/-/devtools-if-9.1.6.tgz#739b195e430e24fbf8f864ec8a51e243e3347385" - integrity sha512-m8Api+kh+BtFa2FZ/JjIdr1ibsGGqBjdKCzWo5BZecEUxBquIeOQZwpokPh/0K5j+/PZleFXkVAMC5mNt+9WdA== - dependencies: - "@intlify/shared" "9.1.6" - -"@intlify/message-compiler@9.1.6": - version "9.1.6" - resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.1.6.tgz#e3e99165c1e6ecc496211017799ae59e15b05a18" - integrity sha512-DR8645VOrVK6x/8tkaCpHnckMAIcoOgeNS5j0wB12RfZoXYQp7vAXMaOP511KMll2mXCREgIB0ojpajiof7yzQ== - dependencies: - "@intlify/message-resolver" "9.1.6" - "@intlify/shared" "9.1.6" - source-map "0.6.1" - -"@intlify/message-resolver@9.1.6": - version "9.1.6" - resolved "https://registry.yarnpkg.com/@intlify/message-resolver/-/message-resolver-9.1.6.tgz#d7493c9f326d5feb0cd8538a6735b648a91d8f2f" - integrity sha512-UUnbawQa5U9sffd5wRIscqtyY1xWlwJbyfwCLPEWLvBhyAnCwPYlvaHGnnO0CSi0fzJTVwlV9DYzobh3agDeMA== - -"@intlify/runtime@9.1.6": - version "9.1.6" - resolved "https://registry.yarnpkg.com/@intlify/runtime/-/runtime-9.1.6.tgz#bf1548d9034c80eef92b06b240cb347effb41f71" - integrity sha512-U1QZ+TPf3kQQvWo4BA2mj3cHAxMRHXNTBhu2u+deh6ubTqXdZ19XGBTMSasrXG6RE+zSio9oM+ndoLja7JGtPg== - dependencies: - "@intlify/message-compiler" "9.1.6" - "@intlify/message-resolver" "9.1.6" - "@intlify/shared" "9.1.6" - -"@intlify/shared@9.1.6": - version "9.1.6" - resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.1.6.tgz#d03c9301898d6ddffe2a54c03e7664174fbcdfd9" - integrity sha512-6MtsKulyfZxdD7OuxjaODjj8QWoHCnLFAk4wkWiHqBCa6UCTC0qXjtEeZ1MxpQihvFmmJZauBUu25EvtngW5qQ== - -"@intlify/vue-devtools@9.1.6": - version "9.1.6" - resolved "https://registry.yarnpkg.com/@intlify/vue-devtools/-/vue-devtools-9.1.6.tgz#88faadf203951a2a10107440fa99b58f4637d40d" - integrity sha512-UdNovg4OML9rIr1sOGZzTfNr1nUy4UQpDf5ni4dNC93T6FIkVJz0n1Np7Vp7e6gDjcmufRYcV99tEwjQSN9+5A== - dependencies: - "@intlify/message-resolver" "9.1.6" - "@intlify/runtime" "9.1.6" - "@intlify/shared" "9.1.6" +"@chenfengyuan/vue-qrcode@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@chenfengyuan/vue-qrcode/-/vue-qrcode-1.0.2.tgz#37d71902e166e1ae58176bd6cb9c40905c1b0949" + integrity sha512-hwy1d4YMJAyEh+V7dLPG8eAKACRvugzSB4ylwb6QNqo84KHTF50/5EJcBYdUhTRPfAqrxG0i6jDAXONWOGyQbQ== + dependencies: + qrcode "^1.4.4" + +"@fortawesome/fontawesome-common-types@^0.2.36": + version "0.2.36" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz#b44e52db3b6b20523e0c57ef8c42d315532cb903" + integrity sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg== + +"@fortawesome/fontawesome-common-types@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz#949995a05c0d8801be7e0a594f775f1dbaa0d893" + integrity sha512-CA3MAZBTxVsF6SkfkHXDerkhcQs0QPofy43eFdbWJJkZiq3SfiaH1msOkac59rQaqto5EqWnASboY1dBuKen5w== + +"@fortawesome/fontawesome-svg-core@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.3.0.tgz#343fac91fa87daa630d26420bfedfba560f85885" + integrity sha512-UIL6crBWhjTNQcONt96ExjUnKt1D68foe3xjEensLDclqQ6YagwCRYVQdrp/hW0ALRp/5Fv/VKw+MqTUWYYvPg== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.3.0" + +"@fortawesome/free-regular-svg-icons@5.15.4": + version "5.15.4" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.4.tgz#b97edab436954333bbeac09cfc40c6a951081a02" + integrity sha512-9VNNnU3CXHy9XednJ3wzQp6SwNwT3XaM26oS4Rp391GsxVYA+0oDR2J194YCIWf7jNRCYKjUCOduxdceLrx+xw== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.36" + +"@fortawesome/free-solid-svg-icons@5.15.4": + version "5.15.4" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz#2a68f3fc3ddda12e52645654142b9e4e8fbb6cc5" + integrity sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.36" + +"@fortawesome/vue-fontawesome@2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-2.0.6.tgz#87e691ed87f28f4667238573a29743f543a087f6" + integrity sha512-V3vT3flY15AKbUS31aZOP12awQI3aAzkr2B1KnqcHLmwrmy51DW3pwyBczKdypV8QxBZ8U68Hl2XxK2nudTxpg== + +"@kazvmoe-infra/pinch-zoom-element@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@kazvmoe-infra/pinch-zoom-element/-/pinch-zoom-element-1.2.0.tgz#eb3ca34c53b4410c689d60aca02f4a497ce84aba" + integrity sha512-HBrhH5O/Fsp2bB7EGTXzCsBAVcMjknSagKC5pBdGpKsF8meHISR0kjDIdw4YoE0S+0oNMwJ6ZUZyIBrdywxPPw== + dependencies: + pointer-tracker "^2.0.3" "@nodelib/fs.scandir@2.1.3": version "2.1.3" @@ -1092,6 +949,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" @@ -1117,6 +982,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" @@ -1149,262 +1019,242 @@ dependencies: "@types/node" "*" -"@ungap/event-target@^0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.1.0.tgz#88d527d40de86c4b0c99a060ca241d755999915b" - 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-transform-on@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz#9b9c691cd06fc855221a2475c3cc831d774bc7dc" - integrity sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA== +"@ungap/event-target@0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.2.3.tgz#be682c681126dca2371c4e1a1721f8e8bb400905" + integrity sha512-7Bz0qdvxNGV9n0f+xcMKU7wsEfK6PNzo8IdAcOiBgMNyCuU0Mk9dv0Hbd/Kgr+MFFfn4xLHFbuOt820egT5qEA== -"@vue/babel-plugin-jsx@^1.0.3": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.0.5.tgz#72820d5fb371c41d2113b31b16787995e8bdf69a" - integrity sha512-Jtipy7oI0am5e1q5Ahunm/cCcCh5ssf5VkMQsLR383S3un5Qh7NBfxgSK9kmWf4IXJEhDeYp9kHv8G/EnMai9A== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@babel/plugin-syntax-jsx" "^7.0.0" - "@babel/template" "^7.0.0" - "@babel/traverse" "^7.0.0" - "@babel/types" "^7.0.0" - "@vue/babel-helper-vue-transform-on" "^1.0.2" - camelcase "^6.0.0" - html-tags "^3.1.0" - svg-tags "^1.0.0" +"@vue/babel-helper-vue-jsx-merge-props@1.2.1", "@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/compiler-core@3.0.11": - version "3.0.11" - resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.0.11.tgz#5ef579e46d7b336b8735228758d1c2c505aae69a" - integrity sha512-6sFj6TBac1y2cWCvYCA8YzHJEbsVkX7zdRs/3yK/n1ilvRqcn983XvpBbnN3v4mZ1UiQycTvOiajJmOgN9EVgw== - dependencies: - "@babel/parser" "^7.12.0" - "@babel/types" "^7.12.0" - "@vue/shared" "3.0.11" - estree-walker "^2.0.1" - source-map "^0.6.1" - -"@vue/compiler-dom@3.0.11": - version "3.0.11" - resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.0.11.tgz#b15fc1c909371fd671746020ba55b5dab4a730ee" - integrity sha512-+3xB50uGeY5Fv9eMKVJs2WSRULfgwaTJsy23OIltKgMrynnIj8hTYY2UL97HCoz78aDw1VDXdrBQ4qepWjnQcw== - dependencies: - "@vue/compiler-core" "3.0.11" - "@vue/shared" "3.0.11" - -"@vue/compiler-sfc@^3.0.7": - version "3.0.11" - resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.0.11.tgz#cd8ca2154b88967b521f5ad3b10f5f8b6b665679" - integrity sha512-7fNiZuCecRleiyVGUWNa6pn8fB2fnuJU+3AGjbjl7r1P5wBivfl02H4pG+2aJP5gh2u+0wXov1W38tfWOphsXw== - dependencies: - "@babel/parser" "^7.13.9" - "@babel/types" "^7.13.0" - "@vue/compiler-core" "3.0.11" - "@vue/compiler-dom" "3.0.11" - "@vue/compiler-ssr" "3.0.11" - "@vue/shared" "3.0.11" - consolidate "^0.16.0" - estree-walker "^2.0.1" - hash-sum "^2.0.0" - lru-cache "^5.1.1" - magic-string "^0.25.7" - merge-source-map "^1.1.0" - postcss "^8.1.10" - postcss-modules "^4.0.0" - postcss-selector-parser "^6.0.4" - source-map "^0.6.1" - -"@vue/compiler-ssr@3.0.11": - version "3.0.11" - resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.0.11.tgz#ac5a05fd1257412fa66079c823d8203b6a889a13" - integrity sha512-66yUGI8SGOpNvOcrQybRIhl2M03PJ+OrDPm78i7tvVln86MHTKhM3ERbALK26F7tXl0RkjX4sZpucCpiKs3MnA== - dependencies: - "@vue/compiler-dom" "3.0.11" - "@vue/shared" "3.0.11" - -"@vue/devtools-api@^6.0.0-beta.7": - version "6.0.0-beta.8" - resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.0.0-beta.8.tgz#0dbb2d21b13818c04b7f70b1b0c3f7aae95e4fb4" - integrity sha512-I2QYmUYvuJnMMX/4/i+fbCf2nlAkjfw58siw0whFlUT7LTvTc5Xv7jcLP5XS3+8LkHd7UtVhU7EBGMfA7jkXXg== - -"@vue/reactivity@3.0.11": - version "3.0.11" - resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.0.11.tgz#07b588349fd05626b17f3500cbef7d4bdb4dbd0b" - integrity sha512-SKM3YKxtXHBPMf7yufXeBhCZ4XZDKP9/iXeQSC8bBO3ivBuzAi4aZi0bNoeE2IF2iGfP/AHEt1OU4ARj4ao/Xw== +"@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: - "@vue/shared" "3.0.11" + "@babel/plugin-syntax-jsx" "^7.2.0" -"@vue/runtime-core@3.0.11": - version "3.0.11" - resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.0.11.tgz#c52dfc6acf3215493623552c1c2919080c562e44" - integrity sha512-87XPNwHfz9JkmOlayBeCCfMh9PT2NBnv795DSbi//C/RaAnc/bGZgECjmkD7oXJ526BZbgk9QZBPdFT8KMxkAg== +"@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: - "@vue/reactivity" "3.0.11" - "@vue/shared" "3.0.11" + "@babel/plugin-syntax-jsx" "^7.2.0" -"@vue/runtime-dom@3.0.11": - version "3.0.11" - resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.0.11.tgz#7a552df21907942721feb6961c418e222a699337" - integrity sha512-jm3FVQESY3y2hKZ2wlkcmFDDyqaPyU3p1IdAX92zTNeCH7I8zZ37PtlE1b9NlCtzV53WjB4TZAYh9yDCMIEumA== +"@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: - "@vue/runtime-core" "3.0.11" - "@vue/shared" "3.0.11" - csstype "^2.6.8" - -"@vue/shared@3.0.11": - version "3.0.11" - resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.0.11.tgz#20d22dd0da7d358bb21c17f9bde8628152642c77" - integrity sha512-b+zB8A2so8eCE0JsxjL24J7vdGl8rzPQ09hZNhystm+KqSbKcAej1A+Hbva1rCMmTTqA+hFnUSDc5kouEo0JzA== - -"@vue/test-utils@^2.0.0-beta.8": - version "2.0.0-rc.6" - resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.0.0-rc.6.tgz#d0aac24d20450d379e183f70542c0822670b8783" - integrity sha512-0cnQBVH589PwgqWpyv1fgCAz+9Ram/MsvN3ZEAEVXi1aPuhUa22EudGc0WezQ9PKwR+L40NrBmt3JBXE2tSRRQ== + "@babel/plugin-syntax-jsx" "^7.2.0" -"@webassemblyjs/ast@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" +"@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/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-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: - "@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" + "@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/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-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: - "@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" + "@vue/babel-plugin-transform-vue-jsx" "^1.2.1" + camelcase "^5.0.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/test-utils@1.0.0-beta.28": + version "1.0.0-beta.28" + resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.0.0-beta.28.tgz#767c43413df8cde86128735e58923803e444b9a5" + integrity sha512-uVbFJG0g/H9hf2pgWUdhvQYItRGzQ44cMFf00wp0YEo85pxuvM9e3mx8QLQfx6R2CogxbK4CvV7qvkLblehXeQ== 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" + dom-event-types "^1.0.0" + lodash "^4.17.4" -"@webassemblyjs/ieee754@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz#712329dbef240f36bf57bd2f7b8fb9bf4154421e" +"@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: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.8.5.tgz#044edeb34ea679f3e04cd4fd9824d5e35767ae10" +"@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: "@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" - -"@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" - 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" - -"@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" - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-buffer" "1.8.5" - "@webassemblyjs/wasm-gen" "1.8.5" - "@webassemblyjs/wasm-parser" "1.8.5" - -"@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" - 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" - -"@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": @@ -1433,18 +1283,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" @@ -1479,6 +1330,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" @@ -1498,6 +1354,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" @@ -1535,6 +1401,11 @@ ansi-regex@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -1545,6 +1416,13 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + ansi-styles@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" @@ -1564,6 +1442,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,12 +1577,6 @@ async@1.x: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^2.0.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381" - dependencies: - lodash "^4.17.11" - async@^2.5.0: version "2.6.1" resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" @@ -1707,9 +1587,10 @@ atob@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" -autoprefixer@^6.3.1, autoprefixer@^6.4.0: +autoprefixer@6.7.7, autoprefixer@^6.3.1: version "6.7.7" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014" + integrity sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ= dependencies: browserslist "^1.7.6" caniuse-db "^1.0.30000634" @@ -1731,12 +1612,12 @@ autoprefixer@^9.8.0: postcss "^7.0.32" postcss-value-parser "^4.1.0" -axios@^0.21.0: - version "0.21.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" - integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== +axios@^0.21.1: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== dependencies: - follow-redirects "^1.10.0" + follow-redirects "^1.14.0" babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: version "6.26.0" @@ -1770,9 +1651,10 @@ babel-core@^6.1.4, babel-core@^6.26.0: slash "^1.0.0" source-map "^0.5.7" -babel-eslint@^7.0.0: +babel-eslint@7.2.3: version "7.2.3" resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.3.tgz#b2fe2d80126470f5c19442dc757253a897710827" + integrity sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc= dependencies: babel-code-frame "^6.22.0" babel-traverse "^6.23.1" @@ -1799,7 +1681,7 @@ babel-helpers@^6.24.1: babel-runtime "^6.22.0" babel-template "^6.24.1" -babel-loader@^8.0.6: +babel-loader@8.0.6: version "8.0.6" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.6.tgz#e33bdb6f362b03f4bb141a0c21ab87c501b70dfb" integrity sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw== @@ -1822,7 +1704,7 @@ babel-plugin-dynamic-import-node@^2.3.0: dependencies: object.assign "^4.1.0" -babel-plugin-lodash@^3.3.4: +babel-plugin-lodash@3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/babel-plugin-lodash/-/babel-plugin-lodash-3.3.4.tgz#4f6844358a1340baed182adbeffa8df9967bc196" integrity sha512-yDZLjK7TCkWl1gpBeBGmuaDIFhZKmkoL+Cu2MUUjv5VxUZx/z7tBGBCBcQs5RI1Bkz5LLmNdjx7paOyQtMovyg== @@ -1914,11 +1796,6 @@ base64-js@^1.0.2: version "1.3.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - base64id@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6" @@ -1953,19 +1830,20 @@ 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" -bluebird@^3.3.0: +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.7.2: +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== @@ -1989,7 +1867,7 @@ body-parser@1.18.3, body-parser@^1.16.1: raw-body "2.3.3" type-is "~1.6.16" -body-scroll-lock@^2.6.4: +body-scroll-lock@2.6.4: version "2.6.4" resolved "https://registry.yarnpkg.com/body-scroll-lock/-/body-scroll-lock-2.6.4.tgz#567abc60ef4d656a79156781771398ef40462e94" integrity sha512-NP08WsovlmxEoZP9pdlqrE+AhNaivlTrz9a0FF37BQsnOrpN48eNqivKkE7SYpM9N+YIPjsdVzfLAUQDBm6OQw== @@ -2034,7 +1912,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== @@ -2148,10 +2026,9 @@ buffer-fill@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" -buffer-from@^1.0.0, buffer-from@^1.1.1: +buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== buffer-xor@^1.0.3: version "1.0.3" @@ -2165,14 +2042,6 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.4.3: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -2185,25 +2054,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" @@ -2321,15 +2215,16 @@ chai-nightwatch@~0.1.x: assertion-error "1.0.0" deep-eql "0.1.3" -chai@^3.5.0: +chai@3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247" + integrity sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc= dependencies: assertion-error "^1.0.1" deep-eql "^0.1.3" type-detect "^1.0.0" -chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: +chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -2387,7 +2282,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: @@ -2405,27 +2300,66 @@ 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" -chromatism@^3.0.0: +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" + integrity sha1-pySdNTweTzV35ESsQRccTi5iSxI= -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" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-87.0.4.tgz#749f69e9427880abff19c1838258c35238397e50" - integrity sha512-kD4N/L8c0nAzh1eEAiAbEIq6Pn5TvGvckODvP5dPqF90q5tPiAJZCoWWSOUV/mrPxiodjHPfmNeOfGERHugzug== +chromedriver@87.0.7: + version "87.0.7" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-87.0.7.tgz#74041e02ff7f633e91b98eb707e2476f713dc4ca" + integrity sha512-7J7iN2rJuSDsKb9BUUMewJt07PuTlZYd809D10dUCT1rjMD3i2jUw7dum9RxdC1xO3aFwMd8TwZ5NR82T+S+Dg== dependencies: "@testim/chrome-version" "^1.0.7" - axios "^0.21.0" + axios "^0.21.1" del "^6.0.0" extract-zip "^2.0.1" https-proxy-agent "^5.0.0" @@ -2490,14 +2424,14 @@ cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" clone-deep@^4.0.1: version "4.0.1" @@ -2585,11 +2519,6 @@ colorette@^1.2.0: resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== -colorette@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== - colormin@^1.0.5: version "1.1.2" resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133" @@ -2598,14 +2527,15 @@ colormin@^1.0.5: css-color-names "0.0.4" has "^1.0.1" +colors@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + colors@^1.1.0: version "1.3.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d" -colors@~0.6.0: - version "0.6.2" - resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc" - colors@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" @@ -2626,9 +2556,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" @@ -2659,9 +2590,10 @@ concat-stream@^1.5.0: readable-stream "^2.2.2" typedarray "^0.0.6" -connect-history-api-fallback@^1.1.0: +connect-history-api-fallback@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" + integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== connect@^3.6.0: version "3.6.6" @@ -2682,12 +2614,11 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" -consolidate@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.16.0.tgz#a11864768930f2f19431660a65906668f5fbdc16" - integrity sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ== +consolidate@^0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.14.5.tgz#5a25047bc76f73072667c8cb52c989888f494c63" dependencies: - bluebird "^3.7.2" + bluebird "^3.1.1" constants-browserify@^1.0.0: version "1.0.0" @@ -2741,6 +2672,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" @@ -2757,6 +2705,18 @@ core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" +cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892" + dependencies: + is-directory "^0.3.1" + js-yaml "^3.4.3" + minimist "^1.2.0" + object-assign "^4.1.0" + os-homedir "^1.0.1" + parse-json "^2.2.0" + require-from-string "^1.1.0" + cosmiconfig@^5.0.0: version "5.2.1" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" @@ -2806,13 +2766,15 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" -cropperjs@^1.4.3: +cropperjs@1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/cropperjs/-/cropperjs-1.4.3.tgz#dc44d6c9e73269e7f96894c726ab91e8913f9e90" + integrity sha512-fsUjHuS9mvKVh2aXVRgNcUDlFplW+v4eEB6sOHVI9kMV4G3GViSD4p1qvNLg1ko4ZhOnF0L8/9uXcY4s2bFQPg== -cross-spawn@^4.0.2: +cross-spawn@4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" + integrity sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE= dependencies: lru-cache "^4.0.1" which "^1.2.9" @@ -2847,9 +2809,10 @@ css-color-names@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" -css-loader@^0.28.0: +css-loader@0.28.11: version "0.28.11" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.11.tgz#c3f9864a700be2711bb5a2462b2389b1a392dab7" + integrity sha512-wovHgjAx8ZIMGSL8pTys7edA1ClmzxHeY6n/d97gg5odgsxEgKjULPR0viqyC+FWMCL9sfqoC/QCUBo62tLvPg== dependencies: babel-code-frame "^6.26.0" css-selector-tokenizer "^0.7.0" @@ -2940,18 +2903,13 @@ csso@~2.3.1: clap "^1.0.9" source-map "^0.5.3" -csstype@^2.6.8: - version "2.6.17" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.17.tgz#4cf30eb87e1d1a005d8b6510f95292413f6a1c0e" - integrity sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A== - currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" dependencies: array-find-index "^1.0.1" -custom-event-polyfill@^1.0.7: +custom-event-polyfill@1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee" integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w== @@ -2983,6 +2941,11 @@ dateformat@^1.0.6: get-stdin "^4.0.1" meow "^3.3.0" +de-indent@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" + integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0= + debug@2, debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -3154,9 +3117,10 @@ diff@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" -diff@^3.0.1, diff@^3.1.0: +diff@3.5.0, diff@^3.1.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== diffie-hellman@^5.0.0: version "5.0.3" @@ -3196,6 +3160,10 @@ dom-converter@~0.2: dependencies: utila "~0.4" +dom-event-types@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dom-event-types/-/dom-event-types-1.0.0.tgz#5830a0a29e1bf837fe50a70cd80a597232813cae" + dom-serialize@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" @@ -3319,6 +3287,11 @@ emojis-list@^3.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== +encode-utf8@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda" + integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw== + encodeurl@~1.0.1, encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -3366,12 +3339,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: @@ -3418,7 +3392,7 @@ escalade@^3.0.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4" integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ== -escape-html@^1.0.3, escape-html@~1.0.3: +escape-html@1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= @@ -3449,13 +3423,15 @@ escodegen@1.x.x, escodegen@^1.6.1: optionalDependencies: source-map "~0.6.1" -eslint-config-standard@^12.0.0: +eslint-config-standard@12.0.0: version "12.0.0" resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-12.0.0.tgz#638b4c65db0bd5a41319f96bba1f15ddad2107d9" + integrity sha512-COUz8FnXhqFitYj4DTqHzidjIL/t4mumGZto5c7DrBpvWoie+Sn3P4sLEzUGeYhRElWuFEf8K1S1EfvD1vixCQ== -eslint-friendly-formatter@^2.0.5: +eslint-friendly-formatter@2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/eslint-friendly-formatter/-/eslint-friendly-formatter-2.0.7.tgz#657f95a19af4989636afebb1cc9de6cebbd088ee" + integrity sha1-ZX+VoZr0mJY2r+uxzJ3mzrvQiO4= dependencies: chalk "^1.0.0" extend "^3.0.0" @@ -3469,9 +3445,10 @@ eslint-import-resolver-node@^0.3.2: debug "^2.6.9" resolve "^1.5.0" -eslint-loader@^2.1.0: +eslint-loader@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-2.1.2.tgz#453542a1230d6ffac90e4e7cb9cadba9d851be68" + integrity sha512-rA9XiXEOilLYPOIInvVH5S/hYfyTPyxag6DZhoQOduM+3TkghAEQ3VcFO8VnX4J4qg/UIBzp72aOf/xvYmpmsg== dependencies: loader-fs-cache "^1.0.0" loader-utils "^1.0.2" @@ -3493,9 +3470,10 @@ eslint-plugin-es@^1.3.1: eslint-utils "^1.3.0" regexpp "^2.0.1" -eslint-plugin-import@^2.13.0: +eslint-plugin-import@2.17.2: version "2.17.2" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.17.2.tgz#d227d5c6dc67eca71eb590d2bb62fb38d86e9fcb" + integrity sha512-m+cSVxM7oLsIpmwNn2WXTJoReOF9f/CtLMo7qOVmKd1KntBy0hEcuNZ3erTmWjx+DxRO0Zcrm5KwAvI9wHcV5g== dependencies: array-includes "^3.0.3" contains-path "^0.1.0" @@ -3509,9 +3487,10 @@ eslint-plugin-import@^2.13.0: read-pkg-up "^2.0.0" resolve "^1.10.0" -eslint-plugin-node@^7.0.0: +eslint-plugin-node@7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-7.0.1.tgz#a6e054e50199b2edd85518b89b4e7b323c9f36db" + integrity sha512-lfVw3TEqThwq0j2Ba/Ckn2ABdwmL5dkOgAux1rvOk6CO7A6yGyPI2+zIxN6FyNkp1X1X/BSvKOceD6mBWSj4Yw== dependencies: eslint-plugin-es "^1.3.1" eslint-utils "^1.3.1" @@ -3520,17 +3499,20 @@ eslint-plugin-node@^7.0.0: resolve "^1.8.1" semver "^5.5.0" -eslint-plugin-promise@^4.0.0: +eslint-plugin-promise@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz#1e08cb68b5b2cd8839f8d5864c796f56d82746db" + integrity sha512-faAHw7uzlNPy7b45J1guyjazw28M+7gJokKUjC5JSFoYfUEyy6Gw/i7YQvmv2Yk00sUjWcmzXQLpU1Ki/C2IZQ== -eslint-plugin-standard@^4.0.0: +eslint-plugin-standard@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.0.tgz#f845b45109c99cd90e77796940a344546c8f6b5c" + integrity sha512-OwxJkR6TQiYMmt1EsNRMe5qG3GsbjlcOhbGUBY4LtavF9DsLaTcoR+j2Tdjqi23oUwKNUqX7qcn5fPStafMdlA== -eslint-plugin-vue@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-5.2.2.tgz#86601823b7721b70bc92d54f1728cfc03b36283c" +eslint-plugin-vue@5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-5.2.3.tgz#3ee7597d823b5478804b2feba9863b1b74273961" + integrity sha512-mGwMqbbJf0+VvpGR5Lllq0PMxvTdrZ/ZPjmhkacrCHbubJeJOt+T6E3HUzAifa2Mxi7RSdJfC9HFpOeSYVMMIw== dependencies: vue-eslint-parser "^5.0.0" @@ -3549,9 +3531,10 @@ eslint-visitor-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" -eslint@^5.16.0: +eslint@5.16.0: version "5.16.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea" + integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg== dependencies: "@babel/code-frame" "^7.0.0" ajv "^6.9.1" @@ -3638,11 +3621,6 @@ estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" -estree-walker@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" - integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== - esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -3659,9 +3637,10 @@ events@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" -eventsource-polyfill@^0.9.6: +eventsource-polyfill@0.9.6: version "0.9.6" resolved "https://registry.yarnpkg.com/eventsource-polyfill/-/eventsource-polyfill-0.9.6.tgz#10e0d187f111b167f28fdab918843ce7d818f13c" + integrity sha1-EODRh/ERsWfyj9q5GIQ859gY8Tw= evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" @@ -3720,9 +3699,10 @@ expand-range@^1.8.1: dependencies: fill-range "^2.1.0" -express@^4.13.3: +express@4.16.4: version "4.16.4" resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" + integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg== dependencies: accepts "~1.3.5" array-flatten "1.1.1" @@ -3831,6 +3811,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" @@ -3873,9 +3865,10 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" -file-loader@^3.0.1: +file-loader@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa" + integrity sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw== dependencies: loader-utils "^1.0.2" schema-utils "^1.0.0" @@ -3946,7 +3939,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: @@ -3954,6 +3947,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" @@ -3973,7 +3975,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== @@ -4010,10 +4012,10 @@ follow-redirects@^1.0.0: dependencies: debug "=3.1.0" -follow-redirects@^1.10.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7" - integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg== +follow-redirects@^1.14.0: + version "1.14.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" + integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" @@ -4058,6 +4060,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" @@ -4078,6 +4087,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" @@ -4085,7 +4099,7 @@ ftp@~0.3.10: readable-stream "1.1.x" xregexp "2.0.0" -function-bind@^1.0.2, function-bind@^1.1.1: +function-bind@1.1.1, function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -4106,13 +4120,6 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" -generic-names@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-2.0.1.tgz#f8a378ead2ccaa7a34f0317b05554832ae41b872" - integrity sha512-kPCHWa1m9wGG/OwQpeweTwM/PYiQLrUIxXbt/P4Nic3LbGjCP0YwrALHW1uNLKZ0LIMg+RF+XRlj2ekT9ZlZAQ== - dependencies: - loader-utils "^1.1.0" - gensync@^1.0.0-beta.1: version "1.0.0-beta.1" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" @@ -4181,6 +4188,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" @@ -4235,6 +4249,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" @@ -4400,11 +4426,6 @@ hash-sum@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04" -hash-sum@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-2.0.0.tgz#81d01bb5de8ea4a214ad5d6ead1b523460b0b45a" - integrity sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg== - hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" @@ -4416,7 +4437,7 @@ he@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" -he@1.2.x: +he@1.2.x, he@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -4469,9 +4490,10 @@ html-tags@^3.1.0: resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg== -html-webpack-plugin@^3.0.0: +html-webpack-plugin@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b" + integrity sha1-sBq71yOsqqeze2r0SS69oD2d03s= dependencies: html-minifier "^3.2.3" loader-utils "^0.2.16" @@ -4519,9 +4541,10 @@ http-proxy-agent@1: debug "2" extend "3" -http-proxy-middleware@^0.17.2: +http-proxy-middleware@0.17.4: version "0.17.4" resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833" + integrity sha1-ZC6ISIUdZvCdTxJJEoRtuutBuDM= dependencies: http-proxy "^1.16.2" is-glob "^3.1.0" @@ -4578,16 +4601,6 @@ icss-utils@^2.1.0: dependencies: postcss "^6.0.1" -icss-utils@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" - integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== - -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - ieee754@^1.1.4: version "1.1.12" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" @@ -4680,6 +4693,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" @@ -4704,9 +4722,10 @@ ini@^1.3.5, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" -inject-loader@^2.0.1: +inject-loader@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/inject-loader/-/inject-loader-2.0.1.tgz#1a7b45d60a81610459ac76079c3ce2a654d0dfc7" + integrity sha1-GntF1gqBYQRZrHYHnDziplTQ38c= dependencies: loader-utils "^0.2.3" @@ -4798,6 +4817,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" @@ -4919,7 +4945,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: @@ -5065,10 +5091,6 @@ isarray@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" -isarray@^2.0.1: - version "2.0.4" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.4.tgz#38e7bcbb0f3ba1b7933c86ba1894ddfc3781bbb7" - isbinaryfile@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.3.tgz#5d6def3edebf6e8ca8cae9c30183a804b5f8be80" @@ -5079,9 +5101,10 @@ isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" -iso-639-1@^2.0.3: +iso-639-1@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/iso-639-1/-/iso-639-1-2.0.3.tgz#72dd3448ac5629c271628c5ac566369428d6ccd0" + integrity sha512-PZhOTDH05ZLJyCqxAH65EzGaLO801KCvoEahAFoiqlp2HmnGUm8sO19KwWPCiWd3odjmoYd9ytzk2WtVYgWyCg== isobject@^2.0.0: version "2.1.0" @@ -5093,9 +5116,10 @@ isobject@^3.0.0, isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" -isparta-loader@^2.0.0: +isparta-loader@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isparta-loader/-/isparta-loader-2.0.0.tgz#4425f496c93f765bbceb4dd938576da307566ed1" + integrity sha1-RCX0lsk/dlu8603ZOFdtowdWbtE= dependencies: isparta "4.x.x" @@ -5149,7 +5173,7 @@ js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" -js-yaml@3.x: +js-yaml@3.x, js-yaml@^3.4.3: version "3.12.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.1.tgz#295c8632a18a23e054cf5c9d3cecafe678167600" dependencies: @@ -5183,9 +5207,10 @@ jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" -json-loader@^0.5.4: +json-loader@0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" + integrity sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w== json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: version "1.0.2" @@ -5227,9 +5252,10 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" -karma-coverage@^1.1.1: +karma-coverage@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/karma-coverage/-/karma-coverage-1.1.2.tgz#cc09dceb589a83101aca5fe70c287645ef387689" + integrity sha512-eQawj4Cl3z/CjxslYy9ariU4uDh7cCNFZHNWXWRpl0pNeblY/4wHR7M7boTYXWrn9bY0z2pZmr11eKje/S/hIw== dependencies: dateformat "^1.0.6" istanbul "^0.4.0" @@ -5237,52 +5263,62 @@ karma-coverage@^1.1.1: minimatch "^3.0.0" source-map "^0.5.1" -karma-firefox-launcher@^1.1.0: +karma-firefox-launcher@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/karma-firefox-launcher/-/karma-firefox-launcher-1.1.0.tgz#2c47030452f04531eb7d13d4fc7669630bb93339" + integrity sha512-LbZ5/XlIXLeQ3cqnCbYLn+rOVhuMIK9aZwlP6eOLGzWdo1UVp7t6CN3DP4SafiRLjexKwHeKHDm0c38Mtd3VxA== -karma-mocha-reporter@^2.2.1: +karma-mocha-reporter@2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz#15120095e8ed819186e47a0b012f3cd741895560" + integrity sha1-FRIAlejtgZGG5HoLAS8810GJVWA= dependencies: chalk "^2.1.0" log-symbols "^2.1.0" strip-ansi "^4.0.0" -karma-mocha@^1.2.0: +karma-mocha@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-1.3.0.tgz#eeaac7ffc0e201eb63c467440d2b69c7cf3778bf" + integrity sha1-7qrH/8DiAetjxGdEDStpx883eL8= dependencies: minimist "1.2.0" -karma-sinon-chai@^2.0.2: +karma-sinon-chai@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/karma-sinon-chai/-/karma-sinon-chai-2.0.2.tgz#e28c109b989973abafc28a7c9f09ef24a05e07c2" + integrity sha512-SDgh6V0CUd+7ruL1d3yG6lFzmJNGRNQuEuCYXLaorruNP9nwQfA7hpsp4clx4CbOo5Gsajh3qUOT7CrVStUKMw== -karma-sourcemap-loader@^0.3.7: - version "0.3.7" - resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz#91322c77f8f13d46fed062b042e1009d4c4505d8" +karma-sourcemap-loader@0.3.8: + version "0.3.8" + resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.8.tgz#d4bae72fb7a8397328a62b75013d2df937bdcf9c" + integrity sha512-zorxyAakYZuBcHRJE+vbrK2o2JXLFWK8VVjiT/6P+ltLBUGUvqTEkUiQ119MGdOrK7mrmxXHZF1/pfT6GgIZ6g== dependencies: graceful-fs "^4.1.2" -karma-spec-reporter@0.0.26: - version "0.0.26" - resolved "https://registry.yarnpkg.com/karma-spec-reporter/-/karma-spec-reporter-0.0.26.tgz#bf5561377dce1b63cf2c975c1af3e35f199e2265" +karma-spec-reporter@0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/karma-spec-reporter/-/karma-spec-reporter-0.0.33.tgz#5b2712c3eaff7ae50dbd6ad4e5fb59befd740a96" + integrity sha512-xRVevDUkiIVhKbDQ3CmeGEpyzA4b3HeVl95Sx5yJAvurpdKUSYF6ZEbQOqKJ7vrtDniABV1hyFez9KX9+7ruBA== dependencies: - colors "~0.6.0" + colors "1.4.0" -karma-webpack@^4.0.0-rc.3: - version "4.0.0-rc.6" - resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-4.0.0-rc.6.tgz#02ac6a47c7fc166c8b208446069a424698082405" +karma-webpack@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-4.0.2.tgz#23219bd95bdda853e3073d3874d34447c77bced0" + integrity sha512-970/okAsdUOmiMOCY8sb17A2I8neS25Ad9uhyK3GHgmRSIFJbDcNEFE8dqqUhNe9OHiCC9k3DMrSmtd/0ymP1A== dependencies: - async "^2.0.0" + clone-deep "^4.0.1" loader-utils "^1.1.0" - source-map "^0.5.6" - webpack-dev-middleware "^3.2.0" + neo-async "^2.6.1" + schema-utils "^1.0.0" + source-map "^0.7.3" + webpack-dev-middleware "^3.7.0" -karma@^3.0.0: +karma@3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/karma/-/karma-3.1.4.tgz#3890ca9722b10d1d14b726e1335931455788499e" + integrity sha512-31Vo8Qr5glN+dZEVIpnPCxEGleqE0EY6CtC2X9TagRV3rRQ3SNrvfhddICkJgUK3AgqpeKSZau03QumTGhGoSw== dependencies: bluebird "^3.3.0" body-parser "^1.16.1" @@ -5392,9 +5428,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" @@ -5413,6 +5450,15 @@ 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" @@ -5422,9 +5468,10 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" -localforage@^1.5.0: +localforage@1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.7.3.tgz#0082b3ca9734679e1bd534995bdd3b24cf10f204" + integrity sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ== dependencies: lie "3.1.1" @@ -5678,10 +5725,6 @@ lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" -lodash.tail@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" - lodash.toplainobject@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash.toplainobject/-/lodash.toplainobject-3.0.0.tgz#28790ad942d293d78aa663a07ecf7f52ca04198d" @@ -5693,7 +5736,12 @@ lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" -lodash@^4.16.4, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.5.0: +lodash@4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.5.0: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" @@ -5736,9 +5784,10 @@ log4js@^3.0.0: rfdc "^1.1.2" streamroller "0.7.0" -lolex@^1.4.0, lolex@^1.6.0: +lolex@1.6.0, lolex@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6" + integrity sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY= longest-streak@^2.0.1: version "2.0.4" @@ -5762,7 +5811,7 @@ lower-case@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" -lru-cache@4.1.x, lru-cache@^4.0.1: +lru-cache@4.1.x, lru-cache@^4.0.1, lru-cache@^4.1.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" dependencies: @@ -5775,17 +5824,17 @@ 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" -magic-string@^0.25.7: - version "0.25.7" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" - integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== - dependencies: - sourcemap-codec "^1.4.4" - make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -5793,9 +5842,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-cache@^0.2.2: version "0.2.2" @@ -5860,13 +5912,21 @@ media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" -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" @@ -5905,13 +5965,6 @@ merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" -merge-source-map@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" - integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== - dependencies: - source-map "^0.6.1" - merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -5939,7 +5992,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: @@ -5996,10 +6049,15 @@ mime@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" -mime@^2.0.3, mime@^2.3.1, mime@^2.4.2: +mime@^2.0.3, mime@^2.3.1: version "2.4.3" resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.3.tgz#229687331e86f68924e6cb59e1cdd937f18275fe" +mime@^2.4.4: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -6009,9 +6067,10 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -mini-css-extract-plugin@^0.5.0: +mini-css-extract-plugin@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz#ac0059b02b9692515a637115b0cc9fed3a35c7b0" + integrity sha512-IuaLjruM0vMKhUUT51fQdQzBYTX49dLj8w68ALEAe2A4iYNpIC4eMac67mt3NzycvjOlf07/kYxJDc0RTl1Wqw== dependencies: loader-utils "^1.1.0" schema-utils "^1.0.0" @@ -6059,6 +6118,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" @@ -6066,12 +6146,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" @@ -6094,13 +6189,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== @@ -6125,9 +6227,10 @@ mocha-nightwatch@3.2.2: mkdirp "0.5.1" supports-color "3.1.2" -mocha@^3.1.0: +mocha@3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d" + integrity sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg== dependencies: browser-stdout "1.3.0" commander "2.9.0" @@ -6178,11 +6281,6 @@ nan@^2.12.1: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" -nanoid@^3.1.22: - version "3.1.22" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844" - integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ== - nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -6227,6 +6325,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" @@ -6235,9 +6338,10 @@ nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" -nightwatch@^0.9.8: +nightwatch@0.9.21: version "0.9.21" resolved "https://registry.yarnpkg.com/nightwatch/-/nightwatch-0.9.21.tgz#9e794a7514b4fd5f46602d368e50515232ab9e90" + integrity sha1-nnlKdRS0/V9GYC02jlBRUjKrnpA= dependencies: chai-nightwatch "~0.1.x" ejs "2.5.7" @@ -6256,9 +6360,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" @@ -6270,7 +6375,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" @@ -6282,7 +6387,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" @@ -6361,7 +6466,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" @@ -6499,9 +6604,10 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" -opn@^4.0.2: +opn@4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/opn/-/opn-4.0.2.tgz#7abc22e644dff63b0a96d5ab7f2790c0f01abc95" + integrity sha1-erwi5kTf9jsKltWrfyeQwPAavJU= dependencies: object-assign "^4.0.1" pinkie-promise "^2.0.0" @@ -6524,9 +6630,10 @@ optionator@^0.8.1, optionator@^0.8.2: type-check "~0.3.2" wordwrap "~1.0.0" -ora@^0.3.0: +ora@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/ora/-/ora-0.3.0.tgz#367a078ad25cfb096da501115eb5b401e07d7495" + integrity sha1-NnoHitJc+wltpQERXrW0AeB9dJU= dependencies: chalk "^1.1.1" cli-cursor "^1.0.2" @@ -6537,7 +6644,7 @@ os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" -os-homedir@^1.0.0: +os-homedir@^1.0.0, os-homedir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -6571,6 +6678,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" @@ -6709,7 +6823,7 @@ parse-json@^5.0.0: json-parse-better-errors "^1.0.1" lines-and-columns "^1.1.6" -parse-link-header@^1.0.1: +parse-link-header@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/parse-link-header/-/parse-link-header-1.0.1.tgz#bedfe0d2118aeb84be75e7b025419ec8a61140a7" integrity sha1-vt/g0hGK64S+deewJUGeyKYRQKc= @@ -6736,9 +6850,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" @@ -6818,9 +6933,15 @@ pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" -phoenix@^1.3.0: +phoenix@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.4.0.tgz#9cec8dbd8cbc59ecd2147bc09ca8ceb56b860d75" + integrity sha512-+M7erjPRtrHM53Bc9sT/WSyNFOEhsAc48PBsex0R87hu0GhBRMNE2uAb8MT2gKtJmAYxv5Quaozh/PBuhO8tdQ== + +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" @@ -6870,9 +6991,27 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -pngjs@^3.3.0: - version "3.3.3" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b" +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@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" + integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== + +pointer-tracker@^2.0.3: + version "2.4.0" + resolved "https://registry.yarnpkg.com/pointer-tracker/-/pointer-tracker-2.4.0.tgz#78721c2d2201486db11ec1094377f03023b621b3" + integrity sha512-pWI2tpaM/XNtc9mUTv42Rmjf6mkHvE8LT5DDEq0G7baPNhxNM9E3CepubPplSoSLk9E5bwQrAMyDcPVmJyTW4g== + +portal-vue@2.1.7: + version "2.1.7" + resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-2.1.7.tgz#ea08069b25b640ca08a5b86f67c612f15f4e4ad4" + integrity sha512-+yCno2oB3xA7irTt0EU5Ezw22L2J51uKAacE/6hMPMoO/mx3h4rXFkkBkT4GFsMDv/vEe8TNKC3ujJJ0PTwb6g== posix-character-classes@^0.1.0: version "0.1.1" @@ -6952,6 +7091,15 @@ postcss-less@^3.1.4: dependencies: postcss "^7.0.14" +postcss-load-config@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" + dependencies: + cosmiconfig "^2.1.0" + object-assign "^4.1.0" + postcss-load-options "^1.2.0" + postcss-load-plugins "^2.3.0" + postcss-load-config@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.0.tgz#c84d692b7bb7b41ddced94ee62e8ab31b417b003" @@ -6960,7 +7108,21 @@ postcss-load-config@^2.0.0: cosmiconfig "^5.0.0" import-cwd "^2.0.0" -postcss-loader@^3.0.0: +postcss-load-options@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c" + dependencies: + cosmiconfig "^2.1.0" + object-assign "^4.1.0" + +postcss-load-plugins@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92" + dependencies: + cosmiconfig "^2.1.1" + object-assign "^4.1.0" + +postcss-loader@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d" integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA== @@ -7042,11 +7204,6 @@ postcss-modules-extract-imports@^1.2.0: dependencies: postcss "^6.0.1" -postcss-modules-extract-imports@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" - integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== - postcss-modules-local-by-default@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069" @@ -7054,15 +7211,6 @@ postcss-modules-local-by-default@^1.2.0: css-selector-tokenizer "^0.7.0" postcss "^6.0.1" -postcss-modules-local-by-default@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" - integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== - dependencies: - icss-utils "^5.0.0" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.1.0" - postcss-modules-scope@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90" @@ -7070,13 +7218,6 @@ postcss-modules-scope@^1.1.0: css-selector-tokenizer "^0.7.0" postcss "^6.0.1" -postcss-modules-scope@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" - integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== - dependencies: - postcss-selector-parser "^6.0.4" - postcss-modules-values@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20" @@ -7084,27 +7225,6 @@ postcss-modules-values@^1.3.0: icss-replace-symbols "^1.1.0" postcss "^6.0.1" -postcss-modules-values@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" - integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== - dependencies: - icss-utils "^5.0.0" - -postcss-modules@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-4.0.0.tgz#2bc7f276ab88f3f1b0fadf6cbd7772d43b5f3b9b" - integrity sha512-ghS/ovDzDqARm4Zj6L2ntadjyQMoyJmi0JkLlYtH2QFLrvNlxH5OAVRPWPeKilB0pY7SbuhO173KOWkPAxRJcw== - dependencies: - generic-names "^2.0.1" - icss-replace-symbols "^1.1.0" - lodash.camelcase "^4.3.0" - postcss-modules-extract-imports "^3.0.0" - postcss-modules-local-by-default "^4.0.0" - postcss-modules-scope "^3.0.0" - postcss-modules-values "^4.0.0" - string-hash "^1.1.1" - postcss-normalize-charset@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1" @@ -7211,14 +7331,6 @@ postcss-selector-parser@^6.0.2: indexes-of "^1.0.1" uniq "^1.0.1" -postcss-selector-parser@^6.0.4: - version "6.0.5" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.5.tgz#042d74e137db83e6f294712096cb413f5aa612c4" - integrity sha512-aFYPoYmXbZ1V6HZaSvat08M97A8HqO6Pjz+PiNpw/DhuRrC72XWAdp3hL6wusDCN31sSmcZyMGa2hZEuX+Xfhg== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - postcss-svgo@^2.1.1: version "2.1.6" resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" @@ -7267,7 +7379,7 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0 source-map "^0.5.6" supports-color "^3.2.3" -postcss@^6.0.1: +postcss@^6.0.1, postcss@^6.0.8: version "6.0.23" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" dependencies: @@ -7293,15 +7405,6 @@ postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0. source-map "^0.6.1" supports-color "^6.1.0" -postcss@^8.1.10: - version "8.2.12" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.12.tgz#81248a1a87e0f575cc594a99a08207fd1c4addc4" - integrity sha512-BJnGT5+0q2tzvs6oQfnY2NpEJ7rIXNfBnZtQOKCIsweeWXBXeDd5k31UgTdS3d/c02ouspufn37mTaHWkJyzMQ== - dependencies: - colorette "^1.2.2" - nanoid "^3.1.22" - source-map "^0.6.1" - prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -7314,6 +7417,10 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" +prettier@^1.16.0: + version "1.17.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.17.1.tgz#ed64b4e93e370cb8a25b9ef7fef3e4fd1c0995db" + pretty-error@^2.0.2: version "2.1.1" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" @@ -7407,7 +7514,7 @@ pumpify@^1.3.3: inherits "^2.0.3" pump "^2.0.0" -punycode.js@^2.1.0: +punycode.js@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.1.0.tgz#f3937f7a914152c2dc17e9c280a2cf86a26b7cda" integrity sha512-LvGUJ9QHiESLM4yn8JuJWicstRcJKRmP46psQw1HvCZ9puLFwYMKJWvkAkP3OHBVzNzZGx/D53EYJrIaKd9gZQ== @@ -7433,17 +7540,14 @@ qjobs@^1.1.4: resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" qrcode@^1.4.4: - version "1.4.4" - resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83" - integrity sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q== + version "1.5.0" + resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.0.tgz#95abb8a91fdafd86f8190f2836abbfc500c72d1b" + integrity sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ== dependencies: - buffer "^5.4.3" - buffer-alloc "^1.2.0" - buffer-from "^1.1.1" dijkstrajs "^1.0.1" - isarray "^2.0.1" - pngjs "^3.3.0" - yargs "^13.2.4" + encode-utf8 "^1.0.3" + pngjs "^5.0.0" + yargs "^15.3.1" qs@6.5.2: version "6.5.2" @@ -7477,9 +7581,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" @@ -7507,9 +7612,10 @@ raw-body@2, raw-body@2.3.3: iconv-lite "0.4.23" unpipe "1.0.0" -raw-loader@^0.5.1: +raw-loader@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" + integrity sha1-DD0L6u2KAclm2Xh793goElKpeao= rc@^1.2.7: version "1.2.8" @@ -7616,6 +7722,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" @@ -7833,6 +7946,10 @@ require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" +require-from-string@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -7868,7 +7985,7 @@ resolve@1.1.x, resolve@^1.1.6: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" -resolve@^1.10.0, resolve@^1.5.0, resolve@^1.8.1: +resolve@^1.10.0, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.8.1: version "1.11.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.0.tgz#4014870ba296176b86343d50b60f3b50609ce232" dependencies: @@ -7908,12 +8025,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" @@ -7928,6 +8052,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +ruffle-mirror@2021.4.11: + 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" @@ -7974,20 +8103,21 @@ samsam@1.x, samsam@^1.1.3: version "1.3.0" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" -"sass-loader@git://github.com/webpack-contrib/sass-loader": - version "7.1.0" - resolved "git://github.com/webpack-contrib/sass-loader#e279f2a129eee0bd0b624b5acd498f23a81ee35e" +sass-loader@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.2.0.tgz#e34115239309d15b2527cb62b5dfefb62a96ff7f" + integrity sha512-h8yUWaWtsbuIiOCgR9fd9c2lRXZ2uG+h8Dzg/AGNj+Hg/3TO8+BBAW9mEP+mh8ei+qBKqSJ0F1FLlYjNBc61OA== dependencies: clone-deep "^4.0.1" loader-utils "^1.0.1" - lodash.tail "^4.1.1" neo-async "^2.5.0" pify "^4.0.1" semver "^5.5.0" -sass@^1.17.3: +sass@1.20.1: version "1.20.1" resolved "https://registry.yarnpkg.com/sass/-/sass-1.20.1.tgz#737b901fe072336da540b6d00ec155e2267420da" + integrity sha512-BnCawee/L5kVG3B/5Jg6BFwASqUwFVE6fj2lnkVuSXDgQ7gMAhY9a2yPeqsKhJMCN+Wgx0r2mAW7XF/aTF5qtA== dependencies: chokidar "^2.0.0" @@ -8003,11 +8133,20 @@ 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" -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0: +"semver@2 || 3 || 4 || 5", semver@5.6.0, semver@^5.3.0, semver@^5.5.0, semver@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" @@ -8020,7 +8159,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== @@ -8047,9 +8186,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" @@ -8060,9 +8209,10 @@ serve-static@1.13.2: parseurl "~1.3.2" send "0.16.2" -serviceworker-webpack-plugin@^1.0.0: +serviceworker-webpack-plugin@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/serviceworker-webpack-plugin/-/serviceworker-webpack-plugin-1.0.1.tgz#481863288487e92da01d49745336c72ef8a6136b" + integrity sha512-VgDEkZ3pA0HajsRaWtl5w6bLxAXx0Y+4dm7YeTcIxVmvC9YXvstex38HOBDuYETeDS5fUlBy/47gC0QYBrG0nw== dependencies: minimatch "^3.0.4" @@ -8120,10 +8270,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.8.4: - version "0.8.4" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" - integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== +shelljs@0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== dependencies: glob "^7.0.0" interpret "^1.0.0" @@ -8133,13 +8283,15 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" -sinon-chai@^2.8.0: +sinon-chai@2.14.0: version "2.14.0" resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-2.14.0.tgz#da7dd4cc83cd6a260b67cca0f7a9fdae26a1205d" + integrity sha512-9stIF1utB0ywNHNT7RgiXbdmen8QDCRsrTjw+G9TgKt1Yexjiv8TOWZ6WHsTPz57Yky3DIswZvEqX8fpuHNDtQ== -sinon@^2.1.0: +sinon@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.4.1.tgz#021fd64b54cb77d9d2fb0d43cdedfae7629c3a36" + integrity sha512-vFTrO9Wt0ECffDYIPSP/E5bBugt0UjcBQOfQUMh66xzkyPEnhl/vM2LRZi2ajuTdkH07sA6DzrM6KvdvGIH8xw== dependencies: diff "^3.1.0" formatio "1.2.0" @@ -8289,9 +8441,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" @@ -8300,25 +8453,25 @@ source-map-url@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" -source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + +source-map@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + source-map@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" dependencies: amdefine ">=0.0.4" -sourcemap-codec@^1.4.4: - version "1.4.8" - resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" - integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== - spdx-correct@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" @@ -8362,6 +8515,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" @@ -8423,11 +8583,6 @@ strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" -string-hash@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" - integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs= - string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -8443,7 +8598,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^3.0.0, string-width@^3.1.0: +string-width@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" dependencies: @@ -8451,6 +8606,15 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" @@ -8506,7 +8670,7 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: +strip-ansi@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" dependencies: @@ -8519,6 +8683,13 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" @@ -8560,14 +8731,14 @@ stylelint-config-recommended@^3.0.0: resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-3.0.0.tgz#e0e547434016c5539fe2650afd58049a2fd1d657" integrity sha512-F6yTRuc06xr1h5Qw/ykb2LuFynJ2IxkKfCMf+1xqPffkxh0S09Zc902XCffcsw/XMFq/OzQ1w54fLIDtmRNHnQ== -stylelint-config-standard@^20.0.0: +stylelint-config-standard@20.0.0: version "20.0.0" resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-20.0.0.tgz#06135090c9e064befee3d594289f50e295b5e20d" integrity sha512-IB2iFdzOTA/zS4jSVav6z+wGtin08qfj+YyExHB3LF9lnouQht//YyB0KZq9gGz5HNPkddHOzcY8HsUey6ZUlA== dependencies: stylelint-config-recommended "^3.0.0" -stylelint-rscss@^0.4.0: +stylelint-rscss@0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/stylelint-rscss/-/stylelint-rscss-0.4.0.tgz#ea77c478e1c703dbda878c00f53bbc002b2d07b7" integrity sha1-6nfEeOHHA9vah4wA9Tu8ACstB7c= @@ -8575,7 +8746,7 @@ stylelint-rscss@^0.4.0: postcss-resolve-nested-selector "0.1.1" postcss-selector-parser "2.2.1" -stylelint@^13.6.1: +stylelint@13.6.1: version "13.6.1" resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.6.1.tgz#cc1d76338116d55e8ff2be94c4a4386c1239b878" integrity sha512-XyvKyNE7eyrqkuZ85Citd/Uv3ljGiuYHC6UiztTR6sWS9rza8j3UeQv/eGcQS9NZz/imiC4GKdk1EVL3wst5vw== @@ -8707,7 +8878,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" @@ -8723,6 +8894,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" @@ -8730,27 +8913,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" @@ -9088,9 +9273,10 @@ urix@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" -url-loader@^1.1.2: +url-loader@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.1.2.tgz#b971d191b83af693c5e3fea4064be9e1f2d7f8d8" + integrity sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg== dependencies: loader-utils "^1.1.0" mime "^2.0.3" @@ -9114,10 +9300,9 @@ useragent@2.3.0: lru-cache "4.1.x" tmp "0.0.x" -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= util.promisify@1.0.0: version "1.0.0" @@ -9150,9 +9335,10 @@ uuid@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" -v-click-outside@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/v-click-outside/-/v-click-outside-2.1.3.tgz#b7297abe833a439dc0895e6418a494381e64b5e7" +v-click-outside@2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/v-click-outside/-/v-click-outside-2.1.5.tgz#aa69172fb41fcc79b26b9a4bc72a30ccf03f7a3c" + integrity sha512-VPNCOTZK6WZy73lcWc+R7IW1uaBFEO3/Csrs5CzWVOdvE30V8Y1+BE/BtTlcEmeDGx0eqdE7bSCg55Jj37PMJg== v8-compile-cache@^2.1.1: version "2.1.1" @@ -9198,11 +9384,10 @@ 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" @@ -9219,76 +9404,106 @@ vue-eslint-parser@^5.0.0: esquery "^1.0.1" lodash "^4.17.11" -vue-i18n@^9.0.0-beta.18: - version "9.1.6" - resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.1.6.tgz#4cf992e2aec5458bc19369973c96ea7d0f560321" - integrity sha512-FEC4HZkTH6QRIu/A0wlo0VS/GH3w/fuCC6xfvoC8IyhhtbG9A+go9NfW+HZ1ZXdAcO4EWcVQi04M+iSwuxgixw== - dependencies: - "@intlify/core-base" "9.1.6" - "@intlify/shared" "9.1.6" - "@intlify/vue-devtools" "9.1.6" - "@vue/devtools-api" "^6.0.0-beta.7" +vue-hot-reload-api@^2.2.0: + version "2.3.3" + resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.3.tgz#2756f46cb3258054c5f4723de8ae7e87302a1ccf" -vue-loader@^16.1.2: - version "16.2.0" - resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-16.2.0.tgz#046a53308dd47e58efe20ddec1edec027ce3b46e" - integrity sha512-TitGhqSQ61RJljMmhIGvfWzJ2zk9m1Qug049Ugml6QP3t0e95o0XJjk29roNEiPKJQBEi8Ord5hFuSuELzSp8Q== +vue-i18n@7.8.1: + version "7.8.1" + resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-7.8.1.tgz#2ce4b6efde679a1e05ddb5d907bfc1bc218803b2" + integrity sha512-BzB+EAPo/iFyFn/GXd/qVdDe67jfk+gmQaWUKD5BANhUclGrFxzRExzW2pYEAbhNm2pg0F12Oo+gL2IMLDcTAw== + +vue-loader@14.2.4: + version "14.2.4" + resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-14.2.4.tgz#d0a0e8236155fa7f9602cde65b0d38259e051ee2" + integrity sha512-bub2/rcTMJ3etEbbeehdH2Em3G2F5vZIjMK7ZUePj5UtgmZSTtOX1xVVawDpDsy021s3vQpO6VpWJ3z3nO8dDw== dependencies: - chalk "^4.1.0" - hash-sum "^2.0.0" - loader-utils "^2.0.0" + consolidate "^0.14.0" + hash-sum "^1.0.2" + loader-utils "^1.1.0" + lru-cache "^4.1.1" + postcss "^6.0.8" + postcss-load-config "^1.1.0" + postcss-selector-parser "^2.0.0" + prettier "^1.16.0" + resolve "^1.4.0" + source-map "^0.6.1" + vue-hot-reload-api "^2.2.0" + vue-style-loader "^4.0.1" + vue-template-es2015-compiler "^1.6.0" -vue-router@^4.0.5: - version "4.0.6" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.0.6.tgz#91750db507d26642f225b0ec6064568e5fe448d6" - integrity sha512-Y04llmK2PyaESj+N33VxLjGCUDuv9t4q2OpItEGU7POZiuQZaugV6cJpE6Qm1sVFtxufodLKN2y2dQl9nk0Reg== +vue-router@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.2.tgz#dedc67afe6c4e2bc25682c8b1c2a8c0d7c7e56be" + integrity sha512-opKtsxjp9eOcFWdp6xLQPLmRGgfM932Tl56U9chYTnoWqKxQ8M20N7AkdEbM5beUh6wICoFGYugAX9vQjyJLFg== -vue-style-loader@^4.0.0: +vue-style-loader@4.1.2, vue-style-loader@^4.0.1: version "4.1.2" resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.2.tgz#dedf349806f25ceb4e64f3ad7c0a44fba735fcf8" dependencies: hash-sum "^1.0.2" loader-utils "^1.0.2" -vue@^3.0.7: - version "3.0.11" - resolved "https://registry.yarnpkg.com/vue/-/vue-3.0.11.tgz#c82f9594cbf4dcc869241d4c8dd3e08d9a8f4b5f" - integrity sha512-3/eUi4InQz8MPzruHYSTQPxtM3LdZ1/S/BvaU021zBnZi0laRUyH6pfuE4wtUeLvI8wmUNwj5wrZFvbHUXL9dw== +vue-template-compiler@2.6.11: + version "2.6.11" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz#c04704ef8f498b153130018993e56309d4698080" + integrity sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA== dependencies: - "@vue/compiler-dom" "3.0.11" - "@vue/runtime-dom" "3.0.11" - "@vue/shared" "3.0.11" + de-indent "^1.0.2" + he "^1.1.0" -vuelidate@^0.7.6: - version "0.7.6" - resolved "https://registry.yarnpkg.com/vuelidate/-/vuelidate-0.7.6.tgz#84100c13b943470660d0416642845cd2a1edf4b2" - integrity sha512-suzIuet1jGcyZ4oUSW8J27R2tNrJ9cIfklAh63EbAkFjE380iv97BAiIeolRYoB9bF9usBXCu4BxftWN1Dkn3g== +vue-template-es2015-compiler@^1.6.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" -vuex@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/vuex/-/vuex-4.0.0.tgz#ac877aa76a9c45368c979471e461b520d38e6cf5" - integrity sha512-56VPujlHscP5q/e7Jlpqc40sja4vOhC4uJD1llBCWolVI8ND4+VzisDVkUMl+z5y0MpIImW6HjhNc+ZvuizgOw== +vue@2.6.11: + version "2.6.11" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5" + integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ== -watchpack@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" +vuelidate@0.7.7: + version "0.7.7" + resolved "https://registry.yarnpkg.com/vuelidate/-/vuelidate-0.7.7.tgz#5df3930a63ddecf56fde7bdacea9dbaf0c9bf899" + integrity sha512-pT/U2lDI67wkIqI4tum7cMSIfGcAMfB+Phtqh2ttdXURwvHRBJEAQ0tVbUsW9Upg83Q5QH59bnCoXI7A9JDGnA== + +vuex@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2" + integrity sha512-wLoqz0B7DSZtgbWL1ShIBBCjv22GV5U+vcBFox658g6V0s4wZV9P4YjCNyoHSyIBpj1f29JBoNQIqD82cR4O3w== + +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" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.0.tgz#ef751d25f4e9a5c8a35da600c5fda3582b5c6cff" +webpack-dev-middleware@3.7.3, webpack-dev-middleware@^3.7.0: + version "3.7.3" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5" + integrity sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ== dependencies: memory-fs "^0.4.1" - mime "^2.4.2" + mime "^2.4.4" + mkdirp "^0.5.1" range-parser "^1.2.1" webpack-log "^2.0.0" -webpack-hot-middleware@^2.12.2: +webpack-hot-middleware@2.24.3: version "2.24.3" resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.24.3.tgz#5bb76259a8fc0d97463ab517640ba91d3382d4a6" + integrity sha512-pPlmcdoR2Fn6UhYjAhp1g/IJy1Yc9hD+T6O9mjRcWV2pFbBjIFoJXhP0CoD0xPOhWJuWXuZXGBga9ybbOdzXpg== dependencies: ansi-html "0.0.7" html-entities "^1.2.0" @@ -9302,50 +9517,59 @@ webpack-log@^2.0.0: ansi-colors "^3.0.0" uuid "^3.3.2" -webpack-merge@^0.14.1: +webpack-merge@0.14.1: version "0.14.1" resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-0.14.1.tgz#d6bfe6d9360a024e1e7f8e6383ae735f1737cd23" + integrity sha1-1r/m2TYKAk4ef45jg65zXxc3zSM= dependencies: lodash.find "^3.2.1" lodash.isequal "^4.2.0" 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.46.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" @@ -9381,14 +9605,14 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" wrappy@1: version "1.0.2" @@ -9447,20 +9671,17 @@ 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" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== -yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^18.1.3: +yargs-parser@^18.1.2, yargs-parser@^18.1.3: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== @@ -9468,21 +9689,22 @@ yargs-parser@^18.1.3: camelcase "^5.0.0" decamelize "^1.2.0" -yargs@^13.2.4: - version "13.3.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== +yargs@^15.3.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== dependencies: - cliui "^5.0.0" - find-up "^3.0.0" + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" get-caller-file "^2.0.1" require-directory "^2.1.1" require-main-filename "^2.0.0" set-blocking "^2.0.0" - string-width "^3.0.0" + string-width "^4.2.0" which-module "^2.0.0" y18n "^4.0.0" - yargs-parser "^13.1.2" + yargs-parser "^18.1.2" yauzl@^2.10.0: version "2.10.0" @@ -9495,3 +9717,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==