Update Direct message component, fix pagination

pull/5865/head
Daniel Supernault 4 months ago
parent c4af4d9921
commit e6ef648574
No known key found for this signature in database
GPG Key ID: 23740873EE6F76A1

@ -1,301 +1,311 @@
<template> <template>
<div class="dms-page-component"> <div class="dms-page-component">
<div v-if="isLoaded" class="container-fluid mt-3"> <div v-if="isLoaded" class="container-fluid mt-3">
<div class="row"> <div class="row">
<div class="col-md-3 d-md-block"> <div class="col-md-3 d-md-block">
<sidebar :user="profile" /> <sidebar :user="profile" />
</div> </div>
<div class="col-md-5 offset-md-1 mb-5 order-2 order-md-1"> <div class="col-md-5 offset-md-1 mb-5 order-2 order-md-1">
<h1 class="font-weight-bold mb-4">Direct Messages</h1> <h1 class="font-weight-bold mb-4">Direct Messages</h1>
<div v-if="threadsLoaded"> <div v-if="threadsLoaded">
<div v-for="(thread, idx) in threads" class="card shadow-sm mb-1" style="border-radius:15px;"> <div v-for="(thread, idx) in threads" class="card shadow-sm mb-1" style="border-radius:15px;">
<div class="card-body p-3"> <div class="card-body p-3">
<div class="media"> <div class="media">
<img :src="thread.accounts[0].avatar" width="45" height="45" class="shadow-sm mr-3" style="border-radius: 15px;" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';"> <img :src="thread.accounts[0].avatar" width="45" height="45" class="shadow-sm mr-3" style="border-radius: 15px;" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
<div class="media-body"> <div class="media-body">
<!-- <p class="lead mb-n2">{{ thread.accounts[0].display_name }}</p> --> <!-- <p class="lead mb-n2">{{ thread.accounts[0].display_name }}</p> -->
<div class="d-flex justify-content-between align-items-start mb-1"> <div class="d-flex justify-content-between align-items-start mb-1">
<p class="dm-display-name font-weight-bold mb-0">&commat;{{ thread.accounts[0].acct }}</p> <p class="dm-display-name font-weight-bold mb-0">&commat;{{ thread.accounts[0].acct }}</p>
<p class="font-weight-bold small text-muted mb-0">{{ timeago(thread.last_status.created_at) }} ago</p> <p class="font-weight-bold small text-muted mb-0">{{ timeago(thread.last_status.created_at) }} ago</p>
</div> </div>
<p class="dm-thread-summary text-muted mr-4" v-html="threadSummary(thread.last_status)"></p> <p class="dm-thread-summary text-muted mr-4" v-html="threadSummary(thread.last_status)"></p>
</div> </div>
<router-link class="btn btn-link stretched-link align-self-center mr-n3" :to="`/i/web/direct/thread/${thread.accounts[0].id}`"> <router-link class="btn btn-link stretched-link align-self-center mr-n3" :to="`/i/web/direct/thread/${thread.accounts[0].id}`">
<i class="fal fa-chevron-right fa-lg text-lighter"></i> <i class="fal fa-chevron-right fa-lg text-lighter"></i>
</router-link> </router-link>
</div> </div>
</div> </div>
</div> </div>
<div v-if="!threads || !threads.length" class="row justify-content-center"> <div v-if="!threads || !threads.length" class="row justify-content-center">
<div class="col-12 text-center"> <div class="col-12 text-center">
<img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="opacity: 0.6;"> <img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="opacity: 0.6;">
<p class="lead text-muted font-weight-bold">Your inbox is empty</p> <p class="lead text-muted font-weight-bold">Your inbox is empty</p>
</div> </div>
</div> </div>
<div v-if="canLoadMore"> <div v-if="canLoadMore">
<intersect @enter="enterIntersect"> <intersect @enter="enterIntersect">
<dm-placeholder /> <dm-placeholder />
</intersect> </intersect>
</div> </div>
</div> </div>
<div v-else> <div v-else>
<dm-placeholder /> <dm-placeholder />
</div> </div>
</div> </div>
<div class="col-md-3 d-md-block order-1 order-md-2 mb-4"> <div class="col-md-3 d-md-block order-1 order-md-2 mb-4">
<button class="btn btn-dark shadow-sm font-weight-bold btn-block" @click="openCompose"><i class="far fa-envelope mr-1"></i> Compose</button> <button class="btn btn-dark shadow-sm font-weight-bold btn-block" @click="openCompose"><i class="far fa-envelope mr-1"></i> Compose</button>
<hr> <hr>
<div class="d-flex d-md-block"> <div class="d-flex d-md-block">
<button <button
v-for="(tab, index) in tabs" v-for="(tab, index) in tabs"
class="btn shadow-sm font-weight-bold btn-block text-capitalize mt-0 mt-md-2 mx-1 mx-md-0" class="btn shadow-sm font-weight-bold btn-block text-capitalize mt-0 mt-md-2 mx-1 mx-md-0"
:class="[ index === tabIndex ? 'btn-primary' : 'btn-light' ]" :class="[ index === tabIndex ? 'btn-primary' : 'btn-light' ]"
@click="toggleTab(index)" @click="toggleTab(index)"
> >
{{ $t('directMessages.' + tab) }} {{ $t('directMessages.' + tab) }}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<drawer /> <drawer />
</div> </div>
<div v-else class="d-flex justify-content-center align-items-center" style="height:calc(100vh - 58px);"> <div v-else class="d-flex justify-content-center align-items-center" style="height:calc(100vh - 58px);">
<b-spinner /> <b-spinner />
</div> </div>
<b-modal <b-modal
ref="compose" ref="compose"
hide-header hide-header
hide-footer hide-footer
centered centered
rounded rounded
size="md" size="md"
> >
<div class="card shadow-none mt-4"> <div class="card shadow-none mt-4">
<div class="card-body d-flex align-items-center justify-content-between flex-column" style="min-height: 50vh;"> <div class="card-body d-flex align-items-center justify-content-between flex-column" style="min-height: 50vh;">
<h3 class="font-weight-bold">New Direct Message</h3> <h3 class="font-weight-bold">New Direct Message</h3>
<div> <div>
<p class="mb-0 font-weight-bold">Select Recipient</p> <p class="mb-0 font-weight-bold">Select Recipient</p>
<autocomplete <autocomplete
:search="composeSearch" :search="composeSearch"
:disabled="composeLoading" :disabled="composeLoading"
placeholder="@dansup" placeholder="@dansup"
aria-label="Search usernames" aria-label="Search usernames"
:get-result-value="getTagResultValue" :get-result-value="getTagResultValue"
@submit="onTagSubmitLocation" @submit="onTagSubmitLocation"
:debounce-time="500" :debounce-time="500"
ref="autocomplete" ref="autocomplete"
> >
</autocomplete> </autocomplete>
<p class="small text-muted">Search by username, or webfinger (@dansup@pixelfed.social)</p> <p class="small text-muted">Search by username, or webfinger (@dansup@pixelfed.social)</p>
<div style="width:300px;"></div> <div style="width:300px;"></div>
</div> </div>
<div> <div>
<button class="btn btn-outline-dark rounded-pill font-weight-bold px-5 py-1" @click="closeCompose">Cancel</button> <button class="btn btn-outline-dark rounded-pill font-weight-bold px-5 py-1" @click="closeCompose">Cancel</button>
</div> </div>
</div> </div>
</div> </div>
</b-modal> </b-modal>
</div> </div>
</template> </template>
<script type="text/javascript"> <script type="text/javascript">
import Drawer from './partials/drawer.vue'; import Drawer from './partials/drawer.vue';
import Sidebar from './partials/sidebar.vue'; import Sidebar from './partials/sidebar.vue';
import Placeholder from './partials/placeholders/DirectMessagePlaceholder.vue'; import Placeholder from './partials/placeholders/DirectMessagePlaceholder.vue';
import Intersect from 'vue-intersect' import Intersect from 'vue-intersect'
import Autocomplete from '@trevoreyre/autocomplete-vue' import Autocomplete from '@trevoreyre/autocomplete-vue'
import '@trevoreyre/autocomplete-vue/dist/style.css'; import '@trevoreyre/autocomplete-vue/dist/style.css';
import { parseLinkHeader } from '@web3-storage/parse-link-header';
export default {
components: { export default {
"drawer": Drawer, components: {
"sidebar": Sidebar, "drawer": Drawer,
"intersect": Intersect, "sidebar": Sidebar,
"dm-placeholder": Placeholder, "intersect": Intersect,
"autocomplete": Autocomplete "dm-placeholder": Placeholder,
}, "autocomplete": Autocomplete
},
data() {
return { data() {
isLoaded: false, return {
profile: undefined, isLoaded: false,
canLoadMore: true, profile: undefined,
threadsLoaded: false, canLoadMore: true,
composeLoading: false, threadsLoaded: false,
threads: [], composeLoading: false,
tabIndex: 0, threads: [],
tabs: [ tabIndex: 0,
'inbox', tabs: [
'sent', 'inbox',
'requests' 'sent',
], 'requests'
page: 1, ],
ids: [], nextUrl: false,
isIntersecting: false ids: [],
} isIntersecting: false
}, }
},
mounted() {
this.profile = window._sharedData.user; mounted() {
this.isLoaded = true; this.profile = window._sharedData.user;
this.fetchThreads(); this.isLoaded = true;
this.fetchThreads();
}, },
methods: { methods: {
fetchThreads() { fetchThreads() {
axios.get('/api/v1/conversations', { axios.get('/api/v1/conversations', {
params: { params: {
scope: this.tabs[this.tabIndex] scope: this.tabs[this.tabIndex]
} }
}) })
.then(res => { .then(res => {
let data = res.data.filter(m => { let data = res.data.filter(m => {
return m && m.hasOwnProperty('last_status') && m.last_status; return m && m.hasOwnProperty('last_status') && m.last_status;
}) })
let ids = data.map(dm => dm.accounts[0].id); if(res.headers && res.headers.link) {
this.ids = ids; const links = parseLinkHeader(res.headers.link);
this.threads = data; if(links.next && links.next.url) {
this.threadsLoaded = true; this.nextUrl = links.next.url
this.page++; } else {
}); this.nextUrl = false;
}, }
}
timeago(ts) { let ids = data.map(dm => dm.accounts[0].id);
return App.util.format.timeAgo(ts); this.ids = ids;
}, this.threads = data;
this.threadsLoaded = true;
enterIntersect() { });
if(this.isIntersecting) { },
return;
} timeago(ts) {
return App.util.format.timeAgo(ts);
this.isIntersecting = true; },
axios.get('/api/v1/conversations', { enterIntersect() {
params: { if(this.isIntersecting || !this.nextUrl) {
scope: this.tabs[this.tabIndex], return;
page: this.page }
}
}) this.isIntersecting = true;
.then(res => {
let data = res.data.filter(m => { axios.get(this.nextUrl)
return m && m.hasOwnProperty('last_status') && m.last_status; .then(res => {
}) if(res.headers && res.headers.link) {
data.forEach(dm => { const links = parseLinkHeader(res.headers.link);
if(this.ids.indexOf(dm.accounts[0].id) == -1) { if(links.next && links.next.url) {
this.ids.push(dm.accounts[0].id); this.nextUrl = links.next.url
this.threads.push(dm); } else {
} this.nextUrl = false;
}) }
// this.threads.push(...res.data); }
if(!res.data.length || res.data.length < 5) { let data = res.data.filter(m => {
this.canLoadMore = false; return m && m.hasOwnProperty('last_status') && m.last_status;
this.isIntersecting = false; })
return; data.forEach(dm => {
} if(this.ids.indexOf(dm.accounts[0].id) == -1) {
this.page++; this.ids.push(dm.accounts[0].id);
this.isIntersecting = false; this.threads.push(dm);
}); }
}, })
// this.threads.push(...res.data);
toggleTab(index) { if(!res.data.length || res.data.length < 5) {
event.currentTarget.blur(); this.canLoadMore = false;
this.threadsLoaded = false; this.isIntersecting = false;
this.page = 1; return;
this.tabIndex = index; }
this.fetchThreads(); this.isIntersecting = false;
}, });
},
threadSummary(status, len = 50) {
if(status.pf_type == 'photo') { toggleTab(index) {
let sender = this.profile.id == status.account.id; event.currentTarget.blur();
let icon = '<div class="' + (sender ? 'text-muted' : 'text-primary') + ' border px-2 py-1 mt-1 rounded" style="font-size:11px;width: fit-content"><i class="far fa-image mr-1"></i> <span>'; this.threadsLoaded = false;
icon += sender ? 'Sent a photo' : 'Received a photo'; this.nextUrl = false;
return icon + '</span></div>'; this.tabIndex = index;
} this.fetchThreads();
},
if(status.pf_type == 'video') {
let sender = this.profile.id == status.account.id; threadSummary(status, len = 50) {
let icon = '<div class="' + (sender ? 'text-muted' : 'text-primary') + ' border px-2 py-1 mt-1 rounded" style="font-size:11px;width: fit-content"><i class="far fa-video mr-1"></i> <span>'; if(status.pf_type == 'photo') {
icon += sender ? 'Sent a video' : 'Received a video'; let sender = this.profile.id == status.account.id;
return icon + '</span></div>'; let icon = '<div class="' + (sender ? 'text-muted' : 'text-primary') + ' border px-2 py-1 mt-1 rounded" style="font-size:11px;width: fit-content"><i class="far fa-image mr-1"></i> <span>';
} icon += sender ? 'Sent a photo' : 'Received a photo';
return icon + '</span></div>';
let res = ''; }
if(this.profile.id == status.account.id) { if(status.pf_type == 'video') {
res += '<i class="far fa-reply-all fa-flip-both"></i> '; let sender = this.profile.id == status.account.id;
} let icon = '<div class="' + (sender ? 'text-muted' : 'text-primary') + ' border px-2 py-1 mt-1 rounded" style="font-size:11px;width: fit-content"><i class="far fa-video mr-1"></i> <span>';
icon += sender ? 'Sent a video' : 'Received a video';
let content = status.content; return icon + '</span></div>';
let text = content.replace(/(<([^>]+)>)/gi, ""); }
if(text.length > len) { let res = '';
return res + text.slice(0, len) + '...';
} if(this.profile.id == status.account.id) {
res += '<i class="far fa-reply-all fa-flip-both"></i> ';
return res + text; }
},
let content = status.content;
openCompose() { let text = content.replace(/(<([^>]+)>)/gi, "");
this.$refs.compose.show();
}, if(text.length > len) {
return res + text.slice(0, len) + '...';
composeSearch(input) { }
if (input.length < 1) { return []; };
let self = this; return res + text;
let results = []; },
return axios.post('/api/direct/lookup', {
q: input openCompose() {
}).then(res => { this.$refs.compose.show();
return res.data; },
});
}, composeSearch(input) {
if (input.length < 1) { return []; };
getTagResultValue(result) { let self = this;
// return '@' + result.name; let results = [];
return result.local ? '@' + result.name : result.name; return axios.post('/api/direct/lookup', {
}, q: input
}).then(res => {
onTagSubmitLocation(result) { return res.data;
//this.$refs.autocomplete.value = ''; });
this.composeLoading = true; },
window.location.href = '/i/web/direct/thread/' + result.id;
return; getTagResultValue(result) {
}, // return '@' + result.name;
return result.local ? '@' + result.name : result.name;
closeCompose() { },
this.$refs.compose.hide();
} onTagSubmitLocation(result) {
//this.$refs.autocomplete.value = '';
this.composeLoading = true;
window.location.href = '/i/web/direct/thread/' + result.id;
return;
},
closeCompose() {
this.$refs.compose.hide();
}
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.dms-page-component { .dms-page-component {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
.dm { .dm {
&-thread-summary { &-thread-summary {
margin-bottom: 0; margin-bottom: 0;
font-size: 12px; font-size: 12px;
line-height: 12px; line-height: 12px;
} }
&-display-name { &-display-name {
font-size: 16px; font-size: 16px;
} }
} }
} }
</style> </style>

Loading…
Cancel
Save