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
v-if="!collapsable"
:conversation="true"
class="rightside-button"
/>
<QuickViewSettings
v-if="!collapsable"
:conversation="true"
class="rightside-button"
/>
</div>
<div class="conversation-body panel-body">

View File

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

View File

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

View File

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

View File

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

View File

@ -109,22 +109,3 @@ export default {
}
}
</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'
import FaviconService from '../../services/favicon_service/favicon_service.js'
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(
faCircleNotch
faCircleNotch,
faArrowUp,
faMinus
)
const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30
@ -34,6 +36,7 @@ const Notifications = {
},
data () {
return {
showScrollTop: false,
bottomedOut: false,
// How many seen notifications to display in the list. The more there are,
// the heavier the page becomes. This count is increased when loading
@ -90,8 +93,20 @@ const Notifications = {
notificationsToDisplay () {
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
},
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
...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: {
unseenCountTitle (count) {
if (count > 0) {
@ -101,9 +116,29 @@ const Notifications = {
FaviconService.clearFaviconBadge()
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: {
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 () {
this.$store.dispatch('markNotificationsAsSeen')
this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT

View File

@ -4,6 +4,7 @@
:to="teleportTarget"
>
<div
ref="root"
:class="{ minimal: minimalMode }"
class="Notifications"
>
@ -19,14 +20,34 @@
class="badge badge-notification unseen-count"
>{{ unseenCount }}</span>
</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
v-if="unseenCount"
class="button-default read-button"
type="button"
@click.prevent="markAsSeen"
>
{{ $t('notifications.read') }}
</button>
<NotificationFilters />
<NotificationFilters class="rightside-button" />
</div>
<div class="panel-body">
<div

View File

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

View File

@ -1,8 +1,35 @@
@import '../../_variables.scss';
.Timeline {
.loadmore-text {
opacity: 1;
.alert-dot {
border-radius: 100%;
height: 8px;
width: 8px;
position: absolute;
left: calc(50% - 4px);
top: calc(50% - 4px);
margin-left: 6px;
margin-top: -6px;
background-color: 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 {

View File

@ -5,6 +5,57 @@
v-if="!embedded"
:timeline-name="timelineName"
/>
<div
class="rightside-button"
v-if="showScrollTop && !embedded"
>
<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>
<template v-if="mobileLayout && !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"
@ -19,8 +70,9 @@
>
{{ $t('timeline.up_to_date') }}
</div>
<QuickFilterSettings v-if="!embedded" />
<QuickViewSettings v-if="!embedded" />
</template>
<QuickFilterSettings v-if="!embedded" class="rightside-button"/>
<QuickViewSettings v-if="!embedded" class="rightside-button"/>
</div>
<div :class="classes.body">
<div

View File

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

View File

@ -45,6 +45,7 @@
.panel-heading,
.panel-footer {
--panel-heading-height-padding: 0.6em;
--__panel-heading-gap: 0.5em;
--__panel-heading-height: 3.2em;
--__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-template-columns: minmax(50%, 1fr);
grid-auto-columns: auto;
grid-column-gap: 0.5em;
grid-column-gap: var(--__panel-heading-gap);
flex: none;
background-size: cover;
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 {

View File

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