diff options
Diffstat (limited to 'src/components/timeline')
| -rw-r--r-- | src/components/timeline/timeline.js | 74 | ||||
| -rw-r--r-- | src/components/timeline/timeline.scss | 59 | ||||
| -rw-r--r-- | src/components/timeline/timeline.vue | 201 | ||||
| -rw-r--r-- | src/components/timeline/timeline_quick_settings.js | 61 | ||||
| -rw-r--r-- | src/components/timeline/timeline_quick_settings.vue | 102 |
5 files changed, 222 insertions, 275 deletions
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 44f749c3..b7414610 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -1,44 +1,40 @@ 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' -import TimelineQuickSettings from './timeline_quick_settings.vue' +import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue' +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 ) -export const getExcludedStatusIdsByPinning = (statuses, pinnedStatusIds) => { - const ids = [] - if (pinnedStatusIds && pinnedStatusIds.length > 0) { - for (let status of statuses) { - if (!pinnedStatusIds.includes(status.id)) { - break - } - ids.push(status.id) - } - } - return ids -} - const Timeline = { props: [ 'timeline', 'timelineName', 'title', 'userId', + 'listId', 'tag', 'embedded', 'count', 'pinnedStatusIds', - 'inProfile' + 'inProfile', + 'footerSlipgate' // reference to an element where we should put our footer ], data () { return { + showScrollTop: false, paused: false, unfocused: false, bottomedOut: false, @@ -50,9 +46,16 @@ const Timeline = { Status, Conversation, TimelineMenu, - TimelineQuickSettings + QuickFilterSettings, + QuickViewSettings }, computed: { + filteredVisibleStatuses () { + return this.timeline.visibleStatuses.filter(status => this.timelineName !== 'user' || (status.id >= this.timeline.minId && status.id <= this.timeline.maxId)) + }, + filteredPinnedStatusIds () { + return (this.pinnedStatusIds || []).filter(statusId => this.timeline.statusesObject[statusId]) + }, newStatusCount () { return this.timeline.newStatusCount }, @@ -66,35 +69,41 @@ 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'] : [] + 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'] : []) } }, // id map of statuses which need to be hidden in the main list due to pinning logic - excludedStatusIdsObject () { - const ids = getExcludedStatusIdsByPinning(this.timeline.visibleStatuses, this.pinnedStatusIds) - // Convert id array to object - return keyBy(ids) - }, pinnedStatusIdsObject () { return keyBy(this.pinnedStatusIds) }, 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 () { return this.$store.getters.mergedConfig.virtualScrolling - } + }, + ...mapState({ + mobileLayout: state => state.interface.layoutType === 'mobile' + }) }, created () { const store = this.$store @@ -111,6 +120,7 @@ const Timeline = { timeline: this.timelineName, showImmediately, userId: this.userId, + listId: this.listId, tag: this.tag }) }, @@ -122,13 +132,16 @@ const Timeline = { window.addEventListener('keydown', this.handleShortKey) setTimeout(this.determineVisibleStatuses, 250) }, - destroyed () { + unmounted () { window.removeEventListener('scroll', this.handleScroll) window.removeEventListener('keydown', this.handleShortKey) if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false) this.$store.commit('setLoading', { timeline: this.timelineName, value: false }) }, methods: { + scrollToTop () { + window.scrollTo({ top: this.$el.offsetTop }) + }, stopBlockingClicks: debounce(function () { this.blockingClicks = false }, 1000), @@ -153,6 +166,7 @@ const Timeline = { this.$store.commit('showNewStatuses', { timeline: this.timelineName }) this.paused = false } + window.scrollTo({ top: 0 }) }, fetchOlderStatuses: throttle(function () { const store = this.$store @@ -165,6 +179,7 @@ const Timeline = { older: true, showImmediately: true, userId: this.userId, + listId: this.listId, tag: this.tag }).then(({ statuses }) => { if (statuses && statuses.length === 0) { @@ -226,6 +241,7 @@ const Timeline = { } }, handleScroll: throttle(function (e) { + this.showScrollTop = this.$el.offsetTop < window.scrollY this.determineVisibleStatuses() this.scrollLoad(e) }, 200), diff --git a/src/components/timeline/timeline.scss b/src/components/timeline/timeline.scss index 2c5a67e2..c6fb1ca7 100644 --- a/src/components/timeline/timeline.scss +++ b/src/components/timeline/timeline.scss @@ -1,31 +1,58 @@ @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 { 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 767428f0..2279f21a 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -1,86 +1,153 @@ <template> - <div :class="[classes.root, 'Timeline']"> + <div :class="['Timeline', classes.root]"> <div :class="classes.header"> - <TimelineMenu v-if="!embedded" /> - <button - v-if="showLoadButton" - class="button-default loadmore-button" - @click.prevent="showNewStatuses" - > - {{ loadButtonString }} - </button> + <TimelineMenu + v-if="!embedded" + :timeline-name="timelineName" + /> <div - v-else - class="loadmore-text faint" - @click.prevent + v-if="showScrollTop && !embedded" + class="rightside-button" > - {{ $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> - <TimelineQuickSettings v-if="!embedded" /> + <template v-if="mobileLayout && !embedded"> + <div + v-if="showLoadButton" + class="rightside-button" + > + <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')" + :aria-disabled="true" + @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 :class="classes.body"> <div ref="timeline" class="timeline" + role="feed" > - <template v-for="statusId in pinnedStatusIds"> - <conversation - v-if="timeline.statusesObject[statusId]" - :key="statusId + '-pinned'" - class="status-fadein" - :status-id="statusId" - :collapsable="true" - :pinned-status-ids-object="pinnedStatusIdsObject" - :in-profile="inProfile" - :profile-user-id="userId" - /> - </template> - <template v-for="status in timeline.visibleStatuses"> - <conversation - v-if="!excludedStatusIdsObject[status.id]" - :key="status.id" - class="status-fadein" - :status-id="status.id" - :collapsable="true" - :in-profile="inProfile" - :profile-user-id="userId" - :virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.includes(status.id)" - /> - </template> + <conversation + v-for="statusId in filteredPinnedStatusIds" + :key="statusId + '-pinned'" + role="listitem" + class="status-fadein" + :status-id="statusId" + :collapsable="true" + :pinned-status-ids-object="pinnedStatusIdsObject" + :in-profile="inProfile" + :profile-user-id="userId" + /> + <conversation + v-for="status in filteredVisibleStatuses" + :key="status.id" + role="listitem" + class="status-fadein" + :status-id="status.id" + :collapsable="true" + :in-profile="inProfile" + :profile-user-id="userId" + :virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.includes(status.id)" + /> </div> </div> <div :class="classes.footer"> - <div - v-if="count===0" - class="new-status-notification text-center faint" - > - {{ $t('timeline.no_statuses') }} - </div> - <div - v-else-if="bottomedOut" - class="new-status-notification text-center faint" - > - {{ $t('timeline.no_more_statuses') }} - </div> - <button - v-else-if="!timeline.loading" - class="button-unstyled -link -fullwidth" - @click.prevent="fetchOlderStatuses()" + <teleport + :to="footerSlipgate" + :disabled="!embedded || !footerSlipgate" > - <div class="new-status-notification text-center"> - {{ $t('timeline.load_older') }} + <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.js b/src/components/timeline/timeline_quick_settings.js deleted file mode 100644 index eae65a55..00000000 --- a/src/components/timeline/timeline_quick_settings.js +++ /dev/null @@ -1,61 +0,0 @@ -import Popover from '../popover/popover.vue' -import { mapGetters } from 'vuex' -import { library } from '@fortawesome/fontawesome-svg-core' -import { faFilter, faFont, faWrench } from '@fortawesome/free-solid-svg-icons' - -library.add( - faFilter, - faFont, - faWrench -) - -const TimelineQuickSettings = { - components: { - Popover - }, - methods: { - setReplyVisibility (visibility) { - this.$store.dispatch('setOption', { name: 'replyVisibility', value: visibility }) - this.$store.dispatch('queueFlushAll') - }, - openTab (tab) { - this.$store.dispatch('openSettingsModalTab', tab) - } - }, - computed: { - ...mapGetters(['mergedConfig']), - loggedIn () { - return !!this.$store.state.users.currentUser - }, - replyVisibilitySelf: { - get () { return this.mergedConfig.replyVisibility === 'self' }, - set () { this.setReplyVisibility('self') } - }, - replyVisibilityFollowing: { - get () { return this.mergedConfig.replyVisibility === 'following' }, - set () { this.setReplyVisibility('following') } - }, - replyVisibilityAll: { - get () { return this.mergedConfig.replyVisibility === 'all' }, - set () { this.setReplyVisibility('all') } - }, - hideMedia: { - get () { return this.mergedConfig.hideAttachments || this.mergedConfig.hideAttachmentsInConv }, - set () { - const value = !this.hideMedia - this.$store.dispatch('setOption', { name: 'hideAttachments', value }) - this.$store.dispatch('setOption', { name: 'hideAttachmentsInConv', value }) - } - }, - hideMutedPosts: { - get () { return this.mergedConfig.hideMutedPosts || this.mergedConfig.hideFilteredStatuses }, - set () { - const value = !this.hideMutedPosts - this.$store.dispatch('setOption', { name: 'hideMutedPosts', value }) - this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value }) - } - } - } -} - -export default TimelineQuickSettings diff --git a/src/components/timeline/timeline_quick_settings.vue b/src/components/timeline/timeline_quick_settings.vue deleted file mode 100644 index 98996ebd..00000000 --- a/src/components/timeline/timeline_quick_settings.vue +++ /dev/null @@ -1,102 +0,0 @@ -<template> - <Popover - trigger="click" - class="TimelineQuickSettings" - :bound-to="{ x: 'container' }" - > - <template v-slot:content> - <div class="dropdown-menu"> - <div v-if="loggedIn"> - <button - class="button-default dropdown-item" - @click="replyVisibilityAll = true" - > - <span - class="menu-checkbox" - :class="{ 'menu-checkbox-radio': replyVisibilityAll }" - />{{ $t('settings.reply_visibility_all') }} - </button> - <button - class="button-default dropdown-item" - @click="replyVisibilityFollowing = true" - > - <span - class="menu-checkbox" - :class="{ 'menu-checkbox-radio': replyVisibilityFollowing }" - />{{ $t('settings.reply_visibility_following_short') }} - </button> - <button - class="button-default dropdown-item" - @click="replyVisibilitySelf = true" - > - <span - class="menu-checkbox" - :class="{ 'menu-checkbox-radio': replyVisibilitySelf }" - />{{ $t('settings.reply_visibility_self_short') }} - </button> - <div - role="separator" - class="dropdown-divider" - /> - </div> - <button - class="button-default dropdown-item" - @click="hideMedia = !hideMedia" - > - <span - class="menu-checkbox" - :class="{ 'menu-checkbox-checked': hideMedia }" - />{{ $t('settings.hide_media_previews') }} - </button> - <button - class="button-default dropdown-item" - @click="hideMutedPosts = !hideMutedPosts" - > - <span - class="menu-checkbox" - :class="{ 'menu-checkbox-checked': hideMutedPosts }" - />{{ $t('settings.hide_all_muted_posts') }} - </button> - <button - class="button-default dropdown-item dropdown-item-icon" - @click="openTab('filtering')" - > - <FAIcon icon="font" />{{ $t('settings.word_filter') }} - </button> - <button - class="button-default dropdown-item dropdown-item-icon" - @click="openTab('general')" - > - <FAIcon icon="wrench" />{{ $t('settings.more_settings') }} - </button> - </div> - </template> - <template v-slot:trigger> - <button class="button-unstyled"> - <FAIcon icon="filter" /> - </button> - </template> - </Popover> -</template> - -<script src="./timeline_quick_settings.js"></script> - -<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%; - } - - .dropdown-item { - margin: 0; - } -} - -</style> |
