logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe
commit: 09736691ea79e66c9e41d6f723384769088eb2d0
parent: 82cd1252b58cce7fd32aafb89a21865715c23eb3
Author: Shpuld Shpludson <shp@cock.li>
Date:   Thu,  7 Mar 2019 16:38:45 +0000

Merge branch '420-redo-user-card-content' into 'develop'

Re-do UserCardContent Component CSS

Closes #420

See merge request pleroma/pleroma-fe!642

Diffstat:

Msrc/components/basic_user_card/basic_user_card.js4++--
Msrc/components/basic_user_card/basic_user_card.vue33++++++++-------------------------
Msrc/components/notification/notification.js4++--
Msrc/components/notification/notification.vue4+---
Msrc/components/notifications/notifications.scss4----
Msrc/components/side_drawer/side_drawer.js4++--
Msrc/components/side_drawer/side_drawer.vue11+----------
Msrc/components/status/status.js4++--
Msrc/components/status/status.vue7++-----
Asrc/components/user_card/user_card.js156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/user_card/user_card.vue419+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/components/user_card_content/user_card_content.js149-------------------------------------------------------------------------------
Dsrc/components/user_card_content/user_card_content.vue417-------------------------------------------------------------------------------
Msrc/components/user_panel/user_panel.js4++--
Msrc/components/user_panel/user_panel.vue12+-----------
Msrc/components/user_profile/user_profile.js4++--
Msrc/components/user_profile/user_profile.vue11+----------
Mtest/unit/specs/boot/routes.spec.js4++--
18 files changed, 603 insertions(+), 648 deletions(-)

