Merge branch 'scrolltotop' into 'develop'

add "scroll to top" button to timelines and notifications

See merge request pleroma/pleroma-fe!1605
This commit is contained in:
tusooa 2022-11-05 19:20:54 +00:00
commit f000eea0bf
16 changed files with 270 additions and 93 deletions

View File

@ -20,10 +20,12 @@
<QuickFilterSettings <QuickFilterSettings
v-if="!collapsable" v-if="!collapsable"
:conversation="true" :conversation="true"
class="rightside-button"
/> />
<QuickViewSettings <QuickViewSettings
v-if="!collapsable" v-if="!collapsable"
:conversation="true" :conversation="true"
class="rightside-button"
/> />
</div> </div>
<div class="conversation-body panel-body"> <div class="conversation-body panel-body">

View File

@ -8,13 +8,17 @@ import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faTimes, faTimes,
faBell, faBell,
faBars faBars,
faArrowUp,
faMinus
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
faTimes, faTimes,
faBell, faBell,
faBars faBars,
faArrowUp,
faMinus
) )
const MobileNav = { const MobileNav = {
@ -25,12 +29,13 @@ const MobileNav = {
}, },
data: () => ({ data: () => ({
notificationsCloseGesture: undefined, notificationsCloseGesture: undefined,
notificationsOpen: false notificationsOpen: false,
notificationsAtTop: true
}), }),
created () { created () {
this.notificationsCloseGesture = GestureService.swipeGesture( this.notificationsCloseGesture = GestureService.swipeGesture(
GestureService.DIRECTION_RIGHT, GestureService.DIRECTION_RIGHT,
this.closeMobileNotifications, () => this.closeMobileNotifications(true),
50 50
) )
}, },
@ -61,12 +66,14 @@ const MobileNav = {
openMobileNotifications () { openMobileNotifications () {
this.notificationsOpen = true this.notificationsOpen = true
}, },
closeMobileNotifications () { closeMobileNotifications (markRead) {
if (this.notificationsOpen) { if (this.notificationsOpen) {
// make sure to mark notifs seen only when the notifs were open and not // make sure to mark notifs seen only when the notifs were open and not
// from close-calls. // from close-calls.
this.notificationsOpen = false this.notificationsOpen = false
this.markNotificationsAsSeen() if (markRead) {
this.markNotificationsAsSeen()
}
} }
}, },
notificationsTouchStart (e) { notificationsTouchStart (e) {
@ -78,6 +85,9 @@ const MobileNav = {
scrollToTop () { scrollToTop () {
window.scrollTo(0, 0) window.scrollTo(0, 0)
}, },
scrollMobileNotificationsToTop () {
this.$refs.mobileNotifications.scrollTo(0, 0)
},
logout () { logout () {
this.$router.replace('/main/public') this.$router.replace('/main/public')
this.$store.dispatch('logout') this.$store.dispatch('logout')
@ -87,6 +97,7 @@ const MobileNav = {
this.$store.dispatch('markNotificationsAsSeen') this.$store.dispatch('markNotificationsAsSeen')
}, },
onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) { onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {
this.notificationsAtTop = scrollTop > 0
if (scrollTop + clientHeight >= scrollHeight) { if (scrollTop + clientHeight >= scrollHeight) {
this.$refs.notifications.fetchOlderNotifications() this.$refs.notifications.fetchOlderNotifications()
} }

View File

@ -48,19 +48,34 @@
> >
<div class="mobile-notifications-header"> <div class="mobile-notifications-header">
<span class="title">{{ $t('notifications.notifications') }}</span> <span class="title">{{ $t('notifications.notifications') }}</span>
<a <span class="spacer"/>
class="mobile-nav-button" <button
@click.stop.prevent="closeMobileNotifications()" v-if="notificationsAtTop"
class="button-unstyled mobile-nav-button"
@click.stop.prevent="scrollMobileNotificationsToTop"
>
<FALayers class="fa-scale-110 fa-old-padding-layer">
<FAIcon icon="arrow-up" />
<FAIcon
icon="minus"
transform="up-7"
/>
</FALayers>
</button>
<button
class="button-unstyled mobile-nav-button"
@click.stop.prevent="closeMobileNotifications(true)"
> >
<FAIcon <FAIcon
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
icon="times" icon="times"
/> />
</a> </button>
</div> </div>
<div <div
id="mobile-notifications" id="mobile-notifications"
class="mobile-notifications" class="mobile-notifications"
ref="mobileNotifications"
@scroll="onScroll" @scroll="onScroll"
/> />
</div> </div>
@ -165,6 +180,10 @@
box-shadow: 0px 0px 4px rgba(0,0,0,.6); box-shadow: 0px 0px 4px rgba(0,0,0,.6);
box-shadow: var(--topBarShadow); box-shadow: var(--topBarShadow);
.spacer {
flex: 1;
}
.title { .title {
font-size: 1.3em; font-size: 1.3em;
margin-left: 0.6em; margin-left: 0.6em;

View File

@ -12,7 +12,7 @@
@click="toggleCollapse" @click="toggleCollapse"
> >
<FAIcon <FAIcon
class="timelines-chevron" class="navigation-chevron"
fixed-width fixed-width
:icon="collapsed ? 'chevron-down' : 'chevron-up'" :icon="collapsed ? 'chevron-down' : 'chevron-up'"
/> />
@ -143,12 +143,17 @@
border: none; border: none;
} }
.timelines-chevron { .navigation-chevron {
margin-left: 0.8em; margin-left: 0.8em;
margin-right: 0.8em; margin-right: 0.8em;
font-size: 1.1em; font-size: 1.1em;
} }
.timelines-chevron {
margin-left: 0.8em;
font-size: 1.1em;
}
.timelines-background { .timelines-background {
padding: 0 0 0 0.6em; padding: 0 0 0 0.6em;
background-color: $fallback--lightBg; background-color: $fallback--lightBg;

View File

@ -39,10 +39,8 @@
height: 0.5em; height: 0.5em;
width: 0.5em; width: 0.5em;
position: absolute; position: absolute;
right: calc(50% - 0.25em); right: calc(50% - 0.75em);
top: calc(50% - 0.25em); top: calc(50% - 0.5em);
margin-left: 6px;
margin-top: -6px;
background-color: $fallback--cRed; background-color: $fallback--cRed;
background-color: var(--badgeNotification, $fallback--cRed); background-color: var(--badgeNotification, $fallback--cRed);
} }

View File

@ -109,22 +109,3 @@ export default {
} }
} }
</script> </script>
<style lang="scss">
.NotificationFilters {
align-self: stretch;
> button {
line-height: 100%;
height: 100%;
width: var(--__panel-heading-height-inner);
text-align: center;
svg {
font-size: 1.2em;
}
}
}
</style>

View File

@ -10,10 +10,12 @@ import {
} from '../../services/notification_utils/notification_utils.js' } from '../../services/notification_utils/notification_utils.js'
import FaviconService from '../../services/favicon_service/favicon_service.js' import FaviconService from '../../services/favicon_service/favicon_service.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import { faCircleNotch, faArrowUp, faMinus } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
faCircleNotch faCircleNotch,
faArrowUp,
faMinus
) )
const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30 const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30
@ -34,6 +36,7 @@ const Notifications = {
}, },
data () { data () {
return { return {
showScrollTop: false,
bottomedOut: false, bottomedOut: false,
// How many seen notifications to display in the list. The more there are, // How many seen notifications to display in the list. The more there are,
// the heavier the page becomes. This count is increased when loading // the heavier the page becomes. This count is increased when loading
@ -90,8 +93,20 @@ const Notifications = {
notificationsToDisplay () { notificationsToDisplay () {
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount) return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
}, },
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
...mapGetters(['unreadChatCount']) ...mapGetters(['unreadChatCount'])
}, },
mounted () {
this.scrollerRef = this.$refs.root.closest('.column.-scrollable')
if (!this.scrollerRef) {
this.scrollerRef = this.$refs.root.closest('.mobile-notifications')
}
this.scrollerRef.addEventListener('scroll', this.updateScrollPosition)
},
unmounted () {
if (!this.scrollerRef) return
this.scrollerRef.removeEventListener('scroll', this.updateScrollPosition)
},
watch: { watch: {
unseenCountTitle (count) { unseenCountTitle (count) {
if (count > 0) { if (count > 0) {
@ -101,9 +116,29 @@ const Notifications = {
FaviconService.clearFaviconBadge() FaviconService.clearFaviconBadge()
this.$store.dispatch('setPageTitle', '') this.$store.dispatch('setPageTitle', '')
} }
},
teleportTarget () {
// handle scroller change
this.$nextTick(() => {
this.scrollerRef.removeEventListener('scroll', this.updateScrollPosition)
this.scrollerRef = this.$refs.root.closest('.column.-scrollable')
if (!this.scrollerRef) {
this.scrollerRef = this.$refs.root.closest('.mobile-notifications')
}
this.scrollerRef.addEventListener('scroll', this.updateScrollPosition)
this.updateScrollPosition()
})
} }
}, },
methods: { methods: {
scrollToTop () {
const scrollable = this.scrollerRef
scrollable.scrollTo({ top: this.$refs.root.offsetTop })
// this.$refs.root.scrollIntoView({ behavior: 'smooth', block: 'start' })
},
updateScrollPosition () {
this.showScrollTop = this.$refs.root.offsetTop < this.scrollerRef.scrollTop
},
markAsSeen () { markAsSeen () {
this.$store.dispatch('markNotificationsAsSeen') this.$store.dispatch('markNotificationsAsSeen')
this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT

View File

@ -4,6 +4,7 @@
:to="teleportTarget" :to="teleportTarget"
> >
<div <div
ref="root"
:class="{ minimal: minimalMode }" :class="{ minimal: minimalMode }"
class="Notifications" class="Notifications"
> >
@ -19,14 +20,34 @@
class="badge badge-notification unseen-count" class="badge badge-notification unseen-count"
>{{ unseenCount }}</span> >{{ unseenCount }}</span>
</div> </div>
<div
class="rightside-button"
v-if="showScrollTop"
>
<button
class="button-unstyled scroll-to-top-button"
type="button"
:title="$t('general.scroll_to_top')"
@click="scrollToTop"
>
<FALayers class="fa-scale-110 fa-old-padding-layer">
<FAIcon icon="arrow-up" />
<FAIcon
icon="minus"
transform="up-7"
/>
</FALayers>
</button>
</div>
<button <button
v-if="unseenCount" v-if="unseenCount"
class="button-default read-button" class="button-default read-button"
type="button"
@click.prevent="markAsSeen" @click.prevent="markAsSeen"
> >
{{ $t('notifications.read') }} {{ $t('notifications.read') }}
</button> </button>
<NotificationFilters /> <NotificationFilters class="rightside-button" />
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div <div

View File

@ -87,21 +87,3 @@
</template> </template>
<script src="./quick_filter_settings.js"></script> <script src="./quick_filter_settings.js"></script>
<style lang="scss">
.QuickFilterSettings {
> button {
line-height: 100%;
height: 100%;
width: var(--__panel-heading-height-inner);
text-align: center;
svg {
font-size: 1.2em;
}
}
}
</style>

View File

@ -74,21 +74,3 @@
</template> </template>
<script src="./quick_view_settings.js"></script> <script src="./quick_view_settings.js"></script>
<style lang="scss">
.QuickViewSettings {
> button {
line-height: 100%;
height: 100%;
width: var(--__panel-heading-height-inner);
text-align: center;
svg {
font-size: 1.2em;
}
}
}
</style>

View File

@ -1,4 +1,5 @@
import Status from '../status/status.vue' import Status from '../status/status.vue'
import { mapState } from 'vuex'
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js' import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
import Conversation from '../conversation/conversation.vue' import Conversation from '../conversation/conversation.vue'
import TimelineMenu from '../timeline_menu/timeline_menu.vue' import TimelineMenu from '../timeline_menu/timeline_menu.vue'
@ -6,11 +7,15 @@ import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.
import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue' import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue'
import { debounce, throttle, keyBy } from 'lodash' import { debounce, throttle, keyBy } from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch, faCog } from '@fortawesome/free-solid-svg-icons' import { faCircleNotch, faCirclePlus, faCog, faMinus, faArrowUp, faCheck } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
faCircleNotch, faCircleNotch,
faCog faCog,
faMinus,
faArrowUp,
faCirclePlus,
faCheck
) )
const Timeline = { const Timeline = {
@ -29,6 +34,7 @@ const Timeline = {
], ],
data () { data () {
return { return {
showScrollTop: false,
paused: false, paused: false,
unfocused: false, unfocused: false,
bottomedOut: false, bottomedOut: false,
@ -63,6 +69,13 @@ const Timeline = {
return `${this.$t('timeline.show_new')} (${this.newStatusCount})` return `${this.$t('timeline.show_new')} (${this.newStatusCount})`
} }
}, },
mobileLoadButtonString () {
if (this.timeline.flushMarker !== 0) {
return '+'
} else {
return this.newStatusCount > 99 ? '∞' : this.newStatusCount
}
},
classes () { classes () {
let rootClasses = !this.embedded ? ['panel', 'panel-default'] : ['-nonpanel'] let rootClasses = !this.embedded ? ['panel', 'panel-default'] : ['-nonpanel']
if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention']) if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention'])
@ -87,7 +100,10 @@ const Timeline = {
}, },
virtualScrollingEnabled () { virtualScrollingEnabled () {
return this.$store.getters.mergedConfig.virtualScrolling return this.$store.getters.mergedConfig.virtualScrolling
} },
...mapState({
mobileLayout: state => state.interface.layoutType === 'mobile'
})
}, },
created () { created () {
const store = this.$store const store = this.$store
@ -123,6 +139,9 @@ const Timeline = {
this.$store.commit('setLoading', { timeline: this.timelineName, value: false }) this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
}, },
methods: { methods: {
scrollToTop () {
window.scrollTo({ top: this.$el.offsetTop })
},
stopBlockingClicks: debounce(function () { stopBlockingClicks: debounce(function () {
this.blockingClicks = false this.blockingClicks = false
}, 1000), }, 1000),
@ -222,6 +241,7 @@ const Timeline = {
} }
}, },
handleScroll: throttle(function (e) { handleScroll: throttle(function (e) {
this.showScrollTop = this.$el.offsetTop < window.scrollY
this.determineVisibleStatuses() this.determineVisibleStatuses()
this.scrollLoad(e) this.scrollLoad(e)
}, 200), }, 200),

View File

@ -1,8 +1,35 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.Timeline { .Timeline {
.loadmore-text { .alert-dot {
opacity: 1; border-radius: 100%;
height: 8px;
width: 8px;
position: absolute;
left: calc(50% - 4px);
top: calc(50% - 4px);
margin-left: 6px;
margin-top: -6px;
background-color: var(--badgeNeutral);
}
.alert-badge {
font-size: 0.75em;
line-height: 1;
text-align: right;
border-radius: var(--tooltipRadius);
position: absolute;
left: calc(50% - 0.5em);
top: calc(50% - 0.4em);
padding: 0.2em;
margin-left: 0.7em;
margin-top: -1em;
background-color: var(--badgeNeutral);
color: var(--badgeNeutralText);
}
.loadmore-button {
position: relative;
} }
&.-blocked { &.-blocked {

View File

@ -5,22 +5,74 @@
v-if="!embedded" v-if="!embedded"
:timeline-name="timelineName" :timeline-name="timelineName"
/> />
<button
v-if="showLoadButton"
class="button-default loadmore-button"
@click.prevent="showNewStatuses"
>
{{ loadButtonString }}
</button>
<div <div
v-else-if="!embedded" class="rightside-button"
class="loadmore-text faint" v-if="showScrollTop && !embedded"
@click.prevent
> >
{{ $t('timeline.up_to_date') }} <button
class="button-unstyled scroll-to-top-button"
type="button"
:title="$t('general.scroll_to_top')"
@click="scrollToTop"
>
<FALayers class="fa-scale-110 fa-old-padding-layer">
<FAIcon icon="arrow-up" />
<FAIcon
icon="minus"
transform="up-7"
/>
</FALayers>
</button>
</div> </div>
<QuickFilterSettings v-if="!embedded" /> <template v-if="mobileLayout && !embedded">
<QuickViewSettings v-if="!embedded" /> <div
class="rightside-button"
v-if="showLoadButton"
>
<button
class="button-unstyled loadmore-button"
:title="loadButtonString"
@click.prevent="showNewStatuses"
>
<FAIcon
fixed-width
icon="circle-plus"
/>
<div class="alert-badge">
{{ mobileLoadButtonString }}
</div>
</button>
</div>
<div
v-else-if="!embedded"
class="loadmore-text faint veryfaint rightside-icon"
:title="$t('timeline.up_to_date')"
@click.prevent
>
<FAIcon
fixed-width
icon="check"
/>
</div>
</template>
<template v-else>
<button
v-if="showLoadButton"
class="button-default loadmore-button"
@click.prevent="showNewStatuses"
>
{{ loadButtonString }}
</button>
<div
v-else-if="!embedded"
class="loadmore-text faint"
@click.prevent
>
{{ $t('timeline.up_to_date') }}
</div>
</template>
<QuickFilterSettings v-if="!embedded" class="rightside-button"/>
<QuickViewSettings v-if="!embedded" class="rightside-button"/>
</div> </div>
<div :class="classes.body"> <div :class="classes.body">
<div <div

View File

@ -84,6 +84,7 @@
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
"peek": "Peek", "peek": "Peek",
"scroll_to_top": "Scroll to top",
"role": { "role": {
"admin": "Admin", "admin": "Admin",
"moderator": "Moderator" "moderator": "Moderator"

View File

@ -45,6 +45,7 @@
.panel-heading, .panel-heading,
.panel-footer { .panel-footer {
--panel-heading-height-padding: 0.6em; --panel-heading-height-padding: 0.6em;
--__panel-heading-gap: 0.5em;
--__panel-heading-height: 3.2em; --__panel-heading-height: 3.2em;
--__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding, 0)); --__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding, 0));
@ -54,7 +55,7 @@
grid-auto-flow: column; grid-auto-flow: column;
grid-template-columns: minmax(50%, 1fr); grid-template-columns: minmax(50%, 1fr);
grid-auto-columns: auto; grid-auto-columns: auto;
grid-column-gap: 0.5em; grid-column-gap: var(--__panel-heading-gap);
flex: none; flex: none;
background-size: cover; background-size: cover;
padding: var(--panel-heading-height-padding); padding: var(--panel-heading-height-padding);
@ -195,6 +196,38 @@
} }
} }
} }
.rightside-button {
align-self: stretch;
text-align: center;
width: var(--__panel-heading-height);
height: var(--__panel-heading-height);
margin: calc(-1 * var(--panel-heading-height-padding)) 0;
margin-right: calc(-1 * var(--__panel-heading-gap));
> button {
box-sizing: border-box;
padding: calc(1 * var(--panel-heading-height-padding)) 0;
height: 100%;
width: 100%;
text-align: center;
svg {
font-size: 1.2em;
}
}
}
.rightside-icon {
align-self: stretch;
text-align: center;
width: var(--__panel-heading-height);
margin-right: calc(-1 * var(--__panel-heading-gap));
svg {
font-size: 1.2em;
}
}
} }
.panel-footer { .panel-footer {

View File

@ -709,6 +709,14 @@ export const SLOT_INHERITANCE = {
textColor: 'bw' textColor: 'bw'
}, },
badgeNeutral: '--cGreen',
badgeNeutralText: {
depends: ['text', 'badgeNeutral'],
layer: 'badge',
variant: 'badgeNeutral',
textColor: 'bw'
},
chatBg: { chatBg: {
depends: ['bg'] depends: ['bg']
}, },