New routes, notifications, other impovements in side drwaer

This commit is contained in:
shpuld 2018-12-28 21:39:54 +02:00
parent 4752081818
commit 85c058e95c
25 changed files with 313 additions and 138 deletions

View file

@ -7,6 +7,7 @@ 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 SideDrawer from './components/side_drawer/side_drawer.vue'
import { unseenNotificationsFromStore } from './services/notification_utils/notification_utils'
export default { export default {
name: 'app', name: 'app',
@ -75,12 +76,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)
}, },

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,50 +7,45 @@
</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="button-icon icon-menu"></i>
<div class="alert-dot" v-if="unseenNotificationsCount"></div>
</a>
<!-- <router-link class="back-button" @click.native="activatePanel('timeline')" :to="{ name: 'root' }" active-class="hidden">
<i class="icon-left-open" :title="$t('nav.back')"></i> <i class="icon-left-open" :title="$t('nav.back')"></i>
</router-link> </router-link> -->
<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'>
<a href="#" @click.stop.prevent="toggleMobileSidebar()"><i class="button-icon icon-menu"></i></a>
<user-finder class="button-icon nav-icon mobile-hidden" @toggled="onFinderToggled"></user-finder> <user-finder class="button-icon nav-icon mobile-hidden" @toggled="onFinderToggled"></user-finder>
<router-link class="mobile-hidden" @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="#" class="mobile-hidden" 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 v-if="" class="container" id="content"> <div v-if="" class="container" id="content">
<side-drawer <side-drawer ref="sideDrawer" :logout="logout"></side-drawer>
ref="sideDrawer" <div class="sidebar-flexer mobile-hidden">
:activatePanel="activatePanel"
:logout="logout"
>
</side-drawer>
<div class="sidebar-flexer">
<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" :class="mobileShowOnlyIn('poststatus')"></user-panel> <user-panel></user-panel>
<nav-panel :activatePanel="activatePanel" class="mobile-hidden"></nav-panel> <nav-panel></nav-panel>
<instance-specific-panel v-if="showInstanceSpecificPanel" class="mobile-hidden"></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" :class="mobileShowOnlyIn('notifications')"></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 [
@ -62,6 +66,10 @@ export default (store) => {
{ 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: '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: '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 }) }
] ]
} }

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) return generateProfileLink(user.id, user.username)
} }
} }
} }

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

