Merge branch 'brendenbice1222/pleroma-fe-issues/pleroma-fe-202-show-boosted-users' into 'develop'

Show favoriting and repeating users in hilighted status

See merge request pleroma/pleroma-fe!768
This commit is contained in:
HJ 2019-04-30 17:39:33 +00:00
commit a954f56e34
11 changed files with 178 additions and 5 deletions

View file

@ -0,0 +1,15 @@
import UserAvatar from '../user_avatar/user_avatar.vue'
const AvatarList = {
props: ['avatars'],
computed: {
slicedAvatars () {
return this.avatars ? this.avatars.slice(0, 15) : []
}
},
components: {
UserAvatar
}
}
export default AvatarList

View file

@ -0,0 +1,38 @@
<template>
<div class="avatars">
<div class="avatars-item" v-for="avatar in slicedAvatars">
<UserAvatar :src="avatar.profile_image_url" class="avatar-small" />
</div>
</div>
</template>
<script src="./avatar_list.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
.avatars {
display: flex;
margin: 0;
padding: 0;
// For hiding overflowing elements
flex-wrap: wrap;
height: 24px;
.avatars-item {
margin: 0 0 5px 5px;
&:first-child {
padding-left: 5px;
}
.avatar-small {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
height: 24px;
width: 24px;
}
}
}
</style>

View file

@ -139,6 +139,7 @@ const conversation = {
}, },
setHighlight (id) { setHighlight (id) {
this.highlight = id this.highlight = id
this.$store.dispatch('fetchFavsAndRepeats', id)
}, },
getHighlight () { getHighlight () {
return this.isExpanded ? this.highlight : null return this.isExpanded ? this.highlight : null

View file

@ -7,11 +7,12 @@ import UserCard from '../user_card/user_card.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import Gallery from '../gallery/gallery.vue' import Gallery from '../gallery/gallery.vue'
import LinkPreview from '../link-preview/link-preview.vue' import LinkPreview from '../link-preview/link-preview.vue'
import AvatarList from '../avatar_list/avatar_list.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service' import fileType from 'src/services/file_type/file_type.service'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js' import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
import { filter, find, unescape } from 'lodash' import { filter, find, unescape, uniqBy } from 'lodash'
const Status = { const Status = {
name: 'Status', name: 'Status',
@ -97,6 +98,10 @@ const Status = {
return this.statusoid return this.statusoid
} }
}, },
statusFromGlobalRepository () {
// NOTE: Consider to replace status with statusFromGlobalRepository
return this.$store.state.statuses.allStatusesObject[this.status.id]
},
loggedIn () { loggedIn () {
return !!this.$store.state.users.currentUser return !!this.$store.state.users.currentUser
}, },
@ -257,6 +262,14 @@ const Status = {
return this.status.statusnet_html return this.status.statusnet_html
} }
return this.status.summary_html + '<br />' + this.status.statusnet_html return this.status.summary_html + '<br />' + this.status.statusnet_html
},
combinedFavsAndRepeatsAvatars () {
// Use the status from the global status repository since favs and repeats are saved in it
const combinedAvatars = [].concat(
this.statusFromGlobalRepository.favoritedBy,
this.statusFromGlobalRepository.rebloggedBy
)
return uniqBy(combinedAvatars, 'id')
} }
}, },
components: { components: {
@ -268,7 +281,8 @@ const Status = {
UserCard, UserCard,
UserAvatar, UserAvatar,
Gallery, Gallery,
LinkPreview LinkPreview,
AvatarList
}, },
methods: { methods: {
visibilityIcon (visibility) { visibilityIcon (visibility) {

View file

@ -133,6 +133,24 @@
<link-preview :card="status.card" :size="attachmentSize" :nsfw="nsfwClickthrough" /> <link-preview :card="status.card" :size="attachmentSize" :nsfw="nsfwClickthrough" />
</div> </div>
<transition name="fade">
<div class="favs-repeated-users" v-if="combinedFavsAndRepeatsAvatars.length > 0 && isFocused">
<div class="stats">
<div class="stat-count" v-if="statusFromGlobalRepository.rebloggedBy && statusFromGlobalRepository.rebloggedBy.length > 0">
<a class="stat-title">{{ $t('status.repeats') }}</a>
<div class="stat-number">{{ statusFromGlobalRepository.rebloggedBy.length }}</div>
</div>
<div class="stat-count" v-if="statusFromGlobalRepository.favoritedBy && statusFromGlobalRepository.favoritedBy.length > 0">
<a class="stat-title">{{ $t('status.favorites') }}</a>
<div class="stat-number">{{ statusFromGlobalRepository.favoritedBy.length }}</div>
</div>
<div class="avatar-row">
<AvatarList :avatars='combinedFavsAndRepeatsAvatars'></AvatarList>
</div>
</div>
</div>
</transition>
<div v-if="!noHeading && !isPreview" class='status-actions media-body'> <div v-if="!noHeading && !isPreview" class='status-actions media-body'>
<div v-if="loggedIn"> <div v-if="loggedIn">
<i class="button-icon icon-reply" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')" :class="{'icon-reply-active': replying}"></i> <i class="button-icon icon-reply" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')" :class="{'icon-reply-active': replying}"></i>
@ -612,6 +630,50 @@ a.unmute {
} }
} }
.favs-repeated-users {
margin-top: $status-margin;
.stats {
width: 100%;
display: flex;
line-height: 1em;
.stat-count {
margin-right: $status-margin;
.stat-title {
color: var(--faint, $fallback--faint);
font-size: 12px;
text-transform: uppercase;
position: relative;
}
.stat-number {
font-weight: bolder;
font-size: 16px;
line-height: 1em;
}
}
.avatar-row {
flex: 1;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
&::before {
content: '';
position: absolute;
height: 100%;
width: 1px;
left: 0;
background-color: var(--faint, $fallback--faint);
}
}
}
}
@media all and (max-width: 800px) { @media all and (max-width: 800px) {
.status-el { .status-el {
.retweet-info { .retweet-info {

View file

@ -16,7 +16,7 @@
</div> </div>
<div :class="classes.body"> <div :class="classes.body">
<div class="timeline"> <div class="timeline">
<conversation <conversation
v-for="status in timeline.visibleStatuses" v-for="status in timeline.visibleStatuses"
class="status-fadein" class="status-fadein"
:key="status.id" :key="status.id"

View file

@ -394,6 +394,8 @@
"no_statuses": "No statuses" "no_statuses": "No statuses"
}, },
"status": { "status": {
"favorites": "Favorites",
"repeats": "Repeats",
"reply_to": "Reply to", "reply_to": "Reply to",
"replies_list": "Replies:" "replies_list": "Replies:"
}, },

View file

@ -222,6 +222,8 @@
"no_more_statuses": "Ei enempää viestejä" "no_more_statuses": "Ei enempää viestejä"
}, },
"status": { "status": {
"favorites": "Tykkäykset",
"repeats": "Toistot",
"reply_to": "Vastaus", "reply_to": "Vastaus",
"replies_list": "Vastaukset:" "replies_list": "Vastaukset:"
}, },

View file

@ -459,6 +459,13 @@ export const mutations = {
}, },
queueFlush (state, { timeline, id }) { queueFlush (state, { timeline, id }) {
state.timelines[timeline].flushMarker = id state.timelines[timeline].flushMarker = id
},
addFavsAndRepeats (state, { id, favoritedByUsers, rebloggedByUsers }) {
state.allStatusesObject[id] = {
...state.allStatusesObject[id],
favoritedBy: favoritedByUsers,
rebloggedBy: rebloggedByUsers
}
} }
} }
@ -524,6 +531,21 @@ const statuses = {
id: rootState.statuses.notifications.maxId, id: rootState.statuses.notifications.maxId,
credentials: rootState.users.currentUser.credentials credentials: rootState.users.currentUser.credentials
}) })
},
fetchFavsAndRepeats ({ rootState, commit }, id) {
Promise.all([
rootState.api.backendInteractor.fetchFavoritedByUsers(id),
rootState.api.backendInteractor.fetchRebloggedByUsers(id)
]).then(([favoritedByUsers, rebloggedByUsers]) =>
commit(
'addFavsAndRepeats',
{
id,
favoritedByUsers: favoritedByUsers.filter(_ => _),
rebloggedByUsers: rebloggedByUsers.filter(_ => _)
}
)
)
} }
}, },
mutations mutations

