Merge remote-tracking branch 'upstream/develop' into shigusegubu
* upstream/develop: (32 commits) [Debug] Avoid duplicates in the who to follow panel updated German translation * added theme settings * added various missing single strings Fix translation typo in registration.vue Update README Fix profiles without statuses not loading Fix conflicting styles Remove commented out back button Cleanup and remove divider element in side drawer New routes, notifications, other impovements in side drwaer Add "noAttachmentLinks" to src/modules/instance.js Make "noAttachmentLinks" configurable No attachment links Treat reserved users like external users in the frontend. User Card Content fixes and updates scopeCopy → true by default Restore old routes, enable user route as fallback. improve web push notifications fix Update japanese translation fix inconsistencies within who_to_follow_panel ...
This commit is contained in:
commit
1eea45cf6d
57 changed files with 1255 additions and 470 deletions
|
@ -6,11 +6,12 @@
|
|||
|
||||
# For Translators
|
||||
|
||||
To translate Pleroma, add your language to [src/i18n/messages.js](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/messages.js). Pleroma will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js.
|
||||
To translate Pleroma-FE, add your language to [src/i18n/messages.js](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/messages.js). Pleroma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js.
|
||||
|
||||
# FOR ADMINS
|
||||
|
||||
You don't need to build Pleroma yourself. Check out https://git.pleroma.social/pleroma/pleroma-fe/wikis/dual-boot-with-qvitter to see how to run Pleroma and Qvitter at the same time.
|
||||
You don't need to build Pleroma-FE yourself. Those using the Pleroma backend will be able to use it out of the box.
|
||||
For the GNU social backend, check out https://git.pleroma.social/pleroma/pleroma-fe/wikis/dual-boot-with-qvitter to see how to run Pleroma-FE and Qvitter at the same time.
|
||||
|
||||
## Build Setup
|
||||
|
||||
|
|
19
src/App.js
19
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
26
src/App.scss
26
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;
|
||||
}
|
||||
}
|
||||
|
|
32
src/App.vue
32
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>
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ const afterStoreSetup = ({ store, i18n }) => {
|
|||
var scopeCopy = (config.scopeCopy)
|
||||
var subjectLineBehavior = (config.subjectLineBehavior)
|
||||
var alwaysShowSubjectInput = (config.alwaysShowSubjectInput)
|
||||
var noAttachmentLinks = (config.noAttachmentLinks)
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'theme', value: theme })
|
||||
store.dispatch('setInstanceOption', { name: 'background', value: background })
|
||||
|
@ -90,6 +91,8 @@ const afterStoreSetup = ({ store, i18n }) => {
|
|||
store.dispatch('setInstanceOption', { name: 'scopeCopy', value: scopeCopy })
|
||||
store.dispatch('setInstanceOption', { name: 'subjectLineBehavior', value: subjectLineBehavior })
|
||||
store.dispatch('setInstanceOption', { name: 'alwaysShowSubjectInput', value: alwaysShowSubjectInput })
|
||||
store.dispatch('setInstanceOption', { name: 'noAttachmentLinks', value: noAttachmentLinks })
|
||||
|
||||
if (chatDisabled) {
|
||||
store.dispatch('disableChat')
|
||||
}
|
||||
|
@ -165,6 +168,8 @@ const afterStoreSetup = ({ store, i18n }) => {
|
|||
store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
|
||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
|
||||
|
||||
const suggestions = metadata.suggestions
|
||||
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
|
||||
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
|
||||
|
|
|
@ -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 [
|
||||
|
@ -20,48 +24,28 @@ export default (store) => {
|
|||
redirect: _to => {
|
||||
return (store.state.users.currentUser
|
||||
? store.state.instance.redirectRootLogin
|
||||
: store.state.instance.redirectRootNoLogin) || '/~/main/all'
|
||||
: store.state.instance.redirectRootNoLogin) || '/main/all'
|
||||
}
|
||||
},
|
||||
{ name: 'public-external-timeline', path: '/~/main/all', component: PublicAndExternalTimeline },
|
||||
{ name: 'public-timeline', path: '/~/main/public', component: PublicTimeline },
|
||||
{ name: 'friends', path: '/~/main/friends', component: FriendsTimeline },
|
||||
// Beginning of temporary redirects
|
||||
{ path: '/main/:route',
|
||||
redirect: to => {
|
||||
const { params } = to
|
||||
const route = params.route ? params.route : 'all'
|
||||
|
||||
return { path: `/~/main/${route}` }
|
||||
}
|
||||
},
|
||||
{ path: '/tag/:tag',
|
||||
redirect: to => {
|
||||
const { params } = to
|
||||
|
||||
return { path: `/~/tag/${params.tag}` }
|
||||
}
|
||||
},
|
||||
{ path: '/notice/:id',
|
||||
redirect: to => {
|
||||
const { params } = to
|
||||
|
||||
return { path: `/~/notice/${params.id}` }
|
||||
}
|
||||
},
|
||||
// End of temporary redirects
|
||||
{ name: 'tag-timeline', path: '/~/tag/:tag', component: TagTimeline },
|
||||
{ name: 'conversation', path: '/~/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||
{ name: 'user-profile', path: '/:name', component: UserProfile },
|
||||
{ name: 'external-user-profile', path: '/~/users/:id', component: UserProfile },
|
||||
{ name: 'mentions', path: '/:username/mentions', component: Mentions },
|
||||
{ name: 'dms', path: '/:username/dms', component: DMs },
|
||||
{ name: 'settings', path: '/~/settings', component: Settings },
|
||||
{ name: 'registration', path: '/~/registration', component: Registration },
|
||||
{ name: 'registration', path: '/~/registration/:token', component: Registration },
|
||||
{ name: 'friend-requests', path: '/~/friend-requests', component: FollowRequests },
|
||||
{ name: 'user-settings', path: '/~/user-settings', component: UserSettings },
|
||||
{ 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: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
|
||||
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
|
||||
{ name: 'friends', path: '/main/friends', component: FriendsTimeline },
|
||||
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
|
||||
{ name: 'mentions', path: '/users/:username/mentions', component: Mentions },
|
||||
{ name: 'dms', path: '/users/:username/dms', component: DMs },
|
||||
{ name: 'settings', path: '/settings', component: Settings },
|
||||
{ name: 'registration', path: '/registration', component: Registration },
|
||||
{ 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 }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
return generateProfileLink(user.id, user.username, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -32,7 +32,7 @@ const LoginForm = {
|
|||
.then((result) => {
|
||||
this.$store.commit('setToken', result.access_token)
|
||||
this.$store.dispatch('loginUser', result.access_token)
|
||||
this.$router.push('/~/main/friends')
|
||||
this.$router.push({name: 'friends'})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
const NavPanel = {
|
||||
props: [ 'activatePanel' ],
|
||||
computed: {
|
||||
currentUser () {
|
||||
return this.$store.state.users.currentUser
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -11,10 +11,7 @@ const Notification = {
|
|||
betterShadow: this.$store.state.interface.browserSupport.cssFilter
|
||||
}
|
||||
},
|
||||
props: [
|
||||
'notification',
|
||||
'activatePanel'
|
||||
],
|
||||
props: [ 'notification' ],
|
||||
components: {
|
||||
Status, StillImage, UserCardContent
|
||||
},
|
||||
|
@ -23,7 +20,7 @@ const Notification = {
|
|||
this.userExpanded = !this.userExpanded
|
||||
},
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name)
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -11,7 +11,7 @@ const oac = {
|
|||
}).then((result) => {
|
||||
this.$store.commit('setToken', result.access_token)
|
||||
this.$store.dispatch('loginUser', result.access_token)
|
||||
this.$router.push('/~/main/friends')
|
||||
this.$router.push({name: 'friends'})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -249,6 +249,7 @@ const PostStatusForm = {
|
|||
}
|
||||
this.$emit('posted')
|
||||
let el = this.$el.querySelector('textarea')
|
||||
el.style.height = 'auto'
|
||||
el.style.height = undefined
|
||||
this.error = null
|
||||
} else {
|
||||
|
|
|
@ -28,7 +28,7 @@ const registration = {
|
|||
},
|
||||
created () {
|
||||
if ((!this.registrationOpen && !this.token) || this.signedIn) {
|
||||
this.$router.push('/~/main/all')
|
||||
this.$router.push({name: 'root'})
|
||||
}
|
||||
|
||||
this.setCaptcha()
|
||||
|
@ -48,15 +48,17 @@ const registration = {
|
|||
async submit () {
|
||||
this.user.nickname = this.user.username
|
||||
this.user.token = this.token
|
||||
|
||||
this.user.captcha_solution = this.captcha.solution
|
||||
this.user.captcha_token = this.captcha.token
|
||||
this.user.captcha_answer_data = this.captcha.answer_data
|
||||
|
||||
this.$v.$touch()
|
||||
|
||||
if (!this.$v.$invalid) {
|
||||
try {
|
||||
await this.signUp(this.user)
|
||||
this.$router.push('/~/main/friends')
|
||||
this.$router.push({name: 'friends'})
|
||||
} catch (error) {
|
||||
console.warn('Registration failed: ' + error)
|
||||
}
|
||||
|
|
|
@ -76,15 +76,16 @@
|
|||
</div>
|
||||
|
||||
<div class="form-group" id="captcha-group" v-if="captcha.type != 'none'">
|
||||
<label class='form--label' for='captcha-label'>{{$t('captcha')}}</label>
|
||||
|
||||
<template v-if="captcha.type == 'kocaptcha'">
|
||||
<img v-bind:src="captcha.url" v-on:click="setCaptcha">
|
||||
|
||||
<sub>Click the image to get a new captcha</sub>
|
||||
<label class='form--label' for='captcha-label'>CAPTCHA</label>
|
||||
<sub>{{$t('registration.new_captcha')}}</sub>
|
||||
|
||||
<input :disabled="isPending"
|
||||
v-model='captcha.solution'
|
||||
class='form-control' id='captcha-answer' type='text'>
|
||||
class='form-control' id='captcha-answer' type='text' autocomplete="off">
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
|
48
src/components/side_drawer/side_drawer.js
Normal file
48
src/components/side_drawer/side_drawer.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
import UserCardContent from '../user_card_content/user_card_content.vue'
|
||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||
|
||||
// TODO: separate touch gesture stuff into their own utils if more components want them
|
||||
const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
|
||||
|
||||
const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY])
|
||||
|
||||
const SideDrawer = {
|
||||
props: [ 'logout' ],
|
||||
data: () => ({
|
||||
closed: true,
|
||||
touchCoord: [0, 0]
|
||||
}),
|
||||
components: { UserCardContent },
|
||||
computed: {
|
||||
currentUser () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
chat () { return this.$store.state.chat.channel.state === 'joined' },
|
||||
unseenNotifications () {
|
||||
return unseenNotificationsFromStore(this.$store)
|
||||
},
|
||||
unseenNotificationsCount () {
|
||||
return this.unseenNotifications.length
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleDrawer () {
|
||||
this.closed = !this.closed
|
||||
},
|
||||
doLogout () {
|
||||
this.logout()
|
||||
this.toggleDrawer()
|
||||
},
|
||||
touchStart (e) {
|
||||
this.touchCoord = touchEventCoord(e)
|
||||
},
|
||||
touchMove (e) {
|
||||
const delta = deltaCoord(this.touchCoord, touchEventCoord(e))
|
||||
if (delta[0] < -30 && Math.abs(delta[1]) < Math.abs(delta[0]) && !this.closed) {
|
||||
this.toggleDrawer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SideDrawer
|
189
src/components/side_drawer/side_drawer.vue
Normal file
189
src/components/side_drawer/side_drawer.vue
Normal file
|
@ -0,0 +1,189 @@
|
|||
<template>
|
||||
<div class="side-drawer-container"
|
||||
:class="{ 'side-drawer-container-closed': closed, 'side-drawer-container-open': !closed }"
|
||||
>
|
||||
<div class="side-drawer"
|
||||
:class="{'side-drawer-closed': closed}"
|
||||
@touchstart="touchStart"
|
||||
@touchmove="touchMove"
|
||||
>
|
||||
<div class="side-drawer-heading" @click="toggleDrawer">
|
||||
<user-card-content :user="currentUser" :switcher="false" :hideBio="true" v-if="currentUser">
|
||||
</user-card-content>
|
||||
</div>
|
||||
<ul>
|
||||
<li v-if="currentUser" @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'new-status', params: { username: currentUser.screen_name } }">
|
||||
{{ $t("post_status.new_status") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-else @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'login' }">
|
||||
{{ $t("login.login") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="currentUser" @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'notifications', params: { username: currentUser.screen_name } }">
|
||||
{{ $t("notifications.notifications") }} {{ unseenNotificationsCount > 0 ? `(${unseenNotificationsCount})` : '' }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="currentUser" @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
|
||||
{{ $t("nav.dms") }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li v-if="currentUser" @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'friends' }">
|
||||
{{ $t("nav.timeline") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="currentUser && currentUser.locked" @click="toggleDrawer">
|
||||
<router-link to='/friend-requests'>
|
||||
{{ $t("nav.friend_requests") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li @click="toggleDrawer">
|
||||
<router-link to='/main/public'>
|
||||
{{ $t("nav.public_tl") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li @click="toggleDrawer">
|
||||
<router-link to='/main/all'>
|
||||
{{ $t("nav.twkn") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="currentUser && chat" @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'chat' }">
|
||||
{{ $t("nav.chat") }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'user-search'}">
|
||||
{{ $t("nav.user_search") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'settings'}">
|
||||
{{ $t("settings.settings") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="currentUser" @click="toggleDrawer">
|
||||
<a @click="doLogout" href="#">
|
||||
{{ $t("login.logout") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="side-drawer-click-outside"
|
||||
@click.stop.prevent="toggleDrawer"
|
||||
:class="{'side-drawer-click-outside-closed': closed}"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./side_drawer.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.side-drawer-container {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.side-drawer-container-open {
|
||||
transition-delay: 0.0s;
|
||||
transition-property: left;
|
||||
}
|
||||
|
||||
.side-drawer-container-closed {
|
||||
left: -100%;
|
||||
transition-delay: 0.5s;
|
||||
transition-property: left;
|
||||
}
|
||||
|
||||
.side-drawer-click-outside {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.side-drawer {
|
||||
overflow-x: hidden;
|
||||
transition: 0.5s;
|
||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||
margin: 0 0 0 -100px;
|
||||
padding: 0 0 1em 100px;
|
||||
width: 80%;
|
||||
max-width: 20em;
|
||||
flex: 0 0 80%;
|
||||
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
|
||||
box-shadow: var(--panelShadow);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
}
|
||||
|
||||
.side-drawer-click-outside-closed {
|
||||
flex: 0 0 0;
|
||||
}
|
||||
|
||||
.side-drawer-closed {
|
||||
margin: 0 0 0 calc(-100% - 100px);
|
||||
}
|
||||
|
||||
.side-drawer-heading {
|
||||
background: transparent;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
min-height: 7em;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.profile-panel-background {
|
||||
border-radius: 0;
|
||||
.panel-heading {
|
||||
background: transparent;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.side-drawer ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
border-bottom: 1px solid;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
margin: 0.2em 0;
|
||||
}
|
||||
|
||||
.side-drawer ul:last-child {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.side-drawer li {
|
||||
padding: 0;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 0.5em 0.85em;
|
||||
|
||||
&:hover {
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -21,8 +21,7 @@ const Status = {
|
|||
'replies',
|
||||
'noReplyLinks',
|
||||
'noHeading',
|
||||
'inlineExpanded',
|
||||
'activatePanel'
|
||||
'inlineExpanded'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
@ -291,7 +290,7 @@ const Status = {
|
|||
this.showPreview = false
|
||||
},
|
||||
userProfileLink (id, name) {
|
||||
return generateProfileLink(id, name)
|
||||
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -33,7 +33,7 @@ const UserCard = {
|
|||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
},
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name)
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
@ -180,7 +180,7 @@ export default {
|
|||
}
|
||||
},
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name)
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,22 +2,25 @@
|
|||
<div id="heading" class="profile-panel-background" :style="headingStyle">
|
||||
<div class="panel-heading text-center">
|
||||
<div class='user-info'>
|
||||
<router-link @click.native="activatePanel && activatePanel('timeline')" :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>
|
||||
</router-link>
|
||||
<a :href="user.statusnet_profile_url" target="_blank" class="floater" v-if="isOtherUser">
|
||||
<i class="icon-link-ext usersettings"></i>
|
||||
</a>
|
||||
<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 :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')" 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 v-if="!hideUserStatsLocal" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
|
||||
<div class="top-line">
|
||||
<div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
|
||||
<div :title="user.name" class='user-name' v-else>{{user.name}}</div>
|
||||
<router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser">
|
||||
<i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i>
|
||||
</router-link>
|
||||
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser">
|
||||
<i class="icon-link-ext usersettings"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<router-link class='user-screen-name' :to="userProfileLink(user)">
|
||||
<span class="handle">@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
|
||||
<span v-if="!hideUserStatsLocal && !hideBio" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,7 +28,7 @@
|
|||
<div v-if="user.follows_you && loggedIn && isOtherUser" class="following">
|
||||
{{ $t('user_card.follows_you') }}
|
||||
</div>
|
||||
<div class="floater" v-if="isOtherUser && (loggedIn || !switcher)">
|
||||
<div class="highlighter" v-if="isOtherUser && (loggedIn || !switcher)">
|
||||
<!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to -->
|
||||
<input class="userHighlightText" type="text" :id="'userHighlightColorTx'+user.id" v-if="userHighlightType !== 'disabled'" v-model="userHighlightColor"/>
|
||||
<input class="userHighlightCl" type="color" :id="'userHighlightColor'+user.id" v-if="userHighlightType !== 'disabled'" v-model="userHighlightColor"/>
|
||||
|
@ -139,7 +142,7 @@
|
|||
border-bottom-right-radius: 0;
|
||||
|
||||
.panel-heading {
|
||||
padding: 0.6em 0em;
|
||||
padding: .6em 0;
|
||||
text-align: center;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
@ -158,10 +161,10 @@
|
|||
.user-info {
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
padding: 0 16px;
|
||||
padding: 0 26px;
|
||||
|
||||
.container {
|
||||
padding: 16px 10px 6px 10px;
|
||||
padding: 16px 0 6px;
|
||||
display: flex;
|
||||
max-height: 56px;
|
||||
|
||||
|
@ -218,11 +221,15 @@
|
|||
vertical-align: middle;
|
||||
object-fit: contain
|
||||
}
|
||||
.top-line {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.user-name{
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.user-screen-name {
|
||||
|
@ -232,27 +239,73 @@
|
|||
font-weight: light;
|
||||
font-size: 15px;
|
||||
padding-right: 0.1em;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
.dailyAvg {
|
||||
min-width: 1px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.handle {
|
||||
min-width: 1px;
|
||||
flex: 0 1 auto;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.user-meta {
|
||||
margin-bottom: .4em;
|
||||
margin-bottom: .15em;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.following {
|
||||
font-size: 14px;
|
||||
flex: 0 0 100%;
|
||||
flex: 1 0 auto;
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
margin-bottom: .25em;
|
||||
text-align: left;
|
||||
float: left;
|
||||
}
|
||||
.floater {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
content: '';
|
||||
clear: both;
|
||||
.highlighter {
|
||||
flex: 0 1 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-right: -.5em;
|
||||
align-self: start;
|
||||
|
||||
.userHighlightCl {
|
||||
padding: 2px 10px;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.userHighlightSel,
|
||||
.userHighlightSel.select {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
.userHighlightSel.select i {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.userHighlightText {
|
||||
width: 70px;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.userHighlightCl,
|
||||
.userHighlightText,
|
||||
.userHighlightSel,
|
||||
.userHighlightSel.select {
|
||||
height: 22px;
|
||||
vertical-align: top;
|
||||
margin-right: .5em;
|
||||
margin-bottom: .25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
.user-interactions {
|
||||
|
@ -260,8 +313,13 @@
|
|||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
margin-right: -.75em;
|
||||
|
||||
div {
|
||||
flex: 1;
|
||||
flex: 1 0 0;
|
||||
margin-right: .75em;
|
||||
margin-bottom: .6em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mute {
|
||||
|
@ -280,8 +338,9 @@
|
|||
}
|
||||
|
||||
button {
|
||||
width: 92%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.remote-button {
|
||||
|
@ -304,10 +363,11 @@
|
|||
justify-content: space-between;
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.user-count {
|
||||
flex: 1;
|
||||
flex: 1 0 auto;
|
||||
padding: .5em 0 .5em 0;
|
||||
margin: 0 .5em;
|
||||
|
||||
|
@ -327,32 +387,5 @@
|
|||
color: #CCC;
|
||||
}
|
||||
.floater {
|
||||
float: right;
|
||||
margin-top: 16px;
|
||||
|
||||
.userHighlightCl {
|
||||
padding: 2px 10px;
|
||||
}
|
||||
.userHighlightSel,
|
||||
.userHighlightSel.select {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.userHighlightSel.select i {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.userHighlightText {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.userHighlightCl,
|
||||
.userHighlightText,
|
||||
.userHighlightSel,
|
||||
.userHighlightSel.select {
|
||||
height: 22px;
|
||||
vertical-align: top;
|
||||
margin-right: 0
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 }
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -6,7 +6,7 @@ const UserProfile = {
|
|||
created () {
|
||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
||||
if (!this.user) {
|
||||
if (!this.user.id) {
|
||||
this.$store.dispatch('fetchUser', this.fetchBy)
|
||||
}
|
||||
},
|
||||
|
@ -29,14 +29,20 @@ const UserProfile = {
|
|||
followers () {
|
||||
return this.user.followers
|
||||
},
|
||||
userInStore () {
|
||||
if (this.isExternal) {
|
||||
return this.$store.getters.userById(this.userId)
|
||||
}
|
||||
return this.$store.getters.userByName(this.userName)
|
||||
},
|
||||
user () {
|
||||
if (this.timeline.statuses[0]) {
|
||||
return this.timeline.statuses[0].user
|
||||
} else {
|
||||
return Object.values(this.$store.state.users.usersObject).filter(user => {
|
||||
return (this.isExternal ? user.id === this.userId : user.screen_name === this.userName)
|
||||
})[0] || {}
|
||||
}
|
||||
if (this.userInStore) {
|
||||
return this.userInStore
|
||||
}
|
||||
return {}
|
||||
},
|
||||
fetchBy () {
|
||||
return this.isExternal ? this.userId : this.userName
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
flex: 2;
|
||||
flex-basis: 500px;
|
||||
|
||||
.panel-heading {
|
||||
.profile-panel-background .panel-heading {
|
||||
background: transparent;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -257,7 +257,7 @@ const UserSettings = {
|
|||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.$store.dispatch('logout')
|
||||
this.$router.push('/~/main/all')
|
||||
this.$router.push({name: 'root'})
|
||||
} else {
|
||||
this.deleteAccountError = res.error
|
||||
}
|
||||
|
|
|
@ -1,63 +1,34 @@
|
|||
import apiService from '../../services/api/api.service.js'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import _ from 'lodash'
|
||||
|
||||
function showWhoToFollow (panel, reply) {
|
||||
var users = reply
|
||||
var cn
|
||||
var index
|
||||
var step = 7
|
||||
cn = Math.floor(Math.random() * step)
|
||||
for (index = 0; index < 3; index++) {
|
||||
var user
|
||||
user = users[cn]
|
||||
var img
|
||||
if (user.avatar) {
|
||||
img = user.avatar
|
||||
} else {
|
||||
img = '/images/avi.png'
|
||||
}
|
||||
var name = user.acct
|
||||
if (index === 0) {
|
||||
panel.img1 = img
|
||||
panel.name1 = name
|
||||
panel.$store.state.api.backendInteractor.externalProfile(name)
|
||||
.then((externalUser) => {
|
||||
if (!externalUser.error) {
|
||||
panel.$store.commit('addNewUsers', [externalUser])
|
||||
panel.id1 = externalUser.id
|
||||
}
|
||||
})
|
||||
} else if (index === 1) {
|
||||
panel.img2 = img
|
||||
panel.name2 = name
|
||||
panel.$store.state.api.backendInteractor.externalProfile(name)
|
||||
.then((externalUser) => {
|
||||
if (!externalUser.error) {
|
||||
panel.$store.commit('addNewUsers', [externalUser])
|
||||
panel.id2 = externalUser.id
|
||||
}
|
||||
})
|
||||
} else if (index === 2) {
|
||||
panel.img3 = img
|
||||
panel.name3 = name
|
||||
panel.$store.state.api.backendInteractor.externalProfile(name)
|
||||
.then((externalUser) => {
|
||||
if (!externalUser.error) {
|
||||
panel.$store.commit('addNewUsers', [externalUser])
|
||||
panel.id3 = externalUser.id
|
||||
}
|
||||
})
|
||||
}
|
||||
cn = (cn + step) % users.length
|
||||
}
|
||||
_.shuffle(reply)
|
||||
|
||||
panel.usersToFollow.forEach((toFollow, index) => {
|
||||
let user = reply[index]
|
||||
let img = user.avatar || '/images/avi.png'
|
||||
let name = user.acct
|
||||
|
||||
toFollow.img = img
|
||||
toFollow.name = name
|
||||
|
||||
panel.$store.state.api.backendInteractor.externalProfile(name)
|
||||
.then((externalUser) => {
|
||||
if (!externalUser.error) {
|
||||
panel.$store.commit('addNewUsers', [externalUser])
|
||||
toFollow.id = externalUser.id
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function getWhoToFollow (panel) {
|
||||
var credentials = panel.$store.state.users.currentUser.credentials
|
||||
if (credentials) {
|
||||
panel.name1 = 'Loading...'
|
||||
panel.name2 = 'Loading...'
|
||||
panel.name3 = 'Loading...'
|
||||
panel.usersToFollow.forEach(toFollow => {
|
||||
toFollow.name = 'Loading...'
|
||||
})
|
||||
apiService.suggestions({credentials: credentials})
|
||||
.then((reply) => {
|
||||
showWhoToFollow(panel, reply)
|
||||
|
@ -67,27 +38,24 @@ function getWhoToFollow (panel) {
|
|||
|
||||
const WhoToFollowPanel = {
|
||||
data: () => ({
|
||||
img1: '/images/avi.png',
|
||||
name1: '',
|
||||
id1: 0,
|
||||
img2: '/images/avi.png',
|
||||
name2: '',
|
||||
id2: 0,
|
||||
img3: '/images/avi.png',
|
||||
name3: '',
|
||||
id3: 0
|
||||
usersToFollow: new Array(3).fill().map(x => (
|
||||
{
|
||||
img: '/images/avi.png',
|
||||
name: '',
|
||||
id: 0
|
||||
}
|
||||
))
|
||||
}),
|
||||
computed: {
|
||||
user: function () {
|
||||
return this.$store.state.users.currentUser.screen_name
|
||||
},
|
||||
moreUrl: function () {
|
||||
var host = window.location.hostname
|
||||
var user = this.user
|
||||
var suggestionsWeb = this.$store.state.instance.suggestionsWeb
|
||||
var url
|
||||
url = suggestionsWeb.replace(/{{host}}/g, encodeURIComponent(host))
|
||||
url = url.replace(/{{user}}/g, encodeURIComponent(user))
|
||||
const host = window.location.hostname
|
||||
const user = this.user
|
||||
const suggestionsWeb = this.$store.state.instance.suggestionsWeb
|
||||
const url = suggestionsWeb.replace(/{{host}}/g, encodeURIComponent(host))
|
||||
.replace(/{{user}}/g, encodeURIComponent(user))
|
||||
return url
|
||||
},
|
||||
suggestionsEnabled () {
|
||||
|
@ -96,7 +64,7 @@ const WhoToFollowPanel = {
|
|||
},
|
||||
methods: {
|
||||
userProfileLink (id, name) {
|
||||
return generateProfileLink(id, name)
|
||||
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="panel-body who-to-follow">
|
||||
<p>
|
||||
<img v-bind:src="img1"/> <router-link :to="userProfileLink(id1, name1)">{{ name1 }}</router-link><br>
|
||||
<img v-bind:src="img2"/> <router-link :to="userProfileLink(id2, name2)">{{ name2 }}</router-link><br>
|
||||
<img v-bind:src="img3"/> <router-link :to="userProfileLink(id3, name3)">{{ name3 }}</router-link><br>
|
||||
<img v-bind:src="$store.state.instance.logo"> <a v-bind:href="moreUrl" target="_blank">{{$t('who_to_follow.more')}}</a>
|
||||
</p>
|
||||
<span v-for="user in usersToFollow">
|
||||
<img v-bind:src="user.img" />
|
||||
<router-link v-bind:to="userProfileLink(user.id, user.name)">
|
||||
{{user.name}}
|
||||
</router-link><br />
|
||||
</span>
|
||||
<img v-bind:src="$store.state.instance.logo"> <a v-bind:href="moreUrl" target="_blank">{{$t('who_to_follow.more')}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -28,7 +29,9 @@
|
|||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.who-to-follow p {
|
||||
.who-to-follow {
|
||||
padding: 0.5em 1em 0.5em 1em;
|
||||
margin: 0px;
|
||||
line-height: 40px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
175
src/i18n/de.json
175
src/i18n/de.json
|
@ -6,7 +6,7 @@
|
|||
"chat": "Chat",
|
||||
"gopher": "Gopher",
|
||||
"media_proxy": "Media Proxy",
|
||||
"scope_options": "Scope options",
|
||||
"scope_options": "Reichweitenoptionen",
|
||||
"text_limit": "Textlimit",
|
||||
"title": "Features",
|
||||
"who_to_follow": "Who to follow"
|
||||
|
@ -29,12 +29,16 @@
|
|||
"username": "Benutzername"
|
||||
},
|
||||
"nav": {
|
||||
"back": "Zurück",
|
||||
"chat": "Lokaler Chat",
|
||||
"friend_requests": "Followanfragen",
|
||||
"mentions": "Erwähnungen",
|
||||
"public_tl": "Lokale Zeitleiste",
|
||||
"dms": "Direktnachrichten",
|
||||
"public_tl": "Öffentliche Zeitleiste",
|
||||
"timeline": "Zeitleiste",
|
||||
"twkn": "Das gesamte bekannte Netzwerk"
|
||||
"twkn": "Das gesamte bekannte Netzwerk",
|
||||
"user_search": "Benutzersuche",
|
||||
"preferences": "Voreinstellungen"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "Unbekannte Nachricht, suche danach...",
|
||||
|
@ -46,6 +50,7 @@
|
|||
"repeated_you": "wiederholte deine Nachricht"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "Neuen Status veröffentlichen",
|
||||
"account_not_locked_warning": "Dein Profil ist nicht {0}. Wer dir folgen will, kann das jederzeit tun und dann auch deine privaten Beiträge sehen.",
|
||||
"account_not_locked_warning_link": "gesperrt",
|
||||
"attachments_sensitive": "Anhänge als heikel markieren",
|
||||
|
@ -69,7 +74,17 @@
|
|||
"fullname": "Angezeigter Name",
|
||||
"password_confirm": "Passwort bestätigen",
|
||||
"registration": "Registrierung",
|
||||
"token": "Einladungsschlüssel"
|
||||
"token": "Einladungsschlüssel",
|
||||
"captcha": "CAPTCHA",
|
||||
"new_captcha": "Zum Erstellen eines neuen Captcha auf das Bild klicken.",
|
||||
"validations": {
|
||||
"username_required": "darf nicht leer sein",
|
||||
"fullname_required": "darf nicht leer sein",
|
||||
"email_required": "darf nicht leer sein",
|
||||
"password_required": "darf nicht leer sein",
|
||||
"password_confirmation_required": "darf nicht leer sein",
|
||||
"password_confirmation_match": "sollte mit dem Passwort identisch sein."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"attachmentRadius": "Anhänge",
|
||||
|
@ -89,6 +104,7 @@
|
|||
"change_password_error": "Es gab ein Problem bei der Änderung des Passworts.",
|
||||
"changed_password": "Passwort erfolgreich geändert!",
|
||||
"collapse_subject": "Beiträge mit Betreff einklappen",
|
||||
"composing": "Verfassen",
|
||||
"confirm_new_password": "Neues Passwort bestätigen",
|
||||
"current_avatar": "Dein derzeitiger Avatar",
|
||||
"current_password": "Aktuelles Passwort",
|
||||
|
@ -112,12 +128,17 @@
|
|||
"general": "Allgemein",
|
||||
"hide_attachments_in_convo": "Anhänge in Unterhaltungen ausblenden",
|
||||
"hide_attachments_in_tl": "Anhänge in der Zeitleiste ausblenden",
|
||||
"hide_isp": "Instanz-spezifisches Panel ausblenden",
|
||||
"preload_images": "Bilder vorausladen",
|
||||
"hide_post_stats": "Beitragsstatistiken verbergen (z.B. die Anzahl der Favoriten)",
|
||||
"hide_user_stats": "Benutzerstatistiken verbergen (z.B. die Anzahl der Follower)",
|
||||
"import_followers_from_a_csv_file": "Importiere Follower, denen du folgen möchtest, aus einer CSV-Datei",
|
||||
"import_theme": "Farbschema laden",
|
||||
"inputRadius": "Eingabefelder",
|
||||
"checkboxRadius": "Auswahlfelder",
|
||||
"instance_default": "(Standard: {value})",
|
||||
"instance_default_simple": "(Standard)",
|
||||
"interface": "Oberfläche",
|
||||
"interfaceLanguage": "Sprache der Oberfläche",
|
||||
"invalid_theme_imported": "Die ausgewählte Datei ist kein unterstütztes Pleroma-Theme. Keine Änderungen wurden vorgenommen.",
|
||||
"limited_availability": "In deinem Browser nicht verfügbar",
|
||||
|
@ -134,6 +155,7 @@
|
|||
"notification_visibility_mentions": "Erwähnungen",
|
||||
"notification_visibility_repeats": "Wiederholungen",
|
||||
"no_rich_text_description": "Rich-Text Formatierungen von allen Beiträgen entfernen",
|
||||
"hide_network_description": "Zeige nicht, wem ich folge und wer mir folgt",
|
||||
"nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind",
|
||||
"panelRadius": "Panel",
|
||||
"pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist",
|
||||
|
@ -150,20 +172,139 @@
|
|||
"saving_err": "Fehler beim Speichern der Einstellungen",
|
||||
"saving_ok": "Einstellungen gespeichert",
|
||||
"security_tab": "Sicherheit",
|
||||
"scope_copy": "Reichweite beim Antworten übernehmen (Direktnachrichten werden immer kopiert)",
|
||||
"set_new_avatar": "Setze einen neuen Avatar",
|
||||
"set_new_profile_background": "Setze einen neuen Hintergrund für dein Profil",
|
||||
"set_new_profile_banner": "Setze einen neuen Banner für dein Profil",
|
||||
"settings": "Einstellungen",
|
||||
"subject_input_always_show": "Betreff-Feld immer anzeigen",
|
||||
"subject_line_behavior": "Betreff beim Antworten kopieren",
|
||||
"subject_line_email": "Wie Email: \"re: Betreff\"",
|
||||
"subject_line_mastodon": "Wie Mastodon: unverändert kopieren",
|
||||
"subject_line_noop": "Nicht kopieren",
|
||||
"stop_gifs": "Play-on-hover GIFs",
|
||||
"streaming": "Aktiviere automatisches Laden (Streaming) von neuen Beiträgen",
|
||||
"text": "Text",
|
||||
"theme": "Farbschema",
|
||||
"theme_help": "Benutze HTML-Farbcodes (#rrggbb) um dein Farbschema anzupassen",
|
||||
"theme_help_v2_1": "Du kannst auch die Farben und die Deckkraft bestimmter Komponenten überschreiben, indem du das Kontrollkästchen umschaltest. Verwende die Schaltfläche \"Alle löschen\", um alle Überschreibungen zurückzusetzen.",
|
||||
"theme_help_v2_2": "Unter einigen Einträgen befinden sich Symbole für Hintergrund-/Textkontrastindikatoren, für detaillierte Informationen fahre mit der Maus darüber. Bitte beachte, dass bei der Verwendung von Transparenz Kontrastindikatoren den schlechtest möglichen Fall darstellen.",
|
||||
"tooltipRadius": "Tooltips/Warnungen",
|
||||
"user_settings": "Benutzereinstellungen",
|
||||
"values": {
|
||||
"false": "nein",
|
||||
"true": "Ja"
|
||||
},
|
||||
"notifications": "Benachrichtigungen",
|
||||
"enable_web_push_notifications": "Web-Pushbenachrichtigungen aktivieren",
|
||||
"style": {
|
||||
"switcher": {
|
||||
"keep_color": "Farben beibehalten",
|
||||
"keep_shadows": "Schatten beibehalten",
|
||||
"keep_opacity": "Deckkraft beibehalten",
|
||||
"keep_roundness": "Abrundungen beibehalten",
|
||||
"keep_fonts": "Schriften beibehalten",
|
||||
"save_load_hint": "Die \"Beibehalten\"-Optionen behalten die aktuell eingestellten Optionen beim Auswählen oder Laden von Designs bei, sie speichern diese Optionen auch beim Exportieren eines Designs. Wenn alle Kontrollkästchen deaktiviert sind, wird beim Exportieren des Designs alles gespeichert.",
|
||||
"reset": "Zurücksetzen",
|
||||
"clear_all": "Alles leeren",
|
||||
"clear_opacity": "Deckkraft leeren"
|
||||
},
|
||||
"common": {
|
||||
"color": "Farbe",
|
||||
"opacity": "Deckkraft",
|
||||
"contrast": {
|
||||
"hint": "Das Kontrastverhältnis ist {ratio}, es {level} {context}",
|
||||
"level": {
|
||||
"aa": "entspricht Level AA Richtlinie (minimum)",
|
||||
"aaa": "entspricht Level AAA Richtlinie (empfohlen)",
|
||||
"bad": "entspricht keiner Richtlinien zur Barrierefreiheit"
|
||||
},
|
||||
"context": {
|
||||
"18pt": "für großen (18pt+) Text",
|
||||
"text": "für Text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common_colors": {
|
||||
"_tab_label": "Allgemein",
|
||||
"main": "Allgemeine Farben",
|
||||
"foreground_hint": "Siehe Reiter \"Erweitert\" für eine detailliertere Einstellungen",
|
||||
"rgbo": "Symbole, Betonungen, Kennzeichnungen"
|
||||
},
|
||||
"advanced_colors": {
|
||||
"_tab_label": "Erweitert",
|
||||
"alert": "Warnhinweis-Hintergrund",
|
||||
"alert_error": "Fehler",
|
||||
"badge": "Kennzeichnungs-Hintergrund",
|
||||
"badge_notification": "Benachrichtigung",
|
||||
"panel_header": "Panel-Kopf",
|
||||
"top_bar": "Obere Leiste",
|
||||
"borders": "Rahmen",
|
||||
"buttons": "Schaltflächen",
|
||||
"inputs": "Eingabefelder",
|
||||
"faint_text": "Verblasster Text"
|
||||
},
|
||||
"radii": {
|
||||
"_tab_label": "Abrundungen"
|
||||
},
|
||||
"shadows": {
|
||||
"_tab_label": "Schatten und Beleuchtung",
|
||||
"component": "Komponente",
|
||||
"override": "Überschreiben",
|
||||
"shadow_id": "Schatten #{value}",
|
||||
"blur": "Unschärfe",
|
||||
"spread": "Streuung",
|
||||
"inset": "Einsatz",
|
||||
"hint": "Für Schatten kannst du auch --variable als Farbwert verwenden, um CSS3-Variablen zu verwenden. Bitte beachte, dass die Einstellung der Deckkraft in diesem Fall nicht funktioniert.",
|
||||
"filter_hint": {
|
||||
"always_drop_shadow": "Achtung, dieser Schatten verwendet immer {0}, wenn der Browser dies unterstützt.",
|
||||
"drop_shadow_syntax": "{0} unterstützt Parameter {1} und Schlüsselwort {2} nicht.",
|
||||
"avatar_inset": "Bitte beachte, dass die Kombination von eingesetzten und nicht eingesetzten Schatten auf Avataren zu unerwarteten Ergebnissen bei transparenten Avataren führen kann.",
|
||||
"spread_zero": "Schatten mit einer Streuung > 0 erscheinen so, als ob sie auf Null gesetzt wären.",
|
||||
"inset_classic": "Eingesetzte Schatten werden mit {0} verwendet"
|
||||
},
|
||||
"components": {
|
||||
"panel": "Panel",
|
||||
"panelHeader": "Panel-Kopf",
|
||||
"topBar": "Obere Leiste",
|
||||
"avatar": "Benutzer-Avatar (in der Profilansicht)",
|
||||
"avatarStatus": "Benutzer-Avatar (in der Beitragsanzeige)",
|
||||
"popup": "Dialogfenster und Hinweistexte",
|
||||
"button": "Schaltfläche",
|
||||
"buttonHover": "Schaltfläche (hover)",
|
||||
"buttonPressed": "Schaltfläche (gedrückt)",
|
||||
"buttonPressedHover": "Schaltfläche (gedrückt+hover)",
|
||||
"input": "Input field"
|
||||
}
|
||||
},
|
||||
"fonts": {
|
||||
"_tab_label": "Schriften",
|
||||
"help": "Wähl die Schriftart, die für Elemente der Benutzeroberfläche verwendet werden soll. Für \" Benutzerdefiniert\" musst du den genauen Schriftnamen eingeben, wie er im System angezeigt wird.",
|
||||
"components": {
|
||||
"interface": "Oberfläche",
|
||||
"input": "Eingabefelder",
|
||||
"post": "Beitragstext",
|
||||
"postCode": "Dicktengleicher Text in einem Beitrag (Rich-Text)"
|
||||
},
|
||||
"family": "Schriftname",
|
||||
"size": "Größe (in px)",
|
||||
"weight": "Gewicht (Dicke)",
|
||||
"custom": "Benutzerdefiniert"
|
||||
},
|
||||
"preview": {
|
||||
"header": "Vorschau",
|
||||
"content": "Inhalt",
|
||||
"error": "Beispielfehler",
|
||||
"button": "Schaltfläche",
|
||||
"text": "Ein Haufen mehr von {0} und {1}",
|
||||
"mono": "Inhalt",
|
||||
"input": "Sitze gerade im Hofbräuhaus.",
|
||||
"faint_link": "Hilfreiche Anleitung",
|
||||
"fine_print": "Lies unser {0}, um nichts Nützliches zu lernen!",
|
||||
"header_faint": "Das ist in Ordnung",
|
||||
"checkbox": "Ich habe die Allgemeinen Geschäftsbedingungen überflogen",
|
||||
"link": "ein netter kleiner Link"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
|
@ -182,10 +323,15 @@
|
|||
"blocked": "Blockiert!",
|
||||
"deny": "Ablehnen",
|
||||
"follow": "Folgen",
|
||||
"follow_sent": "Anfrage gesendet!",
|
||||
"follow_progress": "Anfragen…",
|
||||
"follow_again": "Anfrage erneut senden?",
|
||||
"follow_unfollow": "Folgen beenden",
|
||||
"followees": "Folgt",
|
||||
"followers": "Followers",
|
||||
"following": "Folgst du!",
|
||||
"follows_you": "Folgt dir!",
|
||||
"its_you": "Das bist du!",
|
||||
"mute": "Stummschalten",
|
||||
"muted": "Stummgeschaltet",
|
||||
"per_day": "pro Tag",
|
||||
|
@ -198,5 +344,26 @@
|
|||
"who_to_follow": {
|
||||
"more": "Mehr",
|
||||
"who_to_follow": "Wem soll ich folgen"
|
||||
},
|
||||
"tool_tip": {
|
||||
"media_upload": "Medien hochladen",
|
||||
"repeat": "Wiederholen",
|
||||
"reply": "Antworten",
|
||||
"favorite": "Favorisieren",
|
||||
"user_settings": "Benutzereinstellungen"
|
||||
},
|
||||
"upload":{
|
||||
"error": {
|
||||
"base": "Hochladen fehlgeschlagen.",
|
||||
"file_too_big": "Datei ist zu groß [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
|
||||
"default": "Bitte versuche es später erneut"
|
||||
},
|
||||
"file_size_units": {
|
||||
"B": "B",
|
||||
"KiB": "KiB",
|
||||
"MiB": "MiB",
|
||||
"GiB": "GiB",
|
||||
"TiB": "TiB"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
@ -74,6 +75,8 @@
|
|||
"password_confirm": "Password confirmation",
|
||||
"registration": "Registration",
|
||||
"token": "Invite token",
|
||||
"captcha": "CAPTCHA",
|
||||
"new_captcha": "Click the image to get a new captcha",
|
||||
"validations": {
|
||||
"username_required": "cannot be left blank",
|
||||
"fullname_required": "cannot be left blank",
|
||||
|
|
171
src/i18n/ja.json
171
src/i18n/ja.json
|
@ -29,13 +29,16 @@
|
|||
"username": "ユーザーめい"
|
||||
},
|
||||
"nav": {
|
||||
"back": "もどる",
|
||||
"chat": "ローカルチャット",
|
||||
"friend_requests": "フォローリクエスト",
|
||||
"mentions": "メンション",
|
||||
"dms": "ダイレクトメッセージ",
|
||||
"public_tl": "パブリックタイムライン",
|
||||
"timeline": "タイムライン",
|
||||
"twkn": "つながっているすべてのネットワーク"
|
||||
"twkn": "つながっているすべてのネットワーク",
|
||||
"user_search": "ユーザーをさがす",
|
||||
"preferences": "せってい"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "ステータスがみつかりません。さがしています...",
|
||||
|
@ -70,7 +73,17 @@
|
|||
"fullname": "スクリーンネーム",
|
||||
"password_confirm": "パスワードのかくにん",
|
||||
"registration": "はじめる",
|
||||
"token": "しょうたいトークン"
|
||||
"token": "しょうたいトークン",
|
||||
"captcha": "CAPTCHA",
|
||||
"new_captcha": "もじがよめないときは、がぞうをクリックすると、あたらしいがぞうになります",
|
||||
"validations": {
|
||||
"username_required": "なにかかいてください",
|
||||
"fullname_required": "なにかかいてください",
|
||||
"email_required": "なにかかいてください",
|
||||
"password_required": "なにかかいてください",
|
||||
"password_confirmation_required": "なにかかいてください",
|
||||
"password_confirmation_match": "パスワードがちがいます"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"attachmentRadius": "ファイル",
|
||||
|
@ -90,6 +103,7 @@
|
|||
"change_password_error": "パスワードをかえることが、できなかったかもしれません。",
|
||||
"changed_password": "パスワードが、かわりました!",
|
||||
"collapse_subject": "せつめいのあるとうこうをたたむ",
|
||||
"composing": "とうこう",
|
||||
"confirm_new_password": "あたらしいパスワードのかくにん",
|
||||
"current_avatar": "いまのアバター",
|
||||
"current_password": "いまのパスワード",
|
||||
|
@ -113,17 +127,22 @@
|
|||
"general": "ぜんぱん",
|
||||
"hide_attachments_in_convo": "スレッドのファイルをかくす",
|
||||
"hide_attachments_in_tl": "タイムラインのファイルをかくす",
|
||||
"hide_isp": "インスタンススペシフィックパネルをかくす",
|
||||
"preload_images": "がぞうをさきよみする",
|
||||
"hide_post_stats": "とうこうのとうけいをかくす (れい: おきにいりのかず)",
|
||||
"hide_user_stats": "ユーザーのとうけいをかくす (れい: フォロワーのかず)",
|
||||
"import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする",
|
||||
"import_theme": "ロード",
|
||||
"inputRadius": "インプットフィールド",
|
||||
"checkboxRadius": "チェックボックス",
|
||||
"instance_default": "(デフォルト: {value})",
|
||||
"instance_default_simple": "(デフォルト)",
|
||||
"interface": "インターフェース",
|
||||
"interfaceLanguage": "インターフェースのことば",
|
||||
"invalid_theme_imported": "このファイルはPleromaのテーマではありません。テーマはへんこうされませんでした。",
|
||||
"limited_availability": "あなたのブラウザではできません",
|
||||
"links": "リンク",
|
||||
"lock_account_description": "あなたがみとめたひとだけ、あなたのアカウントをフォローできます",
|
||||
"lock_account_description": "あなたがみとめたひとだけ、あなたのアカウントをフォローできる",
|
||||
"loop_video": "ビデオをくりかえす",
|
||||
"loop_video_silent_only": "おとのないビデオだけくりかえす",
|
||||
"name": "なまえ",
|
||||
|
@ -135,6 +154,7 @@
|
|||
"notification_visibility_mentions": "メンション",
|
||||
"notification_visibility_repeats": "リピート",
|
||||
"no_rich_text_description": "リッチテキストをつかわない",
|
||||
"hide_network_description": "わたしがフォローしているひとと、わたしをフォローしているひとを、みせない",
|
||||
"nsfw_clickthrough": "NSFWなファイルをかくす",
|
||||
"panelRadius": "パネル",
|
||||
"pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",
|
||||
|
@ -151,20 +171,139 @@
|
|||
"saving_err": "せっていをセーブできませんでした",
|
||||
"saving_ok": "せっていをセーブしました",
|
||||
"security_tab": "セキュリティ",
|
||||
"scope_copy": "リプライするとき、こうかいはんいをコピーする (DMのこうかいはんいは、つねにコピーされます)",
|
||||
"set_new_avatar": "あたらしいアバターをせっていする",
|
||||
"set_new_profile_background": "あたらしいプロフィールのバックグラウンドをせっていする",
|
||||
"set_new_profile_banner": "あたらしいプロフィールバナーを設定する",
|
||||
"settings": "せってい",
|
||||
"subject_input_always_show": "サブジェクトフィールドをいつでもひょうじする",
|
||||
"subject_line_behavior": "リプライするときサブジェクトをコピーする",
|
||||
"subject_line_email": "メールふう: \"re: サブジェクト\"",
|
||||
"subject_line_mastodon": "マストドンふう: そのままコピー",
|
||||
"subject_line_noop": "コピーしない",
|
||||
"stop_gifs": "カーソルをかさねたとき、GIFをうごかす",
|
||||
"streaming": "うえまでスクロールしたとき、じどうてきにストリーミングする",
|
||||
"text": "もじ",
|
||||
"theme": "テーマ",
|
||||
"theme_help": "カラーテーマをカスタマイズできます",
|
||||
"theme_help_v2_1": "チェックボックスをONにすると、コンポーネントごとに、いろと、とうめいどを、オーバーライドできます。「すべてクリア」ボタンをおすと、すべてのオーバーライドを、やめます。",
|
||||
"theme_help_v2_2": "バックグラウンドとテキストのコントラストをあらわすアイコンがあります。マウスをホバーすると、くわしいせつめいがでます。とうめいないろをつかっているときは、もっともわるいばあいのコントラストがしめされます。",
|
||||
"tooltipRadius": "ツールチップとアラート",
|
||||
"user_settings": "ユーザーせってい",
|
||||
"values": {
|
||||
"false": "いいえ",
|
||||
"true": "はい"
|
||||
},
|
||||
"notifications": "つうち",
|
||||
"enable_web_push_notifications": "ウェブプッシュつうちをゆるす",
|
||||
"style": {
|
||||
"switcher": {
|
||||
"keep_color": "いろをのこす",
|
||||
"keep_shadows": "かげをのこす",
|
||||
"keep_opacity": "とうめいどをのこす",
|
||||
"keep_roundness": "まるさをのこす",
|
||||
"keep_fonts": "フォントをのこす",
|
||||
"save_load_hint": "「のこす」オプションをONにすると、テーマをえらんだときとロードしたとき、いまのせっていをのこします。また、テーマをエクスポートするとき、これらのオプションをストアします。すべてのチェックボックスをOFFにすると、テーマをエクスポートしたとき、すべてのせっていをセーブします。",
|
||||
"reset": "リセット",
|
||||
"clear_all": "すべてクリア",
|
||||
"clear_opacity": "とうめいどをクリア"
|
||||
},
|
||||
"common": {
|
||||
"color": "いろ",
|
||||
"opacity": "とうめいど",
|
||||
"contrast": {
|
||||
"hint": "コントラストは {ratio} です。{level}。({context})",
|
||||
"level": {
|
||||
"aa": "AAレベルガイドライン (ミニマル) をみたします",
|
||||
"aaa": "AAAレベルガイドライン (レコメンデッド) をみたします。",
|
||||
"bad": "ガイドラインをみたしません。"
|
||||
},
|
||||
"context": {
|
||||
"18pt": "おおきい (18ポイントいじょう) テキスト",
|
||||
"text": "テキスト"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common_colors": {
|
||||
"_tab_label": "きょうつう",
|
||||
"main": "きょうつうのいろ",
|
||||
"foreground_hint": "「くわしく」タブで、もっとこまかくせっていできます",
|
||||
"rgbo": "アイコンとアクセントとバッジ"
|
||||
},
|
||||
"advanced_colors": {
|
||||
"_tab_label": "くわしく",
|
||||
"alert": "アラートのバックグラウンド",
|
||||
"alert_error": "エラー",
|
||||
"badge": "バッジのバックグラウンド",
|
||||
"badge_notification": "つうち",
|
||||
"panel_header": "パネルヘッダー",
|
||||
"top_bar": "トップバー",
|
||||
"borders": "さかいめ",
|
||||
"buttons": "ボタン",
|
||||
"inputs": "インプットフィールド",
|
||||
"faint_text": "うすいテキスト"
|
||||
},
|
||||
"radii": {
|
||||
"_tab_label": "まるさ"
|
||||
},
|
||||
"shadows": {
|
||||
"_tab_label": "ひかりとかげ",
|
||||
"component": "コンポーネント",
|
||||
"override": "オーバーライド",
|
||||
"shadow_id": "かげ #{value}",
|
||||
"blur": "ぼかし",
|
||||
"spread": "ひろがり",
|
||||
"inset": "うちがわ",
|
||||
"hint": "かげのせっていでは、いろのあたいとして --variable をつかうことができます。これはCSS3へんすうです。ただし、とうめいどのせっていは、きかなくなります。",
|
||||
"filter_hint": {
|
||||
"always_drop_shadow": "ブラウザーがサポートしていれば、つねに {0} がつかわれます。",
|
||||
"drop_shadow_syntax": "{0} は、{1} パラメーターと {2} キーワードをサポートしていません。",
|
||||
"avatar_inset": "うちがわのかげと、そとがわのかげを、いっしょにつかうと、とうめいなアバターが、へんなみためになります。",
|
||||
"spread_zero": "ひろがりが 0 よりもおおきなかげは、0 とおなじです。",
|
||||
"inset_classic": "うちがわのかげは {0} をつかいます。"
|
||||
},
|
||||
"components": {
|
||||
"panel": "パネル",
|
||||
"panelHeader": "パネルヘッダー",
|
||||
"topBar": "トップバー",
|
||||
"avatar": "ユーザーアバター (プロフィール)",
|
||||
"avatarStatus": "ユーザーアバター (とうこう)",
|
||||
"popup": "ポップアップとツールチップ",
|
||||
"button": "ボタン",
|
||||
"buttonHover": "ボタン (ホバー)",
|
||||
"buttonPressed": "ボタン (おされているとき)",
|
||||
"buttonPressedHover": "ボタン (ホバー、かつ、おされているとき)",
|
||||
"input": "インプットフィールド"
|
||||
}
|
||||
},
|
||||
"fonts": {
|
||||
"_tab_label": "フォント",
|
||||
"help": "「カスタム」をえらんだときは、システムにあるフォントのなまえを、ただしくにゅうりょくしてください。",
|
||||
"components": {
|
||||
"interface": "インターフェース",
|
||||
"input": "インプットフィールド",
|
||||
"post": "とうこう",
|
||||
"postCode": "モノスペース (とうこうがリッチテキストであるとき)"
|
||||
},
|
||||
"family": "フォントめい",
|
||||
"size": "おおきさ (px)",
|
||||
"weight": "ふとさ",
|
||||
"custom": "カスタム"
|
||||
},
|
||||
"preview": {
|
||||
"header": "プレビュー",
|
||||
"content": "ほんぶん",
|
||||
"error": "エラーのれい",
|
||||
"button": "ボタン",
|
||||
"text": "これは{0}と{1}のれいです。",
|
||||
"mono": "monospace",
|
||||
"input": "はねだくうこうに、つきました。",
|
||||
"faint_link": "とてもたすけになるマニュアル",
|
||||
"fine_print": "わたしたちの{0}を、よまないでください!",
|
||||
"header_faint": "エラーではありません",
|
||||
"checkbox": "りようきやくを、よみました",
|
||||
"link": "ハイパーリンク"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
|
@ -183,10 +322,15 @@
|
|||
"blocked": "ブロックしています!",
|
||||
"deny": "おことわり",
|
||||
"follow": "フォロー",
|
||||
"follow_sent": "リクエストを、おくりました!",
|
||||
"follow_progress": "リクエストしています…",
|
||||
"follow_again": "ふたたびリクエストをおくりますか?",
|
||||
"follow_unfollow": "フォローをやめる",
|
||||
"followees": "フォロー",
|
||||
"followers": "フォロワー",
|
||||
"following": "フォローしています!",
|
||||
"follows_you": "フォローされました!",
|
||||
"its_you": "これはあなたです!",
|
||||
"mute": "ミュート",
|
||||
"muted": "ミュートしています!",
|
||||
"per_day": "/日",
|
||||
|
@ -199,5 +343,26 @@
|
|||
"who_to_follow": {
|
||||
"more": "くわしく",
|
||||
"who_to_follow": "おすすめユーザー"
|
||||
},
|
||||
"tool_tip": {
|
||||
"media_upload": "メディアをアップロード",
|
||||
"repeat": "リピート",
|
||||
"reply": "リプライ",
|
||||
"favorite": "おきにいり",
|
||||
"user_settings": "ユーザーせってい"
|
||||
},
|
||||
"upload":{
|
||||
"error": {
|
||||
"base": "アップロードにしっぱいしました。",
|
||||
"file_too_big": "ファイルがおおきすぎます [{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]",
|
||||
"default": "しばらくしてから、ためしてください"
|
||||
},
|
||||
"file_size_units": {
|
||||
"B": "B",
|
||||
"KiB": "KiB",
|
||||
"MiB": "MiB",
|
||||
"GiB": "GiB",
|
||||
"TiB": "TiB"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
22
src/lib/push_notifications_plugin.js
Normal file
22
src/lib/push_notifications_plugin.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
export default (store) => {
|
||||
store.subscribe((mutation, state) => {
|
||||
const vapidPublicKey = state.instance.vapidPublicKey
|
||||
const webPushNotification = state.config.webPushNotifications
|
||||
const permission = state.interface.notificationPermission === 'granted'
|
||||
const user = state.users.currentUser
|
||||
|
||||
const isUserMutation = mutation.type === 'setCurrentUser'
|
||||
const isVapidMutation = mutation.type === 'setInstanceOption' && mutation.payload.name === 'vapidPublicKey'
|
||||
const isPermMutation = mutation.type === 'setNotificationPermission' && mutation.payload === 'granted'
|
||||
const isUserConfigMutation = mutation.type === 'setOption' && mutation.payload.name === 'webPushNotifications'
|
||||
const isVisibilityMutation = mutation.type === 'setOption' && mutation.payload.name === 'notificationVisibility'
|
||||
|
||||
if (isUserMutation || isVapidMutation || isPermMutation || isUserConfigMutation || isVisibilityMutation) {
|
||||
if (user && vapidPublicKey && permission && webPushNotification) {
|
||||
return store.dispatch('registerPushNotifications')
|
||||
} else if (isUserConfigMutation && !webPushNotification) {
|
||||
return store.dispatch('unregisterPushNotifications')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
28
src/main.js
28
src/main.js
|
@ -15,6 +15,7 @@ import VueTimeago from 'vue-timeago'
|
|||
import VueI18n from 'vue-i18n'
|
||||
|
||||
import createPersistedState from './lib/persisted_state.js'
|
||||
import pushNotifications from './lib/push_notifications_plugin.js'
|
||||
|
||||
import messages from './i18n/messages.js'
|
||||
|
||||
|
@ -51,31 +52,6 @@ const persistedStateOptions = {
|
|||
]
|
||||
}
|
||||
|
||||
const registerPushNotifications = store => {
|
||||
store.subscribe((mutation, state) => {
|
||||
const vapidPublicKey = state.instance.vapidPublicKey
|
||||
const permission = state.interface.notificationPermission === 'granted'
|
||||
const isUserMutation = mutation.type === 'setCurrentUser'
|
||||
|
||||
if (isUserMutation && vapidPublicKey && permission) {
|
||||
return store.dispatch('registerPushNotifications')
|
||||
}
|
||||
|
||||
const user = state.users.currentUser
|
||||
const isVapidMutation = mutation.type === 'setInstanceOption' && mutation.payload.name === 'vapidPublicKey'
|
||||
|
||||
if (isVapidMutation && user && permission) {
|
||||
return store.dispatch('registerPushNotifications')
|
||||
}
|
||||
|
||||
const isPermMutation = mutation.type === 'setNotificationPermission' && mutation.payload === 'granted'
|
||||
|
||||
if (isPermMutation && user && vapidPublicKey) {
|
||||
return store.dispatch('registerPushNotifications')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
createPersistedState(persistedStateOptions).then((persistedState) => {
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
|
@ -88,7 +64,7 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
|
|||
chat: chatModule,
|
||||
oauth: oauthModule
|
||||
},
|
||||
plugins: [persistedState, registerPushNotifications],
|
||||
plugins: [persistedState, pushNotifications],
|
||||
strict: false // Socket modifies itself, let's ignore this for now.
|
||||
// strict: process.env.NODE_ENV !== 'production'
|
||||
})
|
||||
|
|
|
@ -24,7 +24,7 @@ const defaultState = {
|
|||
likes: true,
|
||||
repeats: true
|
||||
},
|
||||
webPushNotifications: true,
|
||||
webPushNotifications: false,
|
||||
muteWords: [],
|
||||
highlight: {},
|
||||
interfaceLanguage: browserLocale,
|
||||
|
|
|
@ -12,8 +12,8 @@ const defaultState = {
|
|||
logo: '/static/logo.png',
|
||||
logoMask: true,
|
||||
logoMargin: '.2em',
|
||||
redirectRootNoLogin: '/~/main/all',
|
||||
redirectRootLogin: '/~/main/friends',
|
||||
redirectRootNoLogin: '/main/all',
|
||||
redirectRootLogin: '/main/friends',
|
||||
showInstanceSpecificPanel: false,
|
||||
scopeOptionsEnabled: true,
|
||||
formattingOptionsEnabled: false,
|
||||
|
@ -27,11 +27,13 @@ const defaultState = {
|
|||
loginMethod: 'password',
|
||||
nsfwCensorImage: undefined,
|
||||
vapidPublicKey: undefined,
|
||||
noAttachmentLinks: false,
|
||||
|
||||
// Nasty stuff
|
||||
pleromaBackend: true,
|
||||
emoji: [],
|
||||
customEmoji: [],
|
||||
restrictedNicknames: [],
|
||||
|
||||
// Feature-set, apparently, not everything here is reported...
|
||||
mediaProxyAvailable: false,
|
||||
|
|
|
@ -27,6 +27,7 @@ export const defaultState = {
|
|||
maxId: 0,
|
||||
minId: Number.POSITIVE_INFINITY,
|
||||
data: [],
|
||||
idStore: {},
|
||||
error: false
|
||||
},
|
||||
favorites: new Set(),
|
||||
|
@ -307,6 +308,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
|||
}
|
||||
|
||||
state.notifications.data.push(result)
|
||||
state.notifications.idStore[notification.id] = result
|
||||
|
||||
if ('Notification' in window && window.Notification.permission === 'granted') {
|
||||
const title = action.user.name
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { compact, map, each, merge } from 'lodash'
|
||||
import { set } from 'vue'
|
||||
import registerPushNotifications from '../services/push/push.js'
|
||||
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
|
||||
import oauthApi from '../services/new_api/oauth'
|
||||
import { humanizeErrors } from './errors'
|
||||
|
||||
|
@ -66,6 +66,9 @@ export const mutations = {
|
|||
setUserForStatus (state, status) {
|
||||
status.user = state.usersObject[status.user.id]
|
||||
},
|
||||
setUserForNotification (state, notification) {
|
||||
notification.action.user = state.usersObject[notification.action.user.id]
|
||||
},
|
||||
setColor (state, { user: { id }, highlighted }) {
|
||||
const user = state.usersObject[id]
|
||||
set(user, 'highlight', highlighted)
|
||||
|
@ -83,6 +86,13 @@ export const mutations = {
|
|||
}
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
userById: state => id =>
|
||||
state.users.find(user => user.id === id),
|
||||
userByName: state => name =>
|
||||
state.users.find(user => user.screen_name === name)
|
||||
}
|
||||
|
||||
export const defaultState = {
|
||||
loggingIn: false,
|
||||
lastLoginName: false,
|
||||
|
@ -96,6 +106,7 @@ export const defaultState = {
|
|||
const users = {
|
||||
state: defaultState,
|
||||
mutations,
|
||||
getters,
|
||||
actions: {
|
||||
fetchUser (store, id) {
|
||||
store.rootState.api.backendInteractor.fetchUser({ id })
|
||||
|
@ -113,8 +124,14 @@ const users = {
|
|||
const token = store.state.currentUser.credentials
|
||||
const vapidPublicKey = store.rootState.instance.vapidPublicKey
|
||||
const isEnabled = store.rootState.config.webPushNotifications
|
||||
const notificationVisibility = store.rootState.config.notificationVisibility
|
||||
|
||||
registerPushNotifications(isEnabled, vapidPublicKey, token)
|
||||
registerPushNotifications(isEnabled, vapidPublicKey, token, notificationVisibility)
|
||||
},
|
||||
unregisterPushNotifications (store) {
|
||||
const token = store.state.currentUser.credentials
|
||||
|
||||
unregisterPushNotifications(token)
|
||||
},
|
||||
addNewStatuses (store, { statuses }) {
|
||||
const users = map(statuses, 'user')
|
||||
|
@ -131,6 +148,21 @@ const users = {
|
|||
store.commit('setUserForStatus', status)
|
||||
})
|
||||
},
|
||||
addNewNotifications (store, { notifications }) {
|
||||
const users = compact(map(notifications, 'from_profile'))
|
||||
const notificationIds = compact(notifications.map(_ => String(_.id)))
|
||||
store.commit('addNewUsers', users)
|
||||
|
||||
const notificationsObject = store.rootState.statuses.notifications.idStore
|
||||
const relevantNotifications = Object.entries(notificationsObject)
|
||||
.filter(([k, val]) => notificationIds.includes(k))
|
||||
.map(([k, val]) => val)
|
||||
|
||||
// Reconnect users to notifications
|
||||
each(relevantNotifications, (notification) => {
|
||||
store.commit('setUserForNotification', notification)
|
||||
})
|
||||
},
|
||||
async signUp (store, userInfo) {
|
||||
store.commit('signUpPending')
|
||||
|
||||
|
|
|
@ -370,12 +370,13 @@ const unretweet = ({ id, credentials }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId}) => {
|
||||
const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType, noAttachmentLinks}) => {
|
||||
const idsText = mediaIds.join(',')
|
||||
const form = new FormData()
|
||||
|
||||
form.append('status', status)
|
||||
form.append('source', 'Pleroma FE')
|
||||
if (noAttachmentLinks) form.append('no_attachment_links', noAttachmentLinks)
|
||||
if (spoilerText) form.append('spoiler_text', spoilerText)
|
||||
if (visibility) form.append('visibility', visibility)
|
||||
if (sensitive) form.append('sensitive', sensitive)
|
||||
|
|
|
@ -5,7 +5,7 @@ const getOrCreateApp = ({oauth, instance}) => {
|
|||
const form = new window.FormData()
|
||||
|
||||
form.append('client_name', `PleromaFE_${Math.random()}`)
|
||||
form.append('redirect_uris', `${window.location.origin}/~/oauth-callback`)
|
||||
form.append('redirect_uris', `${window.location.origin}/oauth-callback`)
|
||||
form.append('scopes', 'read write follow')
|
||||
|
||||
return window.fetch(url, {
|
||||
|
@ -64,7 +64,7 @@ const getToken = ({app, instance, code}) => {
|
|||
form.append('client_secret', app.client_secret)
|
||||
form.append('grant_type', 'authorization_code')
|
||||
form.append('code', code)
|
||||
form.append('redirect_uri', `${window.location.origin}/~/oauth-callback`)
|
||||
form.append('redirect_uri', `${window.location.origin}/oauth-callback`)
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
|
|
20
src/services/notification_utils/notification_utils.js
Normal file
20
src/services/notification_utils/notification_utils.js
Normal 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)
|
|
@ -14,12 +14,12 @@ function isPushSupported () {
|
|||
return 'serviceWorker' in navigator && 'PushManager' in window
|
||||
}
|
||||
|
||||
function registerServiceWorker () {
|
||||
function getOrCreateServiceWorker () {
|
||||
return runtime.register()
|
||||
.catch((err) => console.error('Unable to register service worker.', err))
|
||||
.catch((err) => console.error('Unable to get or create a service worker.', err))
|
||||
}
|
||||
|
||||
function subscribe (registration, isEnabled, vapidPublicKey) {
|
||||
function subscribePush (registration, isEnabled, vapidPublicKey) {
|
||||
if (!isEnabled) return Promise.reject(new Error('Web Push is disabled in config'))
|
||||
if (!vapidPublicKey) return Promise.reject(new Error('VAPID public key is not found'))
|
||||
|
||||
|
@ -30,7 +30,28 @@ function subscribe (registration, isEnabled, vapidPublicKey) {
|
|||
return registration.pushManager.subscribe(subscribeOptions)
|
||||
}
|
||||
|
||||
function sendSubscriptionToBackEnd (subscription, token) {
|
||||
function unsubscribePush (registration) {
|
||||
return registration.pushManager.getSubscription()
|
||||
.then((subscribtion) => {
|
||||
if (subscribtion === null) { return }
|
||||
return subscribtion.unsubscribe()
|
||||
})
|
||||
}
|
||||
|
||||
function deleteSubscriptionFromBackEnd (token) {
|
||||
return window.fetch('/api/v1/push/subscription/', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}).then((response) => {
|
||||
if (!response.ok) throw new Error('Bad status code from server.')
|
||||
return response
|
||||
})
|
||||
}
|
||||
|
||||
function sendSubscriptionToBackEnd (subscription, token, notificationVisibility) {
|
||||
return window.fetch('/api/v1/push/subscription/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
@ -41,29 +62,49 @@ function sendSubscriptionToBackEnd (subscription, token) {
|
|||
subscription,
|
||||
data: {
|
||||
alerts: {
|
||||
follow: true,
|
||||
favourite: true,
|
||||
mention: true,
|
||||
reblog: true
|
||||
follow: notificationVisibility.follows,
|
||||
favourite: notificationVisibility.likes,
|
||||
mention: notificationVisibility.mentions,
|
||||
reblog: notificationVisibility.repeats
|
||||
}
|
||||
}
|
||||
})
|
||||
}).then((response) => {
|
||||
if (!response.ok) throw new Error('Bad status code from server.')
|
||||
return response.json()
|
||||
}).then((responseData) => {
|
||||
if (!responseData.id) throw new Error('Bad response from server.')
|
||||
return responseData
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error('Bad status code from server.')
|
||||
return response.json()
|
||||
})
|
||||
.then((responseData) => {
|
||||
if (!responseData.id) throw new Error('Bad response from server.')
|
||||
return responseData
|
||||
})
|
||||
}
|
||||
|
||||
export default function registerPushNotifications (isEnabled, vapidPublicKey, token) {
|
||||
export function registerPushNotifications (isEnabled, vapidPublicKey, token, notificationVisibility) {
|
||||
if (isPushSupported()) {
|
||||
registerServiceWorker()
|
||||
.then((registration) => subscribe(registration, isEnabled, vapidPublicKey))
|
||||
.then((subscription) => sendSubscriptionToBackEnd(subscription, token))
|
||||
getOrCreateServiceWorker()
|
||||
.then((registration) => subscribePush(registration, isEnabled, vapidPublicKey))
|
||||
.then((subscription) => sendSubscriptionToBackEnd(subscription, token, notificationVisibility))
|
||||
.catch((e) => console.warn(`Failed to setup Web Push Notifications: ${e.message}`))
|
||||
}
|
||||
}
|
||||
|
||||
export function unregisterPushNotifications (token) {
|
||||
if (isPushSupported()) {
|
||||
Promise.all([
|
||||
deleteSubscriptionFromBackEnd(token),
|
||||
getOrCreateServiceWorker()
|
||||
.then((registration) => {
|
||||
return unsubscribePush(registration).then((result) => [registration, result])
|
||||
})
|
||||
.then(([registration, unsubResult]) => {
|
||||
if (!unsubResult) {
|
||||
console.warn('Push subscription cancellation wasn\'t successful, killing SW anyway...')
|
||||
}
|
||||
return registration.unregister().then((result) => {
|
||||
if (!result) {
|
||||
console.warn('Failed to kill SW')
|
||||
}
|
||||
})
|
||||
})
|
||||
]).catch((e) => console.warn(`Failed to disable Web Push Notifications: ${e.message}`))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { map } from 'lodash'
|
||||
import apiService from '../api/api.service.js'
|
||||
|
||||
const postStatus = ({ store, status, spoilerText, visibility, sensitive, media = [], inReplyToStatusId = undefined }) => {
|
||||
const postStatus = ({ store, status, spoilerText, visibility, sensitive, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => {
|
||||
const mediaIds = map(media, 'id')
|
||||
|
||||
return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId})
|
||||
return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType, noAttachmentLinks: store.state.instance.noAttachmentLinks})
|
||||
.then((data) => data.json())
|
||||
.then((data) => {
|
||||
if (!data.error) {
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
const generateProfileLink = (id, screenName) => {
|
||||
import { includes } from 'lodash'
|
||||
|
||||
const generateProfileLink = (id, screenName, restrictedNicknames) => {
|
||||
const complicated = (isExternal(screenName) || includes(restrictedNicknames, screenName))
|
||||
return {
|
||||
name: (isExternal(screenName) ? 'external-user-profile' : 'user-profile'),
|
||||
params: (isExternal(screenName) ? { id } : { name: screenName })
|
||||
name: (complicated ? 'external-user-profile' : 'user-profile'),
|
||||
params: (complicated ? { id } : { name: screenName })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,17 +5,19 @@
|
|||
"logo": "/static/logo.svg",
|
||||
"logoMask": true,
|
||||
"logoMargin": ".1em",
|
||||
"redirectRootNoLogin": "/~/main/all",
|
||||
"redirectRootLogin": "/~/main/friends",
|
||||
"redirectRootNoLogin": "/main/all",
|
||||
"redirectRootLogin": "/main/friends",
|
||||
"chatDisabled": false,
|
||||
"showInstanceSpecificPanel": false,
|
||||
"scopeOptionsEnabled": false,
|
||||
"formattingOptionsEnabled": false,
|
||||
"collapseMessageWithSubject": false,
|
||||
"scopeCopy": false,
|
||||
"scopeCopy": true,
|
||||
"subjectLineBehavior": "noop",
|
||||
"alwaysShowSubjectInput": false,
|
||||
"hidePostStats": false,
|
||||
"hideUserStats": false,
|
||||
"loginMethod": "password"
|
||||
"loginMethod": "password",
|
||||
"webPushNotifications": false,
|
||||
"noAttachmentLinks": false
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ describe('routes', () => {
|
|||
})
|
||||
|
||||
it('root path', () => {
|
||||
router.push('/~/main/all')
|
||||
router.push('/main/all')
|
||||
|
||||
const matchedComponents = router.getMatchedComponents()
|
||||
|
||||
|
@ -26,4 +26,12 @@ describe('routes', () => {
|
|||
|
||||
expect(matchedComponents[0].components.hasOwnProperty('UserCardContent')).to.eql(true)
|
||||
})
|
||||
|
||||
it('user\'s profile at /users', () => {
|
||||
router.push('/users/fake-user-name')
|
||||
|
||||
const matchedComponents = router.getMatchedComponents()
|
||||
|
||||
expect(matchedComponents[0].components.hasOwnProperty('UserCardContent')).to.eql(true)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -2,16 +2,36 @@ import { mount, createLocalVue } from '@vue/test-utils'
|
|||
import Vuex from 'vuex'
|
||||
import UserProfile from 'src/components/user_profile/user_profile.vue'
|
||||
import backendInteractorService from 'src/services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { getters } from 'src/modules/users.js'
|
||||
|
||||
const localVue = createLocalVue()
|
||||
localVue.use(Vuex)
|
||||
|
||||
const mutations = {
|
||||
clearTimeline: () => {}
|
||||
clearTimeline: () => {},
|
||||
setError: () => {}
|
||||
}
|
||||
|
||||
const testGetters = {
|
||||
userByName: state => getters.userByName(state.users),
|
||||
userById: state => getters.userById(state.users)
|
||||
}
|
||||
|
||||
const localUser = {
|
||||
id: 100,
|
||||
is_local: true,
|
||||
screen_name: 'testUser'
|
||||
}
|
||||
|
||||
const extUser = {
|
||||
id: 100,
|
||||
is_local: false,
|
||||
screen_name: 'testUser@test.instance'
|
||||
}
|
||||
|
||||
const externalProfileStore = new Vuex.Store({
|
||||
mutations,
|
||||
getters: testGetters,
|
||||
state: {
|
||||
api: {
|
||||
backendInteractor: backendInteractorService('')
|
||||
|
@ -44,7 +64,7 @@ const externalProfileStore = new Vuex.Store({
|
|||
followers: [],
|
||||
friends: [],
|
||||
viewing: 'statuses',
|
||||
userId: 701,
|
||||
userId: 100,
|
||||
flushMarker: 0
|
||||
}
|
||||
}
|
||||
|
@ -53,58 +73,15 @@ const externalProfileStore = new Vuex.Store({
|
|||
currentUser: {
|
||||
credentials: ''
|
||||
},
|
||||
usersObject: [
|
||||
{
|
||||
background_image: null,
|
||||
cover_photo: 'https://playvicious.social/system/accounts/headers/000/000/001/original/7dae4fc0e8330e83.jpg?1507329206',
|
||||
created_at: 'Mon Dec 18 16:01:35 +0000 2017',
|
||||
default_scope: 'public',
|
||||
description: "Your favorite person's favorite person.",
|
||||
description_html: "<p>Your favorite person's favorite person.</p>",
|
||||
favourites_count: 0,
|
||||
fields: [
|
||||
{
|
||||
name: '✌🏾',
|
||||
value: '<a href="https://thetwelfth.house" rel="me nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="">thetwelfth.house</span><span class="invisible"></span></a>'
|
||||
},
|
||||
{
|
||||
name: '🚧',
|
||||
value: '<a href="https://code.playvicio.us" rel="me nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="">code.playvicio.us</span><span class="invisible"></span></a>'
|
||||
},
|
||||
{
|
||||
name: '❤️',
|
||||
value: '<a href="https://www.patreon.com/Are0h" rel="me nofollow noopener" target="_blank"><span class="invisible">https://www.</span><span class="">patreon.com/Are0h</span><span class="invisible"></span></a>'
|
||||
}
|
||||
],
|
||||
followers_count: 2,
|
||||
following: false,
|
||||
follows_you: false,
|
||||
friends_count: 0,
|
||||
id: 701,
|
||||
is_local: false,
|
||||
locked: false,
|
||||
name: 'Are0h',
|
||||
name_html: 'Are0h',
|
||||
no_rich_text: false,
|
||||
profile_image_url: 'https://playvicious.social/system/accounts/avatars/000/000/001/original/33e9983bc2d96aeb.png?1520872572',
|
||||
profile_image_url_https: 'https://playvicious.social/system/accounts/avatars/000/000/001/original/33e9983bc2d96aeb.png?1520872572',
|
||||
profile_image_url_original: 'https://playvicious.social/system/accounts/avatars/000/000/001/original/33e9983bc2d96aeb.png?1520872572',
|
||||
profile_image_url_profile_size: 'https://playvicious.social/system/accounts/avatars/000/000/001/original/33e9983bc2d96aeb.png?1520872572',
|
||||
rights: {
|
||||
delete_others_notice: false
|
||||
},
|
||||
screen_name: 'Are0h@playvicious.social',
|
||||
statuses_count: 6727,
|
||||
statusnet_blocking: false,
|
||||
statusnet_profile_url: 'https://playvicious.social/users/Are0h'
|
||||
}
|
||||
]
|
||||
usersObject: [extUser],
|
||||
users: [extUser]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const localProfileStore = new Vuex.Store({
|
||||
mutations,
|
||||
getters: testGetters,
|
||||
state: {
|
||||
api: {
|
||||
backendInteractor: backendInteractorService('')
|
||||
|
@ -137,7 +114,7 @@ const localProfileStore = new Vuex.Store({
|
|||
followers: [],
|
||||
friends: [],
|
||||
viewing: 'statuses',
|
||||
userId: 701,
|
||||
userId: 100,
|
||||
flushMarker: 0
|
||||
}
|
||||
}
|
||||
|
@ -146,52 +123,8 @@ const localProfileStore = new Vuex.Store({
|
|||
currentUser: {
|
||||
credentials: ''
|
||||
},
|
||||
usersObject: [
|
||||
{
|
||||
background_image: null,
|
||||
cover_photo: 'https://playvicious.social/system/accounts/headers/000/000/001/original/7dae4fc0e8330e83.jpg?1507329206',
|
||||
created_at: 'Mon Dec 18 16:01:35 +0000 2017',
|
||||
default_scope: 'public',
|
||||
description: "Your favorite person's favorite person.",
|
||||
description_html: "<p>Your favorite person's favorite person.</p>",
|
||||
favourites_count: 0,
|
||||
fields: [
|
||||
{
|
||||
name: '✌🏾',
|
||||
value: '<a href="https://thetwelfth.house" rel="me nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="">thetwelfth.house</span><span class="invisible"></span></a>'
|
||||
},
|
||||
{
|
||||
name: '🚧',
|
||||
value: '<a href="https://code.playvicio.us" rel="me nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="">code.playvicio.us</span><span class="invisible"></span></a>'
|
||||
},
|
||||
{
|
||||
name: '❤️',
|
||||
value: '<a href="https://www.patreon.com/Are0h" rel="me nofollow noopener" target="_blank"><span class="invisible">https://www.</span><span class="">patreon.com/Are0h</span><span class="invisible"></span></a>'
|
||||
}
|
||||
],
|
||||
followers_count: 2,
|
||||
following: false,
|
||||
follows_you: false,
|
||||
friends_count: 0,
|
||||
id: 701,
|
||||
is_local: false,
|
||||
locked: false,
|
||||
name: 'Are0h',
|
||||
name_html: 'Are0h',
|
||||
no_rich_text: false,
|
||||
profile_image_url: 'https://playvicious.social/system/accounts/avatars/000/000/001/original/33e9983bc2d96aeb.png?1520872572',
|
||||
profile_image_url_https: 'https://playvicious.social/system/accounts/avatars/000/000/001/original/33e9983bc2d96aeb.png?1520872572',
|
||||
profile_image_url_original: 'https://playvicious.social/system/accounts/avatars/000/000/001/original/33e9983bc2d96aeb.png?1520872572',
|
||||
profile_image_url_profile_size: 'https://playvicious.social/system/accounts/avatars/000/000/001/original/33e9983bc2d96aeb.png?1520872572',
|
||||
rights: {
|
||||
delete_others_notice: false
|
||||
},
|
||||
screen_name: 'Are0h',
|
||||
statuses_count: 6727,
|
||||
statusnet_blocking: false,
|
||||
statusnet_profile_url: 'https://playvicious.social/users/Are0h'
|
||||
}
|
||||
]
|
||||
usersObject: [localUser],
|
||||
users: [localUser]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -203,14 +136,14 @@ describe('UserProfile', () => {
|
|||
store: externalProfileStore,
|
||||
mocks: {
|
||||
$route: {
|
||||
params: { id: 701 },
|
||||
params: { id: 100 },
|
||||
name: 'external-user-profile'
|
||||
},
|
||||
$t: (msg) => msg
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('.user-screen-name').text()).to.eql('@Are0h@playvicious.social')
|
||||
expect(wrapper.find('.user-screen-name').text()).to.eql('@testUser@test.instance')
|
||||
})
|
||||
|
||||
it('renders local profile', () => {
|
||||
|
@ -219,13 +152,13 @@ describe('UserProfile', () => {
|
|||
store: localProfileStore,
|
||||
mocks: {
|
||||
$route: {
|
||||
params: { name: 'Are0h' },
|
||||
params: { name: 'testUser' },
|
||||
name: 'user-profile'
|
||||
},
|
||||
$t: (msg) => msg
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('.user-screen-name').text()).to.eql('@Are0h')
|
||||
expect(wrapper.find('.user-screen-name').text()).to.eql('@testUser')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,34 +1,62 @@
|
|||
import { cloneDeep } from 'lodash'
|
||||
|
||||
import { defaultState, mutations } from '../../../../src/modules/users.js'
|
||||
import { defaultState, mutations, getters } from '../../../../src/modules/users.js'
|
||||
|
||||
describe('The users module', () => {
|
||||
it('adds new users to the set, merging in new information for old users', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const user = { id: 1, name: 'Guy' }
|
||||
const modUser = { id: 1, name: 'Dude' }
|
||||
describe('mutations', () => {
|
||||
it('adds new users to the set, merging in new information for old users', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const user = { id: 1, name: 'Guy' }
|
||||
const modUser = { id: 1, name: 'Dude' }
|
||||
|
||||
mutations.addNewUsers(state, [user])
|
||||
expect(state.users).to.have.length(1)
|
||||
expect(state.users).to.eql([user])
|
||||
mutations.addNewUsers(state, [user])
|
||||
expect(state.users).to.have.length(1)
|
||||
expect(state.users).to.eql([user])
|
||||
|
||||
mutations.addNewUsers(state, [modUser])
|
||||
expect(state.users).to.have.length(1)
|
||||
expect(state.users).to.eql([user])
|
||||
expect(state.users[0].name).to.eql('Dude')
|
||||
mutations.addNewUsers(state, [modUser])
|
||||
expect(state.users).to.have.length(1)
|
||||
expect(state.users).to.eql([user])
|
||||
expect(state.users[0].name).to.eql('Dude')
|
||||
})
|
||||
|
||||
it('sets a mute bit on users', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const user = { id: 1, name: 'Guy' }
|
||||
|
||||
mutations.addNewUsers(state, [user])
|
||||
mutations.setMuted(state, {user, muted: true})
|
||||
|
||||
expect(user.muted).to.eql(true)
|
||||
|
||||
mutations.setMuted(state, {user, muted: false})
|
||||
|
||||
expect(user.muted).to.eql(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('sets a mute bit on users', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const user = { id: 1, name: 'Guy' }
|
||||
describe('getUserByName', () => {
|
||||
it('returns user with matching screen_name', () => {
|
||||
const state = {
|
||||
users: [
|
||||
{ screen_name: 'Guy', id: 1 }
|
||||
]
|
||||
}
|
||||
const name = 'Guy'
|
||||
const expected = { screen_name: 'Guy', id: 1 }
|
||||
expect(getters.userByName(state)(name)).to.eql(expected)
|
||||
})
|
||||
})
|
||||
|
||||
mutations.addNewUsers(state, [user])
|
||||
mutations.setMuted(state, {user, muted: true})
|
||||
|
||||
expect(user.muted).to.eql(true)
|
||||
|
||||
mutations.setMuted(state, {user, muted: false})
|
||||
|
||||
expect(user.muted).to.eql(false)
|
||||
describe('getUserById', () => {
|
||||
it('returns user with matching id', () => {
|
||||
const state = {
|
||||
users: [
|
||||
{ screen_name: 'Guy', id: 1 }
|
||||
]
|
||||
}
|
||||
const id = 1
|
||||
const expected = { screen_name: 'Guy', id: 1 }
|
||||
expect(getters.userById(state)(id)).to.eql(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -12,4 +12,10 @@ describe('generateProfileLink', () => {
|
|||
name: 'external-user-profile', params: { id: 1 }
|
||||
})
|
||||
})
|
||||
|
||||
it('returns obj for restricted user', () => {
|
||||
expect(generateProfileLink(1, 'lain', ['lain'])).to.eql({
|
||||
name: 'external-user-profile', params: { id: 1 }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue