Skip to content

Commit 8170325

Browse files
rahultrivedi180jackdishmanldmwebchrispanag
authored
feat: show quote reposts (#1080)
* feat: fetch quote reposts of a post * refactor: minor refactor * feat: show quote reposts in stats (#1082) * feat: add ui * refactor: move getQuoteReposts to QuotesPopup * feat: show quote text * style: polish displaying quotes reposts in stats Co-authored-by: Rahul Trivedi <[email protected]> Co-authored-by: Lilian Desvaux de Marigny <[email protected]> * fix: asyncs and trys * fix: don't mutate avatar in-place * fix: make cache return only copies of the objects * fix: make cache return only copies of the objects Co-authored-by: jack dishman <[email protected]> Co-authored-by: Lilian Desvaux de Marigny <[email protected]> Co-authored-by: Christos Panagiotakopoulos <[email protected]>
1 parent 3840f1f commit 8170325

File tree

8 files changed

+212
-38
lines changed

8 files changed

+212
-38
lines changed

src/backend/reposts.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,15 @@ export interface IGetRepostsOptions {
4545
offset?: number
4646
limit?: number
4747
following?: string
48+
type?: `simple` | `quote`
4849
}
4950

5051
export async function getReposts(
51-
filter: { authorID: string; postCID?: string },
52+
filter: { authorID?: string; postCID?: string },
5253
options: IGetRepostsOptions,
5354
): Promise<IRepostResponse[]> {
54-
const { sort, offset = 0, limit = 10, following } = options
55+
const { sort, offset = 0, limit = 10, following, type } = options
56+
5557
if (sort === `FOLLOWING` && !following) {
5658
throw new Error(`Following not specified`)
5759
}
@@ -64,6 +66,7 @@ export async function getReposts(
6466
following,
6567
offset,
6668
limit,
69+
...(type ? { type } : {}),
6770
},
6871
})
6972

src/backend/utilities/caching.ts

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export default function cache<T>(fetchFunction: (key: string) => Promise<T>) {
2424
if (!update) {
2525
const cached = _cache.get(key)
2626
if (cached !== undefined) {
27+
if (typeof cached === `object`) {
28+
return { ...cached }
29+
}
2730
return cached
2831
}
2932
}

src/components/ProfilePreview.vue

+13-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div class="flex items-center">
2+
<div v-if="profile !== null" class="flex items-center">
33
<Avatar :authorID="profile.id" :avatar="avatar" size="w-12 h-12" />
44
<div class="h-12 flex-grow px-4">
55
<nuxt-link :to="`/id/` + profile.id" class="flex flex-col">
@@ -54,18 +54,22 @@ export default Vue.extend({
5454
data(): IData {
5555
return {
5656
isFollowing: false,
57-
avatar: undefined,
57+
avatar: ``,
5858
}
5959
},
6060
async created() {
61-
// fetch avatar
62-
if (this.profile.avatar !== null && this.profile.avatar !== ``) {
63-
this.avatar = await getPhotoFromIPFS(this.profile.avatar)
61+
try {
62+
// fetch avatar
63+
if (this.profile.avatar !== null && this.profile.avatar !== ``) {
64+
this.avatar = await getPhotoFromIPFS(this.profile.avatar)
65+
}
66+
// Check if I am following the listed person
67+
getFollowersAndFollowing(this.profile.id, true).then(({ followers }) => {
68+
this.isFollowing = followers.has(this.$store.state.session.id)
69+
})
70+
} catch (err) {
71+
this.$toastError(err as string)
6472
}
65-
// Check if I am following the listed person
66-
getFollowersAndFollowing(this.profile.id, true).then(({ followers }) => {
67-
this.isFollowing = followers.has(this.$store.state.session.id)
68-
})
6973
},
7074
methods: {
7175
getPhotoFromIPFS,

src/components/popups/QuotesPopup.vue

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<template>
2+
<div
3+
class="bg-darkBG dark:bg-gray5 modal-animation fixed top-0 bottom-0 left-0 right-0 z-30 flex h-screen w-full items-center justify-center bg-opacity-50 dark:bg-opacity-50"
4+
@click.self="$emit(`close`)"
5+
>
6+
<!-- Container -->
7+
<div
8+
v-if="postCID !== null"
9+
class="popup min-h-40 w-full lg:w-600 bg-lightBG dark:bg-darkBGStop card-animation max-h-90 z-10 overflow-y-auto rounded-lg px-6 pt-4 pb-2 shadow-lg"
10+
>
11+
<div class="sticky flex items-center justify-between mb-6">
12+
<h2 class="text-lightPrimaryText dark:text-darkPrimaryText text-3xl font-semibold">Quoted this post</h2>
13+
<button class="focus:outline-none bg-gray1 dark:bg-gray5 rounded-full p-1" @click="$emit(`close`)">
14+
<CloseIcon />
15+
</button>
16+
</div>
17+
<div v-show="isLoading" class="modal-animation flex w-full justify-center z-20 mt-24">
18+
<div
19+
class="loader m-5 border-2 border-gray1 dark:border-gray7 h-8 w-8 rounded-3xl"
20+
:style="`border-top: 2px solid` + $color.hex"
21+
></div>
22+
</div>
23+
<article v-if="!isLoading">
24+
<div v-for="p in quoteReposts" :key="p.authorID + p.timestamp" class="flex flex-col">
25+
<div class="flex items-center">
26+
<Avatar :avatar="p.avatar" :authorID="p.authorID" size="w-12 h-12" />
27+
<div class="h-12 flex flex-col px-4">
28+
<nuxt-link :to="`/id/` + p.authorID" class="flex items-center">
29+
<span v-if="p.name != ``" class="text-base font-medium text-lightPrimaryText dark:text-darkPrimaryText">
30+
{{ p.name }}
31+
</span>
32+
<span v-else class="text-gray5 dark:text-gray3 text-base font-medium"> {{ p.authorID }} </span>
33+
<span class="text-gray5 dark:text-gray3 text-sm ml-2">@{{ p.authorID }}</span>
34+
</nuxt-link>
35+
<span class="mt-1 text-xs text-gray5 dark:text-gray3">{{ $formatDate(p.timestamp) }}</span>
36+
</div>
37+
</div>
38+
<div
39+
class="my-4 pb-4 border-b border-lightBorder dark:border-darkBorder text-lightPrimaryText dark:text-darkPrimaryText"
40+
>
41+
{{ p.content }}
42+
</div>
43+
</div>
44+
</article>
45+
<p v-if="!isLoading && quoteReposts.length === 0" class="text-sm text-gray5 dark:text-gray3 text-center mt-14">
46+
None of the reposters quoted this post
47+
</p>
48+
</div>
49+
</div>
50+
</template>
51+
52+
<script lang="ts">
53+
import Vue from 'vue'
54+
import CloseIcon from '@/components/icons/X.vue'
55+
import Avatar from '@/components/Avatar.vue'
56+
import { getReposts, IGetRepostsOptions } from '@/backend/reposts'
57+
import { getRegularPost } from '@/backend/post'
58+
59+
import { createDefaultProfile, getProfile, Profile } from '@/backend/profile'
60+
import { getPhotoFromIPFS } from '@/backend/getPhoto'
61+
62+
interface IData {
63+
isLoading: boolean
64+
profiles: Array<Profile>
65+
quoteReposters: Array<string>
66+
quoteReposts: Array<any>
67+
followers: Set<string>
68+
}
69+
70+
export default Vue.extend({
71+
components: { CloseIcon, Avatar },
72+
props: {
73+
postCID: {
74+
type: String,
75+
required: true,
76+
},
77+
},
78+
data(): IData {
79+
return {
80+
isLoading: true,
81+
profiles: [],
82+
quoteReposters: [],
83+
quoteReposts: [],
84+
followers: new Set(),
85+
}
86+
},
87+
created() {
88+
this.getQuoteReposts()
89+
},
90+
methods: {
91+
updateFollowers(): void {
92+
this.$emit(`updateFollowers`)
93+
},
94+
95+
async getReposterProfile(p: string) {
96+
let profile = createDefaultProfile(p)
97+
let avatar = ``
98+
await getProfile(p).then((fetchedProfile) => {
99+
if (fetchedProfile.profile) {
100+
profile = fetchedProfile.profile
101+
}
102+
if (profile.avatar !== ``) {
103+
getPhotoFromIPFS(profile.avatar).then((a) => {
104+
avatar = a
105+
})
106+
}
107+
})
108+
return { profile, avatar }
109+
},
110+
async getQuoteReposts() {
111+
const options: IGetRepostsOptions = { sort: `NEW`, offset: 0, limit: 1000, type: `quote` }
112+
const reposts = await getReposts({ postCID: this.postCID }, options)
113+
reposts.forEach((repost) => {
114+
this.fetchQuote(repost.repost._id, repost.repost.authorID)
115+
})
116+
this.isLoading = false
117+
},
118+
async fetchQuote(cid: string, authorID: string) {
119+
const { data: content } = await getRegularPost(cid)
120+
const { profile, avatar } = await this.getReposterProfile(authorID)
121+
const q = {
122+
content: content.content,
123+
timestamp: content.timestamp,
124+
authorID: content.authorID,
125+
name: profile.name,
126+
avatar,
127+
}
128+
this.quoteReposts.push(q)
129+
},
130+
},
131+
})
132+
</script>

src/components/popups/RepostersPopup.vue

+19-10
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
:style="`border-top: 2px solid` + $color.hex"
2020
></div>
2121
</div>
22-
<article v-show="!isLoading">
22+
<article v-if="!isLoading && profiles.length > 0">
2323
<div v-for="p in profiles" :key="p.id">
2424
<ProfilePreview :profile="p" class="pb-4" @updateFollowers="updateFollowers" />
2525
</div>
@@ -59,8 +59,8 @@ export default Vue.extend({
5959
followers: new Set(),
6060
}
6161
},
62-
mounted() {
63-
this.initReposters()
62+
async mounted() {
63+
await this.initReposters()
6464
window.addEventListener(`click`, this.handleCloseClick, true)
6565
},
6666
destroyed() {
@@ -89,17 +89,26 @@ export default Vue.extend({
8989
},
9090
async getFollowers(p: string) {
9191
let profile = createDefaultProfile(p)
92-
const fetchedProfile = await getProfile(p)
93-
if (fetchedProfile.profile) {
94-
profile = fetchedProfile.profile
92+
try {
93+
const fetchedProfile = await getProfile(p)
94+
if (fetchedProfile.profile) {
95+
profile = fetchedProfile.profile
96+
}
97+
this.profiles.push(profile)
98+
} catch (err) {
99+
this.$toastError(err as string)
95100
}
96-
this.profiles.push(profile)
97101
},
98102
async initReposters() {
99103
const options: IGetRepostsOptions = { sort: `NEW`, offset: 0, limit: 1000 }
100-
this.reposters = await getReposters(this.postCID, options)
101-
this.reposters.forEach(this.getFollowers)
102-
this.isLoading = false
104+
try {
105+
this.reposters = await getReposters(this.postCID, options)
106+
this.reposters.forEach(await this.getFollowers)
107+
} catch (err) {
108+
this.$toastError(err as string)
109+
} finally {
110+
this.isLoading = false
111+
}
103112
},
104113
},
105114
})

src/components/post/Actions.vue

+20-17
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,20 @@
3333
</div>
3434
</div>
3535
</div>
36-
<button v-if="profiles.length > 0" class="text-sm text-primary w-1/5 h-fit" @click="openReposters">
37-
See reposters
38-
</button>
39-
<button v-else class="text-sm text-primary w-1/5 h-fit cursor-default" disabled style="opacity: 0">
40-
See reposters
41-
</button>
36+
<div v-if="repostsCount > 0" class="flex flex-col w-1/5">
37+
<!-- Show reposters and quotes -->
38+
<button class="text-sm text-primary h-fit flex items-center" @click="openReposters">
39+
<RepostIcon :isActive="true" :shrink="true" class="mr-2 p-1" />
40+
<p>See reposters</p>
41+
</button>
42+
<button class="text-sm text-primary h-fit flex items-center mt-2" @click="$emit(`openQuotes`)">
43+
<QuoteIcon class="mr-2 p-1" />
44+
<p>See quotes</p>
45+
</button>
46+
</div>
47+
<div v-else class="flex flex-grow">
48+
<!-- Filler -->
49+
</div>
4250
</div>
4351
<!-- Comments Activity -->
4452
<div class="flex h-44 justify-between">
@@ -420,6 +428,8 @@ import CloseIcon from '@/components/icons/X.vue'
420428
import StatsIcon from '@/components/icons/Stats.vue'
421429
import ChevronLeft from '@/components/icons/ChevronLeft.vue'
422430
import ChevronRight from '@/components/icons/ChevronRight.vue'
431+
import RepostIcon from '@/components/icons/Repost.vue'
432+
import QuoteIcon from '@/components/icons/Quote.vue'
423433
import Avatar from '@/components/Avatar.vue'
424434
425435
import { feelings } from '@/config/config'
@@ -432,7 +442,6 @@ import {
432442
getCommentsStats,
433443
ICommentsStats,
434444
} from '@/backend/comment'
435-
import { getReposters, IGetRepostsOptions } from '@/backend/reposts'
436445
import { createDefaultProfile, getProfile, Profile } from '@/backend/profile'
437446
import { getFollowersAndFollowing } from '@/backend/following'
438447
import { getPhotoFromIPFS } from '@/backend/getPhoto'
@@ -456,7 +465,7 @@ interface IData {
456465
showDropdown: boolean
457466
toggleStats: boolean
458467
toggleReposters: boolean
459-
reposters: Array<string>
468+
quoteReposts: Array<any>
460469
profiles: Array<Profile>
461470
followers: Set<string>
462471
following: Set<string>
@@ -484,6 +493,8 @@ export default Vue.extend({
484493
ChevronLeft,
485494
ChevronRight,
486495
SendIcon,
496+
RepostIcon,
497+
QuoteIcon,
487498
},
488499
props: {
489500
postCID: {
@@ -522,7 +533,7 @@ export default Vue.extend({
522533
showDropdown: false,
523534
toggleStats: this.openStats,
524535
toggleReposters: false,
525-
reposters: [],
536+
quoteReposts: [],
526537
profiles: [],
527538
followers: new Set(),
528539
following: new Set(),
@@ -546,7 +557,6 @@ export default Vue.extend({
546557
},
547558
created() {
548559
this.initComments()
549-
this.initReposters()
550560
this.isLoading = false
551561
},
552562
mounted() {
@@ -728,19 +738,12 @@ export default Vue.extend({
728738
return new Promise((resolve) => setTimeout(resolve, ms))
729739
},
730740
openReposters() {
731-
// this.toggleStats = false
732-
// this.toggleReposters = true
733741
this.$emit(`reposters`)
734742
},
735743
closeReposters() {
736744
this.toggleStats = true
737745
this.toggleReposters = false
738746
},
739-
async initReposters() {
740-
const options: IGetRepostsOptions = { sort: `NEW`, offset: 0, limit: 1000 }
741-
this.reposters = await getReposters(this.postCID, options)
742-
this.reposters.forEach(this.getFollowers)
743-
},
744747
async getFollowers(p: string) {
745748
let profile = createDefaultProfile(p)
746749
const fetchedProfile = await getProfile(p)

0 commit comments

Comments
 (0)