diff options
Diffstat (limited to 'src')
48 files changed, 606 insertions, 425 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 () { diff --git a/src/App.scss b/src/App.scss index 925913f2..754ca62e 100644 --- a/src/App.scss +++ b/src/App.scss @@ -870,3 +870,16 @@ nav { 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); +} diff --git a/src/App.vue b/src/App.vue index dbe842ec..1b1c2648 100644 --- a/src/App.vue +++ b/src/App.vue @@ -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 diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 226b67d8..228a0497 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -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 }) diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js index 1a00a1c1..a8931787 100644 --- a/src/components/follow_request_card/follow_request_card.js +++ b/src/components/follow_request_card/follow_request_card.js @@ -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) } } diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js index 0b574a04..0d8f1da6 100644 --- a/src/components/login_form/login_form.js +++ b/src/components/login_form/login_form.js @@ -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 { diff --git a/src/components/mfa_form/recovery_form.js b/src/components/mfa_form/recovery_form.js index 7a3cc22d..b25c65dd 100644 --- a/src/components/mfa_form/recovery_form.js +++ b/src/components/mfa_form/recovery_form.js @@ -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 diff --git a/src/components/mfa_form/totp_form.js b/src/components/mfa_form/totp_form.js index 778bf8dc..b774f2d0 100644 --- a/src/components/mfa_form/totp_form.js +++ b/src/components/mfa_form/totp_form.js @@ -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 diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js index 5a90c31f..c1166a0c 100644 --- a/src/components/mobile_nav/mobile_nav.js +++ b/src/components/mobile_nav/mobile_nav.js @@ -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: { diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue index d1c24e56..51f1d636 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -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" diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js index 8aadc8c5..757166ed 100644 --- a/src/components/moderation_tools/moderation_tools.js +++ b/src/components/moderation_tools/moderation_tools.js @@ -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' diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index 7f783acb..d9268585 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -1,20 +1,18 @@ +import { mapState } from 'vuex' + const NavPanel = { created () { if (this.currentUser && this.currentUser.locked) { 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 diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 28589bb1..034259d9 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -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> diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index 6c4054fd..a89c0cdc 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -47,6 +47,11 @@ const Notifications = { components: { Notification }, + created () { + const { dispatch } = this.$store + + dispatch('fetchAndUpdateNotifications') + }, watch: { unseenCount (count) { if (count > 0) { diff --git a/src/components/public_and_external_timeline/public_and_external_timeline.js b/src/components/public_and_external_timeline/public_and_external_timeline.js index f614c13b..cbd4491b 100644 --- a/src/components/public_and_external_timeline/public_and_external_timeline.js +++ b/src/components/public_and_external_timeline/public_and_external_timeline.js @@ -10,7 +10,7 @@ const PublicAndExternalTimeline = { this.$store.dispatch('startFetchingTimeline', { timeline: 'publicAndExternal' }) }, destroyed () { - this.$store.dispatch('stopFetching', 'publicAndExternal') + this.$store.dispatch('stopFetchingTimeline', 'publicAndExternal') } } diff --git a/src/components/public_timeline/public_timeline.js b/src/components/public_timeline/public_timeline.js index 8976a99c..66c40d3a 100644 --- a/src/components/public_timeline/public_timeline.js +++ b/src/components/public_timeline/public_timeline.js @@ -10,7 +10,7 @@ const PublicTimeline = { this.$store.dispatch('startFetchingTimeline', { timeline: 'public' }) }, destroyed () { - this.$store.dispatch('stopFetching', 'public') + this.$store.dispatch('stopFetchingTimeline', 'public') } } diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue index 5bb06a4f..222b67a8 100644 --- a/src/components/registration/registration.vue +++ b/src/components/registration/registration.vue @@ -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" diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index c49083f9..31a9e9be 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -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 diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index 31329e82..cef492f3 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -74,6 +74,15 @@ </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') }} </Checkbox> diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index 0188cf3e..2534eb8f 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -33,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: { diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 214b8e0c..3fba9058 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -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 { diff --git a/src/components/sticker_picker/sticker_picker.vue b/src/components/sticker_picker/sticker_picker.vue index 323855b9..3863908a 100644 --- a/src/components/sticker_picker/sticker_picker.vue +++ b/src/components/sticker_picker/sticker_picker.vue @@ -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)); + } } } } diff --git a/src/components/tag_timeline/tag_timeline.js b/src/components/tag_timeline/tag_timeline.js index 458eb1c5..400c6a4b 100644 --- a/src/components/tag_timeline/tag_timeline.js +++ b/src/components/tag_timeline/tag_timeline.js @@ -19,7 +19,7 @@ const TagTimeline = { } }, destroyed () { - this.$store.dispatch('stopFetching', 'tag') + this.$store.dispatch('stopFetchingTimeline', 'tag') } } diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 27a9a55e..9a53acd6 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -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 }, diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 93f6f570..9777bd0c 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -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 > @@ -67,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" @@ -93,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> diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 00055707..9558a0bd 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -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() diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js index 833fa98a..38cf117b 100644 --- a/src/components/user_reporting_modal/user_reporting_modal.js +++ b/src/components/user_reporting_modal/user_reporting_modal.js @@ -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() diff --git a/src/components/user_settings/mfa.js b/src/components/user_settings/mfa.js index 3090138a..abf37062 100644 --- a/src/components/user_settings/mfa.js +++ b/src/components/user_settings/mfa.js @@ -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 diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index 3fdc5340..d5d671e4 100644 --- a/src/components/user_settings/user_settings.js +++ b/src/components/user_settings/user_settings.js @@ -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 diff --git a/src/hocs/with_load_more/with_load_more.js b/src/hocs/with_load_more/with_load_more.js index 1e1b2a74..6142f513 100644 --- a/src/hocs/with_load_more/with_load_more.js +++ b/src/hocs/with_load_more/with_load_more.js @@ -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}> diff --git a/src/hocs/with_subscription/with_subscription.js b/src/hocs/with_subscription/with_subscription.js index 91fc4cca..1775adcb 100644 --- a/src/hocs/with_subscription/with_subscription.js +++ b/src/hocs/with_subscription/with_subscription.js @@ -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}> diff --git a/src/i18n/en.json b/src/i18n/en.json index effc139f..75d66b9f 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -361,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.", diff --git a/src/i18n/ja_easy.json b/src/i18n/ja_easy.json index 592a7257..be447f1c 100644 --- a/src/i18n/ja_easy.json +++ b/src/i18n/ja_easy.json @@ -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": "あなたはパスワードのリセットがひつようです。しかし、まずいことに、このインスタンスでは、パスワードのリセットができなくなっています。このインスタンスのアドミニストレーターに、おといあわせください。" } } diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 19e10f1e..4cb2d497 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -219,6 +219,8 @@ "subject_input_always_show": "Всегда показывать поле ввода темы", "stop_gifs": "Проигрывать GIF анимации только при наведении", "streaming": "Включить автоматическую загрузку новых сообщений при прокрутке вверх", + "useStreamingApi": "Получать сообщения и уведомления в реальном времени", + "useStreamingApiWarning": "(Не рекомендуется, экспериментально, сообщения могут пропадать)", "text": "Текст", "theme": "Тема", "theme_help": "Используйте шестнадцатеричные коды цветов (#rrggbb) для настройки темы.", diff --git a/src/modules/api.js b/src/modules/api.js index 1293e3c8..9c296275 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -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,32 +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 }) }, - startFetchingFollowRequest (store) { - // Don't start fetching if we already are. - if (store.state.fetchers['followRequest']) return + stopFetchingNotifications (store) { + const fetcher = store.state.fetchers.notifications + if (!fetcher) return + store.commit('removeFetcher', { fetcherName: 'notifications', fetcher }) + }, + fetchAndUpdateNotifications (store) { + store.state.backendInteractor.fetchAndUpdateNotifications({ store }) + }, - const fetcher = store.state.backendInteractor.startFetchingFollowRequest({ store }) - store.commit('addFetcher', { fetcherName: 'followRequest', fetcher }) + // Follow requests + startFetchingFollowRequests (store) { + if (store.state.fetchers['followRequests']) return + const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store }) + store.commit('addFetcher', { fetcherName: 'followRequests', fetcher }) }, - stopFetching (store, fetcherName) { - const fetcher = store.state.fetchers[fetcherName] - window.clearInterval(fetcher) - store.commit('removeFetcher', { fetcherName }) + 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) }, @@ -72,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) } } } diff --git a/src/modules/auth_flow.js b/src/modules/auth_flow.js index d0a90feb..956d40e8 100644 --- a/src/modules/auth_flow.js +++ b/src/modules/auth_flow.js @@ -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) { diff --git a/src/modules/config.js b/src/modules/config.js index cc47c848..de9f041b 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -36,6 +36,7 @@ export const defaultState = { highlight: {}, interfaceLanguage: browserLocale, hideScopeNotice: false, + useStreamingApi: false, scopeCopy: undefined, // instance default subjectLineBehavior: undefined, // instance default alwaysShowSubjectInput: undefined, // instance default diff --git a/src/modules/instance.js b/src/modules/instance.js index 96f14ed5..625323b9 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -27,6 +27,7 @@ const defaultState = { scopeCopy: true, subjectLineBehavior: 'email', postContentType: 'text/plain', + hideSitename: false, nsfwCensorImage: undefined, vapidPublicKey: undefined, noAttachmentLinks: false, diff --git a/src/modules/oauth_tokens.js b/src/modules/oauth_tokens.js index 0159a3f1..907cae4a 100644 --- a/src/modules/oauth_tokens.js +++ b/src/modules/oauth_tokens.js @@ -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)) } diff --git a/src/modules/polls.js b/src/modules/polls.js index e6158b63..92b89a06 100644 --- a/src/modules/polls.js +++ b/src/modules/polls.js @@ -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 }) diff --git a/src/modules/statuses.js b/src/modules/statuses.js index f3fc0f21..16dae8ce 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -38,6 +38,7 @@ export const defaultState = () => ({ notifications: emptyNotifications(), favorites: new Set(), error: false, + errorData: null, timelines: { mentions: emptyTl(), public: emptyTl(), @@ -483,6 +484,9 @@ export const mutations = { setError (state, { value }) { state.error = value }, + setErrorData (state, { value }) { + state.errorData = value + }, setNotificationsLoading (state, { value }) { state.notifications.loading = value }, @@ -532,6 +536,9 @@ const statuses = { setError ({ rootState, commit }, { value }) { commit('setError', { value }) }, + setErrorData ({ rootState, commit }, { value }) { + commit('setErrorData', { value }) + }, setNotificationsLoading ({ rootState, commit }, { value }) { commit('setNotificationsLoading', { value }) }, @@ -555,45 +562,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 }) { @@ -608,19 +615,19 @@ 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 }) }) }, 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 }) { diff --git a/src/modules/users.js b/src/modules/users.js index f209b23b..84fa0255 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -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 @@ -384,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 @@ -396,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) @@ -433,10 +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('stopFetching', 'followRequest') + store.dispatch('stopFetchingNotifications') + store.dispatch('stopFetchingFollowRequests') store.commit('clearNotifications') store.commit('resetStatuses') }) @@ -471,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') diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 8f5eb416..ef0267aa 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -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 oldfetch = window.fetch @@ -450,20 +452,26 @@ const deleteRight = ({ right, credentials, ...user }) => { }) } -const setActivationStatus = ({ status, credentials, ...user }) => { - const screenName = user.screen_name - const body = { - status: status - } - - const headers = authHeaders(credentials) - headers['Content-Type'] = 'application/json' +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')) +} - 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 }) => { @@ -529,16 +537,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) { + status = data.status + statusText = data.statusText + return data + }) + .then((data) => data.json()) + .then((data) => { + if (!data.error) { + return data.map(isNotifications ? parseNotification : parseStatus) + } else { + data.status = status + data.statusText = statusText return data } - throw new Error('Error fetching timeline', data) }) - .then((data) => data.json()) - .then((data) => data.map(isNotifications ? parseNotification : parseStatus)) } const fetchPinnedStatuses = ({ id, credentials }) => { @@ -932,6 +948,99 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => { }) } +export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => { + return Object.entries({ + ...(credentials + ? { access_token: credentials } + : {} + ), + stream, + ...args + }).reduce((acc, [key, val]) => { + return acc + `${key}=${val}&` + }, MASTODON_STREAMING + '?') +} + +const MASTODON_STREAMING_EVENTS = new Set([ + 'update', + 'notification', + 'delete', + 'filters_changed' +]) + +// A thin wrapper around WebSocket API that allows adding a pre-processor to it +// Uses EventTarget and a CustomEvent to proxy events +export const ProcessedWS = ({ + url, + preprocessor = handleMastoWS, + id = 'Unknown' +}) => { + const eventTarget = new EventTarget() + const socket = new WebSocket(url) + if (!socket) throw new Error(`Failed to create socket ${id}`) + const proxy = (original, eventName, processor = a => a) => { + original.addEventListener(eventName, (eventData) => { + eventTarget.dispatchEvent(new CustomEvent( + eventName, + { detail: processor(eventData) } + )) + }) + } + socket.addEventListener('open', (wsEvent) => { + console.debug(`[WS][${id}] Socket connected`, wsEvent) + }) + socket.addEventListener('error', (wsEvent) => { + console.debug(`[WS][${id}] Socket errored`, wsEvent) + }) + socket.addEventListener('close', (wsEvent) => { + console.debug( + `[WS][${id}] Socket disconnected with code ${wsEvent.code}`, + wsEvent + ) + }) + // Commented code reason: very spammy, uncomment to enable message debug logging + /* + socket.addEventListener('message', (wsEvent) => { + console.debug( + `[WS][${id}] Message received`, + wsEvent + ) + }) + /**/ + + proxy(socket, 'open') + proxy(socket, 'close') + proxy(socket, 'message', preprocessor) + proxy(socket, 'error') + + // 1000 = Normal Closure + eventTarget.close = () => { socket.close(1000, 'Shutting down socket') } + + return eventTarget +} + +export const handleMastoWS = (wsEvent) => { + const { data } = wsEvent + if (!data) return + const parsedEvent = JSON.parse(data) + const { event, payload } = parsedEvent + if (MASTODON_STREAMING_EVENTS.has(event)) { + // MastoBE and PleromaBE both send payload for delete as a PLAIN string + if (event === 'delete') { + return { event, id: payload } + } + const data = payload ? JSON.parse(payload) : null + if (event === 'update') { + return { event, status: parseStatus(data) } + } else if (event === 'notification') { + return { event, notification: parseNotification(data) } + } + } else { + console.warn('Unknown event', wsEvent) + return null + } +} + const apiService = { verifyCredentials, fetchTimeline, @@ -971,7 +1080,8 @@ const apiService = { deleteUser, addRight, deleteRight, - setActivationStatus, + activateUser, + deactivateUser, register, getCaptcha, updateAvatar, diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index c16bd1f1..b7372ed0 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -1,230 +1,39 @@ -import apiService from '../api/api.service.js' +import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js' import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js' import 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 }) - } - - const startFetchingFollowRequest = ({ store }) => { - return followRequestFetcher.startFetching({ store, credentials }) - } - - // eslint-disable-next-line camelcase - const tagUser = ({ screen_name }, tag) => { - return apiService.tagUser({ screen_name, tag, credentials }) - } + }, - // eslint-disable-next-line camelcase - const untagUser = ({ screen_name }, tag) => { - return apiService.untagUser({ screen_name, tag, credentials }) - } + fetchAndUpdateNotifications ({ store }) { + return notificationsFetcher.fetchAndUpdate({ store, credentials }) + }, - // eslint-disable-next-line camelcase - const addRight = ({ screen_name }, right) => { - return apiService.addRight({ screen_name, right, credentials }) - } - - // eslint-disable-next-line camelcase - const deleteRight = ({ screen_name }, right) => { - return apiService.deleteRight({ screen_name, right, credentials }) - } - - // eslint-disable-next-line camelcase - const setActivationStatus = ({ screen_name }, status) => { - return apiService.setActivationStatus({ screen_name, status, credentials }) - } - - // eslint-disable-next-line camelcase - const deleteUser = ({ screen_name }) => { - return apiService.deleteUser({ screen_name, credentials }) - } - - const vote = (pollId, choices) => { - return apiService.vote({ credentials, pollId, choices }) - } - - const fetchPoll = (pollId) => { - return apiService.fetchPoll({ credentials, pollId }) - } - - const updateNotificationSettings = ({ settings }) => { - return apiService.updateNotificationSettings({ credentials, settings }) - } - - const fetchMutes = () => apiService.fetchMutes({ credentials }) - const muteUser = (id) => apiService.muteUser({ credentials, id }) - const unmuteUser = (id) => apiService.unmuteUser({ credentials, id }) - const subscribeUser = (id) => apiService.subscribeUser({ credentials, id }) - const unsubscribeUser = (id) => apiService.unsubscribeUser({ credentials, id }) - const fetchBlocks = () => apiService.fetchBlocks({ credentials }) - const fetchOAuthTokens = () => apiService.fetchOAuthTokens({ credentials }) - const revokeOAuthToken = (id) => apiService.revokeOAuthToken({ id, credentials }) - const fetchPinnedStatuses = (id) => apiService.fetchPinnedStatuses({ credentials, id }) - const pinOwnStatus = (id) => apiService.pinOwnStatus({ credentials, id }) - const unpinOwnStatus = (id) => apiService.unpinOwnStatus({ credentials, id }) - const muteConversation = (id) => apiService.muteConversation({ credentials, id }) - const unmuteConversation = (id) => apiService.unmuteConversation({ credentials, id }) - - const getCaptcha = () => apiService.getCaptcha() - const register = (params) => apiService.register({ credentials, params }) - const updateAvatar = ({ avatar }) => apiService.updateAvatar({ credentials, avatar }) - const updateBg = ({ background }) => apiService.updateBg({ credentials, background }) - const updateBanner = ({ banner }) => apiService.updateBanner({ credentials, banner }) - const updateProfile = ({ params }) => apiService.updateProfile({ credentials, params }) - - const importBlocks = (file) => apiService.importBlocks({ file, credentials }) - const importFollows = (file) => apiService.importFollows({ file, credentials }) - - const deleteAccount = ({ password }) => apiService.deleteAccount({ credentials, password }) - const changeEmail = ({ email, password }) => apiService.changeEmail({ credentials, email, password }) - const changePassword = ({ password, newPassword, newPasswordConfirmation }) => - apiService.changePassword({ credentials, password, newPassword, newPasswordConfirmation }) - - const fetchSettingsMFA = () => apiService.settingsMFA({ credentials }) - const generateMfaBackupCodes = () => apiService.generateMfaBackupCodes({ credentials }) - const mfaSetupOTP = () => apiService.mfaSetupOTP({ credentials }) - const mfaConfirmOTP = ({ password, token }) => apiService.mfaConfirmOTP({ credentials, password, token }) - const mfaDisableOTP = ({ password }) => apiService.mfaDisableOTP({ credentials, password }) - - const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({ id }) - const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({ id }) - const reportUser = (params) => apiService.reportUser({ credentials, ...params }) - - const favorite = (id) => apiService.favorite({ id, credentials }) - const unfavorite = (id) => apiService.unfavorite({ id, credentials }) - const retweet = (id) => apiService.retweet({ id, credentials }) - const unretweet = (id) => apiService.unretweet({ id, credentials }) - const search2 = ({ q, resolve, limit, offset, following }) => - apiService.search2({ credentials, q, resolve, limit, offset, following }) - const searchUsers = (query) => apiService.searchUsers({ query, credentials }) - - const backendInteractorServiceInstance = { - fetchStatus, - fetchConversation, - fetchFriends, - exportFriends, - fetchFollowers, - followUser, - unfollowUser, - blockUser, - unblockUser, - fetchUser, - fetchUserRelationship, - verifyCredentials: apiService.verifyCredentials, - startFetchingTimeline, - startFetchingNotifications, - startFetchingFollowRequest, - fetchMutes, - muteUser, - unmuteUser, - subscribeUser, - unsubscribeUser, - fetchBlocks, - fetchOAuthTokens, - revokeOAuthToken, - fetchPinnedStatuses, - pinOwnStatus, - unpinOwnStatus, - muteConversation, - unmuteConversation, - tagUser, - untagUser, - addRight, - deleteRight, - deleteUser, - setActivationStatus, - register, - getCaptcha, - updateAvatar, - updateBg, - updateBanner, - updateProfile, - importBlocks, - importFollows, - deleteAccount, - changeEmail, - changePassword, - fetchSettingsMFA, - generateMfaBackupCodes, - mfaSetupOTP, - mfaConfirmOTP, - mfaDisableOTP, - approveUser, - denyUser, - vote, - fetchPoll, - fetchFavoritedByUsers, - fetchRebloggedByUsers, - reportUser, - favorite, - unfavorite, - retweet, - unretweet, - updateNotificationSettings, - search2, - searchUsers - } - - return backendInteractorServiceInstance -} + startFetchingFollowRequest ({ store }) { + return followRequestFetcher.startFetching({ store, credentials }) + }, + + startUserSocket ({ store }) { + const serv = store.rootState.instance.server.replace('http', 'ws') + const url = serv + getMastodonSocketURI({ credentials, stream: 'user' }) + return ProcessedWS({ url, id: 'User' }) + }, + + ...Object.entries(apiService).reduce((acc, [key, func]) => { + return { + ...acc, + [key]: (args) => func({ credentials, ...args }) + } + }, {}), + + verifyCredentials: apiService.verifyCredentials +}) export default backendInteractorService diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js index 598cb5f7..29b38a0f 100644 --- a/src/services/follow_manipulate/follow_manipulate.js +++ b/src/services/follow_manipulate/follow_manipulate.js @@ -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({ diff --git a/src/services/new_api/mfa.js b/src/services/new_api/mfa.js index cbba06d5..c944667c 100644 --- a/src/services/new_api/mfa.js +++ b/src/services/new_api/mfa.js @@ -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') diff --git a/src/services/new_api/oauth.js b/src/services/new_api/oauth.js index d0d18c03..3c8e64bd 100644 --- a/src/services/new_api/oauth.js +++ b/src/services/new_api/oauth.js @@ -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) => { diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 9eb30c2d..c6b28ad5 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -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 }) } |
