Merge branch 'develop' into 'fix-move-type-notification'

# Conflicts:
#   static/fontello.json
This commit is contained in:
HJ 2020-01-03 09:05:48 +00:00
commit 215662afde
54 changed files with 1548 additions and 982 deletions

View file

@ -1,5 +1,5 @@
{ {
"presets": ["es2015", "stage-2", "env"], "presets": ["@babel/preset-env"],
"plugins": ["transform-runtime", "lodash", "transform-vue-jsx"], "plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-transform-vue-jsx"],
"comments": false "comments": false
} }

View file

@ -77,6 +77,9 @@ Use custom image for NSFW'd images
### `showFeaturesPanel` ### `showFeaturesPanel`
Show panel showcasing instance features/settings to logged-out visitors Show panel showcasing instance features/settings to logged-out visitors
### `hideSitename`
Hide instance name in header
## Indirect configuration ## Indirect configuration
Some features are configured depending on how backend is configured. In general the approach is "if backend allows it there's no need to hide it, if backend doesn't allow it there's no need to show it. Some features are configured depending on how backend is configured. In general the approach is "if backend allows it there's no need to hide it, if backend doesn't allow it there's no need to show it.
@ -96,3 +99,6 @@ Setting this will change the warning text that is displayed for direct messages.
ATTENTION: If you actually want the behavior to change. You will need to set the appropriate option at the backend. See the backend documentation for information about that. ATTENTION: If you actually want the behavior to change. You will need to set the appropriate option at the backend. See the backend documentation for information about that.
DO NOT activate this without checking the backend configuration first! DO NOT activate this without checking the backend configuration first!
### Private Mode
If the `private` instance setting is enabled in the backend, features that are not accessible without authentication, such as the timelines and search will be disabled for unauthenticated users.

View file

@ -15,9 +15,8 @@
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs" "lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.7.6",
"@chenfengyuan/vue-qrcode": "^1.0.0", "@chenfengyuan/vue-qrcode": "^1.0.0",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-lodash": "^3.2.11",
"body-scroll-lock": "^2.6.4", "body-scroll-lock": "^2.6.4",
"chromatism": "^3.0.0", "chromatism": "^3.0.0",
"cropperjs": "^1.4.3", "cropperjs": "^1.4.3",
@ -40,20 +39,17 @@
"whatwg-fetch": "^2.0.3" "whatwg-fetch": "^2.0.3"
}, },
"devDependencies": { "devDependencies": {
"@babel/polyfill": "^7.0.0", "@babel/core": "^7.7.5",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.6",
"@babel/register": "^7.7.4",
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
"@vue/babel-plugin-transform-vue-jsx": "^1.1.2",
"@vue/test-utils": "^1.0.0-beta.26", "@vue/test-utils": "^1.0.0-beta.26",
"autoprefixer": "^6.4.0", "autoprefixer": "^6.4.0",
"babel-core": "^6.0.0",
"babel-eslint": "^7.0.0", "babel-eslint": "^7.0.0",
"babel-helper-vue-jsx-merge-props": "^2.0.3", "babel-loader": "^8.0.6",
"babel-loader": "^7.0.0", "babel-plugin-lodash": "^3.3.4",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.0.0",
"babel-plugin-transform-vue-jsx": "3",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.0.0",
"babel-preset-stage-2": "^6.0.0",
"babel-register": "^6.0.0",
"chai": "^3.5.0", "chai": "^3.5.0",
"chalk": "^1.1.3", "chalk": "^1.1.3",
"chromedriver": "^2.21.2", "chromedriver": "^2.21.2",

View file

@ -90,6 +90,7 @@ 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' },
hideSitename () { return this.$store.state.instance.hideSitename },
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
showInstanceSpecificPanel () { showInstanceSpecificPanel () {
return this.$store.state.instance.showInstanceSpecificPanel && return this.$store.state.instance.showInstanceSpecificPanel &&
@ -97,7 +98,8 @@ export default {
this.$store.state.instance.instanceSpecificPanelContent this.$store.state.instance.instanceSpecificPanelContent
}, },
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
isMobileLayout () { return this.$store.state.interface.mobileLayout } isMobileLayout () { return this.$store.state.interface.mobileLayout },
privateMode () { return this.$store.state.instance.private }
}, },
methods: { methods: {
scrollToTop () { scrollToTop () {

View file

@ -870,3 +870,16 @@ nav {
transform: rotate(359deg); transform: rotate(359deg);
} }
} }
.new-status-notification {
position:relative;
margin-top: -1px;
font-size: 1.1em;
border-width: 1px 0 0 0;
border-style: solid;
border-color: var(--border, $fallback--border);
padding: 10px;
z-index: 1;
background-color: $fallback--fg;
background-color: var(--panel, $fallback--fg);
}

View file

@ -31,6 +31,7 @@
</div> </div>
<div class="item"> <div class="item">
<router-link <router-link
v-if="!hideSitename"
class="site-name" class="site-name"
:to="{ name: 'root' }" :to="{ name: 'root' }"
active-class="home" active-class="home"
@ -40,6 +41,7 @@
</div> </div>
<div class="item right"> <div class="item right">
<search-bar <search-bar
v-if="currentUser || !privateMode"
class="nav-icon mobile-hidden" class="nav-icon mobile-hidden"
@toggled="onSearchBarToggled" @toggled="onSearchBarToggled"
@click.stop.native @click.stop.native

View file

@ -108,6 +108,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('alwaysShowSubjectInput') copyInstanceOption('alwaysShowSubjectInput')
copyInstanceOption('noAttachmentLinks') copyInstanceOption('noAttachmentLinks')
copyInstanceOption('showFeaturesPanel') copyInstanceOption('showFeaturesPanel')
copyInstanceOption('hideSitename')
return store.dispatch('setTheme', config['theme']) return store.dispatch('setTheme', config['theme'])
} }
@ -218,12 +219,21 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version }) store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' }) store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' })
const priv = metadata.private
store.dispatch('setInstanceOption', { name: 'private', value: priv })
const frontendVersion = window.___pleromafe_commit_hash const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion }) store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') }) store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') })
const federation = metadata.federation const federation = metadata.federation
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation }) store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
store.dispatch('setInstanceOption', {
name: 'federating',
value: typeof federation.enabled === 'undefined'
? true
: federation.enabled
})
const accounts = metadata.staffAccounts const accounts = metadata.staffAccounts
await resolveStaffAccounts({ store, accounts }) await resolveStaffAccounts({ store, accounts })

View file

@ -7,11 +7,11 @@ const FollowRequestCard = {
}, },
methods: { methods: {
approveUser () { approveUser () {
this.$store.state.api.backendInteractor.approveUser(this.user.id) this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
}, },
denyUser () { denyUser () {
this.$store.state.api.backendInteractor.denyUser(this.user.id) this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
} }
} }

View file

@ -58,7 +58,7 @@ const LoginForm = {
).then((result) => { ).then((result) => {
if (result.error) { if (result.error) {
if (result.error === 'mfa_required') { if (result.error === 'mfa_required') {
this.requireMFA({ app: app, settings: result }) this.requireMFA({ settings: result })
} else if (result.identifier === 'password_reset_required') { } else if (result.identifier === 'password_reset_required') {
this.$router.push({ name: 'password-reset', params: { passwordResetRequested: true } }) this.$router.push({ name: 'password-reset', params: { passwordResetRequested: true } })
} else { } else {

View file

@ -8,18 +8,23 @@ export default {
}), }),
computed: { computed: {
...mapGetters({ ...mapGetters({
authApp: 'authFlow/app',
authSettings: 'authFlow/settings' authSettings: 'authFlow/settings'
}), }),
...mapState({ instance: 'instance' }) ...mapState({
instance: 'instance',
oauth: 'oauth'
})
}, },
methods: { methods: {
...mapMutations('authFlow', ['requireTOTP', 'abortMFA']), ...mapMutations('authFlow', ['requireTOTP', 'abortMFA']),
...mapActions({ login: 'authFlow/login' }), ...mapActions({ login: 'authFlow/login' }),
clearError () { this.error = false }, clearError () { this.error = false },
submit () { submit () {
const { clientId, clientSecret } = this.oauth
const data = { const data = {
app: this.authApp, clientId,
clientSecret,
instance: this.instance.server, instance: this.instance.server,
mfaToken: this.authSettings.mfa_token, mfaToken: this.authSettings.mfa_token,
code: this.code code: this.code

View file

@ -7,18 +7,23 @@ export default {
}), }),
computed: { computed: {
...mapGetters({ ...mapGetters({
authApp: 'authFlow/app',
authSettings: 'authFlow/settings' authSettings: 'authFlow/settings'
}), }),
...mapState({ instance: 'instance' }) ...mapState({
instance: 'instance',
oauth: 'oauth'
})
}, },
methods: { methods: {
...mapMutations('authFlow', ['requireRecovery', 'abortMFA']), ...mapMutations('authFlow', ['requireRecovery', 'abortMFA']),
...mapActions({ login: 'authFlow/login' }), ...mapActions({ login: 'authFlow/login' }),
clearError () { this.error = false }, clearError () { this.error = false },
submit () { submit () {
const { clientId, clientSecret } = this.oauth
const data = { const data = {
app: this.authApp, clientId,
clientSecret,
instance: this.instance.server, instance: this.instance.server,
mfaToken: this.authSettings.mfa_token, mfaToken: this.authSettings.mfa_token,
code: this.code code: this.code

View file

@ -29,6 +29,7 @@ const MobileNav = {
unseenNotificationsCount () { unseenNotificationsCount () {
return this.unseenNotifications.length return this.unseenNotifications.length
}, },
hideSitename () { return this.$store.state.instance.hideSitename },
sitename () { return this.$store.state.instance.name } sitename () { return this.$store.state.instance.name }
}, },
methods: { methods: {

View file

@ -17,6 +17,7 @@
<i class="button-icon icon-menu" /> <i class="button-icon icon-menu" />
</a> </a>
<router-link <router-link
v-if="!hideSitename"
class="site-name" class="site-name"
:to="{ name: 'root' }" :to="{ name: 'root' }"
active-class="home" active-class="home"

View file

@ -45,12 +45,12 @@ const ModerationTools = {
toggleTag (tag) { toggleTag (tag) {
const store = this.$store const store = this.$store
if (this.tagsSet.has(tag)) { if (this.tagsSet.has(tag)) {
store.state.api.backendInteractor.untagUser(this.user, tag).then(response => { store.state.api.backendInteractor.untagUser({ user: this.user, tag }).then(response => {
if (!response.ok) { return } if (!response.ok) { return }
store.commit('untagUser', { user: this.user, tag }) store.commit('untagUser', { user: this.user, tag })
}) })
} else { } else {
store.state.api.backendInteractor.tagUser(this.user, tag).then(response => { store.state.api.backendInteractor.tagUser({ user: this.user, tag }).then(response => {
if (!response.ok) { return } if (!response.ok) { return }
store.commit('tagUser', { user: this.user, tag }) store.commit('tagUser', { user: this.user, tag })
}) })
@ -59,24 +59,19 @@ const ModerationTools = {
toggleRight (right) { toggleRight (right) {
const store = this.$store const store = this.$store
if (this.user.rights[right]) { if (this.user.rights[right]) {
store.state.api.backendInteractor.deleteRight(this.user, right).then(response => { store.state.api.backendInteractor.deleteRight({ user: this.user, right }).then(response => {
if (!response.ok) { return } if (!response.ok) { return }
store.commit('updateRight', { user: this.user, right: right, value: false }) store.commit('updateRight', { user: this.user, right, value: false })
}) })
} else { } else {
store.state.api.backendInteractor.addRight(this.user, right).then(response => { store.state.api.backendInteractor.addRight({ user: this.user, right }).then(response => {
if (!response.ok) { return } if (!response.ok) { return }
store.commit('updateRight', { user: this.user, right: right, value: true }) store.commit('updateRight', { user: this.user, right, value: true })
}) })
} }
}, },
toggleActivationStatus () { toggleActivationStatus () {
const store = this.$store this.$store.dispatch('toggleActivationStatus', { user: this.user })
const status = !!this.user.deactivated
store.state.api.backendInteractor.setActivationStatus(this.user, status).then(response => {
if (!response.ok) { return }
store.commit('updateActivationStatus', { user: this.user, status: status })
})
}, },
deleteUserDialog (show) { deleteUserDialog (show) {
this.showDeleteUserDialog = show this.showDeleteUserDialog = show
@ -85,7 +80,7 @@ const ModerationTools = {
const store = this.$store const store = this.$store
const user = this.user const user = this.user
const { id, name } = user const { id, name } = user
store.state.api.backendInteractor.deleteUser(user) store.state.api.backendInteractor.deleteUser({ user })
.then(e => { .then(e => {
this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id) this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id)
const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile' const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'

View file

@ -1,20 +1,18 @@
import { mapState } from 'vuex'
const NavPanel = { const NavPanel = {
created () { created () {
if (this.currentUser && this.currentUser.locked) { if (this.currentUser && this.currentUser.locked) {
this.$store.dispatch('startFetchingFollowRequest') this.$store.dispatch('startFetchingFollowRequest')
} }
}, },
computed: { computed: mapState({
currentUser () { currentUser: state => state.users.currentUser,
return this.$store.state.users.currentUser chat: state => state.chat.channel,
}, followRequestCount: state => state.api.followRequests.length,
chat () { privateMode: state => state.instance.private,
return this.$store.state.chat.channel federating: state => state.instance.federating
}, })
followRequestCount () {
return this.$store.state.api.followRequests.length
}
}
} }
export default NavPanel export default NavPanel

View file

@ -4,22 +4,22 @@
<ul> <ul>
<li v-if="currentUser"> <li v-if="currentUser">
<router-link :to="{ name: 'friends' }"> <router-link :to="{ name: 'friends' }">
{{ $t("nav.timeline") }} <i class="button-icon icon-home-2" /> {{ $t("nav.timeline") }}
</router-link> </router-link>
</li> </li>
<li v-if="currentUser"> <li v-if="currentUser">
<router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }">
{{ $t("nav.interactions") }} <i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }}
</router-link> </router-link>
</li> </li>
<li v-if="currentUser"> <li v-if="currentUser">
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }"> <router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
{{ $t("nav.dms") }} <i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }}
</router-link> </router-link>
</li> </li>
<li v-if="currentUser && currentUser.locked"> <li v-if="currentUser && currentUser.locked">
<router-link :to="{ name: 'friend-requests' }"> <router-link :to="{ name: 'friend-requests' }">
{{ $t("nav.friend_requests") }} <i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }}
<span <span
v-if="followRequestCount > 0" v-if="followRequestCount > 0"
class="badge follow-request-count" class="badge follow-request-count"
@ -28,19 +28,19 @@
</span> </span>
</router-link> </router-link>
</li> </li>
<li> <li v-if="currentUser || !privateMode">
<router-link :to="{ name: 'public-timeline' }"> <router-link :to="{ name: 'public-timeline' }">
{{ $t("nav.public_tl") }} <i class="button-icon icon-users" /> {{ $t("nav.public_tl") }}
</router-link> </router-link>
</li> </li>
<li> <li v-if="federating && !privateMode">
<router-link :to="{ name: 'public-external-timeline' }"> <router-link :to="{ name: 'public-external-timeline' }">
{{ $t("nav.twkn") }} <i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
</router-link> </router-link>
</li> </li>
<li> <li>
<router-link :to="{ name: 'about' }"> <router-link :to="{ name: 'about' }">
{{ $t("nav.about") }} <i class="button-icon icon-info-circled" /> {{ $t("nav.about") }}
</router-link> </router-link>
</li> </li>
</ul> </ul>
@ -113,4 +113,8 @@
} }
} }
} }
.nav-panel .button-icon:before {
width: 1.1em;
}
</style> </style>

View file

@ -47,6 +47,11 @@ const Notifications = {
components: { components: {
Notification Notification
}, },
created () {
const { dispatch } = this.$store
dispatch('fetchAndUpdateNotifications')
},
watch: { watch: {
unseenCount (count) { unseenCount (count) {
if (count > 0) { if (count > 0) {

View file

@ -10,7 +10,7 @@ const PublicAndExternalTimeline = {
this.$store.dispatch('startFetchingTimeline', { timeline: 'publicAndExternal' }) this.$store.dispatch('startFetchingTimeline', { timeline: 'publicAndExternal' })
}, },
destroyed () { destroyed () {
this.$store.dispatch('stopFetching', 'publicAndExternal') this.$store.dispatch('stopFetchingTimeline', 'publicAndExternal')
} }
} }

View file

@ -10,7 +10,7 @@ const PublicTimeline = {
this.$store.dispatch('startFetchingTimeline', { timeline: 'public' }) this.$store.dispatch('startFetchingTimeline', { timeline: 'public' })
}, },
destroyed () { destroyed () {
this.$store.dispatch('stopFetching', 'public') this.$store.dispatch('stopFetchingTimeline', 'public')
} }
} }

View file

@ -172,7 +172,7 @@
for="captcha-label" for="captcha-label"
>{{ $t('captcha') }}</label> >{{ $t('captcha') }}</label>
<template v-if="captcha.type == 'kocaptcha'"> <template v-if="['kocaptcha', 'native'].includes(captcha.type)">
<img <img
:src="captcha.url" :src="captcha.url"
@click="setCaptcha" @click="setCaptcha"

View file

@ -84,7 +84,7 @@ const settings = {
} }
}]) }])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
// Special cases (need to transform values) // Special cases (need to transform values or perform actions first)
muteWordsString: { muteWordsString: {
get () { return this.$store.getters.mergedConfig.muteWords.join('\n') }, get () { return this.$store.getters.mergedConfig.muteWords.join('\n') },
set (value) { set (value) {
@ -93,6 +93,22 @@ const settings = {
value: filter(value.split('\n'), (word) => trim(word).length > 0) value: filter(value.split('\n'), (word) => trim(word).length > 0)
}) })
} }
},
useStreamingApi: {
get () { return this.$store.getters.mergedConfig.useStreamingApi },
set (value) {
const promise = value
? this.$store.dispatch('enableMastoSockets')
: this.$store.dispatch('disableMastoSockets')
promise.then(() => {
this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
}).catch((e) => {
console.error('Failed starting MastoAPI Streaming socket', e)
this.$store.dispatch('disableMastoSockets')
this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
})
}
} }
}, },
// Updating nested properties // Updating nested properties

View file

@ -73,6 +73,15 @@
</li> </li>
</ul> </ul>
</li> </li>
<li>
<Checkbox v-model="useStreamingApi">
{{ $t('settings.useStreamingApi') }}
<br/>
<small>
{{ $t('settings.useStreamingApiWarning') }}
</small>
</Checkbox>
</li>
<li> <li>
<Checkbox v-model="autoLoad"> <Checkbox v-model="autoLoad">
{{ $t('settings.autoload') }} {{ $t('settings.autoload') }}

View file

@ -33,11 +33,20 @@ const SideDrawer = {
logo () { logo () {
return this.$store.state.instance.logo return this.$store.state.instance.logo
}, },
hideSitename () {
return this.$store.state.instance.hideSitename
},
sitename () { sitename () {
return this.$store.state.instance.name return this.$store.state.instance.name
}, },
followRequestCount () { followRequestCount () {
return this.$store.state.api.followRequests.length return this.$store.state.api.followRequests.length
},
privateMode () {
return this.$store.state.instance.private
},
federating () {
return this.$store.state.instance.federating
} }
}, },
methods: { methods: {

View file

@ -27,7 +27,7 @@
class="side-drawer-logo-wrapper" class="side-drawer-logo-wrapper"
> >
<img :src="logo"> <img :src="logo">
<span>{{ sitename }}</span> <span v-if="!hideSitename">{{ sitename }}</span>
</div> </div>
</div> </div>
<ul> <ul>
@ -36,7 +36,7 @@
@click="toggleDrawer" @click="toggleDrawer"
> >
<router-link :to="{ name: 'login' }"> <router-link :to="{ name: 'login' }">
{{ $t("login.login") }} <i class="button-icon icon-login" /> {{ $t("login.login") }}
</router-link> </router-link>
</li> </li>
<li <li
@ -44,7 +44,7 @@
@click="toggleDrawer" @click="toggleDrawer"
> >
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }"> <router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
{{ $t("nav.dms") }} <i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }}
</router-link> </router-link>
</li> </li>
<li <li
@ -52,7 +52,7 @@
@click="toggleDrawer" @click="toggleDrawer"
> >
<router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }">
{{ $t("nav.interactions") }} <i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }}
</router-link> </router-link>
</li> </li>
</ul> </ul>
@ -62,7 +62,7 @@
@click="toggleDrawer" @click="toggleDrawer"
> >
<router-link :to="{ name: 'friends' }"> <router-link :to="{ name: 'friends' }">
{{ $t("nav.timeline") }} <i class="button-icon icon-home-2" /> {{ $t("nav.timeline") }}
</router-link> </router-link>
</li> </li>
<li <li
@ -70,7 +70,7 @@
@click="toggleDrawer" @click="toggleDrawer"
> >
<router-link to="/friend-requests"> <router-link to="/friend-requests">
{{ $t("nav.friend_requests") }} <i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }}
<span <span
v-if="followRequestCount > 0" v-if="followRequestCount > 0"
class="badge follow-request-count" class="badge follow-request-count"
@ -79,14 +79,20 @@
</span> </span>
</router-link> </router-link>
</li> </li>
<li @click="toggleDrawer"> <li
v-if="currentUser || !privateMode"
@click="toggleDrawer"
>
<router-link to="/main/public"> <router-link to="/main/public">
{{ $t("nav.public_tl") }} <i class="button-icon icon-users" /> {{ $t("nav.public_tl") }}
</router-link> </router-link>
</li> </li>
<li @click="toggleDrawer"> <li
v-if="federating && !privateMode"
@click="toggleDrawer"
>
<router-link to="/main/all"> <router-link to="/main/all">
{{ $t("nav.twkn") }} <i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
</router-link> </router-link>
</li> </li>
<li <li
@ -94,14 +100,17 @@
@click="toggleDrawer" @click="toggleDrawer"
> >
<router-link :to="{ name: 'chat' }"> <router-link :to="{ name: 'chat' }">
{{ $t("nav.chat") }} <i class="button-icon icon-chat" /> {{ $t("nav.chat") }}
</router-link> </router-link>
</li> </li>
</ul> </ul>
<ul> <ul>
<li @click="toggleDrawer"> <li
v-if="currentUser || !privateMode"
@click="toggleDrawer"
>
<router-link :to="{ name: 'search' }"> <router-link :to="{ name: 'search' }">
{{ $t("nav.search") }} <i class="button-icon icon-search" /> {{ $t("nav.search") }}
</router-link> </router-link>
</li> </li>
<li <li
@ -109,17 +118,17 @@
@click="toggleDrawer" @click="toggleDrawer"
> >
<router-link :to="{ name: 'who-to-follow' }"> <router-link :to="{ name: 'who-to-follow' }">
{{ $t("nav.who_to_follow") }} <i class="button-icon icon-user-plus" /> {{ $t("nav.who_to_follow") }}
</router-link> </router-link>
</li> </li>
<li @click="toggleDrawer"> <li @click="toggleDrawer">
<router-link :to="{ name: 'settings' }"> <router-link :to="{ name: 'settings' }">
{{ $t("settings.settings") }} <i class="button-icon icon-cog" /> {{ $t("settings.settings") }}
</router-link> </router-link>
</li> </li>
<li @click="toggleDrawer"> <li @click="toggleDrawer">
<router-link :to="{ name: 'about'}"> <router-link :to="{ name: 'about'}">
{{ $t("nav.about") }} <i class="button-icon icon-info-circled" /> {{ $t("nav.about") }}
</router-link> </router-link>
</li> </li>
<li <li
@ -130,7 +139,7 @@
href="/pleroma/admin/#/login-pleroma" href="/pleroma/admin/#/login-pleroma"
target="_blank" target="_blank"
> >
{{ $t("nav.administration") }} <i class="button-icon icon-gauge" /> {{ $t("nav.administration") }}
</a> </a>
</li> </li>
<li <li
@ -141,7 +150,7 @@
href="#" href="#"
@click="doLogout" @click="doLogout"
> >
{{ $t("login.logout") }} <i class="button-icon icon-logout" /> {{ $t("login.logout") }}
</a> </a>
</li> </li>
</ul> </ul>
@ -215,6 +224,10 @@
box-shadow: var(--panelShadow); box-shadow: var(--panelShadow);
background-color: $fallback--bg; background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg); background-color: var(--bg, $fallback--bg);
.button-icon:before {
width: 1.1em;
}
} }
.side-drawer-logo-wrapper { .side-drawer-logo-wrapper {

View file

@ -36,23 +36,23 @@
.sticker-picker { .sticker-picker {
width: 100%; width: 100%;
position: relative; .contents {
.tab-switcher { min-height: 250px;
position: absolute; .sticker-picker-content {
top: 0; display: flex;
bottom: 0; flex-wrap: wrap;
left: 0; padding: 0 4px;
right: 0; .sticker {
} display: flex;
.sticker-picker-content { flex: 1 1 auto;
.sticker { margin: 4px;
display: inline-block; width: 56px;
width: 20%; height: 56px;
height: 20%; img {
img { height: 100%;
width: 100%; &:hover {
&:hover { filter: drop-shadow(0 0 5px var(--link, $fallback--link));
filter: drop-shadow(0 0 5px var(--link, $fallback--link)); }
} }
} }
} }

View file

@ -19,7 +19,7 @@ const TagTimeline = {
} }
}, },
destroyed () { destroyed () {
this.$store.dispatch('stopFetching', 'tag') this.$store.dispatch('stopFetchingTimeline', 'tag')
} }
} }

View file

@ -36,7 +36,12 @@ const Timeline = {
} }
}, },
computed: { computed: {
timelineError () { return this.$store.state.statuses.error }, timelineError () {
return this.$store.state.statuses.error
},
errorData () {
return this.$store.state.statuses.errorData
},
newStatusCount () { newStatusCount () {
return this.timeline.newStatusCount return this.timeline.newStatusCount
}, },

View file

@ -11,15 +11,22 @@
> >
{{ $t('timeline.error_fetching') }} {{ $t('timeline.error_fetching') }}
</div> </div>
<div
v-else-if="errorData"
class="loadmore-error alert error"
@click.prevent
>
{{ errorData.statusText }}
</div>
<button <button
v-if="timeline.newStatusCount > 0 && !timelineError" v-if="timeline.newStatusCount > 0 && !timelineError && !errorData"
class="loadmore-button" class="loadmore-button"
@click.prevent="showNewStatuses" @click.prevent="showNewStatuses"
> >
{{ $t('timeline.show_new') }}{{ newStatusCountStr }} {{ $t('timeline.show_new') }}{{ newStatusCountStr }}
</button> </button>
<div <div
v-if="!timeline.newStatusCount > 0 && !timelineError" v-if="!timeline.newStatusCount > 0 && !timelineError && !errorData"
class="loadmore-text faint" class="loadmore-text faint"
@click.prevent @click.prevent
> >
@ -67,12 +74,18 @@
{{ $t('timeline.no_more_statuses') }} {{ $t('timeline.no_more_statuses') }}
</div> </div>
<a <a
v-else-if="!timeline.loading" v-else-if="!timeline.loading && !errorData"
href="#" href="#"
@click.prevent="fetchOlderStatuses()" @click.prevent="fetchOlderStatuses()"
> >
<div class="new-status-notification text-center panel-footer">{{ $t('timeline.load_older') }}</div> <div class="new-status-notification text-center panel-footer">{{ $t('timeline.load_older') }}</div>
</a> </a>
<a
v-else-if="errorData"
href="#"
>
<div class="new-status-notification text-center panel-footer">{{ errorData.error }}</div>
</a>
<div <div
v-else v-else
class="new-status-notification text-center panel-footer" class="new-status-notification text-center panel-footer"
@ -93,17 +106,4 @@
opacity: 1; opacity: 1;
} }
} }
.new-status-notification {
position:relative;
margin-top: -1px;
font-size: 1.1em;
border-width: 1px 0 0 0;
border-style: solid;
border-color: var(--border, $fallback--border);
padding: 10px;
z-index: 1;
background-color: $fallback--fg;
background-color: var(--panel, $fallback--fg);
}
</style> </style>

View file

@ -112,9 +112,9 @@ const UserProfile = {
} }
}, },
stopFetching () { stopFetching () {
this.$store.dispatch('stopFetching', 'user') this.$store.dispatch('stopFetchingTimeline', 'user')
this.$store.dispatch('stopFetching', 'favorites') this.$store.dispatch('stopFetchingTimeline', 'favorites')
this.$store.dispatch('stopFetching', 'media') this.$store.dispatch('stopFetchingTimeline', 'media')
}, },
switchUser (userNameOrId) { switchUser (userNameOrId) {
this.stopFetching() this.stopFetching()

View file

@ -64,7 +64,7 @@ const UserReportingModal = {
forward: this.forward, forward: this.forward,
statusIds: this.statusIdsToReport statusIds: this.statusIdsToReport
} }
this.$store.state.api.backendInteractor.reportUser(params) this.$store.state.api.backendInteractor.reportUser({ ...params })
.then(() => { .then(() => {
this.processing = false this.processing = false
this.resetState() this.resetState()

View file

@ -139,7 +139,7 @@ const Mfa = {
// fetch settings from server // fetch settings from server
async fetchSettings () { async fetchSettings () {
let result = await this.backendInteractor.fetchSettingsMFA() let result = await this.backendInteractor.settingsMFA()
if (result.error) return if (result.error) return
this.settings = result.settings this.settings = result.settings
this.settings.available = true this.settings.available = true

View file

@ -242,7 +242,7 @@ const UserSettings = {
}) })
}, },
importFollows (file) { importFollows (file) {
return this.$store.state.api.backendInteractor.importFollows(file) return this.$store.state.api.backendInteractor.importFollows({ file })
.then((status) => { .then((status) => {
if (!status) { if (!status) {
throw new Error('failed') throw new Error('failed')
@ -250,7 +250,7 @@ const UserSettings = {
}) })
}, },
importBlocks (file) { importBlocks (file) {
return this.$store.state.api.backendInteractor.importBlocks(file) return this.$store.state.api.backendInteractor.importBlocks({ file })
.then((status) => { .then((status) => {
if (!status) { if (!status) {
throw new Error('failed') throw new Error('failed')
@ -297,7 +297,7 @@ const UserSettings = {
newPassword: this.changePasswordInputs[1], newPassword: this.changePasswordInputs[1],
newPasswordConfirmation: this.changePasswordInputs[2] newPasswordConfirmation: this.changePasswordInputs[2]
} }
this.$store.state.api.backendInteractor.changePassword(params) this.$store.state.api.backendInteractor.changePassword({ params })
.then((res) => { .then((res) => {
if (res.status === 'success') { if (res.status === 'success') {
this.changedPassword = true this.changedPassword = true
@ -314,7 +314,7 @@ const UserSettings = {
email: this.newEmail, email: this.newEmail,
password: this.changeEmailPassword password: this.changeEmailPassword
} }
this.$store.state.api.backendInteractor.changeEmail(params) this.$store.state.api.backendInteractor.changeEmail({ params })
.then((res) => { .then((res) => {
if (res.status === 'success') { if (res.status === 'success') {
this.changedEmail = true this.changedEmail = true

View file

@ -65,7 +65,7 @@ const withLoadMore = ({
} }
} }
}, },
render (createElement) { render (h) {
const props = { const props = {
props: { props: {
...this.$props, ...this.$props,
@ -74,7 +74,7 @@ const withLoadMore = ({
on: this.$listeners, on: this.$listeners,
scopedSlots: this.$scopedSlots scopedSlots: this.$scopedSlots
} }
const children = Object.entries(this.$slots).map(([key, value]) => createElement('template', { slot: key }, value)) const children = Object.entries(this.$slots).map(([key, value]) => h('template', { slot: key }, value))
return ( return (
<div class="with-load-more"> <div class="with-load-more">
<WrappedComponent {...props}> <WrappedComponent {...props}>

View file

@ -49,7 +49,7 @@ const withSubscription = ({
} }
} }
}, },
render (createElement) { render (h) {
if (!this.error && !this.loading) { if (!this.error && !this.loading) {
const props = { const props = {
props: { props: {
@ -59,7 +59,7 @@ const withSubscription = ({
on: this.$listeners, on: this.$listeners,
scopedSlots: this.$scopedSlots scopedSlots: this.$scopedSlots
} }
const children = Object.entries(this.$slots).map(([key, value]) => createElement('template', { slot: key }, value)) const children = Object.entries(this.$slots).map(([key, value]) => h('template', { slot: key }, value))
return ( return (
<div class="with-subscription"> <div class="with-subscription">
<WrappedComponent {...props}> <WrappedComponent {...props}>

View file

@ -361,6 +361,8 @@
"post_status_content_type": "Post status content type", "post_status_content_type": "Post status content type",
"stop_gifs": "Play-on-hover GIFs", "stop_gifs": "Play-on-hover GIFs",
"streaming": "Enable automatic streaming of new posts when scrolled to the top", "streaming": "Enable automatic streaming of new posts when scrolled to the top",
"useStreamingApi": "Receive posts and notifications real-time",
"useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
"text": "Text", "text": "Text",
"theme": "Theme", "theme": "Theme",
"theme_help": "Use hex color codes (#rrggbb) to customize your color theme.", "theme_help": "Use hex color codes (#rrggbb) to customize your color theme.",

View file

@ -1,4 +1,23 @@
{ {
"about": {
"staff": "スタッフ",
"federation": "フェデレーション",
"mrf_policies": "ゆうこうなMRFポリシー",
"mrf_policies_desc": "MRFポリシーは、このインスタンスのフェデレーションのふるまいを、いじります。これらのMRFポリシーがゆうこうになっています:",
"mrf_policy_simple": "インスタンスのポリシー",
"mrf_policy_simple_accept": "うけいれ",
"mrf_policy_simple_accept_desc": "このインスンスは、これらのインスタンスからのメッセージのみをうけいれます:",
"mrf_policy_simple_reject": "おことわり",
"mrf_policy_simple_reject_desc": "このインスタンスは、これらのインスタンスからのメッセージをうけいれません:",
"mrf_policy_simple_quarantine": "けんえき",
"mrf_policy_simple_quarantine_desc": "このインスタンスは、これらのインスタンスに、パブリックなとうこうのみを、おくります:",
"mrf_policy_simple_ftl_removal": "「つながっているすべてのネットワーク」タイムラインからのぞく",
"mrf_policy_simple_ftl_removal_desc": "このインスタンスは、つながっているすべてのネットワーク」タイムラインから、これらのインスタンスを、とりのぞきます:",
"mrf_policy_simple_media_removal": "メディアをのぞく",
"mrf_policy_simple_media_removal_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、とりのぞきます:",
"mrf_policy_simple_media_nsfw": "メディアをすべてセンシティブにする",
"mrf_policy_simple_media_nsfw_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、すべて、センシティブにマークします:"
},
"chat": { "chat": {
"title": "チャット" "title": "チャット"
}, },
@ -68,6 +87,7 @@
}, },
"nav": { "nav": {
"about": "これはなに?", "about": "これはなに?",
"administration": "アドミニストレーション",
"back": "もどる", "back": "もどる",
"chat": "ローカルチャット", "chat": "ローカルチャット",
"friend_requests": "フォローリクエスト", "friend_requests": "フォローリクエスト",
@ -113,7 +133,9 @@
"search_emoji": "えもじをさがす", "search_emoji": "えもじをさがす",
"add_emoji": "えもじをうちこむ", "add_emoji": "えもじをうちこむ",
"custom": "カスタムえもじ", "custom": "カスタムえもじ",
"unicode": "ユニコードえもじ" "unicode": "ユニコードえもじ",
"load_all_hint": "はじめの {saneAmount} このえもじだけがロードされています。すべてのえもじをロードすると、パフォーマンスがわるくなるかもしれません。",
"load_all": "すべてのえもじをロード ({emojiAmount} こあります)"
}, },
"stickers": { "stickers": {
"add_sticker": "ステッカーをふやす" "add_sticker": "ステッカーをふやす"
@ -173,6 +195,11 @@
"password_confirmation_match": "パスワードがちがいます" "password_confirmation_match": "パスワードがちがいます"
} }
}, },
"remote_user_resolver": {
"remote_user_resolver": "リモートユーザーリゾルバー",
"searching_for": "さがしています:",
"error": "みつかりませんでした。"
},
"selectable_list": { "selectable_list": {
"select_all": "すべてえらぶ" "select_all": "すべてえらぶ"
}, },
@ -220,6 +247,9 @@
"cGreen": "リピート", "cGreen": "リピート",
"cOrange": "おきにいり", "cOrange": "おきにいり",
"cRed": "キャンセル", "cRed": "キャンセル",
"change_email": "メールアドレスをかえる",
"change_email_error": "メールアドレスをかえようとしましたが、なにかがおかしいです。",
"changed_email": "メールアドレスをかえることができました!",
"change_password": "パスワードをかえる", "change_password": "パスワードをかえる",
"change_password_error": "パスワードをかえることが、できなかったかもしれません。", "change_password_error": "パスワードをかえることが、できなかったかもしれません。",
"changed_password": "パスワードが、かわりました!", "changed_password": "パスワードが、かわりました!",
@ -279,6 +309,7 @@
"use_contain_fit": "がぞうのサムネイルを、きりぬかない", "use_contain_fit": "がぞうのサムネイルを、きりぬかない",
"name": "なまえ", "name": "なまえ",
"name_bio": "なまえとプロフィール", "name_bio": "なまえとプロフィール",
"new_email": "あたらしいメールアドレス",
"new_password": "あたらしいパスワード", "new_password": "あたらしいパスワード",
"notification_visibility": "ひょうじするつうち", "notification_visibility": "ひょうじするつうち",
"notification_visibility_follows": "フォロー", "notification_visibility_follows": "フォロー",
@ -344,6 +375,8 @@
"false": "いいえ", "false": "いいえ",
"true": "はい" "true": "はい"
}, },
"fun": "おたのしみ",
"greentext": "ミームやじるし",
"notifications": "つうち", "notifications": "つうち",
"notification_setting": "つうちをうけとる:", "notification_setting": "つうちをうけとる:",
"notification_setting_follows": "あなたがフォローしているひとから", "notification_setting_follows": "あなたがフォローしているひとから",
@ -391,6 +424,7 @@
"_tab_label": "くわしく", "_tab_label": "くわしく",
"alert": "アラートのバックグラウンド", "alert": "アラートのバックグラウンド",
"alert_error": "エラー", "alert_error": "エラー",
"alert_warning": "けいこく",
"badge": "バッジのバックグラウンド", "badge": "バッジのバックグラウンド",
"badge_notification": "つうち", "badge_notification": "つうち",
"panel_header": "パネルヘッダー", "panel_header": "パネルヘッダー",
@ -542,6 +576,7 @@
"followers": "フォロワー", "followers": "フォロワー",
"following": "フォローしています!", "following": "フォローしています!",
"follows_you": "フォローされました!", "follows_you": "フォローされました!",
"hidden": "かくされています",
"its_you": "これはあなたです!", "its_you": "これはあなたです!",
"media": "メディア", "media": "メディア",
"mention": "メンション", "mention": "メンション",
@ -559,6 +594,8 @@
"unmute": "ミュートをやめる", "unmute": "ミュートをやめる",
"unmute_progress": "ミュートをとりけしています...", "unmute_progress": "ミュートをとりけしています...",
"mute_progress": "ミュートしています...", "mute_progress": "ミュートしています...",
"hide_repeats": "リピートをかくす",
"show_repeats": "リピートをみる",
"admin_menu": { "admin_menu": {
"moderation": "モデレーション", "moderation": "モデレーション",
"grant_admin": "アドミンにする", "grant_admin": "アドミンにする",
@ -634,6 +671,8 @@
"return_home": "ホームページにもどる", "return_home": "ホームページにもどる",
"not_found": "そのメールアドレスまたはユーザーめいを、みつけることができませんでした。", "not_found": "そのメールアドレスまたはユーザーめいを、みつけることができませんでした。",
"too_many_requests": "パスワードリセットを、ためすことが、おおすぎます。しばらくしてから、ためしてください。", "too_many_requests": "パスワードリセットを、ためすことが、おおすぎます。しばらくしてから、ためしてください。",
"password_reset_disabled": "このインスタンスでは、パスワードリセットは、できません。インスタンスのアドミニストレーターに、おといあわせください。" "password_reset_disabled": "このインスタンスでは、パスワードリセットは、できません。インスタンスのアドミニストレーターに、おといあわせください。",
"password_reset_required": "ログインするには、パスワードをリセットしてください。",
"password_reset_required_but_mailer_is_disabled": "あなたはパスワードのリセットがひつようです。しかし、まずいことに、このインスタンスでは、パスワードのリセットができなくなっています。このインスタンスのアドミニストレーターに、おといあわせください。"
} }
} }

View file

@ -219,6 +219,8 @@
"subject_input_always_show": "Всегда показывать поле ввода темы", "subject_input_always_show": "Всегда показывать поле ввода темы",
"stop_gifs": "Проигрывать GIF анимации только при наведении", "stop_gifs": "Проигрывать GIF анимации только при наведении",
"streaming": "Включить автоматическую загрузку новых сообщений при прокрутке вверх", "streaming": "Включить автоматическую загрузку новых сообщений при прокрутке вверх",
"useStreamingApi": "Получать сообщения и уведомления в реальном времени",
"useStreamingApiWarning": "(Не рекомендуется, экспериментально, сообщения могут пропадать)",
"text": "Текст", "text": "Текст",
"theme": "Тема", "theme": "Тема",
"theme_help": "Используйте шестнадцатеричные коды цветов (#rrggbb) для настройки темы.", "theme_help": "Используйте шестнадцатеричные коды цветов (#rrggbb) для настройки темы.",

View file

@ -6,6 +6,7 @@ const api = {
backendInteractor: backendInteractorService(), backendInteractor: backendInteractorService(),
fetchers: {}, fetchers: {},
socket: null, socket: null,
mastoUserSocket: null,
followRequests: [] followRequests: []
}, },
mutations: { mutations: {
@ -15,7 +16,8 @@ const api = {
addFetcher (state, { fetcherName, fetcher }) { addFetcher (state, { fetcherName, fetcher }) {
state.fetchers[fetcherName] = fetcher state.fetchers[fetcherName] = fetcher
}, },
removeFetcher (state, { fetcherName }) { removeFetcher (state, { fetcherName, fetcher }) {
window.clearInterval(fetcher)
delete state.fetchers[fetcherName] delete state.fetchers[fetcherName]
}, },
setWsToken (state, token) { setWsToken (state, token) {
@ -29,32 +31,134 @@ const api = {
} }
}, },
actions: { actions: {
startFetchingTimeline (store, { timeline = 'friends', tag = false, userId = false }) { // Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets
// Don't start fetching if we already are. enableMastoSockets (store) {
const { state, dispatch } = store
if (state.mastoUserSocket) return
return dispatch('startMastoUserSocket')
},
disableMastoSockets (store) {
const { state, dispatch } = store
if (!state.mastoUserSocket) return
return dispatch('stopMastoUserSocket')
},
// MastoAPI 'User' sockets
startMastoUserSocket (store) {
return new Promise((resolve, reject) => {
try {
const { state, dispatch, rootState } = store
const timelineData = rootState.statuses.timelines.friends
state.mastoUserSocket = state.backendInteractor.startUserSocket({ store })
state.mastoUserSocket.addEventListener(
'message',
({ detail: message }) => {
if (!message) return // pings
if (message.event === 'notification') {
dispatch('addNewNotifications', {
notifications: [message.notification],
older: false
})
} else if (message.event === 'update') {
dispatch('addNewStatuses', {
statuses: [message.status],
userId: false,
showImmediately: timelineData.visibleStatuses.length === 0,
timeline: 'friends'
})
}
}
)
state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
console.error('Error in MastoAPI websocket:', error)
})
state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
const ignoreCodes = new Set([
1000, // Normal (intended) closure
1001 // Going away
])
const { code } = closeEvent
if (ignoreCodes.has(code)) {
console.debug(`Not restarting socket becasue of closure code ${code} is in ignore list`)
} else {
console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
dispatch('restartMastoUserSocket')
}
})
resolve()
} catch (e) {
reject(e)
}
})
},
restartMastoUserSocket ({ dispatch }) {
// This basically starts MastoAPI user socket and stops conventional
// fetchers when connection reestablished
return dispatch('startMastoUserSocket').then(() => {
dispatch('stopFetchingTimeline', { timeline: 'friends' })
dispatch('stopFetchingNotifications')
})
},
stopMastoUserSocket ({ state, dispatch }) {
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
console.log(state.mastoUserSocket)
state.mastoUserSocket.close()
},
// Timelines
startFetchingTimeline (store, {
timeline = 'friends',
tag = false,
userId = false
}) {
if (store.state.fetchers[timeline]) return if (store.state.fetchers[timeline]) return
const fetcher = store.state.backendInteractor.startFetchingTimeline({ timeline, store, userId, tag }) const fetcher = store.state.backendInteractor.startFetchingTimeline({
timeline, store, userId, tag
})
store.commit('addFetcher', { fetcherName: timeline, fetcher }) store.commit('addFetcher', { fetcherName: timeline, fetcher })
}, },
startFetchingNotifications (store) { stopFetchingTimeline (store, timeline) {
// Don't start fetching if we already are. const fetcher = store.state.fetchers[timeline]
if (store.state.fetchers['notifications']) return if (!fetcher) return
store.commit('removeFetcher', { fetcherName: timeline, fetcher })
},
// Notifications
startFetchingNotifications (store) {
if (store.state.fetchers.notifications) return
const fetcher = store.state.backendInteractor.startFetchingNotifications({ store }) const fetcher = store.state.backendInteractor.startFetchingNotifications({ store })
store.commit('addFetcher', { fetcherName: 'notifications', fetcher }) store.commit('addFetcher', { fetcherName: 'notifications', fetcher })
}, },
startFetchingFollowRequest (store) { stopFetchingNotifications (store) {
// Don't start fetching if we already are. const fetcher = store.state.fetchers.notifications
if (store.state.fetchers['followRequest']) return if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
},
fetchAndUpdateNotifications (store) {
store.state.backendInteractor.fetchAndUpdateNotifications({ store })
},
const fetcher = store.state.backendInteractor.startFetchingFollowRequest({ store }) // Follow requests
store.commit('addFetcher', { fetcherName: 'followRequest', fetcher }) startFetchingFollowRequests (store) {
if (store.state.fetchers['followRequests']) return
const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
}, },
stopFetching (store, fetcherName) { stopFetchingFollowRequests (store) {
const fetcher = store.state.fetchers[fetcherName] const fetcher = store.state.fetchers.followRequests
window.clearInterval(fetcher) if (!fetcher) return
store.commit('removeFetcher', { fetcherName }) store.commit('removeFetcher', { fetcherName: 'followRequests', fetcher })
}, },
removeFollowRequest (store, request) {
let requests = store.state.followRequests.filter((it) => it !== request)
store.commit('setFollowRequests', requests)
},
// Pleroma websocket
setWsToken (store, token) { setWsToken (store, token) {
store.commit('setWsToken', token) store.commit('setWsToken', token)
}, },
@ -72,10 +176,6 @@ const api = {
disconnectFromSocket ({ commit, state }) { disconnectFromSocket ({ commit, state }) {
state.socket && state.socket.disconnect() state.socket && state.socket.disconnect()
commit('setSocket', null) commit('setSocket', null)
},
removeFollowRequest (store, request) {
let requests = store.state.followRequests.filter((it) => it !== request)
store.commit('setFollowRequests', requests)
} }
} }
} }

View file

@ -7,7 +7,6 @@ const RECOVERY_STRATEGY = 'recovery'
// initial state // initial state
const state = { const state = {
app: null,
settings: {}, settings: {},
strategy: PASSWORD_STRATEGY, strategy: PASSWORD_STRATEGY,
initStrategy: PASSWORD_STRATEGY // default strategy from config initStrategy: PASSWORD_STRATEGY // default strategy from config
@ -16,14 +15,10 @@ const state = {
const resetState = (state) => { const resetState = (state) => {
state.strategy = state.initStrategy state.strategy = state.initStrategy
state.settings = {} state.settings = {}
state.app = null
} }
// getters // getters
const getters = { const getters = {
app: (state, getters) => {
return state.app
},
settings: (state, getters) => { settings: (state, getters) => {
return state.settings return state.settings
}, },
@ -55,9 +50,8 @@ const mutations = {
requireToken (state) { requireToken (state) {
state.strategy = TOKEN_STRATEGY state.strategy = TOKEN_STRATEGY
}, },
requireMFA (state, { app, settings }) { requireMFA (state, { settings }) {
state.settings = settings state.settings = settings
state.app = app
state.strategy = TOTP_STRATEGY // default strategy of MFA state.strategy = TOTP_STRATEGY // default strategy of MFA
}, },
requireRecovery (state) { requireRecovery (state) {

View file

@ -36,6 +36,7 @@ export const defaultState = {
highlight: {}, highlight: {},
interfaceLanguage: browserLocale, interfaceLanguage: browserLocale,
hideScopeNotice: false, hideScopeNotice: false,
useStreamingApi: false,
scopeCopy: undefined, // instance default scopeCopy: undefined, // instance default
subjectLineBehavior: undefined, // instance default subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined, // instance default alwaysShowSubjectInput: undefined, // instance default

View file

@ -27,6 +27,7 @@ const defaultState = {
scopeCopy: true, scopeCopy: true,
subjectLineBehavior: 'email', subjectLineBehavior: 'email',
postContentType: 'text/plain', postContentType: 'text/plain',
hideSitename: false,
nsfwCensorImage: undefined, nsfwCensorImage: undefined,
vapidPublicKey: undefined, vapidPublicKey: undefined,
noAttachmentLinks: false, noAttachmentLinks: false,

View file

@ -9,7 +9,7 @@ const oauthTokens = {
}) })
}, },
revokeToken ({ rootState, commit, state }, id) { revokeToken ({ rootState, commit, state }, id) {
rootState.api.backendInteractor.revokeOAuthToken(id).then((response) => { rootState.api.backendInteractor.revokeOAuthToken({ id }).then((response) => {
if (response.status === 201) { if (response.status === 201) {
commit('swapTokens', state.tokens.filter(token => token.id !== id)) commit('swapTokens', state.tokens.filter(token => token.id !== id))
} }

View file

@ -40,7 +40,7 @@ const polls = {
commit('mergeOrAddPoll', poll) commit('mergeOrAddPoll', poll)
}, },
updateTrackedPoll ({ rootState, dispatch, commit }, pollId) { updateTrackedPoll ({ rootState, dispatch, commit }, pollId) {
rootState.api.backendInteractor.fetchPoll(pollId).then(poll => { rootState.api.backendInteractor.fetchPoll({ pollId }).then(poll => {
setTimeout(() => { setTimeout(() => {
if (rootState.polls.trackedPolls[pollId]) { if (rootState.polls.trackedPolls[pollId]) {
dispatch('updateTrackedPoll', pollId) dispatch('updateTrackedPoll', pollId)
@ -59,7 +59,7 @@ const polls = {
commit('untrackPoll', pollId) commit('untrackPoll', pollId)
}, },
votePoll ({ rootState, commit }, { id, pollId, choices }) { votePoll ({ rootState, commit }, { id, pollId, choices }) {
return rootState.api.backendInteractor.vote(pollId, choices).then(poll => { return rootState.api.backendInteractor.vote({ pollId, choices }).then(poll => {
commit('mergeOrAddPoll', poll) commit('mergeOrAddPoll', poll)
return poll return poll
}) })

View file

@ -38,6 +38,7 @@ export const defaultState = () => ({
notifications: emptyNotifications(), notifications: emptyNotifications(),
favorites: new Set(), favorites: new Set(),
error: false, error: false,
errorData: null,
timelines: { timelines: {
mentions: emptyTl(), mentions: emptyTl(),
public: emptyTl(), public: emptyTl(),
@ -483,6 +484,9 @@ export const mutations = {
setError (state, { value }) { setError (state, { value }) {
state.error = value state.error = value
}, },
setErrorData (state, { value }) {
state.errorData = value
},
setNotificationsLoading (state, { value }) { setNotificationsLoading (state, { value }) {
state.notifications.loading = value state.notifications.loading = value
}, },
@ -532,6 +536,9 @@ const statuses = {
setError ({ rootState, commit }, { value }) { setError ({ rootState, commit }, { value }) {
commit('setError', { value }) commit('setError', { value })
}, },
setErrorData ({ rootState, commit }, { value }) {
commit('setErrorData', { value })
},
setNotificationsLoading ({ rootState, commit }, { value }) { setNotificationsLoading ({ rootState, commit }, { value }) {
commit('setNotificationsLoading', { value }) commit('setNotificationsLoading', { value })
}, },
@ -555,45 +562,45 @@ const statuses = {
favorite ({ rootState, commit }, status) { favorite ({ rootState, commit }, status) {
// Optimistic favoriting... // Optimistic favoriting...
commit('setFavorited', { status, value: true }) commit('setFavorited', { status, value: true })
rootState.api.backendInteractor.favorite(status.id) rootState.api.backendInteractor.favorite({ id: status.id })
.then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser })) .then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
}, },
unfavorite ({ rootState, commit }, status) { unfavorite ({ rootState, commit }, status) {
// Optimistic unfavoriting... // Optimistic unfavoriting...
commit('setFavorited', { status, value: false }) commit('setFavorited', { status, value: false })
rootState.api.backendInteractor.unfavorite(status.id) rootState.api.backendInteractor.unfavorite({ id: status.id })
.then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser })) .then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
}, },
fetchPinnedStatuses ({ rootState, dispatch }, userId) { fetchPinnedStatuses ({ rootState, dispatch }, userId) {
rootState.api.backendInteractor.fetchPinnedStatuses(userId) rootState.api.backendInteractor.fetchPinnedStatuses({ id: userId })
.then(statuses => dispatch('addNewStatuses', { statuses, timeline: 'user', userId, showImmediately: true, noIdUpdate: true })) .then(statuses => dispatch('addNewStatuses', { statuses, timeline: 'user', userId, showImmediately: true, noIdUpdate: true }))
}, },
pinStatus ({ rootState, dispatch }, statusId) { pinStatus ({ rootState, dispatch }, statusId) {
return rootState.api.backendInteractor.pinOwnStatus(statusId) return rootState.api.backendInteractor.pinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] })) .then((status) => dispatch('addNewStatuses', { statuses: [status] }))
}, },
unpinStatus ({ rootState, dispatch }, statusId) { unpinStatus ({ rootState, dispatch }, statusId) {
rootState.api.backendInteractor.unpinOwnStatus(statusId) rootState.api.backendInteractor.unpinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] })) .then((status) => dispatch('addNewStatuses', { statuses: [status] }))
}, },
muteConversation ({ rootState, commit }, statusId) { muteConversation ({ rootState, commit }, statusId) {
return rootState.api.backendInteractor.muteConversation(statusId) return rootState.api.backendInteractor.muteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status)) .then((status) => commit('setMutedStatus', status))
}, },
unmuteConversation ({ rootState, commit }, statusId) { unmuteConversation ({ rootState, commit }, statusId) {
return rootState.api.backendInteractor.unmuteConversation(statusId) return rootState.api.backendInteractor.unmuteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status)) .then((status) => commit('setMutedStatus', status))
}, },
retweet ({ rootState, commit }, status) { retweet ({ rootState, commit }, status) {
// Optimistic retweeting... // Optimistic retweeting...
commit('setRetweeted', { status, value: true }) commit('setRetweeted', { status, value: true })
rootState.api.backendInteractor.retweet(status.id) rootState.api.backendInteractor.retweet({ id: status.id })
.then(status => commit('setRetweetedConfirm', { status: status.retweeted_status, user: rootState.users.currentUser })) .then(status => commit('setRetweetedConfirm', { status: status.retweeted_status, user: rootState.users.currentUser }))
}, },
unretweet ({ rootState, commit }, status) { unretweet ({ rootState, commit }, status) {
// Optimistic unretweeting... // Optimistic unretweeting...
commit('setRetweeted', { status, value: false }) commit('setRetweeted', { status, value: false })
rootState.api.backendInteractor.unretweet(status.id) rootState.api.backendInteractor.unretweet({ id: status.id })
.then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser })) .then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser }))
}, },
queueFlush ({ rootState, commit }, { timeline, id }) { queueFlush ({ rootState, commit }, { timeline, id }) {
@ -608,19 +615,19 @@ const statuses = {
}, },
fetchFavsAndRepeats ({ rootState, commit }, id) { fetchFavsAndRepeats ({ rootState, commit }, id) {
Promise.all([ Promise.all([
rootState.api.backendInteractor.fetchFavoritedByUsers(id), rootState.api.backendInteractor.fetchFavoritedByUsers({ id }),
rootState.api.backendInteractor.fetchRebloggedByUsers(id) rootState.api.backendInteractor.fetchRebloggedByUsers({ id })
]).then(([favoritedByUsers, rebloggedByUsers]) => { ]).then(([favoritedByUsers, rebloggedByUsers]) => {
commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser }) commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser })
commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser }) commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser })
}) })
}, },
fetchFavs ({ rootState, commit }, id) { fetchFavs ({ rootState, commit }, id) {
rootState.api.backendInteractor.fetchFavoritedByUsers(id) rootState.api.backendInteractor.fetchFavoritedByUsers({ id })
.then(favoritedByUsers => commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser })) .then(favoritedByUsers => commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser }))
}, },
fetchRepeats ({ rootState, commit }, id) { fetchRepeats ({ rootState, commit }, id) {
rootState.api.backendInteractor.fetchRebloggedByUsers(id) rootState.api.backendInteractor.fetchRebloggedByUsers({ id })
.then(rebloggedByUsers => commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser })) .then(rebloggedByUsers => commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser }))
}, },
search (store, { q, resolve, limit, offset, following }) { search (store, { q, resolve, limit, offset, following }) {

View file

@ -32,7 +32,7 @@ const getNotificationPermission = () => {
} }
const blockUser = (store, id) => { const blockUser = (store, id) => {
return store.rootState.api.backendInteractor.blockUser(id) return store.rootState.api.backendInteractor.blockUser({ id })
.then((relationship) => { .then((relationship) => {
store.commit('updateUserRelationship', [relationship]) store.commit('updateUserRelationship', [relationship])
store.commit('addBlockId', id) store.commit('addBlockId', id)
@ -43,12 +43,12 @@ const blockUser = (store, id) => {
} }
const unblockUser = (store, id) => { const unblockUser = (store, id) => {
return store.rootState.api.backendInteractor.unblockUser(id) return store.rootState.api.backendInteractor.unblockUser({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship])) .then((relationship) => store.commit('updateUserRelationship', [relationship]))
} }
const muteUser = (store, id) => { const muteUser = (store, id) => {
return store.rootState.api.backendInteractor.muteUser(id) return store.rootState.api.backendInteractor.muteUser({ id })
.then((relationship) => { .then((relationship) => {
store.commit('updateUserRelationship', [relationship]) store.commit('updateUserRelationship', [relationship])
store.commit('addMuteId', id) store.commit('addMuteId', id)
@ -56,7 +56,7 @@ const muteUser = (store, id) => {
} }
const unmuteUser = (store, id) => { const unmuteUser = (store, id) => {
return store.rootState.api.backendInteractor.unmuteUser(id) return store.rootState.api.backendInteractor.unmuteUser({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship])) .then((relationship) => store.commit('updateUserRelationship', [relationship]))
} }
@ -95,9 +95,9 @@ export const mutations = {
newRights[right] = value newRights[right] = value
set(user, 'rights', newRights) set(user, 'rights', newRights)
}, },
updateActivationStatus (state, { user: { id }, status }) { updateActivationStatus (state, { user: { id }, deactivated }) {
const user = state.usersObject[id] const user = state.usersObject[id]
set(user, 'deactivated', !status) set(user, 'deactivated', deactivated)
}, },
setCurrentUser (state, user) { setCurrentUser (state, user) {
state.lastLoginName = user.screen_name state.lastLoginName = user.screen_name
@ -324,13 +324,18 @@ const users = {
commit('clearFollowers', userId) commit('clearFollowers', userId)
}, },
subscribeUser ({ rootState, commit }, id) { subscribeUser ({ rootState, commit }, id) {
return rootState.api.backendInteractor.subscribeUser(id) return rootState.api.backendInteractor.subscribeUser({ id })
.then((relationship) => commit('updateUserRelationship', [relationship])) .then((relationship) => commit('updateUserRelationship', [relationship]))
}, },
unsubscribeUser ({ rootState, commit }, id) { unsubscribeUser ({ rootState, commit }, id) {
return rootState.api.backendInteractor.unsubscribeUser(id) return rootState.api.backendInteractor.unsubscribeUser({ id })
.then((relationship) => commit('updateUserRelationship', [relationship])) .then((relationship) => commit('updateUserRelationship', [relationship]))
}, },
toggleActivationStatus ({ rootState, commit }, user) {
const api = user.deactivated ? rootState.api.backendInteractor.activateUser : rootState.api.backendInteractor.deactivateUser
api(user)
.then(({ deactivated }) => commit('updateActivationStatus', { user, deactivated }))
},
registerPushNotifications (store) { registerPushNotifications (store) {
const token = store.state.currentUser.credentials const token = store.state.currentUser.credentials
const vapidPublicKey = store.rootState.instance.vapidPublicKey const vapidPublicKey = store.rootState.instance.vapidPublicKey
@ -384,7 +389,7 @@ const users = {
}) })
}, },
searchUsers (store, query) { searchUsers (store, query) {
return store.rootState.api.backendInteractor.searchUsers(query) return store.rootState.api.backendInteractor.searchUsers({ query })
.then((users) => { .then((users) => {
store.commit('addNewUsers', users) store.commit('addNewUsers', users)
return users return users
@ -396,7 +401,7 @@ const users = {
let rootState = store.rootState let rootState = store.rootState
try { try {
let data = await rootState.api.backendInteractor.register(userInfo) let data = await rootState.api.backendInteractor.register({ ...userInfo })
store.commit('signUpSuccess') store.commit('signUpSuccess')
store.commit('setToken', data.access_token) store.commit('setToken', data.access_token)
store.dispatch('loginUser', data.access_token) store.dispatch('loginUser', data.access_token)
@ -433,10 +438,10 @@ const users = {
store.commit('clearCurrentUser') store.commit('clearCurrentUser')
store.dispatch('disconnectFromSocket') store.dispatch('disconnectFromSocket')
store.commit('clearToken') store.commit('clearToken')
store.dispatch('stopFetching', 'friends') store.dispatch('stopFetchingTimeline', 'friends')
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
store.dispatch('stopFetching', 'notifications') store.dispatch('stopFetchingNotifications')
store.dispatch('stopFetching', 'followRequest') store.dispatch('stopFetchingFollowRequests')
store.commit('clearNotifications') store.commit('clearNotifications')
store.commit('resetStatuses') store.commit('resetStatuses')
}) })
@ -471,11 +476,24 @@ const users = {
store.dispatch('initializeSocket') store.dispatch('initializeSocket')
} }
// Start getting fresh posts. const startPolling = () => {
store.dispatch('startFetchingTimeline', { timeline: 'friends' }) // Start getting fresh posts.
store.dispatch('startFetchingTimeline', { timeline: 'friends' })
// Start fetching notifications // Start fetching notifications
store.dispatch('startFetchingNotifications') store.dispatch('startFetchingNotifications')
}
if (store.getters.mergedConfig.useStreamingApi) {
store.dispatch('enableMastoSockets').catch((error) => {
console.error('Failed initializing MastoAPI Streaming socket', error)
startPolling()
}).then(() => {
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
})
} else {
startPolling()
}
// Get user mutes // Get user mutes
store.dispatch('fetchMutes') store.dispatch('fetchMutes')

View file

@ -1,4 +1,4 @@
import { each, map, concat, last } from 'lodash' import { each, map, concat, last, get } from 'lodash'
import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js' import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
import 'whatwg-fetch' import 'whatwg-fetch'
import { RegistrationError, StatusCodeError } from '../errors/errors' import { RegistrationError, StatusCodeError } from '../errors/errors'
@ -12,7 +12,8 @@ const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password' const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
const TAG_USER_URL = '/api/pleroma/admin/users/tag' const TAG_USER_URL = '/api/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}` const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
const ACTIVATION_STATUS_URL = screenName => `/api/pleroma/admin/users/${screenName}/activation_status` const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate'
const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
const ADMIN_USERS_URL = '/api/pleroma/admin/users' const ADMIN_USERS_URL = '/api/pleroma/admin/users'
const SUGGESTIONS_URL = '/api/v1/suggestions' const SUGGESTIONS_URL = '/api/v1/suggestions'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings' const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
@ -22,7 +23,7 @@ const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp' const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp'
const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp' const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp'
const MFA_DISABLE_OTP_URL = '/api/pleroma/account/mfa/totp' const MFA_DISABLE_OTP_URL = '/api/pleroma/accounts/mfa/totp'
const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials' const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
const MASTODON_REGISTRATION_URL = '/api/v1/accounts' const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
@ -71,6 +72,7 @@ const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute` const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
const MASTODON_SEARCH_2 = `/api/v2/search` const MASTODON_SEARCH_2 = `/api/v2/search`
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search' const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
const MASTODON_STREAMING = '/api/v1/streaming'
const oldfetch = window.fetch const oldfetch = window.fetch
@ -450,20 +452,26 @@ const deleteRight = ({ right, credentials, ...user }) => {
}) })
} }
const setActivationStatus = ({ status, credentials, ...user }) => { const activateUser = ({ credentials, user: { screen_name: nickname } }) => {
const screenName = user.screen_name return promisedRequest({
const body = { url: ACTIVATE_USER_URL,
status: status method: 'PATCH',
} credentials,
payload: {
nicknames: [nickname]
}
}).then(response => get(response, 'users.0'))
}
const headers = authHeaders(credentials) const deactivateUser = ({ credentials, user: { screen_name: nickname } }) => {
headers['Content-Type'] = 'application/json' return promisedRequest({
url: DEACTIVATE_USER_URL,
return fetch(ACTIVATION_STATUS_URL(screenName), { method: 'PATCH',
method: 'PUT', credentials,
headers: headers, payload: {
body: JSON.stringify(body) nicknames: [nickname]
}) }
}).then(response => get(response, 'users.0'))
} }
const deleteUser = ({ credentials, ...user }) => { const deleteUser = ({ credentials, ...user }) => {
@ -529,16 +537,24 @@ const fetchTimeline = ({
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&') const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
url += `?${queryString}` url += `?${queryString}`
let status = ''
let statusText = ''
return fetch(url, { headers: authHeaders(credentials) }) return fetch(url, { headers: authHeaders(credentials) })
.then((data) => { .then((data) => {
if (data.ok) { status = data.status
return data statusText = data.statusText
} return data
throw new Error('Error fetching timeline', data)
}) })
.then((data) => data.json()) .then((data) => data.json())
.then((data) => data.map(isNotifications ? parseNotification : parseStatus)) .then((data) => {
if (!data.error) {
return data.map(isNotifications ? parseNotification : parseStatus)
} else {
data.status = status
data.statusText = statusText
return data
}
})
} }
const fetchPinnedStatuses = ({ id, credentials }) => { const fetchPinnedStatuses = ({ id, credentials }) => {
@ -932,6 +948,99 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
}) })
} }
export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
return Object.entries({
...(credentials
? { access_token: credentials }
: {}
),
stream,
...args
}).reduce((acc, [key, val]) => {
return acc + `${key}=${val}&`
}, MASTODON_STREAMING + '?')
}
const MASTODON_STREAMING_EVENTS = new Set([
'update',
'notification',
'delete',
'filters_changed'
])
// A thin wrapper around WebSocket API that allows adding a pre-processor to it
// Uses EventTarget and a CustomEvent to proxy events
export const ProcessedWS = ({
url,
preprocessor = handleMastoWS,
id = 'Unknown'
}) => {
const eventTarget = new EventTarget()
const socket = new WebSocket(url)
if (!socket) throw new Error(`Failed to create socket ${id}`)
const proxy = (original, eventName, processor = a => a) => {
original.addEventListener(eventName, (eventData) => {
eventTarget.dispatchEvent(new CustomEvent(
eventName,
{ detail: processor(eventData) }
))
})
}
socket.addEventListener('open', (wsEvent) => {
console.debug(`[WS][${id}] Socket connected`, wsEvent)
})
socket.addEventListener('error', (wsEvent) => {
console.debug(`[WS][${id}] Socket errored`, wsEvent)
})
socket.addEventListener('close', (wsEvent) => {
console.debug(
`[WS][${id}] Socket disconnected with code ${wsEvent.code}`,
wsEvent
)
})
// Commented code reason: very spammy, uncomment to enable message debug logging
/*
socket.addEventListener('message', (wsEvent) => {
console.debug(
`[WS][${id}] Message received`,
wsEvent
)
})
/**/
proxy(socket, 'open')
proxy(socket, 'close')
proxy(socket, 'message', preprocessor)
proxy(socket, 'error')
// 1000 = Normal Closure
eventTarget.close = () => { socket.close(1000, 'Shutting down socket') }
return eventTarget
}
export const handleMastoWS = (wsEvent) => {
const { data } = wsEvent
if (!data) return
const parsedEvent = JSON.parse(data)
const { event, payload } = parsedEvent
if (MASTODON_STREAMING_EVENTS.has(event)) {
// MastoBE and PleromaBE both send payload for delete as a PLAIN string
if (event === 'delete') {
return { event, id: payload }
}
const data = payload ? JSON.parse(payload) : null
if (event === 'update') {
return { event, status: parseStatus(data) }
} else if (event === 'notification') {
return { event, notification: parseNotification(data) }
}
} else {
console.warn('Unknown event', wsEvent)
return null
}
}
const apiService = { const apiService = {
verifyCredentials, verifyCredentials,
fetchTimeline, fetchTimeline,
@ -971,7 +1080,8 @@ const apiService = {
deleteUser, deleteUser,
addRight, addRight,
deleteRight, deleteRight,
setActivationStatus, activateUser,
deactivateUser,
register, register,
getCaptcha, getCaptcha,
updateAvatar, updateAvatar,

View file

@ -1,230 +1,39 @@
import apiService from '../api/api.service.js' import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js'
import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js' import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js'
import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js' import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service' import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
const backendInteractorService = credentials => { const backendInteractorService = credentials => ({
const fetchStatus = ({ id }) => { startFetchingTimeline ({ timeline, store, userId = false, tag }) {
return apiService.fetchStatus({ id, credentials })
}
const fetchConversation = ({ id }) => {
return apiService.fetchConversation({ id, credentials })
}
const fetchFriends = ({ id, maxId, sinceId, limit }) => {
return apiService.fetchFriends({ id, maxId, sinceId, limit, credentials })
}
const exportFriends = ({ id }) => {
return apiService.exportFriends({ id, credentials })
}
const fetchFollowers = ({ id, maxId, sinceId, limit }) => {
return apiService.fetchFollowers({ id, maxId, sinceId, limit, credentials })
}
const fetchUser = ({ id }) => {
return apiService.fetchUser({ id, credentials })
}
const fetchUserRelationship = ({ id }) => {
return apiService.fetchUserRelationship({ id, credentials })
}
const followUser = ({ id, reblogs }) => {
return apiService.followUser({ credentials, id, reblogs })
}
const unfollowUser = (id) => {
return apiService.unfollowUser({ credentials, id })
}
const blockUser = (id) => {
return apiService.blockUser({ credentials, id })
}
const unblockUser = (id) => {
return apiService.unblockUser({ credentials, id })
}
const approveUser = (id) => {
return apiService.approveUser({ credentials, id })
}
const denyUser = (id) => {
return apiService.denyUser({ credentials, id })
}
const startFetchingTimeline = ({ timeline, store, userId = false, tag }) => {
return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag }) return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag })
} },
const startFetchingNotifications = ({ store }) => { startFetchingNotifications ({ store }) {
return notificationsFetcher.startFetching({ store, credentials }) return notificationsFetcher.startFetching({ store, credentials })
} },
const startFetchingFollowRequest = ({ store }) => { fetchAndUpdateNotifications ({ store }) {
return notificationsFetcher.fetchAndUpdate({ store, credentials })
},
startFetchingFollowRequest ({ store }) {
return followRequestFetcher.startFetching({ store, credentials }) return followRequestFetcher.startFetching({ store, credentials })
} },
// eslint-disable-next-line camelcase startUserSocket ({ store }) {
const tagUser = ({ screen_name }, tag) => { const serv = store.rootState.instance.server.replace('http', 'ws')
return apiService.tagUser({ screen_name, tag, credentials }) const url = serv + getMastodonSocketURI({ credentials, stream: 'user' })
} return ProcessedWS({ url, id: 'User' })
},
// eslint-disable-next-line camelcase ...Object.entries(apiService).reduce((acc, [key, func]) => {
const untagUser = ({ screen_name }, tag) => { return {
return apiService.untagUser({ screen_name, tag, credentials }) ...acc,
} [key]: (args) => func({ credentials, ...args })
}
}, {}),
// eslint-disable-next-line camelcase verifyCredentials: apiService.verifyCredentials
const addRight = ({ screen_name }, right) => { })
return apiService.addRight({ screen_name, right, credentials })
}
// eslint-disable-next-line camelcase
const deleteRight = ({ screen_name }, right) => {
return apiService.deleteRight({ screen_name, right, credentials })
}
// eslint-disable-next-line camelcase
const setActivationStatus = ({ screen_name }, status) => {
return apiService.setActivationStatus({ screen_name, status, credentials })
}
// eslint-disable-next-line camelcase
const deleteUser = ({ screen_name }) => {
return apiService.deleteUser({ screen_name, credentials })
}
const vote = (pollId, choices) => {
return apiService.vote({ credentials, pollId, choices })
}
const fetchPoll = (pollId) => {
return apiService.fetchPoll({ credentials, pollId })
}
const updateNotificationSettings = ({ settings }) => {
return apiService.updateNotificationSettings({ credentials, settings })
}
const fetchMutes = () => apiService.fetchMutes({ credentials })
const muteUser = (id) => apiService.muteUser({ credentials, id })
const unmuteUser = (id) => apiService.unmuteUser({ credentials, id })
const subscribeUser = (id) => apiService.subscribeUser({ credentials, id })
const unsubscribeUser = (id) => apiService.unsubscribeUser({ credentials, id })
const fetchBlocks = () => apiService.fetchBlocks({ credentials })
const fetchOAuthTokens = () => apiService.fetchOAuthTokens({ credentials })
const revokeOAuthToken = (id) => apiService.revokeOAuthToken({ id, credentials })
const fetchPinnedStatuses = (id) => apiService.fetchPinnedStatuses({ credentials, id })
const pinOwnStatus = (id) => apiService.pinOwnStatus({ credentials, id })
const unpinOwnStatus = (id) => apiService.unpinOwnStatus({ credentials, id })
const muteConversation = (id) => apiService.muteConversation({ credentials, id })
const unmuteConversation = (id) => apiService.unmuteConversation({ credentials, id })
const getCaptcha = () => apiService.getCaptcha()
const register = (params) => apiService.register({ credentials, params })
const updateAvatar = ({ avatar }) => apiService.updateAvatar({ credentials, avatar })
const updateBg = ({ background }) => apiService.updateBg({ credentials, background })
const updateBanner = ({ banner }) => apiService.updateBanner({ credentials, banner })
const updateProfile = ({ params }) => apiService.updateProfile({ credentials, params })
const importBlocks = (file) => apiService.importBlocks({ file, credentials })
const importFollows = (file) => apiService.importFollows({ file, credentials })
const deleteAccount = ({ password }) => apiService.deleteAccount({ credentials, password })
const changeEmail = ({ email, password }) => apiService.changeEmail({ credentials, email, password })
const changePassword = ({ password, newPassword, newPasswordConfirmation }) =>
apiService.changePassword({ credentials, password, newPassword, newPasswordConfirmation })
const fetchSettingsMFA = () => apiService.settingsMFA({ credentials })
const generateMfaBackupCodes = () => apiService.generateMfaBackupCodes({ credentials })
const mfaSetupOTP = () => apiService.mfaSetupOTP({ credentials })
const mfaConfirmOTP = ({ password, token }) => apiService.mfaConfirmOTP({ credentials, password, token })
const mfaDisableOTP = ({ password }) => apiService.mfaDisableOTP({ credentials, password })
const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({ id })
const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({ id })
const reportUser = (params) => apiService.reportUser({ credentials, ...params })
const favorite = (id) => apiService.favorite({ id, credentials })
const unfavorite = (id) => apiService.unfavorite({ id, credentials })
const retweet = (id) => apiService.retweet({ id, credentials })
const unretweet = (id) => apiService.unretweet({ id, credentials })
const search2 = ({ q, resolve, limit, offset, following }) =>
apiService.search2({ credentials, q, resolve, limit, offset, following })
const searchUsers = (query) => apiService.searchUsers({ query, credentials })
const backendInteractorServiceInstance = {
fetchStatus,
fetchConversation,
fetchFriends,
exportFriends,
fetchFollowers,
followUser,
unfollowUser,
blockUser,
unblockUser,
fetchUser,
fetchUserRelationship,
verifyCredentials: apiService.verifyCredentials,
startFetchingTimeline,
startFetchingNotifications,
startFetchingFollowRequest,
fetchMutes,
muteUser,
unmuteUser,
subscribeUser,
unsubscribeUser,
fetchBlocks,
fetchOAuthTokens,
revokeOAuthToken,
fetchPinnedStatuses,
pinOwnStatus,
unpinOwnStatus,
muteConversation,
unmuteConversation,
tagUser,
untagUser,
addRight,
deleteRight,
deleteUser,
setActivationStatus,
register,
getCaptcha,
updateAvatar,
updateBg,
updateBanner,
updateProfile,
importBlocks,
importFollows,
deleteAccount,
changeEmail,
changePassword,
fetchSettingsMFA,
generateMfaBackupCodes,
mfaSetupOTP,
mfaConfirmOTP,
mfaDisableOTP,
approveUser,
denyUser,
vote,
fetchPoll,
fetchFavoritedByUsers,
fetchRebloggedByUsers,
reportUser,
favorite,
unfavorite,
retweet,
unretweet,
updateNotificationSettings,
search2,
searchUsers
}
return backendInteractorServiceInstance
}
export default backendInteractorService export default backendInteractorService

View file

@ -39,7 +39,7 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => {
}) })
export const requestUnfollow = (user, store) => new Promise((resolve, reject) => { export const requestUnfollow = (user, store) => new Promise((resolve, reject) => {
store.state.api.backendInteractor.unfollowUser(user.id) store.state.api.backendInteractor.unfollowUser({ id: user.id })
.then((updated) => { .then((updated) => {
store.commit('updateUserRelationship', [updated]) store.commit('updateUserRelationship', [updated])
resolve({ resolve({

View file

@ -1,9 +1,9 @@
const verifyOTPCode = ({ app, instance, mfaToken, code }) => { const verifyOTPCode = ({ clientId, clientSecret, instance, mfaToken, code }) => {
const url = `${instance}/oauth/mfa/challenge` const url = `${instance}/oauth/mfa/challenge`
const form = new window.FormData() const form = new window.FormData()
form.append('client_id', app.client_id) form.append('client_id', clientId)
form.append('client_secret', app.client_secret) form.append('client_secret', clientSecret)
form.append('mfa_token', mfaToken) form.append('mfa_token', mfaToken)
form.append('code', code) form.append('code', code)
form.append('challenge_type', 'totp') form.append('challenge_type', 'totp')
@ -14,12 +14,12 @@ const verifyOTPCode = ({ app, instance, mfaToken, code }) => {
}).then((data) => data.json()) }).then((data) => data.json())
} }
const verifyRecoveryCode = ({ app, instance, mfaToken, code }) => { const verifyRecoveryCode = ({ clientId, clientSecret, instance, mfaToken, code }) => {
const url = `${instance}/oauth/mfa/challenge` const url = `${instance}/oauth/mfa/challenge`
const form = new window.FormData() const form = new window.FormData()
form.append('client_id', app.client_id) form.append('client_id', clientId)
form.append('client_secret', app.client_secret) form.append('client_secret', clientSecret)
form.append('mfa_token', mfaToken) form.append('mfa_token', mfaToken)
form.append('code', code) form.append('code', code)
form.append('challenge_type', 'recovery') form.append('challenge_type', 'recovery')

View file

@ -12,7 +12,7 @@ export const getOrCreateApp = ({ clientId, clientSecret, instance, commit }) =>
form.append('client_name', `PleromaFE_${window.___pleromafe_commit_hash}_${(new Date()).toISOString()}`) form.append('client_name', `PleromaFE_${window.___pleromafe_commit_hash}_${(new Date()).toISOString()}`)
form.append('redirect_uris', REDIRECT_URI) form.append('redirect_uris', REDIRECT_URI)
form.append('scopes', 'read write follow') form.append('scopes', 'read write follow push admin')
return window.fetch(url, { return window.fetch(url, {
method: 'POST', method: 'POST',
@ -28,7 +28,7 @@ const login = ({ instance, clientId }) => {
response_type: 'code', response_type: 'code',
client_id: clientId, client_id: clientId,
redirect_uri: REDIRECT_URI, redirect_uri: REDIRECT_URI,
scope: 'read write follow' scope: 'read write follow push admin'
} }
const dataString = reduce(data, (acc, v, k) => { const dataString = reduce(data, (acc, v, k) => {

View file

@ -6,6 +6,7 @@ const update = ({ store, statuses, timeline, showImmediately, userId }) => {
const ccTimeline = camelCase(timeline) const ccTimeline = camelCase(timeline)
store.dispatch('setError', { value: false }) store.dispatch('setError', { value: false })
store.dispatch('setErrorData', { value: null })
store.dispatch('addNewStatuses', { store.dispatch('addNewStatuses', {
timeline: ccTimeline, timeline: ccTimeline,
@ -45,6 +46,10 @@ const fetchAndUpdate = ({
return apiService.fetchTimeline(args) return apiService.fetchTimeline(args)
.then((statuses) => { .then((statuses) => {
if (statuses.error) {
store.dispatch('setErrorData', { value: statuses })
return
}
if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) { if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) {
store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId }) store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId })
} }

View file

@ -304,10 +304,40 @@
"code": 61668, "code": 61668,
"src": "fontawesome" "src": "fontawesome"
}, },
{
"uid": "31972e4e9d080eaa796290349ae6c1fd",
"css": "users",
"code": 59421,
"src": "fontawesome"
},
{
"uid": "e82cedfa1d5f15b00c5a81c9bd731ea2",
"css": "info-circled",
"code": 59423,
"src": "fontawesome"
},
{
"uid": "w3nzesrlbezu6f30q7ytyq919p6gdlb6",
"css": "home-2",
"code": 59425,
"src": "typicons"
},
{
"uid": "dcedf50ab1ede3283d7a6c70e2fe32f3",
"css": "chat",
"code": 59422,
"src": "fontawesome"
},
{
"uid": "3a00327e61b997b58518bd43ed83c3df",
"css": "login",
"code": 59424,
"src": "fontawesome"
},
{ {
"uid": "f3ebd6751c15a280af5cc5f4a764187d", "uid": "f3ebd6751c15a280af5cc5f4a764187d",
"css": "arrow-curved", "css": "arrow-curved",
"code": 59421, "code": 59426,
"src": "iconic" "src": "iconic"
} }
] ]

View file

@ -1,4 +1,4 @@
require('babel-register') require('@babel/register')
var config = require('../../config') var config = require('../../config')
// http://nightwatchjs.org/guide#settings-file // http://nightwatchjs.org/guide#settings-file

1437
yarn.lock

File diff suppressed because it is too large Load diff