Merge remote-tracking branch 'upstream/develop' into shigusegubu

* upstream/develop: (21 commits)
  Added polyfills for EventTarget (needed for Safari) and CustomEvent (needed for IE)
  Fix missing TWKN when logged in, but server is set to private mode.
  Fix follower request fetching
  Add domain mutes to changelog
  Implement domain mutes v2
  change changelog to reflect actual release history of frontend
  Fix #750 , fix error messages and captcha resetting
  Optimize Notifications Rendering
  update CHANGELOG
  Use last seen notif instead of first unseen notif for sinceId
  Add AMOLED dark theme
  mfa fix
  unify showimmideately
  Some error handling
  wire up staff accounts with correct store data
  remove unused fallback
  Add user migrates filter to interactions
  change the expression of `move`
  Fix target account link
  Add view for moves notifications
  ...
This commit is contained in:
Henry Jameson 2020-01-27 23:32:54 +02:00
commit 948fd2086b
42 changed files with 484 additions and 140 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,8 @@ import Notifications from '../notifications/notifications.vue'
const tabModeDict = { const tabModeDict = {
mentions: ['mention'], mentions: ['mention'],
'likes+repeats': ['repeat', 'like'], 'likes+repeats': ['repeat', 'like'],
follows: ['follow'] follows: ['follow'],
moves: ['move']
} }
const Interactions = { const Interactions = {

View file

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

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

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

View file

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

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

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

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

@ -103,6 +103,10 @@ const settings = {
promise.then(() => { promise.then(() => {
this.$store.dispatch('setOption', { name: 'useStreamingApi', value }) 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 })
}) })
} }
} }

View file

@ -323,6 +323,11 @@
{{ $t('settings.notification_visibility_mentions') }} {{ $t('settings.notification_visibility_mentions') }}
</Checkbox> </Checkbox>
</li> </li>
<li>
<Checkbox v-model="notificationVisibility.moves">
{{ $t('settings.notification_visibility_moves') }}
</Checkbox>
</li>
</ul> </ul>
</div> </div>
<div> <div>

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

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

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"
@ -110,7 +116,8 @@
"notifications": "Notifications", "notifications": "Notifications",
"read": "Read!", "read": "Read!",
"repeated_you": "repeated your status", "repeated_you": "repeated your status",
"no_more_notifications": "No more notifications" "no_more_notifications": "No more notifications",
"migrated_to": "migrated to"
}, },
"polls": { "polls": {
"add_poll": "Add Poll", "add_poll": "Add Poll",
@ -140,6 +147,7 @@
"interactions": { "interactions": {
"favs_repeats": "Repeats and Favorites", "favs_repeats": "Repeats and Favorites",
"follows": "New follows", "follows": "New follows",
"moves": "User migrates",
"load_older": "Load older interactions" "load_older": "Load older interactions"
}, },
"post_status": { "post_status": {
@ -262,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",
@ -311,6 +320,7 @@
"notification_visibility_likes": "Likes", "notification_visibility_likes": "Likes",
"notification_visibility_mentions": "Mentions", "notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats", "notification_visibility_repeats": "Repeats",
"notification_visibility_moves": "User Migrates",
"no_rich_text_description": "Strip rich text formatting from all posts", "no_rich_text_description": "Strip rich text formatting from all posts",
"no_blocks": "No blocks", "no_blocks": "No blocks",
"no_mutes": "No mutes", "no_mutes": "No mutes",
@ -358,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",
@ -366,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

@ -35,60 +35,68 @@ const api = {
enableMastoSockets (store) { enableMastoSockets (store) {
const { state, dispatch } = store const { state, dispatch } = store
if (state.mastoUserSocket) return if (state.mastoUserSocket) return
dispatch('startMastoUserSocket') return dispatch('startMastoUserSocket')
}, },
disableMastoSockets (store) { disableMastoSockets (store) {
const { state, dispatch } = store const { state, dispatch } = store
if (!state.mastoUserSocket) return if (!state.mastoUserSocket) return
dispatch('stopMastoUserSocket') return dispatch('stopMastoUserSocket')
}, },
// MastoAPI 'User' sockets // MastoAPI 'User' sockets
startMastoUserSocket (store) { startMastoUserSocket (store) {
const { state, dispatch } = store return new Promise((resolve, reject) => {
state.mastoUserSocket = state.backendInteractor.startUserSocket({ store }) try {
state.mastoUserSocket.addEventListener( const { state, dispatch, rootState } = store
'message', const timelineData = rootState.statuses.timelines.friends
({ detail: message }) => { state.mastoUserSocket = state.backendInteractor.startUserSocket({ store })
if (!message) return // pings state.mastoUserSocket.addEventListener(
if (message.event === 'notification') { 'message',
dispatch('addNewNotifications', { ({ detail: message }) => {
notifications: [message.notification], if (!message) return // pings
older: false if (message.event === 'notification') {
}) dispatch('addNewNotifications', {
} else if (message.event === 'update') { notifications: [message.notification],
dispatch('addNewStatuses', { older: false
statuses: [message.status], })
userId: false, } else if (message.event === 'update') {
showImmediately: false, dispatch('addNewStatuses', {
timeline: 'friends' 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 }) => { state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
const ignoreCodes = new Set([ console.error('Error in MastoAPI websocket:', error)
1000, // Normal (intended) closure })
1001 // Going away state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
]) const ignoreCodes = new Set([
const { code } = closeEvent 1000, // Normal (intended) closure
if (ignoreCodes.has(code)) { 1001 // Going away
console.debug(`Not restarting socket becasue of closure code ${code} is in ignore list`) ])
} else { const { code } = closeEvent
console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`) if (ignoreCodes.has(code)) {
dispatch('startFetchingTimeline', { timeline: 'friends' }) console.debug(`Not restarting socket becasue of closure code ${code} is in ignore list`)
dispatch('startFetchingNotifications') } else {
dispatch('restartMastoUserSocket') console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
dispatch('restartMastoUserSocket')
}
})
resolve()
} catch (e) {
reject(e)
} }
}) })
}, },
restartMastoUserSocket ({ dispatch }) { restartMastoUserSocket ({ dispatch }) {
// This basically starts MastoAPI user socket and stops conventional // This basically starts MastoAPI user socket and stops conventional
// fetchers when connection reestablished // fetchers when connection reestablished
dispatch('startMastoUserSocket').then(() => { return dispatch('startMastoUserSocket').then(() => {
dispatch('stopFetchingTimeline', { timeline: 'friends' }) dispatch('stopFetchingTimeline', { timeline: 'friends' })
dispatch('stopFetchingNotifications') dispatch('stopFetchingNotifications')
}) })
@ -138,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

@ -28,7 +28,8 @@ export const defaultState = {
follows: true, follows: true,
mentions: true, mentions: true,
likes: true, likes: true,
repeats: true repeats: true,
moves: true
}, },
webPushNotifications: false, webPushNotifications: false,
muteWords: [], muteWords: [],

View file

@ -67,7 +67,8 @@ const visibleNotificationTypes = (rootState) => {
rootState.config.notificationVisibility.likes && 'like', rootState.config.notificationVisibility.likes && 'like',
rootState.config.notificationVisibility.mentions && 'mention', rootState.config.notificationVisibility.mentions && 'mention',
rootState.config.notificationVisibility.repeats && 'repeat', rootState.config.notificationVisibility.repeats && 'repeat',
rootState.config.notificationVisibility.follows && 'follow' rootState.config.notificationVisibility.follows && 'follow',
rootState.config.notificationVisibility.moves && 'move'
].filter(_ => _) ].filter(_ => _)
} }
@ -306,7 +307,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => { const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
each(notifications, (notification) => { each(notifications, (notification) => {
if (notification.type !== 'follow') { if (notification.type !== 'follow' && notification.type !== 'move') {
notification.action = addStatusToGlobalStorage(state, notification.action).item notification.action = addStatusToGlobalStorage(state, notification.action).item
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
} }
@ -339,6 +340,9 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
case 'follow': case 'follow':
i18nString = 'followed_you' i18nString = 'followed_you'
break break
case 'move':
i18nString = 'migrated_to'
break
} }
if (i18nString) { if (i18nString) {

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)
@ -373,8 +416,10 @@ const users = {
}, },
addNewNotifications (store, { notifications }) { addNewNotifications (store, { notifications }) {
const users = map(notifications, 'from_profile') const users = map(notifications, 'from_profile')
const targetUsers = map(notifications, 'target')
const notificationIds = notifications.map(_ => _.id) const notificationIds = notifications.map(_ => _.id)
store.commit('addNewUsers', users) store.commit('addNewUsers', users)
store.commit('addNewUsers', targetUsers)
const notificationsObject = store.rootState.statuses.notifications.idStore const notificationsObject = store.rootState.statuses.notifications.idStore
const relevantNotifications = Object.entries(notificationsObject) const relevantNotifications = Object.entries(notificationsObject)
@ -399,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)
@ -456,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 oldfetch = window.fetch const oldfetch = window.fetch
@ -948,6 +949,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
@ -1110,7 +1133,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

@ -341,10 +341,13 @@ export const parseNotification = (data) => {
if (masto) { if (masto) {
output.type = mastoDict[data.type] || data.type output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen output.seen = data.pleroma.is_seen
output.status = output.type === 'follow' output.status = output.type === 'follow' || output.type === 'move'
? null ? null
: parseStatus(data.status) : parseStatus(data.status)
output.action = output.status // TODO: Refactor, this is unneeded output.action = output.status // TODO: Refactor, this is unneeded
output.target = output.type !== 'move'
? null
: parseUser(data.target)
output.from_profile = parseUser(data.account) output.from_profile = parseUser(data.account)
} else { } else {
const parsedNotice = parseStatus(data.notice) const parsedNotice = parseStatus(data.notice)

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

@ -6,7 +6,8 @@ export const visibleTypes = store => ([
store.state.config.notificationVisibility.likes && 'like', store.state.config.notificationVisibility.likes && 'like',
store.state.config.notificationVisibility.mentions && 'mention', store.state.config.notificationVisibility.mentions && 'mention',
store.state.config.notificationVisibility.repeats && 'repeat', 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(_ => _)) ].filter(_ => _))
const sortById = (a, b) => { const sortById = (a, b) => {
@ -25,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')
@ -35,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

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

View file

@ -333,6 +333,12 @@
"css": "login", "css": "login",
"code": 59424, "code": 59424,
"src": "fontawesome" "src": "fontawesome"
},
{
"uid": "f3ebd6751c15a280af5cc5f4a764187d",
"css": "arrow-curved",
"code": 59426,
"src": "iconic"
} }
] ]
} }

View file

@ -3,6 +3,7 @@
"sigsegv2": [ "SigSeg部", "#003238", "#00616c", "#e8f9fb", "#81ffff", "#ff7b66", "#4ae619", "#00ddff", "#ccef53" ], "sigsegv2": [ "SigSeg部", "#003238", "#00616c", "#e8f9fb", "#81ffff", "#ff7b66", "#4ae619", "#00ddff", "#ccef53" ],
"pleroma-dark": [ "Pleroma Dark", "#121a24", "#182230", "#b9b9ba", "#d8a070", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ], "pleroma-dark": [ "Pleroma Dark", "#121a24", "#182230", "#b9b9ba", "#d8a070", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
"pleroma-light": [ "Pleroma Light", "#f2f4f6", "#dbe0e8", "#304055", "#f86f0f", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ], "pleroma-light": [ "Pleroma Light", "#f2f4f6", "#dbe0e8", "#304055", "#f86f0f", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
"pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"],
"classic-dark": [ "Classic Dark", "#161c20", "#282e32", "#b9b9b9", "#baaa9c", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ], "classic-dark": [ "Classic Dark", "#161c20", "#282e32", "#b9b9b9", "#baaa9c", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
"bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"], "bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
"ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ], "ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ],

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"