@ -1,39 +1,43 @@
import UserCardContent from '../user_card_content/user_card_content.vue' import UserCardContent from '../user_card_content/user_card_content.vue'
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
const deltaX = (oldX, newX) => newX - oldX const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
const touchEventX = e => e.touches[0].screenX const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY])
const SideDrawer = { const SideDrawer = {
props: [ 'activatePanel', 'logout' ], props: [ 'logout' ],
data: () => ({ data: () => ({
closed: true, closed: true,
touchX: 0 touchCoord: [0, 0]
}), }),
components: { UserCardContent }, components: { UserCardContent },
computed: { computed: {
currentUser () { currentUser () {
return this.$store.state.users.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: { methods: {
toggleDrawer () { toggleDrawer () {
this.closed = !this.closed this.closed = !this.closed
}, },
gotoPanel (panel) {
this.activatePanel(panel)
this.toggleDrawer()
},
doLogout () { doLogout () {
this.logout() this.logout()
this.gotoPanel('timeline') this.toggleDrawer()
}, },
touchStart (e) { touchStart (e) {
this.touchX = touchEventX(e) this.touchCoord = touchEventCoord(e)
}, },
touchMove (e) { touchMove (e) {
const delta = deltaX(this.touchX, touchEventX(e)) const delta = deltaCoord(this.touchCoord, touchEventCoord(e))
if (delta < -30 && !this.closed) { if (delta[0] < -30 && Math.abs(delta[1]) < Math.abs(delta[0]) && !this.closed) {
this.toggleDrawer() this.toggleDrawer()
} }
} }

View file

@ -1,61 +1,77 @@
<template> <template>
<div class="side-drawer-container" :class="{ 'side-drawer-container-closed': closed, 'side-drawer-container-open': !closed }"> <div class="side-drawer-container" :class="{ 'side-drawer-container-closed': closed, 'side-drawer-container-open': !closed }">
<div class="panel panel-default side-drawer" <div class="side-drawer"
:class="{'side-drawer-closed': closed}" :class="{'side-drawer-closed': closed}"
@touchstart="touchStart" @touchstart="touchStart"
@touchmove.prevent="touchMove" @touchmove="touchMove"
> >
<div class="side-drawer-heading"> <div class="side-drawer-heading" @click="toggleDrawer">
<user-card-content :activatePanel="activatePanel" :user="currentUser" :switcher="false" :hideBio="true"> <user-card-content :user="currentUser" :switcher="false" :hideBio="true">
</user-card-content> </user-card-content>
</div> </div>
<ul> <ul>
<li v-if='currentUser'> <li v-if="currentUser" @click="toggleDrawer">
<a href="#" @click="gotoPanel('poststatus')"> <router-link :to="{ name: 'new-status', params: { username: currentUser.screen_name } }">
{{ $t("post_status.new_status") }} {{ $t("post_status.new_status") }}
</a>
</li>
<li v-else>
<a href="#" @click="gotoPanel('poststatus')">
{{ $t("login.login") }}
</a>
</li>
<li v-if='currentUser'>
<a href="#" @click="gotoPanel('notifications')">
{{ $t("notifications.notifications") }}
</a>
</li>
<li v-if='currentUser'>
<router-link @click.native="gotoPanel('timeline')" to='/main/friends'>
{{ $t("nav.timeline") }}
</router-link> </router-link>
</li> </li>
<li v-if='currentUser'> <li v-else @click="toggleDrawer">
<router-link @click.native="gotoPanel('timeline')" :to="{ name: 'dms', params: { username: currentUser.screen_name } }"> <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") }} {{ $t("nav.dms") }}
</router-link> </router-link>
</li> </li>
<li v-if='currentUser && currentUser.locked'> <li>
<router-link @click.native="gotoPanel('timeline')" to='/friend-requests'> <div class="divider"></div>
</li>
<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") }} {{ $t("nav.friend_requests") }}
</router-link> </router-link>
</li> </li>
<li> <li @click="toggleDrawer">
<router-link @click.native="gotoPanel('timeline')" to='/main/public'> <router-link to='/main/public'>
{{ $t("nav.public_tl") }} {{ $t("nav.public_tl") }}
</router-link> </router-link>
</li> </li>
<li> <li @click="toggleDrawer">
<router-link @click.native="gotoPanel('timeline')" to='/main/all'> <router-link to='/main/all'>
{{ $t("nav.twkn") }} {{ $t("nav.twkn") }}
</router-link> </router-link>
</li> </li>
<li v-if="currentUser && chat" @click="toggleDrawer">
<router-link :to="{ name: 'chat' }">
{{ $t("nav.chat") }}
</router-link>
</li>
<li> <li>
<router-link @click.native="gotoPanel('timeline')" :to="{ name: 'settings'}"> <div class="divider"></div>
</li>
<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") }} {{ $t("settings.settings") }}
</router-link> </router-link>
</li> </li>
<li v-if="currentUser"> <li v-if="currentUser" @click="toggleDrawer">
<a @click="doLogout" href="#"> <a @click="doLogout" href="#">
{{ $t("login.logout") }} {{ $t("login.logout") }}
</a> </a>
@ -102,9 +118,14 @@
transition: 0.5s; /* 0.5 second transition effect to slide in the sidenav */ transition: 0.5s; /* 0.5 second transition effect to slide in the sidenav */
transition-timing-function: cubic-bezier(0, 1, 0.5, 1); transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
margin: 0 0 0 -100px; margin: 0 0 0 -100px;
padding: 0 0 0 100px; padding: 0 0 1em 100px;
width: 75%; width: 80%;
flex: 0 0 75%; 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 { .side-drawer-click-outside-closed {
@ -115,18 +136,12 @@
margin: 0 0 0 calc(-100% - 100px); margin: 0 0 0 calc(-100% - 100px);
} }
.side-drawer .panel {
overflow: hidden;
margin: 0;
display: flex;
}
.side-drawer-heading { .side-drawer-heading {
background: transparent; background: transparent;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
display: flex; display: flex;
min-height: 8em; min-height: 7em;
padding: 0; padding: 0;
margin: 0; margin: 0;
@ -147,14 +162,18 @@
} }
.side-drawer li { .side-drawer li {
border-bottom: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
padding: 0; padding: 0;
.divider {
border-top: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
margin: 0.2em 0;
}
a { a {
display: block; display: block;
padding: 0.8em 0.85em; padding: 0.5em 0.85em;
&:hover { &:hover {
background-color: $fallback--lightBg; background-color: $fallback--lightBg;

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

@ -2,20 +2,20 @@
<div id="heading" class="profile-panel-background" :style="headingStyle"> <div id="heading" class="profile-panel-background" :style="headingStyle">
<div class="panel-heading text-center"> <div class="panel-heading text-center">
<div class='user-info'> <div class='user-info'>
<router-link @click.native="activatePanel && activatePanel('timeline')" :to="{ name: 'user-settings' }" style="float: right; margin-top:16px;" v-if="!isOtherUser"> <router-link :to="{ name: 'user-settings' }" style="float: right; margin-top:16px;" 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" class="floater" v-if="isOtherUser"> <a :href="user.statusnet_profile_url" target="_blank" class="floater" v-if="isOtherUser">
<i class="icon-link-ext usersettings"></i> <i class="icon-link-ext usersettings"></i>
</a> </a>
<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 :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')" class='user-screen-name' :to="userProfileLink(user)"> <router-link class='user-screen-name' :to="userProfileLink(user)">
<span>@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span> <span>@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
<span v-if="!hideUserStatsLocal" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span> <span v-if="!hideUserStatsLocal" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
</router-link> </router-link>

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

@ -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

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

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)
})
})
})