Merge branch 'develop' into features/favicons
This commit is contained in:
commit
8a9654b511
140 changed files with 7916 additions and 4330 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
32
src/services/resettable_async_component.js
Normal file
32
src/services/resettable_async_component.js
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -356,6 +356,12 @@ export const SLOT_INHERITANCE = {
|
|||
textColor: 'preserve'
|
||||
},
|
||||
|
||||
postGreentext: {
|
||||
depends: ['cGreen'],
|
||||
layer: 'bg',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
|
||||
border: {
|
||||
depends: ['fg'],
|
||||
opacity: 'border',
|
||||
|
|
|
@ -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 },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue