Files
livetrail/src/components/projectFeed.vue
T
franzz badae8a3a0
Deploy Spot / deploy (push) Successful in 34s
Sort out panel CSS
2026-05-26 17:25:21 +02:00

217 lines
5.9 KiB
Vue

<script>
import Simplebar from 'simplebar-vue';
import SpotIcon from '@components/spotIcon';
import ProjectPost from '@components/projectPost';
export default {
components: {
SpotIcon,
ProjectPost,
Simplebar
},
props: {
project: Object,
modeHisto: Boolean
},
data() {
return {
loading: false,
updatable: true,
outOfData: false,
refIdFirst: 0,
refIdLast: 0,
firstChunk: true,
isOpen: false,
posts: [],
refreshRate: 60,
swipe: {x: null, y: null}
};
},
emits: ['request-last-update', 'new-markers', 'toggle'],
inject: ['api', 'lang', 'consts', 'isMobile'],
provide() {
return {
feed: {
checkNewFeed: this.checkNewFeed
}
};
},
watch: {
project() {
this.syncUpdateTimer();
},
modeHisto() {
this.syncUpdateTimer();
},
firstChunk() {
this.syncUpdateTimer();
}
},
mounted() {
this.getScrollElement()?.addEventListener('scroll', this.onFeedScroll);
this.syncUpdateTimer();
},
beforeUnmount() {
this.getScrollElement()?.removeEventListener('scroll', this.onFeedScroll);
this.setUpdateTimer(-1);
},
methods: {
async init() {
this.setUpdateTimer(-1);
this.loading = false;
this.updatable = true;
this.outOfData = false;
this.refIdFirst = 0;
this.refIdLast = 0;
this.firstChunk = true;
this.posts = [];
this.swipe = {x: null, y: null};
await this.$nextTick();
this.toggle(!this.isMobile(), 0);
await this.getNextFeed();
this.getScrollElement().scrollTop = 0;
this.syncUpdateTimer();
},
getScrollElement() {
return this.$refs.feedSimpleBar?.scrollElement;
},
async findPost(sPostType, iPostId) {
let vPost = await this.goToPost(sPostType, iPostId);
if(vPost) {
await vPost.executeMainAction(0);
return vPost;
}
else if(!this.outOfData) {
await this.getNextFeed();
await this.$nextTick();
return this.findPost(sPostType, iPostId);
}
else console.log('Missing element ID "'+iPostId+'" of type "'+sPostType+'"');
return null;
},
async goToPost(sPostType, iPostId) {
let avPosts = this.$refs.posts.filter((post) => {return post.postId == sPostType+'-'+iPostId;});
if(avPosts.length == 0) return null;
//Force next update to have enough subsequent elements to position the post on top of the page
await this.getNextFeed();
let vPost = avPosts[0];
this.getScrollElement().scrollTop += Math.round(
vPost.$el.getBoundingClientRect().top
+ window.pageYOffset
- parseFloat(getComputedStyle(this.$refs.feedSimpleBar.$el).paddingTop)
);
return vPost;
},
async getNextFeed() {
if(!this.project || this.outOfData || this.loading) return true;
//Get next chunk
this.loading = true;
let aoData = await this.api.get('next_feed', {id_project: this.project.id, id: this.refIdLast});
let iPostCount = Object.keys(aoData.feed).length;
//Update pointers
this.outOfData = (iPostCount < this.consts.chunk_size);
if(iPostCount > 0) {
this.refIdLast = aoData.ref_id_last;
if(this.firstChunk) this.refIdFirst = aoData.ref_id_first;
}
//Add posts
this.posts.push(...aoData.feed);
this.loading = false;
this.firstChunk = false;
return true;
},
onFeedScroll(oEvent) {
const box = oEvent.currentTarget;
const content = box.querySelector('.simplebar-content');
if((box.scrollTop + box.clientHeight) / (content?.offsetHeight || 1) >= 0.8) this.getNextFeed();
},
onTouchStart(oEvent) {
if(!this.isMobile() || !this.isOpen || oEvent.touches.length != 1) return;
const oTouch = oEvent.touches[0];
this.swipe = {x: oTouch.clientX, y: oTouch.clientY};
},
onTouchEnd(oEvent) {
const oTouch = oEvent.changedTouches[0];
if(!oTouch || this.swipe.x === null) return;
const iDeltaX = oTouch.clientX - this.swipe.x;
const iDeltaY = oTouch.clientY - this.swipe.y;
if(iDeltaX > 80 && Math.abs(iDeltaX) > Math.abs(iDeltaY) * 1.5) this.toggle();
this.swipe = {x: null, y: null};
},
setUpdateTimer(iSeconds) {
if(typeof this.feedTimer != 'undefined') clearTimeout(this.feedTimer);
if(iSeconds >= 0) this.feedTimer = setTimeout(this.onUpdateTimer, iSeconds * 1000);
},
syncUpdateTimer() {
this.setUpdateTimer((!!this.project && !this.modeHisto && !this.firstChunk)?this.refreshRate:-1);
},
async onUpdateTimer() {
await this.checkNewFeed();
this.syncUpdateTimer();
},
async checkNewFeed() {
if(!this.project) return;
let aoData = await this.api.get('new_feed', {id_project: this.project.id, id: this.refIdFirst});
const aoFeed = aoData.feed || [];
const aoMarkers = aoData.markers || [];
if(aoFeed.length > 0) {
//Update pointer
this.refIdFirst = aoData.ref_id_first;
//Add new posts
this.posts.unshift(...aoFeed);
}
if(aoMarkers.length > 0) this.$emit('new-markers', aoMarkers);
this.$emit('request-last-update');
},
toggle(bShow, iAnimDuration=500) {
this.isOpen = (typeof bShow == 'boolean')?bShow:(!this.isOpen);
this.$emit('toggle', this.isOpen, iAnimDuration);
return this.isOpen;
},
getWidth() {
return this.$el.getBoundingClientRect().width;
}
}
}
</script>
<template>
<div id="feed" class="panel panel-right" @touchstart.passive="onTouchStart" @touchend.passive="onTouchEnd">
<Simplebar id="feed-panel" class="panel-content" ref="feedSimpleBar">
<div id="feed-header">
<ProjectPost v-if="modeHisto" :options="{type: 'archived', headerless: true}" />
<ProjectPost v-else :options="{type: 'poster', relative_time: lang.get('post.new_message')}" />
</div>
<div v-if="project" id="feed-posts">
<ProjectPost v-for="post in posts" :options="post" ref="posts" />
</div>
<div id="feed-footer" v-if="loading">
<ProjectPost :options="{type: 'loading', headerless: true}" />
</div>
</Simplebar>
<div v-if="project" :class="'panel-control panel-control-icon panel-control-'+(isMobile()?'bottom':'top')" @click="toggle">
<SpotIcon :icon="isOpen?'next':'post'" />
</div>
</div>
</template>