Explorar o código

Merge pull request #5865 from pixelfed/staging

Staging
daniel hai 3 meses
pai
achega
4fab7d6968

+ 1 - 0
CHANGELOG.md

@@ -56,6 +56,7 @@
 - Update ApiV1Controller, return empty statuses feed for private accounts instead of 403 response ([cce657d9c](https://github.com/pixelfed/pixelfed/commit/cce657d9c))
 - Update DM config, allow new users to send DMs by default, with a new env variable to enforce a 72h limit ([717f17cde](https://github.com/pixelfed/pixelfed/commit/717f17cde))
 - Update ApiV1Controller, add pagination to conversations endpoint with min/max/since id pagination and link header support ([244e86bad](https://github.com/pixelfed/pixelfed/commit/244e86bad))
+- Update Direct message component, fix pagination ([e6ef64857](https://github.com/pixelfed/pixelfed/commit/e6ef64857))
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 
 ## [v0.12.4 (2024-11-08)](https://github.com/pixelfed/pixelfed/compare/v0.12.4...dev)

BIN=BIN
public/js/dms.chunk.13449036a5b769e6.js


BIN=BIN
public/js/dms.chunk.602ea23f0f63b894.js


BIN=BIN
public/js/manifest.js


BIN=BIN
public/mix-manifest.json


+ 298 - 288
resources/assets/components/Direct.vue

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