aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorShpuld Shpludson <shp@cock.li>2020-11-06 17:27:25 +0000
committerShpuld Shpludson <shp@cock.li>2020-11-06 17:27:25 +0000
commit23232e1c8f35d7ddc8adb7a6dbf813b2831c90ec (patch)
treeb0c8c409d3af0901cb47de648ccbea53da89c16d /src
parentb225c3578f3c89af5ed3a0be3f8f3a6bbcedcc7d (diff)
parent60983ae42b584694de0211ca67ef72d492a293c9 (diff)
Merge branch 'develop' into 'master'
Merge develop into master for 2.2.0 See merge request pleroma/pleroma-fe!1278
Diffstat (limited to 'src')
-rw-r--r--src/App.js51
-rw-r--r--src/App.scss180
-rw-r--r--src/App.vue69
-rw-r--r--src/boot/after_store.js1
-rw-r--r--src/components/account_actions/account_actions.js8
-rw-r--r--src/components/account_actions/account_actions.vue35
-rw-r--r--src/components/attachment/attachment.js22
-rw-r--r--src/components/attachment/attachment.vue22
-rw-r--r--src/components/chat/chat.js116
-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_item/chat_list_item.vue12
-rw-r--r--src/components/chat_message/chat_message.js10
-rw-r--r--src/components/chat_message/chat_message.scss15
-rw-r--r--src/components/chat_message/chat_message.vue7
-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.js20
-rw-r--r--src/components/conversation/conversation.vue11
-rw-r--r--src/components/desktop_nav/desktop_nav.js89
-rw-r--r--src/components/desktop_nav/desktop_nav.scss112
-rw-r--r--src/components/desktop_nav/desktop_nav.vue79
-rw-r--r--src/components/emoji_input/emoji_input.js9
-rw-r--r--src/components/emoji_input/emoji_input.vue2
-rw-r--r--src/components/emoji_picker/emoji_picker.js31
-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/exporter/exporter.js7
-rw-r--r--src/components/exporter/exporter.vue8
-rw-r--r--src/components/extra_buttons/extra_buttons.js20
-rw-r--r--src/components/extra_buttons/extra_buttons.vue53
-rw-r--r--src/components/favorite_button/favorite_button.js14
-rw-r--r--src/components/favorite_button/favorite_button.vue31
-rw-r--r--src/components/follow_requests/follow_requests.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.vue5
-rw-r--r--src/components/image_cropper/image_cropper.js10
-rw-r--r--src/components/image_cropper/image_cropper.vue10
-rw-r--r--src/components/importer/importer.js11
-rw-r--r--src/components/importer/importer.vue14
-rw-r--r--src/components/interface_language_switcher/interface_language_switcher.vue13
-rw-r--r--src/components/login_form/login_form.js8
-rw-r--r--src/components/login_form/login_form.vue5
-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.vue21
-rw-r--r--src/components/mfa_form/recovery_form.js8
-rw-r--r--src/components/mfa_form/recovery_form.vue5
-rw-r--r--src/components/mfa_form/totp_form.js9
-rw-r--r--src/components/mfa_form/totp_form.vue6
-rw-r--r--src/components/mobile_nav/mobile_nav.js12
-rw-r--r--src/components/mobile_nav/mobile_nav.vue282
-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.vue4
-rw-r--r--src/components/moderation_tools/moderation_tools.vue2
-rw-r--r--src/components/nav_panel/nav_panel.js23
-rw-r--r--src/components/nav_panel/nav_panel.vue168
-rw-r--r--src/components/notification/notification.js22
-rw-r--r--src/components/notification/notification.scss32
-rw-r--r--src/components/notification/notification.vue51
-rw-r--r--src/components/notifications/notifications.js6
-rw-r--r--src/components/notifications/notifications.scss31
-rw-r--r--src/components/notifications/notifications.vue6
-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.vue6
-rw-r--r--src/components/poll/poll_form.js12
-rw-r--r--src/components/poll/poll_form.vue35
-rw-r--r--src/components/popover/popover.vue6
-rw-r--r--src/components/post_status_form/post_status_form.js28
-rw-r--r--src/components/post_status_form/post_status_form.vue114
-rw-r--r--src/components/react_button/react_button.js23
-rw-r--r--src/components/react_button/react_button.vue6
-rw-r--r--src/components/reply_button/reply_button.js16
-rw-r--r--src/components/reply_button/reply_button.vue39
-rw-r--r--src/components/retweet_button/retweet_button.js13
-rw-r--r--src/components/retweet_button/retweet_button.vue37
-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.vue8
-rw-r--r--src/components/search_bar/search_bar.js14
-rw-r--r--src/components/search_bar/search_bar.vue94
-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/data_import_export_tab.js28
-rw-r--r--src/components/settings_modal/tabs/data_import_export_tab.vue17
-rw-r--r--src/components/settings_modal/tabs/filtering_tab.js8
-rw-r--r--src/components/settings_modal/tabs/filtering_tab.vue5
-rw-r--r--src/components/settings_modal/tabs/general_tab.js10
-rw-r--r--src/components/settings_modal/tabs/general_tab.vue17
-rw-r--r--src/components/settings_modal/tabs/profile_tab.js12
-rw-r--r--src/components/settings_modal/tabs/profile_tab.scss6
-rw-r--r--src/components/settings_modal/tabs/profile_tab.vue44
-rw-r--r--src/components/settings_modal/tabs/theme_tab/preview.vue43
-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.vue10
-rw-r--r--src/components/shadow_control/shadow_control.js14
-rw-r--r--src/components/shadow_control/shadow_control.vue25
-rw-r--r--src/components/side_drawer/side_drawer.js28
-rw-r--r--src/components/side_drawer/side_drawer.vue88
-rw-r--r--src/components/status/status.js78
-rw-r--r--src/components/status/status.scss62
-rw-r--r--src/components/status/status.vue129
-rw-r--r--src/components/status_content/status_content.js18
-rw-r--r--src/components/status_content/status_content.vue30
-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.js10
-rw-r--r--src/components/still-image/still-image.vue11
-rw-r--r--src/components/tab_switcher/tab_switcher.js3
-rw-r--r--src/components/tab_switcher/tab_switcher.scss4
-rw-r--r--src/components/timeline/timeline.js87
-rw-r--r--src/components/timeline/timeline.vue20
-rw-r--r--src/components/timeline_menu/timeline_menu.js18
-rw-r--r--src/components/timeline_menu/timeline_menu.vue61
-rw-r--r--src/components/user_avatar/user_avatar.vue22
-rw-r--r--src/components/user_card/user_card.js16
-rw-r--r--src/components/user_card/user_card.vue80
-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.vue9
-rw-r--r--src/components/video_attachment/video_attachment.js47
-rw-r--r--src/components/video_attachment/video_attachment.vue3
-rw-r--r--src/hocs/with_load_more/with_load_more.js12
-rw-r--r--src/hocs/with_subscription/with_subscription.js12
-rw-r--r--src/i18n/ca.json190
-rw-r--r--src/i18n/en.json10
-rw-r--r--src/i18n/eo.json79
-rw-r--r--src/i18n/es.json11
-rw-r--r--src/i18n/eu.json63
-rw-r--r--src/i18n/fa.json155
-rw-r--r--src/i18n/fi.json3
-rw-r--r--src/i18n/fr.json43
-rw-r--r--src/i18n/it.json20
-rw-r--r--src/i18n/pl.json59
-rw-r--r--src/i18n/ru.json5
-rw-r--r--src/i18n/zh.json265
-rw-r--r--src/i18n/zh_Hant.json810
-rw-r--r--src/main.js5
-rw-r--r--src/modules/api.js22
-rw-r--r--src/modules/chats.js33
-rw-r--r--src/modules/config.js3
-rw-r--r--src/modules/instance.js4
-rw-r--r--src/modules/statuses.js10
-rw-r--r--src/services/api/api.service.js32
-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.js3
-rw-r--r--src/services/follow_request_fetcher/follow_request_fetcher.service.js5
-rw-r--r--src/services/notifications_fetcher/notifications_fetcher.service.js8
-rw-r--r--src/services/promise_interval/promise_interval.js34
-rw-r--r--src/services/timeline_fetcher/timeline_fetcher.service.js6
163 files changed, 4412 insertions, 1277 deletions
diff --git a/src/App.js b/src/App.js
index ded772fa..52700319 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,6 +10,7 @@ import MediaModal from './components/media_modal/media_modal.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue'
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
import MobileNav from './components/mobile_nav/mobile_nav.vue'
+import DesktopNav from './components/desktop_nav/desktop_nav.vue'
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
@@ -22,7 +22,6 @@ export default {
UserPanel,
NavPanel,
Notifications,
- SearchBar,
InstanceSpecificPanel,
FeaturesPanel,
WhoToFollowPanel,
@@ -31,21 +30,14 @@ export default {
SideDrawer,
MobilePostStatusButton,
MobileNav,
+ DesktopNav,
SettingsModal,
UserReportingModal,
PostStatusModal,
GlobalNoticeList
},
data: () => ({
- mobileActivePanel: 'timeline',
- searchBarHidden: true,
- supportsMask: window.CSS && window.CSS.supports && (
- window.CSS.supports('mask-size', 'contain') ||
- window.CSS.supports('-webkit-mask-size', 'contain') ||
- window.CSS.supports('-moz-mask-size', 'contain') ||
- window.CSS.supports('-ms-mask-size', 'contain') ||
- window.CSS.supports('-o-mask-size', 'contain')
- )
+ mobileActivePanel: 'timeline'
}),
created () {
// Load the locale from the storage
@@ -61,28 +53,6 @@ export default {
background () {
return this.currentUser.background_image || this.$store.state.instance.background
},
- enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
- logoStyle () {
- return {
- 'visibility': this.enableMask ? 'hidden' : 'visible'
- }
- },
- logoMaskStyle () {
- return this.enableMask ? {
- 'mask-image': `url(${this.$store.state.instance.logo})`
- } : {
- 'background-color': this.enableMask ? '' : 'transparent'
- }
- },
- logoBgStyle () {
- return Object.assign({
- 'margin': `${this.$store.state.instance.logoMargin} 0`,
- opacity: this.searchBarHidden ? 1 : 0
- }, this.enableMask ? {} : {
- 'background-color': this.enableMask ? '' : 'transparent'
- })
- },
- logo () { return this.$store.state.instance.logo },
bgStyle () {
return {
'background-image': `url(${this.background})`
@@ -93,9 +63,7 @@ export default {
'--body-background-image': `url(${this.background})`
}
},
- sitename () { return this.$store.state.instance.name },
chat () { return this.$store.state.chat.channel.state === 'joined' },
- hideSitename () { return this.$store.state.instance.hideSitename },
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
showInstanceSpecificPanel () {
return this.$store.state.instance.showInstanceSpecificPanel &&
@@ -112,19 +80,6 @@ export default {
}
},
methods: {
- scrollToTop () {
- window.scrollTo(0, 0)
- },
- logout () {
- this.$router.replace('/main/public')
- this.$store.dispatch('logout')
- },
- onSearchBarToggled (hidden) {
- this.searchBarHidden = hidden
- },
- openSettingsModal () {
- this.$store.dispatch('openSettingsModal')
- },
updateMobileState () {
const mobileLayout = windowWidth() <= 800
const layoutHeight = windowHeight()
diff --git a/src/App.scss b/src/App.scss
index e2e2d079..ca7d33cd 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -85,7 +85,7 @@ button {
font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif);
- i[class*=icon-] {
+ i[class*=icon-], .svg-inline--fa {
color: $fallback--text;
color: var(--btnText, $fallback--text);
}
@@ -106,7 +106,8 @@ button {
color: var(--btnPressedText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--btnPressed, $fallback--fg);
- i {
+
+ svg, i {
color: $fallback--text;
color: var(--btnPressedText, $fallback--text);
}
@@ -118,7 +119,8 @@ button {
color: var(--btnDisabledText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--btnDisabled, $fallback--fg);
- i {
+
+ svg, i {
color: $fallback--text;
color: var(--btnDisabledText, $fallback--text);
}
@@ -131,7 +133,8 @@ button {
background-color: var(--btnToggled, $fallback--fg);
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow);
- i {
+
+ svg, i {
color: $fallback--text;
color: var(--btnToggledText, $fallback--text);
}
@@ -185,7 +188,7 @@ input, textarea, .select, .input {
opacity: 0.5;
}
- .icon-down-open {
+ .select-down-icon {
position: absolute;
top: 0;
bottom: 0;
@@ -279,7 +282,7 @@ input, textarea, .select, .input {
+ label::before {
flex-shrink: 0;
display: inline-block;
- content: '✔';
+ content: '✓';
transition: color 200ms;
width: 1.1em;
height: 1.1em;
@@ -318,7 +321,7 @@ option {
}
}
-i[class*=icon-] {
+i[class*=icon-], .svg-inline--fa {
color: $fallback--icon;
color: var(--icon, $fallback--icon);
}
@@ -356,117 +359,10 @@ i[class*=icon-] {
padding: 0 10px 0 10px;
}
-.item {
- flex: 1;
- line-height: 50px;
- height: 50px;
- overflow: hidden;
- display: flex;
- flex-wrap: wrap;
-
- .nav-icon {
- margin-left: 0.4em;
- }
-
- &.right {
- justify-content: flex-end;
- }
-}
-
.auto-size {
flex: 1
}
-.nav-bar {
- padding: 0;
- width: 100%;
- align-items: center;
- position: fixed;
- height: 50px;
- box-sizing: border-box;
-
- button {
- &, i[class*=icon-] {
- color: $fallback--text;
- color: var(--btnTopBarText, $fallback--text);
- }
-
- &:active {
- background-color: $fallback--fg;
- background-color: var(--btnPressedTopBar, $fallback--fg);
- color: $fallback--text;
- color: var(--btnPressedTopBarText, $fallback--text);
- }
-
- &:disabled {
- color: $fallback--text;
- color: var(--btnDisabledTopBarText, $fallback--text);
- }
-
- &.toggled {
- color: $fallback--text;
- color: var(--btnToggledTopBarText, $fallback--text);
- background-color: $fallback--fg;
- background-color: var(--btnToggledTopBar, $fallback--fg)
- }
- }
-
-
- .logo {
- display: flex;
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
-
- align-items: stretch;
- justify-content: center;
- flex: 0 0 auto;
- z-index: -1;
- transition: opacity;
- transition-timing-function: ease-out;
- transition-duration: 100ms;
-
- .mask {
- mask-repeat: no-repeat;
- mask-position: center;
- mask-size: contain;
- background-color: $fallback--fg;
- background-color: var(--topBarText, $fallback--fg);
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- }
-
- img {
- height: 100%;
- object-fit: contain;
- display: block;
- flex: 0;
- }
- }
-
- .inner-nav {
- position: relative;
- margin: auto;
- box-sizing: border-box;
- padding-left: 10px;
- padding-right: 10px;
- display: flex;
- align-items: center;
- flex-basis: 970px;
- height: 50px;
-
- a, a i {
- color: $fallback--link;
- color: var(--topBarLink, $fallback--link);
- }
- }
-}
-
main-router {
flex: 1;
}
@@ -707,19 +603,24 @@ nav {
flex-grow: 0;
}
}
+
.badge {
+ box-sizing: border-box;
display: inline-block;
border-radius: 99px;
- min-width: 22px;
- max-width: 22px;
- min-height: 22px;
- max-height: 22px;
- font-size: 15px;
- line-height: 22px;
- text-align: center;
+ max-width: 10em;
+ min-width: 1.7em;
+ height: 1.3em;
+ padding: 0.15em 0.15em;
vertical-align: middle;
+ font-weight: normal;
+ font-style: normal;
+ font-size: 0.9em;
+ line-height: 1;
+ text-align: center;
white-space: nowrap;
- padding: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
&.badge-notification {
background-color: $fallback--cRed;
@@ -776,16 +677,6 @@ nav {
}
}
-@media all and (min-width: 800px) {
- .logo {
- opacity: 1 !important;
- }
-}
-
-.item.right {
- text-align: right;
-}
-
.visibility-notice {
padding: .5em;
border: 1px solid $fallback--faint;
@@ -807,8 +698,16 @@ nav {
}
}
-.button-icon {
- font-size: 1.2em;
+.fa-scale-110 {
+ &.svg-inline--fa {
+ font-size: 1.1em;
+ }
+}
+
+.fa-old-padding {
+ &.svg-inline--fa {
+ padding: 0 0.3em;
+ }
}
@keyframes shakeError {
@@ -930,19 +829,6 @@ nav {
background-color: var(--panel, $fallback--fg);
}
-.unread-chat-count {
- font-size: 0.9em;
- font-weight: bolder;
- font-style: normal;
- position: absolute;
- right: 0.6rem;
- padding: 0 0.3em;
- min-width: 1.3rem;
- min-height: 1.3rem;
- max-height: 1.3rem;
- line-height: 1.3rem;
-}
-
.chat-layout {
// Needed for smoother chat navigation in the desktop Safari (otherwise the chat layout "jumps" as the chat opens).
overflow: hidden;
diff --git a/src/App.vue b/src/App.vue
index 0276c6a6..b4eb0524 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -9,74 +9,7 @@
:style="bgStyle"
/>
<MobileNav v-if="isMobileLayout" />
- <nav
- v-else
- id="nav"
- class="nav-bar container"
- @click="scrollToTop()"
- >
- <div class="inner-nav">
- <div
- class="logo"
- :style="logoBgStyle"
- >
- <div
- class="mask"
- :style="logoMaskStyle"
- />
- <img
- :src="logo"
- :style="logoStyle"
- >
- </div>
- <div class="item">
- <router-link
- v-if="!hideSitename"
- class="site-name"
- :to="{ name: 'root' }"
- active-class="home"
- >
- {{ sitename }}
- </router-link>
- </div>
- <div class="item right">
- <search-bar
- v-if="currentUser || !privateMode"
- class="nav-icon mobile-hidden"
- @toggled="onSearchBarToggled"
- @click.stop.native
- />
- <a
- href="#"
- class="mobile-hidden"
- @click.stop="openSettingsModal"
- >
- <i
- class="button-icon icon-cog nav-icon"
- :title="$t('nav.preferences')"
- />
- </a>
- <a
- v-if="currentUser && currentUser.role === 'admin'"
- href="/pleroma/admin/#/login-pleroma"
- class="mobile-hidden"
- target="_blank"
- ><i
- class="button-icon icon-gauge nav-icon"
- :title="$t('nav.administration')"
- /></a>
- <a
- v-if="currentUser"
- href="#"
- class="mobile-hidden"
- @click.prevent="logout"
- ><i
- class="button-icon icon-logout nav-icon"
- :title="$t('login.logout')"
- /></a>
- </div>
- </div>
- </nav>
+ <DesktopNav v-else />
<div class="app-bg-wrapper app-container-wrapper" />
<div
id="content"
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 00ca74a2..3cbbf020 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -130,6 +130,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
? 0
: config.logoMargin
})
+ copyInstanceOption('logoLeft')
store.commit('authFlow/setInitialStrategy', config.loginMethod)
copyInstanceOption('redirectRootNoLogin')
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js
index 6d345bc7..395d6685 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: [
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index 987e94b7..e3ae376e 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -1,5 +1,5 @@
<template>
- <div class="account-actions">
+ <div class="AccountActions">
<Popover
trigger="click"
placement="bottom"
@@ -63,7 +63,10 @@
slot="trigger"
class="btn btn-default ellipsis-button"
>
- <i class="icon-ellipsis trigger-button" />
+ <FAIcon
+ class="icon"
+ icon="ellipsis-v"
+ />
</div>
</Popover>
</div>
@@ -73,22 +76,22 @@
<style lang="scss">
@import '../../_variables.scss';
-.account-actions {
- margin: 0 .8em;
-}
+.AccountActions {
+ button.dropdown-item {
+ margin-left: 0;
+ }
-.account-actions button.dropdown-item {
- margin-left: 0;
-}
+ .ellipsis-button {
+ cursor: pointer;
+ width: 2.5em;
+ margin: -0.5em 0;
+ padding: 0.5em 0;
+ text-align: center;
-.account-actions .trigger-button {
- color: $fallback--lightText;
- color: var(--lightText, $fallback--lightText);
- opacity: .8;
- cursor: pointer;
- &:hover {
- color: $fallback--text;
- color: var(--text, $fallback--text);
+ &:not(:hover) .icon {
+ color: $fallback--lightText;
+ color: var(--lightText, $fallback--lightText);
+ }
}
}
</style>
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index cb31020d..e23fcb1b 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -3,6 +3,20 @@ import VideoAttachment from '../video_attachment/video_attachment.vue'
import nsfwImage from '../../assets/nsfw.png'
import fileTypeService from '../../services/file_type/file_type.service.js'
import { mapGetters } from 'vuex'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faFile,
+ faMusic,
+ faImage,
+ faVideo
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faFile,
+ faMusic,
+ faImage,
+ faVideo
+)
const Attachment = {
props: [
@@ -39,10 +53,10 @@ const Attachment = {
return this.attachment.description
},
placeholderIconClass () {
- if (this.type === 'image') return 'icon-picture'
- if (this.type === 'video') return 'icon-video'
- if (this.type === 'audio') return 'icon-music'
- return 'icon-doc'
+ if (this.type === 'image') return 'image'
+ if (this.type === 'video') return 'video'
+ if (this.type === 'audio') return 'music'
+ return 'file'
},
referrerpolicy () {
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index 63e0ceba..f1fac2c8 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>
@@ -28,7 +28,7 @@
:href="attachment.url"
:alt="attachment.description"
:title="attachment.description"
- @click.prevent="toggleHidden"
+ @click.prevent.stop="toggleHidden"
>
<img
:key="nsfwImage"
@@ -36,9 +36,10 @@
:src="nsfwImage"
:class="{'small': isSmall}"
>
- <i
+ <FAIcon
v-if="type === 'video'"
- class="play-icon icon-play-circled"
+ class="play-icon"
+ icon="play-circle"
/>
</a>
<div
@@ -80,10 +81,13 @@
class="video"
:attachment="attachment"
:controls="allowPlay"
+ @play="$emit('play')"
+ @pause="$emit('pause')"
/>
- <i
+ <FAIcon
v-if="!allowPlay"
- class="play-icon icon-play-circled"
+ class="play-icon"
+ icon="play-circle"
/>
</a>
@@ -93,6 +97,8 @@
:alt="attachment.description"
:title="attachment.description"
controls
+ @play="$emit('play')"
+ @pause="$emit('pause')"
/>
<div
@@ -138,6 +144,10 @@
white-space: nowrap;
text-overflow: ellipsis;
max-width: 100%;
+
+ svg {
+ color: inherit;
+ }
}
.nsfw-placeholder {
diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js
index 803abf69..e57fcb91 100644
--- a/src/components/chat/chat.js
+++ b/src/components/chat/chat.js
@@ -5,11 +5,25 @@ import ChatMessage from '../chat_message/chat_message.vue'
import PostStatusForm from '../post_status_form/post_status_form.vue'
import ChatTitle from '../chat_title/chat_title.vue'
import chatService from '../../services/chat_service/chat_service.js'
-import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight } from './chat_layout_utils.js'
+import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
+import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight, isScrollable } from './chat_layout_utils.js'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faChevronDown,
+ faChevronLeft
+} from '@fortawesome/free-solid-svg-icons'
+import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
+
+library.add(
+ faChevronDown,
+ faChevronLeft
+)
const BOTTOMED_OUT_OFFSET = 10
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150
const SAFE_RESIZE_TIME_OFFSET = 100
+const MARK_AS_READ_DELAY = 1500
+const MAX_RETRIES = 10
const Chat = {
components: {
@@ -23,7 +37,8 @@ const Chat = {
hoveredMessageChainId: undefined,
lastScrollPosition: {},
scrollableContainerHeight: '100%',
- errorLoadingChat: false
+ errorLoadingChat: false,
+ messageRetriers: {}
}
},
created () {
@@ -93,7 +108,7 @@ const Chat = {
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
this.$nextTick(() => {
if (bottomedOutBeforeUpdate) {
- this.scrollDown({ forceRead: !document.hidden })
+ this.scrollDown()
}
})
},
@@ -199,7 +214,7 @@ const Chat = {
this.$nextTick(() => {
scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior })
})
- if (forceRead || this.newMessageCount > 0) {
+ if (forceRead) {
this.readChat()
}
},
@@ -207,7 +222,10 @@ const Chat = {
if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return }
if (document.hidden) { return }
const lastReadId = this.currentChatMessageService.maxId
- this.$store.dispatch('readChat', { id: this.currentChat.id, lastReadId })
+ this.$store.dispatch('readChat', {
+ id: this.currentChat.id,
+ lastReadId
+ })
},
bottomedOut (offset) {
return isBottomedOut(this.$refs.scrollable, offset)
@@ -224,12 +242,18 @@ const Chat = {
} else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
this.jumpToBottomButtonVisible = false
if (this.newMessageCount > 0) {
- this.readChat()
+ // Use a delay before marking as read to prevent situation where new messages
+ // arrive just as you're leaving the view and messages that you didn't actually
+ // get to see get marked as read.
+ window.setTimeout(() => {
+ // Don't mark as read if the element doesn't exist, user has left chat view
+ if (this.$el) this.readChat()
+ }, MARK_AS_READ_DELAY)
}
} else {
this.jumpToBottomButtonVisible = true
}
- }, 100),
+ }, 200),
handleScrollUp (positionBeforeLoading) {
const positionAfterLoading = getScrollPosition(this.$refs.scrollable)
this.$refs.scrollable.scrollTo({
@@ -246,7 +270,7 @@ const Chat = {
const fetchOlderMessages = !!maxId
const sinceId = fetchLatest && chatMessageService.maxId
- this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId })
+ return this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId })
.then((messages) => {
// Clear the current chat in case we're recovering from a ws connection loss.
if (isFirstFetch) {
@@ -263,6 +287,14 @@ const Chat = {
if (isFirstFetch) {
this.updateScrollableContainerHeight()
}
+
+ // In vertical screens, the first batch of fetched messages may not always take the
+ // full height of the scrollable container.
+ // If this is the case, we want to fetch the messages until the scrollable container
+ // is fully populated so that the user has the ability to scroll up and load the history.
+ if (!isScrollable(this.$refs.scrollable) && messages.length > 0) {
+ this.fetchChat({ maxId: this.currentChatMessageService.minId })
+ }
})
})
})
@@ -287,46 +319,78 @@ const Chat = {
},
doStartFetching () {
this.$store.dispatch('startFetchingCurrentChat', {
- fetcher: () => setInterval(() => this.fetchChat({ fetchLatest: true }), 5000)
+ fetcher: () => promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000)
})
this.fetchChat({ isFirstFetch: true })
},
- sendMessage ({ status, media }) {
+ handleAttachmentPosting () {
+ this.$nextTick(() => {
+ this.handleResize()
+ // When the posting form size changes because of a media attachment, we need an extra resize
+ // to account for the potential delay in the DOM update.
+ setTimeout(() => {
+ this.updateScrollableContainerHeight()
+ }, SAFE_RESIZE_TIME_OFFSET)
+ this.scrollDown({ forceRead: true })
+ })
+ },
+ sendMessage ({ status, media, idempotencyKey }) {
const params = {
id: this.currentChat.id,
- content: status
+ content: status,
+ idempotencyKey
}
if (media[0]) {
params.mediaId = media[0].id
}
- return this.backendInteractor.sendChatMessage(params)
+ const fakeMessage = buildFakeMessage({
+ attachments: media,
+ chatId: this.currentChat.id,
+ content: status,
+ userId: this.currentUser.id,
+ idempotencyKey
+ })
+
+ this.$store.dispatch('addChatMessages', {
+ chatId: this.currentChat.id,
+ messages: [fakeMessage]
+ }).then(() => {
+ this.handleAttachmentPosting()
+ })
+
+ return this.doSendMessage({ params, fakeMessage, retriesLeft: MAX_RETRIES })
+ },
+ doSendMessage ({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
+ if (retriesLeft <= 0) return
+
+ this.backendInteractor.sendChatMessage(params)
.then(data => {
this.$store.dispatch('addChatMessages', {
chatId: this.currentChat.id,
- messages: [data],
- updateMaxId: false
- }).then(() => {
- this.$nextTick(() => {
- this.handleResize()
- // When the posting form size changes because of a media attachment, we need an extra resize
- // to account for the potential delay in the DOM update.
- setTimeout(() => {
- this.updateScrollableContainerHeight()
- }, SAFE_RESIZE_TIME_OFFSET)
- this.scrollDown({ forceRead: true })
- })
+ updateMaxId: false,
+ messages: [{ ...data, fakeId: fakeMessage.id }]
})
return data
})
.catch(error => {
console.error('Error sending message', error)
- return {
- error: this.$t('chats.error_sending_message')
+ this.$store.dispatch('handleMessageError', {
+ chatId: this.currentChat.id,
+ fakeId: fakeMessage.id,
+ isRetry: retriesLeft !== MAX_RETRIES
+ })
+ if ((error.statusCode >= 500 && error.statusCode < 600) || error.message === 'Failed to fetch') {
+ this.messageRetriers[fakeMessage.id] = setTimeout(() => {
+ this.doSendMessage({ params, fakeMessage, retriesLeft: retriesLeft - 1 })
+ }, 1000 * (2 ** (MAX_RETRIES - retriesLeft)))
}
+ return {}
})
+
+ return Promise.resolve(fakeMessage)
},
goBack () {
this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })
diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss
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_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..5af744a3 100644
--- a/src/components/chat_message/chat_message.scss
+++ b/src/components/chat_message/chat_message.scss
@@ -24,7 +24,7 @@
}
}
- .icon-ellipsis {
+ .menu-icon {
cursor: pointer;
&:hover, .extra-button-popover.open & {
@@ -101,6 +101,19 @@
}
}
+ .pending {
+ .status-content.media-body, .created-at {
+ color: var(--faint);
+ }
+ }
+
+ .error {
+ .status-content.media-body, .created-at {
+ color: $fallback--cRed;
+ color: var(--badgeNotification, $fallback--cRed);
+ }
+ }
+
.incoming {
a {
color: var(--chatMessageIncomingLink, $fallback--link);
diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue
index e923d694..3849ab6e 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"
@@ -56,15 +56,16 @@
class="dropdown-item dropdown-item-icon"
@click="deleteMessage"
>
- <i class="icon-cancel" /> {{ $t("chats.delete") }}
+ <FAIcon icon="times" /> {{ $t("chats.delete") }}
</button>
</div>
</div>
<button
slot="trigger"
+ class="menu-icon"
:title="$t('chats.more')"
>
- <i class="icon-ellipsis" />
+ <FAIcon icon="ellipsis-h" />
</button>
</Popover>
</div>
diff --git a/src/components/chat_new/chat_new.js b/src/components/chat_new/chat_new.js
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.js b/src/components/conversation/conversation.js
index 45fb2bf6..069c0b40 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -44,7 +44,8 @@ const conversation = {
'isPage',
'pinnedStatusIdsObject',
'inProfile',
- 'profileUserId'
+ 'profileUserId',
+ 'virtualHidden'
],
created () {
if (this.isPage) {
@@ -52,6 +53,13 @@ const conversation = {
}
},
computed: {
+ hideStatus () {
+ if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
+ return this.virtualHidden && this.$refs.statusComponent[0].suspendable
+ } else {
+ return this.virtualHidden
+ }
+ },
status () {
return this.$store.state.statuses.allStatusesObject[this.statusId]
},
@@ -102,6 +110,10 @@ const conversation = {
},
isExpanded () {
return this.expanded || this.isPage
+ },
+ hiddenStyle () {
+ const height = (this.status && this.status.virtualHeight) || '120px'
+ return this.virtualHidden ? { height } : {}
}
},
components: {
@@ -121,6 +133,12 @@ const conversation = {
if (value) {
this.fetchConversation()
}
+ },
+ virtualHidden (value) {
+ this.$store.dispatch(
+ 'setVirtualHeight',
+ { statusId: this.statusId, height: `${this.$el.clientHeight}px` }
+ )
}
},
methods: {
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 997a4d10..e0b9fcc5 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -1,5 +1,7 @@
<template>
<div
+ v-if="!hideStatus"
+ :style="hiddenStyle"
class="Conversation"
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
>
@@ -18,6 +20,7 @@
<status
v-for="status in conversation"
:key="status.id"
+ ref="statusComponent"
:inline-expanded="collapsable && isExpanded"
:statusoid="status"
:expandable="!isExpanded"
@@ -33,6 +36,10 @@
@toggleExpanded="toggleExpanded"
/>
</div>
+ <div
+ v-else
+ :style="hiddenStyle"
+ />
</template>
<script src="./conversation.js"></script>
@@ -53,8 +60,8 @@
.conversation-status {
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
- border-left: 4px solid $fallback--cRed;
- border-left: 4px solid var(--cRed, $fallback--cRed);
+ border-left-color: $fallback--cRed;
+ border-left-color: var(--cRed, $fallback--cRed);
}
.conversation-status:last-child {
diff --git a/src/components/desktop_nav/desktop_nav.js b/src/components/desktop_nav/desktop_nav.js
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..028692a9
--- /dev/null
+++ b/src/components/desktop_nav/desktop_nav.scss
@@ -0,0 +1,112 @@
+@import '../../_variables.scss';
+
+.DesktopNav {
+ height: 50px;
+ width: 100%;
+ position: fixed;
+
+ .inner-nav {
+ display: grid;
+ grid-template-rows: 50px;
+ grid-template-columns: 2fr auto 2fr;
+ grid-template-areas: "sitename logo actions";
+ box-sizing: border-box;
+ padding: 0 1.2em;
+ margin: auto;
+ max-width: 980px;
+ }
+
+ &.-logoLeft {
+ grid-template-columns: auto 2fr 2fr;
+ grid-template-areas: "logo sitename actions";
+ }
+
+ button {
+ &, svg {
+ color: $fallback--text;
+ color: var(--btnTopBarText, $fallback--text);
+ }
+
+ &:active {
+ background-color: $fallback--fg;
+ background-color: var(--btnPressedTopBar, $fallback--fg);
+ color: $fallback--text;
+ color: var(--btnPressedTopBarText, $fallback--text);
+ }
+
+ &:disabled {
+ color: $fallback--text;
+ color: var(--btnDisabledTopBarText, $fallback--text);
+ }
+
+ &.toggled {
+ color: $fallback--text;
+ color: var(--btnToggledTopBarText, $fallback--text);
+ background-color: $fallback--fg;
+ background-color: var(--btnToggledTopBar, $fallback--fg)
+ }
+ }
+
+ .logo {
+ grid-area: logo;
+ position: relative;
+ transition: opacity;
+ transition-timing-function: ease-out;
+ transition-duration: 100ms;
+
+ @media all and (min-width: 800px) {
+ opacity: 1 !important;
+ }
+
+ .mask {
+ mask-repeat: no-repeat;
+ mask-position: center;
+ mask-size: contain;
+ background-color: $fallback--fg;
+ background-color: var(--topBarText, $fallback--fg);
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ }
+
+ img {
+ display: inline-block;
+ height: 50px;
+ }
+ }
+
+ .nav-icon {
+ margin-left: 0.2em;
+ width: 2em;
+ text-align: center;
+ }
+
+ a, a svg {
+ color: $fallback--link;
+ color: var(--topBarLink, $fallback--link);
+ }
+
+ .sitename {
+ grid-area: sitename;
+ }
+
+ .actions {
+ grid-area: actions;
+ }
+
+ .item {
+ flex: 1;
+ line-height: 50px;
+ height: 50px;
+ overflow: hidden;
+ display: flex;
+ flex-wrap: wrap;
+
+ &.right {
+ justify-content: flex-end;
+ text-align: right;
+ }
+ }
+}
diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue
new file mode 100644
index 00000000..3a6e4033
--- /dev/null
+++ b/src/components/desktop_nav/desktop_nav.vue
@@ -0,0 +1,79 @@
+<template>
+ <nav
+ id="nav"
+ class="DesktopNav"
+ :class="{ '-logoLeft': logoLeft }"
+ @click="scrollToTop()"
+ >
+ <div class="inner-nav">
+ <div class="item sitename">
+ <router-link
+ v-if="!hideSitename"
+ class="site-name"
+ :to="{ name: 'root' }"
+ active-class="home"
+ >
+ {{ sitename }}
+ </router-link>
+ </div>
+ <router-link
+ class="logo"
+ :to="{ name: 'root' }"
+ :style="logoBgStyle"
+ >
+ <div
+ class="mask"
+ :style="logoMaskStyle"
+ />
+ <img
+ :src="logo"
+ :style="logoStyle"
+ >
+ </router-link>
+ <div class="item right actions">
+ <search-bar
+ v-if="currentUser || !privateMode"
+ @toggled="onSearchBarToggled"
+ @click.stop.native
+ />
+ <a
+ href="#"
+ class="nav-icon"
+ @click.stop="openSettingsModal"
+ >
+ <FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="cog"
+ :title="$t('nav.preferences')"
+ />
+ </a>
+ <a
+ v-if="currentUser && currentUser.role === 'admin'"
+ href="/pleroma/admin/#/login-pleroma"
+ class="nav-icon"
+ target="_blank"
+ ><FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="tachometer-alt"
+ :title="$t('nav.administration')"
+ /></a>
+ <a
+ v-if="currentUser"
+ href="#"
+ class="nav-icon"
+ @click.prevent="logout"
+ ><FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="sign-out-alt"
+ :title="$t('login.logout')"
+ /></a>
+ </div>
+ </div>
+ </nav>
+</template>
+<script src="./desktop_nav.js"></script>
+
+<style src="./desktop_nav.scss" lang="scss"></style>
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index f0123447..87303d08 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
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index b9a74572..224e72cf 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -11,7 +11,7 @@
class="emoji-picker-icon"
@click.prevent="togglePicker"
>
- <i class="icon-smile" />
+ <FAIcon :icon="['far', 'smile-beam']" />
</div>
<EmojiPicker
v-if="enableEmojiPicker"
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 0f397b59..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
@@ -8,7 +20,20 @@ const LOAD_EMOJI_BY = 60
const LOAD_EMOJI_MARGIN = 64
const filterByKeyword = (list, keyword = '') => {
- return list.filter(x => x.displayText.includes(keyword))
+ if (keyword === '') return list
+
+ const keywordLowercase = keyword.toLowerCase()
+ let orderedEmojiList = []
+ for (const emoji of list) {
+ const indexOfKeyword = emoji.displayText.toLowerCase().indexOf(keywordLowercase)
+ if (indexOfKeyword > -1) {
+ if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
+ orderedEmojiList[indexOfKeyword] = []
+ }
+ orderedEmojiList[indexOfKeyword].push(emoji)
+ }
+ }
+ return orderedEmojiList.flat()
}
const EmojiPicker = {
@@ -164,13 +189,13 @@ const EmojiPicker = {
{
id: 'custom',
text: this.$t('emoji.custom'),
- icon: 'icon-smile',
+ icon: 'smile-beam',
emojis: customEmojis
},
{
id: 'standard',
text: this.$t('emoji.unicode'),
- icon: 'icon-picture',
+ icon: 'box-open',
emojis: filterByKeyword(standardEmojis, this.keyword)
}
]
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
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/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..ecd71bf1 100644
--- a/src/components/exporter/exporter.vue
+++ b/src/components/exporter/exporter.vue
@@ -1,7 +1,12 @@
<template>
<div class="exporter">
<div v-if="processing">
- <i class="icon-spin4 animate-spin exporter-processing" />
+ <FAIcon
+ icon="circle-notch"
+ size="lg"
+ spin
+ />
+
<span>{{ processingMessage }}</span>
</div>
<button
@@ -19,7 +24,6 @@
<style lang="scss">
.exporter {
&-processing {
- font-size: 1.5em;
margin: 0.25em;
}
}
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
index 5e0c36bb..1a8eef72 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -1,4 +1,24 @@
import Popover from '../popover/popover.vue'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faEllipsisH,
+ faBookmark,
+ faEyeSlash,
+ faThumbtack,
+ faShareAlt
+} from '@fortawesome/free-solid-svg-icons'
+import {
+ faBookmark as faBookmarkReg
+} from '@fortawesome/free-regular-svg-icons'
+
+library.add(
+ faEllipsisH,
+ faBookmark,
+ faBookmarkReg,
+ faEyeSlash,
+ faThumbtack,
+ faShareAlt
+)
const ExtraButtons = {
props: [ 'status' ],
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index 7a4e8642..a33f6e87 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -15,14 +15,20 @@
class="dropdown-item dropdown-item-icon"
@click.prevent="muteConversation"
>
- <i class="icon-eye-off" /><span>{{ $t("status.mute_conversation") }}</span>
+ <FAIcon
+ fixed-width
+ icon="eye-slash"
+ /><span>{{ $t("status.mute_conversation") }}</span>
</button>
<button
v-if="canMute && status.thread_muted"
class="dropdown-item dropdown-item-icon"
@click.prevent="unmuteConversation"
>
- <i class="icon-eye-off" /><span>{{ $t("status.unmute_conversation") }}</span>
+ <FAIcon
+ fixed-width
+ icon="eye-slash"
+ /><span>{{ $t("status.unmute_conversation") }}</span>
</button>
<button
v-if="!status.pinned && canPin"
@@ -30,7 +36,10 @@
@click.prevent="pinStatus"
@click="close"
>
- <i class="icon-pin" /><span>{{ $t("status.pin") }}</span>
+ <FAIcon
+ fixed-width
+ icon="thumbtack"
+ /><span>{{ $t("status.pin") }}</span>
</button>
<button
v-if="status.pinned && canPin"
@@ -38,7 +47,10 @@
@click.prevent="unpinStatus"
@click="close"
>
- <i class="icon-pin" /><span>{{ $t("status.unpin") }}</span>
+ <FAIcon
+ fixed-width
+ icon="thumbtack"
+ /><span>{{ $t("status.unpin") }}</span>
</button>
<button
v-if="!status.bookmarked"
@@ -46,7 +58,10 @@
@click.prevent="bookmarkStatus"
@click="close"
>
- <i class="icon-bookmark-empty" /><span>{{ $t("status.bookmark") }}</span>
+ <FAIcon
+ fixed-width
+ :icon="['far', 'bookmark']"
+ /><span>{{ $t("status.bookmark") }}</span>
</button>
<button
v-if="status.bookmarked"
@@ -54,7 +69,10 @@
@click.prevent="unbookmarkStatus"
@click="close"
>
- <i class="icon-bookmark" /><span>{{ $t("status.unbookmark") }}</span>
+ <FAIcon
+ fixed-width
+ icon="bookmark"
+ /><span>{{ $t("status.unbookmark") }}</span>
</button>
<button
v-if="canDelete"
@@ -62,21 +80,29 @@
@click.prevent="deleteStatus"
@click="close"
>
- <i class="icon-cancel" /><span>{{ $t("status.delete") }}</span>
+ <FAIcon
+ fixed-width
+ icon="times"
+ /><span>{{ $t("status.delete") }}</span>
</button>
<button
class="dropdown-item dropdown-item-icon"
@click.prevent="copyLink"
@click="close"
>
- <i class="icon-share" /><span>{{ $t("status.copy_link") }}</span>
+ <FAIcon
+ fixed-width
+ icon="share-alt"
+ /><span>{{ $t("status.copy_link") }}</span>
</button>
</div>
</div>
- <i
- slot="trigger"
- class="icon-ellipsis button-icon"
- />
+ <span slot="trigger">
+ <FAIcon
+ class="ExtraButtons fa-scale-110 fa-old-padding"
+ icon="ellipsis-h"
+ />
+ </span>
</Popover>
</template>
@@ -85,8 +111,9 @@
<style lang="scss">
@import '../../_variables.scss';
-.icon-ellipsis {
+.ExtraButtons {
cursor: pointer;
+ position: static;
&:hover,
.extra-button-popover.open & {
diff --git a/src/components/favorite_button/favorite_button.js b/src/components/favorite_button/favorite_button.js
index 5014d84f..2a2ee84a 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'],
@@ -23,9 +33,7 @@ const FavoriteButton = {
computed: {
classes () {
return {
- 'icon-star-empty': !this.status.favorited,
- 'icon-star': this.status.favorited,
- 'animate-spin': this.animated
+ '-favorited': this.status.favorited
}
},
...mapGetters(['mergedConfig'])
diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue
index fbc90f84..dfe12f86 100644
--- a/src/components/favorite_button/favorite_button.vue
+++ b/src/components/favorite_button/favorite_button.vue
@@ -1,18 +1,21 @@
<template>
<div v-if="loggedIn">
- <i
+ <FAIcon
:class="classes"
- class="button-icon favorite-button fav-active"
+ class="FavoriteButton fa-scale-110 fa-old-padding -interactive"
:title="$t('tool_tip.favorite')"
+ :icon="[status.favorited ? 'fas' : 'far', 'star']"
+ :spin="animated"
@click.prevent="favorite()"
/>
<span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
</div>
<div v-else>
- <i
+ <FAIcon
:class="classes"
- class="button-icon favorite-button"
+ class="FavoriteButton fa-scale-110 fa-old-padding"
:title="$t('tool_tip.favorite')"
+ :icon="['far', 'star']"
/>
<span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
</div>
@@ -23,18 +26,20 @@
<style lang="scss">
@import '../../_variables.scss';
-.fav-active {
- cursor: pointer;
- animation-duration: 0.6s;
+.FavoriteButton {
+ &.-interactive {
+ cursor: pointer;
+ animation-duration: 0.6s;
- &:hover {
+ &:hover {
+ color: $fallback--cOrange;
+ color: var(--cOrange, $fallback--cOrange);
+ }
+ }
+
+ &.-favorited {
color: $fallback--cOrange;
color: var(--cOrange, $fallback--cOrange);
}
}
-
-.favorite-button.icon-star {
- color: $fallback--cOrange;
- color: var(--cOrange, $fallback--cOrange);
-}
</style>
diff --git a/src/components/follow_requests/follow_requests.vue b/src/components/follow_requests/follow_requests.vue
index 5fa4cf39..41f19db8 100644
--- a/src/components/follow_requests/follow_requests.vue
+++ b/src/components/follow_requests/follow_requests.vue
@@ -1,7 +1,9 @@
<template>
<div class="settings panel panel-default">
<div class="panel-heading">
- {{ $t('nav.friend_requests') }}
+ <div class="title">
+ {{ $t('nav.friend_requests') }}
+ </div>
</div>
<div class="panel-body">
<FollowRequestCard
diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js
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..8a33b9eb 100644
--- a/src/components/global_notice_list/global_notice_list.vue
+++ b/src/components/global_notice_list/global_notice_list.vue
@@ -9,8 +9,9 @@
<div class="notice-message">
{{ $t(notice.messageKey, notice.messageArgs) }}
</div>
- <i
- class="button-icon icon-cancel"
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
@click="closeNotice(notice)"
/>
</div>
diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js
index 01361e25..59e4d07e 100644
--- a/src/components/image_cropper/image_cropper.js
+++ b/src/components/image_cropper/image_cropper.js
@@ -1,5 +1,15 @@
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faTimes,
+ faCircleNotch
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faTimes,
+ faCircleNotch
+)
const ImageCropper = {
props: {
diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue
index 4e1b5927..75def612 100644
--- a/src/components/image_cropper/image_cropper.vue
+++ b/src/components/image_cropper/image_cropper.vue
@@ -31,9 +31,10 @@
@click="submit(false)"
v-text="saveWithoutCroppingText"
/>
- <i
+ <FAIcon
v-if="submitting"
- class="icon-spin4 animate-spin"
+ spin
+ icon="circle-notch"
/>
</div>
<div
@@ -41,8 +42,9 @@
class="alert error"
>
{{ submitErrorMsg }}
- <i
- class="button-icon icon-cancel"
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
@click="clearError"
/>
</div>
diff --git a/src/components/importer/importer.js b/src/components/importer/importer.js
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..c4fe5b00 100644
--- a/src/components/importer/importer.vue
+++ b/src/components/importer/importer.vue
@@ -7,9 +7,11 @@
@change="change"
>
</form>
- <i
+ <FAIcon
v-if="submitting"
- class="icon-spin4 animate-spin importer-uploading"
+ class="importer-uploading"
+ spin
+ icon="circle-notch"
/>
<button
v-else
@@ -19,15 +21,15 @@
{{ submitButtonLabel }}
</button>
<div v-if="success">
- <i
- class="icon-cross"
+ <FAIcon
+ icon="times"
@click="dismiss"
/>
<p>{{ successMessage }}</p>
</div>
<div v-else-if="error">
- <i
- class="icon-cross"
+ <FAIcon
+ icon="times"
@click="dismiss"
/>
<p>{{ errorMessage }}</p>
diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue
index dd6800a3..d039e86b 100644
--- a/src/components/interface_language_switcher/interface_language_switcher.vue
+++ b/src/components/interface_language_switcher/interface_language_switcher.vue
@@ -19,7 +19,10 @@
{{ languageNames[i] }}
</option>
</select>
- <i class="icon-down-open" />
+ <FAIcon
+ class="select-down-icon"
+ icon="chevron-down"
+ />
</label>
</div>
</template>
@@ -28,6 +31,14 @@
import languagesObject from '../../i18n/messages'
import ISO6391 from 'iso-639-1'
import _ from 'lodash'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faChevronDown
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faChevronDown
+)
export default {
computed: {
diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
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..a1f77210 100644
--- a/src/components/login_form/login_form.vue
+++ b/src/components/login_form/login_form.vue
@@ -76,8 +76,9 @@
>
<div class="alert error">
{{ error }}
- <i
- class="button-icon icon-cancel"
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
@click="clearError"
/>
</div>
diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js
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..88251a26 100644
--- a/src/components/media_upload/media_upload.vue
+++ b/src/components/media_upload/media_upload.vue
@@ -7,13 +7,16 @@
class="label"
:title="$t('tool_tip.media_upload')"
>
- <i
+ <FAIcon
v-if="uploading"
- class="progress-icon icon-spin4 animate-spin"
+ class="progress-icon"
+ icon="circle-notch"
+ spin
/>
- <i
+ <FAIcon
v-if="!uploading"
- class="new-icon icon-upload"
+ class="new-icon"
+ icon="upload"
/>
<input
v-if="uploadReady"
@@ -40,15 +43,5 @@
.new-icon {
cursor: pointer;
}
-
- .progress-icon {
- display: inline-block;
- line-height: 0;
- &::before {
- /* Overriding fontello to achieve the perfect speeeen */
- margin: 0;
- line-height: 0;
- }
- }
}
</style>
diff --git a/src/components/mfa_form/recovery_form.js b/src/components/mfa_form/recovery_form.js
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..78953649 100644
--- a/src/components/mfa_form/recovery_form.vue
+++ b/src/components/mfa_form/recovery_form.vue
@@ -54,8 +54,9 @@
>
<div class="alert error">
{{ error }}
- <i
- class="button-icon icon-cancel"
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
@click="clearError"
/>
</div>
diff --git a/src/components/mfa_form/totp_form.js b/src/components/mfa_form/totp_form.js
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..9401cad5 100644
--- a/src/components/mfa_form/totp_form.vue
+++ b/src/components/mfa_form/totp_form.vue
@@ -56,8 +56,10 @@
>
<div class="alert error">
{{ error }}
- <i
- class="button-icon icon-cancel"
+ <FAIcon
+ size="lg"
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
@click="clearError"
/>
</div>
diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js
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..5304a500 100644
--- a/src/components/mobile_nav/mobile_nav.vue
+++ b/src/components/mobile_nav/mobile_nav.vue
@@ -1,49 +1,53 @@
<template>
- <div>
+ <div
+ class="MobileNav"
+ >
<nav
id="nav"
- class="nav-bar container"
+ class="mobile-nav"
:class="{ 'mobile-hidden': isChat }"
+ @click="scrollToTop()"
>
- <div
- class="mobile-inner-nav"
- @click="scrollToTop()"
- >
- <div class="item">
- <a
- href="#"
- class="mobile-nav-button"
- @click.stop.prevent="toggleMobileSidebar()"
- >
- <i class="button-icon icon-menu" />
- <div
- v-if="unreadChatCount"
- class="alert-dot"
- />
- </a>
- <router-link
- v-if="!hideSitename"
- class="site-name"
- :to="{ name: 'root' }"
- active-class="home"
- >
- {{ sitename }}
- </router-link>
- </div>
- <div class="item right">
- <a
- v-if="currentUser"
- class="mobile-nav-button"
- href="#"
- @click.stop.prevent="openMobileNotifications()"
- >
- <i class="button-icon icon-bell-alt" />
- <div
- v-if="unseenNotificationsCount"
- class="alert-dot"
- />
- </a>
- </div>
+ <div class="item">
+ <a
+ href="#"
+ class="mobile-nav-button"
+ @click.stop.prevent="toggleMobileSidebar()"
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="bars"
+ />
+ <div
+ v-if="unreadChatCount"
+ class="alert-dot"
+ />
+ </a>
+ <router-link
+ v-if="!hideSitename"
+ class="site-name"
+ :to="{ name: 'root' }"
+ active-class="home"
+ >
+ {{ sitename }}
+ </router-link>
+ </div>
+ <div class="item right">
+ <a
+ v-if="currentUser"
+ class="mobile-nav-button"
+ href="#"
+ @click.stop.prevent="openMobileNotifications()"
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="bell"
+ />
+ <div
+ v-if="unseenNotificationsCount"
+ class="alert-dot"
+ />
+ </a>
</div>
</nav>
<div
@@ -59,7 +63,10 @@
class="mobile-nav-button"
@click.stop.prevent="closeMobileNotifications()"
>
- <i class="button-icon icon-cancel" />
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
+ />
</a>
</div>
<div
@@ -84,101 +91,124 @@
<style lang="scss">
@import '../../_variables.scss';
-.mobile-inner-nav {
- width: 100%;
- display: flex;
- align-items: center;
-}
+.MobileNav {
+ .mobile-nav {
+ display: grid;
+ line-height: 50px;
+ height: 50px;
+ grid-template-rows: 50px;
+ grid-template-columns: 2fr auto;
+ width: 100%;
+ position: fixed;
+ box-sizing: border-box;
+ }
-.mobile-nav-button {
- display: flex;
- justify-content: center;
- width: 50px;
- position: relative;
- cursor: pointer;
-}
+ .mobile-inner-nav {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ }
-.alert-dot {
- border-radius: 100%;
- height: 8px;
- width: 8px;
- position: absolute;
- left: calc(50% - 4px);
- top: calc(50% - 4px);
- margin-left: 6px;
- margin-top: -6px;
- background-color: $fallback--cRed;
- background-color: var(--badgeNotification, $fallback--cRed);
-}
+ .mobile-nav-button {
+ display: inline-block;
+ text-align: center;
+ padding: 0 1em;
+ position: relative;
+ cursor: pointer;
+ }
-.mobile-notifications-drawer {
- width: 100%;
- height: 100vh;
- overflow-x: hidden;
- position: fixed;
- top: 0;
- left: 0;
- box-shadow: 1px 1px 4px rgba(0,0,0,.6);
- box-shadow: var(--panelShadow);
- transition-property: transform;
- transition-duration: 0.25s;
- transform: translateX(0);
- z-index: 1001;
- -webkit-overflow-scrolling: touch;
-
- &.closed {
- transform: translateX(100%);
+ .site-name {
+ padding: 0 .3em;
+ display: inline-block;
}
-}
-.mobile-notifications-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- z-index: 1;
- width: 100%;
- height: 50px;
- line-height: 50px;
- position: absolute;
- color: var(--topBarText);
- background-color: $fallback--fg;
- background-color: var(--topBar, $fallback--fg);
- box-shadow: 0px 0px 4px rgba(0,0,0,.6);
- box-shadow: var(--topBarShadow);
-
- .title {
- font-size: 1.3em;
- margin-left: 0.6em;
+ .item {
+ /* moslty just to get rid of extra whitespaces */
+ display: flex;
}
-}
-.mobile-notifications {
- margin-top: 50px;
- width: 100vw;
- height: calc(100vh - 50px);
- overflow-x: hidden;
- overflow-y: scroll;
-
- color: $fallback--text;
- color: var(--text, $fallback--text);
- background-color: $fallback--bg;
- background-color: var(--bg, $fallback--bg);
-
- .notifications {
- padding: 0;
- border-radius: 0;
- box-shadow: none;
- .panel {
- border-radius: 0;
- margin: 0;
- box-shadow: none;
+ .alert-dot {
+ border-radius: 100%;
+ height: 8px;
+ width: 8px;
+ position: absolute;
+ left: calc(50% - 4px);
+ top: calc(50% - 4px);
+ margin-left: 6px;
+ margin-top: -6px;
+ background-color: $fallback--cRed;
+ background-color: var(--badgeNotification, $fallback--cRed);
+ }
+
+ .mobile-notifications-drawer {
+ width: 100%;
+ height: 100vh;
+ overflow-x: hidden;
+ position: fixed;
+ top: 0;
+ left: 0;
+ box-shadow: 1px 1px 4px rgba(0,0,0,.6);
+ box-shadow: var(--panelShadow);
+ transition-property: transform;
+ transition-duration: 0.25s;
+ transform: translateX(0);
+ z-index: 1001;
+ -webkit-overflow-scrolling: touch;
+
+ &.closed {
+ transform: translateX(100%);
}
- .panel:after {
- border-radius: 0;
+ }
+
+ .mobile-notifications-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ z-index: 1;
+ width: 100%;
+ height: 50px;
+ line-height: 50px;
+ position: absolute;
+ color: var(--topBarText);
+ background-color: $fallback--fg;
+ background-color: var(--topBar, $fallback--fg);
+ box-shadow: 0px 0px 4px rgba(0,0,0,.6);
+ box-shadow: var(--topBarShadow);
+
+ .title {
+ font-size: 1.3em;
+ margin-left: 0.6em;
}
- .panel .panel-heading {
+ }
+
+ .mobile-notifications {
+ margin-top: 50px;
+ width: 100vw;
+ height: calc(100vh - 50px);
+ overflow-x: hidden;
+ overflow-y: scroll;
+
+ color: $fallback--text;
+ color: var(--text, $fallback--text);
+ background-color: $fallback--bg;
+ background-color: var(--bg, $fallback--bg);
+
+ .notifications {
+ padding: 0;
border-radius: 0;
box-shadow: none;
+ .panel {
+ border-radius: 0;
+ margin: 0;
+ box-shadow: none;
+ }
+ .panel:after {
+ border-radius: 0;
+ }
+ .panel .panel-heading {
+ border-radius: 0;
+ box-shadow: none;
+ }
}
}
}
diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js
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..50529878 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
@@ -5,7 +5,7 @@
:class="{ 'hidden': isHidden }"
@click="openPostForm"
>
- <i class="icon-edit" />
+ <FAIcon icon="pen" />
</button>
</div>
</template>
@@ -39,7 +39,7 @@
transform: translateY(150%);
}
- i {
+ svg {
font-size: 1.5em;
color: $fallback--text;
color: var(--text, $fallback--text);
diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue
index b2d5acc5..60fa6ceb 100644
--- a/src/components/moderation_tools/moderation_tools.vue
+++ b/src/components/moderation_tools/moderation_tools.vue
@@ -178,7 +178,7 @@
box-shadow: var(--inputShadow);
&.menu-checkbox-checked::after {
- content: '✔';
+ content: '✓';
}
}
diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js
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 f8459fd1..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,80 +72,88 @@
<style lang="scss">
@import '../../_variables.scss';
-.nav-panel .panel {
- overflow: hidden;
- box-shadow: var(--panelShadow);
-}
-.nav-panel ul {
- list-style: none;
- margin: 0;
- padding: 0;
-}
-
-.follow-request-count {
- margin: -6px 10px;
- background-color: $fallback--bg;
- background-color: var(--input, $fallback--faint);
-}
-
-.nav-panel li {
- border-bottom: 1px solid;
- border-color: $fallback--border;
- border-color: var(--border, $fallback--border);
- padding: 0;
-
- &:first-child a {
- border-top-right-radius: $fallback--panelRadius;
- border-top-right-radius: var(--panelRadius, $fallback--panelRadius);
- border-top-left-radius: $fallback--panelRadius;
- border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
+.NavPanel {
+ .panel {
+ overflow: hidden;
+ box-shadow: var(--panelShadow);
}
- &:last-child a {
- border-bottom-right-radius: $fallback--panelRadius;
- border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius);
- border-bottom-left-radius: $fallback--panelRadius;
- border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius);
+ ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
}
-}
-.nav-panel li:last-child {
- border: none;
-}
+ li {
+ position: relative;
+ border-bottom: 1px solid;
+ border-color: $fallback--border;
+ border-color: var(--border, $fallback--border);
+ padding: 0;
-.nav-panel a {
- display: block;
- padding: 0.8em 0.85em;
+ &:first-child a {
+ border-top-right-radius: $fallback--panelRadius;
+ border-top-right-radius: var(--panelRadius, $fallback--panelRadius);
+ border-top-left-radius: $fallback--panelRadius;
+ border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
+ }
- &:hover {
- background-color: $fallback--lightBg;
- background-color: var(--selectedMenu, $fallback--lightBg);
- color: $fallback--link;
- color: var(--selectedMenuText, $fallback--link);
- --faint: var(--selectedMenuFaintText, $fallback--faint);
- --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
- --lightText: var(--selectedMenuLightText, $fallback--lightText);
- --icon: var(--selectedMenuIcon, $fallback--icon);
+ &:last-child a {
+ border-bottom-right-radius: $fallback--panelRadius;
+ border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius);
+ border-bottom-left-radius: $fallback--panelRadius;
+ border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius);
+ }
}
- &.router-link-active {
- font-weight: bolder;
- background-color: $fallback--lightBg;
- background-color: var(--selectedMenu, $fallback--lightBg);
- color: $fallback--text;
- color: var(--selectedMenuText, $fallback--text);
- --faint: var(--selectedMenuFaintText, $fallback--faint);
- --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
- --lightText: var(--selectedMenuLightText, $fallback--lightText);
- --icon: var(--selectedMenuIcon, $fallback--icon);
+ li:last-child {
+ border: none;
+ }
+
+ a {
+ display: block;
+ box-sizing: border-box;
+ align-items: stretch;
+ height: 3.5em;
+ line-height: 3.5em;
+ padding: 0 1em;
&:hover {
- text-decoration: underline;
+ background-color: $fallback--lightBg;
+ background-color: var(--selectedMenu, $fallback--lightBg);
+ color: $fallback--link;
+ color: var(--selectedMenuText, $fallback--link);
+ --faint: var(--selectedMenuFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuLightText, $fallback--lightText);
+ --icon: var(--selectedMenuIcon, $fallback--icon);
+ }
+
+ &.router-link-active {
+ font-weight: bolder;
+ background-color: $fallback--lightBg;
+ background-color: var(--selectedMenu, $fallback--lightBg);
+ color: $fallback--text;
+ color: var(--selectedMenuText, $fallback--text);
+ --faint: var(--selectedMenuFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuLightText, $fallback--lightText);
+ --icon: var(--selectedMenuIcon, $fallback--icon);
+
+ &:hover {
+ text-decoration: underline;
+ }
}
}
-}
-.nav-panel .button-icon:before {
- width: 1.1em;
+ .fa-scale-110 {
+ margin-right: 0.8em;
+ }
+
+ .badge {
+ position: absolute;
+ right: 0.6rem;
+ top: 1.25em;
+ }
}
</style>
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
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..2bbde108 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"
@@ -18,12 +18,15 @@
href="#"
class="unmute"
@click.prevent="toggleMute"
- ><i class="button-icon icon-eye-off" /></a>
+ ><FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="eye-slash"
+ /></a>
</div>
<div
v-else
- class="non-mention"
- :class="[userClass, { highlighted: userStyle }]"
+ class="Notification non-mention"
+ :class="[userClass, { highlighted: userStyle }, '-type--' + notification.type]"
:style="[ userStyle ]"
>
<a
@@ -60,26 +63,39 @@
:title="'@'+notification.from_profile.screen_name"
>{{ notification.from_profile.name }}</span>
<span v-if="notification.type === 'like'">
- <i class="fa icon-star lit" />
+ <FAIcon
+ class="type-icon"
+ icon="star"
+ />
<small>{{ $t('notifications.favorited_you') }}</small>
</span>
<span v-if="notification.type === 'repeat'">
- <i
- class="fa icon-retweet lit"
+ <FAIcon
+ class="type-icon"
+ icon="retweet"
:title="$t('tool_tip.repeat')"
/>
<small>{{ $t('notifications.repeated_you') }}</small>
</span>
<span v-if="notification.type === 'follow'">
- <i class="fa icon-user-plus lit" />
+ <FAIcon
+ class="type-icon"
+ icon="user-plus"
+ />
<small>{{ $t('notifications.followed_you') }}</small>
</span>
<span v-if="notification.type === 'follow_request'">
- <i class="fa icon-user lit" />
+ <FAIcon
+ class="type-icon"
+ icon="user"
+ />
<small>{{ $t('notifications.follow_request') }}</small>
</span>
<span v-if="notification.type === 'move'">
- <i class="fa icon-arrow-curved lit" />
+ <FAIcon
+ class="type-icon"
+ icon="suitcase-rolling"
+ />
<small>{{ $t('notifications.migrated_to') }}</small>
</span>
<span v-if="notification.type === 'pleroma:emoji_reaction'">
@@ -120,7 +136,10 @@
v-if="needMute"
href="#"
@click.prevent="toggleMute"
- ><i class="button-icon icon-eye-off" /></a>
+ ><FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="eye-slash"
+ /></a>
</span>
<div
v-if="notification.type === 'follow' || notification.type === 'follow_request'"
@@ -136,13 +155,15 @@
v-if="notification.type === 'follow_request'"
style="white-space: nowrap;"
>
- <i
- class="icon-ok button-icon follow-request-accept"
+ <FAIcon
+ icon="check"
+ class="fa-scale-110 fa-old-padding follow-request-accept"
:title="$t('tool_tip.accept_follow_request')"
@click="approveUser()"
/>
- <i
- class="icon-cancel button-icon follow-request-reject"
+ <FAIcon
+ icon="times"
+ class="fa-scale-110 fa-old-padding follow-request-reject"
:title="$t('tool_tip.reject_follow_request')"
@click="denyUser()"
/>
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index d951e2a8..4b479e13 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -6,6 +6,12 @@ import {
filteredNotificationsFromStore,
unseenNotificationsFromStore
} from '../../services/notification_utils/notification_utils.js'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faCircleNotch
+)
const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
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..bd875cca 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -61,7 +61,11 @@
v-else
class="new-status-notification text-center panel-footer"
>
- <i class="icon-spin3 animate-spin" />
+ <FAIcon
+ icon="circle-notch"
+ spin
+ size="lg"
+ />
</div>
</div>
</div>
diff --git a/src/components/panel_loading/panel_loading.vue b/src/components/panel_loading/panel_loading.vue
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..0deb9ccf 100644
--- a/src/components/password_reset/password_reset.vue
+++ b/src/components/password_reset/password_reset.vue
@@ -63,10 +63,10 @@
>
<span>{{ error }}</span>
<a
- class="button-icon dismiss"
+ class="fa-scale-110 fa-old-padding dismiss"
@click.prevent="dismissError()"
>
- <i class="icon-cancel" />
+ <FAIcon icon="times" />
</a>
</p>
</div>
@@ -122,7 +122,7 @@
padding-right: 2rem;
}
- .icon-cancel {
+ .dismiss {
cursor: pointer;
}
}
diff --git a/src/components/poll/poll_form.js b/src/components/poll/poll_form.js
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..31f204a0 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')"
@@ -24,8 +25,9 @@
v-if="options.length > 2"
class="icon-container"
>
- <i
- class="icon-cancel"
+ <FAIcon
+ icon="times"
+ class="delete"
@click="deleteOption(index)"
/>
</div>
@@ -35,7 +37,11 @@
class="add-option faint"
@click="addOption"
>
- <i class="icon-plus" />
+ <FAIcon
+ icon="plus"
+ size="sm"
+ />
+
{{ $t("polls.add_option") }}
</a>
<div class="poll-type-expiry">
@@ -55,7 +61,10 @@
<option value="single">{{ $t('polls.single_choice') }}</option>
<option value="multiple">{{ $t('polls.multiple_choices') }}</option>
</select>
- <i class="icon-down-open" />
+ <FAIcon
+ class="select-down-icon"
+ icon="chevron-down"
+ />
</label>
</div>
<div
@@ -83,7 +92,10 @@
{{ $t(`time.${unit}_short`, ['']) }}
</option>
</select>
- <i class="icon-down-open" />
+ <FAIcon
+ class="select-down-icon"
+ icon="chevron-down"
+ />
</label>
</div>
</div>
@@ -103,6 +115,7 @@
.add-option {
align-self: flex-start;
padding-top: 0.25em;
+ padding-left: 0.1em;
cursor: pointer;
}
@@ -124,9 +137,17 @@
.icon-container {
// Hack: Move the icon over the input box
- width: 2em;
- margin-left: -2em;
+ width: 1.5em;
+ margin-left: -1.5em;
z-index: 1;
+
+ .delete {
+ cursor: pointer;
+
+ &:hover {
+ color: inherit;
+ }
+ }
}
.poll-type-expiry {
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
index 5c99c509..9b8680e5 100644
--- a/src/components/popover/popover.vue
+++ b/src/components/popover/popover.vue
@@ -27,7 +27,7 @@
<script src="./popover.js" />
-<style lang=scss>
+<style lang="scss">
@import '../../_variables.scss';
.popover {
@@ -96,7 +96,7 @@
&-icon {
padding-left: 0.5rem;
- i {
+ svg {
margin-right: 0.25rem;
color: var(--menuPopoverIcon, $fallback--icon)
}
@@ -111,7 +111,7 @@
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
- i {
+ svg {
color: var(--selectedMenuPopoverIcon, $fallback--icon);
}
}
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index ad149506..de583269 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,
@@ -251,7 +273,7 @@ const PostStatusForm = {
if (this.preview) this.previewStatus()
},
async postStatus (event, newStatus, opts = {}) {
- if (this.posting) { return }
+ if (this.posting && !this.optimisticPosting) { return }
if (this.disableSubmit) { return }
if (this.emojiInputShown) { return }
if (this.submitOnEnter) {
@@ -259,6 +281,8 @@ const PostStatusForm = {
event.preventDefault()
}
+ if (this.optimisticPosting && (this.emptyStatus || this.isOverLengthLimit)) { return }
+
if (this.emptyStatus) {
this.error = this.$t('post_status.empty_status_error')
return
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index d67d9ae9..42d3152b 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"
@@ -36,10 +37,10 @@
>
<span>{{ $t('post_status.scope_notice.public') }}</span>
<a
- class="button-icon dismiss"
+ class="fa-scale-110 fa-old-padding dismiss"
@click.prevent="dismissScopeNotice()"
>
- <i class="icon-cancel" />
+ <FAIcon icon="times" />
</a>
</p>
<p
@@ -48,10 +49,10 @@
>
<span>{{ $t('post_status.scope_notice.unlisted') }}</span>
<a
- class="button-icon dismiss"
+ class="fa-scale-110 fa-old-padding dismiss"
@click.prevent="dismissScopeNotice()"
>
- <i class="icon-cancel" />
+ <FAIcon icon="times" />
</a>
</p>
<p
@@ -60,10 +61,10 @@
>
<span>{{ $t('post_status.scope_notice.private') }}</span>
<a
- class="button-icon dismiss"
+ class="fa-scale-110 fa-old-padding dismiss"
@click.prevent="dismissScopeNotice()"
>
- <i class="icon-cancel" />
+ <FAIcon icon="times" />
</a>
</p>
<p
@@ -82,12 +83,18 @@
@click.stop.prevent="togglePreview"
>
{{ $t('post_status.preview') }}
- <i :class="showPreview ? 'icon-left-open' : 'icon-right-open'" />
+ <FAIcon :icon="showPreview ? 'chevron-left' : 'chevron-right'" />
</a>
- <i
+ <div
v-show="previewLoading"
- class="icon-spin3 animate-spin"
- />
+ class="preview-spinner"
+ >
+ <FAIcon
+ class="fa-old-padding"
+ spin
+ icon="circle-notch"
+ />
+ </div>
</div>
<div
v-if="showPreview"
@@ -122,7 +129,8 @@
v-model="newStatus.spoilerText"
type="text"
:placeholder="$t('post_status.content_warning')"
- :disabled="posting"
+ :disabled="posting && !optimisticPosting"
+ size="1"
class="form-post-subject"
>
</EmojiInput>
@@ -147,7 +155,7 @@
:placeholder="placeholder || $t('post_status.default')"
rows="1"
cols="1"
- :disabled="posting"
+ :disabled="posting && !optimisticPosting"
class="form-post-body"
:class="{ 'scrollable-form': !!maxHeight }"
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
@@ -198,7 +206,10 @@
{{ $t(`post_status.content_type["${postFormat}"]`) }}
</option>
</select>
- <i class="icon-down-open" />
+ <FAIcon
+ class="select-down-icon"
+ icon="chevron-down"
+ />
</label>
</div>
<div
@@ -235,22 +246,22 @@
<div
class="emoji-icon"
>
- <i
+ <div
:title="$t('emoji.add_emoji')"
- class="icon-smile btn btn-default"
+ class="btn btn-default"
@click="showEmojiPicker"
- />
+ >
+ <FAIcon icon="smile-beam" />
+ </div>
</div>
<div
v-if="pollsAvailable"
class="poll-icon"
:class="{ selected: pollFormVisible }"
+ :title="$t('polls.add_poll')"
+ @click="togglePollForm"
>
- <i
- :title="$t('polls.add_poll')"
- class="icon-chart-bar btn btn-default"
- @click="togglePollForm"
- />
+ <FAIcon icon="poll-h" />
</div>
</div>
<button
@@ -283,8 +294,9 @@
class="alert error"
>
Error: {{ error }}
- <i
- class="button-icon icon-cancel"
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
@click="clearError"
/>
</div>
@@ -294,8 +306,9 @@
:key="file.url"
class="media-upload-wrapper"
>
- <i
- class="fa button-icon icon-cancel"
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
@click="removeMediaFile(file)"
/>
<attachment
@@ -375,24 +388,19 @@
}
.preview-heading {
- padding-left: 0.5em;
display: flex;
- width: 100%;
-
- .icon-spin3 {
- margin-left: auto;
- }
+ padding-left: 0.5em;
}
.preview-toggle {
- display: flex;
+ flex: 1;
cursor: pointer;
user-select: none;
&:hover {
text-decoration: underline;
}
- i {
+ svg, i {
margin-left: 0.2em;
font-size: 0.8em;
transform: rotate(90deg);
@@ -434,18 +442,20 @@
.media-upload-icon, .poll-icon, .emoji-icon {
font-size: 26px;
+ line-height: 1.1;
flex: 1;
+ padding: 0 0.1em;
&.selected, &:hover {
// needs to be specific to override icon default color
- i, label {
+ svg, i, label {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
}
}
&.disabled {
- i {
+ svg, i {
cursor: not-allowed;
color: $fallback--icon;
color: var(--btnDisabledText, $fallback--icon);
@@ -474,7 +484,7 @@
text-align: right;
}
- .icon-chart-bar {
+ .poll-icon {
cursor: pointer;
}
@@ -487,19 +497,6 @@
margin-bottom: .5em;
width: 18em;
- .icon-cancel {
- display: inline-block;
- position: static;
- margin: 0;
- padding-bottom: 0;
- margin-left: $fallback--attachmentRadius;
- margin-left: var(--attachmentRadius, $fallback--attachmentRadius);
- background-color: $fallback--fg;
- background-color: var(--btn, $fallback--fg);
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
- }
-
img, video {
object-fit: contain;
max-height: 10em;
@@ -522,7 +519,7 @@
flex-direction: column;
}
- .media-upload-wrapper .attachments {
+ .attachments .media-upload-wrapper {
padding: 0 0.5em;
.attachment {
@@ -531,14 +528,18 @@
position: relative;
}
- i {
+ .fa-scale-110 fa-old-padding {
position: absolute;
margin: 10px;
- padding: 5px;
+ margin: .75em;
+ padding: .5em;
background: rgba(230,230,230,0.6);
+ z-index: 2;
+ color: black;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
font-weight: bold;
+ cursor: pointer;
}
}
@@ -612,11 +613,6 @@
cursor: not-allowed;
}
- .icon-cancel {
- cursor: pointer;
- z-index: 4;
- }
-
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 0.6; }
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
index abcf0455..de0df70c 100644
--- a/src/components/react_button/react_button.js
+++ b/src/components/react_button/react_button.js
@@ -1,5 +1,8 @@
import Popover from '../popover/popover.vue'
-import { mapGetters } from 'vuex'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
+
+library.add(faSmileBeam)
const ReactButton = {
props: ['status'],
@@ -29,13 +32,23 @@ const ReactButton = {
emojis () {
if (this.filterWord !== '') {
const filterWordLowercase = this.filterWord.toLowerCase()
- return this.$store.state.instance.emoji.filter(emoji =>
- emoji.displayText.toLowerCase().includes(filterWordLowercase)
- )
+ let orderedEmojiList = []
+ for (const emoji of this.$store.state.instance.emoji) {
+ const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase)
+ if (indexOfFilterWord > -1) {
+ if (!Array.isArray(orderedEmojiList[indexOfFilterWord])) {
+ orderedEmojiList[indexOfFilterWord] = []
+ }
+ orderedEmojiList[indexOfFilterWord].push(emoji)
+ }
+ }
+ return orderedEmojiList.flat()
}
return this.$store.state.instance.emoji || []
},
- ...mapGetters(['mergedConfig'])
+ mergedConfig () {
+ return this.$store.getters.mergedConfig
+ }
}
}
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
index 0b34add1..95d95b11 100644
--- a/src/components/react_button/react_button.vue
+++ b/src/components/react_button/react_button.vue
@@ -12,6 +12,7 @@
<div class="reaction-picker-filter">
<input
v-model="filterWord"
+ size="1"
:placeholder="$t('emoji.search_emoji')"
>
</div>
@@ -36,9 +37,10 @@
<div class="reaction-bottom-fader" />
</div>
</div>
- <i
+ <FAIcon
slot="trigger"
- class="icon-smile button-icon add-reaction-button"
+ class="fa-scale-110 fa-old-padding add-reaction-button"
+ :icon="['far', 'smile-beam']"
:title="$t('tool_tip.add_reaction')"
/>
</Popover>
diff --git a/src/components/reply_button/reply_button.js b/src/components/reply_button/reply_button.js
new file mode 100644
index 00000000..c7bd2a2b
--- /dev/null
+++ b/src/components/reply_button/reply_button.js
@@ -0,0 +1,16 @@
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faReply } from '@fortawesome/free-solid-svg-icons'
+
+library.add(faReply)
+
+const ReplyButton = {
+ name: 'ReplyButton',
+ props: ['status', 'replying'],
+ computed: {
+ loggedIn () {
+ return !!this.$store.state.users.currentUser
+ }
+ }
+}
+
+export default ReplyButton
diff --git a/src/components/reply_button/reply_button.vue b/src/components/reply_button/reply_button.vue
new file mode 100644
index 00000000..a0ac8941
--- /dev/null
+++ b/src/components/reply_button/reply_button.vue
@@ -0,0 +1,39 @@
+<template>
+ <div>
+ <FAIcon
+ v-if="loggedIn"
+ class="ReplyButton fa-scale-110 fa-old-padding -interactive"
+ icon="reply"
+ :title="$t('tool_tip.reply')"
+ :class="{'-active': replying}"
+ @click.prevent="$emit('toggle')"
+ />
+ <FAIcon
+ v-else
+ icon="reply"
+ class="ReplyButton fa-scale-110 fa-old-padding"
+ :title="$t('tool_tip.reply')"
+ />
+ <span v-if="status.replies_count > 0">
+ {{ status.replies_count }}
+ </span>
+ </div>
+</template>
+
+<script src="./reply_button.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.ReplyButton {
+ &.-interactive {
+ cursor: pointer;
+
+ &:hover,
+ &.-active {
+ color: $fallback--cBlue;
+ color: var(--cBlue, $fallback--cBlue);
+ }
+ }
+}
+</style>
diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js
index d9a0f92e..5ee4179a 100644
--- a/src/components/retweet_button/retweet_button.js
+++ b/src/components/retweet_button/retweet_button.js
@@ -1,4 +1,7 @@
-import { mapGetters } from 'vuex'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faRetweet } from '@fortawesome/free-solid-svg-icons'
+
+library.add(faRetweet)
const RetweetButton = {
props: ['status', 'loggedIn', 'visibility'],
@@ -23,12 +26,12 @@ const RetweetButton = {
computed: {
classes () {
return {
- 'retweeted': this.status.repeated,
- 'retweeted-empty': !this.status.repeated,
- 'animate-spin': this.animated
+ '-repeated': this.status.repeated
}
},
- ...mapGetters(['mergedConfig'])
+ mergedConfig () {
+ return this.$store.getters.mergedConfig
+ }
}
}
diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue
index 074f7747..b234f3d9 100644
--- a/src/components/retweet_button/retweet_button.vue
+++ b/src/components/retweet_button/retweet_button.vue
@@ -1,26 +1,30 @@
<template>
<div v-if="loggedIn">
<template v-if="visibility !== 'private' && visibility !== 'direct'">
- <i
+ <FAIcon
:class="classes"
- class="button-icon retweet-button icon-retweet rt-active"
+ class="RetweetButton fa-scale-110 fa-old-padding -interactive"
+ icon="retweet"
+ :spin="animated"
:title="$t('tool_tip.repeat')"
@click.prevent="retweet()"
/>
<span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span>
</template>
<template v-else>
- <i
+ <FAIcon
:class="classes"
- class="button-icon icon-lock"
+ class="RetweetButton fa-scale-110 fa-old-padding"
+ icon="lock"
:title="$t('timeline.no_retweet_hint')"
/>
</template>
</div>
<div v-else-if="!loggedIn">
- <i
+ <FAIcon
:class="classes"
- class="button-icon icon-retweet"
+ class="fa-scale-110 fa-old-padding"
+ icon="retweet"
:title="$t('tool_tip.repeat')"
/>
<span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span>
@@ -31,16 +35,21 @@
<style lang="scss">
@import '../../_variables.scss';
-.rt-active {
- cursor: pointer;
- animation-duration: 0.6s;
- &:hover {
+
+.RetweetButton {
+ &.-interactive {
+ cursor: pointer;
+ animation-duration: 0.6s;
+
+ &:hover {
+ color: $fallback--cGreen;
+ color: var(--cGreen, $fallback--cGreen);
+ }
+ }
+
+ &.-repeated {
color: $fallback--cGreen;
color: var(--cGreen, $fallback--cGreen);
}
}
-.icon-retweet.retweeted {
- color: $fallback--cGreen;
- color: var(--cGreen, $fallback--cGreen);
-}
</style>
diff --git a/src/components/scope_selector/scope_selector.js b/src/components/scope_selector/scope_selector.js
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..a22a4fda 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
+ <span
v-if="showDirect"
- class="icon-mail-alt"
+ class="scope"
:class="css.direct"
:title="$t('post_status.scope.direct')"
@click="changeVis('direct')"
- />
- <i
+ >
+ <FAIcon
+ icon="envelope"
+ class="fa-scale-110 fa-old-padding"
+ />
+ </span>
+ <span
v-if="showPrivate"
- class="icon-lock"
+ class="scope"
:class="css.private"
:title="$t('post_status.scope.private')"
@click="changeVis('private')"
- />
- <i
+ >
+ <FAIcon
+ icon="lock"
+ class="fa-scale-110 fa-old-padding"
+ />
+ </span>
+ <span
v-if="showUnlisted"
- class="icon-lock-open-alt"
+ class="scope"
:class="css.unlisted"
:title="$t('post_status.scope.unlisted')"
@click="changeVis('unlisted')"
- />
- <i
+ >
+ <FAIcon
+ icon="lock-open"
+ class="fa-scale-110 fa-old-padding"
+ />
+ </span>
+ <span
v-if="showPublic"
- class="icon-globe"
+ class="scope"
:class="css.public"
:title="$t('post_status.scope.public')"
@click="changeVis('public')"
- />
+ >
+ <FAIcon
+ icon="globe"
+ class="fa-scale-110 fa-old-padding"
+ />
+ </span>
</div>
</template>
@@ -39,12 +59,16 @@
<style lang="scss">
@import '../../_variables.scss';
-.scope-selector {
- i {
- font-size: 1.2em;
+.ScopeSelector {
+
+ .scope {
+ display: inline-block;
cursor: pointer;
+ min-width: 1.3em;
+ min-height: 1.3em;
+ text-align: center;
- &.selected {
+ &.selected svg {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
}
diff --git a/src/components/search/search.js b/src/components/search/search.js
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..665390f9 100644
--- a/src/components/search/search.vue
+++ b/src/components/search/search.vue
@@ -17,14 +17,18 @@
class="btn search-button"
@click="newQuery(searchTerm)"
>
- <i class="icon-search" />
+ <FAIcon icon="search" />
</button>
</div>
<div
v-if="loading"
class="text-center loading-icon"
>
- <i class="icon-spin3 animate-spin" />
+ <FAIcon
+ icon="circle-notch"
+ spin
+ size="lg"
+ />
</div>
<div v-else-if="loaded">
<div class="search-nav-heading">
diff --git a/src/components/search_bar/search_bar.js b/src/components/search_bar/search_bar.js
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..89a601c8 100644
--- a/src/components/search_bar/search_bar.vue
+++ b/src/components/search_bar/search_bar.vue
@@ -1,40 +1,47 @@
<template>
- <div>
- <div class="search-bar-container">
- <i
- v-if="loading"
- class="icon-spin4 finder-icon animate-spin-slow"
- />
- <a
- v-if="hidden"
- href="#"
- :title="$t('nav.search')"
- ><i
- class="button-icon icon-search"
- @click.prevent.stop="toggleHidden"
- /></a>
- <template v-else>
- <input
- id="search-bar-input"
- ref="searchInput"
- v-model="searchTerm"
- class="search-bar-input"
- :placeholder="$t('nav.search')"
- type="text"
- @keyup.enter="find(searchTerm)"
- >
- <button
- class="btn search-button"
- @click="find(searchTerm)"
- >
- <i class="icon-search" />
- </button>
- <i
- class="button-icon icon-cancel"
+ <div
+ class="SearchBar"
+ :class="{ '-expanded': !hidden }"
+ >
+ <a
+ v-if="hidden"
+ href="#"
+ class="nav-icon"
+ :title="$t('nav.search')"
+ ><FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="search"
+ @click.prevent.stop="toggleHidden"
+ /></a>
+ <template v-else>
+ <input
+ id="search-bar-input"
+ ref="searchInput"
+ v-model="searchTerm"
+ class="search-bar-input"
+ :placeholder="$t('nav.search')"
+ type="text"
+ @keyup.enter="find(searchTerm)"
+ >
+ <button
+ class="btn search-button"
+ @click="find(searchTerm)"
+ >
+ <FAIcon
+ fixed-width
+ icon="search"
+ />
+ </button>
+ <span>
+ <FAIcon
+ fixed-width
+ icon="times"
+ class="cancel-icon fa-scale-110 fa-old-padding"
@click.prevent.stop="toggleHidden"
/>
- </template>
- </div>
+ </span>
+ </template>
</div>
</template>
@@ -43,30 +50,29 @@
<style lang="scss">
@import '../../_variables.scss';
-.search-bar-container {
- max-width: 100%;
+.SearchBar {
display: inline-flex;
align-items: baseline;
vertical-align: baseline;
justify-content: flex-end;
+ &.-expanded {
+ width: 100%;
+ }
+
.search-bar-input,
.search-button {
height: 29px;
}
.search-bar-input {
- // TODO: do this properly without a rough guesstimate of 2 icons + paddings
- max-width: calc(100% - 30px - 30px - 20px);
- }
-
- .search-button {
- margin-left: .5em;
- margin-right: .5em;
+ flex: 1 0 auto;
}
- .icon-cancel {
+ .cancel-icon {
cursor: pointer;
+ color: $fallback--text;
+ color: var(--btnTopBarText, $fallback--text);
}
}
diff --git a/src/components/settings_modal/settings_modal_content.js b/src/components/settings_modal/settings_modal_content.js
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/data_import_export_tab.js b/src/components/settings_modal/tabs/data_import_export_tab.js
index 168f89e1..f4b736d2 100644
--- a/src/components/settings_modal/tabs/data_import_export_tab.js
+++ b/src/components/settings_modal/tabs/data_import_export_tab.js
@@ -1,6 +1,7 @@
import Importer from 'src/components/importer/importer.vue'
import Exporter from 'src/components/exporter/exporter.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
+import { mapState } from 'vuex'
const DataImportExportTab = {
data () {
@@ -18,21 +19,26 @@ const DataImportExportTab = {
Checkbox
},
computed: {
- user () {
- return this.$store.state.users.currentUser
- }
+ ...mapState({
+ backendInteractor: (state) => state.api.backendInteractor,
+ user: (state) => state.users.currentUser
+ })
},
methods: {
getFollowsContent () {
- return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
+ return this.backendInteractor.exportFriends({ id: this.user.id })
.then(this.generateExportableUsersContent)
},
getBlocksContent () {
- return this.$store.state.api.backendInteractor.fetchBlocks()
+ return this.backendInteractor.fetchBlocks()
+ .then(this.generateExportableUsersContent)
+ },
+ getMutesContent () {
+ return this.backendInteractor.fetchMutes()
.then(this.generateExportableUsersContent)
},
importFollows (file) {
- return this.$store.state.api.backendInteractor.importFollows({ file })
+ return this.backendInteractor.importFollows({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
@@ -40,7 +46,15 @@ const DataImportExportTab = {
})
},
importBlocks (file) {
- return this.$store.state.api.backendInteractor.importBlocks({ file })
+ return this.backendInteractor.importBlocks({ file })
+ .then((status) => {
+ if (!status) {
+ throw new Error('failed')
+ }
+ })
+ },
+ importMutes (file) {
+ return this.backendInteractor.importMutes({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
diff --git a/src/components/settings_modal/tabs/data_import_export_tab.vue b/src/components/settings_modal/tabs/data_import_export_tab.vue
index b5d0f5ed..a406077d 100644
--- a/src/components/settings_modal/tabs/data_import_export_tab.vue
+++ b/src/components/settings_modal/tabs/data_import_export_tab.vue
@@ -36,6 +36,23 @@
:export-button-label="$t('settings.block_export_button')"
/>
</div>
+ <div class="setting-item">
+ <h2>{{ $t('settings.mute_import') }}</h2>
+ <p>{{ $t('settings.import_mutes_from_a_csv_file') }}</p>
+ <Importer
+ :submit-handler="importMutes"
+ :success-message="$t('settings.mutes_imported')"
+ :error-message="$t('settings.mute_import_error')"
+ />
+ </div>
+ <div class="setting-item">
+ <h2>{{ $t('settings.mute_export') }}</h2>
+ <Exporter
+ :get-content="getMutesContent"
+ filename="mutes.csv"
+ :export-button-label="$t('settings.mute_export_button')"
+ />
+ </div>
</div>
</template>
diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js
index 3b2df556..5f38a5ae 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 Checkbox from 'src/components/checkbox/checkbox.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faChevronDown
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faChevronDown
+)
const FilteringTab = {
data () {
diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue
index eea41514..813dc4cd 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>
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index 0eb37e44..df592a10 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -2,6 +2,16 @@ import Checkbox from 'src/components/checkbox/checkbox.vue'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faChevronDown,
+ faGlobe
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faChevronDown,
+ faGlobe
+)
const GeneralTab = {
data () {
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 7f06d0bd..c1d0d0ec 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -58,6 +58,11 @@
{{ $t('settings.emoji_reactions_on_timeline') }}
</Checkbox>
</li>
+ <li>
+ <Checkbox v-model="virtualScrolling">
+ {{ $t('settings.virtual_scrolling') }}
+ </Checkbox>
+ </li>
</ul>
</div>
@@ -98,7 +103,10 @@
{{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }}
</option>
</select>
- <i class="icon-down-open" />
+ <FAIcon
+ class="select-down-icon"
+ icon="chevron-down"
+ />
</label>
</div>
</li>
@@ -122,7 +130,10 @@
{{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }}
</option>
</select>
- <i class="icon-down-open" />
+ <FAIcon
+ class="select-down-icon"
+ icon="chevron-down"
+ />
</label>
</div>
</li>
@@ -217,7 +228,7 @@
v-if="!loopSilentAvailable"
class="unavailable"
>
- <i class="icon-globe" />! {{ $t('settings.limited_availability') }}
+ <FAIcon icon="globe" />! {{ $t('settings.limited_availability') }}
</div>
</li>
</ul>
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index bd6bef6a..a3e4feaf 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 () {
diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss
index e14cf054..e821f952 100644
--- a/src/components/settings_modal/tabs/profile_tab.scss
+++ b/src/components/settings_modal/tabs/profile_tab.scss
@@ -119,10 +119,8 @@
&>.icon-container {
width: 20px;
-
- &>.icon-cancel {
- vertical-align: sub;
- }
+ align-self: center;
+ margin: 0 .2em .5em;
}
}
}
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index cf88c4e4..d62bc392 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -127,9 +127,9 @@
<div
class="icon-container"
>
- <i
+ <FAIcon
v-show="newFields.length > 1"
- class="icon-cancel"
+ icon="times"
@click="deleteField(i)"
/>
</div>
@@ -139,7 +139,7 @@
class="add-field faint"
@click="addField"
>
- <i class="icon-plus" />
+ <FAIcon icon="plus" />
{{ $t("settings.profile_fields.add_field") }}
</a>
</div>
@@ -166,10 +166,11 @@
:src="user.profile_image_url_original"
class="current-avatar"
>
- <i
+ <FAIcon
v-if="!isDefaultAvatar && pickAvatarBtnVisible"
:title="$t('settings.reset_avatar')"
- class="reset-button icon-cancel"
+ class="reset-button"
+ icon="times"
type="button"
@click="resetAvatar"
/>
@@ -194,10 +195,11 @@
<h2>{{ $t('settings.profile_banner') }}</h2>
<div class="banner-background-preview">
<img :src="user.cover_photo">
- <i
+ <FAIcon
v-if="!isDefaultBanner"
:title="$t('settings.reset_profile_banner')"
- class="reset-button icon-cancel"
+ class="reset-button"
+ icon="times"
type="button"
@click="resetBanner"
/>
@@ -214,9 +216,11 @@
@change="uploadFile('banner', $event)"
>
</div>
- <i
+ <FAIcon
v-if="bannerUploading"
- class=" icon-spin4 animate-spin uploading"
+ class="uploading"
+ spin
+ icon="circle-notch"
/>
<button
v-else-if="bannerPreview"
@@ -230,8 +234,9 @@
class="alert error"
>
Error: {{ bannerUploadError }}
- <i
- class="button-icon icon-cancel"
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
@click="clearUploadError('banner')"
/>
</div>
@@ -240,10 +245,11 @@
<h2>{{ $t('settings.profile_background') }}</h2>
<div class="banner-background-preview">
<img :src="user.background_image">
- <i
+ <FAIcon
v-if="!isDefaultBackground"
:title="$t('settings.reset_profile_background')"
- class="reset-button icon-cancel"
+ class="reset-button"
+ icon="times"
type="button"
@click="resetBackground"
/>
@@ -260,9 +266,11 @@
@change="uploadFile('background', $event)"
>
</div>
- <i
+ <FAIcon
v-if="backgroundUploading"
- class=" icon-spin4 animate-spin uploading"
+ class="uploading"
+ spin
+ icon="circle-notch"
/>
<button
v-else-if="backgroundPreview"
@@ -276,8 +284,10 @@
class="alert error"
>
Error: {{ backgroundUploadError }}
- <i
- class="button-icon icon-cancel"
+ <FAIcon
+ size="lg"
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
@click="clearUploadError('background')"
/>
</div>
diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue
index 9d984659..02fea0b6 100644
--- a/src/components/settings_modal/tabs/theme_tab/preview.vue
+++ b/src/components/settings_modal/tabs/theme_tab/preview.vue
@@ -39,21 +39,29 @@
</i18n>
<div class="icons">
- <i
+ <FAIcon
+ fixed-width
style="color: var(--cBlue)"
- class="button-icon icon-reply"
+ class="fa-scale-110 fa-old-padding"
+ icon="reply"
/>
- <i
+ <FAIcon
+ fixed-width
style="color: var(--cGreen)"
- class="button-icon icon-retweet"
+ class="fa-scale-110 fa-old-padding"
+ icon="retweet"
/>
- <i
+ <FAIcon
+ fixed-width
style="color: var(--cOrange)"
- class="button-icon icon-star"
+ class="fa-scale-110 fa-old-padding"
+ icon="star"
/>
- <i
+ <FAIcon
+ fixed-width
style="color: var(--cRed)"
- class="button-icon icon-cancel"
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
/>
</div>
</div>
@@ -103,6 +111,25 @@
</div>
</template>
+<script>
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faTimes,
+ faStar,
+ faRetweet,
+ faReply
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faTimes,
+ faStar,
+ faRetweet,
+ faReply
+)
+
+export default {}
+</script>
+
<style lang="scss">
.preview-container {
position: relative;
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
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.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
index 5328c350..280e1955 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
@@ -80,7 +80,10 @@
{{ style[0] || style.name }}
</option>
</select>
- <i class="icon-down-open" />
+ <FAIcon
+ class="select-down-icon"
+ icon="chevron-down"
+ />
</label>
</div>
</template>
@@ -907,7 +910,10 @@
{{ $t('settings.style.shadows.components.' + shadow) }}
</option>
</select>
- <i class="icon-down-open" />
+ <FAIcon
+ class="select-down-icon"
+ icon="chevron-down"
+ />
</label>
</div>
<div class="override">
diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js
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..78f0e544 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"
:disabled="!ready || !present"
@click="del"
>
- <i class="icon-cancel" />
+ <FAIcon
+ fixed-width
+ icon="times"
+ />
</button>
<button
class="btn btn-default"
:disabled="!moveUpValid"
@click="moveUp"
>
- <i class="icon-up-open" />
+ <FAIcon
+ fixed-width
+ icon="chevron-up"
+ />
</button>
<button
class="btn btn-default"
:disabled="!moveDnValid"
@click="moveDn"
>
- <i class="icon-down-open" />
+ <FAIcon
+ fixed-width
+ icon="chevron-down"
+ />
</button>
<button
class="btn btn-default"
:disabled="usingFallback"
@click="add"
>
- <i class="icon-plus" />
+ <FAIcon
+ fixed-width
+ icon="plus"
+ />
</button>
</div>
<div
diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js
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..28c888fe 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>
@@ -90,7 +110,11 @@
@click="toggleDrawer"
>
<router-link :to="{ name: 'chat' }">
- <i class="button-icon icon-megaphone" /> {{ $t("shoutbox.title") }}
+ <FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="bullhorn"
+ /> {{ $t("shoutbox.title") }}
</router-link>
</li>
</ul>
@@ -100,7 +124,11 @@
@click="toggleDrawer"
>
<router-link :to="{ name: 'search' }">
- <i class="button-icon icon-search" /> {{ $t("nav.search") }}
+ <FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="search"
+ /> {{ $t("nav.search") }}
</router-link>
</li>
<li
@@ -108,7 +136,11 @@
@click="toggleDrawer"
>
<router-link :to="{ name: 'who-to-follow' }">
- <i class="button-icon icon-user-plus" /> {{ $t("nav.who_to_follow") }}
+ <FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="user-plus"
+ /> {{ $t("nav.who_to_follow") }}
</router-link>
</li>
<li @click="toggleDrawer">
@@ -116,12 +148,20 @@
href="#"
@click="openSettingsModal"
>
- <i class="button-icon icon-cog" /> {{ $t("settings.settings") }}
+ <FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="cog"
+ /> {{ $t("settings.settings") }}
</a>
</li>
<li @click="toggleDrawer">
<router-link :to="{ name: 'about'}">
- <i class="button-icon icon-info-circled" /> {{ $t("nav.about") }}
+ <FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="info-circle"
+ /> {{ $t("nav.about") }}
</router-link>
</li>
<li
@@ -132,7 +172,11 @@
href="/pleroma/admin/#/login-pleroma"
target="_blank"
>
- <i class="button-icon icon-gauge" /> {{ $t("nav.administration") }}
+ <FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="tachometer-alt"
+ /> {{ $t("nav.administration") }}
</a>
</li>
<li
@@ -143,7 +187,11 @@
href="#"
@click="doLogout"
>
- <i class="button-icon icon-logout" /> {{ $t("login.logout") }}
+ <FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="sign-out-alt"
+ /> {{ $t("login.logout") }}
</a>
</li>
</ul>
@@ -224,8 +272,10 @@
--lightText: var(--popoverLightText, $fallback--lightText);
--icon: var(--popoverIcon, $fallback--icon);
- .button-icon:before {
- width: 1.1em;
+ .badge {
+ position: absolute;
+ right: 0.7rem;
+ top: 1em;
}
}
@@ -272,7 +322,6 @@
border-bottom: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
- margin: 0.2em 0;
}
.side-drawer ul:last-child {
@@ -283,8 +332,11 @@
padding: 0;
a {
+ box-sizing: border-box;
display: block;
- padding: 0.5em 0.85em;
+ height: 3em;
+ line-height: 3em;
+ padding: 0 0.7em;
&:hover {
background-color: $fallback--lightBg;
diff --git a/src/components/status/status.js b/src/components/status/status.js
index d263da68..142e1fc6 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -1,3 +1,4 @@
+import ReplyButton from '../reply_button/reply_button.vue'
import FavoriteButton from '../favorite_button/favorite_button.vue'
import ReactButton from '../react_button/react_button.vue'
import RetweetButton from '../retweet_button/retweet_button.vue'
@@ -15,11 +16,48 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { muteWordHits } from '../../services/status_parser/status_parser.js'
import { unescape, uniqBy } from 'lodash'
-import { mapGetters, mapState } from 'vuex'
+
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faEnvelope,
+ faLock,
+ faLockOpen,
+ faGlobe,
+ faTimes,
+ faRetweet,
+ faReply,
+ faExternalLinkSquareAlt,
+ faPlusSquare,
+ faSmileBeam,
+ faEllipsisH,
+ faStar,
+ faEyeSlash,
+ faEye,
+ faThumbtack
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faEnvelope,
+ faGlobe,
+ faLock,
+ faLockOpen,
+ faTimes,
+ faRetweet,
+ faReply,
+ faExternalLinkSquareAlt,
+ faPlusSquare,
+ faStar,
+ faSmileBeam,
+ faEllipsisH,
+ faEyeSlash,
+ faEye,
+ faThumbtack
+)
const Status = {
name: 'Status',
components: {
+ ReplyButton,
FavoriteButton,
ReactButton,
RetweetButton,
@@ -54,6 +92,8 @@ const Status = {
replying: false,
unmuted: false,
userExpanded: false,
+ mediaPlaying: [],
+ suspendable: true,
error: null
}
},
@@ -157,7 +197,7 @@ const Status = {
return this.mergedConfig.hideFilteredStatuses
},
hideStatus () {
- return this.deleted || (this.muted && this.hideFilteredStatuses)
+ return (this.muted && this.hideFilteredStatuses) || this.virtualHidden
},
isFocused () {
// retweet or root of an expanded conversation
@@ -207,23 +247,30 @@ const Status = {
hidePostStats () {
return this.mergedConfig.hidePostStats
},
- ...mapGetters(['mergedConfig']),
- ...mapState({
- betterShadow: state => state.interface.browserSupport.cssFilter,
- currentUser: state => state.users.currentUser
- })
+ currentUser () {
+ return this.$store.state.users.currentUser
+ },
+ betterShadow () {
+ return this.$store.state.interface.browserSupport.cssFilter
+ },
+ mergedConfig () {
+ return this.$store.getters.mergedConfig
+ },
+ isSuspendable () {
+ return !this.replying && this.mediaPlaying.length === 0
+ }
},
methods: {
visibilityIcon (visibility) {
switch (visibility) {
case 'private':
- return 'icon-lock'
+ return 'lock'
case 'unlisted':
- return 'icon-lock-open-alt'
+ return 'lock-open'
case 'direct':
- return 'icon-mail-alt'
+ return 'envelope'
default:
- return 'icon-globe'
+ return 'globe'
}
},
showError (error) {
@@ -251,6 +298,12 @@ const Status = {
},
generateUserProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
+ },
+ addMediaPlaying (id) {
+ this.mediaPlaying.push(id)
+ },
+ removeMediaPlaying (id) {
+ this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id)
}
},
watch: {
@@ -280,6 +333,9 @@ const Status = {
if (this.isFocused && this.statusFromGlobalRepository.favoritedBy && this.statusFromGlobalRepository.favoritedBy.length !== num) {
this.$store.dispatch('fetchFavs', this.status.id)
}
+ },
+ 'isSuspendable': function (val) {
+ this.suspendable = val
}
},
filters: {
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index 8d292d3f..0a94de32 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 {
@@ -25,6 +26,23 @@ $status-margin: 0.75em;
--icon: var(--selectedPostIcon, $fallback--icon);
}
+ &.-conversation {
+ border-left-width: 4px;
+ border-left-style: solid;
+ }
+
+ .gravestone {
+ padding: $status-margin;
+ color: $fallback--faint;
+ color: var(--faint, $fallback--faint);
+ display: flex;
+
+ .deleted-text {
+ margin: 0.5em 0;
+ align-items: center;
+ }
+ }
+
.status-container {
display: flex;
padding: $status-margin;
@@ -41,6 +59,15 @@ $status-margin: 0.75em;
justify-content: flex-end;
}
+ ._misclick-prevention & {
+ pointer-events: none;
+
+ .attachments {
+ pointer-events: initial;
+ cursor: initial;
+ }
+ }
+
.left-side {
margin-right: $status-margin;
}
@@ -139,11 +166,6 @@ $status-margin: 0.75em;
text-overflow: ellipsis;
overflow-x: hidden;
}
-
- .icon-reply {
- // mirror the icon
- transform: scaleX(-1);
- }
}
& .reply-to-popover,
@@ -183,7 +205,6 @@ $status-margin: 0.75em;
}
.reply-to {
- display: flex;
position: relative;
}
@@ -191,7 +212,6 @@ $status-margin: 0.75em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- margin-left: 0.2em;
}
.replies-separator {
@@ -215,16 +235,10 @@ $status-margin: 0.75em;
.repeat-info {
padding: 0.4em $status-margin;
- line-height: 22px;
-
- .right-side {
- display: flex;
- align-content: center;
- flex-wrap: wrap;
- }
- i {
- padding: 0 0.2em;
+ .repeat-icon {
+ color: $fallback--cGreen;
+ color: var(--cGreen, $fallback--cGreen);
}
}
@@ -274,18 +288,6 @@ $status-margin: 0.75em;
}
}
- .button-reply {
- &:not(.-disabled) {
- cursor: pointer;
- }
-
- &:not(.-disabled):hover,
- &.-active {
- color: $fallback--cBlue;
- color: var(--cBlue, $fallback--cBlue);
- }
- }
-
.muted {
padding: 0.25em 0.6em;
height: 1.2em;
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 282ad37d..21412faa 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-csontainer muted">
+ <div class="status-container muted">
<small class="status-username">
- <i
+ <FAIcon
v-if="muted && retweet"
- class="button-icon icon-retweet"
+ class="fa-scale-110 fa-old-padding repeat-icon"
+ icon="retweet"
/>
<router-link :to="userProfileLink">
{{ status.user.screen_name }}
@@ -46,9 +49,14 @@
</small>
<a
href="#"
- class="unmute"
+ class="unmute fa-scale-110 fa-old-padding"
@click.prevent="toggleMute"
- ><i class="button-icon icon-eye-off" /></a>
+ >
+ <FAIcon
+ icon="eye-slash"
+ class="fa-scale-110 fa-old-padding"
+ />
+ </a>
</div>
</template>
<template v-else>
@@ -56,7 +64,10 @@
v-if="showPinned"
class="pin"
>
- <i class="fa icon-pin faint" />
+ <FAIcon
+ icon="thumbtack"
+ class="faint"
+ />
<span class="faint">{{ $t('status.pinned') }}</span>
</div>
<div
@@ -86,8 +97,9 @@
:to="retweeterProfileLink"
>{{ retweeter }}</router-link>
</span>
- <i
- class="fa icon-retweet retweeted"
+ <FAIcon
+ icon="retweet"
+ class="repeat-icon"
:title="$t('tool_tip.repeat')"
/>
{{ $t('timeline.repeated') }}
@@ -95,6 +107,7 @@
</div>
<div
+ v-if="!deleted"
:class="[userClass, { highlighted: userStyle, '-repeat': retweet && !inConversation }]"
:style="[ userStyle ]"
class="status-container"
@@ -166,15 +179,16 @@
:auto-update="60"
/>
</router-link>
- <div
+ <span
v-if="status.visibility"
- class="button-icon visibility-icon"
+ class="visibility-icon"
+ :title="status.visibility | capitalize"
>
- <i
- :class="visibilityIcon(status.visibility)"
- :title="status.visibility | capitalize"
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ :icon="visibilityIcon(status.visibility)"
/>
- </div>
+ </span>
<a
v-if="!status.is_local && !isPreview"
:href="status.external_url"
@@ -182,22 +196,32 @@
class="source_url"
title="Source"
>
- <i class="button-icon icon-link-ext-alt" />
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="external-link-square-alt"
+ />
+ </a>
+ <a
+ v-if="expandable && !isPreview"
+ href="#"
+ title="Expand"
+ @click.prevent="toggleExpanded"
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="plus-square"
+ />
</a>
- <template v-if="expandable && !isPreview">
- <a
- href="#"
- title="Expand"
- @click.prevent="toggleExpanded"
- >
- <i class="button-icon icon-plus-squared" />
- </a>
- </template>
<a
v-if="unmuted"
href="#"
@click.prevent="toggleMute"
- ><i class="button-icon icon-eye-off" /></a>
+ >
+ <FAIcon
+ icon="eye-slash"
+ class="fa-scale-110 fa-old-padding"
+ />
+ </a>
</span>
</div>
@@ -219,7 +243,11 @@
:aria-label="$t('tool_tip.reply')"
@click.prevent="gotoOriginal(status.in_reply_to_status_id)"
>
- <i class="button-icon reply-button icon-reply" />
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="reply"
+ flip="horizontal"
+ />
<span
class="faint-link reply-to-text"
>
@@ -227,6 +255,7 @@
</span>
</a>
</StatusPopover>
+
<span
v-else
class="reply-to-no-popover"
@@ -272,6 +301,8 @@
:no-heading="noHeading"
:highlight="highlight"
:focused="isFocused"
+ @mediaplay="addMediaPlaying($event)"
+ @mediapause="removeMediaPlaying($event)"
/>
<transition name="fade">
@@ -320,21 +351,11 @@
v-if="!noHeading && !isPreview"
class="status-actions"
>
- <div>
- <i
- v-if="loggedIn"
- class="button-icon button-reply icon-reply"
- :title="$t('tool_tip.reply')"
- :class="{'-active': replying}"
- @click.prevent="toggleReplying"
- />
- <i
- v-else
- class="button-icon button-reply -disabled icon-reply"
- :title="$t('tool_tip.reply')"
- />
- <span v-if="status.replies_count > 0">{{ status.replies_count }}</span>
- </div>
+ <reply-button
+ :replying="replying"
+ :status="status"
+ @toggle="toggleReplying"
+ />
<retweet-button
:visibility="status.visibility"
:logged-in="loggedIn"
@@ -357,6 +378,25 @@
</div>
</div>
<div
+ v-else
+ class="gravestone"
+ >
+ <div class="left-side">
+ <UserAvatar :compact="compact" />
+ </div>
+ <div class="right-side">
+ <div class="deleted-text">
+ {{ $t('status.status_deleted') }}
+ </div>
+ <reply-button
+ v-if="replying"
+ :replying="replying"
+ :status="status"
+ @toggle="toggleReplying"
+ />
+ </div>
+ </div>
+ <div
v-if="replying"
class="status-container reply-form"
>
@@ -376,4 +416,5 @@
</template>
<script src="./status.js" ></script>
+
<style src="./status.scss" lang="scss"></style>
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
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 76fe3278..321cd477 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -55,29 +55,29 @@
@click.prevent="toggleShowMore"
>
{{ $t("status.show_content") }}
- <span
+ <FAIcon
v-if="attachmentTypes.includes('image')"
- class="icon-picture"
+ icon="image"
/>
- <span
+ <FAIcon
v-if="attachmentTypes.includes('video')"
- class="icon-video"
+ icon="video"
/>
- <span
+ <FAIcon
v-if="attachmentTypes.includes('audio')"
- class="icon-music"
+ icon="music"
/>
- <span
+ <FAIcon
v-if="attachmentTypes.includes('unknown')"
- class="icon-doc"
+ icon="file"
/>
- <span
+ <FAIcon
v-if="status.poll && status.poll.options"
- class="icon-chart-bar"
+ icon="poll-h"
/>
- <span
+ <FAIcon
v-if="status.card"
- class="icon-link"
+ icon="link"
/>
</a>
<a
@@ -107,6 +107,8 @@
:attachment="attachment"
:allow-play="true"
:set-media="setMedia()"
+ @play="$emit('mediaplay', attachment.id)"
+ @pause="$emit('mediapause', attachment.id)"
/>
<gallery
v-if="galleryAttachments.length > 0"
@@ -180,6 +182,10 @@ $status-margin: 0.75em;
text-align: center;
display: inline-block;
word-break: break-all;
+
+ svg {
+ color: inherit;
+ }
}
img, video {
diff --git a/src/components/status_popover/status_popover.js b/src/components/status_popover/status_popover.js
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.js b/src/components/still-image/still-image.js
index ab40bbd7..8044e994 100644
--- a/src/components/still-image/still-image.js
+++ b/src/components/still-image/still-image.js
@@ -19,14 +19,16 @@ const StillImage = {
},
methods: {
onLoad () {
- this.imageLoadHandler && this.imageLoadHandler(this.$refs.src)
+ const image = this.$refs.src
+ if (!image) return
+ this.imageLoadHandler && this.imageLoadHandler(image)
const canvas = this.$refs.canvas
if (!canvas) return
- const width = this.$refs.src.naturalWidth
- const height = this.$refs.src.naturalHeight
+ const width = image.naturalWidth
+ const height = image.naturalHeight
canvas.width = width
canvas.height = height
- canvas.getContext('2d').drawImage(this.$refs.src, 0, 0, width, height)
+ canvas.getContext('2d').drawImage(image, 0, 0, width, height)
},
onError () {
this.imageLoadError && this.imageLoadError()
diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue
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..6e6e8193 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'
@@ -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/timeline/timeline.js b/src/components/timeline/timeline.js
index 5a7f7a78..cba46daf 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 = []
@@ -33,7 +39,9 @@ const Timeline = {
return {
paused: false,
unfocused: false,
- bottomedOut: false
+ bottomedOut: false,
+ virtualScrollIndex: 0,
+ blockingClicks: false
}
},
components: {
@@ -63,8 +71,10 @@ const Timeline = {
}
},
classes () {
+ let rootClasses = !this.embedded ? ['panel', 'panel-default'] : []
+ if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention'])
return {
- root: ['timeline'].concat(!this.embedded ? ['panel', 'panel-default'] : []),
+ root: rootClasses,
header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading'] : []),
body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []),
footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : [])
@@ -78,6 +88,16 @@ const Timeline = {
},
pinnedStatusIdsObject () {
return keyBy(this.pinnedStatusIds)
+ },
+ statusesToDisplay () {
+ const amount = this.timeline.visibleStatuses.length
+ const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80))
+ const min = Math.max(0, this.virtualScrollIndex - statusesPerSide)
+ const max = Math.min(amount, this.virtualScrollIndex + statusesPerSide)
+ return this.timeline.visibleStatuses.slice(min, max).map(_ => _.id)
+ },
+ virtualScrollingEnabled () {
+ return this.$store.getters.mergedConfig.virtualScrolling
}
},
created () {
@@ -85,7 +105,7 @@ const Timeline = {
const credentials = store.state.users.currentUser.credentials
const showImmediately = this.timeline.visibleStatuses.length === 0
- window.addEventListener('scroll', this.scrollLoad)
+ window.addEventListener('scroll', this.handleScroll)
if (store.state.api.fetchers[this.timelineName]) { return false }
@@ -104,14 +124,24 @@ const Timeline = {
this.unfocused = document.hidden
}
window.addEventListener('keydown', this.handleShortKey)
+ setTimeout(this.determineVisibleStatuses, 250)
},
destroyed () {
- window.removeEventListener('scroll', this.scrollLoad)
+ window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('keydown', this.handleShortKey)
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
},
methods: {
+ stopBlockingClicks: debounce(function () {
+ this.blockingClicks = false
+ }, 1000),
+ blockClicksTemporarily () {
+ if (!this.blockingClicks) {
+ this.blockingClicks = true
+ }
+ this.stopBlockingClicks()
+ },
handleShortKey (e) {
// Ignore when input fields are focused
if (['textarea', 'input'].includes(e.target.tagName.toLowerCase())) return
@@ -123,6 +153,7 @@ const Timeline = {
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
this.fetchOlderStatuses()
} else {
+ this.blockClicksTemporarily()
this.$store.commit('showNewStatuses', { timeline: this.timelineName })
this.paused = false
}
@@ -146,6 +177,48 @@ const Timeline = {
}
})
}, 1000, this),
+ determineVisibleStatuses () {
+ if (!this.$refs.timeline) return
+ if (!this.virtualScrollingEnabled) return
+
+ const statuses = this.$refs.timeline.children
+ const cappedScrollIndex = Math.max(0, Math.min(this.virtualScrollIndex, statuses.length - 1))
+
+ if (statuses.length === 0) return
+
+ const height = Math.max(document.body.offsetHeight, window.pageYOffset)
+
+ const centerOfScreen = window.pageYOffset + (window.innerHeight * 0.5)
+
+ // Start from approximating the index of some visible status by using the
+ // the center of the screen on the timeline.
+ let approxIndex = Math.floor(statuses.length * (centerOfScreen / height))
+ let err = statuses[approxIndex].getBoundingClientRect().y
+
+ // if we have a previous scroll index that can be used, test if it's
+ // closer than the previous approximation, use it if so
+
+ const virtualScrollIndexY = statuses[cappedScrollIndex].getBoundingClientRect().y
+ if (Math.abs(err) > virtualScrollIndexY) {
+ approxIndex = cappedScrollIndex
+ err = virtualScrollIndexY
+ }
+
+ // if the status is too far from viewport, check the next/previous ones if
+ // they happen to be better
+ while (err < -20 && approxIndex < statuses.length - 1) {
+ err += statuses[approxIndex].offsetHeight
+ approxIndex++
+ }
+ while (err > window.innerHeight + 100 && approxIndex > 0) {
+ approxIndex--
+ err -= statuses[approxIndex].offsetHeight
+ }
+
+ // this status is now the center point for virtual scrolling and visible
+ // statuses will be nearby statuses before and after it
+ this.virtualScrollIndex = approxIndex
+ },
scrollLoad (e) {
const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -(bodyBRect.y))
@@ -155,6 +228,10 @@ const Timeline = {
this.fetchOlderStatuses()
}
},
+ handleScroll: throttle(function (e) {
+ this.determineVisibleStatuses()
+ this.scrollLoad(e)
+ }, 200),
handleVisibilityChange () {
this.unfocused = document.hidden
}
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 2ff933e9..04859852 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -1,5 +1,5 @@
<template>
- <div :class="[classes.root, 'timeline']">
+ <div :class="[classes.root, 'Timeline']">
<div :class="classes.header">
<TimelineMenu v-if="!embedded" />
<div
@@ -32,7 +32,10 @@
</div>
</div>
<div :class="classes.body">
- <div class="timeline">
+ <div
+ ref="timeline"
+ class="timeline"
+ >
<template v-for="statusId in pinnedStatusIds">
<conversation
v-if="timeline.statusesObject[statusId]"
@@ -54,6 +57,7 @@
:collapsable="true"
:in-profile="inProfile"
:profile-user-id="userId"
+ :virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.includes(status.id)"
/>
</template>
</div>
@@ -88,7 +92,11 @@
v-else
class="new-status-notification text-center panel-footer"
>
- <i class="icon-spin3 animate-spin" />
+ <FAIcon
+ icon="circle-notch"
+ spin
+ size="lg"
+ />
</div>
</div>
</div>
@@ -99,10 +107,14 @@
<style lang="scss">
@import '../../_variables.scss';
-.timeline {
+.Timeline {
.loadmore-text {
opacity: 1;
}
+
+ &.-blocked {
+ cursor: progress;
+ }
}
.timeline-heading {
diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js
index 2be75b06..4ccd52b4 100644
--- a/src/components/timeline_menu/timeline_menu.js
+++ b/src/components/timeline_menu/timeline_menu.js
@@ -1,5 +1,23 @@
import Popover from '../popover/popover.vue'
import { mapState } from 'vuex'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faUsers,
+ faGlobe,
+ faBookmark,
+ faEnvelope,
+ faHome,
+ faChevronDown
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faUsers,
+ faGlobe,
+ faBookmark,
+ faEnvelope,
+ faHome,
+ faChevronDown
+)
// Route -> i18n key mapping, exported andnot in the computed
// because nav panel benefits from the same information.
diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue
index be512d60..c46531be 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>
@@ -46,7 +66,10 @@
class="title timeline-menu-title"
>
<span>{{ timelineName() }}</span>
- <i class="icon-down-open" />
+ <FAIcon
+ size="sm"
+ icon="chevron-down"
+ />
</div>
</Popover>
</template>
@@ -56,17 +79,19 @@
<style lang="scss">
@import '../../_variables.scss';
-.timeline-menu {
+.TimelineMenu {
flex-shrink: 1;
margin-right: auto;
min-width: 0;
width: 24rem;
+
.timeline-menu-popover-wrap {
overflow: hidden;
// Match panel heading padding to line up menu with bottom of heading
margin-top: 0.6rem;
padding: 0 15px 15px 15px;
}
+
.timeline-menu-popover {
width: 24rem;
max-width: 100vw;
@@ -77,10 +102,12 @@
transform: translateY(-100%);
transition: transform 100ms;
}
+
.panel::after {
border-top-right-radius: 0;
border-top-left-radius: 0;
}
+
&.open .timeline-menu-popover {
transform: translateY(0);
}
@@ -88,7 +115,6 @@
.timeline-menu-title {
margin: 0;
cursor: pointer;
- display: flex;
user-select: none;
width: 100%;
@@ -98,15 +124,13 @@
white-space: nowrap;
}
- i {
+ svg {
margin-left: 0.6em;
- flex-shrink: 0;
- font-size: 1rem;
transition: transform 100ms;
}
}
- &.open .timeline-menu-title i {
+ &.open .timeline-menu-title svg {
color: $fallback--text;
color: var(--panelText, $fallback--text);
transform: rotate(180deg);
@@ -138,15 +162,11 @@
&:last-child {
border: none;
}
-
- i {
- margin: 0 0.5em;
- }
}
a {
display: block;
- padding: 0.6em 0;
+ padding: 0.6em 0.65em;
&:hover {
background-color: $fallback--lightBg;
@@ -174,6 +194,11 @@
text-decoration: underline;
}
}
+
+ svg {
+ margin-right: 0.4em;
+ margin-left: -0.2em;
+ }
}
}
diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue
index e4e4127c..0f7c584b 100644
--- a/src/components/user_avatar/user_avatar.vue
+++ b/src/components/user_avatar/user_avatar.vue
@@ -1,5 +1,6 @@
<template>
<StillImage
+ v-if="user"
class="Avatar"
:alt="user.screen_name"
:title="user.screen_name"
@@ -7,6 +8,11 @@
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
:image-load-error="imageLoadError"
/>
+ <div
+ v-else
+ class="Avatar -placeholder"
+ :class="{ 'avatar-compact': compact }"
+ />
</template>
<script src="./user_avatar.js"></script>
@@ -14,11 +20,14 @@
@import '../../_variables.scss';
.Avatar {
- --still-image-label-visibility: hidden;
+ --_avatarShadowBox: var(--avatarStatusShadow);
+ --_avatarShadowFilter: var(--avatarStatusShadowFilter);
+ --_avatarShadowInset: var(--avatarStatusShadowInset);
+ --_still-image-label-visibility: hidden;
width: 48px;
height: 48px;
- box-shadow: var(--avatarStatusShadow);
+ box-shadow: var(--_avatarShadowBox);
border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
@@ -28,8 +37,8 @@
}
&.better-shadow {
- box-shadow: var(--avatarStatusShadowInset);
- filter: var(--avatarStatusShadowFilter)
+ box-shadow: var(--_avatarShadowInset);
+ filter: var(--_avatarShadowFilter);
}
&.animated::before {
@@ -42,5 +51,10 @@
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
}
+
+ &.-placeholder {
+ background-color: $fallback--fg;
+ background-color: var(--fg, $fallback--fg);
+ }
}
</style>
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
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..f916af9d 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"
@@ -85,7 +92,13 @@
bot
</span>
</template>
- <span v-if="user.locked"><i class="icon icon-lock" /></span>
+ <span v-if="user.locked">
+ <FAIcon
+ class="lock-icon"
+ icon="lock"
+ size="sm"
+ />
+ </span>
<span
v-if="!mergedConfig.hideUserStats && !hideBio"
class="dailyAvg"
@@ -133,7 +146,10 @@
<option value="striped">Striped bg</option>
<option value="side">Side stripe</option>
</select>
- <i class="icon-down-open" />
+ <FAIcon
+ class="select-down-icon"
+ icon="chevron-down"
+ />
</label>
</div>
</div>
@@ -150,7 +166,7 @@
:click="subscribeUser"
:title="$t('user_card.subscribe')"
>
- <i class="icon-bell-alt" />
+ <FAIcon icon="bell" />
</ProgressButton>
<ProgressButton
v-else
@@ -158,7 +174,18 @@
:click="unsubscribeUser"
:title="$t('user_card.unsubscribe')"
>
- <i class="icon-bell-ringing-o" />
+ <FALayers>
+ <FAIcon
+ icon="rss"
+ transform="left-5 shrink-6 up-3 rotate-20"
+ flip="horizontal"
+ />
+ <FAIcon
+ icon="rss"
+ transform="right-5 shrink-6 up-3 rotate-20"
+ />
+ <FAIcon icon="bell" />
+ </FALayers>
</ProgressButton>
</template>
</div>
@@ -255,6 +282,11 @@
.user-card {
position: relative;
+ &:hover .Avatar {
+ --_still-image-img-visibility: visible;
+ --_still-image-canvas-visibility: hidden;
+ }
+
.panel-heading {
padding: .5em 0;
text-align: center;
@@ -283,7 +315,7 @@
mask: linear-gradient(to top, white, transparent) bottom no-repeat,
linear-gradient(to top, white, white);
// Autoprefixed seem to ignore this one, and also syntax is different
- -webkit-mask-composite: xor;
+ -webkit-mask-composite: xor;
mask-composite: exclude;
background-size: cover;
mask-size: 100% 60%;
@@ -355,20 +387,17 @@
max-height: 56px;
.Avatar {
+ --_avatarShadowBox: var(--avatarShadow);
+ --_avatarShadowFilter: var(--avatarShadowFilter);
+ --_avatarShadowInset: var(--avatarShadowInset);
+
flex: 1 0 100%;
width: 56px;
height: 56px;
- box-shadow: 0px 1px 8px rgba(0,0,0,0.75);
- box-shadow: var(--avatarShadow);
object-fit: cover;
}
}
- &:hover .Avatar {
- --still-image-img: visible;
- --still-image-canvas: hidden;
- }
-
&-avatar-link {
position: relative;
cursor: pointer;
@@ -388,7 +417,7 @@
opacity: 0;
transition: opacity .2s ease;
- i {
+ svg {
color: #FFF;
}
}
@@ -398,10 +427,17 @@
}
}
- .usersettings {
- color: $fallback--lightText;
- color: var(--lightText, $fallback--lightText);
- opacity: .8;
+ .external-link-button {
+ cursor: pointer;
+ width: 2.5em;
+ text-align: center;
+ margin: -0.5em 0;
+ padding: 0.5em 0;
+
+ &:not(:hover) .icon {
+ color: $fallback--lightText;
+ color: var(--lightText, $fallback--lightText);
+ }
}
.user-summary {
@@ -447,6 +483,10 @@
font-weight: light;
font-size: 15px;
+ .lock-icon {
+ margin-left: 0.5em;
+ }
+
.user-screen-name {
min-width: 1px;
flex: 0 1 auto;
@@ -508,7 +548,7 @@
padding-bottom: 0;
flex: 1 0 auto;
}
- .userHighlightSel.select i {
+ .userHighlightSel.select svg {
line-height: 22px;
}
diff --git a/src/components/user_list_popover/user_list_popover.js b/src/components/user_list_popover/user_list_popover.js
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 c7c67c0a..f1f51840 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -122,9 +122,10 @@
</div>
<div class="panel-body">
<span v-if="error">{{ error }}</span>
- <i
+ <FAIcon
v-else
- class="icon-spin3 animate-spin"
+ spin
+ icon="circle-notch"
/>
</div>
</div>
@@ -142,6 +143,7 @@
.user-profile-fields {
margin: 0 0.5em;
+
img {
object-fit: contain;
vertical-align: middle;
@@ -156,8 +158,7 @@
.user-profile-field {
display: flex;
- margin: 0.25em auto;
- max-width: 32em;
+ margin: 0.25em;
border: 1px solid var(--border, $fallback--border);
border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius);
diff --git a/src/components/video_attachment/video_attachment.js b/src/components/video_attachment/video_attachment.js
index f0ca7e89..107b8985 100644
--- a/src/components/video_attachment/video_attachment.js
+++ b/src/components/video_attachment/video_attachment.js
@@ -3,27 +3,48 @@ const VideoAttachment = {
props: ['attachment', 'controls'],
data () {
return {
- loopVideo: this.$store.getters.mergedConfig.loopVideo
+ blocksSuspend: false,
+ // Start from true because removing "loop" property seems buggy in Vue
+ hasAudio: true
+ }
+ },
+ computed: {
+ loopVideo () {
+ if (this.$store.getters.mergedConfig.loopVideoSilentOnly) {
+ return !this.hasAudio
+ }
+ return this.$store.getters.mergedConfig.loopVideo
}
},
methods: {
- onVideoDataLoad (e) {
+ onPlaying (e) {
+ this.setHasAudio(e)
+ if (this.loopVideo) {
+ this.$emit('play', { looping: true })
+ return
+ }
+ this.$emit('play')
+ },
+ onPaused (e) {
+ this.$emit('pause')
+ },
+ setHasAudio (e) {
const target = e.srcElement || e.target
+ // If hasAudio is false, we've already marked this video to not have audio,
+ // a video can't gain audio out of nowhere so don't bother checking again.
+ if (!this.hasAudio) return
if (typeof target.webkitAudioDecodedByteCount !== 'undefined') {
// non-zero if video has audio track
- if (target.webkitAudioDecodedByteCount > 0) {
- this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly
- }
- } else if (typeof target.mozHasAudio !== 'undefined') {
+ if (target.webkitAudioDecodedByteCount > 0) return
+ }
+ if (typeof target.mozHasAudio !== 'undefined') {
// true if video has audio track
- if (target.mozHasAudio) {
- this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly
- }
- } else if (typeof target.audioTracks !== 'undefined') {
- if (target.audioTracks.length > 0) {
- this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly
- }
+ if (target.mozHasAudio) return
+ }
+ if (typeof target.audioTracks !== 'undefined') {
+ if (target.audioTracks.length > 0) return
}
+ this.hasAudio = false
}
}
}
diff --git a/src/components/video_attachment/video_attachment.vue b/src/components/video_attachment/video_attachment.vue
index 1ffed4e0..a4bf01e8 100644
--- a/src/components/video_attachment/video_attachment.vue
+++ b/src/components/video_attachment/video_attachment.vue
@@ -7,7 +7,8 @@
:alt="attachment.description"
:title="attachment.description"
playsinline
- @loadeddata="onVideoDataLoad"
+ @playing="onPlaying"
+ @pause="onPaused"
/>
</template>
diff --git a/src/hocs/with_load_more/with_load_more.js b/src/hocs/with_load_more/with_load_more.js
index 6142f513..afb51a0f 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
@@ -82,7 +92,7 @@ const withLoadMore = ({
</WrappedComponent>
<div class="with-load-more-footer">
{this.error && <a onClick={this.fetchEntries} class="alert error">{this.$t('general.generic_error')}</a>}
- {!this.error && this.loading && <i class="icon-spin3 animate-spin"/>}
+ {!this.error && this.loading && <FAIcon spin icon="circle-notch"/>}
{!this.error && !this.loading && !this.bottomedOut && <a onClick={this.fetchEntries}>{this.$t('general.more')}</a>}
</div>
</div>
diff --git a/src/hocs/with_subscription/with_subscription.js b/src/hocs/with_subscription/with_subscription.js
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/ca.json b/src/i18n/ca.json
index f2bcdd06..b15b69f7 100644
--- a/src/i18n/ca.json
+++ b/src/i18n/ca.json
@@ -9,7 +9,8 @@
"scope_options": "Opcions d'abast i visibilitat",
"text_limit": "Límit de text",
"title": "Funcionalitats",
- "who_to_follow": "A qui seguir"
+ "who_to_follow": "A qui seguir",
+ "pleroma_chat_messages": "Xat de Pleroma"
},
"finder": {
"error_fetching_user": "No s'ha pogut carregar l'usuari/a",
@@ -17,7 +18,21 @@
},
"general": {
"apply": "Aplica",
- "submit": "Desa"
+ "submit": "Desa",
+ "close": "Tanca",
+ "verify": "Verifica",
+ "confirm": "Confirma",
+ "enable": "Habilita",
+ "disable": "Deshabilitar",
+ "cancel": "Cancel·la",
+ "show_less": "Mostra menys",
+ "show_more": "Mostra més",
+ "optional": "opcional",
+ "retry": "Prova de nou",
+ "error_retry": "Si us plau, prova de nou",
+ "generic_error": "Hi ha hagut un error",
+ "loading": "Carregant…",
+ "more": "Més"
},
"login": {
"login": "Inicia sessió",
@@ -25,7 +40,12 @@
"password": "Contrasenya",
"placeholder": "p.ex.: Maria",
"register": "Registra't",
- "username": "Nom d'usuari/a"
+ "username": "Nom d'usuari/a",
+ "recovery_code": "Codi de recuperació",
+ "enter_recovery_code": "Posa un codi de recuperació",
+ "authentication_code": "Codi d'autenticació",
+ "hint": "Entra per participar a la conversa",
+ "description": "Entra amb OAuth"
},
"nav": {
"chat": "Xat local públic",
@@ -33,7 +53,16 @@
"mentions": "Mencions",
"public_tl": "Flux públic del node",
"timeline": "Flux personal",
- "twkn": "Flux de la xarxa coneguda"
+ "twkn": "Flux de la xarxa coneguda",
+ "chats": "Xats",
+ "timelines": "Línies de temps",
+ "preferences": "Preferències",
+ "who_to_follow": "A qui seguir",
+ "search": "Cerca",
+ "dms": "Missatges directes",
+ "interactions": "Interaccions",
+ "back": "Enrere",
+ "administration": "Administració"
},
"notifications": {
"broken_favorite": "No es coneix aquest estat. S'està cercant.",
@@ -42,14 +71,19 @@
"load_older": "Carrega més notificacions",
"notifications": "Notificacions",
"read": "Read!",
- "repeated_you": "ha repetit el teu estat"
+ "repeated_you": "ha repetit el teu estat",
+ "migrated_to": "migrat a",
+ "no_more_notifications": "No més notificacions",
+ "follow_request": "et vol seguir"
},
"post_status": {
"account_not_locked_warning": "El teu compte no està {0}. Qualsevol persona pot seguir-te per llegir les teves entrades reservades només a seguidores.",
"account_not_locked_warning_link": "bloquejat",
"attachments_sensitive": "Marca l'adjunt com a delicat",
"content_type": {
- "text/plain": "Text pla"
+ "text/plain": "Text pla",
+ "text/markdown": "Markdown",
+ "text/html": "HTML"
},
"content_warning": "Assumpte (opcional)",
"default": "Em sento…",
@@ -60,7 +94,13 @@
"private": "Només seguidors/es - Publica només per comptes que et segueixin",
"public": "Pública - Publica als fluxos públics",
"unlisted": "Silenciosa - No la mostris en fluxos públics"
- }
+ },
+ "scope_notice": {
+ "private": "Aquesta entrada serà visible només per a qui et segueixi",
+ "public": "Aquesta entrada serà visible per a tothom"
+ },
+ "preview_empty": "Buida",
+ "preview": "Vista prèvia"
},
"registration": {
"bio": "Presentació",
@@ -68,7 +108,17 @@
"fullname": "Nom per mostrar",
"password_confirm": "Confirma la contrasenya",
"registration": "Registra't",
- "token": "Codi d'invitació"
+ "token": "Codi d'invitació",
+ "validations": {
+ "password_confirmation_match": "hauria de ser la mateixa que la contrasenya",
+ "password_confirmation_required": "no es pot deixar en blanc",
+ "password_required": "no es pot deixar en blanc",
+ "email_required": "no es pot deixar en blanc",
+ "fullname_required": "no es pot deixar en blanc",
+ "username_required": "no es pot deixar en blanc"
+ },
+ "fullname_placeholder": "p. ex. Lain Iwakura",
+ "username_placeholder": "p. ex. lain"
},
"settings": {
"attachmentRadius": "Adjunts",
@@ -94,7 +144,7 @@
"data_import_export_tab": "Importa o exporta dades",
"default_vis": "Abast per defecte de les entrades",
"delete_account": "Esborra el compte",
- "delete_account_description": "Esborra permanentment el teu compte i tots els missatges",
+ "delete_account_description": "Esborra permanentment les teves dades i desactiva el teu compte.",
"delete_account_error": "No s'ha pogut esborrar el compte. Si continua el problema, contacta amb l'administració del node.",
"delete_account_instructions": "Confirma que vols esborrar el compte escrivint la teva contrasenya aquí sota.",
"export_theme": "Desa el tema",
@@ -164,7 +214,57 @@
"values": {
"false": "no",
"true": "sí"
- }
+ },
+ "show_moderator_badge": "Mostra una insígnia de Moderació en el meu perfil",
+ "show_admin_badge": "Mostra una insígnia d'Administració en el meu perfil",
+ "hide_followers_description": "No mostris qui m'està seguint",
+ "hide_follows_description": "No mostris a qui segueixo",
+ "notification_visibility_emoji_reactions": "Reaccions",
+ "new_email": "Nou correu electrònic",
+ "profile_fields": {
+ "value": "Contingut",
+ "name": "Etiqueta",
+ "add_field": "Afegeix un camp",
+ "label": "Metadades del perfil"
+ },
+ "mutes_tab": "Silenciaments",
+ "interface": "Interfície",
+ "instance_default_simple": "(per defecte)",
+ "checkboxRadius": "Caselles",
+ "import_blocks_from_a_csv_file": "Importa bloquejos des d'un arxiu csv",
+ "hide_post_stats": "Amaga les estadístiques de les entrades (p. ex. el nombre de favorits)",
+ "use_one_click_nsfw": "Obre els adjunts NSFW amb només un clic",
+ "hide_muted_posts": "Amaga les entrades de comptes silenciats",
+ "avatar_size_instruction": "La mida mínima recomanada per la imatge de l'avatar és de 150x150 píxels.",
+ "domain_mutes": "Dominis",
+ "discoverable": "Permet la descoberta d'aquest compte en resultats de cerques i altres serveis",
+ "mutes_and_blocks": "Silenciaments i bloquejos",
+ "composing": "Composant",
+ "chatMessageRadius": "Missatge de xat",
+ "changed_email": "Correu electrònic canviat amb èxit!",
+ "change_email_error": "Hi ha hagut un problema al canviar el teu correu electrònic.",
+ "change_email": "Canvia el correu electrònic",
+ "bot": "Aquest és un compte automatitzat",
+ "blocks_tab": "Bloquejos",
+ "blocks_imported": "Bloquejos importats! Processar-los pot trigar una mica.",
+ "block_import_error": "Error al importar bloquejos",
+ "block_import": "Importa bloquejos",
+ "block_export_button": "Exporta els teus bloquejos a un arxiu csv",
+ "block_export": "Exporta bloquejos",
+ "allow_following_move": "Permet el seguiment automàtic quan un compte a qui seguim es mou",
+ "mfa": {
+ "scan": {
+ "secret_code": "Clau"
+ },
+ "authentication_methods": "Mètodes d'autenticació",
+ "waiting_a_recovery_codes": "Rebent còpies de seguretat dels codis…",
+ "recovery_codes": "Codis de recuperació.",
+ "warning_of_generate_new_codes": "Quan generes nous codis de recuperació, els antics ja no funcionaran més.",
+ "generate_new_recovery_codes": "Genera nous codis de recuperació"
+ },
+ "enter_current_password_to_confirm": "Posar la contrasenya actual per confirmar la teva identitat",
+ "security": "Seguretat",
+ "app_name": "Nom de l'aplicació"
},
"time": {
"day": "{0} dia",
@@ -232,5 +332,75 @@
"who_to_follow": {
"more": "More",
"who_to_follow": "A qui seguir"
+ },
+ "selectable_list": {
+ "select_all": "Selecciona-ho tot"
+ },
+ "remote_user_resolver": {
+ "error": "No trobat.",
+ "searching_for": "Cercant per"
+ },
+ "interactions": {
+ "load_older": "Carrega antigues interaccions",
+ "favs_repeats": "Repeticions i favorits"
+ },
+ "emoji": {
+ "stickers": "Adhesius"
+ },
+ "polls": {
+ "expired": "L'enquesta va acabar fa {0}",
+ "expires_in": "L'enquesta acaba en {0}",
+ "multiple_choices": "Múltiples opcions",
+ "single_choice": "Una sola opció",
+ "type": "Tipus d'enquesta",
+ "vote": "Vota",
+ "votes": "vots",
+ "option": "Opció",
+ "add_option": "Afegeix opció",
+ "add_poll": "Afegeix enquesta"
+ },
+ "media_modal": {
+ "next": "Següent",
+ "previous": "Anterior"
+ },
+ "importer": {
+ "error": "Ha succeït un error mentre s'importava aquest arxiu.",
+ "success": "Importat amb èxit."
+ },
+ "image_cropper": {
+ "cancel": "Cancel·la",
+ "save_without_cropping": "Desa sense retallar",
+ "save": "Desa",
+ "crop_picture": "Retalla la imatge"
+ },
+ "exporter": {
+ "processing": "Processant, aviat se't preguntarà per descarregar el teu arxiu",
+ "export": "Exporta"
+ },
+ "domain_mute_card": {
+ "mute_progress": "Silenciant…",
+ "mute": "Silencia"
+ },
+ "about": {
+ "staff": "Equip responsable",
+ "mrf": {
+ "simple": {
+ "quarantine_desc": "Aquesta instància només enviarà entrades públiques a les següents instàncies:",
+ "quarantine": "Quarantena",
+ "reject_desc": "Aquesta instància no acceptarà missatges de les següents instàncies:",
+ "reject": "Rebutja",
+ "accept_desc": "Aquesta instància només accepta missatges de les següents instàncies:",
+ "accept": "Accepta",
+ "simple_policies": "Polítiques específiques de la instància"
+ },
+ "mrf_policies_desc": "Les polítiques MRF controlen el comportament federat de la instància. Les següents polítiques estan habilitades:",
+ "mrf_policies": "Polítiques MRF habilitades",
+ "keyword": {
+ "replace": "Reemplaça",
+ "reject": "Rebutja",
+ "keyword_policies": "Polítiques de paraules clau"
+ },
+ "federation": "Federació"
+ }
}
}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 027e99be..d3d57562 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -275,6 +275,12 @@
"block_import": "Block import",
"block_import_error": "Error importing blocks",
"blocks_imported": "Blocks imported! Processing them will take a while.",
+ "mute_export": "Mute export",
+ "mute_export_button": "Export your mutes to a csv file",
+ "mute_import": "Mute import",
+ "mute_import_error": "Error importing mutes",
+ "mutes_imported": "Mutes imported! Processing them will take a while.",
+ "import_mutes_from_a_csv_file": "Import mutes from a csv file",
"blocks_tab": "Blocks",
"bot": "This is a bot account",
"btnRadius": "Buttons",
@@ -429,6 +435,7 @@
"false": "no",
"true": "yes"
},
+ "virtual_scrolling": "Optimize timeline rendering",
"fun": "Fun",
"greentext": "Meme arrows",
"notifications": "Notifications",
@@ -658,7 +665,8 @@
"show_full_subject": "Show full subject",
"hide_full_subject": "Hide full subject",
"show_content": "Show content",
- "hide_content": "Hide content"
+ "hide_content": "Hide content",
+ "status_deleted": "This post was deleted"
},
"user_card": {
"approve": "Approve",
diff --git a/src/i18n/eo.json b/src/i18n/eo.json
index e73ac2f8..1247d50d 100644
--- a/src/i18n/eo.json
+++ b/src/i18n/eo.json
@@ -5,7 +5,7 @@
"features_panel": {
"chat": "Babilejo",
"gopher": "Gopher",
- "media_proxy": "Vidaŭdaĵa prokurilo",
+ "media_proxy": "Vidaŭdaĵa retperilo",
"scope_options": "Agordoj de amplekso",
"text_limit": "Limo de teksto",
"title": "Funkcioj",
@@ -33,7 +33,8 @@
"show_more": "Montri plion",
"retry": "Reprovi",
"error_retry": "Bonvolu reprovi",
- "loading": "Enlegante…"
+ "loading": "Enlegante…",
+ "peek": "Antaŭmontri"
},
"image_cropper": {
"crop_picture": "Tondi bildon",
@@ -70,9 +71,9 @@
"friend_requests": "Petoj pri abono",
"mentions": "Mencioj",
"dms": "Rektaj mesaĝoj",
- "public_tl": "Publika tempolinio",
- "timeline": "Tempolinio",
- "twkn": "La tuta konata reto",
+ "public_tl": "Publika historio",
+ "timeline": "Historio",
+ "twkn": "Konata reto",
"user_search": "Serĉi uzantojn",
"who_to_follow": "Kiun aboni",
"preferences": "Agordoj",
@@ -80,7 +81,8 @@
"search": "Serĉi",
"interactions": "Interagoj",
"administration": "Administrado",
- "bookmarks": "Legosignoj"
+ "bookmarks": "Legosignoj",
+ "timelines": "Historioj"
},
"notifications": {
"broken_favorite": "Nekonata stato, serĉante ĝin…",
@@ -107,14 +109,14 @@
"text/html": "HTML"
},
"content_warning": "Temo (malnepra)",
- "default": "Ĵus alvenis al la Universala Kongreso!",
+ "default": "Ĵus alvenis Esperantujon!",
"direct_warning": "Ĉi tiu afiŝo estos videbla nur por ĉiuj menciitaj uzantoj.",
"posting": "Afiŝante",
"scope": {
"direct": "Rekta – Afiŝi nur al menciitaj uzantoj",
"private": "Nur abonantoj – Afiŝi nur al abonantoj",
- "public": "Publika – Afiŝi al publikaj tempolinioj",
- "unlisted": "Nelistigita – Ne afiŝi al publikaj tempolinioj"
+ "public": "Publika – Afiŝi al publikaj historioj",
+ "unlisted": "Nelistigita – Ne afiŝi al publikaj historioj"
},
"scope_notice": {
"unlisted": "Ĉi tiu afiŝo ne estos videbla en la Publika historio kaj La tuta konata reto",
@@ -193,7 +195,7 @@
"foreground": "Malfono",
"general": "Ĝenerala",
"hide_attachments_in_convo": "Kaŝi kunsendaĵojn en interparoloj",
- "hide_attachments_in_tl": "Kaŝi kunsendaĵojn en tempolinio",
+ "hide_attachments_in_tl": "Kaŝi kunsendaĵojn en historioj",
"max_thumbnails": "Maksimuma nombro da bildetoj en afiŝo",
"hide_isp": "Kaŝi breton propran al nodo",
"preload_images": "Antaŭ-enlegi bildojn",
@@ -246,7 +248,7 @@
"profile_banner": "Rubando de profilo",
"profile_tab": "Profilo",
"radii_help": "Agordi fasadan rondigon de randoj (bildere)",
- "replies_in_timeline": "Respondoj en tempolinio",
+ "replies_in_timeline": "Respondoj en historioj",
"reply_visibility_all": "Montri ĉiujn respondojn",
"reply_visibility_following": "Montri nur respondojn por mi aŭ miaj abonatoj",
"reply_visibility_self": "Montri nur respondojn por mi",
@@ -297,7 +299,12 @@
"older_version_imported": "La enportita dosiero estis farita per pli malnova versio de PleromaFE.",
"future_version_imported": "La enportita dosiero estis farita per pli nova versio de PleromaFE.",
"v2_imported": "La dosiero, kiun vi enportis, estis farita por malnova versio de PleromaFE. Ni provas maksimumigi interkonformecon, sed tamen eble montriĝos misoj.",
- "upgraded_from_v2": "PleromaFE estis ĝisdatigita; la haŭto eble aspektos malsame ol kiel vi ĝin memoras."
+ "upgraded_from_v2": "PleromaFE estis ĝisdatigita; la haŭto eble aspektos malsame ol kiel vi ĝin memoras.",
+ "snapshot_missing": "Neniu momentokopio de haŭto estis en la dosiero, ĝi povas aspekti iom malsame ol oni intencis.",
+ "snapshot_present": "Ĉiuj valoroj estas transpasataj, ĉar momentokopio de haŭto estas enlegita. Vi povas enlegi anstataŭe la aktualajn datumojn de haŭto.",
+ "snapshot_source_mismatch": "Versioj konfliktas: plej probable la fasado estis reirigita kaj ree ĝisdatigita; se vi ŝanĝis la haŭton per pli malnova versio de la fasado, vi probable volas uzi la malnovan version. Alie uzu la novan.",
+ "migration_napshot_gone": "Ial mankis momentokopio; io povus aspekti malsame ol en via memoro.",
+ "migration_snapshot_ok": "Certige, momentokopio de la haŭto enlegiĝis. Vi povas provi enlegi datumojn de la haŭto."
},
"use_source": "Nova versio",
"use_snapshot": "Malnova versio",
@@ -352,10 +359,11 @@
"icons": "Bildsimboloj",
"poll": "Grafo de enketo",
"underlay": "Subtavolo",
- "popover": "Ŝpruchelpiloj, menuoj",
+ "popover": "Ŝprucaĵoj, menuoj",
"post": "Afiŝoj/Priskriboj de uzantoj",
"alert_neutral": "Neŭtrala",
- "alert_warning": "Averto"
+ "alert_warning": "Averto",
+ "toggled": "Ŝaltita"
},
"radii": {
"_tab_label": "Rondeco"
@@ -388,7 +396,8 @@
"buttonPressed": "Butono (premita)",
"buttonPressedHover": "Butono (premita kaj je ŝvebo)",
"input": "Eniga kampo"
- }
+ },
+ "hintV3": "Kolorojn de ombroj vi ankaŭ povas skribi per la sistemo {0}."
},
"fonts": {
"_tab_label": "Tiparoj",
@@ -411,7 +420,7 @@
"button": "Butono",
"text": "Kelko da pliaj {0} kaj {1}",
"mono": "enhavo",
- "input": "Ĵus alvenis al la Universala Kongreso!",
+ "input": "Ĵus alvenis Esperantujon!",
"faint_link": "helpan manlibron",
"fine_print": "Legu nian {0} por nenion utilan ekscii!",
"header_faint": "Tio estas en ordo",
@@ -420,7 +429,7 @@
}
},
"discoverable": "Permesi trovon de ĉi tiu konto en serĉrezultoj kaj aliaj servoj",
- "mutes_and_blocks": "Silentigitoj kaj blokitoj",
+ "mutes_and_blocks": "Blokado kaj silentigoj",
"chatMessageRadius": "Babileja mesaĝo",
"changed_email": "Retpoŝtadreso sukcese ŝanĝiĝis!",
"change_email_error": "Eraris ŝanĝo de via retpoŝtadreso.",
@@ -448,7 +457,10 @@
"warning_of_generate_new_codes": "Kiam vi estigos novajn rehavajn kodojn, viaj malnovaj ne plu funkcios.",
"generate_new_recovery_codes": "Estigi novajn rehavajn kodojn",
"title": "Duobla aŭtentikigo",
- "otp": "OTP"
+ "otp": "OTP",
+ "wait_pre_setup_otp": "antaŭagordante OTP",
+ "setup_otp": "Agordi OTP",
+ "confirm_and_enable": "Konfirmi kaj ŝalti OTP"
},
"enter_current_password_to_confirm": "Enigu vian pasvorton por konfirmi vian identecon",
"security": "Sekureco",
@@ -480,11 +492,11 @@
},
"import_blocks_from_a_csv_file": "Enporti blokitojn el CSV-dosiero",
"hide_muted_posts": "Kaŝi afiŝojn de silentigitaj uzantoj",
- "emoji_reactions_on_timeline": "Montri bildosignajn reagojn en la tempolinio",
+ "emoji_reactions_on_timeline": "Montri bildosignajn reagojn en historioj",
"pad_emoji": "Meti spacetojn ĉirkaŭ bildosigno post ties elekto",
"domain_mutes": "Retnomoj",
"notification_blocks": "Blokinte uzanton vi malabonos ĝin kaj haltigos ĉiujn sciigojn.",
- "notification_mutes": "Por ne plu ricevi sciigojn de certa uzanto, silentigu.",
+ "notification_mutes": "Por ne plu ricevi sciigojn de certa uzanto, silentigu ĝin.",
"notification_setting_hide_notification_contents": "Kaŝi la sendinton kaj la enhavojn de pasivaj sciigoj",
"notification_setting_privacy": "Privateco",
"notification_setting_block_from_strangers": "Bloki sciigojn de uzantoj, kiujn vi ne abonas",
@@ -495,7 +507,14 @@
"backend_version": "Versio de internaĵo",
"title": "Versio"
},
- "accent": "Emfazo"
+ "accent": "Emfazo",
+ "virtual_scrolling": "Optimumigi bildigon de historioj",
+ "import_mutes_from_a_csv_file": "Enporti silentigojn el CSV-dosiero",
+ "mutes_imported": "Silentigoj enportiĝis! Traktado daŭros iom da tempo.",
+ "mute_import_error": "Eraris enporto de silentigoj",
+ "mute_import": "Enporto de silentigoj",
+ "mute_export_button": "Elportu viajn silentigojn al CSV-dosiero",
+ "mute_export": "Elporto de silentigoj"
},
"timeline": {
"collapse": "Maletendi",
@@ -503,7 +522,7 @@
"error_fetching": "Eraris ĝisdatigo",
"load_older": "Montri pli malnovajn statojn",
"no_retweet_hint": "Afiŝo estas markita kiel rekta aŭ nur por abonantoj, kaj ne eblas ĝin ripeti",
- "repeated": "ripetita",
+ "repeated": "ripetis",
"show_new": "Montri novajn",
"up_to_date": "Ĝisdata",
"no_more_statuses": "Neniuj pliaj statoj",
@@ -648,21 +667,22 @@
"media_nsfw": "Devige marki vidaŭdaĵojn konsternaj",
"media_removal_desc": "Ĉi tiu nodo forigas vidaŭdaĵojn de afiŝoj el la jenaj nodoj:",
"media_removal": "Forigo de vidaŭdaĵoj",
- "ftl_removal": "Forigo de la historio de «La tuta konata reto»",
+ "ftl_removal": "Forigo el la historio de «La tuta konata reto»",
"quarantine_desc": "Ĉi tiu nodo sendos nur publikajn afiŝojn al la jenaj nodoj:",
"quarantine": "Kvaranteno",
"reject_desc": "Ĉi tiu nodo ne akceptos mesaĝojn de la jenaj nodoj:",
"reject": "Rifuzi",
"accept_desc": "Ĉi tiu nodo nur akceptas mesaĝojn de la jenaj nodoj:",
"accept": "Akcepti",
- "simple_policies": "Specialaj politikoj de la nodo"
+ "simple_policies": "Specialaj politikoj de la nodo",
+ "ftl_removal_desc": "Ĉi tiu nodo forigas la jenajn nodojn el la historio de «La tuta konata reto»:"
},
"mrf_policies": "Ŝaltis politikon de Mesaĝa ŝanĝilaro (MRF)",
"keyword": {
"is_replaced_by": "→",
"replace": "Anstataŭigi",
"reject": "Rifuzi",
- "ftl_removal": "Forigo de la historio de «La tuta konata reto»",
+ "ftl_removal": "Forigo el la historio de «La tuta konata reto»",
"keyword_policies": "Politiko pri ŝlosilvortoj"
},
"federation": "Federado",
@@ -707,7 +727,8 @@
"pin": "Fiksi al profilo",
"delete": "Forigi staton",
"repeats": "Ripetoj",
- "favorites": "Ŝatataj"
+ "favorites": "Ŝatoj",
+ "status_deleted": "Ĉi tiu afiŝo foriĝis"
},
"time": {
"years_short": "{0}j",
@@ -769,7 +790,8 @@
"new": "Nova babilo",
"chats": "Babiloj",
"delete": "Forigi",
- "you": "Vi:"
+ "you": "Vi:",
+ "message_user": "Mesaĝi al {nickname}"
},
"password_reset": {
"password_reset_required_but_mailer_is_disabled": "Vi devas restarigi vian pasvorton, sed restarigado de pasvortoj estas malŝaltita. Bonvolu kontakti la administranton de via nodo.",
@@ -791,5 +813,8 @@
"additional_comments": "Aldonaj komentoj",
"add_comment_description": "Ĉi tiu raporto sendiĝos al reguligistoj de via nodo. Vi povas komprenigi kial vi raportas ĉi tiun konton sube:",
"title": "Raportante {0}"
+ },
+ "shoutbox": {
+ "title": "Kriujo"
}
}
diff --git a/src/i18n/es.json b/src/i18n/es.json
index 718d9040..6889df9a 100644
--- a/src/i18n/es.json
+++ b/src/i18n/es.json
@@ -551,7 +551,14 @@
"change_email_error": "Ha ocurrido un error al intentar modificar tu correo electrónico.",
"change_email": "Modificar el correo electrónico",
"bot": "Esta cuenta es un bot",
- "allow_following_move": "Permitir el seguimiento automático, cuando la cuenta que sigues se traslada a otra instancia"
+ "allow_following_move": "Permitir el seguimiento automático, cuando la cuenta que sigues se traslada a otra instancia",
+ "virtual_scrolling": "Optimizar la representación de la linea temporal",
+ "import_mutes_from_a_csv_file": "Importar silenciados desde un archivo csv",
+ "mutes_imported": "¡Silenciados importados! Procesarlos llevará un tiempo.",
+ "mute_import_error": "Error al importar los silenciados",
+ "mute_import": "Importar silenciados",
+ "mute_export_button": "Exportar los silenciados a un archivo csv",
+ "mute_export": "Exportar silenciados"
},
"time": {
"day": "{0} día",
@@ -762,7 +769,7 @@
"ftl_removal_desc": "Esta instancia elimina las siguientes instancias de la línea de tiempo \"Toda la red conocida\":",
"ftl_removal": "Eliminar de la línea de tiempo \"Toda La Red Conocida\"",
"quarantine_desc": "Esta instancia enviará solo publicaciones públicas a las siguientes instancias:",
- "simple_policies": "Políticas sobre instancias específicas",
+ "simple_policies": "Políticas específicas de la instancia",
"reject_desc": "Esta instancia no aceptará mensajes de las siguientes instancias:",
"reject": "Rechazar",
"accept": "Aceptar"
diff --git a/src/i18n/eu.json b/src/i18n/eu.json
index fdca6b95..a45b7cfd 100644
--- a/src/i18n/eu.json
+++ b/src/i18n/eu.json
@@ -13,7 +13,8 @@
"scope_options": "Ikusgaitasun aukerak",
"text_limit": "Testu limitea",
"title": "Ezaugarriak",
- "who_to_follow": "Nori jarraitu"
+ "who_to_follow": "Nori jarraitu",
+ "pleroma_chat_messages": "Pleroma Txata"
},
"finder": {
"error_fetching_user": "Errorea erabiltzailea eskuratzen",
@@ -31,7 +32,13 @@
"disable": "Ezgaitu",
"enable": "Gaitu",
"confirm": "Baieztatu",
- "verify": "Egiaztatu"
+ "verify": "Egiaztatu",
+ "peek": "Begiratu",
+ "close": "Itxi",
+ "dismiss": "Baztertu",
+ "retry": "Saiatu berriro",
+ "error_retry": "Saiatu berriro mesedez",
+ "loading": "Kargatzen…"
},
"image_cropper": {
"crop_picture": "Moztu argazkia",
@@ -81,7 +88,10 @@
"user_search": "Erabiltzailea Bilatu",
"search": "Bilatu",
"who_to_follow": "Nori jarraitu",
- "preferences": "Hobespenak"
+ "preferences": "Hobespenak",
+ "chats": "Txatak",
+ "timelines": "Denbora-lerroak",
+ "bookmarks": "Laster-markak"
},
"notifications": {
"broken_favorite": "Egoera ezezaguna, bilatzen…",
@@ -91,7 +101,10 @@
"notifications": "Jakinarazpenak",
"read": "Irakurrita!",
"repeated_you": "zure mezua errepikatu du",
- "no_more_notifications": "Ez dago jakinarazpen gehiago"
+ "no_more_notifications": "Ez dago jakinarazpen gehiago",
+ "reacted_with": "{0}kin erreakzionatu zuen",
+ "migrated_to": "hona migratua:",
+ "follow_request": "jarraitu nahi zaitu"
},
"polls": {
"add_poll": "Inkesta gehitu",
@@ -114,7 +127,8 @@
"search_emoji": "Bilatu emoji bat",
"add_emoji": "Emoji bat gehitu",
"custom": "Ohiko emojiak",
- "unicode": "Unicode emojiak"
+ "unicode": "Unicode emojiak",
+ "load_all": "{emojiAmount} emoji guztiak kargatzen"
},
"stickers": {
"add_sticker": "Pegatina gehitu"
@@ -226,7 +240,7 @@
"composing": "Idazten",
"confirm_new_password": "Baieztatu pasahitz berria",
"current_avatar": "Zure uneko avatarra",
- "current_password": "Indarrean den pasahitza",
+ "current_password": "Indarrean dagoen pasahitza",
"current_profile_banner": "Zure profilaren banner-a",
"data_import_export_tab": "Datuak Inportatu / Esportatu",
"default_vis": "Lehenetsitako ikusgaitasunak",
@@ -634,9 +648,40 @@
"about": {
"mrf": {
"keyword": {
- "keyword_policies": "Gako-hitz politika"
+ "keyword_policies": "Gako-hitz politika",
+ "ftl_removal": "\"Ezagutzen den Sarea\" denbora-lerrotik ezabatu",
+ "is_replaced_by": "→",
+ "replace": "Ordezkatuak",
+ "reject": "Ukatuak"
},
- "federation": "Federazioa"
- }
+ "federation": "Federazioa",
+ "simple": {
+ "media_nsfw_desc": "Instantzia honek hurrengo instantzien multimediak sentikorrak izatera behartzen ditu:",
+ "media_nsfw": "Behartu Multimedia Sentikor",
+ "media_removal_desc": "Instantzia honek atxikitutako multimedia hurrengo instantzietatik ezabatzen ditu:",
+ "media_removal": "Multimedia Ezabatu",
+ "ftl_removal_desc": "Instantzia honek hurrengo instantziak ezabatzen ditu \"Ezagutzen den Sarea\" denbora-lerrotik:",
+ "ftl_removal": "\"Ezagutzen den Sarea\" denbora-lerrotik ezabatu",
+ "quarantine_desc": "Instantzia honek soilik mezu publikoak bidaliko ditu instantzia hauetara:",
+ "quarantine": "Koarentena",
+ "reject_desc": "Instantzia honek ez ditu hurrengo instantzien mezuak onartuko:",
+ "reject": "Ukatuak",
+ "accept_desc": "Instantzia honek hurrengo instantzietako mezuak soilik onartzen ditu:",
+ "accept": "Onartu",
+ "simple_policies": "Gure instantziaren politika zehatzak"
+ },
+ "mrf_policies_desc": "MRF politikek instantzia honen federazioa manipulatzen dute gainerako instantziekin. Honako politika hauek daude gaituta:",
+ "mrf_policies": "Gaitutako MRF politikak"
+ },
+ "staff": "Arduradunak"
+ },
+ "domain_mute_card": {
+ "unmute_progress": "Isiltasuna kentzen…",
+ "unmute": "Isiltasuna kendu",
+ "mute_progress": "Isiltzen…",
+ "mute": "Isilarazi"
+ },
+ "shoutbox": {
+ "title": "Oihu-kutxa"
}
}
diff --git a/src/i18n/fa.json b/src/i18n/fa.json
new file mode 100644
index 00000000..0e8bda4b
--- /dev/null
+++ b/src/i18n/fa.json
@@ -0,0 +1,155 @@
+{
+ "about": {
+ "mrf": {
+ "simple": {
+ "media_removal_desc": "این نمونه رسانه‌ی پیغام‌های نمونه‌های ذکر شده را حذف می‌کند:",
+ "ftl_removal_desc": "این نمونه،‌ نمونه‌های ذکر شده را از تایم‌لاین «تمام شبکه‌ شناخته شده» حذف می‌کند:",
+ "media_removal": "حذف رسانه",
+ "ftl_removal": "حذف از تایم‌لاین «تمام شبکه شناخته شده»",
+ "quarantine_desc": "این نمونه تنها پیغام‌های عمومی را به نمونه‌های ذکر شده پیغام ارسال می‌کند:",
+ "quarantine": "قرنطینه شده",
+ "reject_desc": "این نمونه از نمونه‌های ذکر شده پیغامی دریافت نمی‌کند:",
+ "reject": "رد کننده",
+ "accept_desc": "این نمونه تنها از نمونه‌های ذکر شده پیغام دریافت می‌کند:",
+ "simple_policies": "سیاست‌های مخصوص نمونه",
+ "accept": "دریافت کننده",
+ "media_nsfw_desc": "این نمونه،‌ رسانه نمونه‌های ذکر شده را به اجبار حساس می‌کند:",
+ "media_nsfw": "به اجبار حساس کردن رسانه"
+ },
+ "federation": "فدراسیون",
+ "mrf_policies_desc": "سیاست‌های MRF رفتار فدراسیون این نمونه را تغییر می‌دهد. سیاست‌هایی که در ادامه آمده اعمال شده است:",
+ "keyword": {
+ "reject": "رد کننده",
+ "replace": "جایگزین کننده",
+ "keyword_policies": "سیاست‌های واژگان کلیدی",
+ "is_replaced_by": "→",
+ "ftl_removal": "حذف از تایم‌لاین «تمام شبکه شناخته شده»"
+ },
+ "mrf_policies": "سیاست‌های MRF(وسیله بازنویسی پیغام) فعال شده"
+ },
+ "staff": "کارکنان"
+ },
+ "image_cropper": {
+ "crop_picture": "برش تصویر",
+ "cancel": "لغو",
+ "save_without_cropping": "ذخیره بدون برش",
+ "save": "ذخیره"
+ },
+ "notifications": {
+ "followed_you": "پیگیر شما شد",
+ "favorited_you": "پیغام شما را پسندید",
+ "broken_favorite": "پیغام ناشناخته، در حال جستجو…"
+ },
+ "nav": {
+ "chats": "گپ‌ها",
+ "timelines": "تایم‌لاین‌ها",
+ "preferences": "ترجیحات",
+ "who_to_follow": "چه کسانی را پیگیری کنیم",
+ "search": "جستجو",
+ "user_search": "جستجوی کاربر",
+ "bookmarks": "نشانک‌ها",
+ "twkn": "شبکه شناخته شده",
+ "timeline": "تایم‌لاین",
+ "public_tl": "تایم‌لاین عمومی",
+ "dms": "پیغام‌های مستقیم",
+ "interactions": "تعاملات",
+ "mentions": "نام بردن‌ها",
+ "friend_requests": "درخواست پیگیری",
+ "back": "قبلی",
+ "administration": "مدیریت",
+ "about": "درباره"
+ },
+ "features_panel": {
+ "who_to_follow": "چه کسانی را پیگیری کنیم",
+ "title": "ویژگی‌ها",
+ "text_limit": "محدودیت متن",
+ "scope_options": "تنظیمات حوزه",
+ "media_proxy": "پروکسی رسانه",
+ "gopher": "گوفر",
+ "pleroma_chat_messages": "گپ پلروما",
+ "chat": "گپ"
+ },
+ "media_modal": {
+ "next": "بعدی",
+ "previous": "قبلی"
+ },
+ "login": {
+ "heading": {
+ "recovery": "بازیابی دو مرحله‌ای",
+ "totp": "احراز هویت دو مرحله‌ای"
+ },
+ "enter_two_factor_code": "کد احراز هویت دو مرحله‌ای را وارد کنید",
+ "recovery_code": "کد بازیابی",
+ "enter_recovery_code": "کد بازیابی را وارد کنید",
+ "authentication_code": "کد احراز هویت",
+ "hint": "برای شرکت در گفتگو، وارد سامانه شوید",
+ "username": "نام کاربری",
+ "register": "ثبت نام",
+ "description": "ورود به سامانه از طریق OAuth",
+ "placeholder": "به عنوان مثال: lain",
+ "password": "رمز عبور",
+ "logout": "خروج از سامانه",
+ "login": "ورود به سامانه"
+ },
+ "importer": {
+ "error": "در حین بارگذاری فایل خطایی رخ داد.",
+ "success": "با موفقیت بارگذاری شد.",
+ "submit": "ارسال"
+ },
+ "general": {
+ "peek": "نگاه سریع",
+ "close": "بستن",
+ "verify": "تأیید",
+ "confirm": "تأیید",
+ "enable": "فعال",
+ "disable": "غیر فعال",
+ "cancel": "لغو",
+ "show_less": "کمتر نشان بده",
+ "show_more": "بیشتر نشان بده",
+ "optional": "اختیاری",
+ "retry": "دوباره امتحان کنید",
+ "error_retry": "لطفاً دوباره امتحان کنید",
+ "generic_error": "خطایی رخ داد",
+ "loading": "در حال بارگذاری…",
+ "more": "بیشتر",
+ "submit": "ارسال",
+ "apply": "اعمال"
+ },
+ "finder": {
+ "find_user": "جستجوی ‌ کاربر",
+ "error_fetching_user": "دریافت کاربر با خطا مواجه شد"
+ },
+ "exporter": {
+ "processing": "در حال پردازش، شما به زودی قادر به دانلود فایل خواهید بود",
+ "export": "صادر کردن"
+ },
+ "domain_mute_card": {
+ "unmute": "صدا دار",
+ "unmute_progress": "در حال صدا دار کردن …",
+ "mute_progress": "در حال بی صدا کردن…",
+ "mute": "بی صدا"
+ },
+ "shoutbox": {
+ "title": "چت باکس"
+ },
+ "display_date": {
+ "today": "امروز"
+ },
+ "file_type": {
+ "file": "فایل",
+ "image": "تصویر",
+ "video": "ویدئو",
+ "audio": "صدا"
+ },
+ "chats": {
+ "empty_chat_list_placeholder": "شما هنوز هیچ گپی ندارید، گپ جدیدی را آغاز کنید!",
+ "delete": "حذف",
+ "error_sending_message": "در حین ارسال پیغام خطایی رخ داد.",
+ "error_loading_chat": "در هنگام بارگذاری گپ خطایی رخ داد.",
+ "delete_confirm": "آیا از حذف این پیغام اطمینان دارید؟",
+ "more": "بیشتر",
+ "empty_message_error": "نمی‌توان پیغام خالی فرستاد",
+ "new": "گپ جدید",
+ "chats": "گپ‌ها"
+ }
+}
diff --git a/src/i18n/fi.json b/src/i18n/fi.json
index 3832dcaa..2524f278 100644
--- a/src/i18n/fi.json
+++ b/src/i18n/fi.json
@@ -578,7 +578,8 @@
"show_full_subject": "Näytä koko otsikko",
"hide_full_subject": "Piilota koko otsikko",
"show_content": "Näytä sisältö",
- "hide_content": "Piilota sisältö"
+ "hide_content": "Piilota sisältö",
+ "status_deleted": "Poistettu viesti"
},
"user_card": {
"approve": "Hyväksy",
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index 3b7eefaf..63ad46d2 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -13,7 +13,8 @@
"scope_options": "Options de visibilité",
"text_limit": "Limite de texte",
"title": "Caractéristiques",
- "who_to_follow": "Personnes à suivre"
+ "who_to_follow": "Personnes à suivre",
+ "pleroma_chat_messages": "Chat Pleroma"
},
"finder": {
"error_fetching_user": "Erreur lors de la recherche de l'utilisateur·ice",
@@ -32,7 +33,12 @@
"enable": "Activer",
"confirm": "Confirmer",
"verify": "Vérifier",
- "dismiss": "Rejeter"
+ "dismiss": "Rejeter",
+ "peek": "Jeter un coup d'œil",
+ "close": "Fermer",
+ "retry": "Réessayez",
+ "error_retry": "Veuillez réessayer",
+ "loading": "Chargement…"
},
"image_cropper": {
"crop_picture": "Rogner l'image",
@@ -77,15 +83,17 @@
"dms": "Messages directs",
"public_tl": "Fil d'actualité public",
"timeline": "Fil d'actualité",
- "twkn": "Ensemble du réseau connu",
+ "twkn": "Réseau connu",
"user_search": "Recherche d'utilisateur·ice",
"who_to_follow": "Qui suivre",
"preferences": "Préférences",
"search": "Recherche",
- "administration": "Administration"
+ "administration": "Administration",
+ "chats": "Chats",
+ "bookmarks": "Marques-Pages"
},
"notifications": {
- "broken_favorite": "Chargement d'un message inconnu…",
+ "broken_favorite": "Message inconnu, chargement…",
"favorited_you": "a aimé votre statut",
"followed_you": "a commencé à vous suivre",
"load_older": "Charger les notifications précédentes",
@@ -115,7 +123,7 @@
"text/bbcode": "BBCode"
},
"content_warning": "Sujet (optionnel)",
- "default": "Écrivez ici votre prochain statut.",
+ "default": "Je viens d'atterrir en Tchéquie.",
"direct_warning_to_all": "Ce message sera visible pour toutes les personnes mentionnées.",
"direct_warning_to_first_only": "Ce message sera visible uniquement pour personnes mentionnées au début du message.",
"posting": "Envoi en cours",
@@ -129,7 +137,12 @@
"private": "Abonné·e·s uniquement - Seul·e·s vos abonné·e·s verront vos billets",
"public": "Publique - Afficher dans les fils publics",
"unlisted": "Non-Listé - Ne pas afficher dans les fils publics"
- }
+ },
+ "media_description_error": "Échec de téléversement du media, essayez encore",
+ "empty_status_error": "Impossible de poster un statut vide sans attachements",
+ "preview_empty": "Vide",
+ "preview": "Prévisualisation",
+ "media_description": "Description de l'attachement"
},
"registration": {
"bio": "Biographie",
@@ -488,7 +501,15 @@
"notification_setting_privacy_option": "Masquer l'expéditeur et le contenu des notifications push",
"notification_setting_privacy": "Intimité",
"hide_followers_count_description": "Masquer le nombre d'abonnés",
- "accent": "Accent"
+ "accent": "Accent",
+ "chatMessageRadius": "Message de chat",
+ "bot": "Ce compte est un robot",
+ "import_mutes_from_a_csv_file": "Importer les masquages depuis un fichier CSV",
+ "mutes_imported": "Masquages importés ! Leur application peut prendre du temps.",
+ "mute_import_error": "Erreur à l'import des masquages",
+ "mute_import": "Import des masquages",
+ "mute_export_button": "Exporter vos masquages dans un fichier CSV",
+ "mute_export": "Export des masquages"
},
"timeline": {
"collapse": "Fermer",
@@ -732,5 +753,11 @@
"return_home": "Retourner à la page d'accueil",
"too_many_requests": "Vos avez atteint la limite d'essais, essayez plus tard.",
"password_reset_required": "Vous devez changer votre mot de passe pour vous authentifier."
+ },
+ "errors": {
+ "storage_unavailable": "Pleroma n'a pas pu accéder au stockage du navigateur. Votre identifiant ou vos mots de passes ne seront sauvegardés et des problèmes pourront être rencontrés. Essayez d'activer les cookies."
+ },
+ "shoutbox": {
+ "title": "Shoutbox"
}
}
diff --git a/src/i18n/it.json b/src/i18n/it.json
index 474e7fde..67e92b32 100644
--- a/src/i18n/it.json
+++ b/src/i18n/it.json
@@ -407,7 +407,14 @@
"reset_background_confirm": "Vuoi veramente azzerare lo sfondo?",
"chatMessageRadius": "Messaggi istantanei",
"notification_setting_hide_notification_contents": "Nascondi mittente e contenuti delle notifiche push",
- "notification_setting_block_from_strangers": "Blocca notifiche da utenti che non segui"
+ "notification_setting_block_from_strangers": "Blocca notifiche da utenti che non segui",
+ "virtual_scrolling": "Velocizza l'elaborazione delle sequenze",
+ "import_mutes_from_a_csv_file": "Importa silenziati da un file CSV",
+ "mutes_imported": "Silenziati importati! Saranno elaborati a breve.",
+ "mute_import_error": "Errore nell'importazione",
+ "mute_import": "Importa silenziati",
+ "mute_export_button": "Esporta la tua lista di silenziati in un file CSV",
+ "mute_export": "Esporta silenziati"
},
"timeline": {
"error_fetching": "Errore nell'aggiornamento",
@@ -591,12 +598,12 @@
"reject": "Rifiuta",
"accept": "Accetta",
"simple_policies": "Regole specifiche alla stanza",
- "accept_desc": "Questa stanza accetta messaggi solo dalle seguenti stanze:",
- "reject_desc": "Questa stanza non accetterà messaggi dalle stanze seguenti:",
+ "accept_desc": "Questa stanza accetta messaggi solo dalle seguenti altre:",
+ "reject_desc": "Questa stanza rifiuterà i messaggi provenienti dalle seguenti:",
"quarantine": "Quarantena",
- "quarantine_desc": "Questa stanza inoltrerà solo messaggi pubblici alle seguenti stanze:",
+ "quarantine_desc": "Questa stanza inoltrerà solo messaggi pubblici alle seguenti:",
"ftl_removal": "Rimozione dalla sequenza globale",
- "ftl_removal_desc": "Questa stanza rimuove le seguenti stanze dalla sequenza globale:",
+ "ftl_removal_desc": "Questa stanza rimuove le seguenti dalla sequenza globale:",
"media_removal": "Rimozione multimedia",
"media_removal_desc": "Questa istanza rimuove gli allegati dalle seguenti stanze:",
"media_nsfw": "Allegati oscurati forzatamente",
@@ -695,7 +702,8 @@
"reply_to": "Rispondi a",
"delete_confirm": "Vuoi veramente eliminare questo messaggio?",
"unbookmark": "Rimuovi segnalibro",
- "bookmark": "Aggiungi segnalibro"
+ "bookmark": "Aggiungi segnalibro",
+ "status_deleted": "Questo messagio è stato cancellato"
},
"time": {
"years_short": "{0}a",
diff --git a/src/i18n/pl.json b/src/i18n/pl.json
index 05a7edf7..67cf38a5 100644
--- a/src/i18n/pl.json
+++ b/src/i18n/pl.json
@@ -49,7 +49,8 @@
"scope_options": "Ustawienia zakresu",
"text_limit": "Limit tekstu",
"title": "Funkcje",
- "who_to_follow": "Propozycje obserwacji"
+ "who_to_follow": "Propozycje obserwacji",
+ "pleroma_chat_messages": "Czat Pleromy"
},
"finder": {
"error_fetching_user": "Błąd przy pobieraniu profilu",
@@ -71,7 +72,9 @@
"verify": "Zweryfikuj",
"close": "Zamknij",
"loading": "Ładowanie…",
- "retry": "Spróbuj ponownie"
+ "retry": "Spróbuj ponownie",
+ "peek": "Spójrz",
+ "error_retry": "Spróbuj ponownie"
},
"image_cropper": {
"crop_picture": "Przytnij obrazek",
@@ -117,12 +120,14 @@
"dms": "Wiadomości prywatne",
"public_tl": "Publiczna oś czasu",
"timeline": "Oś czasu",
- "twkn": "Cała znana sieć",
+ "twkn": "Znana sieć",
"user_search": "Wyszukiwanie użytkowników",
"search": "Wyszukiwanie",
"who_to_follow": "Sugestie obserwacji",
"preferences": "Preferencje",
- "bookmarks": "Zakładki"
+ "bookmarks": "Zakładki",
+ "chats": "Czaty",
+ "timelines": "Osie czasu"
},
"notifications": {
"broken_favorite": "Nieznany status, szukam go…",
@@ -197,7 +202,9 @@
},
"preview_empty": "Pusty",
"preview": "Podgląd",
- "empty_status_error": "Nie można wysłać pustego wpisu bez plików"
+ "empty_status_error": "Nie można wysłać pustego wpisu bez plików",
+ "media_description_error": "Nie udało się zaktualizować mediów, spróbuj ponownie",
+ "media_description": "Opis mediów"
},
"registration": {
"bio": "Bio",
@@ -400,7 +407,7 @@
"theme_help_v2_1": "Możesz też zastąpić kolory i widoczność poszczególnych komponentów przełączając pola wyboru, użyj „Wyczyść wszystko” aby usunąć wszystkie zastąpienia.",
"theme_help_v2_2": "Ikony pod niektórych wpisami są wskaźnikami kontrastu pomiędzy tłem a tekstem, po najechaniu na nie otrzymasz szczegółowe informacje. Zapamiętaj, że jeżeli używasz przezroczystości, wskaźniki pokazują najgorszy możliwy przypadek.",
"tooltipRadius": "Etykiety/alerty",
- "type_domains_to_mute": "Wpisz domeny, które chcesz wyciszyć",
+ "type_domains_to_mute": "Wyszukaj domeny, które chcesz wyciszyć",
"upload_a_photo": "Wyślij zdjęcie",
"user_settings": "Ustawienia użytkownika",
"values": {
@@ -492,7 +499,8 @@
"tabs": "Karty",
"chat": {
"outgoing": "Wiadomości wychodzące",
- "incoming": "Wiadomości przychodzące"
+ "incoming": "Wiadomości przychodzące",
+ "border": "Granica"
}
},
"radii": {
@@ -573,7 +581,22 @@
"add_field": "Dodaj pole"
},
"bot": "To konto jest prowadzone przez bota",
- "notification_setting_hide_notification_contents": "Ukryj nadawcę i zawartość powiadomień push"
+ "notification_setting_hide_notification_contents": "Ukryj nadawcę i zawartość powiadomień push",
+ "notification_setting_block_from_strangers": "Zablokuj powiadomienia od użytkowników których nie obserwujesz",
+ "virtual_scrolling": "Optymalizuj renderowanie osi czasu",
+ "reset_background_confirm": "Czy naprawdę chcesz zresetować tło?",
+ "reset_banner_confirm": "Czy naprawdę chcesz zresetować banner?",
+ "reset_avatar_confirm": "Czy naprawdę chcesz zresetować awatar?",
+ "reset_profile_banner": "Zresetuj banner profilowy",
+ "reset_profile_background": "Zresetuj tło profilowe",
+ "mutes_and_blocks": "Wyciszenia i blokady",
+ "chatMessageRadius": "Wiadomość czatu",
+ "import_mutes_from_a_csv_file": "Zaimportuj wyciszenia z pliku .csv",
+ "mutes_imported": "Zaimportowano wyciszenia! Przetwarzanie zajmie chwilę.",
+ "mute_import_error": "Wystąpił błąd podczas importowania wyciszeń",
+ "mute_import": "Import wyciszeń",
+ "mute_export_button": "Wyeksportuj swoje wyciszenia do pliku .csv",
+ "mute_export": "Eksport wyciszeń"
},
"time": {
"day": "{0} dzień",
@@ -639,7 +662,12 @@
"unbookmark": "Usuń z zakładek",
"bookmark": "Dodaj do zakładek",
"hide_content": "Ukryj zawartość",
- "show_content": "Pokaż zawartość"
+ "show_content": "Pokaż zawartość",
+ "hide_full_subject": "Ukryj cały temat",
+ "show_full_subject": "Pokaż cały temat",
+ "thread_muted_and_words": ", ma słowa:",
+ "thread_muted": "Wątek wyciszony",
+ "status_deleted": "Ten wpis został usunięty"
},
"user_card": {
"approve": "Przyjmij",
@@ -723,7 +751,8 @@
"add_reaction": "Dodaj reakcję",
"user_settings": "Ustawienia użytkownika",
"accept_follow_request": "Akceptuj prośbę o możliwość obserwacji",
- "reject_follow_request": "Odrzuć prośbę o możliwość obserwacji"
+ "reject_follow_request": "Odrzuć prośbę o możliwość obserwacji",
+ "bookmark": "Zakładka"
},
"upload": {
"error": {
@@ -773,9 +802,17 @@
"error_sending_message": "Coś poszło nie tak podczas wysyłania wiadomości.",
"error_loading_chat": "Coś poszło nie tak podczas ładowania czatu.",
"empty_message_error": "Nie można wysłać pustej wiadomości",
- "new": "Nowy czat"
+ "new": "Nowy czat",
+ "empty_chat_list_placeholder": "Nie masz jeszcze żadnych czatów. Zacznij nowy czat!",
+ "chats": "Czaty"
},
"display_date": {
"today": "Dzisiaj"
+ },
+ "shoutbox": {
+ "title": "Shoutbox"
+ },
+ "errors": {
+ "storage_unavailable": "Pleroma nie mogła uzyskać dostępu do pamięci masowej przeglądarki. Twój login lub lokalne ustawienia nie zostaną zapisane i możesz napotkać problemy. Spróbuj włączyć ciasteczka."
}
}
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 3444a26d..8f421b50 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -473,5 +473,10 @@
"tool_tip": {
"accept_follow_request": "Принять запрос на чтение",
"reject_follow_request": "Отклонить запрос на чтение"
+ },
+ "image_cropper": {
+ "save_without_cropping": "Сохранить не обрезая",
+ "save": "Сохранить",
+ "crop_picture": "Обрезать картинку"
}
}
diff --git a/src/i18n/zh.json b/src/i18n/zh.json
index 8c693f4d..09e2ab0d 100644
--- a/src/i18n/zh.json
+++ b/src/i18n/zh.json
@@ -11,9 +11,10 @@
"gopher": "Gopher",
"media_proxy": "媒体代理",
"scope_options": "可见范围设置",
- "text_limit": "文本长度限制",
+ "text_limit": "文字数量限制",
"title": "功能",
- "who_to_follow": "推荐关注"
+ "who_to_follow": "推荐关注",
+ "pleroma_chat_messages": "Pleroma 聊天"
},
"finder": {
"error_fetching_user": "获取用户时发生错误",
@@ -23,8 +24,8 @@
"apply": "应用",
"submit": "提交",
"more": "更多",
- "generic_error": "发生一个错误",
- "optional": "可选项",
+ "generic_error": "发生了一个错误",
+ "optional": "可选",
"show_more": "展开",
"show_less": "收起",
"cancel": "取消",
@@ -32,7 +33,12 @@
"enable": "启用",
"confirm": "确认",
"verify": "验证",
- "dismiss": "忽略"
+ "dismiss": "忽略",
+ "peek": "窥探",
+ "close": "关闭",
+ "retry": "重试",
+ "error_retry": "请重试",
+ "loading": "载入中…"
},
"image_cropper": {
"crop_picture": "裁剪图片",
@@ -77,37 +83,40 @@
"dms": "私信",
"public_tl": "公共时间线",
"timeline": "时间线",
- "twkn": "所有已知网络",
+ "twkn": "已知网络",
"user_search": "用户搜索",
"search": "搜索",
"who_to_follow": "推荐关注",
"preferences": "偏好设置",
- "administration": "管理员"
+ "administration": "管理员",
+ "chats": "聊天",
+ "timelines": "时间线",
+ "bookmarks": "书签"
},
"notifications": {
"broken_favorite": "未知的状态,正在搜索中…",
- "favorited_you": "收藏了你的状态",
+ "favorited_you": "喜欢了你的状态",
"followed_you": "关注了你",
"load_older": "加载更早的通知",
"notifications": "通知",
- "read": "阅读!",
+ "read": "已阅!",
"repeated_you": "转发了你的状态",
"no_more_notifications": "没有更多的通知",
- "reacted_with": "和 {0} 互动过",
- "migrated_to": "迁移到",
+ "reacted_with": "作出了 {0} 的反应",
+ "migrated_to": "迁移到了",
"follow_request": "想要关注你"
},
"polls": {
- "add_poll": "增加问卷调查",
+ "add_poll": "增加投票",
"add_option": "增加选项",
"option": "选项",
"votes": "投票",
"vote": "投票",
- "type": "问卷类型",
- "single_choice": "单选项",
- "multiple_choices": "多选项",
- "expiry": "问卷的时间",
- "expires_in": "投票于 {0} 内结束",
+ "type": "投票类型",
+ "single_choice": "单选",
+ "multiple_choices": "多选",
+ "expiry": "投票期限",
+ "expires_in": "投票于 {0} 后结束",
"expired": "投票 {0} 前已结束",
"not_enough_options": "投票的选项太少"
},
@@ -132,7 +141,7 @@
"text/bbcode": "BBCode"
},
"content_warning": "主题(可选)",
- "default": "刚刚抵达上海",
+ "default": "刚刚抵达洛杉矶",
"direct_warning_to_all": "本条内容只有被提及的用户能够看到。",
"direct_warning_to_first_only": "本条内容只有被在消息开始处提及的用户能够看到。",
"posting": "发送",
@@ -146,12 +155,17 @@
"private": "仅关注者 - 只有关注了你的人能看到",
"public": "公共 - 发送到公共时间轴",
"unlisted": "不公开 - 不会发送到公共时间轴"
- }
+ },
+ "preview_empty": "空的",
+ "preview": "预览",
+ "media_description": "媒体描述",
+ "media_description_error": "更新媒体失败,请重试",
+ "empty_status_error": "不能发布没有内容、没有附件的发文"
},
"registration": {
"bio": "简介",
"email": "电子邮箱",
- "fullname": "全名",
+ "fullname": "显示名称",
"password_confirm": "确认密码",
"registration": "注册",
"token": "邀请码",
@@ -175,7 +189,7 @@
"settings": {
"app_name": "App 名称",
"security": "安全",
- "enter_current_password_to_confirm": "输入你当前密码来确认你的身份",
+ "enter_current_password_to_confirm": "输入您当前的密码来确认您的身份",
"mfa": {
"otp": "OTP",
"setup_otp": "设置 OTP",
@@ -183,18 +197,18 @@
"confirm_and_enable": "确认并启用 OTP",
"title": "双因素验证",
"generate_new_recovery_codes": "生成新的恢复码",
- "warning_of_generate_new_codes": "当你生成新的恢复码时,你的旧恢复码就失效了。",
+ "warning_of_generate_new_codes": "当您生成新的恢复码时,您旧的恢复码将会失效。",
"recovery_codes": "恢复码。",
"waiting_a_recovery_codes": "正在接收备份码…",
- "recovery_codes_warning": "抄写这些号码,或者保存在安全的地方。这些号码不会再次显示。如果你无法访问你的 2FA app,也丢失了你的恢复码,你的账号就再也无法登录了。",
+ "recovery_codes_warning": "抄写这些号码,或者将其保存在安全的地方。这些号码不会再次显示。如果您无法访问您的 2FA app,也丢失了您的恢复码,您就再也无法登录您的账号了。",
"authentication_methods": "身份验证方法",
"scan": {
"title": "扫一下",
- "desc": "使用你的双因素验证 app,扫描这个二维码,或者输入这些文字密钥:",
+ "desc": "使用您的双因素验证 app,扫描这个二维码,或者输入这些文字密钥:",
"secret_code": "密钥"
},
"verify": {
- "desc": "要启用双因素验证,请把你的双因素验证 app 里的数字输入:"
+ "desc": "要启用双因素验证,请输入您的双因素验证 app 里的数字:"
}
},
"attachmentRadius": "附件",
@@ -204,12 +218,12 @@
"avatarRadius": "头像",
"background": "背景",
"bio": "简介",
- "block_export": "拉黑名单导出",
- "block_export_button": "导出你的拉黑名单到一个 csv 文件",
- "block_import": "拉黑名单导入",
- "block_import_error": "导入拉黑名单出错",
- "blocks_imported": "拉黑名单导入成功!需要一点时间来处理。",
- "blocks_tab": "块",
+ "block_export": "屏蔽名单导出",
+ "block_export_button": "导出你的屏蔽名单到一个 csv 文件",
+ "block_import": "屏蔽名单导入",
+ "block_import_error": "导入屏蔽名单出错",
+ "blocks_imported": "屏蔽名单导入成功!需要一点时间来处理。",
+ "blocks_tab": "屏蔽",
"btnRadius": "按钮",
"cBlue": "蓝色(回复,关注)",
"cGreen": "绿色(转发)",
@@ -229,7 +243,7 @@
"delete_account": "删除账户",
"delete_account_description": "永久删除你的帐号和所有数据。",
"delete_account_error": "删除账户时发生错误,如果一直删除不了,请联系实例管理员。",
- "delete_account_instructions": "在下面输入你的密码来确认删除账户",
+ "delete_account_instructions": "在下面输入您的密码来确认删除账户。",
"avatar_size_instruction": "推荐的头像图片最小的尺寸是 150x150 像素。",
"export_theme": "导出预置主题",
"filtering": "过滤器",
@@ -245,7 +259,7 @@
"hide_attachments_in_tl": "在时间线上隐藏附件",
"hide_muted_posts": "不显示被隐藏的用户的帖子",
"max_thumbnails": "最多再每个帖子所能显示的缩略图数量",
- "hide_isp": "隐藏指定实例的面板H",
+ "hide_isp": "隐藏实例独有的面板",
"preload_images": "预载图片",
"use_one_click_nsfw": "点击一次以打开工作场所不适宜的附件",
"hide_post_stats": "隐藏推文相关的统计数据(例如:收藏的次数)",
@@ -263,7 +277,7 @@
"invalid_theme_imported": "您所选择的主题文件不被 Pleroma 支持,因此主题未被修改。",
"limited_availability": "在您的浏览器中无法使用",
"links": "链接",
- "lock_account_description": "你需要手动审核关注请求",
+ "lock_account_description": "您需要手动审核关注请求",
"loop_video": "循环视频",
"loop_video_silent_only": "只循环没有声音的视频(例如:Mastodon 里的“GIF”)",
"mutes_tab": "隐藏",
@@ -278,7 +292,7 @@
"notification_visibility_mentions": "提及",
"notification_visibility_repeats": "转发",
"no_rich_text_description": "不显示富文本格式",
- "no_blocks": "没有拉黑的",
+ "no_blocks": "没有屏蔽",
"no_mutes": "没有隐藏",
"hide_follows_description": "不要显示我所关注的人",
"hide_followers_description": "不要显示关注我的人",
@@ -308,7 +322,7 @@
"search_user_to_mute": "搜索你想要隐藏的用户",
"security_tab": "安全",
"scope_copy": "回复时的复制范围(私信是总是复制的)",
- "minimal_scopes_mode": "最小发文范围",
+ "minimal_scopes_mode": "使发文可见范围的选项最少化",
"set_new_avatar": "设置新头像",
"set_new_profile_background": "设置新的个人资料背景",
"set_new_profile_banner": "设置新的横幅图片",
@@ -324,7 +338,7 @@
"text": "文本",
"theme": "主题",
"theme_help": "使用十六进制代码(#rrggbb)来设置主题颜色。",
- "theme_help_v2_1": "你也可以通过切换复选框来覆盖某些组件的颜色和透明。使用“清除所有”来清楚所有覆盖设置。",
+ "theme_help_v2_1": "您也可以通过选中复选框来覆盖某些组件的颜色和透明度。使用“清除所有”按钮来清除所有覆盖设置。",
"theme_help_v2_2": "某些条目下的图标是背景或文本对比指示器,鼠标悬停可以获取详细信息。请记住,使用透明度来显示最差的情况。",
"tooltipRadius": "提醒",
"upload_a_photo": "上传照片",
@@ -335,7 +349,7 @@
},
"notifications": "通知",
"notification_mutes": "要停止收到某个指定的用户的通知,请使用隐藏功能。",
- "notification_blocks": "拉黑一个用户会停掉所有他的通知,等同于取消关注。",
+ "notification_blocks": "屏蔽一个用户会停止接收来自该用户的所有通知,并且会取消对该用户的关注。",
"enable_web_push_notifications": "启用 web 推送通知",
"style": {
"switcher": {
@@ -350,7 +364,17 @@
"clear_opacity": "清除透明度",
"load_theme": "加载主题",
"help": {
- "upgraded_from_v2": "PleromaFE 已升级,主题会和你记忆中的不太一样。"
+ "upgraded_from_v2": "PleromaFE 已升级,主题会与您记忆中的不太一样。",
+ "older_version_imported": "您导入的文件来自旧版本的 FE。",
+ "future_version_imported": "您导入的文件来自更高版本的 FE。",
+ "v2_imported": "您导入的文件是旧版 FE 的。我们尽可能保持兼容性,但还是可能出现不一致的情况。",
+ "snapshot_source_mismatch": "版本冲突:很有可能是 FE 版本回滚后再次升级了,如果您使用旧版本的 FE 更改了主题那么您可能需要使用旧版本,否则请使用新版本。",
+ "migration_napshot_gone": "不知出于何种原因,主题快照缺失了,一些地方可能与您印象中的不符。",
+ "migration_snapshot_ok": "为保万无一失,加载了主题快照。您可以试着加载主题数据。",
+ "fe_downgraded": "PleromaFE 的版本回滚了。",
+ "fe_upgraded": "PleromaFE 的主题引擎随着版本更新升级了。",
+ "snapshot_missing": "在文件中没有主题快照,所以网站外观可能会与原来预想的不同。",
+ "snapshot_present": "主题快照已加载,因此所有的值均被覆盖。您可以改为加载主题的实际数据。"
},
"use_source": "新版本",
"use_snapshot": "老版本",
@@ -389,7 +413,26 @@
"borders": "边框",
"buttons": "按钮",
"inputs": "输入框",
- "faint_text": "灰度文字"
+ "faint_text": "灰度文字",
+ "chat": {
+ "border": "边框",
+ "outgoing": "发出的",
+ "incoming": "收到的"
+ },
+ "disabled": "禁用的",
+ "pressed": "按下的",
+ "highlight": "强调元素",
+ "selectedMenu": "选中的菜单项",
+ "selectedPost": "选中的发布内容",
+ "icons": "图标",
+ "poll": "投票统计图",
+ "popover": "提示框,菜单,弹出框",
+ "post": "发布内容/用户简介",
+ "alert_neutral": "中性",
+ "alert_warning": "警告",
+ "tabs": "标签页",
+ "underlay": "底衬",
+ "toggled": "勾选的"
},
"radii": {
"_tab_label": "圆角"
@@ -426,7 +469,7 @@
},
"fonts": {
"_tab_label": "字体",
- "help": "给用户界面的元素选择字体。选择 “自选”的你必须输入确切的字体名称。",
+ "help": "为用户界面的元素选择字体。若选择 “自选”,您必须输入与系统显示完全一致的字体名称。",
"components": {
"interface": "界面",
"input": "输入框",
@@ -461,7 +504,7 @@
"notification_setting_filters": "过滤器",
"domain_mutes": "域名",
"changed_email": "邮箱修改成功!",
- "change_email_error": "修改你的电子邮箱时发生错误",
+ "change_email_error": "修改您的电子邮箱时发生错误。",
"change_email": "修改电子邮箱",
"allow_following_move": "正在关注的账号迁移时自动重新关注",
"notification_setting_privacy_option": "在通知推送中隐藏发送者和内容",
@@ -470,7 +513,41 @@
"notification_visibility_emoji_reactions": "互动",
"notification_visibility_moves": "用户迁移",
"new_email": "新邮箱",
- "emoji_reactions_on_timeline": "在时间线上显示表情符号互动"
+ "emoji_reactions_on_timeline": "在时间线上显示表情符号互动",
+ "notification_setting_hide_notification_contents": "隐藏推送通知中的发送者与内容信息",
+ "notification_setting_block_from_strangers": "屏蔽来自你没有关注的用户的通知",
+ "type_domains_to_mute": "搜索需要隐藏的域名",
+ "useStreamingApi": "实时接收帖子和通知",
+ "user_mutes": "用户",
+ "reset_background_confirm": "您确定要重置个人资料背景图吗?",
+ "reset_banner_confirm": "您确定要重置横幅图片吗?",
+ "reset_avatar_confirm": "您确定要重置头像吗?",
+ "reset_profile_banner": "重置横幅图片",
+ "reset_profile_background": "重置个人资料背景图",
+ "reset_avatar": "重置头像",
+ "hide_followers_count_description": "不显示关注者数量",
+ "profile_fields": {
+ "value": "内容",
+ "name": "标签",
+ "add_field": "添加字段",
+ "label": "个人资料元数据"
+ },
+ "accent": "强调色",
+ "pad_emoji": "从表情符号选择器插入表情符号时,在表情两侧插入空格",
+ "discoverable": "允许通过搜索检索等服务找到此账号",
+ "mutes_and_blocks": "隐藏与屏蔽",
+ "bot": "这是一个机器人账号",
+ "fun": "趣味",
+ "useStreamingApiWarning": "(不推荐使用,试验性,已知会跳过一些帖子)",
+ "chatMessageRadius": "聊天消息",
+ "greentext": "Meme 箭头",
+ "virtual_scrolling": "优化时间线渲染",
+ "import_mutes_from_a_csv_file": "从 csv 文件导入隐藏名单",
+ "mutes_imported": "隐藏名单导入成功!处理它们将需要一段时间。",
+ "mute_import_error": "导入隐藏名单出错",
+ "mute_import": "隐藏名单导入",
+ "mute_export_button": "导出你的隐藏名单到一个 csv 文件",
+ "mute_export": "隐藏名单导出"
},
"time": {
"day": "{0} 天",
@@ -516,7 +593,8 @@
"show_new": "显示新内容",
"up_to_date": "已是最新",
"no_more_statuses": "没有更多的状态",
- "no_statuses": "没有状态更新"
+ "no_statuses": "没有状态更新",
+ "reload": "重新载入"
},
"status": {
"favorites": "收藏",
@@ -529,7 +607,18 @@
"reply_to": "回复",
"replies_list": "回复:",
"mute_conversation": "隐藏对话",
- "unmute_conversation": "对话取消隐藏"
+ "unmute_conversation": "对话取消隐藏",
+ "hide_content": "隐藏内容",
+ "show_content": "显示内容",
+ "hide_full_subject": "隐藏此部分标题",
+ "show_full_subject": "显示全部标题",
+ "thread_muted": "此系列消息已被隐藏",
+ "copy_link": "复制状态链接",
+ "status_unavailable": "状态不可取得",
+ "unbookmark": "取消书签",
+ "bookmark": "书签",
+ "thread_muted_and_words": ",含有过滤词:",
+ "status_deleted": "该状态已被删除"
},
"user_card": {
"approve": "允许",
@@ -556,9 +645,9 @@
"statuses": "状态",
"subscribe": "订阅",
"unsubscribe": "退订",
- "unblock": "取消拉黑",
- "unblock_progress": "取消拉黑中…",
- "block_progress": "拉黑中…",
+ "unblock": "取消屏蔽",
+ "unblock_progress": "正在取消屏蔽…",
+ "block_progress": "正在屏蔽…",
"unmute": "取消隐藏",
"unmute_progress": "取消隐藏中…",
"mute_progress": "隐藏中…",
@@ -579,11 +668,13 @@
"disable_any_subscription": "完全禁止关注用户",
"quarantine": "从联合实例中禁止用户帖子",
"delete_user": "删除用户",
- "delete_user_confirmation": "你确认吗?此操作无法撤销。"
+ "delete_user_confirmation": "你确定吗?此操作无法撤销。"
},
"hidden": "已隐藏",
"show_repeats": "显示转发",
- "hide_repeats": "隐藏转发"
+ "hide_repeats": "隐藏转发",
+ "message": "消息",
+ "mention": "提及"
},
"user_profile": {
"timeline_title": "用户时间线",
@@ -592,12 +683,12 @@
},
"user_reporting": {
"title": "报告 {0}",
- "add_comment_description": "此报告会发送给你的实例管理员。你可以在下面提供更多详细信息解释报告的缘由:",
+ "add_comment_description": "此报告会发送给您的实例管理员。您可以在下面提供更多详细信息解释报告的缘由:",
"additional_comments": "其它信息",
"forward_description": "这个账号是从另外一个服务器。同时发送一个副本到那里?",
"forward_to": "转发 {0}",
"submit": "提交",
- "generic_error": "当处理你的请求时,发生了一个错误。"
+ "generic_error": "当处理您的请求时,发生了一个错误。"
},
"who_to_follow": {
"more": "更多",
@@ -610,7 +701,9 @@
"favorite": "收藏",
"user_settings": "用户设置",
"reject_follow_request": "拒绝关注请求",
- "add_reaction": "添加互动"
+ "add_reaction": "添加互动",
+ "bookmark": "书签",
+ "accept_follow_request": "接受关注请求"
},
"upload": {
"error": {
@@ -628,7 +721,7 @@
},
"search": {
"people": "人",
- "hashtags": "Hashtags",
+ "hashtags": "话题标签",
"person_talking": "{count} 人正在讨论",
"people_talking": "{count} 人正在讨论",
"no_results": "没有搜索结果"
@@ -636,12 +729,14 @@
"password_reset": {
"forgot_password": "忘记密码了?",
"password_reset": "重置密码",
- "instruction": "输入你的电邮地址或者用户名,我们将发送一个链接到你的邮箱,用于重置密码。",
- "placeholder": "你的电邮地址或者用户名",
- "check_email": "检查你的邮箱,会有一个链接用于重置密码。",
+ "instruction": "输入您的电邮地址或者用户名,我们将发送一个链接到您的邮箱,用于重置密码。",
+ "placeholder": "您的电邮地址或者用户名",
+ "check_email": "检查您的邮箱,会有一个链接用于重置密码。",
"return_home": "回到首页",
- "too_many_requests": "你触发了尝试的限制,请稍后再试。",
- "password_reset_disabled": "密码重置已经被禁用。请联系你的实例管理员。"
+ "too_many_requests": "您达到了尝试次数的上限,请稍后再试。",
+ "password_reset_disabled": "密码重置已被禁用。请联系您的实例管理员。",
+ "password_reset_required_but_mailer_is_disabled": "您必须重置密码,但是密码重置被禁用了。请联系您所在实例的管理员。",
+ "password_reset_required": "您必须重置密码才能登陆。"
},
"remote_user_resolver": {
"error": "未找到。",
@@ -650,27 +745,34 @@
},
"emoji": {
"keep_open": "选择器保持打开",
- "stickers": "贴图",
+ "stickers": "贴纸",
"unicode": "Unicode 表情符号",
"custom": "自定义表情符号",
"add_emoji": "插入表情符号",
"search_emoji": "搜索表情符号",
- "emoji": "表情符号"
+ "emoji": "表情符号",
+ "load_all": "加载所有表情符号(共 {emojiAmount} 个)",
+ "load_all_hint": "最先加载的 {saneAmount} 表情符号,加载全部表情符号可能会带来性能问题。"
},
"about": {
"mrf": {
"simple": {
- "quarantine_desc": "本实例只会把公开状态发送非下列实例:",
+ "quarantine_desc": "本实例向以下实例仅发送公开的帖子:",
"quarantine": "隔离",
"reject_desc": "本实例不会接收来自下列实例的消息:",
"reject": "拒绝",
"accept_desc": "本实例只接收来自下列实例的消息:",
- "simple_policies": "站规",
+ "simple_policies": "对于特定实例的策略",
"accept": "接受",
- "media_removal": "移除媒体"
+ "media_removal": "移除媒体",
+ "media_nsfw_desc": "本实例将来自以下实例的媒体内容强制设置为敏感内容:",
+ "media_nsfw": "强制设置媒体为敏感内容",
+ "media_removal_desc": "本实例移除来自以下实例的媒体内容:",
+ "ftl_removal_desc": "该实例在从“全部已知网络”时间线上移除了下列实例:",
+ "ftl_removal": "从“全部已知网络”时间线上移除"
},
"mrf_policies_desc": "MRF 策略会影响本实例的互通行为。以下策略已启用:",
- "mrf_policies": "已启动 MRF 策略",
+ "mrf_policies": "已启动的 MRF 策略",
"keyword": {
"ftl_removal": "从“全部已知网络”时间线上移除",
"keyword_policies": "关键词策略",
@@ -678,13 +780,42 @@
"replace": "替换",
"reject": "拒绝"
},
- "federation": "联邦"
- }
+ "federation": "联邦互通"
+ },
+ "staff": "管理人员"
},
"domain_mute_card": {
"unmute_progress": "正在取消隐藏…",
"unmute": "取消隐藏",
"mute_progress": "隐藏中…",
"mute": "隐藏"
+ },
+ "errors": {
+ "storage_unavailable": "Pleroma 无法访问浏览器储存。您的登陆名以及本地设置将不会被保存,您可能遇到意外问题。请尝试启用 cookies。"
+ },
+ "shoutbox": {
+ "title": "留言板"
+ },
+ "display_date": {
+ "today": "今天"
+ },
+ "file_type": {
+ "file": "文件",
+ "image": "图片",
+ "video": "视频",
+ "audio": "音频"
+ },
+ "chats": {
+ "empty_chat_list_placeholder": "您还没有任何聊天记录。开始聊天吧!",
+ "error_sending_message": "发送消息时出了点问题。",
+ "error_loading_chat": "加载聊天时出了点问题。",
+ "delete_confirm": "您确实要删除此消息吗?",
+ "more": "更多",
+ "empty_message_error": "无法发布空消息",
+ "new": "新聊天",
+ "chats": "聊天",
+ "delete": "删除",
+ "message_user": "发消息给 {nickname}",
+ "you": "你:"
}
}
diff --git a/src/i18n/zh_Hant.json b/src/i18n/zh_Hant.json
new file mode 100644
index 00000000..f2625116
--- /dev/null
+++ b/src/i18n/zh_Hant.json
@@ -0,0 +1,810 @@
+{
+ "emoji": {
+ "unicode": "統一碼繪文字",
+ "custom": "自定義繪文字",
+ "add_emoji": "插入繪文字",
+ "search_emoji": "搜索繪文字",
+ "keep_open": "選擇器保持打開",
+ "emoji": "繪文字",
+ "stickers": "貼紙",
+ "load_all": "加載所有繪文字(共 {emojiAmount} 個)",
+ "load_all_hint": "最先加載的 {saneAmount} ,加載全部繪文字可能會帶來性能問題。"
+ },
+ "polls": {
+ "not_enough_options": "投票的選項太少",
+ "expired": "投票 {0} 前已結束",
+ "expires_in": "投票於 {0} 內結束",
+ "expiry": "投票期限",
+ "multiple_choices": "多選",
+ "single_choice": "單選",
+ "type": "問卷類型",
+ "vote": "投票",
+ "votes": "票",
+ "option": "選項",
+ "add_option": "增加選項",
+ "add_poll": "增加投票"
+ },
+ "notifications": {
+ "reacted_with": "和 {0} 互動過",
+ "migrated_to": "遷移到",
+ "no_more_notifications": "沒有更多的通知",
+ "repeated_you": "轉發了你的發文",
+ "read": "已閱!",
+ "notifications": "通知",
+ "load_older": "載入更早的通知",
+ "follow_request": "想要關注你",
+ "followed_you": "關注了你",
+ "favorited_you": "喜歡了你的發文",
+ "broken_favorite": "未知的狀態,正在搜索中…"
+ },
+ "nav": {
+ "chats": "聊天",
+ "timelines": "時間線",
+ "preferences": "偏好設置",
+ "who_to_follow": "推薦關注",
+ "search": "搜索",
+ "user_search": "用戶搜索",
+ "bookmarks": "書籤",
+ "twkn": "已知網絡",
+ "timeline": "時間線",
+ "public_tl": "公共時間線",
+ "dms": "私信",
+ "interactions": "互動",
+ "mentions": "提及",
+ "friend_requests": "關注請求",
+ "back": "後退",
+ "administration": "管理",
+ "about": "關於"
+ },
+ "media_modal": {
+ "next": "往後",
+ "previous": "往前"
+ },
+ "login": {
+ "heading": {
+ "recovery": "雙重因素恢復",
+ "totp": "雙重因素驗證"
+ },
+ "recovery_code": "恢復碼",
+ "enter_two_factor_code": "輸入一個雙重因素驗證碼",
+ "enter_recovery_code": "輸入一個恢復碼",
+ "authentication_code": "驗證碼",
+ "hint": "登錄後加入討論",
+ "username": "用戶名",
+ "register": "註冊",
+ "placeholder": "例:鈴音",
+ "password": "密碼",
+ "logout": "登出",
+ "description": "用 OAuth 登入",
+ "login": "登入"
+ },
+ "importer": {
+ "error": "導入此文件時出現一個錯誤。",
+ "success": "導入成功。",
+ "submit": "提交"
+ },
+ "image_cropper": {
+ "cancel": "取消",
+ "save_without_cropping": "保存不裁剪",
+ "save": "保存",
+ "crop_picture": "裁剪圖片"
+ },
+ "general": {
+ "peek": "窺視",
+ "close": "關閉",
+ "verify": "驗證",
+ "confirm": "確認",
+ "enable": "啟用",
+ "disable": "禁用",
+ "cancel": "取消",
+ "dismiss": "忽略",
+ "show_less": "收起",
+ "show_more": "展開",
+ "optional": "可選",
+ "retry": "再試",
+ "error_retry": "請再試",
+ "generic_error": "發生一個錯誤",
+ "loading": "載入中…",
+ "more": "更多",
+ "submit": "提交",
+ "apply": "應用"
+ },
+ "finder": {
+ "find_user": "尋找用戶",
+ "error_fetching_user": "獲取用戶時發生錯誤"
+ },
+ "features_panel": {
+ "who_to_follow": "推薦關注",
+ "title": "特色",
+ "text_limit": "文字數量限制",
+ "scope_options": "可見範圍設置",
+ "media_proxy": "媒體代理",
+ "pleroma_chat_messages": "Pleroma 聊天",
+ "chat": "聊天",
+ "gopher": "Gopher"
+ },
+ "exporter": {
+ "processing": "正在處理,稍後會提示您下載文件",
+ "export": "導出"
+ },
+ "domain_mute_card": {
+ "unmute_progress": "取消靜音中…",
+ "unmute": "取消靜音",
+ "mute_progress": "靜音中…",
+ "mute": "靜音"
+ },
+ "shoutbox": {
+ "title": "留言板"
+ },
+ "about": {
+ "staff": "職員",
+ "mrf": {
+ "simple": {
+ "media_nsfw_desc": "這個實例強迫以下實例的帖子媒體設定為敏感:",
+ "media_nsfw": "媒體強制設定為敏感",
+ "media_removal_desc": "這個實例移除以下實例的帖子媒體:",
+ "media_removal": "移除媒體",
+ "ftl_removal_desc": "這個實例在所有已知網絡中移除下列實例:",
+ "ftl_removal": "從所有已知網路中移除",
+ "quarantine_desc": "本實例只會把公開發文發送到下列實例:",
+ "quarantine": "隔離",
+ "reject_desc": "本實例不會接收來自下列實例的消息:",
+ "reject": "拒絕",
+ "accept_desc": "本實例只接收來自下列實例的消息:",
+ "simple_policies": "站規",
+ "accept": "接受"
+ },
+ "mrf_policies_desc": "MRF 策略會影響本實例的互通行為。以下策略已啟用:",
+ "keyword": {
+ "ftl_removal": "從“全部已知網絡”時間線上移除",
+ "replace": "取代",
+ "reject": "拒絕",
+ "is_replaced_by": "→",
+ "keyword_policies": "關鍵字政策"
+ },
+ "mrf_policies": "已啟用的MRF政策",
+ "federation": "聯邦"
+ }
+ },
+ "settings": {
+ "style": {
+ "common": {
+ "color": "顏色",
+ "contrast": {
+ "context": {
+ "18pt": "大字文本 (18pt+)",
+ "text": "文本"
+ },
+ "level": {
+ "aaa": "符合 AAA 等級準則(推薦)",
+ "aa": "符合 AA 等級準則(最低)",
+ "bad": "不符合任何輔助功能指南"
+ },
+ "hint": "對比度是 {ratio}, 它 {level} {context}"
+ },
+ "opacity": "透明度"
+ },
+ "advanced_colors": {
+ "faint_text": "灰度文字",
+ "alert_error": "錯誤",
+ "badge_notification": "通知",
+ "alert": "提醒或警告背景色",
+ "_tab_label": "高级",
+ "alert_warning": "警告",
+ "alert_neutral": "中性",
+ "post": "帖子/用戶簡介",
+ "badge": "徽章背景",
+ "popover": "提示框,菜單,彈出框",
+ "panel_header": "面板標題",
+ "top_bar": "頂欄",
+ "borders": "邊框",
+ "buttons": "按鈕",
+ "inputs": "輸入框",
+ "underlay": "底襯",
+ "poll": "投票統計圖",
+ "icons": "圖標",
+ "highlight": "強調元素",
+ "pressed": "按下",
+ "selectedPost": "選中的帖子",
+ "selectedMenu": "選中的菜單項",
+ "disabled": "關閉",
+ "toggled": "切換",
+ "tabs": "標籤",
+ "chat": {
+ "incoming": "收到",
+ "outgoing": "發出",
+ "border": "邊框"
+ }
+ },
+ "preview": {
+ "header_faint": "這很正常",
+ "header": "預覽",
+ "content": "內容",
+ "error": "例子錯誤",
+ "button": "按鈕",
+ "text": "有堆 {0} 和 {1}",
+ "mono": "內容",
+ "input": "剛剛抵達洛杉磯.",
+ "faint_link": "有用的手冊",
+ "fine_print": "閱讀我們的 {0} ,然而什麼有用的也學不到!",
+ "checkbox": "我已經瀏覽了條款及細則",
+ "link": "一個很好的小鏈接"
+ },
+ "shadows": {
+ "override": "覆寫",
+ "_tab_label": "陰影和燈光",
+ "component": "組件",
+ "shadow_id": "陰影 #{value}",
+ "blur": "模糊",
+ "spread": "擴散",
+ "inset": "插圖",
+ "hintV3": "對於陰影,您還可以使用{0}表示法來使用其他顏色插槽。",
+ "filter_hint": {
+ "always_drop_shadow": "警告,此陰影設置會總是使用 {0} ,如果瀏覽器支持的話。",
+ "drop_shadow_syntax": "{0} 不支持參數 {1} 和關鍵詞 {2} 。",
+ "avatar_inset": "請注意組合兩個內部和非內部的陰影到頭像上,在透明頭像上可能會有意料之外的效果。",
+ "spread_zero": "陰影的擴散 > 0 會同設置成零一樣",
+ "inset_classic": "插入內部的陰影會使用 {0}"
+ },
+ "components": {
+ "panel": "面板",
+ "panelHeader": "面板標題",
+ "topBar": "頂欄",
+ "avatar": "用戶頭像(在個人資料欄)",
+ "avatarStatus": "用戶頭像(在帖子顯示欄)",
+ "popup": "彈窗和工具提示",
+ "button": "按鈕",
+ "buttonHover": "按鈕(懸停)",
+ "buttonPressed": "按鈕(按下)",
+ "buttonPressedHover": "按鈕(按下和懸停)",
+ "input": "輸入框"
+ }
+ },
+ "switcher": {
+ "use_snapshot": "舊版",
+ "load_theme": "載入主題",
+ "keep_color": "保留顏色",
+ "keep_shadows": "保留陰影",
+ "keep_opacity": "保留透明度",
+ "keep_roundness": "保留圓角",
+ "help": {
+ "migration_napshot_gone": "不知出於何種原因,主題快照缺失了,一些地方可能與您印象中的不符。",
+ "snapshot_source_mismatch": "版本衝突:很有可能是 FE 版本回滾後再次升級了,如果您使用舊版本的 FE 更改了主題那麼您可能需要使用舊版本,否則請使用新版本。",
+ "future_version_imported": "您導入的文件來自更高版本的 FE。",
+ "older_version_imported": "您導入的文件來自舊版本的 FE。",
+ "snapshot_missing": "在文件中沒有主題快照,所以網站外觀可能會與原來預想的不同。",
+ "fe_upgraded": "PleromaFE 的主題引擎隨著版本更新升級了。",
+ "fe_downgraded": "PleromaFE 的版本回滾了。",
+ "upgraded_from_v2": "PleromaFE 已升級,主題會和你記憶中的不太一樣。",
+ "v2_imported": "您導入的文件是舊版 FE 的。我們儘可能保持兼容性,但還是可能出現不一致的情況。",
+ "snapshot_present": "載入快照已加載,因此所有值均被覆蓋。 您可以改為載入主題實際數據。",
+ "migration_snapshot_ok": "為保萬無一失,載入了主題快照。您可以試著載入主題數據。"
+ },
+ "use_source": "新版本",
+ "keep_as_is": "保持原狀",
+ "clear_opacity": "清除透明度",
+ "clear_all": "清除全部",
+ "reset": "重置",
+ "keep_fonts": "保留字體",
+ "save_load_hint": "\"保留\" 選項在選擇或載入主題時保留當前設置的選項,在導出主題時還會存儲上述選項。當所有複選框未設置時,導出主題將保存所有內容。"
+ },
+ "fonts": {
+ "components": {
+ "interface": "界面",
+ "input": "輸入框",
+ "post": "發帖文字",
+ "postCode": "帖子中使用等間距文字(富文本)"
+ },
+ "_tab_label": "字體",
+ "help": "給用戶界面的元素選擇字體。選擇 “自選”的你必須輸入確切的字體名稱。",
+ "family": "字體名稱",
+ "size": "大小 (像素)",
+ "weight": "字重 (粗體))",
+ "custom": "自選"
+ },
+ "common_colors": {
+ "foreground_hint": "點擊”高級“ 標籤進行細緻的控制",
+ "main": "常用顏色",
+ "_tab_label": "共同",
+ "rgbo": "圖標,強調,徽章"
+ },
+ "radii": {
+ "_tab_label": "圓角"
+ }
+ },
+ "notification_setting_block_from_strangers": "屏蔽來自你沒有關注的用戶的通知",
+ "user_mutes": "用户",
+ "hide_followers_count_description": "不顯示關注者數量",
+ "no_rich_text_description": "不顯示富文本格式",
+ "notification_visibility_moves": "用戶遷移",
+ "notification_visibility_repeats": "轉發",
+ "notification_visibility_mentions": "提及",
+ "notification_visibility_likes": "點贊",
+ "interfaceLanguage": "界面語言",
+ "instance_default": "(默認:{value})",
+ "inputRadius": "輸入框",
+ "import_theme": "導入預置主題",
+ "import_followers_from_a_csv_file": "從 csv 文件中導入關注",
+ "import_blocks_from_a_csv_file": "從 csv 文件中導入封鎖黑名單名單",
+ "hide_filtered_statuses": "隱藏過濾的發文",
+ "lock_account_description": "你需要手動審核關注請求",
+ "loop_video": "循環視頻",
+ "loop_video_silent_only": "只循環沒有聲音的視頻(例如:Mastodon 裡的“GIF”)",
+ "mutes_tab": "靜音",
+ "play_videos_in_modal": "在彈出框內播放視頻",
+ "profile_fields": {
+ "add_field": "添加字段",
+ "name": "標籤",
+ "value": "內容",
+ "label": "個人資料元數據"
+ },
+ "use_contain_fit": "生成縮略圖時不要裁剪附件",
+ "notification_visibility": "要顯示的通知類型",
+ "notification_visibility_follows": "關注",
+ "new_email": "新電郵",
+ "subject_line_mastodon": "比如mastodon: copy as is",
+ "reset_background_confirm": "您確定要重置個人資料背景圖嗎?",
+ "reset_banner_confirm": "您確定要重置橫幅圖片嗎?",
+ "reset_avatar_confirm": "您確定要重置頭像嗎?",
+ "reset_profile_banner": "重置橫幅圖片",
+ "reset_profile_background": "重置個人資料背景圖",
+ "reset_avatar": "重置頭像",
+ "discoverable": "允許通過搜索檢索等服務找到此賬號",
+ "delete_account_error": "刪除賬戶時發生錯誤,如果一直刪除不了,請聯繫實例管理員。",
+ "composing": "正在書寫",
+ "chatMessageRadius": "聊天訊息",
+ "mfa": {
+ "confirm_and_enable": "確認並啟用OTP",
+ "setup_otp": "設置OTP",
+ "otp": "OTP",
+ "wait_pre_setup_otp": "預設OTP",
+ "verify": {
+ "desc": "要啟用雙因素驗證,請把你的雙因素驗證 app 裡的數字輸入:"
+ },
+ "scan": {
+ "secret_code": "密鑰",
+ "desc": "使用你的雙因素驗證 app,掃瞄這個二維碼,或者輸入這些文字密鑰:",
+ "title": "掃瞄"
+ },
+ "authentication_methods": "身份驗證方法",
+ "recovery_codes_warning": "抄寫這些號碼,或者保存在安全的地方。這些號碼不會再次顯示。如果你無法訪問你的 2FA app,也丟失了你的恢復碼,你的賬號就再也無法登錄了。",
+ "waiting_a_recovery_codes": "正在接收備份碼…",
+ "recovery_codes": "恢復碼。",
+ "warning_of_generate_new_codes": "當你生成新的恢復碼時,你的舊恢復碼就失效了。",
+ "generate_new_recovery_codes": "生成新的恢復碼",
+ "title": "雙因素驗證"
+ },
+ "new_password": "新密碼",
+ "name_bio": "名字及簡介",
+ "name": "名字",
+ "domain_mutes": "域名",
+ "delete_account_instructions": "在下面輸入密碼,以確認刪除帳戶。",
+ "delete_account_description": "永久刪除你的帳號和所有數據。",
+ "delete_account": "刪除帳戶",
+ "default_vis": "默認可見性範圍",
+ "data_import_export_tab": "數據導入/導出",
+ "mutes_and_blocks": "靜音與封鎖",
+ "current_password": "當前密碼",
+ "confirm_new_password": "確認新密碼",
+ "collapse_subject": "摺疊帶標題的內容",
+ "checkboxRadius": "複選框",
+ "instance_default_simple": "(默認)",
+ "interface": "界面",
+ "invalid_theme_imported": "您所選擇的主題文件不被 Pleroma 支持,因此主題未被修改。",
+ "limited_availability": "在您的瀏覽器中無法使用",
+ "links": "鏈接",
+ "changed_password": "成功修改了密碼!",
+ "change_password_error": "修改密碼的時候出了點問題。",
+ "change_password": "修改密碼",
+ "changed_email": "郵箱修改成功!",
+ "bot": "這是一個機器人賬號",
+ "change_email": "修改電子郵箱",
+ "cRed": "紅色(取消)",
+ "cOrange": "橙色(收藏)",
+ "cGreen": "綠色(轉發)",
+ "cBlue": "藍色(回覆,關注)",
+ "btnRadius": "按鈕",
+ "notification_visibility_emoji_reactions": "互動",
+ "no_blocks": "沒有封鎖",
+ "no_mutes": "沒有靜音",
+ "hide_follows_description": "不要顯示我所關注的人",
+ "hide_followers_description": "不要顯示關注我的人",
+ "hide_follows_count_description": "不顯示關注數",
+ "nsfw_clickthrough": "將敏感附件隱藏,點擊才能打開",
+ "valid_until": "有效期至",
+ "panelRadius": "面板",
+ "pause_on_unfocused": "在離開頁面時暫停時間線推送",
+ "notifications": "通知",
+ "notification_setting_filters": "過濾器",
+ "notification_setting_privacy": "隱私",
+ "notification_mutes": "要停止收到某個指定的用戶的通知,請使用靜音功能。",
+ "notification_blocks": "封鎖一個用戶會停掉所有他的通知,等同於取消關注。",
+ "enable_web_push_notifications": "啟用 web 推送通知",
+ "presets": "預置",
+ "profile_background": "個人背景圖",
+ "profile_banner": "橫幅圖片",
+ "profile_tab": "個人資料",
+ "radii_help": "設置界面邊緣的圓角 (單位:像素)",
+ "reply_visibility_all": "顯示所有回覆",
+ "autohide_floating_post_button": "自動隱藏新帖子的按鈕(移動設備)",
+ "saving_err": "保存設置時發生錯誤",
+ "saving_ok": "設置已保存",
+ "search_user_to_block": "搜索你想屏蔽的用戶",
+ "search_user_to_mute": "搜索你想要隱藏的用戶",
+ "security_tab": "安全",
+ "set_new_avatar": "設置新頭像",
+ "set_new_profile_background": "設置新的個人背景",
+ "set_new_profile_banner": "設置新的個人橫幅",
+ "settings": "設置",
+ "subject_input_always_show": "總是顯示主題框",
+ "subject_line_behavior": "回覆時複製主題",
+ "subject_line_email": "比如電郵: \"re: 主題\"",
+ "subject_line_noop": "不要複製",
+ "post_status_content_type": "發文內容類型",
+ "stop_gifs": "鼠標懸停時播放GIF",
+ "streaming": "開啟滾動到頂部時的自動推送",
+ "text": "文本",
+ "theme": "主題",
+ "theme_help": "使用十六進制代碼(#rrggbb)來設置主題顏色。",
+ "theme_help_v2_1": "你也可以通過切換複選框來覆蓋某些組件的顏色和透明。使用“清除所有”來清楚所有覆蓋設置。",
+ "theme_help_v2_2": "某些條目下的圖標是背景或文本對比指示器,鼠標懸停可以獲取詳細信息。請記住,使用透明度來顯示最差的情況。",
+ "tooltipRadius": "提醒",
+ "upload_a_photo": "上傳照片",
+ "user_settings": "用戶設置",
+ "values": {
+ "false": "否",
+ "true": "是"
+ },
+ "avatar_size_instruction": "推薦的頭像圖片最小的尺寸是 150x150 像素。",
+ "emoji_reactions_on_timeline": "在時間線上顯示繪文字互動",
+ "export_theme": "導出預置主題",
+ "filtering": "過濾",
+ "filtering_explanation": "所有包含以下詞彙的內容都會被隱藏,一行一個",
+ "follow_export": "導出關注",
+ "follow_export_button": "將關注導出成 csv 文件",
+ "follow_import": "導入關注",
+ "follow_import_error": "導入關注時錯誤",
+ "follows_imported": "關注已導入!尚需要一些時間來處理。",
+ "hide_attachments_in_convo": "在對話中隱藏附件",
+ "hide_attachments_in_tl": "在時間線上隱藏附件",
+ "hide_muted_posts": "不顯示被靜音的用戶的帖子",
+ "max_thumbnails": "最多每個帖子所能顯示的縮略圖數量",
+ "hide_isp": "隱藏指定實例的面板",
+ "preload_images": "預載圖片",
+ "use_one_click_nsfw": "點擊一次以打開工作場所不適宜的附件",
+ "hide_post_stats": "隱藏帖子的統計數據(例如:收藏的次數)",
+ "hide_user_stats": "隱藏用戶的統計數據(例如:關注者的數量)",
+ "general": "通用",
+ "foreground": "前景",
+ "blocks_tab": "封鎖",
+ "blocks_imported": "封鎖黑名單導入成功!需要一點時間來處理。",
+ "block_import_error": "導入封鎖黑名單出錯",
+ "block_import": "封鎖黑名單導入",
+ "block_export_button": "導出你的封鎖黑名單到一個 csv 文件",
+ "block_export": "封鎖黑名單導出",
+ "bio": "簡介",
+ "background": "背景",
+ "avatarRadius": "頭像",
+ "avatarAltRadius": "頭像(通知)",
+ "avatar": "頭像",
+ "attachments": "附件",
+ "attachmentRadius": "附件",
+ "allow_following_move": "正在關注的賬號遷移時自動重新關注",
+ "enter_current_password_to_confirm": "輸入你當前密碼來確認你的身份",
+ "security": "安全",
+ "app_name": "App 名稱",
+ "change_email_error": "修改你的電子郵箱時發生錯誤。",
+ "type_domains_to_mute": "搜索需要隱藏的域名",
+ "pad_emoji": "從繪文字選擇器插入繪文字時,在繪文字兩側插入空格",
+ "useStreamingApi": "實時接收發佈以及通知",
+ "minimal_scopes_mode": "最小發文範圍",
+ "scope_copy": "回覆時的複製範圍(私信是總是複製的)",
+ "reply_visibility_self": "只顯示發送給我的回覆",
+ "reply_visibility_following": "只顯示發送給我的回覆/發送給我關注的用戶的回覆",
+ "replies_in_timeline": "時間線中的回覆",
+ "revoke_token": "撤消",
+ "show_admin_badge": "顯示管理徽章",
+ "accent": "強調色",
+ "greentext": "前文箭頭",
+ "show_moderator_badge": "顯示主持人徽章",
+ "oauth_tokens": "OAuth代幣",
+ "token": "代幣",
+ "refresh_token": "刷新代幣",
+ "useStreamingApiWarning": "(不推薦使用,實驗性的,已知跳過文章)",
+ "fun": "有趣",
+ "notification_setting_hide_notification_contents": "隱藏推送通知中的發送者與內容信息",
+ "version": {
+ "title": "版本",
+ "backend_version": "後端版本",
+ "frontend_version": "前端版本"
+ },
+ "virtual_scrolling": "優化時間線渲染",
+ "import_mutes_from_a_csv_file": "從CSV文件導入靜音",
+ "mutes_imported": "靜音導入了!處理它們將需要一段時間。",
+ "mute_import": "靜音導入",
+ "mute_import_error": "導入靜音時出錯",
+ "mute_export_button": "將靜音導出到csv文件",
+ "mute_export": "靜音導出"
+ },
+ "chats": {
+ "more": "更多",
+ "delete_confirm": "您確實要刪除此消息嗎?",
+ "error_loading_chat": "加載聊天時出了點問題。",
+ "error_sending_message": "發送消息時出了點問題。",
+ "empty_chat_list_placeholder": "您還沒有任何聊天記錄。 開始新的聊天!",
+ "new": "新聊天",
+ "empty_message_error": "無法發布空消息",
+ "you": "你:",
+ "message_user": "發消息給 {nickname}",
+ "delete": "刪除",
+ "chats": "聊天"
+ },
+ "file_type": {
+ "audio": "音頻",
+ "video": "視頻",
+ "image": "图片",
+ "file": "檔案"
+ },
+ "display_date": {
+ "today": "今天"
+ },
+ "status": {
+ "mute_conversation": "靜音對話",
+ "replies_list": "回覆:",
+ "reply_to": "回覆",
+ "pin": "在個人資料置頂",
+ "unpin": "取消在個人資料置頂",
+ "favorites": "喜歡",
+ "repeats": "轉發",
+ "delete": "刪除發文",
+ "pinned": "置頂",
+ "bookmark": "書籤",
+ "unbookmark": "取消書籤",
+ "delete_confirm": "你真的想要刪除這條發文嗎?",
+ "unmute_conversation": "對話取消靜音",
+ "status_unavailable": "發文不可取得",
+ "copy_link": "複製發文鏈接",
+ "thread_muted": "静音線程",
+ "show_full_subject": "顯示完整標題",
+ "thread_muted_and_words": ",有这些字:",
+ "hide_full_subject": "隱藏完整標題",
+ "show_content": "顯示內容",
+ "hide_content": "隱藏內容"
+ },
+ "time": {
+ "hours": "{0} 小時",
+ "days_short": "{0}天",
+ "day_short": "{0}天",
+ "days": "{0} 天",
+ "hour": "{0} 小时",
+ "hour_short": "{0}h",
+ "hours_short": "{0}h",
+ "years_short": "{0} y",
+ "now": "剛剛",
+ "day": "{0} 天",
+ "in_future": "還有 {0}",
+ "in_past": "{0} 之前",
+ "minute": "{0} 分鐘",
+ "minute_short": "{0} 分",
+ "minutes_short": "{0} 分",
+ "minutes": "{0} 分鐘",
+ "month": "{0} 月",
+ "months": "{0} 月",
+ "month_short": "{0} 月",
+ "months_short": "{0} 月",
+ "now_short": "剛剛",
+ "second": "{0} 秒",
+ "seconds": "{0} 秒",
+ "second_short": "{0} 秒",
+ "seconds_short": "{0} 秒",
+ "week": "{0}周",
+ "weeks": "{0}周",
+ "week_short": "{0}周",
+ "weeks_short": "{0}周",
+ "year": "{0} 年",
+ "years": "{0} 年",
+ "year_short": "{0}年"
+ },
+ "post_status": {
+ "media_description_error": "無法更新媒體,請重試",
+ "media_description": "媒體描述",
+ "scope": {
+ "unlisted": "不公開 - 不會發送到公共時間軸",
+ "public": "公共 - 發送到公共時間軸",
+ "private": "僅關注者 - 只有關注了你的人能看到",
+ "direct": "私信 - 只發送給被提及的用戶"
+ },
+ "scope_notice": {
+ "unlisted": "本條內容既不在公共時間線,也不會在所有已知網絡上可見",
+ "private": "關注你的人才能看到本條內容",
+ "public": "本條帖子可以被所有人看到"
+ },
+ "preview_empty": "空的",
+ "preview": "預覽",
+ "posting": "正在發送",
+ "direct_warning_to_first_only": "本條內容只有被在消息開始處提及的用戶能夠看到。",
+ "direct_warning_to_all": "本條內容只有被提及的用戶能夠看到。",
+ "account_not_locked_warning": "你的帳號沒有 {0}。任何人都可以關注你並瀏覽你的上鎖內容。",
+ "new_status": "發佈新發文",
+ "content_warning": "主題(可選)",
+ "content_type": {
+ "text/bbcode": "BBCode",
+ "text/markdown": "Markdown",
+ "text/html": "HTML",
+ "text/plain": "純文本"
+ },
+ "attachments_sensitive": "標記附件為敏感內容",
+ "account_not_locked_warning_link": "上鎖",
+ "default": "剛剛抵達洛杉磯。",
+ "empty_status_error": "無法發佈沒有附件的空發文"
+ },
+ "errors": {
+ "storage_unavailable": "Pleroma無法訪問瀏覽器存儲。您的登錄名或本地設置將不會保存,您可能會遇到意外問題。嘗試啟用Cookie。"
+ },
+ "timeline": {
+ "error_fetching": "獲取更新時發生錯誤",
+ "conversation": "對話",
+ "no_retweet_hint": "這條內容僅關注者可見,或者是私信,因此不能轉發",
+ "collapse": "摺疊",
+ "load_older": "載入更早的發文",
+ "repeated": "已轉發",
+ "show_new": "顯示新內容",
+ "reload": "重新載入",
+ "up_to_date": "已是最新",
+ "no_more_statuses": "没有更多發文",
+ "no_statuses": "没有發文"
+ },
+ "interactions": {
+ "load_older": "載入更早的互動",
+ "moves": "用戶遷移",
+ "follows": "新的關注者",
+ "favs_repeats": "轉發和收藏"
+ },
+ "selectable_list": {
+ "select_all": "選擇全部"
+ },
+ "remote_user_resolver": {
+ "error": "未找到。",
+ "searching_for": "搜索",
+ "remote_user_resolver": "遠程用戶解析器"
+ },
+ "registration": {
+ "validations": {
+ "password_confirmation_match": "不能和密碼一樣",
+ "password_confirmation_required": "不能留空",
+ "password_required": "不能留空",
+ "email_required": "不能留空",
+ "fullname_required": "不能留空",
+ "username_required": "不能留空"
+ },
+ "fullname": "顯示名稱",
+ "bio_placeholder": "例如:\n你好,我是玲音。\n我是一個住在日本郊區的動畫少女。你可能在 Wired 見過我。",
+ "fullname_placeholder": "例如:岩倉玲音",
+ "username_placeholder": "例如:lain",
+ "new_captcha": "點擊圖片獲取新的驗證碼",
+ "captcha": "CAPTCHA",
+ "token": "邀請碼",
+ "registration": "註冊",
+ "password_confirm": "確認密碼",
+ "email": "電子郵箱",
+ "bio": "簡介"
+ },
+ "user_card": {
+ "its_you": "就是你!!",
+ "media": "媒體",
+ "per_day": "每天",
+ "remote_follow": "跨站關注",
+ "subscribe": "訂閱",
+ "mute_progress": "靜音中…",
+ "admin_menu": {
+ "delete_account": "刪除賬號",
+ "delete_user": "刪除用戶",
+ "delete_user_confirmation": "你確認嗎?此操作無法撤銷。",
+ "moderation": "調停",
+ "grant_admin": "賦予管理權限",
+ "revoke_admin": "撤銷管理權限",
+ "grant_moderator": "賦予主持人權限",
+ "revoke_moderator": "撤銷主持人權限",
+ "activate_account": "啟用賬號",
+ "deactivate_account": "關閉賬號",
+ "force_nsfw": "標記所有的帖子都是工作場合不適",
+ "strip_media": "從帖子裡刪除媒體文件",
+ "force_unlisted": "強制帖子為不公開",
+ "sandbox": "強制帖子為只有關注者可看",
+ "disable_remote_subscription": "禁止從遠程實例關注用戶",
+ "disable_any_subscription": "完全禁止關注用戶",
+ "quarantine": "從聯合實例中禁止用戶帖子"
+ },
+ "approve": "批准",
+ "block": "封鎖",
+ "blocked": "已封鎖!",
+ "deny": "拒絕",
+ "favorites": "喜歡",
+ "follow": "關注",
+ "follow_sent": "請求已發送!",
+ "follow_progress": "請求中…",
+ "follow_again": "再次發送請求?",
+ "follow_unfollow": "取消關注",
+ "followees": "正在關注",
+ "followers": "關注者",
+ "following": "正在關注!",
+ "follows_you": "關注了你!",
+ "hidden": "已隱藏",
+ "mention": "提及",
+ "message": "消息",
+ "mute": "靜音",
+ "muted": "已靜音",
+ "report": "報告",
+ "statuses": "發文",
+ "unsubscribe": "退訂",
+ "unblock": "取消封鎖",
+ "unblock_progress": "取消封鎖中…",
+ "block_progress": "封鎖中…",
+ "unmute": "取消靜音",
+ "unmute_progress": "取消靜音中…",
+ "hide_repeats": "隱藏轉發",
+ "show_repeats": "顯示轉發"
+ },
+ "user_profile": {
+ "timeline_title": "用戶時間線",
+ "profile_does_not_exist": "抱歉,此個人資料不存在。",
+ "profile_loading_error": "抱歉,載入個人資料時出錯。"
+ },
+ "user_reporting": {
+ "title": "報告 {0}",
+ "add_comment_description": "此報告會發送給你的實例管理員。你可以在下面提供更多詳細信息解釋報告的緣由:",
+ "forward_to": "轉發 {0}",
+ "submit": "提交",
+ "generic_error": "當處理你的請求時,發生了一個錯誤。",
+ "additional_comments": "其它評論",
+ "forward_description": "這個賬號是從另外一個服務器。同時發送一個報告到那裡?"
+ },
+ "who_to_follow": {
+ "more": "更多",
+ "who_to_follow": "推薦關注"
+ },
+ "tool_tip": {
+ "media_upload": "上傳多媒體",
+ "repeat": "轉發",
+ "favorite": "喜歡",
+ "add_reaction": "添加互動",
+ "reply": "回覆",
+ "user_settings": "用戶設置",
+ "accept_follow_request": "接受關注請求",
+ "reject_follow_request": "拒絕關注請求",
+ "bookmark": "書籤"
+ },
+ "upload": {
+ "file_size_units": {
+ "B": "B",
+ "KiB": "KiB",
+ "TiB": "TiB",
+ "MiB": "MiB",
+ "GiB": "GiB"
+ },
+ "error": {
+ "base": "上傳失敗。",
+ "file_too_big": "文件太大[{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]",
+ "default": "稍後再試"
+ }
+ },
+ "search": {
+ "people": "人",
+ "hashtags": "標籤",
+ "person_talking": "{count} 人正在討論",
+ "people_talking": "{count} 人正在討論",
+ "no_results": "沒有搜索結果"
+ },
+ "password_reset": {
+ "forgot_password": "忘記密碼了?",
+ "password_reset": "重置密碼",
+ "instruction": "輸入你的電郵地址或者用戶名,我們將發送一個鏈接到你的郵箱,用於重置密碼。",
+ "placeholder": "你的電郵地址或者用戶名",
+ "check_email": "檢查你的郵箱,會有一個鏈接用於重置密碼。",
+ "return_home": "回到首頁",
+ "too_many_requests": "你觸發了嘗試的限制,請稍後再試。",
+ "password_reset_disabled": "密碼重置已經被禁用。請聯繫你的實例管理員。",
+ "password_reset_required": "您必須重置密碼才能登陸。",
+ "password_reset_required_but_mailer_is_disabled": "您必須重置密碼,但是密碼重置被禁用了。請聯繫您所在實例的管理員。"
+ }
+}
diff --git a/src/main.js b/src/main.js
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 5e213f0d..08485a30 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -20,7 +20,7 @@ const api = {
state.fetchers[fetcherName] = fetcher
},
removeFetcher (state, { fetcherName, fetcher }) {
- window.clearInterval(fetcher)
+ state.fetchers[fetcherName].stop()
delete state.fetchers[fetcherName]
},
setWsToken (state, token) {
@@ -72,13 +72,21 @@ const api = {
showImmediately: timelineData.visibleStatuses.length === 0,
timeline: 'friends'
})
+ } else if (message.event === 'delete') {
+ dispatch('deleteStatusById', message.id)
} else if (message.event === 'pleroma:chat_update') {
- dispatch('addChatMessages', {
- chatId: message.chatUpdate.id,
- messages: [message.chatUpdate.lastMessage]
- })
- dispatch('updateChat', { chat: message.chatUpdate })
- maybeShowChatNotification(store, message.chatUpdate)
+ // The setTimeout wrapper is a temporary band-aid to avoid duplicates for the user's own messages when doing optimistic sending.
+ // The cause of the duplicates is the WS event arriving earlier than the HTTP response.
+ // This setTimeout wrapper can be removed once the commit `8e41baff` is in the stable Pleroma release.
+ // (`8e41baff` adds the idempotency key to the chat message entity, which PleromaFE uses when it's available, and it makes this artificial delay unnecessary).
+ setTimeout(() => {
+ dispatch('addChatMessages', {
+ chatId: message.chatUpdate.id,
+ messages: [message.chatUpdate.lastMessage]
+ })
+ dispatch('updateChat', { chat: message.chatUpdate })
+ maybeShowChatNotification(store, message.chatUpdate)
+ }, 100)
}
}
)
diff --git a/src/modules/chats.js b/src/modules/chats.js
index c5715c14..0a373d88 100644
--- a/src/modules/chats.js
+++ b/src/modules/chats.js
@@ -3,6 +3,7 @@ import { find, omitBy, orderBy, sumBy } from 'lodash'
import chatService from '../services/chat_service/chat_service.js'
import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js'
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
+import { promiseInterval } from '../services/promise_interval/promise_interval.js'
const emptyChatList = () => ({
data: [],
@@ -15,7 +16,8 @@ const defaultState = {
openedChats: {},
openedChatMessageServices: {},
fetcher: undefined,
- currentChatId: null
+ currentChatId: null,
+ lastReadMessageId: null
}
const getChatById = (state, id) => {
@@ -42,12 +44,10 @@ const chats = {
actions: {
// Chat list
startFetchingChats ({ dispatch, commit }) {
- const fetcher = () => {
- dispatch('fetchChats', { latest: true })
- }
+ const fetcher = () => dispatch('fetchChats', { latest: true })
fetcher()
commit('setChatListFetcher', {
- fetcher: () => setInterval(() => { fetcher() }, 5000)
+ fetcher: () => promiseInterval(fetcher, 5000)
})
},
stopFetchingChats ({ commit }) {
@@ -93,9 +93,14 @@ const chats = {
commit('setCurrentChatFetcher', { fetcher: undefined })
},
readChat ({ rootState, commit, dispatch }, { id, lastReadId }) {
+ const isNewMessage = rootState.chats.lastReadMessageId !== lastReadId
+
dispatch('resetChatNewMessageCount')
- commit('readChat', { id })
- rootState.api.backendInteractor.readChat({ id, lastReadId })
+ commit('readChat', { id, lastReadId })
+
+ if (isNewMessage) {
+ rootState.api.backendInteractor.readChat({ id, lastReadId })
+ }
},
deleteChatMessage ({ rootState, commit }, value) {
rootState.api.backendInteractor.deleteChatMessage(value)
@@ -107,20 +112,23 @@ const chats = {
},
clearOpenedChats ({ rootState, commit, dispatch, rootGetters }) {
commit('clearOpenedChats', { commit })
+ },
+ handleMessageError ({ commit }, value) {
+ commit('handleMessageError', { commit, ...value })
}
},
mutations: {
setChatListFetcher (state, { commit, fetcher }) {
const prevFetcher = state.chatListFetcher
if (prevFetcher) {
- clearInterval(prevFetcher)
+ prevFetcher.stop()
}
state.chatListFetcher = fetcher && fetcher()
},
setCurrentChatFetcher (state, { fetcher }) {
const prevFetcher = state.fetcher
if (prevFetcher) {
- clearInterval(prevFetcher)
+ prevFetcher.stop()
}
state.fetcher = fetcher && fetcher()
},
@@ -209,11 +217,16 @@ const chats = {
}
}
},
- readChat (state, { id }) {
+ readChat (state, { id, lastReadId }) {
+ state.lastReadMessageId = lastReadId
const chat = getChatById(state, id)
if (chat) {
chat.unread = 0
}
+ },
+ handleMessageError (state, { chatId, fakeId, isRetry }) {
+ const chatMessageService = state.openedChatMessageServices[chatId]
+ chatService.handleMessageError(chatMessageService, fakeId, isRetry)
}
}
}
diff --git a/src/modules/config.js b/src/modules/config.js
index 409d77a4..444b8ec7 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -65,7 +65,8 @@ export const defaultState = {
useContainFit: false,
greentext: undefined, // instance default
hidePostStats: undefined, // instance default
- hideUserStats: undefined // instance default
+ hideUserStats: undefined, // instance default
+ virtualScrolling: undefined // instance default
}
// caching the instance default properties
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 3fe3bbf3..5f7bf0ec 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',
@@ -41,6 +42,7 @@ const defaultState = {
sidebarRight: false,
subjectLineBehavior: 'email',
theme: 'pleroma-dark',
+ virtualScrolling: true,
// Nasty stuff
customEmoji: [],
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index e108b2a7..e673141d 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -568,6 +568,9 @@ export const mutations = {
updateStatusWithPoll (state, { id, poll }) {
const status = state.allStatusesObject[id]
status.poll = poll
+ },
+ setVirtualHeight (state, { statusId, height }) {
+ state.allStatusesObject[statusId].virtualHeight = height
}
}
@@ -608,6 +611,10 @@ const statuses = {
commit('setDeleted', { status })
apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
},
+ deleteStatusById ({ rootState, commit }, id) {
+ const status = rootState.statuses.allStatusesObject[id]
+ commit('setDeleted', { status })
+ },
markStatusesAsDeleted ({ commit }, condition) {
commit('setManyDeleted', condition)
},
@@ -753,6 +760,9 @@ const statuses = {
store.commit('addNewStatuses', { statuses: data.statuses })
return data
})
+ },
+ setVirtualHeight ({ commit }, { statusId, height }) {
+ commit('setVirtualHeight', { statusId, height })
}
},
mutations
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index da519001..22b5e8ba 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -3,6 +3,7 @@ import { parseStatus, parseUser, parseNotification, parseAttachment, parseChat,
import { RegistrationError, StatusCodeError } from '../errors/errors'
/* eslint-env browser */
+const MUTES_IMPORT_URL = '/api/pleroma/mutes_import'
const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
@@ -128,7 +129,11 @@ const promisedRequest = ({ method, url, params, payload, credentials, headers =
return reject(new StatusCodeError(response.status, json, { url, options }, response))
}
return resolve(json)
- }))
+ })
+ .catch((error) => {
+ return reject(new StatusCodeError(response.status, error, { url, options }, response))
+ })
+ )
})
}
@@ -539,8 +544,10 @@ const fetchTimeline = ({
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
url += `?${queryString}`
+
let status = ''
let statusText = ''
+
let pagination = {}
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => {
@@ -710,6 +717,17 @@ const setMediaDescription = ({ id, description, credentials }) => {
}).then((data) => parseAttachment(data))
}
+const importMutes = ({ file, credentials }) => {
+ const formData = new FormData()
+ formData.append('list', file)
+ return fetch(MUTES_IMPORT_URL, {
+ body: formData,
+ method: 'POST',
+ headers: authHeaders(credentials)
+ })
+ .then((response) => response.ok)
+}
+
const importBlocks = ({ file, credentials }) => {
const formData = new FormData()
formData.append('list', file)
@@ -1196,7 +1214,7 @@ const chatMessages = ({ id, credentials, maxId, sinceId, limit = 20 }) => {
})
}
-const sendChatMessage = ({ id, content, mediaId = null, credentials }) => {
+const sendChatMessage = ({ id, content, mediaId = null, idempotencyKey, credentials }) => {
const payload = {
'content': content
}
@@ -1205,11 +1223,18 @@ const sendChatMessage = ({ id, content, mediaId = null, credentials }) => {
payload['media_id'] = mediaId
}
+ const headers = {}
+
+ if (idempotencyKey) {
+ headers['idempotency-key'] = idempotencyKey
+ }
+
return promisedRequest({
url: PLEROMA_CHAT_MESSAGES_URL(id),
method: 'POST',
payload: payload,
- credentials
+ credentials,
+ headers
})
}
@@ -1280,6 +1305,7 @@ const apiService = {
getCaptcha,
updateProfileImages,
updateProfile,
+ importMutes,
importBlocks,
importFollows,
deleteAccount,
diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js
index 95c69482..1fc4e390 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.id]
+ }
+ }
+
+ storage.messages = storage.messages.filter(m => failedMessageIds.includes(m.id))
storage.newMessageCount = 0
- storage.lastSeenTimestamp = 0
+ storage.lastSeenMessageId = '0'
storage.minId = undefined
storage.maxId = undefined
}
@@ -37,6 +48,25 @@ const deleteMessage = (storage, messageId) => {
}
}
+const handleMessageError = (storage, fakeId, isRetry) => {
+ if (!storage) { return }
+ const fakeMessage = storage.idIndex[fakeId]
+ if (fakeMessage) {
+ fakeMessage.error = true
+ fakeMessage.pending = false
+ if (!isRetry) {
+ // Ensure the failed message doesn't stay at the bottom of the list.
+ const lastPersistedMessage = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'desc'])[0]
+ if (lastPersistedMessage) {
+ const oldId = fakeMessage.id
+ fakeMessage.id = `${lastPersistedMessage.id}-${new Date().getTime()}`
+ storage.idIndex[fakeMessage.id] = fakeMessage
+ delete storage.idIndex[oldId]
+ }
+ }
+ }
+}
+
const add = (storage, { messages: newMessages, updateMaxId = true }) => {
if (!storage) { return }
for (let i = 0; i < newMessages.length; i++) {
@@ -45,7 +75,25 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => {
// sanity check
if (message.chat_id !== storage.chatId) { return }
- if (!storage.minId || message.id < storage.minId) {
+ if (message.fakeId) {
+ const fakeMessage = storage.idIndex[message.fakeId]
+ if (fakeMessage) {
+ // In case the same id exists (chat update before POST response)
+ // make sure to remove the older duplicate message.
+ if (storage.idIndex[message.id]) {
+ delete storage.idIndex[message.id]
+ storage.messages = storage.messages.filter(msg => msg.id !== message.id)
+ }
+ Object.assign(fakeMessage, message, { error: false })
+ delete fakeMessage['fakeId']
+ storage.idIndex[fakeMessage.id] = fakeMessage
+ delete storage.idIndex[message.fakeId]
+
+ return
+ }
+ }
+
+ if (!storage.minId || (!message.pending && message.id < storage.minId)) {
storage.minId = message.id
}
@@ -55,20 +103,26 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => {
}
}
- if (!storage.idIndex[message.id]) {
- if (storage.lastSeenTimestamp < message.created_at) {
+ if (!storage.idIndex[message.id] && !isConfirmation(storage, message)) {
+ if (storage.lastSeenMessageId < message.id) {
storage.newMessageCount++
}
- storage.messages.push(message)
storage.idIndex[message.id] = message
+ storage.messages.push(storage.idIndex[message.id])
+ storage.idempotencyKeyIndex[message.idempotency_key] = true
}
}
}
+const isConfirmation = (storage, message) => {
+ if (!message.idempotency_key) return
+ return storage.idempotencyKeyIndex[message.idempotency_key]
+}
+
const resetNewMessageCount = (storage) => {
if (!storage) { return }
storage.newMessageCount = 0
- storage.lastSeenTimestamp = new Date()
+ storage.lastSeenMessageId = storage.maxId
}
// Inserts date separators and marks the head and tail if it's the chain of messages made by the same user
@@ -76,7 +130,7 @@ const getView = (storage) => {
if (!storage) { return [] }
const result = []
- const messages = _.sortBy(storage.messages, ['id', 'desc'])
+ const messages = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'asc'])
const firstMessage = messages[0]
let previousMessage = messages[messages.length - 1]
let currentMessageChainId
@@ -148,7 +202,8 @@ const ChatService = {
getView,
deleteMessage,
resetNewMessageCount,
- clear
+ clear,
+ handleMessageError
}
export default ChatService
diff --git a/src/services/chat_utils/chat_utils.js b/src/services/chat_utils/chat_utils.js
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..9d09b8d0 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -429,6 +429,9 @@ export const parseChatMessage = (message) => {
} else {
output.attachments = []
}
+ output.pending = !!message.pending
+ output.error = false
+ output.idempotency_key = message.idempotency_key
output.isNormalized = true
return output
}
diff --git a/src/services/follow_request_fetcher/follow_request_fetcher.service.js b/src/services/follow_request_fetcher/follow_request_fetcher.service.js
index 93fac9bc..74af4081 100644
--- a/src/services/follow_request_fetcher/follow_request_fetcher.service.js
+++ b/src/services/follow_request_fetcher/follow_request_fetcher.service.js
@@ -1,4 +1,5 @@
import apiService from '../api/api.service.js'
+import { promiseInterval } from '../promise_interval/promise_interval.js'
const fetchAndUpdate = ({ store, credentials }) => {
return apiService.fetchFollowRequests({ credentials })
@@ -10,9 +11,9 @@ const fetchAndUpdate = ({ store, credentials }) => {
}
const startFetching = ({ credentials, store }) => {
- fetchAndUpdate({ credentials, store })
const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store })
- return setInterval(boundFetchAndUpdate, 10000)
+ boundFetchAndUpdate()
+ return promiseInterval(boundFetchAndUpdate, 10000)
}
const followRequestFetcher = {
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index 133e913f..6ff7d9df 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -1,4 +1,5 @@
import apiService from '../api/api.service.js'
+import { promiseInterval } from '../promise_interval/promise_interval.js'
const update = ({ store, notifications, older }) => {
store.dispatch('setNotificationsError', { value: false })
@@ -42,6 +43,7 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
args['since'] = Math.max(...readNotifsIds)
fetchNotifications({ store, args, older })
}
+
return result
}
}
@@ -56,13 +58,13 @@ const fetchNotifications = ({ store, args, older }) => {
}
const startFetching = ({ credentials, store }) => {
- fetchAndUpdate({ credentials, store })
- const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store })
// Initially there's set flag to silence all desktop notifications so
// that there won't spam of them when user just opened up the FE we
// reset that flag after a while to show new notifications once again.
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
- return setInterval(boundFetchAndUpdate, 10000)
+ const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store })
+ boundFetchAndUpdate()
+ return promiseInterval(boundFetchAndUpdate, 10000)
}
const notificationsFetcher = {
diff --git a/src/services/promise_interval/promise_interval.js b/src/services/promise_interval/promise_interval.js
new file mode 100644
index 00000000..0c0a66a0
--- /dev/null
+++ b/src/services/promise_interval/promise_interval.js
@@ -0,0 +1,34 @@
+
+// promiseInterval - replacement for setInterval for promises, starts counting
+// the interval only after a promise is done instead of immediately.
+// - promiseCall is a function that returns a promise, it's called the first
+// time after the first interval.
+// - interval is the interval delay in ms.
+
+export const promiseInterval = (promiseCall, interval) => {
+ let stopped = false
+ let timeout = null
+
+ const func = () => {
+ const promise = promiseCall()
+ // something unexpected happened and promiseCall did not
+ // return a promise, abort the loop.
+ if (!(promise && promise.finally)) {
+ console.warn('promiseInterval: promise call did not return a promise, stopping interval.')
+ return
+ }
+ promise.finally(() => {
+ if (stopped) return
+ timeout = window.setTimeout(func, interval)
+ })
+ }
+
+ const stopFetcher = () => {
+ stopped = true
+ window.clearTimeout(timeout)
+ }
+
+ timeout = window.setTimeout(func, interval)
+
+ return { stop: stopFetcher }
+}
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index d0cddf84..72ea4890 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -1,6 +1,7 @@
import { camelCase } from 'lodash'
import apiService from '../api/api.service.js'
+import { promiseInterval } from '../promise_interval/promise_interval.js'
const update = ({ store, statuses, timeline, showImmediately, userId, pagination }) => {
const ccTimeline = camelCase(timeline)
@@ -71,8 +72,9 @@ const startFetching = ({ timeline = 'friends', credentials, store, userId = fals
const showImmediately = timelineData.visibleStatuses.length === 0
timelineData.userId = userId
fetchAndUpdate({ timeline, credentials, store, showImmediately, userId, tag })
- const boundFetchAndUpdate = () => fetchAndUpdate({ timeline, credentials, store, userId, tag })
- return setInterval(boundFetchAndUpdate, 10000)
+ const boundFetchAndUpdate = () =>
+ fetchAndUpdate({ timeline, credentials, store, userId, tag })
+ return promiseInterval(boundFetchAndUpdate, 10000)
}
const timelineFetcher = {
fetchAndUpdate,