aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.js74
-rw-r--r--src/App.scss247
-rw-r--r--src/App.vue72
-rw-r--r--src/boot/after_store.js4
-rw-r--r--src/components/account_actions/account_actions.js10
-rw-r--r--src/components/account_actions/account_actions.vue50
-rw-r--r--src/components/async_component_error/async_component_error.vue2
-rw-r--r--src/components/attachment/attachment.js26
-rw-r--r--src/components/attachment/attachment.vue46
-rw-r--r--src/components/block_card/block_card.vue4
-rw-r--r--src/components/chat/chat.js111
-rw-r--r--src/components/chat/chat.scss24
-rw-r--r--src/components/chat/chat.vue11
-rw-r--r--src/components/chat/chat_layout_utils.js7
-rw-r--r--src/components/chat_list/chat_list.vue5
-rw-r--r--src/components/chat_list_item/chat_list_item.vue12
-rw-r--r--src/components/chat_message/chat_message.js10
-rw-r--r--src/components/chat_message/chat_message.scss18
-rw-r--r--src/components/chat_message/chat_message.vue9
-rw-r--r--src/components/chat_message_date/chat_message_date.vue4
-rw-r--r--src/components/chat_new/chat_new.js10
-rw-r--r--src/components/chat_new/chat_new.scss8
-rw-r--r--src/components/chat_new/chat_new.vue10
-rw-r--r--src/components/chat_panel/chat_panel.js10
-rw-r--r--src/components/chat_panel/chat_panel.vue12
-rw-r--r--src/components/contrast_ratio/contrast_ratio.vue26
-rw-r--r--src/components/conversation/conversation.vue20
-rw-r--r--src/components/desktop_nav/desktop_nav.js89
-rw-r--r--src/components/desktop_nav/desktop_nav.scss117
-rw-r--r--src/components/desktop_nav/desktop_nav.vue81
-rw-r--r--src/components/domain_mute_card/domain_mute_card.vue4
-rw-r--r--src/components/emoji_input/emoji_input.js44
-rw-r--r--src/components/emoji_input/emoji_input.vue10
-rw-r--r--src/components/emoji_input/suggestor.js140
-rw-r--r--src/components/emoji_picker/emoji_picker.js16
-rw-r--r--src/components/emoji_picker/emoji_picker.scss2
-rw-r--r--src/components/emoji_picker/emoji_picker.vue10
-rw-r--r--src/components/emoji_reactions/emoji_reactions.vue2
-rw-r--r--src/components/export_import/export_import.vue4
-rw-r--r--src/components/exporter/exporter.js7
-rw-r--r--src/components/exporter/exporter.vue10
-rw-r--r--src/components/extra_buttons/extra_buttons.js27
-rw-r--r--src/components/extra_buttons/extra_buttons.vue108
-rw-r--r--src/components/favorite_button/favorite_button.js17
-rw-r--r--src/components/favorite_button/favorite_button.vue70
-rw-r--r--src/components/features_panel/features_panel.js5
-rw-r--r--src/components/features_panel/features_panel.vue1
-rw-r--r--src/components/follow_button/follow_button.vue2
-rw-r--r--src/components/follow_request_card/follow_request_card.vue4
-rw-r--r--src/components/font_control/font_control.js8
-rw-r--r--src/components/font_control/font_control.vue5
-rw-r--r--src/components/global_notice_list/global_notice_list.js8
-rw-r--r--src/components/global_notice_list/global_notice_list.vue24
-rw-r--r--src/components/image_cropper/image_cropper.js21
-rw-r--r--src/components/image_cropper/image_cropper.vue21
-rw-r--r--src/components/importer/importer.js11
-rw-r--r--src/components/importer/importer.vue16
-rw-r--r--src/components/interface_language_switcher/interface_language_switcher.vue40
-rw-r--r--src/components/link-preview/link-preview.js15
-rw-r--r--src/components/link-preview/link-preview.vue17
-rw-r--r--src/components/login_form/login_form.js8
-rw-r--r--src/components/login_form/login_form.vue7
-rw-r--r--src/components/media_modal/media_modal.js10
-rw-r--r--src/components/media_modal/media_modal.vue10
-rw-r--r--src/components/media_upload/media_upload.js8
-rw-r--r--src/components/media_upload/media_upload.vue61
-rw-r--r--src/components/mfa_form/recovery_form.js8
-rw-r--r--src/components/mfa_form/recovery_form.vue19
-rw-r--r--src/components/mfa_form/totp_form.js9
-rw-r--r--src/components/mfa_form/totp_form.vue20
-rw-r--r--src/components/mobile_nav/mobile_nav.js12
-rw-r--r--src/components/mobile_nav/mobile_nav.vue280
-rw-r--r--src/components/mobile_post_status_button/mobile_post_status_button.js8
-rw-r--r--src/components/mobile_post_status_button/mobile_post_status_button.vue6
-rw-r--r--src/components/moderation_tools/moderation_tools.vue28
-rw-r--r--src/components/mute_card/mute_card.vue4
-rw-r--r--src/components/nav_panel/nav_panel.js23
-rw-r--r--src/components/nav_panel/nav_panel.vue170
-rw-r--r--src/components/notification/notification.js22
-rw-r--r--src/components/notification/notification.scss32
-rw-r--r--src/components/notification/notification.vue64
-rw-r--r--src/components/notifications/notifications.js9
-rw-r--r--src/components/notifications/notifications.scss31
-rw-r--r--src/components/notifications/notifications.vue21
-rw-r--r--src/components/panel_loading/panel_loading.vue20
-rw-r--r--src/components/password_reset/password_reset.js8
-rw-r--r--src/components/password_reset/password_reset.vue8
-rw-r--r--src/components/poll/poll.vue5
-rw-r--r--src/components/poll/poll_form.js12
-rw-r--r--src/components/poll/poll_form.vue43
-rw-r--r--src/components/popover/popover.js13
-rw-r--r--src/components/popover/popover.vue16
-rw-r--r--src/components/post_status_form/post_status_form.js33
-rw-r--r--src/components/post_status_form/post_status_form.vue146
-rw-r--r--src/components/react_button/react_button.js14
-rw-r--r--src/components/react_button/react_button.vue30
-rw-r--r--src/components/registration/registration.vue2
-rw-r--r--src/components/remote_follow/remote_follow.vue2
-rw-r--r--src/components/reply_button/reply_button.js4
-rw-r--r--src/components/reply_button/reply_button.vue59
-rw-r--r--src/components/retweet_button/retweet_button.js11
-rw-r--r--src/components/retweet_button/retweet_button.vue87
-rw-r--r--src/components/scope_selector/scope_selector.js15
-rw-r--r--src/components/scope_selector/scope_selector.vue58
-rw-r--r--src/components/search/search.js10
-rw-r--r--src/components/search/search.vue10
-rw-r--r--src/components/search_bar/search_bar.js14
-rw-r--r--src/components/search_bar/search_bar.vue96
-rw-r--r--src/components/settings_modal/settings_modal.vue4
-rw-r--r--src/components/settings_modal/settings_modal_content.js23
-rw-r--r--src/components/settings_modal/settings_modal_content.scss2
-rw-r--r--src/components/settings_modal/settings_modal_content.vue8
-rw-r--r--src/components/settings_modal/tabs/filtering_tab.js8
-rw-r--r--src/components/settings_modal/tabs/filtering_tab.vue6
-rw-r--r--src/components/settings_modal/tabs/general_tab.js14
-rw-r--r--src/components/settings_modal/tabs/general_tab.vue17
-rw-r--r--src/components/settings_modal/tabs/mutes_and_blocks_tab.vue10
-rw-r--r--src/components/settings_modal/tabs/notifications_tab.vue2
-rw-r--r--src/components/settings_modal/tabs/profile_tab.js68
-rw-r--r--src/components/settings_modal/tabs/profile_tab.scss13
-rw-r--r--src/components/settings_modal/tabs/profile_tab.vue79
-rw-r--r--src/components/settings_modal/tabs/security_tab/confirm.vue4
-rw-r--r--src/components/settings_modal/tabs/security_tab/mfa.vue10
-rw-r--r--src/components/settings_modal/tabs/security_tab/mfa_totp.vue4
-rw-r--r--src/components/settings_modal/tabs/security_tab/security_tab.js3
-rw-r--r--src/components/settings_modal/tabs/security_tab/security_tab.vue10
-rw-r--r--src/components/settings_modal/tabs/theme_tab/preview.vue47
-rw-r--r--src/components/settings_modal/tabs/theme_tab/theme_tab.js8
-rw-r--r--src/components/settings_modal/tabs/theme_tab/theme_tab.scss3
-rw-r--r--src/components/settings_modal/tabs/theme_tab/theme_tab.vue47
-rw-r--r--src/components/shadow_control/shadow_control.js14
-rw-r--r--src/components/shadow_control/shadow_control.vue33
-rw-r--r--src/components/side_drawer/side_drawer.js28
-rw-r--r--src/components/side_drawer/side_drawer.vue104
-rw-r--r--src/components/status/status.js44
-rw-r--r--src/components/status/status.scss61
-rw-r--r--src/components/status/status.vue112
-rw-r--r--src/components/status_content/status_content.js18
-rw-r--r--src/components/status_content/status_content.vue65
-rw-r--r--src/components/status_popover/status_popover.js6
-rw-r--r--src/components/status_popover/status_popover.vue6
-rw-r--r--src/components/still-image/still-image.vue11
-rw-r--r--src/components/tab_switcher/tab_switcher.js5
-rw-r--r--src/components/tab_switcher/tab_switcher.scss4
-rw-r--r--src/components/timeago/timeago.vue6
-rw-r--r--src/components/timeline/timeline.js37
-rw-r--r--src/components/timeline/timeline.vue51
-rw-r--r--src/components/timeline_menu/timeline_menu.js31
-rw-r--r--src/components/timeline_menu/timeline_menu.vue71
-rw-r--r--src/components/user_avatar/user_avatar.vue11
-rw-r--r--src/components/user_card/user_card.js16
-rw-r--r--src/components/user_card/user_card.vue93
-rw-r--r--src/components/user_list_popover/user_list_popover.js6
-rw-r--r--src/components/user_list_popover/user_list_popover.vue6
-rw-r--r--src/components/user_profile/user_profile.js8
-rw-r--r--src/components/user_profile/user_profile.vue13
-rw-r--r--src/components/user_reporting_modal/user_reporting_modal.js10
-rw-r--r--src/components/user_reporting_modal/user_reporting_modal.vue2
-rw-r--r--src/components/video_attachment/video_attachment.vue1
-rw-r--r--src/hocs/with_load_more/with_load_more.js34
-rw-r--r--src/hocs/with_subscription/with_subscription.js12
-rw-r--r--src/i18n/en.json20
-rw-r--r--src/i18n/eo.json123
-rw-r--r--src/i18n/es.json26
-rw-r--r--src/i18n/he.json8
-rw-r--r--src/i18n/it.json83
-rw-r--r--src/i18n/messages.js4
-rw-r--r--src/i18n/pl.json24
-rw-r--r--src/i18n/pt.json590
-rw-r--r--src/i18n/ru.json276
-rw-r--r--src/i18n/uk.json823
-rw-r--r--src/i18n/zh.json241
-rw-r--r--src/i18n/zh_Hant.json13
-rw-r--r--src/main.js5
-rw-r--r--src/modules/api.js18
-rw-r--r--src/modules/chats.js22
-rw-r--r--src/modules/config.js1
-rw-r--r--src/modules/instance.js7
-rw-r--r--src/modules/reports.js16
-rw-r--r--src/modules/statuses.js23
-rw-r--r--src/modules/users.js6
-rw-r--r--src/services/api/api.service.js26
-rw-r--r--src/services/chat_service/chat_service.js77
-rw-r--r--src/services/chat_utils/chat_utils.js23
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js39
-rw-r--r--src/services/favicon_service/favicon_service.js61
-rw-r--r--src/services/locale/locale.service.js12
-rw-r--r--src/services/notifications_fetcher/notifications_fetcher.service.js18
-rw-r--r--src/services/promise_interval/promise_interval.js9
-rw-r--r--src/services/style_setter/style_setter.js15
-rw-r--r--src/services/theme_data/pleromafe.js4
-rw-r--r--src/services/timeline_fetcher/timeline_fetcher.service.js18
192 files changed, 5276 insertions, 2048 deletions
diff --git a/src/App.js b/src/App.js
index ded772fa..1ca029b6 100644
--- 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,10 +10,12 @@ 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'
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
+import { mapGetters } from 'vuex'
export default {
name: 'app',
@@ -22,7 +23,6 @@ export default {
UserPanel,
NavPanel,
Notifications,
- SearchBar,
InstanceSpecificPanel,
FeaturesPanel,
WhoToFollowPanel,
@@ -31,21 +31,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
@@ -58,44 +51,21 @@ export default {
},
computed: {
currentUser () { return this.$store.state.users.currentUser },
- background () {
- return this.currentUser.background_image || this.$store.state.instance.background
+ userBackground () { return this.currentUser.background_image },
+ instanceBackground () {
+ return this.mergedConfig.hideInstanceWallpaper
+ ? null
+ : 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 },
+ background () { return this.userBackground || this.instanceBackground },
bgStyle () {
- return {
- 'background-image': `url(${this.background})`
- }
- },
- bgAppStyle () {
- return {
- '--body-background-image': `url(${this.background})`
+ if (this.background) {
+ return {
+ '--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 &&
@@ -109,22 +79,10 @@ export default {
return {
'order': this.$store.state.instance.sidebarRight ? 99 : 0
}
- }
+ },
+ ...mapGetters(['mergedConfig'])
},
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
index e1e1bdd0..8b91f3de 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -14,7 +14,9 @@
right: -20px;
background-size: cover;
background-repeat: no-repeat;
- background-position: 0 50%;
+ background-color: var(--wallpaper);
+ background-image: var(--body-background-image);
+ background-position: 50% 50px;
}
i[class^='icon-'] {
@@ -33,6 +35,7 @@ h4 {
max-width: 980px;
align-content: flex-start;
}
+
.underlay {
background-color: rgba(0,0,0,0.15);
background-color: var(--underlay, rgba(0,0,0,0.15));
@@ -69,7 +72,7 @@ a {
color: var(--link, $fallback--link);
}
-button {
+.button-default {
user-select: none;
color: $fallback--text;
color: var(--btnText, $fallback--text);
@@ -85,7 +88,8 @@ 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,6 +110,8 @@ button {
color: var(--btnPressedText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--btnPressed, $fallback--fg);
+
+ svg,
i {
color: $fallback--text;
color: var(--btnPressedText, $fallback--text);
@@ -118,6 +124,8 @@ button {
color: var(--btnDisabledText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--btnDisabled, $fallback--fg);
+
+ svg,
i {
color: $fallback--text;
color: var(--btnDisabledText, $fallback--text);
@@ -131,6 +139,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);
+
+ svg,
i {
color: $fallback--text;
color: var(--btnToggledText, $fallback--text);
@@ -146,6 +156,37 @@ button {
}
}
+.button-unstyled {
+ background: none;
+ border: none;
+ outline: none;
+ display: inline;
+ text-align: initial;
+ font-size: 100%;
+ font-family: inherit;
+ padding: 0;
+ line-height: unset;
+ cursor: pointer;
+ box-sizing: content-box;
+ color: inherit;
+
+ &.-link {
+ color: $fallback--link;
+ color: var(--link, $fallback--link);
+ }
+
+ &.-fullwidth {
+ width: 100%;
+ }
+
+ &.-hover-highlight {
+ &:hover svg {
+ color: $fallback--lightText;
+ color: var(--lightText, $fallback--lightText);
+ }
+ }
+}
+
input, textarea, .select, .input {
&.unstyled {
@@ -185,7 +226,7 @@ input, textarea, .select, .input {
opacity: 0.5;
}
- .icon-down-open {
+ .select-down-icon {
position: absolute;
top: 0;
bottom: 0;
@@ -300,6 +341,10 @@ input, textarea, .select, .input {
box-sizing: border-box;
}
}
+
+ &.resize-height {
+ resize: vertical;
+ }
}
option {
@@ -318,7 +363,7 @@ option {
}
}
-i[class*=icon-] {
+i[class*=icon-], .svg-inline--fa {
color: $fallback--icon;
color: var(--icon, $fallback--icon);
}
@@ -356,117 +401,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;
}
@@ -546,6 +484,7 @@ main-router {
color: $fallback--faint;
color: var(--panelFaint, $fallback--faint);
}
+
.faint-link {
color: $fallback--faint;
color: var(--faintLink, $fallback--faint);
@@ -557,11 +496,8 @@ main-router {
overflow-x: hidden;
}
- button {
- flex-shrink: 0;
- }
-
- button, .alert {
+ .button-default,
+ .alert {
// height: 100%;
line-height: 21px;
min-height: 0;
@@ -572,8 +508,11 @@ main-router {
align-self: stretch;
}
- button {
- &, i[class*=icon-] {
+ .button-default {
+ flex-shrink: 0;
+
+ &,
+ i[class*=icon-] {
color: $fallback--text;
color: var(--btnPanelText, $fallback--text);
}
@@ -596,7 +535,8 @@ main-router {
}
}
- a {
+ a,
+ .-link {
color: $fallback--link;
color: var(--panelLink, $fallback--link)
}
@@ -611,15 +551,15 @@ main-router {
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
-
.faint {
color: $fallback--faint;
color: var(--panelFaint, $fallback--faint);
}
- a {
+ a,
+ .-link {
color: $fallback--link;
- color: var(--panelLink, $fallback--link)
+ color: var(--panelLink, $fallback--link);
}
}
@@ -707,19 +647,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 +721,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 +742,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 {
@@ -898,7 +841,7 @@ nav {
}
}
-.btn.btn-default {
+.btn.button-default {
min-height: 28px;
}
@@ -930,22 +873,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;
- max-width: 10em;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
.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
index 0276c6a6..1a166778 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,82 +1,14 @@
<template>
<div
id="app"
- :style="bgAppStyle"
+ :style="bgStyle"
>
<div
id="app_bg_wrapper"
class="app-bg-wrapper"
- :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
index 00ca74a2..b472fcf6 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -7,6 +7,7 @@ import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
import { applyTheme } from '../services/style_setter/style_setter.js'
+import FaviconService from '../services/favicon_service/favicon_service.js'
let staticInitialResults = null
@@ -130,6 +131,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
? 0
: config.logoMargin
})
+ copyInstanceOption('logoLeft')
store.commit('authFlow/setInitialStrategy', config.loginMethod)
copyInstanceOption('redirectRootNoLogin')
@@ -325,6 +327,8 @@ const afterStoreSetup = async ({ store, i18n }) => {
const width = windowWidth()
store.dispatch('setMobileLayout', width <= 800)
+ FaviconService.initFaviconService()
+
const overrides = window.___pleromafe_dev_overrides || {}
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
store.dispatch('setInstanceOption', { name: 'server', value: server })
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js
index 6d345bc7..e53c4f77 100644
--- 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: [
@@ -27,7 +35,7 @@ const AccountActions = {
this.$store.dispatch('unblockUser', this.user.id)
},
reportUser () {
- this.$store.dispatch('openUserReportingModal', this.user.id)
+ this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
},
openChat () {
this.$router.push({
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index 987e94b7..ab5d1d29 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -1,9 +1,10 @@
<template>
- <div class="account-actions">
+ <div class="AccountActions">
<Popover
trigger="click"
placement="bottom"
:bound-to="{ x: 'container' }"
+ remove-padding
>
<div
slot="content"
@@ -13,14 +14,14 @@
<template v-if="relationship.following">
<button
v-if="relationship.showing_reblogs"
- class="btn btn-default dropdown-item"
+ class="btn button-default dropdown-item"
@click="hideRepeats"
>
{{ $t('user_card.hide_repeats') }}
</button>
<button
v-if="!relationship.showing_reblogs"
- class="btn btn-default dropdown-item"
+ class="btn button-default dropdown-item"
@click="showRepeats"
>
{{ $t('user_card.show_repeats') }}
@@ -32,27 +33,27 @@
</template>
<button
v-if="relationship.blocking"
- class="btn btn-default btn-block dropdown-item"
+ class="btn button-default btn-block dropdown-item"
@click="unblockUser"
>
{{ $t('user_card.unblock') }}
</button>
<button
v-else
- class="btn btn-default btn-block dropdown-item"
+ class="btn button-default btn-block dropdown-item"
@click="blockUser"
>
{{ $t('user_card.block') }}
</button>
<button
- class="btn btn-default btn-block dropdown-item"
+ class="btn button-default btn-block dropdown-item"
@click="reportUser"
>
{{ $t('user_card.report') }}
</button>
<button
v-if="pleromaChatMessagesAvailable"
- class="btn btn-default btn-block dropdown-item"
+ class="btn button-default btn-block dropdown-item"
@click="openChat"
>
{{ $t('user_card.message') }}
@@ -61,9 +62,12 @@
</div>
<div
slot="trigger"
- class="btn btn-default ellipsis-button"
+ class="ellipsis-button"
>
- <i class="icon-ellipsis trigger-button" />
+ <FAIcon
+ class="icon"
+ icon="ellipsis-v"
+ />
</div>
</Popover>
</div>
@@ -73,22 +77,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/async_component_error/async_component_error.vue b/src/components/async_component_error/async_component_error.vue
index b68b98f9..b1b59638 100644
--- a/src/components/async_component_error/async_component_error.vue
+++ b/src/components/async_component_error/async_component_error.vue
@@ -8,7 +8,7 @@
{{ $t('general.error_retry') }}
</p>
<button
- class="btn"
+ class="btn button-default"
@click="retry"
>
{{ $t('general.retry') }}
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index cb31020d..5f5779a0 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -3,6 +3,24 @@ 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,
+ faPlayCircle,
+ faTimes
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faFile,
+ faMusic,
+ faImage,
+ faVideo,
+ faPlayCircle,
+ faTimes
+)
const Attachment = {
props: [
@@ -39,10 +57,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
index 19c713d5..2c1c1682 100644
--- 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>
@@ -36,20 +36,19 @@
: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
+ <button
v-if="nsfw && hideNsfwLocal && !hidden"
- class="hider"
+ class="button-unstyled hider"
+ @click.prevent="toggleHidden"
>
- <a
- href="#"
- @click.prevent="toggleHidden"
- >Hide</a>
- </div>
+ <FAIcon icon="times" />
+ </button>
<a
v-if="type === 'image' && (!hidden || preloadImage)"
@@ -83,9 +82,10 @@
@play="$emit('play')"
@pause="$emit('pause')"
/>
- <i
+ <FAIcon
v-if="!allowPlay"
- class="play-icon icon-play-circled"
+ class="play-icon"
+ icon="play-circle"
/>
</a>
@@ -142,6 +142,10 @@
white-space: nowrap;
text-overflow: ellipsis;
max-width: 100%;
+
+ svg {
+ color: inherit;
+ }
}
.nsfw-placeholder {
@@ -228,15 +232,23 @@
.hider {
position: absolute;
right: 0;
- white-space: nowrap;
margin: 10px;
- padding: 5px;
- background: rgba(230,230,230,0.6);
- font-weight: bold;
+ padding: 0;
z-index: 4;
- line-height: 1;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
+ text-align: center;
+ width: 2em;
+ height: 2em;
+ font-size: 1.25em;
+ // TODO: theming? hard to theme with unknown background image color
+ background: rgba(230, 230, 230, 0.7);
+ .svg-inline--fa {
+ color: rgba(0, 0, 0, 0.6);
+ }
+ &:hover .svg-inline--fa {
+ color: rgba(0, 0, 0, 0.9);
+ }
}
video {
diff --git a/src/components/block_card/block_card.vue b/src/components/block_card/block_card.vue
index 5b00b738..2fe66d4c 100644
--- a/src/components/block_card/block_card.vue
+++ b/src/components/block_card/block_card.vue
@@ -3,7 +3,7 @@
<div class="block-card-content-container">
<button
v-if="blocked"
- class="btn btn-default"
+ class="btn button-default"
:disabled="progress"
@click="unblockUser"
>
@@ -16,7 +16,7 @@
</button>
<button
v-else
- class="btn btn-default"
+ class="btn button-default"
:disabled="progress"
@click="blockUser"
>
diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js
index 34e723d0..e57fcb91 100644
--- a/src/components/chat/chat.js
+++ b/src/components/chat/chat.js
@@ -6,11 +6,24 @@ 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 { promiseInterval } from '../../services/promise_interval/promise_interval.js'
-import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight } from './chat_layout_utils.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: {
@@ -24,7 +37,8 @@ const Chat = {
hoveredMessageChainId: undefined,
lastScrollPosition: {},
scrollableContainerHeight: '100%',
- errorLoadingChat: false
+ errorLoadingChat: false,
+ messageRetriers: {}
}
},
created () {
@@ -94,7 +108,7 @@ const Chat = {
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
this.$nextTick(() => {
if (bottomedOutBeforeUpdate) {
- this.scrollDown({ forceRead: !document.hidden })
+ this.scrollDown()
}
})
},
@@ -200,7 +214,7 @@ const Chat = {
this.$nextTick(() => {
scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior })
})
- if (forceRead || this.newMessageCount > 0) {
+ if (forceRead) {
this.readChat()
}
},
@@ -208,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)
@@ -225,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({
@@ -264,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 })
+ }
})
})
})
@@ -292,42 +323,74 @@ const Chat = {
})
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
index 012a1b1d..aef58495 100644
--- 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
index 2e4538c8..94a0097c 100644
--- 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
index 609dc0c9..50a933ac 100644
--- 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/chat_list.vue b/src/components/chat_list/chat_list.vue
index 17e2f795..e23eec13 100644
--- a/src/components/chat_list/chat_list.vue
+++ b/src/components/chat_list/chat_list.vue
@@ -10,7 +10,10 @@
<span class="title">
{{ $t("chats.chats") }}
</span>
- <button @click="newChat">
+ <button
+ class="button-default"
+ @click="newChat"
+ >
{{ $t("chats.new") }}
</button>
</div>
diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue
index 1f8ecdf6..cd3f436e 100644
--- 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
index be4a7c89..bb380f87 100644
--- 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
index 7d4ff60c..e4351d3b 100644
--- a/src/components/chat_message/chat_message.scss
+++ b/src/components/chat_message/chat_message.scss
@@ -24,16 +24,13 @@
}
}
- .icon-ellipsis {
+ .menu-icon {
cursor: pointer;
&:hover, .extra-button-popover.open & {
color: $fallback--text;
color: var(--text, $fallback--text);
}
-
- border-radius: $fallback--chatMessageRadius;
- border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
}
.popover {
@@ -101,6 +98,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
index e923d694..0777f880 100644
--- 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"
@@ -53,18 +53,19 @@
<div slot="content">
<div class="dropdown-menu">
<button
- class="dropdown-item dropdown-item-icon"
+ class="button-default 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="button-default menu-icon"
:title="$t('chats.more')"
>
- <i class="icon-ellipsis" />
+ <FAIcon icon="ellipsis-h" />
</button>
</Popover>
</div>
diff --git a/src/components/chat_message_date/chat_message_date.vue b/src/components/chat_message_date/chat_message_date.vue
index 79c346b6..98349b75 100644
--- a/src/components/chat_message_date/chat_message_date.vue
+++ b/src/components/chat_message_date/chat_message_date.vue
@@ -5,6 +5,8 @@
</template>
<script>
+import localeService from 'src/services/locale/locale.service.js'
+
export default {
name: 'Timeago',
props: ['date'],
@@ -16,7 +18,7 @@ export default {
if (this.date.getTime() === today.getTime()) {
return this.$t('display_date.today')
} else {
- return this.date.toLocaleDateString('en', { day: 'numeric', month: 'long' })
+ return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' })
}
}
}
diff --git a/src/components/chat_new/chat_new.js b/src/components/chat_new/chat_new.js
index d023efc0..71585995 100644
--- 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
index 11305444..5506143d 100644
--- 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
index 3333dbf9..f3894a3a 100644
--- 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
index f2e3adf0..c3887098 100644
--- 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
index 570435e7..7993c94d 100644
--- 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
index 9dc871b6..374cb9ba 100644
--- 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.vue b/src/components/conversation/conversation.vue
index e0b9fcc5..353859b8 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -10,12 +10,13 @@
class="panel-heading conversation-heading"
>
<span class="title"> {{ $t('timeline.conversation') }} </span>
- <span v-if="collapsable">
- <a
- href="#"
- @click.prevent="toggleExpanded"
- >{{ $t('timeline.collapse') }}</a>
- </span>
+ <button
+ v-if="collapsable"
+ class="button-unstyled -link"
+ @click.prevent="toggleExpanded"
+ >
+ {{ $t('timeline.collapse') }}
+ </button>
</div>
<status
v-for="status in conversation"
@@ -57,13 +58,6 @@
}
&.-expanded {
- .conversation-status {
- border-color: $fallback--border;
- border-color: var(--border, $fallback--border);
- border-left-color: $fallback--cRed;
- border-left-color: var(--cRed, $fallback--cRed);
- }
-
.conversation-status:last-child {
border-bottom: none;
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
diff --git a/src/components/desktop_nav/desktop_nav.js b/src/components/desktop_nav/desktop_nav.js
new file mode 100644
index 00000000..e048f53d
--- /dev/null
+++ 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
new file mode 100644
index 00000000..2d468588
--- /dev/null
+++ b/src/components/desktop_nav/desktop_nav.scss
@@ -0,0 +1,117 @@
+@import '../../_variables.scss';
+
+.DesktopNav {
+ height: 50px;
+ width: 100%;
+ position: fixed;
+
+ a {
+ color: var(--topBarLink, $fallback--link);
+ }
+
+ .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-default {
+ &, 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;
+ height: 100%;
+ text-align: center;
+
+ .svg-inline--fa {
+ 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
new file mode 100644
index 00000000..762aa610
--- /dev/null
+++ b/src/components/desktop_nav/desktop_nav.vue
@@ -0,0 +1,81 @@
+<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
+ />
+ <button
+ class="button-unstyled nav-icon"
+ @click.stop="openSettingsModal"
+ >
+ <FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="cog"
+ :title="$t('nav.preferences')"
+ />
+ </button>
+ <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>
+ <button
+ v-if="currentUser"
+ class="button-unstyled nav-icon"
+ @click.prevent="logout"
+ >
+ <FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="sign-out-alt"
+ :title="$t('login.logout')"
+ />
+ </button>
+ </div>
+ </div>
+ </nav>
+</template>
+<script src="./desktop_nav.js"></script>
+
+<style src="./desktop_nav.scss" lang="scss"></style>
diff --git a/src/components/domain_mute_card/domain_mute_card.vue b/src/components/domain_mute_card/domain_mute_card.vue
index 97aee243..3b5aec14 100644
--- a/src/components/domain_mute_card/domain_mute_card.vue
+++ b/src/components/domain_mute_card/domain_mute_card.vue
@@ -6,7 +6,7 @@
<ProgressButton
v-if="muted"
:click="unmuteDomain"
- class="btn btn-default"
+ class="btn button-default"
>
{{ $t('domain_mute_card.unmute') }}
<template slot="progress">
@@ -16,7 +16,7 @@
<ProgressButton
v-else
:click="muteDomain"
- class="btn btn-default"
+ class="btn button-default"
>
{{ $t('domain_mute_card.mute') }}
<template slot="progress">
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index f0123447..2068a598 100644
--- 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
@@ -105,7 +114,8 @@ const EmojiInput = {
showPicker: false,
temporarilyHideSuggestions: false,
keepOpen: false,
- disableClickOutside: false
+ disableClickOutside: false,
+ suggestions: []
}
},
components: {
@@ -115,21 +125,6 @@ const EmojiInput = {
padEmoji () {
return this.$store.getters.mergedConfig.padEmoji
},
- suggestions () {
- const firstchar = this.textAtCaret.charAt(0)
- if (this.textAtCaret === firstchar) { return [] }
- const matchedSuggestions = this.suggest(this.textAtCaret)
- if (matchedSuggestions.length <= 0) {
- return []
- }
- return take(matchedSuggestions, 5)
- .map(({ imageUrl, ...rest }, index) => ({
- ...rest,
- // eslint-disable-next-line camelcase
- img: imageUrl || '',
- highlighted: index === this.highlighted
- }))
- },
showSuggestions () {
return this.focused &&
this.suggestions &&
@@ -179,6 +174,23 @@ const EmojiInput = {
watch: {
showSuggestions: function (newValue) {
this.$emit('shown', newValue)
+ },
+ textAtCaret: async function (newWord) {
+ const firstchar = newWord.charAt(0)
+ this.suggestions = []
+ if (newWord === firstchar) return
+ const matchedSuggestions = await this.suggest(newWord)
+ // Async: cancel if textAtCaret has changed during wait
+ if (this.textAtCaret !== newWord) return
+ if (matchedSuggestions.length <= 0) return
+ this.suggestions = take(matchedSuggestions, 5)
+ .map(({ imageUrl, ...rest }) => ({
+ ...rest,
+ img: imageUrl || ''
+ }))
+ },
+ suggestions (newValue) {
+ this.$nextTick(this.resize)
}
},
methods: {
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index b9a74572..4becdc41 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -6,13 +6,13 @@
>
<slot />
<template v-if="enableEmojiPicker">
- <div
+ <button
v-if="!hideEmojiButton"
- class="emoji-picker-icon"
+ class="button-unstyled emoji-picker-icon"
@click.prevent="togglePicker"
>
- <i class="icon-smile" />
- </div>
+ <FAIcon :icon="['far', 'smile-beam']" />
+ </button>
<EmojiPicker
v-if="enableEmojiPicker"
ref="picker"
@@ -37,7 +37,7 @@
v-for="(suggestion, index) in suggestions"
:key="index"
class="autocomplete-item"
- :class="{ highlighted: suggestion.highlighted }"
+ :class="{ highlighted: index === highlighted }"
@click.stop.prevent="onClick($event, suggestion)"
>
<span class="image">
diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js
index 8330345b..14a2b41e 100644
--- a/src/components/emoji_input/suggestor.js
+++ b/src/components/emoji_input/suggestor.js
@@ -1,4 +1,3 @@
-import { debounce } from 'lodash'
/**
* suggest - generates a suggestor function to be used by emoji-input
* data: object providing source information for specific types of suggestions:
@@ -11,19 +10,19 @@ import { debounce } from 'lodash'
* doesn't support user linking you can just provide only emoji.
*/
-const debounceUserSearch = debounce((data, input) => {
- data.updateUsersList(input)
-}, 500)
-
-export default data => input => {
- const firstChar = input[0]
- if (firstChar === ':' && data.emoji) {
- return suggestEmoji(data.emoji)(input)
- }
- if (firstChar === '@' && data.users) {
- return suggestUsers(data)(input)
+export default data => {
+ const emojiCurry = suggestEmoji(data.emoji)
+ const usersCurry = data.store && suggestUsers(data.store)
+ return input => {
+ const firstChar = input[0]
+ if (firstChar === ':' && data.emoji) {
+ return emojiCurry(input)
+ }
+ if (firstChar === '@' && usersCurry) {
+ return usersCurry(input)
+ }
+ return []
}
- return []
}
export const suggestEmoji = emojis => input => {
@@ -57,50 +56,75 @@ export const suggestEmoji = emojis => input => {
})
}
-export const suggestUsers = data => input => {
- const noPrefix = input.toLowerCase().substr(1)
- const users = data.users
-
- const newUsers = users.filter(
- user =>
- user.screen_name.toLowerCase().startsWith(noPrefix) ||
- user.name.toLowerCase().startsWith(noPrefix)
-
- /* taking only 20 results so that sorting is a bit cheaper, we display
- * only 5 anyway. could be inaccurate, but we ideally we should query
- * backend anyway
- */
- ).slice(0, 20).sort((a, b) => {
- let aScore = 0
- let bScore = 0
-
- // Matches on screen name (i.e. user@instance) makes a priority
- aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
- bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
-
- // Matches on name takes second priority
- aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
- bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
-
- const diff = (bScore - aScore) * 10
-
- // Then sort alphabetically
- const nameAlphabetically = a.name > b.name ? 1 : -1
- const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
-
- return diff + nameAlphabetically + screenNameAlphabetically
- /* eslint-disable camelcase */
- }).map(({ screen_name, name, profile_image_url_original }) => ({
- displayText: screen_name,
- detailText: name,
- imageUrl: profile_image_url_original,
- replacement: '@' + screen_name + ' '
- }))
-
- // BE search users to get more comprehensive results
- if (data.updateUsersList) {
- debounceUserSearch(data, noPrefix)
+export const suggestUsers = ({ dispatch, state }) => {
+ // Keep some persistent values in closure, most importantly for the
+ // custom debounce to work. Lodash debounce does not return a promise.
+ let suggestions = []
+ let previousQuery = ''
+ let timeout = null
+ let cancelUserSearch = null
+
+ const userSearch = (query) => dispatch('searchUsers', { query })
+ const debounceUserSearch = (query) => {
+ cancelUserSearch && cancelUserSearch()
+ return new Promise((resolve, reject) => {
+ timeout = setTimeout(() => {
+ userSearch(query).then(resolve).catch(reject)
+ }, 300)
+ cancelUserSearch = () => {
+ clearTimeout(timeout)
+ resolve([])
+ }
+ })
+ }
+
+ return async input => {
+ const noPrefix = input.toLowerCase().substr(1)
+ if (previousQuery === noPrefix) return suggestions
+
+ suggestions = []
+ previousQuery = noPrefix
+ // Fetch more and wait, don't fetch if there's the 2nd @ because
+ // the backend user search can't deal with it.
+ // Reference semantics make it so that we get the updated data after
+ // the await.
+ if (!noPrefix.includes('@')) {
+ await debounceUserSearch(noPrefix)
+ }
+
+ const newSuggestions = state.users.users.filter(
+ user =>
+ user.screen_name.toLowerCase().startsWith(noPrefix) ||
+ user.name.toLowerCase().startsWith(noPrefix)
+ ).slice(0, 20).sort((a, b) => {
+ let aScore = 0
+ let bScore = 0
+
+ // Matches on screen name (i.e. user@instance) makes a priority
+ aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
+ bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
+
+ // Matches on name takes second priority
+ aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
+ bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
+
+ const diff = (bScore - aScore) * 10
+
+ // Then sort alphabetically
+ const nameAlphabetically = a.name > b.name ? 1 : -1
+ const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
+
+ return diff + nameAlphabetically + screenNameAlphabetically
+ /* eslint-disable camelcase */
+ }).map(({ screen_name, name, profile_image_url_original }) => ({
+ displayText: screen_name,
+ detailText: name,
+ imageUrl: profile_image_url_original,
+ replacement: '@' + screen_name + ' '
+ }))
+ /* eslint-enable camelcase */
+
+ suggestions = newSuggestions || []
+ return suggestions
}
- return newUsers
- /* eslint-enable camelcase */
}
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 3ad80df3..2716d93f 100644
--- 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
@@ -177,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
index 8bd07e45..ec711758 100644
--- 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
index 191b9fa1..3262a3d9 100644
--- 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/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue
index 2f14b5b2..51d50359 100644
--- a/src/components/emoji_reactions/emoji_reactions.vue
+++ b/src/components/emoji_reactions/emoji_reactions.vue
@@ -6,7 +6,7 @@
:users="accountsForEmoji[reaction.name]"
>
<button
- class="emoji-reaction btn btn-default"
+ class="emoji-reaction btn button-default"
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
@click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()"
diff --git a/src/components/export_import/export_import.vue b/src/components/export_import/export_import.vue
index ae00487f..8ffe34f8 100644
--- a/src/components/export_import/export_import.vue
+++ b/src/components/export_import/export_import.vue
@@ -2,13 +2,13 @@
<div class="import-export-container">
<slot name="before" />
<button
- class="btn"
+ class="btn button-default"
@click="exportData"
>
{{ exportLabel }}
</button>
<button
- class="btn"
+ class="btn button-default"
@click="importData"
>
{{ importLabel }}
diff --git a/src/components/exporter/exporter.js b/src/components/exporter/exporter.js
index 8f507416..51912ac3 100644
--- 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
index f5126dc1..d6a03088 100644
--- a/src/components/exporter/exporter.vue
+++ b/src/components/exporter/exporter.vue
@@ -1,12 +1,17 @@
<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
v-else
- class="btn btn-default"
+ class="btn button-default"
@click="process"
>
{{ exportButtonLabel }}
@@ -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
index 5e0c36bb..dd45b6b9 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -1,4 +1,28 @@
import Popover from '../popover/popover.vue'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faEllipsisH,
+ faBookmark,
+ faEyeSlash,
+ faThumbtack,
+ faShareAlt,
+ faExternalLinkAlt
+} from '@fortawesome/free-solid-svg-icons'
+import {
+ faBookmark as faBookmarkReg,
+ faFlag
+} from '@fortawesome/free-regular-svg-icons'
+
+library.add(
+ faEllipsisH,
+ faBookmark,
+ faBookmarkReg,
+ faEyeSlash,
+ faThumbtack,
+ faShareAlt,
+ faExternalLinkAlt,
+ faFlag
+)
const ExtraButtons = {
props: [ 'status' ],
@@ -44,6 +68,9 @@ const ExtraButtons = {
this.$store.dispatch('unbookmark', { id: this.status.id })
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
+ },
+ reportStatus () {
+ this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
}
},
computed: {
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index 7a4e8642..e845d8fc 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -1,9 +1,11 @@
<template>
<Popover
+ class="ExtraButtons"
trigger="click"
placement="top"
- class="extra-button-popover"
+ :offset="{ y: 5 }"
:bound-to="{ x: 'container' }"
+ remove-padding
>
<div
slot="content"
@@ -12,71 +14,122 @@
<div class="dropdown-menu">
<button
v-if="canMute && !status.thread_muted"
- class="dropdown-item dropdown-item-icon"
+ class="button-default 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"
+ class="button-default 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"
- class="dropdown-item dropdown-item-icon"
+ class="button-default dropdown-item dropdown-item-icon"
@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"
- class="dropdown-item dropdown-item-icon"
+ class="button-default dropdown-item dropdown-item-icon"
@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"
- class="dropdown-item dropdown-item-icon"
+ class="button-default dropdown-item dropdown-item-icon"
@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"
- class="dropdown-item dropdown-item-icon"
+ class="button-default dropdown-item dropdown-item-icon"
@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"
- class="dropdown-item dropdown-item-icon"
+ class="button-default dropdown-item dropdown-item-icon"
@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"
+ class="button-default 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>
+ <a
+ v-if="!status.is_local"
+ class="button-default dropdown-item dropdown-item-icon"
+ title="Source"
+ :href="status.external_url"
+ target="_blank"
+ >
+ <FAIcon
+ fixed-width
+ icon="external-link-alt"
+ /><span>{{ $t("status.external_source") }}</span>
+ </a>
+ <button
+ class="button-default dropdown-item dropdown-item-icon"
+ @click.prevent="reportStatus"
+ @click="close"
+ >
+ <FAIcon
+ fixed-width
+ :icon="['far', 'flag']"
+ /><span>{{ $t("user_card.report") }}</span>
</button>
</div>
</div>
- <i
+ <span
slot="trigger"
- class="icon-ellipsis button-icon"
- />
+ class="popover-trigger"
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="ellipsis-h"
+ />
+ </span>
</Popover>
</template>
@@ -85,13 +138,16 @@
<style lang="scss">
@import '../../_variables.scss';
-.icon-ellipsis {
- cursor: pointer;
+.ExtraButtons {
+ .popover-trigger {
+ position: static;
+ padding: 10px;
+ margin: -10px;
- &:hover,
- .extra-button-popover.open & {
- color: $fallback--text;
- color: var(--text, $fallback--text);
+ &:hover .svg-inline--fa {
+ color: $fallback--text;
+ color: var(--text, $fallback--text);
+ }
}
}
</style>
diff --git a/src/components/favorite_button/favorite_button.js b/src/components/favorite_button/favorite_button.js
index 5014d84f..5cd05f73 100644
--- 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'],
@@ -21,13 +31,6 @@ const FavoriteButton = {
}
},
computed: {
- classes () {
- return {
- 'icon-star-empty': !this.status.favorited,
- 'icon-star': this.status.favorited,
- 'animate-spin': this.animated
- }
- },
...mapGetters(['mergedConfig'])
}
}
diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue
index fbc90f84..dce25e24 100644
--- a/src/components/favorite_button/favorite_button.vue
+++ b/src/components/favorite_button/favorite_button.vue
@@ -1,20 +1,31 @@
<template>
- <div v-if="loggedIn">
- <i
- :class="classes"
- class="button-icon favorite-button fav-active"
+ <div class="FavoriteButton">
+ <button
+ v-if="loggedIn"
+ class="button-unstyled interactive"
+ :class="status.favorited && '-favorited'"
:title="$t('tool_tip.favorite')"
@click.prevent="favorite()"
- />
- <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
- </div>
- <div v-else>
- <i
- :class="classes"
- class="button-icon favorite-button"
- :title="$t('tool_tip.favorite')"
- />
- <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ :icon="[status.favorited ? 'fas' : 'far', 'star']"
+ :spin="animated"
+ />
+ </button>
+ <span v-else>
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ :title="$t('tool_tip.favorite')"
+ :icon="['far', 'star']"
+ />
+ </span>
+ <span
+ v-if="!mergedConfig.hidePostStats && status.fave_num > 0"
+ class="action-counter"
+ >
+ {{ status.fave_num }}
+ </span>
</div>
</template>
@@ -23,18 +34,29 @@
<style lang="scss">
@import '../../_variables.scss';
-.fav-active {
- cursor: pointer;
- animation-duration: 0.6s;
+.FavoriteButton {
+ display: flex;
- &:hover {
- color: $fallback--cOrange;
- color: var(--cOrange, $fallback--cOrange);
+ > :first-child {
+ padding: 10px;
+ margin: -10px -8px -10px -10px;
}
-}
-.favorite-button.icon-star {
- color: $fallback--cOrange;
- color: var(--cOrange, $fallback--cOrange);
+ .action-counter {
+ pointer-events: none;
+ user-select: none;
+ }
+
+ .interactive {
+ .svg-inline--fa {
+ animation-duration: 0.6s;
+ }
+
+ &:hover .svg-inline--fa,
+ &.-favorited .svg-inline--fa {
+ color: $fallback--cOrange;
+ color: var(--cOrange, $fallback--cOrange);
+ }
+ }
}
</style>
diff --git a/src/components/features_panel/features_panel.js b/src/components/features_panel/features_panel.js
index 620a85ea..8b142d08 100644
--- a/src/components/features_panel/features_panel.js
+++ b/src/components/features_panel/features_panel.js
@@ -1,3 +1,5 @@
+import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
+
const FeaturesPanel = {
computed: {
chat: function () { return this.$store.state.instance.chatAvailable },
@@ -6,7 +8,8 @@ const FeaturesPanel = {
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode },
- textlimit: function () { return this.$store.state.instance.textlimit }
+ textlimit: function () { return this.$store.state.instance.textlimit },
+ uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) }
}
}
diff --git a/src/components/features_panel/features_panel.vue b/src/components/features_panel/features_panel.vue
index 608b11c8..9605d09d 100644
--- a/src/components/features_panel/features_panel.vue
+++ b/src/components/features_panel/features_panel.vue
@@ -25,6 +25,7 @@
</li>
<li>{{ $t('features_panel.scope_options') }}</li>
<li>{{ $t('features_panel.text_limit') }} = {{ textlimit }}</li>
+ <li>{{ $t('features_panel.upload_limit') }} = {{ uploadlimit.num }} {{ $t('upload.file_size_units.' + uploadlimit.unit) }}</li>
</ul>
</div>
</div>
diff --git a/src/components/follow_button/follow_button.vue b/src/components/follow_button/follow_button.vue
index bfdc137b..7f85f1d7 100644
--- a/src/components/follow_button/follow_button.vue
+++ b/src/components/follow_button/follow_button.vue
@@ -1,6 +1,6 @@
<template>
<button
- class="btn btn-default follow-button"
+ class="btn button-default follow-button"
:class="{ toggled: isPressed }"
:disabled="inProgress"
:title="title"
diff --git a/src/components/follow_request_card/follow_request_card.vue b/src/components/follow_request_card/follow_request_card.vue
index b217b8ed..1b12ba4b 100644
--- a/src/components/follow_request_card/follow_request_card.vue
+++ b/src/components/follow_request_card/follow_request_card.vue
@@ -2,13 +2,13 @@
<basic-user-card :user="user">
<div class="follow-request-card-content-container">
<button
- class="btn btn-default"
+ class="btn button-default"
@click="approveUser"
>
{{ $t('user_card.approve') }}
</button>
<button
- class="btn btn-default"
+ class="btn button-default"
@click="denyUser"
>
{{ $t('user_card.deny') }}
diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js
index 8e2b0e45..6274780b 100644
--- 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
index 61f0384b..dd117ec0 100644
--- 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
index 3af29c23..e93fba75 100644
--- 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
index 0e4285cc..049e23db 100644
--- a/src/components/global_notice_list/global_notice_list.vue
+++ b/src/components/global_notice_list/global_notice_list.vue
@@ -9,10 +9,15 @@
<div class="notice-message">
{{ $t(notice.messageKey, notice.messageArgs) }}
</div>
- <i
- class="button-icon icon-cancel"
+ <button
+ class="button-unstyled close-notice"
@click="closeNotice(notice)"
- />
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
+ />
+ </button>
</div>
</div>
</template>
@@ -53,7 +58,7 @@
.global-error {
background-color: var(--alertPopupError, $fallback--cRed);
color: var(--alertPopupErrorText, $fallback--text);
- i {
+ .svg-inline--fa {
color: var(--alertPopupErrorText, $fallback--text);
}
}
@@ -61,7 +66,7 @@
.global-warning {
background-color: var(--alertPopupWarning, $fallback--cOrange);
color: var(--alertPopupWarningText, $fallback--text);
- i {
+ .svg-inline--fa {
color: var(--alertPopupWarningText, $fallback--text);
}
}
@@ -69,9 +74,16 @@
.global-info {
background-color: var(--alertPopupNeutral, $fallback--fg);
color: var(--alertPopupNeutralText, $fallback--text);
- i {
+ .svg-inline--fa {
color: var(--alertPopupNeutralText, $fallback--text);
}
}
+
+ .close-notice {
+ padding-right: 0.2em;
+ .svg-inline--fa:hover {
+ opacity: 0.6;
+ }
+ }
}
</style>
diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js
index 01361e25..e8d5ec6d 100644
--- a/src/components/image_cropper/image_cropper.js
+++ b/src/components/image_cropper/image_cropper.js
@@ -1,5 +1,13 @@
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faCircleNotch
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faCircleNotch
+)
const ImageCropper = {
props: {
@@ -43,8 +51,7 @@ const ImageCropper = {
cropper: undefined,
dataUrl: undefined,
filename: undefined,
- submitting: false,
- submitError: null
+ submitting: false
}
},
computed: {
@@ -56,9 +63,6 @@ const ImageCropper = {
},
cancelText () {
return this.cancelButtonLabel || this.$t('image_cropper.cancel')
- },
- submitErrorMsg () {
- return this.submitError && this.submitError instanceof Error ? this.submitError.toString() : this.submitError
}
},
methods: {
@@ -72,12 +76,8 @@ const ImageCropper = {
},
submit (cropping = true) {
this.submitting = true
- this.avatarUploadError = null
this.submitHandler(cropping && this.cropper, this.file)
.then(() => this.destroy())
- .catch((err) => {
- this.submitError = err
- })
.finally(() => {
this.submitting = false
})
@@ -103,9 +103,6 @@ const ImageCropper = {
reader.readAsDataURL(this.file)
this.$emit('changed', this.file, reader)
}
- },
- clearError () {
- this.submitError = null
}
},
mounted () {
diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue
index 4e1b5927..8c48a387 100644
--- a/src/components/image_cropper/image_cropper.vue
+++ b/src/components/image_cropper/image_cropper.vue
@@ -11,39 +11,30 @@
</div>
<div class="image-cropper-buttons-wrapper">
<button
- class="btn"
+ class="button-default btn"
type="button"
:disabled="submitting"
@click="submit()"
v-text="saveText"
/>
<button
- class="btn"
+ class="button-default btn"
type="button"
:disabled="submitting"
@click="destroy"
v-text="cancelText"
/>
<button
- class="btn"
+ class="button-default btn"
type="button"
:disabled="submitting"
@click="submit(false)"
v-text="saveWithoutCroppingText"
/>
- <i
+ <FAIcon
v-if="submitting"
- class="icon-spin4 animate-spin"
- />
- </div>
- <div
- v-if="submitError"
- class="alert error"
- >
- {{ submitErrorMsg }}
- <i
- class="button-icon icon-cancel"
- @click="clearError"
+ spin
+ icon="circle-notch"
/>
</div>
</div>
diff --git a/src/components/importer/importer.js b/src/components/importer/importer.js
index c5f9e4d2..59f9beb1 100644
--- 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
index ed923d59..210823f5 100644
--- a/src/components/importer/importer.vue
+++ b/src/components/importer/importer.vue
@@ -7,27 +7,29 @@
@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
- class="btn btn-default"
+ class="btn button-default"
@click="submit"
>
{{ 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
index dd6800a3..dc3bd408 100644
--- a/src/components/interface_language_switcher/interface_language_switcher.vue
+++ b/src/components/interface_language_switcher/interface_language_switcher.vue
@@ -12,31 +12,39 @@
v-model="language"
>
<option
- v-for="(langCode, i) in languageCodes"
- :key="langCode"
- :value="langCode"
+ v-for="lang in languages"
+ :key="lang.code"
+ :value="lang.code"
>
- {{ languageNames[i] }}
+ {{ lang.name }}
</option>
</select>
- <i class="icon-down-open" />
+ <FAIcon
+ class="select-down-icon"
+ icon="chevron-down"
+ />
</label>
</div>
</template>
<script>
import languagesObject from '../../i18n/messages'
+import localeService from '../../services/locale/locale.service.js'
import ISO6391 from 'iso-639-1'
import _ from 'lodash'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faChevronDown
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faChevronDown
+)
export default {
computed: {
- languageCodes () {
- return languagesObject.languages
- },
-
- languageNames () {
- return _.map(this.languageCodes, this.getLanguageName)
+ languages () {
+ return _.map(languagesObject.languages, (code) => ({ code: code, name: this.getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))
},
language: {
@@ -50,11 +58,13 @@ export default {
methods: {
getLanguageName (code) {
const specialLanguageNames = {
- 'ja': 'Japanese (日本語)',
- 'ja_easy': 'Japanese (やさしいにほんご)',
- 'zh': 'Chinese (简体中文)'
+ 'ja_easy': 'やさしいにほんご',
+ 'zh': '简体中文',
+ 'zh_Hant': '繁體中文'
}
- return specialLanguageNames[code] || ISO6391.getName(code)
+ const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code)
+ const browserLocale = localeService.internalToBrowserLocale(code)
+ return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1)
}
}
}
diff --git a/src/components/link-preview/link-preview.js b/src/components/link-preview/link-preview.js
index 444aafbe..add7c563 100644
--- a/src/components/link-preview/link-preview.js
+++ b/src/components/link-preview/link-preview.js
@@ -1,3 +1,5 @@
+import { mapGetters } from 'vuex'
+
const LinkPreview = {
name: 'LinkPreview',
props: [
@@ -15,11 +17,20 @@ const LinkPreview = {
// Currently BE shoudn't give cards if tagged NSFW, this is a bit paranoid
// as it makes sure to hide the image if somehow NSFW tagged preview can
// exist.
- return this.card.image && !this.nsfw && this.size !== 'hide'
+ return this.card.image && !this.censored && this.size !== 'hide'
+ },
+ censored () {
+ return this.nsfw && this.hideNsfwConfig
},
useDescription () {
return this.card.description && /\S/.test(this.card.description)
- }
+ },
+ hideNsfwConfig () {
+ return this.mergedConfig.hideNsfw
+ },
+ ...mapGetters([
+ 'mergedConfig'
+ ])
},
created () {
if (this.useImage) {
diff --git a/src/components/link-preview/link-preview.vue b/src/components/link-preview/link-preview.vue
index 69171977..d3ca39b8 100644
--- a/src/components/link-preview/link-preview.vue
+++ b/src/components/link-preview/link-preview.vue
@@ -9,12 +9,17 @@
<div
v-if="useImage && imageLoaded"
class="card-image"
- :class="{ 'small-image': size === 'small' }"
>
<img :src="card.image">
</div>
<div class="card-content">
- <span class="card-host faint">{{ card.provider_name }}</span>
+ <span class="card-host faint">
+ <span
+ v-if="censored"
+ class="nsfw-alert alert warning"
+ >{{ $t('status.nsfw') }}</span>
+ {{ card.provider_name }}
+ </span>
<h4 class="card-title">{{ card.title }}</h4>
<p
v-if="useDescription"
@@ -50,10 +55,6 @@
}
}
- .small-image {
- width: 80px;
- }
-
.card-content {
max-height: 100%;
margin: 0.5em;
@@ -76,6 +77,10 @@
max-height: calc(1.2em * 3 - 1px);
}
+ .nsfw-alert {
+ margin: 2em 0;
+ }
+
color: $fallback--text;
color: var(--text, $fallback--text);
border-style: solid;
diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index 0d8f1da6..638bd812 100644
--- 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
index b4fdcefb..bfabb946 100644
--- a/src/components/login_form/login_form.vue
+++ b/src/components/login_form/login_form.vue
@@ -61,7 +61,7 @@
<button
:disabled="loggingIn"
type="submit"
- class="btn btn-default"
+ class="btn button-default"
>
{{ $t('login.login') }}
</button>
@@ -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
index 24764e80..e7384c93 100644
--- 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
index 46931667..ea7f7a7f 100644
--- 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
index 7b8a76cc..669d8190 100644
--- 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
index c8865d77..e955aa72 100644
--- a/src/components/media_upload/media_upload.vue
+++ b/src/components/media_upload/media_upload.vue
@@ -1,30 +1,29 @@
<template>
- <div
+ <label
class="media-upload"
:class="{ disabled: disabled }"
+ :title="$t('tool_tip.media_upload')"
>
- <label
- class="label"
- :title="$t('tool_tip.media_upload')"
+ <FAIcon
+ v-if="uploading"
+ class="progress-icon"
+ icon="circle-notch"
+ spin
+ />
+ <FAIcon
+ v-if="!uploading"
+ class="new-icon"
+ icon="upload"
+ />
+ <input
+ v-if="uploadReady"
+ :disabled="disabled"
+ type="file"
+ style="position: fixed; top: -100em"
+ multiple="true"
+ @change="change"
>
- <i
- v-if="uploading"
- class="progress-icon icon-spin4 animate-spin"
- />
- <i
- v-if="!uploading"
- class="new-icon icon-upload"
- />
- <input
- v-if="uploadReady"
- :disabled="disabled"
- type="file"
- style="position: fixed; top: -100em"
- multiple="true"
- @change="change"
- >
- </label>
- </div>
+ </label>
</template>
<script src="./media_upload.js" ></script>
@@ -33,22 +32,6 @@
@import '../../_variables.scss';
.media-upload {
- .label {
- display: inline-block;
- }
-
- .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;
- }
- }
+ cursor: pointer;
}
</style>
diff --git a/src/components/mfa_form/recovery_form.js b/src/components/mfa_form/recovery_form.js
index b25c65dd..01a62a50 100644
--- 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
index 57294630..0bf68e27 100644
--- a/src/components/mfa_form/recovery_form.vue
+++ b/src/components/mfa_form/recovery_form.vue
@@ -23,23 +23,23 @@
<div class="form-group">
<div class="login-bottom">
<div>
- <a
- href="#"
+ <button
+ class="button-unstyled -link"
@click.prevent="requireTOTP"
>
{{ $t('login.enter_two_factor_code') }}
- </a>
+ </button>
<br>
- <a
- href="#"
+ <button
+ class="button-unstyled -link"
@click.prevent="abortMFA"
>
{{ $t('general.cancel') }}
- </a>
+ </button>
</div>
<button
type="submit"
- class="btn btn-default"
+ class="btn button-default"
>
{{ $t('general.verify') }}
</button>
@@ -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
index b774f2d0..6ee823ed 100644
--- 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
index a344b395..79230148 100644
--- a/src/components/mfa_form/totp_form.vue
+++ b/src/components/mfa_form/totp_form.vue
@@ -25,23 +25,23 @@
<div class="form-group">
<div class="login-bottom">
<div>
- <a
- href="#"
+ <button
+ class="button-unstyled -link"
@click.prevent="requireRecovery"
>
{{ $t('login.enter_recovery_code') }}
- </a>
+ </button>
<br>
- <a
- href="#"
+ <button
+ class="button-unstyled -link"
@click.prevent="abortMFA"
>
{{ $t('general.cancel') }}
- </a>
+ </button>
</div>
<button
type="submit"
- class="btn btn-default"
+ class="btn button-default"
>
{{ $t('general.verify') }}
</button>
@@ -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
index b2b5d264..9e736cfb 100644
--- 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
index abd95f09..0f0ea457 100644
--- a/src/components/mobile_nav/mobile_nav.vue
+++ b/src/components/mobile_nav/mobile_nav.vue
@@ -1,49 +1,51 @@
<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">
+ <button
+ class="button-unstyled mobile-nav-button"
+ @click.stop.prevent="toggleMobileSidebar()"
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="bars"
+ />
+ <div
+ v-if="unreadChatCount"
+ class="alert-dot"
+ />
+ </button>
+ <router-link
+ v-if="!hideSitename"
+ class="site-name"
+ :to="{ name: 'root' }"
+ active-class="home"
+ >
+ {{ sitename }}
+ </router-link>
+ </div>
+ <div class="item right">
+ <button
+ v-if="currentUser"
+ class="button-unstyled mobile-nav-button"
+ @click.stop.prevent="openMobileNotifications()"
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="bell"
+ />
+ <div
+ v-if="unseenNotificationsCount"
+ class="alert-dot"
+ />
+ </button>
</div>
</nav>
<div
@@ -59,7 +61,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 +89,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
index 6348277b..366ea89c 100644
--- 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
index 9cf45de3..767f8244 100644
--- a/src/components/mobile_post_status_button/mobile_post_status_button.vue
+++ b/src/components/mobile_post_status_button/mobile_post_status_button.vue
@@ -1,11 +1,11 @@
<template>
<div v-if="isLoggedIn">
<button
- class="new-status-button"
+ class="button-default new-status-button"
: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
index 60fa6ceb..5c7b82ec 100644
--- a/src/components/moderation_tools/moderation_tools.vue
+++ b/src/components/moderation_tools/moderation_tools.vue
@@ -12,13 +12,13 @@
<div class="dropdown-menu">
<span v-if="user.is_local">
<button
- class="dropdown-item"
+ class="button-default dropdown-item"
@click="toggleRight(&quot;admin&quot;)"
>
{{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }}
</button>
<button
- class="dropdown-item"
+ class="button-default dropdown-item"
@click="toggleRight(&quot;moderator&quot;)"
>
{{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
@@ -29,13 +29,13 @@
/>
</span>
<button
- class="dropdown-item"
+ class="button-default dropdown-item"
@click="toggleActivationStatus()"
>
{{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
</button>
<button
- class="dropdown-item"
+ class="button-default dropdown-item"
@click="deleteUserDialog(true)"
>
{{ $t('user_card.admin_menu.delete_account') }}
@@ -47,7 +47,7 @@
/>
<span v-if="hasTagPolicy">
<button
- class="dropdown-item"
+ class="button-default dropdown-item"
@click="toggleTag(tags.FORCE_NSFW)"
>
{{ $t('user_card.admin_menu.force_nsfw') }}
@@ -57,7 +57,7 @@
/>
</button>
<button
- class="dropdown-item"
+ class="button-default dropdown-item"
@click="toggleTag(tags.STRIP_MEDIA)"
>
{{ $t('user_card.admin_menu.strip_media') }}
@@ -67,7 +67,7 @@
/>
</button>
<button
- class="dropdown-item"
+ class="button-default dropdown-item"
@click="toggleTag(tags.FORCE_UNLISTED)"
>
{{ $t('user_card.admin_menu.force_unlisted') }}
@@ -77,7 +77,7 @@
/>
</button>
<button
- class="dropdown-item"
+ class="button-default dropdown-item"
@click="toggleTag(tags.SANDBOX)"
>
{{ $t('user_card.admin_menu.sandbox') }}
@@ -88,7 +88,7 @@
</button>
<button
v-if="user.is_local"
- class="dropdown-item"
+ class="button-default dropdown-item"
@click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)"
>
{{ $t('user_card.admin_menu.disable_remote_subscription') }}
@@ -99,7 +99,7 @@
</button>
<button
v-if="user.is_local"
- class="dropdown-item"
+ class="button-default dropdown-item"
@click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)"
>
{{ $t('user_card.admin_menu.disable_any_subscription') }}
@@ -110,7 +110,7 @@
</button>
<button
v-if="user.is_local"
- class="dropdown-item"
+ class="button-default dropdown-item"
@click="toggleTag(tags.QUARANTINE)"
>
{{ $t('user_card.admin_menu.quarantine') }}
@@ -124,7 +124,7 @@
</div>
<button
slot="trigger"
- class="btn btn-default btn-block"
+ class="btn button-default btn-block"
:class="{ toggled }"
>
{{ $t('user_card.admin_menu.moderation') }}
@@ -141,13 +141,13 @@
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
<template slot="footer">
<button
- class="btn btn-default"
+ class="btn button-default"
@click="deleteUserDialog(false)"
>
{{ $t('general.cancel') }}
</button>
<button
- class="btn btn-default danger"
+ class="btn button-default danger"
@click="deleteUser()"
>
{{ $t('user_card.admin_menu.delete_user') }}
diff --git a/src/components/mute_card/mute_card.vue b/src/components/mute_card/mute_card.vue
index 9611fb82..ca33c6c5 100644
--- a/src/components/mute_card/mute_card.vue
+++ b/src/components/mute_card/mute_card.vue
@@ -3,7 +3,7 @@
<div class="mute-card-content-container">
<button
v-if="muted"
- class="btn btn-default"
+ class="btn button-default"
:disabled="progress"
@click="unmuteUser"
>
@@ -16,7 +16,7 @@
</button>
<button
v-else
- class="btn btn-default"
+ class="btn button-default"
:disabled="progress"
@click="muteUser"
>
diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js
index 623dfaec..81d49cc2 100644
--- 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
index 4f944c95..0c83d0fe 100644
--- 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,84 +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 {
- margin-right: 0.5em;
-}
+ .fa-scale-110 {
+ margin-right: 0.8em;
+ }
-.nav-panel .button-icon:before {
- width: 1.1em;
+ .badge {
+ position: absolute;
+ right: 0.6rem;
+ top: 1.25em;
+ }
}
</style>
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index bb906b50..4aa9affd 100644
--- 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
index d0e63d81..f5905560 100644
--- 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
index 7fac3840..f56aa977 100644
--- 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"
@@ -14,16 +14,20 @@
{{ notification.from_profile.screen_name }}
</router-link>
</small>
- <a
- href="#"
- class="unmute"
+ <button
+ class="button-unstyled unmute"
@click.prevent="toggleMute"
- ><i class="button-icon icon-eye-off" /></a>
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="eye-slash"
+ />
+ </button>
</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 +64,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'">
@@ -116,11 +133,16 @@
/>
</span>
</div>
- <a
+ <button
v-if="needMute"
- href="#"
+ class="button-unstyled"
@click.prevent="toggleMute"
- ><i class="button-icon icon-eye-off" /></a>
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="eye-slash"
+ />
+ </button>
</span>
<div
v-if="notification.type === 'follow' || notification.type === 'follow_request'"
@@ -136,13 +158,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
index d951e2a8..49258563 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -6,6 +6,13 @@ import {
filteredNotificationsFromStore,
unseenNotificationsFromStore
} from '../../services/notification_utils/notification_utils.js'
+import FaviconService from '../../services/favicon_service/favicon_service.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
@@ -69,8 +76,10 @@ const Notifications = {
watch: {
unseenCountTitle (count) {
if (count > 0) {
+ FaviconService.drawFaviconBadge()
this.$store.dispatch('setPageTitle', `(${count})`)
} else {
+ FaviconService.clearFaviconBadge()
this.$store.dispatch('setPageTitle', '')
}
}
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index c6b2a5b5..682ae127 100644
--- 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
index d477a41b..725d1ad4 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -15,16 +15,9 @@
class="badge badge-notification unseen-count"
>{{ unseenCount }}</span>
</div>
- <div
- v-if="error"
- class="loadmore-error alert error"
- @click.prevent
- >
- {{ $t('timeline.error_fetching') }}
- </div>
<button
v-if="unseenCount"
- class="read-button"
+ class="button-default read-button"
@click.prevent="markAsSeen"
>
{{ $t('notifications.read') }}
@@ -48,20 +41,24 @@
>
{{ $t('notifications.no_more_notifications') }}
</div>
- <a
+ <button
v-else-if="!loading"
- href="#"
+ class="button-unstyled -link -fullwidth"
@click.prevent="fetchOlderNotifications()"
>
<div class="new-status-notification text-center panel-footer">
{{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }}
</div>
- </a>
+ </button>
<div
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
index 4efebb3c..d916d8a6 100644
--- 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
index 5d21d720..3d94f5e7 100644
--- 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
index 713c9dce..a931cb5a 100644
--- a/src/components/password_reset/password_reset.vue
+++ b/src/components/password_reset/password_reset.vue
@@ -51,7 +51,7 @@
<button
:disabled="isPending"
type="submit"
- class="btn btn-default btn-block"
+ class="btn button-default btn-block"
>
{{ $t('general.submit') }}
</button>
@@ -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.vue b/src/components/poll/poll.vue
index 5f54b416..42819c19 100644
--- a/src/components/poll/poll.vue
+++ b/src/components/poll/poll.vue
@@ -42,14 +42,15 @@
:value="index"
>
<label class="option-vote">
- <div>{{ option.title }}</div>
+ <!-- eslint-disable-next-line vue/no-v-html -->
+ <div v-html="option.title_html" />
</label>
</div>
</div>
<div class="footer faint">
<button
v-if="!showResults"
- class="btn btn-default poll-vote-button"
+ class="btn button-default poll-vote-button"
type="button"
:disabled="isDisabled"
@click="vote"
diff --git a/src/components/poll/poll_form.js b/src/components/poll/poll_form.js
index df93f038..1f8df3f9 100644
--- 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
index d53f3837..09496105 100644
--- 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')"
@@ -20,24 +21,26 @@
@keydown.enter.stop.prevent="nextOption(index)"
>
</div>
- <div
+ <button
v-if="options.length > 2"
- class="icon-container"
+ class="delete-option button-unstyled -hover-highlight"
+ @click="deleteOption(index)"
>
- <i
- class="icon-cancel"
- @click="deleteOption(index)"
- />
- </div>
+ <FAIcon icon="times" />
+ </button>
</div>
- <a
+ <button
v-if="options.length < maxOptions"
- class="add-option faint"
+ class="add-option faint button-unstyled -hover-highlight"
@click="addOption"
>
- <i class="icon-plus" />
+ <FAIcon
+ icon="plus"
+ size="sm"
+ />
+
{{ $t("polls.add_option") }}
- </a>
+ </button>
<div class="poll-type-expiry">
<div
class="poll-type"
@@ -55,7 +58,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 +89,10 @@
{{ $t(`time.${unit}_short`, ['']) }}
</option>
</select>
- <i class="icon-down-open" />
+ <FAIcon
+ class="select-down-icon"
+ icon="chevron-down"
+ />
</label>
</div>
</div>
@@ -103,7 +112,7 @@
.add-option {
align-self: flex-start;
padding-top: 0.25em;
- cursor: pointer;
+ padding-left: 0.1em;
}
.poll-option {
@@ -122,10 +131,10 @@
}
}
- .icon-container {
+ .delete-option {
// Hack: Move the icon over the input box
- width: 2em;
- margin-left: -2em;
+ width: 1.5em;
+ margin-left: -1.5em;
z-index: 1;
}
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index 695f73b9..5e417fa0 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -21,7 +21,10 @@ const Popover = {
// Replaces the classes you may want for the popover container.
// Use 'popover-default' in addition to get the default popover
// styles with your custom class.
- popoverClass: String
+ popoverClass: String,
+ // If true, subtract padding when calculating position for the popover,
+ // use it when popover offset looks to be different on top vs bottom.
+ removePadding: Boolean
},
data () {
return {
@@ -96,9 +99,15 @@ const Popover = {
if (origin.y + content.offsetHeight > yBounds.max) usingTop = true
if (origin.y - content.offsetHeight < yBounds.min) usingTop = false
+ let vPadding = 0
+ if (this.removePadding && usingTop) {
+ const anchorStyle = getComputedStyle(anchorEl)
+ vPadding = parseFloat(anchorStyle.paddingTop) + parseFloat(anchorStyle.paddingBottom)
+ }
+
const yOffset = (this.offset && this.offset.y) || 0
const translateY = usingTop
- ? -anchorEl.offsetHeight - yOffset - content.offsetHeight
+ ? -anchorEl.offsetHeight + vPadding - yOffset - content.offsetHeight
: yOffset
const xOffset = (this.offset && this.offset.x) || 0
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
index 5c99c509..2252c68f 100644
--- a/src/components/popover/popover.vue
+++ b/src/components/popover/popover.vue
@@ -3,12 +3,13 @@
@mouseenter="onMouseenter"
@mouseleave="onMouseleave"
>
- <div
+ <button
ref="trigger"
+ class="button-unstyled -fullwidth popover-trigger-button"
@click="onClick"
>
<slot name="trigger" />
- </div>
+ </button>
<div
v-if="!hidden"
ref="content"
@@ -27,9 +28,13 @@
<script src="./popover.js" />
-<style lang=scss>
+<style lang="scss">
@import '../../_variables.scss';
+.popover-trigger-button {
+ display: block;
+}
+
.popover {
z-index: 8;
position: absolute;
@@ -90,13 +95,14 @@
box-shadow: none;
width: 100%;
height: 100%;
+ box-sizing: border-box;
--btnText: var(--popoverText, $fallback--text);
&-icon {
padding-left: 0.5rem;
- i {
+ svg {
margin-right: 0.25rem;
color: var(--menuPopoverIcon, $fallback--icon)
}
@@ -111,7 +117,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
index ad149506..4148381c 100644
--- 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,
@@ -137,8 +159,7 @@ const PostStatusForm = {
...this.$store.state.instance.emoji,
...this.$store.state.instance.customEmoji
],
- users: this.$store.state.users.users,
- updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
+ store: this.$store
})
},
emojiSuggestor () {
@@ -251,7 +272,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 +280,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
@@ -507,7 +530,7 @@ const PostStatusForm = {
!(isFormBiggerThanScroller &&
this.$refs.textarea.selectionStart !== this.$refs.textarea.value.length)
const totalDelta = shouldScrollToBottom ? bottomChangeDelta : 0
- const targetScroll = currentScroll + totalDelta
+ const targetScroll = Math.round(currentScroll + totalDelta)
if (scrollerRef === window) {
scrollerRef.scroll(0, targetScroll)
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index d67d9ae9..73f6a4f1 100644
--- 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"
@@ -23,12 +24,12 @@
tag="p"
class="visibility-notice"
>
- <a
- href="#"
+ <button
+ class="button-unstyled -link"
@click="openProfileTab"
>
{{ $t('post_status.account_not_locked_warning_link') }}
- </a>
+ </button>
</i18n>
<p
v-if="!hideScopeNotice && newStatus.visibility === 'public'"
@@ -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
@@ -232,38 +243,34 @@
@upload-failed="uploadFailed"
@all-uploaded="finishedUploadingFiles"
/>
- <div
- class="emoji-icon"
+ <button
+ class="emoji-icon button-unstyled"
+ :title="$t('emoji.add_emoji')"
+ @click="showEmojiPicker"
>
- <i
- :title="$t('emoji.add_emoji')"
- class="icon-smile btn btn-default"
- @click="showEmojiPicker"
- />
- </div>
- <div
+ <FAIcon icon="smile-beam" />
+ </button>
+ <button
v-if="pollsAvailable"
- class="poll-icon"
+ class="poll-icon button-unstyled"
: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"
- />
- </div>
+ <FAIcon icon="poll-h" />
+ </button>
</div>
<button
v-if="posting"
disabled
- class="btn btn-default"
+ class="btn button-default"
>
{{ $t('post_status.posting') }}
</button>
<button
v-else-if="isOverLengthLimit"
disabled
- class="btn btn-default"
+ class="btn button-default"
>
{{ $t('general.submit') }}
</button>
@@ -271,7 +278,7 @@
<button
v-else
:disabled="uploadingFiles || disableSubmit"
- class="btn btn-default"
+ class="btn button-default"
@touchstart.stop.prevent="postStatus($event, newStatus)"
@click.stop.prevent="postStatus($event, newStatus)"
>
@@ -283,8 +290,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,10 +302,12 @@
:key="file.url"
class="media-upload-wrapper"
>
- <i
- class="fa button-icon icon-cancel"
+ <button
+ class="button-unstyled hider"
@click="removeMediaFile(file)"
- />
+ >
+ <FAIcon icon="times" />
+ </button>
<attachment
:attachment="file"
:set-media="() => $store.dispatch('setMedia', newStatus.files)"
@@ -375,24 +385,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 +439,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 +481,7 @@
text-align: right;
}
- .icon-chart-bar {
+ .poll-icon {
cursor: pointer;
}
@@ -487,19 +494,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,23 +516,12 @@
flex-direction: column;
}
- .media-upload-wrapper .attachments {
- padding: 0 0.5em;
+ .attachments .media-upload-wrapper {
+ position: relative;
.attachment {
margin: 0;
padding: 0;
- position: relative;
- }
-
- i {
- position: absolute;
- margin: 10px;
- padding: 5px;
- background: rgba(230,230,230,0.6);
- border-radius: $fallback--attachmentRadius;
- border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
- font-weight: bold;
}
}
@@ -612,11 +595,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
index dd71e546..5e7b7580 100644
--- a/src/components/react_button/react_button.js
+++ b/src/components/react_button/react_button.js
@@ -1,4 +1,8 @@
import Popover from '../popover/popover.vue'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
+
+library.add(faSmileBeam)
const ReactButton = {
props: ['status'],
@@ -23,13 +27,21 @@ const ReactButton = {
},
computed: {
commonEmojis () {
- return ['👍', '😠', '👀', '😂', '🔥']
+ return [
+ { displayText: 'thumbsup', replacement: '👍' },
+ { displayText: 'angry', replacement: '😠' },
+ { displayText: 'eyes', replacement: '👀' },
+ { displayText: 'joy', replacement: '😂' },
+ { displayText: 'fire', replacement: '🔥' }
+ ]
},
emojis () {
if (this.filterWord !== '') {
const filterWordLowercase = this.filterWord.toLowerCase()
let orderedEmojiList = []
for (const emoji of this.$store.state.instance.emoji) {
+ if (emoji.replacement === this.filterWord) return [emoji]
+
const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase)
if (indexOfFilterWord > -1) {
if (!Array.isArray(orderedEmojiList[indexOfFilterWord])) {
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
index 0b34add1..ac940b98 100644
--- a/src/components/react_button/react_button.vue
+++ b/src/components/react_button/react_button.vue
@@ -3,7 +3,8 @@
trigger="click"
placement="top"
:offset="{ y: 5 }"
- class="react-button-popover"
+ :bound-to="{ x: 'container' }"
+ remove-padding
>
<div
slot="content"
@@ -12,23 +13,26 @@
<div class="reaction-picker-filter">
<input
v-model="filterWord"
+ size="1"
:placeholder="$t('emoji.search_emoji')"
>
</div>
<div class="reaction-picker">
<span
v-for="emoji in commonEmojis"
- :key="emoji"
+ :key="emoji.replacement"
class="emoji-button"
- @click="addReaction($event, emoji, close)"
+ :title="emoji.displayText"
+ @click="addReaction($event, emoji.replacement, close)"
>
- {{ emoji }}
+ {{ emoji.replacement }}
</span>
<div class="reaction-picker-divider" />
<span
v-for="(emoji, key) in emojis"
:key="key"
class="emoji-button"
+ :title="emoji.displayText"
@click="addReaction($event, emoji.replacement, close)"
>
{{ emoji.replacement }}
@@ -36,11 +40,16 @@
<div class="reaction-bottom-fader" />
</div>
</div>
- <i
+ <span
slot="trigger"
- class="icon-smile button-icon add-reaction-button"
+ class="ReactButton"
:title="$t('tool_tip.add_reaction')"
- />
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ :icon="['far', 'smile-beam']"
+ />
+ </span>
</Popover>
</template>
@@ -98,10 +107,11 @@
}
}
-.add-reaction-button {
- cursor: pointer;
+.ReactButton {
+ padding: 10px;
+ margin: -10px;
- &:hover {
+ &:hover .svg-inline--fa {
color: $fallback--text;
color: var(--text, $fallback--text);
}
diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue
index a83ca1e5..100df0d6 100644
--- a/src/components/registration/registration.vue
+++ b/src/components/registration/registration.vue
@@ -211,7 +211,7 @@
<button
:disabled="isPending"
type="submit"
- class="btn btn-default"
+ class="btn button-default"
>
{{ $t('general.submit') }}
</button>
diff --git a/src/components/remote_follow/remote_follow.vue b/src/components/remote_follow/remote_follow.vue
index cb1c2a1b..be827400 100644
--- a/src/components/remote_follow/remote_follow.vue
+++ b/src/components/remote_follow/remote_follow.vue
@@ -16,7 +16,7 @@
>
<button
click="submit"
- class="remote-button"
+ class="button-default remote-button"
>
{{ $t('user_card.remote_follow') }}
</button>
diff --git a/src/components/reply_button/reply_button.js b/src/components/reply_button/reply_button.js
index 22957650..c7bd2a2b 100644
--- a/src/components/reply_button/reply_button.js
+++ b/src/components/reply_button/reply_button.js
@@ -1,3 +1,7 @@
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faReply } from '@fortawesome/free-solid-svg-icons'
+
+library.add(faReply)
const ReplyButton = {
name: 'ReplyButton',
diff --git a/src/components/reply_button/reply_button.vue b/src/components/reply_button/reply_button.vue
index b2904b5c..c17041da 100644
--- a/src/components/reply_button/reply_button.vue
+++ b/src/components/reply_button/reply_button.vue
@@ -1,21 +1,58 @@
<template>
- <div>
- <i
+ <div class="ReplyButton">
+ <button
v-if="loggedIn"
- class="button-icon button-reply icon-reply"
- :title="$t('tool_tip.reply')"
+ class="button-unstyled interactive"
:class="{'-active': replying}"
- @click.prevent="$emit('toggle')"
- />
- <i
- v-else
- class="button-icon button-reply -disabled icon-reply"
:title="$t('tool_tip.reply')"
- />
- <span v-if="status.replies_count > 0">
+ @click.prevent="$emit('toggle')"
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="reply"
+ />
+ </button>
+ <span v-else>
+ <FAIcon
+ icon="reply"
+ class="fa-scale-110 fa-old-padding"
+ :title="$t('tool_tip.reply')"
+ />
+ </span>
+ <span
+ v-if="status.replies_count > 0"
+ class="action-counter"
+ >
{{ status.replies_count }}
</span>
</div>
</template>
<script src="./reply_button.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.ReplyButton {
+ display: flex;
+
+ > :first-child {
+ padding: 10px;
+ margin: -10px -8px -10px -10px;
+ }
+
+ .action-counter {
+ pointer-events: none;
+ user-select: none;
+ }
+
+ .interactive {
+ &:hover .svg-inline--fa,
+ &.-active .svg-inline--fa {
+ 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
index 5a41f22d..2103fd0b 100644
--- a/src/components/retweet_button/retweet_button.js
+++ b/src/components/retweet_button/retweet_button.js
@@ -1,3 +1,7 @@
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faRetweet } from '@fortawesome/free-solid-svg-icons'
+
+library.add(faRetweet)
const RetweetButton = {
props: ['status', 'loggedIn', 'visibility'],
@@ -20,13 +24,6 @@ const RetweetButton = {
}
},
computed: {
- classes () {
- return {
- 'retweeted': this.status.repeated,
- 'retweeted-empty': !this.status.repeated,
- 'animate-spin': this.animated
- }
- },
mergedConfig () {
return this.$store.getters.mergedConfig
}
diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue
index 074f7747..859ce499 100644
--- a/src/components/retweet_button/retweet_button.vue
+++ b/src/components/retweet_button/retweet_button.vue
@@ -1,29 +1,38 @@
<template>
- <div v-if="loggedIn">
- <template v-if="visibility !== 'private' && visibility !== 'direct'">
- <i
- :class="classes"
- class="button-icon retweet-button icon-retweet rt-active"
- :title="$t('tool_tip.repeat')"
- @click.prevent="retweet()"
+ <div class="RetweetButton">
+ <button
+ v-if="visibility !== 'private' && visibility !== 'direct' && loggedIn"
+ class="button-unstyled interactive"
+ :class="status.repeated && '-repeated'"
+ :title="$t('tool_tip.repeat')"
+ @click.prevent="retweet()"
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="retweet"
+ :spin="animated"
/>
- <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span>
- </template>
- <template v-else>
- <i
- :class="classes"
- class="button-icon icon-lock"
+ </button>
+ <span v-else-if="loggedIn">
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="lock"
:title="$t('timeline.no_retweet_hint')"
/>
- </template>
- </div>
- <div v-else-if="!loggedIn">
- <i
- :class="classes"
- class="button-icon icon-retweet"
- :title="$t('tool_tip.repeat')"
- />
- <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span>
+ </span>
+ <span v-else>
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="retweet"
+ :title="$t('tool_tip.repeat')"
+ />
+ </span>
+ <span
+ v-if="!mergedConfig.hidePostStats && status.repeat_num > 0"
+ class="no-event"
+ >
+ {{ status.repeat_num }}
+ </span>
</div>
</template>
@@ -31,16 +40,30 @@
<style lang="scss">
@import '../../_variables.scss';
-.rt-active {
- cursor: pointer;
- animation-duration: 0.6s;
- &:hover {
- color: $fallback--cGreen;
- color: var(--cGreen, $fallback--cGreen);
+
+.RetweetButton {
+ display: flex;
+
+ > :first-child {
+ padding: 10px;
+ margin: -10px -8px -10px -10px;
+ }
+
+ .action-counter {
+ pointer-events: none;
+ user-select: none;
+ }
+
+ .interactive {
+ .svg-inline--fa {
+ animation-duration: 0.6s;
+ }
+
+ &:hover .svg-inline--fa,
+ &.-repeated .svg-inline--fa {
+ 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
index e9ccdefc..74bf7284 100644
--- 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
index 291236f2..66ac612e 100644
--- 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
+ <button
v-if="showDirect"
- class="icon-mail-alt"
+ class="button-unstyled scope"
:class="css.direct"
:title="$t('post_status.scope.direct')"
@click="changeVis('direct')"
- />
- <i
+ >
+ <FAIcon
+ icon="envelope"
+ class="fa-scale-110 fa-old-padding"
+ />
+ </button>
+ <button
v-if="showPrivate"
- class="icon-lock"
+ class="button-unstyled scope"
:class="css.private"
:title="$t('post_status.scope.private')"
@click="changeVis('private')"
- />
- <i
+ >
+ <FAIcon
+ icon="lock"
+ class="fa-scale-110 fa-old-padding"
+ />
+ </button>
+ <button
v-if="showUnlisted"
- class="icon-lock-open-alt"
+ class="button-unstyled 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"
+ />
+ </button>
+ <button
v-if="showPublic"
- class="icon-globe"
+ class="button-unstyled scope"
:class="css.public"
:title="$t('post_status.scope.public')"
@click="changeVis('public')"
- />
+ >
+ <FAIcon
+ icon="globe"
+ class="fa-scale-110 fa-old-padding"
+ />
+ </button>
</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
index 8e903052..b62bc2c5 100644
--- 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
index 746bbaa2..a6503c9f 100644
--- a/src/components/search/search.vue
+++ b/src/components/search/search.vue
@@ -14,17 +14,21 @@
@keyup.enter="newQuery(searchTerm)"
>
<button
- class="btn search-button"
+ class="btn button-default 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
index d7d85676..551649c7 100644
--- 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
index 4d5a1aec..6cf9179e 100644
--- a/src/components/search_bar/search_bar.vue
+++ b/src/components/search_bar/search_bar.vue
@@ -1,40 +1,50 @@
<template>
- <div>
- <div class="search-bar-container">
- <i
- v-if="loading"
- class="icon-spin4 finder-icon animate-spin-slow"
+ <div
+ class="SearchBar"
+ :class="{ '-expanded': !hidden }"
+ >
+ <button
+ v-if="hidden"
+ class="button-unstyled nav-icon"
+ :title="$t('nav.search')"
+ @click.prevent.stop="toggleHidden"
+ >
+ <FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="search"
/>
- <a
- v-if="hidden"
- href="#"
- :title="$t('nav.search')"
- ><i
- class="button-icon icon-search"
+ </button>
+ <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="button-default search-button"
+ @click="find(searchTerm)"
+ >
+ <FAIcon
+ fixed-width
+ icon="search"
+ />
+ </button>
+ <button
+ class="button-unstyled cancel-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"
- @click.prevent.stop="toggleHidden"
+ >
+ <FAIcon
+ fixed-width
+ icon="times"
+ class="cancel-icon fa-scale-110 fa-old-padding"
/>
- </template>
- </div>
+ </button>
+ </template>
</div>
</template>
@@ -43,30 +53,32 @@
<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);
+ flex: 1 0 auto;
}
- .search-button {
- margin-left: .5em;
- margin-right: .5em;
+ .cancel-search {
+ height: 50px;
}
- .icon-cancel {
- cursor: pointer;
+ .cancel-icon {
+ color: $fallback--text;
+ color: var(--btnTopBarText, $fallback--text);
}
}
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
index 6bc64ed0..552ca41f 100644
--- a/src/components/settings_modal/settings_modal.vue
+++ b/src/components/settings_modal/settings_modal.vue
@@ -30,13 +30,13 @@
</template>
</transition>
<button
- class="btn"
+ class="btn button-default"
@click="peekModal"
>
{{ $t('general.peek') }}
</button>
<button
- class="btn"
+ class="btn button-default"
@click="closeModal"
>
{{ $t('general.close') }}
diff --git a/src/components/settings_modal/settings_modal_content.js b/src/components/settings_modal/settings_modal_content.js
index ef1a5ffa..9dcf1b5a 100644
--- 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
index a3fef1cf..f066234c 100644
--- 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
index bc30a0ff..c9ed2a38 100644
--- 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/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js
index 04273211..6e95f7af 100644
--- 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 BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faChevronDown
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faChevronDown
+)
const FilteringTab = {
data () {
diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue
index 6c42718b..18dd4be9 100644
--- 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>
@@ -72,6 +75,7 @@
<p>{{ $t('settings.filtering_explanation') }}</p>
<textarea
id="muteWords"
+ class="resize-height"
v-model="muteWordsString"
/>
</div>
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index 679ef684..2db523be 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -2,6 +2,16 @@ import BooleanSetting from '../helpers/boolean_setting.vue'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faChevronDown,
+ faGlobe
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faChevronDown,
+ faGlobe
+)
const GeneralTab = {
data () {
@@ -24,6 +34,10 @@ const GeneralTab = {
return this.$store.state.instance.postFormats || []
},
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
+ instanceWallpaperUsed () {
+ return this.$store.state.instance.background &&
+ !this.$store.state.users.currentUser.background_image
+ },
...SharedComputedObject()
}
}
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index f794ad6b..92cd2069 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -11,6 +11,11 @@
{{ $t('settings.hide_isp') }}
</BooleanSetting>
</li>
+ <li v-if="instanceWallpaperUsed">
+ <Checkbox v-model="hideInstanceWallpaper">
+ {{ $t('settings.hide_wallpaper') }}
+ </Checkbox>
+ </li>
</ul>
</div>
<div class="setting-item">
@@ -103,7 +108,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>
@@ -127,7 +135,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>
@@ -222,7 +233,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/mutes_and_blocks_tab.vue b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
index 5a1cf2c0..63d36bf9 100644
--- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
@@ -27,7 +27,7 @@
<div class="bulk-actions">
<ProgressButton
v-if="selected.length > 0"
- class="btn btn-default bulk-action-button"
+ class="btn button-default bulk-action-button"
:click="() => blockUsers(selected)"
>
{{ $t('user_card.block') }}
@@ -37,7 +37,7 @@
</ProgressButton>
<ProgressButton
v-if="selected.length > 0"
- class="btn btn-default"
+ class="btn button-default"
:click="() => unblockUsers(selected)"
>
{{ $t('user_card.unblock') }}
@@ -85,7 +85,7 @@
<div class="bulk-actions">
<ProgressButton
v-if="selected.length > 0"
- class="btn btn-default"
+ class="btn button-default"
:click="() => muteUsers(selected)"
>
{{ $t('user_card.mute') }}
@@ -95,7 +95,7 @@
</ProgressButton>
<ProgressButton
v-if="selected.length > 0"
- class="btn btn-default"
+ class="btn button-default"
:click="() => unmuteUsers(selected)"
>
{{ $t('user_card.unmute') }}
@@ -141,7 +141,7 @@
<div class="bulk-actions">
<ProgressButton
v-if="selected.length > 0"
- class="btn btn-default"
+ class="btn button-default"
:click="() => unmuteDomains(selected)"
>
{{ $t('domain_mute_card.unmute') }}
diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue
index 86eed3f5..8f8fe48e 100644
--- a/src/components/settings_modal/tabs/notifications_tab.vue
+++ b/src/components/settings_modal/tabs/notifications_tab.vue
@@ -21,7 +21,7 @@
<p>{{ $t('settings.notification_mutes') }}</p>
<p>{{ $t('settings.notification_blocks') }}</p>
<button
- class="btn btn-default"
+ class="btn button-default"
@click="updateNotificationSettings"
>
{{ $t('general.submit') }}
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index bd6bef6a..9709424c 100644
--- 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 () {
@@ -33,9 +45,7 @@ const ProfileTab = {
banner: null,
bannerPreview: null,
background: null,
- backgroundPreview: null,
- bannerUploadError: null,
- backgroundUploadError: null
+ backgroundPreview: null
}
},
components: {
@@ -56,8 +66,7 @@ const ProfileTab = {
...this.$store.state.instance.emoji,
...this.$store.state.instance.customEmoji
],
- users: this.$store.state.users.users,
- updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
+ store: this.$store
})
},
emojiSuggestor () {
@@ -67,10 +76,7 @@ const ProfileTab = {
] })
},
userSuggestor () {
- return suggestor({
- users: this.$store.state.users.users,
- updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
- })
+ return suggestor({ store: this.$store })
},
fieldsLimits () {
return this.$store.state.instance.fieldsLimits
@@ -154,18 +160,18 @@ const ProfileTab = {
if (file.size > this.$store.state.instance[slot + 'limit']) {
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
- this[slot + 'UploadError'] = [
- this.$t('upload.error.base'),
- this.$t(
- 'upload.error.file_too_big',
- {
+ this.$store.dispatch('pushGlobalNotice', {
+ messageKey: 'upload.error.message',
+ messageArgs: [
+ this.$t('upload.error.file_too_big', {
filesize: filesize.num,
filesizeunit: filesize.unit,
allowedsize: allowedsize.num,
allowedsizeunit: allowedsize.unit
- }
- )
- ].join(' ')
+ })
+ ],
+ level: 'error'
+ })
return
}
// eslint-disable-next-line no-undef
@@ -205,8 +211,9 @@ const ProfileTab = {
that.$store.commit('setCurrentUser', user)
resolve()
})
- .catch((err) => {
- reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
+ .catch((error) => {
+ that.displayUploadError(error)
+ reject(error)
})
}
@@ -227,24 +234,27 @@ const ProfileTab = {
this.$store.commit('setCurrentUser', user)
this.bannerPreview = null
})
- .catch((err) => {
- this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
- })
- .then(() => { this.bannerUploading = false })
+ .catch(this.displayUploadError)
+ .finally(() => { this.bannerUploading = false })
},
submitBackground (background) {
if (!this.backgroundPreview && background !== '') { return }
this.backgroundUploading = true
- this.$store.state.api.backendInteractor.updateProfileImages({ background }).then((data) => {
- if (!data.error) {
+ this.$store.state.api.backendInteractor.updateProfileImages({ background })
+ .then((data) => {
this.$store.commit('addNewUsers', [data])
this.$store.commit('setCurrentUser', data)
this.backgroundPreview = null
- } else {
- this.backgroundUploadError = this.$t('upload.error.base') + data.error
- }
- this.backgroundUploading = false
+ })
+ .catch(this.displayUploadError)
+ .finally(() => { this.backgroundUploading = false })
+ },
+ displayUploadError (error) {
+ this.$store.dispatch('pushGlobalNotice', {
+ messageKey: 'upload.error.message',
+ messageArgs: [error.message],
+ level: 'error'
})
}
}
diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss
index e14cf054..111eaed3 100644
--- a/src/components/settings_modal/tabs/profile_tab.scss
+++ b/src/components/settings_modal/tabs/profile_tab.scss
@@ -111,18 +111,17 @@
.profile-fields {
display: flex;
- &>.emoji-input {
+ & > .emoji-input {
flex: 1 1 auto;
- margin: 0 .2em .5em;
+ margin: 0 0.2em 0.5em;
min-width: 0;
}
- &>.icon-container {
+ .delete-field {
width: 20px;
-
- &>.icon-cancel {
- vertical-align: sub;
- }
+ align-self: center;
+ margin: 0 0.2em 0.5em;
+ padding: 0 0.5em;
}
}
}
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index cf88c4e4..175a0219 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -11,7 +11,7 @@
<input
id="username"
v-model="newName"
- classname="name-changer"
+ class="name-changer"
>
</EmojiInput>
<p>{{ $t('settings.bio') }}</p>
@@ -22,7 +22,7 @@
>
<textarea
v-model="newBio"
- classname="bio"
+ class="bio resize-height"
/>
</EmojiInput>
<p>
@@ -124,24 +124,24 @@
:placeholder="$t('settings.profile_fields.value')"
>
</EmojiInput>
- <div
- class="icon-container"
+ <button
+ class="delete-field button-unstyled -hover-highlight"
+ @click="deleteField(i)"
>
- <i
+ <FAIcon
v-show="newFields.length > 1"
- class="icon-cancel"
- @click="deleteField(i)"
+ icon="times"
/>
- </div>
+ </button>
</div>
- <a
+ <button
v-if="newFields.length < maxFields"
- class="add-field faint"
+ class="add-field faint button-unstyled -hover-highlight"
@click="addField"
>
- <i class="icon-plus" />
+ <FAIcon icon="plus" />
{{ $t("settings.profile_fields.add_field") }}
- </a>
+ </button>
</div>
<p>
<Checkbox v-model="bot">
@@ -150,7 +150,7 @@
</p>
<button
:disabled="newName && newName.length === 0"
- class="btn btn-default"
+ class="btn button-default"
@click="updateProfile"
>
{{ $t('general.submit') }}
@@ -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"
/>
@@ -178,7 +179,7 @@
<button
v-show="pickAvatarBtnVisible"
id="pick-avatar"
- class="btn"
+ class="button-default btn"
type="button"
>
{{ $t('settings.upload_a_photo') }}
@@ -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,36 +216,29 @@
@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"
- class="btn btn-default"
+ class="btn button-default"
@click="submitBanner(banner)"
>
{{ $t('general.submit') }}
</button>
- <div
- v-if="bannerUploadError"
- class="alert error"
- >
- Error: {{ bannerUploadError }}
- <i
- class="button-icon icon-cancel"
- @click="clearUploadError('banner')"
- />
- </div>
</div>
<div class="setting-item">
<h2>{{ $t('settings.profile_background') }}</h2>
<div class="banner-background-preview">
<img :src="user.background_image">
- <i
+ <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,27 +255,19 @@
@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"
- class="btn btn-default"
+ class="btn button-default"
@click="submitBackground(background)"
>
{{ $t('general.submit') }}
</button>
- <div
- v-if="backgroundUploadError"
- class="alert error"
- >
- Error: {{ backgroundUploadError }}
- <i
- class="button-icon icon-cancel"
- @click="clearUploadError('background')"
- />
- </div>
</div>
</div>
</template>
diff --git a/src/components/settings_modal/tabs/security_tab/confirm.vue b/src/components/settings_modal/tabs/security_tab/confirm.vue
index 69b3811b..38c2a610 100644
--- a/src/components/settings_modal/tabs/security_tab/confirm.vue
+++ b/src/components/settings_modal/tabs/security_tab/confirm.vue
@@ -2,14 +2,14 @@
<div>
<slot />
<button
- class="btn btn-default"
+ class="btn button-default"
:disabled="disabled"
@click="confirm"
>
{{ $t('general.confirm') }}
</button>
<button
- class="btn btn-default"
+ class="btn button-default"
:disabled="disabled"
@click="cancel"
>
diff --git a/src/components/settings_modal/tabs/security_tab/mfa.vue b/src/components/settings_modal/tabs/security_tab/mfa.vue
index 7aca3c8d..455d17b6 100644
--- a/src/components/settings_modal/tabs/security_tab/mfa.vue
+++ b/src/components/settings_modal/tabs/security_tab/mfa.vue
@@ -29,7 +29,7 @@
/>
<button
v-if="!confirmNewBackupCodes"
- class="btn btn-default"
+ class="btn button-default"
@click="getBackupCodes"
>
{{ $t('settings.mfa.generate_new_recovery_codes') }}
@@ -61,7 +61,7 @@
<button
v-if="canSetupOTP"
- class="btn btn-default"
+ class="btn button-default"
@click="cancelSetup"
>
{{ $t('general.cancel') }}
@@ -69,7 +69,7 @@
<button
v-if="canSetupOTP"
- class="btn btn-default"
+ class="btn button-default"
@click="setupOTP"
>
{{ $t('settings.mfa.setup_otp') }}
@@ -108,13 +108,13 @@
>
<div class="confirm-otp-actions">
<button
- class="btn btn-default"
+ class="btn button-default"
@click="doConfirmOTP"
>
{{ $t('settings.mfa.confirm_and_enable') }}
</button>
<button
- class="btn btn-default"
+ class="btn button-default"
@click="cancelSetup"
>
{{ $t('general.cancel') }}
diff --git a/src/components/settings_modal/tabs/security_tab/mfa_totp.vue b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue
index c6f2cc7b..8e767bd0 100644
--- a/src/components/settings_modal/tabs/security_tab/mfa_totp.vue
+++ b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue
@@ -4,7 +4,7 @@
<strong>{{ $t('settings.mfa.otp') }}</strong>
<button
v-if="!isActivated"
- class="btn btn-default"
+ class="btn button-default"
@click="doActivate"
>
{{ $t('general.enable') }}
@@ -12,7 +12,7 @@
<button
v-if="isActivated"
- class="btn btn-default"
+ class="btn button-default"
:disabled="deactivate"
@click="doDeactivate"
>
diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.js b/src/components/settings_modal/tabs/security_tab/security_tab.js
index 811161a5..65d20fc0 100644
--- a/src/components/settings_modal/tabs/security_tab/security_tab.js
+++ b/src/components/settings_modal/tabs/security_tab/security_tab.js
@@ -1,6 +1,7 @@
import ProgressButton from 'src/components/progress_button/progress_button.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Mfa from './mfa.vue'
+import localeService from 'src/services/locale/locale.service.js'
const SecurityTab = {
data () {
@@ -37,7 +38,7 @@ const SecurityTab = {
return {
id: oauthToken.id,
appName: oauthToken.app_name,
- validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
+ validUntil: new Date(oauthToken.valid_until).toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale))
}
})
}
diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue
index 3d32d73d..56bea1f4 100644
--- a/src/components/settings_modal/tabs/security_tab/security_tab.vue
+++ b/src/components/settings_modal/tabs/security_tab/security_tab.vue
@@ -19,7 +19,7 @@
>
</div>
<button
- class="btn btn-default"
+ class="btn button-default"
@click="changeEmail"
>
{{ $t('general.submit') }}
@@ -57,7 +57,7 @@
>
</div>
<button
- class="btn btn-default"
+ class="btn button-default"
@click="changePassword"
>
{{ $t('general.submit') }}
@@ -92,7 +92,7 @@
<td>{{ oauthToken.validUntil }}</td>
<td class="actions">
<button
- class="btn btn-default"
+ class="btn button-default"
@click="revokeToken(oauthToken.id)"
>
{{ $t('settings.revoke_token') }}
@@ -116,7 +116,7 @@
type="password"
>
<button
- class="btn btn-default"
+ class="btn button-default"
@click="deleteAccount"
>
{{ $t('settings.delete_account') }}
@@ -130,7 +130,7 @@
</p>
<button
v-if="!deletingAccount"
- class="btn btn-default"
+ class="btn button-default"
@click="confirmDelete"
>
{{ $t('general.submit') }}
diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue
index 9d984659..7ac7b9d3 100644
--- a/src/components/settings_modal/tabs/theme_tab/preview.vue
+++ b/src/components/settings_modal/tabs/theme_tab/preview.vue
@@ -15,7 +15,7 @@
<span class="alert error">
{{ $t('settings.style.preview.error') }}
</span>
- <button class="btn">
+ <button class="btn button-default">
{{ $t('settings.style.preview.button') }}
</button>
</div>
@@ -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>
@@ -94,7 +102,7 @@
>
<label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label>
</span>
- <button class="btn">
+ <button class="btn button-default">
{{ $t('settings.style.preview.button') }}
</button>
</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
index e3c5e80a..6cf75fe7 100644
--- 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.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
index 926eceff..1b7d9f31 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
@@ -165,7 +165,8 @@
border-color: var(--border, $fallback--border);
margin: 1em 0;
padding: 1em;
- background: var(--body-background-image);
+ background-color: var(--wallpaper);
+ background-image: var(--body-background-image);
background-size: cover;
background-position: 50% 50%;
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
index 5328c350..b8add42f 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
@@ -12,13 +12,13 @@
<div class="buttons">
<template v-if="themeWarning.type === 'snapshot_source_mismatch'">
<button
- class="btn"
+ class="btn button-default"
@click="forceLoad"
>
{{ $t('settings.style.switcher.use_source') }}
</button>
<button
- class="btn"
+ class="btn button-default"
@click="forceSnapshot"
>
{{ $t('settings.style.switcher.use_snapshot') }}
@@ -26,7 +26,7 @@
</template>
<template v-else-if="themeWarning.noActionsPossible">
<button
- class="btn"
+ class="btn button-default"
@click="dismissWarning"
>
{{ $t('general.dismiss') }}
@@ -34,13 +34,13 @@
</template>
<template v-else>
<button
- class="btn"
+ class="btn button-default"
@click="forceLoad"
>
{{ $t('settings.style.switcher.load_theme') }}
</button>
<button
- class="btn"
+ class="btn button-default"
@click="dismissWarning"
>
{{ $t('settings.style.switcher.keep_as_is') }}
@@ -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>
@@ -128,13 +131,13 @@
<p>{{ $t('settings.theme_help') }}</p>
<div class="tab-header-buttons">
<button
- class="btn"
+ class="btn button-default"
@click="clearOpacity"
>
{{ $t('settings.style.switcher.clear_opacity') }}
</button>
<button
- class="btn"
+ class="btn button-default"
@click="clearV1"
>
{{ $t('settings.style.switcher.clear_all') }}
@@ -235,13 +238,13 @@
<div class="tab-header">
<p>{{ $t('settings.theme_help') }}</p>
<button
- class="btn"
+ class="btn button-default"
@click="clearOpacity"
>
{{ $t('settings.style.switcher.clear_opacity') }}
</button>
<button
- class="btn"
+ class="btn button-default"
@click="clearV1"
>
{{ $t('settings.style.switcher.clear_all') }}
@@ -614,6 +617,15 @@
/>
</div>
<div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.wallpaper') }}</h4>
+ <ColorInput
+ v-model="wallpaperColorLocal"
+ name="wallpaper"
+ :label="$t('settings.style.advanced_colors.wallpaper')"
+ :fallback="previewTheme.colors.wallpaper"
+ />
+ </div>
+ <div class="color-item">
<h4>{{ $t('settings.style.advanced_colors.poll') }}</h4>
<ColorInput
v-model="pollColorLocal"
@@ -803,7 +815,7 @@
<div class="tab-header">
<p>{{ $t('settings.radii_help') }}</p>
<button
- class="btn"
+ class="btn button-default"
@click="clearRoundness"
>
{{ $t('settings.style.switcher.clear_all') }}
@@ -907,7 +919,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">
@@ -930,7 +945,7 @@
/>
</div>
<button
- class="btn"
+ class="btn button-default"
@click="clearShadows"
>
{{ $t('settings.style.switcher.clear_all') }}
@@ -974,7 +989,7 @@
<div class="tab-header">
<p>{{ $t('settings.style.fonts.help') }}</p>
<button
- class="btn"
+ class="btn button-default"
@click="clearFonts"
>
{{ $t('settings.style.switcher.clear_all') }}
@@ -1011,14 +1026,14 @@
<div class="apply-container">
<button
- class="btn submit"
+ class="btn button-default submit"
:disabled="!themeValid"
@click="setCustomTheme"
>
{{ $t('general.apply') }}
</button>
<button
- class="btn"
+ class="btn button-default"
@click="clearAll"
>
{{ $t('settings.style.switcher.reset') }}
diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js
index f9e7b985..800c39d5 100644
--- 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
index 815a9e59..37d491f0 100644
--- 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"
+ class="btn button-default"
:disabled="!ready || !present"
@click="del"
>
- <i class="icon-cancel" />
+ <FAIcon
+ fixed-width
+ icon="times"
+ />
</button>
<button
- class="btn btn-default"
+ class="btn button-default"
:disabled="!moveUpValid"
@click="moveUp"
>
- <i class="icon-up-open" />
+ <FAIcon
+ fixed-width
+ icon="chevron-up"
+ />
</button>
<button
- class="btn btn-default"
+ class="btn button-default"
:disabled="!moveDnValid"
@click="moveDn"
>
- <i class="icon-down-open" />
+ <FAIcon
+ fixed-width
+ icon="chevron-down"
+ />
</button>
<button
- class="btn btn-default"
+ class="btn button-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
index 281052e5..fe736168 100644
--- 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
index eda5a68c..223b1632 100644
--- 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>
@@ -89,8 +109,12 @@
v-if="chat"
@click="toggleDrawer"
>
- <router-link :to="{ name: 'chat' }">
- <i class="button-icon icon-megaphone" /> {{ $t("shoutbox.title") }}
+ <router-link :to="{ name: 'chat-panel' }">
+ <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,20 +136,32 @@
@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">
- <a
- href="#"
+ <button
+ class="button-unstyled -link -fullwidth"
@click="openSettingsModal"
>
- <i class="button-icon icon-cog" /> {{ $t("settings.settings") }}
- </a>
+ <FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="cog"
+ /> {{ $t("settings.settings") }}
+ </button>
</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,19 +172,27 @@
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
v-if="currentUser"
@click="toggleDrawer"
>
- <a
- href="#"
+ <button
+ class="button-unstyled -link -fullwidth"
@click="doLogout"
>
- <i class="button-icon icon-logout" /> {{ $t("login.logout") }}
- </a>
+ <FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="sign-out-alt"
+ /> {{ $t("login.logout") }}
+ </button>
</li>
</ul>
</div>
@@ -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 {
@@ -282,9 +331,12 @@
.side-drawer li {
padding: 0;
- a {
+ a, button {
+ 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
index e48b2eb8..2bf93a9e 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -17,6 +17,41 @@ import { highlightClass, highlightStyle } from '../../services/user_highlighter/
import { muteWordHits } from '../../services/status_parser/status_parser.js'
import { unescape, uniqBy } from 'lodash'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faEnvelope,
+ faLock,
+ faLockOpen,
+ faGlobe,
+ faTimes,
+ faRetweet,
+ faReply,
+ faPlusSquare,
+ faSmileBeam,
+ faEllipsisH,
+ faStar,
+ faEyeSlash,
+ faEye,
+ faThumbtack
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faEnvelope,
+ faGlobe,
+ faLock,
+ faLockOpen,
+ faTimes,
+ faRetweet,
+ faReply,
+ faPlusSquare,
+ faStar,
+ faSmileBeam,
+ faEllipsisH,
+ faEyeSlash,
+ faEye,
+ faThumbtack
+)
+
const Status = {
name: 'Status',
components: {
@@ -122,6 +157,7 @@ const Status = {
return muteWordHits(this.status, this.muteWords)
},
muted () {
+ if (this.statusoid.user.id === this.currentUser.id) return false
const { status } = this
const { reblog } = status
const relationship = this.$store.getters.relationship(status.user.id)
@@ -227,13 +263,13 @@ const Status = {
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) {
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index 66a91c1e..58b55bc8 100644
--- 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 {
@@ -28,6 +29,8 @@ $status-margin: 0.75em;
&.-conversation {
border-left-width: 4px;
border-left-style: solid;
+ border-left-color: $fallback--cRed;
+ border-left-color: var(--cRed, $fallback--cRed);
}
.gravestone {
@@ -58,6 +61,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;
}
@@ -127,6 +139,20 @@ $status-margin: 0.75em;
.heading-right {
display: flex;
flex-shrink: 0;
+
+ .button-unstyled {
+ padding: 5px;
+ margin: -5px;
+
+ &:hover svg {
+ color: $fallback--lightText;
+ color: var(--lightText, $fallback--lightText);
+ }
+ }
+
+ .svg-inline--fa {
+ margin-left: 0.25em;
+ }
}
.timeago {
@@ -156,11 +182,6 @@ $status-margin: 0.75em;
text-overflow: ellipsis;
overflow-x: hidden;
}
-
- .icon-reply {
- // mirror the icon
- transform: scaleX(-1);
- }
}
& .reply-to-popover,
@@ -200,7 +221,6 @@ $status-margin: 0.75em;
}
.reply-to {
- display: flex;
position: relative;
}
@@ -208,7 +228,6 @@ $status-margin: 0.75em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- margin-left: 0.2em;
}
.replies-separator {
@@ -232,16 +251,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);
}
}
@@ -291,18 +304,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
index ffae32fc..6ee8117f 100644
--- 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-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 }}
@@ -44,11 +47,15 @@
>
{{ muteWordHits.join(', ') }}
</small>
- <a
- href="#"
- class="unmute"
+ <button
+ class="unmute button-unstyled"
@click.prevent="toggleMute"
- ><i class="button-icon icon-eye-off" /></a>
+ >
+ <FAIcon
+ icon="eye-slash"
+ class="fa-scale-110 fa-old-padding"
+ />
+ </button>
</div>
</template>
<template v-else>
@@ -56,7 +63,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 +96,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') }}
@@ -167,38 +178,40 @@
: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
+ fixed-width
+ class="fa-scale-110"
+ :icon="visibilityIcon(status.visibility)"
/>
- </div>
- <a
- v-if="!status.is_local && !isPreview"
- :href="status.external_url"
- target="_blank"
- class="source_url"
- title="Source"
+ </span>
+ <button
+ v-if="expandable && !isPreview"
+ class="button-unstyled"
+ :title="$t('status.expand')"
+ @click.prevent="toggleExpanded"
>
- <i class="button-icon icon-link-ext-alt" />
- </a>
- <template v-if="expandable && !isPreview">
- <a
- href="#"
- title="Expand"
- @click.prevent="toggleExpanded"
- >
- <i class="button-icon icon-plus-squared" />
- </a>
- </template>
- <a
+ <FAIcon
+ fixed-width
+ class="fa-scale-110"
+ icon="plus-square"
+ />
+ </button>
+ <button
v-if="unmuted"
- href="#"
+ class="button-unstyled"
@click.prevent="toggleMute"
- ><i class="button-icon icon-eye-off" /></a>
+ >
+ <FAIcon
+ fixed-width
+ icon="eye-slash"
+ class="fa-scale-110"
+ />
+ </button>
</span>
</div>
@@ -214,19 +227,22 @@
style="min-width: 0"
:class="{ '-strikethrough': !status.parent_visible }"
>
- <a
- class="reply-to"
- href="#"
+ <button
+ class="button-unstyled reply-to"
: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"
>
{{ $t('status.reply_to') }}
</span>
- </a>
+ </button>
</StatusPopover>
<span
@@ -259,11 +275,12 @@
:key="reply.id"
:status-id="reply.id"
>
- <a
- href="#"
- class="reply-link"
+ <button
+ class="button-unstyled -link reply-link"
@click.prevent="gotoOriginal(reply.id)"
- >{{ reply.name }}</a>
+ >
+ {{ reply.name }}
+ </button>
</StatusPopover>
</div>
</div>
@@ -348,7 +365,6 @@
@onSuccess="clearError"
/>
</div>
-
</div>
</div>
<div
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index df095de3..a6f79d76 100644
--- 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
index f7fb5ee2..90bfaf40 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -12,35 +12,34 @@
@click.prevent="linkClicked"
v-html="status.summary_html"
/>
- <a
+ <button
v-if="longSubject && showingLongSubject"
- href="#"
- class="tall-subject-hider"
+ class="button-unstyled -link tall-subject-hider"
@click.prevent="showingLongSubject=false"
- >{{ $t("status.hide_full_subject") }}</a>
- <a
+ >
+ {{ $t("status.hide_full_subject") }}
+ </button>
+ <button
v-else-if="longSubject"
- class="tall-subject-hider"
+ class="button-unstyled -link tall-subject-hider"
:class="{ 'tall-subject-hider_focused': focused }"
- href="#"
@click.prevent="showingLongSubject=true"
>
{{ $t("status.show_full_subject") }}
- </a>
+ </button>
</div>
<div
:class="{'tall-status': hideTallStatus}"
class="status-content-wrapper"
>
- <a
+ <button
v-if="hideTallStatus"
- class="tall-status-hider"
+ class="button-unstyled -link tall-status-hider"
:class="{ 'tall-status-hider_focused': focused }"
- href="#"
@click.prevent="toggleShowMore"
>
{{ $t("general.show_more") }}
- </a>
+ </button>
<div
v-if="!hideSubjectStatus"
:class="{ 'single-line': singleLine }"
@@ -48,46 +47,44 @@
@click.prevent="linkClicked"
v-html="postBodyHtml"
/>
- <a
+ <button
v-if="hideSubjectStatus"
- href="#"
- class="cw-status-hider"
+ class="button-unstyled -link cw-status-hider"
@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
+ </button>
+ <button
v-if="showingMore && !fullContent"
- href="#"
- class="status-unhider"
+ class="button-unstyled -link status-unhider"
@click.prevent="toggleShowMore"
>
{{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
- </a>
+ </button>
</div>
<div v-if="status.poll && status.poll.options && !hideSubjectStatus">
@@ -182,6 +179,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
index 51e7680c..c47f5631 100644
--- 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
index 162eb210..8237ce00 100644
--- 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.vue b/src/components/still-image/still-image.vue
index ad82210d..d3eb5925 100644
--- 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
index 9c1da354..76e7ef03 100644
--- 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'
@@ -80,7 +81,7 @@ export default Vue.component('tab-switcher', {
const tabs = this.$slots.default
.map((slot, index) => {
if (!slot.tag) return
- const classesTab = ['tab']
+ const classesTab = ['tab', 'button-default']
const classesWrapper = ['tab-wrapper']
if (this.activeIndex === index) {
classesTab.push('active')
@@ -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
index d2ef4857..0ed614b7 100644
--- 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/timeago/timeago.vue b/src/components/timeago/timeago.vue
index 6df0524d..55a2dd94 100644
--- a/src/components/timeago/timeago.vue
+++ b/src/components/timeago/timeago.vue
@@ -9,6 +9,7 @@
<script>
import * as DateUtils from 'src/services/date_utils/date_utils.js'
+import localeService from 'src/services/locale/locale.service.js'
export default {
name: 'Timeago',
@@ -21,9 +22,10 @@ export default {
},
computed: {
localeDateString () {
+ const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale)
return typeof this.time === 'string'
- ? new Date(Date.parse(this.time)).toLocaleString()
- : this.time.toLocaleString()
+ ? new Date(Date.parse(this.time)).toLocaleString(browserLocale)
+ : this.time.toLocaleString(browserLocale)
}
},
created () {
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 17680542..665d195e 100644
--- 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 = []
@@ -34,7 +40,8 @@ const Timeline = {
paused: false,
unfocused: false,
bottomedOut: false,
- virtualScrollIndex: 0
+ virtualScrollIndex: 0,
+ blockingClicks: false
}
},
components: {
@@ -43,17 +50,10 @@ const Timeline = {
TimelineMenu
},
computed: {
- timelineError () {
- return this.$store.state.statuses.error
- },
- errorData () {
- return this.$store.state.statuses.errorData
- },
newStatusCount () {
return this.timeline.newStatusCount
},
showLoadButton () {
- if (this.timelineError || this.errorData) return false
return this.timeline.newStatusCount > 0 || this.timeline.flushMarker !== 0
},
loadButtonString () {
@@ -64,8 +64,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'] : [])
@@ -124,6 +126,15 @@ const Timeline = {
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
@@ -135,6 +146,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
}
@@ -152,11 +164,12 @@ const Timeline = {
userId: this.userId,
tag: this.tag
}).then(({ statuses }) => {
- store.commit('setLoading', { timeline: this.timelineName, value: false })
if (statuses && statuses.length === 0) {
this.bottomedOut = true
}
- })
+ }).finally(() =>
+ store.commit('setLoading', { timeline: this.timelineName, value: false })
+ )
}, 1000, this),
determineVisibleStatuses () {
if (!this.$refs.timeline) return
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index c1e2f44b..4c43fe5c 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -1,24 +1,10 @@
<template>
- <div :class="[classes.root, 'timeline']">
+ <div :class="[classes.root, 'Timeline']">
<div :class="classes.header">
<TimelineMenu v-if="!embedded" />
- <div
- v-if="timelineError"
- class="loadmore-error alert error"
- @click.prevent
- >
- {{ $t('timeline.error_fetching') }}
- </div>
- <div
- v-else-if="errorData"
- class="loadmore-error alert error"
- @click.prevent
- >
- {{ errorData.statusText }}
- </div>
<button
- v-else-if="showLoadButton"
- class="loadmore-button"
+ v-if="showLoadButton"
+ class="button-default loadmore-button"
@click.prevent="showNewStatuses"
>
{{ loadButtonString }}
@@ -75,24 +61,24 @@
>
{{ $t('timeline.no_more_statuses') }}
</div>
- <a
- v-else-if="!timeline.loading && !errorData"
- href="#"
+ <button
+ v-else-if="!timeline.loading"
+ class="button-unstyled -link -fullwidth"
@click.prevent="fetchOlderStatuses()"
>
- <div class="new-status-notification text-center panel-footer">{{ $t('timeline.load_older') }}</div>
- </a>
- <a
- v-else-if="errorData"
- href="#"
- >
- <div class="new-status-notification text-center panel-footer">{{ errorData.error }}</div>
- </a>
+ <div class="new-status-notification text-center panel-footer">
+ {{ $t('timeline.load_older') }}
+ </div>
+ </button>
<div
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>
@@ -103,15 +89,20 @@
<style lang="scss">
@import '../../_variables.scss';
-.timeline {
+.Timeline {
.loadmore-text {
opacity: 1;
}
+
+ &.-blocked {
+ cursor: progress;
+ }
}
.timeline-heading {
max-width: 100%;
flex-wrap: nowrap;
+ align-items: center;
.loadmore-button {
flex-shrink: 0;
}
diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js
index 2be75b06..8d6a58b1 100644
--- a/src/components/timeline_menu/timeline_menu.js
+++ b/src/components/timeline_menu/timeline_menu.js
@@ -1,7 +1,25 @@
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'
-// Route -> i18n key mapping, exported andnot in the computed
+library.add(
+ faUsers,
+ faGlobe,
+ faBookmark,
+ faEnvelope,
+ faHome,
+ faChevronDown
+)
+
+// Route -> i18n key mapping, exported and not in the computed
// because nav panel benefits from the same information.
export const timelineNames = () => {
return {
@@ -9,8 +27,7 @@ export const timelineNames = () => {
'bookmarks': 'nav.bookmarks',
'dms': 'nav.dms',
'public-timeline': 'nav.public_tl',
- 'public-external-timeline': 'nav.twkn',
- 'tag-timeline': 'tag'
+ 'public-external-timeline': 'nav.twkn'
}
}
@@ -42,6 +59,14 @@ const TimelineMenu = {
this.isOpen = true
}, 25)
},
+ blockOpen (event) {
+ // For the blank area inside the button element.
+ // Just setting @click.stop="" makes unintuitive behavior when
+ // menu is open and clicking on the blank area doesn't close it.
+ if (!this.isOpen) {
+ event.stopPropagation()
+ }
+ },
timelineName () {
const route = this.$route.name
if (route === 'tag-timeline') {
diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue
index b7e5f2da..3c86842b 100644
--- 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>
@@ -45,8 +65,17 @@
slot="trigger"
class="title timeline-menu-title"
>
- <span>{{ timelineName() }}</span>
- <i class="icon-down-open" />
+ <span class="timeline-title">{{ timelineName() }}</span>
+ <span>
+ <FAIcon
+ size="sm"
+ icon="chevron-down"
+ />
+ </span>
+ <span
+ class="click-blocker"
+ @click="blockOpen"
+ />
</div>
</Popover>
</template>
@@ -56,17 +85,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 +108,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,25 +121,28 @@
.timeline-menu-title {
margin: 0;
cursor: pointer;
- display: flex;
user-select: none;
width: 100%;
+ display: flex;
- span {
+ .timeline-menu-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
- i {
+ svg {
margin-left: 0.6em;
- flex-shrink: 0;
- font-size: 1rem;
transition: transform 100ms;
}
+
+ .click-blocker {
+ cursor: default;
+ flex-grow: 1;
+ }
}
- &.open .timeline-menu-title i {
+ &.open .timeline-menu-title svg {
color: $fallback--text;
color: var(--panelText, $fallback--text);
transform: rotate(180deg);
@@ -171,8 +207,9 @@
}
}
- i {
- margin-right: 0.5em;
+ 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
index eb3d375e..0f7c584b 100644
--- a/src/components/user_avatar/user_avatar.vue
+++ b/src/components/user_avatar/user_avatar.vue
@@ -20,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);
@@ -34,8 +37,8 @@
}
&.better-shadow {
- box-shadow: var(--avatarStatusShadowInset);
- filter: var(--avatarStatusShadowFilter)
+ box-shadow: var(--_avatarShadowInset);
+ filter: var(--_avatarShadowFilter);
}
&.animated::before {
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 8e6b9d7f..3a8efafc 100644
--- 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
index 041bb80f..773f764a 100644
--- 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"
@@ -76,7 +83,7 @@
v-if="!!visibleRole"
class="alert user-role"
>
- {{ visibleRole }}
+ {{ $t(`user_card.roles.${visibleRole}`) }}
</span>
<span
v-if="user.bot"
@@ -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>
@@ -146,33 +162,44 @@
<template v-if="relationship.following">
<ProgressButton
v-if="!relationship.subscribing"
- class="btn btn-default"
+ class="btn button-default"
:click="subscribeUser"
:title="$t('user_card.subscribe')"
>
- <i class="icon-bell-alt" />
+ <FAIcon icon="bell" />
</ProgressButton>
<ProgressButton
v-else
- class="btn btn-default toggled"
+ class="btn button-default toggled"
: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>
<div>
<button
v-if="relationship.muting"
- class="btn btn-default btn-block toggled"
+ class="btn button-default btn-block toggled"
@click="unmuteUser"
>
{{ $t('user_card.muted') }}
</button>
<button
v-else
- class="btn btn-default btn-block"
+ class="btn button-default btn-block"
@click="muteUser"
>
{{ $t('user_card.mute') }}
@@ -180,7 +207,7 @@
</div>
<div>
<button
- class="btn btn-default btn-block"
+ class="btn button-default btn-block"
@click="mentionUser"
>
{{ $t('user_card.mention') }}
@@ -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;
@@ -467,7 +507,6 @@
.user-role {
flex: none;
- text-transform: capitalize;
color: $fallback--text;
color: var(--alertNeutralText, $fallback--text);
background-color: $fallback--fg;
@@ -508,7 +547,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
index b60f2c4c..32ca2b8d 100644
--- 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
index 185c73ca..95673733 100644
--- 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
index 201727d4..c0b55a6c 100644
--- 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
index b26499b4..745e795d 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -20,14 +20,13 @@
:key="index"
class="user-profile-field"
>
+ <!-- eslint-disable vue/no-v-html -->
<dt
:title="user.fields_text[index].name"
class="user-profile-field-name"
@click.prevent="linkClicked"
- >
- {{ field.name }}
- </dt>
- <!-- eslint-disable vue/no-v-html -->
+ v-html="field.name"
+ />
<dd
:title="user.fields_text[index].value"
class="user-profile-field-value"
@@ -122,9 +121,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 +142,7 @@
.user-profile-fields {
margin: 0 0.5em;
+
img {
object-fit: contain;
vertical-align: middle;
diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js
index 38cf117b..8d171b2d 100644
--- a/src/components/user_reporting_modal/user_reporting_modal.js
+++ b/src/components/user_reporting_modal/user_reporting_modal.js
@@ -38,17 +38,23 @@ const UserReportingModal = {
},
statuses () {
return this.$store.state.reports.statuses
+ },
+ preTickedIds () {
+ return this.$store.state.reports.preTickedIds
}
},
watch: {
- userId: 'resetState'
+ userId: 'resetState',
+ preTickedIds (newValue) {
+ this.statusIdsToReport = newValue
+ }
},
methods: {
resetState () {
// Reset state
this.comment = ''
this.forward = false
- this.statusIdsToReport = []
+ this.statusIdsToReport = this.preTickedIds
this.processing = false
this.error = false
},
diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue
index 2a8d8d48..fb43094f 100644
--- a/src/components/user_reporting_modal/user_reporting_modal.vue
+++ b/src/components/user_reporting_modal/user_reporting_modal.vue
@@ -29,7 +29,7 @@
</div>
<div>
<button
- class="btn btn-default"
+ class="btn button-default"
:disabled="processing"
@click="reportUser"
>
diff --git a/src/components/video_attachment/video_attachment.vue b/src/components/video_attachment/video_attachment.vue
index a4bf01e8..8a3ea1e3 100644
--- a/src/components/video_attachment/video_attachment.vue
+++ b/src/components/video_attachment/video_attachment.vue
@@ -1,6 +1,7 @@
<template>
<video
class="video"
+ preload="metadata"
:src="attachment.url"
:loop="loopVideo"
:controls="controls"
diff --git a/src/hocs/with_load_more/with_load_more.js b/src/hocs/with_load_more/with_load_more.js
index 6142f513..671b2b6f 100644
--- 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
@@ -19,12 +29,8 @@ const withLoadMore = ({
return {
loading: false,
bottomedOut: false,
- error: false
- }
- },
- computed: {
- entries () {
- return select(this.$props, this.$store) || []
+ error: false,
+ entries: []
}
},
created () {
@@ -38,6 +44,11 @@ const withLoadMore = ({
destroy && destroy(this.$props, this.$store)
},
methods: {
+ // Entries is not a computed because computed can't track the dynamic
+ // selector for changes and won't trigger after fetch.
+ updateEntries () {
+ this.entries = select(this.$props, this.$store) || []
+ },
fetchEntries () {
if (!this.loading) {
this.loading = true
@@ -51,6 +62,9 @@ const withLoadMore = ({
this.loading = false
this.error = true
})
+ .finally(() => {
+ this.updateEntries()
+ })
}
},
scrollLoad (e) {
@@ -81,8 +95,12 @@ const withLoadMore = ({
{children}
</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 &&
+ <button onClick={this.fetchEntries} class="button-unstyled -link -fullwidth alert error">
+ {this.$t('general.generic_error')}
+ </button>
+ }
+ {!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
index 1775adcb..b1244276 100644
--- 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/en.json b/src/i18n/en.json
index e25bda8f..1d087bbd 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -50,7 +50,8 @@
"scope_options": "Scope options",
"text_limit": "Text limit",
"title": "Features",
- "who_to_follow": "Who to follow"
+ "who_to_follow": "Who to follow",
+ "upload_limit": "Upload limit"
},
"finder": {
"error_fetching_user": "Error fetching user",
@@ -130,6 +131,7 @@
},
"notifications": {
"broken_favorite": "Unknown status, searching for it…",
+ "error": "Error fetching notifications: {0}",
"favorited_you": "favorited your status",
"followed_you": "followed you",
"follow_request": "wants to follow you",
@@ -328,6 +330,7 @@
"hide_muted_posts": "Hide posts of muted users",
"max_thumbnails": "Maximum amount of thumbnails per post",
"hide_isp": "Hide instance-specific panel",
+ "hide_wallpaper": "Hide instance wallpaper",
"preload_images": "Preload images",
"use_one_click_nsfw": "Open NSFW attachments with just one click",
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
@@ -377,7 +380,7 @@
"hide_followers_count_description": "Don't show follower count",
"show_admin_badge": "Show Admin badge in my profile",
"show_moderator_badge": "Show Moderator badge in my profile",
- "nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding",
+ "nsfw_clickthrough": "Enable clickthrough attachment and link preview image hiding for NSFW statuses",
"oauth_tokens": "OAuth tokens",
"token": "Token",
"refresh_token": "Refresh Token",
@@ -515,6 +518,7 @@
"inputs": "Input fields",
"faint_text": "Faded text",
"underlay": "Underlay",
+ "wallpaper": "Wallpaper",
"poll": "Poll graph",
"icons": "Icons",
"highlight": "Highlighted elements",
@@ -635,7 +639,7 @@
"timeline": {
"collapse": "Collapse",
"conversation": "Conversation",
- "error_fetching": "Error fetching updates",
+ "error": "Error fetching timeline: {0}",
"load_older": "Load older statuses",
"no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated",
"repeated": "repeated",
@@ -661,13 +665,16 @@
"unmute_conversation": "Unmute conversation",
"status_unavailable": "Status unavailable",
"copy_link": "Copy link to status",
+ "external_source": "External source",
"thread_muted": "Thread muted",
"thread_muted_and_words": ", has words:",
"show_full_subject": "Show full subject",
"hide_full_subject": "Hide full subject",
"show_content": "Show content",
"hide_content": "Hide content",
- "status_deleted": "This post was deleted"
+ "status_deleted": "This post was deleted",
+ "nsfw": "NSFW",
+ "expand": "Expand"
},
"user_card": {
"approve": "Approve",
@@ -723,6 +730,10 @@
"quarantine": "Disallow user posts from federating",
"delete_user": "Delete user",
"delete_user_confirmation": "Are you absolutely sure? This action cannot be undone."
+ },
+ "roles": {
+ "admin": "Admin",
+ "moderator": "Moderator"
}
},
"user_profile": {
@@ -757,6 +768,7 @@
"upload": {
"error": {
"base": "Upload failed.",
+ "message": "Upload failed: {0}",
"file_too_big": "File too big [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Try again later"
},
diff --git a/src/i18n/eo.json b/src/i18n/eo.json
index e73ac2f8..58f8e125 100644
--- a/src/i18n/eo.json
+++ b/src/i18n/eo.json
@@ -5,12 +5,13 @@
"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",
"who_to_follow": "Kiun aboni",
- "pleroma_chat_messages": "Babilejo de Pleroma"
+ "pleroma_chat_messages": "Babilejo de Pleroma",
+ "upload_limit": "Limo de alŝutoj"
},
"finder": {
"error_fetching_user": "Eraris alporto de uzanto",
@@ -33,7 +34,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 +72,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 +82,8 @@
"search": "Serĉi",
"interactions": "Interagoj",
"administration": "Administrado",
- "bookmarks": "Legosignoj"
+ "bookmarks": "Legosignoj",
+ "timelines": "Historioj"
},
"notifications": {
"broken_favorite": "Nekonata stato, serĉante ĝin…",
@@ -93,7 +96,8 @@
"no_more_notifications": "Neniuj pliaj sciigoj",
"reacted_with": "reagis per {0}",
"migrated_to": "migris al",
- "follow_request": "volas vin aboni"
+ "follow_request": "volas vin aboni",
+ "error": "Eraris akirado de sciigoj: {0}"
},
"post_status": {
"new_status": "Afiŝi novan staton",
@@ -107,14 +111,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",
@@ -132,14 +136,14 @@
"registration": {
"bio": "Priskribo",
"email": "Retpoŝtadreso",
- "fullname": "Vidiga nomo",
+ "fullname": "Prezenta nomo",
"password_confirm": "Konfirmo de pasvorto",
"registration": "Registriĝo",
"token": "Invita ĵetono",
"captcha": "TESTO DE HOMECO",
"new_captcha": "Klaku la bildon por akiri novan teston",
"username_placeholder": "ekz. lain",
- "fullname_placeholder": "ekz. Lain Iwakura",
+ "fullname_placeholder": "ekz. Lain Ivakura",
"bio_placeholder": "ekz.\nSaluton, mi estas Lain.\nMi estas animea knabino vivanta en Japanujo. Eble vi konas min pro la retejo « Wired ».",
"validations": {
"username_required": "ne povas resti malplena",
@@ -162,7 +166,7 @@
"blocks_tab": "Blokitoj",
"btnRadius": "Butonoj",
"cBlue": "Blua (respondi, aboni)",
- "cGreen": "Verda (kunhavigi)",
+ "cGreen": "Verda (diskonigi)",
"cOrange": "Oranĝa (ŝati)",
"cRed": "Ruĝa (nuligi)",
"change_password": "Ŝanĝi pasvorton",
@@ -193,7 +197,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",
@@ -205,8 +209,8 @@
"import_theme": "Enlegi antaŭagordojn",
"inputRadius": "Enigaj kampoj",
"checkboxRadius": "Markbutonoj",
- "instance_default": "(implicita: {value})",
- "instance_default_simple": "(implicita)",
+ "instance_default": "(originale: {value})",
+ "instance_default_simple": "(originale)",
"interface": "Fasado",
"interfaceLanguage": "Lingvo de fasado",
"invalid_theme_imported": "La elektita dosiero ne estas subtenata haŭto de Pleromo. Neniuj ŝanĝoj al via haŭto okazis.",
@@ -217,7 +221,7 @@
"loop_video_silent_only": "Ripetadi nur filmojn sen sono (ekz. la «GIF-ojn» de Mastodon)",
"mutes_tab": "Silentigoj",
"play_videos_in_modal": "Ludi filmojn en ŝpruca kadro",
- "use_contain_fit": "Ne tondi la kunsendaĵon en bildetoj",
+ "use_contain_fit": "Ne pritondi bildetojn de kunsendaĵoj",
"name": "Nomo",
"name_bio": "Nomo kaj priskribo",
"new_password": "Nova pasvorto",
@@ -233,7 +237,7 @@
"hide_followers_description": "Ne montri kiu min sekvas",
"show_admin_badge": "Montri la insignon de administranto en mia profilo",
"show_moderator_badge": "Montri la insignon de reguligisto en mia profilo",
- "nsfw_clickthrough": "Ŝalti traklakan kaŝadon de konsternaj kunsendaĵoj",
+ "nsfw_clickthrough": "Ŝalti traklakan kaŝadon de kunsendaĵoj kaj antaŭmontroj de ligiloj por konsternaj statoj",
"oauth_tokens": "Ĵetonoj de OAuth",
"token": "Ĵetono",
"refresh_token": "Ĵetono de aktualigo",
@@ -246,7 +250,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",
@@ -263,7 +267,7 @@
"subject_line_email": "Kiel retpoŝto: «re: temo»",
"subject_line_mastodon": "Kiel Mastodon: kopii senŝanĝe",
"subject_line_noop": "Ne kopii",
- "post_status_content_type": "Afiŝi specon de la enhavo de la stato",
+ "post_status_content_type": "Speco de enhavo de afiŝo",
"stop_gifs": "Movi GIF-bildojn dum ŝvebo de muso",
"streaming": "Ŝalti memagan fluigon de novaj afiŝoj kiam vi vidas la supron de la paĝo",
"text": "Teksto",
@@ -297,7 +301,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 +361,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"
@@ -371,7 +381,7 @@
"hint": "Por ombroj vi ankaŭ povas uzi --variable kiel koloran valoron, por uzi variantojn de CSS3. Bonvolu rimarki, ke tiuokaze agordoj de maltravidebleco ne funkcios.",
"filter_hint": {
"always_drop_shadow": "Averto: ĉi tiu ombro ĉiam uzas {0} kiam la foliumilo tion subtenas.",
- "drop_shadow_syntax": "{0} ne subtenas parametron {1} kaj ŝlosilvorton {2}.",
+ "drop_shadow_syntax": "{0} ne subtenas parametron {1} kaj ĉefvorton {2}.",
"avatar_inset": "Bonvolu rimarki, ke agordi ambaŭ internajn kaj eksterajn ombrojn por profilbildoj povas redoni neatenditajn rezultojn ĉe profilbildoj travideblaj.",
"spread_zero": "Ombroj kun vastigo > 0 aperos kvazaŭ ĝi estus fakte nulo",
"inset_classic": "Internaj ombroj uzos {0}"
@@ -386,9 +396,10 @@
"button": "Butono",
"buttonHover": "Butono (je ŝvebo)",
"buttonPressed": "Butono (premita)",
- "buttonPressedHover": "Butono (premita kaj je ŝvebo)",
+ "buttonPressedHover": "Butono (je premo kaj ŝvebo)",
"input": "Eniga kampo"
- }
+ },
+ "hintV3": "Kolorojn de ombroj vi ankaŭ povas skribi per la sistemo {0}."
},
"fonts": {
"_tab_label": "Tiparoj",
@@ -411,7 +422,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 +431,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 +459,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 +494,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 +509,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,12 +524,13 @@
"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",
"no_statuses": "Neniuj statoj",
- "reload": "Enlegi ree"
+ "reload": "Enlegi ree",
+ "error": "Eraris akirado de historio: {0}"
},
"user_card": {
"approve": "Aprobi",
@@ -648,22 +670,23 @@
"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»",
- "keyword_policies": "Politiko pri ŝlosilvortoj"
+ "ftl_removal": "Forigo el la historio de «La tuta konata reto»",
+ "keyword_policies": "Politiko pri ĉefvortoj"
},
"federation": "Federado",
"mrf_policies_desc": "Politikoj de Mesaĝa ŝanĝilaro (MRF) efikas sur federa konduto de la nodo. La sekvaj politikoj estas ŝaltitaj:"
@@ -707,7 +730,9 @@
"pin": "Fiksi al profilo",
"delete": "Forigi staton",
"repeats": "Ripetoj",
- "favorites": "Ŝatataj"
+ "favorites": "Ŝatoj",
+ "status_deleted": "Ĉi tiu afiŝo foriĝis",
+ "nsfw": "Konsterna"
},
"time": {
"years_short": "{0}j",
@@ -718,8 +743,8 @@
"week_short": "{0}s",
"weeks": "{0} semajnoj",
"week": "{0} semajno",
- "seconds_short": "{0}s",
- "second_short": "{0}s",
+ "seconds_short": "{0}sek",
+ "second_short": "{0}sek",
"seconds": "{0} sekundoj",
"second": "{0} sekundo",
"now_short": "nun",
@@ -728,14 +753,14 @@
"month_short": "{0}m",
"months": "{0} monatoj",
"month": "{0} monato",
- "minutes_short": "{0}m",
- "minute_short": "{0}m",
+ "minutes_short": "{0}min",
+ "minute_short": "{0}min",
"minutes": "{0} minutoj",
"minute": "{0} minuto",
"in_past": "antaŭ {0}",
"in_future": "post {0}",
- "hours_short": "{0}h",
- "hour_short": "{0}h",
+ "hours_short": "{0}hor",
+ "hour_short": "{0}hor",
"hours": "{0} horoj",
"hour": "{0} horo",
"days_short": "{0}t",
@@ -769,7 +794,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 +817,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
index 6889df9a..f4d87eb3 100644
--- a/src/i18n/es.json
+++ b/src/i18n/es.json
@@ -14,7 +14,8 @@
"text_limit": "Límite de caracteres",
"title": "Características",
"who_to_follow": "A quién seguir",
- "pleroma_chat_messages": "Chat de Pleroma"
+ "pleroma_chat_messages": "Chat de Pleroma",
+ "upload_limit": "Límite de subida"
},
"finder": {
"error_fetching_user": "Error al buscar usuario",
@@ -104,7 +105,8 @@
"no_more_notifications": "No hay más notificaciones",
"reacted_with": "reaccionó con {0}",
"migrated_to": "migrado a",
- "follow_request": "quiere seguirte"
+ "follow_request": "quiere seguirte",
+ "error": "Error obteniendo notificaciones:{0}"
},
"polls": {
"add_poll": "Añadir encuesta",
@@ -313,7 +315,7 @@
"hide_followers_count_description": "No mostrar el número de cuentas que me siguen",
"show_admin_badge": "Mostrar la insignia de Administrador en mi perfil",
"show_moderator_badge": "Mostrar la insignia de Moderador en mi perfil",
- "nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW",
+ "nsfw_clickthrough": "Habilitar la ocultación de la imagen de vista previa del enlace y el adjunto para los estados NSFW por defecto",
"oauth_tokens": "Tokens de OAuth",
"token": "Token",
"refresh_token": "Actualizar el token",
@@ -447,7 +449,8 @@
"underlay": "Subrayado",
"popover": "Sugerencias, menús, superposiciones",
"post": "Publicaciones/Biografías de Usuarios",
- "alert_warning": "Precaución"
+ "alert_warning": "Precaución",
+ "wallpaper": "Fondo de pantalla"
},
"radii": {
"_tab_label": "Redondez"
@@ -558,7 +561,8 @@
"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"
+ "mute_export": "Exportar silenciados",
+ "hide_wallpaper": "Ocultar el fondo de pantalla de la instancia"
},
"time": {
"day": "{0} día",
@@ -605,7 +609,8 @@
"up_to_date": "Actualizado",
"no_more_statuses": "No hay más estados",
"no_statuses": "Sin estados",
- "reload": "Recargar"
+ "reload": "Recargar",
+ "error": "Error obteniendo la linea de tiempo:{0}"
},
"status": {
"favorites": "Favoritos",
@@ -628,7 +633,11 @@
"copy_link": "Copiar el enlace al estado",
"status_unavailable": "Estado no disponible",
"bookmark": "Marcar",
- "unbookmark": "Desmarcar"
+ "unbookmark": "Desmarcar",
+ "status_deleted": "Esta entrada ha sido eliminada",
+ "nsfw": "NSFW (No apropiado para el trabajo)",
+ "expand": "Expandir",
+ "external_source": "Fuente externa"
},
"user_card": {
"approve": "Aprobar",
@@ -719,7 +728,8 @@
"error": {
"base": "Subida fallida.",
"file_too_big": "Archivo demasiado grande [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
- "default": "Inténtalo más tarde"
+ "default": "Inténtalo más tarde",
+ "message": "Error de subida: {0}"
},
"file_size_units": {
"B": "B",
diff --git a/src/i18n/he.json b/src/i18n/he.json
index 7f2bf58f..4b920536 100644
--- a/src/i18n/he.json
+++ b/src/i18n/he.json
@@ -390,5 +390,13 @@
"GiB": "GiB",
"TiB": "TiB"
}
+ },
+ "about": {
+ "mrf": {
+ "keyword": {
+ "keyword_policies": "פוליסת מילות מפתח"
+ },
+ "federation": "פדרציה"
+ }
}
}
diff --git a/src/i18n/it.json b/src/i18n/it.json
index ce508630..b7d21e7e 100644
--- a/src/i18n/it.json
+++ b/src/i18n/it.json
@@ -30,7 +30,7 @@
"administration": "Amministrazione",
"back": "Indietro",
"interactions": "Interazioni",
- "dms": "Messaggi diretti",
+ "dms": "Messaggi privati",
"user_search": "Ricerca utenti",
"search": "Ricerca",
"who_to_follow": "Chi seguire",
@@ -44,13 +44,14 @@
"notifications": "Notifiche",
"read": "Letto!",
"broken_favorite": "Stato sconosciuto, lo sto cercando…",
- "favorited_you": "ha gradito il tuo messaggio",
+ "favorited_you": "gradisce il tuo messaggio",
"load_older": "Carica notifiche precedenti",
"repeated_you": "ha condiviso il tuo messaggio",
"follow_request": "vuole seguirti",
"no_more_notifications": "Fine delle notifiche",
"migrated_to": "è migrato verso",
- "reacted_with": "ha reagito con {0}"
+ "reacted_with": "ha reagito con {0}",
+ "error": "Errore nel caricare le notifiche: {0}"
},
"settings": {
"attachments": "Allegati",
@@ -262,7 +263,8 @@
"border": "Bordo",
"outgoing": "Inviati",
"incoming": "Ricevuti"
- }
+ },
+ "wallpaper": "Sfondo"
},
"common_colors": {
"rgbo": "Icone, accenti, medaglie",
@@ -381,7 +383,7 @@
"preload_images": "Precarica immagini",
"hide_isp": "Nascondi pannello della stanza",
"max_thumbnails": "Numero massimo di anteprime per messaggio",
- "hide_muted_posts": "Nascondi messaggi degli utenti zittiti",
+ "hide_muted_posts": "Nascondi messaggi degli utenti zilenziati",
"accent": "Accento",
"emoji_reactions_on_timeline": "Mostra emoji di reazione sulle sequenze",
"pad_emoji": "Affianca spazi agli emoji inseriti tramite selettore",
@@ -414,7 +416,8 @@
"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"
+ "mute_export": "Esporta silenziati",
+ "hide_wallpaper": "Nascondi sfondo della stanza"
},
"timeline": {
"error_fetching": "Errore nell'aggiornamento",
@@ -427,7 +430,8 @@
"repeated": "condiviso",
"no_statuses": "Nessun messaggio",
"no_more_statuses": "Fine dei messaggi",
- "reload": "Ricarica"
+ "reload": "Ricarica",
+ "error": "Errore nel caricare la sequenza: {0}"
},
"user_card": {
"follow": "Segui",
@@ -483,7 +487,11 @@
"follow_progress": "Richiedo…",
"follow_sent": "Richiesta inviata!",
"favorites": "Preferiti",
- "message": "Contatta"
+ "message": "Contatta",
+ "roles": {
+ "moderator": "Moderatore",
+ "admin": "Amministratore"
+ }
},
"chat": {
"title": "Chat"
@@ -491,16 +499,17 @@
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
- "media_proxy": "Proxy multimedia",
+ "media_proxy": "Proxy allegati",
"scope_options": "Opzioni visibilità",
"text_limit": "Lunghezza massima",
"title": "Caratteristiche",
"who_to_follow": "Chi seguire",
- "pleroma_chat_messages": "Chiacchiere"
+ "pleroma_chat_messages": "Chiacchiere",
+ "upload_limit": "Limite allegati"
},
"finder": {
"error_fetching_user": "Errore nel recupero dell'utente",
- "find_user": "Trova utente"
+ "find_user": "Cerca utente"
},
"login": {
"login": "Accedi",
@@ -510,18 +519,18 @@
"register": "Registrati",
"username": "Nome utente",
"description": "Accedi con OAuth",
- "hint": "Accedi per partecipare alla discussione",
+ "hint": "Accedi per conversare",
"authentication_code": "Codice di autenticazione",
"enter_recovery_code": "Inserisci un codice di recupero",
- "enter_two_factor_code": "Inserisci un codice two-factor",
+ "enter_two_factor_code": "Inserisci un codice 2FA",
"recovery_code": "Codice di recupero",
"heading": {
- "totp": "Autenticazione two-factor",
- "recovery": "Recupero two-factor"
+ "totp": "Autenticazione 2FA",
+ "recovery": "Recupero 2FA"
}
},
"post_status": {
- "account_not_locked_warning": "Il tuo profilo non è {0}. Chiunque può seguirti e vedere i tuoi messaggi riservati ai tuoi seguaci.",
+ "account_not_locked_warning": "Il tuo profilo non è {0}. Chiunque può seguirti e vedere i tuoi messaggi per seguaci.",
"account_not_locked_warning_link": "protetto",
"attachments_sensitive": "Nascondi gli allegati",
"content_type": {
@@ -531,7 +540,7 @@
"text/html": "HTML"
},
"content_warning": "Oggetto (facoltativo)",
- "default": "Sono appena atterrato a Fiumicino.",
+ "default": "Sono appena atterrato a Città Laggiù.",
"direct_warning": "Questo post sarà visibile solo dagli utenti menzionati.",
"posting": "Sto pubblicando",
"scope": {
@@ -598,21 +607,21 @@
"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",
+ "media_nsfw": "Allegati oscurati d'ufficio",
"media_nsfw_desc": "Questa stanza oscura gli allegati dei messaggi provenienti da queste stanze:"
},
"mrf_policies": "Regole RM abilitate",
"mrf_policies_desc": "Le regole RM cambiano il comportamento federativo della stanza. Vigono le seguenti regole:"
},
- "staff": "Equipaggio"
+ "staff": "Responsabili"
},
"domain_mute_card": {
"mute": "Zittisci",
@@ -641,20 +650,20 @@
},
"polls": {
"add_poll": "Sondaggio",
- "add_option": "Alternativa",
+ "add_option": "Aggiungi opzione",
"option": "Opzione",
"votes": "voti",
"vote": "Vota",
"type": "Tipo di sondaggio",
"single_choice": "Scelta singola",
"multiple_choices": "Scelta multipla",
- "expiry": "Scadenza",
- "expires_in": "Scade fra {0}",
- "expired": "Scaduto {0} fa",
+ "expiry": "Età",
+ "expires_in": "Chiude fra {0}",
+ "expired": "Chiuso {0} fa",
"not_enough_options": "Aggiungi altre risposte"
},
"interactions": {
- "favs_repeats": "Condivisi e preferiti",
+ "favs_repeats": "Condivisi e Graditi",
"load_older": "Carica vecchie interazioni",
"moves": "Utenti migrati",
"follows": "Nuovi seguìti"
@@ -663,8 +672,8 @@
"load_all": "Carico tutti i {emojiAmount} emoji",
"load_all_hint": "Primi {saneAmount} emoji caricati, caricarli tutti potrebbe causare rallentamenti.",
"unicode": "Emoji Unicode",
- "custom": "Emoji personale",
- "add_emoji": "Inserisci Emoji",
+ "custom": "Emoji della stanza",
+ "add_emoji": "Inserisci emoji",
"search_emoji": "Cerca un emoji",
"keep_open": "Tieni aperto il menù",
"emoji": "Emoji",
@@ -679,7 +688,7 @@
"remote_user_resolver": "Cerca utenti remoti"
},
"errors": {
- "storage_unavailable": "Pleroma non ha potuto accedere ai dati del tuo browser. Le tue credenziali o le tue impostazioni locali non potranno essere salvate e potresti incontrare strani errori. Prova ad abilitare i cookie."
+ "storage_unavailable": "Pleroma non può accedere ai dati del tuo browser. Il tuo accesso o le tue impostazioni non saranno salvate e potresti incontrare strani errori. Prova ad abilitare i cookie."
},
"status": {
"pinned": "Intestato",
@@ -693,17 +702,20 @@
"hide_full_subject": "Nascondi intero oggetto",
"show_full_subject": "Mostra intero oggetto",
"thread_muted_and_words": ", contiene:",
- "thread_muted": "Discussione zittita",
+ "thread_muted": "Discussione silenziata",
"copy_link": "Copia collegamento",
"status_unavailable": "Messaggio non disponibile",
"unmute_conversation": "Riabilita conversazione",
- "mute_conversation": "Zittisci conversazione",
+ "mute_conversation": "Silenzia conversazione",
"replies_list": "Risposte:",
"reply_to": "Rispondi a",
"delete_confirm": "Vuoi veramente eliminare questo messaggio?",
"unbookmark": "Rimuovi segnalibro",
"bookmark": "Aggiungi segnalibro",
- "status_deleted": "Questo messagio è stato cancellato"
+ "status_deleted": "Questo messagio è stato cancellato",
+ "nsfw": "Pruriginoso",
+ "external_source": "Vai al sito",
+ "expand": "Espandi"
},
"time": {
"years_short": "{0}a",
@@ -778,7 +790,8 @@
"error": {
"default": "Riprova in seguito",
"file_too_big": "File troppo pesante [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
- "base": "Caricamento fallito."
+ "base": "Caricamento fallito.",
+ "message": "Caricamento fallito: {0}"
}
},
"tool_tip": {
diff --git a/src/i18n/messages.js b/src/i18n/messages.js
index c3195f10..2a1161be 100644
--- a/src/i18n/messages.js
+++ b/src/i18n/messages.js
@@ -33,7 +33,9 @@ const loaders = {
ro: () => import('./ro.json'),
ru: () => import('./ru.json'),
te: () => import('./te.json'),
- zh: () => import('./zh.json')
+ uk: () => import('./uk.json'),
+ zh: () => import('./zh.json'),
+ zh_Hant: () => import('./zh_Hant.json')
}
const messages = {
diff --git a/src/i18n/pl.json b/src/i18n/pl.json
index dfa0729d..7cf06796 100644
--- a/src/i18n/pl.json
+++ b/src/i18n/pl.json
@@ -50,7 +50,8 @@
"text_limit": "Limit tekstu",
"title": "Funkcje",
"who_to_follow": "Propozycje obserwacji",
- "pleroma_chat_messages": "Czat Pleromy"
+ "pleroma_chat_messages": "Czat Pleromy",
+ "upload_limit": "Limit wysyłania"
},
"finder": {
"error_fetching_user": "Błąd przy pobieraniu profilu",
@@ -140,7 +141,8 @@
"no_more_notifications": "Nie masz więcej powiadomień",
"migrated_to": "wyemigrował do",
"reacted_with": "zareagował z {0}",
- "follow_request": "chce ciebie obserwować"
+ "follow_request": "chce ciebie obserwować",
+ "error": "Błąd pobierania powiadomień: {0}"
},
"polls": {
"add_poll": "Dodaj ankietę",
@@ -501,7 +503,8 @@
"outgoing": "Wiadomości wychodzące",
"incoming": "Wiadomości przychodzące",
"border": "Granica"
- }
+ },
+ "wallpaper": "Tło"
},
"radii": {
"_tab_label": "Zaokrąglenie"
@@ -596,7 +599,8 @@
"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ń"
+ "mute_export": "Eksport wyciszeń",
+ "hide_wallpaper": "Ukryj tło instancji"
},
"time": {
"day": "{0} dzień",
@@ -643,7 +647,8 @@
"up_to_date": "Na bieżąco",
"no_more_statuses": "Brak kolejnych statusów",
"no_statuses": "Brak statusów",
- "reload": "Odśwież"
+ "reload": "Odśwież",
+ "error": "Błąd pobierania osi czasu: {0}"
},
"status": {
"favorites": "Ulubione",
@@ -666,7 +671,11 @@
"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"
+ "thread_muted": "Wątek wyciszony",
+ "status_deleted": "Ten wpis został usunięty",
+ "expand": "Rozwiń",
+ "nsfw": "NSFW",
+ "external_source": "Zewnętrzne źródło"
},
"user_card": {
"approve": "Przyjmij",
@@ -757,7 +766,8 @@
"error": {
"base": "Wysyłanie nie powiodło się.",
"file_too_big": "Zbyt duży plik [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
- "default": "Spróbuj ponownie później"
+ "default": "Spróbuj ponownie później",
+ "message": "Błąd wysyłania: {0}"
},
"file_size_units": {
"B": "B",
diff --git a/src/i18n/pt.json b/src/i18n/pt.json
index 1b8694d9..b79985e8 100644
--- a/src/i18n/pt.json
+++ b/src/i18n/pt.json
@@ -5,37 +5,61 @@
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
- "media_proxy": "Proxy de mídia",
+ "media_proxy": "Proxy de multimédia",
"scope_options": "Opções de privacidade",
"text_limit": "Limite de caracteres",
- "title": "Funções",
- "who_to_follow": "Quem seguir"
+ "title": "Características",
+ "who_to_follow": "Quem seguir",
+ "upload_limit": "Limite de carregamento",
+ "pleroma_chat_messages": "Chat do Pleroma"
},
"finder": {
- "error_fetching_user": "Erro ao procurar usuário",
- "find_user": "Buscar usuário"
+ "error_fetching_user": "Erro ao pesquisar utilizador",
+ "find_user": "Pesquisar utilizador"
},
"general": {
"apply": "Aplicar",
"submit": "Enviar",
"more": "Mais",
- "generic_error": "Houve um erro",
- "optional": "opcional"
+ "generic_error": "Ocorreu um erro",
+ "optional": "opcional",
+ "peek": "Espreitar",
+ "close": "Fechar",
+ "verify": "Verificar",
+ "confirm": "Confirmar",
+ "enable": "Ativar",
+ "disable": "Desativar",
+ "cancel": "Cancelar",
+ "show_less": "Mostrar menos",
+ "show_more": "Mostrar mais",
+ "retry": "Tenta novamente",
+ "error_retry": "Por favor, tenta novamente",
+ "loading": "A carregar…",
+ "dismiss": "Ignorar"
},
"image_cropper": {
"crop_picture": "Cortar imagem",
- "save": "Salvar",
- "cancel": "Cancelar"
+ "save": "Guardar",
+ "cancel": "Cancelar",
+ "save_without_cropping": "Guardar sem recortar"
},
"login": {
- "login": "Entrar",
- "description": "Entrar com OAuth",
- "logout": "Sair",
- "password": "Senha",
- "placeholder": "p.e. lain",
- "register": "Registrar",
- "username": "Usuário",
- "hint": "Entre para participar da discussão"
+ "login": "Iniciar Sessão",
+ "description": "Iniciar sessão com OAuth",
+ "logout": "Terminar sessão",
+ "password": "Palavra-passe",
+ "placeholder": "ex. lain",
+ "register": "Registar",
+ "username": "Nome de Utilizador",
+ "hint": "Entra para participar na discussão",
+ "heading": {
+ "totp": "Autenticação de dois fatores",
+ "recovery": "Recuperação de dois fatores"
+ },
+ "recovery_code": "Código de recuperação",
+ "authentication_code": "Código de autenticação",
+ "enter_two_factor_code": "Introduza o código de dois fatores",
+ "enter_recovery_code": "Introduza um código de recuperação"
},
"media_modal": {
"previous": "Anterior",
@@ -45,100 +69,125 @@
"about": "Sobre",
"back": "Voltar",
"chat": "Chat local",
- "friend_requests": "Solicitações de seguidores",
+ "friend_requests": "Pedidos de seguidores",
"mentions": "Menções",
- "dms": "Mensagens diretas",
- "public_tl": "Linha do tempo pública",
- "timeline": "Linha do tempo",
- "twkn": "Toda a rede conhecida",
- "user_search": "Buscar usuários",
+ "dms": "Mensagens Diretas",
+ "public_tl": "Cronologia Pública",
+ "timeline": "Cronologia",
+ "twkn": "Rede conhecida",
+ "user_search": "Pesquisa por Utilizadores",
"who_to_follow": "Quem seguir",
- "preferences": "Preferências"
+ "preferences": "Preferências",
+ "search": "Pesquisar",
+ "interactions": "Interações",
+ "administration": "Administração",
+ "chats": "Salas de Chat",
+ "timelines": "Cronologias",
+ "bookmarks": "Itens Guardados"
},
"notifications": {
- "broken_favorite": "Status desconhecido, buscando...",
- "favorited_you": "favoritou sua postagem",
- "followed_you": "seguiu você",
+ "broken_favorite": "Publicação desconhecida, a procurar…",
+ "favorited_you": "gostou do teu post",
+ "followed_you": "seguiu-te",
"load_older": "Carregar notificações antigas",
"notifications": "Notificações",
"read": "Lido!",
- "repeated_you": "repetiu sua postagem",
- "no_more_notifications": "Mais nenhuma notificação"
+ "repeated_you": "partilhou o teu post",
+ "no_more_notifications": "Sem mais notificações",
+ "reacted_with": "reagiu com {0}",
+ "migrated_to": "migrou para",
+ "follow_request": "quer seguir-te",
+ "error": "Erro ao obter notificações: {0}"
},
"post_status": {
- "new_status": "Postar novo status",
- "account_not_locked_warning": "Sua conta não é {0}. Qualquer pessoa pode te seguir e ver seus posts privados (só para seguidores).",
- "account_not_locked_warning_link": "restrita",
+ "new_status": "Publicar nova publicação",
+ "account_not_locked_warning": "A sua conta não é {0}. Qualquer pessoa pode seguir-te e ver os seus posts privados (só para seguidores).",
+ "account_not_locked_warning_link": "restrito",
"attachments_sensitive": "Marcar anexos como sensíveis",
"content_type": {
- "text/plain": "Texto puro"
+ "text/plain": "Texto puro",
+ "text/bbcode": "BBCode",
+ "text/html": "HTML",
+ "text/markdown": "Remarcação"
},
"content_warning": "Assunto (opcional)",
- "default": "Acabei de chegar no Rio!",
+ "default": "Acabei de chegar a Lisboa.",
"direct_warning": "Este post será visível apenas para os usuários mencionados.",
- "posting": "Publicando",
+ "posting": "A publicar",
"scope": {
"direct": "Direto - Enviar somente aos usuários mencionados",
"private": "Apenas para seguidores - Enviar apenas para seguidores",
- "public": "Público - Enviar a linhas do tempo públicas",
- "unlisted": "Não listado - Não enviar a linhas do tempo públicas"
- }
+ "public": "Público - Publicar em cronologias públicas",
+ "unlisted": "Não listado - Não exibir em cronologias públicas"
+ },
+ "scope_notice": {
+ "unlisted": "Esta publicação não será visível na Cronologia pública e na Rede conhecida por todos",
+ "private": "Esta publicação será apenas visível para os teus seguidores",
+ "public": "Esta publicação será visível para todos"
+ },
+ "empty_status_error": "Não consegues publicar um post vazio e sem ficheiros",
+ "preview_empty": "Vazio",
+ "preview": "Pré-visualização",
+ "media_description": "Descrição da multimédia",
+ "media_description_error": "Falha ao atualizar ficheiro, tente novamente",
+ "direct_warning_to_first_only": "Esta publicação só será visível para os utilizadores mencionados no início da mensagem.",
+ "direct_warning_to_all": "Esta publicação será visível para todos os utilizadores mencionados."
},
"registration": {
"bio": "Biografia",
- "email": "Correio eletrônico",
+ "email": "Endereço de e-mail",
"fullname": "Nome para exibição",
- "password_confirm": "Confirmação de senha",
- "registration": "Registro",
+ "password_confirm": "Confirmação de palavra-passe",
+ "registration": "Registo",
"token": "Código do convite",
"captcha": "CAPTCHA",
"new_captcha": "Clique na imagem para carregar um novo captcha",
- "username_placeholder": "p. ex. lain",
- "fullname_placeholder": "p. ex. Lain Iwakura",
- "bio_placeholder": "e.g.\nOi, sou Lain\nSou uma garota que vive no subúrbio do Japão. Você deve me conhecer da Rede.",
+ "username_placeholder": "ex. lain",
+ "fullname_placeholder": "ex. Lain Iwakura",
+ "bio_placeholder": "ex.\nOlá, sou a Lain\nSou uma menina de anime que vive no Japão suburbano. Devem conhecer-me do \"the Wired\".",
"validations": {
"username_required": "não pode ser deixado em branco",
"fullname_required": "não pode ser deixado em branco",
"email_required": "não pode ser deixado em branco",
"password_required": "não pode ser deixado em branco",
"password_confirmation_required": "não pode ser deixado em branco",
- "password_confirmation_match": "deve ser idêntica à senha"
+ "password_confirmation_match": "deve corresponder à palavra-passe"
}
},
"settings": {
- "app_name": "Nome do aplicativo",
+ "app_name": "Nome da aplicação",
"attachmentRadius": "Anexos",
"attachments": "Anexos",
"avatar": "Avatar",
"avatarAltRadius": "Avatares (Notificações)",
"avatarRadius": "Avatares",
- "background": "Pano de Fundo",
+ "background": "Imagem de Fundo",
"bio": "Biografia",
"blocks_tab": "Bloqueios",
"btnRadius": "Botões",
"cBlue": "Azul (Responder, seguir)",
- "cGreen": "Verde (Repetir)",
+ "cGreen": "Verde (Partilhar)",
"cOrange": "Laranja (Favoritar)",
"cRed": "Vermelho (Cancelar)",
- "change_password": "Mudar senha",
- "change_password_error": "Houve um erro ao modificar sua senha.",
- "changed_password": "Senha modificada com sucesso!",
+ "change_password": "Mudar palavra-passe",
+ "change_password_error": "Ocorreu um erro ao modificar a sua palavra-passe.",
+ "changed_password": "Palavra-passe modificada com sucesso!",
"collapse_subject": "Esconder posts com assunto",
"composing": "Escrita",
- "confirm_new_password": "Confirmar nova senha",
+ "confirm_new_password": "Confirmar nova palavra-passe",
"current_avatar": "Seu avatar atual",
- "current_password": "Sua senha atual",
+ "current_password": "Palavra-passe atual",
"current_profile_banner": "Sua capa de perfil atual",
"data_import_export_tab": "Importação/exportação de dados",
"default_vis": "Opção de privacidade padrão",
- "delete_account": "Deletar conta",
- "delete_account_description": "Deletar sua conta e mensagens permanentemente.",
- "delete_account_error": "Houve um problema ao deletar sua conta. Se ele persistir, por favor entre em contato com o/a administrador/a da instância.",
- "delete_account_instructions": "Digite sua senha no campo abaixo para confirmar a exclusão da conta.",
+ "delete_account": "Eliminar conta",
+ "delete_account_description": "Apagar os seus dados permanentemente e desativar a sua conta.",
+ "delete_account_error": "Ocorreu um erro ao remover a sua conta. Se este persistir, por favor entre em contato com o/a administrador/a da instância.",
+ "delete_account_instructions": "Escreva a sua palavra-passe no campo abaixo para confirmar a remoção da conta.",
"avatar_size_instruction": "O tamanho mínimo recomendado para imagens de avatar é 150x150 pixels.",
- "export_theme": "Salvar predefinições",
+ "export_theme": "Guardar predefinições",
"filtering": "Filtragem",
- "filtering_explanation": "Todas as postagens contendo estas palavras serão silenciadas; uma palavra por linha.",
+ "filtering_explanation": "Todas as publicações que contenham estas palavras serão silenciadas; uma palavra por linha",
"follow_export": "Exportar quem você segue",
"follow_export_button": "Exportar quem você segue para um arquivo CSV",
"follow_export_processing": "Processando. Em breve você receberá a solicitação de download do arquivo",
@@ -148,7 +197,7 @@
"foreground": "Primeiro Plano",
"general": "Geral",
"hide_attachments_in_convo": "Ocultar anexos em conversas",
- "hide_attachments_in_tl": "Ocultar anexos na linha do tempo.",
+ "hide_attachments_in_tl": "Ocultar anexos na cronologia",
"max_thumbnails": "Número máximo de miniaturas por post",
"hide_isp": "Esconder painel específico da instância",
"preload_images": "Pré-carregar imagens",
@@ -159,7 +208,7 @@
"import_followers_from_a_csv_file": "Importe seguidores a partir de um arquivo CSV",
"import_theme": "Carregar pré-definição",
"inputRadius": "Campos de entrada",
- "checkboxRadius": "Checkboxes",
+ "checkboxRadius": "Caixas de seleção",
"instance_default": "(padrão: {value})",
"instance_default_simple": "(padrão)",
"interface": "Interface",
@@ -171,16 +220,16 @@
"loop_video": "Repetir vídeos",
"loop_video_silent_only": "Repetir apenas vídeos sem som (como os \"gifs\" do Mastodon)",
"mutes_tab": "Silenciados",
- "play_videos_in_modal": "Tocar vídeos diretamente no visualizador de mídia",
+ "play_videos_in_modal": "Reproduzir vídeos diretamente no visualizador de multimédia",
"use_contain_fit": "Não cortar o anexo na miniatura",
"name": "Nome",
"name_bio": "Nome & Biografia",
- "new_password": "Nova senha",
+ "new_password": "Nova palavra-passe",
"notification_visibility": "Tipos de notificação para mostrar",
"notification_visibility_follows": "Seguidas",
"notification_visibility_likes": "Favoritos",
"notification_visibility_mentions": "Menções",
- "notification_visibility_repeats": "Repetições",
+ "notification_visibility_repeats": "Partilhas",
"no_rich_text_description": "Remover formatação de todos os posts",
"no_blocks": "Sem bloqueios",
"no_mutes": "Sem silenciados",
@@ -188,7 +237,7 @@
"hide_followers_description": "Não mostrar quem me segue",
"show_admin_badge": "Mostrar título de Administrador em meu perfil",
"show_moderator_badge": "Mostrar título de Moderador em meu perfil",
- "nsfw_clickthrough": "Habilitar clique para ocultar anexos sensíveis",
+ "nsfw_clickthrough": "Ativar clique em anexos e pré-visualizações de links para ocultar anexos NSFW",
"oauth_tokens": "Token OAuth",
"token": "Token",
"refresh_token": "Atualizar Token",
@@ -201,7 +250,7 @@
"profile_banner": "Capa de perfil",
"profile_tab": "Perfil",
"radii_help": "Arredondar arestas da interface (em pixel)",
- "replies_in_timeline": "Respostas na linha do tempo",
+ "replies_in_timeline": "Respostas na cronologia",
"reply_visibility_all": "Mostrar todas as respostas",
"reply_visibility_following": "Só mostrar respostas direcionadas a mim ou a usuários que sigo",
"reply_visibility_self": "Só mostrar respostas direcionadas a mim",
@@ -215,7 +264,7 @@
"settings": "Configurações",
"subject_input_always_show": "Sempre mostrar campo de assunto",
"subject_line_behavior": "Copiar assunto ao responder",
- "subject_line_email": "Como em email: \"re: assunto\"",
+ "subject_line_email": "Como num e-mail: \"re: assunto\"",
"subject_line_mastodon": "Como o Mastodon: copiar como está",
"subject_line_noop": "Não copiar",
"post_status_content_type": "Tipo de conteúdo do status",
@@ -225,7 +274,7 @@
"theme": "Tema",
"theme_help": "Use cores em código hexadecimal (#rrggbb) para personalizar seu esquema de cores.",
"theme_help_v2_1": "Você também pode sobrescrever as cores e opacidade de alguns componentes ao modificar o checkbox, use \"Limpar todos\" para limpar todas as modificações.",
- "theme_help_v2_2": "Alguns ícones sob registros são indicadores de fundo/contraste de textos, passe por cima para informações detalhadas. Tenha ciência de que os indicadores de contraste não funcionam muito bem com transparência.",
+ "theme_help_v2_2": "Alguns ícones em registo são indicadores de fundo/contraste de textos, passe por cima para obter informações detalhadas. Tenha em atenção que os indicadores de contraste não funcionam muito bem com transparência.",
"tooltipRadius": "Dicas/alertas",
"upload_a_photo": "Enviar uma foto",
"user_settings": "Configurações de Usuário",
@@ -245,7 +294,24 @@
"save_load_hint": "Manter as opções preserva as opções atuais ao selecionar ou carregar temas; também salva as opções ao exportar um tempo. Quanto todos os campos estiverem desmarcados, tudo será salvo ao exportar o tema.",
"reset": "Restaurar o padrão",
"clear_all": "Limpar tudo",
- "clear_opacity": "Limpar opacidade"
+ "clear_opacity": "Limpar opacidade",
+ "help": {
+ "upgraded_from_v2": "O PleromaFE foi atualizado, a aparência do tema poderá ser um pouco diferente.",
+ "snapshot_source_mismatch": "Conflito de versões: o mais provável é que o FE tenha revertido e voltado a atualizar, foi alterado o tema numa versão anterior do FE, o mais provável é desejar utilizar a versão anterior; caso contrário, utilize a nova versão.",
+ "migration_napshot_gone": "Por algum motivo, a pré-visualização estava em falta, algumas coisas poderão parecer diferentes do que se lembra.",
+ "migration_snapshot_ok": "Para estar seguro, foi carregada uma versão de pré-visualização do tema. Pode tentar carregar dados do tema.",
+ "fe_downgraded": "Versão do PleromaFE revertida.",
+ "fe_upgraded": "O criador de temas do PleromaFE foi atualizado depois da atualização da versão.",
+ "snapshot_missing": "Não existia nenhuma pré-visualização do tema no ficheiro, então pode parecer diferente do previsto originalmente.",
+ "snapshot_present": "Foi carregada uma pré-visualização do tema, todos os valores são substituídos. Caso contrário, pode carregar o tema completo.",
+ "older_version_imported": "O ficheiro que importaste foi criado numa versão antiga do FE.",
+ "future_version_imported": "O ficheiro que importaste foi criado para uma versão mais recente do FE.",
+ "v2_imported": "O ficheiro que importaste foi feito para uma versão antiga do FE. Tentamos maximizar a compatibilidade, porém, poderão existir incongruências."
+ },
+ "use_source": "Nova versão",
+ "use_snapshot": "Versão antiga",
+ "keep_as_is": "Manter como está",
+ "load_theme": "Carregar tema"
},
"common": {
"color": "Cor",
@@ -280,7 +346,27 @@
"borders": "Bordas",
"buttons": "Botões",
"inputs": "Caixas de entrada",
- "faint_text": "Texto esmaecido"
+ "faint_text": "Texto esmaecido",
+ "chat": {
+ "border": "Borda",
+ "outgoing": "Enviadas",
+ "incoming": "Recebidas"
+ },
+ "tabs": "Abas",
+ "toggled": "Alternado",
+ "disabled": "Desativado",
+ "selectedMenu": "Elemento do menu seleccionado",
+ "selectedPost": "Publicação seleccionada",
+ "pressed": "Pressionado",
+ "highlight": "Elementos destacados",
+ "icons": "Ícones",
+ "poll": "Gráfico da sondagem",
+ "wallpaper": "Fundo de ecrã",
+ "underlay": "Sublinhado",
+ "popover": "Sugestões, menus, etiquetas",
+ "post": "Publicações/Bios",
+ "alert_neutral": "Neutro",
+ "alert_warning": "Precaução"
},
"radii": {
"_tab_label": "Arredondado"
@@ -298,7 +384,7 @@
"always_drop_shadow": "Atenção, esta sombra sempre utiliza {0} quando compatível com o navegador.",
"drop_shadow_syntax": "{0} não é compatível com o parâmetro {1} e a palavra-chave {2}.",
"avatar_inset": "Tenha em mente que combinar as sombras de inserção e a não-inserção em avatares pode causar resultados inesperados em avatares transparentes.",
- "spread_zero": "Sombras com uma difusão > 0 aparecerão como se fossem definidas como 0.",
+ "spread_zero": "Sombras com difusão > 0 aparecerão como se fossem definidas como zero",
"inset_classic": "Sombras de inserção utilizarão {0}"
},
"components": {
@@ -313,7 +399,8 @@
"buttonPressed": "Botão (pressionado)",
"buttonPressedHover": "Botão (pressionado+em cima)",
"input": "Campo de entrada"
- }
+ },
+ "hintV3": "Para as sombras, também pode usar a notação {0} para usar outro espaço de cor."
},
"fonts": {
"_tab_label": "Fontes",
@@ -336,30 +423,143 @@
"button": "Botão",
"text": "Vários {0} e {1}",
"mono": "conteúdo",
- "input": "Acabei de chegar no Rio!",
+ "input": "Acabei de chegar a Lisboa.",
"faint_link": "manual útil",
"fine_print": "Leia nosso {0} para não aprender nada!",
- "header_faint": "Está ok!",
+ "header_faint": "Isto está bem",
"checkbox": "Li os termos e condições",
"link": "um belo link"
}
- }
+ },
+ "mfa": {
+ "scan": {
+ "secret_code": "Chave",
+ "title": "Scan",
+ "desc": "Utilizando a sua aplicação de dois fatores, faça scan deste código QR ou insira a chave de texto:"
+ },
+ "authentication_methods": "Métodos de autenticação",
+ "recovery_codes": "Códigos de recuperação.",
+ "generate_new_recovery_codes": "Gerar novos códigos de recuperação",
+ "confirm_and_enable": "Confirmar e ativar a palavra-passe de utilização única",
+ "otp": "Palavra-passe de utilização única",
+ "verify": {
+ "desc": "Para ativar a autenticação de dois fatores, introduza o código da sua aplicação de dois fatores:"
+ },
+ "recovery_codes_warning": "Anote os códigos ou armazene-os num lugar seguro - caso contrário, não os voltará a ver. Se perder acesso à sua aplicação de dois fatores e aos códigos de recuperação, a sua conta ficará bloqueada.",
+ "waiting_a_recovery_codes": "A receber códigos de recuperação…",
+ "warning_of_generate_new_codes": "Quando gera novos códigos de recuperação, os antigos deixam de funcionar.",
+ "title": "Autenticação de Dois Fatores",
+ "wait_pre_setup_otp": "pré-configuração de palavra-passe de utilização única",
+ "setup_otp": "Configurar palavra-passe de utilização única"
+ },
+ "security": "Segurança",
+ "mute_import_error": "Erro ao importar os silenciados",
+ "mute_import": "Importar silenciados",
+ "mute_export_button": "Exporta os silenciados para um ficheiro csv",
+ "mute_export": "Exportar silenciados",
+ "blocks_imported": "Lista de utilizadores bloqueados importada! O processo pode demorar alguns instantes.",
+ "block_import_error": "Erro ao importar a lista de utilizadores bloqueados",
+ "block_import": "Importar utilizadores bloqueados",
+ "block_export_button": "Exporta a tua lista de utilizadores bloqueados para um ficheiro csv",
+ "block_export": "Exportar utilizadores bloqueados",
+ "enter_current_password_to_confirm": "Introduza a sua palavra-passe atual para confirmar a sua identidade",
+ "mutes_and_blocks": "Silenciados e Bloqueados",
+ "chatMessageRadius": "Mensagem de texto",
+ "changed_email": "Endereço de e-mail modificado com sucesso!",
+ "change_email_error": "Ocorreu um erro ao modificar o seu endereço de e-mail.",
+ "change_email": "Mudar Endereço de E-mail",
+ "bot": "Esta uma conta robô",
+ "import_mutes_from_a_csv_file": "Importar silenciados de um ficheiro csv",
+ "mutes_imported": "Silenciados importados! Processá-los pode demorar alguns instantes.",
+ "allow_following_move": "Permitir seguimento automático quando a conta for migrada para outra instância",
+ "domain_mutes": "Domínios",
+ "discoverable": "Permitir a descoberta desta conta em resultados de busca e outros serviços",
+ "emoji_reactions_on_timeline": "Mostrar reações de emoji na timeline",
+ "hide_muted_posts": "Esconder posts de utilizadores silenciados",
+ "hide_follows_count_description": "Não mostrar o número de contas seguidas",
+ "hide_followers_count_description": "Não mostrar o número de seguidores",
+ "notification_visibility_emoji_reactions": "Reações",
+ "new_email": "Novo endereço de e-mail",
+ "profile_fields": {
+ "value": "Conteúdo",
+ "add_field": "Adicionar campo",
+ "label": "Metadados do perfil",
+ "name": "Etiqueta"
+ },
+ "import_blocks_from_a_csv_file": "Importar bloqueados a partir de um arquivo CSV",
+ "hide_wallpaper": "Esconder papel de parede da instância",
+ "notification_setting_privacy": "Privacidade",
+ "notification_setting_filters": "Filtros",
+ "fun": "Divertido",
+ "user_mutes": "Utilizadores",
+ "type_domains_to_mute": "Pesquisar domínios para silenciar",
+ "useStreamingApiWarning": "(não recomendado, experimental, pode omitir publicações)",
+ "useStreamingApi": "Receber publicações e notificações em tempo real",
+ "minimal_scopes_mode": "Minimizar as opções de publicação",
+ "search_user_to_mute": "Pesquisar utilizadores que pretende silenciar",
+ "search_user_to_block": "Pesquisa quais utilizadores desejas bloquear",
+ "notification_setting_hide_notification_contents": "Ocultar o remetente e o conteúdo das notificações push",
+ "version": {
+ "frontend_version": "Versão do Frontend",
+ "backend_version": "Versão do Backend",
+ "title": "Versão"
+ },
+ "notification_blocks": "Bloquear um utilizador previne todas as notificações, bem como as desativa.",
+ "notification_mutes": "Para deixar de receber notificações de um utilizador específico, silencia-o.",
+ "notification_setting_block_from_strangers": "Bloqueia as notificações de utilizadores que não segues",
+ "greentext": "Texto verde (meme arrows)",
+ "virtual_scrolling": "Otimizar a apresentação da cronologia",
+ "reset_background_confirm": "Tens a certeza que desejas redefinir o fundo?",
+ "reset_banner_confirm": "Tens a certeza que desejas redefinir a imagem do cabeçalho?",
+ "reset_avatar_confirm": "Tens a certeza que desejas redefinir o avatar?",
+ "reset_profile_banner": "Redefinir imagem do cabeçalho do perfil",
+ "reset_profile_background": "Redefinir fundo de perfil",
+ "reset_avatar": "Redefinir avatar",
+ "autohide_floating_post_button": "Automaticamente ocultar o botão 'Nova Publicação' (telemóvel)",
+ "notification_visibility_moves": "Utilizador Migrado",
+ "accent": "Destaque",
+ "pad_emoji": "Preencher espaços ao adicionar emojis do seletor"
},
"timeline": {
"collapse": "Esconder",
"conversation": "Conversa",
"error_fetching": "Erro ao buscar atualizações",
"load_older": "Carregar postagens antigas",
- "no_retweet_hint": "Posts apenas para seguidores ou diretos não podem ser repetidos",
- "repeated": "Repetido",
+ "no_retweet_hint": "Posts apenas para seguidores ou diretos não podem ser partilhados",
+ "repeated": "partilhado",
"show_new": "Mostrar novas",
"up_to_date": "Atualizado",
"no_more_statuses": "Sem mais posts",
- "no_statuses": "Sem posts"
+ "no_statuses": "Sem posts",
+ "reload": "Recarregar",
+ "error": "Erro a obter a cronologia: {0}"
},
"status": {
"reply_to": "Responder a",
- "replies_list": "Respostas:"
+ "replies_list": "Respostas:",
+ "unbookmark": "Remover post dos Items Guardados",
+ "expand": "Expandir",
+ "nsfw": "NSFW (Não apropriado para trabalho)",
+ "status_deleted": "Esta publicação foi apagada",
+ "hide_content": "Ocultar o conteúdo",
+ "show_content": "Mostrar o conteúdo",
+ "hide_full_subject": "Ocultar o assunto completo",
+ "show_full_subject": "Mostrar o assunto completo",
+ "thread_muted_and_words": ", contém:",
+ "thread_muted": "Conversação silenciada",
+ "external_source": "Fonte externa",
+ "copy_link": "Copiar o link do post",
+ "status_unavailable": "Publicação indisponível",
+ "unmute_conversation": "Mostrar a conversação",
+ "mute_conversation": "Silenciar a conversação",
+ "delete_confirm": "Tens a certeza que desejas apagar a publicação?",
+ "bookmark": "Guardar",
+ "pin": "Fixar no perfil",
+ "pinned": "Afixado",
+ "unpin": "Desafixar do perfil",
+ "delete": "Eliminar publicação",
+ "repeats": "Partilhados",
+ "favorites": "Favoritos"
},
"user_card": {
"approve": "Aprovar",
@@ -377,21 +577,52 @@
"following": "Seguindo!",
"follows_you": "Segue você!",
"its_you": "É você!",
- "media": "Mídia",
+ "media": "Multimédia",
"mute": "Silenciar",
"muted": "Silenciado",
"per_day": "por dia",
"remote_follow": "Seguir remotamente",
"statuses": "Postagens",
"unblock": "Desbloquear",
- "unblock_progress": "Desbloqueando...",
- "block_progress": "Bloqueando...",
+ "unblock_progress": "A desbloquear…",
+ "block_progress": "A bloquear…",
"unmute": "Retirar silêncio",
- "unmute_progress": "Retirando silêncio...",
- "mute_progress": "Silenciando..."
+ "unmute_progress": "A retirar silêncio…",
+ "mute_progress": "A silenciar…",
+ "admin_menu": {
+ "delete_user_confirmation": "Tens a certeza? Esta ação não pode ser revertida.",
+ "delete_user": "Eliminar utilizador",
+ "quarantine": "Não permitir publicações de utilizadores de instâncias remotas",
+ "disable_any_subscription": "Não permitir que nenhum utilizador te siga",
+ "disable_remote_subscription": "Não permitir seguidores de instâncias remotas",
+ "sandbox": "Forçar publicações apenas para seguidores",
+ "force_unlisted": "Forçar publicações como não listadas",
+ "strip_media": "Eliminar ficheiros multimédia das publicações",
+ "force_nsfw": "Marcar todas as publicações como NSFW (não apropriado para o trabalho)",
+ "delete_account": "Eliminar Conta",
+ "deactivate_account": "Desativar conta",
+ "activate_account": "Ativar conta",
+ "revoke_moderator": "Revogar permissões de Moderador",
+ "grant_moderator": "Conceder permissões de Moderador",
+ "revoke_admin": "Revogar permissões de Admin",
+ "grant_admin": "Conceder permissões de Admin",
+ "moderation": "Moderação"
+ },
+ "show_repeats": "Mostrar partilhas",
+ "hide_repeats": "Ocultar partilhas",
+ "unsubscribe": "Retirar subscrição",
+ "subscribe": "Subscrever",
+ "report": "Denunciar",
+ "message": "Mensagem",
+ "mention": "Mencionar",
+ "hidden": "Ocultar",
+ "roles": {
+ "moderator": "Moderador",
+ "admin": "Admin"
+ }
},
"user_profile": {
- "timeline_title": "Linha do tempo do usuário",
+ "timeline_title": "Cronologia do Utilizador",
"profile_does_not_exist": "Desculpe, este perfil não existe.",
"profile_loading_error": "Desculpe, houve um erro ao carregar este perfil."
},
@@ -400,17 +631,22 @@
"who_to_follow": "Quem seguir"
},
"tool_tip": {
- "media_upload": "Envio de mídia",
- "repeat": "Repetir",
+ "media_upload": "Envio de multimédia",
+ "repeat": "Partilhar",
"reply": "Responder",
"favorite": "Favoritar",
- "user_settings": "Configurações do usuário"
+ "user_settings": "Configurações do usuário",
+ "bookmark": "Guardar",
+ "reject_follow_request": "Rejeitar o pedido de seguimento",
+ "accept_follow_request": "Aceitar o pedido de seguimento",
+ "add_reaction": "Adicionar Reação"
},
- "upload":{
+ "upload": {
"error": {
"base": "Falha no envio.",
"file_too_big": "Arquivo grande demais [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
- "default": "Tente novamente mais tarde"
+ "default": "Tente novamente mais tarde",
+ "message": "Falha ao enviar: {0}"
},
"file_size_units": {
"B": "B",
@@ -419,5 +655,179 @@
"GiB": "GiB",
"TiB": "TiB"
}
+ },
+ "about": {
+ "mrf": {
+ "simple": {
+ "quarantine": "Quarentena",
+ "reject": "Rejeitar",
+ "accept": "Aceitar",
+ "media_removal_desc": "Este domínio remove multimédia das publicações dos seguintes domínios:",
+ "media_removal": "Remoção de multimédia",
+ "ftl_removal_desc": "Este domínio remove os seguintes domínios da cronologia \"Rede conhecida por todos\":",
+ "quarantine_desc": "Este domínio apenas irá publicar nos seguintes domínios:",
+ "reject_desc": "Este domínio não aceitará mensagens dos seguintes domínios:",
+ "accept_desc": "Este domínio aceita apenas mensagens dos seguintes domínios:",
+ "simple_policies": "Políticas especificas do domínio",
+ "media_nsfw": "Forçar definição de multimédia como Sensível",
+ "ftl_removal": "Remoção da cronologia da \"Rede conhecida por todos\"",
+ "media_nsfw_desc": "Este domínio força a multimédia a ser marcada como sensível nos seguintes domínios:"
+ },
+ "keyword": {
+ "replace": "Substituir",
+ "reject": "Rejeitar",
+ "is_replaced_by": "→",
+ "keyword_policies": "Política de Palavras-Chave",
+ "ftl_removal": "Remoção da cronologia da \"Rede conhecida por todos\""
+ },
+ "federation": "Federação",
+ "mrf_policies": "Ativar Políticas MRF",
+ "mrf_policies_desc": "Políticas MRF manipulam o comportamento da federação nos domínios. As seguintes políticas estão ativadas:"
+ },
+ "staff": "Staff"
+ },
+ "remote_user_resolver": {
+ "searching_for": "A pesquisar por",
+ "error": "Não encontrado.",
+ "remote_user_resolver": "Resolução de utilizador remoto"
+ },
+ "emoji": {
+ "unicode": "Emoji Unicode",
+ "custom": "Emoji customizado",
+ "add_emoji": "Inserir emoji",
+ "search_emoji": "Pesquisar por um emoji",
+ "emoji": "Emoji",
+ "load_all": "A carregar todos os {emojiAmount} emojis",
+ "load_all_hint": "Carregado o primeiro emoji {saneAmount}, carregar todos os emojis pode causar problemas de desempenho.",
+ "keep_open": "Manter o seletor aberto",
+ "stickers": "Autocolantes"
+ },
+ "polls": {
+ "single_choice": "Escolha única",
+ "vote": "Vota",
+ "votes": "votos",
+ "option": "Opção",
+ "add_option": "Adicionar Opção",
+ "not_enough_options": "Demasiado poucas opções únicas na sondagem",
+ "expired": "A sondagem terminou há {0}",
+ "expires_in": "A sondagem termina em {0}",
+ "expiry": "Tempo para finalizar sondagem",
+ "multiple_choices": "Escolha múltipla",
+ "type": "Tipo de sondagem",
+ "add_poll": "Adicionar Sondagem"
+ },
+ "importer": {
+ "error": "Ocorreu um erro ao importar este ficheiro.",
+ "success": "Importado com sucesso.",
+ "submit": "Enviar"
+ },
+ "exporter": {
+ "processing": "A processar, brevemente ser-te-á pedido que descarregues o ficheiro",
+ "export": "Exportar"
+ },
+ "domain_mute_card": {
+ "mute_progress": "A silenciar…",
+ "mute": "Silenciar",
+ "unmute": "Remover silêncio",
+ "unmute_progress": "A remover o silêncio…"
+ },
+ "selectable_list": {
+ "select_all": "Seleccionar tudo"
+ },
+ "interactions": {
+ "load_older": "Carregar interações mais antigas",
+ "follows": "Novos seguidores",
+ "favs_repeats": "Gostos e Partilhas",
+ "moves": "O utilizador migra"
+ },
+ "errors": {
+ "storage_unavailable": "O Pleroma não conseguiu aceder ao armazenamento do navegador. A sua sessão ou definições locais não serão armazenadas e poderá encontrar problemas inesperados. Tente ativar as cookies."
+ },
+ "shoutbox": {
+ "title": "Chat Geral"
+ },
+ "chats": {
+ "chats": "Chats",
+ "empty_chat_list_placeholder": "Não tens conversações ainda. Inicia uma nova conversa!",
+ "error_sending_message": "Ocorreu algo de errado ao enviar a mensagem.",
+ "error_loading_chat": "Ocorreu algo de errado ao carregar o chat.",
+ "delete_confirm": "Desejas realmente apagar esta mensagem?",
+ "more": "Mais",
+ "empty_message_error": "Não podes publicar uma mensagem vazia",
+ "new": "Nova conversação",
+ "delete": "Apagar",
+ "message_user": "Mensagem de {nickname}",
+ "you": "Tu:"
+ },
+ "search": {
+ "hashtags": "Hashtags",
+ "no_results": "Sem resultados",
+ "person_talking": "{count} pessoa a falar",
+ "people_talking": "{0} pessoas a falar",
+ "people": "Pessoas"
+ },
+ "display_date": {
+ "today": "Hoje"
+ },
+ "file_type": {
+ "file": "Ficheiro",
+ "image": "Imagem",
+ "video": "Vídeo",
+ "audio": "Áudio"
+ },
+ "password_reset": {
+ "password_reset_required_but_mailer_is_disabled": "Deves repor a tua palavra-passe, porém, a reposição de palavra-passe está desativada. Contacta o administrador da tua instância.",
+ "password_reset_required": "Deves repor a tua palavra-passe para iniciar sessão.",
+ "password_reset_disabled": "A reposição da palavra-passe foi desativada. Contacta o administrador da tua instância.",
+ "too_many_requests": "Alcançaste o limite de tentativas, tenta novamente mais tarde.",
+ "return_home": "Voltar à página principal",
+ "check_email": "Verifica o teu endereço de e-mail para obter um link para repor a tua palavra-passe.",
+ "placeholder": "O teu endereço de e-mail ou nome de utilizador",
+ "instruction": "Introduz o teu endereço de e-mail ou nome de utilizador. Enviaremos um link para repores a tua palavra-passe.",
+ "password_reset": "Repor palavra-passe",
+ "forgot_password": "Esqueceu-se da palavra-passe?"
+ },
+ "user_reporting": {
+ "generic_error": "Ocorreu um erro ao processar o teu pedido.",
+ "submit": "Enviar",
+ "forward_to": "Encaminhar para {0}",
+ "forward_description": "A conta é de outro servidor. Enviar também uma cópia da denúncia à outra instância?",
+ "additional_comments": "Comentários adicionais",
+ "add_comment_description": "Esta denúncia será enviada aos moderadores desta instância. Podes fornecer uma explicação pela qual te encontras a denunciar esta conta abaixo:",
+ "title": "Denunciar {0}"
+ },
+ "time": {
+ "years_short": "{0}a",
+ "year_short": "{0}a",
+ "years": "{0} anos",
+ "year": "{0} ano",
+ "weeks_short": "{0}sem",
+ "week_short": "{0}sem",
+ "weeks": "{0} semanas",
+ "week": "{0} semana",
+ "seconds_short": "{0}s",
+ "second_short": "{0}s",
+ "seconds": "{0} segundos",
+ "second": "{0} segundo",
+ "now": "agora mesmo",
+ "now_short": "agora",
+ "months_short": "{0}m",
+ "month_short": "{0}m",
+ "months": "{0} meses",
+ "month": "{0} mês",
+ "minutes_short": "{0}min",
+ "minute_short": "{0}min",
+ "minutes": "{0} minutos",
+ "minute": "{0} minuto",
+ "in_past": "há {0}",
+ "in_future": "em {0}",
+ "hours_short": "{0}h",
+ "hour_short": "{0}h",
+ "hours": "{0} horas",
+ "hour": "{0} hora",
+ "days_short": "{0}d",
+ "day_short": "{0}d",
+ "days": "{0} dias",
+ "day": "{0} dia"
}
}
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 8f421b50..30a65741 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -18,7 +18,13 @@
"generic_error": "Произошла ошибка",
"optional": "не обязательно",
"show_less": "Показать меньше",
- "show_more": "Показать больше"
+ "show_more": "Показать больше",
+ "peek": "Свернуть",
+ "dismiss": "Закрыть",
+ "retry": "Попробуйте еще раз",
+ "error_retry": "Пожалуйста попробуйте еще раз",
+ "close": "Закрыть",
+ "loading": "Загрузка…"
},
"login": {
"login": "Войти",
@@ -33,8 +39,12 @@
"recovery_code": "Код восстановления",
"heading": {
"TotpForm": "Двухфакторная аутентификация",
- "RecoveryForm": "Two-factor recovery"
- }
+ "RecoveryForm": "Two-factor recovery",
+ "totp": "Двухфакторная аутентификация",
+ "recovery": "Двухфакторное возвращение аккаунта"
+ },
+ "hint": "Войдите чтобы присоединиться к дискуссии",
+ "description": "Войти с помощью OAuth"
},
"nav": {
"back": "Назад",
@@ -46,22 +56,35 @@
"twkn": "Федеративная лента",
"search": "Поиск",
"friend_requests": "Запросы на чтение",
- "bookmarks": "Закладки"
+ "bookmarks": "Закладки",
+ "chats": "Беседы",
+ "timelines": "Ленты",
+ "preferences": "Настройки",
+ "who_to_follow": "Кого читать",
+ "dms": "Личные Сообщения",
+ "administration": "Панель администратора",
+ "about": "О сервере",
+ "user_search": "Поиск пользователей"
},
"notifications": {
- "broken_favorite": "Неизвестный статус, ищем...",
+ "broken_favorite": "Неизвестный статус, ищем…",
"favorited_you": "нравится ваш статус",
"followed_you": "начал(а) читать вас",
"load_older": "Загрузить старые уведомления",
"notifications": "Уведомления",
"read": "Прочесть",
"repeated_you": "повторил(а) ваш статус",
- "follow_request": "хочет читать вас"
+ "follow_request": "хочет читать вас",
+ "reacted_with": "добавил реакцию: {0}",
+ "migrated_to": "мигрировал на",
+ "no_more_notifications": "Нет дальнейших уведомлений",
+ "error": "Ошибка при обновлении уведомлений: {0}"
},
"interactions": {
"favs_repeats": "Повторы и фавориты",
- "follows": "Новые подписки",
- "load_older": "Загрузить старые взаимодействия"
+ "follows": "Новые читатели",
+ "load_older": "Загрузить старые взаимодействия",
+ "moves": "Миграции пользователей"
},
"post_status": {
"account_not_locked_warning": "Ваш аккаунт не {0}. Кто угодно может начать читать вас чтобы видеть посты только для подписчиков.",
@@ -81,7 +104,21 @@
"private": "Для подписчиков - этот пост видят только подписчики",
"public": "Публичный - этот пост виден всем",
"unlisted": "Непубличный - этот пост не виден на публичных лентах"
- }
+ },
+ "preview_empty": "Пустой предпросмотр",
+ "media_description_error": "Не удалось обновить вложение, попробуйте еще раз",
+ "empty_status_error": "Нельзя отправить пустой статус без вложений",
+ "preview": "Предпросмотр",
+ "direct_warning_to_first_only": "Это сообщение увидят только пользователи упомянутые в его начале.",
+ "direct_warning_to_all": "Это сообщение увидят все упомянутые пользователи.",
+ "content_type": {
+ "text/bbcode": "BBCode",
+ "text/html": "HTML",
+ "text/markdown": "Markdown",
+ "text/plain": "Простой текст"
+ },
+ "media_description": "Описание вложения",
+ "new_status": "Написать новый статус"
},
"registration": {
"bio": "Описание",
@@ -97,7 +134,12 @@
"password_required": "не должен быть пустым",
"password_confirmation_required": "не должно быть пустым",
"password_confirmation_match": "должно совпадать с паролем"
- }
+ },
+ "bio_placeholder": "например:\nПривет, я Игорь Печкин.\nРаботаю почтальоном в деревне Простоквашино. С недавних пор велосипедист.",
+ "fullname_placeholder": "например: Почтальон Печкин",
+ "username_placeholder": "например: pechkin",
+ "captcha": "Код подтверждения",
+ "new_captcha": "Нажмите на изображение чтобы получить новый код"
},
"settings": {
"enter_current_password_to_confirm": "Введите свой текущий пароль",
@@ -110,7 +152,7 @@
"generate_new_recovery_codes": "Получить новые коды востановления",
"warning_of_generate_new_codes": "После получения новых кодов восстановления, старые больше не будут работать.",
"recovery_codes": "Коды восстановления.",
- "waiting_a_recovery_codes": "Получение кодов восстановления ...",
+ "waiting_a_recovery_codes": "Получение кодов восстановления…",
"recovery_codes_warning": "Запишите эти коды и держите в безопасном месте - иначе вы их больше не увидите. Если вы потеряете доступ к OTP приложению - без резервных кодов вы больше не сможете залогиниться.",
"authentication_methods": "Методы аутентификации",
"scan": {
@@ -141,14 +183,14 @@
"change_password": "Сменить пароль",
"change_password_error": "Произошла ошибка при попытке изменить пароль.",
"changed_password": "Пароль изменён успешно!",
- "collapse_subject": "Сворачивать посты с темой",
+ "collapse_subject": "Сворачивать статусы с темой",
"confirm_new_password": "Подтверждение нового пароля",
"current_avatar": "Текущий аватар",
"current_password": "Текущий пароль",
"current_profile_banner": "Текущий баннер профиля",
"data_import_export_tab": "Импорт / Экспорт данных",
"delete_account": "Удалить аккаунт",
- "delete_account_description": "Удалить ваш аккаунт и все ваши сообщения.",
+ "delete_account_description": "Удалить вашу учётную запись и все ваши сообщения.",
"delete_account_error": "Возникла ошибка в процессе удаления вашего аккаунта. Если это повторяется, свяжитесь с администратором вашего сервера.",
"delete_account_instructions": "Введите ваш пароль в поле ниже для подтверждения удаления.",
"export_theme": "Сохранить Тему",
@@ -196,7 +238,7 @@
"hide_followers_count_description": "Не показывать число моих подписчиков",
"show_admin_badge": "Показывать значок администратора в моем профиле",
"show_moderator_badge": "Показывать значок модератора в моем профиле",
- "nsfw_clickthrough": "Включить скрытие NSFW вложений",
+ "nsfw_clickthrough": "Включить скрытие вложений и предпросмотра ссылок для NSFW статусов",
"oauth_tokens": "OAuth токены",
"token": "Токен",
"refresh_token": "Рефреш токен",
@@ -249,7 +291,18 @@
"save_load_hint": "Опции \"оставить...\" позволяют сохранить текущие настройки при выборе другой темы или импорта её из файла. Так же они влияют на то какие компоненты будут сохранены при экспорте темы. Когда все галочки сняты все компоненты будут экспортированы.",
"reset": "Сбросить",
"clear_all": "Очистить всё",
- "clear_opacity": "Очистить прозрачность"
+ "clear_opacity": "Очистить прозрачность",
+ "use_source": "Новая версия",
+ "use_snapshot": "Старая версия",
+ "keep_as_is": "Оставить, как есть",
+ "load_theme": "Загрузить тему",
+ "help": {
+ "fe_upgraded": "Движок тем для фронт-энда Pleroma был изменен после обновления.",
+ "older_version_imported": "Файл, который вы импортировали, был сделан в старой версии фронт-энда.",
+ "future_version_imported": "Файл, который вы импортировали, был сделан в новой версии фронт-энда.",
+ "v2_imported": "Файл, который вы импортировали, был сделан под старый фронт-энд. Мы стараемся улучшить совместимость, но все еще возможны несостыковки.",
+ "upgraded_from_v2": "Фронт-энд Pleroma был изменен. Выбранная тема может выглядеть слегка по-другому."
+ }
},
"common": {
"color": "Цвет",
@@ -284,7 +337,9 @@
"borders": "Границы",
"buttons": "Кнопки",
"inputs": "Поля ввода",
- "faint_text": "Маловажный текст"
+ "faint_text": "Маловажный текст",
+ "post": "Сообщения и описание пользователя",
+ "alert_neutral": "Нейтральный"
},
"radii": {
"_tab_label": "Скругление"
@@ -349,7 +404,75 @@
}
},
"allow_following_move": "Разрешить автоматически читать новый аккаунт при перемещении на другой сервер",
- "hide_user_stats": "Не показывать статистику пользователей (например количество читателей)"
+ "hide_user_stats": "Не показывать статистику пользователей (например количество читателей)",
+ "discoverable": "Разрешить показ аккаунта в поисковиках и других сервисах",
+ "default_vis": "Видимость постов по умолчанию",
+ "mutes_and_blocks": "Блокировки и игнорируемые",
+ "composing": "Составление постов",
+ "chatMessageRadius": "Сообщения в беседе",
+ "blocks_tab": "Блокировки",
+ "import_mutes_from_a_csv_file": "Импортировать игнорируемых из CSV файла",
+ "mutes_imported": "Игнорируемые импортированы! Обработка может занять некоторое время.",
+ "mute_import_error": "Произошла ошибка при импорте игнорируемых",
+ "mute_import": "Импорт игнорируемых",
+ "block_export_button": "Экспортирует блокировки в CSV файл",
+ "mute_export_button": "Экспортирует игнорируемых пользователей в CSV файл",
+ "mute_export": "Экспорт игнорируемых",
+ "blocks_imported": "Блокировки импортированы! Обработка может занять некоторое время.",
+ "block_import_error": "Произошла ошибка при импорте блокировок",
+ "block_import": "Импорт блокировок",
+ "block_export": "Экспортировать блокировки",
+ "security": "Безопасность",
+ "app_name": "Приложение",
+ "user_mutes": "Пользователи",
+ "post_status_content_type": "Формат составляемых статусов по умолчанию",
+ "subject_line_noop": "Не копировать",
+ "subject_line_mastodon": "Как в Mastodon: скопировать как есть",
+ "subject_line_email": "Как в e-mail: \"re: тема\"",
+ "subject_line_behavior": "Копировать тему в ответах",
+ "no_mutes": "Нет игнорируемых",
+ "no_blocks": "Нет блокировок",
+ "notification_visibility_emoji_reactions": "Реакции",
+ "notification_visibility_moves": "Миграции пользователей",
+ "use_contain_fit": "Не обрезать вложения в миниатюрах",
+ "profile_fields": {
+ "value": "Значение",
+ "name": "Пункт",
+ "add_field": "Добавить поле",
+ "label": "Таблица метаданных профиля"
+ },
+ "play_videos_in_modal": "Проигрывать видео во всплывающей рамке",
+ "mutes_tab": "Игнорируемые",
+ "invalid_theme_imported": "Выбраный файл не является темой Pleroma. Изменений в тему не было внесено.",
+ "import_blocks_from_a_csv_file": "Импортировать блокировки из CSV файла",
+ "hide_filtered_statuses": "Не показывать отфильтрованные статусы",
+ "hide_muted_posts": "Не показывать статусы игнорируемых пользователей",
+ "hide_post_stats": "Не показывать статистику статусов (например количество отметок «Нравится»)",
+ "use_one_click_nsfw": "Открывать NSFW вложения одним кликом",
+ "preload_images": "Предварительно загружать изображения",
+ "max_thumbnails": "Максимальное число миниатюр показываемых в статусе",
+ "emoji_reactions_on_timeline": "Показывать эмодзи реакции в ленте",
+ "domain_mutes": "Узлы",
+ "notification_setting_privacy": "Приватность",
+ "notification_setting_block_from_strangers": "Не показывать уведомления от пользователей которых вы не читаете",
+ "notification_setting_filters": "Фильтрация",
+ "notifications": "Уведомления",
+ "virtual_scrolling": "Оптимизировать рендеринг ленты",
+ "hide_wallpaper": "Скрыть обои узла",
+ "accent": "Акцент",
+ "upload_a_photo": "Загрузить фото",
+ "notification_mutes": "Чтобы не получать уведомления от определённого пользователя, заглушите его.",
+ "reset_avatar_confirm": "Вы действительно хотите сбросить личный образ?",
+ "reset_profile_banner": "Сбросить личный баннер",
+ "reset_profile_background": "Сбросить личные обои",
+ "reset_avatar": "Сбросить личный образ",
+ "search_user_to_mute": "Искать, кого вы хотите заглушить",
+ "search_user_to_block": "Искать, кого вы хотите заблокировать",
+ "pad_emoji": "Выделять эмодзи пробелами при добавлении из панели",
+ "avatar_size_instruction": "Желательный наименьший размер личного образа 150 на 150 пикселей.",
+ "enable_web_push_notifications": "Включить web push-уведомления",
+ "notification_blocks": "Блокировка пользователя выключает все уведомления от него, а также отписывает вас от него.",
+ "notification_setting_hide_notification_contents": "Скрыть отправителя и содержимое push-уведомлений"
},
"timeline": {
"collapse": "Свернуть",
@@ -359,11 +482,23 @@
"no_retweet_hint": "Пост помечен как \"только для подписчиков\" или \"личное\" и поэтому не может быть повторён",
"repeated": "повторил(а)",
"show_new": "Показать новые",
- "up_to_date": "Обновлено"
+ "up_to_date": "Обновлено",
+ "error": "Ошибка при обновлении ленты: {0}"
},
"status": {
- "bookmark": "В закладки",
- "unbookmark": "Удалить из закладок"
+ "bookmark": "Добавить в закладки",
+ "unbookmark": "Удалить из закладок",
+ "status_deleted": "Пост удален",
+ "reply_to": "Ответ",
+ "repeats": "Повторы",
+ "favorites": "Понравилось",
+ "unmute_conversation": "Прекратить игнорировать разговор",
+ "mute_conversation": "Игнорировать разговор",
+ "thread_muted": "Разговор игнорируется",
+ "external_source": "Перейти к источнику",
+ "delete_confirm": "Вы действительно хотите удалить данный статус?",
+ "delete": "Удалить",
+ "copy_link": "Скопировать ссылку"
},
"user_card": {
"block": "Заблокировать",
@@ -376,7 +511,7 @@
"follow_unfollow": "Перестать читать",
"followees": "Читаемые",
"followers": "Читатели",
- "following": "Читаю!",
+ "following": "Читаете!",
"follows_you": "Читает вас!",
"mute": "Игнорировать",
"muted": "Игнорирую",
@@ -401,6 +536,15 @@
"quarantine": "Не федерировать посты пользователя",
"delete_user": "Удалить пользователя",
"delete_user_confirmation": "Вы уверены? Это действие нельзя отменить."
+ },
+ "media": "С вложениями",
+ "mention": "Упомянуть",
+ "show_repeats": "Показывать повторы",
+ "hide_repeats": "Скрыть повторы",
+ "report": "Пожаловаться",
+ "roles": {
+ "moderator": "Модератор",
+ "admin": "Администратор"
}
},
"user_profile": {
@@ -468,15 +612,99 @@
"media_proxy": "Прокси для внешних вложений",
"text_limit": "Лимит символов",
"title": "Особенности",
- "gopher": "Gopher"
+ "gopher": "Gopher",
+ "who_to_follow": "Предложения кого читать",
+ "pleroma_chat_messages": "Pleroma Чат",
+ "upload_limit": "Наибольший размер загружаемого файла"
},
"tool_tip": {
"accept_follow_request": "Принять запрос на чтение",
- "reject_follow_request": "Отклонить запрос на чтение"
+ "reject_follow_request": "Отклонить запрос на чтение",
+ "media_upload": "Прикрепить вложение"
},
"image_cropper": {
"save_without_cropping": "Сохранить не обрезая",
"save": "Сохранить",
- "crop_picture": "Обрезать картинку"
+ "crop_picture": "Обрезать картинку",
+ "cancel": "Отменить"
+ },
+ "errors": {
+ "storage_unavailable": "Pleroma не смогла получить доступ к хранилищу браузера. Ваша сессия и настройки не будут сохранены, и вы можете столкнуться с непредвиденными проблемами. Попробуйте включить файлы cookie."
+ },
+ "polls": {
+ "not_enough_options": "Недостаточно уникальных вариантов в опросе",
+ "expired": "Опрос закончился {0} назад",
+ "expires_in": "Опрос заканчивается через {0}",
+ "expiry": "Срок опроса",
+ "multiple_choices": "Несколько вариантов",
+ "single_choice": "Один вариант",
+ "type": "Тип опроса",
+ "vote": "Проголосовать",
+ "votes": "голосов",
+ "option": "Вариант",
+ "add_option": "Добавить вариант",
+ "add_poll": "Прикрепить опрос"
+ },
+ "media_modal": {
+ "next": "Следующая",
+ "previous": "Предыдущая"
+ },
+ "importer": {
+ "error": "Произошла ошибка при импорте файла.",
+ "success": "Импорт прошел успешно.",
+ "submit": "Отправить"
+ },
+ "selectable_list": {
+ "select_all": "Выбрать все"
+ },
+ "emoji": {
+ "load_all": "Все {emojiAmount} эмодзи загружаются",
+ "load_all_hint": "Загружены первые {saneAmount} эмодзи, загрузка всех эмодзи может привести к проблемам с производительностью.",
+ "unicode": "Стандартные эмодзи",
+ "custom": "Пользовательские эмодзи",
+ "add_emoji": "Добавить эмодзи",
+ "search_emoji": "Поиск эмодзи",
+ "keep_open": "Оставить окно выбора открытым",
+ "emoji": "Эмодзи",
+ "stickers": "Стикеры"
+ },
+ "shoutbox": {
+ "title": "Болтовня"
+ },
+ "time": {
+ "days_short": "{0}дн",
+ "years_short": "{0}г",
+ "year_short": "{0}г",
+ "weeks_short": "{0}нед",
+ "week_short": "{0}нед",
+ "seconds_short": "{0}сек",
+ "second_short": "{0}с",
+ "now_short": "только что",
+ "now": "только что",
+ "months_short": "{0}мес",
+ "month_short": "{0}мес",
+ "minutes_short": "{0}мин",
+ "minute_short": "{0}мин",
+ "in_past": "{0} назад",
+ "in_future": "через {0}",
+ "hours_short": "{0}ч",
+ "hour_short": "{0}ч",
+ "hour": "{0} час",
+ "day_short": "{0}д",
+ "days": "{0} дней"
+ },
+ "chats": {
+ "empty_chat_list_placeholder": "У вас пока нет бесед. Начните одну!",
+ "delete_confirm": "Вы точно хотите удалить сообщение?",
+ "empty_message_error": "Нельзя отправить пустое сообщение",
+ "new": "Новая беседа",
+ "chats": "Беседы",
+ "delete": "Удалить",
+ "message_user": "Напишите {nickname}",
+ "you": "Вы:"
+ },
+ "remote_user_resolver": {
+ "error": "Не найдено.",
+ "searching_for": "Ищем"
}
}
diff --git a/src/i18n/uk.json b/src/i18n/uk.json
new file mode 100644
index 00000000..040d6f4f
--- /dev/null
+++ b/src/i18n/uk.json
@@ -0,0 +1,823 @@
+{
+ "general": {
+ "dismiss": "Закрити",
+ "close": "Закрити",
+ "verify": "Перевірити",
+ "confirm": "Підтвердити",
+ "enable": "Увімкнути",
+ "disable": "Вимкнути",
+ "cancel": "Скасувати",
+ "show_less": "Показати менше",
+ "show_more": "Показати більше",
+ "optional": "необов'язково",
+ "retry": "Спробуйте ще раз",
+ "error_retry": "Будь ласка, спробуйте ще раз",
+ "generic_error": "Виникла помилка",
+ "loading": "Завантаження…",
+ "more": "Більше",
+ "submit": "Відправити",
+ "apply": "Застосувати",
+ "peek": "Глянути"
+ },
+ "finder": {
+ "error_fetching_user": "Користувача не знайдено",
+ "find_user": "Знайти користувача"
+ },
+ "features_panel": {
+ "gopher": "Gopher",
+ "pleroma_chat_messages": "Чати",
+ "chat": "Міні-чат",
+ "who_to_follow": "Кого відстежувати",
+ "title": "Особливості",
+ "scope_options": "Параметри обсягу",
+ "media_proxy": "Посередник медіа-даних",
+ "text_limit": "Ліміт символів",
+ "upload_limit": "Обмеження завантажень"
+ },
+ "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": "Поточний інстанс не прийматиме повідомлення з перелічених інстансів:",
+ "accept": "Прийняти",
+ "reject": "Відхилити",
+ "accept_desc": "Поточний інстанс приймає повідомлення тільки з перелічених інстансів:",
+ "simple_policies": "Правила поточного інстансу"
+ },
+ "mrf_policies_desc": "Правила MRF розповсюджуються на даний інстанс. Наступні правила активні:",
+ "mrf_policies": "Активувати правила MRF (модуль переписування повідомлень)",
+ "keyword": {
+ "is_replaced_by": "→",
+ "replace": "Замінити",
+ "reject": "Відхилити",
+ "ftl_removal": "Прибрати з федеративної стрічки",
+ "keyword_policies": "Політика щодо ключових слів"
+ },
+ "federation": "Федерація"
+ }
+ },
+ "login": {
+ "hint": "Увійдіть, щоб доєднатися до дискусії",
+ "username": "Ім'я користувача",
+ "register": "Зареєструватись",
+ "password": "Пароль",
+ "logout": "Вийти",
+ "description": "Увійти за допомогою OAuth",
+ "login": "Увійти",
+ "recovery_code": "Код відновлення",
+ "enter_recovery_code": "Введіть код відновлення",
+ "authentication_code": "Код автентифікації",
+ "heading": {
+ "recovery": "Двофакторне відновлення",
+ "totp": "Двофакторна автентифікація"
+ },
+ "enter_two_factor_code": "Введіть двофакторний код автентифікації",
+ "placeholder": "напр. stepan"
+ },
+ "importer": {
+ "error": "Під час імпортування файлу сталася помилка.",
+ "success": "Імпортовано успішно.",
+ "submit": "Відправити"
+ },
+ "image_cropper": {
+ "cancel": "Відмінити",
+ "save_without_cropping": "Зберегти не обрізаючи",
+ "crop_picture": "Обрізати малюнок",
+ "save": "Зберегти"
+ },
+ "polls": {
+ "expired": "Опитування закінчилось {0} тому",
+ "expires_in": "Опитування закінчується через {0}",
+ "expiry": "Термін опитування",
+ "multiple_choices": "Декілька варіантів",
+ "single_choice": "Один варіант",
+ "add_option": "Додати опцію",
+ "type": "Тип опитування",
+ "vote": "Проголосувати",
+ "votes": "голосів",
+ "option": "Відповідь",
+ "add_poll": "Додати опитування",
+ "not_enough_options": "Замало унікальних варіантів в опитуванні"
+ },
+ "notifications": {
+ "reacted_with": "додав реакцію: {0}",
+ "migrated_to": "мігрував на",
+ "no_more_notifications": "Немає більше сповіщень",
+ "repeated_you": "поширив(-ла) ваш допис",
+ "read": "Прочитано!",
+ "notifications": "Сповіщення",
+ "load_older": "Завантажити давніші сповіщення",
+ "follow_request": "хоче підписатись на вас",
+ "followed_you": "підписався(-лась) на вас",
+ "favorited_you": "вподобав(-ла) ваш допис",
+ "broken_favorite": "Невідомий допис, шукаю його…",
+ "error": "Помилка при оновленні сповіщень: {0}"
+ },
+ "nav": {
+ "chats": "Чати",
+ "timelines": "Стрічки",
+ "twkn": "Уся відома мережа",
+ "about": "Інформація",
+ "preferences": "Налаштування",
+ "friend_requests": "Запити послідовників",
+ "who_to_follow": "Кого відстежувати",
+ "search": "Пошук",
+ "user_search": "Пошук користувача",
+ "bookmarks": "Закладки",
+ "timeline": "Домашня стрічка",
+ "public_tl": "Публічна стрічка",
+ "dms": "Приватні повідомлення",
+ "interactions": "Взаємодії",
+ "mentions": "Згадування",
+ "back": "Назад",
+ "administration": "Адміністрування"
+ },
+ "media_modal": {
+ "next": "Наступна",
+ "previous": "Попередня"
+ },
+ "password_reset": {
+ "instruction": "Введіть свою адресу електронної пошти або ім’я користувача. Ми надішлемо вам посилання для скидання пароля.",
+ "placeholder": "Ваша електронна адреса або ім'я користувача",
+ "check_email": "Перевірте електронну пошту на наявність посилання для скидання пароля.",
+ "return_home": "Повернутися на головну сторінку",
+ "too_many_requests": "Ви досягли ліміту спроб, спробуйте ще раз пізніше.",
+ "password_reset_required_but_mailer_is_disabled": "Ви повинні скинути свій пароль, але скидання пароля вимкнено. Зверніться до адміністратора інстансу.",
+ "password_reset_disabled": "Скидання пароля вимкнено. Зверніться до адміністратора інстансу.",
+ "password_reset_required": "Для входу потрібно скинути пароль.",
+ "password_reset": "Відновити пароль",
+ "forgot_password": "Забули пароль?"
+ },
+ "chats": {
+ "you": "Ви:",
+ "message_user": "Повідомлення для {nickname}",
+ "delete": "Видалити",
+ "chats": "Чати",
+ "new": "Новий чат",
+ "empty_message_error": "Не вдається опублікувати порожнє повідомлення",
+ "more": "Більше",
+ "delete_confirm": "Ви дійсно хочете видалити це повідомлення?",
+ "error_loading_chat": "Під час завантаження чату сталася помилка.",
+ "error_sending_message": "Під час надсилання повідомлення сталася помилка.",
+ "empty_chat_list_placeholder": "У вас ще немає чатів. Почніть новий чат!"
+ },
+ "file_type": {
+ "audio": "Аудіо",
+ "video": "Відео",
+ "image": "Зображення",
+ "file": "Файл"
+ },
+ "display_date": {
+ "today": "Сьогодні"
+ },
+ "interactions": {
+ "load_older": "Завантажити давніші взаємодії",
+ "follows": "Нові підписки",
+ "favs_repeats": "Поширення та вподобайки",
+ "moves": "Міграції користувачів"
+ },
+ "errors": {
+ "storage_unavailable": "Pleroma не змогла отримати доступ до сховища браузеру. Ваша сесія та налаштування не будуть збережені, це може спричинити непередбачувані проблеми. Спробуйте увімкнути cookie."
+ },
+ "emoji": {
+ "stickers": "Стікери",
+ "custom": "Користувацькі емодзі",
+ "search_emoji": "Пошук емодзі",
+ "keep_open": "Тримати панель відкритою",
+ "add_emoji": "Додати емодзі",
+ "emoji": "Емодзі",
+ "load_all": "Всі {emojiAmount} эмодзі завантажуються",
+ "load_all_hint": "Завантажені перші {saneAmount} емодзі, завантаження всіх емодзі може призвести до проблем з продуктивністю.",
+ "unicode": "Стандартні емодзі"
+ },
+ "post_status": {
+ "content_type": {
+ "text/bbcode": "BBCode",
+ "text/markdown": "Markdown",
+ "text/html": "HTML",
+ "text/plain": "Текстові дані"
+ },
+ "attachments_sensitive": "Позначити вкладення як чутливі",
+ "account_not_locked_warning_link": "замкнена",
+ "account_not_locked_warning": "Ваша обліковка не {0}. Будь-хто може відстежувати вас для перегляду дописів тільки для підписників.",
+ "new_status": "Створити допис",
+ "direct_warning_to_first_only": "Цей допис побачать лише користувачі, що були згадані на початку повідомлення.",
+ "direct_warning_to_all": "Цей допис побачать всі згадані користувачі.",
+ "default": "Що нового?",
+ "content_warning": "Тема (необов'язково)",
+ "preview": "Попередній перегляд",
+ "posting": "Відправляється",
+ "empty_status_error": "Не можу опублікувати пустий статус без вкладень",
+ "scope": {
+ "unlisted": "Непублічний - цей допис буде відсутній у публічних стрічках",
+ "public": "Публічний - цей допис побачать усі",
+ "private": "Для читачів - цей допис побачать лише ваші читачі",
+ "direct": "Приватний - цей допис побачать лише згадані користувачі"
+ },
+ "scope_notice": {
+ "unlisted": "Цей допис не буде видно в публічній стрічці та усій відомій мережі",
+ "private": "Цей допис побачать лише ваші підписники",
+ "public": "Цей допис бачитимуть усі"
+ },
+ "preview_empty": "Пустий",
+ "media_description_error": "Не вдалось оновити медіа, спробуйте ще раз",
+ "media_description": "Опис медіа"
+ },
+ "settings": {
+ "blocks_imported": "Блокування імпортовані! Їх обробка триватиме певний час.",
+ "block_import_error": "Помилка імпортування блокувань",
+ "block_import": "Імпорт блокувань",
+ "block_export_button": "Експорт блокувань у файл CSV",
+ "block_export": "Експорт блокувань",
+ "bio": "Про Вас",
+ "background": "Обкладинка",
+ "app_name": "Назва програми",
+ "follow_export": "Експортувати відстежуваних",
+ "filtering_explanation": "Усі статуси з цими словами будуть приховані, один на рядок",
+ "filtering": "Фільтрування",
+ "export_theme": "Зберегти переднабір",
+ "avatar_size_instruction": "Рекомендований мінімальний розмір для зображень аватара становить 150x150 пікселів.",
+ "delete_account_instructions": "Введіть ваш пароль в поле нижче, аби підтвердити видалення облікового запису.",
+ "delete_account_error": "Під час видалення вашого облікового запису виникла проблема. Якщо це трапляється постійно, будь ласка, зверніться до адміністратора вашого сервера.",
+ "delete_account_description": "Остаточно видалити ваш обліковий запис та усі ваші повідомлення.",
+ "delete_account": "Видалити обліковий запис",
+ "default_vis": "Обсяг видимості за замовчуванням",
+ "data_import_export_tab": "Імпорт/експорт даних",
+ "current_password": "Поточний пароль",
+ "confirm_new_password": "Підтвердіть новий пароль",
+ "composing": "Складання відповіді",
+ "collapse_subject": "Згорнути дописи з темами",
+ "changed_password": "Пароль успішно змінено!",
+ "change_password_error": "Не вдалося змінити пароль.",
+ "change_password": "Змінити пароль",
+ "cRed": "Червоний (Скасувати)",
+ "cGreen": "Зелений (Поширити)",
+ "cOrange": "Жовтогарячий (Вподобайки)",
+ "cBlue": "Блакитний (Відповісти, читати)",
+ "btnRadius": "Кнопки",
+ "blocks_tab": "Блокування",
+ "avatarRadius": "Аватарки",
+ "avatarAltRadius": "Аватарки у сповіщеннях",
+ "avatar": "Аватар",
+ "attachments": "Вкладення",
+ "attachmentRadius": "Вкладення",
+ "general": "Загальні",
+ "foreground": "Передній план",
+ "follows_imported": "Відстежуваних імпортовано! Їхня обробка потребує часу.",
+ "follow_import_error": "Помилка імпортування відстежуваних",
+ "follow_import": "Імпортувати відстежуваних",
+ "follow_export_button": "Експортувати відстежуваних до csv файлу",
+ "lock_account_description": "Обмежте свій обліковий запис лише схваленими читачами",
+ "links": "Посилання",
+ "limited_availability": "Недоступно у вашому браузері",
+ "invalid_theme_imported": "Вибраний файл не є темою Pleroma. У вашу тему не внесено жодних змін.",
+ "interfaceLanguage": "Мова оболонки",
+ "interface": "Оболонка",
+ "instance_default_simple": "(за замовчуванням)",
+ "instance_default": "(за замовчуванням: {value})",
+ "checkboxRadius": "Прапорці",
+ "inputRadius": "Поля вводу",
+ "import_theme": "Завантажити переднабір",
+ "import_followers_from_a_csv_file": "Імпортувати відстежуваних з csv файлу",
+ "import_blocks_from_a_csv_file": "Імпортувати заблокованих з csv файлу",
+ "hide_filtered_statuses": "Сховати відфільтровані статуси",
+ "hide_user_stats": "Приховувати статистику користувачів (напр. кількість відстежувачів)",
+ "hide_post_stats": "Приховувати статистику дописів (напр. кількість вподобаних)",
+ "use_one_click_nsfw": "Відкривати NSFW вкладення одним кліком миші",
+ "preload_images": "Передзавантажувати світлини",
+ "hide_isp": "Сховати панель з особливостями сервера",
+ "max_thumbnails": "Максимальна кількість мініатюр на повідомлення",
+ "hide_muted_posts": "Приховати повідомлення приглушених користувачів",
+ "hide_attachments_in_tl": "Приховувати вкладення у стрічці",
+ "hide_attachments_in_convo": "Приховувати вкладення у розмовах",
+ "mutes_tab": "Заглушені",
+ "loop_video_silent_only": "Зациклити відео без звуку (напр. Mastodon \"gifs\")",
+ "loop_video": "Зациклити відео",
+ "mfa": {
+ "verify": {
+ "desc": "Щоб увімкнути двофакторну автентифікацію, введіть код з вашого застосунку для двофакторної автентифікації:"
+ },
+ "scan": {
+ "desc": "Відскануйте цей QR-код за допомогою програми двофакторної автентифікації або введіть текстовий ключ:",
+ "title": "Сканування",
+ "secret_code": "Ключ"
+ },
+ "authentication_methods": "Методи автентифікації",
+ "recovery_codes_warning": "Запишіть ці коди і тримайте в безпечному місці - інакше ви їх ніколи не побачите. Якщо ви втратите доступ до OTP додатку - без резервних кодів ви не зможете отримати доступ до свого облікового запису.",
+ "waiting_a_recovery_codes": "Отримую резервні коди…",
+ "recovery_codes": "Резервні коди.",
+ "warning_of_generate_new_codes": "Після отримання нових резервних кодів, старі перестануть працювати.",
+ "generate_new_recovery_codes": "Згенерувати нові резервні коди",
+ "title": "Двофакторна автентифікація",
+ "confirm_and_enable": "Підтвердити та увімкнути OTP",
+ "wait_pre_setup_otp": "попереднє налаштування OTP",
+ "setup_otp": "Налаштування OTP",
+ "otp": "OTP"
+ },
+ "enter_current_password_to_confirm": "Введіть свій поточний пароль",
+ "security": "Безпека",
+ "domain_mutes": "Домени",
+ "discoverable": "Дозволити виявлення цього облікового запису в результатах пошуку та інших службах",
+ "mutes_and_blocks": "Заглушення та блокування",
+ "changed_email": "Email успішно змінено!",
+ "change_email_error": "Сталася помилка під час зміни email.",
+ "change_email": "Змінити email",
+ "bot": "Це обліковий запис бота",
+ "import_mutes_from_a_csv_file": "Імпорт заглушених з csv файлу",
+ "mutes_imported": "Заглушені імпортовані! Їх обробка триватиме певний час.",
+ "mute_export_button": "Експорт заглушених у csv файл",
+ "mute_import_error": "Під час імпорту заглушених сталася помилка",
+ "mute_import": "Імпорт ігнорувань",
+ "mute_export": "Експорт ігнорувань",
+ "new_password": "Новий пароль",
+ "new_email": "Нова ел. пошта",
+ "name_bio": "Особисті дані",
+ "set_new_profile_banner": "Встановити новий банер",
+ "set_new_avatar": "Встановити новий аватар",
+ "security_tab": "Безпека",
+ "saving_ok": "Налаштування збережені",
+ "saving_err": "Помилка при збереженні налаштувань",
+ "reply_visibility_self": "Показувати лише адресовані мені відповіді",
+ "reply_visibility_following": "Показувати відповіді адресовані лише мені або користувачам, яких я читаю",
+ "reply_visibility_all": "Показати всі відповіді",
+ "replies_in_timeline": "Відповіді в стрічці",
+ "profile_tab": "Профіль",
+ "profile_banner": "Банер профілю",
+ "profile_background": "Обкладинка профілю",
+ "revoke_token": "Відкликати",
+ "oauth_tokens": "OAuth ключі",
+ "token": "Ключ",
+ "refresh_token": "Оновити ключ",
+ "valid_until": "Діє до",
+ "use_contain_fit": "Не обрізати краї мініатюр",
+ "name": "Ім'я",
+ "profile_fields": {
+ "value": "Зміст",
+ "name": "Назва",
+ "add_field": "Додати поле",
+ "label": "Метадані профілю"
+ },
+ "play_videos_in_modal": "Відтворювати відео у спливаючій рамці",
+ "accent": "Акцент",
+ "chatMessageRadius": "Повідомлення в бесіді",
+ "notification_mutes": "Щоб перестати отримувати сповіщення від певного користувача, заглушіть його.",
+ "user_mutes": "Користувачі",
+ "no_mutes": "Заглушені відсутні",
+ "emoji_reactions_on_timeline": "Показувати реакції емоджі на стрічці",
+ "pad_emoji": "Автоматично додавати простір з обидвох сторін емоджі",
+ "allow_following_move": "Дозволити автостеження при переміщенні на інший інстанс",
+ "set_new_profile_background": "Встановити нову обкладинку профілю",
+ "radii_help": "Радіус заокруглення кутів інтерфейсу (в пікселях)",
+ "presets": "Переднабір",
+ "show_moderator_badge": "Показувати значок модератора в моєму профілі",
+ "show_admin_badge": "Показувати значок адміністратора в моєму профілі",
+ "hide_followers_description": "Не показувати хто підписаний на мене",
+ "hide_follows_description": "Не показувати на кого я підписаний",
+ "no_rich_text_description": "Видалення всього форматування тексту з усіх дописів",
+ "notification_visibility_emoji_reactions": "Реакції",
+ "notification_visibility_moves": "Міграція користувача",
+ "notification_visibility_repeats": "Поширення допису",
+ "notification_visibility_mentions": "Згадування",
+ "notification_visibility_likes": "Вподобайки",
+ "notification_visibility_follows": "Нові підписки",
+ "notification_visibility": "Отримувати сповіщення про наступні події",
+ "settings": "Налаштування",
+ "panelRadius": "Панелі",
+ "text": "Текст",
+ "tooltipRadius": "Підказки/попередження",
+ "values": {
+ "true": "так",
+ "false": "ні"
+ },
+ "user_settings": "Користувацькі налаштування",
+ "upload_a_photo": "Завантажити фото",
+ "theme": "Тема",
+ "style": {
+ "switcher": {
+ "keep_fonts": "Залишити шрифти",
+ "keep_roundness": "Залишити скруглення",
+ "keep_opacity": "Залишити прозорості",
+ "keep_shadows": "Залишити тіні",
+ "keep_color": "Залишити кольори",
+ "use_source": "Нова версія",
+ "use_snapshot": "Стара версія",
+ "load_theme": "Завантажити тему",
+ "reset": "Скинути",
+ "clear_all": "Очистити все",
+ "help": {
+ "older_version_imported": "Імпортований файл було створено в старішій версії FE.",
+ "future_version_imported": "Імпортований файл було створено в новішій версії FE.",
+ "v2_imported": "Файл, який ви імпортували, був створений для старішої версії інтерфейсу Pleroma. Ми намагаємось покращити сумісність, але все одно можуть бути розбіжності.",
+ "upgraded_from_v2": "PleromaFE було оновлено, тема може дещо відрізнятися від тієї, яку ви пам’ятаєте.",
+ "snapshot_source_mismatch": "Конфлікт версій: Швидше за все, FE повернуто до попередньої версії та оновлено знову, якщо ви змінили тему за допомогою старішої версії FE, швидше за все, ви хочете використовувати стару версію, інакше використовуйте нову версію.",
+ "migration_napshot_gone": "З якоїсь причини знімок зник, деякі речі можуть бути не такими, як ви пам’ятаєте.",
+ "migration_snapshot_ok": "Для безпеки, знімок теми завантажено. Ви можете спробувати завантажити дані теми.",
+ "fe_downgraded": "Версію PleromaFE змінено на старшу.",
+ "fe_upgraded": "Двигун теми PleromaFE оновлено.",
+ "snapshot_missing": "У файлі немає жодного знімка теми, тому він може виглядати інакше, ніж передбачалося спочатку.",
+ "snapshot_present": "Знімок теми завантажено, тому всі значення було перезаписано. Натомість ви можете завантажити правильні дані теми."
+ },
+ "keep_as_is": "Залишити як є",
+ "clear_opacity": "Очистити прозорість",
+ "save_load_hint": "Параметри \"Зберегти\" зберігають поточні параметри під час вибору або завантаження тем, вони також зберігають зазначені параметри під час експорту теми. Коли всі прапорці знято, експортування теми збереже все."
+ },
+ "common": {
+ "color": "Колір",
+ "contrast": {
+ "context": {
+ "text": "для тексту",
+ "18pt": "для великого (18pt+) тексту"
+ },
+ "level": {
+ "bad": "Не відповідає жодним вимогам щодо доступності",
+ "aaa": "відповідає вимогам рівня ААA (рекомендований)",
+ "aa": "відповідає вимогам рівня АА (мінімальний)"
+ },
+ "hint": "Рівень контрасту: {ratio}, {level} {context}"
+ },
+ "opacity": "Прозорість"
+ },
+ "preview": {
+ "mono": "змісту",
+ "text": "Трохи більше {0} та {1}",
+ "button": "Кнопка",
+ "error": "Приклад помилки",
+ "content": "Зміст",
+ "header": "Попередній перегляд",
+ "link": "невеличке посилання",
+ "header_faint": "Це нормально",
+ "input": "Що нового?",
+ "checkbox": "Я переглянув умови використання",
+ "fine_print": "Прочитайте наш {0} аби нічого нового не дізнатись!",
+ "faint_link": "корисний підручник"
+ },
+ "shadows": {
+ "components": {
+ "button": "Кнопка",
+ "input": "Поле вводу",
+ "panel": "Панель",
+ "panelHeader": "Заголовок панелі",
+ "avatarStatus": "Аватар користувача (в стрічці)",
+ "avatar": "Аватар користувача (профіль)",
+ "buttonPressedHover": "Кнопка (натиснута + наведений курсор)",
+ "buttonPressed": "Кнопка (натиснута)",
+ "buttonHover": "Кнопка (при наведенні)",
+ "popup": "Спливаючі вікна та підказки",
+ "topBar": "Верхня панель"
+ },
+ "component": "Компонент",
+ "filter_hint": {
+ "inset_classic": "Тіні спрямовані всередину використовуватимуть {0}",
+ "spread_zero": "Тіні з поширенням > 0 відображатимуться так, ніби було встановлено нуль",
+ "avatar_inset": "Зауважте, що використання як вставних, так і невставних тіней на аватарах може привести до непередбачуваних результатів із прозорими аватарами.",
+ "drop_shadow_syntax": "{0} не підтримує параметр {1} та ключове слово {2}.",
+ "always_drop_shadow": "Увага! Ця тінь завжди використовує {0}, якщо підтримується браузером."
+ },
+ "inset": "Всередину",
+ "blur": "Розмиття",
+ "shadow_id": "Тінь №{value}",
+ "override": "Перевизначити",
+ "_tab_label": "Тінь і підсвічування",
+ "hintV3": "Для тіней ви також можете використовувати позначення {0} для використання іншого кольорового слота.",
+ "spread": "Розмах"
+ },
+ "fonts": {
+ "components": {
+ "input": "Поля вводу",
+ "interface": "Інтерфейс",
+ "postCode": "Моноширинний текст в дописі (форматований текст)",
+ "post": "Текст допису"
+ },
+ "_tab_label": "Шрифти",
+ "size": "Розмір (в пікселях)",
+ "custom": "Нестандартний",
+ "weight": "Товщина",
+ "family": "Назва шрифту",
+ "help": "Виберіть шрифт для елементів інтерфейсу. Для \"нестандартного\" потрібно ввести точну назву шрифту, так як вона відображається в системі."
+ },
+ "advanced_colors": {
+ "alert_warning": "Попередження",
+ "underlay": "Тло",
+ "inputs": "Поля входу",
+ "buttons": "Кнопки",
+ "borders": "Кордони",
+ "top_bar": "Верхня панель",
+ "panel_header": "Заголовок панелі",
+ "badge_notification": "Сповіщення",
+ "popover": "Підказки, меню, поповери",
+ "badge": "Тло значків",
+ "post": "Дописи/Дані користувачів",
+ "alert_neutral": "Нейтральний",
+ "alert_error": "Помилки",
+ "alert": "Фон сповіщень",
+ "_tab_label": "Додатково",
+ "selectedPost": "Вибраний допис",
+ "highlight": "Виділені елементи",
+ "poll": "Діаграма опитування",
+ "icons": "Іконки",
+ "faint_text": "Затемнений текст",
+ "chat": {
+ "border": "Кайма",
+ "outgoing": "Вихідні повідомлення",
+ "incoming": "Вхідні повідомлення"
+ },
+ "toggled": "Переключено",
+ "disabled": "Вимкнено",
+ "selectedMenu": "Вибраний пункт меню",
+ "tabs": "Вкладки",
+ "pressed": "Натиснуто",
+ "wallpaper": "Шпалери"
+ },
+ "common_colors": {
+ "rgbo": "Піктограми, акценти, значки",
+ "foreground_hint": "Перегляньте вкладку \"Додатково\" для більшого контролю",
+ "main": "Загальні кольори",
+ "_tab_label": "Загальні"
+ },
+ "radii": {
+ "_tab_label": "Скруглення"
+ }
+ },
+ "enable_web_push_notifications": "Увімкнути web push-сповіщення",
+ "notifications": "Сповіщення",
+ "fun": "Розваги",
+ "notification_setting_privacy": "Приватність",
+ "notification_setting_filters": "Фільтри",
+ "reset_avatar": "Скинути аватар",
+ "reset_profile_background": "Скинути обкладинку профілю",
+ "reset_avatar_confirm": "Ви дійсно хочете скинути аватар?",
+ "reset_profile_banner": "Скинули банер профілю",
+ "hide_follows_count_description": "Не показувати кількість підписників",
+ "reset_banner_confirm": "Ви дійсно хочете скинути банер?",
+ "reset_background_confirm": "Ви дійсно хочете скинути обкладинку?",
+ "subject_line_behavior": "Вигляд теми при відповіді",
+ "subject_input_always_show": "Завжди показувати поле для вводу теми",
+ "minimal_scopes_mode": "Мінімізувати набір варіантів осягу для допису",
+ "scope_copy": "Копіювати осяг при відповіді (завжди ввімкнено для особистих повідомлень)",
+ "search_user_to_mute": "Шукайте кого ви хочете заглушити",
+ "search_user_to_block": "Шукайте кого ви хочете заблокувати",
+ "autohide_floating_post_button": "Автоматично ховати кнопку \"Новий допис\" (в мобільній версії)",
+ "pause_on_unfocused": "Призупинити трансляцію, коли вкладка неактивна",
+ "hide_followers_count_description": "Не показувати кількість моїх підписників",
+ "notification_blocks": "Блокування користувача зупиняє всі сповіщення від нього, а також скасовує його відстеження.",
+ "notification_setting_hide_notification_contents": "Ховати відправника та вміст push-сповіщень",
+ "notification_setting_block_from_strangers": "Блокувати сповіщення від користувачів за якими ви не слідкуєте",
+ "type_domains_to_mute": "Пошук доменів для заглушення",
+ "nsfw_clickthrough": "Увімкнути приховування NSFW медіа",
+ "greentext": "Мемний текст",
+ "virtual_scrolling": "Оптимізувати оновлення стрічки",
+ "theme_help_v2_2": "Піктограми під деякими записами є показниками контрасту між фоном та текстом. Коли ви наведете на них курсор, ви отримаєте детальну інформацію. Пам'ятайте, якщо ви використовуєте прозорість, індикатори показують найгірший варіант.",
+ "theme_help_v2_1": "Ви також можете замінити кольори та видимість окремих компонентів, перемикаючи прапорці, використовуйте \"Очистити все\", щоб видалити всі заміни.",
+ "theme_help": "Використовувати шістнадцяткові коди кольору (#rrggbb) щоб редагувати тему.",
+ "no_blocks": "Блокування відсутні",
+ "subject_line_mastodon": "Як в mastodon: просто скопіювати",
+ "subject_line_email": "Як в email: \"re: тема\"",
+ "useStreamingApiWarning": "(Не рекомендується, експериментально, повідомлення можуть зникати)",
+ "useStreamingApi": "Отримувати повідомлення та сповіщення в режимі реального часу",
+ "streaming": "Ввімкнути автоматичне завантаження нових повідомлень при прокручуванні вгору",
+ "stop_gifs": "Відтворювати GIF анімації тільки при наведенні",
+ "post_status_content_type": "Тип вмісту допису",
+ "subject_line_noop": "Не копіювати",
+ "version": {
+ "frontend_version": "Версія фронтенду",
+ "backend_version": "Версія бекенду",
+ "title": "Версія"
+ },
+ "hide_wallpaper": "Сховати шпалери екземпляру"
+ },
+ "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": "не може бути порожнім"
+ },
+ "bio_placeholder": "напр.\nНаш народ завжди прагне волі для себе і бажає її для інших народів. Він боровся і бореться за правду і справедливість. Ми хочемо жити у згоді і взаємному шануванні з усіми народами доброї волі. Такі самі права визнаємо за іншими народами, за які боремося для себе.",
+ "fullname_placeholder": "напр. Степан Бандера",
+ "username_placeholder": "напр. stepan",
+ "new_captcha": "Натисніть на зображення, щоб оновити код, якщо він нерозбірливий",
+ "token": "Ключ запрошення",
+ "registration": "Реєстрація",
+ "password_confirm": "Підтвердження паролю",
+ "fullname": "Відображене ім'я",
+ "email": "Ел. пошта",
+ "bio": "Про себе",
+ "captcha": "CAPTCHA"
+ },
+ "who_to_follow": {
+ "who_to_follow": "На кого підписатися",
+ "more": "Більше"
+ },
+ "tool_tip": {
+ "repeat": "Поширити",
+ "reply": "Відповісти",
+ "add_reaction": "Додати реакцію",
+ "user_settings": "Налаштування користувача",
+ "favorite": "Подобається",
+ "reject_follow_request": "Відхилити запит на підписку",
+ "accept_follow_request": "Прийняти запит на підписку",
+ "media_upload": "Завантажити медіа",
+ "bookmark": "Додати до закладок"
+ },
+ "upload": {
+ "error": {
+ "base": "Збій при завантаженні.",
+ "file_too_big": "Файл завеликий [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
+ "default": "Спробуйте ще раз пізніше",
+ "message": "Помилка завантаження: {0}"
+ },
+ "file_size_units": {
+ "TiB": "ТіБ",
+ "GiB": "ГіБ",
+ "MiB": "МіБ",
+ "KiB": "КіБ",
+ "B": "Б"
+ }
+ },
+ "time": {
+ "weeks_short": "{0}тижд.",
+ "week_short": "{0}тижд.",
+ "years_short": "{0}р",
+ "year_short": "{0}р",
+ "years": "{0} роки",
+ "year": "{0} рік",
+ "weeks": "{0} тижнів",
+ "week": "{0} тиждень",
+ "second_short": "{0}с",
+ "second": "{0} секунда",
+ "now_short": "щойно",
+ "now": "щойно",
+ "months_short": "{0}міс.",
+ "month_short": "{0}міс.",
+ "months": "{0} місяці",
+ "month": "{0} місяць",
+ "minutes_short": "{0}хв",
+ "hours_short": "{0}год",
+ "hour_short": "{0}год",
+ "day_short": "{0}д",
+ "minute_short": "{0}хв",
+ "minutes": "{0} хвилини",
+ "minute": "{0} хвилина",
+ "in_past": "{0} тому",
+ "hours": "{0} години",
+ "hour": "{0} година",
+ "days_short": "{0}д",
+ "days": "{0} дні",
+ "day": "{0} день",
+ "seconds_short": "{0}с",
+ "seconds": "{0} секунди",
+ "in_future": "через {0}"
+ },
+ "search": {
+ "no_results": "Немає результатів",
+ "hashtags": "Хештеги",
+ "people": "Люди",
+ "people_talking": "{count} людей говорять про це",
+ "person_talking": "{count} особа говорить про це"
+ },
+ "user_card": {
+ "statuses": "Дописи",
+ "message": "Повідомлення",
+ "follow": "Підписатись",
+ "follow_unfollow": "Відписатись",
+ "follow_again": "Відправити запит знову?",
+ "follow_sent": "Запит відправлено!",
+ "blocked": "Заблоковано!",
+ "admin_menu": {
+ "activate_account": "Активувати обліковий запис",
+ "deactivate_account": "Деактивувати обліковий запис",
+ "delete_account": "Видалити обліковий запис",
+ "moderation": "Модерація",
+ "delete_user_confirmation": "Ви абсолютно впевнені? Цю дію неможливо буде скасовувати.",
+ "delete_user": "Видалити обліковий запис",
+ "strip_media": "Вилучити медіа з дописів користувача",
+ "force_nsfw": "Позначити всі дописи як NSFW",
+ "disable_any_subscription": "Взагалі заборонити підписку на користувача",
+ "disable_remote_subscription": "Заборонити підписуватись на користувачів з віддалених інстансів",
+ "sandbox": "Показувати дописи лише підписникам",
+ "force_unlisted": "Не показувати дописи в стрічці",
+ "revoke_moderator": "Позбавити прав модератора",
+ "grant_moderator": "Надати права модератора",
+ "revoke_admin": "Позбавити прав адміністратора",
+ "grant_admin": "Надати права адміністратора",
+ "quarantine": "Не розповсюджувати дописи на інших інстансах"
+ },
+ "deny": "Відмовити",
+ "block": "Заблокувати",
+ "approve": "Схвалити",
+ "mention": "Згадати",
+ "unsubscribe": "Відписатись",
+ "subscribe": "Підписатись",
+ "report": "Поскаржитись",
+ "per_day": "на день",
+ "favorites": "Вподобання",
+ "media": "Медіа",
+ "show_repeats": "Показати поширення",
+ "hide_repeats": "Приховати поширення",
+ "its_you": "Це ти!",
+ "follows_you": "Підписаний на вас!",
+ "followers": "Підписники",
+ "followees": "Підписаний(-а)",
+ "follow_progress": "Запитую…",
+ "mute_progress": "Глушимо…",
+ "unmute_progress": "Знімаємо глушення…",
+ "unmute": "Зняти глушення",
+ "hidden": "Приховано",
+ "following": "Підписаний!",
+ "block_progress": "Блокуємо…",
+ "unblock_progress": "Розблоковуємо…",
+ "unblock": "Розблокувати",
+ "remote_follow": "Підписатись",
+ "muted": "Заглушений",
+ "mute": "Заглушити",
+ "roles": {
+ "moderator": "Модератор",
+ "admin": "Адміністратор"
+ }
+ },
+ "status": {
+ "copy_link": "Скопіювати посилання на допис",
+ "status_unavailable": "Допис недоступний",
+ "replies_list": "Відповіді:",
+ "delete_confirm": "Ви дійсно хочете видалити цей допис?",
+ "delete": "Видалити допис",
+ "pin": "Закріпити в профілі",
+ "status_deleted": "Цей допис був видалений",
+ "favorites": "Вподобане",
+ "hide_content": "Сховати вміст",
+ "show_content": "Показати вміст",
+ "hide_full_subject": "Сховати всю тему",
+ "show_full_subject": "Показати всю тему",
+ "thread_muted_and_words": ", має слова:",
+ "mute_conversation": "Заглушити розмову",
+ "reply_to": "Відповідь",
+ "unbookmark": "Видалити із закладок",
+ "bookmark": "Додати до закладок",
+ "pinned": "Закріплено",
+ "unpin": "Відкріпити від профілю",
+ "repeats": "Поширення",
+ "nsfw": "Дратівливий вміст",
+ "thread_muted": "Нитка заглушена",
+ "unmute_conversation": "Припинити глушити розмову",
+ "external_source": "Зовнішнє джерело",
+ "expand": "Розгорнути"
+ },
+ "timeline": {
+ "no_more_statuses": "Більше немає дописів",
+ "up_to_date": "Оновлено",
+ "reload": "Оновити",
+ "show_new": "Показати нові",
+ "load_older": "Завантажити давніші дописи",
+ "error": "Помилка завантаження стрічки: {0}",
+ "collapse": "Згорнути",
+ "conversation": "Розмова",
+ "no_statuses": "Ніяких статусів",
+ "repeated": "поширив(-ла)",
+ "no_retweet_hint": "Запис, позначено як \"тільки для підписників\" або \"особисте\" і тому не може бути поширений"
+ },
+ "user_reporting": {
+ "submit": "Відправити",
+ "forward_to": "Переслати до {0}",
+ "forward_description": "Цей обліковий запис належить іншому інстансу. Відправити їм копію скарги?",
+ "additional_comments": "Додаткове пояснення",
+ "add_comment_description": "Скарга буде надіслана модераторам вашого інстансу. Нижче Ви можете додати пояснення чому ви вирішили поскаржитись на цей обліковий запис:",
+ "title": "Поскаржитись на {0}",
+ "generic_error": "Виникла помилка під час обробки вашого запиту."
+ },
+ "user_profile": {
+ "profile_loading_error": "Вибачте, під час завантаження цього профілю виникла помилка.",
+ "profile_does_not_exist": "Вибачте, цей профіль більше не існує.",
+ "timeline_title": "Стрічка користувача"
+ }
+}
diff --git a/src/i18n/zh.json b/src/i18n/zh.json
index 1fb0b548..b4185b30 100644
--- a/src/i18n/zh.json
+++ b/src/i18n/zh.json
@@ -11,10 +11,11 @@
"gopher": "Gopher",
"media_proxy": "媒体代理",
"scope_options": "可见范围设置",
- "text_limit": "文字數量限制",
+ "text_limit": "文字数量限制",
"title": "功能",
"who_to_follow": "推荐关注",
- "pleroma_chat_messages": "Pleroma 聊天"
+ "pleroma_chat_messages": "Pleroma 聊天",
+ "upload_limit": "上传限制"
},
"finder": {
"error_fetching_user": "获取用户时发生错误",
@@ -24,8 +25,8 @@
"apply": "应用",
"submit": "提交",
"more": "更多",
- "generic_error": "发生一个错误",
- "optional": "可选项",
+ "generic_error": "发生了一个错误",
+ "optional": "可选",
"show_more": "展开",
"show_less": "收起",
"cancel": "取消",
@@ -34,7 +35,7 @@
"confirm": "确认",
"verify": "验证",
"dismiss": "忽略",
- "peek": "窥探",
+ "peek": "预览",
"close": "关闭",
"retry": "重试",
"error_retry": "请重试",
@@ -95,28 +96,29 @@
},
"notifications": {
"broken_favorite": "未知的状态,正在搜索中…",
- "favorited_you": "收藏了你的状态",
+ "favorited_you": "喜欢了你的状态",
"followed_you": "关注了你",
"load_older": "加载更早的通知",
"notifications": "通知",
- "read": "阅读!",
+ "read": "已阅!",
"repeated_you": "转发了你的状态",
"no_more_notifications": "没有更多的通知",
- "reacted_with": "和 {0} 互动过",
- "migrated_to": "迁移到",
- "follow_request": "想要关注你"
+ "reacted_with": "作出了 {0} 的反应",
+ "migrated_to": "迁移到了",
+ "follow_request": "想要关注你",
+ "error": "取得通知时发生错误:{0}"
},
"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": "投票的选项太少"
},
@@ -124,7 +126,7 @@
"add_sticker": "添加贴纸"
},
"interactions": {
- "favs_repeats": "转发和收藏",
+ "favs_repeats": "转发和喜欢",
"follows": "新的关注者",
"load_older": "加载更早的互动",
"moves": "用户迁移"
@@ -141,10 +143,10 @@
"text/bbcode": "BBCode"
},
"content_warning": "主题(可选)",
- "default": "刚刚抵达洛杉矶",
+ "default": "刚刚抵达上海。",
"direct_warning_to_all": "本条内容只有被提及的用户能够看到。",
"direct_warning_to_first_only": "本条内容只有被在消息开始处提及的用户能够看到。",
- "posting": "发送",
+ "posting": "发送中",
"scope_notice": {
"public": "本条内容可以被所有人看到",
"private": "关注你的人才能看到本条内容",
@@ -165,7 +167,7 @@
"registration": {
"bio": "简介",
"email": "电子邮箱",
- "fullname": "全名",
+ "fullname": "显示名称",
"password_confirm": "确认密码",
"registration": "注册",
"token": "邀请码",
@@ -189,7 +191,7 @@
"settings": {
"app_name": "App 名称",
"security": "安全",
- "enter_current_password_to_confirm": "输入你当前密码来确认你的身份",
+ "enter_current_password_to_confirm": "输入您当前的密码来确认您的身份",
"mfa": {
"otp": "OTP",
"setup_otp": "设置 OTP",
@@ -197,18 +199,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": "附件",
@@ -218,22 +220,22 @@
"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": "绿色(转发)",
- "cOrange": "橙色(收藏)",
+ "cOrange": "橙色(喜欢)",
"cRed": "红色(取消)",
"change_password": "修改密码",
"change_password_error": "修改密码的时候出了点问题。",
"changed_password": "成功修改了密码!",
"collapse_subject": "折叠带主题的内容",
- "composing": "正在书写",
+ "composing": "写作",
"confirm_new_password": "确认新密码",
"current_avatar": "当前头像",
"current_password": "当前密码",
@@ -243,8 +245,8 @@
"delete_account": "删除账户",
"delete_account_description": "永久删除你的帐号和所有数据。",
"delete_account_error": "删除账户时发生错误,如果一直删除不了,请联系实例管理员。",
- "delete_account_instructions": "在下面输入你的密码来确认删除账户",
- "avatar_size_instruction": "推荐的头像图片最小的尺寸是 150x150 像素。",
+ "delete_account_instructions": "在下面输入您的密码来确认删除账户。",
+ "avatar_size_instruction": "推荐的头像图片最小尺寸为 150x150 像素。",
"export_theme": "导出预置主题",
"filtering": "过滤器",
"filtering_explanation": "所有包含以下词汇的内容都会被隐藏,一行一个",
@@ -258,11 +260,11 @@
"hide_attachments_in_convo": "在对话中隐藏附件",
"hide_attachments_in_tl": "在时间线上隐藏附件",
"hide_muted_posts": "不显示被隐藏的用户的帖子",
- "max_thumbnails": "最多再每个帖子所能显示的缩略图数量",
- "hide_isp": "隐藏指定实例的面板H",
+ "max_thumbnails": "每个帖子最多能显示的缩略图数量",
+ "hide_isp": "隐藏实例独有的面板",
"preload_images": "预载图片",
- "use_one_click_nsfw": "点击一次以打开工作场所不适宜的附件",
- "hide_post_stats": "隐藏推文相关的统计数据(例如:收藏的次数)",
+ "use_one_click_nsfw": "点击一次以打开工作场所不适宜(NSFW)的附件",
+ "hide_post_stats": "隐藏帖子的统计数据(例如:喜欢的次数)",
"hide_user_stats": "隐藏用户的统计数据(例如:关注者的数量)",
"hide_filtered_statuses": "隐藏过滤的状态",
"import_blocks_from_a_csv_file": "从 csv 文件中导入拉黑名单",
@@ -277,7 +279,7 @@
"invalid_theme_imported": "您所选择的主题文件不被 Pleroma 支持,因此主题未被修改。",
"limited_availability": "在您的浏览器中无法使用",
"links": "链接",
- "lock_account_description": "你需要手动审核关注请求",
+ "lock_account_description": "您需要手动审核关注请求",
"loop_video": "循环视频",
"loop_video_silent_only": "只循环没有声音的视频(例如:Mastodon 里的“GIF”)",
"mutes_tab": "隐藏",
@@ -292,13 +294,13 @@
"notification_visibility_mentions": "提及",
"notification_visibility_repeats": "转发",
"no_rich_text_description": "不显示富文本格式",
- "no_blocks": "没有拉黑的",
+ "no_blocks": "没有屏蔽",
"no_mutes": "没有隐藏",
"hide_follows_description": "不要显示我所关注的人",
"hide_followers_description": "不要显示关注我的人",
- "show_admin_badge": "显示管理徽章",
- "show_moderator_badge": "显示版主徽章",
- "nsfw_clickthrough": "将不和谐附件隐藏,点击才能打开",
+ "show_admin_badge": "在我的个人资料中显示管理员徽章",
+ "show_moderator_badge": "在我的个人资料中显示监察员徽章",
+ "nsfw_clickthrough": "将不和谐附件和链接预览隐藏,点击才会显示",
"oauth_tokens": "OAuth令牌",
"token": "令牌",
"refresh_token": "刷新令牌",
@@ -307,7 +309,7 @@
"panelRadius": "面板",
"pause_on_unfocused": "在离开页面时暂停时间线推送",
"presets": "预置",
- "profile_background": "个人资料背景图",
+ "profile_background": "个人背景图",
"profile_banner": "横幅图片",
"profile_tab": "个人资料",
"radii_help": "设置界面边缘的圆角 (单位:像素)",
@@ -321,24 +323,24 @@
"search_user_to_block": "搜索你想屏蔽的用户",
"search_user_to_mute": "搜索你想要隐藏的用户",
"security_tab": "安全",
- "scope_copy": "回复时的复制范围(私信是总是复制的)",
- "minimal_scopes_mode": "最小发文范围",
+ "scope_copy": "回复时复制可见范围(私信中永远会复制)",
+ "minimal_scopes_mode": "使发文可见范围的选项最少化",
"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_mastodon": "比如 mastodon: copy as is",
+ "subject_line_email": "类似电子邮件: \"re: 主题\"",
+ "subject_line_mastodon": "类似 mastodon: 与原主题相同",
"subject_line_noop": "不要复制",
"post_status_content_type": "发文状态内容类型",
"stop_gifs": "鼠标悬停时播放GIF",
- "streaming": "开启滚动到顶部时的自动推送",
+ "streaming": "滚动到顶部时自动推送新内容",
"text": "文本",
"theme": "主题",
"theme_help": "使用十六进制代码(#rrggbb)来设置主题颜色。",
- "theme_help_v2_1": "你也可以通过切换复选框来覆盖某些组件的颜色和透明。使用“清除所有”来清楚所有覆盖设置。",
+ "theme_help_v2_1": "您也可以通过选中复选框来覆盖某些组件的颜色和透明度。使用“清除所有”按钮来清除所有覆盖设置。",
"theme_help_v2_2": "某些条目下的图标是背景或文本对比指示器,鼠标悬停可以获取详细信息。请记住,使用透明度来显示最差的情况。",
"tooltipRadius": "提醒",
"upload_a_photo": "上传照片",
@@ -349,7 +351,7 @@
},
"notifications": "通知",
"notification_mutes": "要停止收到某个指定的用户的通知,请使用隐藏功能。",
- "notification_blocks": "拉黑一个用户会停掉所有他的通知,等同于取消关注。",
+ "notification_blocks": "屏蔽一个用户会停止接收来自该用户的所有通知,并且会取消对该用户的关注。",
"enable_web_push_notifications": "启用 web 推送通知",
"style": {
"switcher": {
@@ -364,7 +366,7 @@
"clear_opacity": "清除透明度",
"load_theme": "加载主题",
"help": {
- "upgraded_from_v2": "PleromaFE 已升级,主题会和你记忆中的不太一样。",
+ "upgraded_from_v2": "PleromaFE 已升级,主题会与您记忆中的不太一样。",
"older_version_imported": "您导入的文件来自旧版本的 FE。",
"future_version_imported": "您导入的文件来自更高版本的 FE。",
"v2_imported": "您导入的文件是旧版 FE 的。我们尽可能保持兼容性,但还是可能出现不一致的情况。",
@@ -400,7 +402,7 @@
"_tab_label": "常规",
"main": "常用颜色",
"foreground_hint": "点击”高级“ 标签进行细致的控制",
- "rgbo": "图标,口音,徽章"
+ "rgbo": "图标,强调,徽章"
},
"advanced_colors": {
"_tab_label": "高级",
@@ -420,7 +422,7 @@
"incoming": "收到的"
},
"disabled": "禁用的",
- "pressed": "按下的",
+ "pressed": "压下的",
"highlight": "强调元素",
"selectedMenu": "选中的菜单项",
"selectedPost": "选中的发布内容",
@@ -431,7 +433,9 @@
"alert_neutral": "中性",
"alert_warning": "警告",
"tabs": "标签页",
- "underlay": "底衬"
+ "underlay": "底衬",
+ "toggled": "按下的",
+ "wallpaper": "壁纸"
},
"radii": {
"_tab_label": "圆角"
@@ -443,14 +447,14 @@
"shadow_id": "阴影 #{value}",
"blur": "模糊",
"spread": "扩散",
- "inset": "插入内部",
+ "inset": "内阴影",
"hint": "对于阴影你还可以使用 --variable 作为颜色值来使用 CSS3 变量。请注意,这种情况下,透明设置将不起作用。",
"filter_hint": {
"always_drop_shadow": "警告,此阴影设置会总是使用 {0} ,如果浏览器支持的话。",
"drop_shadow_syntax": "{0} 不支持参数 {1} 和关键词 {2} 。",
"avatar_inset": "请注意组合两个内部和非内部的阴影到头像上,在透明头像上可能会有意料之外的效果。",
"spread_zero": "阴影的扩散 > 0 会同设置成零一样",
- "inset_classic": "插入内部的阴影会使用 {0}"
+ "inset_classic": "内阴影会使用 {0}"
},
"components": {
"panel": "面板",
@@ -458,17 +462,18 @@
"topBar": "顶栏",
"avatar": "用户头像(在个人资料栏)",
"avatarStatus": "用户头像(在帖子显示栏)",
- "popup": "弹窗和工具提示",
+ "popup": "弹窗与工具提示",
"button": "按钮",
"buttonHover": "按钮(悬停)",
- "buttonPressed": "按钮(按下)",
- "buttonPressedHover": "按钮(按下和悬停)",
+ "buttonPressed": "按钮(压下)",
+ "buttonPressedHover": "按钮(压下和悬停)",
"input": "输入框"
- }
+ },
+ "hintV3": "对于阴影,您还可以使用 {0} 表示法来使用其它颜色插槽。"
},
"fonts": {
"_tab_label": "字体",
- "help": "给用户界面的元素选择字体。选择 “自选”的你必须输入确切的字体名称。",
+ "help": "为用户界面的元素选择字体。若选择 “自选”,您必须输入与系统显示完全一致的字体名称。",
"components": {
"interface": "界面",
"input": "输入框",
@@ -477,22 +482,22 @@
},
"family": "字体名称",
"size": "大小 (in px)",
- "weight": "字重 (粗体))",
+ "weight": "字重 (粗体)",
"custom": "自选"
},
"preview": {
"header": "预览",
"content": "内容",
- "error": "例子错误",
+ "error": "错误示例",
"button": "按钮",
"text": "有堆 {0} 和 {1}",
- "mono": "内容",
- "input": "刚刚抵达上海",
- "faint_link": "帮助菜单",
+ "mono": "monospace 内容",
+ "input": "刚刚抵达上海。",
+ "faint_link": "帮助手册",
"fine_print": "阅读我们的 {0} ,然而什么也学不到!",
"header_faint": "这很正常",
- "checkbox": "我已经浏览了 TOC",
- "link": "一个很棒的摇滚链接"
+ "checkbox": "我已经浏览了条款及细则",
+ "link": "一个棒棒的小小链接"
}
},
"version": {
@@ -503,7 +508,7 @@
"notification_setting_filters": "过滤器",
"domain_mutes": "域名",
"changed_email": "邮箱修改成功!",
- "change_email_error": "修改你的电子邮箱时发生错误",
+ "change_email_error": "修改您的电子邮箱时发生错误。",
"change_email": "修改电子邮箱",
"allow_following_move": "正在关注的账号迁移时自动重新关注",
"notification_setting_privacy_option": "在通知推送中隐藏发送者和内容",
@@ -516,13 +521,13 @@
"notification_setting_hide_notification_contents": "隐藏推送通知中的发送者与内容信息",
"notification_setting_block_from_strangers": "屏蔽来自你没有关注的用户的通知",
"type_domains_to_mute": "搜索需要隐藏的域名",
- "useStreamingApi": "实时接收发布以及通知",
+ "useStreamingApi": "实时接收帖子和通知",
"user_mutes": "用户",
- "reset_background_confirm": "您确定要重置个人资料背景图吗?",
+ "reset_background_confirm": "您确定要重置背景图吗?",
"reset_banner_confirm": "您确定要重置横幅图片吗?",
"reset_avatar_confirm": "您确定要重置头像吗?",
"reset_profile_banner": "重置横幅图片",
- "reset_profile_background": "重置个人资料背景图",
+ "reset_profile_background": "重置个人背景图",
"reset_avatar": "重置头像",
"hide_followers_count_description": "不显示关注者数量",
"profile_fields": {
@@ -537,9 +542,17 @@
"mutes_and_blocks": "隐藏与屏蔽",
"bot": "这是一个机器人账号",
"fun": "趣味",
- "useStreamingApiWarning": "(不推荐使用,试验性,已知会跳过一些消息)",
+ "useStreamingApiWarning": "(不推荐使用,试验性,已知会跳过一些帖子)",
"chatMessageRadius": "聊天消息",
- "greentext": "Meme 箭头"
+ "greentext": "Meme 箭头",
+ "virtual_scrolling": "优化时间线渲染",
+ "import_mutes_from_a_csv_file": "从 csv 文件导入隐藏名单",
+ "mutes_imported": "隐藏名单导入成功!处理它们将需要一段时间。",
+ "mute_import_error": "导入隐藏名单出错",
+ "mute_import": "隐藏名单导入",
+ "mute_export_button": "导出你的隐藏名单到一个 csv 文件",
+ "mute_export": "隐藏名单导出",
+ "hide_wallpaper": "隐藏实例壁纸"
},
"time": {
"day": "{0} 天",
@@ -580,16 +593,17 @@
"conversation": "对话",
"error_fetching": "获取更新时发生错误",
"load_older": "加载更早的状态",
- "no_retweet_hint": "这条内容仅关注者可见,或者是私信,因此不能转发。",
- "repeated": "已转发",
+ "no_retweet_hint": "这条内容仅关注者可见,或者是私信,因此不能转发",
+ "repeated": "转发了",
"show_new": "显示新内容",
"up_to_date": "已是最新",
"no_more_statuses": "没有更多的状态",
"no_statuses": "没有状态更新",
- "reload": "重新载入"
+ "reload": "重新载入",
+ "error": "取得时间轴时发生错误:{0}"
},
"status": {
- "favorites": "收藏",
+ "favorites": "喜欢",
"repeats": "转发",
"delete": "删除状态",
"pin": "在个人资料置顶",
@@ -609,24 +623,28 @@
"status_unavailable": "状态不可取得",
"unbookmark": "取消书签",
"bookmark": "书签",
- "thread_muted_and_words": ",含有过滤词:"
+ "thread_muted_and_words": ",含有过滤词:",
+ "status_deleted": "该状态已被删除",
+ "nsfw": "NSFW",
+ "external_source": "外部来源",
+ "expand": "展开"
},
"user_card": {
- "approve": "允许",
+ "approve": "核准",
"block": "屏蔽",
"blocked": "已屏蔽!",
"deny": "拒绝",
- "favorites": "收藏",
+ "favorites": "喜欢",
"follow": "关注",
"follow_sent": "请求已发送!",
- "follow_progress": "请求中",
+ "follow_progress": "请求中…",
"follow_again": "再次发送请求?",
"follow_unfollow": "取消关注",
"followees": "正在关注",
"followers": "关注者",
"following": "正在关注!",
"follows_you": "关注了你!",
- "its_you": "就是你!!",
+ "its_you": "就是你!",
"media": "媒体",
"mute": "隐藏",
"muted": "已隐藏",
@@ -636,18 +654,18 @@
"statuses": "状态",
"subscribe": "订阅",
"unsubscribe": "退订",
- "unblock": "取消拉黑",
- "unblock_progress": "取消拉黑中…",
- "block_progress": "拉黑中…",
+ "unblock": "取消屏蔽",
+ "unblock_progress": "正在取消屏蔽…",
+ "block_progress": "正在屏蔽…",
"unmute": "取消隐藏",
"unmute_progress": "取消隐藏中…",
"mute_progress": "隐藏中…",
"admin_menu": {
- "moderation": "权限",
+ "moderation": "仲裁",
"grant_admin": "赋予管理权限",
"revoke_admin": "撤销管理权限",
- "grant_moderator": "赋予版主权限",
- "revoke_moderator": "撤销版主权限",
+ "grant_moderator": "赋予监察员权限",
+ "revoke_moderator": "撤销监察员权限",
"activate_account": "激活账号",
"deactivate_account": "关闭账号",
"delete_account": "删除账号",
@@ -659,7 +677,7 @@
"disable_any_subscription": "完全禁止关注用户",
"quarantine": "从联合实例中禁止用户帖子",
"delete_user": "删除用户",
- "delete_user_confirmation": "你确认吗?此操作无法撤销。"
+ "delete_user_confirmation": "你确定吗?此操作无法撤销。"
},
"hidden": "已隐藏",
"show_repeats": "显示转发",
@@ -674,22 +692,22 @@
},
"user_reporting": {
"title": "报告 {0}",
- "add_comment_description": "此报告会发送给你的实例管理员。你可以在下面提供更多详细信息解释报告的缘由:",
+ "add_comment_description": "此报告会发送给您的实例监察员。您可以在下面提供更多详细信息解释报告的缘由:",
"additional_comments": "其它信息",
- "forward_description": "这个账号是从另外一个服务器。同时发送一个副本到那里?",
+ "forward_description": "这个账号来自另一个服务器。是否同时发送一份报告副本到那里?",
"forward_to": "转发 {0}",
"submit": "提交",
- "generic_error": "当处理你的请求时,发生了一个错误。"
+ "generic_error": "当处理您的请求时,发生了一个错误。"
},
"who_to_follow": {
"more": "更多",
"who_to_follow": "推荐关注"
},
"tool_tip": {
- "media_upload": "上传多媒体",
+ "media_upload": "上传媒体",
"repeat": "转发",
"reply": "回复",
- "favorite": "收藏",
+ "favorite": "喜欢",
"user_settings": "用户设置",
"reject_follow_request": "拒绝关注请求",
"add_reaction": "添加互动",
@@ -700,7 +718,8 @@
"error": {
"base": "上传不成功。",
"file_too_big": "文件太大了 [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
- "default": "迟些再试"
+ "default": "迟些再试",
+ "message": "上传错误:{0}"
},
"file_size_units": {
"B": "B",
@@ -720,12 +739,12 @@
"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": "您必须重置密码才能登陆。"
},
@@ -736,7 +755,7 @@
},
"emoji": {
"keep_open": "选择器保持打开",
- "stickers": "贴图",
+ "stickers": "贴纸",
"unicode": "Unicode 表情符号",
"custom": "自定义表情符号",
"add_emoji": "插入表情符号",
@@ -748,22 +767,22 @@
"about": {
"mrf": {
"simple": {
- "quarantine_desc": "本实例只会把公开状态发送非下列实例:",
+ "quarantine_desc": "本实例向以下实例仅发送公开的帖子:",
"quarantine": "隔离",
"reject_desc": "本实例不会接收来自下列实例的消息:",
"reject": "拒绝",
"accept_desc": "本实例只接收来自下列实例的消息:",
- "simple_policies": "站规",
+ "simple_policies": "对于特定实例的策略",
"accept": "接受",
"media_removal": "移除媒体",
- "media_nsfw_desc": "本实例将来自以下实例的媒体强制设置为敏感内容:",
+ "media_nsfw_desc": "本实例将来自以下实例的媒体内容强制设置为敏感内容:",
"media_nsfw": "强制设置媒体为敏感内容",
- "media_removal_desc": "本实例移除了来自以下实例的媒体内容:",
- "ftl_removal_desc": "该实例在从“全部已知网络”时间线上移除了:",
+ "media_removal_desc": "本实例移除来自以下实例的媒体内容:",
+ "ftl_removal_desc": "该实例在从“全部已知网络”时间线上移除了下列实例:",
"ftl_removal": "从“全部已知网络”时间线上移除"
},
"mrf_policies_desc": "MRF 策略会影响本实例的互通行为。以下策略已启用:",
- "mrf_policies": "已启动 MRF 策略",
+ "mrf_policies": "已启用的 MRF 策略",
"keyword": {
"ftl_removal": "从“全部已知网络”时间线上移除",
"keyword_policies": "关键词策略",
@@ -771,7 +790,7 @@
"replace": "替换",
"reject": "拒绝"
},
- "federation": "联邦"
+ "federation": "联邦互通"
},
"staff": "管理人员"
},
diff --git a/src/i18n/zh_Hant.json b/src/i18n/zh_Hant.json
index 79a992fc..a8d0dc3c 100644
--- a/src/i18n/zh_Hant.json
+++ b/src/i18n/zh_Hant.json
@@ -35,7 +35,8 @@
"follow_request": "想要關注你",
"followed_you": "關注了你",
"favorited_you": "喜歡了你的發文",
- "broken_favorite": "未知的狀態,正在搜索中…"
+ "broken_favorite": "未知的狀態,正在搜索中…",
+ "error": "獲取通知錯誤:{0}"
},
"nav": {
"chats": "聊天",
@@ -121,7 +122,8 @@
"media_proxy": "媒體代理",
"pleroma_chat_messages": "Pleroma 聊天",
"chat": "聊天",
- "gopher": "Gopher"
+ "gopher": "Gopher",
+ "upload_limit": "上傳限制"
},
"exporter": {
"processing": "正在處理,稍後會提示您下載文件",
@@ -351,7 +353,7 @@
"reset_avatar": "重置頭像",
"discoverable": "允許通過搜索檢索等服務找到此賬號",
"delete_account_error": "刪除賬戶時發生錯誤,如果一直刪除不了,請聯繫實例管理員。",
- "composing": "正在書寫",
+ "composing": "寫作設置",
"chatMessageRadius": "聊天訊息",
"mfa": {
"confirm_and_enable": "確認並啟用OTP",
@@ -524,7 +526,8 @@
"mute_import": "靜音導入",
"mute_import_error": "導入靜音時出錯",
"mute_export_button": "將靜音導出到csv文件",
- "mute_export": "靜音導出"
+ "mute_export": "靜音導出",
+ "hide_wallpaper": "隱藏實例桌布"
},
"chats": {
"more": "更多",
@@ -680,7 +683,7 @@
"fullname": "顯示名稱",
"bio_placeholder": "例如:\n你好,我是玲音。\n我是一個住在日本郊區的動畫少女。你可能在 Wired 見過我。",
"fullname_placeholder": "例如:岩倉玲音",
- "username_placeholder": "例如:玲音",
+ "username_placeholder": "例如:lain",
"new_captcha": "點擊圖片獲取新的驗證碼",
"captcha": "CAPTCHA",
"token": "邀請碼",
diff --git a/src/main.js b/src/main.js
index 0a898022..90ee2887 100644
--- 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
index 0a354c3f..08485a30 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -75,12 +75,18 @@ const api = {
} 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
index 21e30933..0a373d88 100644
--- a/src/modules/chats.js
+++ b/src/modules/chats.js
@@ -16,7 +16,8 @@ const defaultState = {
openedChats: {},
openedChatMessageServices: {},
fetcher: undefined,
- currentChatId: null
+ currentChatId: null,
+ lastReadMessageId: null
}
const getChatById = (state, id) => {
@@ -92,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)
@@ -106,6 +112,9 @@ const chats = {
},
clearOpenedChats ({ rootState, commit, dispatch, rootGetters }) {
commit('clearOpenedChats', { commit })
+ },
+ handleMessageError ({ commit }, value) {
+ commit('handleMessageError', { commit, ...value })
}
},
mutations: {
@@ -208,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
index cd7c8670..444808cf 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -20,6 +20,7 @@ export const defaultState = {
customTheme: undefined,
customThemeSource: undefined,
hideISP: false,
+ hideInstanceWallpaper: false,
// bad name: actually hides posts of muted USERS
hideMutedPosts: undefined, // instance default
collapseMessageWithSubject: undefined, // instance default
diff --git a/src/modules/instance.js b/src/modules/instance.js
index b3cbffc6..411b1caa 100644
--- 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',
@@ -126,7 +127,7 @@ const instance = {
imageUrl: false,
replacement: values[key]
}
- }).sort((a, b) => a.displayText - b.displayText)
+ }).sort((a, b) => a.name > b.name ? 1 : -1)
commit('setInstanceOption', { name: 'emoji', value: emoji })
} else {
throw (res)
@@ -153,7 +154,7 @@ const instance = {
}
// Technically could use tags but those are kinda useless right now,
// should have been "pack" field, that would be more useful
- }).sort((a, b) => a.displayText.toLowerCase() > b.displayText.toLowerCase() ? 1 : 0)
+ }).sort((a, b) => a.displayText.toLowerCase() > b.displayText.toLowerCase() ? 1 : -1)
commit('setInstanceOption', { name: 'customEmoji', value: emoji })
} else {
throw (res)
diff --git a/src/modules/reports.js b/src/modules/reports.js
index 904022f1..fea83e5f 100644
--- a/src/modules/reports.js
+++ b/src/modules/reports.js
@@ -4,12 +4,14 @@ const reports = {
state: {
userId: null,
statuses: [],
+ preTickedIds: [],
modalActivated: false
},
mutations: {
- openUserReportingModal (state, { userId, statuses }) {
+ openUserReportingModal (state, { userId, statuses, preTickedIds }) {
state.userId = userId
state.statuses = statuses
+ state.preTickedIds = preTickedIds
state.modalActivated = true
},
closeUserReportingModal (state) {
@@ -17,9 +19,15 @@ const reports = {
}
},
actions: {
- openUserReportingModal ({ rootState, commit }, userId) {
- const statuses = filter(rootState.statuses.allStatuses, status => status.user.id === userId)
- commit('openUserReportingModal', { userId, statuses })
+ openUserReportingModal ({ rootState, commit }, { userId, statusIds = [] }) {
+ const preTickedStatuses = statusIds.map(id => rootState.statuses.allStatusesObject[id])
+ const preTickedIds = statusIds
+ const statuses = preTickedStatuses.concat(
+ filter(rootState.statuses.allStatuses,
+ status => status.user.id === userId && !preTickedIds.includes(status.id)
+ )
+ )
+ commit('openUserReportingModal', { userId, statuses, preTickedIds })
},
closeUserReportingModal ({ commit }) {
commit('closeUserReportingModal')
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index e673141d..33c68c57 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -39,8 +39,7 @@ const emptyNotifications = () => ({
minId: Number.POSITIVE_INFINITY,
data: [],
idStore: {},
- loading: false,
- error: false
+ loading: false
})
export const defaultState = () => ({
@@ -50,8 +49,6 @@ export const defaultState = () => ({
maxId: 0,
notifications: emptyNotifications(),
favorites: new Set(),
- error: false,
- errorData: null,
timelines: {
mentions: emptyTl(),
public: emptyTl(),
@@ -462,18 +459,9 @@ export const mutations = {
const newStatus = state.allStatusesObject[id]
newStatus.nsfw = nsfw
},
- setError (state, { value }) {
- state.error = value
- },
- setErrorData (state, { value }) {
- state.errorData = value
- },
setNotificationsLoading (state, { value }) {
state.notifications.loading = value
},
- setNotificationsError (state, { value }) {
- state.notifications.error = value
- },
setNotificationsSilence (state, { value }) {
state.notifications.desktopNotificationSilence = value
},
@@ -588,18 +576,9 @@ const statuses = {
}
commit('addNewNotifications', { dispatch, notifications, older, rootGetters, newNotificationSideEffects })
},
- setError ({ rootState, commit }, { value }) {
- commit('setError', { value })
- },
- setErrorData ({ rootState, commit }, { value }) {
- commit('setErrorData', { value })
- },
setNotificationsLoading ({ rootState, commit }, { value }) {
commit('setNotificationsLoading', { value })
},
- setNotificationsError ({ rootState, commit }, { value }) {
- commit('setNotificationsError', { value })
- },
setNotificationsSilence ({ rootState, commit }, { value }) {
commit('setNotificationsSilence', { value })
},
diff --git a/src/modules/users.js b/src/modules/users.js
index 9245db5c..655db4c7 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -137,11 +137,11 @@ export const mutations = {
},
saveFriendIds (state, { id, friendIds }) {
const user = state.usersObject[id]
- user.friendIds = uniq(concat(user.friendIds, friendIds))
+ user.friendIds = uniq(concat(user.friendIds || [], friendIds))
},
saveFollowerIds (state, { id, followerIds }) {
const user = state.usersObject[id]
- user.followerIds = uniq(concat(user.followerIds, followerIds))
+ user.followerIds = uniq(concat(user.followerIds || [], followerIds))
},
// Because frontend doesn't have a reason to keep these stuff in memory
// outside of viewing someones user profile.
@@ -202,7 +202,9 @@ export const mutations = {
},
setPinnedToUser (state, status) {
const user = state.usersObject[status.user.id]
+ user.pinnedStatusIds = user.pinnedStatusIds || []
const index = user.pinnedStatusIds.indexOf(status.id)
+
if (status.pinned && index === -1) {
user.pinnedStatusIds.push(status.id)
} else if (!status.pinned && index !== -1) {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 1a3495d4..f4483149 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -129,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))
+ })
+ )
})
}
@@ -158,7 +162,12 @@ const updateProfileImages = ({ credentials, avatar = null, banner = null, backgr
body: form
})
.then((data) => data.json())
- .then((data) => parseUser(data))
+ .then((data) => {
+ if (data.error) {
+ throw new Error(data.error)
+ }
+ return parseUser(data)
+ })
}
const updateProfile = ({ credentials, params }) => {
@@ -556,7 +565,7 @@ const fetchTimeline = ({
})
.then((data) => data.json())
.then((data) => {
- if (!data.error) {
+ if (!data.errors) {
return { data: data.map(isNotifications ? parseNotification : parseStatus), pagination }
} else {
data.status = status
@@ -1210,7 +1219,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
}
@@ -1219,11 +1228,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
})
}
diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js
index 95c69482..e653ebc1 100644
--- 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.idempotency_key]
+ }
+ }
+
+ 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
index 583438f7..de6e0625 100644
--- 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
index 1884478a..6ed663e1 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -1,6 +1,16 @@
import escape from 'escape-html'
import parseLinkHeader from 'parse-link-header'
import { isStatusNotification } from '../notification_utils/notification_utils.js'
+import punycode from 'punycode.js'
+
+/** NOTICE! **
+ * Do not initialize UI-generated data here.
+ * It will override existing data.
+ *
+ * i.e. user.pinnedStatusIds was set to [] here
+ * UI code would update it with data but upon next user fetch
+ * it would be reverted back to []
+ */
const qvitterStatusType = (status) => {
if (status.is_post_verb) {
@@ -53,7 +63,7 @@ export const parseUser = (data) => {
output.fields = data.fields
output.fields_html = data.fields.map(field => {
return {
- name: addEmojis(field.name, data.emojis),
+ name: addEmojis(escape(field.name), data.emojis),
value: addEmojis(field.value, data.emojis)
}
})
@@ -173,15 +183,17 @@ export const parseUser = (data) => {
output.locked = data.locked
output.followers_count = data.followers_count
output.statuses_count = data.statuses_count
- output.friendIds = []
- output.followerIds = []
- output.pinnedStatusIds = []
if (data.pleroma) {
output.follow_request_count = data.pleroma.follow_request_count
output.tags = data.pleroma.tags
- output.deactivated = data.pleroma.deactivated
+
+ // deactivated was changed to is_active in Pleroma 2.3.0
+ // so check if is_active is present
+ output.deactivated = typeof data.pleroma.is_active !== 'undefined'
+ ? !data.pleroma.is_active // new backend
+ : data.pleroma.deactivated // old backend
output.notification_settings = data.pleroma.notification_settings
output.unread_chat_count = data.pleroma.unread_chat_count
@@ -191,6 +203,18 @@ export const parseUser = (data) => {
output.rights = output.rights || {}
output.notification_settings = output.notification_settings || {}
+ // Convert punycode to unicode
+ if (output.screen_name.includes('@')) {
+ const parts = output.screen_name.split('@')
+ let unicodeDomain = punycode.toUnicode(parts[1])
+ if (unicodeDomain !== parts[1]) {
+ // Add some identifier so users can potentially spot spoofing attempts:
+ // lain.com and xn--lin-6cd.com would appear identical otherwise.
+ unicodeDomain = '🌏' + unicodeDomain
+ output.screen_name = [parts[0], unicodeDomain].join('@')
+ }
+ }
+
return output
}
@@ -274,7 +298,7 @@ export const parseStatus = (data) => {
if (output.poll) {
output.poll.options = (output.poll.options || []).map(field => ({
...field,
- title_html: addEmojis(field.title, data.emojis)
+ title_html: addEmojis(escape(field.title), data.emojis)
}))
}
output.pinned = data.pinned
@@ -429,6 +453,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/favicon_service/favicon_service.js b/src/services/favicon_service/favicon_service.js
new file mode 100644
index 00000000..d1ddee41
--- /dev/null
+++ b/src/services/favicon_service/favicon_service.js
@@ -0,0 +1,61 @@
+import { find } from 'lodash'
+
+const createFaviconService = () => {
+ let favimg, favcanvas, favcontext, favicon
+ const faviconWidth = 128
+ const faviconHeight = 128
+ const badgeRadius = 32
+
+ const initFaviconService = () => {
+ const nodes = document.getElementsByTagName('link')
+ favicon = find(nodes, node => node.rel === 'icon')
+ if (favicon) {
+ favcanvas = document.createElement('canvas')
+ favcanvas.width = faviconWidth
+ favcanvas.height = faviconHeight
+ favimg = new Image()
+ favimg.src = favicon.href
+ favcontext = favcanvas.getContext('2d')
+ }
+ }
+
+ const isImageLoaded = (img) => img.complete && img.naturalHeight !== 0
+
+ const clearFaviconBadge = () => {
+ if (!favimg || !favcontext || !favicon) return
+
+ favcontext.clearRect(0, 0, faviconWidth, faviconHeight)
+ if (isImageLoaded(favimg)) {
+ favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
+ }
+ favicon.href = favcanvas.toDataURL('image/png')
+ }
+
+ const drawFaviconBadge = () => {
+ if (!favimg || !favcontext || !favcontext) return
+
+ clearFaviconBadge()
+
+ const style = getComputedStyle(document.body)
+ const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}`
+
+ if (isImageLoaded(favimg)) {
+ favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
+ }
+ favcontext.fillStyle = badgeColor
+ favcontext.beginPath()
+ favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false)
+ favcontext.fill()
+ favicon.href = favcanvas.toDataURL('image/png')
+ }
+
+ return {
+ initFaviconService,
+ clearFaviconBadge,
+ drawFaviconBadge
+ }
+}
+
+const FaviconService = createFaviconService()
+
+export default FaviconService
diff --git a/src/services/locale/locale.service.js b/src/services/locale/locale.service.js
new file mode 100644
index 00000000..5be99d81
--- /dev/null
+++ b/src/services/locale/locale.service.js
@@ -0,0 +1,12 @@
+const specialLanguageCodes = {
+ 'ja_easy': 'ja',
+ 'zh_Hant': 'zh-HANT'
+}
+
+const internalToBrowserLocale = code => specialLanguageCodes[code] || code
+
+const localeService = {
+ internalToBrowserLocale
+}
+
+export default localeService
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index c908b644..beeb167c 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -2,7 +2,6 @@ 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 })
store.dispatch('addNewNotifications', { notifications, older })
}
@@ -47,11 +46,22 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
const fetchNotifications = ({ store, args, older }) => {
return apiService.fetchTimeline(args)
- .then(({ data: notifications }) => {
+ .then((response) => {
+ if (response.errors) {
+ throw new Error(`${response.status} ${response.statusText}`)
+ }
+ const notifications = response.data
update({ store, notifications, older })
return notifications
- }, () => store.dispatch('setNotificationsError', { value: true }))
- .catch(() => store.dispatch('setNotificationsError', { value: true }))
+ })
+ .catch((error) => {
+ store.dispatch('pushGlobalNotice', {
+ level: 'error',
+ messageKey: 'notifications.error',
+ messageArgs: [error.message],
+ timeout: 5000
+ })
+ })
}
const startFetching = ({ credentials, store }) => {
diff --git a/src/services/promise_interval/promise_interval.js b/src/services/promise_interval/promise_interval.js
index cf17970d..0c0a66a0 100644
--- a/src/services/promise_interval/promise_interval.js
+++ b/src/services/promise_interval/promise_interval.js
@@ -10,7 +10,14 @@ export const promiseInterval = (promiseCall, interval) => {
let timeout = null
const func = () => {
- promiseCall().finally(() => {
+ 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)
})
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 07425abd..a2bba67b 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -242,9 +242,18 @@ export const generateShadows = (input, colors) => {
panelHeader: 'panel',
input: 'input'
}
- const inputShadows = input.shadows && !input.themeEngineVersion
- ? shadows2to3(input.shadows, input.opacity)
- : input.shadows || {}
+
+ const cleanInputShadows = Object.fromEntries(
+ Object.entries(input.shadows || {})
+ .map(([name, shadowSlot]) => [
+ name,
+ // defaulting color to black to avoid potential problems
+ shadowSlot.map(shadowDef => ({ color: '#000000', ...shadowDef }))
+ ])
+ )
+ const inputShadows = cleanInputShadows && !input.themeEngineVersion
+ ? shadows2to3(cleanInputShadows, input.opacity)
+ : cleanInputShadows || {}
const shadows = Object.entries({
...DEFAULT_SHADOWS,
...inputShadows
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index 7ed85797..bec1eebd 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -84,6 +84,10 @@ export const SLOT_INHERITANCE = {
opacity: 'bg',
priority: 1
},
+ wallpaper: {
+ depends: ['bg'],
+ color: (mod, bg) => brightness(-2 * mod, bg).rgb
+ },
fg: {
depends: [],
priority: 1
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index 72ea4890..921df3ed 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -6,9 +6,6 @@ import { promiseInterval } from '../promise_interval/promise_interval.js'
const update = ({ store, statuses, timeline, showImmediately, userId, pagination }) => {
const ccTimeline = camelCase(timeline)
- store.dispatch('setError', { value: false })
- store.dispatch('setErrorData', { value: null })
-
store.dispatch('addNewStatuses', {
timeline: ccTimeline,
userId,
@@ -52,9 +49,8 @@ const fetchAndUpdate = ({
return apiService.fetchTimeline(args)
.then(response => {
- if (response.error) {
- store.dispatch('setErrorData', { value: response })
- return
+ if (response.errors) {
+ throw new Error(`${response.status} ${response.statusText}`)
}
const { data: statuses, pagination } = response
@@ -63,7 +59,15 @@ const fetchAndUpdate = ({
}
update({ store, statuses, timeline, showImmediately, userId, pagination })
return { statuses, pagination }
- }, () => store.dispatch('setError', { value: true }))
+ })
+ .catch((error) => {
+ store.dispatch('pushGlobalNotice', {
+ level: 'error',
+ messageKey: 'timeline.error',
+ messageArgs: [error.message],
+ timeout: 5000
+ })
+ })
}
const startFetching = ({ timeline = 'friends', credentials, store, userId = false, tag = false }) => {