Browse Source

Add like & share model partial components

Daniel Supernault 2 years ago
parent
commit
b565ead693

+ 239 - 0
resources/assets/components/partials/post/LikeModal.vue

@@ -0,0 +1,239 @@
+<template>
+	<div>
+		<b-modal
+			ref="likesModal"
+			centered
+			size="md"
+			:scrollable="true"
+			hide-footer
+			header-class="py-2"
+			body-class="p-0"
+			title-class="w-100 text-center pl-4 font-weight-bold"
+			title-tag="p"
+			:title="$t('common.likes')">
+			<div v-if="isLoading" class="likes-loader list-group border-top-0" style="max-height: 500px;">
+				<like-placeholder />
+			</div>
+
+			<div v-else>
+				<div v-if="!likes.length" class="d-flex justify-content-center align-items-center" style="height: 140px;">
+					<p class="font-weight-bold mb-0">{{ $t('post.noLikes') }}</p>
+				</div>
+
+				<div v-else class="list-group" style="max-height: 500px;">
+					<div v-for="(account, index) in likes" class="list-group-item border-left-0 border-right-0 px-3" :class="[ index === 0 ? 'border-top-0' : '']">
+						<div class="media align-items-center">
+							<img :src="account.avatar" width="40" height="40" style="border-radius: 8px;" class="mr-3 shadow-sm" onerror="this.src='/storage/avatars/default.jpg?v=0';this.onerror=null;">
+							<div class="media-body">
+								<p class="mb-0 text-truncate"><a :href="account.url" class="text-dark font-weight-bold text-decoration-none" @click.prevent="goToProfile(account)">{{ getUsername(account) }}</a></p>
+								<p class="mb-0 mt-n1 text-dark font-weight-bold small text-break">&commat;{{ account.acct }}</p>
+							</div>
+
+							<div>
+								<button
+									v-if="account.follows == null || account.id == user.id"
+									class="btn btn-outline-muted rounded-pill btn-sm font-weight-bold"
+									@click="goToProfile(profile)"
+									style="width:110px;">
+									View Profile
+								</button>
+								<button
+									v-else-if="account.follows"
+									class="btn btn-outline-muted rounded-pill btn-sm font-weight-bold"
+									:disabled="isUpdatingFollowState"
+									@click="handleUnfollow(index)"
+									style="width:110px;">
+									<span v-if="isUpdatingFollowState && followStateIndex === index">
+										<b-spinner small />
+									</span>
+									<span v-else>Following</span>
+								</button>
+								<button
+									v-else-if="!account.follows"
+									class="btn btn-primary rounded-pill btn-sm font-weight-bold"
+									:disabled="isUpdatingFollowState"
+									@click="handleFollow(index)"
+									style="width:110px;">
+									<span v-if="isUpdatingFollowState && followStateIndex === index">
+										<b-spinner small />
+									</span>
+									<span v-else>Follow</span>
+								</button>
+							</div>
+						</div>
+					</div>
+
+					<div v-if="canLoadMore">
+						<intersect @enter="enterIntersect">
+							<like-placeholder class="border-top-0" />
+						</intersect>
+						<like-placeholder />
+					</div>
+				</div>
+			</div>
+		</b-modal>
+	</div>
+</template>
+
+<script type="text/javascript">
+	import Intersect from 'vue-intersect'
+	import LikePlaceholder from './LikeListPlaceholder.vue';
+	import { parseLinkHeader } from '@web3-storage/parse-link-header';
+
+	export default {
+		props: {
+			status: {
+				type: Object
+			},
+
+			profile: {
+				type: Object
+			}
+		},
+
+		components: {
+			"intersect": Intersect,
+			"like-placeholder": LikePlaceholder
+		},
+
+		data() {
+			return {
+				isOpen: false,
+				isLoading: true,
+				canLoadMore: false,
+				isFetchingMore: false,
+				likes: [],
+				ids: [],
+				page: undefined,
+				isUpdatingFollowState: false,
+				followStateIndex: undefined,
+				user: window._sharedData.user
+			}
+		},
+
+		methods: {
+			clear() {
+				this.isOpen = false;
+				this.isLoading = true;
+				this.canLoadMore = false;
+				this.isFetchingMore = false;
+				this.likes = [];
+				this.ids = [];
+				this.page = undefined;
+			},
+
+			fetchLikes() {
+				axios.get('/api/v1/statuses/'+this.status.id+'/favourited_by', {
+					params: {
+						limit: 40
+					}
+				})
+				.then(res => {
+					this.ids = res.data.map(a => a.id);
+					this.likes = res.data;
+					if(res.headers && res.headers.link) {
+						const links = parseLinkHeader(res.headers.link);
+						if(links.next) {
+							this.page = links.next.cursor;
+							this.canLoadMore = true;
+						} else {
+							this.canLoadMore = false;
+						}
+					}
+					this.isLoading = false;
+				});
+			},
+
+			open() {
+				if(this.page) {
+					this.clear();
+				}
+				this.isOpen = true;
+				this.fetchLikes();
+				this.$refs.likesModal.show();
+			},
+
+			enterIntersect() {
+				if(this.isFetchingMore) {
+					return;
+				}
+
+				this.isFetchingMore = true;
+
+				axios.get('/api/v1/statuses/'+this.status.id+'/favourited_by', {
+					params: {
+						limit: 10,
+						cursor: this.page
+					}
+				}).then(res => {
+					if(!res.data || !res.data.length) {
+						this.canLoadMore = false;
+						this.isFetchingMore = false;
+						return;
+					}
+					res.data.forEach(user => {
+						if(this.ids.indexOf(user.id) == -1) {
+							this.ids.push(user.id);
+							this.likes.push(user);
+						}
+					})
+					if(res.headers && res.headers.link) {
+						const links = parseLinkHeader(res.headers.link);
+						if(links.next) {
+							this.page = links.next.cursor;
+						} else {
+							this.canLoadMore = false;
+						}
+					}
+					this.isFetchingMore = false;
+				})
+			},
+
+			getUsername(account) {
+				return account.display_name ? account.display_name : account.username;
+			},
+
+			goToProfile(account) {
+				this.$router.push({
+					name: 'profile',
+					path: `/i/web/profile/${account.id}`,
+					params: {
+						id: account.id,
+						cachedProfile: account,
+						cachedUser: this.profile
+					}
+				})
+			},
+
+			handleFollow(index) {
+				event.currentTarget.blur();
+
+				this.followStateIndex = index;
+				this.isUpdatingFollowState = true;
+
+				let account = this.likes[index];
+				axios.post('/api/v1/accounts/' + account.id + '/follow')
+				.then(res => {
+					this.likes[index].follows = true;
+					this.followStateIndex = undefined;
+					this.isUpdatingFollowState = false;
+				});
+			},
+
+			handleUnfollow(index) {
+				event.currentTarget.blur();
+
+				this.followStateIndex = index;
+				this.isUpdatingFollowState = true;
+
+				let account = this.likes[index];
+				axios.post('/api/v1/accounts/' + account.id + '/unfollow')
+				.then(res => {
+					this.likes[index].follows = false;
+					this.followStateIndex = undefined;
+					this.isUpdatingFollowState = false;
+				});
+			}
+		}
+	}
+</script>