diff --git a/src/components/basic_user_card/basic_user_card.js b/src/components/basic_user_card/basic_user_card.js @@ -1,4 +1,4 @@ -import UserCardContent from '../user_card_content/user_card_content.vue' +import UserCard from '../user_card/user_card.vue' import UserAvatar from '../user_avatar/user_avatar.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -12,7 +12,7 @@ const BasicUserCard = { } }, components: { - UserCardContent, + UserCard, UserAvatar }, methods: { diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue @@ -1,18 +1,18 @@ <template> - <div class="user-card"> + <div class="basic-user-card"> <router-link :to="userProfileLink(user)"> <UserAvatar class="avatar" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/> </router-link> - <div class="user-card-expanded-content" v-if="userExpanded"> - <user-card-content :user="user" :switcher="false"></user-card-content> + <div class="basic-user-card-expanded-content" v-if="userExpanded"> + <UserCard :user="user" :rounded="true" :bordered="true"/> </div> - <div class="user-card-collapsed-content" v-else> - <div :title="user.name" class="user-card-user-name"> + <div class="basic-user-card-collapsed-content" v-else> + <div :title="user.name" class="basic-user-card-user-name"> <span v-if="user.name_html" v-html="user.name_html"></span> <span v-else>{{ user.name }}</span> </div> <div> - <router-link class="user-card-screen-name" :to="userProfileLink(user)"> + <router-link class="basic-user-card-screen-name" :to="userProfileLink(user)"> @{{user.screen_name}} </router-link> </div> @@ -26,15 +26,15 @@ <style lang="scss"> @import '../../_variables.scss'; -.user-card { +.basic-user-card { display: flex; flex: 1 0; + margin: 0; padding-top: 0.6em; padding-right: 1em; padding-bottom: 0.6em; padding-left: 1em; border-bottom: 1px solid; - margin: 0; border-bottom-color: $fallback--border; border-bottom-color: var(--border, $fallback--border); @@ -57,23 +57,6 @@ &-expanded-content { flex: 1; margin-left: 0.7em; - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); - border-style: solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - border-width: 1px; - overflow: hidden; - - .panel-heading { - background: transparent; - flex-direction: column; - align-items: stretch; - } - - p { - margin-bottom: 0; - } } } </style> diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js @@ -1,6 +1,6 @@ import Status from '../status/status.vue' import UserAvatar from '../user_avatar/user_avatar.vue' -import UserCardContent from '../user_card_content/user_card_content.vue' +import UserCard from '../user_card/user_card.vue' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -13,7 +13,7 @@ const Notification = { }, props: [ 'notification' ], components: { - Status, UserAvatar, UserCardContent + Status, UserAvatar, UserCard }, methods: { toggleUserExpanded () { diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue @@ -5,9 +5,7 @@ <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/> </a> <div class='notification-right'> - <div class="usercard notification-usercard" v-if="userExpanded"> - <user-card-content :user="notification.action.user" :switcher="false"></user-card-content> - </div> + <UserCard :user="notification.action.user" :rounded="true" :bordered="true" v-if="userExpanded"/> <span class="notification-details"> <div class="name-and-action"> <span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span> diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss @@ -45,10 +45,6 @@ } } - .notification-usercard { - margin: 0; - } - .non-mention { display: flex; flex: 1; diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js @@ -1,4 +1,4 @@ -import UserCardContent from '../user_card_content/user_card_content.vue' +import UserCard from '../user_card/user_card.vue' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' // TODO: separate touch gesture stuff into their own utils if more components want them @@ -12,7 +12,7 @@ const SideDrawer = { closed: true, touchCoord: [0, 0] }), - components: { UserCardContent }, + components: { UserCard }, computed: { currentUser () { return this.$store.state.users.currentUser diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue @@ -8,7 +8,7 @@ @touchmove="touchMove" > <div class="side-drawer-heading" @click="toggleDrawer"> - <user-card-content :user="currentUser" :switcher="false" :hideBio="true" v-if="currentUser"/> + <UserCard :user="currentUser" :hideBio="true" v-if="currentUser"/> <div class="side-drawer-logo-wrapper" v-else> <img :src="logo"/> <span>{{sitename}}</span> @@ -181,15 +181,6 @@ display: flex; padding: 0; margin: 0; - - .profile-panel-background { - border-radius: 0; - .panel-heading { - background: transparent; - flex-direction: column; - align-items: stretch; - } - } } .side-drawer ul { diff --git a/src/components/status/status.js b/src/components/status/status.js @@ -3,7 +3,7 @@ import FavoriteButton from '../favorite_button/favorite_button.vue' import RetweetButton from '../retweet_button/retweet_button.vue' import DeleteButton from '../delete_button/delete_button.vue' import PostStatusForm from '../post_status_form/post_status_form.vue' -import UserCardContent from '../user_card_content/user_card_content.vue' +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' @@ -259,7 +259,7 @@ const Status = { RetweetButton, DeleteButton, PostStatusForm, - UserCardContent, + UserCard, UserAvatar, Gallery, LinkPreview diff --git a/src/components/status/status.vue b/src/components/status/status.vue @@ -31,9 +31,7 @@ </router-link> </div> <div class="status-body"> - <div class="usercard" v-if="userExpanded"> - <user-card-content :user="status.user" :switcher="false"></user-card-content> - </div> + <UserCard :user="status.user" :rounded="true" :bordered="true" class="status-usercard" v-if="userExpanded"/> <div v-if="!noHeading" class="media-heading"> <div class="heading-name-row"> <div class="name-and-account-name"> @@ -248,8 +246,7 @@ $status-margin: 0.75em; padding: 0; } - .usercard { - margin: 0; + .status-usercard { margin-bottom: $status-margin; } diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js @@ -0,0 +1,156 @@ +import UserAvatar from '../user_avatar/user_avatar.vue' +import { hex2rgb } from '../../services/color_convert/color_convert.js' +import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' +import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' + +export default { + props: [ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered' ], + data () { + return { + followRequestInProgress: false, + followRequestSent: false, + hideUserStatsLocal: typeof this.$store.state.config.hideUserStats === 'undefined' + ? this.$store.state.instance.hideUserStats + : this.$store.state.config.hideUserStats, + betterShadow: this.$store.state.interface.browserSupport.cssFilter + } + }, + computed: { + classes () { + return [{ + 'user-card-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius + 'user-card-rounded': this.rounded === true, // set border-radius for all sides + 'user-card-bordered': this.bordered === true // set border for all sides + }] + }, + style () { + const color = this.$store.state.config.customTheme.colors + ? this.$store.state.config.customTheme.colors.bg // v2 + : this.$store.state.config.colors.bg // v1 + + if (color) { + const rgb = (typeof color === 'string') ? hex2rgb(color) : color + const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .5)` + + const gradient = [ + [tintColor, this.hideBio ? '60%' : ''], + this.hideBio ? [ + color, '100%' + ] : [ + tintColor, '' + ] + ].map(_ => _.join(' ')).join(', ') + + return { + backgroundColor: `rgb(${Math.floor(rgb.r * 0.53)}, ${Math.floor(rgb.g * 0.56)}, ${Math.floor(rgb.b * 0.59)})`, + backgroundImage: [ + `linear-gradient(to bottom, ${gradient})`, + `url(${this.user.cover_photo})` + ].join(', ') + } + } + }, + isOtherUser () { + return this.user.id !== this.$store.state.users.currentUser.id + }, + subscribeUrl () { + // eslint-disable-next-line no-undef + const serverUrl = new URL(this.user.statusnet_profile_url) + return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus` + }, + loggedIn () { + return this.$store.state.users.currentUser + }, + dailyAvg () { + const days = Math.ceil((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000)) + return Math.round(this.user.statuses_count / days) + }, + userHighlightType: { + get () { + const data = this.$store.state.config.highlight[this.user.screen_name] + return data && data.type || 'disabled' + }, + set (type) { + const data = this.$store.state.config.highlight[this.user.screen_name] + if (type !== 'disabled') { + this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: data && data.color || '#FFFFFF', type }) + } else { + this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: undefined }) + } + } + }, + userHighlightColor: { + get () { + const data = this.$store.state.config.highlight[this.user.screen_name] + return data && data.color + }, + set (color) { + this.$store.dispatch('setHighlight', { user: this.user.screen_name, color }) + } + }, + visibleRole () { + const validRole = (this.user.role === 'admin' || this.user.role === 'moderator') + const showRole = this.isOtherUser || this.user.show_role + + return validRole && showRole && this.user.role + } + }, + components: { + UserAvatar + }, + methods: { + followUser () { + const store = this.$store + this.followRequestInProgress = true + requestFollow(this.user, store).then(({sent}) => { + this.followRequestInProgress = false + this.followRequestSent = sent + }) + }, + unfollowUser () { + const store = this.$store + this.followRequestInProgress = true + requestUnfollow(this.user, store).then(() => { + this.followRequestInProgress = false + store.commit('removeStatus', { timeline: 'friends', userId: this.user.id }) + }) + }, + blockUser () { + const store = this.$store + store.state.api.backendInteractor.blockUser(this.user.id) + .then((blockedUser) => { + store.commit('addNewUsers', [blockedUser]) + store.commit('removeStatus', { timeline: 'friends', userId: this.user.id }) + store.commit('removeStatus', { timeline: 'public', userId: this.user.id }) + store.commit('removeStatus', { timeline: 'publicAndExternal', userId: this.user.id }) + }) + }, + unblockUser () { + const store = this.$store + store.state.api.backendInteractor.unblockUser(this.user.id) + .then((unblockedUser) => store.commit('addNewUsers', [unblockedUser])) + }, + toggleMute () { + const store = this.$store + store.commit('setMuted', {user: this.user, muted: !this.user.muted}) + store.state.api.backendInteractor.setUserMute(this.user) + }, + setProfileView (v) { + if (this.switcher) { + const store = this.$store + store.commit('setProfileView', { v }) + } + }, + linkClicked ({target}) { + if (target.tagName === 'SPAN') { + target = target.parentNode + } + if (target.tagName === 'A') { + window.open(target.href, '_blank') + } + }, + userProfileLink (user) { + return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) + } + } +} diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue @@ -0,0 +1,419 @@ +<template> +<div class="user-card" :class="classes" :style="style"> + <div class="panel-heading"> + <div class='user-info'> + <div class='container'> + <router-link :to="userProfileLink(user)"> + <UserAvatar :betterShadow="betterShadow" :src="user.profile_image_url_original"/> + </router-link> + <div class="name-and-screen-name"> + <div class="top-line"> + <div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div> + <div :title="user.name" class='user-name' v-else>{{user.name}}</div> + <router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser"> + <i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i> + </router-link> + <a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local"> + <i class="icon-link-ext usersettings"></i> + </a> + </div> + + <router-link class='user-screen-name' :to="userProfileLink(user)"> + <span class="handle">@{{user.screen_name}} + <span class="alert staff" v-if="!hideBio && !!visibleRole">{{visibleRole}}</span> + </span><span v-if="user.locked"><i class="icon icon-lock"></i></span> + <span v-if="!hideUserStatsLocal && !hideBio" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span> + </router-link> + </div> + </div> + <div class="user-meta"> + <div v-if="user.follows_you && loggedIn && isOtherUser" class="following"> + {{ $t('user_card.follows_you') }} + </div> + <div class="highlighter" v-if="isOtherUser && (loggedIn || !switcher)"> + <!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to --> + <input class="userHighlightText" type="text" :id="'userHighlightColorTx'+user.id" v-if="userHighlightType !== 'disabled'" v-model="userHighlightColor"/> + <input class="userHighlightCl" type="color" :id="'userHighlightColor'+user.id" v-if="userHighlightType !== 'disabled'" v-model="userHighlightColor"/> + <label for="style-switcher" class='userHighlightSel select'> + <select class="userHighlightSel" :id="'userHighlightSel'+user.id" v-model="userHighlightType"> + <option value="disabled">No highlight</option> + <option value="solid">Solid bg</option> + <option value="striped">Striped bg</option> + <option value="side">Side stripe</option> + </select> + <i class="icon-down-open"/> + </label> + </div> + </div> + <div v-if="isOtherUser" class="user-interactions"> + <div class="follow" v-if="loggedIn"> + <span v-if="user.following"> + <!--Following them!--> + <button @click="unfollowUser" class="pressed" :disabled="followRequestInProgress" :title="$t('user_card.follow_unfollow')"> + <template v-if="followRequestInProgress"> + {{ $t('user_card.follow_progress') }} + </template> + <template v-else> + {{ $t('user_card.following') }} + </template> + </button> + </span> + <span v-if="!user.following"> + <button @click="followUser" :disabled="followRequestInProgress" :title="followRequestSent ? $t('user_card.follow_again') : ''"> + <template v-if="followRequestInProgress"> + {{ $t('user_card.follow_progress') }} + </template> + <template v-else-if="followRequestSent"> + {{ $t('user_card.follow_sent') }} + </template> + <template v-else> + {{ $t('user_card.follow') }} + </template> + </button> + </span> + </div> + <div class='mute' v-if='isOtherUser && loggedIn'> + <span v-if='user.muted'> + <button @click="toggleMute" class="pressed"> + {{ $t('user_card.muted') }} + </button> + </span> + <span v-if='!user.muted'> + <button @click="toggleMute"> + {{ $t('user_card.mute') }} + </button> + </span> + </div> + <div class="remote-follow" v-if='!loggedIn && user.is_local'> + <form method="POST" :action='subscribeUrl'> + <input type="hidden" name="nickname" :value="user.screen_name"> + <input type="hidden" name="profile" value=""> + <button click="submit" class="remote-button"> + {{ $t('user_card.remote_follow') }} + </button> + </form> + </div> + <div class='block' v-if='isOtherUser && loggedIn'> + <span v-if='user.statusnet_blocking'> + <button @click="unblockUser" class="pressed"> + {{ $t('user_card.blocked') }} + </button> + </span> + <span v-if='!user.statusnet_blocking'> + <button @click="blockUser"> + {{ $t('user_card.block') }} + </button> + </span> + </div> + </div> + </div> + </div> + <div class="panel-body" v-if="!hideBio"> + <div v-if="!hideUserStatsLocal && switcher" class="user-counts"> + <div class="user-count" v-on:click.prevent="setProfileView('statuses')"> + <h5>{{ $t('user_card.statuses') }}</h5> + <span>{{user.statuses_count}} <br></span> + </div> + <div class="user-count" v-on:click.prevent="setProfileView('friends')"> + <h5>{{ $t('user_card.followees') }}</h5> + <span>{{user.friends_count}}</span> + </div> + <div class="user-count" v-on:click.prevent="setProfileView('followers')"> + <h5>{{ $t('user_card.followers') }}</h5> + <span>{{user.followers_count}}</span> + </div> + </div> + <p @click.prevent="linkClicked" v-if="!hideBio && user.description_html" class="user-card-bio" v-html="user.description_html"></p> + <p v-else-if="!hideBio" class="user-card-bio">{{ user.description }}</p> + </div> +</div> +</template> + +<script src="./user_card.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.user-card { + background-size: cover; + overflow: hidden; + + .panel-heading { + padding: .5em 0; + text-align: center; + box-shadow: none; + background: transparent; + flex-direction: column; + align-items: stretch; + } + + .panel-body { + word-wrap: break-word; + background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%); + background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%); + } + + p { + margin-bottom: 0; + } + + &-bio { + text-align: center; + } + + // Modifiers + + &-rounded-t { + border-top-left-radius: $fallback--panelRadius; + border-top-left-radius: var(--panelRadius, $fallback--panelRadius); + border-top-right-radius: $fallback--panelRadius; + border-top-right-radius: var(--panelRadius, $fallback--panelRadius); + } + + &-rounded { + border-radius: $fallback--panelRadius; + border-radius: var(--panelRadius, $fallback--panelRadius); + } + + &-bordered { + border-width: 1px; + border-style: solid; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + } +} + +.user-info { + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + padding: 0 26px; + + .container { + padding: 16px 0 6px; + display: flex; + max-height: 56px; + + .avatar { + flex: 1 0 100%; + width: 56px; + height: 56px; + box-shadow: 0px 1px 8px rgba(0,0,0,0.75); + box-shadow: var(--avatarShadow); + object-fit: cover; + } + } + + &:hover .animated.avatar { + canvas { + display: none; + } + img { + visibility: visible; + } + } + + .usersettings { + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + opacity: .8; + } + + .name-and-screen-name { + display: block; + margin-left: 0.6em; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1 1 0; + // This is so that text doesn't get overlapped by avatar's shadow if it has + // big one + z-index: 1; + + img { + width: 26px; + height: 26px; + vertical-align: middle; + object-fit: contain + } + .top-line { + display: flex; + } + } + + .user-name { + text-overflow: ellipsis; + overflow: hidden; + flex: 1 1 auto; + margin-right: 1em; + font-size: 15px; + + img { + object-fit: contain; + height: 16px; + width: 16px; + vertical-align: middle; + } + } + + .user-screen-name { + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + display: inline-block; + font-weight: light; + font-size: 15px; + padding-right: 0.1em; + width: 100%; + display: flex; + + .dailyAvg { + min-width: 1px; + flex: 0 0 auto; + margin-left: 1em; + font-size: 0.7em; + color: $fallback--text; + color: var(--text, $fallback--text); + } + + .handle { + min-width: 1px; + flex: 0 1 auto; + text-overflow: ellipsis; + overflow: hidden; + } + + // TODO use proper colors + .staff { + text-transform: capitalize; + color: $fallback--text; + color: var(--btnText, $fallback--text); + background-color: $fallback--fg; + background-color: var(--btn, $fallback--fg); + } + } + + .user-meta { + margin-bottom: .15em; + display: flex; + align-items: baseline; + font-size: 14px; + line-height: 22px; + flex-wrap: wrap; + + .following { + flex: 1 0 auto; + margin: 0; + margin-bottom: .25em; + text-align: left; + } + + .highlighter { + flex: 0 1 auto; + display: flex; + flex-wrap: wrap; + margin-right: -.5em; + align-self: start; + + .userHighlightCl { + padding: 2px 10px; + flex: 1 0 auto; + } + + .userHighlightSel, + .userHighlightSel.select { + padding-top: 0; + padding-bottom: 0; + flex: 1 0 auto; + } + .userHighlightSel.select i { + line-height: 22px; + } + + .userHighlightText { + width: 70px; + flex: 1 0 auto; + } + + .userHighlightCl, + .userHighlightText, + .userHighlightSel, + .userHighlightSel.select { + height: 22px; + vertical-align: top; + margin-right: .5em; + margin-bottom: .25em; + } + } + } + .user-interactions { + display: flex; + flex-flow: row wrap; + justify-content: space-between; + + margin-right: -.75em; + + div { + flex: 1 0 0; + margin-right: .75em; + margin-bottom: .6em; + white-space: nowrap; + } + + .mute { + max-width: 220px; + min-height: 28px; + } + + .remote-follow { + max-width: 220px; + min-height: 28px; + } + + .follow { + max-width: 220px; + min-height: 28px; + } + + button { + width: 100%; + height: 100%; + margin: 0; + } + + .remote-button { + height: 28px !important; + width: 92%; + } + + .pressed { + border-bottom-color: rgba(255, 255, 255, 0.2); + border-top-color: rgba(0, 0, 0, 0.2); + } + } +} + +.user-counts { + display: flex; + line-height:16px; + padding: .5em 1.5em 0em 1.5em; + text-align: center; + justify-content: space-between; + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + flex-wrap: wrap; +} + +.user-count { + flex: 1 0 auto; + padding: .5em 0 .5em 0; + margin: 0 .5em; + + h5 { + font-size:1em; + font-weight: bolder; + margin: 0 0 0.25em; + } + a { + text-decoration: none; + } +} +</style> diff --git a/src/components/user_card_content/user_card_content.js b/src/components/user_card_content/user_card_content.js @@ -1,149 +0,0 @@ -import UserAvatar from '../user_avatar/user_avatar.vue' -import { hex2rgb } from '../../services/color_convert/color_convert.js' -import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' -import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' - -export default { - props: [ 'user', 'switcher', 'selected', 'hideBio' ], - data () { - return { - followRequestInProgress: false, - followRequestSent: false, - hideUserStatsLocal: typeof this.$store.state.config.hideUserStats === 'undefined' - ? this.$store.state.instance.hideUserStats - : this.$store.state.config.hideUserStats, - betterShadow: this.$store.state.interface.browserSupport.cssFilter - } - }, - computed: { - headingStyle () { - const color = this.$store.state.config.customTheme.colors - ? this.$store.state.config.customTheme.colors.bg // v2 - : this.$store.state.config.colors.bg // v1 - - if (color) { - const rgb = (typeof color === 'string') ? hex2rgb(color) : color - const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .5)` - - const gradient = [ - [tintColor, this.hideBio ? '60%' : ''], - this.hideBio ? [ - color, '100%' - ] : [ - tintColor, '' - ] - ].map(_ => _.join(' ')).join(', ') - - return { - backgroundColor: `rgb(${Math.floor(rgb.r * 0.53)}, ${Math.floor(rgb.g * 0.56)}, ${Math.floor(rgb.b * 0.59)})`, - backgroundImage: [ - `linear-gradient(to bottom, ${gradient})`, - `url(${this.user.cover_photo})` - ].join(', ') - } - } - }, - isOtherUser () { - return this.user.id !== this.$store.state.users.currentUser.id - }, - subscribeUrl () { - // eslint-disable-next-line no-undef - const serverUrl = new URL(this.user.statusnet_profile_url) - return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus` - }, - loggedIn () { - return this.$store.state.users.currentUser - }, - dailyAvg () { - const days = Math.ceil((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000)) - return Math.round(this.user.statuses_count / days) - }, - userHighlightType: { - get () { - const data = this.$store.state.config.highlight[this.user.screen_name] - return data && data.type || 'disabled' - }, - set (type) { - const data = this.$store.state.config.highlight[this.user.screen_name] - if (type !== 'disabled') { - this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: data && data.color || '#FFFFFF', type }) - } else { - this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: undefined }) - } - } - }, - userHighlightColor: { - get () { - const data = this.$store.state.config.highlight[this.user.screen_name] - return data && data.color - }, - set (color) { - this.$store.dispatch('setHighlight', { user: this.user.screen_name, color }) - } - }, - visibleRole () { - const validRole = (this.user.role === 'admin' || this.user.role === 'moderator') - const showRole = this.isOtherUser || this.user.show_role - - return validRole && showRole && this.user.role - } - }, - components: { - UserAvatar - }, - methods: { - followUser () { - const store = this.$store - this.followRequestInProgress = true - requestFollow(this.user, store).then(({sent}) => { - this.followRequestInProgress = false - this.followRequestSent = sent - }) - }, - unfollowUser () { - const store = this.$store - this.followRequestInProgress = true - requestUnfollow(this.user, store).then(() => { - this.followRequestInProgress = false - store.commit('removeStatus', { timeline: 'friends', userId: this.user.id }) - }) - }, - blockUser () { - const store = this.$store - store.state.api.backendInteractor.blockUser(this.user.id) - .then((blockedUser) => { - store.commit('addNewUsers', [blockedUser]) - store.commit('removeStatus', { timeline: 'friends', userId: this.user.id }) - store.commit('removeStatus', { timeline: 'public', userId: this.user.id }) - store.commit('removeStatus', { timeline: 'publicAndExternal', userId: this.user.id }) - }) - }, - unblockUser () { - const store = this.$store - store.state.api.backendInteractor.unblockUser(this.user.id) - .then((unblockedUser) => store.commit('addNewUsers', [unblockedUser])) - }, - toggleMute () { - const store = this.$store - store.commit('setMuted', {user: this.user, muted: !this.user.muted}) - store.state.api.backendInteractor.setUserMute(this.user) - }, - setProfileView (v) { - if (this.switcher) { - const store = this.$store - store.commit('setProfileView', { v }) - } - }, - linkClicked ({target}) { - if (target.tagName === 'SPAN') { - target = target.parentNode - } - if (target.tagName === 'A') { - window.open(target.href, '_blank') - } - }, - userProfileLink (user) { - return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) - } - } -} diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue @@ -1,417 +0,0 @@ -<template> -<div id="heading" class="profile-panel-background" :style="headingStyle"> - <div class="panel-heading text-center"> - <div class='user-info'> - <div class='container'> - <router-link :to="userProfileLink(user)"> - <UserAvatar :betterShadow="betterShadow" :src="user.profile_image_url_original"/> - </router-link> - <div class="name-and-screen-name"> - <div class="top-line"> - <div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div> - <div :title="user.name" class='user-name' v-else>{{user.name}}</div> - <router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser"> - <i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i> - </router-link> - <a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local"> - <i class="icon-link-ext usersettings"></i> - </a> - </div> - - <router-link class='user-screen-name' :to="userProfileLink(user)"> - <span class="handle">@{{user.screen_name}} - <span class="alert staff" v-if="!hideBio && !!visibleRole">{{visibleRole}}</span> - </span><span v-if="user.locked"><i class="icon icon-lock"></i></span> - <span v-if="!hideUserStatsLocal && !hideBio" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span> - </router-link> - </div> - </div> - <div class="user-meta"> - <div v-if="user.follows_you && loggedIn && isOtherUser" class="following"> - {{ $t('user_card.follows_you') }} - </div> - <div class="highlighter" v-if="isOtherUser && (loggedIn || !switcher)"> - <!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to --> - <input class="userHighlightText" type="text" :id="'userHighlightColorTx'+user.id" v-if="userHighlightType !== 'disabled'" v-model="userHighlightColor"/> - <input class="userHighlightCl" type="color" :id="'userHighlightColor'+user.id" v-if="userHighlightType !== 'disabled'" v-model="userHighlightColor"/> - <label for="style-switcher" class='userHighlightSel select'> - <select class="userHighlightSel" :id="'userHighlightSel'+user.id" v-model="userHighlightType"> - <option value="disabled">No highlight</option> - <option value="solid">Solid bg</option> - <option value="striped">Striped bg</option> - <option value="side">Side stripe</option> - </select> - <i class="icon-down-open"/> - </label> - </div> - </div> - <div v-if="isOtherUser" class="user-interactions"> - <div class="follow" v-if="loggedIn"> - <span v-if="user.following"> - <!--Following them!--> - <button @click="unfollowUser" class="pressed" :disabled="followRequestInProgress" :title="$t('user_card.follow_unfollow')"> - <template v-if="followRequestInProgress"> - {{ $t('user_card.follow_progress') }} - </template> - <template v-else> - {{ $t('user_card.following') }} - </template> - </button> - </span> - <span v-if="!user.following"> - <button @click="followUser" :disabled="followRequestInProgress" :title="followRequestSent ? $t('user_card.follow_again') : ''"> - <template v-if="followRequestInProgress"> - {{ $t('user_card.follow_progress') }} - </template> - <template v-else-if="followRequestSent"> - {{ $t('user_card.follow_sent') }} - </template> - <template v-else> - {{ $t('user_card.follow') }} - </template> - </button> - </span> - </div> - <div class='mute' v-if='isOtherUser && loggedIn'> - <span v-if='user.muted'> - <button @click="toggleMute" class="pressed"> - {{ $t('user_card.muted') }} - </button> - </span> - <span v-if='!user.muted'> - <button @click="toggleMute"> - {{ $t('user_card.mute') }} - </button> - </span> - </div> - <div class="remote-follow" v-if='!loggedIn && user.is_local'> - <form method="POST" :action='subscribeUrl'> - <input type="hidden" name="nickname" :value="user.screen_name"> - <input type="hidden" name="profile" value=""> - <button click="submit" class="remote-button"> - {{ $t('user_card.remote_follow') }} - </button> - </form> - </div> - <div class='block' v-if='isOtherUser && loggedIn'> - <span v-if='user.statusnet_blocking'> - <button @click="unblockUser" class="pressed"> - {{ $t('user_card.blocked') }} - </button> - </span> - <span v-if='!user.statusnet_blocking'> - <button @click="blockUser"> - {{ $t('user_card.block') }} - </button> - </span> - </div> - </div> - </div> - </div> - <div class="panel-body profile-panel-body" v-if="!hideBio"> - <div v-if="!hideUserStatsLocal && switcher" class="user-counts"> - <div class="user-count" v-on:click.prevent="setProfileView('statuses')"> - <h5>{{ $t('user_card.statuses') }}</h5> - <span>{{user.statuses_count}} <br></span> - </div> - <div class="user-count" v-on:click.prevent="setProfileView('friends')"> - <h5>{{ $t('user_card.followees') }}</h5> - <span>{{user.friends_count}}</span> - </div> - <div class="user-count" v-on:click.prevent="setProfileView('followers')"> - <h5>{{ $t('user_card.followers') }}</h5> - <span>{{user.followers_count}}</span> - </div> - </div> - <p @click.prevent="linkClicked" v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p> - <p v-else-if="!hideBio" class="profile-bio">{{ user.description }}</p> - </div> -</div> -</template> - -<script src="./user_card_content.js"></script> - -<style lang="scss"> -@import '../../_variables.scss'; - -.profile-panel-background { - background-size: cover; - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); - overflow: hidden; - - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - - .panel-heading { - padding: .5em 0; - text-align: center; - box-shadow: none; - } -} - -.profile-panel-body { - word-wrap: break-word; - background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%); - background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%); - - .profile-bio { - text-align: center; - } -} - -.user-info { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - padding: 0 26px; - - .container { - padding: 16px 0 6px; - display: flex; - max-height: 56px; - - .avatar { - flex: 1 0 100%; - width: 56px; - height: 56px; - box-shadow: 0px 1px 8px rgba(0,0,0,0.75); - box-shadow: var(--avatarShadow); - object-fit: cover; - } - } - - &:hover .animated.avatar { - canvas { - display: none; - } - img { - visibility: visible; - } - } - - .usersettings { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - opacity: .8; - } - - .name-and-screen-name { - display: block; - margin-left: 0.6em; - text-align: left; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1 1 0; - // This is so that text doesn't get overlapped by avatar's shadow if it has - // big one - z-index: 1; - - img { - width: 26px; - height: 26px; - vertical-align: middle; - object-fit: contain - } - .top-line { - display: flex; - } - } - - .user-name { - text-overflow: ellipsis; - overflow: hidden; - flex: 1 1 auto; - margin-right: 1em; - font-size: 15px; - - img { - object-fit: contain; - height: 16px; - width: 16px; - vertical-align: middle; - } - } - - .user-screen-name { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - display: inline-block; - font-weight: light; - font-size: 15px; - padding-right: 0.1em; - width: 100%; - display: flex; - - .dailyAvg { - min-width: 1px; - flex: 0 0 auto; - margin-left: 1em; - font-size: 0.7em; - color: $fallback--text; - color: var(--text, $fallback--text); - } - - .handle { - min-width: 1px; - flex: 0 1 auto; - text-overflow: ellipsis; - overflow: hidden; - } - - // TODO use proper colors - .staff { - text-transform: capitalize; - color: $fallback--text; - color: var(--btnText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btn, $fallback--fg); - } - } - - .user-meta { - margin-bottom: .15em; - display: flex; - align-items: baseline; - font-size: 14px; - line-height: 22px; - flex-wrap: wrap; - - .following { - flex: 1 0 auto; - margin: 0; - margin-bottom: .25em; - text-align: left; - } - - .highlighter { - flex: 0 1 auto; - display: flex; - flex-wrap: wrap; - margin-right: -.5em; - align-self: start; - - .userHighlightCl { - padding: 2px 10px; - flex: 1 0 auto; - } - - .userHighlightSel, - .userHighlightSel.select { - padding-top: 0; - padding-bottom: 0; - flex: 1 0 auto; - } - .userHighlightSel.select i { - line-height: 22px; - } - - .userHighlightText { - width: 70px; - flex: 1 0 auto; - } - - .userHighlightCl, - .userHighlightText, - .userHighlightSel, - .userHighlightSel.select { - height: 22px; - vertical-align: top; - margin-right: .5em; - margin-bottom: .25em; - } - } - } - .user-interactions { - display: flex; - flex-flow: row wrap; - justify-content: space-between; - - margin-right: -.75em; - - div { - flex: 1 0 0; - margin-right: .75em; - margin-bottom: .6em; - white-space: nowrap; - } - - .mute { - max-width: 220px; - min-height: 28px; - } - - .remote-follow { - max-width: 220px; - min-height: 28px; - } - - .follow { - max-width: 220px; - min-height: 28px; - } - - button { - width: 100%; - height: 100%; - margin: 0; - } - - .remote-button { - height: 28px !important; - width: 92%; - } - - .pressed { - border-bottom-color: rgba(255, 255, 255, 0.2); - border-top-color: rgba(0, 0, 0, 0.2); - } - } -} - -.user-counts { - display: flex; - line-height:16px; - padding: .5em 1.5em 0em 1.5em; - text-align: center; - justify-content: space-between; - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - flex-wrap: wrap; -} - -.user-count { - flex: 1 0 auto; - padding: .5em 0 .5em 0; - margin: 0 .5em; - - h5 { - font-size:1em; - font-weight: bolder; - margin: 0 0 0.25em; - } - a { - text-decoration: none; - } -} - -.usercard { - width: fill-available; - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); - border-style: solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - border-width: 1px; - overflow: hidden; - - .panel-heading { - background: transparent; - flex-direction: column; - align-items: stretch; - } - - p { - margin-bottom: 0; - } -} -</style> diff --git a/src/components/user_panel/user_panel.js b/src/components/user_panel/user_panel.js @@ -1,6 +1,6 @@ import LoginForm from '../login_form/login_form.vue' import PostStatusForm from '../post_status_form/post_status_form.vue' -import UserCardContent from '../user_card_content/user_card_content.vue' +import UserCard from '../user_card/user_card.vue' const UserPanel = { computed: { @@ -9,7 +9,7 @@ const UserPanel = { components: { LoginForm, PostStatusForm, - UserCardContent + UserCard } } diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue @@ -1,7 +1,7 @@ <template> <div class="user-panel"> <div v-if='user' class="panel panel-default" style="overflow: visible;"> - <user-card-content :user="user" :switcher="false" :hideBio="true"></user-card-content> + <UserCard :user="user" :hideBio="true" rounded="top"/> <div class="panel-footer"> <post-status-form v-if='user'></post-status-form> </div> @@ -11,13 +11,3 @@ </template> <script src="./user_panel.js"></script> - -<style lang="scss"> -.user-panel { - .profile-panel-background .panel-heading { - background: transparent; - flex-direction: column; - align-items: stretch; - } -} -</style> diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js @@ -1,6 +1,6 @@ import { compose } from 'vue-compose' import get from 'lodash/get' -import UserCardContent from '../user_card_content/user_card_content.vue' +import UserCard from '../user_card/user_card.vue' import FollowCard from '../follow_card/follow_card.vue' import Timeline from '../timeline/timeline.vue' import withLoadMore from '../../hocs/with_load_more/with_load_more' @@ -147,7 +147,7 @@ const UserProfile = { } }, components: { - UserCardContent, + UserCard, Timeline, FollowerList, FriendList diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue @@ -1,11 +1,7 @@ <template> <div> <div v-if="user.id" class="user-profile panel panel-default"> - <user-card-content - :user="user" - :switcher="true" - :selected="timeline.viewing" - /> + <UserCard :user="user" :switcher="true" :selected="timeline.viewing" rounded="top"/> <tab-switcher :renderOnlyFocused="true" ref="tabSwitcher"> <Timeline :label="$t('user_card.statuses')" @@ -64,11 +60,6 @@ flex: 2; flex-basis: 500px; - .profile-panel-background .panel-heading { - background: transparent; - flex-direction: column; - align-items: stretch; - } .userlist-placeholder { display: flex; justify-content: center; diff --git a/test/unit/specs/boot/routes.spec.js b/test/unit/specs/boot/routes.spec.js @@ -24,7 +24,7 @@ describe('routes', () => { const matchedComponents = router.getMatchedComponents() - expect(matchedComponents[0].components.hasOwnProperty('UserCardContent')).to.eql(true) + expect(matchedComponents[0].components.hasOwnProperty('UserCard')).to.eql(true) }) it('user\'s profile at /users', () => { @@ -32,6 +32,6 @@ describe('routes', () => { const matchedComponents = router.getMatchedComponents() - expect(matchedComponents[0].components.hasOwnProperty('UserCardContent')).to.eql(true) + expect(matchedComponents[0].components.hasOwnProperty('UserCard')).to.eql(true) }) })