Merge branch 'develop' into features/favicons

This commit is contained in:
Shpuld Shpuldson 2020-06-18 16:47:37 +03:00
commit 8a9654b511
140 changed files with 7916 additions and 4330 deletions

View file

@ -1,10 +1,8 @@
import { each, map, concat, last, get } from 'lodash'
import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
import 'whatwg-fetch'
import { RegistrationError, StatusCodeError } from '../errors/errors'
/* eslint-env browser */
const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
@ -17,6 +15,7 @@ const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
const ADMIN_USERS_URL = '/api/pleroma/admin/users'
const SUGGESTIONS_URL = '/api/v1/suggestions'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
@ -29,6 +28,7 @@ const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
const MASTODON_DISMISS_NOTIFICATION_URL = id => `/api/v1/notifications/${id}/dismiss`
const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite`
const MASTODON_RETWEET_URL = id => `/api/v1/statuses/${id}/reblog`
@ -74,6 +74,7 @@ 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 MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
@ -323,7 +324,8 @@ const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) => {
const args = [
maxId && `max_id=${maxId}`,
sinceId && `since_id=${sinceId}`,
limit && `limit=${limit}`
limit && `limit=${limit}`,
`with_relationships=true`
].filter(_ => _).join('&')
url = url + (args ? '?' + args : '')
@ -357,7 +359,8 @@ const fetchFollowers = ({ id, maxId, sinceId, limit = 20, credentials }) => {
const args = [
maxId && `max_id=${maxId}`,
sinceId && `since_id=${sinceId}`,
limit && `limit=${limit}`
limit && `limit=${limit}`,
`with_relationships=true`
].filter(_ => _).join('&')
url += args ? '?' + args : ''
@ -495,8 +498,7 @@ const fetchTimeline = ({
until = false,
userId = false,
tag = false,
withMuted = false,
withMove = false
withMuted = false
}) => {
const timelineUrls = {
public: MASTODON_PUBLIC_TIMELINE,
@ -536,12 +538,11 @@ const fetchTimeline = ({
if (timeline === 'public' || timeline === 'publicAndExternal') {
params.push(['only_media', false])
}
if (timeline === 'notifications') {
params.push(['with_move', withMove])
if (timeline !== 'favorites') {
params.push(['with_muted', withMuted])
}
params.push(['count', 20])
params.push(['with_muted', withMuted])
params.push(['limit', 20])
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
url += `?${queryString}`
@ -844,12 +845,16 @@ const suggestions = ({ credentials }) => {
}).then((data) => data.json())
}
const markNotificationsAsSeen = ({ id, credentials }) => {
const markNotificationsAsSeen = ({ id, credentials, single = false }) => {
const body = new FormData()
body.append('latest_id', id)
if (single) {
body.append('id', id)
} else {
body.append('max_id', id)
}
return fetch(QVITTER_USER_NOTIFICATIONS_READ_URL, {
return fetch(NOTIFICATION_READ_URL, {
body,
headers: authHeaders(credentials),
method: 'POST'
@ -880,12 +885,20 @@ const fetchPoll = ({ pollId, credentials }) => {
)
}
const fetchFavoritedByUsers = ({ id }) => {
return promisedRequest({ url: MASTODON_STATUS_FAVORITEDBY_URL(id) }).then((users) => users.map(parseUser))
const fetchFavoritedByUsers = ({ id, credentials }) => {
return promisedRequest({
url: MASTODON_STATUS_FAVORITEDBY_URL(id),
method: 'GET',
credentials
}).then((users) => users.map(parseUser))
}
const fetchRebloggedByUsers = ({ id }) => {
return promisedRequest({ url: MASTODON_STATUS_REBLOGGEDBY_URL(id) }).then((users) => users.map(parseUser))
const fetchRebloggedByUsers = ({ id, credentials }) => {
return promisedRequest({
url: MASTODON_STATUS_REBLOGGEDBY_URL(id),
method: 'GET',
credentials
}).then((users) => users.map(parseUser))
}
const fetchEmojiReactions = ({ id, credentials }) => {
@ -962,6 +975,8 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
params.push(['following', true])
}
params.push(['with_relationships', true])
let queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
url += `?${queryString}`
@ -980,6 +995,10 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
})
}
const fetchKnownDomains = ({ credentials }) => {
return promisedRequest({ url: MASTODON_KNOWN_DOMAIN_LIST_URL, credentials })
}
const fetchDomainMutes = ({ credentials }) => {
return promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials })
}
@ -1002,6 +1021,15 @@ const unmuteDomain = ({ domain, credentials }) => {
})
}
const dismissNotification = ({ credentials, id }) => {
return promisedRequest({
url: MASTODON_DISMISS_NOTIFICATION_URL(id),
method: 'POST',
payload: { id },
credentials
})
}
export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
return Object.entries({
...(credentials
@ -1157,6 +1185,7 @@ const apiService = {
denyUser,
suggestions,
markNotificationsAsSeen,
dismissNotification,
vote,
fetchPoll,
fetchFavoritedByUsers,
@ -1168,6 +1197,7 @@ const apiService = {
updateNotificationSettings,
search2,
searchUsers,
fetchKnownDomains,
fetchDomainMutes,
muteDomain,
unmuteDomain

View file

@ -1,4 +1,5 @@
import escape from 'escape-html'
import { isStatusNotification } from '../notification_utils/notification_utils.js'
const qvitterStatusType = (status) => {
if (status.is_post_verb) {
@ -75,13 +76,7 @@ export const parseUser = (data) => {
output.token = data.pleroma.chat_token
if (relationship) {
output.follows_you = relationship.followed_by
output.requested = relationship.requested
output.following = relationship.following
output.statusnet_blocking = relationship.blocking
output.muted = relationship.muting
output.showing_reblogs = relationship.showing_reblogs
output.subscribed = relationship.subscribing
output.relationship = relationship
}
output.allow_following_move = data.pleroma.allow_following_move
@ -138,16 +133,10 @@ export const parseUser = (data) => {
output.statusnet_profile_url = data.statusnet_profile_url
output.statusnet_blocking = data.statusnet_blocking
output.is_local = data.is_local
output.role = data.role
output.show_role = data.show_role
output.follows_you = data.follows_you
output.muted = data.muted
if (data.rights) {
output.rights = {
moderator: data.rights.delete_others_notice,
@ -161,10 +150,16 @@ export const parseUser = (data) => {
output.hide_follows_count = data.hide_follows_count
output.hide_followers_count = data.hide_followers_count
output.background_image = data.background_image
// on mastoapi this info is contained in a "relationship"
output.following = data.following
// Websocket token
output.token = data.token
// Convert relationsip data to expected format
output.relationship = {
muting: data.muted,
blocking: data.statusnet_blocking,
followed_by: data.follows_you,
following: data.following
}
}
output.created_at = new Date(data.created_at)
@ -216,7 +211,7 @@ export const addEmojis = (string, emojis) => {
const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&')
return acc.replace(
new RegExp(`:${regexSafeShortCode}:`, 'g'),
`<img src='${emoji.url}' alt='${emoji.shortcode}' title='${emoji.shortcode}' class='emoji' />`
`<img src='${emoji.url}' alt=':${emoji.shortcode}:' title=':${emoji.shortcode}:' class='emoji' />`
)
}, string)
}
@ -347,9 +342,7 @@ 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.type === 'move'
? null
: parseStatus(data.status)
output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
output.action = output.status // TODO: Refactor, this is unneeded
output.target = output.type !== 'move'
? null

View file

@ -1,24 +1,27 @@
const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => {
const fetchRelationship = (attempt, userId, store) => new Promise((resolve, reject) => {
setTimeout(() => {
store.state.api.backendInteractor.fetchUser({ id: user.id })
.then((user) => store.commit('addNewUsers', [user]))
.then(() => resolve([user.following, user.requested, user.locked, attempt]))
store.state.api.backendInteractor.fetchUserRelationship({ id: userId })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
return relationship
})
.then((relationship) => resolve([relationship.following, relationship.requested, relationship.locked, attempt]))
.catch((e) => reject(e))
}, 500)
}).then(([following, sent, locked, attempt]) => {
if (!following && !(locked && sent) && attempt <= 3) {
// If we BE reports that we still not following that user - retry,
// increment attempts by one
fetchUser(++attempt, user, store)
fetchRelationship(++attempt, userId, store)
}
})
export const requestFollow = (user, store) => new Promise((resolve, reject) => {
store.state.api.backendInteractor.followUser({ id: user.id })
export const requestFollow = (userId, store) => new Promise((resolve, reject) => {
store.state.api.backendInteractor.followUser({ id: userId })
.then((updated) => {
store.commit('updateUserRelationship', [updated])
if (updated.following || (user.locked && user.requested)) {
if (updated.following || (updated.locked && updated.requested)) {
// If we get result immediately or the account is locked, just stop.
resolve()
return
@ -31,15 +34,15 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => {
// don't know that yet.
// Recursive Promise, it will call itself up to 3 times.
return fetchUser(1, user, store)
return fetchRelationship(1, updated, store)
.then(() => {
resolve()
})
})
})
export const requestUnfollow = (user, store) => new Promise((resolve, reject) => {
store.state.api.backendInteractor.unfollowUser({ id: user.id })
export const requestUnfollow = (userId, store) => new Promise((resolve, reject) => {
store.state.api.backendInteractor.unfollowUser({ id: userId })
.then((updated) => {
store.commit('updateUserRelationship', [updated])
resolve({

View file

@ -1,4 +1,4 @@
import { filter, sortBy } from 'lodash'
import { filter, sortBy, includes } from 'lodash'
export const notificationsFromStore = store => store.state.statuses.notifications.data
@ -7,10 +7,15 @@ export const visibleTypes = store => ([
store.state.config.notificationVisibility.mentions && 'mention',
store.state.config.notificationVisibility.repeats && 'repeat',
store.state.config.notificationVisibility.follows && 'follow',
store.state.config.notificationVisibility.followRequest && 'follow_request',
store.state.config.notificationVisibility.moves && 'move',
store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
].filter(_ => _))
const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction']
export const isStatusNotification = (type) => includes(statusNotifications, type)
const sortById = (a, b) => {
const seqA = Number(a.id)
const seqB = Number(b.id)

View file

@ -11,12 +11,9 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
const rootState = store.rootState || store.state
const timelineData = rootState.statuses.notifications
const hideMutedPosts = getters.mergedConfig.hideMutedPosts
const allowFollowingMove = rootState.users.currentUser.allow_following_move
args['withMuted'] = !hideMutedPosts
args['withMove'] = !allowFollowingMove
args['timeline'] = 'notifications'
if (older) {
if (timelineData.minId !== Number.POSITIVE_INFINITY) {

View file

@ -0,0 +1,32 @@
import Vue from 'vue'
/* By default async components don't have any way to recover, if component is
* failed, it is failed forever. This helper tries to remedy that by recreating
* async component when retry is requested (by user). You need to emit the
* `resetAsyncComponent` event from child to reset the component. Generally,
* this should be done from error component but could be done from loading or
* actual target component itself if needs to be.
*/
function getResettableAsyncComponent (asyncComponent, options) {
const asyncComponentFactory = () => () => ({
component: asyncComponent(),
...options
})
const observe = Vue.observable({ c: asyncComponentFactory() })
return {
functional: true,
render (createElement, { data, children }) {
// emit event resetAsyncComponent to reloading
data.on = {}
data.on.resetAsyncComponent = () => {
observe.c = asyncComponentFactory()
// parent.$forceUpdate()
}
return createElement(observe.c, data, children)
}
}
}
export default getResettableAsyncComponent

View file

@ -1,15 +1,11 @@
import sanitize from 'sanitize-html'
import { filter } from 'lodash'
export const removeAttachmentLinks = (html) => {
return sanitize(html, {
allowedTags: false,
allowedAttributes: false,
exclusiveFilter: ({ tag, attribs }) => tag === 'a' && typeof attribs.class === 'string' && attribs.class.match(/attachment/)
export const muteWordHits = (status, muteWords) => {
const statusText = status.text.toLowerCase()
const statusSummary = status.summary.toLowerCase()
const hits = filter(muteWords, (muteWord) => {
return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase())
})
}
export const parse = (html) => {
return removeAttachmentLinks(html)
return hits
}
export default parse

View file

@ -356,6 +356,12 @@ export const SLOT_INHERITANCE = {
textColor: 'preserve'
},
postGreentext: {
depends: ['cGreen'],
layer: 'bg',
textColor: 'preserve'
},
border: {
depends: ['fg'],
opacity: 'border',

View file

@ -350,16 +350,47 @@ export const getColors = (sourceColors, sourceOpacity) => SLOT_ORDERED.reduce(({
if (!outputColor) {
throw new Error('Couldn\'t generate color for ' + key)
}
const opacitySlot = getOpacitySlot(key)
const opacitySlot = value.opacity || getOpacitySlot(key)
const ownOpacitySlot = value.opacity
if (opacitySlot && (outputColor.a === undefined || ownOpacitySlot)) {
if (ownOpacitySlot === null) {
outputColor.a = 1
} else if (sourceColor === 'transparent') {
outputColor.a = 0
} else {
const opacityOverriden = ownOpacitySlot && sourceOpacity[opacitySlot] !== undefined
const dependencySlot = deps[0]
if (dependencySlot && colors[dependencySlot] === 'transparent') {
outputColor.a = 0
const dependencyColor = dependencySlot && colors[dependencySlot]
if (!ownOpacitySlot && dependencyColor && !value.textColor && ownOpacitySlot !== null) {
// Inheriting color from dependency (weird, i know)
// except if it's a text color or opacity slot is set to 'null'
outputColor.a = dependencyColor.a
} else if (!dependencyColor && !opacitySlot) {
// Remove any alpha channel if no dependency and no opacitySlot found
delete outputColor.a
} else {
outputColor.a = Number(sourceOpacity[opacitySlot]) || OPACITIES[opacitySlot].defaultValue || 1
// Otherwise try to assign opacity
if (dependencyColor && dependencyColor.a === 0) {
// transparent dependency shall make dependents transparent too
outputColor.a = 0
} else {
// Otherwise check if opacity is overriden and use that or default value instead
outputColor.a = Number(
opacityOverriden
? sourceOpacity[opacitySlot]
: (OPACITIES[opacitySlot] || {}).defaultValue
)
}
}
}
if (Number.isNaN(outputColor.a) || outputColor.a === undefined) {
outputColor.a = 1
}
if (opacitySlot) {
return {
colors: { ...colors, [key]: outputColor },