+ 239 - 0
resources/assets/components/partials/post/ShareModal.vue

@@ -0,0 +1,239 @@
+<template>
+	<div>
+		<b-modal
+			ref="sharesModal"
+			centered
+			size="md"
+			:scrollable="true"
+			hide-footer
+			header-class="py-2"
+			body-class="p-0"
+			title-class="w-100 text-center pl-4 font-weight-bold"
+			title-tag="p"
+			title="Shared By">
+			<div v-if="isLoading" class="likes-loader list-group border-top-0" style="max-height: 500px;">
+				<like-placeholder />
+			</div>
+
+			<div v-else>
+				<div v-if="!likes.length" class="d-flex justify-content-center align-items-center" style="height: 140px;">
+					<p class="font-weight-bold mb-0">Nobody has shared this yet!</p>
+				</div>
+
+				<div v-else class="list-group" style="max-height: 500px;">
+					<div v-for="(account, index) in likes" class="list-group-item border-left-0 border-right-0 px-3" :class="[ index === 0 ? 'border-top-0' : '']">
+						<div class="media align-items-center">
+							<img :src="account.avatar" width="40" height="40" style="border-radius: 8px;" class="mr-3 shadow-sm" onerror="this.src='/storage/avatars/default.jpg?v=0';this.onerror=null;">
+							<div class="media-body">
+								<p class="mb-0 text-truncate"><a :href="account.url" class="text-dark font-weight-bold text-decoration-none" @click.prevent="goToProfile(account)">{{ getUsername(account) }}</a></p>
+								<p class="mb-0 mt-n1 text-dark font-weight-bold small text-break">&commat;{{ account.acct }}</p>
+							</div>
+
+							<div>
+								<button
+									v-if="account.id == user.id"
+									class="btn btn-outline-muted rounded-pill btn-sm font-weight-bold"
+									@click="goToProfile(profile)"
+									style="width:110px;">
+									View Profile
+								</button>
+								<button
+									v-else-if="account.follows"
+									class="btn btn-outline-muted rounded-pill btn-sm font-weight-bold"
+									:disabled="isUpdatingFollowState"
+									@click="handleUnfollow(index)"
+									style="width:110px;">
+									<span v-if="isUpdatingFollowState && followStateIndex === index">
+										<b-spinner small />
+									</span>
+									<span v-else>Following</span>
+								</button>
+								<button
+									v-else-if="!account.follows"
+									class="btn btn-primary rounded-pill btn-sm font-weight-bold"
+									:disabled="isUpdatingFollowState"
+									@click="handleFollow(index)"
+									style="width:110px;">
+									<span v-if="isUpdatingFollowState && followStateIndex === index">
+										<b-spinner small />
+									</span>
+									<span v-else>Follow</span>
+								</button>
+							</div>
+						</div>
+					</div>
+
+					<div v-if="canLoadMore">
+						<intersect @enter="enterIntersect">
+							<like-placeholder class="border-top-0" />
+						</intersect>
+						<like-placeholder />
+					</div>
+				</div>
+			</div>
+		</b-modal>
+	</div>
+</template>
+
+<script type="text/javascript">
+	import Intersect from 'vue-intersect'
+	import LikePlaceholder from './LikeListPlaceholder.vue';
+	import { parseLinkHeader } from '@web3-storage/parse-link-header';
+
+	export default {
+		props: {
+			status: {
+				type: Object
+			},
+
+			profile: {
+				type: Object
+			}
+		},
+
+		components: {
+			"intersect": Intersect,
+			"like-placeholder": LikePlaceholder
+		},
+
+		data() {
+			return {
+				isOpen: false,
+				isLoading: true,
+				canLoadMore: false,
+				isFetchingMore: false,
+				likes: [],
+				ids: [],
+				page: undefined,
+				isUpdatingFollowState: false,
+				followStateIndex: undefined,
+				user: window._sharedData.user
+			}
+		},
+
+		methods: {
+			clear() {
+				this.isOpen = false;
+				this.isLoading = true;
+				this.canLoadMore = false;
+				this.isFetchingMore = false;
+				this.likes = [];
+				this.ids = [];
+				this.page = undefined;
+			},
+
+			fetchShares() {
+				axios.get('/api/v1/statuses/'+this.status.id+'/reblogged_by', {
+					params: {
+						limit: 40
+					}
+				})
+				.then(res => {
+					this.ids = res.data.map(a => a.id);
+					this.likes = res.data;
+					if(res.headers && res.headers.link) {
+						const links = parseLinkHeader(res.headers.link);
+						if(links.next) {
+							this.page = links.next.cursor;
+							this.canLoadMore = true;
+						} else {
+							this.canLoadMore = false;
+						}
+					}
+					this.isLoading = false;
+				});
+			},
+
+			open() {
+				if(this.page) {
+					this.clear();
+				}
+				this.isOpen = true;
+				this.fetchShares();
+				this.$refs.sharesModal.show();
+			},
+
+			enterIntersect() {
+				if(this.isFetchingMore) {
+					return;
+				}
+
+				this.isFetchingMore = true;
+
+				axios.get('/api/v1/statuses/'+this.status.id+'/reblogged_by', {
+					params: {
+						limit: 10,
+						cursor: this.page
+					}
+				}).then(res => {
+					if(!res.data || !res.data.length) {
+						this.canLoadMore = false;
+						this.isFetchingMore = false;
+						return;
+					}
+					res.data.forEach(user => {
+						if(this.ids.indexOf(user.id) == -1) {
+							this.ids.push(user.id);
+							this.likes.push(user);
+						}
+					})
+					if(res.headers && res.headers.link) {
+						const links = parseLinkHeader(res.headers.link);
+						if(links.next) {
+							this.page = links.next.cursor;
+						} else {
+							this.canLoadMore = false;
+						}
+					}
+					this.isFetchingMore = false;
+				})
+			},
+
+			getUsername(account) {
+				return account.display_name ? account.display_name : account.username;
+			},
+
+			goToProfile(account) {
+				this.$router.push({
+					name: 'profile',
+					path: `/i/web/profile/${account.id}`,
+					params: {
+						id: account.id,
+						cachedProfile: account,
+						cachedUser: this.profile
+					}
+				})
+			},
+
+			handleFollow(index) {
+				event.currentTarget.blur();
+
+				this.followStateIndex = index;
+				this.isUpdatingFollowState = true;
+
+				let account = this.likes[index];
+				axios.post('/api/v1/accounts/' + account.id + '/follow')
+				.then(res => {
+					this.likes[index].follows = true;
+					this.followStateIndex = undefined;
+					this.isUpdatingFollowState = false;
+				});
+			},
+
+			handleUnfollow(index) {
+				event.currentTarget.blur();
+
+				this.followStateIndex = index;
+				this.isUpdatingFollowState = true;
+
+				let account = this.likes[index];
+				axios.post('/api/v1/accounts/' + account.id + '/unfollow')
+				.then(res => {
+					this.likes[index].follows = false;
+					this.followStateIndex = undefined;
+					this.isUpdatingFollowState = false;
+				});
+			}
+		}
+	}
+</script>