fix merge conflicts
This commit is contained in:
commit
b32888194c
101 changed files with 2167 additions and 2137 deletions
|
@ -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 () {
|
||||
|
|
28
src/App.scss
28
src/App.scss
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -25,9 +25,6 @@ const AccountActions = {
|
|||
},
|
||||
reportUser () {
|
||||
this.$store.dispatch('openUserReportingModal', this.user.id)
|
||||
},
|
||||
mentionUser () {
|
||||
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -43,7 +43,8 @@ const conversation = {
|
|||
'collapsable',
|
||||
'isPage',
|
||||
'pinnedStatusIdsObject',
|
||||
'inProfile'
|
||||
'inProfile',
|
||||
'profileUserId'
|
||||
],
|
||||
created () {
|
||||
if (this.isPage) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -21,6 +21,10 @@
|
|||
key="follows"
|
||||
:label="$t('interactions.follows')"
|
||||
/>
|
||||
<span
|
||||
key="moves"
|
||||
:label="$t('interactions.moves')"
|
||||
/>
|
||||
</tab-switcher>
|
||||
<Notifications
|
||||
ref="notifications"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -47,6 +47,11 @@ const Notifications = {
|
|||
components: {
|
||||
Notification
|
||||
},
|
||||
created () {
|
||||
const { dispatch } = this.$store
|
||||
|
||||
dispatch('fetchAndUpdateNotifications')
|
||||
},
|
||||
watch: {
|
||||
unseenCount (count) {
|
||||
if (count > 0) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ const PublicAndExternalTimeline = {
|
|||
this.$store.dispatch('startFetchingTimeline', { timeline: 'publicAndExternal' })
|
||||
},
|
||||
destroyed () {
|
||||
this.$store.dispatch('stopFetching', 'publicAndExternal')
|
||||
this.$store.dispatch('stopFetchingTimeline', 'publicAndExternal')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ const PublicTimeline = {
|
|||
this.$store.dispatch('startFetchingTimeline', { timeline: 'public' })
|
||||
},
|
||||
destroyed () {
|
||||
this.$store.dispatch('stopFetching', 'public')
|
||||
this.$store.dispatch('stopFetchingTimeline', 'public')
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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('>')) {
|
||||
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
|
||||
return processHtml(html, (string) => {
|
||||
if (string.includes('>') &&
|
||||
string
|
||||
.replace(/<[^>]+?>/gi, '') // remove all tags
|
||||
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
|
||||
.trim()
|
||||
.startsWith('>')) {
|
||||
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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ const TagTimeline = {
|
|||
}
|
||||
},
|
||||
destroyed () {
|
||||
this.$store.dispatch('stopFetching', 'tag')
|
||||
this.$store.dispatch('stopFetchingTimeline', 'tag')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 === "admin""
|
||||
:user="user"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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') }}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "あなたはパスワードのリセットがひつようです。しかし、まずいことに、このインスタンスでは、パスワードのリセットができなくなっています。このインスタンスのアドミニストレーターに、おといあわせください。"
|
||||
}
|
||||
}
|
|
@ -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'),
|
||||
|
|
|
@ -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) для настройки темы.",
|
||||
|
|
|
@ -111,7 +111,7 @@
|
|||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "转发和收藏",
|
||||
"follows": "新的关注着",
|
||||
"follows": "新的关注者",
|
||||
"load_older": "加载更早的互动"
|
||||
},
|
||||
"post_status": {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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 }) {
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue