aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.js45
-rw-r--r--src/App.scss838
-rw-r--r--src/App.vue42
-rw-r--r--src/boot/after_store.js6
-rw-r--r--src/boot/routes.js2
-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/attachment/attachment.scss2
-rw-r--r--src/components/chat/chat.js97
-rw-r--r--src/components/chat/chat.scss88
-rw-r--r--src/components/chat/chat.vue13
-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_new/chat_new.scss8
-rw-r--r--src/components/chat_new/chat_new.vue7
-rw-r--r--src/components/chat_title/chat_title.vue19
-rw-r--r--src/components/conversation/conversation.vue10
-rw-r--r--src/components/desktop_nav/desktop_nav.scss16
-rw-r--r--src/components/dialog_modal/dialog_modal.vue9
-rw-r--r--src/components/emoji_input/emoji_input.vue2
-rw-r--r--src/components/emoji_picker/emoji_picker.js5
-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/global_notice_list/global_notice_list.vue11
-rw-r--r--src/components/interface_language_switcher/interface_language_switcher.vue38
-rw-r--r--src/components/link-preview/link-preview.vue2
-rw-r--r--src/components/login_form/login_form.vue2
-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/mobile_nav/mobile_nav.js3
-rw-r--r--src/components/mobile_nav/mobile_nav.vue17
-rw-r--r--src/components/mobile_post_status_button/mobile_post_status_button.js2
-rw-r--r--src/components/modal/modal.vue2
-rw-r--r--src/components/notification/notification.vue8
-rw-r--r--src/components/notifications/notification_filters.vue22
-rw-r--r--src/components/notifications/notifications.js18
-rw-r--r--src/components/notifications/notifications.scss19
-rw-r--r--src/components/notifications/notifications.vue116
-rw-r--r--src/components/password_reset/password_reset.vue10
-rw-r--r--src/components/popover/popover.vue42
-rw-r--r--src/components/post_status_form/post_status_form.js2
-rw-r--r--src/components/post_status_form/post_status_form.vue69
-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.js14
-rw-r--r--src/components/registration/registration.vue25
-rw-r--r--src/components/remote_follow/remote_follow.vue2
-rw-r--r--src/components/select/select.vue6
-rw-r--r--src/components/settings_modal/settings_modal.scss14
-rw-r--r--src/components/settings_modal/settings_modal.vue23
-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.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.scss2
-rw-r--r--src/components/settings_modal/tabs/profile_tab.vue7
-rw-r--r--src/components/settings_modal/tabs/theme_tab/theme_tab.scss19
-rw-r--r--src/components/shout_panel/shout_panel.vue19
-rw-r--r--src/components/status/status.scss17
-rw-r--r--src/components/status_body/status_body.scss2
-rw-r--r--src/components/still-image/still-image.vue8
-rw-r--r--src/components/tab_switcher/tab_switcher.scss3
-rw-r--r--src/components/timeago/timeago.vue2
-rw-r--r--src/components/timeline/timeline.js13
-rw-r--r--src/components/timeline/timeline.scss28
-rw-r--r--src/components/timeline/timeline.vue66
-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_card/user_card.scss323
-rw-r--r--src/components/user_card/user_card.vue320
-rw-r--r--src/components/user_list_popover/user_list_popover.vue2
-rw-r--r--src/components/user_panel/user_panel.vue1
-rw-r--r--src/components/user_profile/user_profile.js6
-rw-r--r--src/components/user_profile/user_profile.vue27
-rw-r--r--src/components/user_reporting_modal/user_reporting_modal.vue13
-rw-r--r--src/hocs/with_load_more/with_load_more.scss2
-rw-r--r--src/hocs/with_subscription/with_subscription.scss2
-rw-r--r--src/i18n/en.json62
-rw-r--r--src/i18n/messages.js1
-rw-r--r--src/i18n/service_worker_messages.js1
-rw-r--r--src/i18n/sk.json512
-rw-r--r--src/modules/config.js14
-rw-r--r--src/modules/interface.js30
-rw-r--r--src/modules/users.js6
-rw-r--r--src/panel.scss198
-rw-r--r--src/services/api/api.service.js11
-rw-r--r--src/services/date_utils/date_utils.js16
-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/style_setter/style_setter.js8
94 files changed, 2175 insertions, 1465 deletions
diff --git a/src/App.js b/src/App.js
index 294f6000..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,
@@ -50,6 +50,16 @@ export default {
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 () {
@@ -72,31 +82,38 @@ export default {
!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 180c0daf..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-from, .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 b18b3308..21f6f686 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -7,33 +7,26 @@
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,13 +40,14 @@
</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 />
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 76832708..f655c38f 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -8,7 +8,7 @@ import App from '../App.vue'
import routes from './routes'
import VBodyScrollLock from 'src/directives/body_scroll_lock'
-import { windowWidth } from '../services/window_utils/window_utils'
+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'
@@ -332,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()
diff --git a/src/boot/routes.js b/src/boot/routes.js
index 905ffe41..726476a8 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -62,7 +62,7 @@ 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 }) },
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/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/chat/chat.js b/src/components/chat/chat.js
index aef11712..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()
},
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 493c5d5a..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"
@@ -27,10 +26,8 @@
</div>
</div>
<div
- ref="scrollable"
- class="scrollable-message-list"
+ class="message-list"
:style="{ height: scrollableContainerHeight }"
- @scroll="handleScroll"
>
<template v-if="!errorLoadingChat">
<ChatMessage
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_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.vue b/src/components/chat_title/chat_title.vue
index f4706caf..7f6aaaa4 100644
--- a/src/components/chat_title/chat_title.vue
+++ b/src/components/chat_title/chat_title.vue
@@ -4,19 +4,19 @@
: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 || []"
/>
@@ -33,7 +33,6 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- align-items: center;
--emoji-size: 14px;
@@ -46,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/conversation/conversation.vue b/src/components/conversation/conversation.vue
index ccc523a2..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
@@ -201,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;
@@ -223,6 +225,7 @@
--text: var(--faint);
color: var(--text);
}
+
.thread-ancestor-dive-box {
padding-left: var(--status-margin, $status-margin);
border-bottom-width: 1px;
@@ -250,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;
}
@@ -270,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/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.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 6b589079..bd5c2e39 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -6,6 +6,7 @@ import {
faStickyNote,
faSmileBeam
} from '@fortawesome/free-solid-svg-icons'
+import { trim } from 'lodash'
library.add(
faBoxOpen,
@@ -176,7 +177,7 @@ const EmojiPicker = {
filteredEmoji () {
return filterByKeyword(
this.$store.state.instance.customEmoji || [],
- this.keyword
+ trim(this.keyword)
)
},
customEmojiBuffer () {
@@ -197,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/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/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue
index 6d1f83c4..7ad1fe2e 100644
--- a/src/components/interface_language_switcher/interface_language_switcher.vue
+++ b/src/components/interface_language_switcher/interface_language_switcher.vue
@@ -1,12 +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"
@@ -20,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 87fa37c6..21482977 100644
--- a/src/components/login_form/login_form.vue
+++ b/src/components/login_form/login_form.vue
@@ -101,7 +101,7 @@
padding: 0.6em;
.btn {
- min-height: 28px;
+ min-height: 2em;
width: 10em;
}
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/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 f5279b3e..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,12 +88,10 @@
.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);
@@ -156,8 +150,9 @@
z-index: 1001;
-webkit-overflow-scrolling: touch;
- &.closed {
+ &.-closed {
transform: translateX(100%);
+ box-shadow: none;
}
}
@@ -185,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 4866ac57..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
@@ -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/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/notification/notification.vue b/src/components/notification/notification.vue
index 9ecb034f..7d3d0c69 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -120,6 +120,14 @@
</i18n-t>
</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"
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 a285027d..3d3408f7 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;
@@ -113,13 +113,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%;
@@ -142,7 +142,7 @@
}
.timeago {
- margin-right: .2em;
+ margin-right: 0.2em;
}
.status-content {
@@ -155,7 +155,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/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/popover/popover.vue b/src/components/popover/popover.vue
index 332a0398..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,9 +154,13 @@
content: '✓';
}
- &.menu-checkbox-radio::after {
- font-size: 2em;
- content: '•';
+ &.-radio {
+ border-radius: 9999px;
+
+ &.menu-checkbox-checked::after {
+ font-size: 2em;
+ content: '•';
+ }
}
}
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 84a9e29e..2febf226 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -486,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 0fdb6dc7..62613bd1 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -8,15 +8,6 @@
@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-t
v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private' && !disableLockWarning"
@@ -278,6 +269,15 @@
</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"
>
@@ -336,7 +336,7 @@
display: flex;
justify-content: space-between;
padding: 0.5em;
- height: 32px;
+ height: 2.5em;
button {
width: 10em;
@@ -394,7 +394,6 @@
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
padding: 0.5em;
margin: 0;
- line-height: 1.4em;
}
.text-format {
@@ -408,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
@@ -441,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 {
@@ -489,10 +487,6 @@
flex-direction: column;
}
- .btn {
- cursor: pointer;
- }
-
.btn[disabled] {
cursor: not-allowed;
}
@@ -508,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;
@@ -551,10 +539,6 @@
}
}
- .btn {
- cursor: pointer;
- }
-
.btn[disabled] {
cursor: not-allowed;
}
@@ -571,7 +555,6 @@
.drop-indicator {
position: absolute;
- z-index: 1;
width: 100%;
height: 100%;
font-size: 5em;
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 a3ef0f04..6eb316d0 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -1,6 +1,8 @@
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 = {
setup () { return { v$: useVuelidate() } },
@@ -11,10 +13,14 @@ const registration = {
username: '',
password: '',
confirm: '',
- reason: ''
+ reason: '',
+ language: ''
},
captcha: {}
}),
+ components: {
+ InterfaceLanguageSwitcher
+ },
validations () {
return {
user: {
@@ -26,7 +32,8 @@ const registration = {
required,
sameAs: sameAs(this.user.password)
},
- reason: { required: requiredIf(() => this.accountApprovalRequired) }
+ reason: { required: requiredIf(() => this.accountApprovalRequired) },
+ language: {}
}
}
},
@@ -64,6 +71,9 @@ 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()
diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue
index 3d409109..cc655c0b 100644
--- a/src/components/registration/registration.vue
+++ b/src/components/registration/registration.vue
@@ -163,6 +163,18 @@
</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/select/select.vue b/src/components/select/select.vue
index ea8c8b69..92493b0b 100644
--- a/src/components/select/select.vue
+++ b/src/components/select/select.vue
@@ -39,10 +39,10 @@ label.Select {
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;
}
@@ -55,7 +55,7 @@ label.Select {
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/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
index 2f7649a9..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;
}
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
index e0e8304d..d3bed061 100644
--- a/src/components/settings_modal/settings_modal.vue
+++ b/src/components/settings_modal/settings_modal.vue
@@ -11,22 +11,13 @@
{{ $t('settings.settings') }}
</span>
<transition name="fade">
- <div 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>
+ <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
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.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 3c9683cd..201f1a76 100644
--- a/src/components/settings_modal/tabs/profile_tab.scss
+++ b/src/components/settings_modal/tabs/profile_tab.scss
@@ -89,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 881016fb..4cd93772 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -89,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"
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 21b49a32..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,25 +245,8 @@
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;
}
}
}
@@ -342,7 +325,7 @@
.btn {
flex-grow: 1;
- min-height: 28px;
+ min-height: 2em;
min-width: 0;
max-width: 10em;
padding: 0;
diff --git a/src/components/shout_panel/shout_panel.vue b/src/components/shout_panel/shout_panel.vue
index f3e9ff96..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 {
@@ -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/status/status.scss b/src/components/status/status.scss
index 5ed43e0d..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;
}
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/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.scss b/src/components/tab_switcher/tab_switcher.scss
index 575d41e1..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;
diff --git a/src/components/timeago/timeago.vue b/src/components/timeago/timeago.vue
index bed29020..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>
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 8ec5d1e5..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 {
@@ -60,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'] : [])
}
@@ -76,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 () {
@@ -141,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 d37a9e2a..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
>
@@ -46,37 +46,39 @@
</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_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 14b4643a..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
@@ -284,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.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 50949b98..243de387 100644
--- a/src/components/user_panel/user_panel.vue
+++ b/src/components/user_panel/user_panel.vue
@@ -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 eeb6ea40..f779b823 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -39,7 +39,8 @@ const UserProfile = {
return {
error: false,
userId: null,
- tab: defaultTabKey
+ tab: defaultTabKey,
+ footerRef: null
}
},
created () {
@@ -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 cc456365..030ce2c4 100644
--- a/src/components/user_reporting_modal/user_reporting_modal.vue
+++ b/src/components/user_reporting_modal/user_reporting_modal.vue
@@ -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/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.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/en.json b/src/i18n/en.json
index 06c2a5e4..0c1de44c 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -160,7 +160,8 @@
"repeated_you": "repeated your status",
"no_more_notifications": "No more notifications",
"migrated_to": "migrated to",
- "reacted_with": "reacted with {0}"
+ "reacted_with": "reacted with {0}",
+ "poll_ended": "poll has ended"
},
"polls": {
"add_poll": "Add poll",
@@ -254,7 +255,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",
@@ -303,6 +305,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",
@@ -377,7 +380,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",
@@ -437,6 +440,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",
@@ -491,6 +495,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",
@@ -692,38 +702,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",
diff --git a/src/i18n/messages.js b/src/i18n/messages.js
index 977b8eb3..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'),
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/modules/config.js b/src/modules/config.js
index ff5ef270..6ae2e754 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -1,5 +1,9 @@
+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]
@@ -46,6 +50,7 @@ export const defaultState = {
pauseOnUnfocused: true,
stopGifs: true,
replyVisibility: 'all',
+ thirdColumnMode: 'notifications',
notificationVisibility: {
follows: true,
mentions: true,
@@ -54,7 +59,8 @@ export const defaultState = {
moves: true,
emojiReactions: true,
followRequest: true,
- chatMention: true
+ chatMention: true,
+ polls: true
},
webPushNotifications: false,
muteWords: [],
@@ -73,6 +79,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
@@ -162,6 +170,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/interface.js b/src/modules/interface.js
index 17277331..a86193ea 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -13,7 +13,7 @@ const defaultState = {
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
)
},
- mobileLayout: false,
+ layoutType: 'normal',
globalNotices: [],
layoutHeight: 0,
lastTimeline: null
@@ -36,8 +36,8 @@ const interfaceMod = {
setNotificationPermission (state, permission) {
state.notificationPermission = permission
},
- setMobileLayout (state, value) {
- state.mobileLayout = value
+ setLayoutType (state, value) {
+ state.layoutType = value
},
closeSettingsModal (state) {
state.settingsModalState = 'hidden'
@@ -72,6 +72,9 @@ const interfaceMod = {
setLayoutHeight (state, value) {
state.layoutHeight = value
},
+ setLayoutWidth (state, value) {
+ state.layoutWidth = value
+ },
setLastTimeline (state, value) {
state.lastTimeline = value
}
@@ -86,9 +89,6 @@ const interfaceMod = {
setNotificationPermission ({ commit }, permission) {
commit('setNotificationPermission', permission)
},
- setMobileLayout ({ commit }, value) {
- commit('setMobileLayout', value)
- },
closeSettingsModal ({ commit }) {
commit('closeSettingsModal')
},
@@ -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/users.js b/src/modules/users.js
index e5889fdb..f951483f 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -1,4 +1,5 @@
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 { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
@@ -506,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) {
@@ -566,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 d2df353a..bf12b559 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -152,9 +152,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 +198,7 @@ const updateProfile = ({ credentials, params }) => {
// homepage
// location
// token
+// language
const register = ({ params, credentials }) => {
const { nickname, ...rest } = params
return fetch(MASTODON_REGISTRATION_URL, {
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/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 6fef1022..a221b022 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -14,11 +14,12 @@ export const visibleTypes = store => {
rootState.config.notificationVisibility.follows && 'follow',
rootState.config.notificationVisibility.followRequest && 'follow_request',
rootState.config.notificationVisibility.moves && 'move',
- rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
+ rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction',
+ 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)
@@ -98,6 +99,9 @@ export const prepareNotificationObject = (notification, i18n) => {
case 'follow_request':
i18nString = 'follow_request'
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/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')
}