diff options
| author | Shpuld Shpuldson <shpuld@shpposter.club> | 2020-01-13 23:47:32 +0200 |
|---|---|---|
| committer | Shpuld Shpuldson <shpuld@shpposter.club> | 2020-01-13 23:47:32 +0200 |
| commit | b32888194c2b9de286bcfff9998dae009cea224d (patch) | |
| tree | eb907b6457bc6416c9a8661039848aebf8f1727a /src | |
| parent | 33abbed5a1e1d1cf99d21d481b2a22481d7533b2 (diff) | |
| parent | 7257189ea796d51117285814d32ed6138fdb3458 (diff) | |
fix merge conflicts
Diffstat (limited to 'src')
73 files changed, 915 insertions, 502 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 310962b8..754ca62e 100644 --- a/src/App.scss +++ b/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); +} 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/account_actions/account_actions.js b/src/components/account_actions/account_actions.js index 204d506a..d2153680 100644 --- a/src/components/account_actions/account_actions.js +++ b/src/components/account_actions/account_actions.js @@ -25,9 +25,6 @@ const AccountActions = { }, reportUser () { this.$store.dispatch('openUserReportingModal', this.user.id) - }, - mentionUser () { - this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user }) } } } diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index 046cba93..d3235be1 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -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" diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 715804ff..7ff0ac08 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -43,7 +43,8 @@ const conversation = { 'collapsable', 'isPage', 'pinnedStatusIdsObject', - 'inProfile' + 'inProfile', + 'profileUserId' ], created () { if (this.isPage) { diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 0f1de55f..2e48240a 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -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" 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/interactions/interactions.js b/src/components/interactions/interactions.js index 1f8a9de9..cc31ff20 100644 --- a/src/components/interactions/interactions.js +++ b/src/components/interactions/interactions.js @@ -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 = { diff --git a/src/components/interactions/interactions.vue b/src/components/interactions/interactions.vue index 08cee343..a2e252ab 100644 --- a/src/components/interactions/interactions.vue +++ b/src/components/interactions/interactions.vue @@ -21,6 +21,10 @@ key="follows" :label="$t('interactions.follows')" /> + <span + key="moves" + :label="$t('interactions.moves')" + /> </tab-switcher> <Notifications ref="notifications" diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue index 1ca22001..f5ace0cc 100644 --- a/src/components/interface_language_switcher/interface_language_switcher.vue +++ b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -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) 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/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 49e3143e..80d2a8b9 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -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" 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/mrf_transparency_panel/mrf_transparency_panel.js b/src/components/mrf_transparency_panel/mrf_transparency_panel.js index 20f8a08a..6a1baec8 100644 --- a/src/components/mrf_transparency_panel/mrf_transparency_panel.js +++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.js @@ -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 diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue index 2640d68c..d6495dc6 100644 --- a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue +++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue @@ -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> diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index aa3f7605..d9268585 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -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 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/notification/notification.js b/src/components/notification/notification.js index 7d46eb5a..e7bd769e 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -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 } diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 1f192c77..16124e50 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -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" 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/notifications/notifications.scss b/src/components/notifications/notifications.scss index 71876b14..148ac7f2 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -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; diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index af6299e4..74067fef 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -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 } 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 a83489d2..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> @@ -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> diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index 567d2e5e..2534eb8f 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -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: { 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/status/status.js b/src/components/status/status.js index cc0c9e06..18617938 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -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, diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 503de98d..4ea1b74b 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -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 { 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 f1d3903a..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 > @@ -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> diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index a9278200..2f649910 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -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 }) } } } diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 96acf610..93d55fff 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -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" 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/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index 8c18cf49..3f1982a6 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -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') }} 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 febbf2ea..c840fe6d 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -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", diff --git a/src/i18n/ja.json b/src/i18n/ja_easy.json index 592a7257..be447f1c 100644 --- a/src/i18n/ja.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/messages.js b/src/i18n/messages.js index 89c8a8c8..c56ae205 100644 --- a/src/i18n/messages.js +++ b/src/i18n/messages.js @@ -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'), diff --git a/src/i18n/ru.json b/src/i18n/ru.json index f8bcd996..4cb2d497 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.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) для настройки темы.", diff --git a/src/i18n/zh.json b/src/i18n/zh.json index 80c4e0d8..8d9462ab 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -111,7 +111,7 @@ }, "interactions": { "favs_repeats": "转发和收藏", - "follows": "新的关注着", + "follows": "新的关注者", "load_older": "加载更早的互动" }, "post_status": { diff --git a/src/modules/api.js b/src/modules/api.js index eb6a7980..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,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) } } } 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 d4819ee8..de9f041b 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -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 } diff --git a/src/modules/instance.js b/src/modules/instance.js index 7b0e0da4..625323b9 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -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, 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 fcb6d1f3..ae6f6853 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -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 }) { diff --git a/src/modules/users.js b/src/modules/users.js index 1c9ff5e8..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 @@ -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') diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 2e96264a..783c2696 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 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 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 }) => { @@ -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) { + 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 }) => { @@ -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, diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 44233a24..b7372ed0 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -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 }) - } - - // eslint-disable-next-line camelcase - const untagUser = ({ screen_name }, tag) => { - return apiService.untagUser({ screen_name, tag, 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 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 -} + }, + + fetchAndUpdateNotifications ({ store }) { + return notificationsFetcher.fetchAndUpdate({ store, credentials }) + }, + + 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/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 5f45660d..ee007bee 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -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) 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/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index 7021adbd..b08514da 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -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) => { diff --git a/src/services/push/push.js b/src/services/push/push.js index 1b189a29..5836fc26 100644 --- a/src/services/push/push.js +++ b/src/services/push/push.js @@ -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 } } }) 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 }) } diff --git a/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js b/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js new file mode 100644 index 00000000..de6f20ef --- /dev/null +++ b/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js @@ -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 +} |
