Merge branch 'feature/replace-panel-switcher' into 'develop'

Mobile side drawer

See merge request pleroma/pleroma-fe!443
This commit is contained in:
Shpuld Shpludson 2018-12-31 17:35:31 +00:00
commit 76cfb15b3c
27 changed files with 488 additions and 88 deletions

View file

@ -6,6 +6,8 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
import FeaturesPanel from './components/features_panel/features_panel.vue' import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import ChatPanel from './components/chat_panel/chat_panel.vue' import ChatPanel from './components/chat_panel/chat_panel.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue'
import { unseenNotificationsFromStore } from './services/notification_utils/notification_utils'
export default { export default {
name: 'app', name: 'app',
@ -17,7 +19,8 @@ export default {
InstanceSpecificPanel, InstanceSpecificPanel,
FeaturesPanel, FeaturesPanel,
WhoToFollowPanel, WhoToFollowPanel,
ChatPanel ChatPanel,
SideDrawer
}, },
data: () => ({ data: () => ({
mobileActivePanel: 'timeline', mobileActivePanel: 'timeline',
@ -70,12 +73,15 @@ export default {
sitename () { return this.$store.state.instance.name }, sitename () { return this.$store.state.instance.name },
chat () { return this.$store.state.chat.channel.state === 'joined' }, chat () { return this.$store.state.chat.channel.state === 'joined' },
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
showInstanceSpecificPanel () { return this.$store.state.instance.showInstanceSpecificPanel } showInstanceSpecificPanel () { return this.$store.state.instance.showInstanceSpecificPanel },
unseenNotifications () {
return unseenNotificationsFromStore(this.$store)
},
unseenNotificationsCount () {
return this.unseenNotifications.length
}
}, },
methods: { methods: {
activatePanel (panelName) {
this.mobileActivePanel = panelName
},
scrollToTop () { scrollToTop () {
window.scrollTo(0, 0) window.scrollTo(0, 0)
}, },
@ -85,6 +91,9 @@ export default {
}, },
onFinderToggled (hidden) { onFinderToggled (hidden) {
this.finderHidden = hidden this.finderHidden = hidden
},
toggleMobileSidebar () {
this.$refs.sideDrawer.toggleDrawer()
} }
} }
} }

View file

@ -473,6 +473,24 @@ nav {
} }
} }
.menu-button {
display: none;
position: relative;
}
.alert-dot {
border-radius: 100%;
height: 8px;
width: 8px;
position: absolute;
left: calc(50% - 4px);
top: calc(50% - 4px);
margin-left: 6px;
margin-top: -6px;
background-color: $fallback--cRed;
background-color: var(--badgeNotification, $fallback--cRed);
}
.fade-enter-active, .fade-leave-active { .fade-enter-active, .fade-leave-active {
transition: opacity .2s transition: opacity .2s
} }
@ -524,9 +542,6 @@ nav {
.back-button { .back-button {
display: none; display: none;
} }
.site-name {
padding-left: 20px;
}
} }
.sidebar-bounds { .sidebar-bounds {
@ -665,4 +680,9 @@ nav {
max-width: 4em; max-width: 4em;
} }
} }
.menu-button {
display: block;
margin-right: 0.8em;
}
} }

View file

@ -7,44 +7,42 @@
</div> </div>
<div class='inner-nav'> <div class='inner-nav'>
<div class='item'> <div class='item'>
<router-link class="back-button" @click.native="activatePanel('timeline')" :to="{ name: 'root' }" active-class="hidden"> <a href="#" class="menu-button" @click.stop.prevent="toggleMobileSidebar()">
<i class="icon-left-open" :title="$t('nav.back')"></i> <i class="button-icon icon-menu"></i>
</router-link> <div class="alert-dot" v-if="unseenNotificationsCount"></div>
</a>
<router-link class="site-name" :to="{ name: 'root' }" active-class="home">{{sitename}}</router-link> <router-link class="site-name" :to="{ name: 'root' }" active-class="home">{{sitename}}</router-link>
</div> </div>
<div class='item right'> <div class='item right'>
<user-finder class="button-icon nav-icon" @toggled="onFinderToggled"></user-finder> <user-finder class="button-icon nav-icon mobile-hidden" @toggled="onFinderToggled"></user-finder>
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'settings'}"><i class="button-icon icon-cog nav-icon" :title="$t('nav.preferences')"></i></router-link> <router-link class="mobile-hidden" :to="{ name: 'settings'}"><i class="button-icon icon-cog nav-icon" :title="$t('nav.preferences')"></i></router-link>
<a href="#" v-if="currentUser" @click.prevent="logout"><i class="button-icon icon-logout nav-icon" :title="$t('login.logout')"></i></a> <a href="#" class="mobile-hidden" v-if="currentUser" @click.prevent="logout"><i class="button-icon icon-logout nav-icon" :title="$t('login.logout')"></i></a>
</div> </div>
</div> </div>
</nav> </nav>
<div class="container" id="content"> <div v-if="" class="container" id="content">
<div class="panel-switcher"> <side-drawer ref="sideDrawer" :logout="logout"></side-drawer>
<button @click="activatePanel('sidebar')">Sidebar</button> <div class="sidebar-flexer mobile-hidden">
<button @click="activatePanel('timeline')">Timeline</button>
</div>
<div class="sidebar-flexer" :class="{ 'mobile-hidden': mobileActivePanel != 'sidebar'}">
<div class="sidebar-bounds"> <div class="sidebar-bounds">
<div class="sidebar-scroller"> <div class="sidebar-scroller">
<div class="sidebar"> <div class="sidebar">
<user-panel :activatePanel="activatePanel"></user-panel> <user-panel></user-panel>
<nav-panel :activatePanel="activatePanel"></nav-panel> <nav-panel></nav-panel>
<instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel> <instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
<features-panel v-if="!currentUser"></features-panel> <features-panel v-if="!currentUser"></features-panel>
<who-to-follow-panel v-if="currentUser && suggestionsEnabled"></who-to-follow-panel> <who-to-follow-panel v-if="currentUser && suggestionsEnabled"></who-to-follow-panel>
<notifications :activatePanel="activatePanel" v-if="currentUser"></notifications> <notifications v-if="currentUser"></notifications>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="main" :class="{ 'mobile-hidden': mobileActivePanel != 'timeline' }"> <div class="main">
<transition name="fade"> <transition name="fade">
<router-view></router-view> <router-view></router-view>
</transition> </transition>
</div> </div>
</div> </div>
<chat-panel v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel> <chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel>
</div> </div>
</template> </template>

View file

@ -12,6 +12,10 @@ import UserSettings from 'components/user_settings/user_settings.vue'
import FollowRequests from 'components/follow_requests/follow_requests.vue' import FollowRequests from 'components/follow_requests/follow_requests.vue'
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue' import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
import UserSearch from 'components/user_search/user_search.vue' import UserSearch from 'components/user_search/user_search.vue'
import Notifications from 'components/notifications/notifications.vue'
import UserPanel from 'components/user_panel/user_panel.vue'
import LoginForm from 'components/login_form/login_form.vue'
import ChatPanel from 'components/chat_panel/chat_panel.vue'
export default (store) => { export default (store) => {
return [ return [
@ -36,6 +40,10 @@ export default (store) => {
{ name: 'registration', path: '/registration/:token', component: Registration }, { name: 'registration', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
{ name: 'user-settings', path: '/user-settings', component: UserSettings }, { name: 'user-settings', path: '/user-settings', component: UserSettings },
{ name: 'notifications', path: '/:username/notifications', component: Notifications },
{ name: 'new-status', path: '/:username/new-status', component: UserPanel },
{ name: 'login', path: '/login', component: LoginForm },
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }, { name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
{ name: 'user-search', path: '/user-search', component: UserSearch, props: (route) => ({ query: route.query.query }) }, { name: 'user-search', path: '/user-search', component: UserSearch, props: (route) => ({ query: route.query.query }) },
{ name: 'user-profile', path: '/(users/)?:name', component: UserProfile } { name: 'user-profile', path: '/(users/)?:name', component: UserProfile }

View file

@ -1,6 +1,7 @@
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'
const chatPanel = { const chatPanel = {
props: [ 'floating' ],
data () { data () {
return { return {
currentMessage: '', currentMessage: '',
@ -22,7 +23,7 @@ const chatPanel = {
this.collapsed = !this.collapsed this.collapsed = !this.collapsed
}, },
userProfileLink (user) { userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(user.id, user.username, this.$store.state.instance.restrictedNicknames)
} }
} }
} }

View file

@ -1,10 +1,10 @@
<template> <template>
<div class="chat-panel" v-if="!this.collapsed"> <div class="chat-panel" v-if="!this.collapsed || !this.floating">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading timeline-heading chat-heading" @click.stop.prevent="togglePanel"> <div class="panel-heading timeline-heading" :class="{ 'chat-heading': floating }" @click.stop.prevent="togglePanel">
<div class="title"> <div class="title">
{{$t('chat.title')}} {{$t('chat.title')}}
<i class="icon-cancel" style="float: right;"></i> <i class="icon-cancel" style="float: right;" v-if="floating"></i>
</div> </div>
</div> </div>
<div class="chat-window" v-chat-scroll> <div class="chat-window" v-chat-scroll>
@ -52,6 +52,7 @@
right: 0px; right: 0px;
bottom: 0px; bottom: 0px;
z-index: 1000; z-index: 1000;
max-width: 25em;
} }
.chat-heading { .chat-heading {
@ -63,10 +64,13 @@
} }
.chat-window { .chat-window {
width: 345px;
max-height: 40vh;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
max-height: 20em;
}
.chat-window-container {
height: 100%;
} }
.chat-message { .chat-message {

View file

@ -1,5 +1,4 @@
const NavPanel = { const NavPanel = {
props: [ 'activatePanel' ],
computed: { computed: {
currentUser () { currentUser () {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser

View file

@ -3,32 +3,32 @@
<div class="panel panel-default"> <div class="panel panel-default">
<ul> <ul>
<li v-if='currentUser'> <li v-if='currentUser'>
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'friends' }"> <router-link :to="{ name: 'friends' }">
{{ $t("nav.timeline") }} {{ $t("nav.timeline") }}
</router-link> </router-link>
</li> </li>
<li v-if='currentUser'> <li v-if='currentUser'>
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'mentions', params: { username: currentUser.screen_name } }"> <router-link :to="{ name: 'mentions', params: { username: currentUser.screen_name } }">
{{ $t("nav.mentions") }} {{ $t("nav.mentions") }}
</router-link> </router-link>
</li> </li>
<li v-if='currentUser'> <li v-if='currentUser'>
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'dms', params: { username: currentUser.screen_name } }"> <router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
{{ $t("nav.dms") }} {{ $t("nav.dms") }}
</router-link> </router-link>
</li> </li>
<li v-if='currentUser && currentUser.locked'> <li v-if='currentUser && currentUser.locked'>
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'friend-requests' }"> <router-link :to="{ name: 'friend-requests' }">
{{ $t("nav.friend_requests") }} {{ $t("nav.friend_requests") }}
</router-link> </router-link>
</li> </li>
<li> <li>
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'public-timeline' }"> <router-link :to="{ name: 'public-timeline' }">
{{ $t("nav.public_tl") }} {{ $t("nav.public_tl") }}
</router-link> </router-link>
</li> </li>
<li> <li>
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'public-external-timeline' }"> <router-link :to="{ name: 'public-external-timeline' }">
{{ $t("nav.twkn") }} {{ $t("nav.twkn") }}
</router-link> </router-link>
</li> </li>

View file

@ -11,10 +11,7 @@ const Notification = {
betterShadow: this.$store.state.interface.browserSupport.cssFilter betterShadow: this.$store.state.interface.browserSupport.cssFilter
} }
}, },
props: [ props: [ 'notification' ],
'notification',
'activatePanel'
],
components: { components: {
Status, StillImage, UserCardContent Status, StillImage, UserCardContent
}, },

View file

@ -1,5 +1,5 @@
<template> <template>
<status :activatePanel="activatePanel" v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status> <status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else> <div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
<a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded"> <a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
<StillImage class='avatar-compact' :class="{'better-shadow': betterShadow}" :src="notification.action.user.profile_image_url_original"/> <StillImage class='avatar-compact' :class="{'better-shadow': betterShadow}" :src="notification.action.user.profile_image_url_original"/>
@ -25,15 +25,15 @@
<small>{{$t('notifications.followed_you')}}</small> <small>{{$t('notifications.followed_you')}}</small>
</span> </span>
</div> </div>
<small class="timeago"><router-link @click.native="activatePanel('timeline')" v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small> <small class="timeago"><router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
</span> </span>
<div class="follow-text" v-if="notification.type === 'follow'"> <div class="follow-text" v-if="notification.type === 'follow'">
<router-link @click.native="activatePanel('timeline')" :to="userProfileLink(notification.action.user)"> <router-link :to="userProfileLink(notification.action.user)">
@{{notification.action.user.screen_name}} @{{notification.action.user.screen_name}}
</router-link> </router-link>
</div> </div>
<template v-else> <template v-else>
<status :activatePanel="activatePanel" class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status> <status class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
</template> </template>
</div> </div>
</div> </div>

View file

@ -1,10 +1,12 @@
import Notification from '../notification/notification.vue' import Notification from '../notification/notification.vue'
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js' import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
import {
import { sortBy, filter } from 'lodash' notificationsFromStore,
visibleNotificationsFromStore,
unseenNotificationsFromStore
} from '../../services/notification_utils/notification_utils.js'
const Notifications = { const Notifications = {
props: [ 'activatePanel' ],
created () { created () {
const store = this.$store const store = this.$store
const credentials = store.state.users.currentUser.credentials const credentials = store.state.users.currentUser.credentials
@ -12,28 +14,17 @@ const Notifications = {
notificationsFetcher.startFetching({ store, credentials }) notificationsFetcher.startFetching({ store, credentials })
}, },
computed: { computed: {
visibleTypes () {
return [
this.$store.state.config.notificationVisibility.likes && 'like',
this.$store.state.config.notificationVisibility.mentions && 'mention',
this.$store.state.config.notificationVisibility.repeats && 'repeat',
this.$store.state.config.notificationVisibility.follows && 'follow'
].filter(_ => _)
},
notifications () { notifications () {
return this.$store.state.statuses.notifications.data return notificationsFromStore(this.$store)
}, },
error () { error () {
return this.$store.state.statuses.notifications.error return this.$store.state.statuses.notifications.error
}, },
unseenNotifications () { unseenNotifications () {
return filter(this.visibleNotifications, ({seen}) => !seen) return unseenNotificationsFromStore(this.$store)
}, },
visibleNotifications () { visibleNotifications () {
// Don't know why, but sortBy([seen, -action.id]) doesn't work. return visibleNotificationsFromStore(this.$store)
let sortedNotifications = sortBy(this.notifications, ({action}) => -action.id)
sortedNotifications = sortBy(sortedNotifications, 'seen')
return sortedNotifications.filter((notification) => this.visibleTypes.includes(notification.type))
}, },
unseenCount () { unseenCount () {
return this.unseenNotifications.length return this.unseenNotifications.length

View file

@ -14,7 +14,7 @@
<div class="panel-body"> <div class="panel-body">
<div v-for="notification in visibleNotifications" :key="notification.action.id" class="notification" :class='{"unseen": !notification.seen}'> <div v-for="notification in visibleNotifications" :key="notification.action.id" class="notification" :class='{"unseen": !notification.seen}'>
<div class="notification-overlay"></div> <div class="notification-overlay"></div>
<notification :activatePanel="activatePanel" :notification="notification"></notification> <notification :notification="notification"></notification>
</div> </div>
</div> </div>
<div class="panel-footer"> <div class="panel-footer">

View file

@ -0,0 +1,48 @@
import UserCardContent from '../user_card_content/user_card_content.vue'
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
// TODO: separate touch gesture stuff into their own utils if more components want them
const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY])
const SideDrawer = {
props: [ 'logout' ],
data: () => ({
closed: true,
touchCoord: [0, 0]
}),
components: { UserCardContent },
computed: {
currentUser () {
return this.$store.state.users.currentUser
},
chat () { return this.$store.state.chat.channel.state === 'joined' },
unseenNotifications () {
return unseenNotificationsFromStore(this.$store)
},
unseenNotificationsCount () {
return this.unseenNotifications.length
}
},
methods: {
toggleDrawer () {
this.closed = !this.closed
},
doLogout () {
this.logout()
this.toggleDrawer()
},
touchStart (e) {
this.touchCoord = touchEventCoord(e)
},
touchMove (e) {
const delta = deltaCoord(this.touchCoord, touchEventCoord(e))
if (delta[0] < -30 && Math.abs(delta[1]) < Math.abs(delta[0]) && !this.closed) {
this.toggleDrawer()
}
}
}
}
export default SideDrawer

View file

@ -0,0 +1,189 @@
<template>
<div class="side-drawer-container"
:class="{ 'side-drawer-container-closed': closed, 'side-drawer-container-open': !closed }"
>
<div class="side-drawer"
:class="{'side-drawer-closed': closed}"
@touchstart="touchStart"
@touchmove="touchMove"
>
<div class="side-drawer-heading" @click="toggleDrawer">
<user-card-content :user="currentUser" :switcher="false" :hideBio="true" v-if="currentUser">
</user-card-content>
</div>
<ul>
<li v-if="currentUser" @click="toggleDrawer">
<router-link :to="{ name: 'new-status', params: { username: currentUser.screen_name } }">
{{ $t("post_status.new_status") }}
</router-link>
</li>
<li v-else @click="toggleDrawer">
<router-link :to="{ name: 'login' }">
{{ $t("login.login") }}
</router-link>
</li>
<li v-if="currentUser" @click="toggleDrawer">
<router-link :to="{ name: 'notifications', params: { username: currentUser.screen_name } }">
{{ $t("notifications.notifications") }} {{ unseenNotificationsCount > 0 ? `(${unseenNotificationsCount})` : '' }}
</router-link>
</li>
<li v-if="currentUser" @click="toggleDrawer">
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
{{ $t("nav.dms") }}
</router-link>
</li>
</ul>
<ul>
<li v-if="currentUser" @click="toggleDrawer">
<router-link :to="{ name: 'friends' }">
{{ $t("nav.timeline") }}
</router-link>
</li>
<li v-if="currentUser && currentUser.locked" @click="toggleDrawer">
<router-link to='/friend-requests'>
{{ $t("nav.friend_requests") }}
</router-link>
</li>
<li @click="toggleDrawer">
<router-link to='/main/public'>
{{ $t("nav.public_tl") }}
</router-link>
</li>
<li @click="toggleDrawer">
<router-link to='/main/all'>
{{ $t("nav.twkn") }}
</router-link>
</li>
<li v-if="currentUser && chat" @click="toggleDrawer">
<router-link :to="{ name: 'chat' }">
{{ $t("nav.chat") }}
</router-link>
</li>
</ul>
<ul>
<li @click="toggleDrawer">
<router-link :to="{ name: 'user-search'}">
{{ $t("nav.user_search") }}
</router-link>
</li>
<li @click="toggleDrawer">
<router-link :to="{ name: 'settings'}">
{{ $t("settings.settings") }}
</router-link>
</li>
<li v-if="currentUser" @click="toggleDrawer">
<a @click="doLogout" href="#">
{{ $t("login.logout") }}
</a>
</li>
</ul>
</div>
<div class="side-drawer-click-outside"
@click.stop.prevent="toggleDrawer"
:class="{'side-drawer-click-outside-closed': closed}"
></div>
</div>
</template>
<script src="./side_drawer.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
.side-drawer-container {
position: fixed;
z-index: 1000;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: stretch;
}
.side-drawer-container-open {
transition-delay: 0.0s;
transition-property: left;
}
.side-drawer-container-closed {
left: -100%;
transition-delay: 0.5s;
transition-property: left;
}
.side-drawer-click-outside {
flex: 1 1 100%;
}
.side-drawer {
overflow-x: hidden;
transition: 0.5s;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
margin: 0 0 0 -100px;
padding: 0 0 1em 100px;
width: 80%;
max-width: 20em;
flex: 0 0 80%;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
box-shadow: var(--panelShadow);
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
}
.side-drawer-click-outside-closed {
flex: 0 0 0;
}
.side-drawer-closed {
margin: 0 0 0 calc(-100% - 100px);
}
.side-drawer-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
display: flex;
min-height: 7em;
padding: 0;
margin: 0;
.profile-panel-background {
border-radius: 0;
.panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
}
}
.side-drawer ul {
list-style: none;
margin: 0;
padding: 0;
border-bottom: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
margin: 0.2em 0;
}
.side-drawer ul:last-child {
border: 0;
}
.side-drawer li {
padding: 0;
a {
display: block;
padding: 0.5em 0.85em;
&:hover {
background-color: $fallback--lightBg;
background-color: var(--lightBg, $fallback--lightBg);
}
}
}
</style>

View file

@ -21,8 +21,7 @@ const Status = {
'replies', 'replies',
'noReplyLinks', 'noReplyLinks',
'noHeading', 'noHeading',
'inlineExpanded', 'inlineExpanded'
'activatePanel'
], ],
data () { data () {
return { return {

View file

@ -3,7 +3,7 @@
<template v-if="muted && !noReplyLinks"> <template v-if="muted && !noReplyLinks">
<div class="media status container muted"> <div class="media status container muted">
<small> <small>
<router-link @click.native="activatePanel('timeline')" :to="userProfileLink(status.user.id, status.user.screen_name)"> <router-link :to="userProfileLink(status.user.id, status.user.screen_name)">
{{status.user.screen_name}} {{status.user.screen_name}}
</router-link> </router-link>
</small> </small>
@ -38,12 +38,12 @@
<h4 class="user-name" v-if="status.user.name_html" v-html="status.user.name_html"></h4> <h4 class="user-name" v-if="status.user.name_html" v-html="status.user.name_html"></h4>
<h4 class="user-name" v-else>{{status.user.name}}</h4> <h4 class="user-name" v-else>{{status.user.name}}</h4>
<span class="links"> <span class="links">
<router-link @click.native="activatePanel('timeline')" :to="userProfileLink(status.user.id, status.user.screen_name)"> <router-link :to="userProfileLink(status.user.id, status.user.screen_name)">
{{status.user.screen_name}} {{status.user.screen_name}}
</router-link> </router-link>
<span v-if="status.in_reply_to_screen_name" class="faint reply-info"> <span v-if="status.in_reply_to_screen_name" class="faint reply-info">
<i class="icon-right-open"></i> <i class="icon-right-open"></i>
<router-link @click.native="activatePanel('timeline')" :to="userProfileLink(status.in_reply_to_user_id, status.in_reply_to_screen_name)"> <router-link :to="userProfileLink(status.in_reply_to_user_id, status.in_reply_to_screen_name)">
{{status.in_reply_to_screen_name}} {{status.in_reply_to_screen_name}}
</router-link> </router-link>
</span> </span>
@ -60,7 +60,7 @@
</h4> </h4>
</div> </div>
<div class="media-heading-right"> <div class="media-heading-right">
<router-link class="timeago" @click.native="activatePanel('timeline')" :to="{ name: 'conversation', params: { id: status.id } }"> <router-link class="timeago" :to="{ name: 'conversation', params: { id: status.id } }">
<timeago :since="status.created_at" :auto-update="60"></timeago> <timeago :since="status.created_at" :auto-update="60"></timeago>
</router-link> </router-link>
<div class="button-icon visibility-icon" v-if="status.visibility"> <div class="button-icon visibility-icon" v-if="status.visibility">
@ -79,7 +79,7 @@
</div> </div>
<div v-if="showPreview" class="status-preview-container"> <div v-if="showPreview" class="status-preview-container">
<status :activatePanel="activatePanel" class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status> <status class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status>
<div class="status-preview status-preview-loading" v-else> <div class="status-preview status-preview-loading" v-else>
<i class="icon-spin4 animate-spin"></i> <i class="icon-spin4 animate-spin"></i>
</div> </div>

View file

@ -3,7 +3,7 @@ import { hex2rgb } from '../../services/color_convert/color_convert.js'
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'
export default { export default {
props: [ 'user', 'switcher', 'selected', 'hideBio', 'activatePanel' ], props: [ 'user', 'switcher', 'selected', 'hideBio' ],
data () { data () {
return { return {
followRequestInProgress: false, followRequestInProgress: false,

View file

@ -3,14 +3,14 @@
<div class="panel-heading text-center"> <div class="panel-heading text-center">
<div class='user-info'> <div class='user-info'>
<div class='container'> <div class='container'>
<router-link @click.native="activatePanel && activatePanel('timeline')" :to="userProfileLink(user)"> <router-link :to="userProfileLink(user)">
<StillImage class="avatar" :class='{ "better-shadow": betterShadow }' :src="user.profile_image_url_original"/> <StillImage class="avatar" :class='{ "better-shadow": betterShadow }' :src="user.profile_image_url_original"/>
</router-link> </router-link>
<div class="name-and-screen-name"> <div class="name-and-screen-name">
<div class="top-line"> <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-if="user.name_html" v-html="user.name_html"></div>
<div :title="user.name" class='user-name' v-else>{{user.name}}</div> <div :title="user.name" class='user-name' v-else>{{user.name}}</div>
<router-link @click.native="activatePanel && activatePanel('timeline')" :to="{ name: 'user-settings' }" v-if="!isOtherUser"> <router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser">
<i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i> <i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i>
</router-link> </router-link>
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser"> <a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser">
@ -18,7 +18,7 @@
</a> </a>
</div> </div>
<router-link @click.native="activatePanel && activatePanel('timeline')" class='user-screen-name' :to="userProfileLink(user)"> <router-link class='user-screen-name' :to="userProfileLink(user)">
<span class="handle">@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span> <span class="handle">@{{user.screen_name}}</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> <span v-if="!hideUserStatsLocal && !hideBio" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
</router-link> </router-link>

View file

@ -1,14 +1,16 @@
<template> <template>
<div class="user-finder-container"> <div>
<i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" /> <div class="user-finder-container">
<a href="#" v-if="hidden" :title="$t('finder.find_user')"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a> <i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
<template v-else> <a href="#" v-if="hidden" :title="$t('finder.find_user')"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a>
<input class="user-finder-input" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/> <template v-else>
<button class="btn search-button" @click="findUser(username)"> <input class="user-finder-input" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/>
<i class="icon-search"/> <button class="btn search-button" @click="findUser(username)">
</button> <i class="icon-search"/>
<i class="button-icon icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/> </button>
</template> <i class="button-icon icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/>
</template>
</div>
</div> </div>
</template> </template>

View file

@ -3,7 +3,6 @@ import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCardContent from '../user_card_content/user_card_content.vue' import UserCardContent from '../user_card_content/user_card_content.vue'
const UserPanel = { const UserPanel = {
props: [ 'activatePanel' ],
computed: { computed: {
user () { return this.$store.state.users.currentUser } user () { return this.$store.state.users.currentUser }
}, },

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="user-panel"> <div class="user-panel">
<div v-if='user' class="panel panel-default" style="overflow: visible;"> <div v-if='user' class="panel panel-default" style="overflow: visible;">
<user-card-content :activatePanel="activatePanel" :user="user" :switcher="false" :hideBio="true"></user-card-content> <user-card-content :user="user" :switcher="false" :hideBio="true"></user-card-content>
<div class="panel-footer"> <div class="panel-footer">
<post-status-form v-if='user'></post-status-form> <post-status-form v-if='user'></post-status-form>
</div> </div>

View file

@ -9,6 +9,7 @@ const userSearch = {
], ],
data () { data () {
return { return {
username: '',
users: [] users: []
} }
}, },
@ -21,7 +22,14 @@ const userSearch = {
} }
}, },
methods: { methods: {
newQuery (query) {
this.$router.push({ name: 'user-search', query: { query } })
},
search (query) { search (query) {
if (!query) {
this.users = []
return
}
userSearchApi.search({query, store: this.$store}) userSearchApi.search({query, store: this.$store})
.then((res) => { .then((res) => {
this.users = res this.users = res

View file

@ -3,6 +3,12 @@
<div class="panel-heading"> <div class="panel-heading">
{{$t('nav.user_search')}} {{$t('nav.user_search')}}
</div> </div>
<div class="user-search-input-container">
<input class="user-finder-input" @keyup.enter="newQuery(username)" v-model="username" :placeholder="$t('finder.find_user')"/>
<button class="btn search-button" @click="newQuery(username)">
<i class="icon-search"/>
</button>
</div>
<div class="panel-body"> <div class="panel-body">
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card> <user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card>
</div> </div>
@ -10,3 +16,15 @@
</template> </template>
<script src="./user_search.js"></script> <script src="./user_search.js"></script>
<style lang="scss">
.user-search-input-container {
margin: 0.5em;
display: flex;
justify-content: center;
.search-button {
margin-left: 0.5em;
}
}
</style>

View file

@ -50,6 +50,7 @@
"repeated_you": "repeated your status" "repeated_you": "repeated your status"
}, },
"post_status": { "post_status": {
"new_status": "Post new status",
"account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.", "account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.",
"account_not_locked_warning_link": "locked", "account_not_locked_warning_link": "locked",
"attachments_sensitive": "Mark attachments as sensitive", "attachments_sensitive": "Mark attachments as sensitive",

View file

@ -0,0 +1,20 @@
import { filter, sortBy } from 'lodash'
export const notificationsFromStore = store => store.state.statuses.notifications.data
export const visibleTypes = store => ([
store.state.config.notificationVisibility.likes && 'like',
store.state.config.notificationVisibility.mentions && 'mention',
store.state.config.notificationVisibility.repeats && 'repeat',
store.state.config.notificationVisibility.follows && 'follow'
].filter(_ => _))
export const visibleNotificationsFromStore = store => {
// Don't know why, but sortBy([seen, -action.id]) doesn't work.
let sortedNotifications = sortBy(notificationsFromStore(store), ({action}) => -action.id)
sortedNotifications = sortBy(sortedNotifications, 'seen')
return sortedNotifications.filter((notification) => visibleTypes(store).includes(notification.type))
}
export const unseenNotificationsFromStore = store =>
filter(visibleNotificationsFromStore(store), ({seen}) => !seen)

View file

@ -8,7 +8,8 @@ const localVue = createLocalVue()
localVue.use(Vuex) localVue.use(Vuex)
const mutations = { const mutations = {
clearTimeline: () => {} clearTimeline: () => {},
setError: () => {}
} }
const testGetters = { const testGetters = {

View file

@ -0,0 +1,88 @@
import * as NotificationUtils from 'src/services/notification_utils/notification_utils.js'
describe('NotificationUtils', () => {
describe('visibleNotificationsFromStore', () => {
it('should return sorted notifications with configured types', () => {
const store = {
state: {
statuses: {
notifications: {
data: [
{
action: { id: 1 },
type: 'like'
},
{
action: { id: 2 },
type: 'mention'
},
{
action: { id: 3 },
type: 'repeat'
}
]
}
},
config: {
notificationVisibility: {
likes: true,
repeats: true,
mentions: false
}
}
}
}
const expected = [
{
action: { id: 3 },
type: 'repeat'
},
{
action: { id: 1 },
type: 'like'
}
]
expect(NotificationUtils.visibleNotificationsFromStore(store)).to.eql(expected)
})
})
describe('unseenNotificationsFromStore', () => {
it('should return only notifications not marked as seen', () => {
const store = {
state: {
statuses: {
notifications: {
data: [
{
action: { id: 1 },
type: 'like',
seen: false
},
{
action: { id: 2 },
type: 'mention',
seen: true
}
]
}
},
config: {
notificationVisibility: {
likes: true,
repeats: true,
mentions: false
}
}
}
}
const expected = [
{
action: { id: 1 },
type: 'like',
seen: false
}
]
expect(NotificationUtils.unseenNotificationsFromStore(store)).to.eql(expected)
})
})
})