Merge branch 'develop' into feat/emoji-reactions

This commit is contained in:
Shpuld Shpuldson 2020-01-26 15:59:59 +02:00
commit c4beac5f89
28 changed files with 365 additions and 82 deletions

View File

@ -5,12 +5,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Added ### Added
- Icons in nav panel
- Private mode support
- Support for 'Move' type notifications
- Pleroma AMOLED dark theme
- User level domain mutes, under User Settings -> Mutes
### Changed
- Captcha now resets on failed registrations
- Notifications column now cleans itself up to optimize performance when tab is left open for a long time
- 403 messaging
### Fixed
- Single notifications left unread when hitting read on another device/tab
- Registration fixed
- Deactivation of remote accounts from frontend
## [1.1.7 and earlier] - 2019-12-14
### Added
- Ability to hide/show repeats from user - Ability to hide/show repeats from user
- User profile button clutter organized into a menu - User profile button clutter organized into a menu
- Emoji picker - Emoji picker
- Started changelog anew - Started changelog anew
- Ability to change user's email - Ability to change user's email
- About page - About page
- Added remote user redirect
### Changed ### Changed
- changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes - changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes
### Fixed ### Fixed

View File

@ -43,6 +43,7 @@
"@babel/plugin-transform-runtime": "^7.7.6", "@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.6", "@babel/preset-env": "^7.7.6",
"@babel/register": "^7.7.4", "@babel/register": "^7.7.4",
"@ungap/event-target": "^0.1.0",
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0", "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
"@vue/babel-plugin-transform-vue-jsx": "^1.1.2", "@vue/babel-plugin-transform-vue-jsx": "^1.1.2",
"@vue/test-utils": "^1.0.0-beta.26", "@vue/test-utils": "^1.0.0-beta.26",
@ -56,6 +57,7 @@
"connect-history-api-fallback": "^1.1.0", "connect-history-api-fallback": "^1.1.0",
"cross-spawn": "^4.0.2", "cross-spawn": "^4.0.2",
"css-loader": "^0.28.0", "css-loader": "^0.28.0",
"custom-event-polyfill": "^1.0.7",
"eslint": "^5.16.0", "eslint": "^5.16.0",
"eslint-config-standard": "^12.0.0", "eslint-config-standard": "^12.0.0",
"eslint-friendly-formatter": "^2.0.5", "eslint-friendly-formatter": "^2.0.5",

View File

@ -185,12 +185,9 @@ const getAppSecret = async ({ store }) => {
}) })
} }
const resolveStaffAccounts = async ({ store, accounts }) => { const resolveStaffAccounts = ({ store, accounts }) => {
const backendInteractor = store.state.api.backendInteractor const nicknames = accounts.map(uri => uri.split('/').pop())
let nicknames = accounts.map(uri => uri.split('/').pop()) nicknames.map(nickname => store.dispatch('fetchUser', nickname))
.map(id => backendInteractor.fetchUser({ id }))
nicknames = await Promise.all(nicknames)
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames }) store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
} }
@ -236,7 +233,7 @@ const getNodeInfo = async ({ store }) => {
}) })
const accounts = metadata.staffAccounts const accounts = metadata.staffAccounts
await resolveStaffAccounts({ store, accounts }) resolveStaffAccounts({ store, accounts })
} else { } else {
throw (res) throw (res)
} }

View File

@ -0,0 +1,15 @@
import ProgressButton from '../progress_button/progress_button.vue'
const DomainMuteCard = {
props: ['domain'],
components: {
ProgressButton
},
methods: {
unmuteDomain () {
return this.$store.dispatch('unmuteDomain', this.domain)
}
}
}
export default DomainMuteCard

View File

@ -0,0 +1,38 @@
<template>
<div class="domain-mute-card">
<div class="domain-mute-card-domain">
{{ domain }}
</div>
<ProgressButton
:click="unmuteDomain"
class="btn btn-default"
>
{{ $t('domain_mute_card.unmute') }}
<template slot="progress">
{{ $t('domain_mute_card.unmute_progress') }}
</template>
</ProgressButton>
</div>
</template>
<script src="./domain_mute_card.js"></script>
<style lang="scss">
.domain-mute-card {
flex: 1 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.6em 1em 0.6em 0;
&-domain {
margin-right: 1em;
overflow: hidden;
text-overflow: ellipsis;
}
button {
width: 10em;
}
}
</style>

View File

@ -3,7 +3,7 @@ import { mapState } from 'vuex'
const NavPanel = { const NavPanel = {
created () { created () {
if (this.currentUser && this.currentUser.locked) { if (this.currentUser && this.currentUser.locked) {
this.$store.dispatch('startFetchingFollowRequest') this.$store.dispatch('startFetchingFollowRequests')
} }
}, },
computed: mapState({ computed: mapState({

View File

@ -33,7 +33,7 @@
<i class="button-icon icon-users" /> {{ $t("nav.public_tl") }} <i class="button-icon icon-users" /> {{ $t("nav.public_tl") }}
</router-link> </router-link>
</li> </li>
<li v-if="federating && !privateMode"> <li v-if="federating && (currentUser || !privateMode)">
<router-link :to="{ name: 'public-external-timeline' }"> <router-link :to="{ name: 'public-external-timeline' }">
<i class="button-icon icon-globe" /> {{ $t("nav.twkn") }} <i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
</router-link> </router-link>

View File

@ -2,10 +2,12 @@ import Notification from '../notification/notification.vue'
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js' import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
import { import {
notificationsFromStore, notificationsFromStore,
visibleNotificationsFromStore, filteredNotificationsFromStore,
unseenNotificationsFromStore unseenNotificationsFromStore
} from '../../services/notification_utils/notification_utils.js' } from '../../services/notification_utils/notification_utils.js'
const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30
const Notifications = { const Notifications = {
props: { props: {
// Disables display of panel header // Disables display of panel header
@ -18,7 +20,11 @@ const Notifications = {
}, },
data () { data () {
return { return {
bottomedOut: false bottomedOut: false,
// How many seen notifications to display in the list. The more there are,
// the heavier the page becomes. This count is increased when loading
// older notifications, and cut back to default whenever hitting "Read!".
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
} }
}, },
computed: { computed: {
@ -34,14 +40,17 @@ const Notifications = {
unseenNotifications () { unseenNotifications () {
return unseenNotificationsFromStore(this.$store) return unseenNotificationsFromStore(this.$store)
}, },
visibleNotifications () { filteredNotifications () {
return visibleNotificationsFromStore(this.$store, this.filterMode) return filteredNotificationsFromStore(this.$store, this.filterMode)
}, },
unseenCount () { unseenCount () {
return this.unseenNotifications.length return this.unseenNotifications.length
}, },
loading () { loading () {
return this.$store.state.statuses.notifications.loading return this.$store.state.statuses.notifications.loading
},
notificationsToDisplay () {
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
} }
}, },
components: { components: {
@ -64,12 +73,21 @@ const Notifications = {
methods: { methods: {
markAsSeen () { markAsSeen () {
this.$store.dispatch('markNotificationsAsSeen') this.$store.dispatch('markNotificationsAsSeen')
this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT
}, },
fetchOlderNotifications () { fetchOlderNotifications () {
if (this.loading) { if (this.loading) {
return return
} }
const seenCount = this.filteredNotifications.length - this.unseenCount
if (this.seenToDisplayCount < seenCount) {
this.seenToDisplayCount = Math.min(this.seenToDisplayCount + 20, seenCount)
return
} else if (this.seenToDisplayCount > seenCount) {
this.seenToDisplayCount = seenCount
}
const store = this.$store const store = this.$store
const credentials = store.state.users.currentUser.credentials const credentials = store.state.users.currentUser.credentials
store.commit('setNotificationsLoading', { value: true }) store.commit('setNotificationsLoading', { value: true })
@ -82,6 +100,7 @@ const Notifications = {
if (notifs.length === 0) { if (notifs.length === 0) {
this.bottomedOut = true this.bottomedOut = true
} }
this.seenToDisplayCount += notifs.length
}) })
} }
} }

View File

@ -32,7 +32,7 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div <div
v-for="notification in visibleNotifications" v-for="notification in notificationsToDisplay"
:key="notification.id" :key="notification.id"
class="notification" class="notification"
:class="{&quot;unseen&quot;: !minimalMode && !notification.seen}" :class="{&quot;unseen&quot;: !minimalMode && !notification.seen}"

View File

@ -63,7 +63,8 @@ const registration = {
await this.signUp(this.user) await this.signUp(this.user)
this.$router.push({ name: 'friends' }) this.$router.push({ name: 'friends' })
} catch (error) { } catch (error) {
console.warn('Registration failed: ' + error) console.warn('Registration failed: ', error)
this.setCaptcha()
} }
} }
}, },

View File

@ -170,7 +170,7 @@
<label <label
class="form--label" class="form--label"
for="captcha-label" for="captcha-label"
>{{ $t('captcha') }}</label> >{{ $t('registration.captcha') }}</label>
<template v-if="['kocaptcha', 'native'].includes(captcha.type)"> <template v-if="['kocaptcha', 'native'].includes(captcha.type)">
<img <img

View File

@ -12,7 +12,7 @@ const SideDrawer = {
this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, this.toggleDrawer) this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, this.toggleDrawer)
if (this.currentUser && this.currentUser.locked) { if (this.currentUser && this.currentUser.locked) {
this.$store.dispatch('startFetchingFollowRequest') this.$store.dispatch('startFetchingFollowRequests')
} }
}, },
components: { UserCard }, components: { UserCard },

View File

@ -88,7 +88,7 @@
</router-link> </router-link>
</li> </li>
<li <li
v-if="federating && !privateMode" v-if="federating && (currentUser || !privateMode)"
@click="toggleDrawer" @click="toggleDrawer"
> >
<router-link to="/main/all"> <router-link to="/main/all">

View File

@ -1,3 +1,4 @@
import map from 'lodash/map'
import BasicUserCard from '../basic_user_card/basic_user_card.vue' import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const StaffPanel = { const StaffPanel = {
@ -6,7 +7,7 @@ const StaffPanel = {
}, },
computed: { computed: {
staffAccounts () { staffAccounts () {
return this.$store.state.instance.staffAccounts return map(this.$store.state.instance.staffAccounts, nickname => this.$store.getters.findUser(nickname)).filter(_ => _)
} }
} }
} }

View File

@ -9,6 +9,7 @@ import ScopeSelector from '../scope_selector/scope_selector.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js' import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
import BlockCard from '../block_card/block_card.vue' import BlockCard from '../block_card/block_card.vue'
import MuteCard from '../mute_card/mute_card.vue' import MuteCard from '../mute_card/mute_card.vue'
import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue'
import SelectableList from '../selectable_list/selectable_list.vue' import SelectableList from '../selectable_list/selectable_list.vue'
import ProgressButton from '../progress_button/progress_button.vue' import ProgressButton from '../progress_button/progress_button.vue'
import EmojiInput from '../emoji_input/emoji_input.vue' import EmojiInput from '../emoji_input/emoji_input.vue'
@ -32,6 +33,12 @@ const MuteList = withSubscription({
childPropName: 'items' childPropName: 'items'
})(SelectableList) })(SelectableList)
const DomainMuteList = withSubscription({
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
childPropName: 'items'
})(SelectableList)
const UserSettings = { const UserSettings = {
data () { data () {
return { return {
@ -67,7 +74,8 @@ const UserSettings = {
changedPassword: false, changedPassword: false,
changePasswordError: false, changePasswordError: false,
activeTab: 'profile', activeTab: 'profile',
notificationSettings: this.$store.state.users.currentUser.notification_settings notificationSettings: this.$store.state.users.currentUser.notification_settings,
newDomainToMute: ''
} }
}, },
created () { created () {
@ -80,10 +88,12 @@ const UserSettings = {
ImageCropper, ImageCropper,
BlockList, BlockList,
MuteList, MuteList,
DomainMuteList,
EmojiInput, EmojiInput,
Autosuggest, Autosuggest,
BlockCard, BlockCard,
MuteCard, MuteCard,
DomainMuteCard,
ProgressButton, ProgressButton,
Importer, Importer,
Exporter, Exporter,
@ -365,6 +375,13 @@ const UserSettings = {
unmuteUsers (ids) { unmuteUsers (ids) {
return this.$store.dispatch('unmuteUsers', ids) return this.$store.dispatch('unmuteUsers', ids)
}, },
unmuteDomains (domains) {
return this.$store.dispatch('unmuteDomains', domains)
},
muteDomain () {
return this.$store.dispatch('muteDomain', this.newDomainToMute)
.then(() => { this.newDomainToMute = '' })
},
identity (value) { identity (value) {
return value return value
} }

View File

@ -509,59 +509,114 @@
</div> </div>
<div :label="$t('settings.mutes_tab')"> <div :label="$t('settings.mutes_tab')">
<div class="profile-edit-usersearch-wrapper"> <tab-switcher>
<Autosuggest <div label="Users">
:filter="filterUnMutedUsers" <div class="profile-edit-usersearch-wrapper">
:query="queryUserIds" <Autosuggest
:placeholder="$t('settings.search_user_to_mute')" :filter="filterUnMutedUsers"
> :query="queryUserIds"
<MuteCard :placeholder="$t('settings.search_user_to_mute')"
slot-scope="row"
:user-id="row.item"
/>
</Autosuggest>
</div>
<MuteList
:refresh="true"
:get-key="identity"
>
<template
slot="header"
slot-scope="{selected}"
>
<div class="profile-edit-bulk-actions">
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => muteUsers(selected)"
> >
{{ $t('user_card.mute') }} <MuteCard
<template slot="progress"> slot-scope="row"
{{ $t('user_card.mute_progress') }} :user-id="row.item"
</template> />
</ProgressButton> </Autosuggest>
<ProgressButton </div>
v-if="selected.length > 0" <MuteList
class="btn btn-default" :refresh="true"
:click="() => unmuteUsers(selected)" :get-key="identity"
>
<template
slot="header"
slot-scope="{selected}"
> >
{{ $t('user_card.unmute') }} <div class="profile-edit-bulk-actions">
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => muteUsers(selected)"
>
{{ $t('user_card.mute') }}
<template slot="progress">
{{ $t('user_card.mute_progress') }}
</template>
</ProgressButton>
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => unmuteUsers(selected)"
>
{{ $t('user_card.unmute') }}
<template slot="progress">
{{ $t('user_card.unmute_progress') }}
</template>
</ProgressButton>
</div>
</template>
<template
slot="item"
slot-scope="{item}"
>
<MuteCard :user-id="item" />
</template>
<template slot="empty">
{{ $t('settings.no_mutes') }}
</template>
</MuteList>
</div>
<div :label="$t('settings.domain_mutes')">
<div class="profile-edit-domain-mute-form">
<input
v-model="newDomainToMute"
:placeholder="$t('settings.type_domains_to_mute')"
type="text"
@keyup.enter="muteDomain"
>
<ProgressButton
class="btn btn-default"
:click="muteDomain"
>
{{ $t('domain_mute_card.mute') }}
<template slot="progress"> <template slot="progress">
{{ $t('user_card.unmute_progress') }} {{ $t('domain_mute_card.mute_progress') }}
</template> </template>
</ProgressButton> </ProgressButton>
</div> </div>
</template> <DomainMuteList
<template :refresh="true"
slot="item" :get-key="identity"
slot-scope="{item}" >
> <template
<MuteCard :user-id="item" /> slot="header"
</template> slot-scope="{selected}"
<template slot="empty"> >
{{ $t('settings.no_mutes') }} <div class="profile-edit-bulk-actions">
</template> <ProgressButton
</MuteList> v-if="selected.length > 0"
class="btn btn-default"
:click="() => unmuteDomains(selected)"
>
{{ $t('domain_mute_card.unmute') }}
<template slot="progress">
{{ $t('domain_mute_card.unmute_progress') }}
</template>
</ProgressButton>
</div>
</template>
<template
slot="item"
slot-scope="{item}"
>
<DomainMuteCard :domain="item" />
</template>
<template slot="empty">
{{ $t('settings.no_mutes') }}
</template>
</DomainMuteList>
</div>
</tab-switcher>
</div> </div>
</tab-switcher> </tab-switcher>
</div> </div>
@ -639,6 +694,18 @@
} }
} }
&-domain-mute-form {
padding: 1em;
display: flex;
flex-direction: column;
button {
align-self: flex-end;
margin-top: 1em;
width: 10em;
}
}
.setting-subitem { .setting-subitem {
margin-left: 1.75em; margin-left: 1.75em;
} }

View File

@ -21,6 +21,12 @@
"chat": { "chat": {
"title": "Chat" "title": "Chat"
}, },
"domain_mute_card": {
"mute": "Mute",
"mute_progress": "Muting...",
"unmute": "Unmute",
"unmute_progress": "Unmuting..."
},
"exporter": { "exporter": {
"export": "Export", "export": "Export",
"processing": "Processing, you'll soon be asked to download your file" "processing": "Processing, you'll soon be asked to download your file"
@ -264,6 +270,7 @@
"delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.", "delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.",
"delete_account_instructions": "Type your password in the input below to confirm account deletion.", "delete_account_instructions": "Type your password in the input below to confirm account deletion.",
"discoverable": "Allow discovery of this account in search results and other services", "discoverable": "Allow discovery of this account in search results and other services",
"domain_mutes": "Domains",
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.", "avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
"pad_emoji": "Pad emoji with spaces when adding from picker", "pad_emoji": "Pad emoji with spaces when adding from picker",
"export_theme": "Save preset", "export_theme": "Save preset",
@ -361,6 +368,7 @@
"post_status_content_type": "Post status content type", "post_status_content_type": "Post status content type",
"stop_gifs": "Play-on-hover GIFs", "stop_gifs": "Play-on-hover GIFs",
"streaming": "Enable automatic streaming of new posts when scrolled to the top", "streaming": "Enable automatic streaming of new posts when scrolled to the top",
"user_mutes": "Users",
"useStreamingApi": "Receive posts and notifications real-time", "useStreamingApi": "Receive posts and notifications real-time",
"useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)", "useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
"text": "Text", "text": "Text",
@ -369,6 +377,7 @@
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.", "theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.", "theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
"tooltipRadius": "Tooltips/alerts", "tooltipRadius": "Tooltips/alerts",
"type_domains_to_mute": "Type in domains to mute",
"upload_a_photo": "Upload a photo", "upload_a_photo": "Upload a photo",
"user_settings": "User Settings", "user_settings": "User Settings",
"values": { "values": {

View File

@ -0,0 +1,9 @@
import EventTargetPolyfill from '@ungap/event-target'
try {
/* eslint-disable no-new */
new EventTarget()
/* eslint-enable no-new */
} catch (e) {
window.EventTarget = EventTargetPolyfill
}

View File

@ -2,6 +2,9 @@ import Vue from 'vue'
import VueRouter from 'vue-router' import VueRouter from 'vue-router'
import Vuex from 'vuex' import Vuex from 'vuex'
import 'custom-event-polyfill'
import './lib/event_target_polyfill.js'
import interfaceModule from './modules/interface.js' import interfaceModule from './modules/interface.js'
import instanceModule from './modules/instance.js' import instanceModule from './modules/instance.js'
import statusesModule from './modules/statuses.js' import statusesModule from './modules/statuses.js'

View File

@ -146,6 +146,7 @@ const api = {
startFetchingFollowRequests (store) { startFetchingFollowRequests (store) {
if (store.state.fetchers['followRequests']) return if (store.state.fetchers['followRequests']) return
const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store }) const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher }) store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
}, },
stopFetchingFollowRequests (store) { stopFetchingFollowRequests (store) {

View File

@ -72,6 +72,16 @@ const showReblogs = (store, userId) => {
.then((relationship) => store.commit('updateUserRelationship', [relationship])) .then((relationship) => store.commit('updateUserRelationship', [relationship]))
} }
const muteDomain = (store, domain) => {
return store.rootState.api.backendInteractor.muteDomain({ domain })
.then(() => store.commit('addDomainMute', domain))
}
const unmuteDomain = (store, domain) => {
return store.rootState.api.backendInteractor.unmuteDomain({ domain })
.then(() => store.commit('removeDomainMute', domain))
}
export const mutations = { export const mutations = {
setMuted (state, { user: { id }, muted }) { setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id] const user = state.usersObject[id]
@ -177,6 +187,20 @@ export const mutations = {
state.currentUser.muteIds.push(muteId) state.currentUser.muteIds.push(muteId)
} }
}, },
saveDomainMutes (state, domainMutes) {
state.currentUser.domainMutes = domainMutes
},
addDomainMute (state, domain) {
if (state.currentUser.domainMutes.indexOf(domain) === -1) {
state.currentUser.domainMutes.push(domain)
}
},
removeDomainMute (state, domain) {
const index = state.currentUser.domainMutes.indexOf(domain)
if (index !== -1) {
state.currentUser.domainMutes.splice(index, 1)
}
},
setPinnedToUser (state, status) { setPinnedToUser (state, status) {
const user = state.usersObject[status.user.id] const user = state.usersObject[status.user.id]
const index = user.pinnedStatusIds.indexOf(status.id) const index = user.pinnedStatusIds.indexOf(status.id)
@ -297,6 +321,25 @@ const users = {
unmuteUsers (store, ids = []) { unmuteUsers (store, ids = []) {
return Promise.all(ids.map(id => unmuteUser(store, id))) return Promise.all(ids.map(id => unmuteUser(store, id)))
}, },
fetchDomainMutes (store) {
return store.rootState.api.backendInteractor.fetchDomainMutes()
.then((domainMutes) => {
store.commit('saveDomainMutes', domainMutes)
return domainMutes
})
},
muteDomain (store, domain) {
return muteDomain(store, domain)
},
unmuteDomain (store, domain) {
return unmuteDomain(store, domain)
},
muteDomains (store, domains = []) {
return Promise.all(domains.map(domain => muteDomain(store, domain)))
},
unmuteDomains (store, domain = []) {
return Promise.all(domain.map(domain => unmuteDomain(store, domain)))
},
fetchFriends ({ rootState, commit }, id) { fetchFriends ({ rootState, commit }, id) {
const user = rootState.users.usersObject[id] const user = rootState.users.usersObject[id]
const maxId = last(user.friendIds) const maxId = last(user.friendIds)
@ -401,7 +444,9 @@ const users = {
let rootState = store.rootState let rootState = store.rootState
try { try {
let data = await rootState.api.backendInteractor.register({ ...userInfo }) let data = await rootState.api.backendInteractor.register(
{ params: { ...userInfo } }
)
store.commit('signUpSuccess') store.commit('signUpSuccess')
store.commit('setToken', data.access_token) store.commit('setToken', data.access_token)
store.dispatch('loginUser', data.access_token) store.dispatch('loginUser', data.access_token)
@ -458,6 +503,7 @@ const users = {
user.credentials = accessToken user.credentials = accessToken
user.blockIds = [] user.blockIds = []
user.muteIds = [] user.muteIds = []
user.domainMutes = []
commit('setCurrentUser', user) commit('setCurrentUser', user)
commit('addNewUsers', [user]) commit('addNewUsers', [user])

View File

@ -72,6 +72,7 @@ const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute` const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
const MASTODON_SEARCH_2 = `/api/v2/search` const MASTODON_SEARCH_2 = `/api/v2/search`
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search' const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
const MASTODON_STREAMING = '/api/v1/streaming' const MASTODON_STREAMING = '/api/v1/streaming'
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/emoji_reactions_by` 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_REACT_URL = id => `/api/v1/pleroma/statuses/${id}/react_with_emoji`
@ -973,6 +974,28 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
}) })
} }
const fetchDomainMutes = ({ credentials }) => {
return promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials })
}
const muteDomain = ({ domain, credentials }) => {
return promisedRequest({
url: MASTODON_DOMAIN_BLOCKS_URL,
method: 'POST',
payload: { domain },
credentials
})
}
const unmuteDomain = ({ domain, credentials }) => {
return promisedRequest({
url: MASTODON_DOMAIN_BLOCKS_URL,
method: 'DELETE',
payload: { domain },
credentials
})
}
export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => { export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
return Object.entries({ return Object.entries({
...(credentials ...(credentials
@ -1138,7 +1161,10 @@ const apiService = {
reportUser, reportUser,
updateNotificationSettings, updateNotificationSettings,
search2, search2,
searchUsers searchUsers,
fetchDomainMutes,
muteDomain,
unmuteDomain
} }
export default apiService export default apiService

View File

@ -16,7 +16,7 @@ const backendInteractorService = credentials => ({
return notificationsFetcher.fetchAndUpdate({ store, credentials }) return notificationsFetcher.fetchAndUpdate({ store, credentials })
}, },
startFetchingFollowRequest ({ store }) { startFetchingFollowRequests ({ store }) {
return followRequestFetcher.startFetching({ store, credentials }) return followRequestFetcher.startFetching({ store, credentials })
}, },

View File

@ -32,12 +32,18 @@ export class RegistrationError extends Error {
} }
if (typeof error === 'object') { if (typeof error === 'object') {
const errorContents = JSON.parse(error.error)
// keys will have the property that has the error, for example 'ap_id',
// 'email' or 'captcha', the value will be an array of its error
// like "ap_id": ["has been taken"] or "captcha": ["Invalid CAPTCHA"]
// replace ap_id with username // replace ap_id with username
if (error.ap_id) { if (errorContents.ap_id) {
error.username = error.ap_id errorContents.username = errorContents.ap_id
delete error.ap_id delete errorContents.ap_id
} }
this.message = humanizeErrors(error)
this.message = humanizeErrors(errorContents)
} else { } else {
this.message = error this.message = error
} }

View File

@ -26,7 +26,7 @@ const sortById = (a, b) => {
} }
} }
export const visibleNotificationsFromStore = (store, types) => { export const filteredNotificationsFromStore = (store, types) => {
// map is just to clone the array since sort mutates it and it causes some issues // map is just to clone the array since sort mutates it and it causes some issues
let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById) let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)
sortedNotifications = sortBy(sortedNotifications, 'seen') sortedNotifications = sortBy(sortedNotifications, 'seen')
@ -36,4 +36,4 @@ export const visibleNotificationsFromStore = (store, types) => {
} }
export const unseenNotificationsFromStore = store => export const unseenNotificationsFromStore = store =>
filter(visibleNotificationsFromStore(store), ({ seen }) => !seen) filter(filteredNotificationsFromStore(store), ({ seen }) => !seen)

View File

@ -2,7 +2,6 @@ import apiService from '../api/api.service.js'
const update = ({ store, notifications, older }) => { const update = ({ store, notifications, older }) => {
store.dispatch('setNotificationsError', { value: false }) store.dispatch('setNotificationsError', { value: false })
store.dispatch('addNewNotifications', { notifications, older }) store.dispatch('addNewNotifications', { notifications, older })
} }
@ -30,9 +29,9 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
// load unread notifications repeatedly to provide consistency between browser tabs // load unread notifications repeatedly to provide consistency between browser tabs
const notifications = timelineData.data const notifications = timelineData.data
const unread = notifications.filter(n => !n.seen).map(n => n.id) const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
if (unread.length) { if (readNotifsIds.length) {
args['since'] = Math.min(...unread) args['since'] = Math.max(...readNotifsIds)
fetchNotifications({ store, args, older }) fetchNotifications({ store, args, older })
} }

View File

@ -1,7 +1,7 @@
import * as NotificationUtils from 'src/services/notification_utils/notification_utils.js' import * as NotificationUtils from 'src/services/notification_utils/notification_utils.js'
describe('NotificationUtils', () => { describe('NotificationUtils', () => {
describe('visibleNotificationsFromStore', () => { describe('filteredNotificationsFromStore', () => {
it('should return sorted notifications with configured types', () => { it('should return sorted notifications with configured types', () => {
const store = { const store = {
state: { state: {
@ -47,7 +47,7 @@ describe('NotificationUtils', () => {
type: 'like' type: 'like'
} }
] ]
expect(NotificationUtils.visibleNotificationsFromStore(store)).to.eql(expected) expect(NotificationUtils.filteredNotificationsFromStore(store)).to.eql(expected)
}) })
}) })

View File

@ -710,6 +710,11 @@
dependencies: dependencies:
qrcode "^1.3.0" qrcode "^1.3.0"
"@ungap/event-target@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.1.0.tgz#88d527d40de86c4b0c99a060ca241d755999915b"
integrity sha512-W2oyj0Fe1w/XhPZjkI3oUcDUAmu5P4qsdT2/2S8aMhtAWM/CE/jYWtji0pKNPDfxLI75fa5gWSEmnynKMNP/oA==
"@vue/babel-helper-vue-jsx-merge-props@^1.0.0": "@vue/babel-helper-vue-jsx-merge-props@^1.0.0":
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz#048fe579958da408fb7a8b2a3ec050b50a661040" resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz#048fe579958da408fb7a8b2a3ec050b50a661040"
@ -2281,6 +2286,11 @@ currently-unhandled@^0.4.1:
dependencies: dependencies:
array-find-index "^1.0.1" array-find-index "^1.0.1"
custom-event-polyfill@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee"
integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==
custom-event@~1.0.0: custom-event@~1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"