logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: 23232e1c8f35d7ddc8adb7a6dbf813b2831c90ec
parent b225c3578f3c89af5ed3a0be3f8f3a6bbcedcc7d
Author: Shpuld Shpludson <shp@cock.li>
Date:   Fri,  6 Nov 2020 17:27:25 +0000

Merge branch 'develop' into 'master'

Merge develop into master for 2.2.0

See merge request pleroma/pleroma-fe!1278

Diffstat:

MCHANGELOG.md23+++++++++++++++++++++--
Mbuild/webpack.base.conf.js9---------
Mdocs/CONFIGURATION.md4++--
Mdocs/HACKING.md6+++---
Ddocs/USER_GUIDE.md207-------------------------------------------------------------------------------
Rdocs/example_emoji.png -> docs/assets/example_emoji.png0
Adocs/assets/example_markdown.png0
Mdocs/index.md2+-
Adocs/user_guide/index.md44++++++++++++++++++++++++++++++++++++++++++++
Adocs/user_guide/posting_reading_basic_functions.md76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/user_guide/settings.md116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/user_guide/timelines.md13+++++++++++++
Adocs/user_guide/users_follow_mute_block.md11+++++++++++
Mpackage.json5++++-
Msrc/App.js51+++------------------------------------------------
Msrc/App.scss180+++++++++++++++----------------------------------------------------------------
Msrc/App.vue69+--------------------------------------------------------------------
Msrc/boot/after_store.js1+
Msrc/components/account_actions/account_actions.js8++++++++
Msrc/components/account_actions/account_actions.vue35+++++++++++++++++++----------------
Msrc/components/attachment/attachment.js22++++++++++++++++++----
Msrc/components/attachment/attachment.vue22++++++++++++++++------
Msrc/components/chat/chat.js116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/components/chat/chat.scss24++++++++++++++++--------
Msrc/components/chat/chat.vue11++++++++---
Msrc/components/chat/chat_layout_utils.js7+++++++
Msrc/components/chat_list_item/chat_list_item.vue12++++++------
Msrc/components/chat_message/chat_message.js10++++++++++
Msrc/components/chat_message/chat_message.scss15++++++++++++++-
Msrc/components/chat_message/chat_message.vue7++++---
Msrc/components/chat_new/chat_new.js10++++++++++
Msrc/components/chat_new/chat_new.scss8+++++---
Msrc/components/chat_new/chat_new.vue10++++++++--
Msrc/components/chat_panel/chat_panel.js10++++++++++
Msrc/components/chat_panel/chat_panel.vue12++++++++----
Msrc/components/contrast_ratio/contrast_ratio.vue26++++++++++++++++++++------
Msrc/components/conversation/conversation.js20+++++++++++++++++++-
Msrc/components/conversation/conversation.vue11+++++++++--
Asrc/components/desktop_nav/desktop_nav.js89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/desktop_nav/desktop_nav.scss112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/desktop_nav/desktop_nav.vue79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/emoji_input/emoji_input.js9+++++++++
Msrc/components/emoji_input/emoji_input.vue2+-
Msrc/components/emoji_picker/emoji_picker.js31++++++++++++++++++++++++++++---
Msrc/components/emoji_picker/emoji_picker.scss2+-
Msrc/components/emoji_picker/emoji_picker.vue10++++++++--
Msrc/components/exporter/exporter.js7+++++++
Msrc/components/exporter/exporter.vue8++++++--
Msrc/components/extra_buttons/extra_buttons.js20++++++++++++++++++++
Msrc/components/extra_buttons/extra_buttons.vue53++++++++++++++++++++++++++++++++++++++++-------------
Msrc/components/favorite_button/favorite_button.js14+++++++++++---
Msrc/components/favorite_button/favorite_button.vue31++++++++++++++++++-------------
Msrc/components/follow_requests/follow_requests.vue4+++-
Msrc/components/font_control/font_control.js8++++++++
Msrc/components/font_control/font_control.vue5++++-
Msrc/components/global_notice_list/global_notice_list.js8++++++++
Msrc/components/global_notice_list/global_notice_list.vue5+++--
Msrc/components/image_cropper/image_cropper.js10++++++++++
Msrc/components/image_cropper/image_cropper.vue10++++++----
Msrc/components/importer/importer.js11+++++++++++
Msrc/components/importer/importer.vue14++++++++------
Msrc/components/interface_language_switcher/interface_language_switcher.vue13++++++++++++-
Msrc/components/login_form/login_form.js8++++++++
Msrc/components/login_form/login_form.vue5+++--
Msrc/components/media_modal/media_modal.js10++++++++++
Msrc/components/media_modal/media_modal.vue10++++++++--
Msrc/components/media_upload/media_upload.js8++++++++
Msrc/components/media_upload/media_upload.vue21+++++++--------------
Msrc/components/mfa_form/recovery_form.js8++++++++
Msrc/components/mfa_form/recovery_form.vue5+++--
Msrc/components/mfa_form/totp_form.js9+++++++++
Msrc/components/mfa_form/totp_form.vue6++++--
Msrc/components/mobile_nav/mobile_nav.js12++++++++++++
Msrc/components/mobile_nav/mobile_nav.vue282++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/components/mobile_post_status_button/mobile_post_status_button.js8++++++++
Msrc/components/mobile_post_status_button/mobile_post_status_button.vue4++--
Msrc/components/moderation_tools/moderation_tools.vue2+-
Msrc/components/nav_panel/nav_panel.js23+++++++++++++++++++++++
Msrc/components/nav_panel/nav_panel.vue168++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/components/notification/notification.js22++++++++++++++++++++++
Msrc/components/notification/notification.scss32++++++++++++++++++++++++++++++++
Msrc/components/notification/notification.vue51++++++++++++++++++++++++++++++++++++---------------
Msrc/components/notifications/notifications.js6++++++
Msrc/components/notifications/notifications.scss31-------------------------------
Msrc/components/notifications/notifications.vue6+++++-
Msrc/components/panel_loading/panel_loading.vue20+++++++++++++++++---
Msrc/components/password_reset/password_reset.js8++++++++
Msrc/components/password_reset/password_reset.vue6+++---
Msrc/components/poll/poll_form.js12++++++++++++
Msrc/components/poll/poll_form.vue35++++++++++++++++++++++++++++-------
Msrc/components/popover/popover.vue6+++---
Msrc/components/post_status_form/post_status_form.js28++++++++++++++++++++++++++--
Msrc/components/post_status_form/post_status_form.vue114++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/components/react_button/react_button.js23++++++++++++++++++-----
Msrc/components/react_button/react_button.vue6++++--
Asrc/components/reply_button/reply_button.js16++++++++++++++++
Asrc/components/reply_button/reply_button.vue39+++++++++++++++++++++++++++++++++++++++
Msrc/components/retweet_button/retweet_button.js13++++++++-----
Msrc/components/retweet_button/retweet_button.vue37+++++++++++++++++++++++--------------
Msrc/components/scope_selector/scope_selector.js15+++++++++++++++
Msrc/components/scope_selector/scope_selector.vue58+++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/components/search/search.js10++++++++++
Msrc/components/search/search.vue8++++++--
Msrc/components/search_bar/search_bar.js14++++++++++++--
Msrc/components/search_bar/search_bar.vue94++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/components/settings_modal/settings_modal_content.js23+++++++++++++++++++++++
Msrc/components/settings_modal/settings_modal_content.scss2+-
Msrc/components/settings_modal/settings_modal_content.vue8++++----
Msrc/components/settings_modal/tabs/data_import_export_tab.js28+++++++++++++++++++++-------
Msrc/components/settings_modal/tabs/data_import_export_tab.vue17+++++++++++++++++
Msrc/components/settings_modal/tabs/filtering_tab.js8++++++++
Msrc/components/settings_modal/tabs/filtering_tab.vue5++++-
Msrc/components/settings_modal/tabs/general_tab.js10++++++++++
Msrc/components/settings_modal/tabs/general_tab.vue17++++++++++++++---
Msrc/components/settings_modal/tabs/profile_tab.js12++++++++++++
Msrc/components/settings_modal/tabs/profile_tab.scss6++----
Msrc/components/settings_modal/tabs/profile_tab.vue44+++++++++++++++++++++++++++-----------------
Msrc/components/settings_modal/tabs/theme_tab/preview.vue43+++++++++++++++++++++++++++++++++++--------
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.js8++++++++
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.vue10++++++++--
Msrc/components/shadow_control/shadow_control.js14++++++++++++++
Msrc/components/shadow_control/shadow_control.vue25++++++++++++++++++++-----
Msrc/components/side_drawer/side_drawer.js28++++++++++++++++++++++++++++
Msrc/components/side_drawer/side_drawer.vue88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Msrc/components/status/status.js78+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/components/status/status.scss62++++++++++++++++++++++++++++++++------------------------------
Msrc/components/status/status.vue129++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Msrc/components/status_content/status_content.js18++++++++++++++++++
Msrc/components/status_content/status_content.vue30++++++++++++++++++------------
Msrc/components/status_popover/status_popover.js6++++++
Msrc/components/status_popover/status_popover.vue6+++++-
Msrc/components/still-image/still-image.js10++++++----
Msrc/components/still-image/still-image.vue11+++++++----
Msrc/components/tab_switcher/tab_switcher.js3++-
Msrc/components/tab_switcher/tab_switcher.scss4++--
Msrc/components/timeline/timeline.js87++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/components/timeline/timeline.vue20++++++++++++++++----
Msrc/components/timeline_menu/timeline_menu.js18++++++++++++++++++
Msrc/components/timeline_menu/timeline_menu.vue61+++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/components/user_avatar/user_avatar.vue22++++++++++++++++++----
Msrc/components/user_card/user_card.js16++++++++++++++++
Msrc/components/user_card/user_card.vue80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/components/user_list_popover/user_list_popover.js6++++++
Msrc/components/user_list_popover/user_list_popover.vue6+++++-
Msrc/components/user_profile/user_profile.js8++++++++
Msrc/components/user_profile/user_profile.vue9+++++----
Msrc/components/video_attachment/video_attachment.js47++++++++++++++++++++++++++++++++++-------------
Msrc/components/video_attachment/video_attachment.vue3++-
Msrc/hocs/with_load_more/with_load_more.js12+++++++++++-
Msrc/hocs/with_subscription/with_subscription.js12+++++++++++-
Msrc/i18n/ca.json190++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/i18n/en.json10+++++++++-
Msrc/i18n/eo.json79++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Msrc/i18n/es.json11+++++++++--
Msrc/i18n/eu.json63++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Asrc/i18n/fa.json155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/i18n/fi.json3++-
Msrc/i18n/fr.json43+++++++++++++++++++++++++++++++++++--------
Msrc/i18n/it.json20++++++++++++++------
Msrc/i18n/pl.json59++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/i18n/ru.json5+++++
Msrc/i18n/zh.json265+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Asrc/i18n/zh_Hant.json810+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main.js5+++++
Msrc/modules/api.js22+++++++++++++++-------
Msrc/modules/chats.js33+++++++++++++++++++++++----------
Msrc/modules/config.js3++-
Msrc/modules/instance.js4+++-
Msrc/modules/statuses.js10++++++++++
Msrc/services/api/api.service.js32+++++++++++++++++++++++++++++---
Msrc/services/chat_service/chat_service.js77++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/services/chat_utils/chat_utils.js23++++++++++++++++++++++-
Msrc/services/entity_normalizer/entity_normalizer.service.js3+++
Msrc/services/follow_request_fetcher/follow_request_fetcher.service.js5+++--
Msrc/services/notifications_fetcher/notifications_fetcher.service.js8+++++---
Asrc/services/promise_interval/promise_interval.js34++++++++++++++++++++++++++++++++++
Msrc/services/timeline_fetcher/timeline_fetcher.service.js6++++--
Mstatic/config.json3++-
Dstatic/fontello.json417-------------------------------------------------------------------------------
Dstatic/logo.png0
Astatic/logo.svg71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mstatic/terms-of-service.html2+-
Mtest/unit/specs/services/chat_service/chat_service.spec.js9++++++---
Myarn.lock211+++++++++++++++----------------------------------------------------------------
184 files changed, 4821 insertions(+), 2097 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -4,19 +4,38 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] -## [Unreleased patch] +### Added +- New option to optimize timeline rendering to make the site more responsive (enabled by default) +- New instance option `logoLeft` to move logo to the left side in desktop nav bar +- Import/export a muted users +- Proper handling of deletes when using websocket streaming +- Added optimistic chat message sending, so you can start writing next message before the previous one has been sent ## [2.1.2] - 2020-09-17 ### Fixed - Fixed chats list not updating its order when new messages come in -- Fixed chat messages sometimes getting lost when you receive a message at the same time +- Fixed chat messages sometimes getting lost when you receive a message at the same time +- Fixed clicking NSFW hider through status popover +- Fixed chat-view back button being hard to click +- Fixed fresh chat notifications being cleared immediately while leaving the chat view and not having time to actually see the messages +- Fixed multiple regressions in CSS styles +- Fixed multiple issues with input fields when using CJK font as default +- Fixed search field in navbar infringing into logo in some cases +- Fixed not being able to load the chat history in vertical screens when the message list doesn't take the full height of the scrollable container on the first fetch. +### Changed +- Clicking immediately when timeline shifts is now blocked to prevent misclicks +- Icons changed from fontello (FontAwesome 4 + others) to FontAwesome 5 due to problems with fontello. +- Some icons changed for better accessibility (lock, globe) +- Logo is now clickable +- Changed default logo to SVG version ## [2.1.1] - 2020-09-08 ### Changed - Polls will be hidden with status content if "Collapse posts with subjects" is enabled and the post is collapsed. ### Fixed +- Network fetches don't pile up anymore but wait for previous ones to finish to reduce throttling. - Autocomplete won't stop at the second @, so it'll still work with "@lain@l" and not start over. - Fixed weird autocomplete behavior when you write ":custom_emoji: ?" diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js @@ -3,7 +3,6 @@ var config = require('../config') var utils = require('./utils') var projectRoot = path.resolve(__dirname, '../') var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin') -var FontelloPlugin = require("fontello-webpack-plugin") var env = process.env.NODE_ENV // check env & config/index.js to decide weither to enable CSS Sourcemaps for the @@ -94,14 +93,6 @@ module.exports = { new ServiceWorkerWebpackPlugin({ entry: path.join(__dirname, '..', 'src/sw.js'), filename: 'sw-pleroma.js' - }), - new FontelloPlugin({ - config: require('../static/fontello.json'), - name: 'fontello', - output: { - css: 'static/[name].' + now + '.css', // [hash] is not supported. Use the current timestamp instead for versioning. - font: 'static/font/[name].' + now + '.[ext]' - } }) ] } diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md @@ -1,13 +1,13 @@ # Pleroma-FE configuration and customization for instance administrators -* *For user configuration, see [Pleroma-FE user guide](USER_GUIDE.md)* +* *For user configuration, see [Pleroma-FE user guide](../user_guide)* * *For local development server configuration, see [Hacking, tweaking, contributing](HACKING.md)* ## Where configuration is stored PleromaFE gets its configuration from several sources, in order of preference (the one above overrides ones below it) -1. `/api/statusnet/config.json` - this is generated on Backend and contains multiple things including instance name, char limit etc. It also contains FE/Client-specific data, PleromaFE uses `pleromafe` field of it. For more info on changing config on BE, look [here](https://docs-develop.pleroma.social/config.html#frontend_configurations) +1. `/api/statusnet/config.json` - this is generated on Backend and contains multiple things including instance name, char limit etc. It also contains FE/Client-specific data, PleromaFE uses `pleromafe` field of it. For more info on changing config on BE, look [here](../backend/configuration/cheatsheet.md#frontend_configurations) 2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/static/config.json). 3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/modules/instance.js) ) diff --git a/docs/HACKING.md b/docs/HACKING.md @@ -25,7 +25,7 @@ This could be a bit trickier, you basically need steps 1-4 from *develop build* ### Replacing your instance's frontend with custom FE build -This is the most easiest way to use and test FE build: you just need to copy or symlink contents of `dist` folder into backend's [static directory](https://docs.pleroma.social/static_dir.html), by default it is located in `instance/static`, or in `/var/lib/pleroma/static` for OTP release installations, create it if it doesn't exist already. Be aware that running `yarn build` wipes the contents of `dist` folder. +This is the most easiest way to use and test FE build: you just need to copy or symlink contents of `dist` folder into backend's [static directory](../backend/configuration/static_dir.md), by default it is located in `instance/static`, or in `/var/lib/pleroma/static` for OTP release installations, create it if it doesn't exist already. Be aware that running `yarn build` wipes the contents of `dist` folder. ### Running production build locally or on a separate server @@ -67,9 +67,9 @@ server { ### API, Data, Operations -In 99% cases PleromaFE uses [MastoAPI](https://docs.joinmastodon.org/api/) with [Pleroma Extensions](https://docs-develop.pleroma.social/differences_in_mastoapi_responses.html) to fetch the data. The rest is either QvitterAPI leftovers or pleroma-exclusive APIs. QvitterAPI doesn't exactly have documentation and uses different JSON structure and sometimes different parameters and workflows, [this](https://twitter-api.readthedocs.io/en/latest/index.html) could be a good reference though. Some pleroma-exclusive API may still be using QvitterAPI JSON structure. +In 99% cases PleromaFE uses [MastoAPI](https://docs.joinmastodon.org/api/) with [Pleroma Extensions](../backend/API/differences_in_mastoapi_responses.md) to fetch the data. The rest is either QvitterAPI leftovers or pleroma-exclusive APIs. QvitterAPI doesn't exactly have documentation and uses different JSON structure and sometimes different parameters and workflows, [this](https://twitter-api.readthedocs.io/en/latest/index.html) could be a good reference though. Some pleroma-exclusive API may still be using QvitterAPI JSON structure. -PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation. +PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation. For most part, PleromaFE tries to store all the info it can get in global vuex store - every user and post are passed trough updating mechanism where data is either added or merged with existing data, reactively updating the information throughout UI, so if in newest request user's post counter increased, it will be instantly updated in open user profile cards. This is also used to find users, posts and sometimes to build timelines and/or request parameters. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md @@ -1,207 +0,0 @@ -# Pleroma-FE user guide - -> Be prepared for breaking changes, unexpected behavior and this user guide becoming obsolete and wrong. - -> If there was no insanity -> -> it would be necessary to create it. -> -> --Catbag - -## Posting, reading, basic functions. - -After registering and logging in you're presented with your timeline in right column and new post form with timeline list and notifications in the left column. - -Posts will contain the text you are posting, but some content will be modified: - -1. Mentions: Mentions have the form of @user or @user<span></span>@instance.tld. These will become links to the user's profile. In addition, the mentioned user will always get a notification about the post they have been mentioned in, so only mention users that you want to receive this message. -2. URLs: URLs like `http://example.com` will be automatically be turned into a clickable links. -3. Hashtags: Hashtags like #cofe will also be turned into links. - -**Depending on your instance some of the options might not be available or have different defaults** - -Let's clear up some basic stuff. When you post something it's called a **post** or it could be called a **status** or even a **toot** or a **prööt** depending on whom you ask. Post has body/content but it also has some other stuff in it - from attachments, visibility scope, subject line. -* **Emoji** are small images embedded in text, there are two major types of emoji: [unicode emoji](https://en.wikipedia.org/wiki/Emoji) and custom emoji. While unicode emoji are universal and standardized, they can appear differently depending on where you are using them or may not appear at all on older systems. Custom emoji are more *fun* kind - instance administrator can define many images as *custom emoji* for their users. This works very simple - custom emoji is defined by its *shortcode* and an image, so that any shortcode enclosed in colons get replaced with image if such shortcode exist. -Let's say there's `:pleroma:` emoji defined on instance. That means -> First time using :pleroma: pleroma! - -will become -> First time using ![pleroma](./example_emoji.png) pleroma! - -Note that you can only use emoji defined on your instance, you cannot "copy" someone else's emoji, and will have to ask your administrator to copy emoji from other instance to yours. -Lastly, there's two convenience options for emoji: an emoji picker (smiley face to the right of "submit" button) and autocomplete suggestions - when you start typing :shortcode: it will automatically try to suggest you emoj and complete the shortcode for you if you select one. **Note** that if emoji doesn't show up in suggestions nor in emoji picker it means there's no such emoji on your instance, if shortcode doesn't match any defined emoji it will appear as text. -* **Attachments** are fairly simple - you can attach any file to a post as long as the file is within maximum size limits. If you're uploading explicit material you can mark all of your attachments as sensitive (or add `#nsfw` tag) - it will hide the images and videos behind a warning so that it won't be displayed instantly. -* **Subject line** also known as **CW** (Content Warning) could be used as a header to the post and/or to warn others about contents of the post having something that might upset somebody or something among those lines. Several applications allow to hide post content leaving only subject line visible. Using a subject line will not mark your images as sensitive, you will have to do that explicitly (see above). -* **Visiblity scope** controls who will be able to see your posts. There are four scopes available: - -1. `Public`: This is the default, and some fediverse software like GNU Social only supports this. This means that your post is accessible by anyone and will be shown in the public timelines. -2. `Unlisted`: This is the same as public, but your post won't appear in the public timelines. The post will still be accessible by anyone who comes across it (for example, by looking at your profile) or by direct linking. They will also appear in public searches. -3. `Followers only`: This will show your post only to your followers. Only they will be able to interact with it. Be careful: When somebody follows you, they will be able to see all your previous `followers only` posts as well! If you want to restrict who can follow you, consider [locking your account down to only approved followers](#profle). -4. `Direct`: This will only send the message to the people explicitly mentioned in the post. - -A few things to consider about the security and usage of these scopes: - -- None of these options will change the fact that the messages are all saved in the database unencrypted. They will be visible to your server admin and to any other admin of a server who receives this post. Do not share information that you would consider secret or dangerous. Use encrypted messaging systems for these things. -- Follower-only posts can lead to fragmented conversations. If you post a follower-only post and somebody else replies to it with a follower-only post, only people following both of you will see the whole conversation thread. Everybody else will only see half of it. Keep this in mind and keep conversations public if possible. -- Changing scopes during a thread or adding people to a direct message will not retroactively make them see the whole conversation. If you add someone to a direct message conversation, they will not see the post that happened before they were mentioned. -* **Reply-to** if you are replying to someone, your post will also contain a note that your post is referring to the post you're replying to. Person you're replying to will receive a notification *even* if you remove them from mentioned people. You won't receive notifications when replying to your own posts, but it's useful to reply to your own posts to provide people some context if it's a follow-up to a previous post. There's a small "Reply to ..." label under post author's name which you can hover on to see what post it's referring to. - -Sometimes you may encounter posts that seem different than what they are supposed to. For example, you might see a direct message without any mentions in the text. This can happen because internally, the Fediverse has a different addressing mechanism similar to email, with `to` and `cc` fields. While these are not directly accessible in PleromaFE, other software in the Fediverse might generate those posts. Do not worry in these cases, these are normal and not a bug. - -#### Rich text - -By default new posts you make are plaintext, meaning you can't make text **bold** or add custom links or make lists or anything like that. However if your instance allows it you can use Markdown or BBCode or HTML to spice up your text, however there are certain limitations to what HTML tags and what features of Markdown you can use. - -this section will be expanded later - -### Other actions - -In addition to posting you can also *favorite* post also known as *liking* them and *repeat* posts (also known as *retweeting*, *boosting* and even *reprööting*). Favoriting a post increments a counter on it, notifies post author of your affection towards that post and also adds that post to your "favorited" posts list (in your own profile, "Favorites" tab). Reprööting a post does all that and also repeats this post to your followers and your profile page with a note "*user* repeated post". - -Your own posts can be deleted, but this will only reliably delete the post from your own instance. Other instances will receive a deletion notice, but there's no way to force them to actually delete a post. In addition, not all instances that contain the message might even receive the deletion notice, because they might be offline or not known to have the post because they received it through a repeat. Lastly, deletion notice might not reach certain frontends and clients - post will be visible for them until page refresh or cache clear, they probably won't be able to interact with it apart from replying to it (which will have reply-to mark missing). - -If you are a moderator, you can also delete posts by other people. If those people are on your instance, it will delete the post and send out the deletion notice to other servers. If they are not on your instance, it will just remove the post from your local instance. - -There's also an option to report a user via a post (if the feature is available on your instance) which could be used to notify your (and probably other instance's) admin that someone is being naughty. - -## Users - -When you see someone, you can click on their user picture to view their profile, and click on the userpic in that to see *full* profile. You can *follow* them, *mute* and *block* them. Following is self-explanatory, it adds them t your Home Timeline, lists you as a follower and gives you access to follower-only posts if they have any. Muting makes posts and notifications made by them very tiny, giving you an option to see the post if you're curious. However on clients other than PleromaFE their posts will be completely removed. *Blocking* a user removes them from your timeline and notifications and prevents them from following you (automatically unfollows them from you). - -Please note that some users can be "locked", meaning instead of following them you send a follow request they need to approve for you to become their follower. - -## Timelines - -Currently you have several timelines to browse trough: -* **Timeline** aka Home Timeline - this timeline contains all posts by people you follow and your own posts, as well as posts mentioning you directly. -* **Interactions** all interactions you've had with people on the network, basically same as notifications except grouped in convenient way - mentions separate from favorites with repeats separate from follows -* **Direct Messages** all posts with `direct` scope addressed to you or mentioning you. -* **Public Timelines** all posts made by users on instance you're on -* **The Whole Known Network** also known as **TWKN** or **Federated Timeline** - all posts on the network by everyone, almost. Due to nature of the network your instance may not know *all** the instances on the network, so only posts originating from known instances are shown there. - -## Your profile - -By clicking wrench icon above the post form you can access the profile edit or "user settings" screen. - -### Profle - -Here you can set up how you appear to other users among with some other settings: - -- Name: this is text that displays next to your avatar in posts. Please note that you **cannot** change your *@<span></span>handle* -- Bio: this will be displayed under your profile - you can put anything you want there you want for everyone to see. -- Restrict your account to approved followers only: this makes your account "locked", when people follow you - you have to approve or deny their follow requests, this gives more control over who sees your followers only posts. -- Default visibility scope: this chooses your default post scope for new posts -- Strip rich text from all posts: this strips rich text formatting (bold/italics/lists etc) from all incoming posts. Will only affect newly fetched posts. - -If you're admin or moderator on your instance you also get "Show [role] badge in my profile" - this controls whether to show "Admin" or "Moderator** label on your profile page. - -**For all options mentioned above you have to click "Submit" button for changes to take place** - -- Avatar: this changes picture next to your posts. Your avatar shouldn't exceed 2 MiB (2097152 bytes) or it could cause problems with certain instances. -- Banner: this changes background on your profile card. Same as avatar it shouldn't exceed 2 MiB limit. -- Profile Background: this changes background picture for UI. It isn't shown to anyone else **yet**, but some time later it will be shown when viewing your profile. - -### Security - -Here you can change your password, revoke access tokens, configure 2-factor authentication (if available). - -### Notifications - -This screen allows more fine-grained control over what notifications to show to you based on whom it comes from - -### Data Import/Export - -This allows you to export and import a list of people you follow, in case instance's database gets reverted or if you want to move to another server. Note that you **CANNOT export/import list of people who *follow you***, they'll just need to follow you back after you move. - -### Blocks and Mutes - -These screens give access to full list of people you block/mute, useful for *un*blocking/*un*muting people because blocking/muting them most likely removes them out of your sight completely. - -## Other stuff - -By default you can see **ALL** posts made by other users on your Home Timeline, this contrast behavior of Twitter and Mastodon, which shows you only non-reply posts and replies to people you follow. You can set it up to replicate the said behavior, however the option is currently broken. - -You can view other people's profiles and search for users (top-right corner, person with a plus icon). Tag search is possible but not implemented properly yet, right now you can click on tag link in a post to see posts tagged with that post. - -You can also view posts you've favorited on your own profile, but you cannot see favorites by other people. - -Due to nature of how Pleroma (backend) operates you might see old posts appear as if they are new, this is because instance just learned about that post (i.e. your instance is younger that some other ones) and someone interacted with old post. Posts are sorted by date of when they are received, not date they have been posted because it's very easy to spoof the date, so a post claiming it "was" made in year 2077 could hand at top of your TL forever. - -# Customization and configuration - -Clicking on the cog icon in the upper right will go to the settings screen. - -## General - -### Interface - -- Language: Here you can set the interface language. The default language is the one that you set in your browser settings. -- Hide instance-specific panel: This hides the panel in the lower left that usually contains general information about the server. - -### Timeline - -- Hide posts of muted users: If this is set, 'muting' a user will completely hide their posts instead of collapsing them. -- Collapse posts with subjects: This will collapse posts that contain a subject, hiding their content. Subjects are also sometimes called content warnings. -- Enable automatic streaming of new posts when scrolled to the top: With this enabled, new posts will automatically stream in when you are scrolled to the top. Otherwise, you will see a button on the timeline that will let you display the new posts. -- Pause streaming when tab is not focused: This pauses the automatic streaming that the previous option enables when the tab is out of focus. This is useful if you don't want to miss any new posts. -- Enable automatic loading when scrolled to the bottom: When this is disabled, a button will be shown on the bottom of the timeline that will let you load older posts. -- Enable reply-link preview on hover: Status posts in the timeline and notifications contain links to replies and to the post they are a reply to. If this setting is enabled, hovering over that link will display that linked post in a small hovering overlay. - -### Composing - -- Copy scope when replying: When this is activated, the scope of a reply will be the same as the scope of the post it is replying to. This is useful to prevent accidentally moving private discussions to public, or vice versa. -- Always show subject field: Whether or not to display the 'subject' input field in the post form. If you do not want to use subjects, you can deactivate this. -- Copy subject when replying: This controls if the subject of a post will be copied from the post it is replying to. -- Post status content type: Selects the default content type of your post. The options are: Plain text, HTML, BBCode and Markdown. -- Minimize scope selection options: Selecting this will reduce the visibility scopes to 'direct', your default post scope and post scope of post you're replying to. -- Automatically hide New Post button: Mobile interface only: hide floating "New post" button when scrolling - -### Attachments - -- Hide attachments in timeline: Do not display attachments in timelines. They will still display in expanded conversations. This is useful to save bandwidth and for browsing in public. -- Hide attachments in conversations: Also hide attachments in expanded conversations. -- Maximum amount of thumbnails per post: Exactly that :) -- Enable clickthrough NSFW attachment hiding: Hide attachments that are marked as NSFW/sensitive behind a click-through image.` -- Preload images: This will preload the hidden images so that they display faster when clicking through. -- Open NSFW attachments with just one click: Directly open NSFW attachments in a maximised state instead of revealing the image thumbnail. -- Play-on-hover GIFs: With this activated, GIFs images and avatars will only be animated on mouse hover. Otherwise, they will be always animated. This is very useful if your timeline looks too flashy from people's animated avatars and eases the CPU load. -- Loop videos: Whether to loop videos indefinitely. -- Loop only videos without sound: Some instances will use videos without sounds instead of GIFs. This will make only those videos autoplay. -- Play videos directly in the media viewer: Play videos right in the timeline instead of opening it in a modal -- Don't crop the attachment in thumbnails: if enabled, images in attachments will be fit entirely inside the container instead of being zoomed in and cropped. - -### Notifications - -- Enable web push notifications: this enables Web Push notifications, to allow receiving notifications even when the page isn't opened, doesn't affect regular notifications. - -## Theme - -You can change the look and feel of Pleroma Frontend here. You can choose from several instance-provided presets and you can load one from file and save current theme to file. Before you apply new theme you can see what it will look like approximately in preview section. - -Themes engine was made to be easy to use while giving an option for powerful in-depth customization - you can just tweak colors on "Common" tab and leave everything else as is. - -If there's a little check box next to a color picker it means that color is optional and unless checked will be automatically picked based on some other color or defaults. - -For some features you can also adjust transparency of it by changing its opacity, you just need to tick checkbox next to it, otherwise it will be using default opacity. - -Contrast information is also provided - you can see how readable text is based on contrast between text color and background, icons under color pickers represent contrast rating based on [WCAG](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast-contrast) - thumbs up means AAA rating (good), half-filled circle means AA rating (acceptable) and warning icon means it doesn't pass the minimal contrast requirement and probably will be less readable, especially for vision-challenged people, you can hover over icon to see more detailed information. *Please note* that if background is not opaque (opacity != 1) contrast will be measured based on "worst case scenario", i.e. behind semi-transparent background lies some solid color that makes text harder to read, this however is still inaccurate because it doesn't account that background can be noisy/busy, making text even harder to read. - -Apart from colors you can also tweak shadow and lighting, which is used mostly to give buttons proper relief based on their state, give panes their shade, make things glow etc. It's quite powerful, and basically provides somewhat convenient interface for [CSS Shadows](https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow). - -Another thing you can tweak is theme's roundness - some people like sharp edges, some want things more rounded. This is also used if you want circled or square avatars. - -Lastly, you can redefine fonts used in UI without changing fonts in your browser or system, this however requires you to enter font's full name and having that font installed on your system. - -## Filtering - -- Types of notifications to show: This controls what kind of notifications will appear in notification column and which notifications to get in your system outside the web page -- Replies in timeline: You may know that other social networks like Twitter will often not display replies to other people in your timeline, even if you are following the poster. Pleroma usually will show these posts to you to encourage conversation. If you do not like this behavior, you can change it here. -- Hide post statistics: This hides the number of favorites, number of replies, etc. -- Hide user statistics: This hides the number of followers, friends, etc. -- Muted words: A list of words that will be muted (i.e. displayed in a collapsed state) on the timeline and in notifications. An easy way to tune down noise in your timeline. Posts can always be expanded when you actually want to see them. -- Hide filtered statuses: Selecting this will hide the filtered / muted posts completely instead of collapsing them. - - -## Version - -Just displays the backend and frontend version. Useful to mention in bug reports. diff --git a/docs/example_emoji.png b/docs/assets/example_emoji.png Binary files differ. diff --git a/docs/assets/example_markdown.png b/docs/assets/example_markdown.png Binary files differ. diff --git a/docs/index.md b/docs/index.md @@ -5,4 +5,4 @@ Pleroma-FE is the default user-facing frontend for Pleroma. It's user interface ## How can I use it? -If your instance uses Pleroma-FE, you can acces it by going to your instance (e.g. <https://pleroma.soykaf.com>). You can read more about it's basic functionality in the [Pleroma-FE User Guide](./USER_GUIDE.md). We also have [a guide for administrators](./CONFIGURATION.md) and for [hackers/contributors](./HACKING.md). +If your instance uses Pleroma-FE, you can acces it by going to your instance (e.g. <https://pleroma.soykaf.com>). You can read more about it's basic functionality in the [Pleroma-FE User Guide](./user_guide/). We also have [a guide for administrators](./CONFIGURATION.md) and for [hackers/contributors](./HACKING.md). diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md @@ -0,0 +1,44 @@ +# General overview + +> Be prepared for breaking changes, unexpected behavior and this user guide becoming obsolete and wrong. + +> If there was no insanity +> +> it would be necessary to create it. +> +> --Catbag + +Pleroma-FE is the default user-facing frontend for Pleroma. If your instance uses Pleroma-FE, you can access it by going to your instance (e.g. <https://pleroma.soykaf.com>). After logging in you will have two columns in front of you. Here we're going to keep it to the default behaviour, but some instances swap the left and right columns. If you're on such an instance what we refer to as the left column will be on your right and vice versa. + +### Left column + +- first block: This section is dedicated to [posting](posting_reading_basic_functions.md) +- second block: Here you can switch between the different views for the right column. +- Optional third block: This is the Instance panel that can be activated, but is deactivated by default. It's fully customisable by instance admins and by default has links to the Pleroma-FE and Mastodon-FE. +- fourth block: This is the Notifications block, here you will get notified whenever somebody mentions you, follows you, repeats or favorites one of your statuses + +### Right column +This is where the interesting stuff happens! There are different views depending on what you choose in the second block of the left panel. + +- **Timelines** Depending on the [timeline](timelines.md) you will see different statuses, but each status has a standard structure: + - Profile pic, name and link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the reply-to status). Clicking on the profile pic will uncollapse the user's profile where you can find information about the account and can [follow, mute or block the account](users_follow_mute_block.md). + - An arrow icon on the right side allows you to open the status on the instance where it's originating from. + - A `+` button on the rightmost side allows you to Expand/Collapse an entire discussion thread. + - The text of the status, including mentions and attachments. If you click on a mention, it will automatically open the profile page of that person. + - Four buttons (left to right): Reply, Repeat, Favorite and Add Reaction. The three dots next to it are a dropdown menu for extra options including simple moderation, bookmarking, deleting posts, pinning your own posts to your profile and more. +- **Interactions** shows all interactions you've had with people on the network, basically same as notifications except grouped in convenient way. +- **Chats** is the chat feature. You can find your friends and start chatting with them. At the moment chat are only one-on-one, but once groups are introduced groupchats will also be possible. +- **About** is the about-page and lists the staff, the TOS, activated MRF's, and enabled features + +### Top right + +- The magnifier icon opens the search screen + - You can search for statuses, people and hashtags. + - You can import statuses from remote servers by pasting the url to the post in the search field. + - If you want to search for users that your instance doesn't know about yet, you can search for them using the full `name@instance.tld` handle. You can also use the full url from their remote profile. +- The gear icon gives you [settings](settings.md) +- If you have admin rights, you'll see an icon that opens the admin interface +- The last icon is to log out + +### Bottom right +On the bottom right you have the Shoutbox. Here you can communicate with people on the same instance in realtime. It is local-only, very basic and will most probably be removed once the Chats functionality allows group chats. diff --git a/docs/user_guide/posting_reading_basic_functions.md b/docs/user_guide/posting_reading_basic_functions.md @@ -0,0 +1,76 @@ +# Posting, reading, basic functions. + +!!! warning + Depending on your instance some of the options might not be available or have different defaults + +After registering and logging in you're presented with your timeline in right column and new post form with timeline list and notifications in the left column. + +Posts will contain the text you are posting, but some content will be modified: + +1. Mentions: Mentions have the form of @user or @user<span></span>@instance.tld. These will become links to the user's profile. In addition, the mentioned user will always get a notification about the post they have been mentioned in, so only mention users that you want to receive this message. +2. URLs: URLs like `http://example.com` will be automatically be turned into a clickable links. +3. Hashtags: Hashtags like #cofe will also be turned into links. +4. There is a default character limit of 5000 characters. + +Let's clear up some basic stuff. When you post something it's called a **post** or it could be called a **status** or even a **toot** or a **prööt** depending on whom you ask. Post has body/content but it also has some other stuff in it - from attachments, visibility scope, subject line... + +**Emoji** are small images embedded in text, there are two major types of emoji: [unicode emoji](https://en.wikipedia.org/wiki/Emoji) and custom emoji. While unicode emoji are universal and standardized, they can appear differently depending on where you are using them or may not appear at all on older systems. Custom emoji are a more *fun* kind - instance administrator can define many images as *custom emoji* for their users. This works very simple - custom emoji is defined by its *shortcode* and an image, so that any shortcode enclosed in colons get replaced with image if such shortcode exist. +Let's say there's a `:pleroma:` emoji defined on an instance. That means +> First time using :pleroma: pleroma! + +will become +> First time using ![pleroma](../assets/example_emoji.png) pleroma! + +Note that you can only use emoji defined on your instance, you cannot "copy" someone else's emoji, and will have to ask your administrator to copy emoji from other instance to yours. +Lastly, there's two convenience options for emoji: an emoji picker (smiley face to the right of "submit" button) and autocomplete suggestions - when you start typing :shortcode: it will automatically try to suggest you emoji and complete the shortcode for you if you select one. If emoji doesn't show up in suggestions nor in emoji picker it means there's no such emoji on your instance, if shortcode doesn't match any defined emoji it will appear as text. + +**Attachments** are fairly simple - you can attach any file to a post as long as the file is within maximum size limits. If you're uploading explicit material you can mark all of your attachments as sensitive (or add the `#nsfw` tag) - it will hide the images and videos behind a warning so that it won't be displayed instantly. + +**Subject line** also known as **CW** (Content Warning) could be used as a header to the post and/or to warn others about contents of the post having something that might upset somebody or something among those lines. Several applications allow to hide post content leaving only subject line visible. Using a subject line will not mark your images as sensitive, you will have to do that explicitly (see above). + +**Visiblity scope** controls who will be able to see your posts. There are four scopes available: + +1. `Public`: This is the default, and some fediverse software, like GNU Social, only supports this. This means that your post is accessible by anyone and will be shown in the public timelines. +2. `Unlisted`: This is the same as public, but your post won't appear in the public timelines. The post will still be accessible by anyone who comes across it (for example, by looking at your profile) or by direct linking. They will also appear in public searches. +3. `Followers only`: This will show your post only to your followers. Only they will be able to interact with it. Be careful: When somebody follows you, they will be able to see all your previous `followers only` posts as well! If you want to restrict who can follow you, consider [locking your account down to only approved followers](../settings#profile). +4. `Direct`: This will only send the message to the people explicitly mentioned in the post. + +A few things to consider about the security and usage of these scopes: + +- None of these options will change the fact that the messages are all saved in the database unencrypted. They will be visible to your server admin and to any other admin of a server who receives this post. Do not share information that you would consider secret or dangerous. Use encrypted messaging systems for these things. +- Follower-only posts can lead to fragmented conversations. If you post a follower-only post and somebody else replies to it with a follower-only post, only people following both of you will see the whole conversation thread. Everybody else will only see half of it. Keep this in mind and keep conversations public if possible. +- Changing scopes during a thread or adding people to a direct message will not retroactively make them see the whole conversation. If you add someone to a direct message conversation, they will not see the post that happened before they were mentioned. +* **Reply-to** if you are replying to someone, your post will also contain a note that your post is referring to the post you're replying to. Person you're replying to will receive a notification *even* if you remove them from mentioned people. You won't receive notifications when replying to your own posts, but it's useful to reply to your own posts to provide people some context if it's a follow-up to a previous post. There's a small "Reply to ..." label under post author's name which you can hover on to see what post it's referring to. + +Sometimes you may encounter posts that seem different than what they are supposed to. For example, you might see a direct message without any mentions in the text. This can happen because internally, the Fediverse has a different addressing mechanism similar to email, with `to` and `cc` fields. While these are not directly accessible in PleromaFE, other software in the Fediverse might generate those posts. Do not worry in these cases, these are normal and not a bug. + +## Rich text + +By default new posts you make are plaintext, meaning you can't make text **bold** or add custom links or make lists or anything like that. However if your instance allows it you can use Markdown or BBCode or HTML to spice up your text, however there are certain limitations to what HTML tags and what features of Markdown you can use. + +Here is a small example of some text in markdown. + +``` +This is an example of markdown text using **bold** and *cursive* text. +To get a newline we add two spaces at the end of the previous line. + +Let's also add a list + +* with +* some +* items +``` + +If you set the input-method to Markdown, and post this, it will look something like + +![example_markdown](../assets/example_markdown.png) + +## Other actions + +In addition to posting you can also *favorite* posts also known as *liking* them and *repeat* posts (also known as *retweeting*, *boosting* and even *reprööting*). Favoriting a post increments a counter on it, notifies the post author of your affection towards that post and also adds that post to your "favorited" posts list (in your own profile, "Favorites" tab). Reprööting a post does all that and also repeats this post to your followers and your profile page with a note "*user* repeated post". + +Your own posts can be deleted, but this will only reliably delete the post from your own instance. Other instances will receive a deletion notice, but there's no way to force them to actually delete a post. In addition, not all instances that contain the message might even receive the deletion notice, because they might be offline or not known to have the post because they received it through a repeat. Lastly, deletion notice might not reach certain frontends and clients - post will be visible for them until page refresh or cache clear, they probably won't be able to interact with it apart from replying to it (which will have reply-to mark missing). + +If you are a moderator, you can also delete posts by other people. If those people are on your instance, it will delete the post and send out the deletion notice to other servers. If they are not on your instance, it will just remove the post from your local instance. + +There's also an option to report a user's post which can be used to notify your (and optionally the other instance's) admin that someone is being naughty. diff --git a/docs/user_guide/settings.md b/docs/user_guide/settings.md @@ -0,0 +1,116 @@ +# Settings + +On the top-right you will see a gear icon. Click it to open the settings. + +## General + +### Interface + +- **Interface language** is where you can set the interface language. The default language is the one that you set in your browser settings. +- **Hide instance-specific panel** hides the panel in the lower left that usually contains general information about the server. This will only be visible if your admin has activated this panel and is deactivated by default. + +### Timeline + +- **Hide posts of muted users** If this is set, 'muting' a user will completely hide their posts instead of collapsing them. +- **Collapse posts with subjects** This will collapse posts that contain a subject, hiding their content. Subjects are also sometimes called content warnings. +- **Enable automatic streaming of new posts when scrolled to the top** With this enabled, new posts will automatically stream in when you are scrolled to the top. Otherwise, you will see a button on the timeline that will let you display the new posts. +- **Pause streaming when tab is not focused** This pauses the automatic streaming that the previous option enables when the tab is out of focus. This is useful if you don't want to miss any new posts. +- **Enable automatic loading when scrolled to the bottom** When this is disabled, a button will be shown on the bottom of the timeline that will let you load older posts. +- **Enable reply-link preview on hover** Status posts in the timeline and notifications contain links to replies and to the post they are a reply to. If this setting is enabled, hovering over that link will display that linked post in a small hovering overlay. + +### Composing + +- **Copy scope when replying** makes the scope of a reply be the same as the scope of the post it is replying to. This is useful to prevent accidentally moving private discussions to public, or vice versa. +- **Always show subject field** Whether or not to display the 'subject' input field in the post form. If you do not want to use subjects, you can deactivate this. +- **Copy subject when replying** controls if the subject of a post will be copied from the post it is replying to. +- **Post status content type** selects the default content type of your post. The options are: Plain text, HTML, BBCode and Markdown. +- **Minimize scope selection options** will reduce the visibility scopes to 'direct', your default post scope and post scope of post you're replying to. +- **Automatically hide New Post button** hides the floating "New post" button when scrolling on mobile view. +- **Pad emoji with spaces when adding from picker** Will add spaces around emoji you select it from the picker. + +### Attachments + +- **Hide attachments in timeline** Do not display attachments in timelines. They will still display in expanded conversations. This is useful to save bandwidth and for browsing in public. +- **Hide attachments in conversations** Also hide attachments in expanded conversations. +- **Maximum amount of thumbnails per post** Exactly that :) +- **Enable clickthrough NSFW attachment hiding** Hide attachments that are marked as NSFW/sensitive behind a click-through image.` + - **Preload images** This will preload the hidden images so that they display faster when clicking through. + - **Open NSFW attachments with just one click** Directly open NSFW attachments in a maximised state instead of revealing the image thumbnail. +- **Play-on-hover GIFs** With this activated, GIFs images and avatars will only be animated on mouse hover. Otherwise, they will be always animated. This is very useful if your timeline looks too flashy from people's animated avatars and eases the CPU load. +- **Loop videos** Whether to loop videos indefinitely. + - **Loop only videos without sound** Some instances will use videos without sounds instead of GIFs. This will make only those videos autoplay. +- **Play videos directly in the media viewer** Play videos right in the timeline instead of opening it in a modal +- **Don't crop the attachment in thumbnails** if enabled, images in attachments will be fit entirely inside the container instead of being zoomed in and cropped. + +### Notifications + +- **Enable web push notifications** this enables Web Push notifications, to allow receiving notifications even when the page isn't opened, doesn't affect regular notifications. + +### Fun + +- **Meme arrows** will make `> greentext` be shown in green (using the "green" from the theme that is used). + +## Profile + +Here you can set up how you appear to other users among with some other settings: + +- **Name** is text that displays next to your avatar in posts. Please note that you **cannot** change your *@handle* +- **Bio** will be displayed under your profile - you can put anything you want there you want for everyone to see. +- **Restrict your account to approved followers only** makes your account "locked", when people follow you - you have to approve or deny their follow requests, this gives more control over who sees your followers only posts. +- **Default visibility scope** is your default post scope for new posts +- **Strip rich text from all posts** strips rich text formatting (bold/italics/lists etc) from all incoming posts. This will only affect newly fetched posts. + +If you're admin or moderator on your instance you also get **Show [role] badge in my profile** - this controls whether to show "Admin" or "Moderator** label on your profile page. + +**For all options mentioned above you have to click "Submit" button for changes to take place** + +- **Avatar** this changes picture next to your posts. Your avatar shouldn't exceed 2 MiB (2097152 bytes) or it could cause problems with certain instances. +- **Banner** this changes background on your profile card. Same as avatar it shouldn't exceed 2 MiB limit. +- **Profile Background** this changes background picture for UI. It isn't shown to anyone else *yet*, but some time later it will be shown when viewing your profisle. + +## Security + +Here you can change your password, revoke access tokens, configure 2-factor authentication (if available). + +## Filtering + +- **Types of notifications to show** This controls what kind of notifications will appear in notification column and which notifications to get in your system outside the web page +- **Replies in timeline** You may know that other social networks like Twitter will often not display replies to other people in your timeline, even if you are following the poster. Pleroma usually will show these posts to you to encourage conversation. If you do not like this behavior, you can change it here. +- **Hide post statistics** This hides the number of favorites, number of replies, etc. +- **Hide user statistics** This hides the number of followers, friends, etc. +- **Muted words** allows a list of words that will be muted (i.e. displayed in a collapsed state) on the timeline and in notifications. An easy way to tune down noise in your timeline. By default posts can be expanded if you want to see them. +- **Hide filtered statuses** will hide the filtered / muted posts completely instead of collapsing them. + +## Theme + +Here you can change the look and feel of Pleroma-FE. You can choose from several instance-provided presets and you can load one from file and save current theme to file. Before you apply new theme you can see what it will look like approximately in preview section. + +The themes engine was made to be easy to use while giving an option for powerful in-depth customization - you can just tweak colors on "Common" tab and leave everything else as is. + +If there's a little check box next to a color picker it means that color is optional and unless checked will be automatically picked based on some other color or defaults. + +For some features you can also adjust transparency of it by changing its opacity, you just need to tick checkbox next to it, otherwise it will be using default opacity. + +Contrast information is also provided - you can see how readable text is based on contrast between text color and background, icons under color pickers represent contrast rating based on [WCAG](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast-contrast) - thumbs up means AAA rating (good), half-filled circle means AA rating (acceptable) and warning icon means it doesn't pass the minimal contrast requirement and probably will be less readable, especially for vision-challenged people, you can hover over icon to see more detailed information. *Please note* that if background is not opaque (opacity != 1) contrast will be measured based on "worst case scenario", i.e. behind semi-transparent background lies some solid color that makes text harder to read, this however is still inaccurate because it doesn't account that background can be noisy/busy, making text even harder to read. + +Apart from colors you can also tweak shadow and lighting, which is used mostly to give buttons proper relief based on their state, give panes their shade, make things glow etc. It's quite powerful, and basically provides somewhat convenient interface for [CSS Shadows](https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow). + +Another thing you can tweak is theme's roundness - some people like sharp edges, some want things more rounded. This is also used if you want circled or square avatars. + +Lastly, you can redefine fonts used in UI without changing fonts in your browser or system, this however requires you to enter font's full name and having that font installed on your system. + +## Notifications + +This screen allows more fine-grained control over what notifications to show to you based on whom it comes from. + +## Data Import/Export + +This allows you to export and import a list of people you follow and block, in case instance's database gets reverted or if you want to move to another server. Note that you **CANNOT export/import list of people who *follow you***, they'll need to follow you back themselves. + +## Mutes and Blocks + +These screens give access to full list of people you block/mute, useful for *un*blocking/*un*muting people because blocking/muting them most likely removes them out of your sight completely. + +## Version + +Just displays the backend and frontend version. Useful to mention in bug reports. diff --git a/docs/user_guide/timelines.md b/docs/user_guide/timelines.md @@ -0,0 +1,13 @@ +# Timelines + +You have several timelines to browse trough + +- **Timeline** aka Home Timeline - this timeline contains all posts by people you follow and your own posts, as well as posts mentioning you directly. +- **Bookmarks** all the posts you've bookmarked. You can bookmark a post by clicking the three dots on the bottom right of the post and choose Bookmark. +- **Direct Messages** all posts with `direct` scope addressed to you or mentioning you. +- **Public Timelines** all public posts made by users on the instance you're on +- **The Whole Known Network** also known as **TWKN** or **Federated Timeline** - all public posts known by your instance. Due to nature of the network your instance may not know *all* the posts on the network, so only posts known by your instance are shown there. + +Note that by default you will see all posts made by other users on your Home Timeline, this contrast behavior of Twitter and Mastodon, which shows you only non-reply posts and replies to people you follow. You can change said behavior in the [settings](settings.md#filtering). + +By default instances will try to send activities (e.g. posts, favorites, etc.) up to 7 days or until the target server received them. For this reason posts that are up to 7 days old and your server didn't know about yet can pop up on your timeline. This is the default behaviour and can be changed by your admin. diff --git a/docs/user_guide/users_follow_mute_block.md b/docs/user_guide/users_follow_mute_block.md @@ -0,0 +1,11 @@ +# Users: follow, mute, block + +When you see someone, you can click on their user picture to view their profile, and click on the userpic in that to see *full* profile. You can **follow** them, **mute** and **block** them. + +**Following** is self-explanatory, it adds them to your Home Timeline, lists you as a follower and gives you access to follower-only posts if they have any. + +**Muting** collapses posts and notifications made by them, giving you an option to see the post if you're curious. Clients other than PleromaFE may completely remove their posts. + +**Blocking** a user removes them from your timeline and notifications and prevents them from following you (automatically unfollows them from you). + +Please note that some users can be "locked", meaning instead of following them you send a follow request they need to approve for you to become their follower. diff --git a/package.json b/package.json @@ -18,6 +18,10 @@ "dependencies": { "@babel/runtime": "^7.7.6", "@chenfengyuan/vue-qrcode": "^1.0.0", + "@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": "^2.0.0", "body-scroll-lock": "^2.6.4", "chromatism": "^3.0.0", "cropperjs": "^1.4.3", @@ -68,7 +72,6 @@ "eventsource-polyfill": "^0.9.6", "express": "^4.13.3", "file-loader": "^3.0.1", - "fontello-webpack-plugin": "https://github.com/w3geo/fontello-webpack-plugin.git#6149eac8f2672ab6da089e8802fbfcac98487186", "function-bind": "^1.0.2", "html-webpack-plugin": "^3.0.0", "http-proxy-middleware": "^0.17.2", diff --git a/src/App.js b/src/App.js @@ -1,7 +1,6 @@ import UserPanel from './components/user_panel/user_panel.vue' import NavPanel from './components/nav_panel/nav_panel.vue' import Notifications from './components/notifications/notifications.vue' -import SearchBar from './components/search_bar/search_bar.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' @@ -11,6 +10,7 @@ import MediaModal from './components/media_modal/media_modal.vue' import SideDrawer from './components/side_drawer/side_drawer.vue' import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue' import MobileNav from './components/mobile_nav/mobile_nav.vue' +import DesktopNav from './components/desktop_nav/desktop_nav.vue' import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue' import PostStatusModal from './components/post_status_modal/post_status_modal.vue' import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue' @@ -22,7 +22,6 @@ export default { UserPanel, NavPanel, Notifications, - SearchBar, InstanceSpecificPanel, FeaturesPanel, WhoToFollowPanel, @@ -31,21 +30,14 @@ export default { SideDrawer, MobilePostStatusButton, MobileNav, + DesktopNav, SettingsModal, UserReportingModal, PostStatusModal, GlobalNoticeList }, data: () => ({ - mobileActivePanel: 'timeline', - searchBarHidden: true, - supportsMask: window.CSS && window.CSS.supports && ( - window.CSS.supports('mask-size', 'contain') || - window.CSS.supports('-webkit-mask-size', 'contain') || - window.CSS.supports('-moz-mask-size', 'contain') || - window.CSS.supports('-ms-mask-size', 'contain') || - window.CSS.supports('-o-mask-size', 'contain') - ) + mobileActivePanel: 'timeline' }), created () { // Load the locale from the storage @@ -61,28 +53,6 @@ export default { background () { return this.currentUser.background_image || this.$store.state.instance.background }, - enableMask () { return this.supportsMask && this.$store.state.instance.logoMask }, - logoStyle () { - return { - 'visibility': this.enableMask ? 'hidden' : 'visible' - } - }, - logoMaskStyle () { - return this.enableMask ? { - 'mask-image': `url(${this.$store.state.instance.logo})` - } : { - 'background-color': this.enableMask ? '' : 'transparent' - } - }, - logoBgStyle () { - return Object.assign({ - 'margin': `${this.$store.state.instance.logoMargin} 0`, - opacity: this.searchBarHidden ? 1 : 0 - }, this.enableMask ? {} : { - 'background-color': this.enableMask ? '' : 'transparent' - }) - }, - logo () { return this.$store.state.instance.logo }, bgStyle () { return { 'background-image': `url(${this.background})` @@ -93,9 +63,7 @@ export default { '--body-background-image': `url(${this.background})` } }, - sitename () { return this.$store.state.instance.name }, chat () { return this.$store.state.chat.channel.state === 'joined' }, - hideSitename () { return this.$store.state.instance.hideSitename }, suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, showInstanceSpecificPanel () { return this.$store.state.instance.showInstanceSpecificPanel && @@ -112,19 +80,6 @@ export default { } }, methods: { - scrollToTop () { - window.scrollTo(0, 0) - }, - logout () { - this.$router.replace('/main/public') - this.$store.dispatch('logout') - }, - onSearchBarToggled (hidden) { - this.searchBarHidden = hidden - }, - openSettingsModal () { - this.$store.dispatch('openSettingsModal') - }, updateMobileState () { const mobileLayout = windowWidth() <= 800 const layoutHeight = windowHeight() diff --git a/src/App.scss b/src/App.scss @@ -85,7 +85,7 @@ button { font-family: sans-serif; font-family: var(--interfaceFont, sans-serif); - i[class*=icon-] { + i[class*=icon-], .svg-inline--fa { color: $fallback--text; color: var(--btnText, $fallback--text); } @@ -106,7 +106,8 @@ button { color: var(--btnPressedText, $fallback--text); background-color: $fallback--fg; background-color: var(--btnPressed, $fallback--fg); - i { + + svg, i { color: $fallback--text; color: var(--btnPressedText, $fallback--text); } @@ -118,7 +119,8 @@ button { color: var(--btnDisabledText, $fallback--text); background-color: $fallback--fg; background-color: var(--btnDisabled, $fallback--fg); - i { + + svg, i { color: $fallback--text; color: var(--btnDisabledText, $fallback--text); } @@ -131,7 +133,8 @@ button { background-color: var(--btnToggled, $fallback--fg); box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset; box-shadow: var(--buttonPressedShadow); - i { + + svg, i { color: $fallback--text; color: var(--btnToggledText, $fallback--text); } @@ -185,7 +188,7 @@ input, textarea, .select, .input { opacity: 0.5; } - .icon-down-open { + .select-down-icon { position: absolute; top: 0; bottom: 0; @@ -279,7 +282,7 @@ input, textarea, .select, .input { + label::before { flex-shrink: 0; display: inline-block; - content: '✔'; + content: '✓'; transition: color 200ms; width: 1.1em; height: 1.1em; @@ -318,7 +321,7 @@ option { } } -i[class*=icon-] { +i[class*=icon-], .svg-inline--fa { color: $fallback--icon; color: var(--icon, $fallback--icon); } @@ -356,117 +359,10 @@ i[class*=icon-] { padding: 0 10px 0 10px; } -.item { - flex: 1; - line-height: 50px; - height: 50px; - overflow: hidden; - display: flex; - flex-wrap: wrap; - - .nav-icon { - margin-left: 0.4em; - } - - &.right { - justify-content: flex-end; - } -} - .auto-size { flex: 1 } -.nav-bar { - padding: 0; - width: 100%; - align-items: center; - position: fixed; - height: 50px; - box-sizing: border-box; - - button { - &, i[class*=icon-] { - color: $fallback--text; - color: var(--btnTopBarText, $fallback--text); - } - - &:active { - background-color: $fallback--fg; - background-color: var(--btnPressedTopBar, $fallback--fg); - color: $fallback--text; - color: var(--btnPressedTopBarText, $fallback--text); - } - - &:disabled { - color: $fallback--text; - color: var(--btnDisabledTopBarText, $fallback--text); - } - - &.toggled { - color: $fallback--text; - color: var(--btnToggledTopBarText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btnToggledTopBar, $fallback--fg) - } - } - - - .logo { - display: flex; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - - align-items: stretch; - justify-content: center; - flex: 0 0 auto; - z-index: -1; - transition: opacity; - transition-timing-function: ease-out; - transition-duration: 100ms; - - .mask { - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - background-color: $fallback--fg; - background-color: var(--topBarText, $fallback--fg); - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - } - - img { - height: 100%; - object-fit: contain; - display: block; - flex: 0; - } - } - - .inner-nav { - position: relative; - margin: auto; - box-sizing: border-box; - padding-left: 10px; - padding-right: 10px; - display: flex; - align-items: center; - flex-basis: 970px; - height: 50px; - - a, a i { - color: $fallback--link; - color: var(--topBarLink, $fallback--link); - } - } -} - main-router { flex: 1; } @@ -707,19 +603,24 @@ nav { flex-grow: 0; } } + .badge { + box-sizing: border-box; display: inline-block; border-radius: 99px; - min-width: 22px; - max-width: 22px; - min-height: 22px; - max-height: 22px; - font-size: 15px; - line-height: 22px; - text-align: center; + max-width: 10em; + min-width: 1.7em; + height: 1.3em; + padding: 0.15em 0.15em; vertical-align: middle; + font-weight: normal; + font-style: normal; + font-size: 0.9em; + line-height: 1; + text-align: center; white-space: nowrap; - padding: 0; + overflow: hidden; + text-overflow: ellipsis; &.badge-notification { background-color: $fallback--cRed; @@ -776,16 +677,6 @@ nav { } } -@media all and (min-width: 800px) { - .logo { - opacity: 1 !important; - } -} - -.item.right { - text-align: right; -} - .visibility-notice { padding: .5em; border: 1px solid $fallback--faint; @@ -807,8 +698,16 @@ nav { } } -.button-icon { - font-size: 1.2em; +.fa-scale-110 { + &.svg-inline--fa { + font-size: 1.1em; + } +} + +.fa-old-padding { + &.svg-inline--fa { + padding: 0 0.3em; + } } @keyframes shakeError { @@ -930,19 +829,6 @@ nav { background-color: var(--panel, $fallback--fg); } -.unread-chat-count { - font-size: 0.9em; - font-weight: bolder; - font-style: normal; - position: absolute; - right: 0.6rem; - padding: 0 0.3em; - min-width: 1.3rem; - min-height: 1.3rem; - max-height: 1.3rem; - line-height: 1.3rem; -} - .chat-layout { // Needed for smoother chat navigation in the desktop Safari (otherwise the chat layout "jumps" as the chat opens). overflow: hidden; diff --git a/src/App.vue b/src/App.vue @@ -9,74 +9,7 @@ :style="bgStyle" /> <MobileNav v-if="isMobileLayout" /> - <nav - v-else - id="nav" - class="nav-bar container" - @click="scrollToTop()" - > - <div class="inner-nav"> - <div - class="logo" - :style="logoBgStyle" - > - <div - class="mask" - :style="logoMaskStyle" - /> - <img - :src="logo" - :style="logoStyle" - > - </div> - <div class="item"> - <router-link - v-if="!hideSitename" - class="site-name" - :to="{ name: 'root' }" - active-class="home" - > - {{ sitename }} - </router-link> - </div> - <div class="item right"> - <search-bar - v-if="currentUser || !privateMode" - class="nav-icon mobile-hidden" - @toggled="onSearchBarToggled" - @click.stop.native - /> - <a - href="#" - class="mobile-hidden" - @click.stop="openSettingsModal" - > - <i - class="button-icon icon-cog nav-icon" - :title="$t('nav.preferences')" - /> - </a> - <a - v-if="currentUser && currentUser.role === 'admin'" - href="/pleroma/admin/#/login-pleroma" - class="mobile-hidden" - target="_blank" - ><i - class="button-icon icon-gauge nav-icon" - :title="$t('nav.administration')" - /></a> - <a - v-if="currentUser" - href="#" - class="mobile-hidden" - @click.prevent="logout" - ><i - class="button-icon icon-logout nav-icon" - :title="$t('login.logout')" - /></a> - </div> - </div> - </nav> + <DesktopNav v-else /> <div class="app-bg-wrapper app-container-wrapper" /> <div id="content" diff --git a/src/boot/after_store.js b/src/boot/after_store.js @@ -130,6 +130,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { ? 0 : config.logoMargin }) + copyInstanceOption('logoLeft') store.commit('authFlow/setInitialStrategy', config.loginMethod) copyInstanceOption('redirectRootNoLogin') diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js @@ -1,6 +1,14 @@ import { mapState } from 'vuex' import ProgressButton from '../progress_button/progress_button.vue' import Popover from '../popover/popover.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEllipsisV +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faEllipsisV +) const AccountActions = { props: [ diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue @@ -1,5 +1,5 @@ <template> - <div class="account-actions"> + <div class="AccountActions"> <Popover trigger="click" placement="bottom" @@ -63,7 +63,10 @@ slot="trigger" class="btn btn-default ellipsis-button" > - <i class="icon-ellipsis trigger-button" /> + <FAIcon + class="icon" + icon="ellipsis-v" + /> </div> </Popover> </div> @@ -73,22 +76,22 @@ <style lang="scss"> @import '../../_variables.scss'; -.account-actions { - margin: 0 .8em; -} +.AccountActions { + button.dropdown-item { + margin-left: 0; + } -.account-actions button.dropdown-item { - margin-left: 0; -} + .ellipsis-button { + cursor: pointer; + width: 2.5em; + margin: -0.5em 0; + padding: 0.5em 0; + text-align: center; -.account-actions .trigger-button { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - opacity: .8; - cursor: pointer; - &:hover { - color: $fallback--text; - color: var(--text, $fallback--text); + &:not(:hover) .icon { + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + } } } </style> diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js @@ -3,6 +3,20 @@ import VideoAttachment from '../video_attachment/video_attachment.vue' import nsfwImage from '../../assets/nsfw.png' import fileTypeService from '../../services/file_type/file_type.service.js' import { mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faFile, + faMusic, + faImage, + faVideo +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faFile, + faMusic, + faImage, + faVideo +) const Attachment = { props: [ @@ -39,10 +53,10 @@ const Attachment = { return this.attachment.description }, placeholderIconClass () { - if (this.type === 'image') return 'icon-picture' - if (this.type === 'video') return 'icon-video' - if (this.type === 'audio') return 'icon-music' - return 'icon-doc' + if (this.type === 'image') return 'image' + if (this.type === 'video') return 'video' + if (this.type === 'audio') return 'music' + return 'file' }, referrerpolicy () { return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer' diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue @@ -12,7 +12,7 @@ :alt="attachment.description" :title="attachment.description" > - <span :class="placeholderIconClass" /> + <FAIcon :icon="placeholderIconClass" /> <b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }} </a> </div> @@ -28,7 +28,7 @@ :href="attachment.url" :alt="attachment.description" :title="attachment.description" - @click.prevent="toggleHidden" + @click.prevent.stop="toggleHidden" > <img :key="nsfwImage" @@ -36,9 +36,10 @@ :src="nsfwImage" :class="{'small': isSmall}" > - <i + <FAIcon v-if="type === 'video'" - class="play-icon icon-play-circled" + class="play-icon" + icon="play-circle" /> </a> <div @@ -80,10 +81,13 @@ class="video" :attachment="attachment" :controls="allowPlay" + @play="$emit('play')" + @pause="$emit('pause')" /> - <i + <FAIcon v-if="!allowPlay" - class="play-icon icon-play-circled" + class="play-icon" + icon="play-circle" /> </a> @@ -93,6 +97,8 @@ :alt="attachment.description" :title="attachment.description" controls + @play="$emit('play')" + @pause="$emit('pause')" /> <div @@ -138,6 +144,10 @@ white-space: nowrap; text-overflow: ellipsis; max-width: 100%; + + svg { + color: inherit; + } } .nsfw-placeholder { diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js @@ -5,11 +5,25 @@ import ChatMessage from '../chat_message/chat_message.vue' import PostStatusForm from '../post_status_form/post_status_form.vue' import ChatTitle from '../chat_title/chat_title.vue' import chatService from '../../services/chat_service/chat_service.js' -import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight } from './chat_layout_utils.js' +import { promiseInterval } from '../../services/promise_interval/promise_interval.js' +import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight, isScrollable } from './chat_layout_utils.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faChevronDown, + faChevronLeft +} from '@fortawesome/free-solid-svg-icons' +import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js' + +library.add( + faChevronDown, + faChevronLeft +) const BOTTOMED_OUT_OFFSET = 10 const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150 const SAFE_RESIZE_TIME_OFFSET = 100 +const MARK_AS_READ_DELAY = 1500 +const MAX_RETRIES = 10 const Chat = { components: { @@ -23,7 +37,8 @@ const Chat = { hoveredMessageChainId: undefined, lastScrollPosition: {}, scrollableContainerHeight: '100%', - errorLoadingChat: false + errorLoadingChat: false, + messageRetriers: {} } }, created () { @@ -93,7 +108,7 @@ const Chat = { const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET) this.$nextTick(() => { if (bottomedOutBeforeUpdate) { - this.scrollDown({ forceRead: !document.hidden }) + this.scrollDown() } }) }, @@ -199,7 +214,7 @@ const Chat = { this.$nextTick(() => { scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior }) }) - if (forceRead || this.newMessageCount > 0) { + if (forceRead) { this.readChat() } }, @@ -207,7 +222,10 @@ const Chat = { if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return } if (document.hidden) { return } const lastReadId = this.currentChatMessageService.maxId - this.$store.dispatch('readChat', { id: this.currentChat.id, lastReadId }) + this.$store.dispatch('readChat', { + id: this.currentChat.id, + lastReadId + }) }, bottomedOut (offset) { return isBottomedOut(this.$refs.scrollable, offset) @@ -224,12 +242,18 @@ const Chat = { } else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) { this.jumpToBottomButtonVisible = false if (this.newMessageCount > 0) { - this.readChat() + // Use a delay before marking as read to prevent situation where new messages + // arrive just as you're leaving the view and messages that you didn't actually + // get to see get marked as read. + window.setTimeout(() => { + // Don't mark as read if the element doesn't exist, user has left chat view + if (this.$el) this.readChat() + }, MARK_AS_READ_DELAY) } } else { this.jumpToBottomButtonVisible = true } - }, 100), + }, 200), handleScrollUp (positionBeforeLoading) { const positionAfterLoading = getScrollPosition(this.$refs.scrollable) this.$refs.scrollable.scrollTo({ @@ -246,7 +270,7 @@ const Chat = { const fetchOlderMessages = !!maxId const sinceId = fetchLatest && chatMessageService.maxId - this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId }) + return this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId }) .then((messages) => { // Clear the current chat in case we're recovering from a ws connection loss. if (isFirstFetch) { @@ -263,6 +287,14 @@ const Chat = { if (isFirstFetch) { this.updateScrollableContainerHeight() } + + // In vertical screens, the first batch of fetched messages may not always take the + // full height of the scrollable container. + // If this is the case, we want to fetch the messages until the scrollable container + // is fully populated so that the user has the ability to scroll up and load the history. + if (!isScrollable(this.$refs.scrollable) && messages.length > 0) { + this.fetchChat({ maxId: this.currentChatMessageService.minId }) + } }) }) }) @@ -287,46 +319,78 @@ const Chat = { }, doStartFetching () { this.$store.dispatch('startFetchingCurrentChat', { - fetcher: () => setInterval(() => this.fetchChat({ fetchLatest: true }), 5000) + fetcher: () => promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000) }) this.fetchChat({ isFirstFetch: true }) }, - sendMessage ({ status, media }) { + handleAttachmentPosting () { + this.$nextTick(() => { + this.handleResize() + // When the posting form size changes because of a media attachment, we need an extra resize + // to account for the potential delay in the DOM update. + setTimeout(() => { + this.updateScrollableContainerHeight() + }, SAFE_RESIZE_TIME_OFFSET) + this.scrollDown({ forceRead: true }) + }) + }, + sendMessage ({ status, media, idempotencyKey }) { const params = { id: this.currentChat.id, - content: status + content: status, + idempotencyKey } if (media[0]) { params.mediaId = media[0].id } - return this.backendInteractor.sendChatMessage(params) + const fakeMessage = buildFakeMessage({ + attachments: media, + chatId: this.currentChat.id, + content: status, + userId: this.currentUser.id, + idempotencyKey + }) + + this.$store.dispatch('addChatMessages', { + chatId: this.currentChat.id, + messages: [fakeMessage] + }).then(() => { + this.handleAttachmentPosting() + }) + + return this.doSendMessage({ params, fakeMessage, retriesLeft: MAX_RETRIES }) + }, + doSendMessage ({ params, fakeMessage, retriesLeft = MAX_RETRIES }) { + if (retriesLeft <= 0) return + + this.backendInteractor.sendChatMessage(params) .then(data => { this.$store.dispatch('addChatMessages', { chatId: this.currentChat.id, - messages: [data], - updateMaxId: false - }).then(() => { - this.$nextTick(() => { - this.handleResize() - // When the posting form size changes because of a media attachment, we need an extra resize - // to account for the potential delay in the DOM update. - setTimeout(() => { - this.updateScrollableContainerHeight() - }, SAFE_RESIZE_TIME_OFFSET) - this.scrollDown({ forceRead: true }) - }) + updateMaxId: false, + messages: [{ ...data, fakeId: fakeMessage.id }] }) return data }) .catch(error => { console.error('Error sending message', error) - return { - error: this.$t('chats.error_sending_message') + this.$store.dispatch('handleMessageError', { + chatId: this.currentChat.id, + fakeId: fakeMessage.id, + isRetry: retriesLeft !== MAX_RETRIES + }) + if ((error.statusCode >= 500 && error.statusCode < 600) || error.message === 'Failed to fetch') { + this.messageRetriers[fakeMessage.id] = setTimeout(() => { + this.doSendMessage({ params, fakeMessage, retriesLeft: retriesLeft - 1 }) + }, 1000 * (2 ** (MAX_RETRIES - retriesLeft))) } + return {} }) + + return Promise.resolve(fakeMessage) }, goBack () { this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } }) diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss @@ -25,7 +25,7 @@ min-height: 100%; margin: 0 0 0 0; border-radius: 10px 10px 0 0; - border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0 ; + border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0; &::after { border-radius: 0; @@ -58,12 +58,10 @@ .go-back-button { cursor: pointer; - margin-right: 1.4em; - - i { - display: flex; - align-items: center; - } + width: 28px; + text-align: center; + padding: 0.6em; + margin: -0.6em 0.6em -0.6em -0.6em; } .jump-to-bottom-button { @@ -78,7 +76,7 @@ display: flex; justify-content: center; align-items: center; - box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3), 0px 2px 4px rgba(0, 0, 0, 0.3); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3); z-index: 10; transition: 0.35s all; transition-timing-function: cubic-bezier(0, 1, 0.5, 1); @@ -140,11 +138,21 @@ } .chat-view-heading { + box-sizing: border-box; position: static; z-index: 9999; top: 0; margin-top: 0; border-radius: 0; + + /* This practically overlays the panel heading color over panel background + * color. This is needed because we allow transparent panel background and + * it doesn't work well in this "disjointed panel header" case + */ + background: + linear-gradient(to top, var(--panel), var(--panel)), + linear-gradient(to top, var(--bg), var(--bg)); + height: 50px; } .scrollable-message-list { diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue @@ -14,7 +14,10 @@ class="go-back-button" @click="goBack" > - <i class="button-icon icon-left-open" /> + <FAIcon + size="lg" + icon="chevron-left" + /> </a> <div class="title text-center"> <ChatTitle @@ -58,14 +61,15 @@ :class="{ 'visible': jumpToBottomButtonVisible }" @click="scrollDown({ behavior: 'smooth' })" > - <i class="icon-down-open"> + <span> + <FAIcon icon="chevron-down" /> <div v-if="newMessageCount" class="badge badge-notification unread-chat-count unread-message-count" > {{ newMessageCount }} </div> - </i> + </span> </div> <PostStatusForm :disable-subject="true" @@ -76,6 +80,7 @@ :disable-sensitivity-checkbox="true" :disable-submit="errorLoadingChat || !currentChat" :disable-preview="true" + :optimistic-posting="true" :post-handler="sendMessage" :submit-on-enter="!mobileLayout" :preserve-focus="!mobileLayout" diff --git a/src/components/chat/chat_layout_utils.js b/src/components/chat/chat_layout_utils.js @@ -24,3 +24,10 @@ export const isBottomedOut = (el, offset = 0) => { export const scrollableContainerHeight = (inner, header, footer) => { return inner.offsetHeight - header.clientHeight - footer.clientHeight } + +// Returns whether or not the scrollbar is visible. +export const isScrollable = (el) => { + if (!el) return + + return el.scrollHeight > el.clientHeight +} diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue @@ -21,6 +21,12 @@ /> </span> <span class="heading-right" /> + <div class="time-wrapper"> + <Timeago + :time="chat.updated_at" + :auto-update="60" + /> + </div> </div> <div class="chat-preview"> <StatusContent @@ -35,12 +41,6 @@ </div> </div> </div> - <div class="time-wrapper"> - <Timeago - :time="chat.updated_at" - :auto-update="60" - /> - </div> </div> </template> diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js @@ -7,6 +7,16 @@ import LinkPreview from '../link-preview/link-preview.vue' import StatusContent from '../status_content/status_content.vue' import ChatMessageDate from '../chat_message_date/chat_message_date.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes, + faEllipsisH +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes, + faEllipsisH +) const ChatMessage = { name: 'ChatMessage', diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss @@ -24,7 +24,7 @@ } } - .icon-ellipsis { + .menu-icon { cursor: pointer; &:hover, .extra-button-popover.open & { @@ -101,6 +101,19 @@ } } + .pending { + .status-content.media-body, .created-at { + color: var(--faint); + } + } + + .error { + .status-content.media-body, .created-at { + color: $fallback--cRed; + color: var(--badgeNotification, $fallback--cRed); + } + } + .incoming { a { color: var(--chatMessageIncomingLink, $fallback--link); diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue @@ -32,7 +32,7 @@ > <div class="media status" - :class="{ 'without-attachment': !hasAttachment }" + :class="{ 'without-attachment': !hasAttachment, 'pending': chatViewItem.data.pending, 'error': chatViewItem.data.error }" style="position: relative" @mouseenter="hovered = true" @mouseleave="hovered = false" @@ -56,15 +56,16 @@ class="dropdown-item dropdown-item-icon" @click="deleteMessage" > - <i class="icon-cancel" /> {{ $t("chats.delete") }} + <FAIcon icon="times" /> {{ $t("chats.delete") }} </button> </div> </div> <button slot="trigger" + class="menu-icon" :title="$t('chats.more')" > - <i class="icon-ellipsis" /> + <FAIcon icon="ellipsis-h" /> </button> </Popover> </div> diff --git a/src/components/chat_new/chat_new.js b/src/components/chat_new/chat_new.js @@ -1,6 +1,16 @@ import { mapState, mapGetters } from 'vuex' import BasicUserCard from '../basic_user_card/basic_user_card.vue' import UserAvatar from '../user_avatar/user_avatar.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faSearch, + faChevronLeft +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faSearch, + faChevronLeft +) const chatNew = { components: { diff --git a/src/components/chat_new/chat_new.scss b/src/components/chat_new/chat_new.scss @@ -8,9 +8,7 @@ } } - .icon-search { - font-size: 1.5em; - float: right; + .search-icon { margin-right: 0.3em; } @@ -25,5 +23,9 @@ .go-back-button { cursor: pointer; + width: 28px; + text-align: center; + padding: 0.6em; + margin: -0.6em 0.6em -0.6em -0.6em; } } diff --git a/src/components/chat_new/chat_new.vue b/src/components/chat_new/chat_new.vue @@ -11,12 +11,18 @@ class="go-back-button" @click="goBack" > - <i class="button-icon icon-left-open" /> + <FAIcon + size="lg" + icon="chevron-left" + /> </a> </div> <div class="input-wrap"> <div class="input-search"> - <i class="button-icon icon-search" /> + <FAIcon + class="search-icon fa-scale-110 fa-old-padding" + icon="search" + /> </div> <input ref="search" diff --git a/src/components/chat_panel/chat_panel.js b/src/components/chat_panel/chat_panel.js @@ -1,4 +1,14 @@ 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' ], diff --git a/src/components/chat_panel/chat_panel.vue b/src/components/chat_panel/chat_panel.vue @@ -11,9 +11,9 @@ > <div class="title"> <span>{{ $t('shoutbox.title') }}</span> - <i + <FAIcon v-if="floating" - class="icon-cancel" + icon="times" /> </div> </div> @@ -63,7 +63,10 @@ @click.stop.prevent="togglePanel" > <div class="title"> - <i class="icon-megaphone" /> + <FAIcon + class="icon" + icon="bullhorn" + /> {{ $t('shoutbox.title') }} </div> </div> @@ -87,7 +90,8 @@ .chat-panel { .chat-heading { cursor: pointer; - .icon-comment-empty { + + .icon { color: $fallback--text; color: var(--text, $fallback--text); } diff --git a/src/components/contrast_ratio/contrast_ratio.vue b/src/components/contrast_ratio/contrast_ratio.vue @@ -8,13 +8,13 @@ class="rating" > <span v-if="contrast.aaa"> - <i class="icon-thumbs-up-alt" /> + <FAIcon icon="thumbs-up" /> </span> <span v-if="!contrast.aaa && contrast.aa"> - <i class="icon-adjust" /> + <FAIcon icon="adjust" /> </span> <span v-if="!contrast.aaa && !contrast.aa"> - <i class="icon-attention" /> + <FAIcon icon="exclamation-triangle" /> </span> </span> <span @@ -23,19 +23,32 @@ :title="hint_18pt" > <span v-if="contrast.laaa"> - <i class="icon-thumbs-up-alt" /> + <FAIcon icon="thumbs-up" /> </span> <span v-if="!contrast.laaa && contrast.laa"> - <i class="icon-adjust" /> + <FAIcon icon="adjust" /> </span> <span v-if="!contrast.laaa && !contrast.laa"> - <i class="icon-attention" /> + <FAIcon icon="exclamation-triangle" /> </span> </span> </span> </template> <script> +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faAdjust, + faExclamationTriangle, + faThumbsUp +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faAdjust, + faExclamationTriangle, + faThumbsUp +) + export default { props: { large: { @@ -85,6 +98,7 @@ export default { .rating { display: inline-block; text-align: center; + margin-left: 0.5em; } } </style> diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js @@ -44,7 +44,8 @@ const conversation = { 'isPage', 'pinnedStatusIdsObject', 'inProfile', - 'profileUserId' + 'profileUserId', + 'virtualHidden' ], created () { if (this.isPage) { @@ -52,6 +53,13 @@ const conversation = { } }, computed: { + hideStatus () { + if (this.$refs.statusComponent && this.$refs.statusComponent[0]) { + return this.virtualHidden && this.$refs.statusComponent[0].suspendable + } else { + return this.virtualHidden + } + }, status () { return this.$store.state.statuses.allStatusesObject[this.statusId] }, @@ -102,6 +110,10 @@ const conversation = { }, isExpanded () { return this.expanded || this.isPage + }, + hiddenStyle () { + const height = (this.status && this.status.virtualHeight) || '120px' + return this.virtualHidden ? { height } : {} } }, components: { @@ -121,6 +133,12 @@ const conversation = { if (value) { this.fetchConversation() } + }, + virtualHidden (value) { + this.$store.dispatch( + 'setVirtualHeight', + { statusId: this.statusId, height: `${this.$el.clientHeight}px` } + ) } }, methods: { diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue @@ -1,5 +1,7 @@ <template> <div + v-if="!hideStatus" + :style="hiddenStyle" class="Conversation" :class="{ '-expanded' : isExpanded, 'panel' : isExpanded }" > @@ -18,6 +20,7 @@ <status v-for="status in conversation" :key="status.id" + ref="statusComponent" :inline-expanded="collapsable && isExpanded" :statusoid="status" :expandable="!isExpanded" @@ -33,6 +36,10 @@ @toggleExpanded="toggleExpanded" /> </div> + <div + v-else + :style="hiddenStyle" + /> </template> <script src="./conversation.js"></script> @@ -53,8 +60,8 @@ .conversation-status { border-color: $fallback--border; border-color: var(--border, $fallback--border); - border-left: 4px solid $fallback--cRed; - border-left: 4px solid var(--cRed, $fallback--cRed); + border-left-color: $fallback--cRed; + border-left-color: var(--cRed, $fallback--cRed); } .conversation-status:last-child { diff --git a/src/components/desktop_nav/desktop_nav.js b/src/components/desktop_nav/desktop_nav.js @@ -0,0 +1,89 @@ +import SearchBar from 'components/search_bar/search_bar.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faSignInAlt, + faSignOutAlt, + faHome, + faComments, + faBell, + faUserPlus, + faBullhorn, + faSearch, + faTachometerAlt, + faCog, + faInfoCircle +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faSignInAlt, + faSignOutAlt, + faHome, + faComments, + faBell, + faUserPlus, + faBullhorn, + faSearch, + faTachometerAlt, + faCog, + faInfoCircle +) + +export default { + components: { + SearchBar + }, + data: () => ({ + searchBarHidden: true, + supportsMask: window.CSS && window.CSS.supports && ( + window.CSS.supports('mask-size', 'contain') || + window.CSS.supports('-webkit-mask-size', 'contain') || + window.CSS.supports('-moz-mask-size', 'contain') || + window.CSS.supports('-ms-mask-size', 'contain') || + window.CSS.supports('-o-mask-size', 'contain') + ) + }), + computed: { + enableMask () { return this.supportsMask && this.$store.state.instance.logoMask }, + logoStyle () { + return { + 'visibility': this.enableMask ? 'hidden' : 'visible' + } + }, + logoMaskStyle () { + return this.enableMask ? { + 'mask-image': `url(${this.$store.state.instance.logo})` + } : { + 'background-color': this.enableMask ? '' : 'transparent' + } + }, + logoBgStyle () { + return Object.assign({ + 'margin': `${this.$store.state.instance.logoMargin} 0`, + opacity: this.searchBarHidden ? 1 : 0 + }, this.enableMask ? {} : { + 'background-color': this.enableMask ? '' : 'transparent' + }) + }, + logo () { return this.$store.state.instance.logo }, + sitename () { return this.$store.state.instance.name }, + hideSitename () { return this.$store.state.instance.hideSitename }, + logoLeft () { return this.$store.state.instance.logoLeft }, + currentUser () { return this.$store.state.users.currentUser }, + privateMode () { return this.$store.state.instance.private } + }, + methods: { + scrollToTop () { + window.scrollTo(0, 0) + }, + logout () { + this.$router.replace('/main/public') + this.$store.dispatch('logout') + }, + onSearchBarToggled (hidden) { + this.searchBarHidden = hidden + }, + openSettingsModal () { + this.$store.dispatch('openSettingsModal') + } + } +} diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss @@ -0,0 +1,112 @@ +@import '../../_variables.scss'; + +.DesktopNav { + height: 50px; + width: 100%; + position: fixed; + + .inner-nav { + display: grid; + grid-template-rows: 50px; + grid-template-columns: 2fr auto 2fr; + grid-template-areas: "sitename logo actions"; + box-sizing: border-box; + padding: 0 1.2em; + margin: auto; + max-width: 980px; + } + + &.-logoLeft { + grid-template-columns: auto 2fr 2fr; + grid-template-areas: "logo sitename actions"; + } + + button { + &, svg { + color: $fallback--text; + color: var(--btnTopBarText, $fallback--text); + } + + &:active { + background-color: $fallback--fg; + background-color: var(--btnPressedTopBar, $fallback--fg); + color: $fallback--text; + color: var(--btnPressedTopBarText, $fallback--text); + } + + &:disabled { + color: $fallback--text; + color: var(--btnDisabledTopBarText, $fallback--text); + } + + &.toggled { + color: $fallback--text; + color: var(--btnToggledTopBarText, $fallback--text); + background-color: $fallback--fg; + background-color: var(--btnToggledTopBar, $fallback--fg) + } + } + + .logo { + grid-area: logo; + position: relative; + transition: opacity; + transition-timing-function: ease-out; + transition-duration: 100ms; + + @media all and (min-width: 800px) { + opacity: 1 !important; + } + + .mask { + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + background-color: $fallback--fg; + background-color: var(--topBarText, $fallback--fg); + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + + img { + display: inline-block; + height: 50px; + } + } + + .nav-icon { + margin-left: 0.2em; + width: 2em; + text-align: center; + } + + a, a svg { + color: $fallback--link; + color: var(--topBarLink, $fallback--link); + } + + .sitename { + grid-area: sitename; + } + + .actions { + grid-area: actions; + } + + .item { + flex: 1; + line-height: 50px; + height: 50px; + overflow: hidden; + display: flex; + flex-wrap: wrap; + + &.right { + justify-content: flex-end; + text-align: right; + } + } +} diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue @@ -0,0 +1,79 @@ +<template> + <nav + id="nav" + class="DesktopNav" + :class="{ '-logoLeft': logoLeft }" + @click="scrollToTop()" + > + <div class="inner-nav"> + <div class="item sitename"> + <router-link + v-if="!hideSitename" + class="site-name" + :to="{ name: 'root' }" + active-class="home" + > + {{ sitename }} + </router-link> + </div> + <router-link + class="logo" + :to="{ name: 'root' }" + :style="logoBgStyle" + > + <div + class="mask" + :style="logoMaskStyle" + /> + <img + :src="logo" + :style="logoStyle" + > + </router-link> + <div class="item right actions"> + <search-bar + v-if="currentUser || !privateMode" + @toggled="onSearchBarToggled" + @click.stop.native + /> + <a + href="#" + class="nav-icon" + @click.stop="openSettingsModal" + > + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="cog" + :title="$t('nav.preferences')" + /> + </a> + <a + v-if="currentUser && currentUser.role === 'admin'" + href="/pleroma/admin/#/login-pleroma" + class="nav-icon" + target="_blank" + ><FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="tachometer-alt" + :title="$t('nav.administration')" + /></a> + <a + v-if="currentUser" + href="#" + class="nav-icon" + @click.prevent="logout" + ><FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="sign-out-alt" + :title="$t('login.logout')" + /></a> + </div> + </div> + </nav> +</template> +<script src="./desktop_nav.js"></script> + +<style src="./desktop_nav.scss" lang="scss"></style> diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js @@ -3,6 +3,15 @@ import EmojiPicker from '../emoji_picker/emoji_picker.vue' import { take } from 'lodash' import { findOffset } from '../../services/offset_finder/offset_finder.service.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faSmileBeam +} from '@fortawesome/free-regular-svg-icons' + +library.add( + faSmileBeam +) + /** * EmojiInput - augmented inputs for emoji and autocomplete support in inputs * without having to give up the comfort of <input/> and <textarea/> elements diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue @@ -11,7 +11,7 @@ class="emoji-picker-icon" @click.prevent="togglePicker" > - <i class="icon-smile" /> + <FAIcon :icon="['far', 'smile-beam']" /> </div> <EmojiPicker v-if="enableEmojiPicker" diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js @@ -1,4 +1,16 @@ import Checkbox from '../checkbox/checkbox.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faBoxOpen, + faStickyNote, + faSmileBeam +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faBoxOpen, + faStickyNote, + faSmileBeam +) // At widest, approximately 20 emoji are visible in a row, // loading 3 rows, could be overkill for narrow picker @@ -8,7 +20,20 @@ const LOAD_EMOJI_BY = 60 const LOAD_EMOJI_MARGIN = 64 const filterByKeyword = (list, keyword = '') => { - return list.filter(x => x.displayText.includes(keyword)) + if (keyword === '') return list + + const keywordLowercase = keyword.toLowerCase() + let orderedEmojiList = [] + for (const emoji of list) { + const indexOfKeyword = emoji.displayText.toLowerCase().indexOf(keywordLowercase) + if (indexOfKeyword > -1) { + if (!Array.isArray(orderedEmojiList[indexOfKeyword])) { + orderedEmojiList[indexOfKeyword] = [] + } + orderedEmojiList[indexOfKeyword].push(emoji) + } + } + return orderedEmojiList.flat() } const EmojiPicker = { @@ -164,13 +189,13 @@ const EmojiPicker = { { id: 'custom', text: this.$t('emoji.custom'), - icon: 'icon-smile', + icon: 'smile-beam', emojis: customEmojis }, { id: 'standard', text: this.$t('emoji.unicode'), - icon: 'icon-picture', + icon: 'box-open', emojis: filterByKeyword(standardEmojis, this.keyword) } ] diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss @@ -82,7 +82,7 @@ &.active { border-bottom: 4px solid; - i { + svg { color: $fallback--lightText; color: var(--lightText, $fallback--lightText); } diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue @@ -13,7 +13,10 @@ :title="group.text" @click.prevent="highlight(group.id)" > - <i :class="group.icon" /> + <FAIcon + :icon="group.icon" + fixed-width + /> </span> </span> <span @@ -26,7 +29,10 @@ :title="$t('emoji.stickers')" @click.prevent="toggleStickers" > - <i class="icon-star" /> + <FAIcon + icon="sticky-note" + fixed-width + /> </span> </span> </div> diff --git a/src/components/exporter/exporter.js b/src/components/exporter/exporter.js @@ -1,3 +1,10 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch +) + const Exporter = { props: { getContent: { diff --git a/src/components/exporter/exporter.vue b/src/components/exporter/exporter.vue @@ -1,7 +1,12 @@ <template> <div class="exporter"> <div v-if="processing"> - <i class="icon-spin4 animate-spin exporter-processing" /> + <FAIcon + icon="circle-notch" + size="lg" + spin + /> + <span>{{ processingMessage }}</span> </div> <button @@ -19,7 +24,6 @@ <style lang="scss"> .exporter { &-processing { - font-size: 1.5em; margin: 0.25em; } } diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js @@ -1,4 +1,24 @@ import Popover from '../popover/popover.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEllipsisH, + faBookmark, + faEyeSlash, + faThumbtack, + faShareAlt +} from '@fortawesome/free-solid-svg-icons' +import { + faBookmark as faBookmarkReg +} from '@fortawesome/free-regular-svg-icons' + +library.add( + faEllipsisH, + faBookmark, + faBookmarkReg, + faEyeSlash, + faThumbtack, + faShareAlt +) const ExtraButtons = { props: [ 'status' ], diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue @@ -15,14 +15,20 @@ class="dropdown-item dropdown-item-icon" @click.prevent="muteConversation" > - <i class="icon-eye-off" /><span>{{ $t("status.mute_conversation") }}</span> + <FAIcon + fixed-width + icon="eye-slash" + /><span>{{ $t("status.mute_conversation") }}</span> </button> <button v-if="canMute && status.thread_muted" class="dropdown-item dropdown-item-icon" @click.prevent="unmuteConversation" > - <i class="icon-eye-off" /><span>{{ $t("status.unmute_conversation") }}</span> + <FAIcon + fixed-width + icon="eye-slash" + /><span>{{ $t("status.unmute_conversation") }}</span> </button> <button v-if="!status.pinned && canPin" @@ -30,7 +36,10 @@ @click.prevent="pinStatus" @click="close" > - <i class="icon-pin" /><span>{{ $t("status.pin") }}</span> + <FAIcon + fixed-width + icon="thumbtack" + /><span>{{ $t("status.pin") }}</span> </button> <button v-if="status.pinned && canPin" @@ -38,7 +47,10 @@ @click.prevent="unpinStatus" @click="close" > - <i class="icon-pin" /><span>{{ $t("status.unpin") }}</span> + <FAIcon + fixed-width + icon="thumbtack" + /><span>{{ $t("status.unpin") }}</span> </button> <button v-if="!status.bookmarked" @@ -46,7 +58,10 @@ @click.prevent="bookmarkStatus" @click="close" > - <i class="icon-bookmark-empty" /><span>{{ $t("status.bookmark") }}</span> + <FAIcon + fixed-width + :icon="['far', 'bookmark']" + /><span>{{ $t("status.bookmark") }}</span> </button> <button v-if="status.bookmarked" @@ -54,7 +69,10 @@ @click.prevent="unbookmarkStatus" @click="close" > - <i class="icon-bookmark" /><span>{{ $t("status.unbookmark") }}</span> + <FAIcon + fixed-width + icon="bookmark" + /><span>{{ $t("status.unbookmark") }}</span> </button> <button v-if="canDelete" @@ -62,21 +80,29 @@ @click.prevent="deleteStatus" @click="close" > - <i class="icon-cancel" /><span>{{ $t("status.delete") }}</span> + <FAIcon + fixed-width + icon="times" + /><span>{{ $t("status.delete") }}</span> </button> <button class="dropdown-item dropdown-item-icon" @click.prevent="copyLink" @click="close" > - <i class="icon-share" /><span>{{ $t("status.copy_link") }}</span> + <FAIcon + fixed-width + icon="share-alt" + /><span>{{ $t("status.copy_link") }}</span> </button> </div> </div> - <i - slot="trigger" - class="icon-ellipsis button-icon" - /> + <span slot="trigger"> + <FAIcon + class="ExtraButtons fa-scale-110 fa-old-padding" + icon="ellipsis-h" + /> + </span> </Popover> </template> @@ -85,8 +111,9 @@ <style lang="scss"> @import '../../_variables.scss'; -.icon-ellipsis { +.ExtraButtons { cursor: pointer; + position: static; &:hover, .extra-button-popover.open & { diff --git a/src/components/favorite_button/favorite_button.js b/src/components/favorite_button/favorite_button.js @@ -1,4 +1,14 @@ import { mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faStar } from '@fortawesome/free-solid-svg-icons' +import { + faStar as faStarRegular +} from '@fortawesome/free-regular-svg-icons' + +library.add( + faStar, + faStarRegular +) const FavoriteButton = { props: ['status', 'loggedIn'], @@ -23,9 +33,7 @@ const FavoriteButton = { computed: { classes () { return { - 'icon-star-empty': !this.status.favorited, - 'icon-star': this.status.favorited, - 'animate-spin': this.animated + '-favorited': this.status.favorited } }, ...mapGetters(['mergedConfig']) diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue @@ -1,18 +1,21 @@ <template> <div v-if="loggedIn"> - <i + <FAIcon :class="classes" - class="button-icon favorite-button fav-active" + class="FavoriteButton fa-scale-110 fa-old-padding -interactive" :title="$t('tool_tip.favorite')" + :icon="[status.favorited ? 'fas' : 'far', 'star']" + :spin="animated" @click.prevent="favorite()" /> <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span> </div> <div v-else> - <i + <FAIcon :class="classes" - class="button-icon favorite-button" + class="FavoriteButton fa-scale-110 fa-old-padding" :title="$t('tool_tip.favorite')" + :icon="['far', 'star']" /> <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span> </div> @@ -23,18 +26,20 @@ <style lang="scss"> @import '../../_variables.scss'; -.fav-active { - cursor: pointer; - animation-duration: 0.6s; +.FavoriteButton { + &.-interactive { + cursor: pointer; + animation-duration: 0.6s; - &:hover { + &:hover { + color: $fallback--cOrange; + color: var(--cOrange, $fallback--cOrange); + } + } + + &.-favorited { color: $fallback--cOrange; color: var(--cOrange, $fallback--cOrange); } } - -.favorite-button.icon-star { - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); -} </style> diff --git a/src/components/follow_requests/follow_requests.vue b/src/components/follow_requests/follow_requests.vue @@ -1,7 +1,9 @@ <template> <div class="settings panel panel-default"> <div class="panel-heading"> - {{ $t('nav.friend_requests') }} + <div class="title"> + {{ $t('nav.friend_requests') }} + </div> </div> <div class="panel-body"> <FollowRequestCard diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js @@ -1,4 +1,12 @@ import { set } from 'vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faChevronDown +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faChevronDown +) export default { props: [ diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue @@ -41,7 +41,10 @@ {{ option === 'custom' ? $t('settings.style.fonts.custom') : option }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> <input v-if="isCustom" diff --git a/src/components/global_notice_list/global_notice_list.js b/src/components/global_notice_list/global_notice_list.js @@ -1,3 +1,11 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes +) const GlobalNoticeList = { computed: { diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue @@ -9,8 +9,9 @@ <div class="notice-message"> {{ $t(notice.messageKey, notice.messageArgs) }} </div> - <i - class="button-icon icon-cancel" + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" @click="closeNotice(notice)" /> </div> diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js @@ -1,5 +1,15 @@ import Cropper from 'cropperjs' import 'cropperjs/dist/cropper.css' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes, + faCircleNotch +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes, + faCircleNotch +) const ImageCropper = { props: { diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue @@ -31,9 +31,10 @@ @click="submit(false)" v-text="saveWithoutCroppingText" /> - <i + <FAIcon v-if="submitting" - class="icon-spin4 animate-spin" + spin + icon="circle-notch" /> </div> <div @@ -41,8 +42,9 @@ class="alert error" > {{ submitErrorMsg }} - <i - class="button-icon icon-cancel" + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" @click="clearError" /> </div> diff --git a/src/components/importer/importer.js b/src/components/importer/importer.js @@ -1,3 +1,14 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faCircleNotch, + faTimes +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch, + faTimes +) + const Importer = { props: { submitHandler: { diff --git a/src/components/importer/importer.vue b/src/components/importer/importer.vue @@ -7,9 +7,11 @@ @change="change" > </form> - <i + <FAIcon v-if="submitting" - class="icon-spin4 animate-spin importer-uploading" + class="importer-uploading" + spin + icon="circle-notch" /> <button v-else @@ -19,15 +21,15 @@ {{ submitButtonLabel }} </button> <div v-if="success"> - <i - class="icon-cross" + <FAIcon + icon="times" @click="dismiss" /> <p>{{ successMessage }}</p> </div> <div v-else-if="error"> - <i - class="icon-cross" + <FAIcon + icon="times" @click="dismiss" /> <p>{{ errorMessage }}</p> diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -19,7 +19,10 @@ {{ languageNames[i] }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> </template> @@ -28,6 +31,14 @@ import languagesObject from '../../i18n/messages' 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 +) export default { computed: { diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js @@ -1,5 +1,13 @@ import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' import oauthApi from '../../services/new_api/oauth.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes +) const LoginForm = { data: () => ({ diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue @@ -76,8 +76,9 @@ > <div class="alert error"> {{ error }} - <i - class="button-icon icon-cancel" + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" @click="clearError" /> </div> diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js @@ -3,6 +3,16 @@ import VideoAttachment from '../video_attachment/video_attachment.vue' import Modal from '../modal/modal.vue' import fileTypeService from '../../services/file_type/file_type.service.js' import GestureService from '../../services/gesture_service/gesture_service' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faChevronLeft, + faChevronRight +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faChevronLeft, + faChevronRight +) const MediaModal = { components: { diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue @@ -34,7 +34,10 @@ class="modal-view-button-arrow modal-view-button-arrow--prev" @click.stop.prevent="goPrev" > - <i class="icon-left-open arrow-icon" /> + <FAIcon + class="arrow-icon" + icon="chevron-left" + /> </button> <button v-if="canNavigate" @@ -42,7 +45,10 @@ class="modal-view-button-arrow modal-view-button-arrow--next" @click.stop.prevent="goNext" > - <i class="icon-right-open arrow-icon" /> + <FAIcon + class="arrow-icon" + icon="chevron-right" + /> </button> </Modal> </template> diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js @@ -2,6 +2,14 @@ import statusPosterService from '../../services/status_poster/status_poster.service.js' import fileSizeFormatService from '../../services/file_size_format/file_size_format.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faUpload, faCircleNotch } from '@fortawesome/free-solid-svg-icons' + +library.add( + faUpload, + faCircleNotch +) + const mediaUpload = { data () { return { diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue @@ -7,13 +7,16 @@ class="label" :title="$t('tool_tip.media_upload')" > - <i + <FAIcon v-if="uploading" - class="progress-icon icon-spin4 animate-spin" + class="progress-icon" + icon="circle-notch" + spin /> - <i + <FAIcon v-if="!uploading" - class="new-icon icon-upload" + class="new-icon" + icon="upload" /> <input v-if="uploadReady" @@ -40,15 +43,5 @@ .new-icon { cursor: pointer; } - - .progress-icon { - display: inline-block; - line-height: 0; - &::before { - /* Overriding fontello to achieve the perfect speeeen */ - margin: 0; - line-height: 0; - } - } } </style> diff --git a/src/components/mfa_form/recovery_form.js b/src/components/mfa_form/recovery_form.js @@ -1,5 +1,13 @@ import mfaApi from '../../services/new_api/mfa.js' import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes +) export default { data: () => ({ diff --git a/src/components/mfa_form/recovery_form.vue b/src/components/mfa_form/recovery_form.vue @@ -54,8 +54,9 @@ > <div class="alert error"> {{ error }} - <i - class="button-icon icon-cancel" + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" @click="clearError" /> </div> diff --git a/src/components/mfa_form/totp_form.js b/src/components/mfa_form/totp_form.js @@ -1,5 +1,14 @@ import mfaApi from '../../services/new_api/mfa.js' import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes +) + export default { data: () => ({ code: null, diff --git a/src/components/mfa_form/totp_form.vue b/src/components/mfa_form/totp_form.vue @@ -56,8 +56,10 @@ > <div class="alert error"> {{ error }} - <i - class="button-icon icon-cancel" + <FAIcon + size="lg" + class="fa-scale-110 fa-old-padding" + icon="times" @click="clearError" /> </div> diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js @@ -3,6 +3,18 @@ import Notifications from '../notifications/notifications.vue' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' import GestureService from '../../services/gesture_service/gesture_service' import { mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes, + faBell, + faBars +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes, + faBell, + faBars +) const MobileNav = { components: { diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue @@ -1,49 +1,53 @@ <template> - <div> + <div + class="MobileNav" + > <nav id="nav" - class="nav-bar container" + class="mobile-nav" :class="{ 'mobile-hidden': isChat }" + @click="scrollToTop()" > - <div - class="mobile-inner-nav" - @click="scrollToTop()" - > - <div class="item"> - <a - href="#" - class="mobile-nav-button" - @click.stop.prevent="toggleMobileSidebar()" - > - <i class="button-icon icon-menu" /> - <div - v-if="unreadChatCount" - class="alert-dot" - /> - </a> - <router-link - v-if="!hideSitename" - class="site-name" - :to="{ name: 'root' }" - active-class="home" - > - {{ sitename }} - </router-link> - </div> - <div class="item right"> - <a - v-if="currentUser" - class="mobile-nav-button" - href="#" - @click.stop.prevent="openMobileNotifications()" - > - <i class="button-icon icon-bell-alt" /> - <div - v-if="unseenNotificationsCount" - class="alert-dot" - /> - </a> - </div> + <div class="item"> + <a + href="#" + class="mobile-nav-button" + @click.stop.prevent="toggleMobileSidebar()" + > + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="bars" + /> + <div + v-if="unreadChatCount" + class="alert-dot" + /> + </a> + <router-link + v-if="!hideSitename" + class="site-name" + :to="{ name: 'root' }" + active-class="home" + > + {{ sitename }} + </router-link> + </div> + <div class="item right"> + <a + v-if="currentUser" + class="mobile-nav-button" + href="#" + @click.stop.prevent="openMobileNotifications()" + > + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="bell" + /> + <div + v-if="unseenNotificationsCount" + class="alert-dot" + /> + </a> </div> </nav> <div @@ -59,7 +63,10 @@ class="mobile-nav-button" @click.stop.prevent="closeMobileNotifications()" > - <i class="button-icon icon-cancel" /> + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" + /> </a> </div> <div @@ -84,101 +91,124 @@ <style lang="scss"> @import '../../_variables.scss'; -.mobile-inner-nav { - width: 100%; - display: flex; - align-items: center; -} +.MobileNav { + .mobile-nav { + display: grid; + line-height: 50px; + height: 50px; + grid-template-rows: 50px; + grid-template-columns: 2fr auto; + width: 100%; + position: fixed; + box-sizing: border-box; + } -.mobile-nav-button { - display: flex; - justify-content: center; - width: 50px; - position: relative; - cursor: pointer; -} + .mobile-inner-nav { + width: 100%; + display: flex; + align-items: center; + } -.alert-dot { - border-radius: 100%; - height: 8px; - width: 8px; - position: absolute; - left: calc(50% - 4px); - top: calc(50% - 4px); - margin-left: 6px; - margin-top: -6px; - background-color: $fallback--cRed; - background-color: var(--badgeNotification, $fallback--cRed); -} + .mobile-nav-button { + display: inline-block; + text-align: center; + padding: 0 1em; + position: relative; + cursor: pointer; + } -.mobile-notifications-drawer { - width: 100%; - height: 100vh; - overflow-x: hidden; - position: fixed; - top: 0; - left: 0; - box-shadow: 1px 1px 4px rgba(0,0,0,.6); - box-shadow: var(--panelShadow); - transition-property: transform; - transition-duration: 0.25s; - transform: translateX(0); - z-index: 1001; - -webkit-overflow-scrolling: touch; - - &.closed { - transform: translateX(100%); + .site-name { + padding: 0 .3em; + display: inline-block; } -} -.mobile-notifications-header { - display: flex; - align-items: center; - justify-content: space-between; - z-index: 1; - width: 100%; - height: 50px; - line-height: 50px; - position: absolute; - color: var(--topBarText); - background-color: $fallback--fg; - background-color: var(--topBar, $fallback--fg); - box-shadow: 0px 0px 4px rgba(0,0,0,.6); - box-shadow: var(--topBarShadow); - - .title { - font-size: 1.3em; - margin-left: 0.6em; + .item { + /* moslty just to get rid of extra whitespaces */ + display: flex; } -} -.mobile-notifications { - margin-top: 50px; - width: 100vw; - height: calc(100vh - 50px); - overflow-x: hidden; - overflow-y: scroll; - - color: $fallback--text; - color: var(--text, $fallback--text); - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); - - .notifications { - padding: 0; - border-radius: 0; - box-shadow: none; - .panel { - border-radius: 0; - margin: 0; - box-shadow: none; + .alert-dot { + border-radius: 100%; + height: 8px; + width: 8px; + position: absolute; + left: calc(50% - 4px); + top: calc(50% - 4px); + margin-left: 6px; + margin-top: -6px; + background-color: $fallback--cRed; + background-color: var(--badgeNotification, $fallback--cRed); + } + + .mobile-notifications-drawer { + width: 100%; + height: 100vh; + overflow-x: hidden; + position: fixed; + top: 0; + left: 0; + box-shadow: 1px 1px 4px rgba(0,0,0,.6); + box-shadow: var(--panelShadow); + transition-property: transform; + transition-duration: 0.25s; + transform: translateX(0); + z-index: 1001; + -webkit-overflow-scrolling: touch; + + &.closed { + transform: translateX(100%); } - .panel:after { - border-radius: 0; + } + + .mobile-notifications-header { + display: flex; + align-items: center; + justify-content: space-between; + z-index: 1; + width: 100%; + height: 50px; + line-height: 50px; + position: absolute; + color: var(--topBarText); + background-color: $fallback--fg; + background-color: var(--topBar, $fallback--fg); + box-shadow: 0px 0px 4px rgba(0,0,0,.6); + box-shadow: var(--topBarShadow); + + .title { + font-size: 1.3em; + margin-left: 0.6em; } - .panel .panel-heading { + } + + .mobile-notifications { + margin-top: 50px; + width: 100vw; + height: calc(100vh - 50px); + overflow-x: hidden; + overflow-y: scroll; + + color: $fallback--text; + color: var(--text, $fallback--text); + background-color: $fallback--bg; + background-color: var(--bg, $fallback--bg); + + .notifications { + padding: 0; border-radius: 0; box-shadow: none; + .panel { + border-radius: 0; + margin: 0; + box-shadow: none; + } + .panel:after { + border-radius: 0; + } + .panel .panel-heading { + border-radius: 0; + box-shadow: none; + } } } } diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js @@ -1,4 +1,12 @@ import { debounce } from 'lodash' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faPen +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faPen +) const HIDDEN_FOR_PAGES = new Set([ 'chats', 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 @@ -5,7 +5,7 @@ :class="{ 'hidden': isHidden }" @click="openPostForm" > - <i class="icon-edit" /> + <FAIcon icon="pen" /> </button> </div> </template> @@ -39,7 +39,7 @@ transform: translateY(150%); } - i { + svg { font-size: 1.5em; color: $fallback--text; color: var(--text, $fallback--text); diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue @@ -178,7 +178,7 @@ box-shadow: var(--inputShadow); &.menu-checkbox-checked::after { - content: '✔'; + content: '✓'; } } diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js @@ -1,6 +1,29 @@ import { timelineNames } from '../timeline_menu/timeline_menu.js' import { mapState, mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faHome, + faComments, + faBell, + faInfoCircle +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faHome, + faComments, + faBell, + faInfoCircle +) + const NavPanel = { created () { if (this.currentUser && this.currentUser.locked) { diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue @@ -1,5 +1,5 @@ <template> - <div class="nav-panel"> + <div class="NavPanel"> <div class="panel panel-default"> <ul> <li v-if="currentUser || !privateMode"> @@ -7,31 +7,47 @@ :to="{ name: timelinesRoute }" :class="onTimelineRoute && 'router-link-active'" > - <i class="button-icon icon-home-2" /> {{ $t("nav.timelines") }} + <FAIcon + fixed-width + class="fa-scale-110" + icon="home" + />{{ $t("nav.timelines") }} </router-link> </li> <li v-if="currentUser"> <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> - <i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }} + <FAIcon + fixed-width + class="fa-scale-110" + icon="bell" + />{{ $t("nav.interactions") }} </router-link> </li> <li v-if="currentUser && pleromaChatMessagesAvailable"> <router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }"> <div v-if="unreadChatCount" - class="badge badge-notification unread-chat-count" + class="badge badge-notification" > {{ unreadChatCount }} </div> - <i class="button-icon icon-chat" /> {{ $t("nav.chats") }} + <FAIcon + fixed-width + class="fa-scale-110" + icon="comments" + />{{ $t("nav.chats") }} </router-link> </li> <li v-if="currentUser && currentUser.locked"> <router-link :to="{ name: 'friend-requests' }"> - <i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }} + <FAIcon + fixed-width + class="fa-scale-110" + icon="user-plus" + />{{ $t("nav.friend_requests") }} <span v-if="followRequestCount > 0" - class="badge follow-request-count" + class="badge badge-notification" > {{ followRequestCount }} </span> @@ -39,7 +55,11 @@ </li> <li> <router-link :to="{ name: 'about' }"> - <i class="button-icon icon-info-circled" /> {{ $t("nav.about") }} + <FAIcon + fixed-width + class="fa-scale-110" + icon="info-circle" + />{{ $t("nav.about") }} </router-link> </li> </ul> @@ -52,80 +72,88 @@ <style lang="scss"> @import '../../_variables.scss'; -.nav-panel .panel { - overflow: hidden; - box-shadow: var(--panelShadow); -} -.nav-panel ul { - list-style: none; - margin: 0; - padding: 0; -} - -.follow-request-count { - margin: -6px 10px; - background-color: $fallback--bg; - background-color: var(--input, $fallback--faint); -} - -.nav-panel li { - border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - padding: 0; - - &:first-child a { - border-top-right-radius: $fallback--panelRadius; - border-top-right-radius: var(--panelRadius, $fallback--panelRadius); - border-top-left-radius: $fallback--panelRadius; - border-top-left-radius: var(--panelRadius, $fallback--panelRadius); +.NavPanel { + .panel { + overflow: hidden; + box-shadow: var(--panelShadow); } - &:last-child a { - border-bottom-right-radius: $fallback--panelRadius; - border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius); - border-bottom-left-radius: $fallback--panelRadius; - border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius); + ul { + list-style: none; + margin: 0; + padding: 0; } -} -.nav-panel li:last-child { - border: none; -} + li { + position: relative; + border-bottom: 1px solid; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + padding: 0; -.nav-panel a { - display: block; - padding: 0.8em 0.85em; + &:first-child a { + border-top-right-radius: $fallback--panelRadius; + border-top-right-radius: var(--panelRadius, $fallback--panelRadius); + border-top-left-radius: $fallback--panelRadius; + border-top-left-radius: var(--panelRadius, $fallback--panelRadius); + } - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuText, $fallback--link); - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - --icon: var(--selectedMenuIcon, $fallback--icon); + &:last-child a { + border-bottom-right-radius: $fallback--panelRadius; + border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius); + border-bottom-left-radius: $fallback--panelRadius; + border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius); + } } - &.router-link-active { - font-weight: bolder; - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedMenuText, $fallback--text); - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - --icon: var(--selectedMenuIcon, $fallback--icon); + li:last-child { + border: none; + } + + a { + display: block; + box-sizing: border-box; + align-items: stretch; + height: 3.5em; + line-height: 3.5em; + padding: 0 1em; &:hover { - text-decoration: underline; + background-color: $fallback--lightBg; + background-color: var(--selectedMenu, $fallback--lightBg); + color: $fallback--link; + color: var(--selectedMenuText, $fallback--link); + --faint: var(--selectedMenuFaintText, $fallback--faint); + --faintLink: var(--selectedMenuFaintLink, $fallback--faint); + --lightText: var(--selectedMenuLightText, $fallback--lightText); + --icon: var(--selectedMenuIcon, $fallback--icon); + } + + &.router-link-active { + font-weight: bolder; + background-color: $fallback--lightBg; + background-color: var(--selectedMenu, $fallback--lightBg); + color: $fallback--text; + color: var(--selectedMenuText, $fallback--text); + --faint: var(--selectedMenuFaintText, $fallback--faint); + --faintLink: var(--selectedMenuFaintLink, $fallback--faint); + --lightText: var(--selectedMenuLightText, $fallback--lightText); + --icon: var(--selectedMenuIcon, $fallback--icon); + + &:hover { + text-decoration: underline; + } } } -} -.nav-panel .button-icon:before { - width: 1.1em; + .fa-scale-110 { + margin-right: 0.8em; + } + + .badge { + position: absolute; + right: 0.6rem; + top: 1.25em; + } } </style> diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js @@ -7,6 +7,28 @@ import Timeago from '../timeago/timeago.vue' 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' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faCheck, + faTimes, + faStar, + faRetweet, + faUserPlus, + faEyeSlash, + faUser, + faSuitcaseRolling +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faCheck, + faTimes, + faStar, + faRetweet, + faUserPlus, + faUser, + faEyeSlash, + faSuitcaseRolling +) const Notification = { data () { diff --git a/src/components/notification/notification.scss b/src/components/notification/notification.scss @@ -1,3 +1,5 @@ +@import '../../_variables.scss'; + // TODO Copypaste from Status, should unify it somehow .Notification { &.-muted { @@ -49,4 +51,34 @@ display: block; } } + + .type-icon { + margin: 0 0.1em; + } + + &.-type--repeat .type-icon { + color: $fallback--cGreen; + color: var(--cGreen, $fallback--cGreen); + } + + &.-type--follow .type-icon { + color: $fallback--cBlue; + color: var(--cBlue, $fallback--cBlue); + } + + &.-type--follow-request .type-icon { + color: $fallback--cBlue; + color: var(--cBlue, $fallback--cBlue); + } + + &.-type--like .type-icon { + color: orange; + color: $fallback--cOrange; + color: var(--cOrange, $fallback--cOrange); + } + + &.-type--move .type-icon { + color: $fallback--cBlue; + color: var(--cBlue, $fallback--cBlue); + } } diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue @@ -1,5 +1,5 @@ <template> - <status + <Status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status" @@ -18,12 +18,15 @@ href="#" class="unmute" @click.prevent="toggleMute" - ><i class="button-icon icon-eye-off" /></a> + ><FAIcon + class="fa-scale-110 fa-old-padding" + icon="eye-slash" + /></a> </div> <div v-else - class="non-mention" - :class="[userClass, { highlighted: userStyle }]" + class="Notification non-mention" + :class="[userClass, { highlighted: userStyle }, '-type--' + notification.type]" :style="[ userStyle ]" > <a @@ -60,26 +63,39 @@ :title="'@'+notification.from_profile.screen_name" >{{ notification.from_profile.name }}</span> <span v-if="notification.type === 'like'"> - <i class="fa icon-star lit" /> + <FAIcon + class="type-icon" + icon="star" + /> <small>{{ $t('notifications.favorited_you') }}</small> </span> <span v-if="notification.type === 'repeat'"> - <i - class="fa icon-retweet lit" + <FAIcon + class="type-icon" + icon="retweet" :title="$t('tool_tip.repeat')" /> <small>{{ $t('notifications.repeated_you') }}</small> </span> <span v-if="notification.type === 'follow'"> - <i class="fa icon-user-plus lit" /> + <FAIcon + class="type-icon" + icon="user-plus" + /> <small>{{ $t('notifications.followed_you') }}</small> </span> <span v-if="notification.type === 'follow_request'"> - <i class="fa icon-user lit" /> + <FAIcon + class="type-icon" + icon="user" + /> <small>{{ $t('notifications.follow_request') }}</small> </span> <span v-if="notification.type === 'move'"> - <i class="fa icon-arrow-curved lit" /> + <FAIcon + class="type-icon" + icon="suitcase-rolling" + /> <small>{{ $t('notifications.migrated_to') }}</small> </span> <span v-if="notification.type === 'pleroma:emoji_reaction'"> @@ -120,7 +136,10 @@ v-if="needMute" href="#" @click.prevent="toggleMute" - ><i class="button-icon icon-eye-off" /></a> + ><FAIcon + class="fa-scale-110 fa-old-padding" + icon="eye-slash" + /></a> </span> <div v-if="notification.type === 'follow' || notification.type === 'follow_request'" @@ -136,13 +155,15 @@ v-if="notification.type === 'follow_request'" style="white-space: nowrap;" > - <i - class="icon-ok button-icon follow-request-accept" + <FAIcon + icon="check" + class="fa-scale-110 fa-old-padding follow-request-accept" :title="$t('tool_tip.accept_follow_request')" @click="approveUser()" /> - <i - class="icon-cancel button-icon follow-request-reject" + <FAIcon + icon="times" + class="fa-scale-110 fa-old-padding follow-request-reject" :title="$t('tool_tip.reject_follow_request')" @click="denyUser()" /> diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js @@ -6,6 +6,12 @@ import { filteredNotificationsFromStore, unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch +) const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30 diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss @@ -158,37 +158,6 @@ margin-right: .2em; } - .icon-retweet.lit { - color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); - } - - .icon-user.lit { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); - } - - .icon-user-plus.lit { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); - } - - .icon-reply.lit { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); - } - - .icon-star.lit { - color: orange; - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); - } - - .icon-arrow-curved.lit { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); - } - .status-content { margin: 0; max-height: 300px; diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue @@ -61,7 +61,11 @@ v-else class="new-status-notification text-center panel-footer" > - <i class="icon-spin3 animate-spin" /> + <FAIcon + icon="circle-notch" + spin + size="lg" + /> </div> </div> </div> diff --git a/src/components/panel_loading/panel_loading.vue b/src/components/panel_loading/panel_loading.vue @@ -1,12 +1,27 @@ <template> <div class="panel-loading"> <span class="loading-text"> - <i class="icon-spin4 animate-spin" /> + <FAIcon + icon="circle-notch" + spin + size="3x" + /> {{ $t('general.loading') }} </span> </div> </template> +<script> +import { library } from '@fortawesome/fontawesome-svg-core' +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch +) + +export default {} +</script> + <style lang="scss"> @import 'src/_variables.scss'; @@ -18,8 +33,7 @@ font-size: 2em; color: $fallback--text; color: var(--text, $fallback--text); - .loading-text i { - font-size: 3em; + .loading-text svg { line-height: 0; vertical-align: middle; color: $fallback--text; diff --git a/src/components/password_reset/password_reset.js b/src/components/password_reset/password_reset.js @@ -1,5 +1,13 @@ import { mapState } from 'vuex' import passwordResetApi from '../../services/new_api/password_reset.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes +) const passwordReset = { data: () => ({ diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue @@ -63,10 +63,10 @@ > <span>{{ error }}</span> <a - class="button-icon dismiss" + class="fa-scale-110 fa-old-padding dismiss" @click.prevent="dismissError()" > - <i class="icon-cancel" /> + <FAIcon icon="times" /> </a> </p> </div> @@ -122,7 +122,7 @@ padding-right: 2rem; } - .icon-cancel { + .dismiss { cursor: pointer; } } diff --git a/src/components/poll/poll_form.js b/src/components/poll/poll_form.js @@ -1,5 +1,17 @@ import * as DateUtils from 'src/services/date_utils/date_utils.js' import { uniq } from 'lodash' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes, + faChevronDown, + faPlus +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes, + faChevronDown, + faPlus +) export default { name: 'PollForm', diff --git a/src/components/poll/poll_form.vue b/src/components/poll/poll_form.vue @@ -12,6 +12,7 @@ <input :id="`poll-${index}`" v-model="options[index]" + size="1" class="poll-option-input" type="text" :placeholder="$t('polls.option')" @@ -24,8 +25,9 @@ v-if="options.length > 2" class="icon-container" > - <i - class="icon-cancel" + <FAIcon + icon="times" + class="delete" @click="deleteOption(index)" /> </div> @@ -35,7 +37,11 @@ class="add-option faint" @click="addOption" > - <i class="icon-plus" /> + <FAIcon + icon="plus" + size="sm" + /> + {{ $t("polls.add_option") }} </a> <div class="poll-type-expiry"> @@ -55,7 +61,10 @@ <option value="single">{{ $t('polls.single_choice') }}</option> <option value="multiple">{{ $t('polls.multiple_choices') }}</option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> <div @@ -83,7 +92,10 @@ {{ $t(`time.${unit}_short`, ['']) }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> </div> @@ -103,6 +115,7 @@ .add-option { align-self: flex-start; padding-top: 0.25em; + padding-left: 0.1em; cursor: pointer; } @@ -124,9 +137,17 @@ .icon-container { // Hack: Move the icon over the input box - width: 2em; - margin-left: -2em; + width: 1.5em; + margin-left: -1.5em; z-index: 1; + + .delete { + cursor: pointer; + + &:hover { + color: inherit; + } + } } .poll-type-expiry { diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue @@ -27,7 +27,7 @@ <script src="./popover.js" /> -<style lang=scss> +<style lang="scss"> @import '../../_variables.scss'; .popover { @@ -96,7 +96,7 @@ &-icon { padding-left: 0.5rem; - i { + svg { margin-right: 0.25rem; color: var(--menuPopoverIcon, $fallback--icon) } @@ -111,7 +111,7 @@ --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint); --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText); --icon: var(--selectedMenuPopoverIcon, $fallback--icon); - i { + svg { color: var(--selectedMenuPopoverIcon, $fallback--icon); } } diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js @@ -12,6 +12,27 @@ import suggestor from '../emoji_input/suggestor.js' import { mapGetters, mapState } from 'vuex' import Checkbox from '../checkbox/checkbox.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faChevronDown, + faSmileBeam, + faPollH, + faUpload, + faBan, + faTimes, + faCircleNotch +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faChevronDown, + faSmileBeam, + faPollH, + faUpload, + faBan, + faTimes, + faCircleNotch +) + const buildMentionsString = ({ user, attentions = [] }, currentUser) => { let allAttentions = [...attentions] @@ -54,7 +75,8 @@ const PostStatusForm = { 'autoFocus', 'fileLimit', 'submitOnEnter', - 'emojiPickerPlacement' + 'emojiPickerPlacement', + 'optimisticPosting' ], components: { MediaUpload, @@ -251,7 +273,7 @@ const PostStatusForm = { if (this.preview) this.previewStatus() }, async postStatus (event, newStatus, opts = {}) { - if (this.posting) { return } + if (this.posting && !this.optimisticPosting) { return } if (this.disableSubmit) { return } if (this.emojiInputShown) { return } if (this.submitOnEnter) { @@ -259,6 +281,8 @@ const PostStatusForm = { event.preventDefault() } + if (this.optimisticPosting && (this.emptyStatus || this.isOverLengthLimit)) { return } + if (this.emptyStatus) { this.error = this.$t('post_status.empty_status_error') return diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue @@ -12,10 +12,11 @@ v-show="showDropIcon !== 'hide'" :style="{ animation: showDropIcon === 'show' ? 'fade-in 0.25s' : 'fade-out 0.5s' }" class="drop-indicator" - :class="[uploadFileLimitReached ? 'icon-block' : 'icon-upload']" @dragleave="fileDragStop" @drop.stop="fileDrop" - /> + > + <FAIcon :icon="uploadFileLimitReached ? 'ban' : 'upload'" /> + </div> <div class="form-group"> <i18n v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private' && !disableLockWarning" @@ -36,10 +37,10 @@ > <span>{{ $t('post_status.scope_notice.public') }}</span> <a - class="button-icon dismiss" + class="fa-scale-110 fa-old-padding dismiss" @click.prevent="dismissScopeNotice()" > - <i class="icon-cancel" /> + <FAIcon icon="times" /> </a> </p> <p @@ -48,10 +49,10 @@ > <span>{{ $t('post_status.scope_notice.unlisted') }}</span> <a - class="button-icon dismiss" + class="fa-scale-110 fa-old-padding dismiss" @click.prevent="dismissScopeNotice()" > - <i class="icon-cancel" /> + <FAIcon icon="times" /> </a> </p> <p @@ -60,10 +61,10 @@ > <span>{{ $t('post_status.scope_notice.private') }}</span> <a - class="button-icon dismiss" + class="fa-scale-110 fa-old-padding dismiss" @click.prevent="dismissScopeNotice()" > - <i class="icon-cancel" /> + <FAIcon icon="times" /> </a> </p> <p @@ -82,12 +83,18 @@ @click.stop.prevent="togglePreview" > {{ $t('post_status.preview') }} - <i :class="showPreview ? 'icon-left-open' : 'icon-right-open'" /> + <FAIcon :icon="showPreview ? 'chevron-left' : 'chevron-right'" /> </a> - <i + <div v-show="previewLoading" - class="icon-spin3 animate-spin" - /> + class="preview-spinner" + > + <FAIcon + class="fa-old-padding" + spin + icon="circle-notch" + /> + </div> </div> <div v-if="showPreview" @@ -122,7 +129,8 @@ v-model="newStatus.spoilerText" type="text" :placeholder="$t('post_status.content_warning')" - :disabled="posting" + :disabled="posting && !optimisticPosting" + size="1" class="form-post-subject" > </EmojiInput> @@ -147,7 +155,7 @@ :placeholder="placeholder || $t('post_status.default')" rows="1" cols="1" - :disabled="posting" + :disabled="posting && !optimisticPosting" class="form-post-body" :class="{ 'scrollable-form': !!maxHeight }" @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)" @@ -198,7 +206,10 @@ {{ $t(`post_status.content_type["${postFormat}"]`) }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> <div @@ -235,22 +246,22 @@ <div class="emoji-icon" > - <i + <div :title="$t('emoji.add_emoji')" - class="icon-smile btn btn-default" + class="btn btn-default" @click="showEmojiPicker" - /> + > + <FAIcon icon="smile-beam" /> + </div> </div> <div v-if="pollsAvailable" class="poll-icon" :class="{ selected: pollFormVisible }" + :title="$t('polls.add_poll')" + @click="togglePollForm" > - <i - :title="$t('polls.add_poll')" - class="icon-chart-bar btn btn-default" - @click="togglePollForm" - /> + <FAIcon icon="poll-h" /> </div> </div> <button @@ -283,8 +294,9 @@ class="alert error" > Error: {{ error }} - <i - class="button-icon icon-cancel" + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" @click="clearError" /> </div> @@ -294,8 +306,9 @@ :key="file.url" class="media-upload-wrapper" > - <i - class="fa button-icon icon-cancel" + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" @click="removeMediaFile(file)" /> <attachment @@ -375,24 +388,19 @@ } .preview-heading { - padding-left: 0.5em; display: flex; - width: 100%; - - .icon-spin3 { - margin-left: auto; - } + padding-left: 0.5em; } .preview-toggle { - display: flex; + flex: 1; cursor: pointer; user-select: none; &:hover { text-decoration: underline; } - i { + svg, i { margin-left: 0.2em; font-size: 0.8em; transform: rotate(90deg); @@ -434,18 +442,20 @@ .media-upload-icon, .poll-icon, .emoji-icon { font-size: 26px; + line-height: 1.1; flex: 1; + padding: 0 0.1em; &.selected, &:hover { // needs to be specific to override icon default color - i, label { + svg, i, label { color: $fallback--lightText; color: var(--lightText, $fallback--lightText); } } &.disabled { - i { + svg, i { cursor: not-allowed; color: $fallback--icon; color: var(--btnDisabledText, $fallback--icon); @@ -474,7 +484,7 @@ text-align: right; } - .icon-chart-bar { + .poll-icon { cursor: pointer; } @@ -487,19 +497,6 @@ margin-bottom: .5em; width: 18em; - .icon-cancel { - display: inline-block; - position: static; - margin: 0; - padding-bottom: 0; - margin-left: $fallback--attachmentRadius; - margin-left: var(--attachmentRadius, $fallback--attachmentRadius); - background-color: $fallback--fg; - background-color: var(--btn, $fallback--fg); - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - } - img, video { object-fit: contain; max-height: 10em; @@ -522,7 +519,7 @@ flex-direction: column; } - .media-upload-wrapper .attachments { + .attachments .media-upload-wrapper { padding: 0 0.5em; .attachment { @@ -531,14 +528,18 @@ position: relative; } - i { + .fa-scale-110 fa-old-padding { position: absolute; margin: 10px; - padding: 5px; + margin: .75em; + padding: .5em; background: rgba(230,230,230,0.6); + z-index: 2; + color: black; border-radius: $fallback--attachmentRadius; border-radius: var(--attachmentRadius, $fallback--attachmentRadius); font-weight: bold; + cursor: pointer; } } @@ -612,11 +613,6 @@ cursor: not-allowed; } - .icon-cancel { - cursor: pointer; - z-index: 4; - } - @keyframes fade-in { from { opacity: 0; } to { opacity: 0.6; } diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js @@ -1,5 +1,8 @@ import Popover from '../popover/popover.vue' -import { mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faSmileBeam } from '@fortawesome/free-regular-svg-icons' + +library.add(faSmileBeam) const ReactButton = { props: ['status'], @@ -29,13 +32,23 @@ const ReactButton = { emojis () { if (this.filterWord !== '') { const filterWordLowercase = this.filterWord.toLowerCase() - return this.$store.state.instance.emoji.filter(emoji => - emoji.displayText.toLowerCase().includes(filterWordLowercase) - ) + let orderedEmojiList = [] + for (const emoji of this.$store.state.instance.emoji) { + const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase) + if (indexOfFilterWord > -1) { + if (!Array.isArray(orderedEmojiList[indexOfFilterWord])) { + orderedEmojiList[indexOfFilterWord] = [] + } + orderedEmojiList[indexOfFilterWord].push(emoji) + } + } + return orderedEmojiList.flat() } return this.$store.state.instance.emoji || [] }, - ...mapGetters(['mergedConfig']) + mergedConfig () { + return this.$store.getters.mergedConfig + } } } diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue @@ -12,6 +12,7 @@ <div class="reaction-picker-filter"> <input v-model="filterWord" + size="1" :placeholder="$t('emoji.search_emoji')" > </div> @@ -36,9 +37,10 @@ <div class="reaction-bottom-fader" /> </div> </div> - <i + <FAIcon slot="trigger" - class="icon-smile button-icon add-reaction-button" + class="fa-scale-110 fa-old-padding add-reaction-button" + :icon="['far', 'smile-beam']" :title="$t('tool_tip.add_reaction')" /> </Popover> diff --git a/src/components/reply_button/reply_button.js b/src/components/reply_button/reply_button.js @@ -0,0 +1,16 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { faReply } from '@fortawesome/free-solid-svg-icons' + +library.add(faReply) + +const ReplyButton = { + name: 'ReplyButton', + props: ['status', 'replying'], + computed: { + loggedIn () { + return !!this.$store.state.users.currentUser + } + } +} + +export default ReplyButton diff --git a/src/components/reply_button/reply_button.vue b/src/components/reply_button/reply_button.vue @@ -0,0 +1,39 @@ +<template> + <div> + <FAIcon + v-if="loggedIn" + class="ReplyButton fa-scale-110 fa-old-padding -interactive" + icon="reply" + :title="$t('tool_tip.reply')" + :class="{'-active': replying}" + @click.prevent="$emit('toggle')" + /> + <FAIcon + v-else + icon="reply" + class="ReplyButton fa-scale-110 fa-old-padding" + :title="$t('tool_tip.reply')" + /> + <span v-if="status.replies_count > 0"> + {{ status.replies_count }} + </span> + </div> +</template> + +<script src="./reply_button.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.ReplyButton { + &.-interactive { + cursor: pointer; + + &:hover, + &.-active { + color: $fallback--cBlue; + color: var(--cBlue, $fallback--cBlue); + } + } +} +</style> diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js @@ -1,4 +1,7 @@ -import { mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faRetweet } from '@fortawesome/free-solid-svg-icons' + +library.add(faRetweet) const RetweetButton = { props: ['status', 'loggedIn', 'visibility'], @@ -23,12 +26,12 @@ const RetweetButton = { computed: { classes () { return { - 'retweeted': this.status.repeated, - 'retweeted-empty': !this.status.repeated, - 'animate-spin': this.animated + '-repeated': this.status.repeated } }, - ...mapGetters(['mergedConfig']) + mergedConfig () { + return this.$store.getters.mergedConfig + } } } diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue @@ -1,26 +1,30 @@ <template> <div v-if="loggedIn"> <template v-if="visibility !== 'private' && visibility !== 'direct'"> - <i + <FAIcon :class="classes" - class="button-icon retweet-button icon-retweet rt-active" + class="RetweetButton fa-scale-110 fa-old-padding -interactive" + icon="retweet" + :spin="animated" :title="$t('tool_tip.repeat')" @click.prevent="retweet()" /> <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span> </template> <template v-else> - <i + <FAIcon :class="classes" - class="button-icon icon-lock" + class="RetweetButton fa-scale-110 fa-old-padding" + icon="lock" :title="$t('timeline.no_retweet_hint')" /> </template> </div> <div v-else-if="!loggedIn"> - <i + <FAIcon :class="classes" - class="button-icon icon-retweet" + class="fa-scale-110 fa-old-padding" + icon="retweet" :title="$t('tool_tip.repeat')" /> <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span> @@ -31,16 +35,21 @@ <style lang="scss"> @import '../../_variables.scss'; -.rt-active { - cursor: pointer; - animation-duration: 0.6s; - &:hover { + +.RetweetButton { + &.-interactive { + cursor: pointer; + animation-duration: 0.6s; + + &:hover { + color: $fallback--cGreen; + color: var(--cGreen, $fallback--cGreen); + } + } + + &.-repeated { color: $fallback--cGreen; color: var(--cGreen, $fallback--cGreen); } } -.icon-retweet.retweeted { - color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); -} </style> diff --git a/src/components/scope_selector/scope_selector.js b/src/components/scope_selector/scope_selector.js @@ -1,3 +1,18 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEnvelope, + faLock, + faLockOpen, + faGlobe +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faEnvelope, + faGlobe, + faLock, + faLockOpen +) + const ScopeSelector = { props: [ 'showAll', diff --git a/src/components/scope_selector/scope_selector.vue b/src/components/scope_selector/scope_selector.vue @@ -1,36 +1,56 @@ <template> <div v-if="!showNothing" - class="scope-selector" + class="ScopeSelector" > - <i + <span v-if="showDirect" - class="icon-mail-alt" + class="scope" :class="css.direct" :title="$t('post_status.scope.direct')" @click="changeVis('direct')" - /> - <i + > + <FAIcon + icon="envelope" + class="fa-scale-110 fa-old-padding" + /> + </span> + <span v-if="showPrivate" - class="icon-lock" + class="scope" :class="css.private" :title="$t('post_status.scope.private')" @click="changeVis('private')" - /> - <i + > + <FAIcon + icon="lock" + class="fa-scale-110 fa-old-padding" + /> + </span> + <span v-if="showUnlisted" - class="icon-lock-open-alt" + class="scope" :class="css.unlisted" :title="$t('post_status.scope.unlisted')" @click="changeVis('unlisted')" - /> - <i + > + <FAIcon + icon="lock-open" + class="fa-scale-110 fa-old-padding" + /> + </span> + <span v-if="showPublic" - class="icon-globe" + class="scope" :class="css.public" :title="$t('post_status.scope.public')" @click="changeVis('public')" - /> + > + <FAIcon + icon="globe" + class="fa-scale-110 fa-old-padding" + /> + </span> </div> </template> @@ -39,12 +59,16 @@ <style lang="scss"> @import '../../_variables.scss'; -.scope-selector { - i { - font-size: 1.2em; +.ScopeSelector { + + .scope { + display: inline-block; cursor: pointer; + min-width: 1.3em; + min-height: 1.3em; + text-align: center; - &.selected { + &.selected svg { color: $fallback--lightText; color: var(--lightText, $fallback--lightText); } diff --git a/src/components/search/search.js b/src/components/search/search.js @@ -2,6 +2,16 @@ import FollowCard from '../follow_card/follow_card.vue' import Conversation from '../conversation/conversation.vue' import Status from '../status/status.vue' import map from 'lodash/map' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faCircleNotch, + faSearch +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch, + faSearch +) const Search = { components: { diff --git a/src/components/search/search.vue b/src/components/search/search.vue @@ -17,14 +17,18 @@ class="btn search-button" @click="newQuery(searchTerm)" > - <i class="icon-search" /> + <FAIcon icon="search" /> </button> </div> <div v-if="loading" class="text-center loading-icon" > - <i class="icon-spin3 animate-spin" /> + <FAIcon + icon="circle-notch" + spin + size="lg" + /> </div> <div v-else-if="loaded"> <div class="search-nav-heading"> diff --git a/src/components/search_bar/search_bar.js b/src/components/search_bar/search_bar.js @@ -1,9 +1,19 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes, + faSearch +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes, + faSearch +) + const SearchBar = { data: () => ({ searchTerm: undefined, hidden: true, - error: false, - loading: false + error: false }), watch: { '$route': function (route) { diff --git a/src/components/search_bar/search_bar.vue b/src/components/search_bar/search_bar.vue @@ -1,40 +1,47 @@ <template> - <div> - <div class="search-bar-container"> - <i - v-if="loading" - class="icon-spin4 finder-icon animate-spin-slow" - /> - <a - v-if="hidden" - href="#" - :title="$t('nav.search')" - ><i - class="button-icon icon-search" - @click.prevent.stop="toggleHidden" - /></a> - <template v-else> - <input - id="search-bar-input" - ref="searchInput" - v-model="searchTerm" - class="search-bar-input" - :placeholder="$t('nav.search')" - type="text" - @keyup.enter="find(searchTerm)" - > - <button - class="btn search-button" - @click="find(searchTerm)" - > - <i class="icon-search" /> - </button> - <i - class="button-icon icon-cancel" + <div + class="SearchBar" + :class="{ '-expanded': !hidden }" + > + <a + v-if="hidden" + href="#" + class="nav-icon" + :title="$t('nav.search')" + ><FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="search" + @click.prevent.stop="toggleHidden" + /></a> + <template v-else> + <input + id="search-bar-input" + ref="searchInput" + v-model="searchTerm" + class="search-bar-input" + :placeholder="$t('nav.search')" + type="text" + @keyup.enter="find(searchTerm)" + > + <button + class="btn search-button" + @click="find(searchTerm)" + > + <FAIcon + fixed-width + icon="search" + /> + </button> + <span> + <FAIcon + fixed-width + icon="times" + class="cancel-icon fa-scale-110 fa-old-padding" @click.prevent.stop="toggleHidden" /> - </template> - </div> + </span> + </template> </div> </template> @@ -43,30 +50,29 @@ <style lang="scss"> @import '../../_variables.scss'; -.search-bar-container { - max-width: 100%; +.SearchBar { display: inline-flex; align-items: baseline; vertical-align: baseline; justify-content: flex-end; + &.-expanded { + width: 100%; + } + .search-bar-input, .search-button { height: 29px; } .search-bar-input { - // TODO: do this properly without a rough guesstimate of 2 icons + paddings - max-width: calc(100% - 30px - 30px - 20px); - } - - .search-button { - margin-left: .5em; - margin-right: .5em; + flex: 1 0 auto; } - .icon-cancel { + .cancel-icon { cursor: pointer; + color: $fallback--text; + color: var(--btnTopBarText, $fallback--text); } } diff --git a/src/components/settings_modal/settings_modal_content.js b/src/components/settings_modal/settings_modal_content.js @@ -10,6 +10,29 @@ import GeneralTab from './tabs/general_tab.vue' import VersionTab from './tabs/version_tab.vue' import ThemeTab from './tabs/theme_tab/theme_tab.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faWrench, + faUser, + faFilter, + faPaintBrush, + faBell, + faDownload, + faEyeSlash, + faInfo +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faWrench, + faUser, + faFilter, + faPaintBrush, + faBell, + faDownload, + faEyeSlash, + faInfo +) + const SettingsModalContent = { components: { TabSwitcher, diff --git a/src/components/settings_modal/settings_modal_content.scss b/src/components/settings_modal/settings_modal_content.scss @@ -31,7 +31,7 @@ } .unavailable, - .unavailable i { + .unavailable svg { color: var(--cRed, $fallback--cRed); color: $fallback--cRed; } diff --git a/src/components/settings_modal/settings_modal_content.vue b/src/components/settings_modal/settings_modal_content.vue @@ -37,7 +37,7 @@ </div> <div :label="$t('settings.theme')" - icon="brush" + icon="paint-brush" data-tab-name="theme" > <ThemeTab /> @@ -45,7 +45,7 @@ <div v-if="isLoggedIn" :label="$t('settings.notifications')" - icon="bell-ringing-o" + icon="bell" data-tab-name="notifications" > <NotificationsTab /> @@ -62,14 +62,14 @@ v-if="isLoggedIn" :label="$t('settings.mutes_and_blocks')" :fullHeight="true" - icon="eye-off" + icon="eye-slash" data-tab-name="mutesAndBlocks" > <MutesAndBlocksTab /> </div> <div :label="$t('settings.version.title')" - icon="info-circled" + icon="info" data-tab-name="version" > <VersionTab /> diff --git a/src/components/settings_modal/tabs/data_import_export_tab.js b/src/components/settings_modal/tabs/data_import_export_tab.js @@ -1,6 +1,7 @@ import Importer from 'src/components/importer/importer.vue' import Exporter from 'src/components/exporter/exporter.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' +import { mapState } from 'vuex' const DataImportExportTab = { data () { @@ -18,21 +19,26 @@ const DataImportExportTab = { Checkbox }, computed: { - user () { - return this.$store.state.users.currentUser - } + ...mapState({ + backendInteractor: (state) => state.api.backendInteractor, + user: (state) => state.users.currentUser + }) }, methods: { getFollowsContent () { - return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id }) + return this.backendInteractor.exportFriends({ id: this.user.id }) .then(this.generateExportableUsersContent) }, getBlocksContent () { - return this.$store.state.api.backendInteractor.fetchBlocks() + return this.backendInteractor.fetchBlocks() + .then(this.generateExportableUsersContent) + }, + getMutesContent () { + return this.backendInteractor.fetchMutes() .then(this.generateExportableUsersContent) }, importFollows (file) { - return this.$store.state.api.backendInteractor.importFollows({ file }) + return this.backendInteractor.importFollows({ file }) .then((status) => { if (!status) { throw new Error('failed') @@ -40,7 +46,15 @@ const DataImportExportTab = { }) }, importBlocks (file) { - return this.$store.state.api.backendInteractor.importBlocks({ file }) + return this.backendInteractor.importBlocks({ file }) + .then((status) => { + if (!status) { + throw new Error('failed') + } + }) + }, + importMutes (file) { + return this.backendInteractor.importMutes({ file }) .then((status) => { if (!status) { throw new Error('failed') diff --git a/src/components/settings_modal/tabs/data_import_export_tab.vue b/src/components/settings_modal/tabs/data_import_export_tab.vue @@ -36,6 +36,23 @@ :export-button-label="$t('settings.block_export_button')" /> </div> + <div class="setting-item"> + <h2>{{ $t('settings.mute_import') }}</h2> + <p>{{ $t('settings.import_mutes_from_a_csv_file') }}</p> + <Importer + :submit-handler="importMutes" + :success-message="$t('settings.mutes_imported')" + :error-message="$t('settings.mute_import_error')" + /> + </div> + <div class="setting-item"> + <h2>{{ $t('settings.mute_export') }}</h2> + <Exporter + :get-content="getMutesContent" + filename="mutes.csv" + :export-button-label="$t('settings.mute_export_button')" + /> + </div> </div> </template> diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js @@ -2,6 +2,14 @@ import { filter, trim } from 'lodash' import Checkbox from 'src/components/checkbox/checkbox.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 () { diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue @@ -53,7 +53,10 @@ <option value="following">{{ $t('settings.reply_visibility_following') }}</option> <option value="self">{{ $t('settings.reply_visibility_self') }}</option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> <div> diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js @@ -2,6 +2,16 @@ import Checkbox from 'src/components/checkbox/checkbox.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faChevronDown, + faGlobe +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faChevronDown, + faGlobe +) const GeneralTab = { data () { diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue @@ -58,6 +58,11 @@ {{ $t('settings.emoji_reactions_on_timeline') }} </Checkbox> </li> + <li> + <Checkbox v-model="virtualScrolling"> + {{ $t('settings.virtual_scrolling') }} + </Checkbox> + </li> </ul> </div> @@ -98,7 +103,10 @@ {{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> </li> @@ -122,7 +130,10 @@ {{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> </li> @@ -217,7 +228,7 @@ v-if="!loopSilentAvailable" class="unavailable" > - <i class="icon-globe" />! {{ $t('settings.limited_availability') }} + <FAIcon icon="globe" />! {{ $t('settings.limited_availability') }} </div> </li> </ul> diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js @@ -8,6 +8,18 @@ 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 { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes, + faPlus, + faCircleNotch +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes, + faPlus, + faCircleNotch +) const ProfileTab = { data () { diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss @@ -119,10 +119,8 @@ &>.icon-container { width: 20px; - - &>.icon-cancel { - vertical-align: sub; - } + align-self: center; + margin: 0 .2em .5em; } } } diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue @@ -127,9 +127,9 @@ <div class="icon-container" > - <i + <FAIcon v-show="newFields.length > 1" - class="icon-cancel" + icon="times" @click="deleteField(i)" /> </div> @@ -139,7 +139,7 @@ class="add-field faint" @click="addField" > - <i class="icon-plus" /> + <FAIcon icon="plus" /> {{ $t("settings.profile_fields.add_field") }} </a> </div> @@ -166,10 +166,11 @@ :src="user.profile_image_url_original" class="current-avatar" > - <i + <FAIcon v-if="!isDefaultAvatar && pickAvatarBtnVisible" :title="$t('settings.reset_avatar')" - class="reset-button icon-cancel" + class="reset-button" + icon="times" type="button" @click="resetAvatar" /> @@ -194,10 +195,11 @@ <h2>{{ $t('settings.profile_banner') }}</h2> <div class="banner-background-preview"> <img :src="user.cover_photo"> - <i + <FAIcon v-if="!isDefaultBanner" :title="$t('settings.reset_profile_banner')" - class="reset-button icon-cancel" + class="reset-button" + icon="times" type="button" @click="resetBanner" /> @@ -214,9 +216,11 @@ @change="uploadFile('banner', $event)" > </div> - <i + <FAIcon v-if="bannerUploading" - class=" icon-spin4 animate-spin uploading" + class="uploading" + spin + icon="circle-notch" /> <button v-else-if="bannerPreview" @@ -230,8 +234,9 @@ class="alert error" > Error: {{ bannerUploadError }} - <i - class="button-icon icon-cancel" + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" @click="clearUploadError('banner')" /> </div> @@ -240,10 +245,11 @@ <h2>{{ $t('settings.profile_background') }}</h2> <div class="banner-background-preview"> <img :src="user.background_image"> - <i + <FAIcon v-if="!isDefaultBackground" :title="$t('settings.reset_profile_background')" - class="reset-button icon-cancel" + class="reset-button" + icon="times" type="button" @click="resetBackground" /> @@ -260,9 +266,11 @@ @change="uploadFile('background', $event)" > </div> - <i + <FAIcon v-if="backgroundUploading" - class=" icon-spin4 animate-spin uploading" + class="uploading" + spin + icon="circle-notch" /> <button v-else-if="backgroundPreview" @@ -276,8 +284,10 @@ class="alert error" > Error: {{ backgroundUploadError }} - <i - class="button-icon icon-cancel" + <FAIcon + size="lg" + class="fa-scale-110 fa-old-padding" + icon="times" @click="clearUploadError('background')" /> </div> diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue @@ -39,21 +39,29 @@ </i18n> <div class="icons"> - <i + <FAIcon + fixed-width style="color: var(--cBlue)" - class="button-icon icon-reply" + class="fa-scale-110 fa-old-padding" + icon="reply" /> - <i + <FAIcon + fixed-width style="color: var(--cGreen)" - class="button-icon icon-retweet" + class="fa-scale-110 fa-old-padding" + icon="retweet" /> - <i + <FAIcon + fixed-width style="color: var(--cOrange)" - class="button-icon icon-star" + class="fa-scale-110 fa-old-padding" + icon="star" /> - <i + <FAIcon + fixed-width style="color: var(--cRed)" - class="button-icon icon-cancel" + class="fa-scale-110 fa-old-padding" + icon="times" /> </div> </div> @@ -103,6 +111,25 @@ </div> </template> +<script> +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes, + faStar, + faRetweet, + faReply +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes, + faStar, + faRetweet, + faReply +) + +export default {} +</script> + <style lang="scss"> .preview-container { position: relative; 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,6 +35,14 @@ import ExportImport from 'src/components/export_import/export_import.vue' import Checkbox from 'src/components/checkbox/checkbox.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 = [ diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue @@ -80,7 +80,10 @@ {{ style[0] || style.name }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> </template> @@ -907,7 +910,10 @@ {{ $t('settings.style.shadows.components.' + shadow) }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> <div class="override"> diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js @@ -2,6 +2,20 @@ import ColorInput from '../color_input/color_input.vue' import OpacityInput from '../opacity_input/opacity_input.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' +import { + faTimes, + faChevronDown, + faChevronUp, + faPlus +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faChevronDown, + faChevronUp, + faTimes, + faPlus +) const toModel = (object = {}) => ({ x: 0, diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue @@ -78,35 +78,50 @@ {{ $t('settings.style.shadows.shadow_id', { value: index }) }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + icon="chevron-down" + class="select-down-icon" + /> </label> <button class="btn btn-default" :disabled="!ready || !present" @click="del" > - <i class="icon-cancel" /> + <FAIcon + fixed-width + icon="times" + /> </button> <button class="btn btn-default" :disabled="!moveUpValid" @click="moveUp" > - <i class="icon-up-open" /> + <FAIcon + fixed-width + icon="chevron-up" + /> </button> <button class="btn btn-default" :disabled="!moveDnValid" @click="moveDn" > - <i class="icon-down-open" /> + <FAIcon + fixed-width + icon="chevron-down" + /> </button> <button class="btn btn-default" :disabled="usingFallback" @click="add" > - <i class="icon-plus" /> + <FAIcon + fixed-width + icon="plus" + /> </button> </div> <div diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js @@ -2,6 +2,34 @@ import { mapState, mapGetters } from 'vuex' import UserCard from '../user_card/user_card.vue' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' import GestureService from '../../services/gesture_service/gesture_service' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faSignInAlt, + faSignOutAlt, + faHome, + faComments, + faBell, + faUserPlus, + faBullhorn, + faSearch, + faTachometerAlt, + faCog, + faInfoCircle +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faSignInAlt, + faSignOutAlt, + faHome, + faComments, + faBell, + faUserPlus, + faBullhorn, + faSearch, + faTachometerAlt, + faCog, + faInfoCircle +) const SideDrawer = { props: [ 'logout' ], diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue @@ -36,7 +36,11 @@ @click="toggleDrawer" > <router-link :to="{ name: 'login' }"> - <i class="button-icon icon-login" /> {{ $t("login.login") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="sign-in-alt" + /> {{ $t("login.login") }} </router-link> </li> <li @@ -44,7 +48,11 @@ @click="toggleDrawer" > <router-link :to="{ name: timelinesRoute }"> - <i class="button-icon icon-home-2" /> {{ $t("nav.timelines") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="home" + /> {{ $t("nav.timelines") }} </router-link> </li> <li @@ -55,10 +63,14 @@ :to="{ name: 'chats', params: { username: currentUser.screen_name } }" style="position: relative" > - <i class="button-icon icon-chat" /> {{ $t("nav.chats") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="comments" + /> {{ $t("nav.chats") }} <span v-if="unreadChatCount" - class="badge badge-notification unread-chat-count" + class="badge badge-notification" > {{ unreadChatCount }} </span> @@ -68,7 +80,11 @@ <ul v-if="currentUser"> <li @click="toggleDrawer"> <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> - <i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="bell" + /> {{ $t("nav.interactions") }} </router-link> </li> <li @@ -76,10 +92,14 @@ @click="toggleDrawer" > <router-link to="/friend-requests"> - <i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="user-plus" + /> {{ $t("nav.friend_requests") }} <span v-if="followRequestCount > 0" - class="badge follow-request-count" + class="badge badge-notification" > {{ followRequestCount }} </span> @@ -90,7 +110,11 @@ @click="toggleDrawer" > <router-link :to="{ name: 'chat' }"> - <i class="button-icon icon-megaphone" /> {{ $t("shoutbox.title") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="bullhorn" + /> {{ $t("shoutbox.title") }} </router-link> </li> </ul> @@ -100,7 +124,11 @@ @click="toggleDrawer" > <router-link :to="{ name: 'search' }"> - <i class="button-icon icon-search" /> {{ $t("nav.search") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="search" + /> {{ $t("nav.search") }} </router-link> </li> <li @@ -108,7 +136,11 @@ @click="toggleDrawer" > <router-link :to="{ name: 'who-to-follow' }"> - <i class="button-icon icon-user-plus" /> {{ $t("nav.who_to_follow") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="user-plus" + /> {{ $t("nav.who_to_follow") }} </router-link> </li> <li @click="toggleDrawer"> @@ -116,12 +148,20 @@ href="#" @click="openSettingsModal" > - <i class="button-icon icon-cog" /> {{ $t("settings.settings") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="cog" + /> {{ $t("settings.settings") }} </a> </li> <li @click="toggleDrawer"> <router-link :to="{ name: 'about'}"> - <i class="button-icon icon-info-circled" /> {{ $t("nav.about") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="info-circle" + /> {{ $t("nav.about") }} </router-link> </li> <li @@ -132,7 +172,11 @@ href="/pleroma/admin/#/login-pleroma" target="_blank" > - <i class="button-icon icon-gauge" /> {{ $t("nav.administration") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="tachometer-alt" + /> {{ $t("nav.administration") }} </a> </li> <li @@ -143,7 +187,11 @@ href="#" @click="doLogout" > - <i class="button-icon icon-logout" /> {{ $t("login.logout") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="sign-out-alt" + /> {{ $t("login.logout") }} </a> </li> </ul> @@ -224,8 +272,10 @@ --lightText: var(--popoverLightText, $fallback--lightText); --icon: var(--popoverIcon, $fallback--icon); - .button-icon:before { - width: 1.1em; + .badge { + position: absolute; + right: 0.7rem; + top: 1em; } } @@ -272,7 +322,6 @@ border-bottom: 1px solid; border-color: $fallback--border; border-color: var(--border, $fallback--border); - margin: 0.2em 0; } .side-drawer ul:last-child { @@ -283,8 +332,11 @@ padding: 0; a { + box-sizing: border-box; display: block; - padding: 0.5em 0.85em; + height: 3em; + line-height: 3em; + padding: 0 0.7em; &:hover { background-color: $fallback--lightBg; diff --git a/src/components/status/status.js b/src/components/status/status.js @@ -1,3 +1,4 @@ +import ReplyButton from '../reply_button/reply_button.vue' import FavoriteButton from '../favorite_button/favorite_button.vue' import ReactButton from '../react_button/react_button.vue' import RetweetButton from '../retweet_button/retweet_button.vue' @@ -15,11 +16,48 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { muteWordHits } from '../../services/status_parser/status_parser.js' import { unescape, uniqBy } from 'lodash' -import { mapGetters, mapState } from 'vuex' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEnvelope, + faLock, + faLockOpen, + faGlobe, + faTimes, + faRetweet, + faReply, + faExternalLinkSquareAlt, + faPlusSquare, + faSmileBeam, + faEllipsisH, + faStar, + faEyeSlash, + faEye, + faThumbtack +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faEnvelope, + faGlobe, + faLock, + faLockOpen, + faTimes, + faRetweet, + faReply, + faExternalLinkSquareAlt, + faPlusSquare, + faStar, + faSmileBeam, + faEllipsisH, + faEyeSlash, + faEye, + faThumbtack +) const Status = { name: 'Status', components: { + ReplyButton, FavoriteButton, ReactButton, RetweetButton, @@ -54,6 +92,8 @@ const Status = { replying: false, unmuted: false, userExpanded: false, + mediaPlaying: [], + suspendable: true, error: null } }, @@ -157,7 +197,7 @@ const Status = { return this.mergedConfig.hideFilteredStatuses }, hideStatus () { - return this.deleted || (this.muted && this.hideFilteredStatuses) + return (this.muted && this.hideFilteredStatuses) || this.virtualHidden }, isFocused () { // retweet or root of an expanded conversation @@ -207,23 +247,30 @@ const Status = { hidePostStats () { return this.mergedConfig.hidePostStats }, - ...mapGetters(['mergedConfig']), - ...mapState({ - betterShadow: state => state.interface.browserSupport.cssFilter, - currentUser: state => state.users.currentUser - }) + currentUser () { + return this.$store.state.users.currentUser + }, + betterShadow () { + return this.$store.state.interface.browserSupport.cssFilter + }, + mergedConfig () { + return this.$store.getters.mergedConfig + }, + isSuspendable () { + return !this.replying && this.mediaPlaying.length === 0 + } }, methods: { visibilityIcon (visibility) { switch (visibility) { case 'private': - return 'icon-lock' + return 'lock' case 'unlisted': - return 'icon-lock-open-alt' + return 'lock-open' case 'direct': - return 'icon-mail-alt' + return 'envelope' default: - return 'icon-globe' + return 'globe' } }, showError (error) { @@ -251,6 +298,12 @@ const Status = { }, generateUserProfileLink (id, name) { return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) + }, + addMediaPlaying (id) { + this.mediaPlaying.push(id) + }, + removeMediaPlaying (id) { + this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id) } }, watch: { @@ -280,6 +333,9 @@ const Status = { if (this.isFocused && this.statusFromGlobalRepository.favoritedBy && this.statusFromGlobalRepository.favoritedBy.length !== num) { this.$store.dispatch('fetchFavs', this.status.id) } + }, + 'isSuspendable': function (val) { + this.suspendable = val } }, filters: { diff --git a/src/components/status/status.scss b/src/components/status/status.scss @@ -7,8 +7,9 @@ $status-margin: 0.75em; min-width: 0; &:hover { - --still-image-img: visible; - --still-image-canvas: hidden; + --_still-image-img-visibility: visible; + --_still-image-canvas-visibility: hidden; + --_still-image-label-visibility: hidden; } &.-focused { @@ -25,6 +26,23 @@ $status-margin: 0.75em; --icon: var(--selectedPostIcon, $fallback--icon); } + &.-conversation { + border-left-width: 4px; + border-left-style: solid; + } + + .gravestone { + padding: $status-margin; + color: $fallback--faint; + color: var(--faint, $fallback--faint); + display: flex; + + .deleted-text { + margin: 0.5em 0; + align-items: center; + } + } + .status-container { display: flex; padding: $status-margin; @@ -41,6 +59,15 @@ $status-margin: 0.75em; justify-content: flex-end; } + ._misclick-prevention & { + pointer-events: none; + + .attachments { + pointer-events: initial; + cursor: initial; + } + } + .left-side { margin-right: $status-margin; } @@ -139,11 +166,6 @@ $status-margin: 0.75em; text-overflow: ellipsis; overflow-x: hidden; } - - .icon-reply { - // mirror the icon - transform: scaleX(-1); - } } & .reply-to-popover, @@ -183,7 +205,6 @@ $status-margin: 0.75em; } .reply-to { - display: flex; position: relative; } @@ -191,7 +212,6 @@ $status-margin: 0.75em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - margin-left: 0.2em; } .replies-separator { @@ -215,16 +235,10 @@ $status-margin: 0.75em; .repeat-info { padding: 0.4em $status-margin; - line-height: 22px; - - .right-side { - display: flex; - align-content: center; - flex-wrap: wrap; - } - i { - padding: 0 0.2em; + .repeat-icon { + color: $fallback--cGreen; + color: var(--cGreen, $fallback--cGreen); } } @@ -274,18 +288,6 @@ $status-margin: 0.75em; } } - .button-reply { - &:not(.-disabled) { - cursor: pointer; - } - - &:not(.-disabled):hover, - &.-active { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); - } - } - .muted { padding: 0.25em 0.6em; height: 1.2em; diff --git a/src/components/status/status.vue b/src/components/status/status.vue @@ -10,17 +10,20 @@ class="alert error" > {{ error }} - <i - class="button-icon icon-cancel" + <span + class="fa-scale-110 fa-old-padding" @click="clearError" - /> + > + <FAIcon icon="times" /> + </span> </div> <template v-if="muted && !isPreview"> - <div class="status-csontainer muted"> + <div class="status-container muted"> <small class="status-username"> - <i + <FAIcon v-if="muted && retweet" - class="button-icon icon-retweet" + class="fa-scale-110 fa-old-padding repeat-icon" + icon="retweet" /> <router-link :to="userProfileLink"> {{ status.user.screen_name }} @@ -46,9 +49,14 @@ </small> <a href="#" - class="unmute" + class="unmute fa-scale-110 fa-old-padding" @click.prevent="toggleMute" - ><i class="button-icon icon-eye-off" /></a> + > + <FAIcon + icon="eye-slash" + class="fa-scale-110 fa-old-padding" + /> + </a> </div> </template> <template v-else> @@ -56,7 +64,10 @@ v-if="showPinned" class="pin" > - <i class="fa icon-pin faint" /> + <FAIcon + icon="thumbtack" + class="faint" + /> <span class="faint">{{ $t('status.pinned') }}</span> </div> <div @@ -86,8 +97,9 @@ :to="retweeterProfileLink" >{{ retweeter }}</router-link> </span> - <i - class="fa icon-retweet retweeted" + <FAIcon + icon="retweet" + class="repeat-icon" :title="$t('tool_tip.repeat')" /> {{ $t('timeline.repeated') }} @@ -95,6 +107,7 @@ </div> <div + v-if="!deleted" :class="[userClass, { highlighted: userStyle, '-repeat': retweet && !inConversation }]" :style="[ userStyle ]" class="status-container" @@ -166,15 +179,16 @@ :auto-update="60" /> </router-link> - <div + <span v-if="status.visibility" - class="button-icon visibility-icon" + class="visibility-icon" + :title="status.visibility | capitalize" > - <i - :class="visibilityIcon(status.visibility)" - :title="status.visibility | capitalize" + <FAIcon + class="fa-scale-110 fa-old-padding" + :icon="visibilityIcon(status.visibility)" /> - </div> + </span> <a v-if="!status.is_local && !isPreview" :href="status.external_url" @@ -182,22 +196,32 @@ class="source_url" title="Source" > - <i class="button-icon icon-link-ext-alt" /> + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="external-link-square-alt" + /> + </a> + <a + v-if="expandable && !isPreview" + href="#" + title="Expand" + @click.prevent="toggleExpanded" + > + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="plus-square" + /> </a> - <template v-if="expandable && !isPreview"> - <a - href="#" - title="Expand" - @click.prevent="toggleExpanded" - > - <i class="button-icon icon-plus-squared" /> - </a> - </template> <a v-if="unmuted" href="#" @click.prevent="toggleMute" - ><i class="button-icon icon-eye-off" /></a> + > + <FAIcon + icon="eye-slash" + class="fa-scale-110 fa-old-padding" + /> + </a> </span> </div> @@ -219,7 +243,11 @@ :aria-label="$t('tool_tip.reply')" @click.prevent="gotoOriginal(status.in_reply_to_status_id)" > - <i class="button-icon reply-button icon-reply" /> + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="reply" + flip="horizontal" + /> <span class="faint-link reply-to-text" > @@ -227,6 +255,7 @@ </span> </a> </StatusPopover> + <span v-else class="reply-to-no-popover" @@ -272,6 +301,8 @@ :no-heading="noHeading" :highlight="highlight" :focused="isFocused" + @mediaplay="addMediaPlaying($event)" + @mediapause="removeMediaPlaying($event)" /> <transition name="fade"> @@ -320,21 +351,11 @@ v-if="!noHeading && !isPreview" class="status-actions" > - <div> - <i - v-if="loggedIn" - class="button-icon button-reply icon-reply" - :title="$t('tool_tip.reply')" - :class="{'-active': replying}" - @click.prevent="toggleReplying" - /> - <i - v-else - class="button-icon button-reply -disabled icon-reply" - :title="$t('tool_tip.reply')" - /> - <span v-if="status.replies_count > 0">{{ status.replies_count }}</span> - </div> + <reply-button + :replying="replying" + :status="status" + @toggle="toggleReplying" + /> <retweet-button :visibility="status.visibility" :logged-in="loggedIn" @@ -357,6 +378,25 @@ </div> </div> <div + v-else + class="gravestone" + > + <div class="left-side"> + <UserAvatar :compact="compact" /> + </div> + <div class="right-side"> + <div class="deleted-text"> + {{ $t('status.status_deleted') }} + </div> + <reply-button + v-if="replying" + :replying="replying" + :status="status" + @toggle="toggleReplying" + /> + </div> + </div> + <div v-if="replying" class="status-container reply-form" > @@ -376,4 +416,5 @@ </template> <script src="./status.js" ></script> + <style src="./status.scss" lang="scss"></style> diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js @@ -7,6 +7,24 @@ 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 { + faCircleNotch, + faFile, + faMusic, + faImage, + faLink, + faPollH +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch, + faFile, + faMusic, + faImage, + faLink, + faPollH +) const StatusContent = { name: 'StatusContent', diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue @@ -55,29 +55,29 @@ @click.prevent="toggleShowMore" > {{ $t("status.show_content") }} - <span + <FAIcon v-if="attachmentTypes.includes('image')" - class="icon-picture" + icon="image" /> - <span + <FAIcon v-if="attachmentTypes.includes('video')" - class="icon-video" + icon="video" /> - <span + <FAIcon v-if="attachmentTypes.includes('audio')" - class="icon-music" + icon="music" /> - <span + <FAIcon v-if="attachmentTypes.includes('unknown')" - class="icon-doc" + icon="file" /> - <span + <FAIcon v-if="status.poll && status.poll.options" - class="icon-chart-bar" + icon="poll-h" /> - <span + <FAIcon v-if="status.card" - class="icon-link" + icon="link" /> </a> <a @@ -107,6 +107,8 @@ :attachment="attachment" :allow-play="true" :set-media="setMedia()" + @play="$emit('mediaplay', attachment.id)" + @pause="$emit('mediapause', attachment.id)" /> <gallery v-if="galleryAttachments.length > 0" @@ -180,6 +182,10 @@ $status-margin: 0.75em; text-align: center; display: inline-block; word-break: break-all; + + svg { + color: inherit; + } } img, video { diff --git a/src/components/status_popover/status_popover.js b/src/components/status_popover/status_popover.js @@ -1,4 +1,10 @@ import { find } from 'lodash' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch +) const StatusPopover = { name: 'StatusPopover', diff --git a/src/components/status_popover/status_popover.vue b/src/components/status_popover/status_popover.vue @@ -27,7 +27,11 @@ v-else class="status-preview-no-content" > - <i class="icon-spin4 animate-spin" /> + <FAIcon + icon="circle-notch" + spin + size="2x" + /> </div> </div> </Popover> diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js @@ -19,14 +19,16 @@ const StillImage = { }, methods: { onLoad () { - this.imageLoadHandler && this.imageLoadHandler(this.$refs.src) + const image = this.$refs.src + if (!image) return + this.imageLoadHandler && this.imageLoadHandler(image) const canvas = this.$refs.canvas if (!canvas) return - const width = this.$refs.src.naturalWidth - const height = this.$refs.src.naturalHeight + const width = image.naturalWidth + const height = image.naturalHeight canvas.width = width canvas.height = height - canvas.getContext('2d').drawImage(this.$refs.src, 0, 0, width, height) + canvas.getContext('2d').drawImage(image, 0, 0, width, height) }, onError () { this.imageLoadError && this.imageLoadError() diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue @@ -42,7 +42,7 @@ width: 100%; height: 100%; object-fit: contain; - visibility: var(--still-image-canvas, visible); + visibility: var(--_still-image-canvas-visibility, visible); } img { @@ -66,16 +66,19 @@ border-radius: $fallback--tooltipRadius; border-radius: var(--tooltipRadius, $fallback--tooltipRadius); z-index: 2; - visibility: var(--still-image-label-visibility, visible); + visibility: var(--_still-image-label-visibility, visible); } &:hover canvas { display: none; } - &:hover::before, + &:hover::before { + visibility: var(--_still-image-label-visibility, hidden); + } + img { - visibility: var(--still-image-img, hidden); + visibility: var(--_still-image-img-visibility, hidden); } &:hover img { diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js @@ -1,5 +1,6 @@ import Vue from 'vue' import { mapState } from 'vuex' +import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome' import './tab_switcher.scss' @@ -107,7 +108,7 @@ export default Vue.component('tab-switcher', { class={classesTab.join(' ')} type="button" > - {!slot.data.attrs.icon ? '' : (<i class={'tab-icon icon-' + slot.data.attrs.icon}/>)} + {!slot.data.attrs.icon ? '' : (<FAIcon class="tab-icon" size="2x" fixed-width icon={slot.data.attrs.icon}/>)} <span class="text"> {slot.data.attrs.label} </span> diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss @@ -4,7 +4,7 @@ display: flex; .tab-icon { - font-size: 2em; + margin: 0.2em auto; display: block; } @@ -91,7 +91,7 @@ flex-direction: column; @media all and (max-width: 800px) { - min-width: 1em; + min-width: 4em; } &:not(.active)::after { diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js @@ -2,7 +2,13 @@ import Status from '../status/status.vue' import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js' import Conversation from '../conversation/conversation.vue' import TimelineMenu from '../timeline_menu/timeline_menu.vue' -import { throttle, keyBy } from 'lodash' +import { debounce, throttle, keyBy } from 'lodash' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch +) export const getExcludedStatusIdsByPinning = (statuses, pinnedStatusIds) => { const ids = [] @@ -33,7 +39,9 @@ const Timeline = { return { paused: false, unfocused: false, - bottomedOut: false + bottomedOut: false, + virtualScrollIndex: 0, + blockingClicks: false } }, components: { @@ -63,8 +71,10 @@ const Timeline = { } }, classes () { + let rootClasses = !this.embedded ? ['panel', 'panel-default'] : [] + if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention']) return { - root: ['timeline'].concat(!this.embedded ? ['panel', 'panel-default'] : []), + root: rootClasses, header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading'] : []), body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []), footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : []) @@ -78,6 +88,16 @@ const Timeline = { }, pinnedStatusIdsObject () { return keyBy(this.pinnedStatusIds) + }, + statusesToDisplay () { + const amount = this.timeline.visibleStatuses.length + const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80)) + const min = Math.max(0, this.virtualScrollIndex - statusesPerSide) + const max = Math.min(amount, this.virtualScrollIndex + statusesPerSide) + return this.timeline.visibleStatuses.slice(min, max).map(_ => _.id) + }, + virtualScrollingEnabled () { + return this.$store.getters.mergedConfig.virtualScrolling } }, created () { @@ -85,7 +105,7 @@ const Timeline = { const credentials = store.state.users.currentUser.credentials const showImmediately = this.timeline.visibleStatuses.length === 0 - window.addEventListener('scroll', this.scrollLoad) + window.addEventListener('scroll', this.handleScroll) if (store.state.api.fetchers[this.timelineName]) { return false } @@ -104,14 +124,24 @@ const Timeline = { this.unfocused = document.hidden } window.addEventListener('keydown', this.handleShortKey) + setTimeout(this.determineVisibleStatuses, 250) }, destroyed () { - window.removeEventListener('scroll', this.scrollLoad) + window.removeEventListener('scroll', this.handleScroll) window.removeEventListener('keydown', this.handleShortKey) if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false) this.$store.commit('setLoading', { timeline: this.timelineName, value: false }) }, methods: { + stopBlockingClicks: debounce(function () { + this.blockingClicks = false + }, 1000), + blockClicksTemporarily () { + if (!this.blockingClicks) { + this.blockingClicks = true + } + this.stopBlockingClicks() + }, handleShortKey (e) { // Ignore when input fields are focused if (['textarea', 'input'].includes(e.target.tagName.toLowerCase())) return @@ -123,6 +153,7 @@ const Timeline = { this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 }) this.fetchOlderStatuses() } else { + this.blockClicksTemporarily() this.$store.commit('showNewStatuses', { timeline: this.timelineName }) this.paused = false } @@ -146,6 +177,48 @@ const Timeline = { } }) }, 1000, this), + determineVisibleStatuses () { + if (!this.$refs.timeline) return + if (!this.virtualScrollingEnabled) return + + const statuses = this.$refs.timeline.children + const cappedScrollIndex = Math.max(0, Math.min(this.virtualScrollIndex, statuses.length - 1)) + + if (statuses.length === 0) return + + const height = Math.max(document.body.offsetHeight, window.pageYOffset) + + const centerOfScreen = window.pageYOffset + (window.innerHeight * 0.5) + + // Start from approximating the index of some visible status by using the + // the center of the screen on the timeline. + let approxIndex = Math.floor(statuses.length * (centerOfScreen / height)) + let err = statuses[approxIndex].getBoundingClientRect().y + + // if we have a previous scroll index that can be used, test if it's + // closer than the previous approximation, use it if so + + const virtualScrollIndexY = statuses[cappedScrollIndex].getBoundingClientRect().y + if (Math.abs(err) > virtualScrollIndexY) { + approxIndex = cappedScrollIndex + err = virtualScrollIndexY + } + + // if the status is too far from viewport, check the next/previous ones if + // they happen to be better + while (err < -20 && approxIndex < statuses.length - 1) { + err += statuses[approxIndex].offsetHeight + approxIndex++ + } + while (err > window.innerHeight + 100 && approxIndex > 0) { + approxIndex-- + err -= statuses[approxIndex].offsetHeight + } + + // this status is now the center point for virtual scrolling and visible + // statuses will be nearby statuses before and after it + this.virtualScrollIndex = approxIndex + }, scrollLoad (e) { const bodyBRect = document.body.getBoundingClientRect() const height = Math.max(bodyBRect.height, -(bodyBRect.y)) @@ -155,6 +228,10 @@ const Timeline = { this.fetchOlderStatuses() } }, + handleScroll: throttle(function (e) { + this.determineVisibleStatuses() + this.scrollLoad(e) + }, 200), handleVisibilityChange () { this.unfocused = document.hidden } diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue @@ -1,5 +1,5 @@ <template> - <div :class="[classes.root, 'timeline']"> + <div :class="[classes.root, 'Timeline']"> <div :class="classes.header"> <TimelineMenu v-if="!embedded" /> <div @@ -32,7 +32,10 @@ </div> </div> <div :class="classes.body"> - <div class="timeline"> + <div + ref="timeline" + class="timeline" + > <template v-for="statusId in pinnedStatusIds"> <conversation v-if="timeline.statusesObject[statusId]" @@ -54,6 +57,7 @@ :collapsable="true" :in-profile="inProfile" :profile-user-id="userId" + :virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.includes(status.id)" /> </template> </div> @@ -88,7 +92,11 @@ v-else class="new-status-notification text-center panel-footer" > - <i class="icon-spin3 animate-spin" /> + <FAIcon + icon="circle-notch" + spin + size="lg" + /> </div> </div> </div> @@ -99,10 +107,14 @@ <style lang="scss"> @import '../../_variables.scss'; -.timeline { +.Timeline { .loadmore-text { opacity: 1; } + + &.-blocked { + cursor: progress; + } } .timeline-heading { diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js @@ -1,5 +1,23 @@ import Popover from '../popover/popover.vue' import { mapState } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faHome, + faChevronDown +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faHome, + faChevronDown +) // Route -> i18n key mapping, exported andnot in the computed // because nav panel benefits from the same information. diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue @@ -1,7 +1,7 @@ <template> <Popover trigger="click" - class="timeline-menu" + class="TimelineMenu" :class="{ 'open': isOpen }" :margin="{ left: -15, right: -200 }" :bound-to="{ x: 'container' }" @@ -16,27 +16,47 @@ <ul> <li v-if="currentUser"> <router-link :to="{ name: 'friends' }"> - <i class="button-icon icon-home-2" />{{ $t("nav.timeline") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="home" + />{{ $t("nav.timeline") }} </router-link> </li> <li v-if="currentUser"> <router-link :to="{ name: 'bookmarks'}"> - <i class="button-icon icon-bookmark" />{{ $t("nav.bookmarks") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="bookmark" + />{{ $t("nav.bookmarks") }} </router-link> </li> <li v-if="currentUser"> <router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }"> - <i class="button-icon icon-mail-alt" />{{ $t("nav.dms") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="envelope" + />{{ $t("nav.dms") }} </router-link> </li> <li v-if="currentUser || !privateMode"> <router-link :to="{ name: 'public-timeline' }"> - <i class="button-icon icon-users" />{{ $t("nav.public_tl") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="users" + />{{ $t("nav.public_tl") }} </router-link> </li> <li v-if="federating && (currentUser || !privateMode)"> <router-link :to="{ name: 'public-external-timeline' }"> - <i class="button-icon icon-globe" />{{ $t("nav.twkn") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="globe" + />{{ $t("nav.twkn") }} </router-link> </li> </ul> @@ -46,7 +66,10 @@ class="title timeline-menu-title" > <span>{{ timelineName() }}</span> - <i class="icon-down-open" /> + <FAIcon + size="sm" + icon="chevron-down" + /> </div> </Popover> </template> @@ -56,17 +79,19 @@ <style lang="scss"> @import '../../_variables.scss'; -.timeline-menu { +.TimelineMenu { flex-shrink: 1; margin-right: auto; min-width: 0; width: 24rem; + .timeline-menu-popover-wrap { overflow: hidden; // Match panel heading padding to line up menu with bottom of heading margin-top: 0.6rem; padding: 0 15px 15px 15px; } + .timeline-menu-popover { width: 24rem; max-width: 100vw; @@ -77,10 +102,12 @@ transform: translateY(-100%); transition: transform 100ms; } + .panel::after { border-top-right-radius: 0; border-top-left-radius: 0; } + &.open .timeline-menu-popover { transform: translateY(0); } @@ -88,7 +115,6 @@ .timeline-menu-title { margin: 0; cursor: pointer; - display: flex; user-select: none; width: 100%; @@ -98,15 +124,13 @@ white-space: nowrap; } - i { + svg { margin-left: 0.6em; - flex-shrink: 0; - font-size: 1rem; transition: transform 100ms; } } - &.open .timeline-menu-title i { + &.open .timeline-menu-title svg { color: $fallback--text; color: var(--panelText, $fallback--text); transform: rotate(180deg); @@ -138,15 +162,11 @@ &:last-child { border: none; } - - i { - margin: 0 0.5em; - } } a { display: block; - padding: 0.6em 0; + padding: 0.6em 0.65em; &:hover { background-color: $fallback--lightBg; @@ -174,6 +194,11 @@ text-decoration: underline; } } + + svg { + margin-right: 0.4em; + margin-left: -0.2em; + } } } diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue @@ -1,5 +1,6 @@ <template> <StillImage + v-if="user" class="Avatar" :alt="user.screen_name" :title="user.screen_name" @@ -7,6 +8,11 @@ :class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }" :image-load-error="imageLoadError" /> + <div + v-else + class="Avatar -placeholder" + :class="{ 'avatar-compact': compact }" + /> </template> <script src="./user_avatar.js"></script> @@ -14,11 +20,14 @@ @import '../../_variables.scss'; .Avatar { - --still-image-label-visibility: hidden; + --_avatarShadowBox: var(--avatarStatusShadow); + --_avatarShadowFilter: var(--avatarStatusShadowFilter); + --_avatarShadowInset: var(--avatarStatusShadowInset); + --_still-image-label-visibility: hidden; width: 48px; height: 48px; - box-shadow: var(--avatarStatusShadow); + box-shadow: var(--_avatarShadowBox); border-radius: $fallback--avatarRadius; border-radius: var(--avatarRadius, $fallback--avatarRadius); @@ -28,8 +37,8 @@ } &.better-shadow { - box-shadow: var(--avatarStatusShadowInset); - filter: var(--avatarStatusShadowFilter) + box-shadow: var(--_avatarShadowInset); + filter: var(--_avatarShadowFilter); } &.animated::before { @@ -42,5 +51,10 @@ border-radius: $fallback--avatarAltRadius; border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); } + + &.-placeholder { + background-color: $fallback--fg; + background-color: var(--fg, $fallback--fg); + } } </style> diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js @@ -6,6 +6,22 @@ import ModerationTools from '../moderation_tools/moderation_tools.vue' import AccountActions from '../account_actions/account_actions.vue' 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 +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faRss, + faBell, + faChevronDown, + faSearchPlus, + faExternalLinkAlt +) export default { props: [ diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue @@ -21,7 +21,10 @@ :user="user" /> <div class="user-info-avatar-link-overlay"> - <i class="button-icon icon-zoom-in" /> + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="search-plus" + /> </div> </a> <router-link @@ -54,8 +57,12 @@ v-if="isOtherUser && !user.is_local" :href="user.statusnet_profile_url" target="_blank" + class="external-link-button" > - <i class="icon-link-ext usersettings" /> + <FAIcon + class="icon" + icon="external-link-alt" + /> </a> <AccountActions v-if="isOtherUser && loggedIn" @@ -85,7 +92,13 @@ bot </span> </template> - <span v-if="user.locked"><i class="icon icon-lock" /></span> + <span v-if="user.locked"> + <FAIcon + class="lock-icon" + icon="lock" + size="sm" + /> + </span> <span v-if="!mergedConfig.hideUserStats && !hideBio" class="dailyAvg" @@ -133,7 +146,10 @@ <option value="striped">Striped bg</option> <option value="side">Side stripe</option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> </div> @@ -150,7 +166,7 @@ :click="subscribeUser" :title="$t('user_card.subscribe')" > - <i class="icon-bell-alt" /> + <FAIcon icon="bell" /> </ProgressButton> <ProgressButton v-else @@ -158,7 +174,18 @@ :click="unsubscribeUser" :title="$t('user_card.unsubscribe')" > - <i class="icon-bell-ringing-o" /> + <FALayers> + <FAIcon + icon="rss" + transform="left-5 shrink-6 up-3 rotate-20" + flip="horizontal" + /> + <FAIcon + icon="rss" + transform="right-5 shrink-6 up-3 rotate-20" + /> + <FAIcon icon="bell" /> + </FALayers> </ProgressButton> </template> </div> @@ -255,6 +282,11 @@ .user-card { position: relative; + &:hover .Avatar { + --_still-image-img-visibility: visible; + --_still-image-canvas-visibility: hidden; + } + .panel-heading { padding: .5em 0; text-align: center; @@ -283,7 +315,7 @@ mask: linear-gradient(to top, white, transparent) bottom no-repeat, linear-gradient(to top, white, white); // Autoprefixed seem to ignore this one, and also syntax is different - -webkit-mask-composite: xor; + -webkit-mask-composite: xor; mask-composite: exclude; background-size: cover; mask-size: 100% 60%; @@ -355,20 +387,17 @@ max-height: 56px; .Avatar { + --_avatarShadowBox: var(--avatarShadow); + --_avatarShadowFilter: var(--avatarShadowFilter); + --_avatarShadowInset: var(--avatarShadowInset); + flex: 1 0 100%; width: 56px; height: 56px; - box-shadow: 0px 1px 8px rgba(0,0,0,0.75); - box-shadow: var(--avatarShadow); object-fit: cover; } } - &:hover .Avatar { - --still-image-img: visible; - --still-image-canvas: hidden; - } - &-avatar-link { position: relative; cursor: pointer; @@ -388,7 +417,7 @@ opacity: 0; transition: opacity .2s ease; - i { + svg { color: #FFF; } } @@ -398,10 +427,17 @@ } } - .usersettings { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - opacity: .8; + .external-link-button { + cursor: pointer; + width: 2.5em; + text-align: center; + margin: -0.5em 0; + padding: 0.5em 0; + + &:not(:hover) .icon { + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + } } .user-summary { @@ -447,6 +483,10 @@ font-weight: light; font-size: 15px; + .lock-icon { + margin-left: 0.5em; + } + .user-screen-name { min-width: 1px; flex: 0 1 auto; @@ -508,7 +548,7 @@ padding-bottom: 0; flex: 1 0 auto; } - .userHighlightSel.select i { + .userHighlightSel.select svg { line-height: 22px; } diff --git a/src/components/user_list_popover/user_list_popover.js b/src/components/user_list_popover/user_list_popover.js @@ -1,3 +1,9 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch +) const UserListPopover = { name: 'UserListPopover', diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue @@ -31,7 +31,11 @@ </div> </div> <div v-else> - <i class="icon-spin4 animate-spin" /> + <FAIcon + icon="circle-notch" + spin + size="3x" + /> </div> </div> </Popover> diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js @@ -6,6 +6,14 @@ import Conversation from '../conversation/conversation.vue' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js' import List from '../list/list.vue' import withLoadMore from '../../hocs/with_load_more/with_load_more' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faCircleNotch +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch +) const FollowerList = withLoadMore({ fetch: (props, $store) => $store.dispatch('fetchFollowers', props.userId), diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue @@ -122,9 +122,10 @@ </div> <div class="panel-body"> <span v-if="error">{{ error }}</span> - <i + <FAIcon v-else - class="icon-spin3 animate-spin" + spin + icon="circle-notch" /> </div> </div> @@ -142,6 +143,7 @@ .user-profile-fields { margin: 0 0.5em; + img { object-fit: contain; vertical-align: middle; @@ -156,8 +158,7 @@ .user-profile-field { display: flex; - margin: 0.25em auto; - max-width: 32em; + margin: 0.25em; border: 1px solid var(--border, $fallback--border); border-radius: $fallback--inputRadius; border-radius: var(--inputRadius, $fallback--inputRadius); diff --git a/src/components/video_attachment/video_attachment.js b/src/components/video_attachment/video_attachment.js @@ -3,27 +3,48 @@ const VideoAttachment = { props: ['attachment', 'controls'], data () { return { - loopVideo: this.$store.getters.mergedConfig.loopVideo + blocksSuspend: false, + // Start from true because removing "loop" property seems buggy in Vue + hasAudio: true + } + }, + computed: { + loopVideo () { + if (this.$store.getters.mergedConfig.loopVideoSilentOnly) { + return !this.hasAudio + } + return this.$store.getters.mergedConfig.loopVideo } }, methods: { - onVideoDataLoad (e) { + onPlaying (e) { + this.setHasAudio(e) + if (this.loopVideo) { + this.$emit('play', { looping: true }) + return + } + this.$emit('play') + }, + onPaused (e) { + this.$emit('pause') + }, + setHasAudio (e) { const target = e.srcElement || e.target + // If hasAudio is false, we've already marked this video to not have audio, + // a video can't gain audio out of nowhere so don't bother checking again. + if (!this.hasAudio) return if (typeof target.webkitAudioDecodedByteCount !== 'undefined') { // non-zero if video has audio track - if (target.webkitAudioDecodedByteCount > 0) { - this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly - } - } else if (typeof target.mozHasAudio !== 'undefined') { + if (target.webkitAudioDecodedByteCount > 0) return + } + if (typeof target.mozHasAudio !== 'undefined') { // true if video has audio track - if (target.mozHasAudio) { - this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly - } - } else if (typeof target.audioTracks !== 'undefined') { - if (target.audioTracks.length > 0) { - this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly - } + if (target.mozHasAudio) return + } + if (typeof target.audioTracks !== 'undefined') { + if (target.audioTracks.length > 0) return } + this.hasAudio = false } } } diff --git a/src/components/video_attachment/video_attachment.vue b/src/components/video_attachment/video_attachment.vue @@ -7,7 +7,8 @@ :alt="attachment.description" :title="attachment.description" playsinline - @loadeddata="onVideoDataLoad" + @playing="onPlaying" + @pause="onPaused" /> </template> diff --git a/src/hocs/with_load_more/with_load_more.js b/src/hocs/with_load_more/with_load_more.js @@ -3,6 +3,16 @@ import isEmpty from 'lodash/isEmpty' import { getComponentProps } from '../../services/component_utils/component_utils' import './with_load_more.scss' +import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faCircleNotch +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch +) + const withLoadMore = ({ fetch, // function to fetch entries and return a promise select, // function to select data from store @@ -82,7 +92,7 @@ const withLoadMore = ({ </WrappedComponent> <div class="with-load-more-footer"> {this.error && <a onClick={this.fetchEntries} class="alert error">{this.$t('general.generic_error')}</a>} - {!this.error && this.loading && <i class="icon-spin3 animate-spin"/>} + {!this.error && this.loading && <FAIcon spin icon="circle-notch"/>} {!this.error && !this.loading && !this.bottomedOut && <a onClick={this.fetchEntries}>{this.$t('general.more')}</a>} </div> </div> diff --git a/src/hocs/with_subscription/with_subscription.js b/src/hocs/with_subscription/with_subscription.js @@ -3,6 +3,16 @@ import isEmpty from 'lodash/isEmpty' import { getComponentProps } from '../../services/component_utils/component_utils' import './with_subscription.scss' +import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faCircleNotch +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch +) + const withSubscription = ({ fetch, // function to fetch entries and return a promise select, // function to select data from store @@ -72,7 +82,7 @@ const withSubscription = ({ <div class="with-subscription-loading"> {this.error ? <a onClick={this.fetchData} class="alert error">{this.$t('general.generic_error')}</a> - : <i class="icon-spin3 animate-spin"/> + : <FAIcon spin icon="circle-notch"/> } </div> ) diff --git a/src/i18n/ca.json b/src/i18n/ca.json @@ -9,7 +9,8 @@ "scope_options": "Opcions d'abast i visibilitat", "text_limit": "Límit de text", "title": "Funcionalitats", - "who_to_follow": "A qui seguir" + "who_to_follow": "A qui seguir", + "pleroma_chat_messages": "Xat de Pleroma" }, "finder": { "error_fetching_user": "No s'ha pogut carregar l'usuari/a", @@ -17,7 +18,21 @@ }, "general": { "apply": "Aplica", - "submit": "Desa" + "submit": "Desa", + "close": "Tanca", + "verify": "Verifica", + "confirm": "Confirma", + "enable": "Habilita", + "disable": "Deshabilitar", + "cancel": "Cancel·la", + "show_less": "Mostra menys", + "show_more": "Mostra més", + "optional": "opcional", + "retry": "Prova de nou", + "error_retry": "Si us plau, prova de nou", + "generic_error": "Hi ha hagut un error", + "loading": "Carregant…", + "more": "Més" }, "login": { "login": "Inicia sessió", @@ -25,7 +40,12 @@ "password": "Contrasenya", "placeholder": "p.ex.: Maria", "register": "Registra't", - "username": "Nom d'usuari/a" + "username": "Nom d'usuari/a", + "recovery_code": "Codi de recuperació", + "enter_recovery_code": "Posa un codi de recuperació", + "authentication_code": "Codi d'autenticació", + "hint": "Entra per participar a la conversa", + "description": "Entra amb OAuth" }, "nav": { "chat": "Xat local públic", @@ -33,7 +53,16 @@ "mentions": "Mencions", "public_tl": "Flux públic del node", "timeline": "Flux personal", - "twkn": "Flux de la xarxa coneguda" + "twkn": "Flux de la xarxa coneguda", + "chats": "Xats", + "timelines": "Línies de temps", + "preferences": "Preferències", + "who_to_follow": "A qui seguir", + "search": "Cerca", + "dms": "Missatges directes", + "interactions": "Interaccions", + "back": "Enrere", + "administration": "Administració" }, "notifications": { "broken_favorite": "No es coneix aquest estat. S'està cercant.", @@ -42,14 +71,19 @@ "load_older": "Carrega més notificacions", "notifications": "Notificacions", "read": "Read!", - "repeated_you": "ha repetit el teu estat" + "repeated_you": "ha repetit el teu estat", + "migrated_to": "migrat a", + "no_more_notifications": "No més notificacions", + "follow_request": "et vol seguir" }, "post_status": { "account_not_locked_warning": "El teu compte no està {0}. Qualsevol persona pot seguir-te per llegir les teves entrades reservades només a seguidores.", "account_not_locked_warning_link": "bloquejat", "attachments_sensitive": "Marca l'adjunt com a delicat", "content_type": { - "text/plain": "Text pla" + "text/plain": "Text pla", + "text/markdown": "Markdown", + "text/html": "HTML" }, "content_warning": "Assumpte (opcional)", "default": "Em sento…", @@ -60,7 +94,13 @@ "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" + }, + "preview_empty": "Buida", + "preview": "Vista prèvia" }, "registration": { "bio": "Presentació", @@ -68,7 +108,17 @@ "fullname": "Nom per mostrar", "password_confirm": "Confirma la contrasenya", "registration": "Registra't", - "token": "Codi d'invitació" + "token": "Codi d'invitació", + "validations": { + "password_confirmation_match": "hauria de ser la mateixa que la contrasenya", + "password_confirmation_required": "no es pot deixar en blanc", + "password_required": "no es pot deixar en blanc", + "email_required": "no es pot deixar en blanc", + "fullname_required": "no es pot deixar en blanc", + "username_required": "no es pot deixar en blanc" + }, + "fullname_placeholder": "p. ex. Lain Iwakura", + "username_placeholder": "p. ex. lain" }, "settings": { "attachmentRadius": "Adjunts", @@ -94,7 +144,7 @@ "data_import_export_tab": "Importa o exporta dades", "default_vis": "Abast per defecte de les entrades", "delete_account": "Esborra el compte", - "delete_account_description": "Esborra permanentment el teu compte i tots els missatges", + "delete_account_description": "Esborra permanentment les teves dades i desactiva el teu compte.", "delete_account_error": "No s'ha pogut esborrar el compte. Si continua el problema, contacta amb l'administració del node.", "delete_account_instructions": "Confirma que vols esborrar el compte escrivint la teva contrasenya aquí sota.", "export_theme": "Desa el tema", @@ -164,7 +214,57 @@ "values": { "false": "no", "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", + "hide_followers_description": "No mostris qui m'està seguint", + "hide_follows_description": "No mostris a qui segueixo", + "notification_visibility_emoji_reactions": "Reaccions", + "new_email": "Nou correu electrònic", + "profile_fields": { + "value": "Contingut", + "name": "Etiqueta", + "add_field": "Afegeix un camp", + "label": "Metadades del perfil" + }, + "mutes_tab": "Silenciaments", + "interface": "Interfície", + "instance_default_simple": "(per defecte)", + "checkboxRadius": "Caselles", + "import_blocks_from_a_csv_file": "Importa bloquejos des d'un arxiu csv", + "hide_post_stats": "Amaga les estadístiques de les entrades (p. ex. el nombre de favorits)", + "use_one_click_nsfw": "Obre els adjunts NSFW amb només un clic", + "hide_muted_posts": "Amaga les entrades de comptes silenciats", + "avatar_size_instruction": "La mida mínima recomanada per la imatge de l'avatar és de 150x150 píxels.", + "domain_mutes": "Dominis", + "discoverable": "Permet la descoberta d'aquest compte en resultats de cerques i altres serveis", + "mutes_and_blocks": "Silenciaments i bloquejos", + "composing": "Composant", + "chatMessageRadius": "Missatge de xat", + "changed_email": "Correu electrònic canviat amb èxit!", + "change_email_error": "Hi ha hagut un problema al canviar el teu correu electrònic.", + "change_email": "Canvia el correu electrònic", + "bot": "Aquest és un compte automatitzat", + "blocks_tab": "Bloquejos", + "blocks_imported": "Bloquejos importats! Processar-los pot trigar una mica.", + "block_import_error": "Error al importar bloquejos", + "block_import": "Importa bloquejos", + "block_export_button": "Exporta els teus bloquejos a un arxiu csv", + "block_export": "Exporta bloquejos", + "allow_following_move": "Permet el seguiment automàtic quan un compte a qui seguim es mou", + "mfa": { + "scan": { + "secret_code": "Clau" + }, + "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ó" + }, + "enter_current_password_to_confirm": "Posar la contrasenya actual per confirmar la teva identitat", + "security": "Seguretat", + "app_name": "Nom de l'aplicació" }, "time": { "day": "{0} dia", @@ -232,5 +332,75 @@ "who_to_follow": { "more": "More", "who_to_follow": "A qui seguir" + }, + "selectable_list": { + "select_all": "Selecciona-ho tot" + }, + "remote_user_resolver": { + "error": "No trobat.", + "searching_for": "Cercant per" + }, + "interactions": { + "load_older": "Carrega antigues interaccions", + "favs_repeats": "Repeticions i favorits" + }, + "emoji": { + "stickers": "Adhesius" + }, + "polls": { + "expired": "L'enquesta va acabar fa {0}", + "expires_in": "L'enquesta acaba en {0}", + "multiple_choices": "Múltiples opcions", + "single_choice": "Una sola opció", + "type": "Tipus d'enquesta", + "vote": "Vota", + "votes": "vots", + "option": "Opció", + "add_option": "Afegeix opció", + "add_poll": "Afegeix enquesta" + }, + "media_modal": { + "next": "Següent", + "previous": "Anterior" + }, + "importer": { + "error": "Ha succeït un error mentre s'importava aquest arxiu.", + "success": "Importat amb èxit." + }, + "image_cropper": { + "cancel": "Cancel·la", + "save_without_cropping": "Desa sense retallar", + "save": "Desa", + "crop_picture": "Retalla la imatge" + }, + "exporter": { + "processing": "Processant, aviat se't preguntarà per descarregar el teu arxiu", + "export": "Exporta" + }, + "domain_mute_card": { + "mute_progress": "Silenciant…", + "mute": "Silencia" + }, + "about": { + "staff": "Equip responsable", + "mrf": { + "simple": { + "quarantine_desc": "Aquesta instància només enviarà entrades públiques a les següents instàncies:", + "quarantine": "Quarantena", + "reject_desc": "Aquesta instància no acceptarà missatges de les següents instàncies:", + "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" + }, + "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" + }, + "federation": "Federació" + } } } diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -275,6 +275,12 @@ "block_import": "Block import", "block_import_error": "Error importing blocks", "blocks_imported": "Blocks imported! Processing them will take a while.", + "mute_export": "Mute export", + "mute_export_button": "Export your mutes to a csv file", + "mute_import": "Mute import", + "mute_import_error": "Error importing mutes", + "mutes_imported": "Mutes imported! Processing them will take a while.", + "import_mutes_from_a_csv_file": "Import mutes from a csv file", "blocks_tab": "Blocks", "bot": "This is a bot account", "btnRadius": "Buttons", @@ -429,6 +435,7 @@ "false": "no", "true": "yes" }, + "virtual_scrolling": "Optimize timeline rendering", "fun": "Fun", "greentext": "Meme arrows", "notifications": "Notifications", @@ -658,7 +665,8 @@ "show_full_subject": "Show full subject", "hide_full_subject": "Hide full subject", "show_content": "Show content", - "hide_content": "Hide content" + "hide_content": "Hide content", + "status_deleted": "This post was deleted" }, "user_card": { "approve": "Approve", diff --git a/src/i18n/eo.json b/src/i18n/eo.json @@ -5,7 +5,7 @@ "features_panel": { "chat": "Babilejo", "gopher": "Gopher", - "media_proxy": "Vidaŭdaĵa prokurilo", + "media_proxy": "Vidaŭdaĵa retperilo", "scope_options": "Agordoj de amplekso", "text_limit": "Limo de teksto", "title": "Funkcioj", @@ -33,7 +33,8 @@ "show_more": "Montri plion", "retry": "Reprovi", "error_retry": "Bonvolu reprovi", - "loading": "Enlegante…" + "loading": "Enlegante…", + "peek": "Antaŭmontri" }, "image_cropper": { "crop_picture": "Tondi bildon", @@ -70,9 +71,9 @@ "friend_requests": "Petoj pri abono", "mentions": "Mencioj", "dms": "Rektaj mesaĝoj", - "public_tl": "Publika tempolinio", - "timeline": "Tempolinio", - "twkn": "La tuta konata reto", + "public_tl": "Publika historio", + "timeline": "Historio", + "twkn": "Konata reto", "user_search": "Serĉi uzantojn", "who_to_follow": "Kiun aboni", "preferences": "Agordoj", @@ -80,7 +81,8 @@ "search": "Serĉi", "interactions": "Interagoj", "administration": "Administrado", - "bookmarks": "Legosignoj" + "bookmarks": "Legosignoj", + "timelines": "Historioj" }, "notifications": { "broken_favorite": "Nekonata stato, serĉante ĝin…", @@ -107,14 +109,14 @@ "text/html": "HTML" }, "content_warning": "Temo (malnepra)", - "default": "Ĵus alvenis al la Universala Kongreso!", + "default": "Ĵus alvenis Esperantujon!", "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 tempolinioj", - "unlisted": "Nelistigita – Ne afiŝi al publikaj tempolinioj" + "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", @@ -193,7 +195,7 @@ "foreground": "Malfono", "general": "Ĝenerala", "hide_attachments_in_convo": "Kaŝi kunsendaĵojn en interparoloj", - "hide_attachments_in_tl": "Kaŝi kunsendaĵojn en tempolinio", + "hide_attachments_in_tl": "Kaŝi kunsendaĵojn en historioj", "max_thumbnails": "Maksimuma nombro da bildetoj en afiŝo", "hide_isp": "Kaŝi breton propran al nodo", "preload_images": "Antaŭ-enlegi bildojn", @@ -246,7 +248,7 @@ "profile_banner": "Rubando de profilo", "profile_tab": "Profilo", "radii_help": "Agordi fasadan rondigon de randoj (bildere)", - "replies_in_timeline": "Respondoj en tempolinio", + "replies_in_timeline": "Respondoj en historioj", "reply_visibility_all": "Montri ĉiujn respondojn", "reply_visibility_following": "Montri nur respondojn por mi aŭ miaj abonatoj", "reply_visibility_self": "Montri nur respondojn por mi", @@ -297,7 +299,12 @@ "older_version_imported": "La enportita dosiero estis farita per pli malnova versio de PleromaFE.", "future_version_imported": "La enportita dosiero estis farita per pli nova versio de PleromaFE.", "v2_imported": "La dosiero, kiun vi enportis, estis farita por malnova versio de PleromaFE. Ni provas maksimumigi interkonformecon, sed tamen eble montriĝos misoj.", - "upgraded_from_v2": "PleromaFE estis ĝisdatigita; la haŭto eble aspektos malsame ol kiel vi ĝin memoras." + "upgraded_from_v2": "PleromaFE estis ĝisdatigita; la haŭto eble aspektos malsame ol kiel vi ĝin memoras.", + "snapshot_missing": "Neniu momentokopio de haŭto estis en la dosiero, ĝi povas aspekti iom malsame ol oni intencis.", + "snapshot_present": "Ĉiuj valoroj estas transpasataj, ĉar momentokopio de haŭto estas enlegita. Vi povas enlegi anstataŭe la aktualajn datumojn de haŭto.", + "snapshot_source_mismatch": "Versioj konfliktas: plej probable la fasado estis reirigita kaj ree ĝisdatigita; se vi ŝanĝis la haŭton per pli malnova versio de la fasado, vi probable volas uzi la malnovan version. Alie uzu la novan.", + "migration_napshot_gone": "Ial mankis momentokopio; io povus aspekti malsame ol en via memoro.", + "migration_snapshot_ok": "Certige, momentokopio de la haŭto enlegiĝis. Vi povas provi enlegi datumojn de la haŭto." }, "use_source": "Nova versio", "use_snapshot": "Malnova versio", @@ -352,10 +359,11 @@ "icons": "Bildsimboloj", "poll": "Grafo de enketo", "underlay": "Subtavolo", - "popover": "Ŝpruchelpiloj, menuoj", + "popover": "Ŝprucaĵoj, menuoj", "post": "Afiŝoj/Priskriboj de uzantoj", "alert_neutral": "Neŭtrala", - "alert_warning": "Averto" + "alert_warning": "Averto", + "toggled": "Ŝaltita" }, "radii": { "_tab_label": "Rondeco" @@ -388,7 +396,8 @@ "buttonPressed": "Butono (premita)", "buttonPressedHover": "Butono (premita kaj je ŝvebo)", "input": "Eniga kampo" - } + }, + "hintV3": "Kolorojn de ombroj vi ankaŭ povas skribi per la sistemo {0}." }, "fonts": { "_tab_label": "Tiparoj", @@ -411,7 +420,7 @@ "button": "Butono", "text": "Kelko da pliaj {0} kaj {1}", "mono": "enhavo", - "input": "Ĵus alvenis al la Universala Kongreso!", + "input": "Ĵus alvenis Esperantujon!", "faint_link": "helpan manlibron", "fine_print": "Legu nian {0} por nenion utilan ekscii!", "header_faint": "Tio estas en ordo", @@ -420,7 +429,7 @@ } }, "discoverable": "Permesi trovon de ĉi tiu konto en serĉrezultoj kaj aliaj servoj", - "mutes_and_blocks": "Silentigitoj kaj blokitoj", + "mutes_and_blocks": "Blokado kaj silentigoj", "chatMessageRadius": "Babileja mesaĝo", "changed_email": "Retpoŝtadreso sukcese ŝanĝiĝis!", "change_email_error": "Eraris ŝanĝo de via retpoŝtadreso.", @@ -448,7 +457,10 @@ "warning_of_generate_new_codes": "Kiam vi estigos novajn rehavajn kodojn, viaj malnovaj ne plu funkcios.", "generate_new_recovery_codes": "Estigi novajn rehavajn kodojn", "title": "Duobla aŭtentikigo", - "otp": "OTP" + "otp": "OTP", + "wait_pre_setup_otp": "antaŭagordante OTP", + "setup_otp": "Agordi OTP", + "confirm_and_enable": "Konfirmi kaj ŝalti OTP" }, "enter_current_password_to_confirm": "Enigu vian pasvorton por konfirmi vian identecon", "security": "Sekureco", @@ -480,11 +492,11 @@ }, "import_blocks_from_a_csv_file": "Enporti blokitojn el CSV-dosiero", "hide_muted_posts": "Kaŝi afiŝojn de silentigitaj uzantoj", - "emoji_reactions_on_timeline": "Montri bildosignajn reagojn en la tempolinio", + "emoji_reactions_on_timeline": "Montri bildosignajn reagojn en historioj", "pad_emoji": "Meti spacetojn ĉirkaŭ bildosigno post ties elekto", "domain_mutes": "Retnomoj", "notification_blocks": "Blokinte uzanton vi malabonos ĝin kaj haltigos ĉiujn sciigojn.", - "notification_mutes": "Por ne plu ricevi sciigojn de certa uzanto, silentigu.", + "notification_mutes": "Por ne plu ricevi sciigojn de certa uzanto, silentigu ĝin.", "notification_setting_hide_notification_contents": "Kaŝi la sendinton kaj la enhavojn de pasivaj sciigoj", "notification_setting_privacy": "Privateco", "notification_setting_block_from_strangers": "Bloki sciigojn de uzantoj, kiujn vi ne abonas", @@ -495,7 +507,14 @@ "backend_version": "Versio de internaĵo", "title": "Versio" }, - "accent": "Emfazo" + "accent": "Emfazo", + "virtual_scrolling": "Optimumigi bildigon de historioj", + "import_mutes_from_a_csv_file": "Enporti silentigojn el CSV-dosiero", + "mutes_imported": "Silentigoj enportiĝis! Traktado daŭros iom da tempo.", + "mute_import_error": "Eraris enporto de silentigoj", + "mute_import": "Enporto de silentigoj", + "mute_export_button": "Elportu viajn silentigojn al CSV-dosiero", + "mute_export": "Elporto de silentigoj" }, "timeline": { "collapse": "Maletendi", @@ -503,7 +522,7 @@ "error_fetching": "Eraris ĝisdatigo", "load_older": "Montri pli malnovajn statojn", "no_retweet_hint": "Afiŝo estas markita kiel rekta aŭ nur por abonantoj, kaj ne eblas ĝin ripeti", - "repeated": "ripetita", + "repeated": "ripetis", "show_new": "Montri novajn", "up_to_date": "Ĝisdata", "no_more_statuses": "Neniuj pliaj statoj", @@ -648,21 +667,22 @@ "media_nsfw": "Devige marki vidaŭdaĵojn konsternaj", "media_removal_desc": "Ĉi tiu nodo forigas vidaŭdaĵojn de afiŝoj el la jenaj nodoj:", "media_removal": "Forigo de vidaŭdaĵoj", - "ftl_removal": "Forigo de la historio de «La tuta konata reto»", + "ftl_removal": "Forigo el la historio de «La tuta konata reto»", "quarantine_desc": "Ĉi tiu nodo sendos nur publikajn afiŝojn al la jenaj nodoj:", "quarantine": "Kvaranteno", "reject_desc": "Ĉi tiu nodo ne akceptos mesaĝojn de la jenaj nodoj:", "reject": "Rifuzi", "accept_desc": "Ĉi tiu nodo nur akceptas mesaĝojn de la jenaj nodoj:", "accept": "Akcepti", - "simple_policies": "Specialaj politikoj de la nodo" + "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»:" }, "mrf_policies": "Ŝaltis politikon de Mesaĝa ŝanĝilaro (MRF)", "keyword": { "is_replaced_by": "→", "replace": "Anstataŭigi", "reject": "Rifuzi", - "ftl_removal": "Forigo de la historio de «La tuta konata reto»", + "ftl_removal": "Forigo el la historio de «La tuta konata reto»", "keyword_policies": "Politiko pri ŝlosilvortoj" }, "federation": "Federado", @@ -707,7 +727,8 @@ "pin": "Fiksi al profilo", "delete": "Forigi staton", "repeats": "Ripetoj", - "favorites": "Ŝatataj" + "favorites": "Ŝatoj", + "status_deleted": "Ĉi tiu afiŝo foriĝis" }, "time": { "years_short": "{0}j", @@ -769,7 +790,8 @@ "new": "Nova babilo", "chats": "Babiloj", "delete": "Forigi", - "you": "Vi:" + "you": "Vi:", + "message_user": "Mesaĝi al {nickname}" }, "password_reset": { "password_reset_required_but_mailer_is_disabled": "Vi devas restarigi vian pasvorton, sed restarigado de pasvortoj estas malŝaltita. Bonvolu kontakti la administranton de via nodo.", @@ -791,5 +813,8 @@ "additional_comments": "Aldonaj komentoj", "add_comment_description": "Ĉi tiu raporto sendiĝos al reguligistoj de via nodo. Vi povas komprenigi kial vi raportas ĉi tiun konton sube:", "title": "Raportante {0}" + }, + "shoutbox": { + "title": "Kriujo" } } diff --git a/src/i18n/es.json b/src/i18n/es.json @@ -551,7 +551,14 @@ "change_email_error": "Ha ocurrido un error al intentar modificar tu correo electrónico.", "change_email": "Modificar el correo electrónico", "bot": "Esta cuenta es un bot", - "allow_following_move": "Permitir el seguimiento automático, cuando la cuenta que sigues se traslada a otra instancia" + "allow_following_move": "Permitir el seguimiento automático, cuando la cuenta que sigues se traslada a otra instancia", + "virtual_scrolling": "Optimizar la representación de la linea temporal", + "import_mutes_from_a_csv_file": "Importar silenciados desde un archivo csv", + "mutes_imported": "¡Silenciados importados! Procesarlos llevará un tiempo.", + "mute_import_error": "Error al importar los silenciados", + "mute_import": "Importar silenciados", + "mute_export_button": "Exportar los silenciados a un archivo csv", + "mute_export": "Exportar silenciados" }, "time": { "day": "{0} día", @@ -762,7 +769,7 @@ "ftl_removal_desc": "Esta instancia elimina las siguientes instancias de la línea de tiempo \"Toda la red conocida\":", "ftl_removal": "Eliminar de la línea de tiempo \"Toda La Red Conocida\"", "quarantine_desc": "Esta instancia enviará solo publicaciones públicas a las siguientes instancias:", - "simple_policies": "Políticas sobre instancias específicas", + "simple_policies": "Políticas específicas de la instancia", "reject_desc": "Esta instancia no aceptará mensajes de las siguientes instancias:", "reject": "Rechazar", "accept": "Aceptar" diff --git a/src/i18n/eu.json b/src/i18n/eu.json @@ -13,7 +13,8 @@ "scope_options": "Ikusgaitasun aukerak", "text_limit": "Testu limitea", "title": "Ezaugarriak", - "who_to_follow": "Nori jarraitu" + "who_to_follow": "Nori jarraitu", + "pleroma_chat_messages": "Pleroma Txata" }, "finder": { "error_fetching_user": "Errorea erabiltzailea eskuratzen", @@ -31,7 +32,13 @@ "disable": "Ezgaitu", "enable": "Gaitu", "confirm": "Baieztatu", - "verify": "Egiaztatu" + "verify": "Egiaztatu", + "peek": "Begiratu", + "close": "Itxi", + "dismiss": "Baztertu", + "retry": "Saiatu berriro", + "error_retry": "Saiatu berriro mesedez", + "loading": "Kargatzen…" }, "image_cropper": { "crop_picture": "Moztu argazkia", @@ -81,7 +88,10 @@ "user_search": "Erabiltzailea Bilatu", "search": "Bilatu", "who_to_follow": "Nori jarraitu", - "preferences": "Hobespenak" + "preferences": "Hobespenak", + "chats": "Txatak", + "timelines": "Denbora-lerroak", + "bookmarks": "Laster-markak" }, "notifications": { "broken_favorite": "Egoera ezezaguna, bilatzen…", @@ -91,7 +101,10 @@ "notifications": "Jakinarazpenak", "read": "Irakurrita!", "repeated_you": "zure mezua errepikatu du", - "no_more_notifications": "Ez dago jakinarazpen gehiago" + "no_more_notifications": "Ez dago jakinarazpen gehiago", + "reacted_with": "{0}kin erreakzionatu zuen", + "migrated_to": "hona migratua:", + "follow_request": "jarraitu nahi zaitu" }, "polls": { "add_poll": "Inkesta gehitu", @@ -114,7 +127,8 @@ "search_emoji": "Bilatu emoji bat", "add_emoji": "Emoji bat gehitu", "custom": "Ohiko emojiak", - "unicode": "Unicode emojiak" + "unicode": "Unicode emojiak", + "load_all": "{emojiAmount} emoji guztiak kargatzen" }, "stickers": { "add_sticker": "Pegatina gehitu" @@ -226,7 +240,7 @@ "composing": "Idazten", "confirm_new_password": "Baieztatu pasahitz berria", "current_avatar": "Zure uneko avatarra", - "current_password": "Indarrean den pasahitza", + "current_password": "Indarrean dagoen pasahitza", "current_profile_banner": "Zure profilaren banner-a", "data_import_export_tab": "Datuak Inportatu / Esportatu", "default_vis": "Lehenetsitako ikusgaitasunak", @@ -634,9 +648,40 @@ "about": { "mrf": { "keyword": { - "keyword_policies": "Gako-hitz politika" + "keyword_policies": "Gako-hitz politika", + "ftl_removal": "\"Ezagutzen den Sarea\" denbora-lerrotik ezabatu", + "is_replaced_by": "→", + "replace": "Ordezkatuak", + "reject": "Ukatuak" }, - "federation": "Federazioa" - } + "federation": "Federazioa", + "simple": { + "media_nsfw_desc": "Instantzia honek hurrengo instantzien multimediak sentikorrak izatera behartzen ditu:", + "media_nsfw": "Behartu Multimedia Sentikor", + "media_removal_desc": "Instantzia honek atxikitutako multimedia hurrengo instantzietatik ezabatzen ditu:", + "media_removal": "Multimedia Ezabatu", + "ftl_removal_desc": "Instantzia honek hurrengo instantziak ezabatzen ditu \"Ezagutzen den Sarea\" denbora-lerrotik:", + "ftl_removal": "\"Ezagutzen den Sarea\" denbora-lerrotik ezabatu", + "quarantine_desc": "Instantzia honek soilik mezu publikoak bidaliko ditu instantzia hauetara:", + "quarantine": "Koarentena", + "reject_desc": "Instantzia honek ez ditu hurrengo instantzien mezuak onartuko:", + "reject": "Ukatuak", + "accept_desc": "Instantzia honek hurrengo instantzietako mezuak soilik onartzen ditu:", + "accept": "Onartu", + "simple_policies": "Gure instantziaren politika zehatzak" + }, + "mrf_policies_desc": "MRF politikek instantzia honen federazioa manipulatzen dute gainerako instantziekin. Honako politika hauek daude gaituta:", + "mrf_policies": "Gaitutako MRF politikak" + }, + "staff": "Arduradunak" + }, + "domain_mute_card": { + "unmute_progress": "Isiltasuna kentzen…", + "unmute": "Isiltasuna kendu", + "mute_progress": "Isiltzen…", + "mute": "Isilarazi" + }, + "shoutbox": { + "title": "Oihu-kutxa" } } diff --git a/src/i18n/fa.json b/src/i18n/fa.json @@ -0,0 +1,155 @@ +{ + "about": { + "mrf": { + "simple": { + "media_removal_desc": "این نمونه رسانه‌ی پیغام‌های نمونه‌های ذکر شده را حذف می‌کند:", + "ftl_removal_desc": "این نمونه،‌ نمونه‌های ذکر شده را از تایم‌لاین «تمام شبکه‌ شناخته شده» حذف می‌کند:", + "media_removal": "حذف رسانه", + "ftl_removal": "حذف از تایم‌لاین «تمام شبکه شناخته شده»", + "quarantine_desc": "این نمونه تنها پیغام‌های عمومی را به نمونه‌های ذکر شده پیغام ارسال می‌کند:", + "quarantine": "قرنطینه شده", + "reject_desc": "این نمونه از نمونه‌های ذکر شده پیغامی دریافت نمی‌کند:", + "reject": "رد کننده", + "accept_desc": "این نمونه تنها از نمونه‌های ذکر شده پیغام دریافت می‌کند:", + "simple_policies": "سیاست‌های مخصوص نمونه", + "accept": "دریافت کننده", + "media_nsfw_desc": "این نمونه،‌ رسانه نمونه‌های ذکر شده را به اجبار حساس می‌کند:", + "media_nsfw": "به اجبار حساس کردن رسانه" + }, + "federation": "فدراسیون", + "mrf_policies_desc": "سیاست‌های MRF رفتار فدراسیون این نمونه را تغییر می‌دهد. سیاست‌هایی که در ادامه آمده اعمال شده است:", + "keyword": { + "reject": "رد کننده", + "replace": "جایگزین کننده", + "keyword_policies": "سیاست‌های واژگان کلیدی", + "is_replaced_by": "→", + "ftl_removal": "حذف از تایم‌لاین «تمام شبکه شناخته شده»" + }, + "mrf_policies": "سیاست‌های MRF(وسیله بازنویسی پیغام) فعال شده" + }, + "staff": "کارکنان" + }, + "image_cropper": { + "crop_picture": "برش تصویر", + "cancel": "لغو", + "save_without_cropping": "ذخیره بدون برش", + "save": "ذخیره" + }, + "notifications": { + "followed_you": "پیگیر شما شد", + "favorited_you": "پیغام شما را پسندید", + "broken_favorite": "پیغام ناشناخته، در حال جستجو…" + }, + "nav": { + "chats": "گپ‌ها", + "timelines": "تایم‌لاین‌ها", + "preferences": "ترجیحات", + "who_to_follow": "چه کسانی را پیگیری کنیم", + "search": "جستجو", + "user_search": "جستجوی کاربر", + "bookmarks": "نشانک‌ها", + "twkn": "شبکه شناخته شده", + "timeline": "تایم‌لاین", + "public_tl": "تایم‌لاین عمومی", + "dms": "پیغام‌های مستقیم", + "interactions": "تعاملات", + "mentions": "نام بردن‌ها", + "friend_requests": "درخواست پیگیری", + "back": "قبلی", + "administration": "مدیریت", + "about": "درباره" + }, + "features_panel": { + "who_to_follow": "چه کسانی را پیگیری کنیم", + "title": "ویژگی‌ها", + "text_limit": "محدودیت متن", + "scope_options": "تنظیمات حوزه", + "media_proxy": "پروکسی رسانه", + "gopher": "گوفر", + "pleroma_chat_messages": "گپ پلروما", + "chat": "گپ" + }, + "media_modal": { + "next": "بعدی", + "previous": "قبلی" + }, + "login": { + "heading": { + "recovery": "بازیابی دو مرحله‌ای", + "totp": "احراز هویت دو مرحله‌ای" + }, + "enter_two_factor_code": "کد احراز هویت دو مرحله‌ای را وارد کنید", + "recovery_code": "کد بازیابی", + "enter_recovery_code": "کد بازیابی را وارد کنید", + "authentication_code": "کد احراز هویت", + "hint": "برای شرکت در گفتگو، وارد سامانه شوید", + "username": "نام کاربری", + "register": "ثبت نام", + "description": "ورود به سامانه از طریق OAuth", + "placeholder": "به عنوان مثال: lain", + "password": "رمز عبور", + "logout": "خروج از سامانه", + "login": "ورود به سامانه" + }, + "importer": { + "error": "در حین بارگذاری فایل خطایی رخ داد.", + "success": "با موفقیت بارگذاری شد.", + "submit": "ارسال" + }, + "general": { + "peek": "نگاه سریع", + "close": "بستن", + "verify": "تأیید", + "confirm": "تأیید", + "enable": "فعال", + "disable": "غیر فعال", + "cancel": "لغو", + "show_less": "کمتر نشان بده", + "show_more": "بیشتر نشان بده", + "optional": "اختیاری", + "retry": "دوباره امتحان کنید", + "error_retry": "لطفاً دوباره امتحان کنید", + "generic_error": "خطایی رخ داد", + "loading": "در حال بارگذاری…", + "more": "بیشتر", + "submit": "ارسال", + "apply": "اعمال" + }, + "finder": { + "find_user": "جستجوی ‌ کاربر", + "error_fetching_user": "دریافت کاربر با خطا مواجه شد" + }, + "exporter": { + "processing": "در حال پردازش، شما به زودی قادر به دانلود فایل خواهید بود", + "export": "صادر کردن" + }, + "domain_mute_card": { + "unmute": "صدا دار", + "unmute_progress": "در حال صدا دار کردن …", + "mute_progress": "در حال بی صدا کردن…", + "mute": "بی صدا" + }, + "shoutbox": { + "title": "چت باکس" + }, + "display_date": { + "today": "امروز" + }, + "file_type": { + "file": "فایل", + "image": "تصویر", + "video": "ویدئو", + "audio": "صدا" + }, + "chats": { + "empty_chat_list_placeholder": "شما هنوز هیچ گپی ندارید، گپ جدیدی را آغاز کنید!", + "delete": "حذف", + "error_sending_message": "در حین ارسال پیغام خطایی رخ داد.", + "error_loading_chat": "در هنگام بارگذاری گپ خطایی رخ داد.", + "delete_confirm": "آیا از حذف این پیغام اطمینان دارید؟", + "more": "بیشتر", + "empty_message_error": "نمی‌توان پیغام خالی فرستاد", + "new": "گپ جدید", + "chats": "گپ‌ها" + } +} diff --git a/src/i18n/fi.json b/src/i18n/fi.json @@ -578,7 +578,8 @@ "show_full_subject": "Näytä koko otsikko", "hide_full_subject": "Piilota koko otsikko", "show_content": "Näytä sisältö", - "hide_content": "Piilota sisältö" + "hide_content": "Piilota sisältö", + "status_deleted": "Poistettu viesti" }, "user_card": { "approve": "Hyväksy", diff --git a/src/i18n/fr.json b/src/i18n/fr.json @@ -13,7 +13,8 @@ "scope_options": "Options de visibilité", "text_limit": "Limite de texte", "title": "Caractéristiques", - "who_to_follow": "Personnes à suivre" + "who_to_follow": "Personnes à suivre", + "pleroma_chat_messages": "Chat Pleroma" }, "finder": { "error_fetching_user": "Erreur lors de la recherche de l'utilisateur·ice", @@ -32,7 +33,12 @@ "enable": "Activer", "confirm": "Confirmer", "verify": "Vérifier", - "dismiss": "Rejeter" + "dismiss": "Rejeter", + "peek": "Jeter un coup d'œil", + "close": "Fermer", + "retry": "Réessayez", + "error_retry": "Veuillez réessayer", + "loading": "Chargement…" }, "image_cropper": { "crop_picture": "Rogner l'image", @@ -77,15 +83,17 @@ "dms": "Messages directs", "public_tl": "Fil d'actualité public", "timeline": "Fil d'actualité", - "twkn": "Ensemble du réseau connu", + "twkn": "Réseau connu", "user_search": "Recherche d'utilisateur·ice", "who_to_follow": "Qui suivre", "preferences": "Préférences", "search": "Recherche", - "administration": "Administration" + "administration": "Administration", + "chats": "Chats", + "bookmarks": "Marques-Pages" }, "notifications": { - "broken_favorite": "Chargement d'un message inconnu…", + "broken_favorite": "Message inconnu, chargement…", "favorited_you": "a aimé votre statut", "followed_you": "a commencé à vous suivre", "load_older": "Charger les notifications précédentes", @@ -115,7 +123,7 @@ "text/bbcode": "BBCode" }, "content_warning": "Sujet (optionnel)", - "default": "Écrivez ici votre prochain statut.", + "default": "Je viens d'atterrir en Tchéquie.", "direct_warning_to_all": "Ce message sera visible pour toutes les personnes mentionnées.", "direct_warning_to_first_only": "Ce message sera visible uniquement pour personnes mentionnées au début du message.", "posting": "Envoi en cours", @@ -129,7 +137,12 @@ "private": "Abonné·e·s uniquement - Seul·e·s vos abonné·e·s verront vos billets", "public": "Publique - Afficher dans les fils publics", "unlisted": "Non-Listé - Ne pas afficher dans les fils publics" - } + }, + "media_description_error": "Échec de téléversement du media, essayez encore", + "empty_status_error": "Impossible de poster un statut vide sans attachements", + "preview_empty": "Vide", + "preview": "Prévisualisation", + "media_description": "Description de l'attachement" }, "registration": { "bio": "Biographie", @@ -488,7 +501,15 @@ "notification_setting_privacy_option": "Masquer l'expéditeur et le contenu des notifications push", "notification_setting_privacy": "Intimité", "hide_followers_count_description": "Masquer le nombre d'abonnés", - "accent": "Accent" + "accent": "Accent", + "chatMessageRadius": "Message de chat", + "bot": "Ce compte est un robot", + "import_mutes_from_a_csv_file": "Importer les masquages depuis un fichier CSV", + "mutes_imported": "Masquages importés ! Leur application peut prendre du temps.", + "mute_import_error": "Erreur à l'import des masquages", + "mute_import": "Import des masquages", + "mute_export_button": "Exporter vos masquages dans un fichier CSV", + "mute_export": "Export des masquages" }, "timeline": { "collapse": "Fermer", @@ -732,5 +753,11 @@ "return_home": "Retourner à la page d'accueil", "too_many_requests": "Vos avez atteint la limite d'essais, essayez plus tard.", "password_reset_required": "Vous devez changer votre mot de passe pour vous authentifier." + }, + "errors": { + "storage_unavailable": "Pleroma n'a pas pu accéder au stockage du navigateur. Votre identifiant ou vos mots de passes ne seront sauvegardés et des problèmes pourront être rencontrés. Essayez d'activer les cookies." + }, + "shoutbox": { + "title": "Shoutbox" } } diff --git a/src/i18n/it.json b/src/i18n/it.json @@ -407,7 +407,14 @@ "reset_background_confirm": "Vuoi veramente azzerare lo sfondo?", "chatMessageRadius": "Messaggi istantanei", "notification_setting_hide_notification_contents": "Nascondi mittente e contenuti delle notifiche push", - "notification_setting_block_from_strangers": "Blocca notifiche da utenti che non segui" + "notification_setting_block_from_strangers": "Blocca notifiche da utenti che non segui", + "virtual_scrolling": "Velocizza l'elaborazione delle sequenze", + "import_mutes_from_a_csv_file": "Importa silenziati da un file CSV", + "mutes_imported": "Silenziati importati! Saranno elaborati a breve.", + "mute_import_error": "Errore nell'importazione", + "mute_import": "Importa silenziati", + "mute_export_button": "Esporta la tua lista di silenziati in un file CSV", + "mute_export": "Esporta silenziati" }, "timeline": { "error_fetching": "Errore nell'aggiornamento", @@ -591,12 +598,12 @@ "reject": "Rifiuta", "accept": "Accetta", "simple_policies": "Regole specifiche alla stanza", - "accept_desc": "Questa stanza accetta messaggi solo dalle seguenti stanze:", - "reject_desc": "Questa stanza non accetterà messaggi dalle stanze seguenti:", + "accept_desc": "Questa stanza accetta messaggi solo dalle seguenti altre:", + "reject_desc": "Questa stanza rifiuterà i messaggi provenienti dalle seguenti:", "quarantine": "Quarantena", - "quarantine_desc": "Questa stanza inoltrerà solo messaggi pubblici alle seguenti stanze:", + "quarantine_desc": "Questa stanza inoltrerà solo messaggi pubblici alle seguenti:", "ftl_removal": "Rimozione dalla sequenza globale", - "ftl_removal_desc": "Questa stanza rimuove le seguenti stanze dalla sequenza globale:", + "ftl_removal_desc": "Questa stanza rimuove le seguenti dalla sequenza globale:", "media_removal": "Rimozione multimedia", "media_removal_desc": "Questa istanza rimuove gli allegati dalle seguenti stanze:", "media_nsfw": "Allegati oscurati forzatamente", @@ -695,7 +702,8 @@ "reply_to": "Rispondi a", "delete_confirm": "Vuoi veramente eliminare questo messaggio?", "unbookmark": "Rimuovi segnalibro", - "bookmark": "Aggiungi segnalibro" + "bookmark": "Aggiungi segnalibro", + "status_deleted": "Questo messagio è stato cancellato" }, "time": { "years_short": "{0}a", diff --git a/src/i18n/pl.json b/src/i18n/pl.json @@ -49,7 +49,8 @@ "scope_options": "Ustawienia zakresu", "text_limit": "Limit tekstu", "title": "Funkcje", - "who_to_follow": "Propozycje obserwacji" + "who_to_follow": "Propozycje obserwacji", + "pleroma_chat_messages": "Czat Pleromy" }, "finder": { "error_fetching_user": "Błąd przy pobieraniu profilu", @@ -71,7 +72,9 @@ "verify": "Zweryfikuj", "close": "Zamknij", "loading": "Ładowanie…", - "retry": "Spróbuj ponownie" + "retry": "Spróbuj ponownie", + "peek": "Spójrz", + "error_retry": "Spróbuj ponownie" }, "image_cropper": { "crop_picture": "Przytnij obrazek", @@ -117,12 +120,14 @@ "dms": "Wiadomości prywatne", "public_tl": "Publiczna oś czasu", "timeline": "Oś czasu", - "twkn": "Cała znana sieć", + "twkn": "Znana sieć", "user_search": "Wyszukiwanie użytkowników", "search": "Wyszukiwanie", "who_to_follow": "Sugestie obserwacji", "preferences": "Preferencje", - "bookmarks": "Zakładki" + "bookmarks": "Zakładki", + "chats": "Czaty", + "timelines": "Osie czasu" }, "notifications": { "broken_favorite": "Nieznany status, szukam go…", @@ -197,7 +202,9 @@ }, "preview_empty": "Pusty", "preview": "Podgląd", - "empty_status_error": "Nie można wysłać pustego wpisu bez plików" + "empty_status_error": "Nie można wysłać pustego wpisu bez plików", + "media_description_error": "Nie udało się zaktualizować mediów, spróbuj ponownie", + "media_description": "Opis mediów" }, "registration": { "bio": "Bio", @@ -400,7 +407,7 @@ "theme_help_v2_1": "Możesz też zastąpić kolory i widoczność poszczególnych komponentów przełączając pola wyboru, użyj „Wyczyść wszystko” aby usunąć wszystkie zastąpienia.", "theme_help_v2_2": "Ikony pod niektórych wpisami są wskaźnikami kontrastu pomiędzy tłem a tekstem, po najechaniu na nie otrzymasz szczegółowe informacje. Zapamiętaj, że jeżeli używasz przezroczystości, wskaźniki pokazują najgorszy możliwy przypadek.", "tooltipRadius": "Etykiety/alerty", - "type_domains_to_mute": "Wpisz domeny, które chcesz wyciszyć", + "type_domains_to_mute": "Wyszukaj domeny, które chcesz wyciszyć", "upload_a_photo": "Wyślij zdjęcie", "user_settings": "Ustawienia użytkownika", "values": { @@ -492,7 +499,8 @@ "tabs": "Karty", "chat": { "outgoing": "Wiadomości wychodzące", - "incoming": "Wiadomości przychodzące" + "incoming": "Wiadomości przychodzące", + "border": "Granica" } }, "radii": { @@ -573,7 +581,22 @@ "add_field": "Dodaj pole" }, "bot": "To konto jest prowadzone przez bota", - "notification_setting_hide_notification_contents": "Ukryj nadawcę i zawartość powiadomień push" + "notification_setting_hide_notification_contents": "Ukryj nadawcę i zawartość powiadomień push", + "notification_setting_block_from_strangers": "Zablokuj powiadomienia od użytkowników których nie obserwujesz", + "virtual_scrolling": "Optymalizuj renderowanie osi czasu", + "reset_background_confirm": "Czy naprawdę chcesz zresetować tło?", + "reset_banner_confirm": "Czy naprawdę chcesz zresetować banner?", + "reset_avatar_confirm": "Czy naprawdę chcesz zresetować awatar?", + "reset_profile_banner": "Zresetuj banner profilowy", + "reset_profile_background": "Zresetuj tło profilowe", + "mutes_and_blocks": "Wyciszenia i blokady", + "chatMessageRadius": "Wiadomość czatu", + "import_mutes_from_a_csv_file": "Zaimportuj wyciszenia z pliku .csv", + "mutes_imported": "Zaimportowano wyciszenia! Przetwarzanie zajmie chwilę.", + "mute_import_error": "Wystąpił błąd podczas importowania wyciszeń", + "mute_import": "Import wyciszeń", + "mute_export_button": "Wyeksportuj swoje wyciszenia do pliku .csv", + "mute_export": "Eksport wyciszeń" }, "time": { "day": "{0} dzień", @@ -639,7 +662,12 @@ "unbookmark": "Usuń z zakładek", "bookmark": "Dodaj do zakładek", "hide_content": "Ukryj zawartość", - "show_content": "Pokaż zawartość" + "show_content": "Pokaż zawartość", + "hide_full_subject": "Ukryj cały temat", + "show_full_subject": "Pokaż cały temat", + "thread_muted_and_words": ", ma słowa:", + "thread_muted": "Wątek wyciszony", + "status_deleted": "Ten wpis został usunięty" }, "user_card": { "approve": "Przyjmij", @@ -723,7 +751,8 @@ "add_reaction": "Dodaj reakcję", "user_settings": "Ustawienia użytkownika", "accept_follow_request": "Akceptuj prośbę o możliwość obserwacji", - "reject_follow_request": "Odrzuć prośbę o możliwość obserwacji" + "reject_follow_request": "Odrzuć prośbę o możliwość obserwacji", + "bookmark": "Zakładka" }, "upload": { "error": { @@ -773,9 +802,17 @@ "error_sending_message": "Coś poszło nie tak podczas wysyłania wiadomości.", "error_loading_chat": "Coś poszło nie tak podczas ładowania czatu.", "empty_message_error": "Nie można wysłać pustej wiadomości", - "new": "Nowy czat" + "new": "Nowy czat", + "empty_chat_list_placeholder": "Nie masz jeszcze żadnych czatów. Zacznij nowy czat!", + "chats": "Czaty" }, "display_date": { "today": "Dzisiaj" + }, + "shoutbox": { + "title": "Shoutbox" + }, + "errors": { + "storage_unavailable": "Pleroma nie mogła uzyskać dostępu do pamięci masowej przeglądarki. Twój login lub lokalne ustawienia nie zostaną zapisane i możesz napotkać problemy. Spróbuj włączyć ciasteczka." } } diff --git a/src/i18n/ru.json b/src/i18n/ru.json @@ -473,5 +473,10 @@ "tool_tip": { "accept_follow_request": "Принять запрос на чтение", "reject_follow_request": "Отклонить запрос на чтение" + }, + "image_cropper": { + "save_without_cropping": "Сохранить не обрезая", + "save": "Сохранить", + "crop_picture": "Обрезать картинку" } } diff --git a/src/i18n/zh.json b/src/i18n/zh.json @@ -11,9 +11,10 @@ "gopher": "Gopher", "media_proxy": "媒体代理", "scope_options": "可见范围设置", - "text_limit": "文本长度限制", + "text_limit": "文字数量限制", "title": "功能", - "who_to_follow": "推荐关注" + "who_to_follow": "推荐关注", + "pleroma_chat_messages": "Pleroma 聊天" }, "finder": { "error_fetching_user": "获取用户时发生错误", @@ -23,8 +24,8 @@ "apply": "应用", "submit": "提交", "more": "更多", - "generic_error": "发生一个错误", - "optional": "可选项", + "generic_error": "发生了一个错误", + "optional": "可选", "show_more": "展开", "show_less": "收起", "cancel": "取消", @@ -32,7 +33,12 @@ "enable": "启用", "confirm": "确认", "verify": "验证", - "dismiss": "忽略" + "dismiss": "忽略", + "peek": "窥探", + "close": "关闭", + "retry": "重试", + "error_retry": "请重试", + "loading": "载入中…" }, "image_cropper": { "crop_picture": "裁剪图片", @@ -77,37 +83,40 @@ "dms": "私信", "public_tl": "公共时间线", "timeline": "时间线", - "twkn": "所有已知网络", + "twkn": "已知网络", "user_search": "用户搜索", "search": "搜索", "who_to_follow": "推荐关注", "preferences": "偏好设置", - "administration": "管理员" + "administration": "管理员", + "chats": "聊天", + "timelines": "时间线", + "bookmarks": "书签" }, "notifications": { "broken_favorite": "未知的状态,正在搜索中…", - "favorited_you": "收藏了你的状态", + "favorited_you": "喜欢了你的状态", "followed_you": "关注了你", "load_older": "加载更早的通知", "notifications": "通知", - "read": "阅读!", + "read": "已阅!", "repeated_you": "转发了你的状态", "no_more_notifications": "没有更多的通知", - "reacted_with": "和 {0} 互动过", - "migrated_to": "迁移到", + "reacted_with": "作出了 {0} 的反应", + "migrated_to": "迁移到了", "follow_request": "想要关注你" }, "polls": { - "add_poll": "增加问卷调查", + "add_poll": "增加投票", "add_option": "增加选项", "option": "选项", "votes": "投票", "vote": "投票", - "type": "问卷类型", - "single_choice": "单选项", - "multiple_choices": "多选项", - "expiry": "问卷的时间", - "expires_in": "投票于 {0} 内结束", + "type": "投票类型", + "single_choice": "单选", + "multiple_choices": "多选", + "expiry": "投票期限", + "expires_in": "投票于 {0} 后结束", "expired": "投票 {0} 前已结束", "not_enough_options": "投票的选项太少" }, @@ -132,7 +141,7 @@ "text/bbcode": "BBCode" }, "content_warning": "主题(可选)", - "default": "刚刚抵达上海", + "default": "刚刚抵达洛杉矶", "direct_warning_to_all": "本条内容只有被提及的用户能够看到。", "direct_warning_to_first_only": "本条内容只有被在消息开始处提及的用户能够看到。", "posting": "发送", @@ -146,12 +155,17 @@ "private": "仅关注者 - 只有关注了你的人能看到", "public": "公共 - 发送到公共时间轴", "unlisted": "不公开 - 不会发送到公共时间轴" - } + }, + "preview_empty": "空的", + "preview": "预览", + "media_description": "媒体描述", + "media_description_error": "更新媒体失败,请重试", + "empty_status_error": "不能发布没有内容、没有附件的发文" }, "registration": { "bio": "简介", "email": "电子邮箱", - "fullname": "全名", + "fullname": "显示名称", "password_confirm": "确认密码", "registration": "注册", "token": "邀请码", @@ -175,7 +189,7 @@ "settings": { "app_name": "App 名称", "security": "安全", - "enter_current_password_to_confirm": "输入你当前密码来确认你的身份", + "enter_current_password_to_confirm": "输入您当前的密码来确认您的身份", "mfa": { "otp": "OTP", "setup_otp": "设置 OTP", @@ -183,18 +197,18 @@ "confirm_and_enable": "确认并启用 OTP", "title": "双因素验证", "generate_new_recovery_codes": "生成新的恢复码", - "warning_of_generate_new_codes": "当你生成新的恢复码时,你的旧恢复码就失效了。", + "warning_of_generate_new_codes": "当您生成新的恢复码时,您旧的恢复码将会失效。", "recovery_codes": "恢复码。", "waiting_a_recovery_codes": "正在接收备份码…", - "recovery_codes_warning": "抄写这些号码,或者保存在安全的地方。这些号码不会再次显示。如果你无法访问你的 2FA app,也丢失了你的恢复码,你的账号就再也无法登录了。", + "recovery_codes_warning": "抄写这些号码,或者将其保存在安全的地方。这些号码不会再次显示。如果您无法访问您的 2FA app,也丢失了您的恢复码,您就再也无法登录您的账号了。", "authentication_methods": "身份验证方法", "scan": { "title": "扫一下", - "desc": "使用你的双因素验证 app,扫描这个二维码,或者输入这些文字密钥:", + "desc": "使用您的双因素验证 app,扫描这个二维码,或者输入这些文字密钥:", "secret_code": "密钥" }, "verify": { - "desc": "要启用双因素验证,请把你的双因素验证 app 里的数字输入:" + "desc": "要启用双因素验证,请输入您的双因素验证 app 里的数字:" } }, "attachmentRadius": "附件", @@ -204,12 +218,12 @@ "avatarRadius": "头像", "background": "背景", "bio": "简介", - "block_export": "拉黑名单导出", - "block_export_button": "导出你的拉黑名单到一个 csv 文件", - "block_import": "拉黑名单导入", - "block_import_error": "导入拉黑名单出错", - "blocks_imported": "拉黑名单导入成功!需要一点时间来处理。", - "blocks_tab": "块", + "block_export": "屏蔽名单导出", + "block_export_button": "导出你的屏蔽名单到一个 csv 文件", + "block_import": "屏蔽名单导入", + "block_import_error": "导入屏蔽名单出错", + "blocks_imported": "屏蔽名单导入成功!需要一点时间来处理。", + "blocks_tab": "屏蔽", "btnRadius": "按钮", "cBlue": "蓝色(回复,关注)", "cGreen": "绿色(转发)", @@ -229,7 +243,7 @@ "delete_account": "删除账户", "delete_account_description": "永久删除你的帐号和所有数据。", "delete_account_error": "删除账户时发生错误,如果一直删除不了,请联系实例管理员。", - "delete_account_instructions": "在下面输入你的密码来确认删除账户", + "delete_account_instructions": "在下面输入您的密码来确认删除账户。", "avatar_size_instruction": "推荐的头像图片最小的尺寸是 150x150 像素。", "export_theme": "导出预置主题", "filtering": "过滤器", @@ -245,7 +259,7 @@ "hide_attachments_in_tl": "在时间线上隐藏附件", "hide_muted_posts": "不显示被隐藏的用户的帖子", "max_thumbnails": "最多再每个帖子所能显示的缩略图数量", - "hide_isp": "隐藏指定实例的面板H", + "hide_isp": "隐藏实例独有的面板", "preload_images": "预载图片", "use_one_click_nsfw": "点击一次以打开工作场所不适宜的附件", "hide_post_stats": "隐藏推文相关的统计数据(例如:收藏的次数)", @@ -263,7 +277,7 @@ "invalid_theme_imported": "您所选择的主题文件不被 Pleroma 支持,因此主题未被修改。", "limited_availability": "在您的浏览器中无法使用", "links": "链接", - "lock_account_description": "你需要手动审核关注请求", + "lock_account_description": "您需要手动审核关注请求", "loop_video": "循环视频", "loop_video_silent_only": "只循环没有声音的视频(例如:Mastodon 里的“GIF”)", "mutes_tab": "隐藏", @@ -278,7 +292,7 @@ "notification_visibility_mentions": "提及", "notification_visibility_repeats": "转发", "no_rich_text_description": "不显示富文本格式", - "no_blocks": "没有拉黑的", + "no_blocks": "没有屏蔽", "no_mutes": "没有隐藏", "hide_follows_description": "不要显示我所关注的人", "hide_followers_description": "不要显示关注我的人", @@ -308,7 +322,7 @@ "search_user_to_mute": "搜索你想要隐藏的用户", "security_tab": "安全", "scope_copy": "回复时的复制范围(私信是总是复制的)", - "minimal_scopes_mode": "最小发文范围", + "minimal_scopes_mode": "使发文可见范围的选项最少化", "set_new_avatar": "设置新头像", "set_new_profile_background": "设置新的个人资料背景", "set_new_profile_banner": "设置新的横幅图片", @@ -324,7 +338,7 @@ "text": "文本", "theme": "主题", "theme_help": "使用十六进制代码(#rrggbb)来设置主题颜色。", - "theme_help_v2_1": "你也可以通过切换复选框来覆盖某些组件的颜色和透明。使用“清除所有”来清楚所有覆盖设置。", + "theme_help_v2_1": "您也可以通过选中复选框来覆盖某些组件的颜色和透明度。使用“清除所有”按钮来清除所有覆盖设置。", "theme_help_v2_2": "某些条目下的图标是背景或文本对比指示器,鼠标悬停可以获取详细信息。请记住,使用透明度来显示最差的情况。", "tooltipRadius": "提醒", "upload_a_photo": "上传照片", @@ -335,7 +349,7 @@ }, "notifications": "通知", "notification_mutes": "要停止收到某个指定的用户的通知,请使用隐藏功能。", - "notification_blocks": "拉黑一个用户会停掉所有他的通知,等同于取消关注。", + "notification_blocks": "屏蔽一个用户会停止接收来自该用户的所有通知,并且会取消对该用户的关注。", "enable_web_push_notifications": "启用 web 推送通知", "style": { "switcher": { @@ -350,7 +364,17 @@ "clear_opacity": "清除透明度", "load_theme": "加载主题", "help": { - "upgraded_from_v2": "PleromaFE 已升级,主题会和你记忆中的不太一样。" + "upgraded_from_v2": "PleromaFE 已升级,主题会与您记忆中的不太一样。", + "older_version_imported": "您导入的文件来自旧版本的 FE。", + "future_version_imported": "您导入的文件来自更高版本的 FE。", + "v2_imported": "您导入的文件是旧版 FE 的。我们尽可能保持兼容性,但还是可能出现不一致的情况。", + "snapshot_source_mismatch": "版本冲突:很有可能是 FE 版本回滚后再次升级了,如果您使用旧版本的 FE 更改了主题那么您可能需要使用旧版本,否则请使用新版本。", + "migration_napshot_gone": "不知出于何种原因,主题快照缺失了,一些地方可能与您印象中的不符。", + "migration_snapshot_ok": "为保万无一失,加载了主题快照。您可以试着加载主题数据。", + "fe_downgraded": "PleromaFE 的版本回滚了。", + "fe_upgraded": "PleromaFE 的主题引擎随着版本更新升级了。", + "snapshot_missing": "在文件中没有主题快照,所以网站外观可能会与原来预想的不同。", + "snapshot_present": "主题快照已加载,因此所有的值均被覆盖。您可以改为加载主题的实际数据。" }, "use_source": "新版本", "use_snapshot": "老版本", @@ -389,7 +413,26 @@ "borders": "边框", "buttons": "按钮", "inputs": "输入框", - "faint_text": "灰度文字" + "faint_text": "灰度文字", + "chat": { + "border": "边框", + "outgoing": "发出的", + "incoming": "收到的" + }, + "disabled": "禁用的", + "pressed": "按下的", + "highlight": "强调元素", + "selectedMenu": "选中的菜单项", + "selectedPost": "选中的发布内容", + "icons": "图标", + "poll": "投票统计图", + "popover": "提示框,菜单,弹出框", + "post": "发布内容/用户简介", + "alert_neutral": "中性", + "alert_warning": "警告", + "tabs": "标签页", + "underlay": "底衬", + "toggled": "勾选的" }, "radii": { "_tab_label": "圆角" @@ -426,7 +469,7 @@ }, "fonts": { "_tab_label": "字体", - "help": "给用户界面的元素选择字体。选择 “自选”的你必须输入确切的字体名称。", + "help": "为用户界面的元素选择字体。若选择 “自选”,您必须输入与系统显示完全一致的字体名称。", "components": { "interface": "界面", "input": "输入框", @@ -461,7 +504,7 @@ "notification_setting_filters": "过滤器", "domain_mutes": "域名", "changed_email": "邮箱修改成功!", - "change_email_error": "修改你的电子邮箱时发生错误", + "change_email_error": "修改您的电子邮箱时发生错误。", "change_email": "修改电子邮箱", "allow_following_move": "正在关注的账号迁移时自动重新关注", "notification_setting_privacy_option": "在通知推送中隐藏发送者和内容", @@ -470,7 +513,41 @@ "notification_visibility_emoji_reactions": "互动", "notification_visibility_moves": "用户迁移", "new_email": "新邮箱", - "emoji_reactions_on_timeline": "在时间线上显示表情符号互动" + "emoji_reactions_on_timeline": "在时间线上显示表情符号互动", + "notification_setting_hide_notification_contents": "隐藏推送通知中的发送者与内容信息", + "notification_setting_block_from_strangers": "屏蔽来自你没有关注的用户的通知", + "type_domains_to_mute": "搜索需要隐藏的域名", + "useStreamingApi": "实时接收帖子和通知", + "user_mutes": "用户", + "reset_background_confirm": "您确定要重置个人资料背景图吗?", + "reset_banner_confirm": "您确定要重置横幅图片吗?", + "reset_avatar_confirm": "您确定要重置头像吗?", + "reset_profile_banner": "重置横幅图片", + "reset_profile_background": "重置个人资料背景图", + "reset_avatar": "重置头像", + "hide_followers_count_description": "不显示关注者数量", + "profile_fields": { + "value": "内容", + "name": "标签", + "add_field": "添加字段", + "label": "个人资料元数据" + }, + "accent": "强调色", + "pad_emoji": "从表情符号选择器插入表情符号时,在表情两侧插入空格", + "discoverable": "允许通过搜索检索等服务找到此账号", + "mutes_and_blocks": "隐藏与屏蔽", + "bot": "这是一个机器人账号", + "fun": "趣味", + "useStreamingApiWarning": "(不推荐使用,试验性,已知会跳过一些帖子)", + "chatMessageRadius": "聊天消息", + "greentext": "Meme 箭头", + "virtual_scrolling": "优化时间线渲染", + "import_mutes_from_a_csv_file": "从 csv 文件导入隐藏名单", + "mutes_imported": "隐藏名单导入成功!处理它们将需要一段时间。", + "mute_import_error": "导入隐藏名单出错", + "mute_import": "隐藏名单导入", + "mute_export_button": "导出你的隐藏名单到一个 csv 文件", + "mute_export": "隐藏名单导出" }, "time": { "day": "{0} 天", @@ -516,7 +593,8 @@ "show_new": "显示新内容", "up_to_date": "已是最新", "no_more_statuses": "没有更多的状态", - "no_statuses": "没有状态更新" + "no_statuses": "没有状态更新", + "reload": "重新载入" }, "status": { "favorites": "收藏", @@ -529,7 +607,18 @@ "reply_to": "回复", "replies_list": "回复:", "mute_conversation": "隐藏对话", - "unmute_conversation": "对话取消隐藏" + "unmute_conversation": "对话取消隐藏", + "hide_content": "隐藏内容", + "show_content": "显示内容", + "hide_full_subject": "隐藏此部分标题", + "show_full_subject": "显示全部标题", + "thread_muted": "此系列消息已被隐藏", + "copy_link": "复制状态链接", + "status_unavailable": "状态不可取得", + "unbookmark": "取消书签", + "bookmark": "书签", + "thread_muted_and_words": ",含有过滤词:", + "status_deleted": "该状态已被删除" }, "user_card": { "approve": "允许", @@ -556,9 +645,9 @@ "statuses": "状态", "subscribe": "订阅", "unsubscribe": "退订", - "unblock": "取消拉黑", - "unblock_progress": "取消拉黑中…", - "block_progress": "拉黑中…", + "unblock": "取消屏蔽", + "unblock_progress": "正在取消屏蔽…", + "block_progress": "正在屏蔽…", "unmute": "取消隐藏", "unmute_progress": "取消隐藏中…", "mute_progress": "隐藏中…", @@ -579,11 +668,13 @@ "disable_any_subscription": "完全禁止关注用户", "quarantine": "从联合实例中禁止用户帖子", "delete_user": "删除用户", - "delete_user_confirmation": "你确认吗?此操作无法撤销。" + "delete_user_confirmation": "你确定吗?此操作无法撤销。" }, "hidden": "已隐藏", "show_repeats": "显示转发", - "hide_repeats": "隐藏转发" + "hide_repeats": "隐藏转发", + "message": "消息", + "mention": "提及" }, "user_profile": { "timeline_title": "用户时间线", @@ -592,12 +683,12 @@ }, "user_reporting": { "title": "报告 {0}", - "add_comment_description": "此报告会发送给你的实例管理员。你可以在下面提供更多详细信息解释报告的缘由:", + "add_comment_description": "此报告会发送给您的实例管理员。您可以在下面提供更多详细信息解释报告的缘由:", "additional_comments": "其它信息", "forward_description": "这个账号是从另外一个服务器。同时发送一个副本到那里?", "forward_to": "转发 {0}", "submit": "提交", - "generic_error": "当处理你的请求时,发生了一个错误。" + "generic_error": "当处理您的请求时,发生了一个错误。" }, "who_to_follow": { "more": "更多", @@ -610,7 +701,9 @@ "favorite": "收藏", "user_settings": "用户设置", "reject_follow_request": "拒绝关注请求", - "add_reaction": "添加互动" + "add_reaction": "添加互动", + "bookmark": "书签", + "accept_follow_request": "接受关注请求" }, "upload": { "error": { @@ -628,7 +721,7 @@ }, "search": { "people": "人", - "hashtags": "Hashtags", + "hashtags": "话题标签", "person_talking": "{count} 人正在讨论", "people_talking": "{count} 人正在讨论", "no_results": "没有搜索结果" @@ -636,12 +729,14 @@ "password_reset": { "forgot_password": "忘记密码了?", "password_reset": "重置密码", - "instruction": "输入你的电邮地址或者用户名,我们将发送一个链接到你的邮箱,用于重置密码。", - "placeholder": "你的电邮地址或者用户名", - "check_email": "检查你的邮箱,会有一个链接用于重置密码。", + "instruction": "输入您的电邮地址或者用户名,我们将发送一个链接到您的邮箱,用于重置密码。", + "placeholder": "您的电邮地址或者用户名", + "check_email": "检查您的邮箱,会有一个链接用于重置密码。", "return_home": "回到首页", - "too_many_requests": "你触发了尝试的限制,请稍后再试。", - "password_reset_disabled": "密码重置已经被禁用。请联系你的实例管理员。" + "too_many_requests": "您达到了尝试次数的上限,请稍后再试。", + "password_reset_disabled": "密码重置已被禁用。请联系您的实例管理员。", + "password_reset_required_but_mailer_is_disabled": "您必须重置密码,但是密码重置被禁用了。请联系您所在实例的管理员。", + "password_reset_required": "您必须重置密码才能登陆。" }, "remote_user_resolver": { "error": "未找到。", @@ -650,27 +745,34 @@ }, "emoji": { "keep_open": "选择器保持打开", - "stickers": "贴图", + "stickers": "贴纸", "unicode": "Unicode 表情符号", "custom": "自定义表情符号", "add_emoji": "插入表情符号", "search_emoji": "搜索表情符号", - "emoji": "表情符号" + "emoji": "表情符号", + "load_all": "加载所有表情符号(共 {emojiAmount} 个)", + "load_all_hint": "最先加载的 {saneAmount} 表情符号,加载全部表情符号可能会带来性能问题。" }, "about": { "mrf": { "simple": { - "quarantine_desc": "本实例只会把公开状态发送非下列实例:", + "quarantine_desc": "本实例向以下实例仅发送公开的帖子:", "quarantine": "隔离", "reject_desc": "本实例不会接收来自下列实例的消息:", "reject": "拒绝", "accept_desc": "本实例只接收来自下列实例的消息:", - "simple_policies": "站规", + "simple_policies": "对于特定实例的策略", "accept": "接受", - "media_removal": "移除媒体" + "media_removal": "移除媒体", + "media_nsfw_desc": "本实例将来自以下实例的媒体内容强制设置为敏感内容:", + "media_nsfw": "强制设置媒体为敏感内容", + "media_removal_desc": "本实例移除来自以下实例的媒体内容:", + "ftl_removal_desc": "该实例在从“全部已知网络”时间线上移除了下列实例:", + "ftl_removal": "从“全部已知网络”时间线上移除" }, "mrf_policies_desc": "MRF 策略会影响本实例的互通行为。以下策略已启用:", - "mrf_policies": "已启动 MRF 策略", + "mrf_policies": "已启动的 MRF 策略", "keyword": { "ftl_removal": "从“全部已知网络”时间线上移除", "keyword_policies": "关键词策略", @@ -678,13 +780,42 @@ "replace": "替换", "reject": "拒绝" }, - "federation": "联邦" - } + "federation": "联邦互通" + }, + "staff": "管理人员" }, "domain_mute_card": { "unmute_progress": "正在取消隐藏…", "unmute": "取消隐藏", "mute_progress": "隐藏中…", "mute": "隐藏" + }, + "errors": { + "storage_unavailable": "Pleroma 无法访问浏览器储存。您的登陆名以及本地设置将不会被保存,您可能遇到意外问题。请尝试启用 cookies。" + }, + "shoutbox": { + "title": "留言板" + }, + "display_date": { + "today": "今天" + }, + "file_type": { + "file": "文件", + "image": "图片", + "video": "视频", + "audio": "音频" + }, + "chats": { + "empty_chat_list_placeholder": "您还没有任何聊天记录。开始聊天吧!", + "error_sending_message": "发送消息时出了点问题。", + "error_loading_chat": "加载聊天时出了点问题。", + "delete_confirm": "您确实要删除此消息吗?", + "more": "更多", + "empty_message_error": "无法发布空消息", + "new": "新聊天", + "chats": "聊天", + "delete": "删除", + "message_user": "发消息给 {nickname}", + "you": "你:" } } diff --git a/src/i18n/zh_Hant.json b/src/i18n/zh_Hant.json @@ -0,0 +1,810 @@ +{ + "emoji": { + "unicode": "統一碼繪文字", + "custom": "自定義繪文字", + "add_emoji": "插入繪文字", + "search_emoji": "搜索繪文字", + "keep_open": "選擇器保持打開", + "emoji": "繪文字", + "stickers": "貼紙", + "load_all": "加載所有繪文字(共 {emojiAmount} 個)", + "load_all_hint": "最先加載的 {saneAmount} ,加載全部繪文字可能會帶來性能問題。" + }, + "polls": { + "not_enough_options": "投票的選項太少", + "expired": "投票 {0} 前已結束", + "expires_in": "投票於 {0} 內結束", + "expiry": "投票期限", + "multiple_choices": "多選", + "single_choice": "單選", + "type": "問卷類型", + "vote": "投票", + "votes": "票", + "option": "選項", + "add_option": "增加選項", + "add_poll": "增加投票" + }, + "notifications": { + "reacted_with": "和 {0} 互動過", + "migrated_to": "遷移到", + "no_more_notifications": "沒有更多的通知", + "repeated_you": "轉發了你的發文", + "read": "已閱!", + "notifications": "通知", + "load_older": "載入更早的通知", + "follow_request": "想要關注你", + "followed_you": "關注了你", + "favorited_you": "喜歡了你的發文", + "broken_favorite": "未知的狀態,正在搜索中…" + }, + "nav": { + "chats": "聊天", + "timelines": "時間線", + "preferences": "偏好設置", + "who_to_follow": "推薦關注", + "search": "搜索", + "user_search": "用戶搜索", + "bookmarks": "書籤", + "twkn": "已知網絡", + "timeline": "時間線", + "public_tl": "公共時間線", + "dms": "私信", + "interactions": "互動", + "mentions": "提及", + "friend_requests": "關注請求", + "back": "後退", + "administration": "管理", + "about": "關於" + }, + "media_modal": { + "next": "往後", + "previous": "往前" + }, + "login": { + "heading": { + "recovery": "雙重因素恢復", + "totp": "雙重因素驗證" + }, + "recovery_code": "恢復碼", + "enter_two_factor_code": "輸入一個雙重因素驗證碼", + "enter_recovery_code": "輸入一個恢復碼", + "authentication_code": "驗證碼", + "hint": "登錄後加入討論", + "username": "用戶名", + "register": "註冊", + "placeholder": "例:鈴音", + "password": "密碼", + "logout": "登出", + "description": "用 OAuth 登入", + "login": "登入" + }, + "importer": { + "error": "導入此文件時出現一個錯誤。", + "success": "導入成功。", + "submit": "提交" + }, + "image_cropper": { + "cancel": "取消", + "save_without_cropping": "保存不裁剪", + "save": "保存", + "crop_picture": "裁剪圖片" + }, + "general": { + "peek": "窺視", + "close": "關閉", + "verify": "驗證", + "confirm": "確認", + "enable": "啟用", + "disable": "禁用", + "cancel": "取消", + "dismiss": "忽略", + "show_less": "收起", + "show_more": "展開", + "optional": "可選", + "retry": "再試", + "error_retry": "請再試", + "generic_error": "發生一個錯誤", + "loading": "載入中…", + "more": "更多", + "submit": "提交", + "apply": "應用" + }, + "finder": { + "find_user": "尋找用戶", + "error_fetching_user": "獲取用戶時發生錯誤" + }, + "features_panel": { + "who_to_follow": "推薦關注", + "title": "特色", + "text_limit": "文字數量限制", + "scope_options": "可見範圍設置", + "media_proxy": "媒體代理", + "pleroma_chat_messages": "Pleroma 聊天", + "chat": "聊天", + "gopher": "Gopher" + }, + "exporter": { + "processing": "正在處理,稍後會提示您下載文件", + "export": "導出" + }, + "domain_mute_card": { + "unmute_progress": "取消靜音中…", + "unmute": "取消靜音", + "mute_progress": "靜音中…", + "mute": "靜音" + }, + "shoutbox": { + "title": "留言板" + }, + "about": { + "staff": "職員", + "mrf": { + "simple": { + "media_nsfw_desc": "這個實例強迫以下實例的帖子媒體設定為敏感:", + "media_nsfw": "媒體強制設定為敏感", + "media_removal_desc": "這個實例移除以下實例的帖子媒體:", + "media_removal": "移除媒體", + "ftl_removal_desc": "這個實例在所有已知網絡中移除下列實例:", + "ftl_removal": "從所有已知網路中移除", + "quarantine_desc": "本實例只會把公開發文發送到下列實例:", + "quarantine": "隔離", + "reject_desc": "本實例不會接收來自下列實例的消息:", + "reject": "拒絕", + "accept_desc": "本實例只接收來自下列實例的消息:", + "simple_policies": "站規", + "accept": "接受" + }, + "mrf_policies_desc": "MRF 策略會影響本實例的互通行為。以下策略已啟用:", + "keyword": { + "ftl_removal": "從“全部已知網絡”時間線上移除", + "replace": "取代", + "reject": "拒絕", + "is_replaced_by": "→", + "keyword_policies": "關鍵字政策" + }, + "mrf_policies": "已啟用的MRF政策", + "federation": "聯邦" + } + }, + "settings": { + "style": { + "common": { + "color": "顏色", + "contrast": { + "context": { + "18pt": "大字文本 (18pt+)", + "text": "文本" + }, + "level": { + "aaa": "符合 AAA 等級準則(推薦)", + "aa": "符合 AA 等級準則(最低)", + "bad": "不符合任何輔助功能指南" + }, + "hint": "對比度是 {ratio}, 它 {level} {context}" + }, + "opacity": "透明度" + }, + "advanced_colors": { + "faint_text": "灰度文字", + "alert_error": "錯誤", + "badge_notification": "通知", + "alert": "提醒或警告背景色", + "_tab_label": "高级", + "alert_warning": "警告", + "alert_neutral": "中性", + "post": "帖子/用戶簡介", + "badge": "徽章背景", + "popover": "提示框,菜單,彈出框", + "panel_header": "面板標題", + "top_bar": "頂欄", + "borders": "邊框", + "buttons": "按鈕", + "inputs": "輸入框", + "underlay": "底襯", + "poll": "投票統計圖", + "icons": "圖標", + "highlight": "強調元素", + "pressed": "按下", + "selectedPost": "選中的帖子", + "selectedMenu": "選中的菜單項", + "disabled": "關閉", + "toggled": "切換", + "tabs": "標籤", + "chat": { + "incoming": "收到", + "outgoing": "發出", + "border": "邊框" + } + }, + "preview": { + "header_faint": "這很正常", + "header": "預覽", + "content": "內容", + "error": "例子錯誤", + "button": "按鈕", + "text": "有堆 {0} 和 {1}", + "mono": "內容", + "input": "剛剛抵達洛杉磯.", + "faint_link": "有用的手冊", + "fine_print": "閱讀我們的 {0} ,然而什麼有用的也學不到!", + "checkbox": "我已經瀏覽了條款及細則", + "link": "一個很好的小鏈接" + }, + "shadows": { + "override": "覆寫", + "_tab_label": "陰影和燈光", + "component": "組件", + "shadow_id": "陰影 #{value}", + "blur": "模糊", + "spread": "擴散", + "inset": "插圖", + "hintV3": "對於陰影,您還可以使用{0}表示法來使用其他顏色插槽。", + "filter_hint": { + "always_drop_shadow": "警告,此陰影設置會總是使用 {0} ,如果瀏覽器支持的話。", + "drop_shadow_syntax": "{0} 不支持參數 {1} 和關鍵詞 {2} 。", + "avatar_inset": "請注意組合兩個內部和非內部的陰影到頭像上,在透明頭像上可能會有意料之外的效果。", + "spread_zero": "陰影的擴散 > 0 會同設置成零一樣", + "inset_classic": "插入內部的陰影會使用 {0}" + }, + "components": { + "panel": "面板", + "panelHeader": "面板標題", + "topBar": "頂欄", + "avatar": "用戶頭像(在個人資料欄)", + "avatarStatus": "用戶頭像(在帖子顯示欄)", + "popup": "彈窗和工具提示", + "button": "按鈕", + "buttonHover": "按鈕(懸停)", + "buttonPressed": "按鈕(按下)", + "buttonPressedHover": "按鈕(按下和懸停)", + "input": "輸入框" + } + }, + "switcher": { + "use_snapshot": "舊版", + "load_theme": "載入主題", + "keep_color": "保留顏色", + "keep_shadows": "保留陰影", + "keep_opacity": "保留透明度", + "keep_roundness": "保留圓角", + "help": { + "migration_napshot_gone": "不知出於何種原因,主題快照缺失了,一些地方可能與您印象中的不符。", + "snapshot_source_mismatch": "版本衝突:很有可能是 FE 版本回滾後再次升級了,如果您使用舊版本的 FE 更改了主題那麼您可能需要使用舊版本,否則請使用新版本。", + "future_version_imported": "您導入的文件來自更高版本的 FE。", + "older_version_imported": "您導入的文件來自舊版本的 FE。", + "snapshot_missing": "在文件中沒有主題快照,所以網站外觀可能會與原來預想的不同。", + "fe_upgraded": "PleromaFE 的主題引擎隨著版本更新升級了。", + "fe_downgraded": "PleromaFE 的版本回滾了。", + "upgraded_from_v2": "PleromaFE 已升級,主題會和你記憶中的不太一樣。", + "v2_imported": "您導入的文件是舊版 FE 的。我們儘可能保持兼容性,但還是可能出現不一致的情況。", + "snapshot_present": "載入快照已加載,因此所有值均被覆蓋。 您可以改為載入主題實際數據。", + "migration_snapshot_ok": "為保萬無一失,載入了主題快照。您可以試著載入主題數據。" + }, + "use_source": "新版本", + "keep_as_is": "保持原狀", + "clear_opacity": "清除透明度", + "clear_all": "清除全部", + "reset": "重置", + "keep_fonts": "保留字體", + "save_load_hint": "\"保留\" 選項在選擇或載入主題時保留當前設置的選項,在導出主題時還會存儲上述選項。當所有複選框未設置時,導出主題將保存所有內容。" + }, + "fonts": { + "components": { + "interface": "界面", + "input": "輸入框", + "post": "發帖文字", + "postCode": "帖子中使用等間距文字(富文本)" + }, + "_tab_label": "字體", + "help": "給用戶界面的元素選擇字體。選擇 “自選”的你必須輸入確切的字體名稱。", + "family": "字體名稱", + "size": "大小 (像素)", + "weight": "字重 (粗體))", + "custom": "自選" + }, + "common_colors": { + "foreground_hint": "點擊”高級“ 標籤進行細緻的控制", + "main": "常用顏色", + "_tab_label": "共同", + "rgbo": "圖標,強調,徽章" + }, + "radii": { + "_tab_label": "圓角" + } + }, + "notification_setting_block_from_strangers": "屏蔽來自你沒有關注的用戶的通知", + "user_mutes": "用户", + "hide_followers_count_description": "不顯示關注者數量", + "no_rich_text_description": "不顯示富文本格式", + "notification_visibility_moves": "用戶遷移", + "notification_visibility_repeats": "轉發", + "notification_visibility_mentions": "提及", + "notification_visibility_likes": "點贊", + "interfaceLanguage": "界面語言", + "instance_default": "(默認:{value})", + "inputRadius": "輸入框", + "import_theme": "導入預置主題", + "import_followers_from_a_csv_file": "從 csv 文件中導入關注", + "import_blocks_from_a_csv_file": "從 csv 文件中導入封鎖黑名單名單", + "hide_filtered_statuses": "隱藏過濾的發文", + "lock_account_description": "你需要手動審核關注請求", + "loop_video": "循環視頻", + "loop_video_silent_only": "只循環沒有聲音的視頻(例如:Mastodon 裡的“GIF”)", + "mutes_tab": "靜音", + "play_videos_in_modal": "在彈出框內播放視頻", + "profile_fields": { + "add_field": "添加字段", + "name": "標籤", + "value": "內容", + "label": "個人資料元數據" + }, + "use_contain_fit": "生成縮略圖時不要裁剪附件", + "notification_visibility": "要顯示的通知類型", + "notification_visibility_follows": "關注", + "new_email": "新電郵", + "subject_line_mastodon": "比如mastodon: copy as is", + "reset_background_confirm": "您確定要重置個人資料背景圖嗎?", + "reset_banner_confirm": "您確定要重置橫幅圖片嗎?", + "reset_avatar_confirm": "您確定要重置頭像嗎?", + "reset_profile_banner": "重置橫幅圖片", + "reset_profile_background": "重置個人資料背景圖", + "reset_avatar": "重置頭像", + "discoverable": "允許通過搜索檢索等服務找到此賬號", + "delete_account_error": "刪除賬戶時發生錯誤,如果一直刪除不了,請聯繫實例管理員。", + "composing": "正在書寫", + "chatMessageRadius": "聊天訊息", + "mfa": { + "confirm_and_enable": "確認並啟用OTP", + "setup_otp": "設置OTP", + "otp": "OTP", + "wait_pre_setup_otp": "預設OTP", + "verify": { + "desc": "要啟用雙因素驗證,請把你的雙因素驗證 app 裡的數字輸入:" + }, + "scan": { + "secret_code": "密鑰", + "desc": "使用你的雙因素驗證 app,掃瞄這個二維碼,或者輸入這些文字密鑰:", + "title": "掃瞄" + }, + "authentication_methods": "身份驗證方法", + "recovery_codes_warning": "抄寫這些號碼,或者保存在安全的地方。這些號碼不會再次顯示。如果你無法訪問你的 2FA app,也丟失了你的恢復碼,你的賬號就再也無法登錄了。", + "waiting_a_recovery_codes": "正在接收備份碼…", + "recovery_codes": "恢復碼。", + "warning_of_generate_new_codes": "當你生成新的恢復碼時,你的舊恢復碼就失效了。", + "generate_new_recovery_codes": "生成新的恢復碼", + "title": "雙因素驗證" + }, + "new_password": "新密碼", + "name_bio": "名字及簡介", + "name": "名字", + "domain_mutes": "域名", + "delete_account_instructions": "在下面輸入密碼,以確認刪除帳戶。", + "delete_account_description": "永久刪除你的帳號和所有數據。", + "delete_account": "刪除帳戶", + "default_vis": "默認可見性範圍", + "data_import_export_tab": "數據導入/導出", + "mutes_and_blocks": "靜音與封鎖", + "current_password": "當前密碼", + "confirm_new_password": "確認新密碼", + "collapse_subject": "摺疊帶標題的內容", + "checkboxRadius": "複選框", + "instance_default_simple": "(默認)", + "interface": "界面", + "invalid_theme_imported": "您所選擇的主題文件不被 Pleroma 支持,因此主題未被修改。", + "limited_availability": "在您的瀏覽器中無法使用", + "links": "鏈接", + "changed_password": "成功修改了密碼!", + "change_password_error": "修改密碼的時候出了點問題。", + "change_password": "修改密碼", + "changed_email": "郵箱修改成功!", + "bot": "這是一個機器人賬號", + "change_email": "修改電子郵箱", + "cRed": "紅色(取消)", + "cOrange": "橙色(收藏)", + "cGreen": "綠色(轉發)", + "cBlue": "藍色(回覆,關注)", + "btnRadius": "按鈕", + "notification_visibility_emoji_reactions": "互動", + "no_blocks": "沒有封鎖", + "no_mutes": "沒有靜音", + "hide_follows_description": "不要顯示我所關注的人", + "hide_followers_description": "不要顯示關注我的人", + "hide_follows_count_description": "不顯示關注數", + "nsfw_clickthrough": "將敏感附件隱藏,點擊才能打開", + "valid_until": "有效期至", + "panelRadius": "面板", + "pause_on_unfocused": "在離開頁面時暫停時間線推送", + "notifications": "通知", + "notification_setting_filters": "過濾器", + "notification_setting_privacy": "隱私", + "notification_mutes": "要停止收到某個指定的用戶的通知,請使用靜音功能。", + "notification_blocks": "封鎖一個用戶會停掉所有他的通知,等同於取消關注。", + "enable_web_push_notifications": "啟用 web 推送通知", + "presets": "預置", + "profile_background": "個人背景圖", + "profile_banner": "橫幅圖片", + "profile_tab": "個人資料", + "radii_help": "設置界面邊緣的圓角 (單位:像素)", + "reply_visibility_all": "顯示所有回覆", + "autohide_floating_post_button": "自動隱藏新帖子的按鈕(移動設備)", + "saving_err": "保存設置時發生錯誤", + "saving_ok": "設置已保存", + "search_user_to_block": "搜索你想屏蔽的用戶", + "search_user_to_mute": "搜索你想要隱藏的用戶", + "security_tab": "安全", + "set_new_avatar": "設置新頭像", + "set_new_profile_background": "設置新的個人背景", + "set_new_profile_banner": "設置新的個人橫幅", + "settings": "設置", + "subject_input_always_show": "總是顯示主題框", + "subject_line_behavior": "回覆時複製主題", + "subject_line_email": "比如電郵: \"re: 主題\"", + "subject_line_noop": "不要複製", + "post_status_content_type": "發文內容類型", + "stop_gifs": "鼠標懸停時播放GIF", + "streaming": "開啟滾動到頂部時的自動推送", + "text": "文本", + "theme": "主題", + "theme_help": "使用十六進制代碼(#rrggbb)來設置主題顏色。", + "theme_help_v2_1": "你也可以通過切換複選框來覆蓋某些組件的顏色和透明。使用“清除所有”來清楚所有覆蓋設置。", + "theme_help_v2_2": "某些條目下的圖標是背景或文本對比指示器,鼠標懸停可以獲取詳細信息。請記住,使用透明度來顯示最差的情況。", + "tooltipRadius": "提醒", + "upload_a_photo": "上傳照片", + "user_settings": "用戶設置", + "values": { + "false": "否", + "true": "是" + }, + "avatar_size_instruction": "推薦的頭像圖片最小的尺寸是 150x150 像素。", + "emoji_reactions_on_timeline": "在時間線上顯示繪文字互動", + "export_theme": "導出預置主題", + "filtering": "過濾", + "filtering_explanation": "所有包含以下詞彙的內容都會被隱藏,一行一個", + "follow_export": "導出關注", + "follow_export_button": "將關注導出成 csv 文件", + "follow_import": "導入關注", + "follow_import_error": "導入關注時錯誤", + "follows_imported": "關注已導入!尚需要一些時間來處理。", + "hide_attachments_in_convo": "在對話中隱藏附件", + "hide_attachments_in_tl": "在時間線上隱藏附件", + "hide_muted_posts": "不顯示被靜音的用戶的帖子", + "max_thumbnails": "最多每個帖子所能顯示的縮略圖數量", + "hide_isp": "隱藏指定實例的面板", + "preload_images": "預載圖片", + "use_one_click_nsfw": "點擊一次以打開工作場所不適宜的附件", + "hide_post_stats": "隱藏帖子的統計數據(例如:收藏的次數)", + "hide_user_stats": "隱藏用戶的統計數據(例如:關注者的數量)", + "general": "通用", + "foreground": "前景", + "blocks_tab": "封鎖", + "blocks_imported": "封鎖黑名單導入成功!需要一點時間來處理。", + "block_import_error": "導入封鎖黑名單出錯", + "block_import": "封鎖黑名單導入", + "block_export_button": "導出你的封鎖黑名單到一個 csv 文件", + "block_export": "封鎖黑名單導出", + "bio": "簡介", + "background": "背景", + "avatarRadius": "頭像", + "avatarAltRadius": "頭像(通知)", + "avatar": "頭像", + "attachments": "附件", + "attachmentRadius": "附件", + "allow_following_move": "正在關注的賬號遷移時自動重新關注", + "enter_current_password_to_confirm": "輸入你當前密碼來確認你的身份", + "security": "安全", + "app_name": "App 名稱", + "change_email_error": "修改你的電子郵箱時發生錯誤。", + "type_domains_to_mute": "搜索需要隱藏的域名", + "pad_emoji": "從繪文字選擇器插入繪文字時,在繪文字兩側插入空格", + "useStreamingApi": "實時接收發佈以及通知", + "minimal_scopes_mode": "最小發文範圍", + "scope_copy": "回覆時的複製範圍(私信是總是複製的)", + "reply_visibility_self": "只顯示發送給我的回覆", + "reply_visibility_following": "只顯示發送給我的回覆/發送給我關注的用戶的回覆", + "replies_in_timeline": "時間線中的回覆", + "revoke_token": "撤消", + "show_admin_badge": "顯示管理徽章", + "accent": "強調色", + "greentext": "前文箭頭", + "show_moderator_badge": "顯示主持人徽章", + "oauth_tokens": "OAuth代幣", + "token": "代幣", + "refresh_token": "刷新代幣", + "useStreamingApiWarning": "(不推薦使用,實驗性的,已知跳過文章)", + "fun": "有趣", + "notification_setting_hide_notification_contents": "隱藏推送通知中的發送者與內容信息", + "version": { + "title": "版本", + "backend_version": "後端版本", + "frontend_version": "前端版本" + }, + "virtual_scrolling": "優化時間線渲染", + "import_mutes_from_a_csv_file": "從CSV文件導入靜音", + "mutes_imported": "靜音導入了!處理它們將需要一段時間。", + "mute_import": "靜音導入", + "mute_import_error": "導入靜音時出錯", + "mute_export_button": "將靜音導出到csv文件", + "mute_export": "靜音導出" + }, + "chats": { + "more": "更多", + "delete_confirm": "您確實要刪除此消息嗎?", + "error_loading_chat": "加載聊天時出了點問題。", + "error_sending_message": "發送消息時出了點問題。", + "empty_chat_list_placeholder": "您還沒有任何聊天記錄。 開始新的聊天!", + "new": "新聊天", + "empty_message_error": "無法發布空消息", + "you": "你:", + "message_user": "發消息給 {nickname}", + "delete": "刪除", + "chats": "聊天" + }, + "file_type": { + "audio": "音頻", + "video": "視頻", + "image": "图片", + "file": "檔案" + }, + "display_date": { + "today": "今天" + }, + "status": { + "mute_conversation": "靜音對話", + "replies_list": "回覆:", + "reply_to": "回覆", + "pin": "在個人資料置頂", + "unpin": "取消在個人資料置頂", + "favorites": "喜歡", + "repeats": "轉發", + "delete": "刪除發文", + "pinned": "置頂", + "bookmark": "書籤", + "unbookmark": "取消書籤", + "delete_confirm": "你真的想要刪除這條發文嗎?", + "unmute_conversation": "對話取消靜音", + "status_unavailable": "發文不可取得", + "copy_link": "複製發文鏈接", + "thread_muted": "静音線程", + "show_full_subject": "顯示完整標題", + "thread_muted_and_words": ",有这些字:", + "hide_full_subject": "隱藏完整標題", + "show_content": "顯示內容", + "hide_content": "隱藏內容" + }, + "time": { + "hours": "{0} 小時", + "days_short": "{0}天", + "day_short": "{0}天", + "days": "{0} 天", + "hour": "{0} 小时", + "hour_short": "{0}h", + "hours_short": "{0}h", + "years_short": "{0} y", + "now": "剛剛", + "day": "{0} 天", + "in_future": "還有 {0}", + "in_past": "{0} 之前", + "minute": "{0} 分鐘", + "minute_short": "{0} 分", + "minutes_short": "{0} 分", + "minutes": "{0} 分鐘", + "month": "{0} 月", + "months": "{0} 月", + "month_short": "{0} 月", + "months_short": "{0} 月", + "now_short": "剛剛", + "second": "{0} 秒", + "seconds": "{0} 秒", + "second_short": "{0} 秒", + "seconds_short": "{0} 秒", + "week": "{0}周", + "weeks": "{0}周", + "week_short": "{0}周", + "weeks_short": "{0}周", + "year": "{0} 年", + "years": "{0} 年", + "year_short": "{0}年" + }, + "post_status": { + "media_description_error": "無法更新媒體,請重試", + "media_description": "媒體描述", + "scope": { + "unlisted": "不公開 - 不會發送到公共時間軸", + "public": "公共 - 發送到公共時間軸", + "private": "僅關注者 - 只有關注了你的人能看到", + "direct": "私信 - 只發送給被提及的用戶" + }, + "scope_notice": { + "unlisted": "本條內容既不在公共時間線,也不會在所有已知網絡上可見", + "private": "關注你的人才能看到本條內容", + "public": "本條帖子可以被所有人看到" + }, + "preview_empty": "空的", + "preview": "預覽", + "posting": "正在發送", + "direct_warning_to_first_only": "本條內容只有被在消息開始處提及的用戶能夠看到。", + "direct_warning_to_all": "本條內容只有被提及的用戶能夠看到。", + "account_not_locked_warning": "你的帳號沒有 {0}。任何人都可以關注你並瀏覽你的上鎖內容。", + "new_status": "發佈新發文", + "content_warning": "主題(可選)", + "content_type": { + "text/bbcode": "BBCode", + "text/markdown": "Markdown", + "text/html": "HTML", + "text/plain": "純文本" + }, + "attachments_sensitive": "標記附件為敏感內容", + "account_not_locked_warning_link": "上鎖", + "default": "剛剛抵達洛杉磯。", + "empty_status_error": "無法發佈沒有附件的空發文" + }, + "errors": { + "storage_unavailable": "Pleroma無法訪問瀏覽器存儲。您的登錄名或本地設置將不會保存,您可能會遇到意外問題。嘗試啟用Cookie。" + }, + "timeline": { + "error_fetching": "獲取更新時發生錯誤", + "conversation": "對話", + "no_retweet_hint": "這條內容僅關注者可見,或者是私信,因此不能轉發", + "collapse": "摺疊", + "load_older": "載入更早的發文", + "repeated": "已轉發", + "show_new": "顯示新內容", + "reload": "重新載入", + "up_to_date": "已是最新", + "no_more_statuses": "没有更多發文", + "no_statuses": "没有發文" + }, + "interactions": { + "load_older": "載入更早的互動", + "moves": "用戶遷移", + "follows": "新的關注者", + "favs_repeats": "轉發和收藏" + }, + "selectable_list": { + "select_all": "選擇全部" + }, + "remote_user_resolver": { + "error": "未找到。", + "searching_for": "搜索", + "remote_user_resolver": "遠程用戶解析器" + }, + "registration": { + "validations": { + "password_confirmation_match": "不能和密碼一樣", + "password_confirmation_required": "不能留空", + "password_required": "不能留空", + "email_required": "不能留空", + "fullname_required": "不能留空", + "username_required": "不能留空" + }, + "fullname": "顯示名稱", + "bio_placeholder": "例如:\n你好,我是玲音。\n我是一個住在日本郊區的動畫少女。你可能在 Wired 見過我。", + "fullname_placeholder": "例如:岩倉玲音", + "username_placeholder": "例如:lain", + "new_captcha": "點擊圖片獲取新的驗證碼", + "captcha": "CAPTCHA", + "token": "邀請碼", + "registration": "註冊", + "password_confirm": "確認密碼", + "email": "電子郵箱", + "bio": "簡介" + }, + "user_card": { + "its_you": "就是你!!", + "media": "媒體", + "per_day": "每天", + "remote_follow": "跨站關注", + "subscribe": "訂閱", + "mute_progress": "靜音中…", + "admin_menu": { + "delete_account": "刪除賬號", + "delete_user": "刪除用戶", + "delete_user_confirmation": "你確認嗎?此操作無法撤銷。", + "moderation": "調停", + "grant_admin": "賦予管理權限", + "revoke_admin": "撤銷管理權限", + "grant_moderator": "賦予主持人權限", + "revoke_moderator": "撤銷主持人權限", + "activate_account": "啟用賬號", + "deactivate_account": "關閉賬號", + "force_nsfw": "標記所有的帖子都是工作場合不適", + "strip_media": "從帖子裡刪除媒體文件", + "force_unlisted": "強制帖子為不公開", + "sandbox": "強制帖子為只有關注者可看", + "disable_remote_subscription": "禁止從遠程實例關注用戶", + "disable_any_subscription": "完全禁止關注用戶", + "quarantine": "從聯合實例中禁止用戶帖子" + }, + "approve": "批准", + "block": "封鎖", + "blocked": "已封鎖!", + "deny": "拒絕", + "favorites": "喜歡", + "follow": "關注", + "follow_sent": "請求已發送!", + "follow_progress": "請求中…", + "follow_again": "再次發送請求?", + "follow_unfollow": "取消關注", + "followees": "正在關注", + "followers": "關注者", + "following": "正在關注!", + "follows_you": "關注了你!", + "hidden": "已隱藏", + "mention": "提及", + "message": "消息", + "mute": "靜音", + "muted": "已靜音", + "report": "報告", + "statuses": "發文", + "unsubscribe": "退訂", + "unblock": "取消封鎖", + "unblock_progress": "取消封鎖中…", + "block_progress": "封鎖中…", + "unmute": "取消靜音", + "unmute_progress": "取消靜音中…", + "hide_repeats": "隱藏轉發", + "show_repeats": "顯示轉發" + }, + "user_profile": { + "timeline_title": "用戶時間線", + "profile_does_not_exist": "抱歉,此個人資料不存在。", + "profile_loading_error": "抱歉,載入個人資料時出錯。" + }, + "user_reporting": { + "title": "報告 {0}", + "add_comment_description": "此報告會發送給你的實例管理員。你可以在下面提供更多詳細信息解釋報告的緣由:", + "forward_to": "轉發 {0}", + "submit": "提交", + "generic_error": "當處理你的請求時,發生了一個錯誤。", + "additional_comments": "其它評論", + "forward_description": "這個賬號是從另外一個服務器。同時發送一個報告到那裡?" + }, + "who_to_follow": { + "more": "更多", + "who_to_follow": "推薦關注" + }, + "tool_tip": { + "media_upload": "上傳多媒體", + "repeat": "轉發", + "favorite": "喜歡", + "add_reaction": "添加互動", + "reply": "回覆", + "user_settings": "用戶設置", + "accept_follow_request": "接受關注請求", + "reject_follow_request": "拒絕關注請求", + "bookmark": "書籤" + }, + "upload": { + "file_size_units": { + "B": "B", + "KiB": "KiB", + "TiB": "TiB", + "MiB": "MiB", + "GiB": "GiB" + }, + "error": { + "base": "上傳失敗。", + "file_too_big": "文件太大[{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]", + "default": "稍後再試" + } + }, + "search": { + "people": "人", + "hashtags": "標籤", + "person_talking": "{count} 人正在討論", + "people_talking": "{count} 人正在討論", + "no_results": "沒有搜索結果" + }, + "password_reset": { + "forgot_password": "忘記密碼了?", + "password_reset": "重置密碼", + "instruction": "輸入你的電郵地址或者用戶名,我們將發送一個鏈接到你的郵箱,用於重置密碼。", + "placeholder": "你的電郵地址或者用戶名", + "check_email": "檢查你的郵箱,會有一個鏈接用於重置密碼。", + "return_home": "回到首頁", + "too_many_requests": "你觸發了嘗試的限制,請稍後再試。", + "password_reset_disabled": "密碼重置已經被禁用。請聯繫你的實例管理員。", + "password_reset_required": "您必須重置密碼才能登陸。", + "password_reset_required_but_mailer_is_disabled": "您必須重置密碼,但是密碼重置被禁用了。請聯繫您所在實例的管理員。" + } +} diff --git a/src/main.js b/src/main.js @@ -33,6 +33,8 @@ import VueClickOutside from 'v-click-outside' import PortalVue from 'portal-vue' import VBodyScrollLock from './directives/body_scroll_lock' +import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome' + import afterStoreSetup from './boot/after_store.js' const currentLocale = (window.navigator.language || 'en').split('-')[0] @@ -45,6 +47,9 @@ Vue.use(VueClickOutside) Vue.use(PortalVue) Vue.use(VBodyScrollLock) +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', diff --git a/src/modules/api.js b/src/modules/api.js @@ -20,7 +20,7 @@ const api = { state.fetchers[fetcherName] = fetcher }, removeFetcher (state, { fetcherName, fetcher }) { - window.clearInterval(fetcher) + state.fetchers[fetcherName].stop() delete state.fetchers[fetcherName] }, setWsToken (state, token) { @@ -72,13 +72,21 @@ const api = { showImmediately: timelineData.visibleStatuses.length === 0, timeline: 'friends' }) + } else if (message.event === 'delete') { + dispatch('deleteStatusById', message.id) } else if (message.event === 'pleroma:chat_update') { - dispatch('addChatMessages', { - chatId: message.chatUpdate.id, - messages: [message.chatUpdate.lastMessage] - }) - dispatch('updateChat', { chat: message.chatUpdate }) - maybeShowChatNotification(store, message.chatUpdate) + // The setTimeout wrapper is a temporary band-aid to avoid duplicates for the user's own messages when doing optimistic sending. + // The cause of the duplicates is the WS event arriving earlier than the HTTP response. + // This setTimeout wrapper can be removed once the commit `8e41baff` is in the stable Pleroma release. + // (`8e41baff` adds the idempotency key to the chat message entity, which PleromaFE uses when it's available, and it makes this artificial delay unnecessary). + setTimeout(() => { + dispatch('addChatMessages', { + chatId: message.chatUpdate.id, + messages: [message.chatUpdate.lastMessage] + }) + dispatch('updateChat', { chat: message.chatUpdate }) + maybeShowChatNotification(store, message.chatUpdate) + }, 100) } } ) diff --git a/src/modules/chats.js b/src/modules/chats.js @@ -3,6 +3,7 @@ import { find, omitBy, orderBy, sumBy } from 'lodash' import chatService from '../services/chat_service/chat_service.js' import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js' import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js' +import { promiseInterval } from '../services/promise_interval/promise_interval.js' const emptyChatList = () => ({ data: [], @@ -15,7 +16,8 @@ const defaultState = { openedChats: {}, openedChatMessageServices: {}, fetcher: undefined, - currentChatId: null + currentChatId: null, + lastReadMessageId: null } const getChatById = (state, id) => { @@ -42,12 +44,10 @@ const chats = { actions: { // Chat list startFetchingChats ({ dispatch, commit }) { - const fetcher = () => { - dispatch('fetchChats', { latest: true }) - } + const fetcher = () => dispatch('fetchChats', { latest: true }) fetcher() commit('setChatListFetcher', { - fetcher: () => setInterval(() => { fetcher() }, 5000) + fetcher: () => promiseInterval(fetcher, 5000) }) }, stopFetchingChats ({ commit }) { @@ -93,9 +93,14 @@ const chats = { commit('setCurrentChatFetcher', { fetcher: undefined }) }, readChat ({ rootState, commit, dispatch }, { id, lastReadId }) { + const isNewMessage = rootState.chats.lastReadMessageId !== lastReadId + dispatch('resetChatNewMessageCount') - commit('readChat', { id }) - rootState.api.backendInteractor.readChat({ id, lastReadId }) + commit('readChat', { id, lastReadId }) + + if (isNewMessage) { + rootState.api.backendInteractor.readChat({ id, lastReadId }) + } }, deleteChatMessage ({ rootState, commit }, value) { rootState.api.backendInteractor.deleteChatMessage(value) @@ -107,20 +112,23 @@ const chats = { }, clearOpenedChats ({ rootState, commit, dispatch, rootGetters }) { commit('clearOpenedChats', { commit }) + }, + handleMessageError ({ commit }, value) { + commit('handleMessageError', { commit, ...value }) } }, mutations: { setChatListFetcher (state, { commit, fetcher }) { const prevFetcher = state.chatListFetcher if (prevFetcher) { - clearInterval(prevFetcher) + prevFetcher.stop() } state.chatListFetcher = fetcher && fetcher() }, setCurrentChatFetcher (state, { fetcher }) { const prevFetcher = state.fetcher if (prevFetcher) { - clearInterval(prevFetcher) + prevFetcher.stop() } state.fetcher = fetcher && fetcher() }, @@ -209,11 +217,16 @@ const chats = { } } }, - readChat (state, { id }) { + readChat (state, { id, lastReadId }) { + state.lastReadMessageId = lastReadId const chat = getChatById(state, id) if (chat) { chat.unread = 0 } + }, + handleMessageError (state, { chatId, fakeId, isRetry }) { + const chatMessageService = state.openedChatMessageServices[chatId] + chatService.handleMessageError(chatMessageService, fakeId, isRetry) } } } diff --git a/src/modules/config.js b/src/modules/config.js @@ -65,7 +65,8 @@ export const defaultState = { useContainFit: false, greentext: undefined, // instance default hidePostStats: undefined, // instance default - hideUserStats: undefined // instance default + hideUserStats: undefined, // instance default + virtualScrolling: undefined // instance default } // caching the instance default properties diff --git a/src/modules/instance.js b/src/modules/instance.js @@ -27,9 +27,10 @@ const defaultState = { hideSitename: false, hideUserStats: false, loginMethod: 'password', - logo: '/static/logo.png', + logo: '/static/logo.svg', logoMargin: '.2em', logoMask: true, + logoLeft: false, minimalScopesMode: false, nsfwCensorImage: undefined, postContentType: 'text/plain', @@ -41,6 +42,7 @@ const defaultState = { sidebarRight: false, subjectLineBehavior: 'email', theme: 'pleroma-dark', + virtualScrolling: true, // Nasty stuff customEmoji: [], diff --git a/src/modules/statuses.js b/src/modules/statuses.js @@ -568,6 +568,9 @@ export const mutations = { updateStatusWithPoll (state, { id, poll }) { const status = state.allStatusesObject[id] status.poll = poll + }, + setVirtualHeight (state, { statusId, height }) { + state.allStatusesObject[statusId].virtualHeight = height } } @@ -608,6 +611,10 @@ const statuses = { commit('setDeleted', { status }) apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials }) }, + deleteStatusById ({ rootState, commit }, id) { + const status = rootState.statuses.allStatusesObject[id] + commit('setDeleted', { status }) + }, markStatusesAsDeleted ({ commit }, condition) { commit('setManyDeleted', condition) }, @@ -753,6 +760,9 @@ const statuses = { store.commit('addNewStatuses', { statuses: data.statuses }) return data }) + }, + setVirtualHeight ({ commit }, { statusId, height }) { + commit('setVirtualHeight', { statusId, height }) } }, mutations diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js @@ -3,6 +3,7 @@ import { parseStatus, parseUser, parseNotification, parseAttachment, parseChat, import { RegistrationError, StatusCodeError } from '../errors/errors' /* eslint-env browser */ +const MUTES_IMPORT_URL = '/api/pleroma/mutes_import' const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import' const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import' const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account' @@ -128,7 +129,11 @@ const promisedRequest = ({ method, url, params, payload, credentials, headers = return reject(new StatusCodeError(response.status, json, { url, options }, response)) } return resolve(json) - })) + }) + .catch((error) => { + return reject(new StatusCodeError(response.status, error, { url, options }, response)) + }) + ) }) } @@ -539,8 +544,10 @@ const fetchTimeline = ({ const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&') url += `?${queryString}` + let status = '' let statusText = '' + let pagination = {} return fetch(url, { headers: authHeaders(credentials) }) .then((data) => { @@ -710,6 +717,17 @@ const setMediaDescription = ({ id, description, credentials }) => { }).then((data) => parseAttachment(data)) } +const importMutes = ({ file, credentials }) => { + const formData = new FormData() + formData.append('list', file) + return fetch(MUTES_IMPORT_URL, { + body: formData, + method: 'POST', + headers: authHeaders(credentials) + }) + .then((response) => response.ok) +} + const importBlocks = ({ file, credentials }) => { const formData = new FormData() formData.append('list', file) @@ -1196,7 +1214,7 @@ const chatMessages = ({ id, credentials, maxId, sinceId, limit = 20 }) => { }) } -const sendChatMessage = ({ id, content, mediaId = null, credentials }) => { +const sendChatMessage = ({ id, content, mediaId = null, idempotencyKey, credentials }) => { const payload = { 'content': content } @@ -1205,11 +1223,18 @@ const sendChatMessage = ({ id, content, mediaId = null, credentials }) => { payload['media_id'] = mediaId } + const headers = {} + + if (idempotencyKey) { + headers['idempotency-key'] = idempotencyKey + } + return promisedRequest({ url: PLEROMA_CHAT_MESSAGES_URL(id), method: 'POST', payload: payload, - credentials + credentials, + headers }) } @@ -1280,6 +1305,7 @@ const apiService = { getCaptcha, updateProfileImages, updateProfile, + importMutes, importBlocks, importFollows, deleteAccount, diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js @@ -3,9 +3,10 @@ import _ from 'lodash' const empty = (chatId) => { return { idIndex: {}, + idempotencyKeyIndex: {}, messages: [], newMessageCount: 0, - lastSeenTimestamp: 0, + lastSeenMessageId: '0', chatId: chatId, minId: undefined, maxId: undefined @@ -13,10 +14,20 @@ const empty = (chatId) => { } const clear = (storage) => { - storage.idIndex = {} - storage.messages.splice(0, storage.messages.length) + const failedMessageIds = [] + + for (const message of storage.messages) { + if (message.error) { + failedMessageIds.push(message.id) + } else { + delete storage.idIndex[message.id] + delete storage.idempotencyKeyIndex[message.id] + } + } + + storage.messages = storage.messages.filter(m => failedMessageIds.includes(m.id)) storage.newMessageCount = 0 - storage.lastSeenTimestamp = 0 + storage.lastSeenMessageId = '0' storage.minId = undefined storage.maxId = undefined } @@ -37,6 +48,25 @@ const deleteMessage = (storage, messageId) => { } } +const handleMessageError = (storage, fakeId, isRetry) => { + if (!storage) { return } + const fakeMessage = storage.idIndex[fakeId] + if (fakeMessage) { + fakeMessage.error = true + fakeMessage.pending = false + if (!isRetry) { + // Ensure the failed message doesn't stay at the bottom of the list. + const lastPersistedMessage = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'desc'])[0] + if (lastPersistedMessage) { + const oldId = fakeMessage.id + fakeMessage.id = `${lastPersistedMessage.id}-${new Date().getTime()}` + storage.idIndex[fakeMessage.id] = fakeMessage + delete storage.idIndex[oldId] + } + } + } +} + const add = (storage, { messages: newMessages, updateMaxId = true }) => { if (!storage) { return } for (let i = 0; i < newMessages.length; i++) { @@ -45,7 +75,25 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => { // sanity check if (message.chat_id !== storage.chatId) { return } - if (!storage.minId || message.id < storage.minId) { + if (message.fakeId) { + const fakeMessage = storage.idIndex[message.fakeId] + if (fakeMessage) { + // In case the same id exists (chat update before POST response) + // make sure to remove the older duplicate message. + if (storage.idIndex[message.id]) { + delete storage.idIndex[message.id] + storage.messages = storage.messages.filter(msg => msg.id !== message.id) + } + Object.assign(fakeMessage, message, { error: false }) + delete fakeMessage['fakeId'] + storage.idIndex[fakeMessage.id] = fakeMessage + delete storage.idIndex[message.fakeId] + + return + } + } + + if (!storage.minId || (!message.pending && message.id < storage.minId)) { storage.minId = message.id } @@ -55,20 +103,26 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => { } } - if (!storage.idIndex[message.id]) { - if (storage.lastSeenTimestamp < message.created_at) { + if (!storage.idIndex[message.id] && !isConfirmation(storage, message)) { + if (storage.lastSeenMessageId < message.id) { storage.newMessageCount++ } - storage.messages.push(message) storage.idIndex[message.id] = message + storage.messages.push(storage.idIndex[message.id]) + storage.idempotencyKeyIndex[message.idempotency_key] = true } } } +const isConfirmation = (storage, message) => { + if (!message.idempotency_key) return + return storage.idempotencyKeyIndex[message.idempotency_key] +} + const resetNewMessageCount = (storage) => { if (!storage) { return } storage.newMessageCount = 0 - storage.lastSeenTimestamp = new Date() + storage.lastSeenMessageId = storage.maxId } // Inserts date separators and marks the head and tail if it's the chain of messages made by the same user @@ -76,7 +130,7 @@ const getView = (storage) => { if (!storage) { return [] } const result = [] - const messages = _.sortBy(storage.messages, ['id', 'desc']) + const messages = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'asc']) const firstMessage = messages[0] let previousMessage = messages[messages.length - 1] let currentMessageChainId @@ -148,7 +202,8 @@ const ChatService = { getView, deleteMessage, resetNewMessageCount, - clear + clear, + handleMessageError } export default ChatService diff --git a/src/services/chat_utils/chat_utils.js b/src/services/chat_utils/chat_utils.js @@ -3,7 +3,7 @@ import { showDesktopNotification } from '../desktop_notification_utils/desktop_n export const maybeShowChatNotification = (store, chat) => { if (!chat.lastMessage) return if (store.rootState.chats.currentChatId === chat.id && !document.hidden) return - if (store.rootState.users.currentUser.id === chat.lastMessage.account.id) return + if (store.rootState.users.currentUser.id === chat.lastMessage.account_id) return const opts = { tag: chat.lastMessage.id, @@ -18,3 +18,24 @@ export const maybeShowChatNotification = (store, chat) => { showDesktopNotification(store.rootState, opts) } + +export const buildFakeMessage = ({ content, chatId, attachments, userId, idempotencyKey }) => { + const fakeMessage = { + content, + chat_id: chatId, + created_at: new Date(), + id: `${new Date().getTime()}`, + attachments: attachments, + account_id: userId, + idempotency_key: idempotencyKey, + emojis: [], + pending: true, + isNormalized: true + } + + if (attachments[0]) { + fakeMessage.attachment = attachments[0] + } + + return fakeMessage +} diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js @@ -429,6 +429,9 @@ export const parseChatMessage = (message) => { } else { output.attachments = [] } + output.pending = !!message.pending + output.error = false + output.idempotency_key = message.idempotency_key output.isNormalized = true return output } diff --git a/src/services/follow_request_fetcher/follow_request_fetcher.service.js b/src/services/follow_request_fetcher/follow_request_fetcher.service.js @@ -1,4 +1,5 @@ import apiService from '../api/api.service.js' +import { promiseInterval } from '../promise_interval/promise_interval.js' const fetchAndUpdate = ({ store, credentials }) => { return apiService.fetchFollowRequests({ credentials }) @@ -10,9 +11,9 @@ const fetchAndUpdate = ({ store, credentials }) => { } const startFetching = ({ credentials, store }) => { - fetchAndUpdate({ credentials, store }) const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store }) - return setInterval(boundFetchAndUpdate, 10000) + boundFetchAndUpdate() + return promiseInterval(boundFetchAndUpdate, 10000) } const followRequestFetcher = { diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -1,4 +1,5 @@ import apiService from '../api/api.service.js' +import { promiseInterval } from '../promise_interval/promise_interval.js' const update = ({ store, notifications, older }) => { store.dispatch('setNotificationsError', { value: false }) @@ -42,6 +43,7 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => { args['since'] = Math.max(...readNotifsIds) fetchNotifications({ store, args, older }) } + return result } } @@ -56,13 +58,13 @@ const fetchNotifications = ({ store, args, older }) => { } const startFetching = ({ credentials, store }) => { - fetchAndUpdate({ credentials, store }) - const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store }) // Initially there's set flag to silence all desktop notifications so // that there won't spam of them when user just opened up the FE we // reset that flag after a while to show new notifications once again. setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000) - return setInterval(boundFetchAndUpdate, 10000) + const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store }) + boundFetchAndUpdate() + return promiseInterval(boundFetchAndUpdate, 10000) } const notificationsFetcher = { diff --git a/src/services/promise_interval/promise_interval.js b/src/services/promise_interval/promise_interval.js @@ -0,0 +1,34 @@ + +// promiseInterval - replacement for setInterval for promises, starts counting +// the interval only after a promise is done instead of immediately. +// - promiseCall is a function that returns a promise, it's called the first +// time after the first interval. +// - interval is the interval delay in ms. + +export const promiseInterval = (promiseCall, interval) => { + let stopped = false + let timeout = null + + const func = () => { + const promise = promiseCall() + // something unexpected happened and promiseCall did not + // return a promise, abort the loop. + if (!(promise && promise.finally)) { + console.warn('promiseInterval: promise call did not return a promise, stopping interval.') + return + } + promise.finally(() => { + if (stopped) return + timeout = window.setTimeout(func, interval) + }) + } + + const stopFetcher = () => { + stopped = true + window.clearTimeout(timeout) + } + + timeout = window.setTimeout(func, interval) + + return { stop: stopFetcher } +} diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -1,6 +1,7 @@ import { camelCase } from 'lodash' import apiService from '../api/api.service.js' +import { promiseInterval } from '../promise_interval/promise_interval.js' const update = ({ store, statuses, timeline, showImmediately, userId, pagination }) => { const ccTimeline = camelCase(timeline) @@ -71,8 +72,9 @@ const startFetching = ({ timeline = 'friends', credentials, store, userId = fals const showImmediately = timelineData.visibleStatuses.length === 0 timelineData.userId = userId fetchAndUpdate({ timeline, credentials, store, showImmediately, userId, tag }) - const boundFetchAndUpdate = () => fetchAndUpdate({ timeline, credentials, store, userId, tag }) - return setInterval(boundFetchAndUpdate, 10000) + const boundFetchAndUpdate = () => + fetchAndUpdate({ timeline, credentials, store, userId, tag }) + return promiseInterval(boundFetchAndUpdate, 10000) } const timelineFetcher = { fetchAndUpdate, diff --git a/static/config.json b/static/config.json @@ -10,9 +10,10 @@ "hideSitename": false, "hideUserStats": false, "loginMethod": "password", - "logo": "/static/logo.png", + "logo": "/static/logo.svg", "logoMargin": ".1em", "logoMask": true, + "logoLeft": false, "minimalScopesMode": false, "nsfwCensorImage": "", "postContentType": "text/plain", diff --git a/static/fontello.json b/static/fontello.json @@ -1,416 +0,0 @@ -{ - "name": "", - "css_prefix_text": "icon-", - "css_use_suffix": false, - "hinting": true, - "units_per_em": 1000, - "ascent": 857, - "glyphs": [ - { - "uid": "9bd60140934a1eb9236fd7a8ab1ff6ba", - "css": "spin4", - "code": 59444, - "src": "fontelico" - }, - { - "uid": "5211af474d3a9848f67f945e2ccaf143", - "css": "cancel", - "code": 59392, - "src": "fontawesome" - }, - { - "uid": "eeec3208c90b7b48e804919d0d2d4a41", - "css": "upload", - "code": 59393, - "src": "fontawesome" - }, - { - "uid": "2a6740fc2f9d0edea54205963f662594", - "css": "spin3", - "code": 59442, - "src": "fontelico" - }, - { - "uid": "c6be5a58ee4e63a5ec399c2b0d15cf2c", - "css": "reply", - "code": 61714, - "src": "fontawesome" - }, - { - "uid": "474656633f79ea2f1dad59ff63f6bf07", - "css": "star", - "code": 59394, - "src": "fontawesome" - }, - { - "uid": "d17030afaecc1e1c22349b99f3c4992a", - "css": "star-empty", - "code": 59395, - "src": "fontawesome" - }, - { - "uid": "09feb4465d9bd1364f4e301c9ddbaa92", - "css": "retweet", - "code": 59396, - "src": "fontawesome" - }, - { - "uid": "7fd683b2c518ceb9e5fa6757f2276faa", - "css": "eye-off", - "code": 59397, - "src": "fontawesome" - }, - { - "uid": "73ffeb70554099177620847206c12457", - "css": "binoculars", - "code": 61925, - "src": "fontawesome" - }, - { - "uid": "e99461abfef3923546da8d745372c995", - "css": "cog", - "code": 59399, - "src": "fontawesome" - }, - { - "uid": "1bafeeb1808a5fe24484c7890096901a", - "css": "user-plus", - "code": 62004, - "src": "fontawesome" - }, - { - "uid": "559647a6f430b3aeadbecd67194451dd", - "css": "menu", - "code": 61641, - "src": "fontawesome" - }, - { - "uid": "0d20938846444af8deb1920dc85a29fb", - "css": "logout", - "code": 59400, - "src": "fontawesome" - }, - { - "uid": "ccddff8e8670dcd130e3cb55fdfc2fd0", - "css": "down-open", - "code": 59401, - "src": "fontawesome" - }, - { - "uid": "44b9e75612c5fad5505edd70d071651f", - "css": "attach", - "code": 59402, - "src": "entypo" - }, - { - "uid": "e15f0d620a7897e2035c18c80142f6d9", - "css": "link-ext", - "code": 61582, - "src": "fontawesome" - }, - { - "uid": "e35de5ea31cd56970498e33efbcb8e36", - "css": "link-ext-alt", - "code": 61583, - "src": "fontawesome" - }, - { - "uid": "381da2c2f7fd51f8de877c044d7f439d", - "css": "picture", - "code": 59403, - "src": "fontawesome" - }, - { - "uid": "872d9516df93eb6b776cc4d94bd97dac", - "css": "video", - "code": 59404, - "src": "fontawesome" - }, - { - "uid": "399ef63b1e23ab1b761dfbb5591fa4da", - "css": "right-open", - "code": 59405, - "src": "fontawesome" - }, - { - "uid": "d870630ff8f81e6de3958ecaeac532f2", - "css": "left-open", - "code": 59406, - "src": "fontawesome" - }, - { - "uid": "fe6697b391355dec12f3d86d6d490397", - "css": "up-open", - "code": 59407, - "src": "fontawesome" - }, - { - "uid": "9c1376672bb4f1ed616fdd78a23667e9", - "css": "comment-empty", - "code": 61669, - "src": "fontawesome" - }, - { - "uid": "ccc2329632396dc096bb638d4b46fb98", - "css": "mail-alt", - "code": 61664, - "src": "fontawesome" - }, - { - "uid": "c1f1975c885aa9f3dad7810c53b82074", - "css": "lock", - "code": 59409, - "src": "fontawesome" - }, - { - "uid": "05376be04a27d5a46e855a233d6e8508", - "css": "lock-open-alt", - "code": 61758, - "src": "fontawesome" - }, - { - "uid": "197375a3cea8cb90b02d06e4ddf1433d", - "css": "globe", - "code": 59410, - "src": "fontawesome" - }, - { - "uid": "b3a9e2dab4d19ea3b2f628242c926bfe", - "css": "brush", - "code": 59411, - "src": "iconic" - }, - { - "uid": "9dd9e835aebe1060ba7190ad2b2ed951", - "css": "search", - "code": 59398, - "src": "fontawesome" - }, - { - "uid": "ca90da02d2c6a3183f2458e4dc416285", - "css": "adjust", - "code": 59414, - "src": "fontawesome" - }, - { - "uid": "5e2ab018e3044337bcef5f7e94098ea1", - "css": "thumbs-up-alt", - "code": 61796, - "src": "fontawesome" - }, - { - "uid": "c76b7947c957c9b78b11741173c8349b", - "css": "attention", - "code": 59412, - "src": "fontawesome" - }, - { - "uid": "1a5cfa186647e8c929c2b17b9fc4dac1", - "css": "plus-squared", - "code": 61694, - "src": "fontawesome" - }, - { - "uid": "44e04715aecbca7f266a17d5a7863c68", - "css": "plus", - "code": 59413, - "src": "fontawesome" - }, - { - "uid": "41087bc74d4b20b55059c60a33bf4008", - "css": "edit", - "code": 59415, - "src": "fontawesome" - }, - { - "uid": "5717236f6134afe2d2a278a5c9b3927a", - "css": "play-circled", - "code": 61764, - "src": "fontawesome" - }, - { - "uid": "d35a1d35efeb784d1dc9ac18b9b6c2b6", - "css": "pencil", - "code": 59416, - "src": "fontawesome" - }, - { - "uid": "266d5d9adf15a61800477a5acf9a4462", - "css": "chart-bar", - "code": 59419, - "src": "fontawesome" - }, - { - "uid": "d862a10e1448589215be19702f98f2c1", - "css": "smile", - "code": 61720, - "src": "fontawesome" - }, - { - "uid": "671f29fa10dda08074a4c6a341bb4f39", - "css": "bell-alt", - "code": 61683, - "src": "fontawesome" - }, - { - "uid": "5bb103cd29de77e0e06a52638527b575", - "css": "wrench", - "code": 59418, - "src": "fontawesome" - }, - { - "uid": "5b0772e9484a1a11646793a82edd622a", - "css": "pin", - "code": 59417, - "src": "fontawesome" - }, - { - "uid": "22411a88489225a018f68db737de3c77", - "css": "ellipsis", - "code": 61761, - "src": "custom_icons", - "selected": true, - "svg": { - "path": "M214 411V518Q214 540 199 556T161 571H54Q31 571 16 556T0 518V411Q0 388 16 373T54 357H161Q183 357 199 373T214 411ZM500 411V518Q500 540 484 556T446 571H339Q317 571 301 556T286 518V411Q286 388 301 373T339 357H446Q469 357 484 373T500 411ZM786 411V518Q786 540 770 556T732 571H625Q603 571 587 556T571 518V411Q571 388 587 373T625 357H732Q755 357 770 373T786 411Z", - "width": 785.7 - }, - "search": [ - "ellipsis" - ] - }, - { - "uid": "0bef873af785ead27781fdf98b3ae740", - "css": "bell-ringing-o", - "code": 59408, - "src": "custom_icons", - "selected": true, - "svg": { - "path": "M497.8 0C468.3 0 444.4 23.9 444.4 53.3 444.4 61.1 446.1 68.3 448.9 75 301.7 96.7 213.3 213.3 213.3 320 213.3 588.3 117.8 712.8 35.6 782.2 35.6 821.1 67.8 853.3 106.7 853.3H355.6C355.6 931.7 419.4 995.6 497.8 995.6S640 931.7 640 853.3H888.9C927.8 853.3 960 821.1 960 782.2 877.8 712.8 782.2 588.3 782.2 320 782.2 213.3 693.9 96.7 546.7 75 549.4 68.3 551.1 61.1 551.1 53.3 551.1 23.9 527.2 0 497.8 0ZM189.4 44.8C108.4 118.6 70.5 215.1 71.1 320.2L142.2 319.8C141.7 231.2 170.4 158.3 237.3 97.4L189.4 44.8ZM806.2 44.8L758.3 97.4C825.2 158.3 853.9 231.2 853.3 319.8L924.4 320.2C925.1 215.1 887.2 118.6 806.2 44.8ZM408.9 844.4C413.9 844.4 417.8 848.3 417.8 853.3 417.8 897.2 453.9 933.3 497.8 933.3 502.8 933.3 506.7 937.2 506.7 942.2S502.8 951.1 497.8 951.1C443.9 951.1 400 907.2 400 853.3 400 848.3 403.9 844.4 408.9 844.4Z", - "width": 1000 - }, - "search": [ - "bell-ringing-o" - ] - }, - { - "uid": "0b2b66e526028a6972d51a6f10281b4b", - "css": "zoom-in", - "code": 59420, - "src": "fontawesome" - }, - { - "uid": "0bda4bc779d4c32623dec2e43bd67ee8", - "css": "gauge", - "code": 61668, - "src": "fontawesome" - }, - { - "uid": "31972e4e9d080eaa796290349ae6c1fd", - "css": "users", - "code": 59421, - "src": "fontawesome" - }, - { - "uid": "e82cedfa1d5f15b00c5a81c9bd731ea2", - "css": "info-circled", - "code": 59423, - "src": "fontawesome" - }, - { - "uid": "w3nzesrlbezu6f30q7ytyq919p6gdlb6", - "css": "home-2", - "code": 59425, - "src": "typicons" - }, - { - "uid": "dcedf50ab1ede3283d7a6c70e2fe32f3", - "css": "chat", - "code": 59422, - "src": "fontawesome" - }, - { - "uid": "3a00327e61b997b58518bd43ed83c3df", - "css": "login", - "code": 59424, - "src": "fontawesome" - }, - { - "uid": "f3ebd6751c15a280af5cc5f4a764187d", - "css": "arrow-curved", - "code": 59426, - "src": "iconic" - }, - { - "uid": "0ddd3e8201ccc7d41f7b7c9d27eca6c1", - "css": "link", - "code": 59427, - "src": "fontawesome" - }, - { - "uid": "4aad6bb50b02c18508aae9cbe14e784e", - "css": "share", - "code": 61920, - "src": "fontawesome" - }, - { - "uid": "8b80d36d4ef43889db10bc1f0dc9a862", - "css": "user", - "code": 59428, - "src": "fontawesome" - }, - { - "uid": "12f4ece88e46abd864e40b35e05b11cd", - "css": "ok", - "code": 59431, - "src": "fontawesome" - }, - { - "uid": "4109c474ff99cad28fd5a2c38af2ec6f", - "css": "filter", - "code": 61616, - "src": "fontawesome" - }, - { - "uid": "9a76bc135eac17d2c8b8ad4a5774fc87", - "css": "download", - "code": 59429, - "src": "fontawesome" - }, - { - "uid": "f04a5d24e9e659145b966739c4fde82a", - "css": "bookmark", - "code": 59430, - "src": "fontawesome" - }, - { - "uid": "2f5ef6f6b7aaebc56458ab4e865beff5", - "css": "bookmark-empty", - "code": 61591, - "src": "fontawesome" - }, - { - "uid": "9ea0a737ccc45d6c510dcbae56058849", - "css": "music", - "code": 59432, - "src": "fontawesome" - }, - { - "uid": "1b5a5d7b7e3c71437f5a26befdd045ed", - "css": "doc", - "code": 59433, - "src": "fontawesome" - }, - { - "uid": "98d9c83c1ee7c2c25af784b518c522c5", - "css": "block", - "code": 59434, - "src": "fontawesome" - }, - { - "uid": "3e674995cacc2b09692c096ea7eb6165", - "css": "megaphone", - "code": 59435, - "src": "fontawesome" - } - ] -} -\ No newline at end of file diff --git a/static/logo.png b/static/logo.png Binary files differ. diff --git a/static/logo.svg b/static/logo.svg @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + id="svg4485" + width="512" + height="512" + viewBox="0 0 512 512" + sodipodi:docname="logo.svg" + inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"> + <metadata + id="metadata4491"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs4489" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1274" + inkscape:window-height="1410" + id="namedview4487" + showgrid="false" + inkscape:zoom="1.2636719" + inkscape:cx="305.99333" + inkscape:cy="304.30809" + inkscape:window-x="1280" + inkscape:window-y="22" + inkscape:window-maximized="0" + inkscape:current-layer="g4612" + inkscape:document-rotation="0" /> + <g + id="g4612"> + <g + id="g850" + transform="matrix(0.99659595,0,0,0.99659595,0.37313949,0.87143746)"> + <path + style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.175879" + d="m 194.75841,124.65165 a 20.449443,20.449443 0 0 0 -20.44944,20.44945 v 242.24725 h 65.28091 v -262.6967 z" + id="path4497" /> + <path + style="fill:#fba457;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 272.6236,124.65165 V 256 h 45.61799 a 20.449443,20.449443 0 0 0 20.44944,-20.44945 v -110.8989 z" + id="path4516" /> + <path + style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 272.6236,322.06744 v 65.28091 h 45.61799 a 20.449443,20.449443 0 0 0 20.44944,-20.44945 v -44.83146 z" + id="path4516-5" /> + </g> + </g> +</svg> diff --git a/static/terms-of-service.html b/static/terms-of-service.html @@ -6,4 +6,4 @@ Pleroma install containing the real ToS for your instance.</p> <p>See the <a href='https://docs.pleroma.social/backend/configuration/static_dir/'>Pleroma documentation</a> for more information.</p> <br> -<img src="/static/logo.png" style="display: block; margin: auto; max-width: 100%; height: 50px; object-fit: contain;" /> +<img src="/static/logo.svg" style="display: block; margin: auto; max-width: 100%; height: 50px; object-fit: contain;" /> diff --git a/test/unit/specs/services/chat_service/chat_service.spec.js b/test/unit/specs/services/chat_service/chat_service.spec.js @@ -2,17 +2,20 @@ import chatService from '../../../../../src/services/chat_service/chat_service.j const message1 = { id: '9wLkdcmQXD21Oy8lEX', + idempotency_key: '1', created_at: (new Date('2020-06-22T18:45:53.000Z')) } const message2 = { id: '9wLkdp6ihaOVdNj8Wu', + idempotency_key: '2', account_id: '9vmRb29zLQReckr5ay', created_at: (new Date('2020-06-22T18:45:56.000Z')) } const message3 = { id: '9wLke9zL4Dy4OZR2RM', + idempotency_key: '3', account_id: '9vmRb29zLQReckr5ay', created_at: (new Date('2020-07-22T18:45:59.000Z')) } @@ -44,10 +47,10 @@ describe('chatService', () => { chatService.resetNewMessageCount(chat) expect(chat.newMessageCount).to.eql(0) + expect(chat.lastSeenMessageId).to.eql(message2.id) - const createdAt = new Date() - createdAt.setSeconds(createdAt.getSeconds() + 10) - chatService.add(chat, { messages: [ { message3, created_at: createdAt } ] }) + // Add message with higher id + chatService.add(chat, { messages: [ message3 ] }) expect(chat.newMessageCount).to.eql(1) }) }) diff --git a/yarn.lock b/yarn.lock @@ -884,6 +884,37 @@ dependencies: qrcode "^1.3.0" +"@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@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-2.0.0.tgz#63da3e459147cebb0a8d58eed81d6071db9f5973" + integrity sha512-N3VKw7KzRfOm8hShUVldpinlm13HpvLBQgT63QS+aCrIRLwjoEUXY5Rcmttbfb6HkzZaeqjLqd/aZCQ53UjQpg== + "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" @@ -1151,13 +1182,6 @@ agent-base@2: extend "~3.0.0" semver "~5.0.1" -agent-base@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" - integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== - dependencies: - es6-promisify "^5.0.0" - ajv-errors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" @@ -1663,11 +1687,6 @@ better-assert@~1.0.0: dependencies: callsite "1.0.0" -big-integer@^1.6.17: - version "1.6.48" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" - integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== - big.js@^3.1.3: version "3.2.0" resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" @@ -1680,14 +1699,6 @@ binary-extensions@^1.0.0: version "1.12.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14" -binary@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" - integrity sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk= - dependencies: - buffers "~0.1.1" - chainsaw "~0.1.0" - blob@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" @@ -1700,11 +1711,6 @@ bluebird@^3.5.3: version "3.5.4" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.4.tgz#d6cc661595de30d5b3af5fcedd3c0b3ef6ec5714" -bluebird@~3.4.1: - version "3.4.7" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" - integrity sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM= - bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" @@ -1882,11 +1888,6 @@ buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" -buffer-indexof-polyfill@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz#a9fb806ce8145d5428510ce72f278bb363a638bf" - integrity sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8= - buffer-xor@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" @@ -1899,11 +1900,6 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffers@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" - integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s= - builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -2070,13 +2066,6 @@ chai@^3.5.0: deep-eql "^0.1.3" type-detect "^1.0.0" -chainsaw@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" - integrity sha1-XqtQsor+WAdNDVgpE4iCi15fvJg= - dependencies: - traverse ">=0.3.0 <0.4" - 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" @@ -3012,13 +3001,6 @@ domutils@^1.5.1: dom-serializer "0" domelementtype "1" -duplexer2@~0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" - integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= - dependencies: - readable-stream "^2.0.2" - duplexify@^3.4.2, duplexify@^3.6.0: version "3.7.1" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" @@ -3086,13 +3068,6 @@ encodeurl@~1.0.1, encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" -encoding@^0.1.11: - version "0.1.12" - resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" - integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s= - dependencies: - iconv-lite "~0.4.13" - end-of-stream@^1.0.0, end-of-stream@^1.1.0: version "1.4.1" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" @@ -3183,18 +3158,6 @@ es-to-primitive@^1.2.0: is-date-object "^1.0.1" is-symbol "^1.0.2" -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" - integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= - dependencies: - es6-promise "^4.0.3" - escalade@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4" @@ -3800,18 +3763,6 @@ follow-redirects@^1.0.0: dependencies: debug "=3.1.0" -"fontello-webpack-plugin@https://github.com/w3geo/fontello-webpack-plugin.git#6149eac8f2672ab6da089e8802fbfcac98487186": - version "0.4.8" - resolved "https://github.com/w3geo/fontello-webpack-plugin.git#6149eac8f2672ab6da089e8802fbfcac98487186" - dependencies: - form-data "^2.1.2" - html-webpack-plugin "^3.2.0" - https-proxy-agent "^2.1.1" - lodash "^4.17.4" - node-fetch "^1.6.3" - unzipper "^0.10.5" - webpack-sources "^0.2.0" - for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -3826,15 +3777,6 @@ forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" -form-data@^2.1.2: - version "2.5.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" - integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -3896,16 +3838,6 @@ fsevents@^1.2.7: nan "^2.12.1" node-pre-gyp "^0.12.0" -fstream@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" - integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" - ftp@~0.3.10: version "0.3.10" resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d" @@ -4122,11 +4054,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: version "4.1.15" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" -graceful-fs@^4.2.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== - "graceful-readlink@>= 1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" @@ -4310,7 +4237,7 @@ 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: +html-webpack-plugin@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b" dependencies: @@ -4397,21 +4324,13 @@ https-proxy-agent@1: debug "2" extend "3" -https-proxy-agent@^2.1.1: - version "2.2.4" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" - integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== - dependencies: - agent-base "^4.3.0" - debug "^3.1.0" - iconv-lite@0.4.23, iconv-lite@^0.4.4: version "0.4.23" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.4.24, iconv-lite@~0.4.13: +iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" dependencies: @@ -4534,7 +4453,7 @@ inherits@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" -inherits@^2.0.0, inherits@~2.0.0: +inherits@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -4852,7 +4771,7 @@ is-regexp@^2.0.0: resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d" integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA== -is-stream@^1.0.1, is-stream@^1.1.0: +is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -5250,11 +5169,6 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -listenercount@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" - integrity sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc= - load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -5974,7 +5888,7 @@ 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.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.0, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: @@ -6121,14 +6035,6 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" -node-fetch@^1.6.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" - integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== - dependencies: - encoding "^0.1.11" - is-stream "^1.0.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" @@ -7807,13 +7713,6 @@ rfdc@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" -rimraf@2: - 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@2.6.3, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" @@ -7987,7 +7886,7 @@ set-value@^2.0.0: is-plain-object "^2.0.3" split-string "^3.0.1" -setimmediate@^1.0.4, setimmediate@~1.0.4: +setimmediate@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= @@ -8159,11 +8058,6 @@ sort-keys@^1.0.0: dependencies: is-plain-obj "^1.0.0" -source-list-map@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-1.1.2.tgz#9889019d1024cce55cdc069498337ef6186a11a1" - integrity sha1-mIkBnRAkzOVc3AaUmDN+9hhqEaE= - source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -8203,7 +8097,7 @@ 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.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.3: +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" @@ -8760,11 +8654,6 @@ tough-cookie@~2.4.3: psl "^1.1.24" punycode "^1.4.1" -"traverse@>=0.3.0 <0.4": - version "0.3.9" - resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" - integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk= - trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -9003,22 +8892,6 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" -unzipper@^0.10.5: - version "0.10.5" - resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.5.tgz#4d189ae6f8af634b26efe1a1817c399e0dd4a1a0" - integrity sha512-i5ufkXNjWZYxU/0nKKf6LkvW8kn9YzRvfwuPWjXP+JTFce/8bqeR0gEfbiN2IDdJa6ZU6/2IzFRLK0z1v0uptw== - dependencies: - big-integer "^1.6.17" - binary "~0.3.0" - bluebird "~3.4.1" - buffer-indexof-polyfill "~1.0.0" - duplexer2 "~0.1.4" - fstream "^1.0.12" - graceful-fs "^4.2.2" - listenercount "~1.0.1" - readable-stream "~2.3.6" - setimmediate "~1.0.4" - upath@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" @@ -9283,14 +9156,6 @@ webpack-merge@^0.14.1: lodash.isplainobject "^3.2.0" lodash.merge "^3.3.2" -webpack-sources@^0.2.0: - version "0.2.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.2.3.tgz#17c62bfaf13c707f9d02c479e0dcdde8380697fb" - integrity sha1-F8Yr+vE8cH+dAsR54Nzd6DgGl/s= - dependencies: - source-list-map "^1.1.1" - source-map "~0.5.3" - webpack-sources@^1.1.0, webpack-sources@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85"