diff options
Diffstat (limited to 'src')
34 files changed, 758 insertions, 719 deletions
@@ -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,6 +82,10 @@ export default { !this.$store.getters.mergedConfig.hideISP && this.$store.state.instance.instanceSpecificPanelContent }, + newPostButtonShown () { + if (this.$route.name === 'chat' || this.$route.name === 'chats') 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 @@ -79,22 +93,22 @@ export default { 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 () { return this.$store.getters.mergedConfig.sidebarRight }, + noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders }, + showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars }, ...mapGetters(['mergedConfig']) }, methods: { updateMobileState () { const mobileLayout = windowWidth() <= 800 + const wideLayout = windowWidth() >= 1300 const layoutHeight = windowHeight() - const changed = mobileLayout !== this.isMobileLayout + const layoutType = wideLayout ? 'wide' : (mobileLayout ? 'mobile' : 'normal') + const changed = layoutType !== this.layoutType if (changed) { - this.$store.dispatch('setMobileLayout', mobileLayout) + this.$store.dispatch('setLayoutType', layoutType) } this.$store.dispatch('setLayoutHeight', layoutHeight) } diff --git a/src/App.scss b/src/App.scss index 180c0daf..01359f2c 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,48 +1,8 @@ +// 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: 50px; } html { @@ -50,28 +10,279 @@ html { } 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; &.hidden { display: none; } } +@media (any-pointer: fine) { + body { + background: var(--bg); + } + + * { + 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); + } + + &::-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%; + } + } + } + } +} + a { text-decoration: none; color: $fallback--link; color: var(--link, $fallback--link); } +h4 { + margin: 0; +} + +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 { + margin-top: var(--navbar-height); + 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: 25em; + --maxiColumn: minmax(var(--miniColumn), 45em); + --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; + + .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) / 2) 0; + row-gap: var(--___columnMargin); + align-content: start; + + &.-scrollable { + --___paddingIncrease: calc(var(--columnGap) / 2); + + padding-top: 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); + + // Only show custom scrollbars on devices which + // have a cursor/pointer to operate them + &: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) / -2); + } + } + } + + &.-has-new-post-button { + .column { + padding-bottom: 20em; + } + } + + &.-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: 0; + } + + .underlay, + #sidebar, + #notifs-column { + display: none; + } + } + + &.-normal { + #notifs-column { + display: none; + } + } +} + +.text-center { + text-align: center; +} + .button-default { user-select: none; color: $fallback--text; @@ -103,12 +314,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 +352,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 +402,9 @@ a { } } -input, textarea, .input { - +input, +textarea, +.input { &.unstyled { border-radius: 0; background: none; @@ -203,7 +415,7 @@ input, textarea, .input { 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); @@ -219,9 +431,11 @@ input, textarea, .input { height: 28px; line-height: 16px; hyphens: none; - padding: 8px .5em; + padding: 8px 0.5em; - &:disabled, &[disabled=disabled], &.disabled { + &:disabled, + &[disabled=disabled], + &.disabled { cursor: not-allowed; opacity: 0.5; } @@ -236,18 +450,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,9 +473,9 @@ 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; @@ -274,17 +491,20 @@ input, textarea, .input { &[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,9 +514,9 @@ 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; @@ -324,6 +544,7 @@ option { .hide-number-spinner { -moz-appearance: textfield; + &[type=number]::-webkit-inner-spin-button, &[type=number]::-webkit-outer-spin-button { opacity: 0; @@ -331,7 +552,8 @@ option { } } -i[class*=icon-], .svg-inline--fa { +i[class*=icon-], +.svg-inline--fa { color: $fallback--icon; color: var(--icon, $fallback--icon); } @@ -362,273 +584,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; @@ -712,7 +677,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 +692,7 @@ nav { position: absolute; top: 0; right: 0; - padding: .5em; + padding: 0.5em; color: inherit; } } @@ -744,72 +709,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,7 +718,7 @@ nav { a { display: inline-block; - padding: 1em 0px; + padding: 1em 0; width: 100%; } } @@ -828,9 +727,17 @@ nav { min-height: 28px; } -.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 +750,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..094a8ab9 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"> <div v-if="!currentUser" class="login-hint panel panel-default" @@ -47,8 +40,9 @@ </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" diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 76832708..ce96141d 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -332,8 +332,11 @@ const checkOAuthToken = async ({ store }) => { } const afterStoreSetup = async ({ store, i18n }) => { - const width = windowWidth() - store.dispatch('setMobileLayout', width <= 800) + // TODO cleanup copypasta + const mobileLayout = windowWidth() <= 800 + const wideLayout = windowWidth() >= 1300 + const layoutType = wideLayout ? 'wide' : (mobileLayout ? 'mobile' : 'normal') + store.dispatch('setLayoutType', layoutType) FaviconService.initFaviconService() 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/chat/chat.js b/src/components/chat/chat.js index aef11712..9a0ae107 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,6 @@ const Chat = { }, created () { this.startFetching() - window.addEventListener('resize', this.handleLayoutChange) }, mounted () { window.addEventListener('scroll', this.handleScroll) @@ -52,15 +51,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 +91,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 +109,6 @@ const Chat = { '$route': function () { this.startFetching() }, - layoutHeight () { - this.handleResize({ expand: true }) - }, mastoUserSocketStatus (newValue) { if (newValue === WSConnectionStatus.JOINED) { this.fetchChat({ isFirstFetch: true }) @@ -132,7 +123,6 @@ const Chat = { onFilesDropped () { this.$nextTick(() => { this.handleResize() - this.updateScrollableContainerHeight() }) }, handleVisibilityChange () { @@ -142,42 +132,6 @@ 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. handleResize (opts = {}) { const { expand = false, delayed = false } = opts @@ -190,29 +144,21 @@ const Chat = { } this.$nextTick(() => { - this.updateScrollableContainerHeight() - const { offsetHeight = undefined } = this.lastScrollPosition - this.lastScrollPosition = getScrollPosition(this.$refs.scrollable) + this.lastScrollPosition = getScrollPosition() const diff = this.lastScrollPosition.offsetHeight - offsetHeight 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 }) }) } }) }, 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..1bad51b9 100644 --- a/src/components/chat/chat.scss +++ b/src/components/chat/chat.scss @@ -1,19 +1,15 @@ .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%; + // Stick the form to the top/bottom of screen + margin-bottom: calc(var(--___columnMargin) / -2); + margin-top: calc(var(--___columnMargin) / -2); .chat-view-inner { height: auto; width: 100%; overflow: visible; display: flex; - margin: 0.5em 0.5em 0 0.5em; } .chat-view-body { @@ -32,11 +28,9 @@ } } - .scrollable-message-list { + .message-list { padding: 0 0.8em; height: 100%; - overflow-y: scroll; - overflow-x: hidden; display: flex; flex-direction: column; } @@ -44,12 +38,14 @@ .footer { position: sticky; bottom: 0; + background-color: $fallback--bg; + background-color: var(--bg, $fallback--bg); + z-index: 10; } .chat-view-heading { align-items: center; justify-content: space-between; - top: 50px; display: flex; z-index: 2; position: sticky; @@ -115,56 +111,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..d909bed9 100644 --- a/src/components/chat/chat.vue +++ b/src/components/chat/chat.vue @@ -2,13 +2,12 @@ <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 mobile-hidden" > <a class="go-back-button" @@ -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_new/chat_new.vue b/src/components/chat_new/chat_new.vue index f3894a3a..0790bb37 100644 --- a/src/components/chat_new/chat_new.vue +++ b/src/components/chat_new/chat_new.vue @@ -1,6 +1,5 @@ <template> <div - id="nav" class="panel-default panel chat-new" > <div diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue index a92028e8..06ad129d 100644 --- a/src/components/chat_title/chat_title.vue +++ b/src/components/chat_title/chat_title.vue @@ -15,7 +15,7 @@ </router-link> <RichContent class="username" - :title="'@'+user.screen_name_ui" + :title="'@'+(user && user.screen_name_ui)" :html="htmlTitle" :emoji="user.emoji" /> diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index ccc523a2..2e7f8684 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -270,5 +270,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..1e731c70 100644 --- a/src/components/desktop_nav/desktop_nav.scss +++ b/src/components/desktop_nav/desktop_nav.scss @@ -1,9 +1,7 @@ @import '../../_variables.scss'; .DesktopNav { - height: 50px; width: 100%; - position: fixed; a { color: var(--topBarLink, $fallback--link); diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 708a43c6..c7dcd007 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, 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..362441bb 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -51,7 +51,7 @@ <div v-if="currentUser" class="mobile-notifications-drawer" - :class="{ 'closed': !notificationsOpen }" + :class="{ '-closed': !notificationsOpen }" @touchstart.stop="notificationsTouchStart" @touchmove.stop="notificationsTouchMove" > @@ -69,12 +69,9 @@ </div> <div class="mobile-notifications" + id="mobile-notifications" @scroll="onScroll" > - <Notifications - ref="notifications" - :no-heading="true" - /> </div> </div> <SideDrawer @@ -92,12 +89,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 +151,9 @@ z-index: 1001; -webkit-overflow-scrolling: touch; - &.closed { + &.-closed { transform: translateX(100%); + box-shadow: none; } } @@ -185,7 +181,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/notifications/notifications.js b/src/components/notifications/notifications.js index c8f1ebcb..fb2579a5 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -23,8 +23,6 @@ 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, @@ -65,6 +63,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.vue b/src/components/notifications/notifications.vue index 2ce5d56f..bfc97dc0 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" :to="teleportTarget"> + <div + :class="{ minimal: minimalMode }" + class="Notifications" + > + <div :class="mainClass"> <div - v-for="notification in notificationsToDisplay" - :key="notification.id" - class="notification" - :class="{"unseen": !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 notifications-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..e0579568 100644 --- a/src/components/password_reset/password_reset.vue +++ b/src/components/password_reset/password_reset.vue @@ -91,6 +91,9 @@ flex-direction: column; margin-top: 0.6em; max-width: 18rem; + > * { + min-width: 0; + } } .form-group { diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue index 332a0398..3f2a81f5 100644 --- a/src/components/popover/popover.vue +++ b/src/components/popover/popover.vue @@ -37,7 +37,7 @@ } .popover { - z-index: 8; + z-index: 500; position: absolute; min-width: 0; } diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue index 3d409109..1e735c69 100644 --- a/src/components/registration/registration.vue +++ b/src/components/registration/registration.vue @@ -271,7 +271,10 @@ $validations-cRed: #f04124; .container { display: flex; flex-direction: row; - //margin-bottom: 1em; + + > * { + min-width: 0; + } } .terms-of-service { diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss index 2f7649a9..f3738a8c 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: diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index a2c6bffa..e29bc7cf 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -61,6 +61,16 @@ </BooleanSetting> </li> <li> + <BooleanSetting path="disableStickyHeaders"> + {{ $t('settings.disable_sticky_headers') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="showScrollbars"> + {{ $t('settings.show_scrollbars') }} + </BooleanSetting> + </li> + <li> <BooleanSetting path="alwaysShowNewPostButton" expert="1" diff --git a/src/components/status/status.scss b/src/components/status/status.scss index 5ed43e0d..e38e2a53 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; } diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 8ec5d1e5..4ae7410e 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -64,7 +64,7 @@ const Timeline = { 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'] : []) } @@ -141,6 +141,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..c10375d4 100644 --- a/src/components/timeline/timeline.scss +++ b/src/components/timeline/timeline.scss @@ -13,7 +13,6 @@ max-width: 100%; flex-wrap: nowrap; align-items: center; - position: relative; .loadmore-button { flex-shrink: 0; diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 14b4643a..43a660e1 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 @@ -289,6 +289,7 @@ .user-card { position: relative; + z-index: 1; &:hover { --_still-image-img-visibility: visible; @@ -331,6 +332,7 @@ 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; @@ -385,11 +387,16 @@ 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); diff --git a/src/modules/config.js b/src/modules/config.js index ff5ef270..6d2e6ce0 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -73,6 +73,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 diff --git a/src/modules/interface.js b/src/modules/interface.js index 17277331..9df72b88 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' @@ -86,8 +86,8 @@ const interfaceMod = { setNotificationPermission ({ commit }, permission) { commit('setNotificationPermission', permission) }, - setMobileLayout ({ commit }, value) { - commit('setMobileLayout', value) + setLayoutType ({ commit }, value) { + commit('setLayoutType', value) }, closeSettingsModal ({ commit }) { commit('closeSettingsModal') diff --git a/src/panel.scss b/src/panel.scss new file mode 100644 index 00000000..049144ba --- /dev/null +++ b/src/panel.scss @@ -0,0 +1,203 @@ +.panel { + position: relative; + display: flex; + flex-direction: column; + z-index: 0; + 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: 100; + box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6); + box-shadow: var(--panelShadow); + pointer-events: none; + } +} + +.panel-body:empty::before { + content: "¯\\_(ツ)_/¯"; // Could use words but it'd require translations + display: block; margin: 1em; + text-align: center; +} + +.panel-heading { + position: relative; + box-sizing: border-box; + 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: 0.6em 0.6em; + text-align: left; + line-height: 28px; + color: var(--panelText); + background-color: $fallback--bg; + background-color: var(--bg, $fallback--bg); + align-items: baseline; + height: var(--panelHeadingHeight); + z-index: -2; + + --panelHeadingHeight: 45px; + + &.-flexible-height { + --panelHeadingHeight: auto; + + &::after, + &::before { + display: none; + } + } + + &.-sticky { + position: sticky; + top: var(--navbar-height); + z-index: 2; + } + + &::after, + &::before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + pointer-events: none; + } + + &::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); + } + + &::before { + bottom: -20px; + z-index: -1; + border-radius: $fallback--panelRadius $fallback--panelRadius 0 0; + border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0; + mask: linear-gradient(to bottom, white var(--panelHeadingHeight), transparent var(--panelHeadingHeight)); + } + + .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: 0.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 { + 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; +} 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') } |
