Merge branch 'develop' into kPherox/pleroma-fe-iss-149/profile-fields-display
This commit is contained in:
commit
f8cf92a01f
303 changed files with 14875 additions and 9999 deletions
|
@ -1,10 +1,8 @@
|
|||
import { each, map, concat, last } from 'lodash'
|
||||
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'
|
||||
|
@ -12,22 +10,25 @@ const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
|
|||
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
|
||||
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
|
||||
const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
|
||||
const ACTIVATION_STATUS_URL = screenName => `/api/pleroma/admin/users/${screenName}/activation_status`
|
||||
const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate'
|
||||
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'
|
||||
|
||||
const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp'
|
||||
const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp'
|
||||
const MFA_DISABLE_OTP_URL = '/api/pleroma/account/mfa/totp'
|
||||
const MFA_DISABLE_OTP_URL = '/api/pleroma/accounts/mfa/totp'
|
||||
|
||||
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`
|
||||
|
@ -71,6 +72,12 @@ 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 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}`
|
||||
|
||||
const oldfetch = window.fetch
|
||||
|
||||
|
@ -317,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 : '')
|
||||
|
@ -351,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 : ''
|
||||
|
@ -396,8 +405,8 @@ const fetchStatus = ({ id, credentials }) => {
|
|||
.then((data) => parseStatus(data))
|
||||
}
|
||||
|
||||
const tagUser = ({ tag, credentials, ...options }) => {
|
||||
const screenName = options.screen_name
|
||||
const tagUser = ({ tag, credentials, user }) => {
|
||||
const screenName = user.screen_name
|
||||
const form = {
|
||||
nicknames: [screenName],
|
||||
tags: [tag]
|
||||
|
@ -413,8 +422,8 @@ const tagUser = ({ tag, credentials, ...options }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const untagUser = ({ tag, credentials, ...options }) => {
|
||||
const screenName = options.screen_name
|
||||
const untagUser = ({ tag, credentials, user }) => {
|
||||
const screenName = user.screen_name
|
||||
const body = {
|
||||
nicknames: [screenName],
|
||||
tags: [tag]
|
||||
|
@ -430,7 +439,7 @@ const untagUser = ({ tag, credentials, ...options }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const addRight = ({ right, credentials, ...user }) => {
|
||||
const addRight = ({ right, credentials, user }) => {
|
||||
const screenName = user.screen_name
|
||||
|
||||
return fetch(PERMISSION_GROUP_URL(screenName, right), {
|
||||
|
@ -440,7 +449,7 @@ const addRight = ({ right, credentials, ...user }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const deleteRight = ({ right, credentials, ...user }) => {
|
||||
const deleteRight = ({ right, credentials, user }) => {
|
||||
const screenName = user.screen_name
|
||||
|
||||
return fetch(PERMISSION_GROUP_URL(screenName, right), {
|
||||
|
@ -450,23 +459,29 @@ const deleteRight = ({ right, credentials, ...user }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const setActivationStatus = ({ status, credentials, ...user }) => {
|
||||
const screenName = user.screen_name
|
||||
const body = {
|
||||
status: status
|
||||
}
|
||||
|
||||
const headers = authHeaders(credentials)
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
return fetch(ACTIVATION_STATUS_URL(screenName), {
|
||||
method: 'PUT',
|
||||
headers: headers,
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
const activateUser = ({ credentials, user: { screen_name: nickname } }) => {
|
||||
return promisedRequest({
|
||||
url: ACTIVATE_USER_URL,
|
||||
method: 'PATCH',
|
||||
credentials,
|
||||
payload: {
|
||||
nicknames: [nickname]
|
||||
}
|
||||
}).then(response => get(response, 'users.0'))
|
||||
}
|
||||
|
||||
const deleteUser = ({ credentials, ...user }) => {
|
||||
const deactivateUser = ({ credentials, user: { screen_name: nickname } }) => {
|
||||
return promisedRequest({
|
||||
url: DEACTIVATE_USER_URL,
|
||||
method: 'PATCH',
|
||||
credentials,
|
||||
payload: {
|
||||
nicknames: [nickname]
|
||||
}
|
||||
}).then(response => get(response, 'users.0'))
|
||||
}
|
||||
|
||||
const deleteUser = ({ credentials, user }) => {
|
||||
const screenName = user.screen_name
|
||||
const headers = authHeaders(credentials)
|
||||
|
||||
|
@ -523,22 +538,32 @@ const fetchTimeline = ({
|
|||
if (timeline === 'public' || timeline === 'publicAndExternal') {
|
||||
params.push(['only_media', false])
|
||||
}
|
||||
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}`
|
||||
|
||||
let status = ''
|
||||
let statusText = ''
|
||||
return fetch(url, { headers: authHeaders(credentials) })
|
||||
.then((data) => {
|
||||
if (data.ok) {
|
||||
return data
|
||||
}
|
||||
throw new Error('Error fetching timeline', data)
|
||||
status = data.status
|
||||
statusText = data.statusText
|
||||
return data
|
||||
})
|
||||
.then((data) => data.json())
|
||||
.then((data) => data.map(isNotifications ? parseNotification : parseStatus))
|
||||
.then((data) => {
|
||||
if (!data.error) {
|
||||
return data.map(isNotifications ? parseNotification : parseStatus)
|
||||
} else {
|
||||
data.status = status
|
||||
data.statusText = statusText
|
||||
return data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const fetchPinnedStatuses = ({ id, credentials }) => {
|
||||
|
@ -820,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'
|
||||
|
@ -856,12 +885,44 @@ 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 }) => {
|
||||
return promisedRequest({ url: PLEROMA_EMOJI_REACTIONS_URL(id), credentials })
|
||||
.then((reactions) => reactions.map(r => {
|
||||
r.accounts = r.accounts.map(parseUser)
|
||||
return r
|
||||
}))
|
||||
}
|
||||
|
||||
const reactWithEmoji = ({ id, emoji, credentials }) => {
|
||||
return promisedRequest({
|
||||
url: PLEROMA_EMOJI_REACT_URL(id, emoji),
|
||||
method: 'PUT',
|
||||
credentials
|
||||
}).then(parseStatus)
|
||||
}
|
||||
|
||||
const unreactWithEmoji = ({ id, emoji, credentials }) => {
|
||||
return promisedRequest({
|
||||
url: PLEROMA_EMOJI_UNREACT_URL(id, emoji),
|
||||
method: 'DELETE',
|
||||
credentials
|
||||
}).then(parseStatus)
|
||||
}
|
||||
|
||||
const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
|
||||
|
@ -914,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}`
|
||||
|
||||
|
@ -932,6 +995,134 @@ 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 })
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
? { access_token: credentials }
|
||||
: {}
|
||||
),
|
||||
stream,
|
||||
...args
|
||||
}).reduce((acc, [key, val]) => {
|
||||
return acc + `${key}=${val}&`
|
||||
}, MASTODON_STREAMING + '?')
|
||||
}
|
||||
|
||||
const MASTODON_STREAMING_EVENTS = new Set([
|
||||
'update',
|
||||
'notification',
|
||||
'delete',
|
||||
'filters_changed'
|
||||
])
|
||||
|
||||
// A thin wrapper around WebSocket API that allows adding a pre-processor to it
|
||||
// Uses EventTarget and a CustomEvent to proxy events
|
||||
export const ProcessedWS = ({
|
||||
url,
|
||||
preprocessor = handleMastoWS,
|
||||
id = 'Unknown'
|
||||
}) => {
|
||||
const eventTarget = new EventTarget()
|
||||
const socket = new WebSocket(url)
|
||||
if (!socket) throw new Error(`Failed to create socket ${id}`)
|
||||
const proxy = (original, eventName, processor = a => a) => {
|
||||
original.addEventListener(eventName, (eventData) => {
|
||||
eventTarget.dispatchEvent(new CustomEvent(
|
||||
eventName,
|
||||
{ detail: processor(eventData) }
|
||||
))
|
||||
})
|
||||
}
|
||||
socket.addEventListener('open', (wsEvent) => {
|
||||
console.debug(`[WS][${id}] Socket connected`, wsEvent)
|
||||
})
|
||||
socket.addEventListener('error', (wsEvent) => {
|
||||
console.debug(`[WS][${id}] Socket errored`, wsEvent)
|
||||
})
|
||||
socket.addEventListener('close', (wsEvent) => {
|
||||
console.debug(
|
||||
`[WS][${id}] Socket disconnected with code ${wsEvent.code}`,
|
||||
wsEvent
|
||||
)
|
||||
})
|
||||
// Commented code reason: very spammy, uncomment to enable message debug logging
|
||||
/*
|
||||
socket.addEventListener('message', (wsEvent) => {
|
||||
console.debug(
|
||||
`[WS][${id}] Message received`,
|
||||
wsEvent
|
||||
)
|
||||
})
|
||||
/**/
|
||||
|
||||
proxy(socket, 'open')
|
||||
proxy(socket, 'close')
|
||||
proxy(socket, 'message', preprocessor)
|
||||
proxy(socket, 'error')
|
||||
|
||||
// 1000 = Normal Closure
|
||||
eventTarget.close = () => { socket.close(1000, 'Shutting down socket') }
|
||||
|
||||
return eventTarget
|
||||
}
|
||||
|
||||
export const handleMastoWS = (wsEvent) => {
|
||||
const { data } = wsEvent
|
||||
if (!data) return
|
||||
const parsedEvent = JSON.parse(data)
|
||||
const { event, payload } = parsedEvent
|
||||
if (MASTODON_STREAMING_EVENTS.has(event)) {
|
||||
// MastoBE and PleromaBE both send payload for delete as a PLAIN string
|
||||
if (event === 'delete') {
|
||||
return { event, id: payload }
|
||||
}
|
||||
const data = payload ? JSON.parse(payload) : null
|
||||
if (event === 'update') {
|
||||
return { event, status: parseStatus(data) }
|
||||
} else if (event === 'notification') {
|
||||
return { event, notification: parseNotification(data) }
|
||||
}
|
||||
} else {
|
||||
console.warn('Unknown event', wsEvent)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const apiService = {
|
||||
verifyCredentials,
|
||||
fetchTimeline,
|
||||
|
@ -971,7 +1162,8 @@ const apiService = {
|
|||
deleteUser,
|
||||
addRight,
|
||||
deleteRight,
|
||||
setActivationStatus,
|
||||
activateUser,
|
||||
deactivateUser,
|
||||
register,
|
||||
getCaptcha,
|
||||
updateAvatar,
|
||||
|
@ -993,14 +1185,22 @@ const apiService = {
|
|||
denyUser,
|
||||
suggestions,
|
||||
markNotificationsAsSeen,
|
||||
dismissNotification,
|
||||
vote,
|
||||
fetchPoll,
|
||||
fetchFavoritedByUsers,
|
||||
fetchRebloggedByUsers,
|
||||
fetchEmojiReactions,
|
||||
reactWithEmoji,
|
||||
unreactWithEmoji,
|
||||
reportUser,
|
||||
updateNotificationSettings,
|
||||
search2,
|
||||
searchUsers
|
||||
searchUsers,
|
||||
fetchKnownDomains,
|
||||
fetchDomainMutes,
|
||||
muteDomain,
|
||||
unmuteDomain
|
||||
}
|
||||
|
||||
export default apiService
|
||||
|
|
|
@ -1,230 +1,39 @@
|
|||
import apiService from '../api/api.service.js'
|
||||
import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js'
|
||||
import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js'
|
||||
import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
|
||||
import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
|
||||
|
||||
const backendInteractorService = credentials => {
|
||||
const fetchStatus = ({ id }) => {
|
||||
return apiService.fetchStatus({ id, credentials })
|
||||
}
|
||||
|
||||
const fetchConversation = ({ id }) => {
|
||||
return apiService.fetchConversation({ id, credentials })
|
||||
}
|
||||
|
||||
const fetchFriends = ({ id, maxId, sinceId, limit }) => {
|
||||
return apiService.fetchFriends({ id, maxId, sinceId, limit, credentials })
|
||||
}
|
||||
|
||||
const exportFriends = ({ id }) => {
|
||||
return apiService.exportFriends({ id, credentials })
|
||||
}
|
||||
|
||||
const fetchFollowers = ({ id, maxId, sinceId, limit }) => {
|
||||
return apiService.fetchFollowers({ id, maxId, sinceId, limit, credentials })
|
||||
}
|
||||
|
||||
const fetchUser = ({ id }) => {
|
||||
return apiService.fetchUser({ id, credentials })
|
||||
}
|
||||
|
||||
const fetchUserRelationship = ({ id }) => {
|
||||
return apiService.fetchUserRelationship({ id, credentials })
|
||||
}
|
||||
|
||||
const followUser = ({ id, reblogs }) => {
|
||||
return apiService.followUser({ credentials, id, reblogs })
|
||||
}
|
||||
|
||||
const unfollowUser = (id) => {
|
||||
return apiService.unfollowUser({ credentials, id })
|
||||
}
|
||||
|
||||
const blockUser = (id) => {
|
||||
return apiService.blockUser({ credentials, id })
|
||||
}
|
||||
|
||||
const unblockUser = (id) => {
|
||||
return apiService.unblockUser({ credentials, id })
|
||||
}
|
||||
|
||||
const approveUser = (id) => {
|
||||
return apiService.approveUser({ credentials, id })
|
||||
}
|
||||
|
||||
const denyUser = (id) => {
|
||||
return apiService.denyUser({ credentials, id })
|
||||
}
|
||||
|
||||
const startFetchingTimeline = ({ timeline, store, userId = false, tag }) => {
|
||||
const backendInteractorService = credentials => ({
|
||||
startFetchingTimeline ({ timeline, store, userId = false, tag }) {
|
||||
return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag })
|
||||
}
|
||||
},
|
||||
|
||||
const startFetchingNotifications = ({ store }) => {
|
||||
startFetchingNotifications ({ store }) {
|
||||
return notificationsFetcher.startFetching({ store, credentials })
|
||||
}
|
||||
},
|
||||
|
||||
const startFetchingFollowRequest = ({ store }) => {
|
||||
fetchAndUpdateNotifications ({ store }) {
|
||||
return notificationsFetcher.fetchAndUpdate({ store, credentials })
|
||||
},
|
||||
|
||||
startFetchingFollowRequests ({ store }) {
|
||||
return followRequestFetcher.startFetching({ store, credentials })
|
||||
}
|
||||
},
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
const tagUser = ({ screen_name }, tag) => {
|
||||
return apiService.tagUser({ screen_name, tag, credentials })
|
||||
}
|
||||
startUserSocket ({ store }) {
|
||||
const serv = store.rootState.instance.server.replace('http', 'ws')
|
||||
const url = serv + getMastodonSocketURI({ credentials, stream: 'user' })
|
||||
return ProcessedWS({ url, id: 'User' })
|
||||
},
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
const untagUser = ({ screen_name }, tag) => {
|
||||
return apiService.untagUser({ screen_name, tag, credentials })
|
||||
}
|
||||
...Object.entries(apiService).reduce((acc, [key, func]) => {
|
||||
return {
|
||||
...acc,
|
||||
[key]: (args) => func({ credentials, ...args })
|
||||
}
|
||||
}, {}),
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
const addRight = ({ screen_name }, right) => {
|
||||
return apiService.addRight({ screen_name, right, credentials })
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
const deleteRight = ({ screen_name }, right) => {
|
||||
return apiService.deleteRight({ screen_name, right, credentials })
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
const setActivationStatus = ({ screen_name }, status) => {
|
||||
return apiService.setActivationStatus({ screen_name, status, credentials })
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
const deleteUser = ({ screen_name }) => {
|
||||
return apiService.deleteUser({ screen_name, credentials })
|
||||
}
|
||||
|
||||
const vote = (pollId, choices) => {
|
||||
return apiService.vote({ credentials, pollId, choices })
|
||||
}
|
||||
|
||||
const fetchPoll = (pollId) => {
|
||||
return apiService.fetchPoll({ credentials, pollId })
|
||||
}
|
||||
|
||||
const updateNotificationSettings = ({ settings }) => {
|
||||
return apiService.updateNotificationSettings({ credentials, settings })
|
||||
}
|
||||
|
||||
const fetchMutes = () => apiService.fetchMutes({ credentials })
|
||||
const muteUser = (id) => apiService.muteUser({ credentials, id })
|
||||
const unmuteUser = (id) => apiService.unmuteUser({ credentials, id })
|
||||
const subscribeUser = (id) => apiService.subscribeUser({ credentials, id })
|
||||
const unsubscribeUser = (id) => apiService.unsubscribeUser({ credentials, id })
|
||||
const fetchBlocks = () => apiService.fetchBlocks({ credentials })
|
||||
const fetchOAuthTokens = () => apiService.fetchOAuthTokens({ credentials })
|
||||
const revokeOAuthToken = (id) => apiService.revokeOAuthToken({ id, credentials })
|
||||
const fetchPinnedStatuses = (id) => apiService.fetchPinnedStatuses({ credentials, id })
|
||||
const pinOwnStatus = (id) => apiService.pinOwnStatus({ credentials, id })
|
||||
const unpinOwnStatus = (id) => apiService.unpinOwnStatus({ credentials, id })
|
||||
const muteConversation = (id) => apiService.muteConversation({ credentials, id })
|
||||
const unmuteConversation = (id) => apiService.unmuteConversation({ credentials, id })
|
||||
|
||||
const getCaptcha = () => apiService.getCaptcha()
|
||||
const register = (params) => apiService.register({ credentials, params })
|
||||
const updateAvatar = ({ avatar }) => apiService.updateAvatar({ credentials, avatar })
|
||||
const updateBg = ({ background }) => apiService.updateBg({ credentials, background })
|
||||
const updateBanner = ({ banner }) => apiService.updateBanner({ credentials, banner })
|
||||
const updateProfile = ({ params }) => apiService.updateProfile({ credentials, params })
|
||||
|
||||
const importBlocks = (file) => apiService.importBlocks({ file, credentials })
|
||||
const importFollows = (file) => apiService.importFollows({ file, credentials })
|
||||
|
||||
const deleteAccount = ({ password }) => apiService.deleteAccount({ credentials, password })
|
||||
const changeEmail = ({ email, password }) => apiService.changeEmail({ credentials, email, password })
|
||||
const changePassword = ({ password, newPassword, newPasswordConfirmation }) =>
|
||||
apiService.changePassword({ credentials, password, newPassword, newPasswordConfirmation })
|
||||
|
||||
const fetchSettingsMFA = () => apiService.settingsMFA({ credentials })
|
||||
const generateMfaBackupCodes = () => apiService.generateMfaBackupCodes({ credentials })
|
||||
const mfaSetupOTP = () => apiService.mfaSetupOTP({ credentials })
|
||||
const mfaConfirmOTP = ({ password, token }) => apiService.mfaConfirmOTP({ credentials, password, token })
|
||||
const mfaDisableOTP = ({ password }) => apiService.mfaDisableOTP({ credentials, password })
|
||||
|
||||
const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({ id })
|
||||
const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({ id })
|
||||
const reportUser = (params) => apiService.reportUser({ credentials, ...params })
|
||||
|
||||
const favorite = (id) => apiService.favorite({ id, credentials })
|
||||
const unfavorite = (id) => apiService.unfavorite({ id, credentials })
|
||||
const retweet = (id) => apiService.retweet({ id, credentials })
|
||||
const unretweet = (id) => apiService.unretweet({ id, credentials })
|
||||
const search2 = ({ q, resolve, limit, offset, following }) =>
|
||||
apiService.search2({ credentials, q, resolve, limit, offset, following })
|
||||
const searchUsers = (query) => apiService.searchUsers({ query, credentials })
|
||||
|
||||
const backendInteractorServiceInstance = {
|
||||
fetchStatus,
|
||||
fetchConversation,
|
||||
fetchFriends,
|
||||
exportFriends,
|
||||
fetchFollowers,
|
||||
followUser,
|
||||
unfollowUser,
|
||||
blockUser,
|
||||
unblockUser,
|
||||
fetchUser,
|
||||
fetchUserRelationship,
|
||||
verifyCredentials: apiService.verifyCredentials,
|
||||
startFetchingTimeline,
|
||||
startFetchingNotifications,
|
||||
startFetchingFollowRequest,
|
||||
fetchMutes,
|
||||
muteUser,
|
||||
unmuteUser,
|
||||
subscribeUser,
|
||||
unsubscribeUser,
|
||||
fetchBlocks,
|
||||
fetchOAuthTokens,
|
||||
revokeOAuthToken,
|
||||
fetchPinnedStatuses,
|
||||
pinOwnStatus,
|
||||
unpinOwnStatus,
|
||||
muteConversation,
|
||||
unmuteConversation,
|
||||
tagUser,
|
||||
untagUser,
|
||||
addRight,
|
||||
deleteRight,
|
||||
deleteUser,
|
||||
setActivationStatus,
|
||||
register,
|
||||
getCaptcha,
|
||||
updateAvatar,
|
||||
updateBg,
|
||||
updateBanner,
|
||||
updateProfile,
|
||||
importBlocks,
|
||||
importFollows,
|
||||
deleteAccount,
|
||||
changeEmail,
|
||||
changePassword,
|
||||
fetchSettingsMFA,
|
||||
generateMfaBackupCodes,
|
||||
mfaSetupOTP,
|
||||
mfaConfirmOTP,
|
||||
mfaDisableOTP,
|
||||
approveUser,
|
||||
denyUser,
|
||||
vote,
|
||||
fetchPoll,
|
||||
fetchFavoritedByUsers,
|
||||
fetchRebloggedByUsers,
|
||||
reportUser,
|
||||
favorite,
|
||||
unfavorite,
|
||||
retweet,
|
||||
unretweet,
|
||||
updateNotificationSettings,
|
||||
search2,
|
||||
searchUsers
|
||||
}
|
||||
|
||||
return backendInteractorServiceInstance
|
||||
}
|
||||
verifyCredentials: apiService.verifyCredentials
|
||||
})
|
||||
|
||||
export default backendInteractorService
|
||||
|
|
|
@ -1,16 +1,27 @@
|
|||
import { map } from 'lodash'
|
||||
import { invertLightness, contrastRatio } from 'chromatism'
|
||||
|
||||
const rgb2hex = (r, g, b) => {
|
||||
// useful for visualizing color when debugging
|
||||
export const consoleColor = (color) => console.log('%c##########', 'background: ' + color + '; color: ' + color)
|
||||
|
||||
/**
|
||||
* Convert r, g, b values into hex notation. All components are [0-255]
|
||||
*
|
||||
* @param {Number|String|Object} r - Either red component, {r,g,b} object, or hex string
|
||||
* @param {Number} [g] - Green component
|
||||
* @param {Number} [b] - Blue component
|
||||
*/
|
||||
export const rgb2hex = (r, g, b) => {
|
||||
if (r === null || typeof r === 'undefined') {
|
||||
return undefined
|
||||
}
|
||||
if (r[0] === '#') {
|
||||
// TODO: clean up this mess
|
||||
if (r[0] === '#' || r === 'transparent') {
|
||||
return r
|
||||
}
|
||||
if (typeof r === 'object') {
|
||||
({ r, g, b } = r)
|
||||
}
|
||||
[r, g, b] = map([r, g, b], (val) => {
|
||||
[r, g, b] = [r, g, b].map(val => {
|
||||
val = Math.ceil(val)
|
||||
val = val < 0 ? 0 : val
|
||||
val = val > 255 ? 255 : val
|
||||
|
@ -58,7 +69,7 @@ const srgbToLinear = (srgb) => {
|
|||
* @param {Object} srgb - sRGB color
|
||||
* @returns {Number} relative luminance
|
||||
*/
|
||||
const relativeLuminance = (srgb) => {
|
||||
export const relativeLuminance = (srgb) => {
|
||||
const { r, g, b } = srgbToLinear(srgb)
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
}
|
||||
|
@ -71,7 +82,7 @@ const relativeLuminance = (srgb) => {
|
|||
* @param {Object} b - sRGB color
|
||||
* @returns {Number} color ratio
|
||||
*/
|
||||
const getContrastRatio = (a, b) => {
|
||||
export const getContrastRatio = (a, b) => {
|
||||
const la = relativeLuminance(a)
|
||||
const lb = relativeLuminance(b)
|
||||
const [l1, l2] = la > lb ? [la, lb] : [lb, la]
|
||||
|
@ -79,6 +90,17 @@ const getContrastRatio = (a, b) => {
|
|||
return (l1 + 0.05) / (l2 + 0.05)
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as `getContrastRatio` but for multiple layers in-between
|
||||
*
|
||||
* @param {Object} text - text color (topmost layer)
|
||||
* @param {[Object, Number]} layers[] - layers between text and bedrock
|
||||
* @param {Object} bedrock - layer at the very bottom
|
||||
*/
|
||||
export const getContrastRatioLayers = (text, layers, bedrock) => {
|
||||
return getContrastRatio(alphaBlendLayers(bedrock, layers), text)
|
||||
}
|
||||
|
||||
/**
|
||||
* This performs alpha blending between solid background and semi-transparent foreground
|
||||
*
|
||||
|
@ -87,7 +109,7 @@ const getContrastRatio = (a, b) => {
|
|||
* @param {Object} bg - bottom layer color
|
||||
* @returns {Object} sRGB of resulting color
|
||||
*/
|
||||
const alphaBlend = (fg, fga, bg) => {
|
||||
export const alphaBlend = (fg, fga, bg) => {
|
||||
if (fga === 1 || typeof fga === 'undefined') return fg
|
||||
return 'rgb'.split('').reduce((acc, c) => {
|
||||
// Simplified https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
|
||||
|
@ -97,14 +119,30 @@ const alphaBlend = (fg, fga, bg) => {
|
|||
}, {})
|
||||
}
|
||||
|
||||
const invert = (rgb) => {
|
||||
/**
|
||||
* Same as `alphaBlend` but for multiple layers in-between
|
||||
*
|
||||
* @param {Object} bedrock - layer at the very bottom
|
||||
* @param {[Object, Number]} layers[] - layers between text and bedrock
|
||||
*/
|
||||
export const alphaBlendLayers = (bedrock, layers) => layers.reduce((acc, [color, opacity]) => {
|
||||
return alphaBlend(color, opacity, acc)
|
||||
}, bedrock)
|
||||
|
||||
export const invert = (rgb) => {
|
||||
return 'rgb'.split('').reduce((acc, c) => {
|
||||
acc[c] = 255 - rgb[c]
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const hex2rgb = (hex) => {
|
||||
/**
|
||||
* Converts #rrggbb hex notation into an {r, g, b} object
|
||||
*
|
||||
* @param {String} hex - #rrggbb string
|
||||
* @returns {Object} rgb representation of the color, values are 0-255
|
||||
*/
|
||||
export const hex2rgb = (hex) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
|
@ -113,18 +151,72 @@ const hex2rgb = (hex) => {
|
|||
} : null
|
||||
}
|
||||
|
||||
const mixrgb = (a, b) => {
|
||||
return Object.keys(a).reduce((acc, k) => {
|
||||
/**
|
||||
* Old somewhat weird function for mixing two colors together
|
||||
*
|
||||
* @param {Object} a - one color (rgb)
|
||||
* @param {Object} b - other color (rgb)
|
||||
* @returns {Object} result
|
||||
*/
|
||||
export const mixrgb = (a, b) => {
|
||||
return 'rgb'.split('').reduce((acc, k) => {
|
||||
acc[k] = (a[k] + b[k]) / 2
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
export {
|
||||
rgb2hex,
|
||||
hex2rgb,
|
||||
mixrgb,
|
||||
invert,
|
||||
getContrastRatio,
|
||||
alphaBlend
|
||||
/**
|
||||
* Converts rgb object into a CSS rgba() color
|
||||
*
|
||||
* @param {Object} color - rgb
|
||||
* @returns {String} CSS rgba() color
|
||||
*/
|
||||
export const rgba2css = function (rgba) {
|
||||
return `rgba(${Math.floor(rgba.r)}, ${Math.floor(rgba.g)}, ${Math.floor(rgba.b)}, ${rgba.a})`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text color for given background color and intended text color
|
||||
* This checks if text and background don't have enough color and inverts
|
||||
* text color's lightness if needed. If text color is still not enough it
|
||||
* will fall back to black or white
|
||||
*
|
||||
* @param {Object} bg - background color
|
||||
* @param {Object} text - intended text color
|
||||
* @param {Boolean} preserve - try to preserve intended text color's hue/saturation (i.e. no BW)
|
||||
*/
|
||||
export const getTextColor = function (bg, text, preserve) {
|
||||
const contrast = getContrastRatio(bg, text)
|
||||
|
||||
if (contrast < 4.5) {
|
||||
const base = typeof text.a !== 'undefined' ? { a: text.a } : {}
|
||||
const result = Object.assign(base, invertLightness(text).rgb)
|
||||
if (!preserve && getContrastRatio(bg, result) < 4.5) {
|
||||
// B&W
|
||||
return contrastRatio(bg, text).rgb
|
||||
}
|
||||
// Inverted color
|
||||
return result
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts color to CSS Color value
|
||||
*
|
||||
* @param {Object|String} input - color
|
||||
* @param {Number} [a] - alpha value
|
||||
* @returns {String} a CSS Color value
|
||||
*/
|
||||
export const getCssColor = (input, a) => {
|
||||
let rgb = {}
|
||||
if (typeof input === 'object') {
|
||||
rgb = input
|
||||
} else if (typeof input === 'string') {
|
||||
if (input.startsWith('#')) {
|
||||
rgb = hex2rgb(input)
|
||||
} else {
|
||||
return input
|
||||
}
|
||||
}
|
||||
return rgba2css({ ...rgb, a })
|
||||
}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import escape from 'escape-html'
|
||||
import { isStatusNotification } from '../notification_utils/notification_utils.js'
|
||||
|
||||
const qvitterStatusType = (status) => {
|
||||
if (status.is_post_verb) {
|
||||
return 'status'
|
||||
|
@ -41,7 +44,7 @@ export const parseUser = (data) => {
|
|||
}
|
||||
|
||||
output.name = data.display_name
|
||||
output.name_html = addEmojis(data.display_name, data.emojis)
|
||||
output.name_html = addEmojis(escape(data.display_name), data.emojis)
|
||||
|
||||
output.description = data.note
|
||||
output.description_html = addEmojis(data.note, data.emojis)
|
||||
|
@ -78,15 +81,11 @@ 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
|
||||
|
||||
output.hide_follows = data.pleroma.hide_follows
|
||||
output.hide_followers = data.pleroma.hide_followers
|
||||
output.hide_follows_count = data.pleroma.hide_follows_count
|
||||
|
@ -139,16 +138,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,
|
||||
|
@ -162,10 +155,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)
|
||||
|
@ -217,7 +216,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)
|
||||
}
|
||||
|
@ -248,6 +247,7 @@ export const parseStatus = (data) => {
|
|||
output.is_local = pleroma.local
|
||||
output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
|
||||
output.thread_muted = pleroma.thread_muted
|
||||
output.emoji_reactions = pleroma.emoji_reactions
|
||||
} else {
|
||||
output.text = data.content
|
||||
output.summary = data.spoiler_text
|
||||
|
@ -261,7 +261,7 @@ export const parseStatus = (data) => {
|
|||
output.retweeted_status = parseStatus(data.reblog)
|
||||
}
|
||||
|
||||
output.summary_html = addEmojis(data.spoiler_text, data.emojis)
|
||||
output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis)
|
||||
output.external_url = data.url
|
||||
output.poll = data.poll
|
||||
output.pinned = data.pinned
|
||||
|
@ -347,11 +347,13 @@ export const parseNotification = (data) => {
|
|||
if (masto) {
|
||||
output.type = mastoDict[data.type] || data.type
|
||||
output.seen = data.pleroma.is_seen
|
||||
output.status = output.type === 'follow'
|
||||
? 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
|
||||
: parseUser(data.target)
|
||||
output.from_profile = parseUser(data.account)
|
||||
output.emoji = data.emoji
|
||||
} else {
|
||||
const parsedNotice = parseStatus(data.notice)
|
||||
output.type = data.ntype
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(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,9 +1,9 @@
|
|||
const verifyOTPCode = ({ app, instance, mfaToken, code }) => {
|
||||
const verifyOTPCode = ({ clientId, clientSecret, instance, mfaToken, code }) => {
|
||||
const url = `${instance}/oauth/mfa/challenge`
|
||||
const form = new window.FormData()
|
||||
|
||||
form.append('client_id', app.client_id)
|
||||
form.append('client_secret', app.client_secret)
|
||||
form.append('client_id', clientId)
|
||||
form.append('client_secret', clientSecret)
|
||||
form.append('mfa_token', mfaToken)
|
||||
form.append('code', code)
|
||||
form.append('challenge_type', 'totp')
|
||||
|
@ -14,12 +14,12 @@ const verifyOTPCode = ({ app, instance, mfaToken, code }) => {
|
|||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const verifyRecoveryCode = ({ app, instance, mfaToken, code }) => {
|
||||
const verifyRecoveryCode = ({ clientId, clientSecret, instance, mfaToken, code }) => {
|
||||
const url = `${instance}/oauth/mfa/challenge`
|
||||
const form = new window.FormData()
|
||||
|
||||
form.append('client_id', app.client_id)
|
||||
form.append('client_secret', app.client_secret)
|
||||
form.append('client_id', clientId)
|
||||
form.append('client_secret', clientSecret)
|
||||
form.append('mfa_token', mfaToken)
|
||||
form.append('code', code)
|
||||
form.append('challenge_type', 'recovery')
|
||||
|
|
|
@ -12,7 +12,7 @@ export const getOrCreateApp = ({ clientId, clientSecret, instance, commit }) =>
|
|||
|
||||
form.append('client_name', `PleromaFE_${window.___pleromafe_commit_hash}_${(new Date()).toISOString()}`)
|
||||
form.append('redirect_uris', REDIRECT_URI)
|
||||
form.append('scopes', 'read write follow')
|
||||
form.append('scopes', 'read write follow push admin')
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
|
@ -28,7 +28,7 @@ const login = ({ instance, clientId }) => {
|
|||
response_type: 'code',
|
||||
client_id: clientId,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
scope: 'read write follow'
|
||||
scope: 'read write follow push admin'
|
||||
}
|
||||
|
||||
const dataString = reduce(data, (acc, v, k) => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { filter, sortBy } from 'lodash'
|
||||
import { filter, sortBy, includes } from 'lodash'
|
||||
|
||||
export const notificationsFromStore = store => store.state.statuses.notifications.data
|
||||
|
||||
|
@ -6,9 +6,16 @@ export const visibleTypes = store => ([
|
|||
store.state.config.notificationVisibility.likes && 'like',
|
||||
store.state.config.notificationVisibility.mentions && 'mention',
|
||||
store.state.config.notificationVisibility.repeats && 'repeat',
|
||||
store.state.config.notificationVisibility.follows && 'follow'
|
||||
store.state.config.notificationVisibility.follows && 'follow',
|
||||
store.state.config.notificationVisibility.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)
|
||||
|
@ -25,7 +32,7 @@ const sortById = (a, b) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const visibleNotificationsFromStore = (store, types) => {
|
||||
export const filteredNotificationsFromStore = (store, types) => {
|
||||
// map is just to clone the array since sort mutates it and it causes some issues
|
||||
let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)
|
||||
sortedNotifications = sortBy(sortedNotifications, 'seen')
|
||||
|
@ -35,4 +42,4 @@ export const visibleNotificationsFromStore = (store, types) => {
|
|||
}
|
||||
|
||||
export const unseenNotificationsFromStore = store =>
|
||||
filter(visibleNotificationsFromStore(store), ({ seen }) => !seen)
|
||||
filter(filteredNotificationsFromStore(store), ({ seen }) => !seen)
|
||||
|
|
|
@ -2,7 +2,6 @@ import apiService from '../api/api.service.js'
|
|||
|
||||
const update = ({ store, notifications, older }) => {
|
||||
store.dispatch('setNotificationsError', { value: false })
|
||||
|
||||
store.dispatch('addNewNotifications', { notifications, older })
|
||||
}
|
||||
|
||||
|
@ -30,9 +29,9 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
|
|||
|
||||
// load unread notifications repeatedly to provide consistency between browser tabs
|
||||
const notifications = timelineData.data
|
||||
const unread = notifications.filter(n => !n.seen).map(n => n.id)
|
||||
if (unread.length) {
|
||||
args['since'] = Math.min(...unread)
|
||||
const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
|
||||
if (readNotifsIds.length) {
|
||||
args['since'] = Math.max(...readNotifsIds)
|
||||
fetchNotifications({ store, args, older })
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,8 @@ function sendSubscriptionToBackEnd (subscription, token, notificationVisibility)
|
|||
follow: notificationVisibility.follows,
|
||||
favourite: notificationVisibility.likes,
|
||||
mention: notificationVisibility.mentions,
|
||||
reblog: notificationVisibility.repeats
|
||||
reblog: notificationVisibility.repeats,
|
||||
move: notificationVisibility.moves
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
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
|
||||
|
|
|
@ -1,78 +1,9 @@
|
|||
import { times } from 'lodash'
|
||||
import { brightness, invertLightness, convert, contrastRatio } from 'chromatism'
|
||||
import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend } from '../color_convert/color_convert.js'
|
||||
import { convert } from 'chromatism'
|
||||
import { rgb2hex, hex2rgb, rgba2css, getCssColor, relativeLuminance } from '../color_convert/color_convert.js'
|
||||
import { getColors, computeDynamicColor, getOpacitySlot } from '../theme_data/theme_data.service.js'
|
||||
|
||||
// While this is not used anymore right now, I left it in if we want to do custom
|
||||
// styles that aren't just colors, so user can pick from a few different distinct
|
||||
// styles as well as set their own colors in the future.
|
||||
|
||||
const setStyle = (href, commit) => {
|
||||
/***
|
||||
What's going on here?
|
||||
I want to make it easy for admins to style this application. To have
|
||||
a good set of default themes, I chose the system from base16
|
||||
(https://chriskempson.github.io/base16/) to style all elements. They
|
||||
all have the base00..0F classes. So the only thing an admin needs to
|
||||
do to style Pleroma is to change these colors in that one css file.
|
||||
Some default things (body text color, link color) need to be set dy-
|
||||
namically, so this is done here by waiting for the stylesheet to be
|
||||
loaded and then creating an element with the respective classes.
|
||||
|
||||
It is a bit weird, but should make life for admins somewhat easier.
|
||||
***/
|
||||
const head = document.head
|
||||
const body = document.body
|
||||
body.classList.add('hidden')
|
||||
const cssEl = document.createElement('link')
|
||||
cssEl.setAttribute('rel', 'stylesheet')
|
||||
cssEl.setAttribute('href', href)
|
||||
head.appendChild(cssEl)
|
||||
|
||||
const setDynamic = () => {
|
||||
const baseEl = document.createElement('div')
|
||||
body.appendChild(baseEl)
|
||||
|
||||
let colors = {}
|
||||
times(16, (n) => {
|
||||
const name = `base0${n.toString(16).toUpperCase()}`
|
||||
baseEl.setAttribute('class', name)
|
||||
const color = window.getComputedStyle(baseEl).getPropertyValue('color')
|
||||
colors[name] = color
|
||||
})
|
||||
|
||||
body.removeChild(baseEl)
|
||||
|
||||
const styleEl = document.createElement('style')
|
||||
head.appendChild(styleEl)
|
||||
// const styleSheet = styleEl.sheet
|
||||
|
||||
body.classList.remove('hidden')
|
||||
}
|
||||
|
||||
cssEl.addEventListener('load', setDynamic)
|
||||
}
|
||||
|
||||
const rgb2rgba = function (rgba) {
|
||||
return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`
|
||||
}
|
||||
|
||||
const getTextColor = function (bg, text, preserve) {
|
||||
const bgIsLight = convert(bg).hsl.l > 50
|
||||
const textIsLight = convert(text).hsl.l > 50
|
||||
|
||||
if ((bgIsLight && textIsLight) || (!bgIsLight && !textIsLight)) {
|
||||
const base = typeof text.a !== 'undefined' ? { a: text.a } : {}
|
||||
const result = Object.assign(base, invertLightness(text).rgb)
|
||||
if (!preserve && getContrastRatio(bg, result) < 4.5) {
|
||||
return contrastRatio(bg, text).rgb
|
||||
}
|
||||
return result
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
const applyTheme = (input, commit) => {
|
||||
const { rules, theme } = generatePreset(input)
|
||||
export const applyTheme = (input) => {
|
||||
const { rules } = generatePreset(input)
|
||||
const head = document.head
|
||||
const body = document.body
|
||||
body.classList.add('hidden')
|
||||
|
@ -87,14 +18,9 @@ const applyTheme = (input, commit) => {
|
|||
styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max')
|
||||
styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max')
|
||||
body.classList.remove('hidden')
|
||||
|
||||
// commit('setOption', { name: 'colors', value: htmlColors })
|
||||
// commit('setOption', { name: 'radii', value: radii })
|
||||
commit('setOption', { name: 'customTheme', value: input })
|
||||
commit('setOption', { name: 'colors', value: theme.colors })
|
||||
}
|
||||
|
||||
const getCssShadow = (input, usesDropShadow) => {
|
||||
export const getCssShadow = (input, usesDropShadow) => {
|
||||
if (input.length === 0) {
|
||||
return 'none'
|
||||
}
|
||||
|
@ -132,122 +58,18 @@ const getCssShadowFilter = (input) => {
|
|||
.join(' ')
|
||||
}
|
||||
|
||||
const getCssColor = (input, a) => {
|
||||
let rgb = {}
|
||||
if (typeof input === 'object') {
|
||||
rgb = input
|
||||
} else if (typeof input === 'string') {
|
||||
if (input.startsWith('#')) {
|
||||
rgb = hex2rgb(input)
|
||||
} else if (input.startsWith('--')) {
|
||||
return `var(${input})`
|
||||
} else {
|
||||
return input
|
||||
}
|
||||
}
|
||||
return rgb2rgba({ ...rgb, a })
|
||||
}
|
||||
export const generateColors = (themeData) => {
|
||||
const sourceColors = !themeData.themeEngineVersion
|
||||
? colors2to3(themeData.colors || themeData)
|
||||
: themeData.colors || themeData
|
||||
|
||||
const generateColors = (input) => {
|
||||
const colors = {}
|
||||
const opacity = Object.assign({
|
||||
alert: 0.5,
|
||||
input: 0.5,
|
||||
faint: 0.5
|
||||
}, Object.entries(input.opacity || {}).reduce((acc, [k, v]) => {
|
||||
if (typeof v !== 'undefined') {
|
||||
acc[k] = v
|
||||
}
|
||||
return acc
|
||||
}, {}))
|
||||
const col = Object.entries(input.colors || input).reduce((acc, [k, v]) => {
|
||||
if (typeof v === 'object') {
|
||||
acc[k] = v
|
||||
} else {
|
||||
acc[k] = hex2rgb(v)
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const isLightOnDark = convert(col.bg).hsl.l < convert(col.text).hsl.l
|
||||
const mod = isLightOnDark ? 1 : -1
|
||||
|
||||
colors.text = col.text
|
||||
colors.lightText = brightness(20 * mod, colors.text).rgb
|
||||
colors.link = col.link
|
||||
colors.faint = col.faint || Object.assign({}, col.text)
|
||||
|
||||
colors.bg = col.bg
|
||||
colors.lightBg = col.lightBg || brightness(5, colors.bg).rgb
|
||||
|
||||
colors.fg = col.fg
|
||||
colors.fgText = col.fgText || getTextColor(colors.fg, colors.text)
|
||||
colors.fgLink = col.fgLink || getTextColor(colors.fg, colors.link, true)
|
||||
|
||||
colors.border = col.border || brightness(2 * mod, colors.fg).rgb
|
||||
|
||||
colors.btn = col.btn || Object.assign({}, col.fg)
|
||||
colors.btnText = col.btnText || getTextColor(colors.btn, colors.fgText)
|
||||
|
||||
colors.input = col.input || Object.assign({}, col.fg)
|
||||
colors.inputText = col.inputText || getTextColor(colors.input, colors.lightText)
|
||||
|
||||
colors.panel = col.panel || Object.assign({}, col.fg)
|
||||
colors.panelText = col.panelText || getTextColor(colors.panel, colors.fgText)
|
||||
colors.panelLink = col.panelLink || getTextColor(colors.panel, colors.fgLink)
|
||||
colors.panelFaint = col.panelFaint || getTextColor(colors.panel, colors.faint)
|
||||
|
||||
colors.topBar = col.topBar || Object.assign({}, col.fg)
|
||||
colors.topBarText = col.topBarText || getTextColor(colors.topBar, colors.fgText)
|
||||
colors.topBarLink = col.topBarLink || getTextColor(colors.topBar, colors.fgLink)
|
||||
|
||||
colors.faintLink = col.faintLink || Object.assign({}, col.link)
|
||||
colors.linkBg = alphaBlend(colors.link, 0.4, colors.bg)
|
||||
|
||||
colors.icon = mixrgb(colors.bg, colors.text)
|
||||
|
||||
colors.cBlue = col.cBlue || hex2rgb('#0000FF')
|
||||
colors.cRed = col.cRed || hex2rgb('#FF0000')
|
||||
colors.cGreen = col.cGreen || hex2rgb('#00FF00')
|
||||
colors.cOrange = col.cOrange || hex2rgb('#E3FF00')
|
||||
|
||||
colors.alertError = col.alertError || Object.assign({}, colors.cRed)
|
||||
colors.alertErrorText = getTextColor(alphaBlend(colors.alertError, opacity.alert, colors.bg), colors.text)
|
||||
colors.alertErrorPanelText = getTextColor(alphaBlend(colors.alertError, opacity.alert, colors.panel), colors.panelText)
|
||||
|
||||
colors.alertWarning = col.alertWarning || Object.assign({}, colors.cOrange)
|
||||
colors.alertWarningText = getTextColor(alphaBlend(colors.alertWarning, opacity.alert, colors.bg), colors.text)
|
||||
colors.alertWarningPanelText = getTextColor(alphaBlend(colors.alertWarning, opacity.alert, colors.panel), colors.panelText)
|
||||
|
||||
colors.badgeNotification = col.badgeNotification || Object.assign({}, colors.cRed)
|
||||
colors.badgeNotificationText = contrastRatio(colors.badgeNotification).rgb
|
||||
|
||||
Object.entries(opacity).forEach(([ k, v ]) => {
|
||||
if (typeof v === 'undefined') return
|
||||
if (k === 'alert') {
|
||||
colors.alertError.a = v
|
||||
colors.alertWarning.a = v
|
||||
return
|
||||
}
|
||||
if (k === 'faint') {
|
||||
colors[k + 'Link'].a = v
|
||||
colors['panelFaint'].a = v
|
||||
}
|
||||
if (k === 'bg') {
|
||||
colors['lightBg'].a = v
|
||||
}
|
||||
if (colors[k]) {
|
||||
colors[k].a = v
|
||||
} else {
|
||||
console.error('Wrong key ' + k)
|
||||
}
|
||||
})
|
||||
const { colors, opacity } = getColors(sourceColors, themeData.opacity || {})
|
||||
|
||||
const htmlColors = Object.entries(colors)
|
||||
.reduce((acc, [k, v]) => {
|
||||
if (!v) return acc
|
||||
acc.solid[k] = rgb2hex(v)
|
||||
acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgb2rgba(v)
|
||||
acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgba2css(v)
|
||||
return acc
|
||||
}, { complete: {}, solid: {} })
|
||||
return {
|
||||
|
@ -264,7 +86,7 @@ const generateColors = (input) => {
|
|||
}
|
||||
}
|
||||
|
||||
const generateRadii = (input) => {
|
||||
export const generateRadii = (input) => {
|
||||
let inputRadii = input.radii || {}
|
||||
// v1 -> v2
|
||||
if (typeof input.btnRadius !== 'undefined') {
|
||||
|
@ -297,7 +119,7 @@ const generateRadii = (input) => {
|
|||
}
|
||||
}
|
||||
|
||||
const generateFonts = (input) => {
|
||||
export const generateFonts = (input) => {
|
||||
const fonts = Object.entries(input.fonts || {}).filter(([k, v]) => v).reduce((acc, [k, v]) => {
|
||||
acc[k] = Object.entries(v).filter(([k, v]) => v).reduce((acc, [k, v]) => {
|
||||
acc[k] = v
|
||||
|
@ -332,89 +154,123 @@ const generateFonts = (input) => {
|
|||
}
|
||||
}
|
||||
|
||||
const generateShadows = (input) => {
|
||||
const border = (top, shadow) => ({
|
||||
x: 0,
|
||||
y: top ? 1 : -1,
|
||||
blur: 0,
|
||||
const border = (top, shadow) => ({
|
||||
x: 0,
|
||||
y: top ? 1 : -1,
|
||||
blur: 0,
|
||||
spread: 0,
|
||||
color: shadow ? '#000000' : '#FFFFFF',
|
||||
alpha: 0.2,
|
||||
inset: true
|
||||
})
|
||||
const buttonInsetFakeBorders = [border(true, false), border(false, true)]
|
||||
const inputInsetFakeBorders = [border(true, true), border(false, false)]
|
||||
const hoverGlow = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '--faint',
|
||||
alpha: 1
|
||||
}
|
||||
|
||||
export const DEFAULT_SHADOWS = {
|
||||
panel: [{
|
||||
x: 1,
|
||||
y: 1,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: shadow ? '#000000' : '#FFFFFF',
|
||||
alpha: 0.2,
|
||||
inset: true
|
||||
})
|
||||
const buttonInsetFakeBorders = [border(true, false), border(false, true)]
|
||||
const inputInsetFakeBorders = [border(true, true), border(false, false)]
|
||||
const hoverGlow = {
|
||||
color: '#000000',
|
||||
alpha: 0.6
|
||||
}],
|
||||
topBar: [{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '--faint',
|
||||
color: '#000000',
|
||||
alpha: 0.6
|
||||
}],
|
||||
popup: [{
|
||||
x: 2,
|
||||
y: 2,
|
||||
blur: 3,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.5
|
||||
}],
|
||||
avatar: [{
|
||||
x: 0,
|
||||
y: 1,
|
||||
blur: 8,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.7
|
||||
}],
|
||||
avatarStatus: [],
|
||||
panelHeader: [],
|
||||
button: [{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 2,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 1
|
||||
}, ...buttonInsetFakeBorders],
|
||||
buttonHover: [hoverGlow, ...buttonInsetFakeBorders],
|
||||
buttonPressed: [hoverGlow, ...inputInsetFakeBorders],
|
||||
input: [...inputInsetFakeBorders, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 2,
|
||||
inset: true,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 1
|
||||
}]
|
||||
}
|
||||
export const generateShadows = (input, colors) => {
|
||||
// TODO this is a small hack for `mod` to work with shadows
|
||||
// this is used to get the "context" of shadow, i.e. for `mod` properly depend on background color of element
|
||||
const hackContextDict = {
|
||||
button: 'btn',
|
||||
panel: 'bg',
|
||||
top: 'topBar',
|
||||
popup: 'popover',
|
||||
avatar: 'bg',
|
||||
panelHeader: 'panel',
|
||||
input: 'input'
|
||||
}
|
||||
|
||||
const shadows = {
|
||||
panel: [{
|
||||
x: 1,
|
||||
y: 1,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.6
|
||||
}],
|
||||
topBar: [{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.6
|
||||
}],
|
||||
popup: [{
|
||||
x: 2,
|
||||
y: 2,
|
||||
blur: 3,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.5
|
||||
}],
|
||||
avatar: [{
|
||||
x: 0,
|
||||
y: 1,
|
||||
blur: 8,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.7
|
||||
}],
|
||||
avatarStatus: [],
|
||||
panelHeader: [],
|
||||
button: [{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 2,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 1
|
||||
}, ...buttonInsetFakeBorders],
|
||||
buttonHover: [hoverGlow, ...buttonInsetFakeBorders],
|
||||
buttonPressed: [hoverGlow, ...inputInsetFakeBorders],
|
||||
input: [...inputInsetFakeBorders, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 2,
|
||||
inset: true,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 1
|
||||
}],
|
||||
...(input.shadows || {})
|
||||
}
|
||||
const inputShadows = input.shadows && !input.themeEngineVersion
|
||||
? shadows2to3(input.shadows, input.opacity)
|
||||
: input.shadows || {}
|
||||
const shadows = Object.entries({
|
||||
...DEFAULT_SHADOWS,
|
||||
...inputShadows
|
||||
}).reduce((shadowsAcc, [slotName, shadowDefs]) => {
|
||||
const slotFirstWord = slotName.replace(/[A-Z].*$/, '')
|
||||
const colorSlotName = hackContextDict[slotFirstWord]
|
||||
const isLightOnDark = relativeLuminance(convert(colors[colorSlotName]).rgb) < 0.5
|
||||
const mod = isLightOnDark ? 1 : -1
|
||||
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
|
||||
...shadowAcc,
|
||||
{
|
||||
...def,
|
||||
color: rgb2hex(computeDynamicColor(
|
||||
def.color,
|
||||
(variableSlot) => convert(colors[variableSlot]).rgb,
|
||||
mod
|
||||
))
|
||||
}
|
||||
], [])
|
||||
return { ...shadowsAcc, [slotName]: newShadow }
|
||||
}, {})
|
||||
|
||||
return {
|
||||
rules: {
|
||||
shadows: Object
|
||||
.entries(shadows)
|
||||
// TODO for v2.1: if shadow doesn't have non-inset shadows with spread > 0 - optionally
|
||||
// TODO for v2.2: if shadow doesn't have non-inset shadows with spread > 0 - optionally
|
||||
// convert all non-inset shadows into filter: drop-shadow() to boost performance
|
||||
.map(([k, v]) => [
|
||||
`--${k}Shadow: ${getCssShadow(v)}`,
|
||||
|
@ -429,7 +285,7 @@ const generateShadows = (input) => {
|
|||
}
|
||||
}
|
||||
|
||||
const composePreset = (colors, radii, shadows, fonts) => {
|
||||
export const composePreset = (colors, radii, shadows, fonts) => {
|
||||
return {
|
||||
rules: {
|
||||
...shadows.rules,
|
||||
|
@ -446,98 +302,110 @@ const composePreset = (colors, radii, shadows, fonts) => {
|
|||
}
|
||||
}
|
||||
|
||||
const generatePreset = (input) => {
|
||||
const shadows = generateShadows(input)
|
||||
export const generatePreset = (input) => {
|
||||
const colors = generateColors(input)
|
||||
const radii = generateRadii(input)
|
||||
const fonts = generateFonts(input)
|
||||
|
||||
return composePreset(colors, radii, shadows, fonts)
|
||||
return composePreset(
|
||||
colors,
|
||||
generateRadii(input),
|
||||
generateShadows(input, colors.theme.colors, colors.mod),
|
||||
generateFonts(input)
|
||||
)
|
||||
}
|
||||
|
||||
const getThemes = () => {
|
||||
return window.fetch('/static/styles.json')
|
||||
export const getThemes = () => {
|
||||
const cache = 'no-store'
|
||||
|
||||
return window.fetch('/static/styles.json', { cache })
|
||||
.then((data) => data.json())
|
||||
.then((themes) => {
|
||||
return Promise.all(Object.entries(themes).map(([k, v]) => {
|
||||
return Object.entries(themes).map(([k, v]) => {
|
||||
let promise = null
|
||||
if (typeof v === 'object') {
|
||||
return Promise.resolve([k, v])
|
||||
promise = Promise.resolve(v)
|
||||
} else if (typeof v === 'string') {
|
||||
return window.fetch(v)
|
||||
promise = window.fetch(v, { cache })
|
||||
.then((data) => data.json())
|
||||
.then((theme) => {
|
||||
return [k, theme]
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
return []
|
||||
return null
|
||||
})
|
||||
}
|
||||
}))
|
||||
return [k, promise]
|
||||
})
|
||||
})
|
||||
.then((promises) => {
|
||||
return promises
|
||||
.filter(([k, v]) => v)
|
||||
.reduce((acc, [k, v]) => {
|
||||
acc[k] = v
|
||||
return acc
|
||||
}, {})
|
||||
})
|
||||
}
|
||||
export const colors2to3 = (colors) => {
|
||||
return Object.entries(colors).reduce((acc, [slotName, color]) => {
|
||||
const btnPositions = ['', 'Panel', 'TopBar']
|
||||
switch (slotName) {
|
||||
case 'lightBg':
|
||||
return { ...acc, highlight: color }
|
||||
case 'btnText':
|
||||
return {
|
||||
...acc,
|
||||
...btnPositions
|
||||
.reduce(
|
||||
(statePositionAcc, position) =>
|
||||
({ ...statePositionAcc, ['btn' + position + 'Text']: color })
|
||||
, {}
|
||||
)
|
||||
}
|
||||
default:
|
||||
return { ...acc, [slotName]: color }
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
const setPreset = (val, commit) => {
|
||||
return getThemes().then((themes) => {
|
||||
const theme = themes[val] ? themes[val] : themes['pleroma-dark']
|
||||
const isV1 = Array.isArray(theme)
|
||||
const data = isV1 ? {} : theme.theme
|
||||
|
||||
if (isV1) {
|
||||
const bgRgb = hex2rgb(theme[1])
|
||||
const fgRgb = hex2rgb(theme[2])
|
||||
const textRgb = hex2rgb(theme[3])
|
||||
const linkRgb = hex2rgb(theme[4])
|
||||
|
||||
const cRedRgb = hex2rgb(theme[5] || '#FF0000')
|
||||
const cGreenRgb = hex2rgb(theme[6] || '#00FF00')
|
||||
const cBlueRgb = hex2rgb(theme[7] || '#0000FF')
|
||||
const cOrangeRgb = hex2rgb(theme[8] || '#E3FF00')
|
||||
|
||||
data.colors = {
|
||||
bg: bgRgb,
|
||||
fg: fgRgb,
|
||||
text: textRgb,
|
||||
link: linkRgb,
|
||||
cRed: cRedRgb,
|
||||
cBlue: cBlueRgb,
|
||||
cGreen: cGreenRgb,
|
||||
cOrange: cOrangeRgb
|
||||
/**
|
||||
* This handles compatibility issues when importing v2 theme's shadows to current format
|
||||
*
|
||||
* Back in v2 shadows allowed you to use dynamic colors however those used pure CSS3 variables
|
||||
*/
|
||||
export const shadows2to3 = (shadows, opacity) => {
|
||||
return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => {
|
||||
const isDynamic = ({ color }) => color.startsWith('--')
|
||||
const getOpacity = ({ color }) => opacity[getOpacitySlot(color.substring(2).split(',')[0])]
|
||||
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
|
||||
...shadowAcc,
|
||||
{
|
||||
...def,
|
||||
alpha: isDynamic(def) ? getOpacity(def) || 1 : def.alpha
|
||||
}
|
||||
}
|
||||
|
||||
// This is a hack, this function is only called during initial load.
|
||||
// We want to cancel loading the theme from config.json if we're already
|
||||
// loading a theme from the persisted state.
|
||||
// Needed some way of dealing with the async way of things.
|
||||
// load config -> set preset -> wait for styles.json to load ->
|
||||
// load persisted state -> set colors -> styles.json loaded -> set colors
|
||||
if (!window.themeLoaded) {
|
||||
applyTheme(data, commit)
|
||||
}
|
||||
})
|
||||
], [])
|
||||
return { ...shadowsAcc, [slotName]: newShadow }
|
||||
}, {})
|
||||
}
|
||||
|
||||
export {
|
||||
setStyle,
|
||||
setPreset,
|
||||
applyTheme,
|
||||
getTextColor,
|
||||
generateColors,
|
||||
generateRadii,
|
||||
generateShadows,
|
||||
generateFonts,
|
||||
generatePreset,
|
||||
getThemes,
|
||||
composePreset,
|
||||
getCssShadow,
|
||||
getCssShadowFilter
|
||||
export const getPreset = (val) => {
|
||||
return getThemes()
|
||||
.then((themes) => themes[val] ? themes[val] : themes['pleroma-dark'])
|
||||
.then((theme) => {
|
||||
const isV1 = Array.isArray(theme)
|
||||
const data = isV1 ? {} : theme.theme
|
||||
|
||||
if (isV1) {
|
||||
const bg = hex2rgb(theme[1])
|
||||
const fg = hex2rgb(theme[2])
|
||||
const text = hex2rgb(theme[3])
|
||||
const link = hex2rgb(theme[4])
|
||||
|
||||
const cRed = hex2rgb(theme[5] || '#FF0000')
|
||||
const cGreen = hex2rgb(theme[6] || '#00FF00')
|
||||
const cBlue = hex2rgb(theme[7] || '#0000FF')
|
||||
const cOrange = hex2rgb(theme[8] || '#E3FF00')
|
||||
|
||||
data.colors = { bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
|
||||
}
|
||||
|
||||
return { theme: data, source: theme.source }
|
||||
})
|
||||
}
|
||||
|
||||
export const setPreset = (val) => getPreset(val).then(data => applyTheme(data.theme))
|
||||
|
|
637
src/services/theme_data/pleromafe.js
Normal file
637
src/services/theme_data/pleromafe.js
Normal file
|
@ -0,0 +1,637 @@
|
|||
import { invertLightness, brightness } from 'chromatism'
|
||||
import { alphaBlend, mixrgb } from '../color_convert/color_convert.js'
|
||||
/* This is a definition of all layer combinations
|
||||
* each key is a topmost layer, each value represents layer underneath
|
||||
* this is essentially a simplified tree
|
||||
*/
|
||||
export const LAYERS = {
|
||||
undelay: null, // root
|
||||
topBar: null, // no transparency support
|
||||
badge: null, // no transparency support
|
||||
profileTint: null, // doesn't matter
|
||||
fg: null,
|
||||
bg: 'underlay',
|
||||
highlight: 'bg',
|
||||
panel: 'bg',
|
||||
popover: 'bg',
|
||||
selectedMenu: 'popover',
|
||||
btn: 'bg',
|
||||
btnPanel: 'panel',
|
||||
btnTopBar: 'topBar',
|
||||
input: 'bg',
|
||||
inputPanel: 'panel',
|
||||
inputTopBar: 'topBar',
|
||||
alert: 'bg',
|
||||
alertPanel: 'panel',
|
||||
poll: 'bg'
|
||||
}
|
||||
|
||||
/* By default opacity slots have 1 as default opacity
|
||||
* this allows redefining it to something else
|
||||
*/
|
||||
export const DEFAULT_OPACITY = {
|
||||
profileTint: 0.5,
|
||||
alert: 0.5,
|
||||
input: 0.5,
|
||||
faint: 0.5,
|
||||
underlay: 0.15
|
||||
}
|
||||
|
||||
/** SUBJECT TO CHANGE IN THE FUTURE, this is all beta
|
||||
* Color and opacity slots definitions. Each key represents a slot.
|
||||
*
|
||||
* Short-hands:
|
||||
* String beginning with `--` - value after dashes treated as sole
|
||||
* dependency - i.e. `--value` equivalent to { depends: ['value']}
|
||||
* String beginning with `#` - value would be treated as solid color
|
||||
* defined in hexadecimal representation (i.e. #FFFFFF) and will be
|
||||
* used as default. `#FFFFFF` is equivalent to { default: '#FFFFFF'}
|
||||
*
|
||||
* Full definition:
|
||||
* @property {String[]} depends - color slot names this color depends ones.
|
||||
* cyclic dependencies are supported to some extent but not recommended.
|
||||
* @property {String} [opacity] - opacity slot used by this color slot.
|
||||
* opacity is inherited from parents. To break inheritance graph use null
|
||||
* @property {Number} [priority] - EXPERIMENTAL. used to pre-sort slots so
|
||||
* that slots with higher priority come earlier
|
||||
* @property {Function(mod, ...colors)} [color] - function that will be
|
||||
* used to determine the color. By default it just copies first color in
|
||||
* dependency list.
|
||||
* @argument {Number} mod - `1` (light-on-dark) or `-1` (dark-on-light)
|
||||
* depending on background color (for textColor)/given color.
|
||||
* @argument {...Object} deps - each argument after mod represents each
|
||||
* color from `depends` array. All colors take user customizations into
|
||||
* account and represented by { r, g, b } objects.
|
||||
* @returns {Object} resulting color, should be in { r, g, b } form
|
||||
*
|
||||
* @property {Boolean|String} [textColor] - true to mark color slot as text
|
||||
* color. This enables automatic text color generation for the slot. Use
|
||||
* 'preserve' string if you don't want text color to fall back to
|
||||
* black/white. Use 'bw' to only ever use black or white. This also makes
|
||||
* following properties required:
|
||||
* @property {String} [layer] - which layer the text sit on top on - used
|
||||
* to account for transparency in text color calculation
|
||||
* layer is inherited from parents. To break inheritance graph use null
|
||||
* @property {String} [variant] - which color slot is background (same as
|
||||
* above, used to account for transparency)
|
||||
*/
|
||||
export const SLOT_INHERITANCE = {
|
||||
bg: {
|
||||
depends: [],
|
||||
opacity: 'bg',
|
||||
priority: 1
|
||||
},
|
||||
fg: {
|
||||
depends: [],
|
||||
priority: 1
|
||||
},
|
||||
text: {
|
||||
depends: [],
|
||||
layer: 'bg',
|
||||
opacity: null,
|
||||
priority: 1
|
||||
},
|
||||
underlay: {
|
||||
default: '#000000',
|
||||
opacity: 'underlay'
|
||||
},
|
||||
link: {
|
||||
depends: ['accent'],
|
||||
priority: 1
|
||||
},
|
||||
accent: {
|
||||
depends: ['link'],
|
||||
priority: 1
|
||||
},
|
||||
faint: {
|
||||
depends: ['text'],
|
||||
opacity: 'faint'
|
||||
},
|
||||
faintLink: {
|
||||
depends: ['link'],
|
||||
opacity: 'faint'
|
||||
},
|
||||
postFaintLink: {
|
||||
depends: ['postLink'],
|
||||
opacity: 'faint'
|
||||
},
|
||||
|
||||
cBlue: '#0000ff',
|
||||
cRed: '#FF0000',
|
||||
cGreen: '#00FF00',
|
||||
cOrange: '#E3FF00',
|
||||
|
||||
profileBg: {
|
||||
depends: ['bg'],
|
||||
color: (mod, bg) => ({
|
||||
r: Math.floor(bg.r * 0.53),
|
||||
g: Math.floor(bg.g * 0.56),
|
||||
b: Math.floor(bg.b * 0.59)
|
||||
})
|
||||
},
|
||||
profileTint: {
|
||||
depends: ['bg'],
|
||||
layer: 'profileTint',
|
||||
opacity: 'profileTint'
|
||||
},
|
||||
|
||||
highlight: {
|
||||
depends: ['bg'],
|
||||
color: (mod, bg) => brightness(5 * mod, bg).rgb
|
||||
},
|
||||
highlightLightText: {
|
||||
depends: ['lightText'],
|
||||
layer: 'highlight',
|
||||
textColor: true
|
||||
},
|
||||
highlightPostLink: {
|
||||
depends: ['postLink'],
|
||||
layer: 'highlight',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
highlightFaintText: {
|
||||
depends: ['faint'],
|
||||
layer: 'highlight',
|
||||
textColor: true
|
||||
},
|
||||
highlightFaintLink: {
|
||||
depends: ['faintLink'],
|
||||
layer: 'highlight',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
highlightPostFaintLink: {
|
||||
depends: ['postFaintLink'],
|
||||
layer: 'highlight',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
highlightText: {
|
||||
depends: ['text'],
|
||||
layer: 'highlight',
|
||||
textColor: true
|
||||
},
|
||||
highlightLink: {
|
||||
depends: ['link'],
|
||||
layer: 'highlight',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
highlightIcon: {
|
||||
depends: ['highlight', 'highlightText'],
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
},
|
||||
|
||||
popover: {
|
||||
depends: ['bg'],
|
||||
opacity: 'popover'
|
||||
},
|
||||
popoverLightText: {
|
||||
depends: ['lightText'],
|
||||
layer: 'popover',
|
||||
textColor: true
|
||||
},
|
||||
popoverPostLink: {
|
||||
depends: ['postLink'],
|
||||
layer: 'popover',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
popoverFaintText: {
|
||||
depends: ['faint'],
|
||||
layer: 'popover',
|
||||
textColor: true
|
||||
},
|
||||
popoverFaintLink: {
|
||||
depends: ['faintLink'],
|
||||
layer: 'popover',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
popoverPostFaintLink: {
|
||||
depends: ['postFaintLink'],
|
||||
layer: 'popover',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
popoverText: {
|
||||
depends: ['text'],
|
||||
layer: 'popover',
|
||||
textColor: true
|
||||
},
|
||||
popoverLink: {
|
||||
depends: ['link'],
|
||||
layer: 'popover',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
popoverIcon: {
|
||||
depends: ['popover', 'popoverText'],
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
},
|
||||
|
||||
selectedPost: '--highlight',
|
||||
selectedPostFaintText: {
|
||||
depends: ['highlightFaintText'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: true
|
||||
},
|
||||
selectedPostLightText: {
|
||||
depends: ['highlightLightText'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: true
|
||||
},
|
||||
selectedPostPostLink: {
|
||||
depends: ['highlightPostLink'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
selectedPostFaintLink: {
|
||||
depends: ['highlightFaintLink'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
selectedPostText: {
|
||||
depends: ['highlightText'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: true
|
||||
},
|
||||
selectedPostLink: {
|
||||
depends: ['highlightLink'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
selectedPostIcon: {
|
||||
depends: ['selectedPost', 'selectedPostText'],
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
},
|
||||
|
||||
selectedMenu: {
|
||||
depends: ['bg'],
|
||||
color: (mod, bg) => brightness(5 * mod, bg).rgb
|
||||
},
|
||||
selectedMenuLightText: {
|
||||
depends: ['highlightLightText'],
|
||||
layer: 'selectedMenu',
|
||||
variant: 'selectedMenu',
|
||||
textColor: true
|
||||
},
|
||||
selectedMenuFaintText: {
|
||||
depends: ['highlightFaintText'],
|
||||
layer: 'selectedMenu',
|
||||
variant: 'selectedMenu',
|
||||
textColor: true
|
||||
},
|
||||
selectedMenuFaintLink: {
|
||||
depends: ['highlightFaintLink'],
|
||||
layer: 'selectedMenu',
|
||||
variant: 'selectedMenu',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
selectedMenuText: {
|
||||
depends: ['highlightText'],
|
||||
layer: 'selectedMenu',
|
||||
variant: 'selectedMenu',
|
||||
textColor: true
|
||||
},
|
||||
selectedMenuLink: {
|
||||
depends: ['highlightLink'],
|
||||
layer: 'selectedMenu',
|
||||
variant: 'selectedMenu',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
selectedMenuIcon: {
|
||||
depends: ['selectedMenu', 'selectedMenuText'],
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
},
|
||||
|
||||
selectedMenuPopover: {
|
||||
depends: ['popover'],
|
||||
color: (mod, bg) => brightness(5 * mod, bg).rgb
|
||||
},
|
||||
selectedMenuPopoverLightText: {
|
||||
depends: ['selectedMenuLightText'],
|
||||
layer: 'selectedMenuPopover',
|
||||
variant: 'selectedMenuPopover',
|
||||
textColor: true
|
||||
},
|
||||
selectedMenuPopoverFaintText: {
|
||||
depends: ['selectedMenuFaintText'],
|
||||
layer: 'selectedMenuPopover',
|
||||
variant: 'selectedMenuPopover',
|
||||
textColor: true
|
||||
},
|
||||
selectedMenuPopoverFaintLink: {
|
||||
depends: ['selectedMenuFaintLink'],
|
||||
layer: 'selectedMenuPopover',
|
||||
variant: 'selectedMenuPopover',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
selectedMenuPopoverText: {
|
||||
depends: ['selectedMenuText'],
|
||||
layer: 'selectedMenuPopover',
|
||||
variant: 'selectedMenuPopover',
|
||||
textColor: true
|
||||
},
|
||||
selectedMenuPopoverLink: {
|
||||
depends: ['selectedMenuLink'],
|
||||
layer: 'selectedMenuPopover',
|
||||
variant: 'selectedMenuPopover',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
selectedMenuPopoverIcon: {
|
||||
depends: ['selectedMenuPopover', 'selectedMenuText'],
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
},
|
||||
|
||||
lightText: {
|
||||
depends: ['text'],
|
||||
layer: 'bg',
|
||||
textColor: 'preserve',
|
||||
color: (mod, text) => brightness(20 * mod, text).rgb
|
||||
},
|
||||
|
||||
postLink: {
|
||||
depends: ['link'],
|
||||
layer: 'bg',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
|
||||
postGreentext: {
|
||||
depends: ['cGreen'],
|
||||
layer: 'bg',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
|
||||
border: {
|
||||
depends: ['fg'],
|
||||
opacity: 'border',
|
||||
color: (mod, fg) => brightness(2 * mod, fg).rgb
|
||||
},
|
||||
|
||||
poll: {
|
||||
depends: ['accent', 'bg'],
|
||||
copacity: 'poll',
|
||||
color: (mod, accent, bg) => alphaBlend(accent, 0.4, bg)
|
||||
},
|
||||
pollText: {
|
||||
depends: ['text'],
|
||||
layer: 'poll',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
icon: {
|
||||
depends: ['bg', 'text'],
|
||||
inheritsOpacity: false,
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
},
|
||||
|
||||
// Foreground
|
||||
fgText: {
|
||||
depends: ['text'],
|
||||
layer: 'fg',
|
||||
textColor: true
|
||||
},
|
||||
fgLink: {
|
||||
depends: ['link'],
|
||||
layer: 'fg',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
|
||||
// Panel header
|
||||
panel: {
|
||||
depends: ['fg'],
|
||||
opacity: 'panel'
|
||||
},
|
||||
panelText: {
|
||||
depends: ['text'],
|
||||
layer: 'panel',
|
||||
textColor: true
|
||||
},
|
||||
panelFaint: {
|
||||
depends: ['fgText'],
|
||||
layer: 'panel',
|
||||
opacity: 'faint',
|
||||
textColor: true
|
||||
},
|
||||
panelLink: {
|
||||
depends: ['fgLink'],
|
||||
layer: 'panel',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
|
||||
// Top bar
|
||||
topBar: '--fg',
|
||||
topBarText: {
|
||||
depends: ['fgText'],
|
||||
layer: 'topBar',
|
||||
textColor: true
|
||||
},
|
||||
topBarLink: {
|
||||
depends: ['fgLink'],
|
||||
layer: 'topBar',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
|
||||
// Tabs
|
||||
tab: {
|
||||
depends: ['btn']
|
||||
},
|
||||
tabText: {
|
||||
depends: ['btnText'],
|
||||
layer: 'btn',
|
||||
textColor: true
|
||||
},
|
||||
tabActiveText: {
|
||||
depends: ['text'],
|
||||
layer: 'bg',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
// Buttons
|
||||
btn: {
|
||||
depends: ['fg'],
|
||||
variant: 'btn',
|
||||
opacity: 'btn'
|
||||
},
|
||||
btnText: {
|
||||
depends: ['fgText'],
|
||||
layer: 'btn',
|
||||
textColor: true
|
||||
},
|
||||
btnPanelText: {
|
||||
depends: ['btnText'],
|
||||
layer: 'btnPanel',
|
||||
variant: 'btn',
|
||||
textColor: true
|
||||
},
|
||||
btnTopBarText: {
|
||||
depends: ['btnText'],
|
||||
layer: 'btnTopBar',
|
||||
variant: 'btn',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
// Buttons: pressed
|
||||
btnPressed: {
|
||||
depends: ['btn'],
|
||||
layer: 'btn'
|
||||
},
|
||||
btnPressedText: {
|
||||
depends: ['btnText'],
|
||||
layer: 'btn',
|
||||
variant: 'btnPressed',
|
||||
textColor: true
|
||||
},
|
||||
btnPressedPanel: {
|
||||
depends: ['btnPressed'],
|
||||
layer: 'btn'
|
||||
},
|
||||
btnPressedPanelText: {
|
||||
depends: ['btnPanelText'],
|
||||
layer: 'btnPanel',
|
||||
variant: 'btnPressed',
|
||||
textColor: true
|
||||
},
|
||||
btnPressedTopBar: {
|
||||
depends: ['btnPressed'],
|
||||
layer: 'btn'
|
||||
},
|
||||
btnPressedTopBarText: {
|
||||
depends: ['btnTopBarText'],
|
||||
layer: 'btnTopBar',
|
||||
variant: 'btnPressed',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
// Buttons: toggled
|
||||
btnToggled: {
|
||||
depends: ['btn'],
|
||||
layer: 'btn',
|
||||
color: (mod, btn) => brightness(mod * 20, btn).rgb
|
||||
},
|
||||
btnToggledText: {
|
||||
depends: ['btnText'],
|
||||
layer: 'btn',
|
||||
variant: 'btnToggled',
|
||||
textColor: true
|
||||
},
|
||||
btnToggledPanelText: {
|
||||
depends: ['btnPanelText'],
|
||||
layer: 'btnPanel',
|
||||
variant: 'btnToggled',
|
||||
textColor: true
|
||||
},
|
||||
btnToggledTopBarText: {
|
||||
depends: ['btnTopBarText'],
|
||||
layer: 'btnTopBar',
|
||||
variant: 'btnToggled',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
// Buttons: disabled
|
||||
btnDisabled: {
|
||||
depends: ['btn', 'bg'],
|
||||
color: (mod, btn, bg) => alphaBlend(btn, 0.25, bg)
|
||||
},
|
||||
btnDisabledText: {
|
||||
depends: ['btnText', 'btnDisabled'],
|
||||
layer: 'btn',
|
||||
variant: 'btnDisabled',
|
||||
color: (mod, text, btn) => alphaBlend(text, 0.25, btn)
|
||||
},
|
||||
btnDisabledPanelText: {
|
||||
depends: ['btnPanelText', 'btnDisabled'],
|
||||
layer: 'btnPanel',
|
||||
variant: 'btnDisabled',
|
||||
color: (mod, text, btn) => alphaBlend(text, 0.25, btn)
|
||||
},
|
||||
btnDisabledTopBarText: {
|
||||
depends: ['btnTopBarText', 'btnDisabled'],
|
||||
layer: 'btnTopBar',
|
||||
variant: 'btnDisabled',
|
||||
color: (mod, text, btn) => alphaBlend(text, 0.25, btn)
|
||||
},
|
||||
|
||||
// Input fields
|
||||
input: {
|
||||
depends: ['fg'],
|
||||
opacity: 'input'
|
||||
},
|
||||
inputText: {
|
||||
depends: ['text'],
|
||||
layer: 'input',
|
||||
textColor: true
|
||||
},
|
||||
inputPanelText: {
|
||||
depends: ['panelText'],
|
||||
layer: 'inputPanel',
|
||||
variant: 'input',
|
||||
textColor: true
|
||||
},
|
||||
inputTopbarText: {
|
||||
depends: ['topBarText'],
|
||||
layer: 'inputTopBar',
|
||||
variant: 'input',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
alertError: {
|
||||
depends: ['cRed'],
|
||||
opacity: 'alert'
|
||||
},
|
||||
alertErrorText: {
|
||||
depends: ['text'],
|
||||
layer: 'alert',
|
||||
variant: 'alertError',
|
||||
textColor: true
|
||||
},
|
||||
alertErrorPanelText: {
|
||||
depends: ['panelText'],
|
||||
layer: 'alertPanel',
|
||||
variant: 'alertError',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
alertWarning: {
|
||||
depends: ['cOrange'],
|
||||
opacity: 'alert'
|
||||
},
|
||||
alertWarningText: {
|
||||
depends: ['text'],
|
||||
layer: 'alert',
|
||||
variant: 'alertWarning',
|
||||
textColor: true
|
||||
},
|
||||
alertWarningPanelText: {
|
||||
depends: ['panelText'],
|
||||
layer: 'alertPanel',
|
||||
variant: 'alertWarning',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
alertNeutral: {
|
||||
depends: ['text'],
|
||||
opacity: 'alert'
|
||||
},
|
||||
alertNeutralText: {
|
||||
depends: ['text'],
|
||||
layer: 'alert',
|
||||
variant: 'alertNeutral',
|
||||
color: (mod, text) => invertLightness(text).rgb,
|
||||
textColor: true
|
||||
},
|
||||
alertNeutralPanelText: {
|
||||
depends: ['panelText'],
|
||||
layer: 'alertPanel',
|
||||
variant: 'alertNeutral',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
badgeNotification: '--cRed',
|
||||
badgeNotificationText: {
|
||||
depends: ['text', 'badgeNotification'],
|
||||
layer: 'badge',
|
||||
variant: 'badgeNotification',
|
||||
textColor: 'bw'
|
||||
}
|
||||
}
|
405
src/services/theme_data/theme_data.service.js
Normal file
405
src/services/theme_data/theme_data.service.js
Normal file
|
@ -0,0 +1,405 @@
|
|||
import { convert, brightness, contrastRatio } from 'chromatism'
|
||||
import { alphaBlendLayers, getTextColor, relativeLuminance } from '../color_convert/color_convert.js'
|
||||
import { LAYERS, DEFAULT_OPACITY, SLOT_INHERITANCE } from './pleromafe.js'
|
||||
|
||||
/*
|
||||
* # What's all this?
|
||||
* Here be theme engine for pleromafe. All of this supposed to ease look
|
||||
* and feel customization, making widget styles and make developer's life
|
||||
* easier when it comes to supporting themes. Like many other theme systems
|
||||
* it operates on color definitions, or "slots" - for example you define
|
||||
* "button" color slot and then in UI component Button's CSS you refer to
|
||||
* it as a CSS3 Variable.
|
||||
*
|
||||
* Some applications allow you to customize colors for certain things.
|
||||
* Some UI toolkits allow you to define colors for each type of widget.
|
||||
* Most of them are pretty barebones and have no assistance for common
|
||||
* problems and cases, and in general themes themselves are very hard to
|
||||
* maintain in all aspects. This theme engine tries to solve all of the
|
||||
* common problems with themes.
|
||||
*
|
||||
* You don't have redefine several similar colors if you just want to
|
||||
* change one color - all color slots are derived from other ones, so you
|
||||
* can have at least one or two "basic" colors defined and have all other
|
||||
* components inherit and modify basic ones.
|
||||
*
|
||||
* You don't have to test contrast ratio for colors or pick text color for
|
||||
* each element even if you have light-on-dark elements in dark-on-light
|
||||
* theme.
|
||||
*
|
||||
* You don't have to maintain order of code for inheriting slots from othet
|
||||
* slots - dependency graph resolving does it for you.
|
||||
*/
|
||||
|
||||
/* This indicates that this version of code outputs similar theme data and
|
||||
* should be incremented if output changes - for instance if getTextColor
|
||||
* function changes and older themes no longer render text colors as
|
||||
* author intended previously.
|
||||
*/
|
||||
export const CURRENT_VERSION = 3
|
||||
|
||||
export const getLayersArray = (layer, data = LAYERS) => {
|
||||
let array = [layer]
|
||||
let parent = data[layer]
|
||||
while (parent) {
|
||||
array.unshift(parent)
|
||||
parent = data[parent]
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
export const getLayers = (layer, variant = layer, opacitySlot, colors, opacity) => {
|
||||
return getLayersArray(layer).map((currentLayer) => ([
|
||||
currentLayer === layer
|
||||
? colors[variant]
|
||||
: colors[currentLayer],
|
||||
currentLayer === layer
|
||||
? opacity[opacitySlot] || 1
|
||||
: opacity[currentLayer]
|
||||
]))
|
||||
}
|
||||
|
||||
const getDependencies = (key, inheritance) => {
|
||||
const data = inheritance[key]
|
||||
if (typeof data === 'string' && data.startsWith('--')) {
|
||||
return [data.substring(2)]
|
||||
} else {
|
||||
if (data === null) return []
|
||||
const { depends, layer, variant } = data
|
||||
const layerDeps = layer
|
||||
? getLayersArray(layer).map(currentLayer => {
|
||||
return currentLayer === layer
|
||||
? variant || layer
|
||||
: currentLayer
|
||||
})
|
||||
: []
|
||||
if (Array.isArray(depends)) {
|
||||
return [...depends, ...layerDeps]
|
||||
} else {
|
||||
return [...layerDeps]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts inheritance object topologically - dependant slots come after
|
||||
* dependencies
|
||||
*
|
||||
* @property {Object} inheritance - object defining the nodes
|
||||
* @property {Function} getDeps - function that returns dependencies for
|
||||
* given value and inheritance object.
|
||||
* @returns {String[]} keys of inheritance object, sorted in topological
|
||||
* order. Additionally, dependency-less nodes will always be first in line
|
||||
*/
|
||||
export const topoSort = (
|
||||
inheritance = SLOT_INHERITANCE,
|
||||
getDeps = getDependencies
|
||||
) => {
|
||||
// This is an implementation of https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
|
||||
|
||||
const allKeys = Object.keys(inheritance)
|
||||
const whites = new Set(allKeys)
|
||||
const grays = new Set()
|
||||
const blacks = new Set()
|
||||
const unprocessed = [...allKeys]
|
||||
const output = []
|
||||
|
||||
const step = (node) => {
|
||||
if (whites.has(node)) {
|
||||
// Make node "gray"
|
||||
whites.delete(node)
|
||||
grays.add(node)
|
||||
// Do step for each node connected to it (one way)
|
||||
getDeps(node, inheritance).forEach(step)
|
||||
// Make node "black"
|
||||
grays.delete(node)
|
||||
blacks.add(node)
|
||||
// Put it into the output list
|
||||
output.push(node)
|
||||
} else if (grays.has(node)) {
|
||||
console.debug('Cyclic depenency in topoSort, ignoring')
|
||||
output.push(node)
|
||||
} else if (blacks.has(node)) {
|
||||
// do nothing
|
||||
} else {
|
||||
throw new Error('Unintended condition in topoSort!')
|
||||
}
|
||||
}
|
||||
while (unprocessed.length > 0) {
|
||||
step(unprocessed.pop())
|
||||
}
|
||||
return output.sort((a, b) => {
|
||||
const depsA = getDeps(a, inheritance).length
|
||||
const depsB = getDeps(b, inheritance).length
|
||||
|
||||
if (depsA === depsB || (depsB !== 0 && depsA !== 0)) return 0
|
||||
if (depsA === 0 && depsB !== 0) return -1
|
||||
if (depsB === 0 && depsA !== 0) return 1
|
||||
})
|
||||
}
|
||||
|
||||
const expandSlotValue = (value) => {
|
||||
if (typeof value === 'object') return value
|
||||
return {
|
||||
depends: value.startsWith('--') ? [value.substring(2)] : [],
|
||||
default: value.startsWith('#') ? value : undefined
|
||||
}
|
||||
}
|
||||
/**
|
||||
* retrieves opacity slot for given slot. This goes up the depenency graph
|
||||
* to find which parent has opacity slot defined for it.
|
||||
* TODO refactor this
|
||||
*/
|
||||
export const getOpacitySlot = (
|
||||
k,
|
||||
inheritance = SLOT_INHERITANCE,
|
||||
getDeps = getDependencies
|
||||
) => {
|
||||
const value = expandSlotValue(inheritance[k])
|
||||
if (value.opacity === null) return
|
||||
if (value.opacity) return value.opacity
|
||||
const findInheritedOpacity = (key, visited = [k]) => {
|
||||
const depSlot = getDeps(key, inheritance)[0]
|
||||
if (depSlot === undefined) return
|
||||
const dependency = inheritance[depSlot]
|
||||
if (dependency === undefined) return
|
||||
if (dependency.opacity || dependency === null) {
|
||||
return dependency.opacity
|
||||
} else if (dependency.depends && visited.includes(depSlot)) {
|
||||
return findInheritedOpacity(depSlot, [...visited, depSlot])
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (value.depends) {
|
||||
return findInheritedOpacity(k)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves layer slot for given slot. This goes up the depenency graph
|
||||
* to find which parent has opacity slot defined for it.
|
||||
* this is basically copypaste of getOpacitySlot except it checks if key is
|
||||
* in LAYERS
|
||||
* TODO refactor this
|
||||
*/
|
||||
export const getLayerSlot = (
|
||||
k,
|
||||
inheritance = SLOT_INHERITANCE,
|
||||
getDeps = getDependencies
|
||||
) => {
|
||||
const value = expandSlotValue(inheritance[k])
|
||||
if (LAYERS[k]) return k
|
||||
if (value.layer === null) return
|
||||
if (value.layer) return value.layer
|
||||
const findInheritedLayer = (key, visited = [k]) => {
|
||||
const depSlot = getDeps(key, inheritance)[0]
|
||||
if (depSlot === undefined) return
|
||||
const dependency = inheritance[depSlot]
|
||||
if (dependency === undefined) return
|
||||
if (dependency.layer || dependency === null) {
|
||||
return dependency.layer
|
||||
} else if (dependency.depends) {
|
||||
return findInheritedLayer(dependency, [...visited, depSlot])
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (value.depends) {
|
||||
return findInheritedLayer(k)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* topologically sorted SLOT_INHERITANCE
|
||||
*/
|
||||
export const SLOT_ORDERED = topoSort(
|
||||
Object.entries(SLOT_INHERITANCE)
|
||||
.sort(([aK, aV], [bK, bV]) => ((aV && aV.priority) || 0) - ((bV && bV.priority) || 0))
|
||||
.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})
|
||||
)
|
||||
|
||||
/**
|
||||
* All opacity slots used in color slots, their default values and affected
|
||||
* color slots.
|
||||
*/
|
||||
export const OPACITIES = Object.entries(SLOT_INHERITANCE).reduce((acc, [k, v]) => {
|
||||
const opacity = getOpacitySlot(k, SLOT_INHERITANCE, getDependencies)
|
||||
if (opacity) {
|
||||
return {
|
||||
...acc,
|
||||
[opacity]: {
|
||||
defaultValue: DEFAULT_OPACITY[opacity] || 1,
|
||||
affectedSlots: [...((acc[opacity] && acc[opacity].affectedSlots) || []), k]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return acc
|
||||
}
|
||||
}, {})
|
||||
|
||||
/**
|
||||
* Handle dynamic color
|
||||
*/
|
||||
export const computeDynamicColor = (sourceColor, getColor, mod) => {
|
||||
if (typeof sourceColor !== 'string' || !sourceColor.startsWith('--')) return sourceColor
|
||||
let targetColor = null
|
||||
// Color references other color
|
||||
const [variable, modifier] = sourceColor.split(/,/g).map(str => str.trim())
|
||||
const variableSlot = variable.substring(2)
|
||||
targetColor = getColor(variableSlot)
|
||||
if (modifier) {
|
||||
targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
|
||||
}
|
||||
return targetColor
|
||||
}
|
||||
|
||||
/**
|
||||
* THE function you want to use. Takes provided colors and opacities
|
||||
* value and uses inheritance data to figure out color needed for the slot.
|
||||
*/
|
||||
export const getColors = (sourceColors, sourceOpacity) => SLOT_ORDERED.reduce(({ colors, opacity }, key) => {
|
||||
const sourceColor = sourceColors[key]
|
||||
const value = expandSlotValue(SLOT_INHERITANCE[key])
|
||||
const deps = getDependencies(key, SLOT_INHERITANCE)
|
||||
const isTextColor = !!value.textColor
|
||||
const variant = value.variant || value.layer
|
||||
|
||||
let backgroundColor = null
|
||||
|
||||
if (isTextColor) {
|
||||
backgroundColor = alphaBlendLayers(
|
||||
{ ...(colors[deps[0]] || convert(sourceColors[key] || '#FF00FF').rgb) },
|
||||
getLayers(
|
||||
getLayerSlot(key) || 'bg',
|
||||
variant || 'bg',
|
||||
getOpacitySlot(variant),
|
||||
colors,
|
||||
opacity
|
||||
)
|
||||
)
|
||||
} else if (variant && variant !== key) {
|
||||
backgroundColor = colors[variant] || convert(sourceColors[variant]).rgb
|
||||
} else {
|
||||
backgroundColor = colors.bg || convert(sourceColors.bg)
|
||||
}
|
||||
|
||||
const isLightOnDark = relativeLuminance(backgroundColor) < 0.5
|
||||
const mod = isLightOnDark ? 1 : -1
|
||||
|
||||
let outputColor = null
|
||||
if (sourceColor) {
|
||||
// Color is defined in source color
|
||||
let targetColor = sourceColor
|
||||
if (targetColor === 'transparent') {
|
||||
// We take only layers below current one
|
||||
const layers = getLayers(
|
||||
getLayerSlot(key),
|
||||
key,
|
||||
getOpacitySlot(key) || key,
|
||||
colors,
|
||||
opacity
|
||||
).slice(0, -1)
|
||||
targetColor = {
|
||||
...alphaBlendLayers(
|
||||
convert('#FF00FF').rgb,
|
||||
layers
|
||||
),
|
||||
a: 0
|
||||
}
|
||||
} else if (typeof sourceColor === 'string' && sourceColor.startsWith('--')) {
|
||||
targetColor = computeDynamicColor(
|
||||
sourceColor,
|
||||
variableSlot => colors[variableSlot] || sourceColors[variableSlot],
|
||||
mod
|
||||
)
|
||||
} else if (typeof sourceColor === 'string' && sourceColor.startsWith('#')) {
|
||||
targetColor = convert(targetColor).rgb
|
||||
}
|
||||
outputColor = { ...targetColor }
|
||||
} else if (value.default) {
|
||||
// same as above except in object form
|
||||
outputColor = convert(value.default).rgb
|
||||
} else {
|
||||
// calculate color
|
||||
const defaultColorFunc = (mod, dep) => ({ ...dep })
|
||||
const colorFunc = value.color || defaultColorFunc
|
||||
|
||||
if (value.textColor) {
|
||||
if (value.textColor === 'bw') {
|
||||
outputColor = contrastRatio(backgroundColor).rgb
|
||||
} else {
|
||||
let color = { ...colors[deps[0]] }
|
||||
if (value.color) {
|
||||
color = colorFunc(mod, ...deps.map((dep) => ({ ...colors[dep] })))
|
||||
}
|
||||
outputColor = getTextColor(
|
||||
backgroundColor,
|
||||
{ ...color },
|
||||
value.textColor === 'preserve'
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// background color case
|
||||
outputColor = colorFunc(
|
||||
mod,
|
||||
...deps.map((dep) => ({ ...colors[dep] }))
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!outputColor) {
|
||||
throw new Error('Couldn\'t generate color for ' + key)
|
||||
}
|
||||
|
||||
const opacitySlot = value.opacity || getOpacitySlot(key)
|
||||
const ownOpacitySlot = value.opacity
|
||||
|
||||
if (ownOpacitySlot === null) {
|
||||
outputColor.a = 1
|
||||
} else if (sourceColor === 'transparent') {
|
||||
outputColor.a = 0
|
||||
} else {
|
||||
const opacityOverriden = ownOpacitySlot && sourceOpacity[opacitySlot] !== undefined
|
||||
|
||||
const dependencySlot = deps[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 {
|
||||
// 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 },
|
||||
opacity: { ...opacity, [opacitySlot]: outputColor.a }
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
colors: { ...colors, [key]: outputColor },
|
||||
opacity
|
||||
}
|
||||
}
|
||||
}, { colors: {}, opacity: {} })
|
|
@ -6,6 +6,7 @@ const update = ({ store, statuses, timeline, showImmediately, userId }) => {
|
|||
const ccTimeline = camelCase(timeline)
|
||||
|
||||
store.dispatch('setError', { value: false })
|
||||
store.dispatch('setErrorData', { value: null })
|
||||
|
||||
store.dispatch('addNewStatuses', {
|
||||
timeline: ccTimeline,
|
||||
|
@ -45,6 +46,10 @@ const fetchAndUpdate = ({
|
|||
|
||||
return apiService.fetchTimeline(args)
|
||||
.then((statuses) => {
|
||||
if (statuses.error) {
|
||||
store.dispatch('setErrorData', { value: statuses })
|
||||
return
|
||||
}
|
||||
if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) {
|
||||
store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId })
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue