fix merge conflicts

This commit is contained in:
Shpuld Shpuldson 2020-01-13 23:47:32 +02:00
commit b32888194c
101 changed files with 2167 additions and 2137 deletions

View file

@ -90,6 +90,7 @@ export default {
},
sitename () { return this.$store.state.instance.name },
chat () { return this.$store.state.chat.channel.state === 'joined' },
hideSitename () { return this.$store.state.instance.hideSitename },
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
showInstanceSpecificPanel () {
return this.$store.state.instance.showInstanceSpecificPanel &&
@ -97,7 +98,8 @@ export default {
this.$store.state.instance.instanceSpecificPanelContent
},
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: {
scrollToTop () {

View file

@ -855,3 +855,31 @@ nav {
.btn.btn-default {
min-height: 28px;
}
.animate-spin {
animation: spin 2s infinite linear;
display: inline-block;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
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 class="item">
<router-link
v-if="!hideSitename"
class="site-name"
:to="{ name: 'root' }"
active-class="home"
@ -40,6 +41,7 @@
</div>
<div class="item right">
<search-bar
v-if="currentUser || !privateMode"
class="nav-icon mobile-hidden"
@toggled="onSearchBarToggled"
@click.stop.native

View file

@ -108,6 +108,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('alwaysShowSubjectInput')
copyInstanceOption('noAttachmentLinks')
copyInstanceOption('showFeaturesPanel')
copyInstanceOption('hideSitename')
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: 'pleromaBackend', value: software.name === 'pleroma' })
const priv = metadata.private
store.dispatch('setInstanceOption', { name: 'private', value: priv })
const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') })
const federation = metadata.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
await resolveStaffAccounts({ store, accounts })

View file

@ -25,9 +25,6 @@ const AccountActions = {
},
reportUser () {
this.$store.dispatch('openUserReportingModal', this.user.id)
},
mentionUser () {
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
}
}
}

View file

@ -9,17 +9,7 @@
>
<div slot="popover">
<div class="dropdown-menu">
<button
class="btn btn-default btn-block dropdown-item"
@click="mentionUser"
>
{{ $t('user_card.mention') }}
</button>
<template v-if="user.following">
<div
role="separator"
class="dropdown-divider"
/>
<button
v-if="user.showing_reblogs"
class="btn btn-default dropdown-item"
@ -34,11 +24,11 @@
>
{{ $t('user_card.show_repeats') }}
</button>
<div
role="separator"
class="dropdown-divider"
/>
</template>
<div
role="separator"
class="dropdown-divider"
/>
<button
v-if="user.statusnet_blocking"
class="btn btn-default btn-block dropdown-item"

View file

@ -43,7 +43,8 @@ const conversation = {
'collapsable',
'isPage',
'pinnedStatusIdsObject',
'inProfile'
'inProfile',
'profileUserId'
],
created () {
if (this.isPage) {

View file

@ -27,6 +27,7 @@
:highlight="getHighlight()"
:replies="getReplies(status.id)"
:in-profile="inProfile"
:profile-user-id="profileUserId"
class="status-fadein panel-body"
@goto="setHighlight"
@toggleExpanded="toggleExpanded"

View file

@ -7,11 +7,11 @@ const FollowRequestCard = {
},
methods: {
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)
},
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)
}
}

View file

@ -3,7 +3,8 @@ import Notifications from '../notifications/notifications.vue'
const tabModeDict = {
mentions: ['mention'],
'likes+repeats': ['repeat', 'like'],
follows: ['follow']
follows: ['follow'],
moves: ['move']
}
const Interactions = {

View file

@ -21,6 +21,10 @@
key="follows"
:label="$t('interactions.follows')"
/>
<span
key="moves"
:label="$t('interactions.moves')"
/>
</tab-switcher>
<Notifications
ref="notifications"

View file

@ -51,8 +51,8 @@ export default {
methods: {
getLanguageName (code) {
const specialLanguageNames = {
'ja': 'Japanese (やさしいにほんご)',
'ja_pedantic': 'Japanese (日本語)',
'ja': 'Japanese (日本語)',
'ja_easy': 'Japanese (やさしいにほんご)',
'zh': 'Chinese (简体中文)'
}
return specialLanguageNames[code] || ISO6391.getName(code)

View file

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

View file

@ -10,13 +10,13 @@
:src="currentMedia.url"
@touchstart.stop="mediaTouchStart"
@touchmove.stop="mediaTouchMove"
@click="hide"
>
<VideoAttachment
v-if="type === 'video'"
class="modal-image"
:attachment="currentMedia"
:controls="true"
@click.stop.native=""
/>
<button
v-if="canNavigate"

View file

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

View file

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

View file

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

View file

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

View file

@ -45,12 +45,12 @@ const ModerationTools = {
toggleTag (tag) {
const store = this.$store
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 }
store.commit('untagUser', { user: this.user, tag })
})
} 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 }
store.commit('tagUser', { user: this.user, tag })
})
@ -59,24 +59,19 @@ const ModerationTools = {
toggleRight (right) {
const store = this.$store
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 }
store.commit('updateRight', { user: this.user, right: right, value: false })
store.commit('updateRight', { user: this.user, right, value: false })
})
} 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 }
store.commit('updateRight', { user: this.user, right: right, value: true })
store.commit('updateRight', { user: this.user, right, value: true })
})
}
},
toggleActivationStatus () {
const store = this.$store
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 })
})
this.$store.dispatch('toggleActivationStatus', { user: this.user })
},
deleteUserDialog (show) {
this.showDeleteUserDialog = show
@ -85,7 +80,7 @@ const ModerationTools = {
const store = this.$store
const user = this.user
const { id, name } = user
store.state.api.backendInteractor.deleteUser(user)
store.state.api.backendInteractor.deleteUser({ user })
.then(e => {
this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id)
const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'

View file

@ -1,16 +1,27 @@
import { mapState } from 'vuex'
import { get } from 'lodash'
const MRFTransparencyPanel = {
computed: mapState({
federationPolicy: state => state.instance.federationPolicy,
mrfPolicies: state => state.instance.federationPolicy.mrf_policies,
acceptInstances: state => state.instance.federationPolicy.mrf_simple.accept,
rejectInstances: state => state.instance.federationPolicy.mrf_simple.reject,
quarantineInstances: state => state.instance.federationPolicy.quarantined_instances,
ftlRemovalInstances: state => state.instance.federationPolicy.mrf_simple.federated_timeline_removal,
mediaNsfwInstances: state => state.instance.federationPolicy.mrf_simple.media_nsfw,
mediaRemovalInstances: state => state.instance.federationPolicy.mrf_simple.media_removal
})
computed: {
...mapState({
federationPolicy: state => get(state, 'instance.federationPolicy'),
mrfPolicies: state => get(state, 'instance.federationPolicy.mrf_policies', []),
quarantineInstances: state => get(state, 'instance.federationPolicy.quarantined_instances', []),
acceptInstances: state => get(state, 'instance.federationPolicy.mrf_simple.accept', []),
rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []),
ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
mediaNsfwInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []),
mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', [])
}),
hasInstanceSpecificPolicies () {
return this.quarantineInstances.length ||
this.acceptInstances.length ||
this.rejectInstances.length ||
this.ftlRemovalInstances.length ||
this.mediaNsfwInstances.length ||
this.mediaRemovalInstances.length
}
}
}
export default MRFTransparencyPanel

View file

@ -22,7 +22,9 @@
/>
</ul>
<h2>{{ $t("about.mrf_policy_simple") }}</h2>
<h2 v-if="hasInstanceSpecificPolicies">
{{ $t("about.mrf_policy_simple") }}
</h2>
<div v-if="acceptInstances.length">
<h4>{{ $t("about.mrf_policy_simple_accept") }}</h4>

View file

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

View file

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

View file

@ -43,18 +43,18 @@ const Notification = {
const user = this.notification.from_profile
return highlightStyle(highlight[user.screen_name])
},
userInStore () {
return this.$store.getters.findUser(this.notification.from_profile.id)
},
user () {
if (this.userInStore) {
return this.userInStore
}
return this.notification.from_profile
return this.$store.getters.findUser(this.notification.from_profile.id)
},
userProfileLink () {
return this.generateUserProfileLink(this.user)
},
targetUser () {
return this.$store.getters.findUser(this.notification.target.id)
},
targetUserProfileLink () {
return this.generateUserProfileLink(this.targetUser)
},
needMute () {
return this.user.muted
}

View file

@ -74,9 +74,13 @@
<i class="fa icon-user-plus lit" />
<small>{{ $t('notifications.followed_you') }}</small>
</span>
<span v-if="notification.type === 'move'">
<i class="fa icon-arrow-curved lit" />
<small>{{ $t('notifications.migrated_to') }}</small>
</span>
</div>
<div
v-if="notification.type === 'follow'"
v-if="notification.type === 'follow' || notification.type === 'move'"
class="timeago"
>
<span class="faint">
@ -115,6 +119,14 @@
@{{ notification.from_profile.screen_name }}
</router-link>
</div>
<div
v-else-if="notification.type === 'move'"
class="move-text"
>
<router-link :to="targetUserProfileLink">
@{{ notification.target.screen_name }}
</router-link>
</div>
<template v-else>
<status
class="faint"

View file

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

View file

@ -76,7 +76,7 @@
}
}
.follow-text {
.follow-text, .move-text {
padding: 0.5em 0;
}
@ -151,6 +151,11 @@
color: var(--cOrange, $fallback--cOrange);
}
.icon-arrow-curved.lit {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
.status-content {
margin: 0;
max-height: 300px;

View file

@ -169,9 +169,7 @@ const PostStatusForm = {
if (this.submitDisabled) { return }
if (this.newStatus.status === '') {
if (this.newStatus.files.length > 0) {
this.newStatus.status = '\u200b' // hack
} else {
if (this.newStatus.files.length === 0) {
this.error = 'Cannot post an empty status with no files'
return
}

View file

@ -10,7 +10,7 @@ const PublicAndExternalTimeline = {
this.$store.dispatch('startFetchingTimeline', { timeline: 'publicAndExternal' })
},
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' })
},
destroyed () {
this.$store.dispatch('stopFetching', 'public')
this.$store.dispatch('stopFetchingTimeline', 'public')
}
}

View file

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

View file

@ -84,7 +84,7 @@ const settings = {
}
}])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
// Special cases (need to transform values)
// Special cases (need to transform values or perform actions first)
muteWordsString: {
get () { return this.$store.getters.mergedConfig.muteWords.join('\n') },
set (value) {
@ -93,6 +93,22 @@ const settings = {
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

View file

@ -73,6 +73,15 @@
</li>
</ul>
</li>
<li>
<Checkbox v-model="useStreamingApi">
{{ $t('settings.useStreamingApi') }}
<br/>
<small>
{{ $t('settings.useStreamingApiWarning') }}
</small>
</Checkbox>
</li>
<li>
<Checkbox v-model="autoLoad">
{{ $t('settings.autoload') }}
@ -270,6 +279,17 @@
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.fun') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="greentext">
{{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
</Checkbox>
</li>
</ul>
</div>
</div>
<div :label="$t('settings.theme')">
@ -303,6 +323,11 @@
{{ $t('settings.notification_visibility_mentions') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationVisibility.moves">
{{ $t('settings.notification_visibility_moves') }}
</Checkbox>
</li>
</ul>
</div>
<div>

View file

@ -10,6 +10,10 @@ const SideDrawer = {
}),
created () {
this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, this.toggleDrawer)
if (this.currentUser && this.currentUser.locked) {
this.$store.dispatch('startFetchingFollowRequest')
}
},
components: { UserCard },
computed: {
@ -29,11 +33,20 @@ const SideDrawer = {
logo () {
return this.$store.state.instance.logo
},
hideSitename () {
return this.$store.state.instance.hideSitename
},
sitename () {
return this.$store.state.instance.name
},
followRequestCount () {
return this.$store.state.api.followRequests.length
},
privateMode () {
return this.$store.state.instance.private
},
federating () {
return this.$store.state.instance.federating
}
},
methods: {

View file

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

View file

@ -14,10 +14,11 @@ import Timeago from '../timeago/timeago.vue'
import StatusPopover from '../status_popover/status_popover.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service'
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
import { filter, unescape, uniqBy } from 'lodash'
import { mapGetters } from 'vuex'
import { mapGetters, mapState } from 'vuex'
const Status = {
name: 'Status',
@ -33,7 +34,8 @@ const Status = {
'noHeading',
'inlineExpanded',
'showPinned',
'inProfile'
'inProfile',
'profileUserId'
],
data () {
return {
@ -43,8 +45,8 @@ const Status = {
showingTall: this.inConversation && this.focused,
showingLongSubject: false,
error: null,
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
betterShadow: this.$store.state.interface.browserSupport.cssFilter
// not as computed because it sets the initial state which will be changed later
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
}
},
computed: {
@ -104,7 +106,7 @@ const Status = {
return this.$store.state.statuses.allStatusesObject[this.status.id]
},
loggedIn () {
return !!this.$store.state.users.currentUser
return !!this.currentUser
},
muteWordHits () {
const statusText = this.status.text.toLowerCase()
@ -115,7 +117,7 @@ const Status = {
return hits
},
muted () { return !this.unmuted && ((!this.inProfile && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) },
muted () { return !this.unmuted && ((!(this.inProfile && this.status.user.id === this.profileUserId) && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) },
hideFilteredStatuses () {
return this.mergedConfig.hideFilteredStatuses
},
@ -164,7 +166,7 @@ const Status = {
if (this.inConversation || !this.isReply) {
return false
}
if (this.status.user.id === this.$store.state.users.currentUser.id) {
if (this.status.user.id === this.currentUser.id) {
return false
}
if (this.status.type === 'retweet') {
@ -179,7 +181,7 @@ const Status = {
if (checkFollowing && taggedUser && taggedUser.following) {
return false
}
if (this.status.attentions[i].id === this.$store.state.users.currentUser.id) {
if (this.status.attentions[i].id === this.currentUser.id) {
return false
}
}
@ -256,11 +258,41 @@ const Status = {
maxThumbnails () {
return this.mergedConfig.maxThumbnails
},
postBodyHtml () {
const html = this.status.statusnet_html
if (this.mergedConfig.greentext) {
try {
if (html.includes('&gt;')) {
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
return processHtml(html, (string) => {
if (string.includes('&gt;') &&
string
.replace(/<[^>]+?>/gi, '') // remove all tags
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
.trim()
.startsWith('&gt;')) {
return `<span class='greentext'>${string}</span>`
} else {
return string
}
})
} else {
return html
}
} catch (e) {
console.err('Failed to process status html', e)
return html
}
} else {
return html
}
},
contentHtml () {
if (!this.status.summary_html) {
return this.status.statusnet_html
return this.postBodyHtml
}
return this.status.summary_html + '<br />' + this.status.statusnet_html
return this.status.summary_html + '<br />' + this.postBodyHtml
},
combinedFavsAndRepeatsUsers () {
// Use the status from the global status repository since favs and repeats are saved in it
@ -271,7 +303,7 @@ const Status = {
return uniqBy(combinedUsers, 'id')
},
ownStatus () {
return this.status.user.id === this.$store.state.users.currentUser.id
return this.status.user.id === this.currentUser.id
},
tags () {
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
@ -282,7 +314,11 @@ const Status = {
emojiReactions () {
return this.status.emojiReactions
},
...mapGetters(['mergedConfig'])
...mapGetters(['mergedConfig']),
...mapState({
betterShadow: state => state.interface.browserSupport.cssFilter,
currentUser: state => state.users.currentUser
})
},
components: {
Attachment,

View file

@ -623,7 +623,7 @@ $status-margin: 0.75em;
height: 100%;
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
linear-gradient(to top, white, white);
// Autoprefixed seem to ignore this one, and also syntax is different
/* Autoprefixed seem to ignore this one, and also syntax is different */
-webkit-mask-composite: xor;
mask-composite: exclude;
}
@ -769,7 +769,8 @@ $status-margin: 0.75em;
}
.greentext {
color: green;
color: $fallback--cGreen;
color: var(--cGreen, $fallback--cGreen);
}
.status-conversation {

View file

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

View file

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

View file

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

View file

@ -11,15 +11,22 @@
>
{{ $t('timeline.error_fetching') }}
</div>
<div
v-else-if="errorData"
class="loadmore-error alert error"
@click.prevent
>
{{ errorData.statusText }}
</div>
<button
v-if="timeline.newStatusCount > 0 && !timelineError"
v-if="timeline.newStatusCount > 0 && !timelineError && !errorData"
class="loadmore-button"
@click.prevent="showNewStatuses"
>
{{ $t('timeline.show_new') }}{{ newStatusCountStr }}
</button>
<div
v-if="!timeline.newStatusCount > 0 && !timelineError"
v-if="!timeline.newStatusCount > 0 && !timelineError && !errorData"
class="loadmore-text faint"
@click.prevent
>
@ -37,6 +44,7 @@
:collapsable="true"
:pinned-status-ids-object="pinnedStatusIdsObject"
:in-profile="inProfile"
:profile-user-id="userId"
/>
</template>
<template v-for="status in timeline.visibleStatuses">
@ -47,6 +55,7 @@
:status-id="status.id"
:collapsable="true"
:in-profile="inProfile"
:profile-user-id="userId"
/>
</template>
</div>
@ -65,12 +74,18 @@
{{ $t('timeline.no_more_statuses') }}
</div>
<a
v-else-if="!timeline.loading"
v-else-if="!timeline.loading && !errorData"
href="#"
@click.prevent="fetchOlderStatuses()"
>
<div class="new-status-notification text-center panel-footer">{{ $t('timeline.load_older') }}</div>
</a>
<a
v-else-if="errorData"
href="#"
>
<div class="new-status-notification text-center panel-footer">{{ errorData.error }}</div>
</a>
<div
v-else
class="new-status-notification text-center panel-footer"
@ -91,17 +106,4 @@
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>

View file

@ -149,6 +149,9 @@ export default {
}
this.$store.dispatch('setMedia', [attachment])
this.$store.dispatch('setCurrent', attachment)
},
mentionUser () {
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
}
}
}

View file

@ -175,6 +175,14 @@
{{ $t('user_card.mute') }}
</button>
</div>
<div>
<button
class="btn btn-default btn-block"
@click="mentionUser"
>
{{ $t('user_card.mention') }}
</button>
</div>
<ModerationTools
v-if="loggedIn.role === &quot;admin&quot;"
:user="user"

View file

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

View file

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

View file

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

View file

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

View file

@ -104,7 +104,7 @@
{{ $t('settings.hide_followers_count_description') }}
</Checkbox>
</p>
<p>
<p v-if="role === 'admin' || role === 'moderator'">
<Checkbox v-model="showRole">
<template v-if="role === 'admin'">
{{ $t('settings.show_admin_badge') }}

View file

@ -65,7 +65,7 @@ const withLoadMore = ({
}
}
},
render (createElement) {
render (h) {
const props = {
props: {
...this.$props,
@ -74,7 +74,7 @@ const withLoadMore = ({
on: this.$listeners,
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 (
<div class="with-load-more">
<WrappedComponent {...props}>

View file

@ -49,7 +49,7 @@ const withSubscription = ({
}
}
},
render (createElement) {
render (h) {
if (!this.error && !this.loading) {
const props = {
props: {
@ -59,7 +59,7 @@ const withSubscription = ({
on: this.$listeners,
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 (
<div class="with-subscription">
<WrappedComponent {...props}>

View file

@ -110,7 +110,8 @@
"notifications": "Notifications",
"read": "Read!",
"repeated_you": "repeated your status",
"no_more_notifications": "No more notifications"
"no_more_notifications": "No more notifications",
"migrated_to": "migrated to"
},
"polls": {
"add_poll": "Add Poll",
@ -140,6 +141,7 @@
"interactions": {
"favs_repeats": "Repeats and Favorites",
"follows": "New follows",
"moves": "User migrates",
"load_older": "Load older interactions"
},
"post_status": {
@ -311,6 +313,7 @@
"notification_visibility_likes": "Likes",
"notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats",
"notification_visibility_moves": "User Migrates",
"no_rich_text_description": "Strip rich text formatting from all posts",
"no_blocks": "No blocks",
"no_mutes": "No mutes",
@ -358,6 +361,8 @@
"post_status_content_type": "Post status content type",
"stop_gifs": "Play-on-hover GIFs",
"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",
"theme": "Theme",
"theme_help": "Use hex color codes (#rrggbb) to customize your color theme.",
@ -370,6 +375,8 @@
"false": "no",
"true": "yes"
},
"fun": "Fun",
"greentext": "Meme arrows",
"notifications": "Notifications",
"notification_setting": "Receive notifications from:",
"notification_setting_follows": "Users you follow",

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

View file

@ -23,8 +23,8 @@ const messages = {
he: require('./he.json'),
hu: require('./hu.json'),
it: require('./it.json'),
ja: require('./ja.json'),
ja_pedantic: require('./ja_pedantic.json'),
ja: require('./ja_pedantic.json'),
ja_easy: require('./ja_easy.json'),
ko: require('./ko.json'),
nb: require('./nb.json'),
nl: require('./nl.json'),

View file

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

View file

@ -111,7 +111,7 @@
},
"interactions": {
"favs_repeats": "转发和收藏",
"follows": "新的关注",
"follows": "新的关注",
"load_older": "加载更早的互动"
},
"post_status": {

View file

@ -6,6 +6,7 @@ const api = {
backendInteractor: backendInteractorService(),
fetchers: {},
socket: null,
mastoUserSocket: null,
followRequests: []
},
mutations: {
@ -15,7 +16,8 @@ const api = {
addFetcher (state, { fetcherName, fetcher }) {
state.fetchers[fetcherName] = fetcher
},
removeFetcher (state, { fetcherName }) {
removeFetcher (state, { fetcherName, fetcher }) {
window.clearInterval(fetcher)
delete state.fetchers[fetcherName]
},
setWsToken (state, token) {
@ -29,25 +31,134 @@ const api = {
}
},
actions: {
startFetchingTimeline (store, { timeline = 'friends', tag = false, userId = false }) {
// Don't start fetching if we already are.
// Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets
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
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 })
},
startFetchingNotifications (store) {
// Don't start fetching if we already are.
if (store.state.fetchers['notifications']) return
stopFetchingTimeline (store, timeline) {
const fetcher = store.state.fetchers[timeline]
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 })
store.commit('addFetcher', { fetcherName: 'notifications', fetcher })
},
stopFetching (store, fetcherName) {
const fetcher = store.state.fetchers[fetcherName]
window.clearInterval(fetcher)
store.commit('removeFetcher', { fetcherName })
stopFetchingNotifications (store) {
const fetcher = store.state.fetchers.notifications
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
},
fetchAndUpdateNotifications (store) {
store.state.backendInteractor.fetchAndUpdateNotifications({ store })
},
// Follow requests
startFetchingFollowRequests (store) {
if (store.state.fetchers['followRequests']) return
const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
},
stopFetchingFollowRequests (store) {
const fetcher = store.state.fetchers.followRequests
if (!fetcher) return
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) {
store.commit('setWsToken', token)
},
@ -65,10 +176,6 @@ const api = {
disconnectFromSocket ({ commit, state }) {
state.socket && state.socket.disconnect()
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
const state = {
app: null,
settings: {},
strategy: PASSWORD_STRATEGY,
initStrategy: PASSWORD_STRATEGY // default strategy from config
@ -16,14 +15,10 @@ const state = {
const resetState = (state) => {
state.strategy = state.initStrategy
state.settings = {}
state.app = null
}
// getters
const getters = {
app: (state, getters) => {
return state.app
},
settings: (state, getters) => {
return state.settings
},
@ -55,9 +50,8 @@ const mutations = {
requireToken (state) {
state.strategy = TOKEN_STRATEGY
},
requireMFA (state, { app, settings }) {
requireMFA (state, { settings }) {
state.settings = settings
state.app = app
state.strategy = TOTP_STRATEGY // default strategy of MFA
},
requireRecovery (state) {

View file

@ -28,13 +28,15 @@ export const defaultState = {
follows: true,
mentions: true,
likes: true,
repeats: true
repeats: true,
moves: true
},
webPushNotifications: false,
muteWords: [],
highlight: {},
interfaceLanguage: browserLocale,
hideScopeNotice: false,
useStreamingApi: false,
scopeCopy: undefined, // instance default
subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined, // instance default
@ -45,6 +47,7 @@ export const defaultState = {
playVideosInModal: false,
useOneClickNsfw: false,
useContainFit: false,
greentext: undefined, // instance default
hidePostStats: undefined, // instance default
hideUserStats: undefined // instance default
}

View file

@ -27,11 +27,13 @@ const defaultState = {
scopeCopy: true,
subjectLineBehavior: 'email',
postContentType: 'text/plain',
hideSitename: false,
nsfwCensorImage: undefined,
vapidPublicKey: undefined,
noAttachmentLinks: false,
showFeaturesPanel: true,
minimalScopesMode: false,
greentext: false,
// Nasty stuff
pleromaBackend: true,

View file

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

View file

@ -40,7 +40,7 @@ const polls = {
commit('mergeOrAddPoll', poll)
},
updateTrackedPoll ({ rootState, dispatch, commit }, pollId) {
rootState.api.backendInteractor.fetchPoll(pollId).then(poll => {
rootState.api.backendInteractor.fetchPoll({ pollId }).then(poll => {
setTimeout(() => {
if (rootState.polls.trackedPolls[pollId]) {
dispatch('updateTrackedPoll', pollId)
@ -59,7 +59,7 @@ const polls = {
commit('untrackPoll', pollId)
},
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)
return poll
})

View file

@ -54,6 +54,7 @@ export const defaultState = () => ({
notifications: emptyNotifications(),
favorites: new Set(),
error: false,
errorData: null,
timelines: {
mentions: emptyTl(),
public: emptyTl(),
@ -82,7 +83,8 @@ const visibleNotificationTypes = (rootState) => {
rootState.config.notificationVisibility.likes && 'like',
rootState.config.notificationVisibility.mentions && 'mention',
rootState.config.notificationVisibility.repeats && 'repeat',
rootState.config.notificationVisibility.follows && 'follow'
rootState.config.notificationVisibility.follows && 'follow',
rootState.config.notificationVisibility.moves && 'move'
].filter(_ => _)
}
@ -321,7 +323,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
each(notifications, (notification) => {
if (notification.type !== 'follow') {
if (notification.type !== 'follow' && notification.type !== 'move') {
notification.action = addStatusToGlobalStorage(state, notification.action).item
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
}
@ -354,6 +356,9 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
case 'follow':
i18nString = 'followed_you'
break
case 'move':
i18nString = 'migrated_to'
break
}
if (i18nString) {
@ -495,6 +500,9 @@ export const mutations = {
setError (state, { value }) {
state.error = value
},
setErrorData (state, { value }) {
state.errorData = value
},
setNotificationsLoading (state, { value }) {
state.notifications.loading = value
},
@ -570,6 +578,9 @@ const statuses = {
setError ({ rootState, commit }, { value }) {
commit('setError', { value })
},
setErrorData ({ rootState, commit }, { value }) {
commit('setErrorData', { value })
},
setNotificationsLoading ({ rootState, commit }, { value }) {
commit('setNotificationsLoading', { value })
},
@ -593,45 +604,45 @@ const statuses = {
favorite ({ rootState, commit }, status) {
// Optimistic favoriting...
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 }))
},
unfavorite ({ rootState, commit }, status) {
// Optimistic unfavoriting...
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 }))
},
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 }))
},
pinStatus ({ rootState, dispatch }, statusId) {
return rootState.api.backendInteractor.pinOwnStatus(statusId)
return rootState.api.backendInteractor.pinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
unpinStatus ({ rootState, dispatch }, statusId) {
rootState.api.backendInteractor.unpinOwnStatus(statusId)
rootState.api.backendInteractor.unpinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
muteConversation ({ rootState, commit }, statusId) {
return rootState.api.backendInteractor.muteConversation(statusId)
return rootState.api.backendInteractor.muteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status))
},
unmuteConversation ({ rootState, commit }, statusId) {
return rootState.api.backendInteractor.unmuteConversation(statusId)
return rootState.api.backendInteractor.unmuteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status))
},
retweet ({ rootState, commit }, status) {
// Optimistic retweeting...
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 }))
},
unretweet ({ rootState, commit }, status) {
// Optimistic unretweeting...
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 }))
},
queueFlush ({ rootState, commit }, { timeline, id }) {
@ -646,8 +657,8 @@ const statuses = {
},
fetchFavsAndRepeats ({ rootState, commit }, id) {
Promise.all([
rootState.api.backendInteractor.fetchFavoritedByUsers(id),
rootState.api.backendInteractor.fetchRebloggedByUsers(id)
rootState.api.backendInteractor.fetchFavoritedByUsers({ id }),
rootState.api.backendInteractor.fetchRebloggedByUsers({ id })
]).then(([favoritedByUsers, rebloggedByUsers]) => {
commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser })
commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser })
@ -656,7 +667,7 @@ const statuses = {
reactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
const currentUser = rootState.users.currentUser
commit('addOwnReaction', { id, emoji, currentUser })
rootState.api.backendInteractor.reactWithEmoji(id, emoji).then(
rootState.api.backendInteractor.reactWithEmoji({ id, emoji }).then(
status => {
dispatch('fetchEmojiReactions', id)
}
@ -665,25 +676,25 @@ const statuses = {
unreactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
const currentUser = rootState.users.currentUser
commit('removeOwnReaction', { id, emoji, currentUser })
rootState.api.backendInteractor.unreactWithEmoji(id, emoji).then(
rootState.api.backendInteractor.unreactWithEmoji({ id, emoji }).then(
status => {
dispatch('fetchEmojiReactions', id)
}
)
},
fetchEmojiReactions ({ rootState, commit }, id) {
rootState.api.backendInteractor.fetchEmojiReactions(id).then(
rootState.api.backendInteractor.fetchEmojiReactions({ id }).then(
emojiReactions => {
commit('addEmojiReactions', { id, emojiReactions, currentUser: rootState.users.currentUser })
}
)
},
fetchFavs ({ rootState, commit }, id) {
rootState.api.backendInteractor.fetchFavoritedByUsers(id)
rootState.api.backendInteractor.fetchFavoritedByUsers({ id })
.then(favoritedByUsers => commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser }))
},
fetchRepeats ({ rootState, commit }, id) {
rootState.api.backendInteractor.fetchRebloggedByUsers(id)
rootState.api.backendInteractor.fetchRebloggedByUsers({ id })
.then(rebloggedByUsers => commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser }))
},
search (store, { q, resolve, limit, offset, following }) {

View file

@ -32,7 +32,7 @@ const getNotificationPermission = () => {
}
const blockUser = (store, id) => {
return store.rootState.api.backendInteractor.blockUser(id)
return store.rootState.api.backendInteractor.blockUser({ id })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
store.commit('addBlockId', id)
@ -43,12 +43,12 @@ const blockUser = (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]))
}
const muteUser = (store, id) => {
return store.rootState.api.backendInteractor.muteUser(id)
return store.rootState.api.backendInteractor.muteUser({ id })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
store.commit('addMuteId', id)
@ -56,7 +56,7 @@ const muteUser = (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]))
}
@ -95,9 +95,9 @@ export const mutations = {
newRights[right] = value
set(user, 'rights', newRights)
},
updateActivationStatus (state, { user: { id }, status }) {
updateActivationStatus (state, { user: { id }, deactivated }) {
const user = state.usersObject[id]
set(user, 'deactivated', !status)
set(user, 'deactivated', deactivated)
},
setCurrentUser (state, user) {
state.lastLoginName = user.screen_name
@ -324,13 +324,18 @@ const users = {
commit('clearFollowers', userId)
},
subscribeUser ({ rootState, commit }, id) {
return rootState.api.backendInteractor.subscribeUser(id)
return rootState.api.backendInteractor.subscribeUser({ id })
.then((relationship) => commit('updateUserRelationship', [relationship]))
},
unsubscribeUser ({ rootState, commit }, id) {
return rootState.api.backendInteractor.unsubscribeUser(id)
return rootState.api.backendInteractor.unsubscribeUser({ id })
.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) {
const token = store.state.currentUser.credentials
const vapidPublicKey = store.rootState.instance.vapidPublicKey
@ -368,8 +373,10 @@ const users = {
},
addNewNotifications (store, { notifications }) {
const users = map(notifications, 'from_profile')
const targetUsers = map(notifications, 'target')
const notificationIds = notifications.map(_ => _.id)
store.commit('addNewUsers', users)
store.commit('addNewUsers', targetUsers)
const notificationsObject = store.rootState.statuses.notifications.idStore
const relevantNotifications = Object.entries(notificationsObject)
@ -382,7 +389,7 @@ const users = {
})
},
searchUsers (store, query) {
return store.rootState.api.backendInteractor.searchUsers(query)
return store.rootState.api.backendInteractor.searchUsers({ query })
.then((users) => {
store.commit('addNewUsers', users)
return users
@ -394,7 +401,7 @@ const users = {
let rootState = store.rootState
try {
let data = await rootState.api.backendInteractor.register(userInfo)
let data = await rootState.api.backendInteractor.register({ ...userInfo })
store.commit('signUpSuccess')
store.commit('setToken', data.access_token)
store.dispatch('loginUser', data.access_token)
@ -431,9 +438,10 @@ const users = {
store.commit('clearCurrentUser')
store.dispatch('disconnectFromSocket')
store.commit('clearToken')
store.dispatch('stopFetching', 'friends')
store.dispatch('stopFetchingTimeline', 'friends')
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
store.dispatch('stopFetching', 'notifications')
store.dispatch('stopFetchingNotifications')
store.dispatch('stopFetchingFollowRequests')
store.commit('clearNotifications')
store.commit('resetStatuses')
})
@ -468,11 +476,24 @@ const users = {
store.dispatch('initializeSocket')
}
// Start getting fresh posts.
store.dispatch('startFetchingTimeline', { timeline: 'friends' })
const startPolling = () => {
// Start getting fresh posts.
store.dispatch('startFetchingTimeline', { timeline: 'friends' })
// Start fetching notifications
store.dispatch('startFetchingNotifications')
// Start fetching notifications
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
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 'whatwg-fetch'
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 TAG_USER_URL = '/api/pleroma/admin/users/tag'
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 SUGGESTIONS_URL = '/api/v1/suggestions'
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_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_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_SEARCH_2 = `/api/v2/search`
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
const MASTODON_STREAMING = '/api/v1/streaming'
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/emoji_reactions_by`
const PLEROMA_EMOJI_REACT_URL = id => `/api/v1/pleroma/statuses/${id}/react_with_emoji`
const PLEROMA_EMOJI_UNREACT_URL = id => `/api/v1/pleroma/statuses/${id}/unreact_with_emoji`
@ -453,20 +455,26 @@ const deleteRight = ({ right, credentials, ...user }) => {
})
}
const setActivationStatus = ({ status, credentials, ...user }) => {
const screenName = user.screen_name
const body = {
status: status
}
const activateUser = ({ credentials, user: { screen_name: nickname } }) => {
return promisedRequest({
url: ACTIVATE_USER_URL,
method: 'PATCH',
credentials,
payload: {
nicknames: [nickname]
}
}).then(response => get(response, 'users.0'))
}
const headers = authHeaders(credentials)
headers['Content-Type'] = 'application/json'
return fetch(ACTIVATION_STATUS_URL(screenName), {
method: 'PUT',
headers: headers,
body: JSON.stringify(body)
})
const deactivateUser = ({ credentials, user: { screen_name: nickname } }) => {
return promisedRequest({
url: DEACTIVATE_USER_URL,
method: 'PATCH',
credentials,
payload: {
nicknames: [nickname]
}
}).then(response => get(response, 'users.0'))
}
const deleteUser = ({ credentials, ...user }) => {
@ -532,16 +540,24 @@ const fetchTimeline = ({
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
url += `?${queryString}`
let status = ''
let statusText = ''
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => {
if (data.ok) {
return data
}
throw new Error('Error fetching timeline', data)
status = data.status
statusText = data.statusText
return data
})
.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 }) => {
@ -957,6 +973,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 = {
verifyCredentials,
fetchTimeline,
@ -996,7 +1105,8 @@ const apiService = {
deleteUser,
addRight,
deleteRight,
setActivationStatus,
activateUser,
deactivateUser,
register,
getCaptcha,
updateAvatar,

View file

@ -1,232 +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 notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
const backendInteractorService = credentials => {
const fetchStatus = ({ id }) => {
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 }) => {
const backendInteractorService = credentials => ({
startFetchingTimeline ({ timeline, store, userId = false, tag }) {
return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag })
}
},
const startFetchingNotifications = ({ store }) => {
startFetchingNotifications ({ store }) {
return notificationsFetcher.startFetching({ store, credentials })
}
},
// eslint-disable-next-line camelcase
const tagUser = ({ screen_name }, tag) => {
return apiService.tagUser({ screen_name, tag, credentials })
}
fetchAndUpdateNotifications ({ store }) {
return notificationsFetcher.fetchAndUpdate({ store, credentials })
},
// eslint-disable-next-line camelcase
const untagUser = ({ screen_name }, tag) => {
return apiService.untagUser({ screen_name, tag, credentials })
}
startFetchingFollowRequest ({ store }) {
return followRequestFetcher.startFetching({ store, credentials })
},
// eslint-disable-next-line camelcase
const addRight = ({ screen_name }, right) => {
return apiService.addRight({ screen_name, right, credentials })
}
startUserSocket ({ store }) {
const serv = store.rootState.instance.server.replace('http', 'ws')
const url = serv + getMastodonSocketURI({ credentials, stream: 'user' })
return ProcessedWS({ url, id: 'User' })
},
// eslint-disable-next-line camelcase
const deleteRight = ({ screen_name }, right) => {
return apiService.deleteRight({ screen_name, right, credentials })
}
...Object.entries(apiService).reduce((acc, [key, func]) => {
return {
...acc,
[key]: (args) => func({ credentials, ...args })
}
}, {}),
// 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 fetchFollowRequests = () => apiService.fetchFollowRequests({ 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 fetchEmojiReactions = (id) => apiService.fetchEmojiReactions({ id })
const reactWithEmoji = (id, emoji) => apiService.reactWithEmoji({ id, emoji, credentials })
const unreactWithEmoji = (id, emoji) => apiService.unreactWithEmoji({ id, emoji, credentials })
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,
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,
fetchFollowRequests,
approveUser,
denyUser,
vote,
fetchPoll,
fetchFavoritedByUsers,
fetchRebloggedByUsers,
fetchEmojiReactions,
reactWithEmoji,
unreactWithEmoji,
reportUser,
favorite,
unfavorite,
retweet,
unretweet,
updateNotificationSettings,
search2,
searchUsers
}
return backendInteractorServiceInstance
}
verifyCredentials: apiService.verifyCredentials
})
export default backendInteractorService

View file

@ -46,6 +46,14 @@ export const parseUser = (data) => {
output.description = data.note
output.description_html = addEmojis(data.note, data.emojis)
output.fields = data.fields
output.fields_html = data.fields.map(field => {
return {
name: addEmojis(field.name, data.emojis),
value: addEmojis(field.value, data.emojis)
}
})
// Utilize avatar_static for gif avatars?
output.profile_image_url = data.avatar
output.profile_image_url_original = data.avatar
@ -95,6 +103,7 @@ export const parseUser = (data) => {
if (data.source) {
output.description = data.source.note
output.default_scope = data.source.privacy
output.fields = data.source.fields
if (data.source.pleroma) {
output.no_rich_text = data.source.pleroma.no_rich_text
output.show_role = data.source.pleroma.show_role
@ -332,10 +341,13 @@ export const parseNotification = (data) => {
if (masto) {
output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen
output.status = output.type === 'follow'
output.status = output.type === 'follow' || output.type === 'move'
? null
: parseStatus(data.status)
output.action = output.status // TODO: Refactor, this is unneeded
output.target = output.type !== 'move'
? null
: parseUser(data.target)
output.from_profile = parseUser(data.account)
} else {
const parsedNotice = parseStatus(data.notice)

View file

@ -39,7 +39,7 @@ export const requestFollow = (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) => {
store.commit('updateUserRelationship', [updated])
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 form = new window.FormData()
form.append('client_id', app.client_id)
form.append('client_secret', app.client_secret)
form.append('client_id', clientId)
form.append('client_secret', clientSecret)
form.append('mfa_token', mfaToken)
form.append('code', code)
form.append('challenge_type', 'totp')
@ -14,12 +14,12 @@ const verifyOTPCode = ({ app, instance, mfaToken, code }) => {
}).then((data) => data.json())
}
const verifyRecoveryCode = ({ app, instance, mfaToken, code }) => {
const verifyRecoveryCode = ({ clientId, clientSecret, instance, mfaToken, code }) => {
const url = `${instance}/oauth/mfa/challenge`
const form = new window.FormData()
form.append('client_id', app.client_id)
form.append('client_secret', app.client_secret)
form.append('client_id', clientId)
form.append('client_secret', clientSecret)
form.append('mfa_token', mfaToken)
form.append('code', code)
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('redirect_uris', REDIRECT_URI)
form.append('scopes', 'read write follow')
form.append('scopes', 'read write follow push admin')
return window.fetch(url, {
method: 'POST',
@ -28,7 +28,7 @@ const login = ({ instance, clientId }) => {
response_type: 'code',
client_id: clientId,
redirect_uri: REDIRECT_URI,
scope: 'read write follow'
scope: 'read write follow push admin'
}
const dataString = reduce(data, (acc, v, k) => {

View file

@ -6,7 +6,8 @@ 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'
store.state.config.notificationVisibility.follows && 'follow',
store.state.config.notificationVisibility.moves && 'move'
].filter(_ => _))
const sortById = (a, b) => {

View file

@ -65,7 +65,8 @@ function sendSubscriptionToBackEnd (subscription, token, notificationVisibility)
follow: notificationVisibility.follows,
favourite: notificationVisibility.likes,
mention: notificationVisibility.mentions,
reblog: notificationVisibility.repeats
reblog: notificationVisibility.repeats,
move: notificationVisibility.moves
}
}
})

View file

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

View file

@ -0,0 +1,94 @@
/**
* This is a tiny purpose-built HTML parser/processor. This basically detects any type of visual newline and
* allows it to be processed, useful for greentexting, mostly
*
* known issue: doesn't handle CDATA so nested CDATA might not work well
*
* @param {Object} input - input data
* @param {(string) => string} processor - function that will be called on every line
* @return {string} processed html
*/
export const processHtml = (html, processor) => {
const handledTags = new Set(['p', 'br', 'div'])
const openCloseTags = new Set(['p', 'div'])
let buffer = '' // Current output buffer
const level = [] // How deep we are in tags and which tags were there
let textBuffer = '' // Current line content
let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
// Extracts tag name from tag, i.e. <span a="b"> => span
const getTagName = (tag) => {
const result = /(?:<\/(\w+)>|<(\w+)\s?[^/]*?\/?>)/gi.exec(tag)
return result && (result[1] || result[2])
}
const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
if (textBuffer.trim().length > 0) {
buffer += processor(textBuffer)
} else {
buffer += textBuffer
}
textBuffer = ''
}
const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
flush()
buffer += tag
}
const handleOpen = (tag) => { // handles opening tags
flush()
buffer += tag
level.push(tag)
}
const handleClose = (tag) => { // handles closing tags
flush()
buffer += tag
if (level[level.length - 1] === tag) {
level.pop()
}
}
for (let i = 0; i < html.length; i++) {
const char = html[i]
if (char === '<' && tagBuffer === null) {
tagBuffer = char
} else if (char !== '>' && tagBuffer !== null) {
tagBuffer += char
} else if (char === '>' && tagBuffer !== null) {
tagBuffer += char
const tagFull = tagBuffer
tagBuffer = null
const tagName = getTagName(tagFull)
if (handledTags.has(tagName)) {
if (tagName === 'br') {
handleBr(tagFull)
} else if (openCloseTags.has(tagName)) {
if (tagFull[1] === '/') {
handleClose(tagFull)
} else if (tagFull[tagFull.length - 2] === '/') {
// self-closing
handleBr(tagFull)
} else {
handleOpen(tagFull)
}
}
} else {
textBuffer += tagFull
}
} else if (char === '\n') {
handleBr(char)
} else {
textBuffer += char
}
}
if (tagBuffer) {
textBuffer += tagBuffer
}
flush()
return buffer
}