aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.js49
-rw-r--r--src/App.scss838
-rw-r--r--src/App.vue46
-rw-r--r--src/boot/after_store.js47
-rw-r--r--src/boot/routes.js6
-rw-r--r--src/components/about/about.vue2
-rw-r--r--src/components/account_actions/account_actions.js2
-rw-r--r--src/components/account_actions/account_actions.vue4
-rw-r--r--src/components/async_component_error/async_component_error.vue1
-rw-r--r--src/components/attachment/attachment.scss2
-rw-r--r--src/components/auth_form/auth_form.js5
-rw-r--r--src/components/basic_user_card/basic_user_card.vue2
-rw-r--r--src/components/bookmark_timeline/bookmark_timeline.js2
-rw-r--r--src/components/chat/chat.js99
-rw-r--r--src/components/chat/chat.scss88
-rw-r--r--src/components/chat/chat.vue125
-rw-r--r--src/components/chat/chat_layout_utils.js27
-rw-r--r--src/components/chat_list/chat_list.vue2
-rw-r--r--src/components/chat_list_item/chat_list_item.scss4
-rw-r--r--src/components/chat_message/chat_message.js1
-rw-r--r--src/components/chat_new/chat_new.scss8
-rw-r--r--src/components/chat_new/chat_new.vue7
-rw-r--r--src/components/chat_title/chat_title.js9
-rw-r--r--src/components/chat_title/chat_title.vue22
-rw-r--r--src/components/checkbox/checkbox.vue13
-rw-r--r--src/components/color_input/color_input.vue23
-rw-r--r--src/components/conversation/conversation.vue58
-rw-r--r--src/components/desktop_nav/desktop_nav.scss16
-rw-r--r--src/components/desktop_nav/desktop_nav.vue2
-rw-r--r--src/components/dialog_modal/dialog_modal.vue9
-rw-r--r--src/components/emoji_input/emoji_input.js31
-rw-r--r--src/components/emoji_input/emoji_input.vue2
-rw-r--r--src/components/emoji_picker/emoji_picker.js14
-rw-r--r--src/components/emoji_picker/emoji_picker.scss8
-rw-r--r--src/components/emoji_picker/emoji_picker.vue1
-rw-r--r--src/components/exporter/exporter.js14
-rw-r--r--src/components/exporter/exporter.vue4
-rw-r--r--src/components/font_control/font_control.js11
-rw-r--r--src/components/font_control/font_control.vue3
-rw-r--r--src/components/gallery/gallery.js4
-rw-r--r--src/components/gallery/gallery.vue1
-rw-r--r--src/components/global_notice_list/global_notice_list.vue11
-rw-r--r--src/components/image_cropper/image_cropper.js2
-rw-r--r--src/components/importer/importer.js21
-rw-r--r--src/components/importer/importer.vue28
-rw-r--r--src/components/interactions/interactions.js4
-rw-r--r--src/components/interface_language_switcher/interface_language_switcher.vue39
-rw-r--r--src/components/link-preview/link-preview.vue2
-rw-r--r--src/components/login_form/login_form.vue14
-rw-r--r--src/components/media_modal/media_modal.js2
-rw-r--r--src/components/media_modal/media_modal.vue4
-rw-r--r--src/components/media_upload/media_upload.vue8
-rw-r--r--src/components/mention_link/mention_link.vue6
-rw-r--r--src/components/mentions_line/mentions_line.vue2
-rw-r--r--src/components/mfa_form/recovery_form.vue12
-rw-r--r--src/components/mfa_form/totp_form.vue14
-rw-r--r--src/components/mobile_nav/mobile_nav.js3
-rw-r--r--src/components/mobile_nav/mobile_nav.vue20
-rw-r--r--src/components/mobile_post_status_button/mobile_post_status_button.js4
-rw-r--r--src/components/mobile_post_status_button/mobile_post_status_button.vue57
-rw-r--r--src/components/modal/modal.vue2
-rw-r--r--src/components/moderation_tools/moderation_tools.vue4
-rw-r--r--src/components/notification/notification.vue51
-rw-r--r--src/components/notifications/notification_filters.vue22
-rw-r--r--src/components/notifications/notifications.js18
-rw-r--r--src/components/notifications/notifications.scss23
-rw-r--r--src/components/notifications/notifications.vue116
-rw-r--r--src/components/opacity_input/opacity_input.vue13
-rw-r--r--src/components/password_reset/password_reset.vue10
-rw-r--r--src/components/poll/poll.js2
-rw-r--r--src/components/poll/poll.vue19
-rw-r--r--src/components/poll/poll_form.vue1
-rw-r--r--src/components/popover/popover.js2
-rw-r--r--src/components/popover/popover.vue67
-rw-r--r--src/components/post_status_form/post_status_form.js8
-rw-r--r--src/components/post_status_form/post_status_form.vue88
-rw-r--r--src/components/public_and_external_timeline/public_and_external_timeline.js2
-rw-r--r--src/components/public_timeline/public_timeline.js2
-rw-r--r--src/components/range_input/range_input.vue15
-rw-r--r--src/components/react_button/react_button.js3
-rw-r--r--src/components/react_button/react_button.vue3
-rw-r--r--src/components/registration/registration.js26
-rw-r--r--src/components/registration/registration.vue63
-rw-r--r--src/components/remote_follow/remote_follow.vue2
-rw-r--r--src/components/rich_content/rich_content.jsx22
-rw-r--r--src/components/scope_selector/scope_selector.vue3
-rw-r--r--src/components/search/search.js4
-rw-r--r--src/components/select/select.js7
-rw-r--r--src/components/select/select.vue15
-rw-r--r--src/components/selectable_list/selectable_list.vue8
-rw-r--r--src/components/settings_modal/helpers/boolean_setting.vue5
-rw-r--r--src/components/settings_modal/helpers/choice_setting.vue5
-rw-r--r--src/components/settings_modal/helpers/integer_setting.js2
-rw-r--r--src/components/settings_modal/helpers/integer_setting.vue1
-rw-r--r--src/components/settings_modal/settings_modal.js4
-rw-r--r--src/components/settings_modal/settings_modal.scss19
-rw-r--r--src/components/settings_modal/settings_modal.vue35
-rw-r--r--src/components/settings_modal/settings_modal_content.js9
-rw-r--r--src/components/settings_modal/settings_modal_content.vue1
-rw-r--r--src/components/settings_modal/tabs/data_import_export_tab.js29
-rw-r--r--src/components/settings_modal/tabs/data_import_export_tab.vue61
-rw-r--r--src/components/settings_modal/tabs/filtering_tab.vue14
-rw-r--r--src/components/settings_modal/tabs/general_tab.js11
-rw-r--r--src/components/settings_modal/tabs/general_tab.vue26
-rw-r--r--src/components/settings_modal/tabs/mutes_and_blocks_tab.js2
-rw-r--r--src/components/settings_modal/tabs/mutes_and_blocks_tab.scss2
-rw-r--r--src/components/settings_modal/tabs/notifications_tab.vue5
-rw-r--r--src/components/settings_modal/tabs/profile_tab.js46
-rw-r--r--src/components/settings_modal/tabs/profile_tab.scss8
-rw-r--r--src/components/settings_modal/tabs/profile_tab.vue49
-rw-r--r--src/components/settings_modal/tabs/security_tab/security_tab.js55
-rw-r--r--src/components/settings_modal/tabs/security_tab/security_tab.vue108
-rw-r--r--src/components/settings_modal/tabs/theme_tab/preview.vue11
-rw-r--r--src/components/settings_modal/tabs/theme_tab/theme_tab.js19
-rw-r--r--src/components/settings_modal/tabs/theme_tab/theme_tab.scss49
-rw-r--r--src/components/settings_modal/tabs/theme_tab/theme_tab.vue58
-rw-r--r--src/components/settings_modal/tabs/version_tab.vue2
-rw-r--r--src/components/shadow_control/shadow_control.js11
-rw-r--r--src/components/shadow_control/shadow_control.vue7
-rw-r--r--src/components/shout_panel/shout_panel.vue21
-rw-r--r--src/components/side_drawer/side_drawer.js2
-rw-r--r--src/components/status/status.js18
-rw-r--r--src/components/status/status.scss21
-rw-r--r--src/components/status/status.vue18
-rw-r--r--src/components/status_body/status_body.scss2
-rw-r--r--src/components/status_body/status_body.vue10
-rw-r--r--src/components/status_content/status_content.js6
-rw-r--r--src/components/status_popover/status_popover.js5
-rw-r--r--src/components/sticker_picker/sticker_picker.js2
-rw-r--r--src/components/still-image/still-image.vue8
-rw-r--r--src/components/tab_switcher/tab_switcher.jsx (renamed from src/components/tab_switcher/tab_switcher.js)75
-rw-r--r--src/components/tab_switcher/tab_switcher.scss10
-rw-r--r--src/components/tag_timeline/tag_timeline.js2
-rw-r--r--src/components/thread_tree/thread_tree.vue50
-rw-r--r--src/components/timeago/timeago.vue4
-rw-r--r--src/components/timeline/timeline.js21
-rw-r--r--src/components/timeline/timeline.scss28
-rw-r--r--src/components/timeline/timeline.vue110
-rw-r--r--src/components/timeline/timeline_quick_settings.vue24
-rw-r--r--src/components/timeline_menu/timeline_menu.vue4
-rw-r--r--src/components/user_avatar/user_avatar.vue96
-rw-r--r--src/components/user_card/user_card.scss323
-rw-r--r--src/components/user_card/user_card.vue321
-rw-r--r--src/components/user_list_popover/user_list_popover.js8
-rw-r--r--src/components/user_list_popover/user_list_popover.vue2
-rw-r--r--src/components/user_panel/user_panel.vue3
-rw-r--r--src/components/user_profile/user_profile.js10
-rw-r--r--src/components/user_profile/user_profile.vue27
-rw-r--r--src/components/user_reporting_modal/user_reporting_modal.vue17
-rw-r--r--src/directives/body_scroll_lock.js6
-rw-r--r--src/hocs/with_load_more/with_load_more.jsx (renamed from src/hocs/with_load_more/with_load_more.js)25
-rw-r--r--src/hocs/with_load_more/with_load_more.scss2
-rw-r--r--src/hocs/with_subscription/with_subscription.jsx (renamed from src/hocs/with_subscription/with_subscription.js)19
-rw-r--r--src/hocs/with_subscription/with_subscription.scss2
-rw-r--r--src/i18n/ca.json1
-rw-r--r--src/i18n/de.json1
-rw-r--r--src/i18n/en.json107
-rw-r--r--src/i18n/eo.json1
-rw-r--r--src/i18n/es.json3
-rw-r--r--src/i18n/eu.json3
-rw-r--r--src/i18n/fi.json3
-rw-r--r--src/i18n/fr.json3
-rw-r--r--src/i18n/he.json3
-rw-r--r--src/i18n/id.json3
-rw-r--r--src/i18n/it.json1
-rw-r--r--src/i18n/ja_easy.json3
-rw-r--r--src/i18n/ja_pedantic.json3
-rw-r--r--src/i18n/messages.js5
-rw-r--r--src/i18n/nb.json3
-rw-r--r--src/i18n/nl.json1
-rw-r--r--src/i18n/oc.json3
-rw-r--r--src/i18n/pl.json3
-rw-r--r--src/i18n/pt.json1
-rw-r--r--src/i18n/ru.json3
-rw-r--r--src/i18n/service_worker_messages.js1
-rw-r--r--src/i18n/sk.json512
-rw-r--r--src/i18n/te.json2
-rw-r--r--src/i18n/uk.json1
-rw-r--r--src/i18n/vi.json3
-rw-r--r--src/i18n/zh.json3
-rw-r--r--src/i18n/zh_Hant.json1
-rw-r--r--src/lib/persisted_state.js4
-rw-r--r--src/main.js33
-rw-r--r--src/modules/chats.js22
-rw-r--r--src/modules/config.js21
-rw-r--r--src/modules/instance.js3
-rw-r--r--src/modules/interface.js50
-rw-r--r--src/modules/oauth.js4
-rw-r--r--src/modules/polls.js13
-rw-r--r--src/modules/serverSideConfig.js5
-rw-r--r--src/modules/shout.js15
-rw-r--r--src/modules/statuses.js15
-rw-r--r--src/modules/users.js29
-rw-r--r--src/panel.scss198
-rw-r--r--src/services/api/api.service.js82
-rw-r--r--src/services/date_utils/date_utils.js16
-rw-r--r--src/services/export_import/export_import.js4
-rw-r--r--src/services/locale/locale.service.js27
-rw-r--r--src/services/notification_utils/notification_utils.js8
-rw-r--r--src/services/offset_finder/offset_finder.service.js2
-rw-r--r--src/services/resettable_async_component.js22
-rw-r--r--src/services/style_setter/style_setter.js8
-rw-r--r--src/sw.js6
203 files changed, 3465 insertions, 2241 deletions
diff --git a/src/App.js b/src/App.js
index f5e0b9e9..f01f8788 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,6 +1,5 @@
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 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'
@@ -16,13 +15,14 @@ import PostStatusModal from './components/post_status_modal/post_status_modal.vu
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
import { mapGetters } from 'vuex'
+import { defineAsyncComponent } from 'vue'
export default {
name: 'app',
components: {
UserPanel,
NavPanel,
- Notifications,
+ Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
InstanceSpecificPanel,
FeaturesPanel,
WhoToFollowPanel,
@@ -46,10 +46,20 @@ export default {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
window.addEventListener('resize', this.updateMobileState)
},
- destroyed () {
+ unmounted () {
window.removeEventListener('resize', this.updateMobileState)
},
computed: {
+ classes () {
+ return [
+ {
+ '-reverse': this.reverseLayout,
+ '-no-sticky-headers': this.noSticky,
+ '-has-new-post-button': this.newPostButtonShown
+ },
+ '-' + this.layoutType
+ ]
+ },
currentUser () { return this.$store.state.users.currentUser },
userBackground () { return this.currentUser.background_image },
instanceBackground () {
@@ -65,38 +75,45 @@ export default {
}
}
},
- shout () { return this.$store.state.shout.channel.state === 'joined' },
+ shout () { return this.$store.state.shout.joined },
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
showInstanceSpecificPanel () {
return this.$store.state.instance.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent
},
+ isChats () {
+ return this.$route.name === 'chat' || this.$route.name === 'chats'
+ },
+ newPostButtonShown () {
+ if (this.isChats) return false
+ return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
+ },
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
shoutboxPosition () {
- return this.$store.getters.mergedConfig.showNewPostButton || false
+ return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
},
hideShoutbox () {
return this.$store.getters.mergedConfig.hideShoutbox
},
- isMobileLayout () { return this.$store.state.interface.mobileLayout },
+ layoutType () { return this.$store.state.interface.layoutType },
privateMode () { return this.$store.state.instance.private },
- sidebarAlign () {
- return {
- 'order': this.$store.getters.mergedConfig.sidebarRight ? 99 : 0
+ reverseLayout () {
+ const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig
+ if (this.layoutType !== 'wide') {
+ return reverseSetting
+ } else {
+ return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting
}
},
+ noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
+ showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
...mapGetters(['mergedConfig'])
},
methods: {
updateMobileState () {
- const mobileLayout = windowWidth() <= 800
- const layoutHeight = windowHeight()
- const changed = mobileLayout !== this.isMobileLayout
- if (changed) {
- this.$store.dispatch('setMobileLayout', mobileLayout)
- }
- this.$store.dispatch('setLayoutHeight', layoutHeight)
+ this.$store.dispatch('setLayoutWidth', windowWidth())
+ this.$store.dispatch('setLayoutHeight', windowHeight())
}
}
}
diff --git a/src/App.scss b/src/App.scss
index bc027f4f..7e6d0dfc 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -1,77 +1,332 @@
+// stylelint-disable rscss/class-format
@import './_variables.scss';
-#app {
- min-height: 100vh;
- max-width: 100%;
- overflow: hidden;
-}
-
-.app-bg-wrapper {
- position: fixed;
- z-index: -1;
- height: 100%;
- left: 0;
- right: -20px;
- background-size: cover;
- background-repeat: no-repeat;
- background-color: var(--wallpaper);
- background-image: var(--body-background-image);
- background-position: 50% 50px;
-}
-
-i[class^='icon-'] {
- user-select: none;
-}
-
-h4 {
- margin: 0;
-}
-
-#content {
- box-sizing: border-box;
- padding-top: 60px;
- margin: auto;
- min-height: 100vh;
- max-width: 980px;
- align-content: flex-start;
-}
-
-.underlay {
- background-color: rgba(0,0,0,0.15);
- background-color: var(--underlay, rgba(0,0,0,0.15));
-}
-
-.text-center {
- text-align: center;
+:root {
+ --navbar-height: 3.5rem;
+ --post-line-height: 1.4;
}
html {
font-size: 14px;
+ // overflow-x: clip causes my browser's tab to crash with SIGILL lul
}
body {
- overscroll-behavior-y: none;
font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif);
margin: 0;
color: $fallback--text;
color: var(--text, $fallback--text);
- max-width: 100vw;
- overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
+ overscroll-behavior-y: none;
+ overflow-x: clip;
+ overflow-y: scroll;
&.hidden {
display: none;
}
}
+// ## Custom scrollbars
+// Only show custom scrollbars on devices which
+// have a cursor/pointer to operate them
+@media (any-pointer: fine) {
+ * {
+ scrollbar-color: var(--btn) transparent;
+
+ &::-webkit-scrollbar {
+ background: transparent;
+ }
+
+ &::-webkit-scrollbar-button,
+ &::-webkit-scrollbar-thumb {
+ background-color: var(--btn);
+ box-shadow: var(--buttonShadow);
+ border-radius: var(--btnRadius);
+ }
+
+ // horizontal/vertical/increment/decrement are webkit-specific stuff
+ // that indicates whether we're affecting vertical scrollbar, increase button etc
+ // stylelint-disable selector-pseudo-class-no-unknown
+ &::-webkit-scrollbar-button {
+ --___bgPadding: 2px;
+
+ color: var(--btnText);
+ background-repeat: no-repeat, no-repeat;
+
+ &:horizontal {
+ background-size: 50% calc(50% - var(--___bgPadding)), 50% calc(50% - var(--___bgPadding));
+
+ &:increment {
+ background-image:
+ linear-gradient(45deg, var(--btnText) 50%, transparent 51%),
+ linear-gradient(-45deg, transparent 50%, var(--btnText) 51%);
+ background-position: top var(--___bgPadding) left 50%, right 50% bottom var(--___bgPadding);
+ }
+
+ &:decrement {
+ background-image:
+ linear-gradient(45deg, transparent 50%, var(--btnText) 51%),
+ linear-gradient(-45deg, var(--btnText) 50%, transparent 51%);
+ background-position: bottom var(--___bgPadding) right 50%, left 50% top var(--___bgPadding);
+ }
+ }
+
+ &:vertical {
+ background-size: calc(50% - var(--___bgPadding)) 50%, calc(50% - var(--___bgPadding)) 50%;
+
+ &:increment {
+ background-image:
+ linear-gradient(-45deg, transparent 50%, var(--btnText) 51%),
+ linear-gradient(45deg, transparent 50%, var(--btnText) 51%);
+ background-position: right var(--___bgPadding) top 50%, left var(--___bgPadding) top 50%;
+ }
+
+ &:decrement {
+ background-image:
+ linear-gradient(-45deg, var(--btnText) 50%, transparent 51%),
+ linear-gradient(45deg, var(--btnText) 50%, transparent 51%);
+ background-position: left var(--___bgPadding) top 50%, right var(--___bgPadding) top 50%;
+ }
+ }
+ }
+ // stylelint-enable selector-pseudo-class-no-unknown
+ }
+ // Body should have background to scrollbar otherwise it will use white (body color?)
+ html {
+ scrollbar-color: var(--selectedMenu) var(--wallpaper);
+ background: var(--wallpaper);
+ }
+}
+
a {
text-decoration: none;
color: $fallback--link;
color: var(--link, $fallback--link);
}
+h4 {
+ margin: 0;
+}
+
+i[class*=icon-],
+.svg-inline--fa {
+ color: $fallback--icon;
+ color: var(--icon, $fallback--icon);
+}
+
+nav {
+ z-index: 1000;
+ color: var(--topBarText);
+ background-color: $fallback--fg;
+ background-color: var(--topBar, $fallback--fg);
+ color: $fallback--faint;
+ color: var(--faint, $fallback--faint);
+ box-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
+ box-shadow: var(--topBarShadow);
+ box-sizing: border-box;
+ height: var(--navbar-height);
+ position: fixed;
+}
+
+#sidebar {
+ grid-area: sidebar;
+}
+
+.column.-scrollable {
+ top: var(--navbar-height);
+ position: sticky;
+}
+
+#main-scroller {
+ grid-area: content;
+ position: relative;
+}
+
+#notifs-column {
+ grid-area: notifs;
+}
+
+.app-bg-wrapper {
+ position: fixed;
+ height: 100%;
+ top: var(--navbar-height);
+ z-index: -1000;
+ left: 0;
+ right: -20px;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-color: var(--wallpaper);
+ background-image: var(--body-background-image);
+ background-position: 50%;
+}
+
+.underlay {
+ grid-column-start: 1;
+ grid-column-end: span 3;
+ grid-row-start: 1;
+ grid-row-end: 1;
+ pointer-events: none;
+ background-color: rgba(0, 0, 0, 0.15);
+ background-color: var(--underlay, rgba(0, 0, 0, 0.15));
+ z-index: -1000;
+}
+
+.app-layout {
+ --miniColumn: 25rem;
+ --maxiColumn: minmax(var(--miniColumn), 45rem);
+ --columnGap: 1em;
+ --status-margin: 0.75em;
+
+ position: relative;
+ display: grid;
+ grid-template-columns: var(--miniColumn) var(--maxiColumn);
+ grid-template-areas: "sidebar content";
+ grid-template-rows: 1fr;
+ box-sizing: border-box;
+ margin: 0 auto;
+ align-content: flex-start;
+ flex-wrap: wrap;
+ justify-content: center;
+ min-height: 100vh;
+ overflow-x: clip;
+
+ .column {
+ --___columnMargin: var(--columnGap);
+
+ display: grid;
+ grid-template-columns: 100%;
+ box-sizing: border-box;
+ grid-row-start: 1;
+ grid-row-end: 1;
+ margin: 0 calc(var(--___columnMargin) / 2);
+ padding: calc(var(--___columnMargin)) 0;
+ row-gap: var(--___columnMargin);
+ align-content: start;
+
+ &:not(.-scrollable) {
+ margin-top: var(--navbar-height);
+ }
+
+ &:hover {
+ z-index: 2;
+ }
+
+ &.-full-height {
+ margin-bottom: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+
+ &.-scrollable {
+ --___paddingIncrease: calc(var(--columnGap) / 2);
+
+ position: sticky;
+ top: var(--navbar-height);
+ max-height: calc(100vh - var(--navbar-height));
+ overflow-y: auto;
+ overflow-x: hidden;
+ margin-left: calc(var(--___paddingIncrease) * -1);
+ padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
+
+ // On browsers that don't support hiding scrollbars we enforce "show scrolbars" mode
+ // might implement old style of hiding scrollbars later if there's demand
+ @supports (scrollbar-width: none) or (-webkit-text-fill-color: initial) {
+ &:not(.-show-scrollbar) {
+ scrollbar-width: none;
+ margin-right: calc(var(--___paddingIncrease) * -1);
+ padding-right: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
+
+ &::-webkit-scrollbar {
+ display: block;
+ width: 0;
+ }
+ }
+ }
+
+ .panel-heading.-sticky {
+ top: calc(var(--columnGap) / -1);
+ }
+ }
+ }
+
+ &.-has-new-post-button {
+ .column {
+ padding-bottom: 10rem;
+ }
+ }
+
+ &.-no-sticky-headers {
+ .column {
+ .panel-heading.-sticky {
+ position: relative;
+ top: 0;
+ }
+ }
+ }
+
+ .column-inner {
+ display: grid;
+ grid-template-columns: 100%;
+ box-sizing: border-box;
+ row-gap: 1em;
+ align-content: start;
+ }
+
+ &.-reverse:not(.-wide):not(.-mobile) {
+ grid-template-columns: var(--maxiColumn) var(--miniColumn);
+ grid-template-areas: "content sidebar";
+ }
+
+ &.-wide {
+ grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn);
+ grid-template-areas: "sidebar content notifs";
+
+ &.-reverse {
+ grid-template-areas: "notifs content sidebar";
+ }
+ }
+
+ &.-mobile {
+ grid-template-columns: 100vw;
+ grid-template-areas: "content";
+ padding: 0;
+
+ .column {
+ margin-left: 0;
+ margin-right: 0;
+ padding-top: 0;
+ margin-top: var(--navbar-height);
+ margin-bottom: 0;
+ }
+
+ .panel-heading,
+ .panel-heading::after,
+ .panel-heading::before,
+ .panel,
+ .panel::after {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+
+ #sidebar,
+ #notifs-column {
+ display: none;
+ }
+ }
+
+ &.-normal {
+ #notifs-column {
+ display: none;
+ }
+ }
+}
+
+.text-center {
+ text-align: center;
+}
+
.button-default {
user-select: none;
color: $fallback--text;
@@ -84,7 +339,7 @@ a {
cursor: pointer;
box-shadow: $fallback--buttonShadow;
box-shadow: var(--buttonShadow);
- font-size: 14px;
+ font-size: 1em;
font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif);
@@ -103,12 +358,12 @@ a {
}
&:hover {
- box-shadow: 0px 0px 4px rgba(255, 255, 255, 0.3);
+ box-shadow: 0 0 4px rgba(255, 255, 255, 0.3);
box-shadow: var(--buttonHoverShadow);
}
&:active {
- 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: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow);
color: $fallback--text;
color: var(--btnPressedText, $fallback--text);
@@ -141,7 +396,7 @@ a {
color: var(--btnToggledText, $fallback--text);
background-color: $fallback--fg;
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: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow);
svg,
@@ -191,8 +446,9 @@ a {
}
}
-input, textarea, .input {
-
+input,
+textarea,
+.input {
&.unstyled {
border-radius: 0;
background: none;
@@ -200,10 +456,12 @@ input, textarea, .input {
height: unset;
}
+ --_padding: 0.5em;
+
border: none;
border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius);
- box-shadow: 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 0px 2px 0px rgba(0, 0, 0, 1) inset;
+ box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset;
box-shadow: var(--inputShadow);
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
@@ -211,17 +469,18 @@ input, textarea, .input {
color: var(--inputText, $fallback--lightText);
font-family: sans-serif;
font-family: var(--inputFont, sans-serif);
- font-size: 14px;
+ font-size: 1em;
margin: 0;
box-sizing: border-box;
display: inline-block;
position: relative;
- height: 28px;
- line-height: 16px;
+ line-height: 2;
hyphens: none;
- padding: 8px .5em;
+ padding: 0 var(--_padding);
- &:disabled, &[disabled=disabled], &.disabled {
+ &:disabled,
+ &[disabled=disabled],
+ &.disabled {
cursor: not-allowed;
opacity: 0.5;
}
@@ -236,18 +495,21 @@ input, textarea, .input {
&[type=radio] {
display: none;
+
&:checked + label::before {
- box-shadow: 0px 0px 2px black inset, 0px 0px 0px 4px $fallback--fg inset;
- box-shadow: var(--inputShadow), 0px 0px 0px 4px var(--fg, $fallback--fg) inset;
+ box-shadow: 0 0 2px black inset, 0 0 0 4px $fallback--fg inset;
+ box-shadow: var(--inputShadow), 0 0 0 4px var(--fg, $fallback--fg) inset;
background-color: var(--accent, $fallback--link);
}
+
&:disabled {
&,
& + label,
& + label::before {
- opacity: .5;
+ opacity: 0.5;
}
}
+
+ label::before {
flex-shrink: 0;
display: inline-block;
@@ -256,35 +518,37 @@ input, textarea, .input {
width: 1.1em;
height: 1.1em;
border-radius: 100%; // Radio buttons should always be circle
- box-shadow: 0px 0px 2px black inset;
+ box-shadow: 0 0 2px black inset;
box-shadow: var(--inputShadow);
- margin-right: .5em;
+ margin-right: 0.5em;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
vertical-align: top;
text-align: center;
- line-height: 1.1em;
+ line-height: 1.1;
font-size: 1.1em;
box-sizing: border-box;
color: transparent;
overflow: hidden;
- box-sizing: border-box;
}
}
&[type=checkbox] {
display: none;
+
&:checked + label::before {
color: $fallback--text;
color: var(--inputText, $fallback--text);
}
+
&:disabled {
&,
& + label,
& + label::before {
- opacity: .5;
+ opacity: 0.5;
}
}
+
+ label::before {
flex-shrink: 0;
display: inline-block;
@@ -294,19 +558,18 @@ input, textarea, .input {
height: 1.1em;
border-radius: $fallback--checkboxRadius;
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
- box-shadow: 0px 0px 2px black inset;
+ box-shadow: 0 0 2px black inset;
box-shadow: var(--inputShadow);
- margin-right: .5em;
+ margin-right: 0.5em;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
vertical-align: top;
text-align: center;
- line-height: 1.1em;
+ line-height: 1.1;
font-size: 1.1em;
box-sizing: border-box;
color: transparent;
overflow: hidden;
- box-sizing: border-box;
}
}
@@ -315,6 +578,12 @@ input, textarea, .input {
}
}
+// Textareas should have stock line-height + vertical padding instead of huge line-height
+textarea {
+ padding: var(--_padding);
+ line-height: var(--post-line-height);
+}
+
option {
color: $fallback--text;
color: var(--text, $fallback--text);
@@ -324,6 +593,7 @@ option {
.hide-number-spinner {
-moz-appearance: textfield;
+
&[type=number]::-webkit-inner-spin-button,
&[type=number]::-webkit-outer-spin-button {
opacity: 0;
@@ -331,11 +601,6 @@ option {
}
}
-i[class*=icon-], .svg-inline--fa {
- color: $fallback--icon;
- color: var(--icon, $fallback--icon);
-}
-
.btn-block {
display: block;
width: 100%;
@@ -362,273 +627,16 @@ i[class*=icon-], .svg-inline--fa {
}
}
-.container {
- display: flex;
- flex-wrap: wrap;
- margin: 0;
- padding: 0 10px 0 10px;
-}
-
-.auto-size {
- flex: 1
-}
-
-main-router {
- flex: 1;
-}
-
-.status.compact {
- color: rgba(0, 0, 0, 0.42);
- font-weight: 300;
-
- p {
- margin: 0;
- font-size: 0.8em
- }
-}
-
-/* Panel */
-
-.panel {
- display: flex;
- position: relative;
-
- flex-direction: column;
- margin: 0.5em;
-
- background-color: $fallback--bg;
- background-color: var(--bg, $fallback--bg);
-
- &::after, & {
- border-radius: $fallback--panelRadius;
- border-radius: var(--panelRadius, $fallback--panelRadius);
- }
-
- &::after {
- content: '';
- position: absolute;
-
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
-
- pointer-events: none;
-
- box-shadow: 1px 1px 4px rgba(0,0,0,.6);
- box-shadow: var(--panelShadow);
- }
-}
-
-.panel-body:empty::before {
- content: "¯\\_(ツ)_/¯"; // Could use words but it'd require translations
- display: block;
- margin: 1em;
- text-align: center;
-}
-
-.panel-heading {
- display: flex;
- flex: none;
- border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
- border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
- background-size: cover;
- padding: .6em .6em;
- text-align: left;
- line-height: 28px;
- color: var(--panelText);
- background-color: $fallback--fg;
- background-color: var(--panel, $fallback--fg);
- align-items: baseline;
- box-shadow: var(--panelHeaderShadow);
-
- .title {
- flex: 1 0 auto;
- font-size: 1.3em;
- }
-
- .faint {
- background-color: transparent;
- color: $fallback--faint;
- color: var(--panelFaint, $fallback--faint);
- }
-
- .faint-link {
- color: $fallback--faint;
- color: var(--faintLink, $fallback--faint);
- }
-
- .alert {
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow-x: hidden;
- }
-
- .button-default,
- .alert {
- // height: 100%;
- line-height: 21px;
- min-height: 0;
- box-sizing: border-box;
- margin: 0;
- margin-left: .5em;
- min-width: 1px;
- align-self: stretch;
- }
-
- .button-default {
- flex-shrink: 0;
-
- &,
- i[class*=icon-] {
- color: $fallback--text;
- color: var(--btnPanelText, $fallback--text);
- }
-
- &:active {
- background-color: $fallback--fg;
- background-color: var(--btnPressedPanel, $fallback--fg);
- color: $fallback--text;
- color: var(--btnPressedPanelText, $fallback--text);
- }
-
- &:disabled {
- color: $fallback--text;
- color: var(--btnDisabledPanelText, $fallback--text);
- }
-
- &.toggled {
- color: $fallback--text;
- color: var(--btnToggledPanelText, $fallback--text);
- }
- }
-
- a,
- .-link {
- color: $fallback--link;
- color: var(--panelLink, $fallback--link)
- }
-}
-
-.panel-heading.stub {
- border-radius: $fallback--panelRadius;
- border-radius: var(--panelRadius, $fallback--panelRadius);
-}
-
-/* TODO Should remove timeline-footer from here when we refactor panels into
- * separate component and utilize slots
- */
-.panel-footer, .timeline-footer {
- display: flex;
- border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
- border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
- flex: none;
- padding: 0.6em 0.6em;
- text-align: left;
- line-height: 28px;
- align-items: baseline;
- border-width: 1px 0 0 0;
- border-style: solid;
- border-color: var(--border, $fallback--border);
-
- .faint {
- color: $fallback--faint;
- color: var(--panelFaint, $fallback--faint);
- }
-
- a,
- .-link {
- color: $fallback--link;
- color: var(--panelLink, $fallback--link);
- }
-}
-
-.panel-body > p {
- line-height: 18px;
- padding: 1em;
- margin: 0;
-}
-
-.container > * {
- min-width: 0px;
-}
+@import './panel.scss';
.fa {
color: grey;
}
-nav {
- z-index: 1000;
- color: var(--topBarText);
- background-color: $fallback--fg;
- background-color: var(--topBar, $fallback--fg);
- color: $fallback--faint;
- color: var(--faint, $fallback--faint);
- box-shadow: 0px 0px 4px rgba(0,0,0,.6);
- box-shadow: var(--topBarShadow);
- box-sizing: border-box;
-}
-
-.fade-enter-active, .fade-leave-active {
- transition: opacity .2s
-}
-.fade-enter, .fade-leave-active {
- opacity: 0
-}
-
-.main {
- flex-basis: 50%;
- flex-grow: 1;
- flex-shrink: 1;
-}
-
-.sidebar-bounds {
- flex: 0;
- flex-basis: 35%;
-}
-
-.sidebar-flexer {
- flex: 1;
- flex-basis: 345px;
- width: 365px;
-}
-
.mobile-shown {
display: none;
}
-@media all and (min-width: 800px) {
- body {
- overflow-y: scroll;
- }
-
- .sidebar-bounds {
- overflow: hidden;
- max-height: 100vh;
- width: 345px;
- position: fixed;
- margin-top: -10px;
-
- .sidebar-scroller {
- height: 96vh;
- width: 365px;
- padding-top: 10px;
- padding-right: 50px;
- overflow-x: hidden;
- overflow-y: scroll;
- }
-
- .sidebar {
- width: 345px;
- }
- }
- .sidebar-flexer {
- max-height: 96vh;
- flex-shrink: 0;
- flex-grow: 0;
- }
-}
-
.badge {
box-sizing: border-box;
display: inline-block;
@@ -656,12 +664,10 @@ nav {
}
.alert {
- margin: 0.35em;
- padding: 0.25em;
+ margin: 0 0.35em;
+ padding: 0 0.25em;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
- min-height: 28px;
- line-height: 28px;
&.error {
background-color: $fallback--alertError;
@@ -712,7 +718,7 @@ nav {
}
.visibility-notice {
- padding: .5em;
+ padding: 0.5em;
border: 1px solid $fallback--faint;
border: 1px solid var(--faint, $fallback--faint);
border-radius: $fallback--inputRadius;
@@ -727,7 +733,7 @@ nav {
position: absolute;
top: 0;
right: 0;
- padding: .5em;
+ padding: 0.5em;
color: inherit;
}
}
@@ -744,72 +750,6 @@ nav {
}
}
-@keyframes shakeError {
- 0% {
- transform: translateX(0);
- }
- 15% {
- transform: translateX(0.375rem);
- }
- 30% {
- transform: translateX(-0.375rem);
- }
- 45% {
- transform: translateX(0.375rem);
- }
- 60% {
- transform: translateX(-0.375rem);
- }
- 75% {
- transform: translateX(0.375rem);
- }
- 90% {
- transform: translateX(-0.375rem);
- }
- 100% {
- transform: translateX(0);
- }
-}
-
-@media all and (max-width: 800px) {
- .mobile-hidden {
- display: none;
- }
-
- .panel-switcher {
- display: flex;
- }
-
- .container {
- padding: 0;
- }
-
- .panel {
- margin: 0.5em 0 0.5em 0;
- }
-
- .menu-button {
- display: block;
- margin-right: 0.8em;
- }
-
- .main {
- margin-bottom: 7em;
- }
-}
-
-.setting-list,
-.option-list{
- list-style-type: none;
- padding-left: 2em;
- li {
- margin-bottom: 0.5em;
- }
- .suboptions {
- margin-top: 0.3em
- }
-}
-
.login-hint {
text-align: center;
@@ -819,18 +759,26 @@ nav {
a {
display: inline-block;
- padding: 1em 0px;
+ padding: 1em 0;
width: 100%;
}
}
.btn.button-default {
- min-height: 28px;
+ min-height: 2em;
}
-.animate-spin {
- animation: spin 2s infinite linear;
- display: inline-block;
+.new-status-notification {
+ position: relative;
+ font-size: 1.1em;
+ z-index: 1;
+ flex: 1;
+}
+
+@media all and (max-width: 800px) {
+ .mobile-hidden {
+ display: none;
+ }
}
@keyframes spin {
@@ -843,49 +791,47 @@ nav {
}
}
-.new-status-notification {
- position: relative;
- font-size: 1.1em;
- z-index: 1;
- flex: 1;
-}
+@keyframes shakeError {
+ 0% {
+ transform: translateX(0);
+ }
-.chat-layout {
- // Needed for smoother chat navigation in the desktop Safari (otherwise the chat layout "jumps" as the chat opens).
- overflow: hidden;
- height: 100%;
+ 15% {
+ transform: translateX(0.375rem);
+ }
- // Get rid of scrollbar on body as scrolling happens on different element
- body {
- overflow: hidden;
+ 30% {
+ transform: translateX(-0.375rem);
}
- // Ensures the fixed position of the mobile browser bars on scroll up / down events.
- // Prevents the mobile browser bars from overlapping or hiding the message posting form.
- @media all and (max-width: 800px) {
- body {
- height: 100%;
- }
+ 45% {
+ transform: translateX(0.375rem);
+ }
- #app {
- height: 100%;
- overflow: hidden;
- min-height: auto;
- }
+ 60% {
+ transform: translateX(-0.375rem);
+ }
- #app_bg_wrapper {
- overflow: hidden;
- }
+ 75% {
+ transform: translateX(0.375rem);
+ }
- .main {
- overflow: hidden;
- height: 100%;
- }
+ 90% {
+ transform: translateX(-0.375rem);
+ }
- #content {
- padding-top: 0;
- height: 100%;
- overflow: visible;
- }
+ 100% {
+ transform: translateX(0);
}
}
+
+// Vue transitions
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.2s;
+}
+
+.fade-enter-from,
+.fade-leave-active {
+ opacity: 0;
+}
diff --git a/src/App.vue b/src/App.vue
index eb65b548..21f6f686 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,39 +1,32 @@
<template>
<div
- id="app"
+ id="app-loaded"
:style="bgStyle"
>
<div
id="app_bg_wrapper"
class="app-bg-wrapper"
/>
- <MobileNav v-if="isMobileLayout" />
+ <MobileNav v-if="layoutType === 'mobile'" />
<DesktopNav v-else />
- <div class="app-bg-wrapper app-container-wrapper" />
+ <Notifications v-if="currentUser" />
<div
id="content"
- class="container underlay"
+ class="app-layout container"
+ :class="classes"
>
- <div
- class="sidebar-flexer mobile-hidden"
- :style="sidebarAlign"
- >
- <div class="sidebar-bounds">
- <div class="sidebar-scroller">
- <div class="sidebar">
- <user-panel />
- <div v-if="!isMobileLayout">
- <nav-panel />
- <instance-specific-panel v-if="showInstanceSpecificPanel" />
- <features-panel v-if="!currentUser && showFeaturesPanel" />
- <who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
- <notifications v-if="currentUser" />
- </div>
- </div>
- </div>
- </div>
+ <div class="underlay"/>
+ <div id="sidebar" class="column -scrollable" :class="{ '-show-scrollbar': showScrollbars }">
+ <user-panel />
+ <template v-if="layoutType !== 'mobile'">
+ <nav-panel />
+ <instance-specific-panel v-if="showInstanceSpecificPanel" />
+ <features-panel v-if="!currentUser && showFeaturesPanel" />
+ <who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
+ <div id="notifs-sidebar" />
+ </template>
</div>
- <div class="main">
+ <div id="main-scroller" class="column main" :class="{ '-full-height': isChats }">
<div
v-if="!currentUser"
class="login-hint panel panel-default"
@@ -47,19 +40,20 @@
</div>
<router-view />
</div>
- <media-modal />
+ <div id="notifs-column" class="column -scrollable" :class="{ '-show-scrollbar': showScrollbars }"/>
</div>
+ <media-modal />
<shout-panel
v-if="currentUser && shout && !hideShoutbox"
:floating="true"
class="floating-shout mobile-hidden"
- :class="{ 'left': shoutboxPosition }"
+ :class="{ '-left': shoutboxPosition }"
/>
<MobilePostStatusButton />
<UserReportingModal />
<PostStatusModal />
<SettingsModal />
- <portal-target name="modal" />
+ <div id="modal" />
<GlobalNoticeList />
</div>
</template>
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index c4a0a800..f655c38f 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -1,8 +1,14 @@
-import Vue from 'vue'
-import VueRouter from 'vue-router'
-import routes from './routes'
+import { createApp } from 'vue'
+import { createRouter, createWebHistory } from 'vue-router'
+import vClickOutside from 'click-outside-vue3'
+
+import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
+
import App from '../App.vue'
-import { windowWidth } from '../services/window_utils/window_utils'
+import routes from './routes'
+import VBodyScrollLock from 'src/directives/body_scroll_lock'
+
+import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
@@ -326,8 +332,8 @@ const checkOAuthToken = async ({ store }) => {
}
const afterStoreSetup = async ({ store, i18n }) => {
- const width = windowWidth()
- store.dispatch('setMobileLayout', width <= 800)
+ store.dispatch('setLayoutWidth', windowWidth())
+ store.dispatch('setLayoutHeight', windowHeight())
FaviconService.initFaviconService()
@@ -367,25 +373,32 @@ const afterStoreSetup = async ({ store, i18n }) => {
getTOS({ store })
getStickers({ store })
- const router = new VueRouter({
- mode: 'history',
+ const router = createRouter({
+ history: createWebHistory(),
routes: routes(store),
scrollBehavior: (to, _from, savedPosition) => {
if (to.matched.some(m => m.meta.dontScroll)) {
return false
}
- return savedPosition || { x: 0, y: 0 }
+ return savedPosition || { left: 0, top: 0 }
}
})
- /* eslint-disable no-new */
- return new Vue({
- router,
- store,
- i18n,
- el: '#app',
- render: h => h(App)
- })
+ const app = createApp(App)
+
+ app.use(router)
+ app.use(store)
+ app.use(i18n)
+
+ app.use(vClickOutside)
+ app.use(VBodyScrollLock)
+
+ app.component('FAIcon', FontAwesomeIcon)
+ app.component('FALayers', FontAwesomeLayers)
+
+ app.mount('#app')
+
+ return app
}
export default afterStoreSetup
diff --git a/src/boot/routes.js b/src/boot/routes.js
index 1bc1f9f7..726476a8 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -46,7 +46,7 @@ export default (store) => {
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
{ name: 'remote-user-profile-acct',
- path: '/remote-users/(@?):username([^/@]+)@:hostname([^/@]+)',
+ path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
component: RemoteUserResolver,
beforeEnter: validateAuthenticatedRoute
},
@@ -62,14 +62,14 @@ export default (store) => {
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
{ name: 'registration-token', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
- { name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
+ { name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
{ name: 'login', path: '/login', component: AuthForm },
{ name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, props: () => ({ floating: false }) },
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
{ name: 'about', path: '/about', component: About },
- { name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
+ { name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile }
]
if (store.state.instance.pleromaChatMessagesAvailable) {
diff --git a/src/components/about/about.vue b/src/components/about/about.vue
index 518f6184..5d5d6479 100644
--- a/src/components/about/about.vue
+++ b/src/components/about/about.vue
@@ -1,5 +1,5 @@
<template>
- <div class="sidebar">
+ <div class="column-inner">
<instance-specific-panel v-if="showInstanceSpecificPanel" />
<staff-panel />
<terms-of-service-panel />
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js
index e53c4f77..99762562 100644
--- a/src/components/account_actions/account_actions.js
+++ b/src/components/account_actions/account_actions.js
@@ -40,7 +40,7 @@ const AccountActions = {
openChat () {
this.$router.push({
name: 'chat',
- params: { recipient_id: this.user.id }
+ params: { username: this.$store.state.users.currentUser.screen_name, recipient_id: this.user.id }
})
}
},
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index 1e31151c..c35d01af 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -74,10 +74,6 @@
<style lang="scss">
@import '../../_variables.scss';
.AccountActions {
- button.dropdown-item {
- margin-left: 0;
- }
-
.ellipsis-button {
width: 2.5em;
margin: -0.5em 0;
diff --git a/src/components/async_component_error/async_component_error.vue b/src/components/async_component_error/async_component_error.vue
index b1b59638..26ab5d21 100644
--- a/src/components/async_component_error/async_component_error.vue
+++ b/src/components/async_component_error/async_component_error.vue
@@ -19,6 +19,7 @@
<script>
export default {
+ emits: ['resetAsyncComponent'],
methods: {
retry () {
this.$emit('resetAsyncComponent')
diff --git a/src/components/attachment/attachment.scss b/src/components/attachment/attachment.scss
index dfda15bf..b2dea98d 100644
--- a/src/components/attachment/attachment.scss
+++ b/src/components/attachment/attachment.scss
@@ -173,7 +173,7 @@
margin: 8px;
word-break: break-all;
h1 {
- font-size: 14px;
+ font-size: 1rem;
margin: 0px;
}
}
diff --git a/src/components/auth_form/auth_form.js b/src/components/auth_form/auth_form.js
index e9a6e2d5..a86a3dca 100644
--- a/src/components/auth_form/auth_form.js
+++ b/src/components/auth_form/auth_form.js
@@ -1,3 +1,4 @@
+import { h, resolveComponent } from 'vue'
import LoginForm from '../login_form/login_form.vue'
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
import MFATOTPForm from '../mfa_form/totp_form.vue'
@@ -5,8 +6,8 @@ import { mapGetters } from 'vuex'
const AuthForm = {
name: 'AuthForm',
- render (createElement) {
- return createElement('component', { is: this.authForm })
+ render () {
+ return h(resolveComponent(this.authForm))
},
computed: {
authForm () {
diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index 53deb1df..eeca7828 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -4,7 +4,7 @@
<UserAvatar
class="avatar"
:user="user"
- @click.prevent.native="toggleUserExpanded"
+ @click.prevent="toggleUserExpanded"
/>
</router-link>
<div
diff --git a/src/components/bookmark_timeline/bookmark_timeline.js b/src/components/bookmark_timeline/bookmark_timeline.js
index 64b69e5d..5ac43d90 100644
--- a/src/components/bookmark_timeline/bookmark_timeline.js
+++ b/src/components/bookmark_timeline/bookmark_timeline.js
@@ -9,7 +9,7 @@ const Bookmarks = {
components: {
Timeline
},
- destroyed () {
+ unmounted () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
}
}
diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js
index b54f5fb2..9f6e64e3 100644
--- a/src/components/chat/chat.js
+++ b/src/components/chat/chat.js
@@ -6,7 +6,7 @@ import PostStatusForm from '../post_status_form/post_status_form.vue'
import ChatTitle from '../chat_title/chat_title.vue'
import chatService from '../../services/chat_service/chat_service.js'
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
-import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight, isScrollable } from './chat_layout_utils.js'
+import { getScrollPosition, getNewTopPosition, isBottomedOut, isScrollable } from './chat_layout_utils.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faChevronDown,
@@ -20,7 +20,7 @@ library.add(
)
const BOTTOMED_OUT_OFFSET = 10
-const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150
+const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 10
const SAFE_RESIZE_TIME_OFFSET = 100
const MARK_AS_READ_DELAY = 1500
const MAX_RETRIES = 10
@@ -43,7 +43,7 @@ const Chat = {
},
created () {
this.startFetching()
- window.addEventListener('resize', this.handleLayoutChange)
+ window.addEventListener('resize', this.handleResize)
},
mounted () {
window.addEventListener('scroll', this.handleScroll)
@@ -52,15 +52,11 @@ const Chat = {
}
this.$nextTick(() => {
- this.updateScrollableContainerHeight()
this.handleResize()
})
- this.setChatLayout()
},
- destroyed () {
+ unmounted () {
window.removeEventListener('scroll', this.handleScroll)
- window.removeEventListener('resize', this.handleLayoutChange)
- this.unsetChatLayout()
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
this.$store.dispatch('clearCurrentChat')
},
@@ -96,8 +92,7 @@ const Chat = {
...mapState({
backendInteractor: state => state.api.backendInteractor,
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus,
- mobileLayout: state => state.interface.mobileLayout,
- layoutHeight: state => state.interface.layoutHeight,
+ mobileLayout: state => state.interface.layoutType === 'mobile',
currentUser: state => state.users.currentUser
})
},
@@ -115,9 +110,6 @@ const Chat = {
'$route': function () {
this.startFetching()
},
- layoutHeight () {
- this.handleResize({ expand: true })
- },
mastoUserSocketStatus (newValue) {
if (newValue === WSConnectionStatus.JOINED) {
this.fetchChat({ isFirstFetch: true })
@@ -132,7 +124,6 @@ const Chat = {
onFilesDropped () {
this.$nextTick(() => {
this.handleResize()
- this.updateScrollableContainerHeight()
})
},
handleVisibilityChange () {
@@ -142,43 +133,7 @@ const Chat = {
}
})
},
- setChatLayout () {
- // This is a hacky way to adjust the global layout to the mobile chat (without modifying the rest of the app).
- // This layout prevents empty spaces from being visible at the bottom
- // of the chat on iOS Safari (`safe-area-inset`) when
- // - the on-screen keyboard appears and the user starts typing
- // - the user selects the text inside the input area
- // - the user selects and deletes the text that is multiple lines long
- // TODO: unify the chat layout with the global layout.
- let html = document.querySelector('html')
- if (html) {
- html.classList.add('chat-layout')
- }
-
- this.$nextTick(() => {
- this.updateScrollableContainerHeight()
- })
- },
- unsetChatLayout () {
- let html = document.querySelector('html')
- if (html) {
- html.classList.remove('chat-layout')
- }
- },
- handleLayoutChange () {
- this.$nextTick(() => {
- this.updateScrollableContainerHeight()
- this.scrollDown()
- })
- },
- // Ensures the proper position of the posting form in the mobile layout (the mobile browser panel does not overlap or hide it)
- updateScrollableContainerHeight () {
- const header = this.$refs.header
- const footer = this.$refs.footer
- const inner = this.mobileLayout ? window.document.body : this.$refs.inner
- this.scrollableContainerHeight = scrollableContainerHeight(inner, header, footer) + 'px'
- },
- // Preserves the scroll position when OSK appears or the posting form changes its height.
+ // "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
handleResize (opts = {}) {
const { expand = false, delayed = false } = opts
@@ -190,29 +145,20 @@ const Chat = {
}
this.$nextTick(() => {
- this.updateScrollableContainerHeight()
-
- const { offsetHeight = undefined } = this.lastScrollPosition
- this.lastScrollPosition = getScrollPosition(this.$refs.scrollable)
-
+ const { offsetHeight = undefined } = getScrollPosition()
const diff = this.lastScrollPosition.offsetHeight - offsetHeight
- if (diff < 0 || (!this.bottomedOut() && expand)) {
+ if (diff !== 0 || (!this.bottomedOut() && expand)) {
this.$nextTick(() => {
- this.updateScrollableContainerHeight()
- this.$refs.scrollable.scrollTo({
- top: this.$refs.scrollable.scrollTop - diff,
- left: 0
- })
+ window.scrollTo({ top: window.scrollY + diff })
})
}
+ this.lastScrollPosition = getScrollPosition()
})
},
scrollDown (options = {}) {
const { behavior = 'auto', forceRead = false } = options
- const scrollable = this.$refs.scrollable
- if (!scrollable) { return }
this.$nextTick(() => {
- scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior })
+ window.scrollTo({ top: document.documentElement.scrollHeight, behavior })
})
if (forceRead) {
this.readChat()
@@ -228,11 +174,10 @@ const Chat = {
})
},
bottomedOut (offset) {
- return isBottomedOut(this.$refs.scrollable, offset)
+ return isBottomedOut(offset)
},
reachedTop () {
- const scrollable = this.$refs.scrollable
- return scrollable && scrollable.scrollTop <= 0
+ return window.scrollY <= 0
},
cullOlderCheck () {
window.setTimeout(() => {
@@ -263,10 +208,9 @@ const Chat = {
}
}, 200),
handleScrollUp (positionBeforeLoading) {
- const positionAfterLoading = getScrollPosition(this.$refs.scrollable)
- this.$refs.scrollable.scrollTo({
- top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
- left: 0
+ const positionAfterLoading = getScrollPosition()
+ window.scrollTo({
+ top: getNewTopPosition(positionBeforeLoading, positionAfterLoading)
})
},
fetchChat ({ isFirstFetch = false, fetchLatest = false, maxId }) {
@@ -285,22 +229,18 @@ const Chat = {
chatService.clear(chatMessageService)
}
- const positionBeforeUpdate = getScrollPosition(this.$refs.scrollable)
+ const positionBeforeUpdate = getScrollPosition()
this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => {
this.$nextTick(() => {
if (fetchOlderMessages) {
this.handleScrollUp(positionBeforeUpdate)
}
- 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) {
+ if (!isScrollable() && messages.length > 0) {
this.fetchChat({ maxId: this.currentChatMessageService.minId })
}
})
@@ -336,9 +276,6 @@ const Chat = {
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 })
})
},
diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss
index 3a26686c..f2e154ab 100644
--- a/src/components/chat/chat.scss
+++ b/src/components/chat/chat.scss
@@ -1,28 +1,22 @@
.chat-view {
display: flex;
- height: calc(100vh - 60px);
- width: 100%;
-
- .chat-title {
- // prevents chat header jumping on when the user avatar loads
- height: 28px;
- }
+ height: 100%;
.chat-view-inner {
height: auto;
width: 100%;
overflow: visible;
display: flex;
- margin: 0.5em 0.5em 0 0.5em;
}
.chat-view-body {
+ box-sizing: border-box;
background-color: var(--chatBg, $fallback--bg);
display: flex;
flex-direction: column;
width: 100%;
overflow: visible;
- min-height: 100%;
+ min-height: calc(100vh - var(--navbar-height));
margin: 0 0 0 0;
border-radius: 10px 10px 0 0;
border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0;
@@ -32,36 +26,32 @@
}
}
- .scrollable-message-list {
+ .message-list {
padding: 0 0.8em;
height: 100%;
- overflow-y: scroll;
- overflow-x: hidden;
display: flex;
flex-direction: column;
+ justify-content: end;
}
.footer {
position: sticky;
bottom: 0;
+ background-color: $fallback--bg;
+ background-color: var(--bg, $fallback--bg);
+ z-index: 1;
}
.chat-view-heading {
- align-items: center;
- justify-content: space-between;
- top: 50px;
- display: flex;
- z-index: 2;
- position: sticky;
- overflow: hidden;
+ grid-template-columns: auto minmax(50%, 1fr);
}
.go-back-button {
- cursor: pointer;
- width: 28px;
text-align: center;
- padding: 0.6em;
- margin: -0.6em 0.6em -0.6em -0.6em;
+ line-height: 1;
+ height: 100%;
+ align-self: start;
+ width: var(--__panel-heading-height-inner);
}
.jump-to-bottom-button {
@@ -115,56 +105,4 @@
}
}
}
-
- @media all and (max-width: 800px) {
- height: 100%;
- overflow: hidden;
-
- .chat-view-inner {
- overflow: hidden;
- height: 100%;
- margin-top: 0;
- margin-left: 0;
- margin-right: 0;
- }
-
- .chat-view-body {
- display: flex;
- min-height: auto;
- overflow: hidden;
- height: 100%;
- margin: 0;
- border-radius: 0;
- }
-
- .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 {
- display: unset;
- overflow-y: scroll;
- overflow-x: hidden;
- -webkit-overflow-scrolling: touch;
- }
-
- .footer {
- position: sticky;
- bottom: auto;
- }
- }
}
diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue
index 94a0097c..2e7df7bd 100644
--- a/src/components/chat/chat.vue
+++ b/src/components/chat/chat.vue
@@ -2,23 +2,22 @@
<div class="chat-view">
<div class="chat-view-inner">
<div
- id="nav"
ref="inner"
class="panel-default panel chat-view-body"
>
<div
ref="header"
- class="panel-heading chat-view-heading mobile-hidden"
+ class="panel-heading -sticky chat-view-heading"
>
- <a
- class="go-back-button"
+ <button
+ class="button-unstyled go-back-button"
@click="goBack"
>
<FAIcon
size="lg"
icon="chevron-left"
/>
- </a>
+ </button>
<div class="title text-center">
<ChatTitle
:user="recipient"
@@ -26,73 +25,69 @@
/>
</div>
</div>
- <template>
+ <div
+ class="message-list"
+ :style="{ height: scrollableContainerHeight }"
+ >
+ <template v-if="!errorLoadingChat">
+ <ChatMessage
+ v-for="chatViewItem in chatViewItems"
+ :key="chatViewItem.id"
+ :author="recipient"
+ :chat-view-item="chatViewItem"
+ :hovered-message-chain="chatViewItem.messageChainId === hoveredMessageChainId"
+ @hover="onMessageHover"
+ />
+ </template>
<div
- ref="scrollable"
- class="scrollable-message-list"
- :style="{ height: scrollableContainerHeight }"
- @scroll="handleScroll"
+ v-else
+ class="chat-loading-error"
>
- <template v-if="!errorLoadingChat">
- <ChatMessage
- v-for="chatViewItem in chatViewItems"
- :key="chatViewItem.id"
- :author="recipient"
- :chat-view-item="chatViewItem"
- :hovered-message-chain="chatViewItem.messageChainId === hoveredMessageChainId"
- @hover="onMessageHover"
- />
- </template>
- <div
- v-else
- class="chat-loading-error"
- >
- <div class="alert error">
- {{ $t('chats.error_loading_chat') }}
- </div>
+ <div class="alert error">
+ {{ $t('chats.error_loading_chat') }}
</div>
</div>
+ </div>
+ <div
+ ref="footer"
+ class="panel-body footer"
+ >
<div
- ref="footer"
- class="panel-body footer"
+ class="jump-to-bottom-button"
+ :class="{ 'visible': jumpToBottomButtonVisible }"
+ @click="scrollDown({ behavior: 'smooth' })"
>
- <div
- class="jump-to-bottom-button"
- :class="{ 'visible': jumpToBottomButtonVisible }"
- @click="scrollDown({ behavior: 'smooth' })"
- >
- <span>
- <FAIcon icon="chevron-down" />
- <div
- v-if="newMessageCount"
- class="badge badge-notification unread-chat-count unread-message-count"
- >
- {{ newMessageCount }}
- </div>
- </span>
- </div>
- <PostStatusForm
- :disable-subject="true"
- :disable-scope-selector="true"
- :disable-notice="true"
- :disable-lock-warning="true"
- :disable-polls="true"
- :disable-sensitivity-checkbox="true"
- :disable-submit="errorLoadingChat || !currentChat"
- :disable-preview="true"
- :optimistic-posting="true"
- :post-handler="sendMessage"
- :submit-on-enter="!mobileLayout"
- :preserve-focus="!mobileLayout"
- :auto-focus="!mobileLayout"
- :placeholder="formPlaceholder"
- :file-limit="1"
- max-height="160"
- emoji-picker-placement="top"
- @resize="handleResize"
- />
+ <span>
+ <FAIcon icon="chevron-down" />
+ <div
+ v-if="newMessageCount"
+ class="badge badge-notification unread-chat-count unread-message-count"
+ >
+ {{ newMessageCount }}
+ </div>
+ </span>
</div>
- </template>
+ <PostStatusForm
+ :disable-subject="true"
+ :disable-scope-selector="true"
+ :disable-notice="true"
+ :disable-lock-warning="true"
+ :disable-polls="true"
+ :disable-sensitivity-checkbox="true"
+ :disable-submit="errorLoadingChat || !currentChat"
+ :disable-preview="true"
+ :optimistic-posting="true"
+ :post-handler="sendMessage"
+ :submit-on-enter="!mobileLayout"
+ :preserve-focus="!mobileLayout"
+ :auto-focus="!mobileLayout"
+ :placeholder="formPlaceholder"
+ :file-limit="1"
+ max-height="160"
+ emoji-picker-placement="top"
+ @resize="handleResize"
+ />
+ </div>
</div>
</div>
</div>
diff --git a/src/components/chat/chat_layout_utils.js b/src/components/chat/chat_layout_utils.js
index 50a933ac..c187892d 100644
--- a/src/components/chat/chat_layout_utils.js
+++ b/src/components/chat/chat_layout_utils.js
@@ -1,9 +1,9 @@
// Captures a scroll position
-export const getScrollPosition = (el) => {
+export const getScrollPosition = () => {
return {
- scrollTop: el.scrollTop,
- scrollHeight: el.scrollHeight,
- offsetHeight: el.offsetHeight
+ scrollTop: window.scrollY,
+ scrollHeight: document.documentElement.scrollHeight,
+ offsetHeight: window.innerHeight
}
}
@@ -13,21 +13,12 @@ export const getNewTopPosition = (previousPosition, newPosition) => {
return previousPosition.scrollTop + (newPosition.scrollHeight - previousPosition.scrollHeight)
}
-export const isBottomedOut = (el, offset = 0) => {
- if (!el) { return }
- const scrollHeight = el.scrollTop + offset
- const totalHeight = el.scrollHeight - el.offsetHeight
+export const isBottomedOut = (offset = 0) => {
+ const scrollHeight = window.scrollY + offset
+ const totalHeight = document.documentElement.scrollHeight - window.innerHeight
return totalHeight <= scrollHeight
}
-
-// Height of the scrollable container. The dynamic height is needed to ensure the mobile browser panel doesn't overlap or hide the posting form.
-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
+export const isScrollable = () => {
+ return document.documentElement.scrollHeight > window.innerHeight
}
diff --git a/src/components/chat_list/chat_list.vue b/src/components/chat_list/chat_list.vue
index f98b7ed2..58e8d0b3 100644
--- a/src/components/chat_list/chat_list.vue
+++ b/src/components/chat_list/chat_list.vue
@@ -6,7 +6,7 @@
v-else
class="chat-list panel panel-default"
>
- <div class="panel-heading">
+ <div class="panel-heading -sticky">
<span class="title">
{{ $t("chats.chats") }}
</span>
diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss
index 57332bed..c6b45c34 100644
--- a/src/components/chat_list_item/chat_list_item.scss
+++ b/src/components/chat_list_item/chat_list_item.scss
@@ -43,7 +43,7 @@
white-space: nowrap;
overflow: hidden;
flex-shrink: 1;
- line-height: 1.4em;
+ line-height: var(--post-line-height);
}
.chat-preview {
@@ -82,7 +82,7 @@
}
.time-wrapper {
- line-height: 1.4em;
+ line-height: var(--post-line-height);
}
.chat-preview-body {
diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js
index eb195bc1..5bac7736 100644
--- a/src/components/chat_message/chat_message.js
+++ b/src/components/chat_message/chat_message.js
@@ -27,6 +27,7 @@ const ChatMessage = {
'chatViewItem',
'hoveredMessageChain'
],
+ emits: ['hover'],
components: {
Popover,
Attachment,
diff --git a/src/components/chat_new/chat_new.scss b/src/components/chat_new/chat_new.scss
index 5506143d..240e1a38 100644
--- a/src/components/chat_new/chat_new.scss
+++ b/src/components/chat_new/chat_new.scss
@@ -22,10 +22,10 @@
}
.go-back-button {
- cursor: pointer;
- width: 28px;
text-align: center;
- padding: 0.6em;
- margin: -0.6em 0.6em -0.6em -0.6em;
+ line-height: 1;
+ height: 100%;
+ align-self: start;
+ width: var(--__panel-heading-height-inner);
}
}
diff --git a/src/components/chat_new/chat_new.vue b/src/components/chat_new/chat_new.vue
index f3894a3a..bf09a379 100644
--- a/src/components/chat_new/chat_new.vue
+++ b/src/components/chat_new/chat_new.vue
@@ -1,21 +1,20 @@
<template>
<div
- id="nav"
class="panel-default panel chat-new"
>
<div
ref="header"
class="panel-heading"
>
- <a
- class="go-back-button"
+ <button
+ class="button-unstyled go-back-button"
@click="goBack"
>
<FAIcon
size="lg"
icon="chevron-left"
/>
- </a>
+ </button>
</div>
<div class="input-wrap">
<div class="input-search">
diff --git a/src/components/chat_title/chat_title.js b/src/components/chat_title/chat_title.js
index edfbe7a4..f6e299ad 100644
--- a/src/components/chat_title/chat_title.js
+++ b/src/components/chat_title/chat_title.js
@@ -1,11 +1,12 @@
-import Vue from 'vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import UserAvatar from '../user_avatar/user_avatar.vue'
+import RichContent from 'src/components/rich_content/rich_content.jsx'
-export default Vue.component('chat-title', {
+export default {
name: 'ChatTitle',
components: {
- UserAvatar
+ UserAvatar,
+ RichContent
},
props: [
'user', 'withAvatar'
@@ -23,4 +24,4 @@ export default Vue.component('chat-title', {
return generateProfileLink(user.id, user.screen_name)
}
}
-})
+}
diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue
index a92028e8..7f6aaaa4 100644
--- a/src/components/chat_title/chat_title.vue
+++ b/src/components/chat_title/chat_title.vue
@@ -4,20 +4,21 @@
:title="title"
>
<router-link
+ class="avatar-container"
v-if="withAvatar && user"
:to="getUserProfileLink(user)"
>
<UserAvatar
+ class="titlebar-avatar"
:user="user"
- width="23px"
- height="23px"
/>
</router-link>
<RichContent
+ v-if="user"
class="username"
- :title="'@'+user.screen_name_ui"
+ :title="'@'+(user && user.screen_name_ui)"
:html="htmlTitle"
- :emoji="user.emoji"
+ :emoji="user.emoji || []"
/>
</div>
</template>
@@ -32,7 +33,6 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- align-items: center;
--emoji-size: 14px;
@@ -45,11 +45,15 @@
overflow: hidden;
}
- .Avatar {
- width: 23px;
- height: 23px;
- margin-right: 0.5em;
+ .avatar-container {
+ align-self: center;
+ line-height: 1;
+ }
+ .titlebar-avatar {
+ margin-right: 0.5em;
+ height: 1.5em;
+ width: 1.5em;
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue
index d28c2cfd..83695912 100644
--- a/src/components/checkbox/checkbox.vue
+++ b/src/components/checkbox/checkbox.vue
@@ -6,9 +6,9 @@
<input
type="checkbox"
:disabled="disabled"
- :checked="checked"
- :indeterminate.prop="indeterminate"
- @change="$emit('change', $event.target.checked)"
+ :checked="modelValue"
+ :indeterminate="indeterminate"
+ @change="$emit('update:modelValue', $event.target.checked)"
>
<i class="checkbox-indicator" />
<span
@@ -22,12 +22,9 @@
<script>
export default {
- model: {
- prop: 'checked',
- event: 'change'
- },
+ emits: ['update:modelValue'],
props: [
- 'checked',
+ 'modelValue',
'indeterminate',
'disabled'
]
diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue
index 8fb16113..e84603c3 100644
--- a/src/components/color_input/color_input.vue
+++ b/src/components/color_input/color_input.vue
@@ -11,28 +11,28 @@
</label>
<Checkbox
v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
- :checked="present"
+ :model-value="present"
:disabled="disabled"
class="opt"
- @change="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
+ @update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
/>
<div class="input color-input-field">
<input
:id="name + '-t'"
class="textColor unstyled"
type="text"
- :value="value || fallback"
+ :value="modelValue || fallback"
:disabled="!present || disabled"
- @input="$emit('input', $event.target.value)"
+ @input="$emit('update:modelValue', $event.target.value)"
>
<input
v-if="validColor"
:id="name"
class="nativeColor unstyled"
type="color"
- :value="value || fallback"
+ :value="modelValue || fallback"
:disabled="!present || disabled"
- @input="$emit('input', $event.target.value)"
+ @input="$emit('update:modelValue', $event.target.value)"
>
<div
v-if="transparentColor"
@@ -67,7 +67,7 @@ export default {
},
// Color value, should be required but vue cannot tell the difference
// between "property missing" and "property set to undefined"
- value: {
+ modelValue: {
required: false,
type: String,
default: undefined
@@ -91,18 +91,19 @@ export default {
default: true
}
},
+ emits: ['update:modelValue'],
computed: {
present () {
- return typeof this.value !== 'undefined'
+ return typeof this.modelValue !== 'undefined'
},
validColor () {
- return hex2rgb(this.value || this.fallback)
+ return hex2rgb(this.modelValue || this.fallback)
},
transparentColor () {
- return this.value === 'transparent'
+ return this.modelValue === 'transparent'
},
computedColor () {
- return this.value && this.value.startsWith('--')
+ return this.modelValue && this.modelValue.startsWith('--')
}
}
}
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 7628ceaa..6088e1ca 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -7,7 +7,7 @@
>
<div
v-if="isExpanded"
- class="panel-heading conversation-heading"
+ class="panel-heading conversation-heading -sticky"
>
<span class="title"> {{ $t('timeline.conversation') }} </span>
<button
@@ -27,20 +27,24 @@
v-if="shouldShowAllConversationButton"
class="conversation-dive-to-top-level-box"
>
- <i18n
- path="status.show_all_conversation_with_icon"
+ <i18n-t
+ keypath="status.show_all_conversation_with_icon"
tag="button"
class="button-unstyled -link"
@click.prevent="diveToTopLevel"
+ scope="global"
>
- <FAIcon
- place="icon"
- icon="angle-double-left"
- />
- <span place="text">
- {{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
- </span>
- </i18n>
+ <template #icon>
+ <FAIcon
+ icon="angle-double-left"
+ />
+ </template>
+ <template #text>
+ <span>
+ {{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
+ </span>
+ </template>
+ </i18n-t>
</div>
<div
v-if="shouldShowAncestors"
@@ -96,20 +100,24 @@
<div
class="thread-ancestor-dive-box-inner"
>
- <i18n
+ <i18n-t
tag="button"
- path="status.ancestor_follow_with_icon"
+ scope="global"
+ keypath="status.ancestor_follow_with_icon"
class="button-unstyled -link thread-tree-show-replies-button"
@click.prevent="diveIntoStatus(status.id)"
>
- <FAIcon
- place="icon"
- icon="angle-double-right"
- />
- <span place="text">
- {{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }}
- </span>
- </i18n>
+ <template #icon>
+ <FAIcon
+ icon="angle-double-right"
+ />
+ </template>
+ <template #text>
+ <span>
+ {{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }}
+ </span>
+ </template>
+ </i18n-t>
</div>
</div>
</div>
@@ -193,6 +201,8 @@
@import '../../_variables.scss';
.Conversation {
+ z-index: 1;
+
.conversation-dive-to-top-level-box {
padding: var(--status-margin, $status-margin);
border-bottom-width: 1px;
@@ -215,6 +225,7 @@
--text: var(--faint);
color: var(--text);
}
+
.thread-ancestor-dive-box {
padding-left: var(--status-margin, $status-margin);
border-bottom-width: 1px;
@@ -242,6 +253,7 @@
.thread-ancestor-has-other-replies .conversation-status,
.thread-ancestor:last-child .conversation-status,
.thread-ancestor:last-child .thread-ancestor-dive-box,
+ &:last-child .conversation-status,
&.-expanded .thread-tree .conversation-status {
border-bottom: none;
}
@@ -262,5 +274,9 @@
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
border-bottom: 1px solid var(--border, $fallback--border);
}
+
+ &.-expanded.status-fadein {
+ margin: calc(var(--status-margin, $status-margin) / 2);
+ }
}
</style>
diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss
index 2d468588..eddd9707 100644
--- a/src/components/desktop_nav/desktop_nav.scss
+++ b/src/components/desktop_nav/desktop_nav.scss
@@ -1,9 +1,11 @@
@import '../../_variables.scss';
.DesktopNav {
- height: 50px;
width: 100%;
- position: fixed;
+
+ input {
+ color: var(--inputTopbarText, var(--inputText));
+ }
a {
color: var(--topBarLink, $fallback--link);
@@ -11,7 +13,7 @@
.inner-nav {
display: grid;
- grid-template-rows: 50px;
+ grid-template-rows: var(--navbar-height);
grid-template-columns: 2fr auto 2fr;
grid-template-areas: "sitename logo actions";
box-sizing: border-box;
@@ -20,7 +22,7 @@
max-width: 980px;
}
- &.-logoLeft {
+ &.-logoLeft .inner-nav {
grid-template-columns: auto 2fr 2fr;
grid-template-areas: "logo sitename actions";
}
@@ -77,7 +79,7 @@
img {
display: inline-block;
- height: 50px;
+ height: var(--navbar-height);
}
}
@@ -103,8 +105,8 @@
.item {
flex: 1;
- line-height: 50px;
- height: 50px;
+ line-height: var(--navbar-height);
+ height: var(--navbar-height);
overflow: hidden;
display: flex;
flex-wrap: wrap;
diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue
index 304baf9d..bab3ca81 100644
--- a/src/components/desktop_nav/desktop_nav.vue
+++ b/src/components/desktop_nav/desktop_nav.vue
@@ -34,7 +34,7 @@
<search-bar
v-if="currentUser || !privateMode"
@toggled="onSearchBarToggled"
- @click.stop.native
+ @click.stop
/>
<button
class="button-unstyled nav-icon"
diff --git a/src/components/dialog_modal/dialog_modal.vue b/src/components/dialog_modal/dialog_modal.vue
index 3241ce3e..06b270c3 100644
--- a/src/components/dialog_modal/dialog_modal.vue
+++ b/src/components/dialog_modal/dialog_modal.vue
@@ -58,16 +58,7 @@
background-color: var(--bg, $fallback--bg);
.dialog-modal-heading {
- padding: .5em .5em;
- margin-right: auto;
- margin-bottom: 0;
- white-space: nowrap;
- color: var(--panelText);
- background-color: $fallback--fg;
- background-color: var(--panel, $fallback--fg);
-
.title {
- margin-bottom: 0;
text-align: center;
}
}
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 902ec384..391cc5b5 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -31,6 +31,7 @@ library.add(
*/
const EmojiInput = {
+ emits: ['update:modelValue', 'shown'],
props: {
suggest: {
/**
@@ -57,8 +58,7 @@ const EmojiInput = {
required: true,
type: Function
},
- // TODO VUE3: change to modelValue, change 'input' event to 'input'
- value: {
+ modelValue: {
/**
* Used for v-model
*/
@@ -137,8 +137,8 @@ const EmojiInput = {
return (this.wordAtCaret || {}).word || ''
},
wordAtCaret () {
- if (this.value && this.caret) {
- const word = Completion.wordAtPosition(this.value, this.caret - 1) || {}
+ if (this.modelValue && this.caret) {
+ const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
return word
}
}
@@ -189,8 +189,11 @@ const EmojiInput = {
img: imageUrl || ''
}))
},
- suggestions (newValue) {
- this.$nextTick(this.resize)
+ suggestions: {
+ handler (newValue) {
+ this.$nextTick(this.resize)
+ },
+ deep: true
}
},
methods: {
@@ -225,13 +228,13 @@ const EmojiInput = {
}
},
replace (replacement) {
- const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
- this.$emit('input', newValue)
+ const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement)
+ this.$emit('update:modelValue', newValue)
this.caret = 0
},
insert ({ insertion, keepOpen, surroundingSpace = true }) {
- const before = this.value.substring(0, this.caret) || ''
- const after = this.value.substring(this.caret) || ''
+ const before = this.modelValue.substring(0, this.caret) || ''
+ const after = this.modelValue.substring(this.caret) || ''
/* Using a bit more smart approach to padding emojis with spaces:
* - put a space before cursor if there isn't one already, unless we
@@ -259,7 +262,7 @@ const EmojiInput = {
after
].join('')
this.keepOpen = keepOpen
- this.$emit('input', newValue)
+ this.$emit('update:modelValue', newValue)
const position = this.caret + (insertion + spaceAfter + spaceBefore).length
if (!keepOpen) {
this.input.focus()
@@ -278,8 +281,8 @@ const EmojiInput = {
if (len > 0 || suggestion) {
const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
const replacement = chosenSuggestion.replacement
- const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
- this.$emit('input', newValue)
+ const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement)
+ this.$emit('update:modelValue', newValue)
this.highlighted = 0
const position = this.wordAtCaret.start + replacement.length
@@ -455,7 +458,7 @@ const EmojiInput = {
this.showPicker = false
this.setCaret(e)
this.resize()
- this.$emit('input', e.target.value)
+ this.$emit('update:modelValue', e.target.value)
},
onClickInput (e) {
this.showPicker = false
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index aa2950ce..7d95ab7e 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -78,7 +78,7 @@
top: 0;
right: 0;
margin: .2em .25em;
- font-size: 16px;
+ font-size: 1.3em;
cursor: pointer;
line-height: 24px;
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 2716d93f..bd5c2e39 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,3 +1,4 @@
+import { defineAsyncComponent } from 'vue'
import Checkbox from '../checkbox/checkbox.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@@ -5,6 +6,7 @@ import {
faStickyNote,
faSmileBeam
} from '@fortawesome/free-solid-svg-icons'
+import { trim } from 'lodash'
library.add(
faBoxOpen,
@@ -57,7 +59,7 @@ const EmojiPicker = {
}
},
components: {
- StickerPicker: () => import('../sticker_picker/sticker_picker.vue'),
+ StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
Checkbox
},
methods: {
@@ -79,7 +81,7 @@ const EmojiPicker = {
},
highlight (key) {
const ref = this.$refs['group-' + key]
- const top = ref[0].offsetTop
+ const top = ref.offsetTop
this.setShowStickers(false)
this.activeGroup = key
this.$nextTick(() => {
@@ -96,7 +98,7 @@ const EmojiPicker = {
}
},
triggerLoadMore (target) {
- const ref = this.$refs['group-end-custom'][0]
+ const ref = this.$refs['group-end-custom']
if (!ref) return
const bottom = ref.offsetTop + ref.offsetHeight
@@ -119,7 +121,7 @@ const EmojiPicker = {
this.$nextTick(() => {
this.emojisView.forEach(group => {
const ref = this.$refs['group-' + group.id]
- if (ref[0].offsetTop <= top) {
+ if (ref.offsetTop <= top) {
this.activeGroup = group.id
}
})
@@ -175,7 +177,7 @@ const EmojiPicker = {
filteredEmoji () {
return filterByKeyword(
this.$store.state.instance.customEmoji || [],
- this.keyword
+ trim(this.keyword)
)
},
customEmojiBuffer () {
@@ -196,7 +198,7 @@ const EmojiPicker = {
id: 'standard',
text: this.$t('emoji.unicode'),
icon: 'box-open',
- emojis: filterByKeyword(standardEmojis, this.keyword)
+ emojis: filterByKeyword(standardEmojis, trim(this.keyword))
}
]
},
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index ec711758..2055e02e 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -7,7 +7,7 @@
right: 0;
left: 0;
margin: 0 !important;
- z-index: 1;
+ z-index: 100;
background-color: $fallback--bg;
background-color: var(--popover, $fallback--bg);
color: $fallback--link;
@@ -73,12 +73,13 @@
&-item {
padding: 0 7px;
cursor: pointer;
- font-size: 24px;
+ font-size: 1.85em;
&.disabled {
opacity: 0.5;
pointer-events: none;
}
+
&.active {
border-bottom: 4px solid;
@@ -151,9 +152,10 @@
justify-content: left;
&-title {
- font-size: 12px;
+ font-size: 0.85em;
width: 100%;
margin: 0;
+
&.disabled {
display: none;
}
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 3262a3d9..a7269120 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -47,6 +47,7 @@
type="text"
class="form-control"
:placeholder="$t('emoji.search_emoji')"
+ @input="$event.target.composing = false"
>
</div>
<div
diff --git a/src/components/exporter/exporter.js b/src/components/exporter/exporter.js
index 51912ac3..fc75372e 100644
--- a/src/components/exporter/exporter.js
+++ b/src/components/exporter/exporter.js
@@ -15,18 +15,8 @@ const Exporter = {
type: String,
default: 'export.csv'
},
- exportButtonLabel: {
- type: String,
- default () {
- return this.$t('exporter.export')
- }
- },
- processingMessage: {
- type: String,
- default () {
- return this.$t('exporter.processing')
- }
- }
+ exportButtonLabel: { type: String },
+ processingMessage: { type: String }
},
data () {
return {
diff --git a/src/components/exporter/exporter.vue b/src/components/exporter/exporter.vue
index d6a03088..79defdf6 100644
--- a/src/components/exporter/exporter.vue
+++ b/src/components/exporter/exporter.vue
@@ -7,14 +7,14 @@
spin
/>
- <span>{{ processingMessage }}</span>
+ <span>{{ processingMessage || $t('exporter.processing') }}</span>
</div>
<button
v-else
class="btn button-default"
@click="process"
>
- {{ exportButtonLabel }}
+ {{ exportButtonLabel || $t('exporter.export') }}
</button>
</div>
</template>
diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js
index 137ef9c0..92ee3f30 100644
--- a/src/components/font_control/font_control.js
+++ b/src/components/font_control/font_control.js
@@ -1,4 +1,4 @@
-import { set } from 'vue'
+import { set } from 'lodash'
import Select from '../select/select.vue'
export default {
@@ -6,11 +6,12 @@ export default {
Select
},
props: [
- 'name', 'label', 'value', 'fallback', 'options', 'no-inherit'
+ 'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'
],
+ emits: ['update:modelValue'],
data () {
return {
- lValue: this.value,
+ lValue: this.modelValue,
availableOptions: [
this.noInherit ? '' : 'inherit',
'custom',
@@ -22,7 +23,7 @@ export default {
}
},
beforeUpdate () {
- this.lValue = this.value
+ this.lValue = this.modelValue
},
computed: {
present () {
@@ -37,7 +38,7 @@ export default {
},
set (v) {
set(this.lValue, 'family', v)
- this.$emit('input', this.lValue)
+ this.$emit('update:modelValue', this.lValue)
}
},
isCustom () {
diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue
index 29605084..f100c3a9 100644
--- a/src/components/font_control/font_control.vue
+++ b/src/components/font_control/font_control.vue
@@ -15,13 +15,14 @@
class="opt exlcude-disabled"
type="checkbox"
:checked="present"
- @input="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
+ @change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
>
<label
v-if="typeof fallback !== 'undefined'"
class="opt-l"
:for="name + '-o'"
/>
+ {{ ' ' }}
<Select
:id="name + '-font-switcher'"
v-model="preset"
diff --git a/src/components/gallery/gallery.js b/src/components/gallery/gallery.js
index 094b3e57..4e1bda55 100644
--- a/src/components/gallery/gallery.js
+++ b/src/components/gallery/gallery.js
@@ -1,5 +1,5 @@
import Attachment from '../attachment/attachment.vue'
-import { sumBy } from 'lodash'
+import { sumBy, set } from 'lodash'
const Gallery = {
props: [
@@ -85,7 +85,7 @@ const Gallery = {
},
methods: {
onNaturalSizeLoad ({ id, width, height }) {
- this.$set(this.sizes, id, { width, height })
+ set(this.sizes, id, { width, height })
},
rowStyle (row) {
if (row.audio) {
diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue
index f2e1b5ce..ccf6e3e2 100644
--- a/src/components/gallery/gallery.vue
+++ b/src/components/gallery/gallery.vue
@@ -22,7 +22,6 @@
class="gallery-item"
:nsfw="nsfw"
:attachment="attachment"
- :allow-play="false"
:size="size"
:editable="editable"
:remove="removeAttachment"
diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue
index a45f4586..ddc45b81 100644
--- a/src/components/global_notice_list/global_notice_list.vue
+++ b/src/components/global_notice_list/global_notice_list.vue
@@ -44,20 +44,18 @@
max-width: calc(100% - 3em);
display: flex;
padding-left: 1.5em;
- line-height: 2em;
+ line-height: 2;
+ margin-bottom: 0.5em;
+
.notice-message {
flex: 1 1 100%;
}
- i {
- flex: 0 0;
- width: 1.5em;
- cursor: pointer;
- }
}
.global-error {
background-color: var(--alertPopupError, $fallback--cRed);
color: var(--alertPopupErrorText, $fallback--text);
+
.svg-inline--fa {
color: var(--alertPopupErrorText, $fallback--text);
}
@@ -66,6 +64,7 @@
.global-warning {
background-color: var(--alertPopupWarning, $fallback--cOrange);
color: var(--alertPopupWarningText, $fallback--text);
+
.svg-inline--fa {
color: var(--alertPopupWarningText, $fallback--text);
}
diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js
index e8d5ec6d..05f6fd4c 100644
--- a/src/components/image_cropper/image_cropper.js
+++ b/src/components/image_cropper/image_cropper.js
@@ -117,7 +117,7 @@ const ImageCropper = {
const fileInput = this.$refs.input
fileInput.addEventListener('change', this.readFile)
},
- beforeDestroy: function () {
+ beforeUnmount: function () {
// remove the event listeners
const trigger = this.getTriggerDOM()
if (trigger) {
diff --git a/src/components/importer/importer.js b/src/components/importer/importer.js
index 59f9beb1..da86a223 100644
--- a/src/components/importer/importer.js
+++ b/src/components/importer/importer.js
@@ -15,24 +15,9 @@ const Importer = {
type: Function,
required: true
},
- submitButtonLabel: {
- type: String,
- default () {
- return this.$t('importer.submit')
- }
- },
- successMessage: {
- type: String,
- default () {
- return this.$t('importer.success')
- }
- },
- errorMessage: {
- type: String,
- default () {
- return this.$t('importer.error')
- }
- }
+ submitButtonLabel: { type: String },
+ successMessage: { type: String },
+ errorMessage: { type: String }
},
data () {
return {
diff --git a/src/components/importer/importer.vue b/src/components/importer/importer.vue
index 210823f5..2a63b31a 100644
--- a/src/components/importer/importer.vue
+++ b/src/components/importer/importer.vue
@@ -18,21 +18,31 @@
class="btn button-default"
@click="submit"
>
- {{ submitButtonLabel }}
+ {{ submitButtonLabel || $t('importer.submit') }}
</button>
<div v-if="success">
- <FAIcon
- icon="times"
+ <button
+ class="button-unstyled"
@click="dismiss"
- />
- <p>{{ successMessage }}</p>
+ >
+ <FAIcon
+ icon="times"
+ />
+ </button>
+ {{ ' ' }}
+ <span>{{ successMessage || $t('importer.success') }}</span>
</div>
<div v-else-if="error">
- <FAIcon
- icon="times"
+ <button
+ class="button-unstyled"
@click="dismiss"
- />
- <p>{{ errorMessage }}</p>
+ >
+ <FAIcon
+ icon="times"
+ />
+ </button>
+ {{ ' ' }}
+ <span>{{ errorMessage || $t('importer.error') }}</span>
</div>
</div>
</template>
diff --git a/src/components/interactions/interactions.js b/src/components/interactions/interactions.js
index 53ec7d49..95087eac 100644
--- a/src/components/interactions/interactions.js
+++ b/src/components/interactions/interactions.js
@@ -1,4 +1,5 @@
import Notifications from '../notifications/notifications.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
const tabModeDict = {
mentions: ['mention'],
@@ -23,7 +24,8 @@ const Interactions = {
}
},
components: {
- Notifications
+ Notifications,
+ TabSwitcher
}
}
diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue
index cf307a24..7ad1fe2e 100644
--- a/src/components/interface_language_switcher/interface_language_switcher.vue
+++ b/src/components/interface_language_switcher/interface_language_switcher.vue
@@ -1,11 +1,12 @@
<template>
<div>
<label for="interface-language-switcher">
- {{ $t('settings.interfaceLanguage') }}
+ {{ promptText }}
</label>
+ {{ ' ' }}
<Select
id="interface-language-switcher"
- v-model="language"
+ v-model="controlledLanguage"
>
<option
v-for="lang in languages"
@@ -19,39 +20,43 @@
</template>
<script>
-import languagesObject from '../../i18n/messages'
import localeService from '../../services/locale/locale.service.js'
-import ISO6391 from 'iso-639-1'
-import _ from 'lodash'
import Select from '../select/select.vue'
export default {
components: {
Select
},
+ props: {
+ promptText: {
+ type: String,
+ required: true
+ },
+ language: {
+ type: String,
+ required: true
+ },
+ setLanguage: {
+ type: Function,
+ required: true
+ }
+ },
computed: {
languages () {
- return _.map(languagesObject.languages, (code) => ({ code: code, name: this.getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))
+ return localeService.languages
},
- language: {
- get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
+ controlledLanguage: {
+ get: function () { return this.language },
set: function (val) {
- this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
+ this.setLanguage(val)
}
}
},
methods: {
getLanguageName (code) {
- const specialLanguageNames = {
- 'ja_easy': 'やさしいにほんご',
- 'zh': '简体中文',
- 'zh_Hant': '繁體中文'
- }
- const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code)
- const browserLocale = localeService.internalToBrowserLocale(code)
- return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1)
+ return localeService.getLanguageName(code)
}
}
}
diff --git a/src/components/link-preview/link-preview.vue b/src/components/link-preview/link-preview.vue
index d3ca39b8..220527f2 100644
--- a/src/components/link-preview/link-preview.vue
+++ b/src/components/link-preview/link-preview.vue
@@ -63,7 +63,7 @@
}
.card-host {
- font-size: 12px;
+ font-size: 0.85em;
}
.card-description {
diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue
index bfabb946..21482977 100644
--- a/src/components/login_form/login_form.vue
+++ b/src/components/login_form/login_form.vue
@@ -76,11 +76,15 @@
>
<div class="alert error">
{{ error }}
- <FAIcon
- class="fa-scale-110 fa-old-padding"
- icon="times"
+ <button
+ class="button-unstyled"
@click="clearError"
- />
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
+ />
+ </button>
</div>
</div>
</div>
@@ -97,7 +101,7 @@
padding: 0.6em;
.btn {
- min-height: 28px;
+ min-height: 2em;
width: 10em;
}
diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js
index 01a90377..ff993664 100644
--- a/src/components/media_modal/media_modal.js
+++ b/src/components/media_modal/media_modal.js
@@ -142,7 +142,7 @@ const MediaModal = {
document.addEventListener('keyup', this.handleKeyupEvent)
document.addEventListener('keydown', this.handleKeydownEvent)
},
- destroyed () {
+ unmounted () {
window.removeEventListener('popstate', this.hide)
document.removeEventListener('keyup', this.handleKeyupEvent)
document.removeEventListener('keydown', this.handleKeydownEvent)
diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue
index 708a43c6..8b76aafb 100644
--- a/src/components/media_modal/media_modal.vue
+++ b/src/components/media_modal/media_modal.vue
@@ -121,7 +121,7 @@ $modal-view-button-icon-width: 3em;
$modal-view-button-icon-margin: 0.5em;
.modal-view.media-modal-view {
- z-index: 1001;
+ z-index: 9000;
flex-direction: column;
.modal-view-button-arrow,
@@ -234,7 +234,7 @@ $modal-view-button-icon-margin: 0.5em;
position: absolute;
height: $modal-view-button-icon-height;
width: $modal-view-button-icon-width;
- font-size: 14px;
+ font-size: 1rem;
line-height: $modal-view-button-icon-height;
color: #FFF;
text-align: center;
diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue
index e955aa72..7cc59f5a 100644
--- a/src/components/media_upload/media_upload.vue
+++ b/src/components/media_upload/media_upload.vue
@@ -17,9 +17,9 @@
/>
<input
v-if="uploadReady"
+ class="hidden-input-file"
:disabled="disabled"
type="file"
- style="position: fixed; top: -100em"
multiple="true"
@change="change"
>
@@ -32,6 +32,10 @@
@import '../../_variables.scss';
.media-upload {
- cursor: pointer;
+ cursor: pointer; // We use <label> for interactivity... i wonder if it's fine
+
+ .hidden-input-file {
+ display: none;
+ }
}
</style>
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 3562f511..022f04c7 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -41,10 +41,12 @@
class="serverName"
:class="{ '-faded': shouldFadeDomain }"
v-html="'@' + serverName"
- /></span><span
+ />
+ </span>
+ <span
v-if="isYou && shouldShowYous"
:class="{ '-you': shouldBoldenYou }"
- > {{ $t('status.you') }}</span>
+ > {{ ' ' + $t('status.you') }}</span>
<!-- eslint-enable vue/no-v-html -->
</a><span
v-if="shouldShowTooltip"
diff --git a/src/components/mentions_line/mentions_line.vue b/src/components/mentions_line/mentions_line.vue
index f375e3b0..09b6a1d6 100644
--- a/src/components/mentions_line/mentions_line.vue
+++ b/src/components/mentions_line/mentions_line.vue
@@ -6,7 +6,6 @@
class="mention-link"
:content="mention.content"
:url="mention.url"
- :first-mention="false"
/><span
v-if="manyMentions"
class="extraMentions"
@@ -21,7 +20,6 @@
class="mention-link"
:content="mention.content"
:url="mention.url"
- :first-mention="false"
/>
</span><button
v-if="!expanded"
diff --git a/src/components/mfa_form/recovery_form.vue b/src/components/mfa_form/recovery_form.vue
index 7c594228..a9cf39aa 100644
--- a/src/components/mfa_form/recovery_form.vue
+++ b/src/components/mfa_form/recovery_form.vue
@@ -56,11 +56,15 @@
>
<div class="alert error">
{{ error }}
- <FAIcon
- class="fa-scale-110 fa-old-padding"
- icon="times"
+ <button
+ class="button-unstyled"
@click="clearError"
- />
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
+ />
+ </button>
</div>
</div>
</div>
diff --git a/src/components/mfa_form/totp_form.vue b/src/components/mfa_form/totp_form.vue
index 4ee13992..709eb9b8 100644
--- a/src/components/mfa_form/totp_form.vue
+++ b/src/components/mfa_form/totp_form.vue
@@ -58,12 +58,16 @@
>
<div class="alert error">
{{ error }}
- <FAIcon
- size="lg"
- class="fa-scale-110 fa-old-padding"
- icon="times"
+ <button
+ class="button-unstyled"
@click="clearError"
- />
+ >
+ <FAIcon
+ size="lg"
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
+ />
+ </button>
</div>
</div>
</div>
diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js
index 9e736cfb..877d52a9 100644
--- a/src/components/mobile_nav/mobile_nav.js
+++ b/src/components/mobile_nav/mobile_nav.js
@@ -78,7 +78,8 @@ const MobileNav = {
this.$store.dispatch('logout')
},
markNotificationsAsSeen () {
- this.$refs.notifications.markAsSeen()
+ // this.$refs.notifications.markAsSeen()
+ this.$store.dispatch('markNotificationsAsSeen')
},
onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {
if (scrollTop + clientHeight >= scrollHeight) {
diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue
index 0f0ea457..d2d48a03 100644
--- a/src/components/mobile_nav/mobile_nav.vue
+++ b/src/components/mobile_nav/mobile_nav.vue
@@ -5,7 +5,6 @@
<nav
id="nav"
class="mobile-nav"
- :class="{ 'mobile-hidden': isChat }"
@click="scrollToTop()"
>
<div class="item">
@@ -51,7 +50,7 @@
<div
v-if="currentUser"
class="mobile-notifications-drawer"
- :class="{ 'closed': !notificationsOpen }"
+ :class="{ '-closed': !notificationsOpen }"
@touchstart.stop="notificationsTouchStart"
@touchmove.stop="notificationsTouchMove"
>
@@ -69,12 +68,9 @@
</div>
<div
class="mobile-notifications"
+ id="mobile-notifications"
@scroll="onScroll"
>
- <Notifications
- ref="notifications"
- :no-heading="true"
- />
</div>
</div>
<SideDrawer
@@ -92,13 +88,14 @@
.MobileNav {
.mobile-nav {
display: grid;
- line-height: 50px;
- height: 50px;
+ line-height: var(--navbar-height);
grid-template-rows: 50px;
grid-template-columns: 2fr auto;
width: 100%;
- position: fixed;
box-sizing: border-box;
+ a {
+ color: var(--topBarLink, $fallback--link);
+ }
}
.mobile-inner-nav {
@@ -153,8 +150,9 @@
z-index: 1001;
-webkit-overflow-scrolling: touch;
- &.closed {
+ &.-closed {
transform: translateX(100%);
+ box-shadow: none;
}
}
@@ -182,7 +180,7 @@
.mobile-notifications {
margin-top: 50px;
width: 100vw;
- height: calc(100vh - 50px);
+ height: calc(100vh - var(--navbar-height));
overflow-x: hidden;
overflow-y: scroll;
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 d27fb3b8..ecf79b64 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
@@ -29,7 +29,7 @@ const MobilePostStatusButton = {
}
window.addEventListener('resize', this.handleOSK)
},
- destroyed () {
+ unmounted () {
if (this.autohideFloatingPostButton) {
this.deactivateFloatingPostButtonAutohide()
}
@@ -45,7 +45,7 @@ const MobilePostStatusButton = {
return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
},
isPersistent () {
- return !!this.$store.getters.mergedConfig.showNewPostButton
+ return !!this.$store.getters.mergedConfig.alwaysShowNewPostButton
},
autohideFloatingPostButton () {
return !!this.$store.getters.mergedConfig.autohideFloatingPostButton
diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.vue b/src/components/mobile_post_status_button/mobile_post_status_button.vue
index 37becf4c..9fcdf6f8 100644
--- a/src/components/mobile_post_status_button/mobile_post_status_button.vue
+++ b/src/components/mobile_post_status_button/mobile_post_status_button.vue
@@ -1,13 +1,12 @@
<template>
- <div v-if="isLoggedIn">
- <button
- class="button-default new-status-button"
- :class="{ 'hidden': isHidden, 'always-show': isPersistent }"
- @click="openPostForm"
- >
- <FAIcon icon="pen" />
- </button>
- </div>
+ <button
+ v-if="isLoggedIn"
+ class="MobilePostButton button-default new-status-button"
+ :class="{ 'hidden': isHidden, 'always-show': isPersistent }"
+ @click="openPostForm"
+ >
+ <FAIcon icon="pen" />
+ </button>
</template>
<script src="./mobile_post_status_button.js"></script>
@@ -15,25 +14,27 @@
<style lang="scss">
@import '../../_variables.scss';
-.new-status-button {
- width: 5em;
- height: 5em;
- border-radius: 100%;
- position: fixed;
- bottom: 1.5em;
- right: 1.5em;
- // TODO: this needs its own color, it has to stand out enough and link color
- // is not very optimal for this particular use.
- background-color: $fallback--fg;
- background-color: var(--btn, $fallback--fg);
- display: flex;
- justify-content: center;
- align-items: center;
- box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3), 0px 4px 6px rgba(0, 0, 0, 0.3);
- z-index: 10;
-
- transition: 0.35s transform;
- transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
+.MobilePostButton {
+ &.button-default {
+ width: 5em;
+ height: 5em;
+ border-radius: 100%;
+ position: fixed;
+ bottom: 1.5em;
+ right: 1.5em;
+ // TODO: this needs its own color, it has to stand out enough and link color
+ // is not very optimal for this particular use.
+ background-color: $fallback--fg;
+ background-color: var(--btn, $fallback--fg);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3), 0px 4px 6px rgba(0, 0, 0, 0.3);
+ z-index: 10;
+
+ transition: 0.35s transform;
+ transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
+ }
&.hidden {
transform: translateY(150%);
diff --git a/src/components/modal/modal.vue b/src/components/modal/modal.vue
index 2b58913f..9394efff 100644
--- a/src/components/modal/modal.vue
+++ b/src/components/modal/modal.vue
@@ -35,7 +35,7 @@ export default {
<style lang="scss">
.modal-view {
- z-index: 1000;
+ z-index: 2000;
position: fixed;
top: 0;
left: 0;
diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue
index 96476abe..96b8c3a3 100644
--- a/src/components/moderation_tools/moderation_tools.vue
+++ b/src/components/moderation_tools/moderation_tools.vue
@@ -132,7 +132,7 @@
</button>
</template>
</Popover>
- <portal to="modal">
+ <teleport to="#modal">
<DialogModal
v-if="showDeleteUserDialog"
:on-cancel="deleteUserDialog.bind(this, false)"
@@ -156,7 +156,7 @@
</button>
</template>
</DialogModal>
- </portal>
+ </teleport>
</div>
</template>
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index e10eff00..ebbe5f95 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -33,7 +33,7 @@
>
<a
class="avatar-container"
- :href="notification.from_profile.statusnet_profile_url"
+ :href="$router.resolve(userProfileLink).href"
@click.stop.prevent.capture="toggleUserExpanded"
>
<UserAvatar
@@ -65,12 +65,16 @@
v-else
class="username"
:title="'@'+notification.from_profile.screen_name_ui"
- >{{ notification.from_profile.name }}</span>
+ >
+ {{ notification.from_profile.name }}
+ </span>
+ {{ ' ' }}
<span v-if="notification.type === 'like'">
<FAIcon
class="type-icon"
icon="star"
/>
+ {{ ' ' }}
<small>{{ $t('notifications.favorited_you') }}</small>
</span>
<span v-if="notification.type === 'repeat'">
@@ -79,6 +83,7 @@
icon="retweet"
:title="$t('tool_tip.repeat')"
/>
+ {{ ' ' }}
<small>{{ $t('notifications.repeated_you') }}</small>
</span>
<span v-if="notification.type === 'follow'">
@@ -86,6 +91,7 @@
class="type-icon"
icon="user-plus"
/>
+ {{ ' ' }}
<small>{{ $t('notifications.followed_you') }}</small>
</span>
<span v-if="notification.type === 'follow_request'">
@@ -93,6 +99,7 @@
class="type-icon"
icon="user"
/>
+ {{ ' ' }}
<small>{{ $t('notifications.follow_request') }}</small>
</span>
<span v-if="notification.type === 'move'">
@@ -100,18 +107,30 @@
class="type-icon"
icon="suitcase-rolling"
/>
+ {{ ' ' }}
<small>{{ $t('notifications.migrated_to') }}</small>
</span>
<span v-if="notification.type === 'pleroma:emoji_reaction'">
<small>
- <i18n path="notifications.reacted_with">
+ <i18n-t
+ scope="global"
+ keypath="notifications.reacted_with"
+ >
<span class="emoji-reaction-emoji">{{ notification.emoji }}</span>
- </i18n>
+ </i18n-t>
</small>
</span>
<span v-if="notification.type === 'pleroma:report'">
<small>{{ $t('notifications.submitted_report') }}</small>
</span>
+ <span v-if="notification.type === 'poll'">
+ <FAIcon
+ class="type-icon"
+ icon="poll-h"
+ />
+ {{ ' ' }}
+ <small>{{ $t('notifications.poll_ended') }}</small>
+ </span>
</div>
<div
v-if="isStatusNotification"
@@ -164,18 +183,26 @@
v-if="notification.type === 'follow_request'"
style="white-space: nowrap;"
>
- <FAIcon
- icon="check"
- class="fa-scale-110 fa-old-padding follow-request-accept"
+ <button
+ class="button-unstyled"
:title="$t('tool_tip.accept_follow_request')"
@click="approveUser()"
- />
- <FAIcon
- icon="times"
- class="fa-scale-110 fa-old-padding follow-request-reject"
+ >
+ <FAIcon
+ icon="check"
+ class="fa-scale-110 fa-old-padding follow-request-accept"
+ />
+ </button>
+ <button
+ class="button-unstyled"
:title="$t('tool_tip.reject_follow_request')"
@click="denyUser()"
- />
+ >
+ <FAIcon
+ icon="times"
+ class="fa-scale-110 fa-old-padding follow-request-reject"
+ />
+ </button>
</div>
</div>
<div
diff --git a/src/components/notifications/notification_filters.vue b/src/components/notifications/notification_filters.vue
index ba0e90a0..00a531b3 100644
--- a/src/components/notifications/notification_filters.vue
+++ b/src/components/notifications/notification_filters.vue
@@ -61,10 +61,19 @@
:class="{ 'menu-checkbox-checked': filters.moves }"
/>{{ $t('settings.notification_visibility_moves') }}
</button>
+ <button
+ class="button-default dropdown-item"
+ @click="toggleNotificationFilter('polls')"
+ >
+ <span
+ class="menu-checkbox"
+ :class="{ 'menu-checkbox-checked': filters.polls }"
+ />{{ $t('settings.notification_visibility_polls') }}
+ </button>
</div>
</template>
<template v-slot:trigger>
- <button class="button-unstyled">
+ <button class="filter-trigger-button button-unstyled">
<FAIcon icon="filter" />
</button>
</template>
@@ -107,15 +116,14 @@ export default {
align-self: stretch;
> button {
- font-size: 1.2em;
- padding-left: 0.7em;
- padding-right: 0.2em;
line-height: 100%;
height: 100%;
- }
+ width: var(--__panel-heading-height-inner);
+ text-align: center;
- .dropdown-item {
- margin: 0;
+ svg {
+ font-size: 1.2em;
+ }
}
}
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index c8f1ebcb..82aa1489 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -23,13 +23,13 @@ const Notifications = {
NotificationFilters
},
props: {
- // Disables display of panel header
- noHeading: Boolean,
// Disables panel styles, unread mark, potentially other notification-related actions
// meant for "Interactions" timeline
minimalMode: Boolean,
// Custom filter mode, an array of strings, possible values 'mention', 'repeat', 'like', 'follow', used to override global filter for use in "Interactions" timeline
- filterMode: Array
+ filterMode: Array,
+ // Disable teleporting (i.e. for /users/user/notifications)
+ disableTeleport: Boolean
},
data () {
return {
@@ -65,6 +65,18 @@ const Notifications = {
loading () {
return this.$store.state.statuses.notifications.loading
},
+ noHeading () {
+ const { layoutType } = this.$store.state.interface
+ return this.minimalMode || layoutType === 'mobile'
+ },
+ teleportTarget () {
+ const { layoutType } = this.$store.state.interface
+ const map = {
+ wide: '#notifs-column',
+ mobile: '#mobile-notifications'
+ }
+ return map[layoutType] || '#notifs-sidebar'
+ },
notificationsToDisplay () {
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
},
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 47c035f1..f71f9b76 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -11,10 +11,6 @@
color: var(--text, $fallback--text);
}
- .notifications-footer {
- border: none;
- }
-
.notification {
position: relative;
@@ -47,6 +43,10 @@
}
}
+ &:last-child .Notification {
+ border-bottom: none;
+ }
+
.non-mention {
display: flex;
flex: 1;
@@ -66,8 +66,6 @@
}
.follow-request-accept {
- cursor: pointer;
-
&:hover {
color: $fallback--text;
color: var(--text, $fallback--text);
@@ -75,8 +73,6 @@
}
.follow-request-reject {
- cursor: pointer;
-
&:hover {
color: $fallback--cRed;
color: var(--cRed, $fallback--cRed);
@@ -119,13 +115,13 @@
}
.emoji-reaction-emoji {
- font-size: 16px;
+ font-size: 1.3em;
}
.notification-details {
- min-width: 0px;
+ min-width: 0;
word-wrap: break-word;
- line-height:18px;
+ line-height: var(--post-line-height);
position: relative;
overflow: hidden;
width: 100%;
@@ -148,7 +144,7 @@
}
.timeago {
- margin-right: .2em;
+ margin-right: 0.2em;
}
.status-content {
@@ -161,7 +157,8 @@
margin: 0 0 0.3em;
padding: 0;
font-size: 1em;
- line-height:20px;
+ line-height: 1.5;
+
small {
font-weight: lighter;
}
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index 2ce5d56f..b46c06aa 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -1,69 +1,71 @@
<template>
- <div
- :class="{ minimal: minimalMode }"
- class="Notifications"
- >
- <div :class="mainClass">
- <div
- v-if="!noHeading"
- class="panel-heading"
- >
- <div class="title">
- {{ $t('notifications.notifications') }}
- <span
- v-if="unseenCount"
- class="badge badge-notification unseen-count"
- >{{ unseenCount }}</span>
- </div>
- <button
- v-if="unseenCount"
- class="button-default read-button"
- @click.prevent="markAsSeen"
- >
- {{ $t('notifications.read') }}
- </button>
- <NotificationFilters />
- </div>
- <div class="panel-body">
+ <teleport :disabled="minimalMode || disableTeleport" :to="teleportTarget">
+ <div
+ :class="{ minimal: minimalMode }"
+ class="Notifications"
+ >
+ <div :class="mainClass">
<div
- v-for="notification in notificationsToDisplay"
- :key="notification.id"
- class="notification"
- :class="{&quot;unseen&quot;: !minimalMode && !notification.seen}"
+ v-if="!noHeading"
+ class="notifications-heading panel-heading -sticky"
>
- <div class="notification-overlay" />
- <notification :notification="notification" />
+ <div class="title">
+ {{ $t('notifications.notifications') }}
+ <span
+ v-if="unseenCount"
+ class="badge badge-notification unseen-count"
+ >{{ unseenCount }}</span>
+ </div>
+ <button
+ v-if="unseenCount"
+ class="button-default read-button"
+ @click.prevent="markAsSeen"
+ >
+ {{ $t('notifications.read') }}
+ </button>
+ <NotificationFilters />
</div>
- </div>
- <div class="panel-footer notifications-footer">
- <div
- v-if="bottomedOut"
- class="new-status-notification text-center faint"
- >
- {{ $t('notifications.no_more_notifications') }}
+ <div class="panel-body">
+ <div
+ v-for="notification in notificationsToDisplay"
+ :key="notification.id"
+ class="notification"
+ :class="{unseen: !minimalMode && !notification.seen}"
+ >
+ <div class="notification-overlay" />
+ <notification :notification="notification" />
+ </div>
</div>
- <button
- v-else-if="!loading"
- class="button-unstyled -link -fullwidth"
- @click.prevent="fetchOlderNotifications()"
- >
- <div class="new-status-notification text-center">
- {{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }}
+ <div class="panel-footer">
+ <div
+ v-if="bottomedOut"
+ class="new-status-notification text-center faint"
+ >
+ {{ $t('notifications.no_more_notifications') }}
+ </div>
+ <button
+ v-else-if="!loading"
+ class="button-unstyled -link -fullwidth"
+ @click.prevent="fetchOlderNotifications()"
+ >
+ <div class="new-status-notification text-center">
+ {{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }}
+ </div>
+ </button>
+ <div
+ v-else
+ class="new-status-notification text-center"
+ >
+ <FAIcon
+ icon="circle-notch"
+ spin
+ size="lg"
+ />
</div>
- </button>
- <div
- v-else
- class="new-status-notification text-center"
- >
- <FAIcon
- icon="circle-notch"
- spin
- size="lg"
- />
</div>
</div>
</div>
- </div>
+ </teleport>
</template>
<script src="./notifications.js"></script>
diff --git a/src/components/opacity_input/opacity_input.vue b/src/components/opacity_input/opacity_input.vue
index 3cc3942b..15d08e04 100644
--- a/src/components/opacity_input/opacity_input.vue
+++ b/src/components/opacity_input/opacity_input.vue
@@ -11,21 +11,21 @@
</label>
<Checkbox
v-if="typeof fallback !== 'undefined'"
- :checked="present"
+ :model-value="present"
:disabled="disabled"
class="opt"
- @change="$emit('input', !present ? fallback : undefined)"
+ @update:modelValue="$emit('update:modelValue', !present ? fallback : undefined)"
/>
<input
:id="name"
class="input-number"
type="number"
- :value="value || fallback"
+ :value="modelValue || fallback"
:disabled="!present || disabled"
max="1"
min="0"
step=".05"
- @input="$emit('input', $event.target.value)"
+ @input="$emit('update:modelValue', $event.target.value)"
>
</div>
</template>
@@ -37,11 +37,12 @@ export default {
Checkbox
},
props: [
- 'name', 'value', 'fallback', 'disabled'
+ 'name', 'modelValue', 'fallback', 'disabled'
],
+ emits: ['update:modelValue'],
computed: {
present () {
- return typeof this.value !== 'undefined'
+ return typeof this.modelValue !== 'undefined'
}
}
}
diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue
index 3ffa5425..90673f44 100644
--- a/src/components/password_reset/password_reset.vue
+++ b/src/components/password_reset/password_reset.vue
@@ -91,14 +91,18 @@
flex-direction: column;
margin-top: 0.6em;
max-width: 18rem;
+
+ > * {
+ min-width: 0;
+ }
}
.form-group {
display: flex;
flex-direction: column;
margin-bottom: 1em;
- padding: 0.3em 0.0em 0.3em;
- line-height: 24px;
+ padding: 0.3em 0;
+ line-height: 1.85em;
}
.error {
@@ -110,7 +114,7 @@
.alert {
padding: 0.5em;
- margin: 0.3em 0.0em 1em;
+ margin: 0.3em 0 1em;
}
.password-reset-required {
diff --git a/src/components/poll/poll.js b/src/components/poll/poll.js
index a69b7886..eda1733a 100644
--- a/src/components/poll/poll.js
+++ b/src/components/poll/poll.js
@@ -21,7 +21,7 @@ export default {
}
this.$store.dispatch('trackPoll', this.pollId)
},
- destroyed () {
+ unmounted () {
this.$store.dispatch('untrackPoll', this.pollId)
},
computed: {
diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue
index 63b44e4f..f6b12a54 100644
--- a/src/components/poll/poll.vue
+++ b/src/components/poll/poll.vue
@@ -71,13 +71,18 @@
{{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }}&nbsp;·&nbsp;
</template>
</div>
- <i18n :path="expired ? 'polls.expired' : 'polls.expires_in'">
- <Timeago
- :time="expiresAt"
- :auto-update="60"
- :now-threshold="0"
- />
- </i18n>
+ <span>
+ <i18n-t
+ scope="global"
+ :keypath="expired ? 'polls.expired' : 'polls.expires_in'"
+ >
+ <Timeago
+ :time="expiresAt"
+ :auto-update="60"
+ :now-threshold="0"
+ />
+ </i18n-t>
+ </span>
</div>
</div>
</template>
diff --git a/src/components/poll/poll_form.vue b/src/components/poll/poll_form.vue
index 3620075a..f269d60e 100644
--- a/src/components/poll/poll_form.vue
+++ b/src/components/poll/poll_form.vue
@@ -72,6 +72,7 @@
:max="maxExpirationInCurrentUnit"
@change="expiryAmountChange"
>
+ {{ ' ' }}
<Select
v-model="expiryUnit"
unstyled="true"
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index 6ccf32f0..a30a37c9 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -178,7 +178,7 @@ const Popover = {
created () {
document.addEventListener('click', this.onClickOutside)
},
- destroyed () {
+ unmounted () {
document.removeEventListener('click', this.onClickOutside)
this.hidePopover()
}
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
index 8588b351..c2a3e801 100644
--- a/src/components/popover/popover.vue
+++ b/src/components/popover/popover.vue
@@ -5,7 +5,7 @@
>
<button
ref="trigger"
- class="button-unstyled -fullwidth popover-trigger-button"
+ class="button-unstyled popover-trigger-button"
type="button"
@click="onClick"
>
@@ -37,7 +37,7 @@
}
.popover {
- z-index: 8;
+ z-index: 500;
position: absolute;
min-width: 0;
}
@@ -45,8 +45,19 @@
.popover-default {
transition: opacity 0.3s;
- box-shadow: 1px 1px 4px rgba(0,0,0,.6);
- box-shadow: var(--panelShadow);
+ &:after {
+ content: '';
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 3;
+ box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
+ box-shadow: var(--panelShadow);
+ pointer-events: none;
+ }
+
border-radius: $fallback--btnRadius;
border-radius: var(--btnRadius, $fallback--btnRadius);
@@ -65,11 +76,11 @@
.dropdown-menu {
display: block;
padding: .5rem 0;
- font-size: 1rem;
+ font-size: 1em;
text-align: left;
list-style: none;
max-width: 100vw;
- z-index: 10;
+ z-index: 200;
white-space: nowrap;
.dropdown-divider {
@@ -82,9 +93,9 @@
.dropdown-item {
line-height: 21px;
- overflow: auto;
+ overflow: hidden;
display: block;
- padding: .5em 0.75em;
+ padding: 0.5em 0.75em;
clear: both;
font-weight: 400;
text-align: inherit;
@@ -110,14 +121,15 @@
&:active, &:hover {
background-color: $fallback--lightBg;
background-color: var(--selectedMenuPopover, $fallback--lightBg);
- color: $fallback--link;
- color: var(--selectedMenuPopoverText, $fallback--link);
+ box-shadow: none;
+ --btnText: var(--selectedMenuPopoverText, $fallback--link);
--faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
svg {
color: var(--selectedMenuPopoverIcon, $fallback--icon);
+ --icon: var(--selectedMenuPopoverIcon, $fallback--icon);
}
}
@@ -142,12 +154,41 @@
content: '✓';
}
- &.menu-checkbox-radio::after {
- font-size: 2em;
- content: '•';
+ &.-radio {
+ border-radius: 9999px;
+
+ &.menu-checkbox-checked::after {
+ font-size: 2em;
+ content: '•';
+ }
}
}
}
+
+ .button-default.dropdown-item {
+ &,
+ i[class*=icon-] {
+ color: $fallback--text;
+ color: var(--btnText, $fallback--text);
+ }
+
+ &:active {
+ background-color: $fallback--lightBg;
+ background-color: var(--selectedMenuPopover, $fallback--lightBg);
+ color: $fallback--link;
+ color: var(--selectedMenuPopoverText, $fallback--link);
+ }
+
+ &:disabled {
+ color: $fallback--text;
+ color: var(--btnDisabledText, $fallback--text);
+ }
+
+ &.toggled {
+ color: $fallback--text;
+ color: var(--btnToggledText, $fallback--text);
+ }
+ }
}
</style>
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index fe07309f..2febf226 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -78,6 +78,12 @@ const PostStatusForm = {
'emojiPickerPlacement',
'optimisticPosting'
],
+ emits: [
+ 'posted',
+ 'resize',
+ 'mediaplay',
+ 'mediapause'
+ ],
components: {
MediaUpload,
EmojiInput,
@@ -480,7 +486,7 @@ const PostStatusForm = {
const bottomBottomPaddingStr = window.getComputedStyle(bottomRef)['padding-bottom']
const bottomBottomPadding = pxStringToNumber(bottomBottomPaddingStr)
- const scrollerRef = this.$el.closest('.sidebar-scroller') ||
+ const scrollerRef = this.$el.closest('.column.-scrollable') ||
this.$el.closest('.post-form-modal-view') ||
window
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 2e0980a2..62613bd1 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -8,21 +8,13 @@
@submit.prevent
@dragover.prevent="fileDrag"
>
- <div
- v-show="showDropIcon !== 'hide'"
- :style="{ animation: showDropIcon === 'show' ? 'fade-in 0.25s' : 'fade-out 0.5s' }"
- class="drop-indicator"
- @dragleave="fileDragStop"
- @drop.stop="fileDrop"
- >
- <FAIcon :icon="uploadFileLimitReached ? 'ban' : 'upload'" />
- </div>
<div class="form-group">
- <i18n
+ <i18n-t
v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private' && !disableLockWarning"
- path="post_status.account_not_locked_warning"
+ keypath="post_status.account_not_locked_warning"
tag="p"
class="visibility-notice"
+ scope="global"
>
<button
class="button-unstyled -link"
@@ -30,7 +22,7 @@
>
{{ $t('post_status.account_not_locked_warning_link') }}
</button>
- </i18n>
+ </i18n-t>
<p
v-if="!hideScopeNotice && newStatus.visibility === 'public'"
class="visibility-notice notice-dismissible"
@@ -277,15 +269,28 @@
</button>
</div>
<div
+ v-show="showDropIcon !== 'hide'"
+ :style="{ animation: showDropIcon === 'show' ? 'fade-in 0.25s' : 'fade-out 0.5s' }"
+ class="drop-indicator"
+ @dragleave="fileDragStop"
+ @drop.stop="fileDrop"
+ >
+ <FAIcon :icon="uploadFileLimitReached ? 'ban' : 'upload'" />
+ </div>
+ <div
v-if="error"
class="alert error"
>
Error: {{ error }}
- <FAIcon
- class="fa-scale-110 fa-old-padding"
- icon="times"
+ <button
+ class="button-unstyled"
@click="clearError"
- />
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
+ />
+ </button>
</div>
<gallery
v-if="newStatus.files && newStatus.files.length > 0"
@@ -331,7 +336,7 @@
display: flex;
justify-content: space-between;
padding: 0.5em;
- height: 32px;
+ height: 2.5em;
button {
width: 10em;
@@ -389,7 +394,6 @@
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
padding: 0.5em;
margin: 0;
- line-height: 1.4em;
}
.text-format {
@@ -403,13 +407,16 @@
display: flex;
justify-content: space-between;
padding-top: 5px;
+ align-items: baseline;
}
.media-upload-icon, .poll-icon, .emoji-icon {
- font-size: 26px;
+ font-size: 1.85em;
line-height: 1.1;
flex: 1;
padding: 0 0.1em;
+ display: flex;
+ align-items: center;
&.selected, &:hover {
// needs to be specific to override icon default color
@@ -436,21 +443,17 @@
// Order is not necessary but a good indicator
.media-upload-icon {
order: 1;
- text-align: left;
+ justify-content: left;
}
.emoji-icon {
order: 2;
- text-align: center;
+ justify-content: center;
}
.poll-icon {
order: 3;
- text-align: right;
- }
-
- .poll-icon {
- cursor: pointer;
+ justify-content: right;
}
.error {
@@ -484,10 +487,6 @@
flex-direction: column;
}
- .btn {
- cursor: pointer;
- }
-
.btn[disabled] {
cursor: not-allowed;
}
@@ -503,26 +502,20 @@
display: flex;
flex-direction: column;
padding: 0.25em 0.5em 0.5em;
- line-height:24px;
- }
-
- form textarea.form-cw {
- line-height:16px;
- resize: none;
- overflow: hidden;
- transition: min-height 200ms 100ms;
- min-height: 1px;
+ line-height: 1.85;
}
.form-post-body {
- height: 16px; // Only affects the empty-height
- line-height: 16px;
- resize: none;
+ // TODO: make a resizable textarea component?
+ box-sizing: content-box; // needed for easier computation of dynamic size
overflow: hidden;
transition: min-height 200ms 100ms;
- padding-bottom: 1.75em;
- min-height: 1px;
- box-sizing: content-box;
+ // stock padding + 1 line of text (for counter)
+ padding-bottom: calc(var(--_padding) + var(--post-line-height) * 1em);
+ // two lines of text
+ height: calc(var(--post-line-height) * 1em);
+ min-height: calc(var(--post-line-height) * 1em);
+ resize: none;
&.scrollable-form {
overflow-y: auto;
@@ -546,10 +539,6 @@
}
}
- .btn {
- cursor: pointer;
- }
-
.btn[disabled] {
cursor: not-allowed;
}
@@ -566,7 +555,6 @@
.drop-indicator {
position: absolute;
- z-index: 1;
width: 100%;
height: 100%;
font-size: 5em;
diff --git a/src/components/public_and_external_timeline/public_and_external_timeline.js b/src/components/public_and_external_timeline/public_and_external_timeline.js
index cbd4491b..bfcce6ae 100644
--- a/src/components/public_and_external_timeline/public_and_external_timeline.js
+++ b/src/components/public_and_external_timeline/public_and_external_timeline.js
@@ -9,7 +9,7 @@ const PublicAndExternalTimeline = {
created () {
this.$store.dispatch('startFetchingTimeline', { timeline: 'publicAndExternal' })
},
- destroyed () {
+ unmounted () {
this.$store.dispatch('stopFetchingTimeline', 'publicAndExternal')
}
}
diff --git a/src/components/public_timeline/public_timeline.js b/src/components/public_timeline/public_timeline.js
index 66c40d3a..30693544 100644
--- a/src/components/public_timeline/public_timeline.js
+++ b/src/components/public_timeline/public_timeline.js
@@ -9,7 +9,7 @@ const PublicTimeline = {
created () {
this.$store.dispatch('startFetchingTimeline', { timeline: 'public' })
},
- destroyed () {
+ unmounted () {
this.$store.dispatch('stopFetchingTimeline', 'public')
}
diff --git a/src/components/range_input/range_input.vue b/src/components/range_input/range_input.vue
index 5857a5c1..1e7e42d5 100644
--- a/src/components/range_input/range_input.vue
+++ b/src/components/range_input/range_input.vue
@@ -15,7 +15,7 @@
class="opt"
type="checkbox"
:checked="present"
- @input="$emit('input', !present ? fallback : undefined)"
+ @change="$emit('update:modelValue', !present ? fallback : undefined)"
>
<label
v-if="typeof fallback !== 'undefined'"
@@ -26,23 +26,23 @@
:id="name"
class="input-number"
type="range"
- :value="value || fallback"
+ :value="modelValue || fallback"
:disabled="!present || disabled"
:max="max || hardMax || 100"
:min="min || hardMin || 0"
:step="step || 1"
- @input="$emit('input', $event.target.value)"
+ @input="$emit('update:modelValue', $event.target.value)"
>
<input
:id="name"
class="input-number"
type="number"
- :value="value || fallback"
+ :value="modelValue || fallback"
:disabled="!present || disabled"
:max="hardMax"
:min="hardMin"
:step="step || 1"
- @input="$emit('input', $event.target.value)"
+ @input="$emit('update:modelValue', $event.target.value)"
>
</div>
</template>
@@ -50,11 +50,12 @@
<script>
export default {
props: [
- 'name', 'value', 'fallback', 'disabled', 'label', 'max', 'min', 'step', 'hardMin', 'hardMax'
+ 'name', 'modelValue', 'fallback', 'disabled', 'label', 'max', 'min', 'step', 'hardMin', 'hardMax'
],
+ emits: ['update:modelValue'],
computed: {
present () {
- return typeof this.value !== 'undefined'
+ return typeof this.modelValue !== 'undefined'
}
}
}
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
index ce82c90d..e6f9dbff 100644
--- a/src/components/react_button/react_button.js
+++ b/src/components/react_button/react_button.js
@@ -1,6 +1,7 @@
import Popover from '../popover/popover.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
+import { trim } from 'lodash'
library.add(faSmileBeam)
@@ -43,7 +44,7 @@ const ReactButton = {
},
emojis () {
if (this.filterWord !== '') {
- const filterWordLowercase = this.filterWord.toLowerCase()
+ const filterWordLowercase = trim(this.filterWord.toLowerCase())
let orderedEmojiList = []
for (const emoji of this.$store.state.instance.emoji) {
if (emoji.replacement === this.filterWord) return [emoji]
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
index c69c315b..8a4b4d3b 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"
+ @input="$event.target.composing = false"
size="1"
:placeholder="$t('emoji.search_emoji')"
>
@@ -101,7 +102,7 @@
cursor: pointer;
flex-basis: 20%;
- line-height: 1.5em;
+ line-height: 1.5;
align-content: center;
&:hover {
diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index 1ac8e8be..6eb316d0 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -1,9 +1,11 @@
-import { validationMixin } from 'vuelidate'
-import { required, requiredIf, sameAs } from 'vuelidate/lib/validators'
+import useVuelidate from '@vuelidate/core'
+import { required, requiredIf, sameAs } from '@vuelidate/validators'
import { mapActions, mapState } from 'vuex'
+import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
+import localeService from '../../services/locale/locale.service.js'
const registration = {
- mixins: [validationMixin],
+ setup () { return { v$: useVuelidate() } },
data: () => ({
user: {
email: '',
@@ -11,10 +13,14 @@ const registration = {
username: '',
password: '',
confirm: '',
- reason: ''
+ reason: '',
+ language: ''
},
captcha: {}
}),
+ components: {
+ InterfaceLanguageSwitcher
+ },
validations () {
return {
user: {
@@ -24,9 +30,10 @@ const registration = {
password: { required },
confirm: {
required,
- sameAsPassword: sameAs('password')
+ sameAs: sameAs(this.user.password)
},
- reason: { required: requiredIf(() => this.accountApprovalRequired) }
+ reason: { required: requiredIf(() => this.accountApprovalRequired) },
+ language: {}
}
}
},
@@ -64,10 +71,13 @@ const registration = {
this.user.captcha_solution = this.captcha.solution
this.user.captcha_token = this.captcha.token
this.user.captcha_answer_data = this.captcha.answer_data
+ if (this.user.language) {
+ this.user.language = localeService.internalToBackendLocale(this.user.language)
+ }
- this.$v.$touch()
+ this.v$.$touch()
- if (!this.$v.$invalid) {
+ if (!this.v$.$invalid) {
try {
await this.signUp(this.user)
this.$router.push({ name: 'friends' })
diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue
index 65b4bb33..cc655c0b 100644
--- a/src/components/registration/registration.vue
+++ b/src/components/registration/registration.vue
@@ -12,7 +12,7 @@
<div class="text-fields">
<div
class="form-group"
- :class="{ 'form-group--error': $v.user.username.$error }"
+ :class="{ 'form-group--error': v$.user.username.$error }"
>
<label
class="form--label"
@@ -20,18 +20,18 @@
>{{ $t('login.username') }}</label>
<input
id="sign-up-username"
- v-model.trim="$v.user.username.$model"
+ v-model.trim="v$.user.username.$model"
:disabled="isPending"
class="form-control"
:placeholder="$t('registration.username_placeholder')"
>
</div>
<div
- v-if="$v.user.username.$dirty"
+ v-if="v$.user.username.$dirty"
class="form-error"
>
<ul>
- <li v-if="!$v.user.username.required">
+ <li v-if="!v$.user.username.required">
<span>{{ $t('registration.validations.username_required') }}</span>
</li>
</ul>
@@ -39,7 +39,7 @@
<div
class="form-group"
- :class="{ 'form-group--error': $v.user.fullname.$error }"
+ :class="{ 'form-group--error': v$.user.fullname.$error }"
>
<label
class="form--label"
@@ -47,18 +47,18 @@
>{{ $t('registration.fullname') }}</label>
<input
id="sign-up-fullname"
- v-model.trim="$v.user.fullname.$model"
+ v-model.trim="v$.user.fullname.$model"
:disabled="isPending"
class="form-control"
:placeholder="$t('registration.fullname_placeholder')"
>
</div>
<div
- v-if="$v.user.fullname.$dirty"
+ v-if="v$.user.fullname.$dirty"
class="form-error"
>
<ul>
- <li v-if="!$v.user.fullname.required">
+ <li v-if="!v$.user.fullname.required">
<span>{{ $t('registration.validations.fullname_required') }}</span>
</li>
</ul>
@@ -66,7 +66,7 @@
<div
class="form-group"
- :class="{ 'form-group--error': $v.user.email.$error }"
+ :class="{ 'form-group--error': v$.user.email.$error }"
>
<label
class="form--label"
@@ -74,18 +74,18 @@
>{{ $t('registration.email') }}</label>
<input
id="email"
- v-model="$v.user.email.$model"
+ v-model="v$.user.email.$model"
:disabled="isPending"
class="form-control"
type="email"
>
</div>
<div
- v-if="$v.user.email.$dirty"
+ v-if="v$.user.email.$dirty"
class="form-error"
>
<ul>
- <li v-if="!$v.user.email.required">
+ <li v-if="!v$.user.email.required">
<span>{{ $t('registration.validations.email_required') }}</span>
</li>
</ul>
@@ -107,7 +107,7 @@
<div
class="form-group"
- :class="{ 'form-group--error': $v.user.password.$error }"
+ :class="{ 'form-group--error': v$.user.password.$error }"
>
<label
class="form--label"
@@ -122,11 +122,11 @@
>
</div>
<div
- v-if="$v.user.password.$dirty"
+ v-if="v$.user.password.$dirty"
class="form-error"
>
<ul>
- <li v-if="!$v.user.password.required">
+ <li v-if="!v$.user.password.required">
<span>{{ $t('registration.validations.password_required') }}</span>
</li>
</ul>
@@ -134,7 +134,7 @@
<div
class="form-group"
- :class="{ 'form-group--error': $v.user.confirm.$error }"
+ :class="{ 'form-group--error': v$.user.confirm.$error }"
>
<label
class="form--label"
@@ -149,20 +149,32 @@
>
</div>
<div
- v-if="$v.user.confirm.$dirty"
+ v-if="v$.user.confirm.$dirty"
class="form-error"
>
<ul>
- <li v-if="!$v.user.confirm.required">
+ <li v-if="!v$.user.confirm.required">
<span>{{ $t('registration.validations.password_confirmation_required') }}</span>
</li>
- <li v-if="!$v.user.confirm.sameAsPassword">
+ <li v-if="!v$.user.confirm.sameAsPassword">
<span>{{ $t('registration.validations.password_confirmation_match') }}</span>
</li>
</ul>
</div>
<div
+ class="form-group"
+ :class="{ 'form-group--error': v$.user.language.$error }"
+ >
+ <interface-language-switcher
+ for="email-language"
+ :prompt-text="$t('registration.email_language')"
+ :language="v$.user.language.$model"
+ :set-language="val => v$.user.language.$model = val"
+ />
+ </div>
+
+ <div
v-if="accountApprovalRequired"
class="form-group"
>
@@ -271,7 +283,10 @@ $validations-cRed: #f04124;
.container {
display: flex;
flex-direction: row;
- //margin-bottom: 1em;
+
+ > * {
+ min-width: 0;
+ }
}
.terms-of-service {
@@ -294,8 +309,8 @@ $validations-cRed: #f04124;
.form-group {
display: flex;
flex-direction: column;
- padding: 0.3em 0.0em 0.3em;
- line-height:24px;
+ padding: 0.3em 0;
+ line-height: 2;
margin-bottom: 1em;
}
@@ -315,7 +330,7 @@ $validations-cRed: #f04124;
text-align: left;
span {
- font-size: 12px;
+ font-size: 0.85em;
}
}
@@ -341,7 +356,7 @@ $validations-cRed: #f04124;
.btn {
margin-top: 0.6em;
- height: 28px;
+ height: 2em;
}
.error {
diff --git a/src/components/remote_follow/remote_follow.vue b/src/components/remote_follow/remote_follow.vue
index be827400..e17aa2e9 100644
--- a/src/components/remote_follow/remote_follow.vue
+++ b/src/components/remote_follow/remote_follow.vue
@@ -32,7 +32,7 @@
.remote-button {
width: 100%;
- min-height: 28px;
+ min-height: 2em;
}
}
</style>
diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx
index 46bc661a..ca075270 100644
--- a/src/components/rich_content/rich_content.jsx
+++ b/src/components/rich_content/rich_content.jsx
@@ -1,4 +1,3 @@
-import Vue from 'vue'
import { unescape, flattenDeep } from 'lodash'
import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_converter/utility.service.js'
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
@@ -27,8 +26,12 @@ import './rich_content.scss'
*
* Apart from that one small hiccup with emit in render this _should_ be vue3-ready
*/
-export default Vue.component('RichContent', {
+export default {
name: 'RichContent',
+ components: {
+ MentionsLine,
+ HashtagLink
+ },
props: {
// Original html content
html: {
@@ -58,7 +61,7 @@ export default Vue.component('RichContent', {
}
},
// NEVER EVER TOUCH DATA INSIDE RENDER
- render (h) {
+ render () {
// Pre-process HTML
const { newHtml: html } = preProcessPerLine(this.html, this.greentext)
let currentMentions = null // Current chain of mentions, we group all mentions together
@@ -76,18 +79,19 @@ export default Vue.component('RichContent', {
const renderImage = (tag) => {
return <StillImage
- {...{ attrs: getAttrs(tag) }}
+ {...getAttrs(tag)}
class="img"
/>
}
const renderHashtag = (attrs, children, encounteredTextReverse) => {
- const linkData = getLinkData(attrs, children, tagsIndex++)
+ const { index, ...linkData } = getLinkData(attrs, children, tagsIndex++)
writtenTags.push(linkData)
if (!encounteredTextReverse) {
lastTags.push(linkData)
}
- return <HashtagLink {...{ props: linkData }}/>
+ const { url, tag, content } = linkData
+ return <HashtagLink url={url} tag={tag} content={content}/>
}
const renderMention = (attrs, children) => {
@@ -222,7 +226,7 @@ export default Vue.component('RichContent', {
attrs.target = '_blank'
const newChildren = [...children].reverse().map(processItemReverse).reverse()
- return <a {...{ attrs }}>
+ return <a {...attrs}>
{ newChildren }
</a>
}
@@ -235,7 +239,7 @@ export default Vue.component('RichContent', {
const newChildren = Array.isArray(children)
? [...children].reverse().map(processItemReverse).reverse()
: children
- return <Tag {...{ attrs: getAttrs(opener) }}>
+ return <Tag {...getAttrs(opener)}>
{ newChildren }
</Tag>
} else {
@@ -266,7 +270,7 @@ export default Vue.component('RichContent', {
return result
}
-})
+}
const getLinkData = (attrs, children, index) => {
const stripTags = (item) => {
diff --git a/src/components/scope_selector/scope_selector.vue b/src/components/scope_selector/scope_selector.vue
index a01242fc..f3bee183 100644
--- a/src/components/scope_selector/scope_selector.vue
+++ b/src/components/scope_selector/scope_selector.vue
@@ -16,6 +16,7 @@
class="fa-scale-110 fa-old-padding"
/>
</button>
+ {{ ' ' }}
<button
v-if="showPrivate"
class="button-unstyled scope"
@@ -29,6 +30,7 @@
class="fa-scale-110 fa-old-padding"
/>
</button>
+ {{ ' ' }}
<button
v-if="showUnlisted"
class="button-unstyled scope"
@@ -42,6 +44,7 @@
class="fa-scale-110 fa-old-padding"
/>
</button>
+ {{ ' ' }}
<button
v-if="showPublic"
class="button-unstyled scope"
diff --git a/src/components/search/search.js b/src/components/search/search.js
index b62bc2c5..76ac30ef 100644
--- a/src/components/search/search.js
+++ b/src/components/search/search.js
@@ -1,6 +1,7 @@
import FollowCard from '../follow_card/follow_card.vue'
import Conversation from '../conversation/conversation.vue'
import Status from '../status/status.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import map from 'lodash/map'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@@ -17,7 +18,8 @@ const Search = {
components: {
FollowCard,
Conversation,
- Status
+ Status,
+ TabSwitcher
},
props: [
'query'
diff --git a/src/components/select/select.js b/src/components/select/select.js
index 49535d07..ec571a14 100644
--- a/src/components/select/select.js
+++ b/src/components/select/select.js
@@ -8,12 +8,9 @@ library.add(
)
export default {
- model: {
- prop: 'value',
- event: 'change'
- },
+ emits: ['update:modelValue'],
props: [
- 'value',
+ 'modelValue',
'disabled',
'unstyled',
'kind'
diff --git a/src/components/select/select.vue b/src/components/select/select.vue
index 8d6528ff..92493b0b 100644
--- a/src/components/select/select.vue
+++ b/src/components/select/select.vue
@@ -1,4 +1,3 @@
-
<template>
<label
class="Select input"
@@ -6,11 +5,12 @@
>
<select
:disabled="disabled"
- :value="value"
- @change="$emit('change', $event.target.value)"
+ :value="modelValue"
+ @change="$emit('update:modelValue', $event.target.value)"
>
<slot />
</select>
+ {{ ' ' }}
<FAIcon
class="select-down-icon"
icon="chevron-down"
@@ -23,7 +23,8 @@
<style lang="scss">
@import '../../_variables.scss';
-.Select {
+/* TODO fix order of styles */
+label.Select {
padding: 0;
select {
@@ -38,10 +39,10 @@
padding: 0 2em 0 .2em;
font-family: sans-serif;
font-family: var(--inputFont, sans-serif);
- font-size: 14px;
+ font-size: 1em;
width: 100%;
z-index: 1;
- height: 28px;
+ height: 2em;
line-height: 16px;
}
@@ -54,7 +55,7 @@
width: 0.875em;
color: $fallback--text;
color: var(--inputText, $fallback--text);
- line-height: 28px;
+ line-height: 2;
z-index: 0;
pointer-events: none;
}
diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue
index 3f885881..3c80660e 100644
--- a/src/components/selectable_list/selectable_list.vue
+++ b/src/components/selectable_list/selectable_list.vue
@@ -6,9 +6,9 @@
>
<div class="selectable-list-checkbox-wrapper">
<Checkbox
- :checked="allSelected"
+ :model-value="allSelected"
:indeterminate="someSelected"
- @change="toggleAll"
+ @update:model-value="toggleAll"
>
{{ $t('selectable_list.select_all') }}
</Checkbox>
@@ -31,8 +31,8 @@
>
<div class="selectable-list-checkbox-wrapper">
<Checkbox
- :checked="isSelected(item)"
- @change="checked => toggle(checked, item)"
+ :model-value="isSelected(item)"
+ @update:model-value="checked => toggle(checked, item)"
/>
</div>
<slot
diff --git a/src/components/settings_modal/helpers/boolean_setting.vue b/src/components/settings_modal/helpers/boolean_setting.vue
index e0d825f2..69584808 100644
--- a/src/components/settings_modal/helpers/boolean_setting.vue
+++ b/src/components/settings_modal/helpers/boolean_setting.vue
@@ -4,9 +4,9 @@
class="BooleanSetting"
>
<Checkbox
- :checked="state"
+ :model-value="state"
:disabled="disabled"
- @change="update"
+ @update:modelValue="update"
>
<span
v-if="!!$slots.default"
@@ -14,6 +14,7 @@
>
<slot />
</span>
+ {{ ' ' }}
<ModifiedIndicator :changed="isChanged" /><ServerSideIndicator :server-side="isServerSide" /> </Checkbox>
</label>
</template>
diff --git a/src/components/settings_modal/helpers/choice_setting.vue b/src/components/settings_modal/helpers/choice_setting.vue
index 54f5d0a7..258c7422 100644
--- a/src/components/settings_modal/helpers/choice_setting.vue
+++ b/src/components/settings_modal/helpers/choice_setting.vue
@@ -4,10 +4,11 @@
class="ChoiceSetting"
>
<slot />
+ {{ ' ' }}
<Select
- :value="state"
+ :model-value="state"
:disabled="disabled"
- @change="update"
+ @update:modelValue="update"
>
<option
v-for="option in options"
diff --git a/src/components/settings_modal/helpers/integer_setting.js b/src/components/settings_modal/helpers/integer_setting.js
index 4a19bd7c..17dc0e7b 100644
--- a/src/components/settings_modal/helpers/integer_setting.js
+++ b/src/components/settings_modal/helpers/integer_setting.js
@@ -8,7 +8,7 @@ export default {
path: String,
disabled: Boolean,
min: Number,
- expert: Number
+ expert: [Number, String]
},
computed: {
pathDefault () {
diff --git a/src/components/settings_modal/helpers/integer_setting.vue b/src/components/settings_modal/helpers/integer_setting.vue
index 408b0925..e661a025 100644
--- a/src/components/settings_modal/helpers/integer_setting.vue
+++ b/src/components/settings_modal/helpers/integer_setting.vue
@@ -16,6 +16,7 @@
:value="state"
@change="update"
>
+ {{ ' ' }}
<ModifiedIndicator :changed="isChanged" />
</span>
</template>
diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js
index 82ea410e..0a72dca1 100644
--- a/src/components/settings_modal/settings_modal.js
+++ b/src/components/settings_modal/settings_modal.js
@@ -56,8 +56,8 @@ const SettingsModal = {
SettingsModalContent: getResettableAsyncComponent(
() => import('./settings_modal_content.vue'),
{
- loading: PanelLoading,
- error: AsyncComponentError,
+ loadingComponent: PanelLoading,
+ errorComponent: AsyncComponentError,
delay: 0
}
)
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
index fb466f2f..13cb0e65 100644
--- a/src/components/settings_modal/settings_modal.scss
+++ b/src/components/settings_modal/settings_modal.scss
@@ -2,6 +2,18 @@
.settings-modal {
overflow: hidden;
+ .setting-list,
+ .option-list {
+ list-style-type: none;
+ padding-left: 2em;
+ li {
+ margin-bottom: 0.5em;
+ }
+ .suboptions {
+ margin-top: 0.3em
+ }
+ }
+
&.peek {
.settings-modal-panel {
/* Explanation:
@@ -42,7 +54,7 @@
overflow-y: hidden;
.btn {
- min-height: 28px;
+ min-height: 2em;
min-width: 10em;
padding: 0 2em;
}
@@ -54,5 +66,10 @@
>* {
margin-right: 0.5em;
}
+
+ .extra-content {
+ display: flex;
+ flex-grow: 1;
+ }
}
}
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
index 1805c77f..d3bed061 100644
--- a/src/components/settings_modal/settings_modal.vue
+++ b/src/components/settings_modal/settings_modal.vue
@@ -11,23 +11,14 @@
{{ $t('settings.settings') }}
</span>
<transition name="fade">
- <template v-if="currentSaveStateNotice">
- <div
- v-if="currentSaveStateNotice.error"
- class="alert error"
- @click.prevent
- >
- {{ $t('settings.saving_err') }}
- </div>
-
- <div
- v-if="!currentSaveStateNotice.error"
- class="alert transparent"
- @click.prevent
- >
- {{ $t('settings.saving_ok') }}
- </div>
- </template>
+ <div
+ v-if="currentSaveStateNotice"
+ class="alert"
+ :class="{ transparent: !currentSaveStateNotice.error, error: currentSaveStateNotice.error}"
+ @click.prevent
+ >
+ {{ currentSaveStateNotice.error ? $t('settings.saving_err') : $t('settings.saving_ok') }}
+ </div>
</transition>
<button
class="btn button-default"
@@ -68,6 +59,7 @@
:title="$t('general.close')"
>
<span>{{ $t("settings.file_export_import.backup_restore") }}</span>
+ {{ ' ' }}
<FAIcon
icon="chevron-down"
/>
@@ -109,9 +101,16 @@
</template>
</Popover>
- <Checkbox v-model="expertLevel">
+ <Checkbox
+ :model-value="!!expertLevel"
+ @update:modelValue="expertLevel = Number($event)"
+ >
{{ $t("settings.expert_mode") }}
</Checkbox>
+ <span
+ id="unscrolled-content"
+ class="extra-content"
+ />
</div>
</div>
</Modal>
diff --git a/src/components/settings_modal/settings_modal_content.js b/src/components/settings_modal/settings_modal_content.js
index 9dcf1b5a..9ac0301f 100644
--- a/src/components/settings_modal/settings_modal_content.js
+++ b/src/components/settings_modal/settings_modal_content.js
@@ -1,4 +1,4 @@
-import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import DataImportExportTab from './tabs/data_import_export_tab.vue'
import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'
@@ -53,6 +53,9 @@ const SettingsModalContent = {
},
open () {
return this.$store.state.interface.settingsModalState !== 'hidden'
+ },
+ bodyLock () {
+ return this.$store.state.interface.settingsModalState === 'visible'
}
},
methods: {
@@ -60,8 +63,8 @@ const SettingsModalContent = {
const targetTab = this.$store.state.interface.settingsModalTargetTab
// We're being told to open in specific tab
if (targetTab) {
- const tabIndex = this.$refs.tabSwitcher.$slots.default.findIndex(elm => {
- return elm.data && elm.data.attrs['data-tab-name'] === targetTab
+ const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => {
+ return elm.props && elm.props['data-tab-name'] === targetTab
})
if (tabIndex >= 0) {
this.$refs.tabSwitcher.setTab(tabIndex)
diff --git a/src/components/settings_modal/settings_modal_content.vue b/src/components/settings_modal/settings_modal_content.vue
index c9ed2a38..0be76d22 100644
--- a/src/components/settings_modal/settings_modal_content.vue
+++ b/src/components/settings_modal/settings_modal_content.vue
@@ -4,6 +4,7 @@
class="settings_tab-switcher"
:side-tab-bar="true"
:scrollable-tabs="true"
+ :body-scroll-lock="bodyLock"
>
<div
:label="$t('settings.general')"
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 f4b736d2..4895733c 100644
--- a/src/components/settings_modal/tabs/data_import_export_tab.js
+++ b/src/components/settings_modal/tabs/data_import_export_tab.js
@@ -7,11 +7,16 @@ const DataImportExportTab = {
data () {
return {
activeTab: 'profile',
- newDomainToMute: ''
+ newDomainToMute: '',
+ listBackupsError: false,
+ addBackupError: false,
+ addedBackup: false,
+ backups: []
}
},
created () {
this.$store.dispatch('fetchTokens')
+ this.fetchBackups()
},
components: {
Importer,
@@ -72,6 +77,28 @@ const DataImportExportTab = {
}
return user.screen_name
}).join('\n')
+ },
+ addBackup () {
+ this.$store.state.api.backendInteractor.addBackup()
+ .then((res) => {
+ this.addedBackup = true
+ this.addBackupError = false
+ })
+ .catch((error) => {
+ this.addedBackup = false
+ this.addBackupError = error
+ })
+ .then(() => this.fetchBackups())
+ },
+ fetchBackups () {
+ this.$store.state.api.backendInteractor.listBackups()
+ .then((res) => {
+ this.backups = res
+ this.listBackupsError = false
+ })
+ .catch((error) => {
+ this.listBackupsError = error.error
+ })
}
}
}
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 a406077d..e3b7f407 100644
--- a/src/components/settings_modal/tabs/data_import_export_tab.vue
+++ b/src/components/settings_modal/tabs/data_import_export_tab.vue
@@ -53,6 +53,67 @@
:export-button-label="$t('settings.mute_export_button')"
/>
</div>
+ <div class="setting-item">
+ <h2>{{ $t('settings.account_backup') }}</h2>
+ <p>{{ $t('settings.account_backup_description') }}</p>
+ <table>
+ <thead>
+ <tr>
+ <th>{{ $t('settings.account_backup_table_head') }}</th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ <tr
+ v-for="backup in backups"
+ :key="backup.id"
+ >
+ <td>{{ backup.inserted_at }}</td>
+ <td class="actions">
+ <a
+ v-if="backup.processed"
+ target="_blank"
+ :href="backup.url"
+ >
+ {{ $t('settings.download_backup') }}
+ </a>
+ <span
+ v-else
+ >
+ {{ $t('settings.backup_not_ready') }}
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <div
+ v-if="listBackupsError"
+ class="alert error"
+ >
+ {{ $t('settings.list_backups_error', { error }) }}
+ <button
+ :title="$t('settings.hide_list_backups_error_action')"
+ @click="listBackupsError = false"
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
+ />
+ </button>
+ </div>
+ <button
+ class="btn button-default"
+ @click="addBackup"
+ >
+ {{ $t('settings.add_backup') }}
+ </button>
+ <p v-if="addedBackup">
+ {{ $t('settings.added_backup') }}
+ </p>
+ <template v-if="addBackupError !== false">
+ <p>{{ $t('settings.add_backup_error', { error: addBackupError }) }}</p>
+ </template>
+ </div>
</div>
</template>
diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue
index dc48902f..97046ff0 100644
--- a/src/components/settings_modal/tabs/filtering_tab.vue
+++ b/src/components/settings_modal/tabs/filtering_tab.vue
@@ -72,22 +72,10 @@
<div>{{ $t('settings.filtering_explanation') }}</div>
</li>
<h3>{{ $t('settings.attachments') }}</h3>
- <li v-if="expertLevel > 0">
- <label for="maxThumbnails">
- {{ $t('settings.max_thumbnails') }}
- </label>
- <input
- id="maxThumbnails"
- path.number="maxThumbnails"
- class="number-input"
- type="number"
- min="0"
- step="1"
- >
- </li>
<li>
<IntegerSetting
path="maxThumbnails"
+ expert="1"
:min="0"
>
{{ $t('settings.max_thumbnails') }}
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index 62d86176..1e11b9e0 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -38,6 +38,11 @@ const GeneralTab = {
value: mode,
label: this.$t(`settings.mention_link_display_${mode}`)
})),
+ thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
+ key: mode,
+ value: mode,
+ label: this.$t(`settings.third_column_mode_${mode}`)
+ })),
loopSilentAvailable:
// Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
@@ -72,6 +77,12 @@ const GeneralTab = {
!this.$store.state.users.currentUser.background_image
},
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
+ language: {
+ get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
+ set: function (val) {
+ this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
+ }
+ },
...SharedComputedObject()
},
methods: {
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index a2c6bffa..1fe51b6d 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -4,7 +4,11 @@
<h2>{{ $t('settings.interface') }}</h2>
<ul class="setting-list">
<li>
- <interface-language-switcher />
+ <interface-language-switcher
+ :prompt-text="$t('settings.interfaceLanguage')"
+ :language="language"
+ :set-language="val => language = val"
+ />
</li>
<li v-if="instanceSpecificPanelPresent">
<BooleanSetting path="hideISP">
@@ -61,6 +65,26 @@
</BooleanSetting>
</li>
<li>
+ <BooleanSetting path="disableStickyHeaders">
+ {{ $t('settings.disable_sticky_headers') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="showScrollbars">
+ {{ $t('settings.show_scrollbars') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <ChoiceSetting
+ v-if="user"
+ id="thirdColumnMode"
+ path="thirdColumnMode"
+ :options="thirdColumnModeOptions"
+ >
+ {{ $t('settings.third_column_mode') }}
+ </ChoiceSetting>
+ </li>
+ <li>
<BooleanSetting
path="alwaysShowNewPostButton"
expert="1"
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
index 40a87b81..6cfeea35 100644
--- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
@@ -2,7 +2,7 @@ import get from 'lodash/get'
import map from 'lodash/map'
import reject from 'lodash/reject'
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
-import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import BlockCard from 'src/components/block_card/block_card.vue'
import MuteCard from 'src/components/mute_card/mute_card.vue'
import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue'
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss b/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss
index ceb64efb..2adff847 100644
--- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss
@@ -8,7 +8,7 @@
.bulk-actions {
text-align: right;
padding: 0 1em;
- min-height: 28px;
+ min-height: 2em;
}
.bulk-action-button {
diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue
index 86be6095..dd3806ed 100644
--- a/src/components/settings_modal/tabs/notifications_tab.vue
+++ b/src/components/settings_modal/tabs/notifications_tab.vue
@@ -41,6 +41,11 @@
{{ $t('settings.notification_visibility_emoji_reactions') }}
</BooleanSetting>
</li>
+ <li>
+ <BooleanSetting path="notificationVisibility.polls">
+ {{ $t('settings.notification_visibility_polls') }}
+ </BooleanSetting>
+ </li>
</ul>
</li>
</ul>
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index bee8a7bb..8781bb91 100644
--- a/src/components/settings_modal/tabs/profile_tab.js
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -8,8 +8,10 @@ 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 InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
+import localeService from 'src/services/locale/locale.service.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@@ -40,7 +42,8 @@ const ProfileTab = {
banner: null,
bannerPreview: null,
background: null,
- backgroundPreview: null
+ backgroundPreview: null,
+ emailLanguage: this.$store.state.users.currentUser.language || ''
}
},
components: {
@@ -50,7 +53,8 @@ const ProfileTab = {
Autosuggest,
ProgressButton,
Checkbox,
- BooleanSetting
+ BooleanSetting,
+ InterfaceLanguageSwitcher
},
computed: {
user () {
@@ -111,19 +115,25 @@ const ProfileTab = {
},
methods: {
updateProfile () {
+ const params = {
+ note: this.newBio,
+ locked: this.newLocked,
+ // Backend notation.
+ /* eslint-disable camelcase */
+ display_name: this.newName,
+ fields_attributes: this.newFields.filter(el => el != null),
+ bot: this.bot,
+ show_role: this.showRole
+ /* eslint-enable camelcase */
+ }
+
+ if (this.emailLanguage) {
+ params.language = localeService.internalToBackendLocale(this.emailLanguage)
+ }
+
this.$store.state.api.backendInteractor
- .updateProfile({
- params: {
- note: this.newBio,
- locked: this.newLocked,
- // Backend notation.
- /* eslint-disable camelcase */
- display_name: this.newName,
- fields_attributes: this.newFields.filter(el => el != null),
- bot: this.bot,
- show_role: this.showRole
- /* eslint-enable camelcase */
- } }).then((user) => {
+ .updateProfile({ params })
+ .then((user) => {
this.newFields.splice(user.fields.length)
merge(this.newFields, user.fields)
this.$store.commit('addNewUsers', [user])
@@ -193,8 +203,8 @@ const ProfileTab = {
submitAvatar (cropper, file) {
const that = this
return new Promise((resolve, reject) => {
- function updateAvatar (avatar) {
- that.$store.state.api.backendInteractor.updateProfileImages({ avatar })
+ function updateAvatar (avatar, avatarName) {
+ that.$store.state.api.backendInteractor.updateProfileImages({ avatar, avatarName })
.then((user) => {
that.$store.commit('addNewUsers', [user])
that.$store.commit('setCurrentUser', user)
@@ -207,9 +217,9 @@ const ProfileTab = {
}
if (cropper) {
- cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
+ cropper.getCroppedCanvas().toBlob((data) => updateAvatar(data, file.name), file.type)
} else {
- updateAvatar(file)
+ updateAvatar(file, file.name)
}
})
},
diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss
index 111eaed3..201f1a76 100644
--- a/src/components/settings_modal/tabs/profile_tab.scss
+++ b/src/components/settings_modal/tabs/profile_tab.scss
@@ -54,16 +54,20 @@
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
background-color: rgba(0, 0, 0, 0.6);
opacity: 0.7;
- color: white;
width: 1.5em;
height: 1.5em;
text-align: center;
line-height: 1.5em;
font-size: 1.5em;
cursor: pointer;
+
&:hover {
opacity: 1;
}
+
+ svg {
+ color: white;
+ }
}
.oauth-tokens {
@@ -85,7 +89,7 @@
&-bulk-actions {
text-align: right;
padding: 0 1em;
- min-height: 28px;
+ min-height: 2em;
button {
width: 10em;
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index e00f6e5b..4cd93772 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -68,8 +68,9 @@
class="delete-field button-unstyled -hover-highlight"
@click="deleteField(i)"
>
+ <!-- TODO something is wrong with v-show here -->
<FAIcon
- v-show="newFields.length > 1"
+ v-if="newFields.length > 1"
icon="times"
/>
</button>
@@ -88,6 +89,13 @@
{{ $t('settings.bot') }}
</Checkbox>
</p>
+ <p>
+ <interface-language-switcher
+ :prompt-text="$t('settings.email_language')"
+ :language="emailLanguage"
+ :set-language="val => emailLanguage = val"
+ />
+ </p>
<button
:disabled="newName && newName.length === 0"
class="btn button-default"
@@ -106,14 +114,17 @@
:src="user.profile_image_url_original"
class="current-avatar"
>
- <FAIcon
+ <button
v-if="!isDefaultAvatar && pickAvatarBtnVisible"
:title="$t('settings.reset_avatar')"
- class="reset-button"
- icon="times"
- type="button"
@click="resetAvatar"
- />
+ class="button-unstyled reset-button"
+ >
+ <FAIcon
+ icon="times"
+ type="button"
+ />
+ </button>
</div>
<p>{{ $t('settings.set_new_avatar') }}</p>
<button
@@ -135,14 +146,17 @@
<h2>{{ $t('settings.profile_banner') }}</h2>
<div class="banner-background-preview">
<img :src="user.cover_photo">
- <FAIcon
+ <button
v-if="!isDefaultBanner"
+ class="button-unstyled reset-button"
:title="$t('settings.reset_profile_banner')"
- class="reset-button"
- icon="times"
- type="button"
@click="resetBanner"
- />
+ >
+ <FAIcon
+ icon="times"
+ type="button"
+ />
+ </button>
</div>
<p>{{ $t('settings.set_new_profile_banner') }}</p>
<img
@@ -174,14 +188,17 @@
<h2>{{ $t('settings.profile_background') }}</h2>
<div class="banner-background-preview">
<img :src="user.background_image">
- <FAIcon
+ <button
v-if="!isDefaultBackground"
+ class="button-unstyled reset-button"
:title="$t('settings.reset_profile_background')"
- class="reset-button"
- icon="times"
- type="button"
@click="resetBackground"
- />
+ >
+ <FAIcon
+ icon="times"
+ type="button"
+ />
+ </button>
</div>
<p>{{ $t('settings.set_new_profile_background') }}</p>
<img
diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.js b/src/components/settings_modal/tabs/security_tab/security_tab.js
index 65d20fc0..fc732936 100644
--- a/src/components/settings_modal/tabs/security_tab/security_tab.js
+++ b/src/components/settings_modal/tabs/security_tab/security_tab.js
@@ -15,11 +15,21 @@ const SecurityTab = {
deleteAccountError: false,
changePasswordInputs: [ '', '', '' ],
changedPassword: false,
- changePasswordError: false
+ changePasswordError: false,
+ moveAccountTarget: '',
+ moveAccountPassword: '',
+ movedAccount: false,
+ moveAccountError: false,
+ aliases: [],
+ listAliasesError: false,
+ addAliasTarget: '',
+ addedAlias: false,
+ addAliasError: false
}
},
created () {
this.$store.dispatch('fetchTokens')
+ this.fetchAliases()
},
components: {
ProgressButton,
@@ -92,6 +102,49 @@ const SecurityTab = {
}
})
},
+ moveAccount () {
+ const params = {
+ targetAccount: this.moveAccountTarget,
+ password: this.moveAccountPassword
+ }
+ this.$store.state.api.backendInteractor.moveAccount(params)
+ .then((res) => {
+ if (res.status === 'success') {
+ this.movedAccount = true
+ this.moveAccountError = false
+ } else {
+ this.movedAccount = false
+ this.moveAccountError = res.error
+ }
+ })
+ },
+ removeAlias (alias) {
+ this.$store.state.api.backendInteractor.deleteAlias({ alias })
+ .then(() => this.fetchAliases())
+ },
+ addAlias () {
+ this.$store.state.api.backendInteractor.addAlias({ alias: this.addAliasTarget })
+ .then((res) => {
+ this.addedAlias = true
+ this.addAliasError = false
+ this.addAliasTarget = ''
+ })
+ .catch((error) => {
+ this.addedAlias = false
+ this.addAliasError = error
+ })
+ .then(() => this.fetchAliases())
+ },
+ fetchAliases () {
+ this.$store.state.api.backendInteractor.listAliases()
+ .then((res) => {
+ this.aliases = res.aliases
+ this.listAliasesError = false
+ })
+ .catch((error) => {
+ this.listAliasesError = error.error
+ })
+ },
logout () {
this.$store.dispatch('logout')
this.$router.replace('/')
diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue
index 275d4616..c74a0c67 100644
--- a/src/components/settings_modal/tabs/security_tab/security_tab.vue
+++ b/src/components/settings_modal/tabs/security_tab/security_tab.vue
@@ -103,6 +103,114 @@
</table>
</div>
<mfa />
+
+ <div class="setting-item">
+ <h2>{{ $t('settings.account_alias') }}</h2>
+ <table>
+ <thead>
+ <tr>
+ <th>{{ $t('settings.account_alias_table_head') }}</th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ <tr
+ v-for="alias in aliases"
+ :key="alias"
+ >
+ <td>{{ alias }}</td>
+ <td class="actions">
+ <button
+ class="btn button-default"
+ @click="removeAlias(alias)"
+ >
+ {{ $t('settings.remove_alias') }}
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <div
+ v-if="listAliasesError"
+ class="alert error"
+ >
+ {{ $t('settings.list_aliases_error', { error }) }}
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="times"
+ :title="$t('settings.hide_list_aliases_error_action')"
+ @click="listAliasesError = false"
+ />
+ </div>
+ <div>
+ <i18n
+ path="settings.new_alias_target"
+ tag="p"
+ >
+ <code
+ place="example"
+ >
+ foo@example.org
+ </code>
+ </i18n>
+ <input
+ v-model="addAliasTarget"
+ >
+ </div>
+ <button
+ class="btn button-default"
+ @click="addAlias"
+ >
+ {{ $t('settings.save') }}
+ </button>
+ <p v-if="addedAlias">
+ {{ $t('settings.added_alias') }}
+ </p>
+ <template v-if="addAliasError !== false">
+ <p>{{ $t('settings.add_alias_error', { error: addAliasError }) }}</p>
+ </template>
+ </div>
+
+ <div class="setting-item">
+ <h2>{{ $t('settings.move_account') }}</h2>
+ <p>{{ $t('settings.move_account_notes') }}</p>
+ <div>
+ <i18n
+ path="settings.move_account_target"
+ tag="p"
+ >
+ <code
+ place="example"
+ >
+ foo@example.org
+ </code>
+ </i18n>
+ <input
+ v-model="moveAccountTarget"
+ >
+ </div>
+ <div>
+ <p>{{ $t('settings.current_password') }}</p>
+ <input
+ v-model="moveAccountPassword"
+ type="password"
+ autocomplete="current-password"
+ >
+ </div>
+ <button
+ class="btn button-default"
+ @click="moveAccount"
+ >
+ {{ $t('settings.save') }}
+ </button>
+ <p v-if="movedAccount">
+ {{ $t('settings.moved_account') }}
+ </p>
+ <template v-if="moveAccountError !== false">
+ <p>{{ $t('settings.move_account_error', { error: moveAccountError }) }}</p>
+ </template>
+ </div>
+
<div class="setting-item">
<h2>{{ $t('settings.delete_account') }}</h2>
<p v-if="!deletingAccount">
diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue
index 7ac7b9d3..f266b603 100644
--- a/src/components/settings_modal/tabs/theme_tab/preview.vue
+++ b/src/components/settings_modal/tabs/theme_tab/preview.vue
@@ -29,14 +29,14 @@
{{ $t('settings.style.preview.content') }}
</h4>
- <i18n path="settings.style.preview.text">
+ <i18n-t scope="global" keypath="settings.style.preview.text">
<code style="font-family: var(--postCodeFont)">
{{ $t('settings.style.preview.mono') }}
</code>
<a style="color: var(--link)">
{{ $t('settings.style.preview.link') }}
</a>
- </i18n>
+ </i18n-t>
<div class="icons">
<FAIcon
@@ -72,15 +72,16 @@
:^)
</div>
<div class="content">
- <i18n
- path="settings.style.preview.fine_print"
+ <i18n-t
+ keypath="settings.style.preview.fine_print"
tag="span"
class="faint"
+ scope="global"
>
<a style="color: var(--faintLink)">
{{ $t('settings.style.preview.faint_link') }}
</a>
- </i18n>
+ </i18n-t>
</div>
</div>
<div class="separator" />
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 0b6669fc..7e1da7ab 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
@@ -1,4 +1,3 @@
-import { set, delete as del } from 'vue'
import {
rgb2hex,
hex2rgb,
@@ -34,7 +33,7 @@ import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
import FontControl from 'src/components/font_control/font_control.vue'
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
-import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Select from 'src/components/select/select.vue'
@@ -320,9 +319,9 @@ export default {
},
set (val) {
if (val) {
- set(this.shadowsLocal, this.shadowSelected, this.currentShadowFallback.map(_ => Object.assign({}, _)))
+ this.shadowsLocal[this.shadowSelected] = this.currentShadowFallback.map(_ => Object.assign({}, _))
} else {
- del(this.shadowsLocal, this.shadowSelected)
+ delete this.shadowsLocal[this.shadowSelected]
}
}
},
@@ -334,7 +333,7 @@ export default {
return this.shadowsLocal[this.shadowSelected]
},
set (v) {
- set(this.shadowsLocal, this.shadowSelected, v)
+ this.shadowsLocal[this.shadowSelected] = v
}
},
themeValid () {
@@ -378,6 +377,10 @@ export default {
// To separate from other random JSON files and possible future source formats
_pleroma_theme_version: 2, theme, source
}
+ },
+ isActive () {
+ const tabSwitcher = this.$parent
+ return tabSwitcher ? tabSwitcher.isActive('theme') : false
}
},
components: {
@@ -557,7 +560,7 @@ export default {
.filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal'))
.filter(_ => !v1OnlyNames.includes(_))
.forEach(key => {
- set(this.$data, key, undefined)
+ this.$data[key] = undefined
})
},
@@ -565,7 +568,7 @@ export default {
Object.keys(this.$data)
.filter(_ => _.endsWith('RadiusLocal'))
.forEach(key => {
- set(this.$data, key, undefined)
+ this.$data[key] = undefined
})
},
@@ -573,7 +576,7 @@ export default {
Object.keys(this.$data)
.filter(_ => _.endsWith('OpacityLocal'))
.forEach(key => {
- set(this.$data, key, undefined)
+ this.$data[key] = undefined
})
},
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
index 0db21537..bad6f51b 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
@@ -245,36 +245,12 @@
border-color: var(--border, $fallback--border);
}
- .panel-heading {
- .badge, .alert, .btn, .faint {
- margin-left: 1em;
- white-space: nowrap;
- }
- .faint {
- text-overflow: ellipsis;
- min-width: 2em;
- overflow-x: hidden;
- }
- .flex-spacer {
- flex: 1;
- }
- }
.btn {
- margin-left: 0;
- padding: 0 1em;
min-width: 3em;
- min-height: 30px;
}
}
}
- .apply-container {
- justify-content: center;
- position: absolute;
- bottom: 8px;
- right: 5px;
- }
-
.radius-item,
.color-item {
min-width: 20em;
@@ -334,16 +310,25 @@
padding: 20px;
}
- .apply-container {
- .btn {
- min-height: 28px;
- min-width: 10em;
- padding: 0 2em;
- }
- }
-
.btn {
margin-left: .25em;
margin-right: .25em;
}
}
+
+.extra-content {
+ .apply-container {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-around;
+ flex-grow: 1;
+
+ .btn {
+ flex-grow: 1;
+ min-height: 2em;
+ min-width: 0;
+ max-width: 10em;
+ padding: 0;
+ }
+ }
+}
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 c02986ed..ff2fece9 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
@@ -903,6 +903,7 @@
<div class="tab-header shadow-selector">
<div class="select-container">
{{ $t('settings.style.shadows.component') }}
+ {{ ' ' }}
<Select
id="shadow-switcher"
v-model="shadowSelected"
@@ -924,6 +925,7 @@
>
{{ $t('settings.style.shadows.override') }}
</label>
+ {{ ' ' }}
<input
id="override"
v-model="currentShadowOverriden"
@@ -949,27 +951,30 @@
:fallback="currentShadowFallback"
/>
<div v-if="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'">
- <i18n
- path="settings.style.shadows.filter_hint.always_drop_shadow"
+ <i18n-t
+ scope="global"
+ keypath="settings.style.shadows.filter_hint.always_drop_shadow"
tag="p"
>
<code>filter: drop-shadow()</code>
- </i18n>
+ </i18n-t>
<p>{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}</p>
- <i18n
- path="settings.style.shadows.filter_hint.drop_shadow_syntax"
+ <i18n-t
+ scope="global"
+ keypath="settings.style.shadows.filter_hint.drop_shadow_syntax"
tag="p"
>
<code>drop-shadow</code>
<code>spread-radius</code>
<code>inset</code>
- </i18n>
- <i18n
- path="settings.style.shadows.filter_hint.inset_classic"
+ </i18n-t>
+ <i18n-t
+ scope="global"
+ keypath="settings.style.shadows.filter_hint.inset_classic"
tag="p"
>
<code>box-shadow</code>
- </i18n>
+ </i18n-t>
<p>{{ $t('settings.style.shadows.filter_hint.spread_zero') }}</p>
</div>
</div>
@@ -1016,21 +1021,26 @@
</tab-switcher>
</keep-alive>
- <div class="apply-container">
- <button
- class="btn button-default submit"
- :disabled="!themeValid"
- @click="setCustomTheme"
- >
- {{ $t('general.apply') }}
- </button>
- <button
- class="btn button-default"
- @click="clearAll"
- >
- {{ $t('settings.style.switcher.reset') }}
- </button>
- </div>
+ <teleport
+ v-if="isActive"
+ to="#unscrolled-content"
+ >
+ <div class="apply-container">
+ <button
+ class="btn button-default submit"
+ :disabled="!themeValid"
+ @click="setCustomTheme"
+ >
+ {{ $t('general.apply') }}
+ </button>
+ <button
+ class="btn button-default"
+ @click="clearAll"
+ >
+ {{ $t('settings.style.switcher.reset') }}
+ </button>
+ </div>
+ </teleport>
</div>
</template>
diff --git a/src/components/settings_modal/tabs/version_tab.vue b/src/components/settings_modal/tabs/version_tab.vue
index d35ff25e..0330d49f 100644
--- a/src/components/settings_modal/tabs/version_tab.vue
+++ b/src/components/settings_modal/tabs/version_tab.vue
@@ -28,4 +28,4 @@
</div>
</div>
</template>
-<script src="./version_tab.js">
+<script src="./version_tab.js" />
diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js
index 2d5d6eb1..386a5756 100644
--- a/src/components/shadow_control/shadow_control.js
+++ b/src/components/shadow_control/shadow_control.js
@@ -30,18 +30,19 @@ const toModel = (object = {}) => ({
})
export default {
- // 'Value' and 'Fallback' can be undefined, but if they are
+ // 'modelValue' and 'Fallback' can be undefined, but if they are
// initially vue won't detect it when they become something else
// therefore i'm using "ready" which should be passed as true when
// data becomes available
props: [
- 'value', 'fallback', 'ready'
+ 'modelValue', 'fallback', 'ready'
],
+ emits: ['update:modelValue'],
data () {
return {
selectedId: 0,
// TODO there are some bugs regarding display of array (it's not getting updated when deleting for some reason)
- cValue: (this.value || this.fallback || []).map(toModel)
+ cValue: (this.modelValue || this.fallback || []).map(toModel)
}
},
components: {
@@ -70,7 +71,7 @@ export default {
}
},
beforeUpdate () {
- this.cValue = this.value || this.fallback
+ this.cValue = this.modelValue || this.fallback
},
computed: {
anyShadows () {
@@ -105,7 +106,7 @@ export default {
!this.usingFallback
},
usingFallback () {
- return typeof this.value === 'undefined'
+ return typeof this.modelValue === 'undefined'
},
rgb () {
return hex2rgb(this.selected.color)
diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue
index 511e07f3..f2fc7b99 100644
--- a/src/components/shadow_control/shadow_control.vue
+++ b/src/components/shadow_control/shadow_control.vue
@@ -204,12 +204,13 @@
v-model="selected.alpha"
:disabled="!present"
/>
- <i18n
- path="settings.style.shadows.hintV3"
+ <i18n-t
+ scope="global"
+ keypath="settings.style.shadows.hintV3"
tag="p"
>
<code>--variable,mod</code>
- </i18n>
+ </i18n-t>
</div>
</div>
</template>
diff --git a/src/components/shout_panel/shout_panel.vue b/src/components/shout_panel/shout_panel.vue
index c88797d1..1eca88a7 100644
--- a/src/components/shout_panel/shout_panel.vue
+++ b/src/components/shout_panel/shout_panel.vue
@@ -57,7 +57,7 @@
>
<div class="panel panel-default">
<div
- class="panel-heading stub timeline-heading shout-heading"
+ class="panel-heading -stub timeline-heading shout-heading"
@click.stop.prevent="togglePanel"
>
<div class="title">
@@ -79,17 +79,17 @@
.floating-shout {
position: fixed;
- bottom: 0px;
+ bottom: 0.5em;
z-index: 1000;
max-width: 25em;
-}
-.floating-shout.left {
- left: 0px;
-}
+ &.-left {
+ left: 0.5em;
+ }
-.floating-shout:not(.left) {
- right: 0px;
+ &:not(.-left) {
+ right: 0.5em;
+ }
}
.shout-panel {
@@ -98,7 +98,7 @@
.icon {
color: $fallback--text;
- color: var(--text, $fallback--text);
+ color: var(--panelText, $fallback--text);
margin-right: 0.5em;
}
@@ -121,7 +121,7 @@
.shout-message {
display: flex;
- padding: 0.2em 0.5em
+ padding: 0.2em 0.5em;
}
.shout-avatar {
@@ -137,6 +137,7 @@
.shout-input {
display: flex;
+
textarea {
flex: 1;
margin: 0.6em;
diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js
index 89719df3..bad1806b 100644
--- a/src/components/side_drawer/side_drawer.js
+++ b/src/components/side_drawer/side_drawer.js
@@ -49,7 +49,7 @@ const SideDrawer = {
currentUser () {
return this.$store.state.users.currentUser
},
- shout () { return this.$store.state.shout.channel.state === 'joined' },
+ shout () { return this.$store.state.shout.joined },
unseenNotifications () {
return unseenNotificationsFromStore(this.$store)
},
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 4c0ef3e0..a925f30b 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -69,7 +69,7 @@ const controlledOrUncontrolledGetters = list => list.reduce((res, name) => {
const controlledName = `controlled${camelized}`
const uncontrolledName = `uncontrolled${camelized}`
res[name] = function () {
- return this[toggle] ? this[controlledName] : this[uncontrolledName]
+ return ((this.$data[toggle] !== undefined || this.$props[toggle] !== undefined) && this[toggle]) ? this[controlledName] : this[uncontrolledName]
}
return res
}, {})
@@ -225,12 +225,18 @@ const Status = {
muteWordHits () {
return muteWordHits(this.status, this.muteWords)
},
+ rtBotStatus () {
+ return this.statusoid.user.bot
+ },
botStatus () {
return this.status.user.bot
},
botIndicator () {
return this.botStatus && !this.hideBotIndication
},
+ rtBotIndicator () {
+ return this.rtBotStatus && !this.hideBotIndication
+ },
mentionsLine () {
if (!this.headTailLinks) return []
const writtenSet = new Set(this.headTailLinks.writtenMentions.map(_ => _.url))
@@ -305,7 +311,7 @@ const Status = {
return this.mergedConfig.hideWordFilteredPosts
},
hideStatus () {
- return (this.virtualHidden || !this.shouldNotMute) && (
+ return (!this.shouldNotMute) && (
(this.muted && this.hideFilteredStatuses) ||
(this.userIsMuted && this.hideMutedUsers) ||
(this.status.thread_muted && this.hideMutedThreads) ||
@@ -383,6 +389,9 @@ const Status = {
},
threadShowing () {
return this.controlledThreadDisplayStatus === 'showing'
+ },
+ visibilityLocalized () {
+ return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility)
}
},
methods: {
@@ -472,11 +481,6 @@ const Status = {
'isSuspendable': function (val) {
this.suspendable = val
}
- },
- filters: {
- capitalize: function (str) {
- return str.charAt(0).toUpperCase() + str.slice(1)
- }
}
}
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index 3f647b25..b3ad3818 100644
--- a/src/components/status/status.scss
+++ b/src/components/status/status.scss
@@ -42,6 +42,10 @@
display: flex;
padding: var(--status-margin, $status-margin);
+ > * {
+ min-width: 0;
+ }
+
&.-repeat {
padding-top: 0;
}
@@ -78,7 +82,6 @@
.status-username {
white-space: nowrap;
- font-size: 14px;
overflow: hidden;
max-width: 85%;
font-weight: bold;
@@ -103,7 +106,7 @@
.heading-name-row {
display: flex;
justify-content: space-between;
- line-height: 18px;
+ line-height: 1.3;
a {
display: inline-block;
@@ -156,7 +159,7 @@
& .heading-reply-row {
position: relative;
align-content: baseline;
- font-size: 12px;
+ font-size: 0.85em;
margin-top: 0.2em;
line-height: 130%;
max-width: 100%;
@@ -224,8 +227,8 @@
.replies {
margin-top: 0.25em;
- line-height: 18px;
- font-size: 12px;
+ line-height: 1.3;
+ font-size: 0.85em;
display: flex;
flex-wrap: wrap;
@@ -385,14 +388,14 @@
.stat-title {
color: var(--faint, $fallback--faint);
- font-size: 12px;
+ font-size: 0.85em;
text-transform: uppercase;
position: relative;
}
.stat-number {
font-weight: bolder;
- font-size: 16px;
+ font-size: 1.1em;
line-height: 1em;
}
@@ -406,13 +409,13 @@
margin-left: 20px;
}
- .avatar:not(.repeater-avatar) {
+ .post-avatar {
width: 40px;
height: 40px;
// TODO define those other way somehow?
// stylelint-disable rscss/class-format
- &.avatar-compact {
+ &.-compact {
width: 32px;
height: 32px;
}
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 1679834e..67ce999a 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -1,6 +1,7 @@
<template>
<div
v-if="!hideStatus"
+ ref="root"
class="Status"
:class="[{ '-focused': isFocused }, { '-conversation': inlineExpanded }]"
>
@@ -77,7 +78,7 @@
<UserAvatar
v-if="retweet"
class="left-side repeater-avatar"
- :bot="botIndicator"
+ :bot="rtBotIndicator"
:better-shadow="betterShadow"
:user="statusoid.user"
/>
@@ -100,6 +101,7 @@
:to="retweeterProfileLink"
>{{ retweeter }}</router-link>
</span>
+ {{ ' ' }}
<FAIcon
icon="retweet"
class="repeat-icon"
@@ -120,17 +122,18 @@
v-if="!noHeading"
class="left-side"
>
- <router-link
- :to="userProfileLink"
- @click.stop.prevent.capture.native="toggleUserExpanded"
+ <a
+ :href="$router.resolve(userProfileLink).href"
+ @click.stop.prevent.capture="toggleUserExpanded"
>
<UserAvatar
+ class="post-avatar"
:bot="botIndicator"
:compact="compact"
:better-shadow="betterShadow"
:user="status.user"
/>
- </router-link>
+ </a>
</div>
<div class="right-side">
<UserCard
@@ -190,7 +193,7 @@
<span
v-if="status.visibility"
class="visibility-icon"
- :title="status.visibility | capitalize"
+ :title="visibilityLocalized"
>
<FAIcon
fixed-width
@@ -273,6 +276,7 @@
icon="reply"
flip="horizontal"
/>
+ {{ ' ' }}
<span
class="reply-to-text"
>
@@ -292,7 +296,6 @@
:url="replyProfileLink"
:user-id="status.in_reply_to_user_id"
:user-screen-name="status.in_reply_to_screen_name"
- :first-mention="false"
/>
</span>
@@ -454,6 +457,7 @@
>
<div class="left-side">
<UserAvatar
+ class="post-avatar"
:compact="compact"
:bot="botIndicator"
/>
diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss
index f261108e..039d4c7f 100644
--- a/src/components/status_body/status_body.scss
+++ b/src/components/status_body/status_body.scss
@@ -19,7 +19,7 @@
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
- line-height: 1.4em;
+ line-height: var(--post-line-height);
}
.summary {
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index 24d842c2..976fe98c 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -15,14 +15,14 @@
:emoji="status.emojis"
/>
<button
- v-if="longSubject && showingLongSubject"
+ v-show="longSubject && showingLongSubject"
class="button-unstyled -link tall-subject-hider"
@click.prevent="toggleShowingLongSubject"
>
{{ $t("status.hide_full_subject") }}
</button>
<button
- v-else-if="longSubject"
+ v-show="longSubject && !showingLongSubject"
class="button-unstyled -link tall-subject-hider"
@click.prevent="toggleShowingLongSubject"
>
@@ -34,7 +34,7 @@
class="text-wrapper"
>
<button
- v-if="hideTallStatus"
+ v-show="hideTallStatus"
class="button-unstyled -link tall-status-hider"
:class="{ '-focused': focused }"
@click.prevent="toggleShowMore"
@@ -54,7 +54,7 @@
/>
<button
- v-if="hideSubjectStatus"
+ v-show="hideSubjectStatus"
class="button-unstyled -link cw-status-hider"
@click.prevent="toggleShowMore"
>
@@ -85,7 +85,7 @@
/>
</button>
<button
- v-if="showingMore && !fullContent"
+ v-show="showingMore && !fullContent"
class="button-unstyled -link status-unhider"
@click.prevent="toggleShowMore"
>
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index cf72ccb8..89f0aa51 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -31,7 +31,7 @@ const controlledOrUncontrolledGetters = list => list.reduce((res, name) => {
const controlledName = `controlled${camelized}`
const uncontrolledName = `uncontrolled${camelized}`
res[name] = function () {
- return this[toggle] ? this[controlledName] : this[uncontrolledName]
+ return ((this.$data[toggle] !== undefined || this.$props[toggle] !== undefined) && this[toggle]) ? this[controlledName] : this[uncontrolledName]
}
return res
}, {})
@@ -59,7 +59,9 @@ const StatusContent = {
'controlledShowingTall',
'controlledExpandingSubject',
'controlledToggleShowingTall',
- 'controlledToggleExpandingSubject'
+ 'controlledToggleExpandingSubject',
+ 'controlledShowingLongSubject',
+ 'controlledToggleShowingLongSubject'
],
data () {
return {
diff --git a/src/components/status_popover/status_popover.js b/src/components/status_popover/status_popover.js
index c47f5631..e0962ccd 100644
--- a/src/components/status_popover/status_popover.js
+++ b/src/components/status_popover/status_popover.js
@@ -1,6 +1,7 @@
import { find } from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
+import { defineAsyncComponent } from 'vue'
library.add(
faCircleNotch
@@ -22,8 +23,8 @@ const StatusPopover = {
}
},
components: {
- Status: () => import('../status/status.vue'),
- Popover: () => import('../popover/popover.vue')
+ Status: defineAsyncComponent(() => import('../status/status.vue')),
+ Popover: defineAsyncComponent(() => import('../popover/popover.vue'))
},
methods: {
enter () {
diff --git a/src/components/sticker_picker/sticker_picker.js b/src/components/sticker_picker/sticker_picker.js
index 8daf3f07..3a2d3914 100644
--- a/src/components/sticker_picker/sticker_picker.js
+++ b/src/components/sticker_picker/sticker_picker.js
@@ -1,6 +1,6 @@
/* eslint-env browser */
import statusPosterService from '../../services/status_poster/status_poster.service.js'
-import TabSwitcher from '../tab_switcher/tab_switcher.js'
+import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
const StickerPicker = {
components: {
diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue
index e939b532..ab3080c8 100644
--- a/src/components/still-image/still-image.vue
+++ b/src/components/still-image/still-image.vue
@@ -58,10 +58,10 @@
zoom: var(--_still_image-label-scale, 1);
content: 'gif';
position: absolute;
- line-height: 10px;
- font-size: 10px;
- top: 5px;
- left: 5px;
+ line-height: 1;
+ font-size: 0.7em;
+ top: 0.5em;
+ left: 0.5em;
background: rgba(127, 127, 127, 0.5);
color: #fff;
display: block;
diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.jsx
index 12aac8e6..c8d390bc 100644
--- a/src/components/tab_switcher/tab_switcher.js
+++ b/src/components/tab_switcher/tab_switcher.jsx
@@ -1,10 +1,13 @@
-import Vue from 'vue'
+// eslint-disable-next-line no-unused
+import { h, Fragment } from 'vue'
import { mapState } from 'vuex'
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
import './tab_switcher.scss'
-export default Vue.component('tab-switcher', {
+const findFirstUsable = (slots) => slots.findIndex(_ => _.props)
+
+export default {
name: 'TabSwitcher',
props: {
renderOnlyFocused: {
@@ -31,22 +34,33 @@ export default Vue.component('tab-switcher', {
required: false,
type: Boolean,
default: false
+ },
+ bodyScrollLock: {
+ required: false,
+ type: Boolean,
+ default: false
}
},
data () {
return {
- active: this.$slots.default.findIndex(_ => _.tag)
+ active: findFirstUsable(this.slots())
}
},
computed: {
activeIndex () {
// In case of controlled component
if (this.activeTab) {
- return this.$slots.default.findIndex(slot => this.activeTab === slot.key)
+ return this.slots().findIndex(slot => slot && slot.props && this.activeTab === slot.props.key)
} else {
return this.active
}
},
+ isActive () {
+ return tabName => {
+ const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName
+ return this.$slots.default().findIndex(isWanted) === this.activeIndex
+ }
+ },
settingsModalVisible () {
return this.settingsModalState === 'visible'
},
@@ -55,9 +69,9 @@ export default Vue.component('tab-switcher', {
})
},
beforeUpdate () {
- const currentSlot = this.$slots.default[this.active]
- if (!currentSlot.tag) {
- this.active = this.$slots.default.findIndex(_ => _.tag)
+ const currentSlot = this.slots()[this.active]
+ if (!currentSlot.props) {
+ this.active = findFirstUsable(this.slots())
}
},
methods: {
@@ -67,9 +81,16 @@ export default Vue.component('tab-switcher', {
this.setTab(index)
}
},
+ // DO NOT put it to computed, it doesn't work (caching?)
+ slots () {
+ if (this.$slots.default()[0].type === Fragment) {
+ return this.$slots.default()[0].children
+ }
+ return this.$slots.default()
+ },
setTab (index) {
if (typeof this.onSwitch === 'function') {
- this.onSwitch.call(null, this.$slots.default[index].key)
+ this.onSwitch.call(null, this.slots()[index].key)
}
this.active = index
if (this.scrollableTabs) {
@@ -77,27 +98,28 @@ export default Vue.component('tab-switcher', {
}
}
},
- render (h) {
- const tabs = this.$slots.default
+ render () {
+ const tabs = this.slots()
.map((slot, index) => {
- if (!slot.tag) return
+ const props = slot.props
+ if (!props) return
const classesTab = ['tab', 'button-default']
const classesWrapper = ['tab-wrapper']
if (this.activeIndex === index) {
classesTab.push('active')
classesWrapper.push('active')
}
- if (slot.data.attrs.image) {
+ if (props.image) {
return (
<div class={classesWrapper.join(' ')}>
<button
- disabled={slot.data.attrs.disabled}
+ disabled={props.disabled}
onClick={this.clickTab(index)}
class={classesTab.join(' ')}
type="button"
>
- <img src={slot.data.attrs.image} title={slot.data.attrs['image-tooltip']}/>
- {slot.data.attrs.label ? '' : slot.data.attrs.label}
+ <img src={props.image} title={props['image-tooltip']}/>
+ {props.label ? '' : props.label}
</button>
</div>
)
@@ -105,25 +127,26 @@ export default Vue.component('tab-switcher', {
return (
<div class={classesWrapper.join(' ')}>
<button
- disabled={slot.data.attrs.disabled}
+ disabled={props.disabled}
onClick={this.clickTab(index)}
class={classesTab.join(' ')}
type="button"
>
- {!slot.data.attrs.icon ? '' : (<FAIcon class="tab-icon" size="2x" fixed-width icon={slot.data.attrs.icon}/>)}
+ {!props.icon ? '' : (<FAIcon class="tab-icon" size="2x" fixed-width icon={props.icon}/>)}
<span class="text">
- {slot.data.attrs.label}
+ {props.label}
</span>
</button>
</div>
)
})
- const contents = this.$slots.default.map((slot, index) => {
- if (!slot.tag) return
+ const contents = this.slots().map((slot, index) => {
+ const props = slot.props
+ if (!props) return
const active = this.activeIndex === index
const classes = [ active ? 'active' : 'hidden' ]
- if (slot.data.attrs.fullHeight) {
+ if (props.fullHeight) {
classes.push('full-height')
}
const renderSlot = (!this.renderOnlyFocused || active)
@@ -134,7 +157,7 @@ export default Vue.component('tab-switcher', {
<div class={classes}>
{
this.sideTabBar
- ? <h1 class="mobile-label">{slot.data.attrs.label}</h1>
+ ? <h1 class="mobile-label">{props.label}</h1>
: ''
}
{renderSlot}
@@ -147,10 +170,14 @@ export default Vue.component('tab-switcher', {
<div class="tabs">
{tabs}
</div>
- <div ref="contents" class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')} v-body-scroll-lock={this.settingsModalVisible}>
+ <div
+ ref="contents"
+ class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
+ v-body-scroll-lock={this.bodyScrollLock}
+ >
{contents}
</div>
</div>
)
}
-})
+}
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index 0ed614b7..7a086b26 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -25,8 +25,9 @@
border-bottom-color: $fallback--border;
border-bottom-color: var(--border, $fallback--border);
}
+
.tab-wrapper {
- height: 28px;
+ height: 2em;
&:not(.active)::after {
left: 0;
@@ -166,13 +167,6 @@
position: relative;
white-space: nowrap;
padding: 6px 1em;
- background-color: $fallback--fg;
- background-color: var(--tab, $fallback--fg);
-
- &, &:active .tab-icon {
- color: $fallback--text;
- color: var(--tabText, $fallback--text);
- }
&:not(.active) {
z-index: 4;
diff --git a/src/components/tag_timeline/tag_timeline.js b/src/components/tag_timeline/tag_timeline.js
index 400c6a4b..bda61ae0 100644
--- a/src/components/tag_timeline/tag_timeline.js
+++ b/src/components/tag_timeline/tag_timeline.js
@@ -18,7 +18,7 @@ const TagTimeline = {
this.$store.dispatch('startFetchingTimeline', { timeline: 'tag', tag: this.tag })
}
},
- destroyed () {
+ unmounted () {
this.$store.dispatch('stopFetchingTimeline', 'tag')
}
}
diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue
index e64455e0..4eaf597d 100644
--- a/src/components/thread_tree/thread_tree.vue
+++ b/src/components/thread_tree/thread_tree.vue
@@ -1,5 +1,5 @@
<template>
- <div class="thread-tree panel-body">
+ <div class="thread-tree">
<status
:key="status.id"
ref="statusComponent"
@@ -74,36 +74,44 @@
v-if="currentReplies.length && !threadShowing"
class="thread-tree-replies thread-tree-replies-hidden"
>
- <i18n
+ <i18n-t
v-if="simple"
+ scope="global"
tag="button"
- path="status.thread_follow_with_icon"
+ keypath="status.thread_follow_with_icon"
class="button-unstyled -link thread-tree-show-replies-button"
@click.prevent="dive(status.id)"
>
- <FAIcon
- place="icon"
- icon="angle-double-right"
- />
- <span place="text">
- {{ $tc('status.thread_follow', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id] }) }}
- </span>
- </i18n>
- <i18n
+ <template #icon>
+ <FAIcon
+ icon="angle-double-right"
+ />
+ </template>
+ <template #text>
+ <span>
+ {{ $tc('status.thread_follow', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id] }) }}
+ </span>
+ </template>
+ </i18n-t>
+ <i18n-t
v-else
+ scope="global"
tag="button"
- path="status.thread_show_full_with_icon"
+ keypath="status.thread_show_full_with_icon"
class="button-unstyled -link thread-tree-show-replies-button"
@click.prevent="showThreadRecursively(status.id)"
>
- <FAIcon
- place="icon"
- icon="angle-double-down"
- />
- <span place="text">
- {{ $tc('status.thread_show_full', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id], depth: totalReplyDepth[status.id] }) }}
- </span>
- </i18n>
+ <template #icon>
+ <FAIcon
+ icon="angle-double-down"
+ />
+ </template>
+ <template #text>
+ <span>
+ {{ $tc('status.thread_show_full', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id], depth: totalReplyDepth[status.id] }) }}
+ </span>
+ </template>
+ </i18n-t>
</div>
</div>
</template>
diff --git a/src/components/timeago/timeago.vue b/src/components/timeago/timeago.vue
index 55a2dd94..2b487dfd 100644
--- a/src/components/timeago/timeago.vue
+++ b/src/components/timeago/timeago.vue
@@ -3,7 +3,7 @@
:datetime="time"
:title="localeDateString"
>
- {{ $t(relativeTime.key, [relativeTime.num]) }}
+ {{ $tc(relativeTime.key, relativeTime.num, [relativeTime.num]) }}
</time>
</template>
@@ -31,7 +31,7 @@ export default {
created () {
this.refreshRelativeTimeObject()
},
- destroyed () {
+ unmounted () {
clearTimeout(this.interval)
},
methods: {
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 04f0e7d6..c575e876 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -22,7 +22,8 @@ const Timeline = {
'embedded',
'count',
'pinnedStatusIds',
- 'inProfile'
+ 'inProfile',
+ 'footerSlipgate' // reference to an element where we should put our footer
],
data () {
return {
@@ -40,6 +41,12 @@ const Timeline = {
TimelineQuickSettings
},
computed: {
+ filteredVisibleStatuses () {
+ return this.timeline.visibleStatuses.filter(status => this.timelineName !== 'user' || (status.id >= this.timeline.minId && status.id <= this.timeline.maxId))
+ },
+ filteredPinnedStatusIds () {
+ return (this.pinnedStatusIds || []).filter(statusId => this.timeline.statusesObject[statusId])
+ },
newStatusCount () {
return this.timeline.newStatusCount
},
@@ -54,11 +61,11 @@ const Timeline = {
}
},
classes () {
- let rootClasses = !this.embedded ? ['panel', 'panel-default'] : []
+ let rootClasses = !this.embedded ? ['panel', 'panel-default'] : ['-nonpanel']
if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention'])
return {
root: rootClasses,
- header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading'] : []),
+ header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading', '-sticky'] : []),
body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []),
footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : [])
}
@@ -70,8 +77,9 @@ const Timeline = {
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)
+ const nonPinnedIndex = this.virtualScrollIndex - this.filteredPinnedStatusIds.length
+ const min = Math.max(0, nonPinnedIndex - statusesPerSide)
+ const max = Math.min(amount, nonPinnedIndex + statusesPerSide)
return this.timeline.visibleStatuses.slice(min, max).map(_ => _.id)
},
virtualScrollingEnabled () {
@@ -104,7 +112,7 @@ const Timeline = {
window.addEventListener('keydown', this.handleShortKey)
setTimeout(this.determineVisibleStatuses, 250)
},
- destroyed () {
+ unmounted () {
window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('keydown', this.handleShortKey)
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
@@ -135,6 +143,7 @@ const Timeline = {
this.$store.commit('showNewStatuses', { timeline: this.timelineName })
this.paused = false
}
+ window.scrollTo({ top: 0 })
},
fetchOlderStatuses: throttle(function () {
const store = this.$store
diff --git a/src/components/timeline/timeline.scss b/src/components/timeline/timeline.scss
index 2c5a67e2..9e009fd3 100644
--- a/src/components/timeline/timeline.scss
+++ b/src/components/timeline/timeline.scss
@@ -9,23 +9,23 @@
cursor: progress;
}
- .timeline-heading {
- max-width: 100%;
- flex-wrap: nowrap;
- align-items: center;
- position: relative;
+ .conversation-heading {
+ top: calc(var(--__panel-heading-height) * var(--currentPanelStack, 2));
+ z-index: 2;
+ }
- .loadmore-button {
- flex-shrink: 0;
+ &.-nonpanel {
+ .timeline-heading {
+ text-align: center;
+ line-height: 2.75em;
+ padding: 0 0.5em;
}
- .loadmore-text {
- flex-shrink: 0;
- line-height: 1em;
+ .timeline-heading {
+ .button-default, .alert {
+ line-height: 2em;
+ width: 100%;
+ }
}
}
-
- .timeline-footer {
- border: none;
- }
}
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index ff16208d..f65881b6 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="['Timeline', classes.root]">
<div :class="classes.header">
<TimelineMenu v-if="!embedded" />
<button
@@ -10,7 +10,7 @@
{{ loadButtonString }}
</button>
<div
- v-else
+ v-else-if="!embedded"
class="loadmore-text faint"
@click.prevent
>
@@ -23,64 +23,62 @@
ref="timeline"
class="timeline"
>
- <template v-for="statusId in pinnedStatusIds">
- <conversation
- v-if="timeline.statusesObject[statusId]"
- :key="statusId + '-pinned'"
- class="status-fadein"
- :status-id="statusId"
- :collapsable="true"
- :pinned-status-ids-object="pinnedStatusIdsObject"
- :in-profile="inProfile"
- :profile-user-id="userId"
- />
- </template>
- <template v-for="status in timeline.visibleStatuses">
- <conversation
- v-if="timelineName !== 'user' || (status.id >= timeline.minId && status.id <= timeline.maxId)"
- :key="status.id"
- class="status-fadein"
- :status-id="status.id"
- :collapsable="true"
- :in-profile="inProfile"
- :profile-user-id="userId"
- :virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.includes(status.id)"
- />
- </template>
+ <conversation
+ v-for="statusId in filteredPinnedStatusIds"
+ :key="statusId + '-pinned'"
+ class="status-fadein"
+ :status-id="statusId"
+ :collapsable="true"
+ :pinned-status-ids-object="pinnedStatusIdsObject"
+ :in-profile="inProfile"
+ :profile-user-id="userId"
+ />
+ <conversation
+ v-for="status in filteredVisibleStatuses"
+ :key="status.id"
+ class="status-fadein"
+ :status-id="status.id"
+ :collapsable="true"
+ :in-profile="inProfile"
+ :profile-user-id="userId"
+ :virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.includes(status.id)"
+ />
</div>
</div>
<div :class="classes.footer">
- <div
- v-if="count===0"
- class="new-status-notification text-center faint"
- >
- {{ $t('timeline.no_statuses') }}
- </div>
- <div
- v-else-if="bottomedOut"
- class="new-status-notification text-center faint"
- >
- {{ $t('timeline.no_more_statuses') }}
- </div>
- <button
- v-else-if="!timeline.loading"
- class="button-unstyled -link -fullwidth"
- @click.prevent="fetchOlderStatuses()"
- >
- <div class="new-status-notification text-center">
- {{ $t('timeline.load_older') }}
+ <teleport :to="footerSlipgate" :disabled="!embedded || !footerSlipgate">
+ <div
+ v-if="count===0"
+ class="new-status-notification text-center faint"
+ >
+ {{ $t('timeline.no_statuses') }}
</div>
- </button>
- <div
- v-else
- class="new-status-notification text-center"
- >
- <FAIcon
- icon="circle-notch"
- spin
- size="lg"
- />
- </div>
+ <div
+ v-else-if="bottomedOut"
+ class="new-status-notification text-center faint"
+ >
+ {{ $t('timeline.no_more_statuses') }}
+ </div>
+ <button
+ v-else-if="!timeline.loading"
+ class="button-unstyled -link"
+ @click.prevent="fetchOlderStatuses()"
+ >
+ <div class="new-status-notification text-center">
+ {{ $t('timeline.load_older') }}
+ </div>
+ </button>
+ <div
+ v-else
+ class="new-status-notification text-center"
+ >
+ <FAIcon
+ icon="circle-notch"
+ spin
+ size="lg"
+ />
+ </div>
+ </teleport>
</div>
</div>
</template>
diff --git a/src/components/timeline/timeline_quick_settings.vue b/src/components/timeline/timeline_quick_settings.vue
index 4d67e06b..98fab926 100644
--- a/src/components/timeline/timeline_quick_settings.vue
+++ b/src/components/timeline/timeline_quick_settings.vue
@@ -12,8 +12,8 @@
@click="replyVisibilityAll = true"
>
<span
- class="menu-checkbox"
- :class="{ 'menu-checkbox-radio': replyVisibilityAll }"
+ class="menu-checkbox -radio"
+ :class="{ 'menu-checkbox-checked': replyVisibilityAll }"
/>{{ $t('settings.reply_visibility_all') }}
</button>
<button
@@ -21,8 +21,8 @@
@click="replyVisibilityFollowing = true"
>
<span
- class="menu-checkbox"
- :class="{ 'menu-checkbox-radio': replyVisibilityFollowing }"
+ class="menu-checkbox -radio"
+ :class="{ 'menu-checkbox-checked': replyVisibilityFollowing }"
/>{{ $t('settings.reply_visibility_following_short') }}
</button>
<button
@@ -30,8 +30,8 @@
@click="replyVisibilitySelf = true"
>
<span
- class="menu-checkbox"
- :class="{ 'menu-checkbox-radio': replyVisibilitySelf }"
+ class="menu-checkbox -radio"
+ :class="{ 'menu-checkbox-checked': replyVisibilitySelf }"
/>{{ $t('settings.reply_visibility_self_short') }}
</button>
<div
@@ -93,18 +93,16 @@
<style lang="scss">
.TimelineQuickSettings {
- align-self: stretch;
> button {
- font-size: 1.2em;
- padding-left: 0.7em;
- padding-right: 0.2em;
line-height: 100%;
height: 100%;
- }
+ width: var(--__panel-heading-height-inner);
+ text-align: center;
- .dropdown-item {
- margin: 0;
+ svg {
+ font-size: 1.2em;
+ }
}
}
diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue
index 8f14093f..61119482 100644
--- a/src/components/timeline_menu/timeline_menu.vue
+++ b/src/components/timeline_menu/timeline_menu.vue
@@ -43,6 +43,10 @@
min-width: 0;
width: 24rem;
+ .popover-trigger-button {
+ vertical-align: bottom;
+ }
+
.timeline-menu-popover-wrap {
overflow: hidden;
// Match panel heading padding to line up menu with bottom of heading
diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue
index 847d654b..f4d294df 100644
--- a/src/components/user_avatar/user_avatar.vue
+++ b/src/components/user_avatar/user_avatar.vue
@@ -1,24 +1,28 @@
<template>
- <StillImage
- v-if="user"
+ <span
class="Avatar"
- :alt="user.screen_name_ui"
- :title="user.screen_name_ui"
- :src="imgSrc(user.profile_image_url_original)"
- :class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
- :image-load-error="imageLoadError"
+ :class="{ '-compact': compact }"
>
+ <StillImage
+ v-if="user"
+ class="avatar"
+ :alt="user.screen_name_ui"
+ :title="user.screen_name_ui"
+ :src="imgSrc(user.profile_image_url_original)"
+ :image-load-error="imageLoadError"
+ :class="{ '-compact': compact, '-better-shadow': betterShadow }"
+ />
+ <div
+ v-else
+ class="avatar -placeholder"
+ :class="{ '-compact': compact }"
+ />
<FAIcon
v-if="bot"
icon="robot"
class="bot-indicator"
/>
- </StillImage>
- <div
- v-else
- class="Avatar -placeholder"
- :class="{ 'avatar-compact': compact }"
- />
+ </span>
</template>
<script src="./user_avatar.js"></script>
@@ -31,42 +35,60 @@
--_avatarShadowInset: var(--avatarStatusShadowInset);
--_still-image-label-visibility: hidden;
+ display: inline-block;
+ position: relative;
width: 48px;
height: 48px;
- box-shadow: var(--_avatarShadowBox);
- border-radius: $fallback--avatarRadius;
- border-radius: var(--avatarRadius, $fallback--avatarRadius);
- img {
+ &.-compact {
+ width: 32px;
+ height: 32px;
+ border-radius: $fallback--avatarAltRadius;
+ border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
+ }
+
+ .avatar {
width: 100%;
height: 100%;
- }
+ box-shadow: var(--_avatarShadowBox);
+ border-radius: $fallback--avatarRadius;
+ border-radius: var(--avatarRadius, $fallback--avatarRadius);
- & > .bot-indicator {
- position: absolute;
- bottom: 0;
- right: 0;
- }
+ &.-better-shadow {
+ box-shadow: var(--_avatarShadowInset);
+ filter: var(--_avatarShadowFilter);
+ }
- &.better-shadow {
- box-shadow: var(--_avatarShadowInset);
- filter: var(--_avatarShadowFilter);
- }
+ &.-animated::before {
+ display: none;
+ }
+
+ &.-compact {
+ border-radius: $fallback--avatarAltRadius;
+ border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
+ }
- &.animated::before {
- display: none;
+ &.-placeholder {
+ background-color: $fallback--fg;
+ background-color: var(--fg, $fallback--fg);
+ }
}
- &.avatar-compact {
- width: 32px;
- height: 32px;
- border-radius: $fallback--avatarAltRadius;
- border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
+ img {
+ width: 100%;
+ height: 100%;
}
- &.-placeholder {
- background-color: $fallback--fg;
- background-color: var(--fg, $fallback--fg);
+ .bot-indicator {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ margin: -0.2em;
+ padding: 0.2em;
+ background: rgba(127, 127, 127, 0.5);
+ color: #fff;
+ border-radius: var(--tooltipRadius);
}
+
}
</style>
diff --git a/src/components/user_card/user_card.scss b/src/components/user_card/user_card.scss
new file mode 100644
index 00000000..2e153120
--- /dev/null
+++ b/src/components/user_card/user_card.scss
@@ -0,0 +1,323 @@
+@import '../../_variables.scss';
+
+.user-card {
+ position: relative;
+ z-index: 1;
+
+ &:hover {
+ --_still-image-img-visibility: visible;
+ --_still-image-canvas-visibility: hidden;
+ --_still-image-label-visibility: hidden;
+ }
+
+ .panel-heading {
+ padding: .5em 0;
+ text-align: center;
+ box-shadow: none;
+ background: transparent;
+ flex-direction: column;
+ align-items: stretch;
+ // create new stacking context
+ position: relative;
+ }
+
+ .panel-body {
+ word-wrap: break-word;
+ border-bottom-right-radius: inherit;
+ border-bottom-left-radius: inherit;
+ // create new stacking context
+ position: relative;
+ }
+
+ .background-image {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ mask: linear-gradient(to top, white, transparent) bottom no-repeat,
+ linear-gradient(to top, white, white);
+ // Autoprefixer seem to ignore this one, and also syntax is different
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+ background-size: cover;
+ mask-size: 100% 60%;
+ border-top-left-radius: calc(var(--panelRadius) - 1px);
+ border-top-right-radius: calc(var(--panelRadius) - 1px);
+ background-color: var(--profileBg);
+ z-index: -2;
+
+ &.hide-bio {
+ mask-size: 100% 40px;
+ }
+ }
+
+ &-bio {
+ text-align: center;
+ display: block;
+ line-height: 1.3;
+ padding: 1em;
+ margin: 0;
+
+ a {
+ color: $fallback--link;
+ color: var(--postLink, $fallback--link);
+ }
+
+ img {
+ object-fit: contain;
+ vertical-align: middle;
+ max-width: 100%;
+ max-height: 400px;
+ }
+ }
+
+ // Modifiers
+
+ &-rounded-t {
+ border-top-left-radius: $fallback--panelRadius;
+ border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
+ border-top-right-radius: $fallback--panelRadius;
+ border-top-right-radius: var(--panelRadius, $fallback--panelRadius);
+ }
+
+ &-rounded {
+ border-radius: $fallback--panelRadius;
+ border-radius: var(--panelRadius, $fallback--panelRadius);
+ }
+
+ &-bordered {
+ border-width: 1px;
+ border-style: solid;
+ border-color: $fallback--border;
+ border-color: var(--border, $fallback--border);
+ }
+}
+
+.user-info {
+ color: $fallback--lightText;
+ color: var(--lightText, $fallback--lightText);
+ padding: 0 26px;
+
+ .container {
+ min-width: 0;
+ padding: 16px 0 6px;
+ display: flex;
+ align-items: flex-start;
+ max-height: 56px;
+
+ > * {
+ min-width: 0;
+ }
+
+ .Avatar {
+ --_avatarShadowBox: var(--avatarShadow);
+ --_avatarShadowFilter: var(--avatarShadowFilter);
+ --_avatarShadowInset: var(--avatarShadowInset);
+
+ flex: 1 0 100%;
+ width: 56px;
+ height: 56px;
+ object-fit: cover;
+ }
+ }
+
+ &-avatar-link {
+ position: relative;
+ cursor: pointer;
+
+ &-overlay {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.3);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: $fallback--avatarRadius;
+ border-radius: var(--avatarRadius, $fallback--avatarRadius);
+ opacity: 0;
+ transition: opacity .2s ease;
+
+ svg {
+ color: #FFF;
+ }
+ }
+
+ &:hover &-overlay {
+ opacity: 1;
+ }
+ }
+
+ .external-link-button, .edit-profile-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 {
+ display: block;
+ margin-left: 0.6em;
+ text-align: left;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1 1 0;
+ // This is so that text doesn't get overlapped by avatar's shadow if it has
+ // big one
+ z-index: 1;
+ line-height: 2em;
+
+ --emoji-size: 1.7em;
+
+ .top-line,
+ .bottom-line {
+ display: flex;
+ }
+ }
+
+ .user-name {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ flex: 1 1 auto;
+ margin-right: 1em;
+ font-size: 1.1em;
+ }
+
+ .bottom-line {
+ font-weight: light;
+ font-size: 1.1em;
+ align-items: baseline;
+
+ .lock-icon {
+ margin-left: 0.5em;
+ }
+
+ .user-screen-name {
+ min-width: 1px;
+ flex: 0 1 auto;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ color: $fallback--lightText;
+ color: var(--lightText, $fallback--lightText);
+ }
+
+ .dailyAvg {
+ min-width: 1px;
+ flex: 0 0 auto;
+ margin-left: 1em;
+ font-size: 0.7em;
+ color: $fallback--text;
+ color: var(--text, $fallback--text);
+ }
+
+ .user-role {
+ flex: none;
+ color: $fallback--text;
+ color: var(--alertNeutralText, $fallback--text);
+ background-color: $fallback--fg;
+ background-color: var(--alertNeutral, $fallback--fg);
+ }
+ }
+
+ .user-meta {
+ margin-bottom: .15em;
+ display: flex;
+ align-items: baseline;
+ line-height: 22px;
+ flex-wrap: wrap;
+
+ .following {
+ flex: 1 0 auto;
+ margin: 0;
+ margin-bottom: .25em;
+ text-align: left;
+ }
+
+ .highlighter {
+ flex: 0 1 auto;
+ display: flex;
+ flex-wrap: wrap;
+ margin-right: -.5em;
+ align-self: start;
+
+ .userHighlightCl {
+ padding: 2px 10px;
+ flex: 1 0 auto;
+ }
+
+ .userHighlightSel {
+ padding-top: 0;
+ padding-bottom: 0;
+ flex: 1 0 auto;
+ }
+
+ .userHighlightText {
+ width: 70px;
+ flex: 1 0 auto;
+ }
+
+ .userHighlightCl,
+ .userHighlightText,
+ .userHighlightSel {
+ vertical-align: top;
+ margin-right: .5em;
+ margin-bottom: .25em;
+ }
+ }
+ }
+ .user-interactions {
+ position: relative;
+ display: flex;
+ flex-flow: row wrap;
+ margin-right: -.75em;
+
+ > * {
+ margin: 0 .75em .6em 0;
+ white-space: nowrap;
+ min-width: 95px;
+ }
+
+ button {
+ margin: 0;
+ }
+ }
+}
+
+.sidebar .edit-profile-button {
+ display: none;
+}
+
+.user-counts {
+ display: flex;
+ line-height:16px;
+ padding: .5em 1.5em 0em 1.5em;
+ text-align: center;
+ justify-content: space-between;
+ color: $fallback--lightText;
+ color: var(--lightText, $fallback--lightText);
+ flex-wrap: wrap;
+}
+
+.user-count {
+ flex: 1 0 auto;
+ padding: .5em 0 .5em 0;
+ margin: 0 .5em;
+
+ h5 {
+ font-size:1em;
+ font-weight: bolder;
+ margin: 0 0 0.25em;
+ }
+ a {
+ text-decoration: none;
+ }
+}
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 0708f387..67837845 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -8,7 +8,7 @@
:style="style"
class="background-image"
/>
- <div class="panel-heading">
+ <div class="panel-heading -flexible-height">
<div class="user-info">
<div class="container">
<a
@@ -141,6 +141,7 @@
class="userHighlightCl"
type="color"
>
+ {{ ' ' }}
<Select
:id="'userHighlightSel'+user.id"
v-model="userHighlightType"
@@ -283,320 +284,4 @@
<script src="./user_card.js"></script>
-<style lang="scss">
-@import '../../_variables.scss';
-
-.user-card {
- position: relative;
-
- &:hover {
- --_still-image-img-visibility: visible;
- --_still-image-canvas-visibility: hidden;
- --_still-image-label-visibility: hidden;
- }
-
- .panel-heading {
- padding: .5em 0;
- text-align: center;
- box-shadow: none;
- background: transparent;
- flex-direction: column;
- align-items: stretch;
- // create new stacking context
- position: relative;
- }
-
- .panel-body {
- word-wrap: break-word;
- border-bottom-right-radius: inherit;
- border-bottom-left-radius: inherit;
- // create new stacking context
- position: relative;
- }
-
- .background-image {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- 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;
- mask-composite: exclude;
- background-size: cover;
- mask-size: 100% 60%;
- border-top-left-radius: calc(var(--panelRadius) - 1px);
- border-top-right-radius: calc(var(--panelRadius) - 1px);
- background-color: var(--profileBg);
-
- &.hide-bio {
- mask-size: 100% 40px;
- }
- }
-
- &-bio {
- text-align: center;
- display: block;
- line-height: 18px;
- padding: 1em;
- margin: 0;
-
- a {
- color: $fallback--link;
- color: var(--postLink, $fallback--link);
- }
-
- img {
- object-fit: contain;
- vertical-align: middle;
- max-width: 100%;
- max-height: 400px;
- }
- }
-
- // Modifiers
-
- &-rounded-t {
- border-top-left-radius: $fallback--panelRadius;
- border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
- border-top-right-radius: $fallback--panelRadius;
- border-top-right-radius: var(--panelRadius, $fallback--panelRadius);
- }
-
- &-rounded {
- border-radius: $fallback--panelRadius;
- border-radius: var(--panelRadius, $fallback--panelRadius);
- }
-
- &-bordered {
- border-width: 1px;
- border-style: solid;
- border-color: $fallback--border;
- border-color: var(--border, $fallback--border);
- }
-}
-
-.user-info {
- color: $fallback--lightText;
- color: var(--lightText, $fallback--lightText);
- padding: 0 26px;
-
- .container {
- padding: 16px 0 6px;
- display: flex;
- align-items: flex-start;
- max-height: 56px;
-
- .Avatar {
- --_avatarShadowBox: var(--avatarShadow);
- --_avatarShadowFilter: var(--avatarShadowFilter);
- --_avatarShadowInset: var(--avatarShadowInset);
-
- flex: 1 0 100%;
- width: 56px;
- height: 56px;
- object-fit: cover;
- }
- }
-
- &-avatar-link {
- position: relative;
- cursor: pointer;
-
- &-overlay {
- position: absolute;
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
- background-color: rgba(0, 0, 0, 0.3);
- display: flex;
- justify-content: center;
- align-items: center;
- border-radius: $fallback--avatarRadius;
- border-radius: var(--avatarRadius, $fallback--avatarRadius);
- opacity: 0;
- transition: opacity .2s ease;
-
- svg {
- color: #FFF;
- }
- }
-
- &:hover &-overlay {
- opacity: 1;
- }
- }
-
- .external-link-button, .edit-profile-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 {
- display: block;
- margin-left: 0.6em;
- text-align: left;
- text-overflow: ellipsis;
- white-space: nowrap;
- flex: 1 1 0;
- // This is so that text doesn't get overlapped by avatar's shadow if it has
- // big one
- z-index: 1;
-
- .top-line {
- display: flex;
- }
- }
-
- .user-name {
- text-overflow: ellipsis;
- overflow: hidden;
- flex: 1 1 auto;
- margin-right: 1em;
- font-size: 15px;
-
- --emoji-size: 14px;
- }
-
- .bottom-line {
- display: flex;
- font-weight: light;
- font-size: 15px;
-
- .lock-icon {
- margin-left: 0.5em;
- }
-
- .user-screen-name {
- min-width: 1px;
- flex: 0 1 auto;
- text-overflow: ellipsis;
- overflow: hidden;
- color: $fallback--lightText;
- color: var(--lightText, $fallback--lightText);
- }
-
- .dailyAvg {
- min-width: 1px;
- flex: 0 0 auto;
- margin-left: 1em;
- font-size: 0.7em;
- color: $fallback--text;
- color: var(--text, $fallback--text);
- }
-
- .user-role {
- flex: none;
- color: $fallback--text;
- color: var(--alertNeutralText, $fallback--text);
- background-color: $fallback--fg;
- background-color: var(--alertNeutral, $fallback--fg);
- }
- }
-
- .user-meta {
- margin-bottom: .15em;
- display: flex;
- align-items: baseline;
- font-size: 14px;
- line-height: 22px;
- flex-wrap: wrap;
-
- .following {
- flex: 1 0 auto;
- margin: 0;
- margin-bottom: .25em;
- text-align: left;
- }
-
- .highlighter {
- flex: 0 1 auto;
- display: flex;
- flex-wrap: wrap;
- margin-right: -.5em;
- align-self: start;
-
- .userHighlightCl {
- padding: 2px 10px;
- flex: 1 0 auto;
- }
-
- .userHighlightSel {
- padding-top: 0;
- padding-bottom: 0;
- flex: 1 0 auto;
- }
-
- .userHighlightText {
- width: 70px;
- flex: 1 0 auto;
- }
-
- .userHighlightCl,
- .userHighlightText,
- .userHighlightSel {
- vertical-align: top;
- margin-right: .5em;
- margin-bottom: .25em;
- }
- }
- }
- .user-interactions {
- position: relative;
- display: flex;
- flex-flow: row wrap;
- margin-right: -.75em;
-
- > * {
- margin: 0 .75em .6em 0;
- white-space: nowrap;
- min-width: 95px;
- }
-
- button {
- margin: 0;
- }
- }
-}
-
-.sidebar .edit-profile-button {
- display: none;
-}
-
-.user-counts {
- display: flex;
- line-height:16px;
- padding: .5em 1.5em 0em 1.5em;
- text-align: center;
- justify-content: space-between;
- color: $fallback--lightText;
- color: var(--lightText, $fallback--lightText);
- flex-wrap: wrap;
-}
-
-.user-count {
- flex: 1 0 auto;
- padding: .5em 0 .5em 0;
- margin: 0 .5em;
-
- h5 {
- font-size:1em;
- font-weight: bolder;
- margin: 0 0 0.25em;
- }
- a {
- text-decoration: none;
- }
-}
-</style>
+<style lang="scss" src="./user_card.scss" />
diff --git a/src/components/user_list_popover/user_list_popover.js b/src/components/user_list_popover/user_list_popover.js
index 32ca2b8d..e24eb9f7 100644
--- a/src/components/user_list_popover/user_list_popover.js
+++ b/src/components/user_list_popover/user_list_popover.js
@@ -1,3 +1,6 @@
+import { defineAsyncComponent } from 'vue'
+import RichContent from 'src/components/rich_content/rich_content.jsx'
+
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
@@ -11,8 +14,9 @@ const UserListPopover = {
'users'
],
components: {
- Popover: () => import('../popover/popover.vue'),
- UserAvatar: () => import('../user_avatar/user_avatar.vue')
+ RichContent,
+ Popover: defineAsyncComponent(() => import('../popover/popover.vue')),
+ UserAvatar: defineAsyncComponent(() => import('../user_avatar/user_avatar.vue'))
},
computed: {
usersCapped () {
diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue
index 8706d0ff..bdc3aa92 100644
--- a/src/components/user_list_popover/user_list_popover.vue
+++ b/src/components/user_list_popover/user_list_popover.vue
@@ -73,7 +73,7 @@
}
.user-list-screen-name {
- font-size: 9px;
+ font-size: 0.65em;
}
}
}
diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue
index 5685916a..243de387 100644
--- a/src/components/user_panel/user_panel.vue
+++ b/src/components/user_panel/user_panel.vue
@@ -2,7 +2,7 @@
<div class="user-panel">
<div
v-if="signedIn"
- key="user-panel"
+ key="user-panel-signed"
class="panel panel-default signed-in"
>
<UserCard
@@ -24,5 +24,6 @@
<style lang="scss">
.user-panel .signed-in {
overflow: visible;
+ z-index: 10;
}
</style>
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 7a475609..f779b823 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -3,7 +3,7 @@ import UserCard from '../user_card/user_card.vue'
import FollowCard from '../follow_card/follow_card.vue'
import Timeline from '../timeline/timeline.vue'
import Conversation from '../conversation/conversation.vue'
-import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import List from '../list/list.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more'
@@ -39,7 +39,8 @@ const UserProfile = {
return {
error: false,
userId: null,
- tab: defaultTabKey
+ tab: defaultTabKey,
+ footerRef: null
}
},
created () {
@@ -47,7 +48,7 @@ const UserProfile = {
this.load(routeParams.name || routeParams.id)
this.tab = get(this.$route, 'query.tab', defaultTabKey)
},
- destroyed () {
+ unmounted () {
this.stopFetching()
},
computed: {
@@ -78,6 +79,9 @@ const UserProfile = {
}
},
methods: {
+ setFooterRef (el) {
+ this.footerRef = el
+ },
load (userNameOrId) {
const startFetchingTimeline = (timeline, userId) => {
// Clear timeline only if load another user's profile
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 726216ff..62792599 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -56,6 +56,7 @@
:user-id="userId"
:pinned-status-ids="user.pinnedStatusIds"
:in-profile="true"
+ :footerSlipgate="footerRef"
/>
<div
v-if="followsTabVisible"
@@ -94,6 +95,7 @@
:timeline="media"
:user-id="userId"
:in-profile="true"
+ :footerSlipgate="footerRef"
/>
<Timeline
v-if="isUs"
@@ -105,8 +107,10 @@
timeline-name="favorites"
:timeline="favorites"
:in-profile="true"
+ :footerSlipgate="footerRef"
/>
</tab-switcher>
+ <div class="panel-footer" :ref="setFooterRef"></div>
</div>
<div
v-else
@@ -138,6 +142,9 @@
flex: 2;
flex-basis: 500px;
+ // No sticky header on user profile
+ --currentPanelStack: 1;
+
.user-profile-fields {
margin: 0 0.5em;
@@ -176,7 +183,7 @@
}
.user-profile-field-name, .user-profile-field-value {
- line-height: 18px;
+ line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
@@ -192,24 +199,6 @@
align-items: middle;
padding: 2em;
}
-
- .timeline-heading {
- display: flex;
- justify-content: center;
-
- .loadmore-button, .alert {
- flex: 1;
- }
-
- .loadmore-button {
- height: 28px;
- margin: 10px .6em;
- }
-
- .title, .loadmore-text {
- display: none
- }
- }
}
.user-profile-placeholder {
.panel-body {
diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue
index 1f67a5cc..030ce2c4 100644
--- a/src/components/user_reporting_modal/user_reporting_modal.vue
+++ b/src/components/user_reporting_modal/user_reporting_modal.vue
@@ -53,8 +53,8 @@
:statusoid="item"
/>
<Checkbox
- :checked="isChecked(item.id)"
- @change="checked => toggleStatus(checked, item.id)"
+ :model-value="isChecked(item.id)"
+ @update:model-value="checked => toggleStatus(checked, item.id)"
/>
</div>
</template>
@@ -76,17 +76,6 @@
min-height: 20vh;
max-height: 80vh;
- .panel-heading {
- .title {
- text-align: center;
- // TODO: Consider making these as default of panel
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- }
-
.panel-body {
display: flex;
flex-direction: column-reverse;
@@ -98,7 +87,7 @@
&-left {
padding: 1.1em 0.7em 0.7em;
- line-height: 1.4em;
+ line-height: var(--post-line-height);
box-sizing: border-box;
> div {
diff --git a/src/directives/body_scroll_lock.js b/src/directives/body_scroll_lock.js
index 13a6de1c..b6d16790 100644
--- a/src/directives/body_scroll_lock.js
+++ b/src/directives/body_scroll_lock.js
@@ -50,12 +50,12 @@ const enableBodyScroll = (el) => {
}
const directive = {
- inserted: (el, binding) => {
+ mounted: (el, binding) => {
if (binding.value) {
disableBodyScroll(el)
}
},
- componentUpdated: (el, binding) => {
+ updated: (el, binding) => {
if (binding.oldValue === binding.value) {
return
}
@@ -66,7 +66,7 @@ const directive = {
enableBodyScroll(el)
}
},
- unbind: (el) => {
+ unmounted: (el) => {
enableBodyScroll(el)
}
}
diff --git a/src/hocs/with_load_more/with_load_more.js b/src/hocs/with_load_more/with_load_more.jsx
index 671b2b6f..c0ae1856 100644
--- a/src/hocs/with_load_more/with_load_more.js
+++ b/src/hocs/with_load_more/with_load_more.jsx
@@ -1,4 +1,5 @@
-import Vue from 'vue'
+// eslint-disable-next-line no-unused
+import { h } from 'vue'
import isEmpty from 'lodash/isEmpty'
import { getComponentProps } from '../../services/component_utils/component_utils'
import './with_load_more.scss'
@@ -16,14 +17,14 @@ library.add(
const withLoadMore = ({
fetch, // function to fetch entries and return a promise
select, // function to select data from store
- destroy, // function called at "destroyed" lifecycle
+ unmounted, // function called at "destroyed" lifecycle
childPropName = 'entries', // name of the prop to be passed into the wrapped component
additionalPropNames = [] // additional prop name list of the wrapper component
}) => (WrappedComponent) => {
const originalProps = Object.keys(getComponentProps(WrappedComponent))
const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames)
- return Vue.component('withLoadMore', {
+ return {
props,
data () {
return {
@@ -39,9 +40,9 @@ const withLoadMore = ({
this.fetchEntries()
}
},
- destroyed () {
+ unmounted () {
window.removeEventListener('scroll', this.scrollLoad)
- destroy && destroy(this.$props, this.$store)
+ unmounted && unmounted(this.$props, this.$store)
},
methods: {
// Entries is not a computed because computed can't track the dynamic
@@ -79,16 +80,12 @@ const withLoadMore = ({
}
}
},
- render (h) {
+ render () {
const props = {
- props: {
- ...this.$props,
- [childPropName]: this.entries
- },
- on: this.$listeners,
- scopedSlots: this.$scopedSlots
+ ...this.$props,
+ [childPropName]: this.entries
}
- const children = Object.entries(this.$slots).map(([key, value]) => h('template', { slot: key }, value))
+ const children = this.$slots
return (
<div class="with-load-more">
<WrappedComponent {...props}>
@@ -106,7 +103,7 @@ const withLoadMore = ({
</div>
)
}
- })
+ }
}
export default withLoadMore
diff --git a/src/hocs/with_load_more/with_load_more.scss b/src/hocs/with_load_more/with_load_more.scss
index 1a26eb8d..de86ed4a 100644
--- a/src/hocs/with_load_more/with_load_more.scss
+++ b/src/hocs/with_load_more/with_load_more.scss
@@ -10,7 +10,7 @@
border-top-color: var(--border, $fallback--border);
.error {
- font-size: 14px;
+ font-size: 1rem;
}
a {
diff --git a/src/hocs/with_subscription/with_subscription.js b/src/hocs/with_subscription/with_subscription.jsx
index b1244276..d3f5506a 100644
--- a/src/hocs/with_subscription/with_subscription.js
+++ b/src/hocs/with_subscription/with_subscription.jsx
@@ -1,4 +1,5 @@
-import Vue from 'vue'
+// eslint-disable-next-line no-unused
+import { h } from 'vue'
import isEmpty from 'lodash/isEmpty'
import { getComponentProps } from '../../services/component_utils/component_utils'
import './with_subscription.scss'
@@ -22,7 +23,7 @@ const withSubscription = ({
const originalProps = Object.keys(getComponentProps(WrappedComponent))
const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames)
- return Vue.component('withSubscription', {
+ return {
props: [
...props,
'refresh' // boolean saying to force-fetch data whenever created
@@ -59,17 +60,13 @@ const withSubscription = ({
}
}
},
- render (h) {
+ render () {
if (!this.error && !this.loading) {
const props = {
- props: {
- ...this.$props,
- [childPropName]: this.fetchedData
- },
- on: this.$listeners,
- scopedSlots: this.$scopedSlots
+ ...this.$props,
+ [childPropName]: this.fetchedData
}
- const children = Object.entries(this.$slots).map(([key, value]) => h('template', { slot: key }, value))
+ const children = this.$slots
return (
<div class="with-subscription">
<WrappedComponent {...props}>
@@ -88,7 +85,7 @@ const withSubscription = ({
)
}
}
- })
+ }
}
export default withSubscription
diff --git a/src/hocs/with_subscription/with_subscription.scss b/src/hocs/with_subscription/with_subscription.scss
index 52c7d94c..7fd83802 100644
--- a/src/hocs/with_subscription/with_subscription.scss
+++ b/src/hocs/with_subscription/with_subscription.scss
@@ -4,7 +4,7 @@
text-align: center;
.error {
- font-size: 14px;
+ font-size: 1rem;
}
}
} \ No newline at end of file
diff --git a/src/i18n/ca.json b/src/i18n/ca.json
index 74260143..5f2795a8 100644
--- a/src/i18n/ca.json
+++ b/src/i18n/ca.json
@@ -621,7 +621,6 @@
"disable_any_subscription": "Deshabilita completament seguir algú",
"quarantine": "Deshabilita la federació a les entrades de les usuàries",
"moderation": "Moderació",
- "delete_user_confirmation": "Estàs completament segur/a? Aquesta acció no es pot desfer.",
"revoke_admin": "Revoca l'Admin",
"activate_account": "Activa el compte",
"deactivate_account": "Desactiva el compte",
diff --git a/src/i18n/de.json b/src/i18n/de.json
index b6599594..4bf897ef 100644
--- a/src/i18n/de.json
+++ b/src/i18n/de.json
@@ -582,7 +582,6 @@
"statuses": "Beiträge",
"admin_menu": {
"sandbox": "Erzwinge Beiträge nur für Follower sichtbar zu sein",
- "delete_user_confirmation": "Achtung! Diese Entscheidung kann nicht rückgängig gemacht werden! Trotzdem durchführen?",
"grant_admin": "Administratorprivilegien gewähren",
"delete_user": "Nutzer löschen",
"strip_media": "Medien von Beiträgen entfernen",
diff --git a/src/i18n/en.json b/src/i18n/en.json
index d2abac1d..51e1a0c3 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -46,7 +46,7 @@
"processing": "Processing, you'll soon be asked to download your file"
},
"features_panel": {
- "chat": "Chat",
+ "shout": "Shoutbox",
"pleroma_chat_messages": "Pleroma Chat",
"gopher": "Gopher",
"media_proxy": "Media proxy",
@@ -86,7 +86,13 @@
},
"flash_content": "Click to show Flash content using Ruffle (Experimental, may not work).",
"flash_security": "Note that this can be potentially dangerous since Flash content is still arbitrary code.",
- "flash_fail": "Failed to load flash content, see console for details."
+ "flash_fail": "Failed to load flash content, see console for details.",
+ "scope_in_timeline": {
+ "direct": "Direct",
+ "private": "Followers-only",
+ "public": "Public",
+ "unlisted": "Unlisted"
+ }
},
"image_cropper": {
"crop_picture": "Crop picture",
@@ -156,7 +162,8 @@
"no_more_notifications": "No more notifications",
"migrated_to": "migrated to",
"reacted_with": "reacted with {0}",
- "submitted_report": "submitted a report"
+ "submitted_report": "submitted a report",
+ "poll_ended": "poll has ended"
},
"polls": {
"add_poll": "Add poll",
@@ -252,7 +259,8 @@
"password_required": "cannot be left blank",
"password_confirmation_required": "cannot be left blank",
"password_confirmation_match": "should be the same as password"
- }
+ },
+ "email_language": "In which language do you want to receive emails from the server?"
},
"remote_user_resolver": {
"remote_user_resolver": "Remote user resolver",
@@ -311,6 +319,7 @@
"avatarRadius": "Avatars",
"background": "Background",
"bio": "Bio",
+ "email_language": "Language for receiving emails from the server",
"block_export": "Block export",
"block_export_button": "Export your blocks to a csv file",
"block_import": "Block import",
@@ -322,6 +331,16 @@
"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",
+ "account_backup": "Account backup",
+ "account_backup_description": "This allows you to download an archive of your account information and your posts, but they cannot yet be imported into a Pleroma account.",
+ "account_backup_table_head": "Backup",
+ "download_backup": "Download",
+ "backup_not_ready": "This backup is not ready yet.",
+ "remove_backup": "Remove",
+ "list_backups_error": "Error fetching backup list: {error}",
+ "add_backup": "Create a new backup",
+ "added_backup": "Added a new backup.",
+ "add_backup_error": "Error adding a new backup: {error}",
"blocks_tab": "Blocks",
"bot": "This is a bot account",
"btnRadius": "Buttons",
@@ -347,6 +366,19 @@
"delete_account_description": "Permanently delete your data and deactivate your account.",
"delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.",
"delete_account_instructions": "Type your password in the input below to confirm account deletion.",
+ "account_alias": "Account aliases",
+ "account_alias_table_head": "Alias",
+ "list_aliases_error": "Error fetching aliases: {error}",
+ "hide_list_aliases_error_action": "Close",
+ "remove_alias": "Remove this alias",
+ "new_alias_target": "Add a new alias (e.g. {example})",
+ "added_alias": "Alias is added.",
+ "add_alias_error": "Error adding alias: {error}",
+ "move_account": "Move account",
+ "move_account_notes": "If you want to move the account somewhere else, you must go to your target account and add an alias pointing here.",
+ "move_account_target": "Target account (e.g. {example})",
+ "moved_account": "Account is moved.",
+ "move_account_error": "Error moving account: {error}",
"discoverable": "Allow discovery of this account in search results and other services",
"domain_mutes": "Domains",
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
@@ -375,7 +407,7 @@
"max_thumbnails": "Maximum amount of thumbnails per post (empty = no limit)",
"hide_isp": "Hide instance-specific panel",
"hide_shoutbox": "Hide instance shoutbox",
- "right_sidebar": "Show sidebar on the right side",
+ "right_sidebar": "Reverse order of columns",
"always_show_post_button": "Always show floating New Post button",
"hide_wallpaper": "Hide instance wallpaper",
"preload_images": "Preload images",
@@ -435,6 +467,7 @@
"notification_visibility_repeats": "Repeats",
"notification_visibility_moves": "User Migrates",
"notification_visibility_emoji_reactions": "Reactions",
+ "notification_visibility_polls": "Ends of polls you voted in",
"no_rich_text_description": "Strip rich text formatting from all posts",
"no_blocks": "No blocks",
"no_mutes": "No mutes",
@@ -489,6 +522,12 @@
"subject_line_noop": "Do not copy",
"conversation_display": "Conversation display style",
"conversation_display_tree": "Tree-style",
+ "disable_sticky_headers": "Don't stick column headers to top of the screen",
+ "show_scrollbars": "Show side column's scrollbars",
+ "third_column_mode": "When there's enough space, show third column containing",
+ "third_column_mode_none": "Don't show third column at all",
+ "third_column_mode_notifications": "Notifications column",
+ "third_column_mode_postform": "Main post form and navigation",
"tree_advanced": "Allow more flexible navigation in tree view",
"tree_fade_ancestors": "Display ancestors of the current status in faint text",
"conversation_display_linear": "Linear-style",
@@ -516,14 +555,14 @@
"true": "yes"
},
"virtual_scrolling": "Optimize timeline rendering",
- "use_at_icon": "Display @ symbol as an icon instead of text",
+ "use_at_icon": "Display {'@'} symbol as an icon instead of text",
"mention_link_display": "Display mention links",
- "mention_link_display_short": "always as short names (e.g. @foo)",
- "mention_link_display_full_for_remote": "as full names only for remote users (e.g. @foo@example.org)",
- "mention_link_display_full": "always as full names (e.g. @foo@example.org)",
+ "mention_link_display_short": "always as short names (e.g. {'@'}foo)",
+ "mention_link_display_full_for_remote": "as full names only for remote users (e.g. {'@'}foo{'@'}example.org)",
+ "mention_link_display_full": "always as full names (e.g. {'@'}foo{'@'}example.org)",
"mention_link_show_tooltip": "Show full user names as tooltip for remote users",
"mention_link_show_avatar": "Show user avatar beside the link",
- "mention_link_fade_domain": "Fade domains (e.g. @example.org in @foo@example.org)",
+ "mention_link_fade_domain": "Fade domains (e.g. {'@'}example.org in {'@'}foo{'@'}example.org)",
"mention_link_bolden_you": "Highlight mention of you when you are mentioned",
"fun": "Fun",
"greentext": "Meme arrows",
@@ -690,38 +729,26 @@
}
},
"time": {
- "day": "{0} day",
- "days": "{0} days",
- "day_short": "{0}d",
- "days_short": "{0}d",
- "hour": "{0} hour",
- "hours": "{0} hours",
- "hour_short": "{0}h",
- "hours_short": "{0}h",
+ "unit": {
+ "days": "{0} day | {0} days",
+ "days_short": "{0}d",
+ "hours": "{0} hour | {0} hours",
+ "hours_short": "{0}h",
+ "minutes": "{0} minute | {0} minutes",
+ "minutes_short": "{0}min",
+ "months": "{0} month | {0} months",
+ "months_short": "{0}mo",
+ "seconds": "{0} second | {0} seconds",
+ "seconds_short": "{0}s",
+ "weeks": "{0} week | {0} weeks",
+ "weeks_short": "{0}w",
+ "years": "{0} year | {0} years",
+ "years_short": "{0}y"
+ },
"in_future": "in {0}",
"in_past": "{0} ago",
- "minute": "{0} minute",
- "minutes": "{0} minutes",
- "minute_short": "{0}min",
- "minutes_short": "{0}min",
- "month": "{0} month",
- "months": "{0} months",
- "month_short": "{0}mo",
- "months_short": "{0}mo",
"now": "just now",
- "now_short": "now",
- "second": "{0} second",
- "seconds": "{0} seconds",
- "second_short": "{0}s",
- "seconds_short": "{0}s",
- "week": "{0} week",
- "weeks": "{0} weeks",
- "week_short": "{0}w",
- "weeks_short": "{0}w",
- "year": "{0} year",
- "years": "{0} years",
- "year_short": "{0}y",
- "years_short": "{0}y"
+ "now_short": "now"
},
"timeline": {
"collapse": "Collapse",
@@ -847,7 +874,7 @@
"disable_any_subscription": "Disallow following user at all",
"quarantine": "Disallow user posts from federating",
"delete_user": "Delete user",
- "delete_user_confirmation": "Are you absolutely sure? This action cannot be undone."
+ "delete_user_data_and_deactivate_confirmation": "This will permanently delete the data from this account and deactivate it. Are you absolutely sure?"
},
"highlight": {
"disabled": "No highlight",
diff --git a/src/i18n/eo.json b/src/i18n/eo.json
index 659b5960..3c401b30 100644
--- a/src/i18n/eo.json
+++ b/src/i18n/eo.json
@@ -606,7 +606,6 @@
"mention": "Mencio",
"hidden": "Kaŝita",
"admin_menu": {
- "delete_user_confirmation": "Ĉu vi tute certas? Ĉi tiu ago ne estas malfarebla.",
"delete_user": "Forigi uzanton",
"quarantine": "Malpermesi federadon de afiŝoj de uzanto",
"disable_any_subscription": "Malpermesi ĉian abonadon al uzanto",
diff --git a/src/i18n/es.json b/src/i18n/es.json
index eb9fc0a5..9887f007 100644
--- a/src/i18n/es.json
+++ b/src/i18n/es.json
@@ -731,8 +731,7 @@
"disable_remote_subscription": "No permitir que usuarios de instancias remotas te siga",
"disable_any_subscription": "No permitir que ningún usuario te siga",
"quarantine": "No permitir publicaciones de usuarios de instancias remotas",
- "delete_user": "Eliminar usuario",
- "delete_user_confirmation": "¿Estás completamente seguro? Esta acción no se puede deshacer."
+ "delete_user": "Eliminar usuario"
},
"show_repeats": "Mostrar repetidos",
"hide_repeats": "Ocultar repetidos",
diff --git a/src/i18n/eu.json b/src/i18n/eu.json
index 539ee1bd..4e6ea550 100644
--- a/src/i18n/eu.json
+++ b/src/i18n/eu.json
@@ -609,8 +609,7 @@
"disable_remote_subscription": "Ez utzi istantzia kanpoko erabiltzaileak zuri jarraitzea",
"disable_any_subscription": "Ez utzi beste erabiltzaileak zuri jarraitzea",
"quarantine": "Ez onartu mezuak beste instantzietatik",
- "delete_user": "Erabiltzailea ezabatu",
- "delete_user_confirmation": "Erabat ziur zaude? Ekintza hau ezin da desegin."
+ "delete_user": "Erabiltzailea ezabatu"
}
},
"user_profile": {
diff --git a/src/i18n/fi.json b/src/i18n/fi.json
index 7b5244cb..f8c3b4ae 100644
--- a/src/i18n/fi.json
+++ b/src/i18n/fi.json
@@ -620,8 +620,7 @@
"sandbox": "Pakota viestit vain seuraajille",
"disable_remote_subscription": "Estä seuraaminen ulkopuolisilta sivuilta",
"quarantine": "Estä käyttäjän viestin federoituminen",
- "delete_user": "Poista käyttäjä",
- "delete_user_confirmation": "Oletko aivan varma? Tätä ei voi kumota."
+ "delete_user": "Poista käyttäjä"
},
"favorites": "Tykkäykset",
"mention": "Mainitse",
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index 6d3c75d1..fae7c7a2 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -659,8 +659,7 @@
"disable_remote_subscription": "Interdir de s'abonner a l'utilisateur depuis l'instance distante",
"disable_any_subscription": "Interdir de s'abonner à l'utilisateur tout court",
"quarantine": "Interdir les statuts de l'utilisateur à fédérer",
- "delete_user": "Supprimer l'utilisateur",
- "delete_user_confirmation": "Êtes-vous absolument-sûr⋅e ? Cette action ne peut être annulée."
+ "delete_user": "Supprimer l'utilisateur"
},
"mention": "Mention",
"hidden": "Caché",
diff --git a/src/i18n/he.json b/src/i18n/he.json
index b0c59a30..6c62acc4 100644
--- a/src/i18n/he.json
+++ b/src/i18n/he.json
@@ -347,8 +347,7 @@
"disable_remote_subscription": "אל תאפשר עקיבה של המשתמש מאינסטנס אחר",
"disable_any_subscription": "אל תאפשר עקיבה של המשתמש בכלל",
"quarantine": "אל תאפשר פדרציה של ההודעות של המשתמש",
- "delete_user": "מחק משתמש",
- "delete_user_confirmation": "בטוח? פעולה זו הינה בלתי הפיכה."
+ "delete_user": "מחק משתמש"
}
},
"user_profile": {
diff --git a/src/i18n/id.json b/src/i18n/id.json
index fd64b7ae..73cc2a71 100644
--- a/src/i18n/id.json
+++ b/src/i18n/id.json
@@ -327,8 +327,7 @@
"delete_account": "Hapus akun",
"force_nsfw": "Tandai semua postingan sebagai NSFW",
"strip_media": "Hapus media dari postingan-postingan",
- "delete_user": "Hapus pengguna",
- "delete_user_confirmation": "Apakah Anda benar-benar yakin? Tindakan ini tidak dapat dibatalkan."
+ "delete_user": "Hapus pengguna"
},
"follow_unfollow": "Berhenti mengikuti",
"followees": "Mengikuti",
diff --git a/src/i18n/it.json b/src/i18n/it.json
index a1ec37a2..c8c74b70 100644
--- a/src/i18n/it.json
+++ b/src/i18n/it.json
@@ -485,7 +485,6 @@
"deny": "Nega",
"remote_follow": "Segui da remoto",
"admin_menu": {
- "delete_user_confirmation": "Ne sei completamente sicuro? Non potrai tornare indietro.",
"delete_user": "Elimina utente",
"quarantine": "I messaggi non arriveranno alle altre stanze",
"disable_any_subscription": "Rendi utente non seguibile",
diff --git a/src/i18n/ja_easy.json b/src/i18n/ja_easy.json
index f64943d9..abca262b 100644
--- a/src/i18n/ja_easy.json
+++ b/src/i18n/ja_easy.json
@@ -608,8 +608,7 @@
"disable_remote_subscription": "ほかのインスタンスからフォローされないようにする",
"disable_any_subscription": "フォローされないようにする",
"quarantine": "ほかのインスタンスのユーザーのとうこうをとめる",
- "delete_user": "ユーザーをけす",
- "delete_user_confirmation": "あなたは、ほんとうに、きはたしかですか? これは、とりけすことが、できません。"
+ "delete_user": "ユーザーをけす"
}
},
"user_profile": {
diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json
index be334651..fddf24db 100644
--- a/src/i18n/ja_pedantic.json
+++ b/src/i18n/ja_pedantic.json
@@ -729,8 +729,7 @@
"disable_remote_subscription": "他のインスタンスからフォローされないようにする",
"disable_any_subscription": "フォローされないようにする",
"quarantine": "他のインスタンスからの投稿を止める",
- "delete_user": "ユーザーを削除",
- "delete_user_confirmation": "あなたの精神状態に何か問題はございませんか? この操作を取り消すことはできません。"
+ "delete_user": "ユーザーを削除"
},
"roles": {
"moderator": "モデレーター",
diff --git a/src/i18n/messages.js b/src/i18n/messages.js
index 2a1161be..18ed79b7 100644
--- a/src/i18n/messages.js
+++ b/src/i18n/messages.js
@@ -32,6 +32,7 @@ const loaders = {
pt: () => import('./pt.json'),
ro: () => import('./ro.json'),
ru: () => import('./ru.json'),
+ sk: () => import('./sk.json'),
te: () => import('./te.json'),
uk: () => import('./uk.json'),
zh: () => import('./zh.json'),
@@ -41,12 +42,12 @@ const loaders = {
const messages = {
languages: ['en', ...Object.keys(loaders)],
default: {
- en: require('./en.json')
+ en: require('./en.json').default
},
setLanguage: async (i18n, language) => {
if (loaders[language]) {
let messages = await loaders[language]()
- i18n.setLocaleMessage(language, messages)
+ i18n.setLocaleMessage(language, messages.default)
}
i18n.locale = language
}
diff --git a/src/i18n/nb.json b/src/i18n/nb.json
index 5e3e8ef3..1c160afb 100644
--- a/src/i18n/nb.json
+++ b/src/i18n/nb.json
@@ -553,8 +553,7 @@
"disable_remote_subscription": "Fjern mulighet til å følge brukeren fra andre instanser",
"disable_any_subscription": "Fjern mulighet til å følge brukeren",
"quarantine": "Gjør at statuser fra brukeren ikke kan sendes til andre instanser",
- "delete_user": "Slett bruker",
- "delete_user_confirmation": "Er du helt sikker? Denne handlingen kan ikke omgjøres."
+ "delete_user": "Slett bruker"
}
},
"user_profile": {
diff --git a/src/i18n/nl.json b/src/i18n/nl.json
index fd61572c..64c92b68 100644
--- a/src/i18n/nl.json
+++ b/src/i18n/nl.json
@@ -580,7 +580,6 @@
"remote_follow": "Volg vanop afstand",
"statuses": "Statussen",
"admin_menu": {
- "delete_user_confirmation": "Weet je het heel zeker? Deze uitvoering kan niet ongedaan worden gemaakt.",
"delete_user": "Gebruiker verwijderen",
"quarantine": "Federeren van gebruikers berichten verbieden",
"disable_any_subscription": "Volgen van gebruiker in zijn geheel verbieden",
diff --git a/src/i18n/oc.json b/src/i18n/oc.json
index 40f48149..556b3d0b 100644
--- a/src/i18n/oc.json
+++ b/src/i18n/oc.json
@@ -501,8 +501,7 @@
"disable_remote_subscription": "Desactivar lo seguiment d’utilizaire d’instàncias alonhadas",
"disable_any_subscription": "Desactivar tot seguiment",
"quarantine": "Defendre la federacion de las publicacions de l’utilizaire",
- "delete_user": "Suprimir l’utilizaire",
- "delete_user_confirmation": "Volètz vertadièrament far aquò ? Aquesta accion se pòt pas anullar."
+ "delete_user": "Suprimir l’utilizaire"
}
},
"user_profile": {
diff --git a/src/i18n/pl.json b/src/i18n/pl.json
index 304a0349..efebcc83 100644
--- a/src/i18n/pl.json
+++ b/src/i18n/pl.json
@@ -762,8 +762,7 @@
"disable_remote_subscription": "Zakaż obserwowania użytkownika ze zdalnych instancji",
"disable_any_subscription": "Zakaż całkowicie obserwowania użytkownika",
"quarantine": "Zakaż federowania postów od tego użytkownika",
- "delete_user": "Usuń użytkownika",
- "delete_user_confirmation": "Czy jesteś absolutnie pewny(-a)? Ta operacja nie może być cofnięta."
+ "delete_user": "Usuń użytkownika"
},
"message": "Napisz",
"edit_profile": "Edytuj profil",
diff --git a/src/i18n/pt.json b/src/i18n/pt.json
index e32a95e4..b997701c 100644
--- a/src/i18n/pt.json
+++ b/src/i18n/pt.json
@@ -594,7 +594,6 @@
"unmute_progress": "A retirar silêncio…",
"mute_progress": "A silenciar…",
"admin_menu": {
- "delete_user_confirmation": "Tens a certeza? Esta ação não pode ser revertida.",
"delete_user": "Eliminar utilizador",
"quarantine": "Não permitir publicações de utilizadores de instâncias remotas",
"disable_any_subscription": "Não permitir que nenhum utilizador te siga",
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index ba0cec28..7e6ff3f5 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -576,8 +576,7 @@
"disable_remote_subscription": "Запретить читать с других узлов",
"disable_any_subscription": "Запретить читать пользователя",
"quarantine": "Не федерировать статусы пользователя",
- "delete_user": "Удалить пользователя",
- "delete_user_confirmation": "Вы уверены? Это действие нельзя отменить."
+ "delete_user": "Удалить пользователя"
},
"media": "С вложениями",
"mention": "Упомянуть",
diff --git a/src/i18n/service_worker_messages.js b/src/i18n/service_worker_messages.js
index 270ed043..f691f1c4 100644
--- a/src/i18n/service_worker_messages.js
+++ b/src/i18n/service_worker_messages.js
@@ -27,6 +27,7 @@ const messages = {
pt: require('../lib/notification-i18n-loader.js!./pt.json'),
ro: require('../lib/notification-i18n-loader.js!./ro.json'),
ru: require('../lib/notification-i18n-loader.js!./ru.json'),
+ sk: require('../lib/notification-i18n-loader.js!./sk.json'),
te: require('../lib/notification-i18n-loader.js!./te.json'),
zh: require('../lib/notification-i18n-loader.js!./zh.json'),
en: require('../lib/notification-i18n-loader.js!./en.json')
diff --git a/src/i18n/sk.json b/src/i18n/sk.json
new file mode 100644
index 00000000..cee76f5e
--- /dev/null
+++ b/src/i18n/sk.json
@@ -0,0 +1,512 @@
+{
+ "about": {
+ "mrf": {
+ "federation": "Federácia",
+ "keyword": {
+ "keyword_policies": "Pravidlá pre kľúčové slová",
+ "ftl_removal": "Odstránenie z časovej osy \"Celej známej siete\"",
+ "reject": "Odmietni",
+ "replace": "Nahraď",
+ "is_replaced_by": "→"
+ },
+ "mrf_policies": "Povoliť MRF pravidlá",
+ "mrf_policies_desc": "MRF pravidlá upravujú správanie servera v rámci federácie s inými. Nasledovné pravidlá sú aktívne:",
+ "simple": {
+ "simple_policies": "Pravidlá špecifické pre tento server",
+ "instance": "Server",
+ "reason": "Dôvod",
+ "not_applicable": "N/A",
+ "accept": "Prijať",
+ "accept_desc": "Tento server preberá správy len z nasledovných serverov:",
+ "reject": "Odmietnuť",
+ "reject_desc": "Tento server preberá správy spravy z nasledovných serverov:",
+ "quarantine": "Karanténa",
+ "quarantine_desc": "Tento server posiela verejné oznamy len na nasledovné servre:",
+ "ftl_removal": "Odstránenie časovej osy \"Známa sieť\"",
+ "ftl_removal_desc": "Tento server odstraňuje nasledovné serverov zo svojej časovej osy \"Známa sieť\":",
+ "media_removal": "Odstránenie médií",
+ "media_removal_desc": "Tento server odstraňuje médiá zo správ nasledovných serverov:",
+ "media_nsfw": "Označenie médií ako citlivých",
+ "media_nsfw_desc": "Tento server označuje média ako citlivé v správach z nasledovných serverov:"
+ }
+ },
+ "staff": "Personál"
+ },
+ "shoutbox": {
+ "title": "Verejné fórum"
+ },
+ "domain_mute_card": {
+ "mute": "Utíš",
+ "mute_progress": "Utišujem…",
+ "unmute": "Povoľ oznamy",
+ "unmute_progress": "Povoľujem oznamy…"
+ },
+ "exporter": {
+ "export": "Export",
+ "processing": "Spracováva sa, čoskoro sa ti ponúknu na stiahnutie súbory s dátami exportu"
+ },
+ "features_panel": {
+ "shout": "Verejné fórum",
+ "pleroma_chat_messages": "Pleroma Chat",
+ "gopher": "Gopher",
+ "media_proxy": "Proxy pre médiá",
+ "scope_options": "Nastavenia rámca",
+ "text_limit": "Limit počtu znakov",
+ "title": "Vlastnosti",
+ "who_to_follow": "Koho nasledovať",
+ "upload_limit": "Limit nahrávania"
+ },
+ "finder": {
+ "error_fetching_user": "Chyba načítavania užívateľa",
+ "find_user": "Nájsť užívateľa"
+ },
+ "general": {
+ "apply": "Použiť",
+ "submit": "Odoslať",
+ "more": "Viac",
+ "loading": "Nahrávam…",
+ "generic_error": "Nastala chyba",
+ "error_retry": "Zopakuj znova, prosím",
+ "retry": "Zopakuj znova",
+ "optional": "nepovinné",
+ "show_more": "Zobraz viac",
+ "show_less": "Zobraz menej",
+ "dismiss": "Zahoď",
+ "cancel": "Zruš",
+ "disable": "Vypni",
+ "enable": "Zapni",
+ "confirm": "Potvrdiť",
+ "verify": "Overiť",
+ "close": "Zatvoriť",
+ "peek": "Vybrať",
+ "role": {
+ "admin": "Správca",
+ "moderator": "Moderátor"
+ },
+ "flash_content": "Klikni pre zobrazenie Flash obsahu prostredníctvom Ruffle (experimentálne, nemusí fungovať).",
+ "flash_security": "Flash obsah je potencionálne nebezpečný, keďže je to produkt s uzatvoreným kódom.",
+ "flash_fail": "Nepodarilo sa nahrať Flash obsah, pre detaily pozri konzolu prehliadača.",
+ "scope_in_timeline": {
+ "direct": "Priame",
+ "private": "Len pre nasledovníkov",
+ "public": "Verejné",
+ "unlisted": "Nezaradené"
+ }
+ },
+ "image_cropper": {
+ "crop_picture": "Orezať obrázok",
+ "save": "Uložiť",
+ "save_without_cropping": "Ulož bez orezania",
+ "cancel": "Zrušiť"
+ },
+ "importer": {
+ "submit": "Odoslať",
+ "success": "Úspečne naimportované.",
+ "error": "Pri importe súboru nastala chyba."
+ },
+ "login": {
+ "login": "Prihlásiť sa",
+ "description": "Prihlásiť pomocou OAuth",
+ "logout": "Odhlásiť sa",
+ "password": "Heslo",
+ "placeholder": "napr. peter",
+ "register": "Registrácia",
+ "username": "Meno užívateľa",
+ "hint": "Prihlás sa, aby si sa mohol zúčastniť konverzácie",
+ "authentication_code": "Autentifikačný kód",
+ "enter_recovery_code": "Zadaj kód obnovenia",
+ "enter_two_factor_code": "Zadaj 2-fázový validačný kód",
+ "recovery_code": "Kód obnovenia",
+ "heading": {
+ "totp": "2-fázové overenie",
+ "recovery": "2-fázové obnova"
+ }
+ },
+ "media_modal": {
+ "previous": "Predchádzajúce",
+ "next": "Nasledujúce",
+ "counter": "{current} / {total}",
+ "hide": "Zatvoriť prehliadač médií"
+ },
+ "nav": {
+ "about": "O stránke",
+ "administration": "Administrácia",
+ "back": "Späť",
+ "friend_requests": "Žiadosti o priateľstvo",
+ "mentions": "Zmienky",
+ "interactions": "Interakcie",
+ "dms": "Priame správy",
+ "public_tl": "Verejná časová os",
+ "timeline": "Časová os",
+ "home_timeline": "Domáca časová os",
+ "twkn": "Známa sieť",
+ "bookmarks": "Záložky",
+ "user_search": "Hľadanie užívateľa",
+ "search": "Hladať",
+ "who_to_follow": "Koho nasledovať",
+ "preferences": "Nastavenia",
+ "timelines": "Časové osy",
+ "chats": "Chaty"
+ },
+ "notifications": {
+ "broken_favorite": "Neznáma správa, dohľadávam ju…",
+ "error": "Chyba získavania upozornení: {0}",
+ "favorited_you": "si obľúbil tvoju správu",
+ "followed_you": "ťa nasleduje",
+ "follow_request": "ťa chce nasledovať",
+ "load_older": "Nahrať staršie upozornenia",
+ "notifications": "Upozornenia",
+ "read": "Prečítané!",
+ "repeated_you": "zopakoval tvoju správu",
+ "no_more_notifications": "Žiadne ďalšie upozornenia",
+ "migrated_to": "sa presťahoval na",
+ "reacted_with": "reagoval nasledovne {0}"
+ },
+ "polls": {
+ "add_poll": "Pridať anketu",
+ "add_option": "Pridať možnosť",
+ "option": "Možnosť",
+ "votes": "hlasy",
+ "people_voted_count": "{count} volič | {count} voličov",
+ "votes_count": "{count} hlas | {count} hlasov",
+ "vote": "Hlas",
+ "type": "Typ ankety",
+ "single_choice": "Výber jednej možnosti",
+ "multiple_choices": "Výber viacerých možností",
+ "expiry": "Vek ankety",
+ "expires_in": "Anketa končí za {0}",
+ "expired": "Anketa skončila pre {0}",
+ "not_enough_options": "Príliš málo jedinečných možností v ankete"
+ },
+ "emoji": {
+ "stickers": "Nálepka",
+ "emoji": "Emotikon",
+ "keep_open": "Ponechaj okno výberu otvorené",
+ "search_emoji": "Vyhladať emotikon",
+ "add_emoji": "Vložiť emotikon",
+ "custom": "Vlastný emotikon",
+ "unicode": "Unicode emotikon",
+ "load_all_hint": "Nahralo sa prvých {saneAmount} emotikonov, nahranie všetkých by mohlo spôsobiť zníženie výkonu.",
+ "load_all": "Nahrať všetkých {emojiAmount} emotikonov"
+ },
+ "errors": {
+ "storage_unavailable": "Pleroma nemôže používať úložisko prehliadača. Tvoje prihlasovacie meno a lokálne nastavenia nebudú uchované a môžu sa vyskytnúť neočakávané chyby. Skús povoliť cookie."
+ },
+ "interactions": {
+ "favs_repeats": "Zopakovania a obľúbené",
+ "follows": "Nový nasledovatelia",
+ "moves": "Užívateľ sa sťahuje",
+ "load_older": "Nahrať staršiu komunikáciu"
+ },
+ "post_status": {
+ "new_status": "Poslať novú správu",
+ "account_not_locked_warning": "Tvoj účen nie je {0}. Ktokoľvek ťa môže začať nasledovať a tak vidieť správy určené len pre nasledovateľov.",
+ "account_not_locked_warning_link": "uzamknuté",
+ "attachments_sensitive": "Označiť prílohy ako citlivé",
+ "media_description": "Popis média",
+ "content_type": {
+ "text/plain": "Obyčajný text",
+ "text/html": "HTML",
+ "text/markdown": "Markdown",
+ "text/bbcode": "BBCode"
+ },
+ "content_warning": "Nadpis (nepovinné)",
+ "default": "Práve som ...",
+ "direct_warning_to_all": "Túto správu bude vidieť každý užívateľ, ktorého v nej spomenieš.",
+ "direct_warning_to_first_only": "Táto správa bude viditeľná len pre užívateľov, ktorých vymenuješ na začiatku správy.",
+ "posting": "Posielanie",
+ "post": "Poslať",
+ "preview": "Náhľad",
+ "preview_empty": "Prázdne",
+ "empty_status_error": "Nie je možné odoslať prázdnu správu bez priložených súborov",
+ "media_description_error": "Nepodarilo sa aktualizovať média, skús znova",
+ "scope_notice": {
+ "public": "Túto správu bude vidieť každý",
+ "private": "Túto správu budú vidieť len tvoji nasledovníci",
+ "unlisted": "Táto správa nebude viditeľná na verejnej časovej osi a v celej známej sieti"
+ },
+ "scope": {
+ "direct": "Priama správa - zobrazí sa len užívateľom spomenutým v správe",
+ "private": "Pre nasledovníkov - zobrazí sa len tvojim nasledovníkom",
+ "public": "Verejné - zobrazí sa vo všetkých časových osiach",
+ "unlisted": "Nezaradené - nezobrazí sa v žiadnej časovej osy"
+ }
+ },
+ "registration": {
+ "bio": "Životopis",
+ "email": "Email",
+ "fullname": "Zobrazované meno",
+ "password_confirm": "Potvrdenie hesla",
+ "registration": "Registrácia",
+ "token": "Pozývací kód",
+ "captcha": "CAPTCHA",
+ "new_captcha": "Klikni na obrázok a vnikne nová captcha",
+ "username_placeholder": "napr. peter",
+ "fullname_placeholder": "napr. Peter Kukurica",
+ "bio_placeholder": "e.g.\nHi, I'm Lain.\nI’m an anime girl living in suburban Japan. You may know me from the Wired.",
+ "reason": "Dôvod registrácie",
+ "reason_placeholder": "Tento server schvaľuje registrácie manuálne.\nZanechaj správcom dôvod, prečo máš záujem vytvoriť si tu účet.",
+ "register": "Registrácia",
+ "validations": {
+ "username_required": "nemôže byť prázdne",
+ "fullname_required": "nemôže byť prázdne",
+ "email_required": "nemôže byť prázdne",
+ "password_required": "nemôže byť prázdne",
+ "password_confirmation_required": "nemôže byť prázdne",
+ "password_confirmation_match": "musí byť rovnaké ako heslo"
+ }
+ },
+ "remote_user_resolver": {
+ "remote_user_resolver": "Vzdialené overenie užívateľa",
+ "searching_for": "Hľadám...",
+ "error": "Nenájdené."
+ },
+ "selectable_list": {
+ "select_all": "Vybrať všetko"
+ },
+ "time": {
+ "day": "{0} deň",
+ "days": "{0} dní",
+ "day_short": "{0}d",
+ "days_short": "{0}d",
+ "hour": "{0} hodina",
+ "hours": "{0} hodín",
+ "hour_short": "{0}h",
+ "hours_short": "{0}h",
+ "in_future": "za {0}",
+ "in_past": "pred {0}",
+ "minute": "{0} minúta",
+ "minutes": "{0} minút",
+ "minute_short": "{0}min",
+ "minutes_short": "{0}min",
+ "month": "{0} mesiac",
+ "months": "{0} mesiacov",
+ "month_short": "{0}mes",
+ "months_short": "{0}mes",
+ "now": "práve teraz",
+ "now_short": "teraz",
+ "second": "{0} sekunda",
+ "seconds": "{0} sekúnd",
+ "second_short": "{0}s",
+ "seconds_short": "{0}s",
+ "week": "{0} týždeň",
+ "weeks": "{0} týždňov",
+ "week_short": "{0}t",
+ "weeks_short": "{0}t",
+ "year": "{0} rok",
+ "years": "{0} rokov",
+ "year_short": "{0}r",
+ "years_short": "{0}r"
+ },
+ "timeline": {
+ "collapse": "Zbaliť",
+ "conversation": "Konverzácia",
+ "error": "Chyba pri nahrávaní časovej správy: {0}",
+ "load_older": "Nahrať staršie správy",
+ "no_retweet_hint": "Správa je označená ako len-pre-nasledovateľov alebo ako priama a nemôže byť zopakovaná na tvojej časovej osy.",
+ "repeated": "zopakované",
+ "show_new": "Zobraziť nové",
+ "reload": "Znovu nahrať",
+ "up_to_date": "Aktuálne",
+ "no_more_statuses": "Žiadne ďalšie správy",
+ "no_statuses": "Žiadne správy",
+ "socket_reconnected": "Prepojenie v reálnom čase bolo úspešne vytvorené",
+ "socket_broke": "Strata prepojenia v reálnom čase: chyba CloseEvent kód {0}"
+ },
+ "status": {
+ "favorites": "Obľúbené",
+ "repeats": "Opakovania",
+ "delete": "Zmazať správu",
+ "pin": "Pripnúť na stránku užívateľa",
+ "unpin": "Odopnúť zo stránky užívateľa",
+ "pinned": "Pripnuté",
+ "bookmark": "Vytvoriť záložku",
+ "unbookmark": "Zmazať záložku",
+ "delete_confirm": "Skutočne chceš zmazať túto správu?",
+ "reply_to": "Odpovedať komu",
+ "mentions": "Spomenutia",
+ "replies_list": "Odpovede:",
+ "replies_list_with_others": "Odpoveď (+{numReplies} iný): | Odpoveď (+{numReplies} iných):",
+ "mute_conversation": "Stíšiť konverzáciu",
+ "unmute_conversation": "Oznamovať konverzáciu",
+ "status_unavailable": "Neznámy status",
+ "copy_link": "Skopírovať odkaz do správy",
+ "external_source": "Vzdialený zdroj",
+ "thread_muted": "Konverzácia stíšená",
+ "thread_muted_and_words": ", má slová:",
+ "show_full_subject": "Zobraziť celý nadpis",
+ "hide_full_subject": "Skry celý nadpis",
+ "show_content": "Zobraziť obsah",
+ "hide_content": "Skryť obsah",
+ "status_deleted": "Táto správa bola zmazaná",
+ "nsfw": "NSFW",
+ "expand": "Rozbaliť správu",
+ "you": "(ty)",
+ "plus_more": "+{number} ďalších",
+ "many_attachments": "Správa má {number} príloh",
+ "collapse_attachments": "Zabaliť médiá",
+ "show_all_attachments": "Zobraz všetky prílohy",
+ "show_attachment_in_modal": "Zobraz médiá modálne",
+ "show_attachment_description": "Náhľad popisku (otvor prílohu pre zobrazenie celého popisku)",
+ "hide_attachment": "Skryť prílohy",
+ "remove_attachment": "Odstrániť prílohy",
+ "attachment_stop_flash": "Zastaviť prehrávač Flashu",
+ "move_up": "Presuň prílohu doľava",
+ "move_down": "Presuň prílohu doprava",
+ "open_gallery": "Otvoriť galériu",
+ "thread_hide": "Skry túto konverzáciu",
+ "thread_show": "Zobraz túto konverzáciu",
+ "thread_show_full": "Zobraz všetko pod touto konverzáciou (celkovo {numStatus} správa, max hĺbka {depth}) | Zobraz všetko pod touto konverzáciou (celkovo {numStatus} správ, max hĺbka {depth})",
+ "thread_show_full_with_icon": "{icon} {text}",
+ "thread_follow": "Zobraz zvyšnú časť tejto konverzácie (celkovo {numStatus} správa) | Zobraz zvyšnú časť tejto konverzácie (celkovo {numStatus} správ)",
+ "thread_follow_with_icon": "{icon} {text}",
+ "ancestor_follow": "Pozri {numReplies} ďalšiu odpoveď pod touto správou | Pozri {numReplies} ďalších odpovedí pod touto správou",
+ "ancestor_follow_with_icon": "{icon} {text}",
+ "show_all_conversation_with_icon": "{icon} {text}",
+ "show_all_conversation": "Zobraz celú konverzáciu ({numStatus} iná správa) | Zobraz celú konverzáciu ({numStatus} iných správ)",
+ "show_only_conversation_under_this": "Zobraz len správy súvisiace s touto správou"
+ },
+ "user_card": {
+ "approve": "Schváliť",
+ "block": "Zablokovať",
+ "blocked": "Blokované!",
+ "deactivated": "Neaktívne",
+ "deny": "Zakázané",
+ "edit_profile": "Uraviť profil",
+ "favorites": "Obľúbené",
+ "follow": "Nasledovať",
+ "follow_cancel": "Požiadavka zrušená",
+ "follow_sent": "Požiadavka zaslaná!",
+ "follow_progress": "Žiadam o povolenie…",
+ "follow_unfollow": "Prestať sledovať",
+ "followees": "Nasleduje",
+ "followers": "Nasledovatelia",
+ "following": "Nasleduješ!",
+ "follows_you": "Nasleduje teba!",
+ "hidden": "Skryté",
+ "its_you": "To si ty!",
+ "media": "Média",
+ "mention": "Spomenul",
+ "message": "Správa",
+ "mute": "Stíšiť",
+ "muted": "Stíšené",
+ "per_day": "za deň",
+ "remote_follow": "Nasledovanie z ďaleka",
+ "report": "Nahlásiť",
+ "statuses": "Vytvorených správ",
+ "subscribe": "Prihlásiť k odberu",
+ "unsubscribe": "Odhlásiť z odberu",
+ "unblock": "Odblokovať",
+ "unblock_progress": "Oblokováva sa…",
+ "block_progress": "Blokujem…",
+ "unmute": "Povoliť oznamy",
+ "unmute_progress": "Povoľujem oznamy…",
+ "mute_progress": "Stišujem…",
+ "hide_repeats": "Skry zopakovania",
+ "show_repeats": "Zobraz zopakovania",
+ "bot": "Robot",
+ "admin_menu": {
+ "moderation": "Moderovanie",
+ "grant_admin": "Povoliť spravovanie",
+ "revoke_admin": "Zakázať spravovanie",
+ "grant_moderator": "Povoliť moderovanie",
+ "revoke_moderator": "Zakázať moderovanie",
+ "activate_account": "Aktivovať účet",
+ "deactivate_account": "Deaktivovať účet",
+ "delete_account": "Zmazať účet",
+ "force_nsfw": "Označ všetky správy ako NSFW",
+ "strip_media": "Odstrániť média zo správy",
+ "force_unlisted": "Vynúť, aby správy neboli zobrazované",
+ "sandbox": "Vynúť, aby správy boli len pre nasledovateľov",
+ "disable_remote_subscription": "Odstrániť prístup k serveru nasledovnému vzdialenému užívateľovi",
+ "disable_any_subscription": "Zakázať nasledovanie užívateľov",
+ "quarantine": "Zakázať federáciu správ užívateľa",
+ "delete_user": "Zmazať užívateľa",
+ "delete_user_confirmation": "Si si úplne istý? Táto akcia sa nedá zobrať späť."
+ },
+ "highlight": {
+ "disabled": "Bez zvýraznenia",
+ "solid": "Jednoliate pozadie",
+ "striped": "Šrafované pozadie",
+ "side": "Pásik na boku"
+ }
+ },
+ "user_profile": {
+ "timeline_title": "Časová os užívateľa",
+ "profile_does_not_exist": "Prepáč, tento profil neexistuje.",
+ "profile_loading_error": "Prepáč, nastala chyba pri nahrávaní profilu."
+ },
+ "user_reporting": {
+ "title": "Nahlásení {0}",
+ "add_comment_description": "Hlásnenie bude zaslané moderátorom servera. Nižšie môžeš napísať dôvod prečo tento účet nahlasuješ:",
+ "additional_comments": "Ďalšie poznámky",
+ "forward_description": "Účet je z iného servera. Poslať kópiu tohto hlásenia aj tam?",
+ "forward_to": "Preposlať komu {0}",
+ "submit": "Odoslať",
+ "generic_error": "Nastala chyba pri vykonaní tvojej požiadavky."
+ },
+ "who_to_follow": {
+ "more": "Viac",
+ "who_to_follow": "Koho nasledovať"
+ },
+ "tool_tip": {
+ "media_upload": "Nahrať médium",
+ "repeat": "Zopakovať",
+ "reply": "Odpovedať",
+ "favorite": "Obľúbené",
+ "add_reaction": "Reagovať",
+ "user_settings": "Nastavenia užívateľa",
+ "accept_follow_request": "Prijať požiadavku nasledovníka",
+ "reject_follow_request": "Odmietnuť požiadavku nasledovníka",
+ "bookmark": "Záložka"
+ },
+ "upload": {
+ "error": {
+ "base": "Nahrávanie bolo neúspešné.",
+ "message": "Nahrávanie bolo neúspešné: {0}",
+ "file_too_big": "Súbor je príliš veľký [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
+ "default": "Vyskúšaj opäť neskôr"
+ }
+ },
+ "search": {
+ "people": "Ľudia",
+ "hashtags": "Haštagy",
+ "person_talking": "{count} človek hovorí",
+ "people_talking": "{count} ľudí hovorí",
+ "no_results": "Žiadne výsledky"
+ },
+ "password_reset": {
+ "forgot_password": "Zabudol si heslo?",
+ "password_reset": "Obnovenie hesla",
+ "instruction": "Zadaj svoju emailovú adresu alebo užívateľské meno. Pošleme ti odkaz pomocou, ktorého môžeš obnoviť svoje heslo.",
+ "placeholder": "Tvoj email alebo užívateľské meno",
+ "check_email": "V novom emaile ti bol doručený odkaz na spôsob, ako obnovíš svoje heslo.",
+ "return_home": "Návrat na domácu stránku",
+ "too_many_requests": "Prekročil si limit pokusov, skús znova neskôr.",
+ "password_reset_disabled": "Obnova hesla je vypnutá. Kontaktuj, prosím, správcu tohto servera.",
+ "password_reset_required": "Musíš najskôr obnoviť heslo, ak sa chceš prihlásiť.",
+ "password_reset_required_but_mailer_is_disabled": "Musíš obnoviť svoje heslo, ale obnova hesla je na serveri vypnutá. Kontaktuj, prosím, správcu tohto servera."
+ },
+ "chats": {
+ "you": "Ty:",
+ "message_user": "Správa {nickname}",
+ "delete": "Zmazať",
+ "chats": "Rozhovor",
+ "new": "Nový rozhovor",
+ "empty_message_error": "Nie je možné odoslať prázdnu správu",
+ "more": "Viac",
+ "delete_confirm": "Skutočne chceš zmazať túto správu?",
+ "error_loading_chat": "Nastala chyba pri nahrávaní rozhovoru.",
+ "error_sending_message": "Nastala chyba pri odosielaní správ.",
+ "empty_chat_list_placeholder": "Nemáš za sebou žiadne rozhovory. Začni nový rozhovor!"
+ },
+ "file_type": {
+ "audio": "Audio",
+ "video": "Video",
+ "image": "Obrázok",
+ "file": "Súbor"
+ },
+ "display_date": {
+ "today": "Dnes"
+ }
+}
diff --git a/src/i18n/te.json b/src/i18n/te.json
index 1216de59..4f255505 100644
--- a/src/i18n/te.json
+++ b/src/i18n/te.json
@@ -49,7 +49,7 @@
"notifications.repeated_you": "మీ స్థితిని పునరావృతం చేసారు",
"notifications.no_more_notifications": "ఇక నోటిఫికేషన్లు లేవు",
"post_status.new_status": "క్రొత్త స్థితిని పోస్ట్ చేయండి",
- "post_status.account_not_locked_warning": "మీ ఖాతా {౦} కాదు. ఎవరైనా మిమ్మల్ని అనుసరించి అనుచరులకు మాత్రమే ఉద్దేశించిన పోస్టులను చూడవచ్చు.",
+ "post_status.account_not_locked_warning": "మీ ఖాతా {0} కాదు. ఎవరైనా మిమ్మల్ని అనుసరించి అనుచరులకు మాత్రమే ఉద్దేశించిన పోస్టులను చూడవచ్చు.",
"post_status.account_not_locked_warning_link": "తాళం వేయబడినది",
"post_status.attachments_sensitive": "జోడింపులను సున్నితమైనవిగా గుర్తించండి",
"post_status.content_type.text/plain": "సాధారణ అక్షరాలు",
diff --git a/src/i18n/uk.json b/src/i18n/uk.json
index d9833087..4e62b4a9 100644
--- a/src/i18n/uk.json
+++ b/src/i18n/uk.json
@@ -755,7 +755,6 @@
"deactivate_account": "Деактивувати обліковий запис",
"delete_account": "Видалити обліковий запис",
"moderation": "Модерація",
- "delete_user_confirmation": "Ви абсолютно впевнені? Цю дію неможливо буде скасовувати.",
"delete_user": "Видалити обліковий запис",
"strip_media": "Вилучити медіа з дописів користувача",
"force_nsfw": "Позначити всі дописи як NSFW",
diff --git a/src/i18n/vi.json b/src/i18n/vi.json
index c77ad4ca..fd7ae25c 100644
--- a/src/i18n/vi.json
+++ b/src/i18n/vi.json
@@ -772,8 +772,7 @@
"quarantine": "Không cho phép tút liên hợp",
"delete_user": "Xóa người dùng",
"revoke_moderator": "Gỡ bỏ Quản trị viên",
- "force_unlisted": "Đánh dấu tất cả tút là hạn chế",
- "delete_user_confirmation": "Bạn chắc chắn chưa? Hành động này không thể phục hồi."
+ "force_unlisted": "Đánh dấu tất cả tút là hạn chế"
},
"highlight": {
"disabled": "Không nổi bật",
diff --git a/src/i18n/zh.json b/src/i18n/zh.json
index abba4be9..dd0e6827 100644
--- a/src/i18n/zh.json
+++ b/src/i18n/zh.json
@@ -714,8 +714,7 @@
"disable_remote_subscription": "禁止从远程实例关注用户",
"disable_any_subscription": "完全禁止关注用户",
"quarantine": "从联合实例中禁止用户帖子",
- "delete_user": "删除用户",
- "delete_user_confirmation": "你确定吗?此操作无法撤销。"
+ "delete_user": "删除用户"
},
"hidden": "已隐藏",
"show_repeats": "显示转发",
diff --git a/src/i18n/zh_Hant.json b/src/i18n/zh_Hant.json
index 2c4dc3fb..6f0f63b5 100644
--- a/src/i18n/zh_Hant.json
+++ b/src/i18n/zh_Hant.json
@@ -747,7 +747,6 @@
"admin_menu": {
"delete_account": "刪除賬號",
"delete_user": "刪除用戶",
- "delete_user_confirmation": "你確認嗎?此操作無法撤銷。",
"moderation": "調停",
"grant_admin": "賦予管理權限",
"revoke_admin": "撤銷管理權限",
diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js
index 8ecb66a8..24b835da 100644
--- a/src/lib/persisted_state.js
+++ b/src/lib/persisted_state.js
@@ -1,6 +1,6 @@
import merge from 'lodash.merge'
import localforage from 'localforage'
-import { each, get, set } from 'lodash'
+import { each, get, set, cloneDeep } from 'lodash'
let loaded = false
@@ -69,7 +69,7 @@ export default function createPersistedState ({
subscriber(store)((mutation, state) => {
try {
if (saveImmedeatelyActions.includes(mutation.type)) {
- setState(key, reducer(state, paths), storage)
+ setState(key, reducer(cloneDeep(state), paths), storage)
.then(success => {
if (typeof success !== 'undefined') {
if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') {
diff --git a/src/main.js b/src/main.js
index bdf8368b..eacd554c 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,6 +1,4 @@
-import Vue from 'vue'
-import VueRouter from 'vue-router'
-import Vuex from 'vuex'
+import { createStore } from 'vuex'
import 'custom-event-polyfill'
import './lib/event_target_polyfill.js'
@@ -22,36 +20,18 @@ import pollsModule from './modules/polls.js'
import postStatusModule from './modules/postStatus.js'
import chatsModule from './modules/chats.js'
-import VueI18n from 'vue-i18n'
+import { createI18n } from 'vue-i18n'
import createPersistedState from './lib/persisted_state.js'
import pushNotifications from './lib/push_notifications_plugin.js'
import messages from './i18n/messages.js'
-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]
-Vue.use(Vuex)
-Vue.use(VueRouter)
-Vue.use(VueI18n)
-Vue.use(VueClickOutside)
-Vue.use(PortalVue)
-Vue.use(VBodyScrollLock)
-
-Vue.config.ignoredElements = ['pinch-zoom']
-
-Vue.component('FAIcon', FontAwesomeIcon)
-Vue.component('FALayers', FontAwesomeLayers)
-
-const i18n = new VueI18n({
+const i18n = createI18n({
// By default, use the browser locale, we will update it if neccessary
locale: 'en',
fallbackLocale: 'en',
@@ -78,17 +58,18 @@ const persistedStateOptions = {
console.error(e)
storageError = true
}
- const store = new Vuex.Store({
+ const store = createStore({
modules: {
i18n: {
getters: {
- i18n: () => i18n
+ i18n: () => i18n.global
}
},
interface: interfaceModule,
instance: instanceModule,
- statuses: statusesModule,
+ // TODO refactor users/statuses modules, they depend on each other
users: usersModule,
+ statuses: statusesModule,
api: apiModule,
config: configModule,
serverSideConfig: serverSideConfigModule,
diff --git a/src/modules/chats.js b/src/modules/chats.js
index 69d683bd..f28c2603 100644
--- a/src/modules/chats.js
+++ b/src/modules/chats.js
@@ -1,4 +1,4 @@
-import Vue from 'vue'
+import { reactive } from 'vue'
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'
@@ -13,8 +13,8 @@ const emptyChatList = () => ({
const defaultState = {
chatList: emptyChatList(),
chatListFetcher: null,
- openedChats: {},
- openedChatMessageServices: {},
+ openedChats: reactive({}),
+ openedChatMessageServices: reactive({}),
fetcher: undefined,
currentChatId: null,
lastReadMessageId: null
@@ -137,10 +137,10 @@ const chats = {
},
addOpenedChat (state, { _dispatch, chat }) {
state.currentChatId = chat.id
- Vue.set(state.openedChats, chat.id, chat)
+ state.openedChats[chat.id] = chat
if (!state.openedChatMessageServices[chat.id]) {
- Vue.set(state.openedChatMessageServices, chat.id, chatService.empty(chat.id))
+ state.openedChatMessageServices[chat.id] = chatService.empty(chat.id)
}
},
setCurrentChatId (state, { chatId }) {
@@ -160,7 +160,7 @@ const chats = {
}
} else {
state.chatList.data.push(updatedChat)
- Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
+ state.chatList.idStore[updatedChat.id] = updatedChat
}
})
},
@@ -172,7 +172,7 @@ const chats = {
chat.updated_at = updatedChat.updated_at
}
if (!chat) { state.chatList.data.unshift(updatedChat) }
- Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
+ state.chatList.idStore[updatedChat.id] = updatedChat
},
deleteChat (state, { _dispatch, id, _rootGetters }) {
state.chats.data = state.chats.data.filter(conversation =>
@@ -186,8 +186,8 @@ const chats = {
commit('setChatListFetcher', { fetcher: undefined })
for (const chatId in state.openedChats) {
chatService.clear(state.openedChatMessageServices[chatId])
- Vue.delete(state.openedChats, chatId)
- Vue.delete(state.openedChatMessageServices, chatId)
+ delete state.openedChats[chatId]
+ delete state.openedChatMessageServices[chatId]
}
},
setChatsLoading (state, { value }) {
@@ -215,8 +215,8 @@ const chats = {
for (const chatId in state.openedChats) {
if (currentChatId !== chatId) {
chatService.clear(state.openedChatMessageServices[chatId])
- Vue.delete(state.openedChats, chatId)
- Vue.delete(state.openedChatMessageServices, chatId)
+ delete state.openedChats[chatId]
+ delete state.openedChatMessageServices[chatId]
}
}
},
diff --git a/src/modules/config.js b/src/modules/config.js
index 93487b99..c4b08390 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -1,6 +1,9 @@
-import { set, delete as del } from 'vue'
+import Cookies from 'js-cookie'
import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
import messages from '../i18n/messages'
+import localeService from '../services/locale/locale.service.js'
+
+const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage'
const browserLocale = (window.navigator.language || 'en').split('-')[0]
@@ -47,6 +50,7 @@ export const defaultState = {
pauseOnUnfocused: true,
stopGifs: true,
replyVisibility: 'all',
+ thirdColumnMode: 'notifications',
notificationVisibility: {
follows: true,
mentions: true,
@@ -56,7 +60,8 @@ export const defaultState = {
emojiReactions: true,
followRequest: true,
reports: true,
- chatMention: true
+ chatMention: true,
+ polls: true
},
webPushNotifications: false,
muteWords: [],
@@ -75,6 +80,8 @@ export const defaultState = {
playVideosInModal: false,
useOneClickNsfw: false,
useContainFit: true,
+ disableStickyHeaders: false,
+ showScrollbars: false,
greentext: undefined, // instance default
useAtIcon: undefined, // instance default
mentionLinkDisplay: undefined, // instance default
@@ -123,14 +130,14 @@ const config = {
},
mutations: {
setOption (state, { name, value }) {
- set(state, name, value)
+ state[name] = value
},
setHighlight (state, { user, color, type }) {
const data = this.state.config.highlight[user]
if (color || type) {
- set(state.highlight, user, { color: color || data.color, type: type || data.type })
+ state.highlight[user] = { color: color || data.color, type: type || data.type }
} else {
- del(state.highlight, user)
+ delete state.highlight[user]
}
}
},
@@ -164,6 +171,10 @@ const config = {
break
case 'interfaceLanguage':
messages.setLanguage(this.getters.i18n, value)
+ Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value))
+ break
+ case 'thirdColumnMode':
+ dispatch('setLayoutWidth', undefined)
break
}
}
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 79c54096..220463ca 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -1,4 +1,3 @@
-import { set } from 'vue'
import { getPreset, applyTheme } from '../services/style_setter/style_setter.js'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
import apiService from '../services/api/api.service.js'
@@ -102,7 +101,7 @@ const instance = {
mutations: {
setInstanceOption (state, { name, value }) {
if (typeof value !== 'undefined') {
- set(state, name, value)
+ state[name] = value
}
},
setKnownDomains (state, domains) {
diff --git a/src/modules/interface.js b/src/modules/interface.js
index d6db32fd..a86193ea 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -1,5 +1,3 @@
-import { set, delete as del } from 'vue'
-
const defaultState = {
settingsModalState: 'hidden',
settingsModalLoaded: false,
@@ -15,7 +13,7 @@ const defaultState = {
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
)
},
- mobileLayout: false,
+ layoutType: 'normal',
globalNotices: [],
layoutHeight: 0,
lastTimeline: null
@@ -29,18 +27,17 @@ const interfaceMod = {
if (state.noticeClearTimeout) {
clearTimeout(state.noticeClearTimeout)
}
- set(state.settings, 'currentSaveStateNotice', { error: false, data: success })
- set(state.settings, 'noticeClearTimeout',
- setTimeout(() => del(state.settings, 'currentSaveStateNotice'), 2000))
+ state.settings.currentSaveStateNotice = { error: false, data: success }
+ state.settings.noticeClearTimeout = setTimeout(() => delete state.settings.currentSaveStateNotice, 2000)
} else {
- set(state.settings, 'currentSaveStateNotice', { error: true, errorData: error })
+ state.settings.currentSaveStateNotice = { error: true, errorData: error }
}
},
setNotificationPermission (state, permission) {
state.notificationPermission = permission
},
- setMobileLayout (state, value) {
- state.mobileLayout = value
+ setLayoutType (state, value) {
+ state.layoutType = value
},
closeSettingsModal (state) {
state.settingsModalState = 'hidden'
@@ -75,6 +72,9 @@ const interfaceMod = {
setLayoutHeight (state, value) {
state.layoutHeight = value
},
+ setLayoutWidth (state, value) {
+ state.layoutWidth = value
+ },
setLastTimeline (state, value) {
state.lastTimeline = value
}
@@ -89,9 +89,6 @@ const interfaceMod = {
setNotificationPermission ({ commit }, permission) {
commit('setNotificationPermission', permission)
},
- setMobileLayout ({ commit }, value) {
- commit('setMobileLayout', value)
- },
closeSettingsModal ({ commit }) {
commit('closeSettingsModal')
},
@@ -109,7 +106,7 @@ const interfaceMod = {
commit('openSettingsModal')
},
pushGlobalNotice (
- { commit, dispatch },
+ { commit, dispatch, state },
{
messageKey,
messageArgs = {},
@@ -121,11 +118,14 @@ const interfaceMod = {
messageArgs,
level
}
+ commit('pushGlobalNotice', notice)
+ // Adding a new element to array wraps it in a Proxy, which breaks the comparison
+ // TODO: Generate UUID or something instead or relying on !== operator?
+ const newNotice = state.globalNotices[state.globalNotices.length - 1]
if (timeout) {
- setTimeout(() => dispatch('removeGlobalNotice', notice), timeout)
+ setTimeout(() => dispatch('removeGlobalNotice', newNotice), timeout)
}
- commit('pushGlobalNotice', notice)
- return notice
+ return newNotice
},
removeGlobalNotice ({ commit }, notice) {
commit('removeGlobalNotice', notice)
@@ -133,6 +133,24 @@ const interfaceMod = {
setLayoutHeight ({ commit }, value) {
commit('setLayoutHeight', value)
},
+ // value is optional, assuming it was cached prior
+ setLayoutWidth ({ commit, state, rootGetters, rootState }, value) {
+ let width = value
+ if (value !== undefined) {
+ commit('setLayoutWidth', value)
+ } else {
+ width = state.layoutWidth
+ }
+ const mobileLayout = width <= 800
+ const normalOrMobile = mobileLayout ? 'mobile' : 'normal'
+ const { thirdColumnMode } = rootGetters.mergedConfig
+ if (thirdColumnMode === 'none' || !rootState.users.currentUser) {
+ commit('setLayoutType', normalOrMobile)
+ } else {
+ const wideLayout = width >= 1300
+ commit('setLayoutType', wideLayout ? 'wide' : normalOrMobile)
+ }
+ },
setLastTimeline ({ commit }, value) {
commit('setLastTimeline', value)
}
diff --git a/src/modules/oauth.js b/src/modules/oauth.js
index a2a83450..038bc3f3 100644
--- a/src/modules/oauth.js
+++ b/src/modules/oauth.js
@@ -1,5 +1,3 @@
-import { delete as del } from 'vue'
-
const oauth = {
state: {
clientId: false,
@@ -29,7 +27,7 @@ const oauth = {
state.userToken = false
// state.token is userToken with older name, coming from persistent state
// let's clear it as well, since it is being used as a fallback of state.userToken
- del(state, 'token')
+ delete state.token
}
},
getters: {
diff --git a/src/modules/polls.js b/src/modules/polls.js
index 92b89a06..1c4f98a4 100644
--- a/src/modules/polls.js
+++ b/src/modules/polls.js
@@ -1,5 +1,4 @@
import { merge } from 'lodash'
-import { set } from 'vue'
const polls = {
state: {
@@ -13,25 +12,25 @@ const polls = {
// Make expired-state change trigger re-renders properly
poll.expired = Date.now() > Date.parse(poll.expires_at)
if (existingPoll) {
- set(state.pollsObject, poll.id, merge(existingPoll, poll))
+ state.pollsObject[poll.id] = merge(existingPoll, poll)
} else {
- set(state.pollsObject, poll.id, poll)
+ state.pollsObject[poll.id] = poll
}
},
trackPoll (state, pollId) {
const currentValue = state.trackedPolls[pollId]
if (currentValue) {
- set(state.trackedPolls, pollId, currentValue + 1)
+ state.trackedPolls[pollId] = currentValue + 1
} else {
- set(state.trackedPolls, pollId, 1)
+ state.trackedPolls[pollId] = 1
}
},
untrackPoll (state, pollId) {
const currentValue = state.trackedPolls[pollId]
if (currentValue) {
- set(state.trackedPolls, pollId, currentValue - 1)
+ state.trackedPolls[pollId] = currentValue - 1
} else {
- set(state.trackedPolls, pollId, 0)
+ state.trackedPolls[pollId] = 0
}
}
},
diff --git a/src/modules/serverSideConfig.js b/src/modules/serverSideConfig.js
index 5c1baedb..4b73af26 100644
--- a/src/modules/serverSideConfig.js
+++ b/src/modules/serverSideConfig.js
@@ -55,7 +55,10 @@ export const settingsMap = {
get: 'pleroma.allow_following_move',
set: 'allow_following_move'
},
- 'discoverable': 'source.discoverable',
+ 'discoverable': {
+ get: 'source.pleroma.discoverable',
+ set: 'discoverable'
+ },
'hideFavorites': {
get: 'pleroma.hide_favorites',
set: 'hide_favorites'
diff --git a/src/modules/shout.js b/src/modules/shout.js
index 507a4d83..88aefbfe 100644
--- a/src/modules/shout.js
+++ b/src/modules/shout.js
@@ -1,7 +1,8 @@
const shout = {
state: {
messages: [],
- channel: { state: '' }
+ channel: { state: '' },
+ joined: false
},
mutations: {
setChannel (state, channel) {
@@ -13,11 +14,23 @@ const shout = {
},
setMessages (state, messages) {
state.messages = messages.slice(-19, 20)
+ },
+ setJoined (state, joined) {
+ state.joined = joined
}
},
actions: {
initializeShout (store, socket) {
const channel = socket.channel('chat:public')
+ channel.joinPush.receive('ok', () => {
+ store.commit('setJoined', true)
+ })
+ channel.onClose(() => {
+ store.commit('setJoined', false)
+ })
+ channel.onError(() => {
+ store.commit('setJoined', false)
+ })
channel.on('new_msg', (msg) => {
store.commit('addMessage', msg)
})
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index cb2a8d46..66cc82bc 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -12,7 +12,6 @@ import {
isArray,
omitBy
} from 'lodash'
-import { set } from 'vue'
import {
isStatusNotification,
isValidNotification,
@@ -92,7 +91,7 @@ const mergeOrAdd = (arr, obj, item) => {
// This is a new item, prepare it
prepareStatus(item)
arr.push(item)
- set(obj, item.id, item)
+ obj[item.id] = item
return { item, new: true }
}
}
@@ -131,7 +130,7 @@ const addStatusToGlobalStorage = (state, data) => {
if (conversationsObject[conversationId]) {
conversationsObject[conversationId].push(status)
} else {
- set(conversationsObject, conversationId, [status])
+ conversationsObject[conversationId] = [status]
}
}
return result
@@ -527,7 +526,7 @@ export const mutations = {
},
addEmojiReactionsBy (state, { id, emojiReactions, currentUser }) {
const status = state.allStatusesObject[id]
- set(status, 'emoji_reactions', emojiReactions)
+ status['emoji_reactions'] = emojiReactions
},
addOwnReaction (state, { id, emoji, currentUser }) {
const status = state.allStatusesObject[id]
@@ -546,9 +545,9 @@ export const mutations = {
// Update count of existing reaction if it exists, otherwise append at the end
if (reactionIndex >= 0) {
- set(status.emoji_reactions, reactionIndex, newReaction)
+ status.emoji_reactions[reactionIndex] = newReaction
} else {
- set(status, 'emoji_reactions', [...status.emoji_reactions, newReaction])
+ status['emoji_reactions'] = [...status.emoji_reactions, newReaction]
}
},
removeOwnReaction (state, { id, emoji, currentUser }) {
@@ -567,9 +566,9 @@ export const mutations = {
}
if (newReaction.count > 0) {
- set(status.emoji_reactions, reactionIndex, newReaction)
+ status.emoji_reactions[reactionIndex] = newReaction
} else {
- set(status, 'emoji_reactions', status.emoji_reactions.filter(r => r.name !== emoji))
+ status['emoji_reactions'] = status.emoji_reactions.filter(r => r.name !== emoji)
}
},
updateStatusWithPoll (state, { id, poll }) {
diff --git a/src/modules/users.js b/src/modules/users.js
index 05ff44d5..f951483f 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -1,7 +1,7 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
+import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
import oauthApi from '../services/new_api/oauth.js'
import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
-import { set } from 'vue'
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
// TODO: Unify with mergeOrAdd in statuses.js
@@ -15,9 +15,9 @@ export const mergeOrAdd = (arr, obj, item) => {
} else {
// This is a new item, prepare it
arr.push(item)
- set(obj, item.id, item)
+ obj[item.id] = item
if (item.screen_name && !item.screen_name.includes('@')) {
- set(obj, item.screen_name.toLowerCase(), item)
+ obj[item.screen_name.toLowerCase()] = item
}
return { item, new: true }
}
@@ -103,23 +103,23 @@ export const mutations = {
const user = state.usersObject[id]
const tags = user.tags || []
const newTags = tags.concat([tag])
- set(user, 'tags', newTags)
+ user['tags'] = newTags
},
untagUser (state, { user: { id }, tag }) {
const user = state.usersObject[id]
const tags = user.tags || []
const newTags = tags.filter(t => t !== tag)
- set(user, 'tags', newTags)
+ user['tags'] = newTags
},
updateRight (state, { user: { id }, right, value }) {
const user = state.usersObject[id]
let newRights = user.rights
newRights[right] = value
- set(user, 'rights', newRights)
+ user['rights'] = newRights
},
updateActivationStatus (state, { user: { id }, deactivated }) {
const user = state.usersObject[id]
- set(user, 'deactivated', deactivated)
+ user['deactivated'] = deactivated
},
setCurrentUser (state, user) {
state.lastLoginName = user.screen_name
@@ -148,26 +148,26 @@ export const mutations = {
clearFriends (state, userId) {
const user = state.usersObject[userId]
if (user) {
- set(user, 'friendIds', [])
+ user['friendIds'] = []
}
},
clearFollowers (state, userId) {
const user = state.usersObject[userId]
if (user) {
- set(user, 'followerIds', [])
+ user['followerIds'] = []
}
},
addNewUsers (state, users) {
each(users, (user) => {
if (user.relationship) {
- set(state.relationships, user.relationship.id, user.relationship)
+ state.relationships[user.relationship.id] = user.relationship
}
mergeOrAdd(state.users, state.usersObject, user)
})
},
updateUserRelationship (state, relationships) {
relationships.forEach((relationship) => {
- set(state.relationships, relationship.id, relationship)
+ state.relationships[relationship.id] = relationship
})
},
saveBlockIds (state, blockIds) {
@@ -222,7 +222,7 @@ export const mutations = {
},
setColor (state, { user: { id }, highlighted }) {
const user = state.usersObject[id]
- set(user, 'highlight', highlighted)
+ user['highlight'] = highlighted
},
signUpPending (state) {
state.signUpPending = true
@@ -507,6 +507,8 @@ const users = {
store.commit('resetStatuses')
store.dispatch('resetChats')
store.dispatch('setLastTimeline', 'public-timeline')
+ store.dispatch('setLayoutWidth', windowWidth())
+ store.dispatch('setLayoutHeight', windowHeight())
})
},
loginUser (store, accessToken) {
@@ -567,6 +569,9 @@ const users = {
// Get user mutes
store.dispatch('fetchMutes')
+ store.dispatch('setLayoutWidth', windowWidth())
+ store.dispatch('setLayoutHeight', windowHeight())
+
// Fetch our friends
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
.then((friends) => commit('addNewUsers', friends))
diff --git a/src/panel.scss b/src/panel.scss
new file mode 100644
index 00000000..3a814269
--- /dev/null
+++ b/src/panel.scss
@@ -0,0 +1,198 @@
+.panel {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ background-color: $fallback--bg;
+ background-color: var(--bg, $fallback--bg);
+
+ &::after,
+ & {
+ border-radius: $fallback--panelRadius;
+ border-radius: var(--panelRadius, $fallback--panelRadius);
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 5;
+ box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
+ box-shadow: var(--panelShadow);
+ pointer-events: none;
+ }
+}
+
+.panel-body {
+ padding: var(--panel-body-padding, 0);
+
+ &:empty::before {
+ content: "¯\\_(ツ)_/¯"; // Could use words but it'd require translations
+ display: block;
+ margin: 1em;
+ text-align: center;
+ }
+
+ > p {
+ line-height: 1.3;
+ padding: 1em;
+ margin: 0;
+ }
+}
+
+.panel-heading,
+.panel-footer {
+ --panel-heading-height-padding: 0.6em;
+ --__panel-heading-height: 3.2em;
+ --__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding));
+
+ position: relative;
+ box-sizing: border-box;
+ display: grid;
+ grid-auto-flow: column;
+ grid-template-columns: minmax(50%, 1fr);
+ grid-auto-columns: auto;
+ grid-column-gap: 0.5em;
+ flex: none;
+ background-size: cover;
+ padding: 0.6em;
+ height: var(--__panel-heading-height);
+ line-height: var(--__panel-heading-height-inner);
+ z-index: 4;
+
+ &.-flexible-height {
+ --__panel-heading-height: auto;
+
+ &::after,
+ &::before {
+ display: none;
+ }
+ }
+
+ &.-stub {
+ &,
+ &::after {
+ border-radius: $fallback--panelRadius;
+ border-radius: var(--panelRadius, $fallback--panelRadius);
+ }
+ }
+
+ &.-sticky {
+ position: sticky;
+ top: var(--navbar-height);
+ }
+
+ &::after,
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ pointer-events: none;
+ }
+
+ .title {
+ font-size: 1.3em;
+ }
+
+ .alert {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow-x: hidden;
+ }
+
+ &:not(.-flexible-height) {
+ > .button-default,
+ > .alert {
+ height: var(--__panel-heading-height-inner);
+ min-height: 0;
+ box-sizing: border-box;
+ margin: 0;
+ min-width: 1px;
+ padding-top: 0;
+ padding-bottom: 0;
+ align-self: stretch;
+ }
+ }
+}
+
+// TODO Should refactor panels into separate component and utilize slots
+
+.panel-heading {
+ border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
+ border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
+ border-width: 0 0 1px 0;
+ align-items: start;
+ // panel theme
+ color: var(--panelText);
+ background-color: $fallback--bg;
+ background-color: var(--bg, $fallback--bg);
+
+ &::after {
+ background-color: $fallback--fg;
+ background-color: var(--panel, $fallback--fg);
+ z-index: -2;
+ border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
+ border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
+ box-shadow: var(--panelHeaderShadow);
+ }
+
+ a,
+ .-link {
+ color: $fallback--link;
+ color: var(--panelLink, $fallback--link);
+ }
+
+ .faint {
+ background-color: transparent;
+ color: $fallback--faint;
+ color: var(--panelFaint, $fallback--faint);
+ }
+
+ .faint-link {
+ color: $fallback--faint;
+ color: var(--faintLink, $fallback--faint);
+ }
+
+ &:not(.-flexible-height) {
+ > .button-default {
+ flex-shrink: 0;
+
+ &,
+ i[class*=icon-] {
+ color: $fallback--text;
+ color: var(--btnPanelText, $fallback--text);
+ }
+
+ &:active {
+ background-color: $fallback--fg;
+ background-color: var(--btnPressedPanel, $fallback--fg);
+ color: $fallback--text;
+ color: var(--btnPressedPanelText, $fallback--text);
+ }
+
+ &:disabled {
+ color: $fallback--text;
+ color: var(--btnDisabledPanelText, $fallback--text);
+ }
+
+ &.toggled {
+ color: $fallback--text;
+ color: var(--btnToggledPanelText, $fallback--text);
+ }
+ }
+ }
+}
+
+.panel-footer {
+ border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
+ border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
+ align-items: center;
+ border-width: 1px 0 0 0;
+ border-style: solid;
+ border-color: var(--border, $fallback--border);
+}
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index c17d0476..8f09545c 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -9,6 +9,8 @@ const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
+const MOVE_ACCOUNT_URL = '/api/pleroma/move_account'
+const ALIASES_URL = '/api/pleroma/aliases'
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate'
@@ -88,6 +90,7 @@ const PLEROMA_CHAT_MESSAGES_URL = id => `/api/v1/pleroma/chats/${id}/messages`
const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read`
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
const PLEROMA_ADMIN_REPORTS = '/api/pleroma/admin/reports'
+const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
const oldfetch = window.fetch
@@ -152,9 +155,15 @@ const updateNotificationSettings = ({ credentials, settings }) => {
}).then((data) => data.json())
}
-const updateProfileImages = ({ credentials, avatar = null, banner = null, background = null }) => {
+const updateProfileImages = ({ credentials, avatar = null, avatarName = null, banner = null, background = null }) => {
const form = new FormData()
- if (avatar !== null) form.append('avatar', avatar)
+ if (avatar !== null) {
+ if (avatarName !== null) {
+ form.append('avatar', avatar, avatarName)
+ } else {
+ form.append('avatar', avatar)
+ }
+ }
if (banner !== null) form.append('header', banner)
if (background !== null) form.append('pleroma_background_image', background)
return fetch(MASTODON_PROFILE_UPDATE_URL, {
@@ -192,6 +201,7 @@ const updateProfile = ({ credentials, params }) => {
// homepage
// location
// token
+// language
const register = ({ params, credentials }) => {
const { nickname, ...rest } = params
return fetch(MASTODON_REGISTRATION_URL, {
@@ -789,6 +799,49 @@ const changeEmail = ({ credentials, email, password }) => {
.then((response) => response.json())
}
+const moveAccount = ({ credentials, password, targetAccount }) => {
+ const form = new FormData()
+
+ form.append('password', password)
+ form.append('target_account', targetAccount)
+
+ return fetch(MOVE_ACCOUNT_URL, {
+ body: form,
+ method: 'POST',
+ headers: authHeaders(credentials)
+ })
+ .then((response) => response.json())
+}
+
+const addAlias = ({ credentials, alias }) => {
+ return promisedRequest({
+ url: ALIASES_URL,
+ method: 'PUT',
+ credentials,
+ payload: { alias }
+ })
+}
+
+const deleteAlias = ({ credentials, alias }) => {
+ return promisedRequest({
+ url: ALIASES_URL,
+ method: 'DELETE',
+ credentials,
+ payload: { alias }
+ })
+}
+
+const listAliases = ({ credentials }) => {
+ return promisedRequest({
+ url: ALIASES_URL,
+ method: 'GET',
+ credentials,
+ params: {
+ _cacheBooster: (new Date()).getTime()
+ }
+ })
+}
+
const changePassword = ({ credentials, password, newPassword, newPasswordConfirmation }) => {
const form = new FormData()
@@ -875,6 +928,25 @@ const fetchBlocks = ({ credentials }) => {
.then((users) => users.map(parseUser))
}
+const addBackup = ({ credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_BACKUP_URL,
+ method: 'POST',
+ credentials
+ })
+}
+
+const listBackups = ({ credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_BACKUP_URL,
+ method: 'GET',
+ credentials,
+ params: {
+ _cacheBooster: (new Date()).getTime()
+ }
+ })
+}
+
const fetchOAuthTokens = ({ credentials }) => {
const url = '/api/oauth_tokens.json'
@@ -1358,12 +1430,18 @@ const apiService = {
importFollows,
deleteAccount,
changeEmail,
+ moveAccount,
+ addAlias,
+ deleteAlias,
+ listAliases,
changePassword,
settingsMFA,
mfaDisableOTP,
generateMfaBackupCodes,
mfaSetupOTP,
mfaConfirmOTP,
+ addBackup,
+ listBackups,
fetchFollowRequests,
approveUser,
denyUser,
diff --git a/src/services/date_utils/date_utils.js b/src/services/date_utils/date_utils.js
index 32e13bca..677c184c 100644
--- a/src/services/date_utils/date_utils.js
+++ b/src/services/date_utils/date_utils.js
@@ -10,31 +10,29 @@ export const relativeTime = (date, nowThreshold = 1) => {
if (typeof date === 'string') date = Date.parse(date)
const round = Date.now() > date ? Math.floor : Math.ceil
const d = Math.abs(Date.now() - date)
- let r = { num: round(d / YEAR), key: 'time.years' }
+ let r = { num: round(d / YEAR), key: 'time.unit.years' }
if (d < nowThreshold * SECOND) {
r.num = 0
r.key = 'time.now'
} else if (d < MINUTE) {
r.num = round(d / SECOND)
- r.key = 'time.seconds'
+ r.key = 'time.unit.seconds'
} else if (d < HOUR) {
r.num = round(d / MINUTE)
- r.key = 'time.minutes'
+ r.key = 'time.unit.minutes'
} else if (d < DAY) {
r.num = round(d / HOUR)
- r.key = 'time.hours'
+ r.key = 'time.unit.hours'
} else if (d < WEEK) {
r.num = round(d / DAY)
- r.key = 'time.days'
+ r.key = 'time.unit.days'
} else if (d < MONTH) {
r.num = round(d / WEEK)
- r.key = 'time.weeks'
+ r.key = 'time.unit.weeks'
} else if (d < YEAR) {
r.num = round(d / MONTH)
- r.key = 'time.months'
+ r.key = 'time.unit.months'
}
- // Remove plural form when singular
- if (r.num === 1) r.key = r.key.slice(0, -1)
return r
}
diff --git a/src/services/export_import/export_import.js b/src/services/export_import/export_import.js
index ac67cf9c..7fee0ad3 100644
--- a/src/services/export_import/export_import.js
+++ b/src/services/export_import/export_import.js
@@ -1,9 +1,11 @@
+import utf8 from 'utf8'
+
export const newExporter = ({
filename = 'data',
getExportedObject
}) => ({
exportData () {
- const stringified = JSON.stringify(getExportedObject(), null, 2) // Pretty-print and indent with 2 spaces
+ const stringified = utf8.encode(JSON.stringify(getExportedObject(), null, 2)) // Pretty-print and indent with 2 spaces
// Create an invisible link with a data url and simulate a click
const e = document.createElement('a')
diff --git a/src/services/locale/locale.service.js b/src/services/locale/locale.service.js
index 5be99d81..8cef2522 100644
--- a/src/services/locale/locale.service.js
+++ b/src/services/locale/locale.service.js
@@ -1,12 +1,35 @@
+import languagesObject from '../../i18n/messages'
+import ISO6391 from 'iso-639-1'
+import _ from 'lodash'
+
const specialLanguageCodes = {
'ja_easy': 'ja',
- 'zh_Hant': 'zh-HANT'
+ 'zh_Hant': 'zh-HANT',
+ 'zh': 'zh-Hans'
}
const internalToBrowserLocale = code => specialLanguageCodes[code] || code
+const internalToBackendLocale = code => internalToBrowserLocale(code).replace('_', '-')
+
+const getLanguageName = (code) => {
+ const specialLanguageNames = {
+ 'ja_easy': 'やさしいにほんご',
+ 'zh': '简体中文',
+ 'zh_Hant': '繁體中文'
+ }
+ const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code)
+ const browserLocale = internalToBrowserLocale(code)
+ return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1)
+}
+
+const languages = _.map(languagesObject.languages, (code) => ({ code: code, name: getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))
+
const localeService = {
- internalToBrowserLocale
+ internalToBrowserLocale,
+ internalToBackendLocale,
+ languages,
+ getLanguageName
}
export default localeService
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index 8c70ea53..0f8b9b02 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -15,11 +15,12 @@ export const visibleTypes = store => {
rootState.config.notificationVisibility.followRequest && 'follow_request',
rootState.config.notificationVisibility.moves && 'move',
rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction',
- rootState.config.notificationVisibility.reports && 'pleroma:report'
+ rootState.config.notificationVisibility.reports && 'pleroma:report',
+ rootState.config.notificationVisibility.polls && 'poll'
].filter(_ => _))
}
-const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction']
+const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction', 'poll']
export const isStatusNotification = (type) => includes(statusNotifications, type)
@@ -102,6 +103,9 @@ export const prepareNotificationObject = (notification, i18n) => {
case 'pleroma:report':
i18nString = 'submitted_report'
break
+ case 'poll':
+ i18nString = 'poll_ended'
+ break
}
if (notification.type === 'pleroma:emoji_reaction') {
diff --git a/src/services/offset_finder/offset_finder.service.js b/src/services/offset_finder/offset_finder.service.js
index 9034f8c8..5a904f08 100644
--- a/src/services/offset_finder/offset_finder.service.js
+++ b/src/services/offset_finder/offset_finder.service.js
@@ -9,7 +9,7 @@ export const findOffset = (child, parent, { top = 0, left = 0 } = {}, ignorePadd
result.left += ignorePadding ? 0 : leftPadding
}
- if (child.offsetParent && (parent === window || parent.contains(child.offsetParent) || parent === child.offsetParent)) {
+ if (child.offsetParent && window.getComputedStyle(child.offsetParent).position !== 'sticky' && (parent === window || parent.contains(child.offsetParent) || parent === child.offsetParent)) {
return findOffset(child.offsetParent, parent, result, false)
} else {
if (parent !== window) {
diff --git a/src/services/resettable_async_component.js b/src/services/resettable_async_component.js
index 517bbd88..1c046ce7 100644
--- a/src/services/resettable_async_component.js
+++ b/src/services/resettable_async_component.js
@@ -1,4 +1,4 @@
-import Vue from 'vue'
+import { defineAsyncComponent, shallowReactive, h } from 'vue'
/* By default async components don't have any way to recover, if component is
* failed, it is failed forever. This helper tries to remedy that by recreating
@@ -8,23 +8,21 @@ import Vue from 'vue'
* actual target component itself if needs to be.
*/
function getResettableAsyncComponent (asyncComponent, options) {
- const asyncComponentFactory = () => () => ({
- component: asyncComponent(),
+ const asyncComponentFactory = () => () => defineAsyncComponent({
+ loader: asyncComponent,
...options
})
- const observe = Vue.observable({ c: asyncComponentFactory() })
+ const observe = shallowReactive({ c: asyncComponentFactory() })
return {
- functional: true,
- render (createElement, { data, children }) {
+ render () {
// emit event resetAsyncComponent to reloading
- data.on = {}
- data.on.resetAsyncComponent = () => {
- observe.c = asyncComponentFactory()
- // parent.$forceUpdate()
- }
- return createElement(observe.c, data, children)
+ return h(observe.c(), {
+ onResetAsyncComponent () {
+ observe.c = asyncComponentFactory()
+ }
+ })
}
}
}
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index f75e6916..543aa874 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -13,10 +13,10 @@ export const applyTheme = (input) => {
const styleSheet = styleEl.sheet
styleSheet.toString()
- styleSheet.insertRule(`body { ${rules.radii} }`, 'index-max')
- styleSheet.insertRule(`body { ${rules.colors} }`, 'index-max')
- styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max')
- styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max')
+ styleSheet.insertRule(`:root { ${rules.radii} }`, 'index-max')
+ styleSheet.insertRule(`:root { ${rules.colors} }`, 'index-max')
+ styleSheet.insertRule(`:root { ${rules.shadows} }`, 'index-max')
+ styleSheet.insertRule(`:root { ${rules.fonts} }`, 'index-max')
body.classList.remove('hidden')
}
diff --git a/src/sw.js b/src/sw.js
index f5e34dd6..9118dd71 100644
--- a/src/sw.js
+++ b/src/sw.js
@@ -3,12 +3,10 @@
import localForage from 'localforage'
import { parseNotification } from './services/entity_normalizer/entity_normalizer.service.js'
import { prepareNotificationObject } from './services/notification_utils/notification_utils.js'
-import Vue from 'vue'
-import VueI18n from 'vue-i18n'
+import { createI18n } from 'vue-i18n'
import messages from './i18n/service_worker_messages.js'
-Vue.use(VueI18n)
-const i18n = new VueI18n({
+const i18n = createI18n({
// By default, use the browser locale, we will update it if neccessary
locale: 'en',
fallbackLocale: 'en',