mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
- we were asked to hide the shouts from other users by Dennis Hack, he believes people will be more likely to shout a post if it is anonymous. This only works from our UI to our backend... further steps would need to be taken to truly make it anonymous. Also, we were asked to implement it in such a way as to give the user the choice to show their shouts to others. Co-authored-by: Alina Beck <alina.beck@mail.com>
569 lines
19 KiB
Vue
569 lines
19 KiB
Vue
<template>
|
|
<div>
|
|
<ds-card v-if="user && user.image">
|
|
<p>PROFILE IMAGE</p>
|
|
</ds-card>
|
|
<ds-space />
|
|
<ds-flex v-if="user" :width="{ base: '100%' }" gutter="base">
|
|
<ds-flex-item :width="{ base: '100%', sm: 2, md: 2, lg: 1 }">
|
|
<ds-card
|
|
:class="{ 'disabled-content': user.disabled }"
|
|
style="position: relative; height: auto;"
|
|
>
|
|
<hc-upload v-if="myProfile" :user="user">
|
|
<hc-avatar :user="user" class="profile-avatar" size="x-large"></hc-avatar>
|
|
</hc-upload>
|
|
<hc-avatar v-else :user="user" class="profile-avatar" size="x-large" />
|
|
<!-- Menu -->
|
|
<client-only>
|
|
<content-menu
|
|
placement="bottom-end"
|
|
resource-type="user"
|
|
:resource="user"
|
|
:is-owner="myProfile"
|
|
class="user-content-menu"
|
|
@block="block"
|
|
@unblock="unblock"
|
|
/>
|
|
</client-only>
|
|
<ds-space margin="small">
|
|
<ds-heading tag="h3" align="center" no-margin>
|
|
{{ userName }}
|
|
</ds-heading>
|
|
<ds-text align="center" color="soft">
|
|
{{ userSlug }}
|
|
</ds-text>
|
|
<ds-text v-if="user.location" align="center" color="soft" size="small">
|
|
<ds-icon name="map-marker" />
|
|
{{ user.location.name }}
|
|
</ds-text>
|
|
<ds-text align="center" color="soft" size="small">
|
|
{{ $t('profile.memberSince') }} {{ user.createdAt | date('MMMM yyyy') }}
|
|
</ds-text>
|
|
</ds-space>
|
|
<ds-space v-if="user.badges && user.badges.length" margin="x-small">
|
|
<hc-badges :badges="user.badges" />
|
|
</ds-space>
|
|
<ds-flex>
|
|
<ds-flex-item>
|
|
<client-only>
|
|
<ds-number :label="$t('profile.followers')">
|
|
<hc-count-to
|
|
slot="count"
|
|
:start-val="followedByCountStartValue"
|
|
:end-val="user.followedByCount"
|
|
/>
|
|
</ds-number>
|
|
</client-only>
|
|
</ds-flex-item>
|
|
<ds-flex-item>
|
|
<client-only>
|
|
<ds-number :label="$t('profile.following')">
|
|
<hc-count-to slot="count" :end-val="user.followingCount" />
|
|
</ds-number>
|
|
</client-only>
|
|
</ds-flex-item>
|
|
</ds-flex>
|
|
<ds-space margin="small">
|
|
<template v-if="!myProfile">
|
|
<hc-follow-button
|
|
v-if="!user.isBlocked"
|
|
:follow-id="user.id"
|
|
:is-followed="user.followedByCurrentUser"
|
|
@optimistic="optimisticFollow"
|
|
@update="updateFollow"
|
|
/>
|
|
<ds-button v-else fullwidth @click="unblock(user)">
|
|
{{ $t('settings.blocked-users.unblock') }}
|
|
</ds-button>
|
|
</template>
|
|
</ds-space>
|
|
<template v-if="user.about">
|
|
<hr />
|
|
<ds-space margin-top="small" margin-bottom="small">
|
|
<ds-text color="soft" size="small" class="hyphenate-text">{{ user.about }}</ds-text>
|
|
</ds-space>
|
|
</template>
|
|
</ds-card>
|
|
<ds-space />
|
|
<ds-heading tag="h3" soft style="text-align: center; margin-bottom: 10px;">
|
|
{{ $t('profile.network.title') }}
|
|
</ds-heading>
|
|
<ds-card style="position: relative; height: auto;">
|
|
<ds-space v-if="user.following && user.following.length" margin="x-small">
|
|
<ds-text tag="h5" color="soft">
|
|
{{ userName | truncate(15) }} {{ $t('profile.network.following') }}
|
|
</ds-text>
|
|
</ds-space>
|
|
<template v-if="user.following && user.following.length">
|
|
<ds-space v-for="follow in uniq(user.following)" :key="follow.id" margin="x-small">
|
|
<!-- TODO: find better solution for rendering errors -->
|
|
<client-only>
|
|
<user :user="follow" :trunc="15" />
|
|
</client-only>
|
|
</ds-space>
|
|
<ds-space v-if="user.followingCount - user.following.length" margin="small">
|
|
<ds-text size="small" color="softer">
|
|
{{
|
|
$t('profile.network.andMore', {
|
|
number: user.followingCount - user.following.length,
|
|
})
|
|
}}
|
|
</ds-text>
|
|
</ds-space>
|
|
</template>
|
|
<template v-else>
|
|
<p style="text-align: center; opacity: .5;">
|
|
{{ userName }} {{ $t('profile.network.followingNobody') }}
|
|
</p>
|
|
</template>
|
|
</ds-card>
|
|
<ds-space />
|
|
<ds-card style="position: relative; height: auto;">
|
|
<ds-space v-if="user.followedBy && user.followedBy.length" margin="x-small">
|
|
<ds-text tag="h5" color="soft">
|
|
{{ userName | truncate(15) }} {{ $t('profile.network.followedBy') }}
|
|
</ds-text>
|
|
</ds-space>
|
|
<template v-if="user.followedBy && user.followedBy.length">
|
|
<ds-space v-for="follow in uniq(user.followedBy)" :key="follow.id" margin="x-small">
|
|
<!-- TODO: find better solution for rendering errors -->
|
|
<client-only>
|
|
<user :user="follow" :trunc="15" />
|
|
</client-only>
|
|
</ds-space>
|
|
<ds-space v-if="user.followedByCount - user.followedBy.length" margin="small">
|
|
<ds-text size="small" color="softer">
|
|
{{
|
|
$t('profile.network.andMore', {
|
|
number: user.followedByCount - user.followedBy.length,
|
|
})
|
|
}}
|
|
</ds-text>
|
|
</ds-space>
|
|
</template>
|
|
<template v-else>
|
|
<p style="text-align: center; opacity: .5;">
|
|
{{ userName }} {{ $t('profile.network.followedByNobody') }}
|
|
</p>
|
|
</template>
|
|
</ds-card>
|
|
<ds-space v-if="user.socialMedia && user.socialMedia.length" margin="large">
|
|
<ds-card style="position: relative; height: auto;">
|
|
<ds-space margin="x-small">
|
|
<ds-text tag="h5" color="soft">
|
|
{{ $t('profile.socialMedia') }} {{ userName | truncate(15) }}?
|
|
</ds-text>
|
|
<template>
|
|
<ds-space v-for="link in socialMediaLinks" :key="link.username" margin="x-small">
|
|
<a :href="link.url" target="_blank">
|
|
<ds-avatar :image="link.favicon" />
|
|
{{ link.username }}
|
|
</a>
|
|
</ds-space>
|
|
</template>
|
|
</ds-space>
|
|
</ds-card>
|
|
</ds-space>
|
|
</ds-flex-item>
|
|
|
|
<ds-flex-item :width="{ base: '100%', sm: 3, md: 5, lg: 3 }">
|
|
<masonry-grid class="user-profile-posts-list">
|
|
<ds-grid-item class="profile-top-navigation" :row-span="3" column-span="fullWidth">
|
|
<ds-card class="ds-tab-nav">
|
|
<ul class="Tabs">
|
|
<li class="Tabs__tab pointer" :class="{ active: tabActive === 'post' }">
|
|
<a @click="handleTab('post')">
|
|
<ds-space margin="small">
|
|
<client-only placeholder="Loading...">
|
|
<ds-number :label="$t('common.post', null, user.contributionsCount)">
|
|
<hc-count-to slot="count" :end-val="user.contributionsCount" />
|
|
</ds-number>
|
|
</client-only>
|
|
</ds-space>
|
|
</a>
|
|
</li>
|
|
<li class="Tabs__tab pointer" :class="{ active: tabActive === 'comment' }">
|
|
<a @click="handleTab('comment')">
|
|
<ds-space margin="small">
|
|
<client-only placeholder="Loading...">
|
|
<ds-number :label="$t('profile.commented')">
|
|
<hc-count-to slot="count" :end-val="user.commentedCount" />
|
|
</ds-number>
|
|
</client-only>
|
|
</ds-space>
|
|
</a>
|
|
</li>
|
|
<li
|
|
class="Tabs__tab pointer"
|
|
:class="{ active: tabActive === 'shout' }"
|
|
v-if="myProfile"
|
|
>
|
|
<a @click="handleTab('shout')">
|
|
<ds-space margin="small">
|
|
<client-only placeholder="Loading...">
|
|
<ds-number :label="$t('profile.shouted')">
|
|
<hc-count-to slot="count" :end-val="user.shoutedCount" />
|
|
</ds-number>
|
|
</client-only>
|
|
</ds-space>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</ds-card>
|
|
</ds-grid-item>
|
|
|
|
<ds-grid-item :row-span="2" column-span="fullWidth">
|
|
<ds-space centered>
|
|
<ds-button
|
|
v-if="myProfile"
|
|
v-tooltip="{
|
|
content: $t('contribution.newPost'),
|
|
placement: 'left',
|
|
delay: { show: 500 },
|
|
}"
|
|
:path="{ name: 'post-create' }"
|
|
class="profile-post-add-button"
|
|
icon="plus"
|
|
size="large"
|
|
primary
|
|
/>
|
|
</ds-space>
|
|
</ds-grid-item>
|
|
|
|
<template v-if="posts.length">
|
|
<masonry-grid-item v-for="post in posts" :key="post.id">
|
|
<hc-post-card
|
|
:post="post"
|
|
:width="{ base: '100%', md: '100%', xl: '50%' }"
|
|
@removePostFromList="removePostFromList"
|
|
@pinPost="pinPost"
|
|
@unpinPost="unpinPost"
|
|
/>
|
|
</masonry-grid-item>
|
|
</template>
|
|
<template v-else-if="$apollo.loading">
|
|
<ds-grid-item column-span="fullWidth">
|
|
<ds-space centered>
|
|
<ds-spinner size="base"></ds-spinner>
|
|
</ds-space>
|
|
</ds-grid-item>
|
|
</template>
|
|
<template v-else>
|
|
<ds-grid-item column-span="fullWidth">
|
|
<hc-empty margin="xx-large" icon="file" />
|
|
</ds-grid-item>
|
|
</template>
|
|
</masonry-grid>
|
|
<div
|
|
v-if="hasMore && posts.length >= pageSize"
|
|
v-infinite-scroll="showMoreContributions"
|
|
:infinite-scroll-disabled="$apollo.loading"
|
|
:infinite-scroll-distance="10"
|
|
:infinite-scroll-throttle-delay="800"
|
|
:infinite-scroll-immediate-check="true"
|
|
>
|
|
<hc-load-more :loading="$apollo.loading" @click="showMoreContributions" />
|
|
</div>
|
|
</ds-flex-item>
|
|
</ds-flex>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import uniqBy from 'lodash/uniqBy'
|
|
import User from '~/components/User/User'
|
|
import HcPostCard from '~/components/PostCard/PostCard.vue'
|
|
import HcFollowButton from '~/components/FollowButton.vue'
|
|
import HcCountTo from '~/components/CountTo.vue'
|
|
import HcBadges from '~/components/Badges.vue'
|
|
import HcLoadMore from '~/components/LoadMore.vue'
|
|
import HcEmpty from '~/components/Empty.vue'
|
|
import ContentMenu from '~/components/ContentMenu'
|
|
import HcUpload from '~/components/Upload'
|
|
import HcAvatar from '~/components/Avatar/Avatar.vue'
|
|
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
|
|
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
|
import { profilePagePosts } from '~/graphql/PostQuery'
|
|
import UserQuery from '~/graphql/User'
|
|
import { Block, Unblock } from '~/graphql/settings/BlockedUsers'
|
|
import PostMutations from '~/graphql/PostMutations'
|
|
|
|
const tabToFilterMapping = ({ tab, id }) => {
|
|
return {
|
|
post: { author: { id } },
|
|
comment: { comments_some: { author: { id } } },
|
|
shout: { shoutedBy_some: { id } },
|
|
}[tab]
|
|
}
|
|
|
|
export default {
|
|
name: 'HcUserProfile',
|
|
components: {
|
|
User,
|
|
HcPostCard,
|
|
HcFollowButton,
|
|
HcCountTo,
|
|
HcBadges,
|
|
HcLoadMore,
|
|
HcEmpty,
|
|
HcAvatar,
|
|
ContentMenu,
|
|
HcUpload,
|
|
MasonryGrid,
|
|
MasonryGridItem,
|
|
},
|
|
transition: {
|
|
name: 'slide-up',
|
|
mode: 'out-in',
|
|
},
|
|
data() {
|
|
const filter = tabToFilterMapping({ tab: 'post', id: this.$route.params.id })
|
|
return {
|
|
User: [],
|
|
posts: [],
|
|
hasMore: true,
|
|
offset: 0,
|
|
pageSize: 6,
|
|
tabActive: 'post',
|
|
filter,
|
|
followedByCountStartValue: 0,
|
|
}
|
|
},
|
|
computed: {
|
|
myProfile() {
|
|
return this.$route.params.id === this.$store.getters['auth/user'].id
|
|
},
|
|
user() {
|
|
return this.User ? this.User[0] : {}
|
|
},
|
|
socialMediaLinks() {
|
|
const { socialMedia = [] } = this.user
|
|
return socialMedia.map(socialMedia => {
|
|
const { url } = socialMedia
|
|
const matches = url.match(/^(?:https?:\/\/)?(?:[^@\n])?(?:www\.)?([^:/\n?]+)/g)
|
|
const [domain] = matches || []
|
|
const favicon = domain ? `${domain}/favicon.ico` : null
|
|
const username = url.split('/').pop()
|
|
return { url, username, favicon }
|
|
})
|
|
},
|
|
userName() {
|
|
const { name } = this.user || {}
|
|
return name || this.$t('profile.userAnonym')
|
|
},
|
|
userSlug() {
|
|
const { slug } = this.user || {}
|
|
return slug && `@${slug}`
|
|
},
|
|
},
|
|
watch: {
|
|
User(val) {
|
|
if (!val || !val.length) {
|
|
throw new Error('User not found!')
|
|
}
|
|
},
|
|
},
|
|
methods: {
|
|
removePostFromList(deletedPost) {
|
|
this.posts = this.posts.filter(post => {
|
|
return post.id !== deletedPost.id
|
|
})
|
|
},
|
|
handleTab(tab) {
|
|
this.tabActive = tab
|
|
this.filter = tabToFilterMapping({ tab, id: this.$route.params.id })
|
|
this.resetPostList()
|
|
},
|
|
uniq(items, field = 'id') {
|
|
return uniqBy(items, field)
|
|
},
|
|
showMoreContributions() {
|
|
const { profilePagePosts: PostQuery } = this.$apollo.queries
|
|
if (!PostQuery) return // seems this can be undefined on subpages
|
|
this.offset += this.pageSize
|
|
|
|
PostQuery.fetchMore({
|
|
variables: {
|
|
offset: this.offset,
|
|
filter: this.filter,
|
|
first: this.pageSize,
|
|
orderBy: 'createdAt_desc',
|
|
},
|
|
updateQuery: (previousResult, { fetchMoreResult }) => {
|
|
if (!fetchMoreResult || fetchMoreResult.profilePagePosts.length < this.pageSize) {
|
|
this.hasMore = false
|
|
}
|
|
const result = Object.assign({}, previousResult, {
|
|
profilePagePosts: [
|
|
...previousResult.profilePagePosts,
|
|
...fetchMoreResult.profilePagePosts,
|
|
],
|
|
})
|
|
return result
|
|
},
|
|
})
|
|
},
|
|
resetPostList() {
|
|
this.offset = 0
|
|
this.posts = []
|
|
this.hasMore = true
|
|
},
|
|
async block(user) {
|
|
await this.$apollo.mutate({ mutation: Block(), variables: { id: user.id } })
|
|
this.$apollo.queries.User.refetch()
|
|
this.resetPostList()
|
|
this.$apollo.queries.profilePagePosts.refetch()
|
|
},
|
|
async unblock(user) {
|
|
await this.$apollo.mutate({ mutation: Unblock(), variables: { id: user.id } })
|
|
this.$apollo.queries.User.refetch()
|
|
this.resetPostList()
|
|
this.$apollo.queries.profilePagePosts.refetch()
|
|
},
|
|
pinPost(post) {
|
|
this.$apollo
|
|
.mutate({
|
|
mutation: PostMutations().pinPost,
|
|
variables: { id: post.id },
|
|
})
|
|
.then(() => {
|
|
this.$toast.success(this.$t('post.menu.pinnedSuccessfully'))
|
|
this.resetPostList()
|
|
this.$apollo.queries.profilePagePosts.refetch()
|
|
})
|
|
.catch(error => this.$toast.error(error.message))
|
|
},
|
|
unpinPost(post) {
|
|
this.$apollo
|
|
.mutate({
|
|
mutation: PostMutations().unpinPost,
|
|
variables: { id: post.id },
|
|
})
|
|
.then(() => {
|
|
this.$toast.success(this.$t('post.menu.unpinnedSuccessfully'))
|
|
this.resetPostList()
|
|
this.$apollo.queries.profilePagePosts.refetch()
|
|
})
|
|
.catch(error => this.$toast.error(error.message))
|
|
},
|
|
optimisticFollow({ followedByCurrentUser }) {
|
|
/*
|
|
* Note: followedByCountStartValue is updated to avoid counting from 0 when follow/unfollow
|
|
*/
|
|
this.followedByCountStartValue = this.user.followedByCount
|
|
const currentUser = this.$store.getters['auth/user']
|
|
if (followedByCurrentUser) {
|
|
this.user.followedByCount++
|
|
this.user.followedBy = [currentUser, ...this.user.followedBy]
|
|
} else {
|
|
this.user.followedByCount--
|
|
this.user.followedBy = this.user.followedBy.filter(user => user.id !== currentUser.id)
|
|
}
|
|
this.user.followedByCurrentUser = followedByCurrentUser
|
|
},
|
|
updateFollow({ followedByCurrentUser, followedBy, followedByCount }) {
|
|
this.followedByCountStartValue = this.user.followedByCount
|
|
this.user.followedByCount = followedByCount
|
|
this.user.followedByCurrentUser = followedByCurrentUser
|
|
this.user.followedBy = followedBy
|
|
},
|
|
},
|
|
apollo: {
|
|
profilePagePosts: {
|
|
query() {
|
|
return profilePagePosts(this.$i18n)
|
|
},
|
|
variables() {
|
|
return {
|
|
filter: this.filter,
|
|
first: this.pageSize,
|
|
offset: 0,
|
|
orderBy: 'createdAt_desc',
|
|
}
|
|
},
|
|
update({ profilePagePosts }) {
|
|
this.posts = profilePagePosts
|
|
},
|
|
fetchPolicy: 'cache-and-network',
|
|
},
|
|
User: {
|
|
query() {
|
|
return UserQuery(this.$i18n)
|
|
},
|
|
variables() {
|
|
return { id: this.$route.params.id }
|
|
},
|
|
fetchPolicy: 'cache-and-network',
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.pointer {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.Tabs {
|
|
position: relative;
|
|
background-color: #fff;
|
|
height: 100%;
|
|
display: flex;
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
|
|
&__tab {
|
|
text-align: center;
|
|
height: 100%;
|
|
flex-grow: 1;
|
|
|
|
&:hover {
|
|
border-bottom: 2px solid #c9c6ce;
|
|
}
|
|
|
|
&.active {
|
|
border-bottom: 2px solid #17b53f;
|
|
}
|
|
}
|
|
}
|
|
.profile-avatar.ds-avatar {
|
|
display: block;
|
|
margin: auto;
|
|
margin-top: -60px;
|
|
border: #fff 5px solid;
|
|
}
|
|
.page-name-profile-id-slug {
|
|
.ds-flex-item:first-child .content-menu {
|
|
position: absolute;
|
|
top: $space-x-small;
|
|
right: $space-x-small;
|
|
}
|
|
}
|
|
.profile-top-navigation {
|
|
position: sticky;
|
|
top: 53px;
|
|
z-index: 2;
|
|
}
|
|
.ds-tab-nav {
|
|
.ds-card-content {
|
|
padding: 0 !important;
|
|
.ds-tab-nav-item {
|
|
&.ds-tab-nav-item-active {
|
|
border-bottom: 3px solid #17b53f;
|
|
&:first-child {
|
|
border-bottom-left-radius: $border-radius-x-large;
|
|
}
|
|
&:last-child {
|
|
border-bottom-right-radius: $border-radius-x-large;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.profile-post-add-button {
|
|
box-shadow: $box-shadow-x-large;
|
|
}
|
|
</style>
|