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
|
# 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
|
# 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
|
## 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 FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||||
import ChatPanel from './components/chat_panel/chat_panel.vue'
|
import ChatPanel from './components/chat_panel/chat_panel.vue'
|
||||||
|
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||||
|
import { unseenNotificationsFromStore } from './services/notification_utils/notification_utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'app',
|
||||||
|
@ -17,7 +19,8 @@ export default {
|
||||||
InstanceSpecificPanel,
|
InstanceSpecificPanel,
|
||||||
FeaturesPanel,
|
FeaturesPanel,
|
||||||
WhoToFollowPanel,
|
WhoToFollowPanel,
|
||||||
ChatPanel
|
ChatPanel,
|
||||||
|
SideDrawer
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
mobileActivePanel: 'timeline',
|
mobileActivePanel: 'timeline',
|
||||||
|
@ -70,12 +73,15 @@ export default {
|
||||||
sitename () { return this.$store.state.instance.name },
|
sitename () { return this.$store.state.instance.name },
|
||||||
chat () { return this.$store.state.chat.channel.state === 'joined' },
|
chat () { return this.$store.state.chat.channel.state === 'joined' },
|
||||||
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
||||||
showInstanceSpecificPanel () { return this.$store.state.instance.showInstanceSpecificPanel }
|
showInstanceSpecificPanel () { return this.$store.state.instance.showInstanceSpecificPanel },
|
||||||
|
unseenNotifications () {
|
||||||
|
return unseenNotificationsFromStore(this.$store)
|
||||||
|
},
|
||||||
|
unseenNotificationsCount () {
|
||||||
|
return this.unseenNotifications.length
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
activatePanel (panelName) {
|
|
||||||
this.mobileActivePanel = panelName
|
|
||||||
},
|
|
||||||
scrollToTop () {
|
scrollToTop () {
|
||||||
window.scrollTo(0, 0)
|
window.scrollTo(0, 0)
|
||||||
},
|
},
|
||||||
|
@ -85,6 +91,9 @@ export default {
|
||||||
},
|
},
|
||||||
onFinderToggled (hidden) {
|
onFinderToggled (hidden) {
|
||||||
this.finderHidden = hidden
|
this.finderHidden = hidden
|
||||||
|
},
|
||||||
|
toggleMobileSidebar () {
|
||||||
|
this.$refs.sideDrawer.toggleDrawer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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 {
|
.fade-enter-active, .fade-leave-active {
|
||||||
transition: opacity .2s
|
transition: opacity .2s
|
||||||
}
|
}
|
||||||
|
@ -524,9 +542,6 @@ nav {
|
||||||
.back-button {
|
.back-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.site-name {
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-bounds {
|
.sidebar-bounds {
|
||||||
|
@ -665,4 +680,9 @@ nav {
|
||||||
max-width: 4em;
|
max-width: 4em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-button {
|
||||||
|
display: block;
|
||||||
|
margin-right: 0.8em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
32
src/App.vue
32
src/App.vue
|
@ -7,44 +7,42 @@
|
||||||
</div>
|
</div>
|
||||||
<div class='inner-nav'>
|
<div class='inner-nav'>
|
||||||
<div class='item'>
|
<div class='item'>
|
||||||
<router-link class="back-button" @click.native="activatePanel('timeline')" :to="{ name: 'root' }" active-class="hidden">
|
<a href="#" class="menu-button" @click.stop.prevent="toggleMobileSidebar()">
|
||||||
<i class="icon-left-open" :title="$t('nav.back')"></i>
|
<i class="button-icon icon-menu"></i>
|
||||||
</router-link>
|
<div class="alert-dot" v-if="unseenNotificationsCount"></div>
|
||||||
|
</a>
|
||||||
<router-link class="site-name" :to="{ name: 'root' }" active-class="home">{{sitename}}</router-link>
|
<router-link class="site-name" :to="{ name: 'root' }" active-class="home">{{sitename}}</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class='item right'>
|
<div class='item right'>
|
||||||
<user-finder class="button-icon nav-icon" @toggled="onFinderToggled"></user-finder>
|
<user-finder class="button-icon nav-icon mobile-hidden" @toggled="onFinderToggled"></user-finder>
|
||||||
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'settings'}"><i class="button-icon icon-cog nav-icon" :title="$t('nav.preferences')"></i></router-link>
|
<router-link class="mobile-hidden" :to="{ name: 'settings'}"><i class="button-icon icon-cog nav-icon" :title="$t('nav.preferences')"></i></router-link>
|
||||||
<a href="#" v-if="currentUser" @click.prevent="logout"><i class="button-icon icon-logout nav-icon" :title="$t('login.logout')"></i></a>
|
<a href="#" class="mobile-hidden" v-if="currentUser" @click.prevent="logout"><i class="button-icon icon-logout nav-icon" :title="$t('login.logout')"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="container" id="content">
|
<div v-if="" class="container" id="content">
|
||||||
<div class="panel-switcher">
|
<side-drawer ref="sideDrawer" :logout="logout"></side-drawer>
|
||||||
<button @click="activatePanel('sidebar')">Sidebar</button>
|
<div class="sidebar-flexer mobile-hidden">
|
||||||
<button @click="activatePanel('timeline')">Timeline</button>
|
|
||||||
</div>
|
|
||||||
<div class="sidebar-flexer" :class="{ 'mobile-hidden': mobileActivePanel != 'sidebar'}">
|
|
||||||
<div class="sidebar-bounds">
|
<div class="sidebar-bounds">
|
||||||
<div class="sidebar-scroller">
|
<div class="sidebar-scroller">
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<user-panel :activatePanel="activatePanel"></user-panel>
|
<user-panel></user-panel>
|
||||||
<nav-panel :activatePanel="activatePanel"></nav-panel>
|
<nav-panel></nav-panel>
|
||||||
<instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
|
<instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
|
||||||
<features-panel v-if="!currentUser"></features-panel>
|
<features-panel v-if="!currentUser"></features-panel>
|
||||||
<who-to-follow-panel v-if="currentUser && suggestionsEnabled"></who-to-follow-panel>
|
<who-to-follow-panel v-if="currentUser && suggestionsEnabled"></who-to-follow-panel>
|
||||||
<notifications :activatePanel="activatePanel" v-if="currentUser"></notifications>
|
<notifications v-if="currentUser"></notifications>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="main" :class="{ 'mobile-hidden': mobileActivePanel != 'timeline' }">
|
<div class="main">
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<chat-panel v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel>
|
<chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,7 @@ const afterStoreSetup = ({ store, i18n }) => {
|
||||||
var scopeCopy = (config.scopeCopy)
|
var scopeCopy = (config.scopeCopy)
|
||||||
var subjectLineBehavior = (config.subjectLineBehavior)
|
var subjectLineBehavior = (config.subjectLineBehavior)
|
||||||
var alwaysShowSubjectInput = (config.alwaysShowSubjectInput)
|
var alwaysShowSubjectInput = (config.alwaysShowSubjectInput)
|
||||||
|
var noAttachmentLinks = (config.noAttachmentLinks)
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', { name: 'theme', value: theme })
|
store.dispatch('setInstanceOption', { name: 'theme', value: theme })
|
||||||
store.dispatch('setInstanceOption', { name: 'background', value: background })
|
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: 'scopeCopy', value: scopeCopy })
|
||||||
store.dispatch('setInstanceOption', { name: 'subjectLineBehavior', value: subjectLineBehavior })
|
store.dispatch('setInstanceOption', { name: 'subjectLineBehavior', value: subjectLineBehavior })
|
||||||
store.dispatch('setInstanceOption', { name: 'alwaysShowSubjectInput', value: alwaysShowSubjectInput })
|
store.dispatch('setInstanceOption', { name: 'alwaysShowSubjectInput', value: alwaysShowSubjectInput })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'noAttachmentLinks', value: noAttachmentLinks })
|
||||||
|
|
||||||
if (chatDisabled) {
|
if (chatDisabled) {
|
||||||
store.dispatch('disableChat')
|
store.dispatch('disableChat')
|
||||||
}
|
}
|
||||||
|
@ -165,6 +168,8 @@ const afterStoreSetup = ({ store, i18n }) => {
|
||||||
store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
|
store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
|
||||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||||
|
|
||||||
|
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
|
||||||
|
|
||||||
const suggestions = metadata.suggestions
|
const suggestions = metadata.suggestions
|
||||||
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
|
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
|
||||||
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
|
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 FollowRequests from 'components/follow_requests/follow_requests.vue'
|
||||||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||||
import UserSearch from 'components/user_search/user_search.vue'
|
import UserSearch from 'components/user_search/user_search.vue'
|
||||||
|
import Notifications from 'components/notifications/notifications.vue'
|
||||||
|
import UserPanel from 'components/user_panel/user_panel.vue'
|
||||||
|
import LoginForm from 'components/login_form/login_form.vue'
|
||||||
|
import ChatPanel from 'components/chat_panel/chat_panel.vue'
|
||||||
|
|
||||||
export default (store) => {
|
export default (store) => {
|
||||||
return [
|
return [
|
||||||
|
@ -20,48 +24,28 @@ export default (store) => {
|
||||||
redirect: _to => {
|
redirect: _to => {
|
||||||
return (store.state.users.currentUser
|
return (store.state.users.currentUser
|
||||||
? store.state.instance.redirectRootLogin
|
? 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-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
|
||||||
{ name: 'public-timeline', path: '/~/main/public', component: PublicTimeline },
|
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
|
||||||
{ name: 'friends', path: '/~/main/friends', component: FriendsTimeline },
|
{ name: 'friends', path: '/main/friends', component: FriendsTimeline },
|
||||||
// Beginning of temporary redirects
|
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||||
{ path: '/main/:route',
|
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||||
redirect: to => {
|
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
|
||||||
const { params } = to
|
{ name: 'mentions', path: '/users/:username/mentions', component: Mentions },
|
||||||
const route = params.route ? params.route : 'all'
|
{ name: 'dms', path: '/users/:username/dms', component: DMs },
|
||||||
|
{ name: 'settings', path: '/settings', component: Settings },
|
||||||
return { path: `/~/main/${route}` }
|
{ name: 'registration', path: '/registration', component: Registration },
|
||||||
}
|
{ name: 'registration', path: '/registration/:token', component: Registration },
|
||||||
},
|
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
|
||||||
{ path: '/tag/:tag',
|
{ name: 'user-settings', path: '/user-settings', component: UserSettings },
|
||||||
redirect: to => {
|
{ name: 'notifications', path: '/:username/notifications', component: Notifications },
|
||||||
const { params } = to
|
{ name: 'new-status', path: '/:username/new-status', component: UserPanel },
|
||||||
|
{ name: 'login', path: '/login', component: LoginForm },
|
||||||
return { path: `/~/tag/${params.tag}` }
|
{ 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 }) },
|
||||||
{ path: '/notice/:id',
|
{ name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
|
||||||
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 }) }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
|
||||||
const chatPanel = {
|
const chatPanel = {
|
||||||
|
props: [ 'floating' ],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
currentMessage: '',
|
currentMessage: '',
|
||||||
|
@ -22,7 +23,7 @@ const chatPanel = {
|
||||||
this.collapsed = !this.collapsed
|
this.collapsed = !this.collapsed
|
||||||
},
|
},
|
||||||
userProfileLink (user) {
|
userProfileLink (user) {
|
||||||
return generateProfileLink(user.id, user.screen_name)
|
return generateProfileLink(user.id, user.username, this.$store.state.instance.restrictedNicknames)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="chat-panel" v-if="!this.collapsed">
|
<div class="chat-panel" v-if="!this.collapsed || !this.floating">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading timeline-heading chat-heading" @click.stop.prevent="togglePanel">
|
<div class="panel-heading timeline-heading" :class="{ 'chat-heading': floating }" @click.stop.prevent="togglePanel">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{{$t('chat.title')}}
|
{{$t('chat.title')}}
|
||||||
<i class="icon-cancel" style="float: right;"></i>
|
<i class="icon-cancel" style="float: right;" v-if="floating"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-window" v-chat-scroll>
|
<div class="chat-window" v-chat-scroll>
|
||||||
|
@ -52,6 +52,7 @@
|
||||||
right: 0px;
|
right: 0px;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
max-width: 25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-heading {
|
.chat-heading {
|
||||||
|
@ -63,10 +64,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-window {
|
.chat-window {
|
||||||
width: 345px;
|
|
||||||
max-height: 40vh;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
max-height: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-window-container {
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message {
|
.chat-message {
|
||||||
|
|
|
@ -32,7 +32,7 @@ const LoginForm = {
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.$store.commit('setToken', result.access_token)
|
this.$store.commit('setToken', result.access_token)
|
||||||
this.$store.dispatch('loginUser', 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 = {
|
const NavPanel = {
|
||||||
props: [ 'activatePanel' ],
|
|
||||||
computed: {
|
computed: {
|
||||||
currentUser () {
|
currentUser () {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
|
|
|
@ -3,32 +3,32 @@
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-if='currentUser'>
|
<li v-if='currentUser'>
|
||||||
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'friends' }">
|
<router-link :to="{ name: 'friends' }">
|
||||||
{{ $t("nav.timeline") }}
|
{{ $t("nav.timeline") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li v-if='currentUser'>
|
<li v-if='currentUser'>
|
||||||
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'mentions', params: { username: currentUser.screen_name } }">
|
<router-link :to="{ name: 'mentions', params: { username: currentUser.screen_name } }">
|
||||||
{{ $t("nav.mentions") }}
|
{{ $t("nav.mentions") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li v-if='currentUser'>
|
<li v-if='currentUser'>
|
||||||
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
|
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
|
||||||
{{ $t("nav.dms") }}
|
{{ $t("nav.dms") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li v-if='currentUser && currentUser.locked'>
|
<li v-if='currentUser && currentUser.locked'>
|
||||||
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'friend-requests' }">
|
<router-link :to="{ name: 'friend-requests' }">
|
||||||
{{ $t("nav.friend_requests") }}
|
{{ $t("nav.friend_requests") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'public-timeline' }">
|
<router-link :to="{ name: 'public-timeline' }">
|
||||||
{{ $t("nav.public_tl") }}
|
{{ $t("nav.public_tl") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'public-external-timeline' }">
|
<router-link :to="{ name: 'public-external-timeline' }">
|
||||||
{{ $t("nav.twkn") }}
|
{{ $t("nav.twkn") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -11,10 +11,7 @@ const Notification = {
|
||||||
betterShadow: this.$store.state.interface.browserSupport.cssFilter
|
betterShadow: this.$store.state.interface.browserSupport.cssFilter
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: [
|
props: [ 'notification' ],
|
||||||
'notification',
|
|
||||||
'activatePanel'
|
|
||||||
],
|
|
||||||
components: {
|
components: {
|
||||||
Status, StillImage, UserCardContent
|
Status, StillImage, UserCardContent
|
||||||
},
|
},
|
||||||
|
@ -23,7 +20,7 @@ const Notification = {
|
||||||
this.userExpanded = !this.userExpanded
|
this.userExpanded = !this.userExpanded
|
||||||
},
|
},
|
||||||
userProfileLink (user) {
|
userProfileLink (user) {
|
||||||
return generateProfileLink(user.id, user.screen_name)
|
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<status :activatePanel="activatePanel" v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
|
<status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
|
||||||
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
|
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
|
||||||
<a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
|
<a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
|
||||||
<StillImage class='avatar-compact' :class="{'better-shadow': betterShadow}" :src="notification.action.user.profile_image_url_original"/>
|
<StillImage class='avatar-compact' :class="{'better-shadow': betterShadow}" :src="notification.action.user.profile_image_url_original"/>
|
||||||
|
@ -25,15 +25,15 @@
|
||||||
<small>{{$t('notifications.followed_you')}}</small>
|
<small>{{$t('notifications.followed_you')}}</small>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<small class="timeago"><router-link @click.native="activatePanel('timeline')" v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
|
<small class="timeago"><router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
|
||||||
</span>
|
</span>
|
||||||
<div class="follow-text" v-if="notification.type === 'follow'">
|
<div class="follow-text" v-if="notification.type === 'follow'">
|
||||||
<router-link @click.native="activatePanel('timeline')" :to="userProfileLink(notification.action.user)">
|
<router-link :to="userProfileLink(notification.action.user)">
|
||||||
@{{notification.action.user.screen_name}}
|
@{{notification.action.user.screen_name}}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<status :activatePanel="activatePanel" class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
|
<status class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import Notification from '../notification/notification.vue'
|
import Notification from '../notification/notification.vue'
|
||||||
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
|
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
|
||||||
|
import {
|
||||||
import { sortBy, filter } from 'lodash'
|
notificationsFromStore,
|
||||||
|
visibleNotificationsFromStore,
|
||||||
|
unseenNotificationsFromStore
|
||||||
|
} from '../../services/notification_utils/notification_utils.js'
|
||||||
|
|
||||||
const Notifications = {
|
const Notifications = {
|
||||||
props: [ 'activatePanel' ],
|
|
||||||
created () {
|
created () {
|
||||||
const store = this.$store
|
const store = this.$store
|
||||||
const credentials = store.state.users.currentUser.credentials
|
const credentials = store.state.users.currentUser.credentials
|
||||||
|
@ -12,28 +14,17 @@ const Notifications = {
|
||||||
notificationsFetcher.startFetching({ store, credentials })
|
notificationsFetcher.startFetching({ store, credentials })
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
visibleTypes () {
|
|
||||||
return [
|
|
||||||
this.$store.state.config.notificationVisibility.likes && 'like',
|
|
||||||
this.$store.state.config.notificationVisibility.mentions && 'mention',
|
|
||||||
this.$store.state.config.notificationVisibility.repeats && 'repeat',
|
|
||||||
this.$store.state.config.notificationVisibility.follows && 'follow'
|
|
||||||
].filter(_ => _)
|
|
||||||
},
|
|
||||||
notifications () {
|
notifications () {
|
||||||
return this.$store.state.statuses.notifications.data
|
return notificationsFromStore(this.$store)
|
||||||
},
|
},
|
||||||
error () {
|
error () {
|
||||||
return this.$store.state.statuses.notifications.error
|
return this.$store.state.statuses.notifications.error
|
||||||
},
|
},
|
||||||
unseenNotifications () {
|
unseenNotifications () {
|
||||||
return filter(this.visibleNotifications, ({seen}) => !seen)
|
return unseenNotificationsFromStore(this.$store)
|
||||||
},
|
},
|
||||||
visibleNotifications () {
|
visibleNotifications () {
|
||||||
// Don't know why, but sortBy([seen, -action.id]) doesn't work.
|
return visibleNotificationsFromStore(this.$store)
|
||||||
let sortedNotifications = sortBy(this.notifications, ({action}) => -action.id)
|
|
||||||
sortedNotifications = sortBy(sortedNotifications, 'seen')
|
|
||||||
return sortedNotifications.filter((notification) => this.visibleTypes.includes(notification.type))
|
|
||||||
},
|
},
|
||||||
unseenCount () {
|
unseenCount () {
|
||||||
return this.unseenNotifications.length
|
return this.unseenNotifications.length
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div v-for="notification in visibleNotifications" :key="notification.action.id" class="notification" :class='{"unseen": !notification.seen}'>
|
<div v-for="notification in visibleNotifications" :key="notification.action.id" class="notification" :class='{"unseen": !notification.seen}'>
|
||||||
<div class="notification-overlay"></div>
|
<div class="notification-overlay"></div>
|
||||||
<notification :activatePanel="activatePanel" :notification="notification"></notification>
|
<notification :notification="notification"></notification>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-footer">
|
<div class="panel-footer">
|
||||||
|
|
|
@ -11,7 +11,7 @@ const oac = {
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
this.$store.commit('setToken', result.access_token)
|
this.$store.commit('setToken', result.access_token)
|
||||||
this.$store.dispatch('loginUser', 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')
|
this.$emit('posted')
|
||||||
let el = this.$el.querySelector('textarea')
|
let el = this.$el.querySelector('textarea')
|
||||||
|
el.style.height = 'auto'
|
||||||
el.style.height = undefined
|
el.style.height = undefined
|
||||||
this.error = null
|
this.error = null
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -28,7 +28,7 @@ const registration = {
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
if ((!this.registrationOpen && !this.token) || this.signedIn) {
|
if ((!this.registrationOpen && !this.token) || this.signedIn) {
|
||||||
this.$router.push('/~/main/all')
|
this.$router.push({name: 'root'})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setCaptcha()
|
this.setCaptcha()
|
||||||
|
@ -48,15 +48,17 @@ const registration = {
|
||||||
async submit () {
|
async submit () {
|
||||||
this.user.nickname = this.user.username
|
this.user.nickname = this.user.username
|
||||||
this.user.token = this.token
|
this.user.token = this.token
|
||||||
|
|
||||||
this.user.captcha_solution = this.captcha.solution
|
this.user.captcha_solution = this.captcha.solution
|
||||||
this.user.captcha_token = this.captcha.token
|
this.user.captcha_token = this.captcha.token
|
||||||
|
this.user.captcha_answer_data = this.captcha.answer_data
|
||||||
|
|
||||||
this.$v.$touch()
|
this.$v.$touch()
|
||||||
|
|
||||||
if (!this.$v.$invalid) {
|
if (!this.$v.$invalid) {
|
||||||
try {
|
try {
|
||||||
await this.signUp(this.user)
|
await this.signUp(this.user)
|
||||||
this.$router.push('/~/main/friends')
|
this.$router.push({name: 'friends'})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Registration failed: ' + error)
|
console.warn('Registration failed: ' + error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,15 +76,16 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="captcha-group" v-if="captcha.type != 'none'">
|
<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'">
|
<template v-if="captcha.type == 'kocaptcha'">
|
||||||
<img v-bind:src="captcha.url" v-on:click="setCaptcha">
|
<img v-bind:src="captcha.url" v-on:click="setCaptcha">
|
||||||
|
|
||||||
<sub>Click the image to get a new captcha</sub>
|
<sub>{{$t('registration.new_captcha')}}</sub>
|
||||||
<label class='form--label' for='captcha-label'>CAPTCHA</label>
|
|
||||||
|
|
||||||
<input :disabled="isPending"
|
<input :disabled="isPending"
|
||||||
v-model='captcha.solution'
|
v-model='captcha.solution'
|
||||||
class='form-control' id='captcha-answer' type='text'>
|
class='form-control' id='captcha-answer' type='text' autocomplete="off">
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</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',
|
'replies',
|
||||||
'noReplyLinks',
|
'noReplyLinks',
|
||||||
'noHeading',
|
'noHeading',
|
||||||
'inlineExpanded',
|
'inlineExpanded'
|
||||||
'activatePanel'
|
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -291,7 +290,7 @@ const Status = {
|
||||||
this.showPreview = false
|
this.showPreview = false
|
||||||
},
|
},
|
||||||
userProfileLink (id, name) {
|
userProfileLink (id, name) {
|
||||||
return generateProfileLink(id, name)
|
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<template v-if="muted && !noReplyLinks">
|
<template v-if="muted && !noReplyLinks">
|
||||||
<div class="media status container muted">
|
<div class="media status container muted">
|
||||||
<small>
|
<small>
|
||||||
<router-link @click.native="activatePanel('timeline')" :to="userProfileLink(status.user.id, status.user.screen_name)">
|
<router-link :to="userProfileLink(status.user.id, status.user.screen_name)">
|
||||||
{{status.user.screen_name}}
|
{{status.user.screen_name}}
|
||||||
</router-link>
|
</router-link>
|
||||||
</small>
|
</small>
|
||||||
|
@ -38,12 +38,12 @@
|
||||||
<h4 class="user-name" v-if="status.user.name_html" v-html="status.user.name_html"></h4>
|
<h4 class="user-name" v-if="status.user.name_html" v-html="status.user.name_html"></h4>
|
||||||
<h4 class="user-name" v-else>{{status.user.name}}</h4>
|
<h4 class="user-name" v-else>{{status.user.name}}</h4>
|
||||||
<span class="links">
|
<span class="links">
|
||||||
<router-link @click.native="activatePanel('timeline')" :to="userProfileLink(status.user.id, status.user.screen_name)">
|
<router-link :to="userProfileLink(status.user.id, status.user.screen_name)">
|
||||||
{{status.user.screen_name}}
|
{{status.user.screen_name}}
|
||||||
</router-link>
|
</router-link>
|
||||||
<span v-if="status.in_reply_to_screen_name" class="faint reply-info">
|
<span v-if="status.in_reply_to_screen_name" class="faint reply-info">
|
||||||
<i class="icon-right-open"></i>
|
<i class="icon-right-open"></i>
|
||||||
<router-link @click.native="activatePanel('timeline')" :to="userProfileLink(status.in_reply_to_user_id, status.in_reply_to_screen_name)">
|
<router-link :to="userProfileLink(status.in_reply_to_user_id, status.in_reply_to_screen_name)">
|
||||||
{{status.in_reply_to_screen_name}}
|
{{status.in_reply_to_screen_name}}
|
||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-heading-right">
|
<div class="media-heading-right">
|
||||||
<router-link class="timeago" @click.native="activatePanel('timeline')" :to="{ name: 'conversation', params: { id: status.id } }">
|
<router-link class="timeago" :to="{ name: 'conversation', params: { id: status.id } }">
|
||||||
<timeago :since="status.created_at" :auto-update="60"></timeago>
|
<timeago :since="status.created_at" :auto-update="60"></timeago>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="button-icon visibility-icon" v-if="status.visibility">
|
<div class="button-icon visibility-icon" v-if="status.visibility">
|
||||||
|
@ -79,7 +79,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showPreview" class="status-preview-container">
|
<div v-if="showPreview" class="status-preview-container">
|
||||||
<status :activatePanel="activatePanel" class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status>
|
<status class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status>
|
||||||
<div class="status-preview status-preview-loading" v-else>
|
<div class="status-preview status-preview-loading" v-else>
|
||||||
<i class="icon-spin4 animate-spin"></i>
|
<i class="icon-spin4 animate-spin"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -33,7 +33,7 @@ const UserCard = {
|
||||||
this.$store.dispatch('removeFollowRequest', this.user)
|
this.$store.dispatch('removeFollowRequest', this.user)
|
||||||
},
|
},
|
||||||
userProfileLink (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'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: [ 'user', 'switcher', 'selected', 'hideBio', 'activatePanel' ],
|
props: [ 'user', 'switcher', 'selected', 'hideBio' ],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
followRequestInProgress: false,
|
followRequestInProgress: false,
|
||||||
|
@ -180,7 +180,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
userProfileLink (user) {
|
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 id="heading" class="profile-panel-background" :style="headingStyle">
|
||||||
<div class="panel-heading text-center">
|
<div class="panel-heading text-center">
|
||||||
<div class='user-info'>
|
<div class='user-info'>
|
||||||
<router-link @click.native="activatePanel && activatePanel('timeline')" :to="{ name: 'user-settings' }" style="float: right; margin-top:16px;" v-if="!isOtherUser">
|
|
||||||
<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'>
|
<div class='container'>
|
||||||
<router-link @click.native="activatePanel && activatePanel('timeline')" :to="userProfileLink(user)">
|
<router-link :to="userProfileLink(user)">
|
||||||
<StillImage class="avatar" :class='{ "better-shadow": betterShadow }' :src="user.profile_image_url_original"/>
|
<StillImage class="avatar" :class='{ "better-shadow": betterShadow }' :src="user.profile_image_url_original"/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="name-and-screen-name">
|
<div class="name-and-screen-name">
|
||||||
|
<div class="top-line">
|
||||||
<div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
|
<div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
|
||||||
<div :title="user.name" class='user-name' v-else>{{user.name}}</div>
|
<div :title="user.name" class='user-name' v-else>{{user.name}}</div>
|
||||||
<router-link @click.native="activatePanel && activatePanel('timeline')" class='user-screen-name' :to="userProfileLink(user)">
|
<router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser">
|
||||||
<span>@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
|
<i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i>
|
||||||
<span v-if="!hideUserStatsLocal" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
|
</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>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,7 +28,7 @@
|
||||||
<div v-if="user.follows_you && loggedIn && isOtherUser" class="following">
|
<div v-if="user.follows_you && loggedIn && isOtherUser" class="following">
|
||||||
{{ $t('user_card.follows_you') }}
|
{{ $t('user_card.follows_you') }}
|
||||||
</div>
|
</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 -->
|
<!-- 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="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"/>
|
<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;
|
border-bottom-right-radius: 0;
|
||||||
|
|
||||||
.panel-heading {
|
.panel-heading {
|
||||||
padding: 0.6em 0em;
|
padding: .6em 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
@ -158,10 +161,10 @@
|
||||||
.user-info {
|
.user-info {
|
||||||
color: $fallback--lightText;
|
color: $fallback--lightText;
|
||||||
color: var(--lightText, $fallback--lightText);
|
color: var(--lightText, $fallback--lightText);
|
||||||
padding: 0 16px;
|
padding: 0 26px;
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
padding: 16px 10px 6px 10px;
|
padding: 16px 0 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
max-height: 56px;
|
max-height: 56px;
|
||||||
|
|
||||||
|
@ -218,11 +221,15 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
object-fit: contain
|
object-fit: contain
|
||||||
}
|
}
|
||||||
|
.top-line {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-name{
|
.user-name{
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
flex: 1 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-screen-name {
|
.user-screen-name {
|
||||||
|
@ -232,27 +239,73 @@
|
||||||
font-weight: light;
|
font-weight: light;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
padding-right: 0.1em;
|
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 {
|
.user-meta {
|
||||||
margin-bottom: .4em;
|
margin-bottom: .15em;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.following {
|
.following {
|
||||||
font-size: 14px;
|
flex: 1 0 auto;
|
||||||
flex: 0 0 100%;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 16px;
|
margin-bottom: .25em;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
.floater {
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
.highlighter {
|
||||||
display: block;
|
flex: 0 1 auto;
|
||||||
content: '';
|
display: flex;
|
||||||
clear: both;
|
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 {
|
.user-interactions {
|
||||||
|
@ -260,8 +313,13 @@
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
|
margin-right: -.75em;
|
||||||
|
|
||||||
div {
|
div {
|
||||||
flex: 1;
|
flex: 1 0 0;
|
||||||
|
margin-right: .75em;
|
||||||
|
margin-bottom: .6em;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mute {
|
.mute {
|
||||||
|
@ -280,8 +338,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
width: 92%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remote-button {
|
.remote-button {
|
||||||
|
@ -304,10 +363,11 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
color: $fallback--lightText;
|
color: $fallback--lightText;
|
||||||
color: var(--lightText, $fallback--lightText);
|
color: var(--lightText, $fallback--lightText);
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-count {
|
.user-count {
|
||||||
flex: 1;
|
flex: 1 0 auto;
|
||||||
padding: .5em 0 .5em 0;
|
padding: .5em 0 .5em 0;
|
||||||
margin: 0 .5em;
|
margin: 0 .5em;
|
||||||
|
|
||||||
|
@ -327,32 +387,5 @@
|
||||||
color: #CCC;
|
color: #CCC;
|
||||||
}
|
}
|
||||||
.floater {
|
.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>
|
</style>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
|
<div>
|
||||||
<div class="user-finder-container">
|
<div class="user-finder-container">
|
||||||
<i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
|
<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>
|
<a href="#" v-if="hidden" :title="$t('finder.find_user')"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a>
|
||||||
|
@ -10,6 +11,7 @@
|
||||||
<i class="button-icon icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/>
|
<i class="button-icon icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./user_finder.js"></script>
|
<script src="./user_finder.js"></script>
|
||||||
|
|
|
@ -3,7 +3,6 @@ import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||||
import UserCardContent from '../user_card_content/user_card_content.vue'
|
import UserCardContent from '../user_card_content/user_card_content.vue'
|
||||||
|
|
||||||
const UserPanel = {
|
const UserPanel = {
|
||||||
props: [ 'activatePanel' ],
|
|
||||||
computed: {
|
computed: {
|
||||||
user () { return this.$store.state.users.currentUser }
|
user () { return this.$store.state.users.currentUser }
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="user-panel">
|
<div class="user-panel">
|
||||||
<div v-if='user' class="panel panel-default" style="overflow: visible;">
|
<div v-if='user' class="panel panel-default" style="overflow: visible;">
|
||||||
<user-card-content :activatePanel="activatePanel" :user="user" :switcher="false" :hideBio="true"></user-card-content>
|
<user-card-content :user="user" :switcher="false" :hideBio="true"></user-card-content>
|
||||||
<div class="panel-footer">
|
<div class="panel-footer">
|
||||||
<post-status-form v-if='user'></post-status-form>
|
<post-status-form v-if='user'></post-status-form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@ const UserProfile = {
|
||||||
created () {
|
created () {
|
||||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
||||||
if (!this.user) {
|
if (!this.user.id) {
|
||||||
this.$store.dispatch('fetchUser', this.fetchBy)
|
this.$store.dispatch('fetchUser', this.fetchBy)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -29,14 +29,20 @@ const UserProfile = {
|
||||||
followers () {
|
followers () {
|
||||||
return this.user.followers
|
return this.user.followers
|
||||||
},
|
},
|
||||||
|
userInStore () {
|
||||||
|
if (this.isExternal) {
|
||||||
|
return this.$store.getters.userById(this.userId)
|
||||||
|
}
|
||||||
|
return this.$store.getters.userByName(this.userName)
|
||||||
|
},
|
||||||
user () {
|
user () {
|
||||||
if (this.timeline.statuses[0]) {
|
if (this.timeline.statuses[0]) {
|
||||||
return this.timeline.statuses[0].user
|
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 () {
|
fetchBy () {
|
||||||
return this.isExternal ? this.userId : this.userName
|
return this.isExternal ? this.userId : this.userName
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
flex: 2;
|
flex: 2;
|
||||||
flex-basis: 500px;
|
flex-basis: 500px;
|
||||||
|
|
||||||
.panel-heading {
|
.profile-panel-background .panel-heading {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
|
@ -9,6 +9,7 @@ const userSearch = {
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
username: '',
|
||||||
users: []
|
users: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -21,7 +22,14 @@ const userSearch = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
newQuery (query) {
|
||||||
|
this.$router.push({ name: 'user-search', query: { query } })
|
||||||
|
},
|
||||||
search (query) {
|
search (query) {
|
||||||
|
if (!query) {
|
||||||
|
this.users = []
|
||||||
|
return
|
||||||
|
}
|
||||||
userSearchApi.search({query, store: this.$store})
|
userSearchApi.search({query, store: this.$store})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.users = res
|
this.users = res
|
||||||
|
|
|
@ -3,6 +3,12 @@
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
{{$t('nav.user_search')}}
|
{{$t('nav.user_search')}}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="user-search-input-container">
|
||||||
|
<input class="user-finder-input" @keyup.enter="newQuery(username)" v-model="username" :placeholder="$t('finder.find_user')"/>
|
||||||
|
<button class="btn search-button" @click="newQuery(username)">
|
||||||
|
<i class="icon-search"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card>
|
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,3 +16,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./user_search.js"></script>
|
<script src="./user_search.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.user-search-input-container {
|
||||||
|
margin: 0.5em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -257,7 +257,7 @@ const UserSettings = {
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === 'success') {
|
if (res.status === 'success') {
|
||||||
this.$store.dispatch('logout')
|
this.$store.dispatch('logout')
|
||||||
this.$router.push('/~/main/all')
|
this.$router.push({name: 'root'})
|
||||||
} else {
|
} else {
|
||||||
this.deleteAccountError = res.error
|
this.deleteAccountError = res.error
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,63 +1,34 @@
|
||||||
import apiService from '../../services/api/api.service.js'
|
import apiService from '../../services/api/api.service.js'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
function showWhoToFollow (panel, reply) {
|
function showWhoToFollow (panel, reply) {
|
||||||
var users = reply
|
_.shuffle(reply)
|
||||||
var cn
|
|
||||||
var index
|
panel.usersToFollow.forEach((toFollow, index) => {
|
||||||
var step = 7
|
let user = reply[index]
|
||||||
cn = Math.floor(Math.random() * step)
|
let img = user.avatar || '/images/avi.png'
|
||||||
for (index = 0; index < 3; index++) {
|
let name = user.acct
|
||||||
var user
|
|
||||||
user = users[cn]
|
toFollow.img = img
|
||||||
var img
|
toFollow.name = name
|
||||||
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)
|
panel.$store.state.api.backendInteractor.externalProfile(name)
|
||||||
.then((externalUser) => {
|
.then((externalUser) => {
|
||||||
if (!externalUser.error) {
|
if (!externalUser.error) {
|
||||||
panel.$store.commit('addNewUsers', [externalUser])
|
panel.$store.commit('addNewUsers', [externalUser])
|
||||||
panel.id1 = externalUser.id
|
toFollow.id = 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWhoToFollow (panel) {
|
function getWhoToFollow (panel) {
|
||||||
var credentials = panel.$store.state.users.currentUser.credentials
|
var credentials = panel.$store.state.users.currentUser.credentials
|
||||||
if (credentials) {
|
if (credentials) {
|
||||||
panel.name1 = 'Loading...'
|
panel.usersToFollow.forEach(toFollow => {
|
||||||
panel.name2 = 'Loading...'
|
toFollow.name = 'Loading...'
|
||||||
panel.name3 = 'Loading...'
|
})
|
||||||
apiService.suggestions({credentials: credentials})
|
apiService.suggestions({credentials: credentials})
|
||||||
.then((reply) => {
|
.then((reply) => {
|
||||||
showWhoToFollow(panel, reply)
|
showWhoToFollow(panel, reply)
|
||||||
|
@ -67,27 +38,24 @@ function getWhoToFollow (panel) {
|
||||||
|
|
||||||
const WhoToFollowPanel = {
|
const WhoToFollowPanel = {
|
||||||
data: () => ({
|
data: () => ({
|
||||||
img1: '/images/avi.png',
|
usersToFollow: new Array(3).fill().map(x => (
|
||||||
name1: '',
|
{
|
||||||
id1: 0,
|
img: '/images/avi.png',
|
||||||
img2: '/images/avi.png',
|
name: '',
|
||||||
name2: '',
|
id: 0
|
||||||
id2: 0,
|
}
|
||||||
img3: '/images/avi.png',
|
))
|
||||||
name3: '',
|
|
||||||
id3: 0
|
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
user: function () {
|
user: function () {
|
||||||
return this.$store.state.users.currentUser.screen_name
|
return this.$store.state.users.currentUser.screen_name
|
||||||
},
|
},
|
||||||
moreUrl: function () {
|
moreUrl: function () {
|
||||||
var host = window.location.hostname
|
const host = window.location.hostname
|
||||||
var user = this.user
|
const user = this.user
|
||||||
var suggestionsWeb = this.$store.state.instance.suggestionsWeb
|
const suggestionsWeb = this.$store.state.instance.suggestionsWeb
|
||||||
var url
|
const url = suggestionsWeb.replace(/{{host}}/g, encodeURIComponent(host))
|
||||||
url = suggestionsWeb.replace(/{{host}}/g, encodeURIComponent(host))
|
.replace(/{{user}}/g, encodeURIComponent(user))
|
||||||
url = url.replace(/{{user}}/g, encodeURIComponent(user))
|
|
||||||
return url
|
return url
|
||||||
},
|
},
|
||||||
suggestionsEnabled () {
|
suggestionsEnabled () {
|
||||||
|
@ -96,7 +64,7 @@ const WhoToFollowPanel = {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
userProfileLink (id, name) {
|
userProfileLink (id, name) {
|
||||||
return generateProfileLink(id, name)
|
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
@ -7,12 +7,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body who-to-follow">
|
<div class="panel-body who-to-follow">
|
||||||
<p>
|
<span v-for="user in usersToFollow">
|
||||||
<img v-bind:src="img1"/> <router-link :to="userProfileLink(id1, name1)">{{ name1 }}</router-link><br>
|
<img v-bind:src="user.img" />
|
||||||
<img v-bind:src="img2"/> <router-link :to="userProfileLink(id2, name2)">{{ name2 }}</router-link><br>
|
<router-link v-bind:to="userProfileLink(user.id, user.name)">
|
||||||
<img v-bind:src="img3"/> <router-link :to="userProfileLink(id3, name3)">{{ name3 }}</router-link><br>
|
{{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>
|
<img v-bind:src="$store.state.instance.logo"> <a v-bind:href="moreUrl" target="_blank">{{$t('who_to_follow.more')}}</a>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,7 +29,9 @@
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
.who-to-follow p {
|
.who-to-follow {
|
||||||
|
padding: 0.5em 1em 0.5em 1em;
|
||||||
|
margin: 0px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
175
src/i18n/de.json
175
src/i18n/de.json
|
@ -6,7 +6,7 @@
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
"gopher": "Gopher",
|
"gopher": "Gopher",
|
||||||
"media_proxy": "Media Proxy",
|
"media_proxy": "Media Proxy",
|
||||||
"scope_options": "Scope options",
|
"scope_options": "Reichweitenoptionen",
|
||||||
"text_limit": "Textlimit",
|
"text_limit": "Textlimit",
|
||||||
"title": "Features",
|
"title": "Features",
|
||||||
"who_to_follow": "Who to follow"
|
"who_to_follow": "Who to follow"
|
||||||
|
@ -29,12 +29,16 @@
|
||||||
"username": "Benutzername"
|
"username": "Benutzername"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
|
"back": "Zurück",
|
||||||
"chat": "Lokaler Chat",
|
"chat": "Lokaler Chat",
|
||||||
"friend_requests": "Followanfragen",
|
"friend_requests": "Followanfragen",
|
||||||
"mentions": "Erwähnungen",
|
"mentions": "Erwähnungen",
|
||||||
"public_tl": "Lokale Zeitleiste",
|
"dms": "Direktnachrichten",
|
||||||
|
"public_tl": "Öffentliche Zeitleiste",
|
||||||
"timeline": "Zeitleiste",
|
"timeline": "Zeitleiste",
|
||||||
"twkn": "Das gesamte bekannte Netzwerk"
|
"twkn": "Das gesamte bekannte Netzwerk",
|
||||||
|
"user_search": "Benutzersuche",
|
||||||
|
"preferences": "Voreinstellungen"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"broken_favorite": "Unbekannte Nachricht, suche danach...",
|
"broken_favorite": "Unbekannte Nachricht, suche danach...",
|
||||||
|
@ -46,6 +50,7 @@
|
||||||
"repeated_you": "wiederholte deine Nachricht"
|
"repeated_you": "wiederholte deine Nachricht"
|
||||||
},
|
},
|
||||||
"post_status": {
|
"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": "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",
|
"account_not_locked_warning_link": "gesperrt",
|
||||||
"attachments_sensitive": "Anhänge als heikel markieren",
|
"attachments_sensitive": "Anhänge als heikel markieren",
|
||||||
|
@ -69,7 +74,17 @@
|
||||||
"fullname": "Angezeigter Name",
|
"fullname": "Angezeigter Name",
|
||||||
"password_confirm": "Passwort bestätigen",
|
"password_confirm": "Passwort bestätigen",
|
||||||
"registration": "Registrierung",
|
"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": {
|
"settings": {
|
||||||
"attachmentRadius": "Anhänge",
|
"attachmentRadius": "Anhänge",
|
||||||
|
@ -89,6 +104,7 @@
|
||||||
"change_password_error": "Es gab ein Problem bei der Änderung des Passworts.",
|
"change_password_error": "Es gab ein Problem bei der Änderung des Passworts.",
|
||||||
"changed_password": "Passwort erfolgreich geändert!",
|
"changed_password": "Passwort erfolgreich geändert!",
|
||||||
"collapse_subject": "Beiträge mit Betreff einklappen",
|
"collapse_subject": "Beiträge mit Betreff einklappen",
|
||||||
|
"composing": "Verfassen",
|
||||||
"confirm_new_password": "Neues Passwort bestätigen",
|
"confirm_new_password": "Neues Passwort bestätigen",
|
||||||
"current_avatar": "Dein derzeitiger Avatar",
|
"current_avatar": "Dein derzeitiger Avatar",
|
||||||
"current_password": "Aktuelles Passwort",
|
"current_password": "Aktuelles Passwort",
|
||||||
|
@ -112,12 +128,17 @@
|
||||||
"general": "Allgemein",
|
"general": "Allgemein",
|
||||||
"hide_attachments_in_convo": "Anhänge in Unterhaltungen ausblenden",
|
"hide_attachments_in_convo": "Anhänge in Unterhaltungen ausblenden",
|
||||||
"hide_attachments_in_tl": "Anhänge in der Zeitleiste 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_post_stats": "Beitragsstatistiken verbergen (z.B. die Anzahl der Favoriten)",
|
||||||
"hide_user_stats": "Benutzerstatistiken verbergen (z.B. die Anzahl der Follower)",
|
"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_followers_from_a_csv_file": "Importiere Follower, denen du folgen möchtest, aus einer CSV-Datei",
|
||||||
"import_theme": "Farbschema laden",
|
"import_theme": "Farbschema laden",
|
||||||
"inputRadius": "Eingabefelder",
|
"inputRadius": "Eingabefelder",
|
||||||
|
"checkboxRadius": "Auswahlfelder",
|
||||||
"instance_default": "(Standard: {value})",
|
"instance_default": "(Standard: {value})",
|
||||||
|
"instance_default_simple": "(Standard)",
|
||||||
|
"interface": "Oberfläche",
|
||||||
"interfaceLanguage": "Sprache der Oberfläche",
|
"interfaceLanguage": "Sprache der Oberfläche",
|
||||||
"invalid_theme_imported": "Die ausgewählte Datei ist kein unterstütztes Pleroma-Theme. Keine Änderungen wurden vorgenommen.",
|
"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",
|
"limited_availability": "In deinem Browser nicht verfügbar",
|
||||||
|
@ -134,6 +155,7 @@
|
||||||
"notification_visibility_mentions": "Erwähnungen",
|
"notification_visibility_mentions": "Erwähnungen",
|
||||||
"notification_visibility_repeats": "Wiederholungen",
|
"notification_visibility_repeats": "Wiederholungen",
|
||||||
"no_rich_text_description": "Rich-Text Formatierungen von allen Beiträgen entfernen",
|
"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",
|
"nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind",
|
||||||
"panelRadius": "Panel",
|
"panelRadius": "Panel",
|
||||||
"pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist",
|
"pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist",
|
||||||
|
@ -150,20 +172,139 @@
|
||||||
"saving_err": "Fehler beim Speichern der Einstellungen",
|
"saving_err": "Fehler beim Speichern der Einstellungen",
|
||||||
"saving_ok": "Einstellungen gespeichert",
|
"saving_ok": "Einstellungen gespeichert",
|
||||||
"security_tab": "Sicherheit",
|
"security_tab": "Sicherheit",
|
||||||
|
"scope_copy": "Reichweite beim Antworten übernehmen (Direktnachrichten werden immer kopiert)",
|
||||||
"set_new_avatar": "Setze einen neuen Avatar",
|
"set_new_avatar": "Setze einen neuen Avatar",
|
||||||
"set_new_profile_background": "Setze einen neuen Hintergrund für dein Profil",
|
"set_new_profile_background": "Setze einen neuen Hintergrund für dein Profil",
|
||||||
"set_new_profile_banner": "Setze einen neuen Banner für dein Profil",
|
"set_new_profile_banner": "Setze einen neuen Banner für dein Profil",
|
||||||
"settings": "Einstellungen",
|
"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",
|
"stop_gifs": "Play-on-hover GIFs",
|
||||||
"streaming": "Aktiviere automatisches Laden (Streaming) von neuen Beiträgen",
|
"streaming": "Aktiviere automatisches Laden (Streaming) von neuen Beiträgen",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"theme": "Farbschema",
|
"theme": "Farbschema",
|
||||||
"theme_help": "Benutze HTML-Farbcodes (#rrggbb) um dein Farbschema anzupassen",
|
"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",
|
"tooltipRadius": "Tooltips/Warnungen",
|
||||||
"user_settings": "Benutzereinstellungen",
|
"user_settings": "Benutzereinstellungen",
|
||||||
"values": {
|
"values": {
|
||||||
"false": "nein",
|
"false": "nein",
|
||||||
"true": "Ja"
|
"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": {
|
"timeline": {
|
||||||
|
@ -182,10 +323,15 @@
|
||||||
"blocked": "Blockiert!",
|
"blocked": "Blockiert!",
|
||||||
"deny": "Ablehnen",
|
"deny": "Ablehnen",
|
||||||
"follow": "Folgen",
|
"follow": "Folgen",
|
||||||
|
"follow_sent": "Anfrage gesendet!",
|
||||||
|
"follow_progress": "Anfragen…",
|
||||||
|
"follow_again": "Anfrage erneut senden?",
|
||||||
|
"follow_unfollow": "Folgen beenden",
|
||||||
"followees": "Folgt",
|
"followees": "Folgt",
|
||||||
"followers": "Followers",
|
"followers": "Followers",
|
||||||
"following": "Folgst du!",
|
"following": "Folgst du!",
|
||||||
"follows_you": "Folgt dir!",
|
"follows_you": "Folgt dir!",
|
||||||
|
"its_you": "Das bist du!",
|
||||||
"mute": "Stummschalten",
|
"mute": "Stummschalten",
|
||||||
"muted": "Stummgeschaltet",
|
"muted": "Stummgeschaltet",
|
||||||
"per_day": "pro Tag",
|
"per_day": "pro Tag",
|
||||||
|
@ -198,5 +344,26 @@
|
||||||
"who_to_follow": {
|
"who_to_follow": {
|
||||||
"more": "Mehr",
|
"more": "Mehr",
|
||||||
"who_to_follow": "Wem soll ich folgen"
|
"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"
|
"repeated_you": "repeated your status"
|
||||||
},
|
},
|
||||||
"post_status": {
|
"post_status": {
|
||||||
|
"new_status": "Post new status",
|
||||||
"account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.",
|
"account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.",
|
||||||
"account_not_locked_warning_link": "locked",
|
"account_not_locked_warning_link": "locked",
|
||||||
"attachments_sensitive": "Mark attachments as sensitive",
|
"attachments_sensitive": "Mark attachments as sensitive",
|
||||||
|
@ -74,6 +75,8 @@
|
||||||
"password_confirm": "Password confirmation",
|
"password_confirm": "Password confirmation",
|
||||||
"registration": "Registration",
|
"registration": "Registration",
|
||||||
"token": "Invite token",
|
"token": "Invite token",
|
||||||
|
"captcha": "CAPTCHA",
|
||||||
|
"new_captcha": "Click the image to get a new captcha",
|
||||||
"validations": {
|
"validations": {
|
||||||
"username_required": "cannot be left blank",
|
"username_required": "cannot be left blank",
|
||||||
"fullname_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": "ユーザーめい"
|
"username": "ユーザーめい"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
|
"back": "もどる",
|
||||||
"chat": "ローカルチャット",
|
"chat": "ローカルチャット",
|
||||||
"friend_requests": "フォローリクエスト",
|
"friend_requests": "フォローリクエスト",
|
||||||
"mentions": "メンション",
|
"mentions": "メンション",
|
||||||
"dms": "ダイレクトメッセージ",
|
"dms": "ダイレクトメッセージ",
|
||||||
"public_tl": "パブリックタイムライン",
|
"public_tl": "パブリックタイムライン",
|
||||||
"timeline": "タイムライン",
|
"timeline": "タイムライン",
|
||||||
"twkn": "つながっているすべてのネットワーク"
|
"twkn": "つながっているすべてのネットワーク",
|
||||||
|
"user_search": "ユーザーをさがす",
|
||||||
|
"preferences": "せってい"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"broken_favorite": "ステータスがみつかりません。さがしています...",
|
"broken_favorite": "ステータスがみつかりません。さがしています...",
|
||||||
|
@ -70,7 +73,17 @@
|
||||||
"fullname": "スクリーンネーム",
|
"fullname": "スクリーンネーム",
|
||||||
"password_confirm": "パスワードのかくにん",
|
"password_confirm": "パスワードのかくにん",
|
||||||
"registration": "はじめる",
|
"registration": "はじめる",
|
||||||
"token": "しょうたいトークン"
|
"token": "しょうたいトークン",
|
||||||
|
"captcha": "CAPTCHA",
|
||||||
|
"new_captcha": "もじがよめないときは、がぞうをクリックすると、あたらしいがぞうになります",
|
||||||
|
"validations": {
|
||||||
|
"username_required": "なにかかいてください",
|
||||||
|
"fullname_required": "なにかかいてください",
|
||||||
|
"email_required": "なにかかいてください",
|
||||||
|
"password_required": "なにかかいてください",
|
||||||
|
"password_confirmation_required": "なにかかいてください",
|
||||||
|
"password_confirmation_match": "パスワードがちがいます"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"attachmentRadius": "ファイル",
|
"attachmentRadius": "ファイル",
|
||||||
|
@ -90,6 +103,7 @@
|
||||||
"change_password_error": "パスワードをかえることが、できなかったかもしれません。",
|
"change_password_error": "パスワードをかえることが、できなかったかもしれません。",
|
||||||
"changed_password": "パスワードが、かわりました!",
|
"changed_password": "パスワードが、かわりました!",
|
||||||
"collapse_subject": "せつめいのあるとうこうをたたむ",
|
"collapse_subject": "せつめいのあるとうこうをたたむ",
|
||||||
|
"composing": "とうこう",
|
||||||
"confirm_new_password": "あたらしいパスワードのかくにん",
|
"confirm_new_password": "あたらしいパスワードのかくにん",
|
||||||
"current_avatar": "いまのアバター",
|
"current_avatar": "いまのアバター",
|
||||||
"current_password": "いまのパスワード",
|
"current_password": "いまのパスワード",
|
||||||
|
@ -113,17 +127,22 @@
|
||||||
"general": "ぜんぱん",
|
"general": "ぜんぱん",
|
||||||
"hide_attachments_in_convo": "スレッドのファイルをかくす",
|
"hide_attachments_in_convo": "スレッドのファイルをかくす",
|
||||||
"hide_attachments_in_tl": "タイムラインのファイルをかくす",
|
"hide_attachments_in_tl": "タイムラインのファイルをかくす",
|
||||||
|
"hide_isp": "インスタンススペシフィックパネルをかくす",
|
||||||
|
"preload_images": "がぞうをさきよみする",
|
||||||
"hide_post_stats": "とうこうのとうけいをかくす (れい: おきにいりのかず)",
|
"hide_post_stats": "とうこうのとうけいをかくす (れい: おきにいりのかず)",
|
||||||
"hide_user_stats": "ユーザーのとうけいをかくす (れい: フォロワーのかず)",
|
"hide_user_stats": "ユーザーのとうけいをかくす (れい: フォロワーのかず)",
|
||||||
"import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする",
|
"import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする",
|
||||||
"import_theme": "ロード",
|
"import_theme": "ロード",
|
||||||
"inputRadius": "インプットフィールド",
|
"inputRadius": "インプットフィールド",
|
||||||
|
"checkboxRadius": "チェックボックス",
|
||||||
"instance_default": "(デフォルト: {value})",
|
"instance_default": "(デフォルト: {value})",
|
||||||
|
"instance_default_simple": "(デフォルト)",
|
||||||
|
"interface": "インターフェース",
|
||||||
"interfaceLanguage": "インターフェースのことば",
|
"interfaceLanguage": "インターフェースのことば",
|
||||||
"invalid_theme_imported": "このファイルはPleromaのテーマではありません。テーマはへんこうされませんでした。",
|
"invalid_theme_imported": "このファイルはPleromaのテーマではありません。テーマはへんこうされませんでした。",
|
||||||
"limited_availability": "あなたのブラウザではできません",
|
"limited_availability": "あなたのブラウザではできません",
|
||||||
"links": "リンク",
|
"links": "リンク",
|
||||||
"lock_account_description": "あなたがみとめたひとだけ、あなたのアカウントをフォローできます",
|
"lock_account_description": "あなたがみとめたひとだけ、あなたのアカウントをフォローできる",
|
||||||
"loop_video": "ビデオをくりかえす",
|
"loop_video": "ビデオをくりかえす",
|
||||||
"loop_video_silent_only": "おとのないビデオだけくりかえす",
|
"loop_video_silent_only": "おとのないビデオだけくりかえす",
|
||||||
"name": "なまえ",
|
"name": "なまえ",
|
||||||
|
@ -135,6 +154,7 @@
|
||||||
"notification_visibility_mentions": "メンション",
|
"notification_visibility_mentions": "メンション",
|
||||||
"notification_visibility_repeats": "リピート",
|
"notification_visibility_repeats": "リピート",
|
||||||
"no_rich_text_description": "リッチテキストをつかわない",
|
"no_rich_text_description": "リッチテキストをつかわない",
|
||||||
|
"hide_network_description": "わたしがフォローしているひとと、わたしをフォローしているひとを、みせない",
|
||||||
"nsfw_clickthrough": "NSFWなファイルをかくす",
|
"nsfw_clickthrough": "NSFWなファイルをかくす",
|
||||||
"panelRadius": "パネル",
|
"panelRadius": "パネル",
|
||||||
"pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",
|
"pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",
|
||||||
|
@ -151,20 +171,139 @@
|
||||||
"saving_err": "せっていをセーブできませんでした",
|
"saving_err": "せっていをセーブできませんでした",
|
||||||
"saving_ok": "せっていをセーブしました",
|
"saving_ok": "せっていをセーブしました",
|
||||||
"security_tab": "セキュリティ",
|
"security_tab": "セキュリティ",
|
||||||
|
"scope_copy": "リプライするとき、こうかいはんいをコピーする (DMのこうかいはんいは、つねにコピーされます)",
|
||||||
"set_new_avatar": "あたらしいアバターをせっていする",
|
"set_new_avatar": "あたらしいアバターをせっていする",
|
||||||
"set_new_profile_background": "あたらしいプロフィールのバックグラウンドをせっていする",
|
"set_new_profile_background": "あたらしいプロフィールのバックグラウンドをせっていする",
|
||||||
"set_new_profile_banner": "あたらしいプロフィールバナーを設定する",
|
"set_new_profile_banner": "あたらしいプロフィールバナーを設定する",
|
||||||
"settings": "せってい",
|
"settings": "せってい",
|
||||||
|
"subject_input_always_show": "サブジェクトフィールドをいつでもひょうじする",
|
||||||
|
"subject_line_behavior": "リプライするときサブジェクトをコピーする",
|
||||||
|
"subject_line_email": "メールふう: \"re: サブジェクト\"",
|
||||||
|
"subject_line_mastodon": "マストドンふう: そのままコピー",
|
||||||
|
"subject_line_noop": "コピーしない",
|
||||||
"stop_gifs": "カーソルをかさねたとき、GIFをうごかす",
|
"stop_gifs": "カーソルをかさねたとき、GIFをうごかす",
|
||||||
"streaming": "うえまでスクロールしたとき、じどうてきにストリーミングする",
|
"streaming": "うえまでスクロールしたとき、じどうてきにストリーミングする",
|
||||||
"text": "もじ",
|
"text": "もじ",
|
||||||
"theme": "テーマ",
|
"theme": "テーマ",
|
||||||
"theme_help": "カラーテーマをカスタマイズできます",
|
"theme_help": "カラーテーマをカスタマイズできます",
|
||||||
|
"theme_help_v2_1": "チェックボックスをONにすると、コンポーネントごとに、いろと、とうめいどを、オーバーライドできます。「すべてクリア」ボタンをおすと、すべてのオーバーライドを、やめます。",
|
||||||
|
"theme_help_v2_2": "バックグラウンドとテキストのコントラストをあらわすアイコンがあります。マウスをホバーすると、くわしいせつめいがでます。とうめいないろをつかっているときは、もっともわるいばあいのコントラストがしめされます。",
|
||||||
"tooltipRadius": "ツールチップとアラート",
|
"tooltipRadius": "ツールチップとアラート",
|
||||||
"user_settings": "ユーザーせってい",
|
"user_settings": "ユーザーせってい",
|
||||||
"values": {
|
"values": {
|
||||||
"false": "いいえ",
|
"false": "いいえ",
|
||||||
"true": "はい"
|
"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": {
|
"timeline": {
|
||||||
|
@ -183,10 +322,15 @@
|
||||||
"blocked": "ブロックしています!",
|
"blocked": "ブロックしています!",
|
||||||
"deny": "おことわり",
|
"deny": "おことわり",
|
||||||
"follow": "フォロー",
|
"follow": "フォロー",
|
||||||
|
"follow_sent": "リクエストを、おくりました!",
|
||||||
|
"follow_progress": "リクエストしています…",
|
||||||
|
"follow_again": "ふたたびリクエストをおくりますか?",
|
||||||
|
"follow_unfollow": "フォローをやめる",
|
||||||
"followees": "フォロー",
|
"followees": "フォロー",
|
||||||
"followers": "フォロワー",
|
"followers": "フォロワー",
|
||||||
"following": "フォローしています!",
|
"following": "フォローしています!",
|
||||||
"follows_you": "フォローされました!",
|
"follows_you": "フォローされました!",
|
||||||
|
"its_you": "これはあなたです!",
|
||||||
"mute": "ミュート",
|
"mute": "ミュート",
|
||||||
"muted": "ミュートしています!",
|
"muted": "ミュートしています!",
|
||||||
"per_day": "/日",
|
"per_day": "/日",
|
||||||
|
@ -199,5 +343,26 @@
|
||||||
"who_to_follow": {
|
"who_to_follow": {
|
||||||
"more": "くわしく",
|
"more": "くわしく",
|
||||||
"who_to_follow": "おすすめユーザー"
|
"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 VueI18n from 'vue-i18n'
|
||||||
|
|
||||||
import createPersistedState from './lib/persisted_state.js'
|
import createPersistedState from './lib/persisted_state.js'
|
||||||
|
import pushNotifications from './lib/push_notifications_plugin.js'
|
||||||
|
|
||||||
import messages from './i18n/messages.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) => {
|
createPersistedState(persistedStateOptions).then((persistedState) => {
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
|
@ -88,7 +64,7 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
|
||||||
chat: chatModule,
|
chat: chatModule,
|
||||||
oauth: oauthModule
|
oauth: oauthModule
|
||||||
},
|
},
|
||||||
plugins: [persistedState, registerPushNotifications],
|
plugins: [persistedState, pushNotifications],
|
||||||
strict: false // Socket modifies itself, let's ignore this for now.
|
strict: false // Socket modifies itself, let's ignore this for now.
|
||||||
// strict: process.env.NODE_ENV !== 'production'
|
// strict: process.env.NODE_ENV !== 'production'
|
||||||
})
|
})
|
||||||
|
|
|
@ -24,7 +24,7 @@ const defaultState = {
|
||||||
likes: true,
|
likes: true,
|
||||||
repeats: true
|
repeats: true
|
||||||
},
|
},
|
||||||
webPushNotifications: true,
|
webPushNotifications: false,
|
||||||
muteWords: [],
|
muteWords: [],
|
||||||
highlight: {},
|
highlight: {},
|
||||||
interfaceLanguage: browserLocale,
|
interfaceLanguage: browserLocale,
|
||||||
|
|
|
@ -12,8 +12,8 @@ const defaultState = {
|
||||||
logo: '/static/logo.png',
|
logo: '/static/logo.png',
|
||||||
logoMask: true,
|
logoMask: true,
|
||||||
logoMargin: '.2em',
|
logoMargin: '.2em',
|
||||||
redirectRootNoLogin: '/~/main/all',
|
redirectRootNoLogin: '/main/all',
|
||||||
redirectRootLogin: '/~/main/friends',
|
redirectRootLogin: '/main/friends',
|
||||||
showInstanceSpecificPanel: false,
|
showInstanceSpecificPanel: false,
|
||||||
scopeOptionsEnabled: true,
|
scopeOptionsEnabled: true,
|
||||||
formattingOptionsEnabled: false,
|
formattingOptionsEnabled: false,
|
||||||
|
@ -27,11 +27,13 @@ const defaultState = {
|
||||||
loginMethod: 'password',
|
loginMethod: 'password',
|
||||||
nsfwCensorImage: undefined,
|
nsfwCensorImage: undefined,
|
||||||
vapidPublicKey: undefined,
|
vapidPublicKey: undefined,
|
||||||
|
noAttachmentLinks: false,
|
||||||
|
|
||||||
// Nasty stuff
|
// Nasty stuff
|
||||||
pleromaBackend: true,
|
pleromaBackend: true,
|
||||||
emoji: [],
|
emoji: [],
|
||||||
customEmoji: [],
|
customEmoji: [],
|
||||||
|
restrictedNicknames: [],
|
||||||
|
|
||||||
// Feature-set, apparently, not everything here is reported...
|
// Feature-set, apparently, not everything here is reported...
|
||||||
mediaProxyAvailable: false,
|
mediaProxyAvailable: false,
|
||||||
|
|
|
@ -27,6 +27,7 @@ export const defaultState = {
|
||||||
maxId: 0,
|
maxId: 0,
|
||||||
minId: Number.POSITIVE_INFINITY,
|
minId: Number.POSITIVE_INFINITY,
|
||||||
data: [],
|
data: [],
|
||||||
|
idStore: {},
|
||||||
error: false
|
error: false
|
||||||
},
|
},
|
||||||
favorites: new Set(),
|
favorites: new Set(),
|
||||||
|
@ -307,6 +308,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
||||||
}
|
}
|
||||||
|
|
||||||
state.notifications.data.push(result)
|
state.notifications.data.push(result)
|
||||||
|
state.notifications.idStore[notification.id] = result
|
||||||
|
|
||||||
if ('Notification' in window && window.Notification.permission === 'granted') {
|
if ('Notification' in window && window.Notification.permission === 'granted') {
|
||||||
const title = action.user.name
|
const title = action.user.name
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import { compact, map, each, merge } from 'lodash'
|
import { compact, map, each, merge } from 'lodash'
|
||||||
import { set } from 'vue'
|
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 oauthApi from '../services/new_api/oauth'
|
||||||
import { humanizeErrors } from './errors'
|
import { humanizeErrors } from './errors'
|
||||||
|
|
||||||
|
@ -66,6 +66,9 @@ export const mutations = {
|
||||||
setUserForStatus (state, status) {
|
setUserForStatus (state, status) {
|
||||||
status.user = state.usersObject[status.user.id]
|
status.user = state.usersObject[status.user.id]
|
||||||
},
|
},
|
||||||
|
setUserForNotification (state, notification) {
|
||||||
|
notification.action.user = state.usersObject[notification.action.user.id]
|
||||||
|
},
|
||||||
setColor (state, { user: { id }, highlighted }) {
|
setColor (state, { user: { id }, highlighted }) {
|
||||||
const user = state.usersObject[id]
|
const user = state.usersObject[id]
|
||||||
set(user, 'highlight', highlighted)
|
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 = {
|
export const defaultState = {
|
||||||
loggingIn: false,
|
loggingIn: false,
|
||||||
lastLoginName: false,
|
lastLoginName: false,
|
||||||
|
@ -96,6 +106,7 @@ export const defaultState = {
|
||||||
const users = {
|
const users = {
|
||||||
state: defaultState,
|
state: defaultState,
|
||||||
mutations,
|
mutations,
|
||||||
|
getters,
|
||||||
actions: {
|
actions: {
|
||||||
fetchUser (store, id) {
|
fetchUser (store, id) {
|
||||||
store.rootState.api.backendInteractor.fetchUser({ id })
|
store.rootState.api.backendInteractor.fetchUser({ id })
|
||||||
|
@ -113,8 +124,14 @@ const users = {
|
||||||
const token = store.state.currentUser.credentials
|
const token = store.state.currentUser.credentials
|
||||||
const vapidPublicKey = store.rootState.instance.vapidPublicKey
|
const vapidPublicKey = store.rootState.instance.vapidPublicKey
|
||||||
const isEnabled = store.rootState.config.webPushNotifications
|
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 }) {
|
addNewStatuses (store, { statuses }) {
|
||||||
const users = map(statuses, 'user')
|
const users = map(statuses, 'user')
|
||||||
|
@ -131,6 +148,21 @@ const users = {
|
||||||
store.commit('setUserForStatus', status)
|
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) {
|
async signUp (store, userInfo) {
|
||||||
store.commit('signUpPending')
|
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 idsText = mediaIds.join(',')
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
|
|
||||||
form.append('status', status)
|
form.append('status', status)
|
||||||
form.append('source', 'Pleroma FE')
|
form.append('source', 'Pleroma FE')
|
||||||
|
if (noAttachmentLinks) form.append('no_attachment_links', noAttachmentLinks)
|
||||||
if (spoilerText) form.append('spoiler_text', spoilerText)
|
if (spoilerText) form.append('spoiler_text', spoilerText)
|
||||||
if (visibility) form.append('visibility', visibility)
|
if (visibility) form.append('visibility', visibility)
|
||||||
if (sensitive) form.append('sensitive', sensitive)
|
if (sensitive) form.append('sensitive', sensitive)
|
||||||
|
|
|
@ -5,7 +5,7 @@ const getOrCreateApp = ({oauth, instance}) => {
|
||||||
const form = new window.FormData()
|
const form = new window.FormData()
|
||||||
|
|
||||||
form.append('client_name', `PleromaFE_${Math.random()}`)
|
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')
|
form.append('scopes', 'read write follow')
|
||||||
|
|
||||||
return window.fetch(url, {
|
return window.fetch(url, {
|
||||||
|
@ -64,7 +64,7 @@ const getToken = ({app, instance, code}) => {
|
||||||
form.append('client_secret', app.client_secret)
|
form.append('client_secret', app.client_secret)
|
||||||
form.append('grant_type', 'authorization_code')
|
form.append('grant_type', 'authorization_code')
|
||||||
form.append('code', 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, {
|
return window.fetch(url, {
|
||||||
method: 'POST',
|
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
|
return 'serviceWorker' in navigator && 'PushManager' in window
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerServiceWorker () {
|
function getOrCreateServiceWorker () {
|
||||||
return runtime.register()
|
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 (!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'))
|
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)
|
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/', {
|
return window.fetch('/api/v1/push/subscription/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -41,29 +62,49 @@ function sendSubscriptionToBackEnd (subscription, token) {
|
||||||
subscription,
|
subscription,
|
||||||
data: {
|
data: {
|
||||||
alerts: {
|
alerts: {
|
||||||
follow: true,
|
follow: notificationVisibility.follows,
|
||||||
favourite: true,
|
favourite: notificationVisibility.likes,
|
||||||
mention: true,
|
mention: notificationVisibility.mentions,
|
||||||
reblog: true
|
reblog: notificationVisibility.repeats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}).then((response) => {
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) throw new Error('Bad status code from server.')
|
if (!response.ok) throw new Error('Bad status code from server.')
|
||||||
return response.json()
|
return response.json()
|
||||||
})
|
}).then((responseData) => {
|
||||||
.then((responseData) => {
|
|
||||||
if (!responseData.id) throw new Error('Bad response from server.')
|
if (!responseData.id) throw new Error('Bad response from server.')
|
||||||
return responseData
|
return responseData
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function registerPushNotifications (isEnabled, vapidPublicKey, token) {
|
export function registerPushNotifications (isEnabled, vapidPublicKey, token, notificationVisibility) {
|
||||||
if (isPushSupported()) {
|
if (isPushSupported()) {
|
||||||
registerServiceWorker()
|
getOrCreateServiceWorker()
|
||||||
.then((registration) => subscribe(registration, isEnabled, vapidPublicKey))
|
.then((registration) => subscribePush(registration, isEnabled, vapidPublicKey))
|
||||||
.then((subscription) => sendSubscriptionToBackEnd(subscription, token))
|
.then((subscription) => sendSubscriptionToBackEnd(subscription, token, notificationVisibility))
|
||||||
.catch((e) => console.warn(`Failed to setup Web Push Notifications: ${e.message}`))
|
.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 { map } from 'lodash'
|
||||||
import apiService from '../api/api.service.js'
|
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')
|
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) => data.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (!data.error) {
|
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 {
|
return {
|
||||||
name: (isExternal(screenName) ? 'external-user-profile' : 'user-profile'),
|
name: (complicated ? 'external-user-profile' : 'user-profile'),
|
||||||
params: (isExternal(screenName) ? { id } : { name: screenName })
|
params: (complicated ? { id } : { name: screenName })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,17 +5,19 @@
|
||||||
"logo": "/static/logo.svg",
|
"logo": "/static/logo.svg",
|
||||||
"logoMask": true,
|
"logoMask": true,
|
||||||
"logoMargin": ".1em",
|
"logoMargin": ".1em",
|
||||||
"redirectRootNoLogin": "/~/main/all",
|
"redirectRootNoLogin": "/main/all",
|
||||||
"redirectRootLogin": "/~/main/friends",
|
"redirectRootLogin": "/main/friends",
|
||||||
"chatDisabled": false,
|
"chatDisabled": false,
|
||||||
"showInstanceSpecificPanel": false,
|
"showInstanceSpecificPanel": false,
|
||||||
"scopeOptionsEnabled": false,
|
"scopeOptionsEnabled": false,
|
||||||
"formattingOptionsEnabled": false,
|
"formattingOptionsEnabled": false,
|
||||||
"collapseMessageWithSubject": false,
|
"collapseMessageWithSubject": false,
|
||||||
"scopeCopy": false,
|
"scopeCopy": true,
|
||||||
"subjectLineBehavior": "noop",
|
"subjectLineBehavior": "noop",
|
||||||
"alwaysShowSubjectInput": false,
|
"alwaysShowSubjectInput": false,
|
||||||
"hidePostStats": false,
|
"hidePostStats": false,
|
||||||
"hideUserStats": false,
|
"hideUserStats": false,
|
||||||
"loginMethod": "password"
|
"loginMethod": "password",
|
||||||
|
"webPushNotifications": false,
|
||||||
|
"noAttachmentLinks": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ describe('routes', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('root path', () => {
|
it('root path', () => {
|
||||||
router.push('/~/main/all')
|
router.push('/main/all')
|
||||||
|
|
||||||
const matchedComponents = router.getMatchedComponents()
|
const matchedComponents = router.getMatchedComponents()
|
||||||
|
|
||||||
|
@ -26,4 +26,12 @@ describe('routes', () => {
|
||||||
|
|
||||||
expect(matchedComponents[0].components.hasOwnProperty('UserCardContent')).to.eql(true)
|
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 Vuex from 'vuex'
|
||||||
import UserProfile from 'src/components/user_profile/user_profile.vue'
|
import UserProfile from 'src/components/user_profile/user_profile.vue'
|
||||||
import backendInteractorService from 'src/services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from 'src/services/backend_interactor_service/backend_interactor_service.js'
|
||||||
|
import { getters } from 'src/modules/users.js'
|
||||||
|
|
||||||
const localVue = createLocalVue()
|
const localVue = createLocalVue()
|
||||||
localVue.use(Vuex)
|
localVue.use(Vuex)
|
||||||
|
|
||||||
const mutations = {
|
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({
|
const externalProfileStore = new Vuex.Store({
|
||||||
mutations,
|
mutations,
|
||||||
|
getters: testGetters,
|
||||||
state: {
|
state: {
|
||||||
api: {
|
api: {
|
||||||
backendInteractor: backendInteractorService('')
|
backendInteractor: backendInteractorService('')
|
||||||
|
@ -44,7 +64,7 @@ const externalProfileStore = new Vuex.Store({
|
||||||
followers: [],
|
followers: [],
|
||||||
friends: [],
|
friends: [],
|
||||||
viewing: 'statuses',
|
viewing: 'statuses',
|
||||||
userId: 701,
|
userId: 100,
|
||||||
flushMarker: 0
|
flushMarker: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,58 +73,15 @@ const externalProfileStore = new Vuex.Store({
|
||||||
currentUser: {
|
currentUser: {
|
||||||
credentials: ''
|
credentials: ''
|
||||||
},
|
},
|
||||||
usersObject: [
|
usersObject: [extUser],
|
||||||
{
|
users: [extUser]
|
||||||
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'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const localProfileStore = new Vuex.Store({
|
const localProfileStore = new Vuex.Store({
|
||||||
mutations,
|
mutations,
|
||||||
|
getters: testGetters,
|
||||||
state: {
|
state: {
|
||||||
api: {
|
api: {
|
||||||
backendInteractor: backendInteractorService('')
|
backendInteractor: backendInteractorService('')
|
||||||
|
@ -137,7 +114,7 @@ const localProfileStore = new Vuex.Store({
|
||||||
followers: [],
|
followers: [],
|
||||||
friends: [],
|
friends: [],
|
||||||
viewing: 'statuses',
|
viewing: 'statuses',
|
||||||
userId: 701,
|
userId: 100,
|
||||||
flushMarker: 0
|
flushMarker: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,52 +123,8 @@ const localProfileStore = new Vuex.Store({
|
||||||
currentUser: {
|
currentUser: {
|
||||||
credentials: ''
|
credentials: ''
|
||||||
},
|
},
|
||||||
usersObject: [
|
usersObject: [localUser],
|
||||||
{
|
users: [localUser]
|
||||||
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'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -203,14 +136,14 @@ describe('UserProfile', () => {
|
||||||
store: externalProfileStore,
|
store: externalProfileStore,
|
||||||
mocks: {
|
mocks: {
|
||||||
$route: {
|
$route: {
|
||||||
params: { id: 701 },
|
params: { id: 100 },
|
||||||
name: 'external-user-profile'
|
name: 'external-user-profile'
|
||||||
},
|
},
|
||||||
$t: (msg) => msg
|
$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', () => {
|
it('renders local profile', () => {
|
||||||
|
@ -219,13 +152,13 @@ describe('UserProfile', () => {
|
||||||
store: localProfileStore,
|
store: localProfileStore,
|
||||||
mocks: {
|
mocks: {
|
||||||
$route: {
|
$route: {
|
||||||
params: { name: 'Are0h' },
|
params: { name: 'testUser' },
|
||||||
name: 'user-profile'
|
name: 'user-profile'
|
||||||
},
|
},
|
||||||
$t: (msg) => msg
|
$t: (msg) => msg
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(wrapper.find('.user-screen-name').text()).to.eql('@Are0h')
|
expect(wrapper.find('.user-screen-name').text()).to.eql('@testUser')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { cloneDeep } from 'lodash'
|
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', () => {
|
describe('The users module', () => {
|
||||||
|
describe('mutations', () => {
|
||||||
it('adds new users to the set, merging in new information for old users', () => {
|
it('adds new users to the set, merging in new information for old users', () => {
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
const user = { id: 1, name: 'Guy' }
|
const user = { id: 1, name: 'Guy' }
|
||||||
|
@ -32,3 +33,30 @@ describe('The users module', () => {
|
||||||
expect(user.muted).to.eql(false)
|
expect(user.muted).to.eql(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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 }
|
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