Merge remote-tracking branch 'upstream/develop' into shigusegubu

* upstream/develop: (42 commits)
  filter outside of component
  fix i18n
  fix overflow behavior
  update styles and remove unnecessary stuff
  refactor showing favs and repeats logic
  display favs & reblogged users on expanded post in timeline view
  display avatars only on highlighted post
  add fadein effect to panel
  remove commented code
  display avatars list on highlighted post
  use transition wrapper component
  remove custom fadein logic
  use a tag for favs/repeated title
  use normal font weight instead of lighter
  use filter array function
  remove important in the property value
  remove important in css property
  use global fallback variable
  remove duplicates from avatars list
  add hover effect to label
  ...
This commit is contained in:
Henry Jameson 2019-04-30 21:20:02 +03:00
commit de54368b87
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) {
this.highlight = id
this.$store.dispatch('fetchFavsAndRepeats', id)
},
getHighlight () {
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 Gallery from '../gallery/gallery.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 fileType from 'src/services/file_type/file_type.service'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.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 = {
name: 'Status',
@ -97,6 +98,10 @@ const Status = {
return this.statusoid
}
},
statusFromGlobalRepository () {
// NOTE: Consider to replace status with statusFromGlobalRepository
return this.$store.state.statuses.allStatusesObject[this.status.id]
},
loggedIn () {
return !!this.$store.state.users.currentUser
},
@ -257,6 +262,14 @@ const Status = {
return 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: {
@ -268,7 +281,8 @@ const Status = {
UserCard,
UserAvatar,
Gallery,
LinkPreview
LinkPreview,
AvatarList
},
methods: {
visibilityIcon (visibility) {

View file

@ -133,6 +133,24 @@
<link-preview :card="status.card" :size="attachmentSize" :nsfw="nsfwClickthrough" />
</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="loggedIn">
<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) {
.status-el {
.retweet-info {

View file

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

View file

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

View file

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

View file

@ -459,6 +459,13 @@ export const mutations = {
},
queueFlush (state, { timeline, 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,
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

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_POST_STATUS_URL = '/api/v1/statuses'
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'
import { each, map, concat, last } from 'lodash'
@ -712,6 +714,14 @@ const markNotificationsAsSeen = ({id, credentials}) => {
}).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 = {
verifyCredentials,
fetchTimeline,
@ -761,7 +771,9 @@ const apiService = {
approveUser,
denyUser,
suggestions,
markNotificationsAsSeen
markNotificationsAsSeen,
fetchFavoritedByUsers,
fetchRebloggedByUsers
}
export default apiService

View file

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