View file

@ -47,6 +47,8 @@ const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute`
const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute` const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
const MASTODON_POST_STATUS_URL = '/api/v1/statuses' const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media' const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
const MASTODON_STATUS_FAVORITEDBY_URL = id => `/api/v1/statuses/${id}/favourited_by`
const MASTODON_STATUS_REBLOGGEDBY_URL = id => `/api/v1/statuses/${id}/reblogged_by`
const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials' const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials'
import { each, map, concat, last } from 'lodash' import { each, map, concat, last } from 'lodash'
@ -712,6 +714,14 @@ const markNotificationsAsSeen = ({id, credentials}) => {
}).then((data) => data.json()) }).then((data) => data.json())
} }
const fetchFavoritedByUsers = ({id}) => {
return promisedRequest(MASTODON_STATUS_FAVORITEDBY_URL(id)).then((users) => users.map(parseUser))
}
const fetchRebloggedByUsers = ({id}) => {
return promisedRequest(MASTODON_STATUS_REBLOGGEDBY_URL(id)).then((users) => users.map(parseUser))
}
const apiService = { const apiService = {
verifyCredentials, verifyCredentials,
fetchTimeline, fetchTimeline,
@ -761,7 +771,9 @@ const apiService = {
approveUser, approveUser,
denyUser, denyUser,
suggestions, suggestions,
markNotificationsAsSeen markNotificationsAsSeen,
fetchFavoritedByUsers,
fetchRebloggedByUsers
} }
export default apiService export default apiService

View file

@ -113,6 +113,9 @@ const backendInteractorService = (credentials) => {
const deleteAccount = ({password}) => apiService.deleteAccount({credentials, password}) const deleteAccount = ({password}) => apiService.deleteAccount({credentials, password})
const changePassword = ({password, newPassword, newPasswordConfirmation}) => apiService.changePassword({credentials, password, newPassword, newPasswordConfirmation}) const changePassword = ({password, newPassword, newPasswordConfirmation}) => apiService.changePassword({credentials, password, newPassword, newPasswordConfirmation})
const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({id})
const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({id})
const backendInteractorServiceInstance = { const backendInteractorServiceInstance = {
fetchStatus, fetchStatus,
fetchConversation, fetchConversation,
@ -154,7 +157,9 @@ const backendInteractorService = (credentials) => {
changePassword, changePassword,
fetchFollowRequests, fetchFollowRequests,
approveUser, approveUser,
denyUser denyUser,
fetchFavoritedByUsers,
fetchRebloggedByUsers
} }
return backendInteractorServiceInstance return backendInteractorServiceInstance