commit: 76cfb15b3c76531b7c3da0261720d13004c8cfdd
parent: 7aa42c01eb5f05c2e3ed71fc52be6a30e45802bf
Author: Shpuld Shpludson <shp@cock.li>
Date: Mon, 31 Dec 2018 17:35:31 +0000
Merge branch 'feature/replace-panel-switcher' into 'develop'
Mobile side drawer
See merge request pleroma/pleroma-fe!443
Diffstat:
27 files changed, 488 insertions(+), 88 deletions(-)
diff --git a/src/App.js b/src/App.js
@@ -6,6 +6,8 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
import FeaturesPanel from './components/features_panel/features_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 SideDrawer from './components/side_drawer/side_drawer.vue'
+import { unseenNotificationsFromStore } from './services/notification_utils/notification_utils'
export default {
name: 'app',
@@ -17,7 +19,8 @@ export default {
InstanceSpecificPanel,
FeaturesPanel,
WhoToFollowPanel,
- ChatPanel
+ ChatPanel,
+ SideDrawer
},
data: () => ({
mobileActivePanel: 'timeline',
@@ -70,12 +73,15 @@ export default {
sitename () { return this.$store.state.instance.name },
chat () { return this.$store.state.chat.channel.state === 'joined' },
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: {
- activatePanel (panelName) {
- this.mobileActivePanel = panelName
- },
scrollToTop () {
window.scrollTo(0, 0)
},
@@ -85,6 +91,9 @@ export default {
},
onFinderToggled (hidden) {
this.finderHidden = hidden
+ },
+ toggleMobileSidebar () {
+ this.$refs.sideDrawer.toggleDrawer()
}
}
}
diff --git a/src/App.scss b/src/App.scss
@@ -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 {
transition: opacity .2s
}
@@ -524,9 +542,6 @@ nav {
.back-button {
display: none;
}
- .site-name {
- padding-left: 20px;
- }
}
.sidebar-bounds {
@@ -665,4 +680,9 @@ nav {
max-width: 4em;
}
}
+
+ .menu-button {
+ display: block;
+ margin-right: 0.8em;
+ }
}
diff --git a/src/App.vue b/src/App.vue
@@ -7,44 +7,42 @@
</div>
<div class='inner-nav'>
<div class='item'>
- <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>
- </router-link>
+ <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="site-name" :to="{ name: 'root' }" active-class="home">{{sitename}}</router-link>
</div>
<div class='item right'>
- <user-finder class="button-icon nav-icon" @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>
- <a href="#" v-if="currentUser" @click.prevent="logout"><i class="button-icon icon-logout nav-icon" :title="$t('login.logout')"></i></a>
+ <user-finder class="button-icon nav-icon mobile-hidden" @toggled="onFinderToggled"></user-finder>
+ <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>
</div>
</div>
</nav>
- <div class="container" id="content">
- <div class="panel-switcher">
- <button @click="activatePanel('sidebar')">Sidebar</button>
- <button @click="activatePanel('timeline')">Timeline</button>
- </div>
- <div class="sidebar-flexer" :class="{ 'mobile-hidden': mobileActivePanel != 'sidebar'}">
+ <div v-if="" class="container" id="content">
+ <side-drawer ref="sideDrawer" :logout="logout"></side-drawer>
+ <div class="sidebar-flexer mobile-hidden">
<div class="sidebar-bounds">
<div class="sidebar-scroller">
<div class="sidebar">
- <user-panel :activatePanel="activatePanel"></user-panel>
- <nav-panel :activatePanel="activatePanel"></nav-panel>
+ <user-panel></user-panel>
+ <nav-panel></nav-panel>
<instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
<features-panel v-if="!currentUser"></features-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 class="main" :class="{ 'mobile-hidden': mobileActivePanel != 'timeline' }">
+ <div class="main">
<transition name="fade">
<router-view></router-view>
</transition>
</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>
</template>
diff --git a/src/boot/routes.js b/src/boot/routes.js
@@ -12,6 +12,10 @@ import UserSettings from 'components/user_settings/user_settings.vue'
import FollowRequests from 'components/follow_requests/follow_requests.vue'
import OAuthCallback from 'components/oauth_callback/oauth_callback.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) => {
return [
@@ -36,6 +40,10 @@ export default (store) => {
{ name: 'registration', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
{ 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: 'user-search', path: '/user-search', component: UserSearch, props: (route) => ({ query: route.query.query }) },
{ name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
diff --git a/src/components/chat_panel/chat_panel.js b/src/components/chat_panel/chat_panel.js
@@ -1,6 +1,7 @@
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const chatPanel = {
+ props: [ 'floating' ],
data () {
return {
currentMessage: '',
@@ -22,7 +23,7 @@ const chatPanel = {
this.collapsed = !this.collapsed
},
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)
}
}
}
diff --git a/src/components/chat_panel/chat_panel.vue b/src/components/chat_panel/chat_panel.vue
@@ -1,10 +1,10 @@
<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-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">
{{$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 class="chat-window" v-chat-scroll>
@@ -52,6 +52,7 @@
right: 0px;
bottom: 0px;
z-index: 1000;
+ max-width: 25em;
}
.chat-heading {
@@ -63,10 +64,13 @@
}
.chat-window {
- width: 345px;
- max-height: 40vh;
overflow-y: auto;
overflow-x: hidden;
+ max-height: 20em;
+}
+
+.chat-window-container {
+ height: 100%;
}
.chat-message {
diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js
@@ -1,5 +1,4 @@
const NavPanel = {
- props: [ 'activatePanel' ],
computed: {
currentUser () {
return this.$store.state.users.currentUser
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
@@ -3,32 +3,32 @@
<div class="panel panel-default">
<ul>
<li v-if='currentUser'>
- <router-link @click.native="activatePanel('timeline')" :to="{ name: 'friends' }">
+ <router-link :to="{ name: 'friends' }">
{{ $t("nav.timeline") }}
</router-link>
</li>
<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") }}
</router-link>
</li>
<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") }}
</router-link>
</li>
<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") }}
</router-link>
</li>
<li>
- <router-link @click.native="activatePanel('timeline')" :to="{ name: 'public-timeline' }">
+ <router-link :to="{ name: 'public-timeline' }">
{{ $t("nav.public_tl") }}
</router-link>
</li>
<li>
- <router-link @click.native="activatePanel('timeline')" :to="{ name: 'public-external-timeline' }">
+ <router-link :to="{ name: 'public-external-timeline' }">
{{ $t("nav.twkn") }}
</router-link>
</li>
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
@@ -11,10 +11,7 @@ const Notification = {
betterShadow: this.$store.state.interface.browserSupport.cssFilter
}
},
- props: [
- 'notification',
- 'activatePanel'
- ],
+ props: [ 'notification' ],
components: {
Status, StillImage, UserCardContent
},
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
@@ -1,5 +1,5 @@
<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>
<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"/>
@@ -25,15 +25,15 @@
<small>{{$t('notifications.followed_you')}}</small>
</span>
</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>
<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}}
</router-link>
</div>
<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>
</div>
</div>
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
@@ -1,10 +1,12 @@
import Notification from '../notification/notification.vue'
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
-
-import { sortBy, filter } from 'lodash'
+import {
+ notificationsFromStore,
+ visibleNotificationsFromStore,
+ unseenNotificationsFromStore
+} from '../../services/notification_utils/notification_utils.js'
const Notifications = {
- props: [ 'activatePanel' ],
created () {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
@@ -12,28 +14,17 @@ const Notifications = {
notificationsFetcher.startFetching({ store, credentials })
},
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 () {
- return this.$store.state.statuses.notifications.data
+ return notificationsFromStore(this.$store)
},
error () {
return this.$store.state.statuses.notifications.error
},
unseenNotifications () {
- return filter(this.visibleNotifications, ({seen}) => !seen)
+ return unseenNotificationsFromStore(this.$store)
},
visibleNotifications () {
- // Don't know why, but sortBy([seen, -action.id]) doesn't work.
- let sortedNotifications = sortBy(this.notifications, ({action}) => -action.id)
- sortedNotifications = sortBy(sortedNotifications, 'seen')
- return sortedNotifications.filter((notification) => this.visibleTypes.includes(notification.type))
+ return visibleNotificationsFromStore(this.$store)
},
unseenCount () {
return this.unseenNotifications.length
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
@@ -14,7 +14,7 @@
<div class="panel-body">
<div v-for="notification in visibleNotifications" :key="notification.action.id" class="notification" :class='{"unseen": !notification.seen}'>
<div class="notification-overlay"></div>
- <notification :activatePanel="activatePanel" :notification="notification"></notification>
+ <notification :notification="notification"></notification>
</div>
</div>
<div class="panel-footer">
diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js
@@ -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
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
@@ -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>
diff --git a/src/components/status/status.js b/src/components/status/status.js
@@ -21,8 +21,7 @@ const Status = {
'replies',
'noReplyLinks',
'noHeading',
- 'inlineExpanded',
- 'activatePanel'
+ 'inlineExpanded'
],
data () {
return {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
@@ -3,7 +3,7 @@
<template v-if="muted && !noReplyLinks">
<div class="media status container muted">
<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}}
</router-link>
</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-else>{{status.user.name}}</h4>
<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}}
</router-link>
<span v-if="status.in_reply_to_screen_name" class="faint reply-info">
<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}}
</router-link>
</span>
@@ -60,7 +60,7 @@
</h4>
</div>
<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>
</router-link>
<div class="button-icon visibility-icon" v-if="status.visibility">
@@ -79,7 +79,7 @@
</div>
<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>
<i class="icon-spin4 animate-spin"></i>
</div>
diff --git a/src/components/user_card_content/user_card_content.js b/src/components/user_card_content/user_card_content.js
@@ -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'
export default {
- props: [ 'user', 'switcher', 'selected', 'hideBio', 'activatePanel' ],
+ props: [ 'user', 'switcher', 'selected', 'hideBio' ],
data () {
return {
followRequestInProgress: false,
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue
@@ -3,14 +3,14 @@
<div class="panel-heading text-center">
<div class='user-info'>
<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"/>
</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 @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>
</router-link>
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser">
@@ -18,7 +18,7 @@
</a>
</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 v-if="!hideUserStatsLocal && !hideBio" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
</router-link>
diff --git a/src/components/user_finder/user_finder.vue b/src/components/user_finder/user_finder.vue
@@ -1,14 +1,16 @@
<template>
- <div class="user-finder-container">
- <i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
- <a href="#" v-if="hidden" :title="$t('finder.find_user')"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a>
- <template v-else>
- <input class="user-finder-input" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/>
- <button class="btn search-button" @click="findUser(username)">
- <i class="icon-search"/>
- </button>
- <i class="button-icon icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/>
- </template>
+ <div>
+ <div class="user-finder-container">
+ <i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
+ <a href="#" v-if="hidden" :title="$t('finder.find_user')"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a>
+ <template v-else>
+ <input class="user-finder-input" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/>
+ <button class="btn search-button" @click="findUser(username)">
+ <i class="icon-search"/>
+ </button>
+ <i class="button-icon icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/>
+ </template>
+ </div>
</div>
</template>
diff --git a/src/components/user_panel/user_panel.js b/src/components/user_panel/user_panel.js
@@ -3,7 +3,6 @@ import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCardContent from '../user_card_content/user_card_content.vue'
const UserPanel = {
- props: [ 'activatePanel' ],
computed: {
user () { return this.$store.state.users.currentUser }
},
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 :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">
<post-status-form v-if='user'></post-status-form>
</div>
diff --git a/src/components/user_search/user_search.js b/src/components/user_search/user_search.js
@@ -9,6 +9,7 @@ const userSearch = {
],
data () {
return {
+ username: '',
users: []
}
},
@@ -21,7 +22,14 @@ const userSearch = {
}
},
methods: {
+ newQuery (query) {
+ this.$router.push({ name: 'user-search', query: { query } })
+ },
search (query) {
+ if (!query) {
+ this.users = []
+ return
+ }
userSearchApi.search({query, store: this.$store})
.then((res) => {
this.users = res
diff --git a/src/components/user_search/user_search.vue b/src/components/user_search/user_search.vue
@@ -3,6 +3,12 @@
<div class="panel-heading">
{{$t('nav.user_search')}}
</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">
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card>
</div>
@@ -10,3 +16,15 @@
</template>
<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>
diff --git a/src/i18n/en.json b/src/i18n/en.json
@@ -50,6 +50,7 @@
"repeated_you": "repeated your 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_link": "locked",
"attachments_sensitive": "Mark attachments as sensitive",
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
@@ -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)
diff --git a/test/unit/specs/components/user_profile.spec.js b/test/unit/specs/components/user_profile.spec.js
@@ -8,7 +8,8 @@ const localVue = createLocalVue()
localVue.use(Vuex)
const mutations = {
- clearTimeline: () => {}
+ clearTimeline: () => {},
+ setError: () => {}
}
const testGetters = {
diff --git a/test/unit/specs/services/notification_utils/notification_utils.spec.js b/test/unit/specs/services/notification_utils/notification_utils.spec.js
@@ -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)
+ })
+ })
+})