forked from AkkomaGang/akkoma-fe
Merge branch 'develop' into feat/emoji-reactions
This commit is contained in:
commit
c4beac5f89
28 changed files with 365 additions and 82 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
15
src/components/domain_mute_card/domain_mute_card.js
Normal file
15
src/components/domain_mute_card/domain_mute_card.js
Normal 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
|
38
src/components/domain_mute_card/domain_mute_card.vue
Normal file
38
src/components/domain_mute_card/domain_mute_card.vue
Normal 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>
|
|
@ -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({
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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="{"unseen": !minimalMode && !notification.seen}"
|
:class="{"unseen": !minimalMode && !notification.seen}"
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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(_ => _)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -509,6 +509,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :label="$t('settings.mutes_tab')">
|
<div :label="$t('settings.mutes_tab')">
|
||||||
|
<tab-switcher>
|
||||||
|
<div label="Users">
|
||||||
<div class="profile-edit-usersearch-wrapper">
|
<div class="profile-edit-usersearch-wrapper">
|
||||||
<Autosuggest
|
<Autosuggest
|
||||||
:filter="filterUnMutedUsers"
|
:filter="filterUnMutedUsers"
|
||||||
|
@ -563,6 +565,59 @@
|
||||||
</template>
|
</template>
|
||||||
</MuteList>
|
</MuteList>
|
||||||
</div>
|
</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">
|
||||||
|
{{ $t('domain_mute_card.mute_progress') }}
|
||||||
|
</template>
|
||||||
|
</ProgressButton>
|
||||||
|
</div>
|
||||||
|
<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>
|
</tab-switcher>
|
||||||
</div>
|
</div>
|
||||||
</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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
9
src/lib/event_target_polyfill.js
Normal file
9
src/lib/event_target_polyfill.js
Normal 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
|
||||||
|
}
|
|
@ -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'
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue