...
 
Commits (27)
......@@ -5,12 +5,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### 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
- User profile button clutter organized into a menu
- Emoji picker
- Started changelog anew
- Ability to change user's email
- About page
- Added remote user redirect
### 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
### Fixed
......
......@@ -185,12 +185,9 @@ const getAppSecret = async ({ store }) => {
})
}
const resolveStaffAccounts = async ({ store, accounts }) => {
const backendInteractor = store.state.api.backendInteractor
let nicknames = accounts.map(uri => uri.split('/').pop())
.map(id => backendInteractor.fetchUser({ id }))
nicknames = await Promise.all(nicknames)
const resolveStaffAccounts = ({ store, accounts }) => {
const nicknames = accounts.map(uri => uri.split('/').pop())
nicknames.map(nickname => store.dispatch('fetchUser', nickname))
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
}
......@@ -236,7 +233,7 @@ const getNodeInfo = async ({ store }) => {
})
const accounts = metadata.staffAccounts
await resolveStaffAccounts({ store, accounts })
resolveStaffAccounts({ store, accounts })
} else {
throw (res)
}
......
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
<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>
......@@ -3,7 +3,8 @@ import Notifications from '../notifications/notifications.vue'
const tabModeDict = {
mentions: ['mention'],
'likes+repeats': ['repeat', 'like'],
follows: ['follow']
follows: ['follow'],
moves: ['move']
}
const Interactions = {
......
......@@ -21,6 +21,10 @@
key="follows"
:label="$t('interactions.follows')"
/>
<span
key="moves"
:label="$t('interactions.moves')"
/>
</tab-switcher>
<Notifications
ref="notifications"
......
......@@ -43,18 +43,18 @@ const Notification = {
const user = this.notification.from_profile
return highlightStyle(highlight[user.screen_name])
},
userInStore () {
return this.$store.getters.findUser(this.notification.from_profile.id)
},
user () {
if (this.userInStore) {
return this.userInStore
}
return this.notification.from_profile
return this.$store.getters.findUser(this.notification.from_profile.id)
},
userProfileLink () {
return this.generateUserProfileLink(this.user)
},
targetUser () {
return this.$store.getters.findUser(this.notification.target.id)
},
targetUserProfileLink () {
return this.generateUserProfileLink(this.targetUser)
},
needMute () {
return this.user.muted
}
......
......@@ -74,9 +74,13 @@
<i class="fa icon-user-plus lit" />
<small>{{ $t('notifications.followed_you') }}</small>
</span>
<span v-if="notification.type === 'move'">
<i class="fa icon-arrow-curved lit" />
<small>{{ $t('notifications.migrated_to') }}</small>
</span>
</div>
<div
v-if="notification.type === 'follow'"
v-if="notification.type === 'follow' || notification.type === 'move'"
class="timeago"
>
<span class="faint">
......@@ -115,6 +119,14 @@
@{{ notification.from_profile.screen_name }}
</router-link>
</div>
<div
v-else-if="notification.type === 'move'"
class="move-text"
>
<router-link :to="targetUserProfileLink">
@{{ notification.target.screen_name }}
</router-link>
</div>
<template v-else>
<status
class="faint"
......
......@@ -2,10 +2,12 @@ import Notification from '../notification/notification.vue'
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
import {
notificationsFromStore,
visibleNotificationsFromStore,
filteredNotificationsFromStore,
unseenNotificationsFromStore
} from '../../services/notification_utils/notification_utils.js'
const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30
const Notifications = {
props: {
// Disables display of panel header
......@@ -18,7 +20,11 @@ const Notifications = {
},
data () {
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: {
......@@ -34,14 +40,17 @@ const Notifications = {
unseenNotifications () {
return unseenNotificationsFromStore(this.$store)
},
visibleNotifications () {
return visibleNotificationsFromStore(this.$store, this.filterMode)
filteredNotifications () {
return filteredNotificationsFromStore(this.$store, this.filterMode)
},
unseenCount () {
return this.unseenNotifications.length
},
loading () {
return this.$store.state.statuses.notifications.loading
},
notificationsToDisplay () {
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
}
},
components: {
......@@ -64,12 +73,21 @@ const Notifications = {
methods: {
markAsSeen () {
this.$store.dispatch('markNotificationsAsSeen')
this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT
},
fetchOlderNotifications () {
if (this.loading) {
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 credentials = store.state.users.currentUser.credentials
store.commit('setNotificationsLoading', { value: true })
......@@ -82,6 +100,7 @@ const Notifications = {
if (notifs.length === 0) {
this.bottomedOut = true
}
this.seenToDisplayCount += notifs.length
})
}
}
......
......@@ -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;
......
......@@ -32,7 +32,7 @@
</div>
<div class="panel-body">
<div
v-for="notification in visibleNotifications"
v-for="notification in notificationsToDisplay"
:key="notification.id"
class="notification"
:class="{&quot;unseen&quot;: !minimalMode && !notification.seen}"
......
......@@ -63,7 +63,8 @@ const registration = {
await this.signUp(this.user)
this.$router.push({ name: 'friends' })
} catch (error) {
console.warn('Registration failed: ' + error)
console.warn('Registration failed: ', error)
this.setCaptcha()
}
}
},
......
......@@ -170,7 +170,7 @@
<label
class="form--label"
for="captcha-label"
>{{ $t('captcha') }}</label>
>{{ $t('registration.captcha') }}</label>
<template v-if="['kocaptcha', 'native'].includes(captcha.type)">
<img
......
......@@ -323,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>
......
import map from 'lodash/map'
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const StaffPanel = {
......@@ -6,7 +7,7 @@ const StaffPanel = {
},
computed: {
staffAccounts () {
return this.$store.state.instance.staffAccounts
return map(this.$store.state.instance.staffAccounts, nickname => this.$store.getters.findUser(nickname)).filter(_ => _)
}
}
}
......
......@@ -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
......
......@@ -9,6 +9,7 @@ import ScopeSelector from '../scope_selector/scope_selector.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
import BlockCard from '../block_card/block_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 ProgressButton from '../progress_button/progress_button.vue'
import EmojiInput from '../emoji_input/emoji_input.vue'
......@@ -32,6 +33,12 @@ const MuteList = withSubscription({
childPropName: 'items'
})(SelectableList)
const DomainMuteList = withSubscription({
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
childPropName: 'items'
})(SelectableList)
const UserSettings = {
data () {
return {
......@@ -69,7 +76,8 @@ const UserSettings = {
activeTab: 'profile',
notificationSettings: this.$store.state.users.currentUser.notification_settings,
defaultAvatar: (this.$store.state.instance.server + this.$store.state.instance.defaultAvatar),
defaultBanner: (this.$store.state.instance.server + this.$store.state.instance.defaultBanner)
defaultBanner: (this.$store.state.instance.server + this.$store.state.instance.defaultBanner),
newDomainToMute: ''
}
},
created () {
......@@ -82,10 +90,12 @@ const UserSettings = {
ImageCropper,
BlockList,
MuteList,
DomainMuteList,
EmojiInput,
Autosuggest,
BlockCard,
MuteCard,
DomainMuteCard,
ProgressButton,
Importer,
Exporter,
......@@ -454,6 +464,13 @@ const UserSettings = {
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) {
return value
}
......
......@@ -526,59 +526,114 @@
</div>
<div :label="$t('settings.mutes_tab')">
<div class="profile-edit-usersearch-wrapper">
<Autosuggest
:filter="filterUnMutedUsers"
:query="queryUserIds"
:placeholder="$t('settings.search_user_to_mute')"
>
<MuteCard
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)"
<tab-switcher>
<div label="Users">
<div class="profile-edit-usersearch-wrapper">
<Autosuggest
:filter="filterUnMutedUsers"
:query="queryUserIds"
:placeholder="$t('settings.search_user_to_mute')"
>
<MuteCard
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') }}
<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"
>
{{ $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)"
:click="muteDomain"
>
{{ $t('user_card.unmute') }}
{{ $t('domain_mute_card.mute') }}
<template slot="progress">
{{ $t('user_card.unmute_progress') }}
{{ $t('domain_mute_card.mute_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>
<DomainMuteList
: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="() => 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>
</tab-switcher>
</div>
......@@ -656,6 +711,18 @@
}
}
&-domain-mute-form {
padding: 1em;
display: flex;
flex-direction: column;
button {
align-self: flex-end;
margin-top: 1em;
width: 10em;
}
}
.setting-subitem {
margin-left: 1.75em;
}
......
......@@ -21,6 +21,12 @@
"chat": {
"title": "Chat"
},
"domain_mute_card": {
"mute": "Mute",
"mute_progress": "Muting...",
"unmute": "Unmute",
"unmute_progress": "Unmuting..."
},
"exporter": {
"export": "Export",
"processing": "Processing, you'll soon be asked to download your file"
......@@ -110,7 +116,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 +147,7 @@
"interactions": {
"favs_repeats": "Repeats and Favorites",
"follows": "New follows",
"moves": "User migrates",
"load_older": "Load older interactions"
},
"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_instructions": "Type your password in the input below to confirm account deletion.",
"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.",
"pad_emoji": "Pad emoji with spaces when adding from picker",
"export_theme": "Save preset",
......@@ -311,6 +320,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",
......@@ -364,6 +374,7 @@
"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",
"user_mutes": "Users",
"useStreamingApi": "Receive posts and notifications real-time",
"useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
"text": "Text",
......@@ -372,6 +383,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_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",
"type_domains_to_mute": "Type in domains to mute",
"upload_a_photo": "Upload a photo",
"user_settings": "User Settings",
"values": {
......
......@@ -28,7 +28,8 @@ export const defaultState = {
follows: true,
mentions: true,
likes: true,
repeats: true
repeats: true,
moves: true
},
webPushNotifications: false,
muteWords: [],
......
......@@ -67,7 +67,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(_ => _)
}
......@@ -306,7 +307,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
}
......@@ -339,6 +340,9 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
case 'follow':
i18nString = 'followed_you'
break
case 'move':
i18nString = 'migrated_to'
break
}
if (i18nString) {
......
......@@ -72,6 +72,16 @@ const showReblogs = (store, userId) => {
.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 = {
setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id]
......@@ -177,6 +187,20 @@ export const mutations = {
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) {
const user = state.usersObject[status.user.id]
const index = user.pinnedStatusIds.indexOf(status.id)
......@@ -297,6 +321,25 @@ const users = {
unmuteUsers (store, ids = []) {
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) {
const user = rootState.users.usersObject[id]
const maxId = last(user.friendIds)
......@@ -373,8 +416,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)
......@@ -399,7 +444,9 @@ const users = {
let rootState = store.rootState
try {
let data = await rootState.api.backendInteractor.register({ ...userInfo })
let data = await rootState.api.backendInteractor.register(
{ params: { ...userInfo } }
)
store.commit('signUpSuccess')
store.commit('setToken', data.access_token)
store.dispatch('loginUser', data.access_token)
......@@ -456,6 +503,7 @@ const users = {
user.credentials = accessToken
user.blockIds = []
user.muteIds = []
user.domainMutes = []
commit('setCurrentUser', user)
commit('addNewUsers', [user])
......
......@@ -76,6 +76,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_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
const MASTODON_STREAMING = '/api/v1/streaming'
const oldfetch = window.fetch
......@@ -985,6 +986,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 = {} }) => {
return Object.entries({
...(credentials
......@@ -1150,7 +1173,10 @@ const apiService = {
reportUser,
updateNotificationSettings,
search2,
searchUsers
searchUsers,
fetchDomainMutes,
muteDomain,
unmuteDomain
}
export default apiService
......@@ -341,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)
......
......@@ -32,12 +32,18 @@ export class RegistrationError extends Error {
}
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
if (error.ap_id) {
error.username = error.ap_id
delete error.ap_id
if (errorContents.ap_id) {
errorContents.username = errorContents.ap_id
delete errorContents.ap_id
}
this.message = humanizeErrors(error)
this.message = humanizeErrors(errorContents)
} else {
this.message = error
}
......
......@@ -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) => {
......@@ -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
let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)
sortedNotifications = sortBy(sortedNotifications, 'seen')
......@@ -35,4 +36,4 @@ export const visibleNotificationsFromStore = (store, types) => {
}
export const unseenNotificationsFromStore = store =>
filter(visibleNotificationsFromStore(store), ({ seen }) => !seen)
filter(filteredNotificationsFromStore(store), ({ seen }) => !seen)
......@@ -2,7 +2,6 @@ import apiService from '../api/api.service.js'
const update = ({ store, notifications, older }) => {
store.dispatch('setNotificationsError', { value: false })
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
const notifications = timelineData.data
const unread = notifications.filter(n => !n.seen).map(n => n.id)
if (unread.length) {
args['since'] = Math.min(...unread)
const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
if (readNotifsIds.length) {
args['since'] = Math.max(...readNotifsIds)
fetchNotifications({ store, args, older })
}
......
......@@ -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
}
}
})
......
......@@ -333,6 +333,12 @@
"css": "login",
"code": 59424,
"src": "fontawesome"
},
{
"uid": "f3ebd6751c15a280af5cc5f4a764187d",
"css": "arrow-curved",
"code": 59426,
"src": "iconic"
}
]
}
\ No newline at end of file
{
"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-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"],
"classic-dark": [ "Classic Dark", "#161c20", "#282e32", "#b9b9b9", "#baaa9c", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
"bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
"ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ],
......
import * as NotificationUtils from 'src/services/notification_utils/notification_utils.js'
describe('NotificationUtils', () => {
describe('visibleNotificationsFromStore', () => {
describe('filteredNotificationsFromStore', () => {
it('should return sorted notifications with configured types', () => {
const store = {
state: {
......@@ -47,7 +47,7 @@ describe('NotificationUtils', () => {
type: 'like'
}
]
expect(NotificationUtils.visibleNotificationsFromStore(store)).to.eql(expected)
expect(NotificationUtils.filteredNotificationsFromStore(store)).to.eql(expected)
})
})
......