Merge branch 'develop' into feat/emoji-reactions

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

View File

@ -5,12 +5,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### 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

View File

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

View File

@ -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)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -2,10 +2,12 @@ import Notification from '../notification/notification.vue'
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
import {
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
})
}
}

View File

@ -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}"

View File

@ -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()
}
}
},

View File

@ -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

View File

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

View File

@ -88,7 +88,7 @@
</router-link>
</li>
<li
v-if="federating && !privateMode"
v-if="federating && (currentUser || !privateMode)"
@click="toggleDrawer"
>
<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'
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(_ => _)
}
}
}

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 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 {
@ -67,7 +74,8 @@ const UserSettings = {
changedPassword: false,
changePasswordError: false,
activeTab: 'profile',
notificationSettings: this.$store.state.users.currentUser.notification_settings
notificationSettings: this.$store.state.users.currentUser.notification_settings,
newDomainToMute: ''
}
},
created () {
@ -80,10 +88,12 @@ const UserSettings = {
ImageCropper,
BlockList,
MuteList,
DomainMuteList,
EmojiInput,
Autosuggest,
BlockCard,
MuteCard,
DomainMuteCard,
ProgressButton,
Importer,
Exporter,
@ -365,6 +375,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
}

View File

@ -509,6 +509,8 @@
</div>
<div :label="$t('settings.mutes_tab')">
<tab-switcher>
<div label="Users">
<div class="profile-edit-usersearch-wrapper">
<Autosuggest
:filter="filterUnMutedUsers"
@ -563,6 +565,59 @@
</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">
{{ $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>
</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 {
margin-left: 1.75em;
}

View File

@ -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"
@ -264,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",
@ -361,6 +368,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",
@ -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_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": {

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 Vuex from 'vuex'
import 'custom-event-polyfill'
import './lib/event_target_polyfill.js'
import interfaceModule from './modules/interface.js'
import instanceModule from './modules/instance.js'
import statusesModule from './modules/statuses.js'

View File

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

View File

@ -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)
@ -401,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)
@ -458,6 +503,7 @@ const users = {
user.credentials = accessToken
user.blockIds = []
user.muteIds = []
user.domainMutes = []
commit('setCurrentUser', 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_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 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`
@ -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 = {} }) => {
return Object.entries({
...(credentials
@ -1138,7 +1161,10 @@ const apiService = {
reportUser,
updateNotificationSettings,
search2,
searchUsers
searchUsers,
fetchDomainMutes,
muteDomain,
unmuteDomain
}
export default apiService

View File

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

View File

@ -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
}

View File

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

View File

@ -1,7 +1,7 @@
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)
})
})

View File

@ -710,6 +710,11 @@
dependencies:
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":
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"
@ -2281,6 +2286,11 @@ currently-unhandled@^0.4.1:
dependencies:
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:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"