logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe
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:

Msrc/App.js19++++++++++++++-----
Msrc/App.scss26+++++++++++++++++++++++---
Msrc/App.vue32+++++++++++++++-----------------
Msrc/boot/routes.js8++++++++
Msrc/components/chat_panel/chat_panel.js3++-
Msrc/components/chat_panel/chat_panel.vue14+++++++++-----
Msrc/components/nav_panel/nav_panel.js1-
Msrc/components/nav_panel/nav_panel.vue12++++++------
Msrc/components/notification/notification.js5+----
Msrc/components/notification/notification.vue8++++----
Msrc/components/notifications/notifications.js25++++++++-----------------
Msrc/components/notifications/notifications.vue2+-
Asrc/components/side_drawer/side_drawer.js48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/side_drawer/side_drawer.vue189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/status/status.js3+--
Msrc/components/status/status.vue10+++++-----
Msrc/components/user_card_content/user_card_content.js2+-
Msrc/components/user_card_content/user_card_content.vue6+++---
Msrc/components/user_finder/user_finder.vue22++++++++++++----------
Msrc/components/user_panel/user_panel.js1-
Msrc/components/user_panel/user_panel.vue2+-
Msrc/components/user_search/user_search.js8++++++++
Msrc/components/user_search/user_search.vue18++++++++++++++++++
Msrc/i18n/en.json1+
Asrc/services/notification_utils/notification_utils.js20++++++++++++++++++++
Mtest/unit/specs/components/user_profile.spec.js3++-
Atest/unit/specs/services/notification_utils/notification_utils.spec.js88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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) + }) + }) +})