biome format --write

This commit is contained in:
Henry Jameson 2026-01-06 16:22:52 +02:00
commit 9262e803ec
415 changed files with 54076 additions and 17419 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,9 @@
import { kebabCase } from 'lodash'
const propsToNative = props => Object.keys(props).reduce((acc, cur) => {
acc[kebabCase(cur)] = props[cur]
return acc
}, {})
const propsToNative = (props) =>
Object.keys(props).reduce((acc, cur) => {
acc[kebabCase(cur)] = props[cur]
return acc
}, {})
export { propsToNative }

View file

@ -1,40 +1,60 @@
import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js'
import apiService, {
getMastodonSocketURI,
ProcessedWS,
} from '../api/api.service.js'
import timelineFetcher 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'
import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js'
import bookmarkFoldersFetcher from '../../services/bookmark_folders_fetcher/bookmark_folders_fetcher.service.js'
const backendInteractorService = credentials => ({
startFetchingTimeline ({ timeline, store, userId = false, listId = false, statusId = false, bookmarkFolderId = false, tag }) {
return timelineFetcher.startFetching({ timeline, store, credentials, userId, listId, statusId, bookmarkFolderId, tag })
const backendInteractorService = (credentials) => ({
startFetchingTimeline({
timeline,
store,
userId = false,
listId = false,
statusId = false,
bookmarkFolderId = false,
tag,
}) {
return timelineFetcher.startFetching({
timeline,
store,
credentials,
userId,
listId,
statusId,
bookmarkFolderId,
tag,
})
},
fetchTimeline (args) {
fetchTimeline(args) {
return timelineFetcher.fetchAndUpdate({ ...args, credentials })
},
startFetchingNotifications ({ store }) {
startFetchingNotifications({ store }) {
return notificationsFetcher.startFetching({ store, credentials })
},
fetchNotifications (args) {
fetchNotifications(args) {
return notificationsFetcher.fetchAndUpdate({ ...args, credentials })
},
startFetchingFollowRequests ({ store }) {
startFetchingFollowRequests({ store }) {
return followRequestFetcher.startFetching({ store, credentials })
},
startFetchingLists ({ store }) {
startFetchingLists({ store }) {
return listsFetcher.startFetching({ store, credentials })
},
startFetchingBookmarkFolders ({ store }) {
startFetchingBookmarkFolders({ store }) {
return bookmarkFoldersFetcher.startFetching({ store, credentials })
},
startUserSocket ({ store }) {
startUserSocket({ store }) {
const serv = store.rootState.instance.server.replace('http', 'ws')
const url = getMastodonSocketURI({}, serv)
return ProcessedWS({ url, id: 'Unified', credentials })
@ -43,11 +63,11 @@ const backendInteractorService = credentials => ({
...Object.entries(apiService).reduce((acc, [key, func]) => {
return {
...acc,
[key]: (args) => func({ credentials, ...args })
[key]: (args) => func({ credentials, ...args }),
}
}, {}),
verifyCredentials: apiService.verifyCredentials
verifyCredentials: apiService.verifyCredentials,
})
export default backendInteractorService

View file

@ -3,10 +3,14 @@ import { promiseInterval } from '../promise_interval/promise_interval.js'
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
const fetchAndUpdate = ({ credentials }) => {
return apiService.fetchBookmarkFolders({ credentials })
.then(bookmarkFolders => {
useBookmarkFoldersStore().setBookmarkFolders(bookmarkFolders)
}, () => {})
return apiService
.fetchBookmarkFolders({ credentials })
.then(
(bookmarkFolders) => {
useBookmarkFoldersStore().setBookmarkFolders(bookmarkFolders)
},
() => {},
)
.catch(() => {})
}
@ -17,7 +21,7 @@ const startFetching = ({ credentials, store }) => {
}
const bookmarkFoldersFetcher = {
startFetching
startFetching,
}
export default bookmarkFoldersFetcher

View file

@ -9,7 +9,7 @@ const empty = (chatId) => {
lastSeenMessageId: '0',
chatId,
minId: undefined,
maxId: undefined
maxId: undefined,
}
}
@ -25,7 +25,9 @@ const clear = (storage) => {
}
}
storage.messages = storage.messages.filter(m => failedMessageIds.includes(m.id))
storage.messages = storage.messages.filter((m) =>
failedMessageIds.includes(m.id),
)
storage.newMessageCount = 0
storage.lastSeenMessageId = '0'
storage.minId = undefined
@ -33,8 +35,10 @@ const clear = (storage) => {
}
const deleteMessage = (storage, messageId) => {
if (!storage) { return }
storage.messages = storage.messages.filter(m => m.id !== messageId)
if (!storage) {
return
}
storage.messages = storage.messages.filter((m) => m.id !== messageId)
delete storage.idIndex[messageId]
if (storage.maxId === messageId) {
@ -65,14 +69,20 @@ const cullOlderMessages = (storage) => {
}
const handleMessageError = (storage, fakeId, isRetry) => {
if (!storage) { return }
if (!storage) {
return
}
const fakeMessage = storage.idIndex[fakeId]
if (fakeMessage) {
fakeMessage.error = true
fakeMessage.pending = false
if (!isRetry) {
// Ensure the failed message doesn't stay at the bottom of the list.
const lastPersistedMessage = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'desc'])[0]
const lastPersistedMessage = _.orderBy(
storage.messages,
['pending', 'id'],
['asc', 'desc'],
)[0]
if (lastPersistedMessage) {
const oldId = fakeMessage.id
fakeMessage.id = `${lastPersistedMessage.id}-${new Date().getTime()}`
@ -84,12 +94,16 @@ const handleMessageError = (storage, fakeId, isRetry) => {
}
const add = (storage, { messages: newMessages, updateMaxId = true }) => {
if (!storage) { return }
if (!storage) {
return
}
for (let i = 0; i < newMessages.length; i++) {
const message = newMessages[i]
// sanity check
if (message.chat_id !== storage.chatId) { return }
if (message.chat_id !== storage.chatId) {
return
}
if (message.fakeId) {
const fakeMessage = storage.idIndex[message.fakeId]
@ -98,7 +112,9 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => {
// make sure to remove the older duplicate message.
if (storage.idIndex[message.id]) {
delete storage.idIndex[message.id]
storage.messages = storage.messages.filter(msg => msg.id !== message.id)
storage.messages = storage.messages.filter(
(msg) => msg.id !== message.id,
)
}
Object.assign(fakeMessage, message, { error: false })
delete fakeMessage.fakeId
@ -136,17 +152,25 @@ const isConfirmation = (storage, message) => {
}
const resetNewMessageCount = (storage) => {
if (!storage) { return }
if (!storage) {
return
}
storage.newMessageCount = 0
storage.lastSeenMessageId = storage.maxId
}
// Inserts date separators and marks the head and tail if it's the chain of messages made by the same user
const getView = (storage) => {
if (!storage) { return [] }
if (!storage) {
return []
}
const result = []
const messages = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'asc'])
const messages = _.orderBy(
storage.messages,
['pending', 'id'],
['asc', 'asc'],
)
const firstMessage = messages[0]
let previousMessage = messages[messages.length - 1]
let currentMessageChainId
@ -157,7 +181,7 @@ const getView = (storage) => {
result.push({
type: 'date',
date,
id: date.getTime().toString()
id: date.getTime().toString(),
})
}
@ -175,7 +199,7 @@ const getView = (storage) => {
result.push({
type: 'date',
date,
id: date.getTime().toString()
id: date.getTime().toString(),
})
previousMessage.isTail = true
@ -188,7 +212,7 @@ const getView = (storage) => {
data: message,
date,
id: message.id,
messageChainId: currentMessageChainId
messageChainId: currentMessageChainId,
}
// end a message chian
@ -198,7 +222,12 @@ const getView = (storage) => {
}
// start a new message chain
if ((previousMessage && previousMessage.data && previousMessage.data.account_id) !== message.account_id || afterDate) {
if (
(previousMessage &&
previousMessage.data &&
previousMessage.data.account_id) !== message.account_id ||
afterDate
) {
currentMessageChainId = _.uniqueId()
object.isHead = true
object.messageChainId = currentMessageChainId
@ -220,7 +249,7 @@ const ChatService = {
cullOlderMessages,
resetNewMessageCount,
clear,
handleMessageError
handleMessageError,
}
export default ChatService

View file

@ -2,24 +2,35 @@ import { showDesktopNotification } from '../desktop_notification_utils/desktop_n
export const maybeShowChatNotification = (store, chat) => {
if (!chat.lastMessage) return
if (store.rootState.chats.currentChatId === chat.id && !document.hidden) return
if (store.rootState.users.currentUser.id === chat.lastMessage.account_id) return
if (store.rootState.chats.currentChatId === chat.id && !document.hidden)
return
if (store.rootState.users.currentUser.id === chat.lastMessage.account_id)
return
const opts = {
tag: chat.lastMessage.id,
title: chat.account.name,
icon: chat.account.profile_image_url,
body: chat.lastMessage.content
body: chat.lastMessage.content,
}
if (chat.lastMessage.attachment && chat.lastMessage.attachment.type === 'image') {
if (
chat.lastMessage.attachment &&
chat.lastMessage.attachment.type === 'image'
) {
opts.image = chat.lastMessage.attachment.preview_url
}
showDesktopNotification(store.rootState, opts)
}
export const buildFakeMessage = ({ content, chatId, attachments, userId, idempotencyKey }) => {
export const buildFakeMessage = ({
content,
chatId,
attachments,
userId,
idempotencyKey,
}) => {
const fakeMessage = {
content,
chat_id: chatId,
@ -30,7 +41,7 @@ export const buildFakeMessage = ({ content, chatId, attachments, userId, idempot
idempotency_key: idempotencyKey,
emojis: [],
pending: true,
isNormalized: true
isNormalized: true,
}
if (attachments[0]) {

View file

@ -19,9 +19,9 @@ export const rgb2hex = (r, g, b) => {
return r
}
if (typeof r === 'object') {
({ r, g, b } = r)
;({ r, g, b } = r)
}
[r, g, b] = [r, g, b].map(val => {
;[r, g, b] = [r, g, b].map((val) => {
val = Math.ceil(val)
val = val < 0 ? 0 : val
val = val > 255 ? 255 : val
@ -137,9 +137,9 @@ export const alphaBlend = (fg, fga, bg) => {
// Simplified https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
// for opaque bg and transparent fg
return {
r: (fg.r * fga + bg.r * (1 - fga)),
g: (fg.g * fga + bg.g * (1 - fga)),
b: (fg.b * fga + bg.b * (1 - fga))
r: fg.r * fga + bg.r * (1 - fga),
g: fg.g * fga + bg.g * (1 - fga),
b: fg.b * fga + bg.b * (1 - fga),
}
}
@ -149,15 +149,16 @@ export const alphaBlend = (fg, fga, bg) => {
* @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 alphaBlendLayers = (bedrock, layers) =>
layers.reduce((acc, [color, opacity]) => {
return alphaBlend(color, opacity, acc)
}, bedrock)
export const invert = (rgb) => {
return {
r: 255 - rgb.r,
g: 255 - rgb.g,
b: 255 - rgb.b
b: 255 - rgb.b,
}
}
@ -174,7 +175,7 @@ export const hex2rgb = (hex) => {
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
b: parseInt(result[3], 16),
}
: null
}
@ -190,7 +191,7 @@ export const mixrgb = (a, b) => {
return {
r: (a.r + b.r) / 2,
g: (a.g + b.g) / 2,
b: (a.b + b.b) / 2
b: (a.b + b.b) / 2,
}
}
@ -205,7 +206,7 @@ export const rgba2css = function (rgba) {
r: 0,
g: 0,
b: 0,
a: 1
a: 1,
}
if (rgba !== null) {

View file

@ -12,26 +12,30 @@ export const wordAtPosition = (str, pos) => {
}
export const addPositionToWords = (words) => {
return reduce(words, (result, word) => {
const data = {
word,
start: 0,
end: word.length
}
return reduce(
words,
(result, word) => {
const data = {
word,
start: 0,
end: word.length,
}
if (result.length > 0) {
const previous = result.pop()
if (result.length > 0) {
const previous = result.pop()
data.start += previous.end
data.end += previous.end
data.start += previous.end
data.end += previous.end
result.push(previous)
}
result.push(previous)
}
result.push(data)
result.push(data)
return result
}, [])
return result
},
[],
)
}
export const splitByWhitespaceBoundary = (str) => {
@ -64,7 +68,7 @@ const completion = {
wordAtPosition,
addPositionToWords,
splitByWhitespaceBoundary,
replaceWord
replaceWord,
}
export default completion

View file

@ -1,10 +1,8 @@
import isFunction from 'lodash/isFunction'
const getComponentOptions = (Component) => (isFunction(Component)) ? Component.options : Component
const getComponentOptions = (Component) =>
isFunction(Component) ? Component.options : Component
const getComponentProps = (Component) => getComponentOptions(Component).props
export {
getComponentOptions,
getComponentProps
}
export { getComponentOptions, getComponentProps }

View file

@ -47,17 +47,23 @@ export const relativeTimeShort = (date, nowThreshold = 1) => {
export const unitToSeconds = (unit, amount) => {
switch (unit) {
case 'minutes': return 0.001 * amount * MINUTE
case 'hours': return 0.001 * amount * HOUR
case 'days': return 0.001 * amount * DAY
case 'minutes':
return 0.001 * amount * MINUTE
case 'hours':
return 0.001 * amount * HOUR
case 'days':
return 0.001 * amount * DAY
}
}
export const secondsToUnit = (unit, amount) => {
switch (unit) {
case 'minutes': return (1000 * amount) / MINUTE
case 'hours': return (1000 * amount) / HOUR
case 'days': return (1000 * amount) / DAY
case 'minutes':
return (1000 * amount) / MINUTE
case 'hours':
return (1000 * amount) / HOUR
case 'days':
return (1000 * amount) / DAY
}
}
@ -66,14 +72,15 @@ export const isSameYear = (a, b) => {
}
export const isSameMonth = (a, b) => {
return a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth()
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth()
}
export const isSameDay = (a, b) => {
return a.getFullYear() === b.getFullYear() &&
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
)
}
export const durationStrToMs = (str) => {

View file

@ -1,19 +1,27 @@
import {
showDesktopNotification as swDesktopNotification,
closeDesktopNotification as swCloseDesktopNotification,
isSWSupported
isSWSupported,
} from '../sw/sw.js'
const state = { failCreateNotif: false }
export const showDesktopNotification = (rootState, desktopNotificationOpts) => {
if (!('Notification' in window && window.Notification.permission === 'granted')) return
if (rootState.notifications.desktopNotificationSilence) { return }
if (
!('Notification' in window && window.Notification.permission === 'granted')
)
return
if (rootState.notifications.desktopNotificationSilence) {
return
}
if (isSWSupported()) {
swDesktopNotification(desktopNotificationOpts)
} else if (!state.failCreateNotif) {
try {
const desktopNotification = new window.Notification(desktopNotificationOpts.title, desktopNotificationOpts)
const desktopNotification = new window.Notification(
desktopNotificationOpts.title,
desktopNotificationOpts,
)
setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
} catch {
state.failCreateNotif = true
@ -22,7 +30,10 @@ export const showDesktopNotification = (rootState, desktopNotificationOpts) => {
}
export const closeDesktopNotification = (rootState, { id }) => {
if (!('Notification' in window && window.Notification.permission === 'granted')) return
if (
!('Notification' in window && window.Notification.permission === 'granted')
)
return
if (isSWSupported()) {
swCloseDesktopNotification({ id })
@ -30,7 +41,10 @@ export const closeDesktopNotification = (rootState, { id }) => {
}
export const closeAllDesktopNotifications = () => {
if (!('Notification' in window && window.Notification.permission === 'granted')) return
if (
!('Notification' in window && window.Notification.permission === 'granted')
)
return
if (isSWSupported()) {
swCloseDesktopNotification({})

View file

@ -21,16 +21,25 @@ const qvitterStatusType = (status) => {
return 'retweet'
}
if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) ||
(typeof status.text === 'string' && status.text.match(/favorited/))) {
if (
(typeof status.uri === 'string' &&
status.uri.match(/(fave|objectType=Favourite)/)) ||
(typeof status.text === 'string' && status.text.match(/favorited/))
) {
return 'favorite'
}
if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) {
if (
status.text.match(/deleted notice {{tag/) ||
status.qvitter_delete_notice
) {
return 'deletion'
}
if (status.text.match(/started following/) || status.activity_type === 'follow') {
if (
status.text.match(/started following/) ||
status.activity_type === 'follow'
) {
return 'follow'
}
@ -69,16 +78,16 @@ export const parseUser = (data) => {
output.description_html = data.note
output.fields = data.fields
output.fields_html = data.fields.map(field => {
output.fields_html = data.fields.map((field) => {
return {
name: escape(field.name),
value: field.value
value: field.value,
}
})
output.fields_text = data.fields.map(field => {
output.fields_text = data.fields.map((field) => {
return {
name: unescape(field.name.replace(/<[^>]*>/g, '')),
value: unescape(field.value.replace(/<[^>]*>/g, ''))
value: unescape(field.value.replace(/<[^>]*>/g, '')),
}
})
@ -119,7 +128,7 @@ export const parseUser = (data) => {
output.rights = {
moderator: data.pleroma.is_moderator,
admin: data.pleroma.is_admin
admin: data.pleroma.is_admin,
}
// TODO: Clean up in UI? This is duplication from what BE does for qvitterapi
if (output.rights.admin) {
@ -149,13 +158,10 @@ export const parseUser = (data) => {
'moderation_log_read',
'announcements_manage_announcements',
'emoji_manage_emoji',
'statistics_read'
'statistics_read',
]
} else if (data.pleroma.is_moderator) {
output.privileges = [
'messages_delete',
'reports_manage_reports'
]
output.privileges = ['messages_delete', 'reports_manage_reports']
} else {
output.privileges = []
}
@ -203,7 +209,7 @@ export const parseUser = (data) => {
if (data.rights) {
output.rights = {
moderator: data.rights.delete_others_notice,
admin: data.rights.admin
admin: data.rights.admin,
}
}
output.no_rich_text = data.no_rich_text
@ -221,7 +227,7 @@ export const parseUser = (data) => {
muting: data.muted,
blocking: data.statusnet_blocking,
followed_by: data.follows_you,
following: data.following
following: data.following,
}
}
@ -237,9 +243,10 @@ export const parseUser = (data) => {
// deactivated was changed to is_active in Pleroma 2.3.0
// so check if is_active is present
output.deactivated = typeof data.pleroma.is_active !== 'undefined'
? !data.pleroma.is_active // new backend
: data.pleroma.deactivated // old backend
output.deactivated =
typeof data.pleroma.is_active !== 'undefined'
? !data.pleroma.is_active // new backend
: data.pleroma.deactivated // old backend
output.notification_settings = data.pleroma.notification_settings
output.unread_chat_count = data.pleroma.unread_chat_count
@ -324,14 +331,19 @@ export const parseStatus = (data) => {
const { pleroma } = data
if (data.pleroma) {
output.text = pleroma.content ? data.pleroma.content['text/plain'] : data.content
output.summary = pleroma.spoiler_text ? data.pleroma.spoiler_text['text/plain'] : data.spoiler_text
output.text = pleroma.content
? data.pleroma.content['text/plain']
: data.content
output.summary = pleroma.spoiler_text
? data.pleroma.spoiler_text['text/plain']
: data.spoiler_text
output.statusnet_conversation_id = data.pleroma.conversation_id
output.is_local = pleroma.local
output.in_reply_to_screen_name = pleroma.in_reply_to_account_acct
output.thread_muted = pleroma.thread_muted
output.emoji_reactions = pleroma.emoji_reactions
output.parent_visible = pleroma.parent_visible === undefined ? true : pleroma.parent_visible
output.parent_visible =
pleroma.parent_visible === undefined ? true : pleroma.parent_visible
output.quote_visible = pleroma.quote_visible || true
output.quotes_count = pleroma.quotes_count
output.bookmark_folder_id = pleroma.bookmark_folder
@ -341,9 +353,10 @@ export const parseStatus = (data) => {
}
const quoteRaw = pleroma?.quote || data.quote
const quoteData = quoteRaw ? parseStatus(quoteRaw) : undefined
const quoteData = quoteRaw ? parseStatus(quoteRaw) : undefined
output.quote = quoteData
output.quote_id = data.quote?.id ?? data.quote_id ?? quoteData?.id ?? pleroma.quote_id
output.quote_id =
data.quote?.id ?? data.quote_id ?? quoteData?.id ?? pleroma.quote_id
output.quote_url = data.quote?.url ?? quoteData?.url ?? pleroma.quote_url
output.in_reply_to_status_id = data.in_reply_to_id
@ -358,9 +371,9 @@ export const parseStatus = (data) => {
output.external_url = data.url
output.poll = data.poll
if (output.poll) {
output.poll.options = (output.poll.options || []).map(field => ({
output.poll.options = (output.poll.options || []).map((field) => ({
...field,
title_html: escape(field.title)
title_html: escape(field.title),
}))
}
output.pinned = data.pinned
@ -419,10 +432,13 @@ export const parseStatus = (data) => {
output.user = parseUser(masto ? data.account : data.user)
output.attentions = ((masto ? data.mentions : data.attentions) || []).map(parseUser)
output.attentions = ((masto ? data.mentions : data.attentions) || []).map(
parseUser,
)
output.attachments = ((masto ? data.media_attachments : data.attachments) || [])
.map(parseAttachment)
output.attachments = (
(masto ? data.media_attachments : data.attachments) || []
).map(parseAttachment)
const retweetedStatus = masto ? data.reblog : data.retweeted_status
if (retweetedStatus) {
@ -442,7 +458,7 @@ export const parseStatus = (data) => {
export const parseNotification = (data) => {
const mastoDict = {
favourite: 'like',
reblog: 'repeat'
reblog: 'repeat',
}
const masto = !Object.hasOwn(data, 'ntype')
const output = {}
@ -452,10 +468,11 @@ export const parseNotification = (data) => {
output.seen = data.pleroma.is_seen
// TODO: null check should be a temporary fix, I guess.
// Investigate why backend does this.
output.status = isStatusNotification(output.type) && data.status !== null ? parseStatus(data.status) : null
output.target = output.type !== 'move'
? null
: parseUser(data.target)
output.status =
isStatusNotification(output.type) && data.status !== null
? parseStatus(data.status)
: null
output.target = output.type !== 'move' ? null : parseUser(data.target)
output.from_profile = parseUser(data.account)
output.emoji = data.emoji
output.emoji_url = data.emoji_url
@ -470,11 +487,15 @@ export const parseNotification = (data) => {
const parsedNotice = parseStatus(data.notice)
output.type = data.ntype
output.seen = Boolean(data.is_seen)
output.status = output.type === 'like'
? parseStatus(data.notice.favorited_status)
: parsedNotice
output.status =
output.type === 'like'
? parseStatus(data.notice.favorited_status)
: parsedNotice
output.action = parsedNotice
output.from_profile = output.type === 'pleroma:chat_mention' ? parseUser(data.account) : parseUser(data.from_profile)
output.from_profile =
output.type === 'pleroma:chat_mention'
? parseUser(data.account)
: parseUser(data.from_profile)
}
output.created_at = new Date(data.created_at)
@ -485,7 +506,10 @@ export const parseNotification = (data) => {
const isNsfw = (status) => {
const nsfwRegex = /#nsfw/i
return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex)
return (
(status.tags || []).includes('nsfw') ||
!!(status.text || '').match(nsfwRegex)
)
}
export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
@ -497,7 +521,7 @@ export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
return {
maxId: flakeId ? maxId : parseInt(maxId, 10),
minId: flakeId ? minId : parseInt(minId, 10)
minId: flakeId ? minId : parseInt(minId, 10),
}
}
@ -512,8 +536,12 @@ export const parseChat = (chat) => {
}
export const parseChatMessage = (message) => {
if (!message) { return }
if (message.isNormalized) { return message }
if (!message) {
return
}
if (message.isNormalized) {
return message
}
const output = message
output.id = message.id
output.created_at = new Date(message.created_at)

View file

@ -1,6 +1,6 @@
import { capitalize } from 'lodash'
function humanizeErrors (errors) {
function humanizeErrors(errors) {
return Object.entries(errors).reduce((errs, [k, val]) => {
const message = val.reduce((acc, message) => {
const key = capitalize(k.replace(/_/g, ' '))
@ -10,15 +10,17 @@ function humanizeErrors (errors) {
}, [])
}
export function StatusCodeError (statusCode, body, options, response) {
export function StatusCodeError(statusCode, body, options, response) {
this.name = 'StatusCodeError'
this.statusCode = statusCode
this.message = statusCode + ' - ' + (JSON && JSON.stringify ? JSON.stringify(body) : body)
this.message =
statusCode + ' - ' + (JSON && JSON.stringify ? JSON.stringify(body) : body)
this.error = body // legacy attribute
this.options = options
this.response = response
if (Error.captureStackTrace) { // required for non-V8 environments
if (Error.captureStackTrace) {
// required for non-V8 environments
Error.captureStackTrace(this)
}
}
@ -26,7 +28,7 @@ StatusCodeError.prototype = Object.create(Error.prototype)
StatusCodeError.prototype.constructor = StatusCodeError
export class RegistrationError extends Error {
constructor (error) {
constructor(error) {
super()
if (Error.captureStackTrace) {
Error.captureStackTrace(this)

View file

@ -4,9 +4,9 @@ export const newExporter = ({
filename = 'data',
mime = 'application/json',
extension = 'json',
getExportedObject
getExportedObject,
}) => ({
exportData () {
exportData() {
let stringified
if (mime === 'application/json') {
stringified = utf8.encode(JSON.stringify(getExportedObject(), null, 2)) // Pretty-print and indent with 2 spaces
@ -24,7 +24,7 @@ export const newExporter = ({
document.body.appendChild(e)
e.click()
document.body.removeChild(e)
}
},
})
export const newImporter = ({
@ -32,14 +32,14 @@ export const newImporter = ({
parser = (string) => JSON.parse(string),
onImport,
onImportFailure,
validator = () => true
validator = () => true,
}) => ({
importData () {
importData() {
const filePicker = document.createElement('input')
filePicker.setAttribute('type', 'file')
filePicker.setAttribute('accept', accept)
filePicker.addEventListener('change', event => {
filePicker.addEventListener('change', (event) => {
if (event.target.files[0]) {
const filename = event.target.files[0].name
@ -64,5 +64,5 @@ export const newImporter = ({
document.body.appendChild(filePicker)
filePicker.click()
document.body.removeChild(filePicker)
}
},
})

View file

@ -1,18 +1,18 @@
const checkCanvasExtractPermission = () => {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const canvas = document.createElement('canvas')
canvas.width = 1
canvas.height = 1
const ctx = canvas.getContext('2d');
if (!ctx) return false;
const ctx = canvas.getContext('2d')
if (!ctx) return false
ctx.fillStyle = '#0f161e';
ctx.fillRect(0, 0, 1, 1);
ctx.fillStyle = '#0f161e'
ctx.fillRect(0, 0, 1, 1)
const { data } = ctx.getImageData(0, 0, 1, 1);
const { data } = ctx.getImageData(0, 0, 1, 1)
return data.join(',') === '15,22,30,255';
};
return data.join(',') === '15,22,30,255'
}
const createFaviconService = () => {
const favicons = []
@ -21,10 +21,10 @@ const createFaviconService = () => {
const badgeRadius = 32
const initFaviconService = () => {
if (!checkCanvasExtractPermission()) return;
if (!checkCanvasExtractPermission()) return
const nodes = document.querySelectorAll('link[rel="icon"]')
nodes.forEach(favicon => {
nodes.forEach((favicon) => {
if (favicon) {
const favcanvas = document.createElement('canvas')
favcanvas.width = faviconWidth
@ -47,7 +47,17 @@ const createFaviconService = () => {
favcontext.clearRect(0, 0, faviconWidth, faviconHeight)
if (isImageLoaded(favimg)) {
favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
favcontext.drawImage(
favimg,
0,
0,
favimg.width,
favimg.height,
0,
0,
faviconWidth,
faviconHeight,
)
}
favicon.href = favcanvas.toDataURL('image/png')
})
@ -63,11 +73,28 @@ const createFaviconService = () => {
const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}`
if (isImageLoaded(favimg)) {
favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
favcontext.drawImage(
favimg,
0,
0,
favimg.width,
favimg.height,
0,
0,
faviconWidth,
faviconHeight,
)
}
favcontext.fillStyle = badgeColor
favcontext.beginPath()
favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false)
favcontext.arc(
faviconWidth - badgeRadius,
badgeRadius,
badgeRadius,
0,
2 * Math.PI,
false,
)
favcontext.fill()
favicon.href = favcanvas.toDataURL('image/png')
})
@ -79,7 +106,7 @@ const createFaviconService = () => {
initFaviconService,
clearFaviconBadge,
drawFaviconBadge,
getOriginalFavicons
getOriginalFavicons,
}
}

View file

@ -5,12 +5,15 @@ const fileSizeFormat = (numArg) => {
return num + ' ' + units[0]
}
const exponent = Math.min(Math.floor(Math.log(num) / Math.log(1024)), units.length - 1)
const exponent = Math.min(
Math.floor(Math.log(num) / Math.log(1024)),
units.length - 1,
)
num = (num / Math.pow(1024, exponent)).toFixed(2) * 1
const unit = units[exponent]
return { num, unit }
}
const fileSizeFormatService = {
fileSizeFormat
fileSizeFormat,
}
export default fileSizeFormatService

View file

@ -1,7 +1,7 @@
// TODO this func might as well take the entire file and use its mimetype
// or the entire service could be just mimetype service that only operates
// on mimetypes and not files. Currently the naming is confusing.
export const fileType = mimetype => {
export const fileType = (mimetype) => {
if (mimetype.match(/flash/)) {
return 'flash'
}
@ -25,26 +25,30 @@ export const fileType = mimetype => {
return 'unknown'
}
export const fileTypeExt = url => {
export const fileTypeExt = (url) => {
if (url.match(/\.(a?png|jpe?g|gif|webp|avif)$/)) {
return 'image'
}
if (url.match(/\.(ogv|mp4|webm|mov)$/)) {
return 'video'
}
if (url.match(/\.(it|s3m|mod|umx|mp3|aac|m4a|flac|alac|ogg|oga|opus|wav|ape|midi?)$/)) {
if (
url.match(
/\.(it|s3m|mod|umx|mp3|aac|m4a|flac|alac|ogg|oga|opus|wav|ape|midi?)$/,
)
) {
return 'audio'
}
return 'unknown'
}
export const fileMatchesSomeType = (types, file) =>
types.some(type => fileType(file.mimetype) === type)
types.some((type) => fileType(file.mimetype) === type)
const fileTypeService = {
fileType,
fileTypeExt,
fileMatchesSomeType
fileMatchesSomeType,
}
export default fileTypeService

View file

@ -1,52 +1,64 @@
const fetchRelationship = (attempt, userId, store) => new Promise((resolve, reject) => {
setTimeout(() => {
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
fetchRelationship(++attempt, userId, store)
}
})
const fetchRelationship = (attempt, userId, store) =>
new Promise((resolve, reject) => {
setTimeout(() => {
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
fetchRelationship(++attempt, userId, store)
}
})
export const requestFollow = (userId, store) => new Promise((resolve) => {
store.state.api.backendInteractor.followUser({ id: userId })
.then((updated) => {
store.commit('updateUserRelationship', [updated])
export const requestFollow = (userId, store) =>
new Promise((resolve) => {
store.state.api.backendInteractor
.followUser({ id: userId })
.then((updated) => {
store.commit('updateUserRelationship', [updated])
if (updated.following || (updated.locked && updated.requested)) {
// If we get result immediately or the account is locked, just stop.
resolve()
return
}
if (updated.following || (updated.locked && updated.requested)) {
// If we get result immediately or the account is locked, just stop.
resolve()
return
}
// But usually we don't get result immediately, so we ask server
// for updated user profile to confirm if we are following them
// Sometimes it takes several tries. Sometimes we end up not following
// user anyway, probably because they locked themselves and we
// don't know that yet.
// Recursive Promise, it will call itself up to 3 times.
// But usually we don't get result immediately, so we ask server
// for updated user profile to confirm if we are following them
// Sometimes it takes several tries. Sometimes we end up not following
// user anyway, probably because they locked themselves and we
// don't know that yet.
// Recursive Promise, it will call itself up to 3 times.
return fetchRelationship(1, updated, store)
.then(() => {
return fetchRelationship(1, updated, store).then(() => {
resolve()
})
})
})
export const requestUnfollow = (userId, store) => new Promise((resolve) => {
store.state.api.backendInteractor.unfollowUser({ id: userId })
.then((updated) => {
store.commit('updateUserRelationship', [updated])
resolve({
updated
})
})
})
})
export const requestUnfollow = (userId, store) =>
new Promise((resolve) => {
store.state.api.backendInteractor
.unfollowUser({ id: userId })
.then((updated) => {
store.commit('updateUserRelationship', [updated])
resolve({
updated,
})
})
})

View file

@ -2,11 +2,15 @@ import apiService from '../api/api.service.js'
import { promiseInterval } from '../promise_interval/promise_interval.js'
const fetchAndUpdate = ({ store, credentials }) => {
return apiService.fetchFollowRequests({ credentials })
.then((requests) => {
store.commit('setFollowRequests', requests)
store.commit('addNewUsers', requests)
}, () => {})
return apiService
.fetchFollowRequests({ credentials })
.then(
(requests) => {
store.commit('setFollowRequests', requests)
store.commit('addNewUsers', requests)
},
() => {},
)
.catch(() => {})
}
@ -17,7 +21,7 @@ const startFetching = ({ credentials, store }) => {
}
const followRequestFetcher = {
startFetching
startFetching,
}
export default followRequestFetcher

View file

@ -5,22 +5,25 @@ const DIRECTION_DOWN = [0, 1]
const BUTTON_LEFT = 0
const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
const deltaCoord = (oldCoord, newCoord) => [
newCoord[0] - oldCoord[0],
newCoord[1] - oldCoord[1],
]
const touchCoord = touch => [touch.screenX, touch.screenY]
const touchCoord = (touch) => [touch.screenX, touch.screenY]
const touchEventCoord = e => touchCoord(e.touches[0])
const touchEventCoord = (e) => touchCoord(e.touches[0])
const pointerEventCoord = e => [e.clientX, e.clientY]
const pointerEventCoord = (e) => [e.clientX, e.clientY]
const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1])
const vectorLength = (v) => Math.sqrt(v[0] * v[0] + v[1] * v[1])
const perpendicular = v => [v[1], -v[0]]
const perpendicular = (v) => [v[1], -v[0]]
const dotProduct = (v1, v2) => v1[0] * v2[0] + v1[1] * v2[1]
const project = (v1, v2) => {
const scalar = (dotProduct(v1, v2) / dotProduct(v2, v2))
const scalar = dotProduct(v1, v2) / dotProduct(v2, v2)
return [scalar * v2[0], scalar * v2[1]]
}
@ -30,14 +33,19 @@ const project = (v1, v2) => {
// divergentTolerance: a scalar for much of divergent direction we tolerate when
// above threshold. for example, with 1.0 we only call the callback if
// divergent component of delta is < 1.0 * direction component of delta.
const swipeGesture = (direction, onSwipe, threshold = 30, perpendicularTolerance = 1.0) => {
const swipeGesture = (
direction,
onSwipe,
threshold = 30,
perpendicularTolerance = 1.0,
) => {
return {
direction,
onSwipe,
threshold,
perpendicularTolerance,
_startPos: [0, 0],
_swiping: false
_swiping: false,
}
}
@ -60,7 +68,8 @@ const updateSwipe = (event, gesture) => {
if (
vectorLength(towardsDir) * gesture.perpendicularTolerance <
vectorLength(towardsPerpendicular)
) return
)
return
gesture.onSwipe()
gesture._swiping = false
@ -73,7 +82,7 @@ class SwipeAndClickGesture {
// sign: if the swipe does not meet the threshold, 0
// if the swipe meets the threshold in the positive direction, 1
// if the swipe meets the threshold in the negative direction, -1
constructor ({
constructor({
direction,
// swipeStartCallback
swipePreviewCallback,
@ -82,7 +91,7 @@ class SwipeAndClickGesture {
swipelessClickCallback,
threshold = 30,
perpendicularTolerance = 1.0,
disableClickThreshold = 1
disableClickThreshold = 1,
}) {
const nop = () => {}
this.direction = direction
@ -90,13 +99,17 @@ class SwipeAndClickGesture {
this.swipeEndCallback = swipeEndCallback || nop
this.swipeCancelCallback = swipeCancelCallback || nop
this.swipelessClickCallback = swipelessClickCallback || nop
this.threshold = typeof threshold === 'function' ? threshold : () => threshold
this.disableClickThreshold = typeof disableClickThreshold === 'function' ? disableClickThreshold : () => disableClickThreshold
this.threshold =
typeof threshold === 'function' ? threshold : () => threshold
this.disableClickThreshold =
typeof disableClickThreshold === 'function'
? disableClickThreshold
: () => disableClickThreshold
this.perpendicularTolerance = perpendicularTolerance
this._reset()
}
_reset () {
_reset() {
this._startPos = [0, 0]
this._pointerId = -1
this._swiping = false
@ -104,7 +117,7 @@ class SwipeAndClickGesture {
this._preventNextClick = false
}
start (event) {
start(event) {
// Only handle left click
if (event.button !== BUTTON_LEFT) {
return
@ -116,7 +129,7 @@ class SwipeAndClickGesture {
this._swiped = false
}
move (event) {
move(event) {
if (this._swiping && this._pointerId === event.pointerId) {
this._swiped = true
@ -127,7 +140,7 @@ class SwipeAndClickGesture {
}
}
cancel (event) {
cancel(event) {
if (!this._swiping || this._pointerId !== event.pointerId) {
return
}
@ -135,7 +148,7 @@ class SwipeAndClickGesture {
this.swipeCancelCallback()
}
end (event) {
end(event) {
if (!this._swiping) {
return
}
@ -163,7 +176,7 @@ class SwipeAndClickGesture {
const towardsPerpendicular = project(delta, perpendicularDir)
if (
vectorLength(towardsDir) * this.perpendicularTolerance <
vectorLength(towardsPerpendicular)
vectorLength(towardsPerpendicular)
) {
return 0
}
@ -179,12 +192,15 @@ class SwipeAndClickGesture {
// the end point is far from the starting point
// so for other kinds of pointers do not check
// whether we have swiped
if (vectorLength(delta) >= this.disableClickThreshold() && event.pointerType === 'mouse') {
if (
vectorLength(delta) >= this.disableClickThreshold() &&
event.pointerType === 'mouse'
) {
this._preventNextClick = true
}
}
click () {
click() {
if (!this._preventNextClick) {
this.swipelessClickCallback()
}
@ -200,7 +216,7 @@ const GestureService = {
swipeGesture,
beginSwipe,
updateSwipe,
SwipeAndClickGesture
SwipeAndClickGesture,
}
export default GestureService

View file

@ -22,16 +22,58 @@ export const convertHtmlToLines = (html = '') => {
// Elements that are implicitly self-closing
// https://developer.mozilla.org/en-US/docs/Glossary/empty_element
const emptyElements = new Set([
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr',
])
// Block-level element (they make a visual line)
// https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
const blockElements = new Set([
'address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'dd',
'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li', 'main',
'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul'
'address',
'article',
'aside',
'blockquote',
'details',
'dialog',
'dd',
'div',
'dl',
'dt',
'fieldset',
'figcaption',
'figure',
'footer',
'form',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'header',
'hgroup',
'hr',
'li',
'main',
'nav',
'ol',
'p',
'pre',
'section',
'table',
'ul',
])
// br is very weird in a way that it's technically not block-level, it's
// essentially converted to a \n (or \r\n). There's also wbr but it doesn't
@ -40,7 +82,7 @@ export const convertHtmlToLines = (html = '') => {
const visualLineElements = new Set([
...blockElements.values(),
...linebreakElements.values()
...linebreakElements.values(),
])
// All block-level elements that aren't empty elements, i.e. not <hr>
@ -53,7 +95,7 @@ export const convertHtmlToLines = (html = '') => {
// All elements that we are recognizing
const allElements = new Set([
...nonEmptyElements.values(),
...emptyElements.values()
...emptyElements.values(),
])
const buffer = [] // Current output buffer
@ -61,7 +103,8 @@ export const convertHtmlToLines = (html = '') => {
let textBuffer = '' // Current line content
let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
const flush = () => {
// Processes current line buffer, adds it to output buffer and clears line buffer
if (textBuffer.trim().length > 0) {
buffer.push({ level: [...level], text: textBuffer })
} else {
@ -70,23 +113,27 @@ export const convertHtmlToLines = (html = '') => {
textBuffer = ''
}
const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
const handleBr = (tag) => {
// handles single newlines/linebreaks/selfclosing
flush()
buffer.push(tag)
}
const handleOpen = (tag) => { // handles opening tags
const handleOpen = (tag) => {
// handles opening tags
flush()
buffer.push(tag)
level.unshift(getTagName(tag))
}
const handleClose = (tag) => { // handles closing tags
const handleClose = (tag) => {
// handles closing tags
if (level[0] === getTagName(tag)) {
flush()
buffer.push(tag)
level.shift()
} else { // Broken case
} else {
// Broken case
textBuffer += tag
}
}

View file

@ -24,8 +24,21 @@ export const convertHtmlToTree = (html = '') => {
// Elements that are implicitly self-closing
// https://developer.mozilla.org/en-US/docs/Glossary/empty_element
const emptyElements = new Set([
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr',
])
// TODO For future - also parse HTML5 multi-source components?
@ -38,7 +51,8 @@ export const convertHtmlToTree = (html = '') => {
return levels[levels.length - 1][1]
}
const flushText = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
const flushText = () => {
// Processes current line buffer, adds it to output buffer and clears line buffer
if (textBuffer === '') return
getCurrentBuffer().push(textBuffer)
textBuffer = ''
@ -79,7 +93,10 @@ export const convertHtmlToTree = (html = '') => {
const tagName = getTagName(tagFull)
if (tagFull[1] === '/') {
handleClose(tagFull)
} else if (emptyElements.has(tagName) || tagFull[tagFull.length - 2] === '/') {
} else if (
emptyElements.has(tagName) ||
tagFull[tagFull.length - 2] === '/'
) {
// self-closing
handleSelfClosing(tagFull)
} else {

View file

@ -22,7 +22,9 @@ export const getAttrs = (tag, filter) => {
.replace(new RegExp('^' + getTagName(tag)), '')
.replace(/\/?$/, '')
.trim()
const attrs = Array.from(innertag.matchAll(/([a-z]+[a-z0-9-]*)(?:=("[^"]+?"|'[^']+?'))?/gi))
const attrs = Array.from(
innertag.matchAll(/([a-z]+[a-z0-9-]*)(?:=("[^"]+?"|'[^']+?'))?/gi),
)
.map(([, key, value]) => [key, value])
.map(([k, v]) => {
if (!v) return [k, true]
@ -59,7 +61,10 @@ export const processTextForEmoji = (text, emojis, processor) => {
const next = text.slice(i + 1)
let found = false
for (const emoji of emojis) {
if (next.slice(0, emoji.shortcode.length + 1) === (emoji.shortcode + ':')) {
if (
next.slice(0, emoji.shortcode.length + 1) ===
emoji.shortcode + ':'
) {
found = emoji
break
}

View file

@ -3,10 +3,14 @@ import apiService from '../api/api.service.js'
import { promiseInterval } from '../promise_interval/promise_interval.js'
const fetchAndUpdate = ({ credentials }) => {
return apiService.fetchLists({ credentials })
.then(lists => {
useListsStore().setLists(lists)
}, () => {})
return apiService
.fetchLists({ credentials })
.then(
(lists) => {
useListsStore().setLists(lists)
},
() => {},
)
.catch(() => {})
}
@ -17,7 +21,7 @@ const startFetching = ({ credentials, store }) => {
}
const listsFetcher = {
startFetching
startFetching,
}
export default listsFetcher

View file

@ -6,13 +6,14 @@ const specialLanguageCodes = {
pdc: 'en',
ja_easy: 'ja',
zh_Hant: 'zh-HANT',
zh: 'zh-Hans'
zh: 'zh-Hans',
}
const internalToBrowserLocale = code => specialLanguageCodes[code] || code
const internalToBrowserLocale = (code) => specialLanguageCodes[code] || code
const internalToBackendLocale = code => internalToBrowserLocale(code).replace('_', '-')
const internalToBackendLocaleMulti = codes => {
const internalToBackendLocale = (code) =>
internalToBrowserLocale(code).replace('_', '-')
const internalToBackendLocaleMulti = (codes) => {
const langs = Array.isArray(codes) ? codes : [codes]
return langs.map(internalToBackendLocale).join(',')
}
@ -23,21 +24,27 @@ const getLanguageName = (code) => {
ja_easy: 'やさしいにほんご',
'nan-TW': '臺語(閩南語)',
zh: '简体中文',
zh_Hant: '繁體中文'
zh_Hant: '繁體中文',
}
const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code)
const browserLocale = internalToBrowserLocale(code)
return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1)
return (
languageName.charAt(0).toLocaleUpperCase(browserLocale) +
languageName.slice(1)
)
}
const languages = _.map(languagesObject.languages, (code) => ({ code, name: getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))
const languages = _.map(languagesObject.languages, (code) => ({
code,
name: getLanguageName(code),
})).sort((a, b) => a.name.localeCompare(b.name))
const localeService = {
internalToBrowserLocale,
internalToBackendLocale,
internalToBackendLocaleMulti,
languages,
getLanguageName
getLanguageName,
}
export default localeService

View file

@ -3,7 +3,10 @@ export const mentionMatchesUrl = (attention, url) => {
return true
}
const [namepart, instancepart] = attention.screen_name.split('@')
const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g')
const matchstring = new RegExp(
'://' + instancepart + '/.*' + namepart + '$',
'g',
)
return !!url.match(matchstring)
}
@ -17,7 +20,8 @@ export const extractTagFromUrl = (url) => {
const decoded = decodeURI(url)
// https://git.pleroma.social/pleroma/elixir-libraries/linkify/-/blob/master/lib/linkify/parser.ex
// https://www.pcre.org/original/doc/html/pcrepattern.html
const regex = /tag[s]*\/([\p{L}\p{N}_]*[\p{Alphabetic}_·\u{200c}][\p{L}\p{N}_·\p{M}\u{200c}]*)$/ug
const regex =
/tag[s]*\/([\p{L}\p{N}_]*[\p{Alphabetic}_·\u{200c}][\p{L}\p{N}_·\p{M}\u{200c}]*)$/gu
const result = regex.exec(decoded)
if (!result) {
return false

View file

@ -1,4 +1,10 @@
const verifyOTPCode = ({ clientId, clientSecret, instance, mfaToken, code }) => {
const verifyOTPCode = ({
clientId,
clientSecret,
instance,
mfaToken,
code,
}) => {
const url = `${instance}/oauth/mfa/challenge`
const form = new window.FormData()
@ -8,13 +14,21 @@ const verifyOTPCode = ({ clientId, clientSecret, instance, mfaToken, code }) =>
form.append('code', code)
form.append('challenge_type', 'totp')
return window.fetch(url, {
method: 'POST',
body: form
}).then((data) => data.json())
return window
.fetch(url, {
method: 'POST',
body: form,
})
.then((data) => data.json())
}
const verifyRecoveryCode = ({ clientId, clientSecret, instance, mfaToken, code }) => {
const verifyRecoveryCode = ({
clientId,
clientSecret,
instance,
mfaToken,
code,
}) => {
const url = `${instance}/oauth/mfa/challenge`
const form = new window.FormData()
@ -24,15 +38,17 @@ const verifyRecoveryCode = ({ clientId, clientSecret, instance, mfaToken, code }
form.append('code', code)
form.append('challenge_type', 'recovery')
return window.fetch(url, {
method: 'POST',
body: form
}).then((data) => data.json())
return window
.fetch(url, {
method: 'POST',
body: form,
})
.then((data) => data.json())
}
const mfa = {
verifyOTPCode,
verifyRecoveryCode
verifyRecoveryCode,
}
export default mfa

View file

@ -5,12 +5,16 @@ const REDIRECT_URI = `${window.location.origin}/oauth-callback`
export const getJsonOrError = async (response) => {
if (response.ok) {
return response.json()
.catch((error) => {
throw new StatusCodeError(response.status, error, {}, response)
})
return response.json().catch((error) => {
throw new StatusCodeError(response.status, error, {}, response)
})
} else {
throw new StatusCodeError(response.status, await response.text(), {}, response)
throw new StatusCodeError(
response.status,
await response.text(),
{},
response,
)
}
}
@ -23,19 +27,24 @@ export const createApp = (instance) => {
form.append('redirect_uris', REDIRECT_URI)
form.append('scopes', 'read write follow push admin')
return window.fetch(url, {
method: 'POST',
body: form
})
return window
.fetch(url, {
method: 'POST',
body: form,
})
.then(getJsonOrError)
.then((app) => ({ clientId: app.client_id, clientSecret: app.client_secret }))
.then((app) => ({
clientId: app.client_id,
clientSecret: app.client_secret,
}))
}
export const verifyAppToken = ({ instance, appToken }) => {
return window.fetch(`${instance}/api/v1/apps/verify_credentials`, {
method: 'GET',
headers: { Authorization: `Bearer ${appToken}` }
})
return window
.fetch(`${instance}/api/v1/apps/verify_credentials`, {
method: 'GET',
headers: { Authorization: `Bearer ${appToken}` },
})
.then(getJsonOrError)
}
@ -44,17 +53,21 @@ const login = ({ instance, clientId }) => {
response_type: 'code',
client_id: clientId,
redirect_uri: REDIRECT_URI,
scope: 'read write follow push admin'
scope: 'read write follow push admin',
}
const dataString = reduce(data, (acc, v, k) => {
const encoded = `${k}=${encodeURIComponent(v)}`
if (!acc) {
return encoded
} else {
return `${acc}&${encoded}`
}
}, false)
const dataString = reduce(
data,
(acc, v, k) => {
const encoded = `${k}=${encodeURIComponent(v)}`
if (!acc) {
return encoded
} else {
return `${acc}&${encoded}`
}
},
false,
)
// Do the redirect...
const url = `${instance}/oauth/authorize?${dataString}`
@ -62,7 +75,13 @@ const login = ({ instance, clientId }) => {
window.location.href = url
}
const getTokenWithCredentials = ({ clientId, clientSecret, instance, username, password }) => {
const getTokenWithCredentials = ({
clientId,
clientSecret,
instance,
username,
password,
}) => {
const url = `${instance}/oauth/token`
const form = new window.FormData()
@ -72,10 +91,12 @@ const getTokenWithCredentials = ({ clientId, clientSecret, instance, username, p
form.append('username', username)
form.append('password', password)
return window.fetch(url, {
method: 'POST',
body: form
}).then((data) => data.json())
return window
.fetch(url, {
method: 'POST',
body: form,
})
.then((data) => data.json())
}
const getToken = ({ clientId, clientSecret, instance, code }) => {
@ -88,10 +109,11 @@ const getToken = ({ clientId, clientSecret, instance, code }) => {
form.append('code', code)
form.append('redirect_uri', `${window.location.origin}/oauth-callback`)
return window.fetch(url, {
method: 'POST',
body: form
})
return window
.fetch(url, {
method: 'POST',
body: form,
})
.then((data) => data.json())
}
@ -104,10 +126,12 @@ export const getClientToken = ({ clientId, clientSecret, instance }) => {
form.append('grant_type', 'client_credentials')
form.append('redirect_uri', `${window.location.origin}/oauth-callback`)
return window.fetch(url, {
method: 'POST',
body: form
}).then(getJsonOrError)
return window
.fetch(url, {
method: 'POST',
body: form,
})
.then(getJsonOrError)
}
const verifyOTPCode = ({ app, instance, mfaToken, code }) => {
const url = `${instance}/oauth/mfa/challenge`
@ -119,10 +143,12 @@ const verifyOTPCode = ({ app, instance, mfaToken, code }) => {
form.append('code', code)
form.append('challenge_type', 'totp')
return window.fetch(url, {
method: 'POST',
body: form
}).then((data) => data.json())
return window
.fetch(url, {
method: 'POST',
body: form,
})
.then((data) => data.json())
}
const verifyRecoveryCode = ({ app, instance, mfaToken, code }) => {
@ -135,10 +161,12 @@ const verifyRecoveryCode = ({ app, instance, mfaToken, code }) => {
form.append('code', code)
form.append('challenge_type', 'recovery')
return window.fetch(url, {
method: 'POST',
body: form
}).then((data) => data.json())
return window
.fetch(url, {
method: 'POST',
body: form,
})
.then((data) => data.json())
}
const revokeToken = ({ app, instance, token }) => {
@ -149,10 +177,12 @@ const revokeToken = ({ app, instance, token }) => {
form.append('client_secret', app.clientSecret)
form.append('token', token)
return window.fetch(url, {
method: 'POST',
body: form
}).then((data) => data.json())
return window
.fetch(url, {
method: 'POST',
body: form,
})
.then((data) => data.json())
}
const oauth = {
@ -161,7 +191,7 @@ const oauth = {
getTokenWithCredentials,
verifyOTPCode,
verifyRecoveryCode,
revokeToken
revokeToken,
}
export default oauth

View file

@ -4,14 +4,18 @@ const MASTODON_PASSWORD_RESET_URL = '/auth/password'
const resetPassword = ({ instance, email }) => {
const params = { email }
const query = reduce(params, (acc, v, k) => {
const encoded = `${k}=${encodeURIComponent(v)}`
return `${acc}&${encoded}`
}, '')
const query = reduce(
params,
(acc, v, k) => {
const encoded = `${k}=${encodeURIComponent(v)}`
return `${acc}&${encoded}`
},
'',
)
const url = `${instance}${MASTODON_PASSWORD_RESET_URL}?${query}`
return window.fetch(url, {
method: 'POST'
method: 'POST',
})
}

View file

@ -5,19 +5,23 @@ import { useAnnouncementsStore } from 'src/stores/announcements'
import FaviconService from 'src/services/favicon_service/favicon_service.js'
export const ACTIONABLE_NOTIFICATION_TYPES = new Set(['mention', 'pleroma:report', 'follow_request'])
export const ACTIONABLE_NOTIFICATION_TYPES = new Set([
'mention',
'pleroma:report',
'follow_request',
])
let cachedBadgeUrl = null
export const notificationsFromStore = store => store.state.notifications.data
export const notificationsFromStore = (store) => store.state.notifications.data
export const visibleTypes = store => {
export const visibleTypes = (store) => {
// When called from within a module we need rootGetters to access wider scope
// however when called from a component (i.e. this.$store) we already have wider scope
const rootGetters = store.rootGetters || store.getters
const { notificationVisibility } = rootGetters.mergedConfig
return ([
return [
notificationVisibility.likes && 'like',
notificationVisibility.mentions && 'mention',
notificationVisibility.statuses && 'status',
@ -27,11 +31,18 @@ export const visibleTypes = store => {
notificationVisibility.moves && 'move',
notificationVisibility.emojiReactions && 'pleroma:emoji_reaction',
notificationVisibility.reports && 'pleroma:report',
notificationVisibility.polls && 'poll'
].filter(_ => _))
notificationVisibility.polls && 'poll',
].filter((_) => _)
}
const statusNotifications = new Set(['like', 'mention', 'status', 'repeat', 'pleroma:emoji_reaction', 'poll'])
const statusNotifications = new Set([
'like',
'mention',
'status',
'repeat',
'pleroma:emoji_reaction',
'poll',
])
export const isStatusNotification = (type) => statusNotifications.has(type)
@ -69,22 +80,28 @@ export const maybeShowNotification = (store, notification) => {
if (notification.seen) return
if (!visibleTypes(store).includes(notification.type)) return
if (notification.type === 'mention' && isMutedNotification(notification)) return
if (notification.type === 'mention' && isMutedNotification(notification))
return
const notificationObject = prepareNotificationObject(notification, useI18nStore().i18n)
const notificationObject = prepareNotificationObject(
notification,
useI18nStore().i18n,
)
showDesktopNotification(rootState, notificationObject)
}
export const filteredNotificationsFromStore = (store, types) => {
// map is just to clone the array since sort mutates it and it causes some issues
const sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)
const sortedNotifications = notificationsFromStore(store)
.map((_) => _)
.sort(sortById)
// TODO implement sorting elsewhere and make it optional
return sortedNotifications.filter(
(notification) => (types || visibleTypes(store)).includes(notification.type)
return sortedNotifications.filter((notification) =>
(types || visibleTypes(store)).includes(notification.type),
)
}
export const unseenNotificationsFromStore = store => {
export const unseenNotificationsFromStore = (store) => {
const rootGetters = store.rootGetters || store.getters
const ignoreInactionableSeen = rootGetters.mergedConfig.ignoreInactionableSeen
@ -109,7 +126,7 @@ export const prepareNotificationObject = (notification, i18n) => {
const notifObj = {
tag: notification.id,
type: notification.type,
badge: cachedBadgeUrl
badge: cachedBadgeUrl,
}
const status = notification.status
const title = notification.from_profile.name
@ -152,8 +169,13 @@ export const prepareNotificationObject = (notification, i18n) => {
}
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
if (status && status.attachments && status.attachments.length > 0 && !status.nsfw &&
status.attachments[0].mimetype.startsWith('image/')) {
if (
status &&
status.attachments &&
status.attachments.length > 0 &&
!status.nsfw &&
status.attachments[0].mimetype.startsWith('image/')
) {
notifObj.image = status.attachments[0].url
}
@ -169,8 +191,14 @@ export const countExtraNotifications = (store) => {
}
return [
mergedConfig.showChatsInExtraNotifications ? rootGetters.unreadChatCount : 0,
mergedConfig.showAnnouncementsInExtraNotifications ? useAnnouncementsStore().unreadAnnouncementCount : 0,
mergedConfig.showFollowRequestsInExtraNotifications ? rootGetters.followRequestCount : 0
mergedConfig.showChatsInExtraNotifications
? rootGetters.unreadChatCount
: 0,
mergedConfig.showAnnouncementsInExtraNotifications
? useAnnouncementsStore().unreadAnnouncementCount
: 0,
mergedConfig.showFollowRequestsInExtraNotifications
? rootGetters.followRequestCount
: 0,
].reduce((a, c) => a + c, 0)
}

View file

@ -18,11 +18,10 @@ const mastoApiNotificationTypes = new Set([
'move',
'poll',
'pleroma:emoji_reaction',
'pleroma:report'
'pleroma:report',
])
const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
const args = { credentials }
const { getters } = store
const rootState = store.rootState || store.state
@ -44,7 +43,10 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
return fetchNotifications({ store, args, older })
} else {
// fetch new notifications
if (since === undefined && timelineData.maxId !== Number.POSITIVE_INFINITY) {
if (
since === undefined &&
timelineData.maxId !== Number.POSITIVE_INFINITY
) {
args.since = timelineData.maxId
} else if (since !== null) {
args.since = since
@ -57,8 +59,10 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
// we can update the state in this session to mark them as read as well.
// The normal maxId-check does not tell if older notifications have changed
const notifications = timelineData.data
const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
const unreadNotifsIds = notifications.filter(n => !n.seen).map(n => n.id)
const readNotifsIds = notifications.filter((n) => n.seen).map((n) => n.id)
const unreadNotifsIds = notifications
.filter((n) => !n.seen)
.map((n) => n.id)
if (readNotifsIds.length > 0 && readNotifsIds.length > 0) {
const minId = Math.min(...unreadNotifsIds) // Oldest known unread notification
if (minId !== Infinity) {
@ -73,16 +77,19 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
}
const fetchNotifications = ({ store, args, older }) => {
return apiService.fetchTimeline(args)
return apiService
.fetchTimeline(args)
.then((response) => {
if (response.errors) {
if (response.status === 400 && response.statusText.includes('Invalid value for enum')) {
response
.statusText
if (
response.status === 400 &&
response.statusText.includes('Invalid value for enum')
) {
response.statusText
.matchAll(/(\w+) - Invalid value for enum./g)
.toArray()
.map(x => x[1])
.forEach(x => mastoApiNotificationTypes.delete(x))
.map((x) => x[1])
.forEach((x) => mastoApiNotificationTypes.delete(x))
return fetchNotifications({ store, args, older })
} else {
throw new Error(`${response.status} ${response.statusText}`)
@ -97,7 +104,7 @@ const fetchNotifications = ({ store, args, older }) => {
level: 'error',
messageKey: 'notifications.error',
messageArgs: [error.message],
timeout: 5000
timeout: 5000,
})
console.error(error)
})
@ -115,7 +122,7 @@ const startFetching = ({ credentials, store }) => {
const notificationsFetcher = {
fetchAndUpdate,
startFetching
startFetching,
}
export default notificationsFetcher

View file

@ -1,7 +1,12 @@
export const findOffset = (child, parent, { top = 0, left = 0 } = {}, ignorePadding = true) => {
export const findOffset = (
child,
parent,
{ top = 0, left = 0 } = {},
ignorePadding = true,
) => {
const result = {
top: top + child.offsetTop,
left: left + child.offsetLeft
left: left + child.offsetLeft,
}
if (!ignorePadding && child !== window) {
const { topPadding, leftPadding } = findPadding(child)
@ -9,7 +14,13 @@ export const findOffset = (child, parent, { top = 0, left = 0 } = {}, ignorePadd
result.left += ignorePadding ? 0 : leftPadding
}
if (child.offsetParent && window.getComputedStyle(child.offsetParent).position !== 'sticky' && (parent === window || parent.contains(child.offsetParent) || parent === child.offsetParent)) {
if (
child.offsetParent &&
window.getComputedStyle(child.offsetParent).position !== 'sticky' &&
(parent === window ||
parent.contains(child.offsetParent) ||
parent === child.offsetParent)
) {
return findOffset(child.offsetParent, parent, result, false)
} else {
if (parent !== window) {
@ -23,9 +34,13 @@ export const findOffset = (child, parent, { top = 0, left = 0 } = {}, ignorePadd
const findPadding = (el) => {
const topPaddingStr = window.getComputedStyle(el)['padding-top']
const topPadding = Number(topPaddingStr.substring(0, topPaddingStr.length - 2))
const topPadding = Number(
topPaddingStr.substring(0, topPaddingStr.length - 2),
)
const leftPaddingStr = window.getComputedStyle(el)['padding-left']
const leftPadding = Number(leftPaddingStr.substring(0, leftPaddingStr.length - 2))
const leftPadding = Number(
leftPaddingStr.substring(0, leftPaddingStr.length - 2),
)
return { topPadding, leftPadding }
}

View file

@ -5,7 +5,7 @@ const pollFallbackValues = {
pollType: 'single',
options: ['', ''],
expiryAmount: 10,
expiryUnit: 'minutes'
expiryUnit: 'minutes',
}
const pollFallback = (object, attr) => {
@ -15,10 +15,12 @@ const pollFallback = (object, attr) => {
const pollFormToMasto = (poll) => {
const expiresIn = DateUtils.unitToSeconds(
pollFallback(poll, 'expiryUnit'),
pollFallback(poll, 'expiryAmount')
pollFallback(poll, 'expiryAmount'),
)
const options = uniq(pollFallback(poll, 'options').filter(option => option !== ''))
const options = uniq(
pollFallback(poll, 'options').filter((option) => option !== ''),
)
if (options.length < 2) {
return { errorKey: 'polls.not_enough_options' }
}
@ -26,11 +28,8 @@ const pollFormToMasto = (poll) => {
return {
options,
multiple: pollFallback(poll, 'pollType') === 'multiple',
expiresIn
expiresIn,
}
}
export {
pollFallback,
pollFormToMasto
}
export { pollFallback, pollFormToMasto }

View file

@ -13,7 +13,9 @@ export const promiseInterval = (promiseCall, interval) => {
// something unexpected happened and promiseCall did not
// return a promise, abort the loop.
if (!(promise && promise.finally)) {
console.warn('promiseInterval: promise call did not return a promise, stopping interval.')
console.warn(
'promiseInterval: promise call did not return a promise, stopping interval.',
)
return
}
promise.finally(() => {

View file

@ -7,23 +7,24 @@ import { defineAsyncComponent, shallowReactive, h } from 'vue'
* 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 = () => () => defineAsyncComponent({
loader: asyncComponent,
...options
})
function getResettableAsyncComponent(asyncComponent, options) {
const asyncComponentFactory = () => () =>
defineAsyncComponent({
loader: asyncComponent,
...options,
})
const observe = shallowReactive({ c: asyncComponentFactory() })
return {
render () {
render() {
// emit event resetAsyncComponent to reloading
return h(observe.c(), {
onResetAsyncComponent () {
onResetAsyncComponent() {
observe.c = asyncComponentFactory()
}
},
})
}
},
}
}

View file

@ -1,37 +1,44 @@
const createRuffleService = () => {
let ruffleInstance = null
const getRuffle = async () => new Promise((resolve, reject) => {
if (ruffleInstance) {
resolve(ruffleInstance)
return
}
const getRuffle = async () =>
new Promise((resolve, reject) => {
if (ruffleInstance) {
resolve(ruffleInstance)
return
}
// Ruffle needs these to be set before it's loaded
// https://github.com/ruffle-rs/ruffle/issues/3952
window.RufflePlayer = {}
window.RufflePlayer.config = {
polyfills: false,
publicPath: '/static/ruffle'
}
// Ruffle needs these to be set before it's loaded
// https://github.com/ruffle-rs/ruffle/issues/3952
window.RufflePlayer = {}
window.RufflePlayer.config = {
polyfills: false,
publicPath: '/static/ruffle',
}
// Currently it's seems like a better way of loading ruffle
// because it needs the wasm publically accessible, but it needs path to it
// and filename of wasm seems to be pseudo-randomly generated (is it a hash?)
const script = document.createElement('script')
// see webpack config, using CopyPlugin to copy it from node_modules
// provided via ruffle-mirror
script.src = '/static/ruffle/ruffle.js'
script.type = 'text/javascript'
script.onerror = (e) => { reject(e) }
script.onabort = (e) => { reject(e) }
script.oncancel = (e) => { reject(e) }
script.onload = () => {
ruffleInstance = window.RufflePlayer
resolve(ruffleInstance)
}
document.body.appendChild(script)
})
// Currently it's seems like a better way of loading ruffle
// because it needs the wasm publically accessible, but it needs path to it
// and filename of wasm seems to be pseudo-randomly generated (is it a hash?)
const script = document.createElement('script')
// see webpack config, using CopyPlugin to copy it from node_modules
// provided via ruffle-mirror
script.src = '/static/ruffle/ruffle.js'
script.type = 'text/javascript'
script.onerror = (e) => {
reject(e)
}
script.onabort = (e) => {
reject(e)
}
script.oncancel = (e) => {
reject(e)
}
script.onload = () => {
ruffleInstance = window.RufflePlayer
resolve(ruffleInstance)
}
document.body.appendChild(script)
})
return { getRuffle }
}

View file

@ -3,58 +3,65 @@ export const muteFilterHits = (muteFilters, status) => {
const statusSummary = status.summary.toLowerCase()
const replyToUser = status.in_reply_to_screen_name?.toLowerCase()
const poster = status.user.screen_name?.toLowerCase()
const mentions = (status.attentions || []).map(att => att.screen_name.toLowerCase())
const mentions = (status.attentions || []).map((att) =>
att.screen_name.toLowerCase(),
)
return muteFilters.toSorted((a,b) => b.order - a.order).map(filter => {
const { hide, expires, name, value, type, enabled} = filter
if (!enabled) return false
if (value === '') return false
if (expires !== null && expires < Date.now()) return false
switch (type) {
case 'word': {
const lowercaseValue = value.toLowerCase()
if (statusText.toLowerCase().includes(lowercaseValue) || statusSummary.toLowerCase().includes(lowercaseValue)) {
return { hide, name }
}
break
}
case 'regexp': {
try {
const re = new RegExp(value, 'i')
if (re.test(statusText) || re.test(statusSummary)) {
return { hide, name }
}
return false
} catch {
return false
}
}
case 'user': {
if (
poster.includes(value) ||
replyToUser.includes(value) ||
mentions.some(mention => mention.includes(value))
) {
return { hide, name }
}
break
}
case 'user_regexp': {
try {
const re = new RegExp(value, 'i')
return muteFilters
.toSorted((a, b) => b.order - a.order)
.map((filter) => {
const { hide, expires, name, value, type, enabled } = filter
if (!enabled) return false
if (value === '') return false
if (expires !== null && expires < Date.now()) return false
switch (type) {
case 'word': {
const lowercaseValue = value.toLowerCase()
if (
re.test(poster) ||
re.test(replyToUser) ||
mentions.some(mention => re.test(mention))
statusText.toLowerCase().includes(lowercaseValue) ||
statusSummary.toLowerCase().includes(lowercaseValue)
) {
return { hide, name }
}
return false
} catch {
return false
break
}
case 'regexp': {
try {
const re = new RegExp(value, 'i')
if (re.test(statusText) || re.test(statusSummary)) {
return { hide, name }
}
return false
} catch {
return false
}
}
case 'user': {
if (
poster.includes(value) ||
replyToUser.includes(value) ||
mentions.some((mention) => mention.includes(value))
) {
return { hide, name }
}
break
}
case 'user_regexp': {
try {
const re = new RegExp(value, 'i')
if (
re.test(poster) ||
re.test(replyToUser) ||
mentions.some((mention) => re.test(mention))
) {
return { hide, name }
}
return false
} catch {
return false
}
}
}
}
}).filter(_ => _)
})
.filter((_) => _)
}

View file

@ -13,38 +13,39 @@ const postStatus = ({
quoteId = undefined,
contentType = 'text/plain',
preview = false,
idempotencyKey = ''
idempotencyKey = '',
}) => {
const mediaIds = map(media, 'id')
return apiService.postStatus({
credentials: store.state.users.currentUser.credentials,
status,
spoilerText,
visibility,
sensitive,
mediaIds,
inReplyToStatusId,
quoteId,
contentType,
poll,
preview,
idempotencyKey
})
return apiService
.postStatus({
credentials: store.state.users.currentUser.credentials,
status,
spoilerText,
visibility,
sensitive,
mediaIds,
inReplyToStatusId,
quoteId,
contentType,
poll,
preview,
idempotencyKey,
})
.then((data) => {
if (!data.error && !preview) {
store.dispatch('addNewStatuses', {
statuses: [data],
timeline: 'friends',
showImmediately: true,
noIdUpdate: true // To prevent missing notices on next pull.
noIdUpdate: true, // To prevent missing notices on next pull.
})
}
return data
})
.catch((err) => {
return {
error: err.message
error: err.message,
}
})
}
@ -57,27 +58,28 @@ const editStatus = ({
sensitive,
poll,
media = [],
contentType = 'text/plain'
contentType = 'text/plain',
}) => {
const mediaIds = map(media, 'id')
return apiService.editStatus({
id: statusId,
credentials: store.state.users.currentUser.credentials,
status,
spoilerText,
sensitive,
poll,
mediaIds,
contentType
})
return apiService
.editStatus({
id: statusId,
credentials: store.state.users.currentUser.credentials,
status,
spoilerText,
sensitive,
poll,
mediaIds,
contentType,
})
.then((data) => {
if (!data.error) {
store.dispatch('addNewStatuses', {
statuses: [data],
timeline: 'friends',
showImmediately: true,
noIdUpdate: true // To prevent missing notices on next pull.
noIdUpdate: true, // To prevent missing notices on next pull.
})
}
return data
@ -85,7 +87,7 @@ const editStatus = ({
.catch((err) => {
console.error('Error editing status', err)
return {
error: err.message
error: err.message,
}
})
}
@ -104,7 +106,7 @@ const statusPosterService = {
postStatus,
editStatus,
uploadMedia,
setMediaDescription
setMediaDescription,
}
export default statusPosterService

View file

@ -16,40 +16,40 @@ export const createStyleSheet = (id, priority = 1000) => {
rules: [],
ready: false,
priority,
clear () {
clear() {
this.rules = []
},
addRule (rule) {
addRule(rule) {
let newRule = rule
if (!CSS.supports?.('backdrop-filter', 'blur()')) {
newRule = newRule.replace(/backdrop-filter:[^;]+;/g, '') // Remove backdrop-filter
}
// firefox doesn't like invalid selectors
if (!CSS.supports?.('selector(::-webkit-scrollbar)') && newRule.startsWith('::-webkit')) {
if (
!CSS.supports?.('selector(::-webkit-scrollbar)') &&
newRule.startsWith('::-webkit')
) {
return
}
this.rules.push(
newRule
.replace(/var\(--shadowFilter\)[^;]*;/g, '') // Remove shadowFilter references
newRule.replace(/var\(--shadowFilter\)[^;]*;/g, ''), // Remove shadowFilter references
)
}
},
}
stylesheets[id] = newStyleSheet
return newStyleSheet
}
export const adoptStyleSheets = throttle(() => {
if (supportsAdoptedStyleSheets) {
document.adoptedStyleSheets = Object
.values(stylesheets)
.filter(x => x.ready)
document.adoptedStyleSheets = Object.values(stylesheets)
.filter((x) => x.ready)
.sort((a, b) => a.priority - b.priority)
.map(sheet => {
.map((sheet) => {
const css = new CSSStyleSheet()
sheet.rules.forEach(r => css.insertRule(r))
sheet.rules.forEach((r) => css.insertRule(r))
return css
})
} else {
@ -59,12 +59,11 @@ export const adoptStyleSheets = throttle(() => {
holder.sheet.deleteRule(i)
}
Object
.values(stylesheets)
.filter(x => x.ready)
Object.values(stylesheets)
.filter((x) => x.ready)
.sort((a, b) => a.priority - b.priority)
.forEach(sheet => {
sheet.rules.forEach(r => holder.sheet.insertRule(r))
.forEach((sheet) => {
sheet.rules.forEach((r) => holder.sheet.insertRule(r))
})
}
// Some older browsers do not support document.adoptedStyleSheets.
@ -73,7 +72,6 @@ export const adoptStyleSheets = throttle(() => {
// is nothing to do here.
}, 500)
const EAGER_STYLE_ID = 'pleroma-eager-styles'
const LAZY_STYLE_ID = 'pleroma-lazy-styles'
@ -81,15 +79,15 @@ export const generateTheme = (inputRuleset, callbacks, debug) => {
const {
onNewRule = () => {},
onLazyFinished = () => {},
onEagerFinished = () => {}
onEagerFinished = () => {},
} = callbacks
const themes3 = init({
inputRuleset,
debug
debug,
})
getCssRules(themes3.eager, debug).forEach(rule => {
getCssRules(themes3.eager, debug).forEach((rule) => {
// Hacks to support multiple selectors on same component
onNewRule(rule, false)
})
@ -103,8 +101,11 @@ export const generateTheme = (inputRuleset, callbacks, debug) => {
// let t0 = performance.now()
const processChunk = () => {
const chunk = chunks[counter]
Promise.all(chunk.map(x => x())).then(result => {
getCssRules(result.filter(x => x), debug).forEach(rule => {
Promise.all(chunk.map((x) => x())).then((result) => {
getCssRules(
result.filter((x) => x),
debug,
).forEach((rule) => {
onNewRule(rule, true)
})
// const t1 = performance.now()
@ -131,8 +132,8 @@ export const tryLoadCache = async () => {
const eagerStyles = createStyleSheet(EAGER_STYLE_ID, 10)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID, 20)
cache.data[0].forEach(rule => eagerStyles.addRule(rule))
cache.data[1].forEach(rule => lazyStyles.addRule(rule))
cache.data[0].forEach((rule) => eagerStyles.addRule(rule))
cache.data[1].forEach((rule) => lazyStyles.addRule(rule))
eagerStyles.ready = true
lazyStyles.ready = true
@ -140,7 +141,7 @@ export const tryLoadCache = async () => {
console.info(`Loaded theme from cache`)
return true
} else {
console.warn('Engine checksum doesn\'t match, cache not usable, clearing')
console.warn("Engine checksum doesn't match, cache not usable, clearing")
localStorage.removeItem('pleroma-fe-theme-cache')
}
} catch (e) {
@ -153,7 +154,7 @@ export const applyTheme = (
input,
onEagerFinish = () => {},
onFinish = () => {},
debug
debug,
) => {
const eagerStyles = createStyleSheet(EAGER_STYLE_ID, 10)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID, 20)
@ -164,29 +165,34 @@ export const applyTheme = (
const { lazyProcessFunc } = generateTheme(
input,
{
onNewRule (rule, isLazy) {
onNewRule(rule, isLazy) {
if (isLazy) {
lazyStyles.addRule(rule)
} else {
eagerStyles.addRule(rule)
}
},
onEagerFinished () {
onEagerFinished() {
eagerStyles.ready = true
adoptStyleSheets()
onEagerFinish()
console.info('Eager part of theme finished, waiting for lazy part to finish to store cache')
console.info(
'Eager part of theme finished, waiting for lazy part to finish to store cache',
)
},
onLazyFinished () {
onLazyFinished() {
lazyStyles.ready = true
adoptStyleSheets()
const cache = { engineChecksum: getEngineChecksum(), data: [eagerStyles.rules, lazyStyles.rules] }
const cache = {
engineChecksum: getEngineChecksum(),
data: [eagerStyles.rules, lazyStyles.rules],
}
onFinish(cache)
localforage.setItem('pleromafe-theme-cache', cache)
console.info('Theme cache stored')
}
},
},
debug
debug,
)
setTimeout(lazyProcessFunc, 0)
@ -202,18 +208,19 @@ const extractStyleConfig = ({
navbarSize,
panelHeaderSize,
textSize,
forcedRoundness
forcedRoundness,
}) => {
const result = {
sidebarColumnWidth,
contentColumnWidth,
notifsColumnWidth,
themeEditorMinWidth: parseInt(themeEditorMinWidth) === 0 ? 'fit-content' : themeEditorMinWidth,
themeEditorMinWidth:
parseInt(themeEditorMinWidth) === 0 ? 'fit-content' : themeEditorMinWidth,
emojiReactionsScale,
emojiSize,
navbarSize,
panelHeaderSize,
textSize
textSize,
}
switch (forcedRoundness) {
@ -243,10 +250,10 @@ export const applyConfig = (input) => {
return
}
const rules = Object
.entries(config)
const rules = Object.entries(config)
.filter(([, v]) => v)
.map(([k, v]) => `--${k}: ${v}`).join(';')
.map(([k, v]) => `--${k}: ${v}`)
.join(';')
const styleSheet = createStyleSheet('theme-holder', 30)
@ -270,28 +277,27 @@ export const getResourcesIndex = async (url, parser = JSON.parse) => {
let custom
const resourceTransform = (resources) => {
return Object
.entries(resources)
.map(([k, v]) => {
if (typeof v === 'object') {
return [k, () => Promise.resolve(v)]
} else if (typeof v === 'string') {
return [
k,
() => window
return Object.entries(resources).map(([k, v]) => {
if (typeof v === 'object') {
return [k, () => Promise.resolve(v)]
} else if (typeof v === 'string') {
return [
k,
() =>
window
.fetch(v, { cache })
.then(data => data.text())
.then(text => parser(text))
.catch(e => {
.then((data) => data.text())
.then((text) => parser(text))
.catch((e) => {
console.error(e)
return null
})
]
} else {
console.error(`Unknown resource format - ${k} is a ${typeof v}`)
return [k, null]
}
})
}),
]
} else {
console.error(`Unknown resource format - ${k} is a ${typeof v}`)
return [k, null]
}
})
}
try {
@ -314,7 +320,11 @@ export const getResourcesIndex = async (url, parser = JSON.parse) => {
const total = [...custom, ...builtin]
if (total.length === 0) {
return Promise.reject(new Error(`Resource at ${url} and ${customUrl} completely unavailable. Panicking`))
return Promise.reject(
new Error(
`Resource at ${url} and ${customUrl} completely unavailable. Panicking`,
),
)
}
return Promise.resolve(Object.fromEntries(total))
}

View file

@ -1,89 +1,100 @@
/* global process */
function urlBase64ToUint8Array (base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4)
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/')
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const rawData = window.atob(base64)
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)))
}
export function isSWSupported () {
export function isSWSupported() {
return 'serviceWorker' in navigator
}
function isPushSupported () {
function isPushSupported() {
return 'PushManager' in window
}
function getOrCreateServiceWorker () {
function getOrCreateServiceWorker() {
if (!isSWSupported()) return
const swType = process.env.HAS_MODULE_SERVICE_WORKER ? 'module' : 'classic'
return navigator.serviceWorker.register('/sw-pleroma.js', { type: swType })
.catch((err) => console.error('Unable to get or create a service worker.', err))
return navigator.serviceWorker
.register('/sw-pleroma.js', { type: swType })
.catch((err) =>
console.error('Unable to get or create a service worker.', err),
)
}
function subscribePush (registration, isEnabled, vapidPublicKey) {
if (!isEnabled) return Promise.reject(new Error('Web Push is disabled in config'))
if (!vapidPublicKey) return Promise.reject(new Error('VAPID public key is not found'))
function subscribePush(registration, isEnabled, vapidPublicKey) {
if (!isEnabled)
return Promise.reject(new Error('Web Push is disabled in config'))
if (!vapidPublicKey)
return Promise.reject(new Error('VAPID public key is not found'))
const subscribeOptions = {
userVisibleOnly: false,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
}
return registration.pushManager.subscribe(subscribeOptions)
}
function unsubscribePush (registration) {
return registration.pushManager.getSubscription()
.then((subscription) => {
if (subscription === null) { return }
return subscription.unsubscribe()
})
function unsubscribePush(registration) {
return registration.pushManager.getSubscription().then((subscription) => {
if (subscription === null) {
return
}
return subscription.unsubscribe()
})
}
function deleteSubscriptionFromBackEnd (token) {
function deleteSubscriptionFromBackEnd(token) {
return fetch('/api/v1/push/subscription/', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
Authorization: `Bearer ${token}`,
},
}).then((response) => {
if (!response.ok) throw new Error('Bad status code from server.')
return response
})
}
function sendSubscriptionToBackEnd (subscription, token, notificationVisibility) {
return window.fetch('/api/v1/push/subscription/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
subscription,
data: {
alerts: {
follow: notificationVisibility.follows,
favourite: notificationVisibility.likes,
mention: notificationVisibility.mentions,
reblog: notificationVisibility.repeats,
move: notificationVisibility.moves
}
}
function sendSubscriptionToBackEnd(
subscription,
token,
notificationVisibility,
) {
return window
.fetch('/api/v1/push/subscription/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
subscription,
data: {
alerts: {
follow: notificationVisibility.follows,
favourite: notificationVisibility.likes,
mention: notificationVisibility.mentions,
reblog: notificationVisibility.repeats,
move: notificationVisibility.moves,
},
},
}),
})
.then((response) => {
if (!response.ok) throw new Error('Bad status code from server.')
return response.json()
})
.then((responseData) => {
if (!responseData.id) throw new Error('Bad response from server.')
return responseData
})
}).then((response) => {
if (!response.ok) throw new Error('Bad status code from server.')
return response.json()
}).then((responseData) => {
if (!responseData.id) throw new Error('Bad response from server.')
return responseData
})
}
export async function initServiceWorker (store) {
export async function initServiceWorker(store) {
if (!isSWSupported()) return
await getOrCreateServiceWorker()
navigator.serviceWorker.addEventListener('message', (event) => {
@ -97,16 +108,18 @@ export async function initServiceWorker (store) {
})
}
export async function showDesktopNotification (content) {
export async function showDesktopNotification(content) {
if (!isSWSupported) return
const { active: sw } = (await window.navigator.serviceWorker.getRegistration()) || {}
const { active: sw } =
(await window.navigator.serviceWorker.getRegistration()) || {}
if (!sw) return console.error('No serviceworker found!')
sw.postMessage({ type: 'desktopNotification', content })
}
export async function closeDesktopNotification ({ id }) {
export async function closeDesktopNotification({ id }) {
if (!isSWSupported) return
const { active: sw } = (await window.navigator.serviceWorker.getRegistration()) || {}
const { active: sw } =
(await window.navigator.serviceWorker.getRegistration()) || {}
if (!sw) return console.error('No serviceworker found!')
if (id >= 0) {
sw.postMessage({ type: 'desktopNotificationClose', content: { id } })
@ -115,36 +128,53 @@ export async function closeDesktopNotification ({ id }) {
}
}
export async function updateFocus () {
export async function updateFocus() {
if (!isSWSupported) return
const { active: sw } = (await window.navigator.serviceWorker.getRegistration()) || {}
const { active: sw } =
(await window.navigator.serviceWorker.getRegistration()) || {}
if (!sw) return console.error('No serviceworker found!')
sw.postMessage({ type: 'updateFocus' })
}
export function registerPushNotifications (isEnabled, vapidPublicKey, token, notificationVisibility) {
export function registerPushNotifications(
isEnabled,
vapidPublicKey,
token,
notificationVisibility,
) {
if (isPushSupported()) {
getOrCreateServiceWorker()
.then((registration) => subscribePush(registration, isEnabled, vapidPublicKey))
.then((subscription) => sendSubscriptionToBackEnd(subscription, token, notificationVisibility))
.catch((e) => console.warn(`Failed to setup Web Push Notifications: ${e.message}`))
.then((registration) =>
subscribePush(registration, isEnabled, vapidPublicKey),
)
.then((subscription) =>
sendSubscriptionToBackEnd(subscription, token, notificationVisibility),
)
.catch((e) =>
console.warn(`Failed to setup Web Push Notifications: ${e.message}`),
)
}
}
export function unregisterPushNotifications (token) {
export function unregisterPushNotifications(token) {
if (isPushSupported()) {
Promise.all([
deleteSubscriptionFromBackEnd(token),
getOrCreateServiceWorker()
.then((registration) => {
return unsubscribePush(registration).then((result) => [registration, result])
return unsubscribePush(registration).then((result) => [
registration,
result,
])
})
.then(([, unsubResult]) => {
if (!unsubResult) {
console.warn('Push subscription cancellation wasn\'t successful')
console.warn("Push subscription cancellation wasn't successful")
}
})
]).catch((e) => console.warn(`Failed to disable Web Push Notifications: ${e.message}`))
}),
]).catch((e) =>
console.warn(`Failed to disable Web Push Notifications: ${e.message}`),
)
}
}

View file

@ -2,7 +2,8 @@ import { convert } from 'chromatism'
import { hex2rgb, rgba2css } from '../color_convert/color_convert.js'
export const getCssColorString = (color, alpha = 1) => rgba2css({ ...convert(color).rgb, a: alpha })
export const getCssColorString = (color, alpha = 1) =>
rgba2css({ ...convert(color).rgb, a: alpha })
export const getCssShadow = (input, usesDropShadow) => {
if (input.length === 0) {
@ -10,16 +11,17 @@ export const getCssShadow = (input, usesDropShadow) => {
}
return input
.filter(_ => usesDropShadow ? _.inset : _)
.map((shad) => [
shad.x,
shad.y,
shad.blur,
shad.spread
].map(_ => _ + 'px ').concat([
getCssColorString(shad.color, shad.alpha),
shad.inset ? 'inset' : ''
]).join(' ')).join(', ')
.filter((_) => (usesDropShadow ? _.inset : _))
.map((shad) =>
[shad.x, shad.y, shad.blur, shad.spread]
.map((_) => _ + 'px ')
.concat([
getCssColorString(shad.color, shad.alpha),
shad.inset ? 'inset' : '',
])
.join(' '),
)
.join(', ')
}
export const getCssShadowFilter = (input) => {
@ -27,124 +29,156 @@ export const getCssShadowFilter = (input) => {
return 'none'
}
return input
// drop-shadow doesn't support inset or spread
.filter((shad) => !shad.inset && Number(shad.spread) === 0)
.map((shad) => [
shad.x,
shad.y,
// drop-shadow's blur is twice as strong compared to box-shadow
shad.blur / 2
].map(_ => _ + 'px').concat([
getCssColorString(shad.color, shad.alpha)
]).join(' '))
.map(_ => `drop-shadow(${_})`)
.join(' ')
return (
input
// drop-shadow doesn't support inset or spread
.filter((shad) => !shad.inset && Number(shad.spread) === 0)
.map((shad) =>
[
shad.x,
shad.y,
// drop-shadow's blur is twice as strong compared to box-shadow
shad.blur / 2,
]
.map((_) => _ + 'px')
.concat([getCssColorString(shad.color, shad.alpha)])
.join(' '),
)
.map((_) => `drop-shadow(${_})`)
.join(' ')
)
}
// `debug` changes what backgrounds are used to "stacked" solid colors so you can see
// what theme engine "thinks" is actual background color is for purposes of text color
// generation and for when --stacked variable is used
export const getCssRules = (rules, debug) => rules.map(rule => {
let selector = rule.selector
if (!selector) {
selector = 'html'
}
const header = selector + ' {'
const footer = '}'
const virtualDirectives = Object.entries(rule.virtualDirectives || {}).map(([k, v]) => {
return ' ' + k + ': ' + v
}).join(';\n')
const directives = Object.entries(rule.directives).map(([k, v]) => {
switch (k) {
case 'roundness': {
return ' ' + [
'--roundness: ' + v + 'px'
].join(';\n ')
export const getCssRules = (rules, debug) =>
rules
.map((rule) => {
let selector = rule.selector
if (!selector) {
selector = 'html'
}
case 'shadow': {
if (!rule.dynamicVars.shadow) {
return ''
}
return ' ' + [
'--shadow: ' + getCssShadow(rule.dynamicVars.shadow),
'--shadowFilter: ' + getCssShadowFilter(rule.dynamicVars.shadow),
'--shadowInset: ' + getCssShadow(rule.dynamicVars.shadow, true)
].join(';\n ')
}
case 'background': {
if (debug) {
return `
const header = selector + ' {'
const footer = '}'
const virtualDirectives = Object.entries(rule.virtualDirectives || {})
.map(([k, v]) => {
return ' ' + k + ': ' + v
})
.join(';\n')
const directives = Object.entries(rule.directives)
.map(([k, v]) => {
switch (k) {
case 'roundness': {
return ' ' + ['--roundness: ' + v + 'px'].join(';\n ')
}
case 'shadow': {
if (!rule.dynamicVars.shadow) {
return ''
}
return (
' ' +
[
'--shadow: ' + getCssShadow(rule.dynamicVars.shadow),
'--shadowFilter: ' +
getCssShadowFilter(rule.dynamicVars.shadow),
'--shadowInset: ' +
getCssShadow(rule.dynamicVars.shadow, true),
].join(';\n ')
)
}
case 'background': {
if (debug) {
return `
--background: ${getCssColorString(rule.dynamicVars.stacked)};
background-color: ${getCssColorString(rule.dynamicVars.stacked)};
`
}
if (v === 'transparent') {
if (rule.component === 'Root') return null
return [
rule.directives.backgroundNoCssColor !== 'yes' ? ('background-color: ' + v) : '',
' --background: ' + v
].filter(x => x).join(';\n')
}
const color = getCssColorString(rule.dynamicVars.background, rule.directives.opacity)
const cssDirectives = ['--background: ' + color]
if (rule.directives.backgroundNoCssColor !== 'yes') {
cssDirectives.push('background-color: ' + color)
}
return cssDirectives.filter(x => x).join(';\n')
}
case 'blur': {
const cssDirectives = []
if (rule.directives.opacity < 1) {
cssDirectives.push(`--backdrop-filter: blur(${v}) `)
if (rule.directives.backgroundNoCssColor !== 'yes') {
cssDirectives.push(`backdrop-filter: blur(${v}) `)
}
}
return cssDirectives.join(';\n')
}
case 'font': {
return 'font-family: ' + v
}
case 'textColor': {
if (rule.directives.textNoCssColor === 'yes') { return '' }
return 'color: ' + v
}
default:
if (k.startsWith('--')) {
const [type, value] = v.split('|').map(x => x.trim())
switch (type) {
case 'color': {
const color = rule.dynamicVars[k]
if (typeof color === 'string') {
return k + ': ' + rgba2css(hex2rgb(color))
} else {
return k + ': ' + rgba2css(color)
}
if (v === 'transparent') {
if (rule.component === 'Root') return null
return [
rule.directives.backgroundNoCssColor !== 'yes'
? 'background-color: ' + v
: '',
' --background: ' + v,
]
.filter((x) => x)
.join(';\n')
}
const color = getCssColorString(
rule.dynamicVars.background,
rule.directives.opacity,
)
const cssDirectives = ['--background: ' + color]
if (rule.directives.backgroundNoCssColor !== 'yes') {
cssDirectives.push('background-color: ' + color)
}
return cssDirectives.filter((x) => x).join(';\n')
}
case 'blur': {
const cssDirectives = []
if (rule.directives.opacity < 1) {
cssDirectives.push(`--backdrop-filter: blur(${v}) `)
if (rule.directives.backgroundNoCssColor !== 'yes') {
cssDirectives.push(`backdrop-filter: blur(${v}) `)
}
}
return cssDirectives.join(';\n')
}
case 'font': {
return 'font-family: ' + v
}
case 'textColor': {
if (rule.directives.textNoCssColor === 'yes') {
return ''
}
return 'color: ' + v
}
case 'generic':
return k + ': ' + value
default:
if (k.startsWith('--')) {
const [type, value] = v.split('|').map((x) => x.trim())
switch (type) {
case 'color': {
const color = rule.dynamicVars[k]
if (typeof color === 'string') {
return k + ': ' + rgba2css(hex2rgb(color))
} else {
return k + ': ' + rgba2css(color)
}
}
case 'generic':
return k + ': ' + value
default:
return null
}
}
return null
}
}
return null
}
}).filter(x => x).map(x => ' ' + x + ';').join('\n')
})
.filter((x) => x)
.map((x) => ' ' + x + ';')
.join('\n')
return [
header,
directives,
(rule.component === 'Text' && rule.state.indexOf('faint') < 0 && rule.directives.textNoCssColor !== 'yes') ? ' color: var(--text);' : '',
virtualDirectives,
footer
].filter(x => x).join('\n')
}).filter(x => x)
return [
header,
directives,
rule.component === 'Text' &&
rule.state.indexOf('faint') < 0 &&
rule.directives.textNoCssColor !== 'yes'
? ' color: var(--text);'
: '',
virtualDirectives,
footer,
]
.filter((x) => x)
.join('\n')
})
.filter((x) => x)
export const getScopedVersion = (rules, newScope) => {
return rules.map(x => {
return rules.map((x) => {
if (x.startsWith('html')) {
return x.replace('html', newScope)
} else if (x.startsWith('#content')) {

View file

@ -1,7 +1,17 @@
import { flattenDeep } from 'lodash'
export const deserializeShadow = string => {
const modes = ['_full', 'inset', 'x', 'y', 'blur', 'spread', 'color', 'alpha', 'name']
export const deserializeShadow = (string) => {
const modes = [
'_full',
'inset',
'x',
'y',
'blur',
'spread',
'color',
'alpha',
'name',
]
const regexPrep = [
// inset keyword (optional)
'^',
@ -20,7 +30,7 @@ export const deserializeShadow = string => {
'(?:\\s+\\/\\s+([0-9]+(?:\\.[0-9]+)?)\\s*)?',
// name
'(?:\\s+#(\\w+)\\s*)?',
'$'
'$',
].join('')
const regex = new RegExp(regexPrep, 'gis') // global, (stable) indices, single-string
const result = regex.exec(string)
@ -32,20 +42,26 @@ export const deserializeShadow = string => {
}
} else {
const numeric = new Set(['x', 'y', 'blur', 'spread', 'alpha'])
const { x, y, blur, spread, alpha, inset, color, name } = Object.fromEntries(modes.map((mode, i) => {
if (numeric.has(mode)) {
const number = Number(result[i])
if (Number.isNaN(number)) {
if (mode === 'alpha') return [mode, 1]
return [mode, 0]
}
return [mode, number]
} else if (mode === 'inset') {
return [mode, !!result[i]]
} else {
return [mode, result[i]]
}
}).filter(([, v]) => v !== false).slice(1))
const { x, y, blur, spread, alpha, inset, color, name } =
Object.fromEntries(
modes
.map((mode, i) => {
if (numeric.has(mode)) {
const number = Number(result[i])
if (Number.isNaN(number)) {
if (mode === 'alpha') return [mode, 1]
return [mode, 0]
}
return [mode, number]
} else if (mode === 'inset') {
return [mode, !!result[i]]
} else {
return [mode, result[i]]
}
})
.filter(([, v]) => v !== false)
.slice(1),
)
return { x, y, blur, spread, color, alpha, inset, name }
}
@ -94,77 +110,87 @@ const parseIss = (input) => {
}
export const deserialize = (input) => {
const ast = parseIss(input)
const finalResult = ast.filter(i => i.selector != null).map(item => {
const { selector, content } = item
let stateCount = 0
const selectors = selector.split(/,/g)
const result = selectors.map(selector => {
const output = { component: '' }
let currentDepth = null
const finalResult = ast
.filter((i) => i.selector != null)
.map((item) => {
const { selector, content } = item
let stateCount = 0
const selectors = selector.split(/,/g)
const result = selectors.map((selector) => {
const output = { component: '' }
let currentDepth = null
selector.split(/ /g).reverse().forEach((fragment, index, arr) => {
const fragmentObject = { component: '' }
selector
.split(/ /g)
.reverse()
.forEach((fragment, index, arr) => {
const fragmentObject = { component: '' }
let mode = 'component'
for (let i = 0; i < fragment.length; i++) {
const char = fragment[i]
switch (char) {
case '.': {
mode = 'variant'
fragmentObject.variant = ''
break
}
case ':': {
mode = 'state'
fragmentObject.state = fragmentObject.state || []
stateCount++
break
}
default: {
if (mode === 'state') {
const currentState = fragmentObject.state[stateCount - 1]
if (currentState == null) {
fragmentObject.state.push('')
let mode = 'component'
for (let i = 0; i < fragment.length; i++) {
const char = fragment[i]
switch (char) {
case '.': {
mode = 'variant'
fragmentObject.variant = ''
break
}
case ':': {
mode = 'state'
fragmentObject.state = fragmentObject.state || []
stateCount++
break
}
default: {
if (mode === 'state') {
const currentState = fragmentObject.state[stateCount - 1]
if (currentState == null) {
fragmentObject.state.push('')
}
fragmentObject.state[stateCount - 1] += char
} else {
fragmentObject[mode] += char
}
}
fragmentObject.state[stateCount - 1] += char
} else {
fragmentObject[mode] += char
}
}
}
}
if (currentDepth !== null) {
currentDepth.parent = { ...fragmentObject }
currentDepth = currentDepth.parent
} else {
Object.keys(fragmentObject).forEach(key => {
output[key] = fragmentObject[key]
if (currentDepth !== null) {
currentDepth.parent = { ...fragmentObject }
currentDepth = currentDepth.parent
} else {
Object.keys(fragmentObject).forEach((key) => {
output[key] = fragmentObject[key]
})
if (index !== arr.length - 1) {
output.parent = { component: '' }
}
currentDepth = output
}
})
if (index !== (arr.length - 1)) {
output.parent = { component: '' }
}
currentDepth = output
}
output.directives = Object.fromEntries(
content.map((d) => {
const [property, value] = d.split(':')
let realValue = (value || '').trim()
if (property === 'shadow') {
if (realValue === 'none') {
realValue = []
} else {
realValue = value
.split(',')
.map((v) => deserializeShadow(v.trim()))
}
}
if (!Number.isNaN(Number(value))) {
realValue = Number(value)
}
return [property, realValue]
}),
)
return output
})
output.directives = Object.fromEntries(content.map(d => {
const [property, value] = d.split(':')
let realValue = (value || '').trim()
if (property === 'shadow') {
if (realValue === 'none') {
realValue = []
} else {
realValue = value.split(',').map(v => deserializeShadow(v.trim()))
}
} if (!Number.isNaN(Number(value))) {
realValue = Number(value)
}
return [property, realValue]
}))
return output
return result
})
return result
})
return flattenDeep(finalResult)
}

View file

@ -14,40 +14,54 @@ export const serializeShadow = (s) => {
}
export const serialize = (ruleset) => {
return ruleset.map((rule) => {
if (Object.keys(rule.directives || {}).length === 0) return false
return ruleset
.map((rule) => {
if (Object.keys(rule.directives || {}).length === 0) return false
const header = unroll(rule).reverse().map(rule => {
const { component } = rule
const newVariant = (rule.variant == null || rule.variant === 'normal') ? '' : ('.' + rule.variant)
const newState = (rule.state || []).filter(st => st !== 'normal')
const header = unroll(rule)
.reverse()
.map((rule) => {
const { component } = rule
const newVariant =
rule.variant == null || rule.variant === 'normal'
? ''
: '.' + rule.variant
const newState = (rule.state || []).filter((st) => st !== 'normal')
return `${component}${newVariant}${newState.map(st => ':' + st).join('')}`
}).join(' ')
return `${component}${newVariant}${newState.map((st) => ':' + st).join('')}`
})
.join(' ')
const content = Object.entries(rule.directives).map(([directive, value]) => {
if (directive.startsWith('--')) {
const [valType, newValue] = value.split('|') // only first one! intentional!
switch (valType) {
case 'shadow':
return ` ${directive}: ${valType.trim()} | ${newValue.map(serializeShadow).map(s => s.trim()).join(', ')}`
default:
return ` ${directive}: ${valType.trim()} | ${newValue.trim()}`
}
} else {
switch (directive) {
case 'shadow':
if (value.length > 0) {
return ` ${directive}: ${value.map(serializeShadow).join(', ')}`
} else {
return ` ${directive}: none`
const content = Object.entries(rule.directives).map(
([directive, value]) => {
if (directive.startsWith('--')) {
const [valType, newValue] = value.split('|') // only first one! intentional!
switch (valType) {
case 'shadow':
return ` ${directive}: ${valType.trim()} | ${newValue
.map(serializeShadow)
.map((s) => s.trim())
.join(', ')}`
default:
return ` ${directive}: ${valType.trim()} | ${newValue.trim()}`
}
default:
return ` ${directive}: ${value}`
}
}
})
} else {
switch (directive) {
case 'shadow':
if (value.length > 0) {
return ` ${directive}: ${value.map(serializeShadow).join(', ')}`
} else {
return ` ${directive}: none`
}
default:
return ` ${directive}: ${value}`
}
}
},
)
return `${header} {\n${content.join(';\n')}\n}`
}).filter(x => x).join('\n\n')
return `${header} {\n${content.join(';\n')}\n}`
})
.filter((x) => x)
.join('\n\n')
}

View file

@ -15,18 +15,18 @@ export const unroll = (item) => {
// This gives you an array of arrays of all possible unique (i.e. order-insensitive) combinations
// Can only accept primitives. Duplicates are not supported and can cause unexpected behavior
export const getAllPossibleCombinations = (array) => {
const combos = [array.map(x => [x])]
const combos = [array.map((x) => [x])]
for (let comboSize = 2; comboSize <= array.length; comboSize++) {
const previous = combos[combos.length - 1]
const newCombos = previous.map(self => {
const newCombos = previous.map((self) => {
const selfSet = new Set()
self.forEach(x => selfSet.add(x))
const nonSelf = array.filter(x => !selfSet.has(x))
return nonSelf.map(x => [...self, x])
self.forEach((x) => selfSet.add(x))
const nonSelf = array.filter((x) => !selfSet.has(x))
return nonSelf.map((x) => [...self, x])
})
const flatCombos = newCombos.reduce((acc, x) => [...acc, ...x], [])
const uniqueComboStrings = new Set()
const uniqueCombos = flatCombos.map(sortBy).filter(x => {
const uniqueCombos = flatCombos.map(sortBy).filter((x) => {
if (uniqueComboStrings.has(x.join())) {
return false
} else {
@ -56,75 +56,98 @@ export const getAllPossibleCombinations = (array) => {
*
* @returns {String} CSS selector (or path)
*/
export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, liteMode, children) => {
const isParent = !!children
if (!rule && !isParent) return null
const component = components[rule.component]
const { states = {}, variants = {}, outOfTreeSelector } = component
export const genericRuleToSelector =
(components) => (rule, ignoreOutOfTreeSelector, liteMode, children) => {
const isParent = !!children
if (!rule && !isParent) return null
const component = components[rule.component]
const { states = {}, variants = {}, outOfTreeSelector } = component
const expand = (array = [], subArray = []) => {
if (array.length === 0) return subArray.map(x => [x])
if (subArray.length === 0) return array.map(x => [x])
return array.map(a => {
return subArray.map(b => [a, b])
}).flat()
}
let componentSelectors = Array.isArray(component.selector) ? component.selector : [component.selector]
if (ignoreOutOfTreeSelector || liteMode) componentSelectors = [componentSelectors[0]]
componentSelectors = componentSelectors.map(selector => {
if (selector === ':root') {
return ''
} else if (isParent) {
return selector
} else {
if (outOfTreeSelector && !ignoreOutOfTreeSelector) return outOfTreeSelector
return selector
const expand = (array = [], subArray = []) => {
if (array.length === 0) return subArray.map((x) => [x])
if (subArray.length === 0) return array.map((x) => [x])
return array
.map((a) => {
return subArray.map((b) => [a, b])
})
.flat()
}
})
const applicableVariantName = (rule.variant || 'normal')
let variantSelectors = null
if (applicableVariantName !== 'normal') {
variantSelectors = variants[applicableVariantName]
} else {
variantSelectors = variants?.normal ?? ''
let componentSelectors = Array.isArray(component.selector)
? component.selector
: [component.selector]
if (ignoreOutOfTreeSelector || liteMode)
componentSelectors = [componentSelectors[0]]
componentSelectors = componentSelectors.map((selector) => {
if (selector === ':root') {
return ''
} else if (isParent) {
return selector
} else {
if (outOfTreeSelector && !ignoreOutOfTreeSelector)
return outOfTreeSelector
return selector
}
})
const applicableVariantName = rule.variant || 'normal'
let variantSelectors = null
if (applicableVariantName !== 'normal') {
variantSelectors = variants[applicableVariantName]
} else {
variantSelectors = variants?.normal ?? ''
}
variantSelectors = Array.isArray(variantSelectors)
? variantSelectors
: [variantSelectors]
if (ignoreOutOfTreeSelector || liteMode)
variantSelectors = [variantSelectors[0]]
const applicableStates = (rule.state || []).filter((x) => x !== 'normal')
// const applicableStates = (rule.state || [])
const statesSelectors = applicableStates.map((state) => {
const selector = states[state] || ''
let arraySelector = Array.isArray(selector) ? selector : [selector]
if (ignoreOutOfTreeSelector || liteMode)
arraySelector = [arraySelector[0]]
arraySelector
.sort((a) => {
if (a.startsWith(':')) return 1
if (/^[a-z]/.exec(a)) return -1
else return 0
})
.join('')
return arraySelector
})
const statesSelectorsFlat = statesSelectors.reduce((acc, s) => {
return expand(acc, s).map((st) => st.join(''))
}, [])
const componentVariant = expand(componentSelectors, variantSelectors).map(
(cv) => cv.join(''),
)
const componentVariantStates = expand(
componentVariant,
statesSelectorsFlat,
).map((cvs) => cvs.join(''))
const selectors = expand(componentVariantStates, children).map((cvsc) =>
cvsc.join(' '),
)
/*
*/
if (rule.parent) {
return genericRuleToSelector(components)(
rule.parent,
ignoreOutOfTreeSelector,
liteMode,
selectors,
)
}
return selectors.join(', ').trim()
}
variantSelectors = Array.isArray(variantSelectors) ? variantSelectors : [variantSelectors]
if (ignoreOutOfTreeSelector || liteMode) variantSelectors = [variantSelectors[0]]
const applicableStates = (rule.state || []).filter(x => x !== 'normal')
// const applicableStates = (rule.state || [])
const statesSelectors = applicableStates.map(state => {
const selector = states[state] || ''
let arraySelector = Array.isArray(selector) ? selector : [selector]
if (ignoreOutOfTreeSelector || liteMode) arraySelector = [arraySelector[0]]
arraySelector
.sort((a) => {
if (a.startsWith(':')) return 1
if (/^[a-z]/.exec(a)) return -1
else return 0
})
.join('')
return arraySelector
})
const statesSelectorsFlat = statesSelectors.reduce((acc, s) => {
return expand(acc, s).map(st => st.join(''))
}, [])
const componentVariant = expand(componentSelectors, variantSelectors).map(cv => cv.join(''))
const componentVariantStates = expand(componentVariant, statesSelectorsFlat).map(cvs => cvs.join(''))
const selectors = expand(componentVariantStates, children).map(cvsc => cvsc.join(' '))
/*
*/
if (rule.parent) {
return genericRuleToSelector(components)(rule.parent, ignoreOutOfTreeSelector, liteMode, selectors)
}
return selectors.join(', ').trim()
}
/**
* Check if combination matches
@ -151,8 +174,8 @@ export const combinationsMatch = (criteria, subject, strict) => {
const criteriaStatesSet = new Set(criteria.state)
const setsAreEqual =
[...criteriaStatesSet].every(state => subjectStatesSet.has(state)) &&
[...subjectStatesSet].every(state => criteriaStatesSet.has(state))
[...criteriaStatesSet].every((state) => subjectStatesSet.has(state)) &&
[...subjectStatesSet].every((state) => criteriaStatesSet.has(state))
if (!setsAreEqual) return false
}
@ -168,7 +191,7 @@ export const combinationsMatch = (criteria, subject, strict) => {
*
* @return function that returns true/false if subject matches
*/
export const findRules = (criteria, strict) => subject => {
export const findRules = (criteria, strict) => (subject) => {
// If we searching for "general" rules - ignore "specific" ones
if (criteria.parent === null && !!subject.parent) return false
if (!combinationsMatch(criteria, subject, strict)) return false
@ -186,14 +209,15 @@ export const findRules = (criteria, strict) => subject => {
const criteriaParent = pathCriteria[i]
const subjectParent = pathSubject[i]
if (!subjectParent) return true
if (!combinationsMatch(criteriaParent, subjectParent, strict)) return false
if (!combinationsMatch(criteriaParent, subjectParent, strict))
return false
}
}
return true
}
// Pre-fills 'normal' state/variant if missing
export const normalizeCombination = rule => {
export const normalizeCombination = (rule) => {
rule.variant = rule.variant ?? 'normal'
rule.state = [...new Set(['normal', ...(rule.state || [])])]
}

View file

@ -25,7 +25,7 @@ export const LAYERS = {
alertPanel: 'panel',
poll: 'bg',
chatBg: 'underlay',
chatMessage: 'chatBg'
chatMessage: 'chatBg',
}
/* By default opacity slots have 1 as default opacity
@ -37,7 +37,7 @@ export const DEFAULT_OPACITY = {
input: 0.5,
faint: 0.5,
underlay: 0.15,
alertPopup: 0.95
alertPopup: 0.95,
}
/** SUBJECT TO CHANGE IN THE FUTURE, this is all beta
@ -82,45 +82,45 @@ export const SLOT_INHERITANCE = {
bg: {
depends: [],
opacity: 'bg',
priority: 1
priority: 1,
},
wallpaper: {
depends: ['bg'],
color: (mod, bg) => brightness(-2 * mod, bg).rgb
color: (mod, bg) => brightness(-2 * mod, bg).rgb,
},
fg: {
depends: [],
priority: 1
priority: 1,
},
text: {
depends: [],
layer: 'bg',
opacity: null,
priority: 1
priority: 1,
},
underlay: {
default: '#000000',
opacity: 'underlay'
opacity: 'underlay',
},
link: {
depends: ['accent'],
priority: 1
priority: 1,
},
accent: {
depends: ['link'],
priority: 1
priority: 1,
},
faint: {
depends: ['text'],
opacity: 'faint'
opacity: 'faint',
},
faintLink: {
depends: ['link'],
opacity: 'faint'
opacity: 'faint',
},
postFaintLink: {
depends: ['postLink'],
opacity: 'faint'
opacity: 'faint',
},
cBlue: '#0000ff',
@ -133,101 +133,101 @@ export const SLOT_INHERITANCE = {
color: (mod, bg) => ({
r: Math.floor(bg.r * 0.53),
g: Math.floor(bg.g * 0.56),
b: Math.floor(bg.b * 0.59)
})
b: Math.floor(bg.b * 0.59),
}),
},
profileTint: {
depends: ['bg'],
layer: 'profileTint',
opacity: 'profileTint'
opacity: 'profileTint',
},
highlight: {
depends: ['bg'],
color: (mod, bg) => brightness(5 * mod, bg).rgb
color: (mod, bg) => brightness(5 * mod, bg).rgb,
},
highlightLightText: {
depends: ['lightText'],
layer: 'highlight',
textColor: true
textColor: true,
},
highlightPostLink: {
depends: ['postLink'],
layer: 'highlight',
textColor: 'preserve'
textColor: 'preserve',
},
highlightFaintText: {
depends: ['faint'],
layer: 'highlight',
textColor: true
textColor: true,
},
highlightFaintLink: {
depends: ['faintLink'],
layer: 'highlight',
textColor: 'preserve'
textColor: 'preserve',
},
highlightPostFaintLink: {
depends: ['postFaintLink'],
layer: 'highlight',
textColor: 'preserve'
textColor: 'preserve',
},
highlightText: {
depends: ['text'],
layer: 'highlight',
textColor: true
textColor: true,
},
highlightLink: {
depends: ['link'],
layer: 'highlight',
textColor: 'preserve'
textColor: 'preserve',
},
highlightIcon: {
depends: ['highlight', 'highlightText'],
color: (mod, bg, text) => mixrgb(bg, text)
color: (mod, bg, text) => mixrgb(bg, text),
},
popover: {
depends: ['bg'],
opacity: 'popover'
opacity: 'popover',
},
popoverLightText: {
depends: ['lightText'],
layer: 'popover',
textColor: true
textColor: true,
},
popoverPostLink: {
depends: ['postLink'],
layer: 'popover',
textColor: 'preserve'
textColor: 'preserve',
},
popoverFaintText: {
depends: ['faint'],
layer: 'popover',
textColor: true
textColor: true,
},
popoverFaintLink: {
depends: ['faintLink'],
layer: 'popover',
textColor: 'preserve'
textColor: 'preserve',
},
popoverPostFaintLink: {
depends: ['postFaintLink'],
layer: 'popover',
textColor: 'preserve'
textColor: 'preserve',
},
popoverText: {
depends: ['text'],
layer: 'popover',
textColor: true
textColor: true,
},
popoverLink: {
depends: ['link'],
layer: 'popover',
textColor: 'preserve'
textColor: 'preserve',
},
popoverIcon: {
depends: ['popover', 'popoverText'],
color: (mod, bg, text) => mixrgb(bg, text)
color: (mod, bg, text) => mixrgb(bg, text),
},
selectedPost: '--highlight',
@ -235,201 +235,201 @@ export const SLOT_INHERITANCE = {
depends: ['highlightFaintText'],
layer: 'highlight',
variant: 'selectedPost',
textColor: true
textColor: true,
},
selectedPostLightText: {
depends: ['highlightLightText'],
layer: 'highlight',
variant: 'selectedPost',
textColor: true
textColor: true,
},
selectedPostPostLink: {
depends: ['highlightPostLink'],
layer: 'highlight',
variant: 'selectedPost',
textColor: 'preserve'
textColor: 'preserve',
},
selectedPostFaintLink: {
depends: ['highlightFaintLink'],
layer: 'highlight',
variant: 'selectedPost',
textColor: 'preserve'
textColor: 'preserve',
},
selectedPostText: {
depends: ['highlightText'],
layer: 'highlight',
variant: 'selectedPost',
textColor: true
textColor: true,
},
selectedPostLink: {
depends: ['highlightLink'],
layer: 'highlight',
variant: 'selectedPost',
textColor: 'preserve'
textColor: 'preserve',
},
selectedPostIcon: {
depends: ['selectedPost', 'selectedPostText'],
color: (mod, bg, text) => mixrgb(bg, text)
color: (mod, bg, text) => mixrgb(bg, text),
},
selectedMenu: {
depends: ['bg'],
color: (mod, bg) => brightness(5 * mod, bg).rgb
color: (mod, bg) => brightness(5 * mod, bg).rgb,
},
selectedMenuLightText: {
depends: ['highlightLightText'],
layer: 'selectedMenu',
variant: 'selectedMenu',
textColor: true
textColor: true,
},
selectedMenuFaintText: {
depends: ['highlightFaintText'],
layer: 'selectedMenu',
variant: 'selectedMenu',
textColor: true
textColor: true,
},
selectedMenuFaintLink: {
depends: ['highlightFaintLink'],
layer: 'selectedMenu',
variant: 'selectedMenu',
textColor: 'preserve'
textColor: 'preserve',
},
selectedMenuText: {
depends: ['highlightText'],
layer: 'selectedMenu',
variant: 'selectedMenu',
textColor: true
textColor: true,
},
selectedMenuLink: {
depends: ['highlightLink'],
layer: 'selectedMenu',
variant: 'selectedMenu',
textColor: 'preserve'
textColor: 'preserve',
},
selectedMenuIcon: {
depends: ['selectedMenu', 'selectedMenuText'],
color: (mod, bg, text) => mixrgb(bg, text)
color: (mod, bg, text) => mixrgb(bg, text),
},
selectedMenuPopover: {
depends: ['popover'],
color: (mod, bg) => brightness(5 * mod, bg).rgb
color: (mod, bg) => brightness(5 * mod, bg).rgb,
},
selectedMenuPopoverLightText: {
depends: ['selectedMenuLightText'],
layer: 'selectedMenuPopover',
variant: 'selectedMenuPopover',
textColor: true
textColor: true,
},
selectedMenuPopoverFaintText: {
depends: ['selectedMenuFaintText'],
layer: 'selectedMenuPopover',
variant: 'selectedMenuPopover',
textColor: true
textColor: true,
},
selectedMenuPopoverFaintLink: {
depends: ['selectedMenuFaintLink'],
layer: 'selectedMenuPopover',
variant: 'selectedMenuPopover',
textColor: 'preserve'
textColor: 'preserve',
},
selectedMenuPopoverText: {
depends: ['selectedMenuText'],
layer: 'selectedMenuPopover',
variant: 'selectedMenuPopover',
textColor: true
textColor: true,
},
selectedMenuPopoverLink: {
depends: ['selectedMenuLink'],
layer: 'selectedMenuPopover',
variant: 'selectedMenuPopover',
textColor: 'preserve'
textColor: 'preserve',
},
selectedMenuPopoverIcon: {
depends: ['selectedMenuPopover', 'selectedMenuText'],
color: (mod, bg, text) => mixrgb(bg, text)
color: (mod, bg, text) => mixrgb(bg, text),
},
lightText: {
depends: ['text'],
layer: 'bg',
textColor: 'preserve',
color: (mod, text) => brightness(20 * mod, text).rgb
color: (mod, text) => brightness(20 * mod, text).rgb,
},
postLink: {
depends: ['link'],
layer: 'bg',
textColor: 'preserve'
textColor: 'preserve',
},
postGreentext: {
depends: ['cGreen'],
layer: 'bg',
textColor: 'preserve'
textColor: 'preserve',
},
postCyantext: {
depends: ['cBlue'],
layer: 'bg',
textColor: 'preserve'
textColor: 'preserve',
},
border: {
depends: ['fg'],
opacity: 'border',
color: (mod, fg) => brightness(2 * mod, fg).rgb
color: (mod, fg) => brightness(2 * mod, fg).rgb,
},
poll: {
depends: ['accent', 'bg'],
copacity: 'poll',
color: (mod, accent, bg) => alphaBlend(accent, 0.4, bg)
color: (mod, accent, bg) => alphaBlend(accent, 0.4, bg),
},
pollText: {
depends: ['text'],
layer: 'poll',
textColor: true
textColor: true,
},
icon: {
depends: ['bg', 'text'],
inheritsOpacity: false,
color: (mod, bg, text) => mixrgb(bg, text)
color: (mod, bg, text) => mixrgb(bg, text),
},
// Foreground
fgText: {
depends: ['text'],
layer: 'fg',
textColor: true
textColor: true,
},
fgLink: {
depends: ['link'],
layer: 'fg',
textColor: 'preserve'
textColor: 'preserve',
},
// Panel header
panel: {
depends: ['fg'],
opacity: 'panel'
opacity: 'panel',
},
panelText: {
depends: ['text'],
layer: 'panel',
textColor: true
textColor: true,
},
panelFaint: {
depends: ['fgText'],
layer: 'panel',
opacity: 'faint',
textColor: true
textColor: true,
},
panelLink: {
depends: ['fgLink'],
layer: 'panel',
textColor: 'preserve'
textColor: 'preserve',
},
// Top bar
@ -437,268 +437,268 @@ export const SLOT_INHERITANCE = {
topBarText: {
depends: ['fgText'],
layer: 'topBar',
textColor: true
textColor: true,
},
topBarLink: {
depends: ['fgLink'],
layer: 'topBar',
textColor: 'preserve'
textColor: 'preserve',
},
// Tabs
tab: {
depends: ['btn']
depends: ['btn'],
},
tabText: {
depends: ['btnText'],
layer: 'btn',
textColor: true
textColor: true,
},
tabActiveText: {
depends: ['text'],
layer: 'bg',
textColor: true
textColor: true,
},
// Buttons
btn: {
depends: ['fg'],
variant: 'btn',
opacity: 'btn'
opacity: 'btn',
},
btnText: {
depends: ['fgText'],
layer: 'btn',
textColor: true
textColor: true,
},
btnPanelText: {
depends: ['btnText'],
layer: 'btnPanel',
variant: 'btn',
textColor: true
textColor: true,
},
btnTopBarText: {
depends: ['btnText'],
layer: 'btnTopBar',
variant: 'btn',
textColor: true
textColor: true,
},
// Buttons: pressed
btnPressed: {
depends: ['btn'],
layer: 'btn'
layer: 'btn',
},
btnPressedText: {
depends: ['btnText'],
layer: 'btn',
variant: 'btnPressed',
textColor: true
textColor: true,
},
btnPressedPanel: {
depends: ['btnPressed'],
layer: 'btn'
layer: 'btn',
},
btnPressedPanelText: {
depends: ['btnPanelText'],
layer: 'btnPanel',
variant: 'btnPressed',
textColor: true
textColor: true,
},
btnPressedTopBar: {
depends: ['btnPressed'],
layer: 'btn'
layer: 'btn',
},
btnPressedTopBarText: {
depends: ['btnTopBarText'],
layer: 'btnTopBar',
variant: 'btnPressed',
textColor: true
textColor: true,
},
// Buttons: toggled
btnToggled: {
depends: ['btn'],
layer: 'btn',
color: (mod, btn) => brightness(mod * 20, btn).rgb
color: (mod, btn) => brightness(mod * 20, btn).rgb,
},
btnToggledText: {
depends: ['btnText'],
layer: 'btn',
variant: 'btnToggled',
textColor: true
textColor: true,
},
btnToggledPanelText: {
depends: ['btnPanelText'],
layer: 'btnPanel',
variant: 'btnToggled',
textColor: true
textColor: true,
},
btnToggledTopBarText: {
depends: ['btnTopBarText'],
layer: 'btnTopBar',
variant: 'btnToggled',
textColor: true
textColor: true,
},
// Buttons: disabled
btnDisabled: {
depends: ['btn', 'bg'],
color: (mod, btn, bg) => alphaBlend(btn, 0.25, 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)
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)
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)
color: (mod, text, btn) => alphaBlend(text, 0.25, btn),
},
// Input fields
input: {
depends: ['fg'],
opacity: 'input'
opacity: 'input',
},
inputText: {
depends: ['text'],
layer: 'input',
textColor: true
textColor: true,
},
inputPanelText: {
depends: ['panelText'],
layer: 'inputPanel',
variant: 'input',
textColor: true
textColor: true,
},
inputTopbarText: {
depends: ['topBarText'],
layer: 'inputTopBar',
variant: 'input',
textColor: true
textColor: true,
},
alertError: {
depends: ['cRed'],
opacity: 'alert'
opacity: 'alert',
},
alertErrorText: {
depends: ['text'],
layer: 'alert',
variant: 'alertError',
textColor: true
textColor: true,
},
alertErrorPanelText: {
depends: ['panelText'],
layer: 'alertPanel',
variant: 'alertError',
textColor: true
textColor: true,
},
alertWarning: {
depends: ['cOrange'],
opacity: 'alert'
opacity: 'alert',
},
alertWarningText: {
depends: ['text'],
layer: 'alert',
variant: 'alertWarning',
textColor: true
textColor: true,
},
alertWarningPanelText: {
depends: ['panelText'],
layer: 'alertPanel',
variant: 'alertWarning',
textColor: true
textColor: true,
},
alertSuccess: {
depends: ['cGreen'],
opacity: 'alert'
opacity: 'alert',
},
alertSuccessText: {
depends: ['text'],
layer: 'alert',
variant: 'alertSuccess',
textColor: true
textColor: true,
},
alertSuccessPanelText: {
depends: ['panelText'],
layer: 'alertPanel',
variant: 'alertSuccess',
textColor: true
textColor: true,
},
alertNeutral: {
depends: ['text'],
opacity: 'alert'
opacity: 'alert',
},
alertNeutralText: {
depends: ['text'],
layer: 'alert',
variant: 'alertNeutral',
color: (mod, text) => invertLightness(text).rgb,
textColor: true
textColor: true,
},
alertNeutralPanelText: {
depends: ['panelText'],
layer: 'alertPanel',
variant: 'alertNeutral',
textColor: true
textColor: true,
},
alertPopupError: {
depends: ['alertError'],
opacity: 'alertPopup'
opacity: 'alertPopup',
},
alertPopupErrorText: {
depends: ['alertErrorText'],
layer: 'popover',
variant: 'alertPopupError',
textColor: true
textColor: true,
},
alertPopupWarning: {
depends: ['alertWarning'],
opacity: 'alertPopup'
opacity: 'alertPopup',
},
alertPopupWarningText: {
depends: ['alertWarningText'],
layer: 'popover',
variant: 'alertPopupWarning',
textColor: true
textColor: true,
},
alertPopupSuccess: {
depends: ['alertSuccess'],
opacity: 'alertPopup'
opacity: 'alertPopup',
},
alertPopupSuccessText: {
depends: ['alertSuccessText'],
layer: 'popover',
variant: 'alertPopupSuccess',
textColor: true
textColor: true,
},
alertPopupNeutral: {
depends: ['alertNeutral'],
opacity: 'alertPopup'
opacity: 'alertPopup',
},
alertPopupNeutralText: {
depends: ['alertNeutralText'],
layer: 'popover',
variant: 'alertPopupNeutral',
textColor: true
textColor: true,
},
badgeNotification: '--cRed',
@ -706,7 +706,7 @@ export const SLOT_INHERITANCE = {
depends: ['text', 'badgeNotification'],
layer: 'badge',
variant: 'badgeNotification',
textColor: 'bw'
textColor: 'bw',
},
badgeNeutral: '--cGreen',
@ -714,59 +714,59 @@ export const SLOT_INHERITANCE = {
depends: ['text', 'badgeNeutral'],
layer: 'badge',
variant: 'badgeNeutral',
textColor: 'bw'
textColor: 'bw',
},
chatBg: {
depends: ['bg']
depends: ['bg'],
},
chatMessageIncomingBg: {
depends: ['chatBg']
depends: ['chatBg'],
},
chatMessageIncomingText: {
depends: ['text'],
layer: 'chatMessage',
variant: 'chatMessageIncomingBg',
textColor: true
textColor: true,
},
chatMessageIncomingLink: {
depends: ['link'],
layer: 'chatMessage',
variant: 'chatMessageIncomingBg',
textColor: 'preserve'
textColor: 'preserve',
},
chatMessageIncomingBorder: {
depends: ['border'],
opacity: 'border',
color: (mod, border) => brightness(2 * mod, border).rgb
color: (mod, border) => brightness(2 * mod, border).rgb,
},
chatMessageOutgoingBg: {
depends: ['chatMessageIncomingBg'],
color: (mod, chatMessage) => brightness(5 * mod, chatMessage).rgb
color: (mod, chatMessage) => brightness(5 * mod, chatMessage).rgb,
},
chatMessageOutgoingText: {
depends: ['text'],
layer: 'chatMessage',
variant: 'chatMessageOutgoingBg',
textColor: true
textColor: true,
},
chatMessageOutgoingLink: {
depends: ['link'],
layer: 'chatMessage',
variant: 'chatMessageOutgoingBg',
textColor: 'preserve'
textColor: 'preserve',
},
chatMessageOutgoingBorder: {
depends: ['chatMessageOutgoingBg'],
opacity: 'border',
color: (mod, border) => brightness(2 * mod, border).rgb
}
color: (mod, border) => brightness(2 * mod, border).rgb,
},
}

View file

@ -173,5 +173,5 @@ export default [
'chatMessageOutgoingBg',
'chatMessageOutgoingText',
'chatMessageOutgoingLink',
'chatMessageOutgoingBorder'
'chatMessageOutgoingBorder',
]

View file

@ -14,15 +14,10 @@ export const basePaletteKeys = new Set([
'cGreen',
'cOrange',
'wallpaper'
'wallpaper',
])
export const fontsKeys = new Set([
'interface',
'input',
'post',
'postCode'
])
export const fontsKeys = new Set(['interface', 'input', 'post', 'postCode'])
export const opacityKeys = new Set([
'alert',
@ -35,7 +30,7 @@ export const opacityKeys = new Set([
'panel',
'popover',
'profileTint',
'underlay'
'underlay',
])
export const shadowsKeys = new Set([
@ -48,7 +43,7 @@ export const shadowsKeys = new Set([
'button',
'buttonHover',
'buttonPressed',
'input'
'input',
])
export const radiiKeys = new Set([
@ -60,14 +55,11 @@ export const radiiKeys = new Set([
'avatarAlt',
'tooltip',
'attachment',
'chatMessage'
'chatMessage',
])
// Keys that are not available in editor and never meant to be edited
export const hiddenKeys = new Set([
'profileBg',
'profileTint'
])
export const hiddenKeys = new Set(['profileBg', 'profileTint'])
export const extendedBasePrefixes = [
'border',
@ -93,33 +85,30 @@ export const extendedBasePrefixes = [
'poll',
'chatBg',
'chatMessage'
'chatMessage',
]
export const nonComponentPrefixes = new Set([
'border',
'icon',
'highlight',
'lightText',
'chatBg'
'chatBg',
])
export const extendedBaseKeys = Object.fromEntries(
extendedBasePrefixes.map(prefix => [
extendedBasePrefixes.map((prefix) => [
prefix,
allKeys.filter(k => {
allKeys.filter((k) => {
if (prefix === 'alert') {
return k.startsWith(prefix) && !k.startsWith('alertPopup')
}
return k.startsWith(prefix)
})
])
}),
]),
)
// Keysets that are only really used intermideately, i.e. to generate other colors
export const temporary = new Set([
'',
'highlight'
])
export const temporary = new Set(['', 'highlight'])
export const temporaryColors = {}
@ -128,16 +117,18 @@ export const convertTheme2To3 = (data) => {
data.colors.link = data.colors.link || data.colors.accent
const generateRoot = () => {
const directives = {}
basePaletteKeys.forEach(key => { directives['--' + key] = 'color | ' + convert(data.colors[key]).hex })
basePaletteKeys.forEach((key) => {
directives['--' + key] = 'color | ' + convert(data.colors[key]).hex
})
return {
component: 'Root',
directives
directives,
}
}
const convertOpacity = () => {
const newRules = []
Object.keys(data.opacity || {}).forEach(key => {
Object.keys(data.opacity || {}).forEach((key) => {
if (!opacityKeys.has(key) || data.opacity[key] === undefined) return null
const originalOpacity = data.opacity[key]
const rule = { source: '2to3' }
@ -201,7 +192,12 @@ export const convertTheme2To3 = (data) => {
if (rule.component === 'Button') {
newRules.push({ ...rule, component: 'ScrollbarElement' })
newRules.push({ ...rule, component: 'Tab' })
newRules.push({ ...rule, component: 'Tab', state: ['active'], directives: { opacity: 0 } })
newRules.push({
...rule,
component: 'Tab',
state: ['active'],
directives: { opacity: 0 },
})
}
if (rule.component === 'Panel') {
newRules.push({ ...rule, component: 'Post' })
@ -212,7 +208,7 @@ export const convertTheme2To3 = (data) => {
const convertRadii = () => {
const newRules = []
Object.keys(data.radii || {}).forEach(key => {
Object.keys(data.radii || {}).forEach((key) => {
if (!radiiKeys.has(key) || data.radii[key] === undefined) return null
const originalRadius = data.radii[key]
const rule = { source: '2to3' }
@ -249,7 +245,7 @@ export const convertTheme2To3 = (data) => {
break
}
rule.directives = {
roundness: originalRadius
roundness: originalRadius,
}
newRules.push(rule)
if (rule.component === 'Button') {
@ -262,7 +258,7 @@ export const convertTheme2To3 = (data) => {
const convertFonts = () => {
const newRules = []
Object.keys(data.fonts || {}).forEach(key => {
Object.keys(data.fonts || {}).forEach((key) => {
if (!fontsKeys.has(key)) return
if (!data.fonts[key]) return
const originalFont = data.fonts[key].family
@ -297,7 +293,7 @@ export const convertTheme2To3 = (data) => {
}
const convertShadows = () => {
const newRules = []
Object.keys(data.shadows || {}).forEach(key => {
Object.keys(data.shadows || {}).forEach((key) => {
if (!shadowsKeys.has(key)) return
const originalShadow = data.shadows[key]
const rule = { source: '2to3' }
@ -338,11 +334,15 @@ export const convertTheme2To3 = (data) => {
break
}
rule.directives = {
shadow: originalShadow
shadow: originalShadow,
}
newRules.push(rule)
if (key === 'topBar') {
newRules.push({ ...rule, component: 'PanelHeader', parent: { component: 'MobileDrawer' } })
newRules.push({
...rule,
component: 'PanelHeader',
parent: { component: 'MobileDrawer' },
})
}
if (key === 'avatarStatus') {
newRules.push({ ...rule, parent: { component: 'Notification' } })
@ -363,169 +363,211 @@ export const convertTheme2To3 = (data) => {
return newRules
}
const extendedRules = Object.entries(extendedBaseKeys).map(([prefix, keys]) => {
if (nonComponentPrefixes.has(prefix)) return null
const rule = { source: '2to3' }
if (prefix === 'alertPopup') {
rule.component = 'Alert'
rule.parent = { component: 'Popover' }
} else if (prefix === 'selectedPost') {
rule.component = 'Post'
rule.state = ['selected']
} else if (prefix === 'selectedMenu') {
rule.component = 'MenuItem'
rule.state = ['hover']
} else if (prefix === 'chatMessageIncoming') {
rule.component = 'ChatMessage'
} else if (prefix === 'chatMessageOutgoing') {
rule.component = 'ChatMessage'
rule.variant = 'outgoing'
} else if (prefix === 'panel') {
rule.component = 'PanelHeader'
} else if (prefix === 'topBar') {
rule.component = 'TopBar'
} else if (prefix === 'chatMessage') {
rule.component = 'ChatMessage'
} else if (prefix === 'poll') {
rule.component = 'PollGraph'
} else if (prefix === 'btn') {
rule.component = 'Button'
} else {
rule.component = prefix[0].toUpperCase() + prefix.slice(1).toLowerCase()
}
return keys.map((key) => {
if (!data.colors[key]) return null
const leftoverKey = key.replace(prefix, '')
const parts = (leftoverKey || 'Bg').match(/[A-Z][a-z]*/g)
const last = parts.slice(-1)[0]
let newRule = { source: '2to3', directives: {} }
let variantArray = []
switch (last) {
case 'Text':
case 'Faint': // typo
case 'Link':
case 'Icon':
case 'Greentext':
case 'Cyantext':
case 'Border':
newRule.parent = rule
newRule.directives.textColor = data.colors[key]
variantArray = parts.slice(0, -1)
break
default:
newRule = { ...rule, directives: {} }
newRule.directives.background = data.colors[key]
variantArray = parts
break
const extendedRules = Object.entries(extendedBaseKeys).map(
([prefix, keys]) => {
if (nonComponentPrefixes.has(prefix)) return null
const rule = { source: '2to3' }
if (prefix === 'alertPopup') {
rule.component = 'Alert'
rule.parent = { component: 'Popover' }
} else if (prefix === 'selectedPost') {
rule.component = 'Post'
rule.state = ['selected']
} else if (prefix === 'selectedMenu') {
rule.component = 'MenuItem'
rule.state = ['hover']
} else if (prefix === 'chatMessageIncoming') {
rule.component = 'ChatMessage'
} else if (prefix === 'chatMessageOutgoing') {
rule.component = 'ChatMessage'
rule.variant = 'outgoing'
} else if (prefix === 'panel') {
rule.component = 'PanelHeader'
} else if (prefix === 'topBar') {
rule.component = 'TopBar'
} else if (prefix === 'chatMessage') {
rule.component = 'ChatMessage'
} else if (prefix === 'poll') {
rule.component = 'PollGraph'
} else if (prefix === 'btn') {
rule.component = 'Button'
} else {
rule.component = prefix[0].toUpperCase() + prefix.slice(1).toLowerCase()
}
return keys.map((key) => {
if (!data.colors[key]) return null
const leftoverKey = key.replace(prefix, '')
const parts = (leftoverKey || 'Bg').match(/[A-Z][a-z]*/g)
const last = parts.slice(-1)[0]
let newRule = { source: '2to3', directives: {} }
let variantArray = []
if (last === 'Text' || last === 'Link') {
const secondLast = parts.slice(-2)[0]
if (secondLast === 'Light') {
return null // unsupported
} else if (secondLast === 'Faint') {
newRule.state = ['faint']
variantArray = parts.slice(0, -2)
switch (last) {
case 'Text':
case 'Faint': // typo
case 'Link':
case 'Icon':
case 'Greentext':
case 'Cyantext':
case 'Border':
newRule.parent = rule
newRule.directives.textColor = data.colors[key]
variantArray = parts.slice(0, -1)
break
default:
newRule = { ...rule, directives: {} }
newRule.directives.background = data.colors[key]
variantArray = parts
break
}
}
switch (last) {
case 'Text':
case 'Link':
case 'Icon':
case 'Border':
newRule.component = last
break
case 'Greentext':
case 'Cyantext':
newRule.component = 'FunText'
newRule.variant = last.toLowerCase()
break
case 'Faint':
newRule.component = 'Text'
newRule.state = ['faint']
break
}
variantArray = variantArray.filter(x => x !== 'Bg')
if (last === 'Link' && prefix === 'selectedPost') {
// selectedPost has typo - duplicate 'Post'
variantArray = variantArray.filter(x => x !== 'Post')
}
if (prefix === 'popover' && variantArray[0] === 'Post') {
newRule.component = 'Post'
newRule.parent = { source: '2to3hack', component: 'Popover' }
variantArray = variantArray.filter(x => x !== 'Post')
}
if (prefix === 'selectedMenu' && variantArray[0] === 'Popover') {
newRule.parent = { source: '2to3hack', component: 'Popover' }
variantArray = variantArray.filter(x => x !== 'Popover')
}
switch (prefix) {
case 'btn':
case 'input':
case 'alert': {
const hasPanel = variantArray.find(x => x === 'Panel')
if (hasPanel) {
newRule.parent = { source: '2to3hack', component: 'PanelHeader', parent: newRule.parent }
variantArray = variantArray.filter(x => x !== 'Panel')
if (last === 'Text' || last === 'Link') {
const secondLast = parts.slice(-2)[0]
if (secondLast === 'Light') {
return null // unsupported
} else if (secondLast === 'Faint') {
newRule.state = ['faint']
variantArray = parts.slice(0, -2)
}
const hasTop = variantArray.find(x => x === 'Top') // TopBar
if (hasTop) {
newRule.parent = { source: '2to3hack', component: 'TopBar', parent: newRule.parent }
variantArray = variantArray.filter(x => x !== 'Top' && x !== 'Bar')
}
switch (last) {
case 'Text':
case 'Link':
case 'Icon':
case 'Border':
newRule.component = last
break
case 'Greentext':
case 'Cyantext':
newRule.component = 'FunText'
newRule.variant = last.toLowerCase()
break
case 'Faint':
newRule.component = 'Text'
newRule.state = ['faint']
break
}
variantArray = variantArray.filter((x) => x !== 'Bg')
if (last === 'Link' && prefix === 'selectedPost') {
// selectedPost has typo - duplicate 'Post'
variantArray = variantArray.filter((x) => x !== 'Post')
}
if (prefix === 'popover' && variantArray[0] === 'Post') {
newRule.component = 'Post'
newRule.parent = { source: '2to3hack', component: 'Popover' }
variantArray = variantArray.filter((x) => x !== 'Post')
}
if (prefix === 'selectedMenu' && variantArray[0] === 'Popover') {
newRule.parent = { source: '2to3hack', component: 'Popover' }
variantArray = variantArray.filter((x) => x !== 'Popover')
}
switch (prefix) {
case 'btn':
case 'input':
case 'alert': {
const hasPanel = variantArray.find((x) => x === 'Panel')
if (hasPanel) {
newRule.parent = {
source: '2to3hack',
component: 'PanelHeader',
parent: newRule.parent,
}
variantArray = variantArray.filter((x) => x !== 'Panel')
}
const hasTop = variantArray.find((x) => x === 'Top') // TopBar
if (hasTop) {
newRule.parent = {
source: '2to3hack',
component: 'TopBar',
parent: newRule.parent,
}
variantArray = variantArray.filter(
(x) => x !== 'Top' && x !== 'Bar',
)
}
break
}
break
}
}
if (variantArray.length > 0) {
if (prefix === 'btn') {
newRule.state = variantArray.map(x => x.toLowerCase())
} else {
newRule.variant = variantArray[0].toLowerCase()
if (variantArray.length > 0) {
if (prefix === 'btn') {
newRule.state = variantArray.map((x) => x.toLowerCase())
} else {
newRule.variant = variantArray[0].toLowerCase()
}
}
}
if (newRule.component === 'Panel') {
return [newRule, { ...newRule, component: 'MobileDrawer' }]
} else if (newRule.component === 'Button') {
const rules = [
newRule,
{ ...newRule, component: 'Tab' },
{ ...newRule, component: 'ScrollbarElement' }
]
if (newRule.state?.indexOf('toggled') >= 0) {
rules.push({ ...newRule, state: [...newRule.state, 'focused'] })
rules.push({ ...newRule, state: [...newRule.state, 'hover'] })
rules.push({ ...newRule, state: [...newRule.state, 'hover', 'focused'] })
}
if (newRule.state?.indexOf('hover') >= 0) {
rules.push({ ...newRule, state: [...newRule.state, 'focused'] })
}
return rules
} else if (newRule.component === 'Badge') {
if (newRule.variant === 'notification') {
return [newRule, { component: 'Root', directives: { '--badgeNotification': 'color | ' + newRule.directives.background } }]
} else if (newRule.variant === 'neutral') {
return [{ ...newRule, variant: 'normal' }]
if (newRule.component === 'Panel') {
return [newRule, { ...newRule, component: 'MobileDrawer' }]
} else if (newRule.component === 'Button') {
const rules = [
newRule,
{ ...newRule, component: 'Tab' },
{ ...newRule, component: 'ScrollbarElement' },
]
if (newRule.state?.indexOf('toggled') >= 0) {
rules.push({ ...newRule, state: [...newRule.state, 'focused'] })
rules.push({ ...newRule, state: [...newRule.state, 'hover'] })
rules.push({
...newRule,
state: [...newRule.state, 'hover', 'focused'],
})
}
if (newRule.state?.indexOf('hover') >= 0) {
rules.push({ ...newRule, state: [...newRule.state, 'focused'] })
}
return rules
} else if (newRule.component === 'Badge') {
if (newRule.variant === 'notification') {
return [
newRule,
{
component: 'Root',
directives: {
'--badgeNotification':
'color | ' + newRule.directives.background,
},
},
]
} else if (newRule.variant === 'neutral') {
return [{ ...newRule, variant: 'normal' }]
} else {
return [newRule]
}
} else if (newRule.component === 'TopBar') {
return [
newRule,
{
...newRule,
parent: { component: 'MobileDrawer' },
component: 'PanelHeader',
},
]
} else {
return [newRule]
}
} else if (newRule.component === 'TopBar') {
return [newRule, { ...newRule, parent: { component: 'MobileDrawer' }, component: 'PanelHeader' }]
} else {
return [newRule]
}
})
})
})
},
)
const flatExtRules = extendedRules.filter(x => x).reduce((acc, x) => [...acc, ...x], []).filter(x => x).reduce((acc, x) => [...acc, ...x], [])
const flatExtRules = extendedRules
.filter((x) => x)
.reduce((acc, x) => [...acc, ...x], [])
.filter((x) => x)
.reduce((acc, x) => [...acc, ...x], [])
return [generateRoot(), ...convertShadows(), ...convertRadii(), ...convertOpacity(), ...convertFonts(), ...flatExtRules]
return [
generateRoot(),
...convertShadows(),
...convertRadii(),
...convertOpacity(),
...convertFonts(),
...flatExtRules,
]
}

View file

@ -1,13 +1,28 @@
import { convert, brightness } from 'chromatism'
import { alphaBlend, arithmeticBlend, getTextColor, relativeLuminance } from '../color_convert/color_convert.js'
import {
alphaBlend,
arithmeticBlend,
getTextColor,
relativeLuminance,
} from '../color_convert/color_convert.js'
export const process = (text, functions, { findColor, findShadow }, { dynamicVars, staticVars }) => {
const { funcName, argsString } = /\$(?<funcName>\w+)\((?<argsString>[#a-zA-Z0-9-+,.'"\s]*)\)/.exec(text).groups
const args = argsString.split(/ /g).map(a => a.trim())
export const process = (
text,
functions,
{ findColor, findShadow },
{ dynamicVars, staticVars },
) => {
const { funcName, argsString } =
/\$(?<funcName>\w+)\((?<argsString>[#a-zA-Z0-9-+,.'"\s]*)\)/.exec(
text,
).groups
const args = argsString.split(/ /g).map((a) => a.trim())
const func = functions[funcName]
if (args.length < func.argsNeeded) {
throw new Error(`$${funcName} requires at least ${func.argsNeeded} arguments, but ${args.length} were provided`)
throw new Error(
`$${funcName} requires at least ${func.argsNeeded} arguments, but ${args.length} were provided`,
)
}
return func.exec(args, { findColor, findShadow }, { dynamicVars, staticVars })
}
@ -15,53 +30,57 @@ export const process = (text, functions, { findColor, findShadow }, { dynamicVar
export const colorFunctions = {
alpha: {
argsNeeded: 2,
documentation: 'Changes alpha value of the color only to be used for CSS variables',
args: [
'color: source color used',
'amount: alpha value'
],
documentation:
'Changes alpha value of the color only to be used for CSS variables',
args: ['color: source color used', 'amount: alpha value'],
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
const [color, amountArg] = args
const colorArg = convert(findColor(color, { dynamicVars, staticVars })).rgb
const colorArg = convert(
findColor(color, { dynamicVars, staticVars }),
).rgb
const amount = Number(amountArg)
return { ...colorArg, a: amount }
}
},
},
brightness: {
argsNeeded: 2,
document: 'Changes brightness/lightness of color in HSL colorspace',
args: [
'color: source color used',
'amount: lightness value'
],
args: ['color: source color used', 'amount: lightness value'],
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
const [color, amountArg] = args
const colorArg = convert(findColor(color, { dynamicVars, staticVars })).hsl
const colorArg = convert(
findColor(color, { dynamicVars, staticVars }),
).hsl
colorArg.l += Number(amountArg)
return { ...convert(colorArg).rgb }
}
},
},
textColor: {
argsNeeded: 2,
documentation: 'Get text color with adequate contrast for given background and intended text color. Same function is used internally',
documentation:
'Get text color with adequate contrast for given background and intended text color. Same function is used internally',
args: [
'background: color of backdrop where text will be shown',
'foreground: intended text color',
`[preserve]: (optional) intended color preservation:
'preserve' - try to preserve the color
'no-preserve' - if can't get adequate color - fall back to black or white
'no-auto' - don't do anything (useless as a color function)`
'no-auto' - don't do anything (useless as a color function)`,
],
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
const [backgroundArg, foregroundArg, preserve = 'preserve'] = args
const background = convert(findColor(backgroundArg, { dynamicVars, staticVars })).rgb
const foreground = convert(findColor(foregroundArg, { dynamicVars, staticVars })).rgb
const background = convert(
findColor(backgroundArg, { dynamicVars, staticVars }),
).rgb
const foreground = convert(
findColor(foregroundArg, { dynamicVars, staticVars }),
).rgb
return getTextColor(background, foreground, preserve === 'preserve')
}
},
},
blend: {
argsNeeded: 3,
@ -69,17 +88,21 @@ export const colorFunctions = {
args: [
'background: bottom layer color',
'amount: opacity of top layer',
'foreground: upper layer color'
'foreground: upper layer color',
],
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
const [backgroundArg, amountArg, foregroundArg] = args
const background = convert(findColor(backgroundArg, { dynamicVars, staticVars })).rgb
const foreground = convert(findColor(foregroundArg, { dynamicVars, staticVars })).rgb
const background = convert(
findColor(backgroundArg, { dynamicVars, staticVars }),
).rgb
const foreground = convert(
findColor(foregroundArg, { dynamicVars, staticVars }),
).rgb
const amount = Number(amountArg)
return alphaBlend(background, amount, foreground)
}
},
},
shift: {
argsNeeded: 2,
@ -87,66 +110,72 @@ export const colorFunctions = {
args: [
'origin: base color',
'value: shift value',
'operator: math operator to use (+ or -)'
'operator: math operator to use (+ or -)',
],
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
const [originArg, valueArg, operatorArg] = args
const origin = convert(findColor(originArg, { dynamicVars, staticVars })).rgb
const value = convert(findColor(valueArg, { dynamicVars, staticVars })).rgb
const origin = convert(
findColor(originArg, { dynamicVars, staticVars }),
).rgb
const value = convert(
findColor(valueArg, { dynamicVars, staticVars }),
).rgb
return arithmeticBlend(origin, value, operatorArg)
}
},
},
boost: {
argsNeeded: 2,
documentation: 'If given color is dark makes it darker, if color is light - makes it lighter',
args: [
'color: source color',
'amount: how much darken/brighten the color'
],
documentation:
'If given color is dark makes it darker, if color is light - makes it lighter',
args: ['color: source color', 'amount: how much darken/brighten the color'],
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
const [colorArg, amountArg] = args
const color = convert(findColor(colorArg, { dynamicVars, staticVars })).rgb
const color = convert(
findColor(colorArg, { dynamicVars, staticVars }),
).rgb
const amount = Number(amountArg)
const isLight = relativeLuminance(color) < 0.5
const mod = isLight ? -1 : 1
return brightness(amount * mod, color).rgb
}
},
},
mod: {
argsNeeded: 2,
documentation: 'Old function that increases or decreases brightness depending if background color is dark or light. Advised against using it as it might give unexpected results.',
args: [
'color: source color',
'amount: how much darken/brighten the color'
],
documentation:
'Old function that increases or decreases brightness depending if background color is dark or light. Advised against using it as it might give unexpected results.',
args: ['color: source color', 'amount: how much darken/brighten the color'],
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
const [colorArg, amountArg] = args
const color = convert(findColor(colorArg, { dynamicVars, staticVars })).rgb
const color = convert(
findColor(colorArg, { dynamicVars, staticVars }),
).rgb
const amount = Number(amountArg)
const effectiveBackground = dynamicVars.lowerLevelBackground ?? color
const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5
const isLightOnDark =
relativeLuminance(convert(effectiveBackground).rgb) < 0.5
const mod = isLightOnDark ? 1 : -1
return brightness(amount * mod, color).rgb
}
}
},
},
}
export const shadowFunctions = {
borderSide: {
argsNeeded: 3,
documentation: 'Simulate a border on a side with a shadow, best works on inset border',
documentation:
'Simulate a border on a side with a shadow, best works on inset border',
args: [
'color: border color',
'side: string indicating on which side border should be, takes either one word or two words joined by dash (i.e. "left" or "bottom-right")',
'width: border width (thickness)',
'[alpha]: (Optional) border opacity, defaults to 1 (fully opaque)',
'[inset]: (Optional) whether border should be on the inside or outside, defaults to inside'
'[inset]: (Optional) whether border should be on the inside or outside, defaults to inside',
],
exec: (args) => {
const [color, side, alpha = '1', widthArg = '1', inset = 'inset'] = args
@ -161,7 +190,7 @@ export const shadowFunctions = {
spread: 0,
color,
alpha: Number(alpha),
inset: isInset
inset: isInset,
}
side.split('-').forEach((position) => {
@ -181,6 +210,6 @@ export const shadowFunctions = {
}
})
return [targetShadow]
}
}
},
},
}

View file

@ -1,5 +1,12 @@
import { convert, brightness, contrastRatio } from 'chromatism'
import { rgb2hex, rgba2css, alphaBlendLayers, getTextColor, relativeLuminance, getCssColor } from '../color_convert/color_convert.js'
import {
rgb2hex,
rgba2css,
alphaBlendLayers,
getTextColor,
relativeLuminance,
getCssColor,
} from '../color_convert/color_convert.js'
import { LAYERS, DEFAULT_OPACITY, SLOT_INHERITANCE } from './pleromafe.js'
/*
@ -48,15 +55,17 @@ export const getLayersArray = (layer, data = LAYERS) => {
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]
]))
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) => {
@ -67,11 +76,9 @@ const getDependencies = (key, inheritance) => {
if (data === null) return []
const { depends, layer, variant } = data
const layerDeps = layer
? getLayersArray(layer).map(currentLayer => {
return currentLayer === layer
? variant || layer
: currentLayer
})
? getLayersArray(layer).map((currentLayer) => {
return currentLayer === layer ? variant || layer : currentLayer
})
: []
if (Array.isArray(depends)) {
return [...depends, ...layerDeps]
@ -93,7 +100,7 @@ const getDependencies = (key, inheritance) => {
*/
export const topoSort = (
inheritance = SLOT_INHERITANCE,
getDeps = getDependencies
getDeps = getDependencies,
) => {
// This is an implementation of https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
@ -130,22 +137,25 @@ export const topoSort = (
// The index thing is to make sorting stable on browsers
// where Array.sort() isn't stable
return output.map((data, index) => ({ data, index })).sort(({ data: a, index: ai }, { data: b, index: bi }) => {
const depsA = getDeps(a, inheritance).length
const depsB = getDeps(b, inheritance).length
return output
.map((data, index) => ({ data, index }))
.sort(({ data: a, index: ai }, { data: b, index: bi }) => {
const depsA = getDeps(a, inheritance).length
const depsB = getDeps(b, inheritance).length
if (depsA === depsB || (depsB !== 0 && depsA !== 0)) return ai - bi
if (depsA === 0 && depsB !== 0) return -1
if (depsB === 0 && depsA !== 0) return 1
return 0 // failsafe, shouldn't happen?
}).map(({ data }) => data)
if (depsA === depsB || (depsB !== 0 && depsA !== 0)) return ai - bi
if (depsA === 0 && depsB !== 0) return -1
if (depsB === 0 && depsA !== 0) return 1
return 0 // failsafe, shouldn't happen?
})
.map(({ data }) => data)
}
const expandSlotValue = (value) => {
if (typeof value === 'object') return value
return {
depends: value.startsWith('--') ? [value.substring(2)] : [],
default: value.startsWith('#') ? value : undefined
default: value.startsWith('#') ? value : undefined,
}
}
/**
@ -156,7 +166,7 @@ const expandSlotValue = (value) => {
export const getOpacitySlot = (
k,
inheritance = SLOT_INHERITANCE,
getDeps = getDependencies
getDeps = getDependencies,
) => {
const value = expandSlotValue(inheritance[k])
if (value.opacity === null) return
@ -189,7 +199,7 @@ export const getOpacitySlot = (
export const getLayerSlot = (
k,
inheritance = SLOT_INHERITANCE,
getDeps = getDependencies
getDeps = getDependencies,
) => {
const value = expandSlotValue(inheritance[k])
if (LAYERS[k]) return k
@ -218,8 +228,11 @@ export const getLayerSlot = (
*/
export const SLOT_ORDERED = topoSort(
Object.entries(SLOT_INHERITANCE)
.sort(([, aV], [, bV]) => ((aV && aV.priority) || 0) - ((bV && bV.priority) || 0))
.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})
.sort(
([, aV], [, bV]) =>
((aV && aV.priority) || 0) - ((bV && bV.priority) || 0),
)
.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {}),
)
/**
@ -233,8 +246,11 @@ export const OPACITIES = Object.entries(SLOT_INHERITANCE).reduce((acc, [k]) => {
...acc,
[opacity]: {
defaultValue: DEFAULT_OPACITY[opacity] || 1,
affectedSlots: [...((acc[opacity] && acc[opacity].affectedSlots) || []), k]
}
affectedSlots: [
...((acc[opacity] && acc[opacity].affectedSlots) || []),
k,
],
},
}
} else {
return acc
@ -245,10 +261,11 @@ export const OPACITIES = Object.entries(SLOT_INHERITANCE).reduce((acc, [k]) => {
* Handle dynamic color
*/
export const computeDynamicColor = (sourceColor, getColor, mod) => {
if (typeof sourceColor !== 'string' || !sourceColor.startsWith('--')) return sourceColor
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 [variable, modifier] = sourceColor.split(/,/g).map((str) => str.trim())
const variableSlot = variable.substring(2)
targetColor = getColor(variableSlot)
if (modifier) {
@ -261,151 +278,167 @@ export const computeDynamicColor = (sourceColor, getColor, mod) => {
* 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
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
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'
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)
}
} 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
const isLightOnDark = relativeLuminance(backgroundColor) < 0.5
const mod = isLightOnDark ? 1 : -1
if (ownOpacitySlot === null) {
outputColor.a = 1
} else if (sourceColor === 'transparent') {
outputColor.a = 0
} else {
const opacityOverriden = ownOpacitySlot && sourceOpacity[opacitySlot] !== undefined
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
const dependencySlot = deps[0]
const dependencyColor = dependencySlot && colors[dependencySlot]
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)
}
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
const opacitySlot = value.opacity || getOpacitySlot(key)
const ownOpacitySlot = value.opacity
if (ownOpacitySlot === null) {
outputColor.a = 1
} else if (sourceColor === 'transparent') {
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
)
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 (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: {} })
if (opacitySlot) {
return {
colors: { ...colors, [key]: outputColor },
opacity: { ...opacity, [opacitySlot]: outputColor.a },
}
} else {
return {
colors: { ...colors, [key]: outputColor },
opacity,
}
}
},
{ colors: {}, opacity: {} },
)
export const composePreset = (colors, radii, shadows, fonts) => {
return {
@ -413,14 +446,14 @@ export const composePreset = (colors, radii, shadows, fonts) => {
...shadows.rules,
...colors.rules,
...radii.rules,
...fonts.rules
...fonts.rules,
},
theme: {
...shadows.theme,
...colors.theme,
...radii.theme,
...fonts.theme
}
...fonts.theme,
},
}
}
@ -430,7 +463,7 @@ export const generatePreset = (input) => {
colors,
generateRadii(input),
generateShadows(input, colors.theme.colors, colors.mod),
generateFonts(input)
generateFonts(input),
)
}
@ -440,16 +473,17 @@ export const getCssShadow = (input, usesDropShadow) => {
}
return input
.filter(_ => usesDropShadow ? _.inset : _)
.map((shad) => [
shad.x,
shad.y,
shad.blur,
shad.spread
].map(_ => _ + 'px').concat([
getCssColor(shad.color, shad.alpha),
shad.inset ? 'inset' : ''
]).join(' ')).join(', ')
.filter((_) => (usesDropShadow ? _.inset : _))
.map((shad) =>
[shad.x, shad.y, shad.blur, shad.spread]
.map((_) => _ + 'px')
.concat([
getCssColor(shad.color, shad.alpha),
shad.inset ? 'inset' : '',
])
.join(' '),
)
.join(', ')
}
export const getCssShadowFilter = (input) => {
@ -457,19 +491,24 @@ export const getCssShadowFilter = (input) => {
return 'none'
}
return input
// drop-shadow doesn't support inset or spread
.filter((shad) => !shad.inset && Number(shad.spread) === 0)
.map((shad) => [
shad.x,
shad.y,
// drop-shadow's blur is twice as strong compared to box-shadow
shad.blur / 2
].map(_ => _ + 'px').concat([
getCssColor(shad.color, shad.alpha)
]).join(' '))
.map(_ => `drop-shadow(${_})`)
.join(' ')
return (
input
// drop-shadow doesn't support inset or spread
.filter((shad) => !shad.inset && Number(shad.spread) === 0)
.map((shad) =>
[
shad.x,
shad.y,
// drop-shadow's blur is twice as strong compared to box-shadow
shad.blur / 2,
]
.map((_) => _ + 'px')
.concat([getCssColor(shad.color, shad.alpha)])
.join(' '),
)
.map((_) => `drop-shadow(${_})`)
.join(' ')
)
}
export const generateColors = (themeData) => {
@ -479,24 +518,26 @@ export const generateColors = (themeData) => {
const { colors, opacity } = getColors(sourceColors, themeData.opacity || {})
const htmlColors = Object.entries(colors)
.reduce((acc, [k, v]) => {
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) : rgba2css(v)
return acc
}, { complete: {}, solid: {} })
},
{ complete: {}, solid: {} },
)
return {
rules: {
colors: Object.entries(htmlColors.complete)
.filter(([, v]) => v)
.map(([k, v]) => `--${k}: ${v}`)
.join(';')
.join(';'),
},
theme: {
colors: htmlColors.solid,
opacity
}
opacity,
},
}
}
@ -504,68 +545,85 @@ export const generateRadii = (input) => {
let inputRadii = input.radii || {}
// v1 -> v2
if (typeof input.btnRadius !== 'undefined') {
inputRadii = Object
.entries(input)
inputRadii = Object.entries(input)
.filter(([k]) => k.endsWith('Radius'))
.reduce((acc, e) => { acc[e[0].split('Radius')[0]] = e[1]; return acc }, {})
.reduce((acc, e) => {
acc[e[0].split('Radius')[0]] = e[1]
return acc
}, {})
}
const radii = Object.entries(inputRadii).filter(([, v]) => v).reduce((acc, [k, v]) => {
acc[k] = v
return acc
}, {
btn: 4,
input: 4,
checkbox: 2,
panel: 10,
avatar: 5,
avatarAlt: 50,
tooltip: 2,
attachment: 5,
chatMessage: inputRadii.panel
})
const radii = Object.entries(inputRadii)
.filter(([, v]) => v)
.reduce(
(acc, [k, v]) => {
acc[k] = v
return acc
},
{
btn: 4,
input: 4,
checkbox: 2,
panel: 10,
avatar: 5,
avatarAlt: 50,
tooltip: 2,
attachment: 5,
chatMessage: inputRadii.panel,
},
)
return {
rules: {
radii: Object.entries(radii).filter(([, v]) => v).map(([k, v]) => `--${k}Radius: ${v}px`).join(';')
radii: Object.entries(radii)
.filter(([, v]) => v)
.map(([k, v]) => `--${k}Radius: ${v}px`)
.join(';'),
},
theme: {
radii
}
radii,
},
}
}
export const generateFonts = (input) => {
const fonts = Object.entries(input.fonts || {}).filter(([, v]) => v).reduce((acc, [k, v]) => {
acc[k] = Object.entries(v).filter(([, v]) => v).reduce((acc, [k, v]) => {
acc[k] = v
return acc
}, acc[k])
return acc
}, {
interface: {
family: 'sans-serif'
},
input: {
family: 'inherit'
},
post: {
family: 'inherit'
},
postCode: {
family: 'monospace'
}
})
const fonts = Object.entries(input.fonts || {})
.filter(([, v]) => v)
.reduce(
(acc, [k, v]) => {
acc[k] = Object.entries(v)
.filter(([, v]) => v)
.reduce((acc, [k, v]) => {
acc[k] = v
return acc
}, acc[k])
return acc
},
{
interface: {
family: 'sans-serif',
},
input: {
family: 'inherit',
},
post: {
family: 'inherit',
},
postCode: {
family: 'monospace',
},
},
)
return {
rules: {
fonts: Object
.entries(fonts)
fonts: Object.entries(fonts)
.filter(([, v]) => v)
.map(([k, v]) => `--${k}Font: ${v.family}`).join(';')
.map(([k, v]) => `--${k}Font: ${v.family}`)
.join(';'),
},
theme: {
fonts
}
fonts,
},
}
}
@ -576,7 +634,7 @@ const border = (top, shadow) => ({
spread: 0,
color: shadow ? '#000000' : '#FFFFFF',
alpha: 0.2,
inset: true
inset: true,
})
const buttonInsetFakeBorders = [border(true, false), border(false, true)]
const inputInsetFakeBorders = [border(true, true), border(false, false)]
@ -586,63 +644,77 @@ const hoverGlow = {
blur: 4,
spread: 0,
color: '--faint',
alpha: 1
alpha: 1,
}
export const DEFAULT_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
}],
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],
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: [
...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
@ -654,58 +726,65 @@ export const generateShadows = (input, colors) => {
popup: 'popover',
avatar: 'bg',
panelHeader: 'panel',
input: 'input'
input: 'input',
}
const cleanInputShadows = Object.fromEntries(
Object.entries(input.shadows || {})
.map(([name, shadowSlot]) => [
name,
// defaulting color to black to avoid potential problems
shadowSlot.map(shadowDef => ({ color: '#000000', ...shadowDef }))
])
Object.entries(input.shadows || {}).map(([name, shadowSlot]) => [
name,
// defaulting color to black to avoid potential problems
shadowSlot.map((shadowDef) => ({ color: '#000000', ...shadowDef })),
]),
)
const inputShadows = cleanInputShadows && !input.themeEngineVersion
? shadows2to3(cleanInputShadows, input.opacity)
: cleanInputShadows || {}
const inputShadows =
cleanInputShadows && !input.themeEngineVersion
? shadows2to3(cleanInputShadows, input.opacity)
: cleanInputShadows || {}
const shadows = Object.entries({
...DEFAULT_SHADOWS,
...inputShadows
...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 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
))
}
], [])
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.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)}`,
`--${k}ShadowFilter: ${getCssShadowFilter(v)}`,
`--${k}ShadowInset: ${getCssShadow(v, true)}`
].join(';'))
.join(';')
shadows: Object.entries(shadows)
// 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)}`,
`--${k}ShadowFilter: ${getCssShadowFilter(v)}`,
`--${k}ShadowInset: ${getCssShadow(v, true)}`,
].join(';'),
)
.join(';'),
},
theme: {
shadows
}
shadows,
},
}
}
@ -715,18 +794,25 @@ export const generateShadows = (input, colors) => {
* 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 = '#000000' }) => 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
}
], [])
return { ...shadowsAcc, [slotName]: newShadow }
}, {})
return Object.entries(shadows).reduce(
(shadowsAcc, [slotName, shadowDefs]) => {
const isDynamic = ({ color = '#000000' }) => 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,
},
],
[],
)
return { ...shadowsAcc, [slotName]: newShadow }
},
{},
)
}
export const colors2to3 = (colors) => {
@ -738,12 +824,13 @@ export const colors2to3 = (colors) => {
case 'btnText':
return {
...acc,
...btnPositions
.reduce(
(statePositionAcc, position) =>
({ ...statePositionAcc, ['btn' + position + 'Text']: color })
, {}
)
...btnPositions.reduce(
(statePositionAcc, position) => ({
...statePositionAcc,
['btn' + position + 'Text']: color,
}),
{},
),
}
default:
return { ...acc, [slotName]: color }

View file

@ -6,13 +6,13 @@ import {
getTextColor,
rgba2css,
mixrgb,
relativeLuminance
relativeLuminance,
} from '../color_convert/color_convert.js'
import {
colorFunctions,
shadowFunctions,
process
process,
} from './theme3_slot_functions.js'
import {
@ -20,7 +20,7 @@ import {
getAllPossibleCombinations,
genericRuleToSelector,
normalizeCombination,
findRules
findRules,
} from './iss_utils.js'
import { deserializeShadow } from './iss_deserializer.js'
@ -36,15 +36,20 @@ const components = {
Panel: null,
Chat: null,
ChatMessage: null,
Button: null
Button: null,
}
export const findShadow = (shadows, { dynamicVars, staticVars }) => {
return (shadows || []).map(shadow => {
return (shadows || []).map((shadow) => {
let targetShadow
if (typeof shadow === 'string') {
if (shadow.startsWith('$')) {
targetShadow = process(shadow, shadowFunctions, { findColor, findShadow }, { dynamicVars, staticVars })
targetShadow = process(
shadow,
shadowFunctions,
{ findColor, findShadow },
{ dynamicVars, staticVars },
)
} else if (shadow.startsWith('--')) {
// modifiers are completely unsupported here
const variableSlot = shadow.substring(2)
@ -56,21 +61,27 @@ export const findShadow = (shadows, { dynamicVars, staticVars }) => {
targetShadow = shadow
}
const shadowArray = Array.isArray(targetShadow) ? targetShadow : [targetShadow]
return shadowArray.map(s => ({
const shadowArray = Array.isArray(targetShadow)
? targetShadow
: [targetShadow]
return shadowArray.map((s) => ({
...s,
color: findColor(s.color, { dynamicVars, staticVars })
color: findColor(s.color, { dynamicVars, staticVars }),
}))
})
}
export const findColor = (color, { dynamicVars, staticVars }) => {
try {
if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color
if (
typeof color !== 'string' ||
(!color.startsWith('--') && !color.startsWith('$'))
)
return color
let targetColor = null
if (color.startsWith('--')) {
// Modifier support is pretty much for v2 themes only
const [variable, modifier] = color.split(/,/g).map(str => str.trim())
const [variable, modifier] = color.split(/,/g).map((str) => str.trim())
const variableSlot = variable.substring(2)
if (variableSlot === 'stack') {
const { r, g, b } = dynamicVars.stacked
@ -81,7 +92,9 @@ export const findColor = (color, { dynamicVars, staticVars }) => {
targetColor = { r, g, b }
} else {
const virtualSlot = variableSlot.replace(/^parent/, '')
targetColor = convert(dynamicVars.lowerLevelVirtualDirectivesRaw[virtualSlot]).rgb
targetColor = convert(
dynamicVars.lowerLevelVirtualDirectivesRaw[virtualSlot],
).rgb
}
} else {
const staticVar = staticVars[variableSlot]
@ -98,18 +111,32 @@ ${JSON.stringify(dynamicVars, null, 2)}`)
}
if (modifier) {
const effectiveBackground = dynamicVars.lowerLevelBackground ?? targetColor
const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5
const effectiveBackground =
dynamicVars.lowerLevelBackground ?? targetColor
const isLightOnDark =
relativeLuminance(convert(effectiveBackground).rgb) < 0.5
const mod = isLightOnDark ? 1 : -1
targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
targetColor = brightness(
Number.parseFloat(modifier) * mod,
targetColor,
).rgb
}
}
if (color.startsWith('$')) {
try {
targetColor = process(color, colorFunctions, { findColor }, { dynamicVars, staticVars })
targetColor = process(
color,
colorFunctions,
{ findColor },
{ dynamicVars, staticVars },
)
} catch (e) {
console.error('Failure executing color function', e ,'\n Function: ' + color)
console.error(
'Failure executing color function',
e,
'\n Function: ' + color,
)
targetColor = '#FF00FF'
}
}
@ -124,10 +151,17 @@ ${JSON.stringify(dynamicVars, null, 2)}\nError: ${e}`)
}
}
const getTextColorAlpha = (directives, intendedTextColor, dynamicVars, staticVars) => {
const getTextColorAlpha = (
directives,
intendedTextColor,
dynamicVars,
staticVars,
) => {
const opacity = directives.textOpacity
const backgroundColor = convert(dynamicVars.lowerLevelBackground).rgb
const textColor = convert(findColor(intendedTextColor, { dynamicVars, staticVars })).rgb
const textColor = convert(
findColor(intendedTextColor, { dynamicVars, staticVars }),
).rgb
if (opacity === null || opacity === undefined || opacity >= 1) {
return convert(textColor).hex
}
@ -148,26 +182,29 @@ const getTextColorAlpha = (directives, intendedTextColor, dynamicVars, staticVar
// Loading all style.js[on] files dynamically
const componentsContext = import.meta.glob(
['/src/**/*.style.js', '/src/**/*.style.json'],
{ eager: true }
{ eager: true },
)
Object.keys(componentsContext).forEach(key => {
Object.keys(componentsContext).forEach((key) => {
const component = componentsContext[key].default
if (components[component.name] != null) {
console.warn(`Component in file ${key} is trying to override existing component ${component.name}! You have collisions/duplicates!`)
console.warn(
`Component in file ${key} is trying to override existing component ${component.name}! You have collisions/duplicates!`,
)
}
components[component.name] = component
})
Object.keys(components).forEach(key => {
Object.keys(components).forEach((key) => {
if (key === 'Root') return
components.Root.validInnerComponents = components.Root.validInnerComponents || []
components.Root.validInnerComponents =
components.Root.validInnerComponents || []
components.Root.validInnerComponents.push(key)
})
Object.keys(components).forEach(key => {
Object.keys(components).forEach((key) => {
const component = components[key]
const { validInnerComponents = [] } = component
validInnerComponents.forEach(inner => {
validInnerComponents.forEach((inner) => {
const child = components[inner]
component.possibleChildren = component.possibleChildren || []
component.possibleChildren.push(child)
@ -176,7 +213,6 @@ Object.keys(components).forEach(key => {
})
})
const engineChecksum = sum(components)
const ruleToSelector = genericRuleToSelector(components)
@ -206,7 +242,7 @@ export const init = ({
liteMode = false,
editMode = false,
onlyNormalState = false,
initialStaticVars = {}
initialStaticVars = {},
}) => {
const rootComponentName = 'Root'
if (!inputRuleset) throw new Error('Ruleset is null or undefined!')
@ -216,10 +252,16 @@ export const init = ({
const rulesetUnsorted = [
...Object.values(components)
.map(c => (c.defaultRules || []).map(r => ({ source: 'Built-in', component: c.name, ...r })))
.map((c) =>
(c.defaultRules || []).map((r) => ({
source: 'Built-in',
component: c.name,
...r,
})),
)
.reduce((acc, arr) => [...acc, ...arr], []),
...inputRuleset
].map(rule => {
...inputRuleset,
].map((rule) => {
normalizeCombination(rule)
let currentParent = rule.parent
while (currentParent) {
@ -245,8 +287,8 @@ export const init = ({
aScore += a.variant !== 'normal' ? 100 : 0
bScore += b.variant !== 'normal' ? 100 : 0
aScore += a.state.filter(x => x !== 'normal').length * 1000
bScore += b.state.filter(x => x !== 'normal').length * 1000
aScore += a.state.filter((x) => x !== 'normal').length * 1000
bScore += b.state.filter((x) => x !== 'normal').length * 1000
aScore += a.component === 'Text' ? 1 : 0
bScore += b.component === 'Text' ? 1 : 0
@ -263,24 +305,44 @@ export const init = ({
.map(({ data }) => data)
if (!ultimateBackgroundColor) {
console.warn('No ultimate background color provided, falling back to panel color')
const rootRule = ruleset.findLast((x) => (x.component === 'Root' && x.directives?.['--bg']))
console.warn(
'No ultimate background color provided, falling back to panel color',
)
const rootRule = ruleset.findLast(
(x) => x.component === 'Root' && x.directives?.['--bg'],
)
ultimateBackgroundColor = rootRule.directives['--bg'].split('|')[1].trim()
}
const virtualComponents = new Set(Object.values(components).filter(c => c.virtual).map(c => c.name))
const transparentComponents = new Set(Object.values(components).filter(c => c.transparent).map(c => c.name))
const nonEditableComponents = new Set(Object.values(components).filter(c => c.notEditable).map(c => c.name))
const virtualComponents = new Set(
Object.values(components)
.filter((c) => c.virtual)
.map((c) => c.name),
)
const transparentComponents = new Set(
Object.values(components)
.filter((c) => c.transparent)
.map((c) => c.name),
)
const nonEditableComponents = new Set(
Object.values(components)
.filter((c) => c.notEditable)
.map((c) => c.name),
)
const extraCompileComponents = new Set([])
Object.values(components).forEach(component => {
const relevantRules = ruleset.filter(r => r.component === component.name)
const backgrounds = relevantRules.map(r => r.directives.background).filter(x => x)
const opacities = relevantRules.map(r => r.directives.opacity).filter(x => x)
Object.values(components).forEach((component) => {
const relevantRules = ruleset.filter((r) => r.component === component.name)
const backgrounds = relevantRules
.map((r) => r.directives.background)
.filter((x) => x)
const opacities = relevantRules
.map((r) => r.directives.opacity)
.filter((x) => x)
if (
backgrounds.some(x => x.match(/--parent/)) ||
opacities.some(x => x != null && x < 1))
{
backgrounds.some((x) => x.match(/--parent/)) ||
opacities.some((x) => x != null && x < 1)
) {
extraCompileComponents.add(component.name)
}
})
@ -299,25 +361,26 @@ export const init = ({
// FIXME hack for editor until it supports handling component backgrounds
lowerLevelBackground = '#00FFFF'
}
const lowerLevelVirtualDirectives = computed[lowerLevelSelector]?.virtualDirectives
const lowerLevelVirtualDirectivesRaw = computed[lowerLevelSelector]?.virtualDirectivesRaw
const lowerLevelVirtualDirectives =
computed[lowerLevelSelector]?.virtualDirectives
const lowerLevelVirtualDirectivesRaw =
computed[lowerLevelSelector]?.virtualDirectivesRaw
const dynamicVars = computed[selector] || {
lowerLevelSelector,
lowerLevelBackground,
lowerLevelVirtualDirectives,
lowerLevelVirtualDirectivesRaw
lowerLevelVirtualDirectivesRaw,
}
// Inheriting all of the applicable rules
const existingRules = ruleset.filter(findRules(combination))
const computedDirectives =
existingRules
.map(r => r.directives)
.reduce((acc, directives) => ({ ...acc, ...directives }), {})
const computedDirectives = existingRules
.map((r) => r.directives)
.reduce((acc, directives) => ({ ...acc, ...directives }), {})
const computedRule = {
...combination,
directives: computedDirectives
directives: computedDirectives,
}
computed[selector] = computed[selector] || {}
@ -327,7 +390,8 @@ export const init = ({
// avoid putting more stuff into actual CSS
computed[selector].virtualDirectives = {}
// but still be able to access it i.e. from --parent
computed[selector].virtualDirectivesRaw = computed[lowerLevelSelector]?.virtualDirectivesRaw || {}
computed[selector].virtualDirectivesRaw =
computed[lowerLevelSelector]?.virtualDirectivesRaw || {}
if (virtualComponents.has(combination.component)) {
const virtualName = [
@ -335,22 +399,37 @@ export const init = ({
combination.component.toLowerCase(),
combination.variant === 'normal'
? ''
: combination.variant[0].toUpperCase() + combination.variant.slice(1).toLowerCase(),
...sortBy(combination.state.filter(x => x !== 'normal')).map(state => state[0].toUpperCase() + state.slice(1).toLowerCase())
: combination.variant[0].toUpperCase() +
combination.variant.slice(1).toLowerCase(),
...sortBy(combination.state.filter((x) => x !== 'normal')).map(
(state) => state[0].toUpperCase() + state.slice(1).toLowerCase(),
),
].join('')
let inheritedTextColor = computedDirectives.textColor
let inheritedTextAuto = computedDirectives.textAuto
let inheritedTextOpacity = computedDirectives.textOpacity
let inheritedTextOpacityMode = computedDirectives.textOpacityMode
const lowerLevelTextSelector = [...selector.split(/ /g).slice(0, -1), soloSelector].join(' ')
const lowerLevelTextSelector = [
...selector.split(/ /g).slice(0, -1),
soloSelector,
].join(' ')
const lowerLevelTextRule = computed[lowerLevelTextSelector]
if (inheritedTextColor == null || inheritedTextOpacity == null || inheritedTextOpacityMode == null) {
inheritedTextColor = computedDirectives.textColor ?? lowerLevelTextRule.textColor
inheritedTextAuto = computedDirectives.textAuto ?? lowerLevelTextRule.textAuto
inheritedTextOpacity = computedDirectives.textOpacity ?? lowerLevelTextRule.textOpacity
inheritedTextOpacityMode = computedDirectives.textOpacityMode ?? lowerLevelTextRule.textOpacityMode
if (
inheritedTextColor == null ||
inheritedTextOpacity == null ||
inheritedTextOpacityMode == null
) {
inheritedTextColor =
computedDirectives.textColor ?? lowerLevelTextRule.textColor
inheritedTextAuto =
computedDirectives.textAuto ?? lowerLevelTextRule.textAuto
inheritedTextOpacity =
computedDirectives.textOpacity ?? lowerLevelTextRule.textOpacity
inheritedTextOpacityMode =
computedDirectives.textOpacityMode ??
lowerLevelTextRule.textOpacityMode
}
const newTextRule = {
@ -360,26 +439,37 @@ export const init = ({
textColor: inheritedTextColor,
textAuto: inheritedTextAuto ?? 'preserve',
textOpacity: inheritedTextOpacity,
textOpacityMode: inheritedTextOpacityMode
}
textOpacityMode: inheritedTextOpacityMode,
},
}
dynamicVars.inheritedBackground = lowerLevelBackground
dynamicVars.stacked = convert(stacked[lowerLevelSelector]).rgb
const intendedTextColor = convert(findColor(inheritedTextColor, { dynamicVars, staticVars })).rgb
const textColor = newTextRule.directives.textAuto === 'no-auto'
? intendedTextColor
: getTextColor(
convert(stacked[lowerLevelSelector]).rgb,
intendedTextColor,
newTextRule.directives.textAuto === 'preserve'
)
const virtualDirectives = { ...(computed[lowerLevelSelector].virtualDirectives || {}) }
const virtualDirectivesRaw = { ...(computed[lowerLevelSelector].virtualDirectivesRaw || {}) }
const intendedTextColor = convert(
findColor(inheritedTextColor, { dynamicVars, staticVars }),
).rgb
const textColor =
newTextRule.directives.textAuto === 'no-auto'
? intendedTextColor
: getTextColor(
convert(stacked[lowerLevelSelector]).rgb,
intendedTextColor,
newTextRule.directives.textAuto === 'preserve',
)
const virtualDirectives = {
...(computed[lowerLevelSelector].virtualDirectives || {}),
}
const virtualDirectivesRaw = {
...(computed[lowerLevelSelector].virtualDirectivesRaw || {}),
}
// Storing color data in lower layer to use as custom css properties
virtualDirectives[virtualName] = getTextColorAlpha(newTextRule.directives, textColor, dynamicVars)
virtualDirectives[virtualName] = getTextColorAlpha(
newTextRule.directives,
textColor,
dynamicVars,
)
virtualDirectivesRaw[virtualName] = textColor
computed[lowerLevelSelector].virtualDirectives = virtualDirectives
@ -391,13 +481,14 @@ export const init = ({
...combination,
directives: {},
virtualDirectives,
virtualDirectivesRaw
virtualDirectivesRaw,
}
} else {
computed[selector] = computed[selector] || {}
// TODO: DEFAULT TEXT COLOR
const lowerLevelStackedBackground = stacked[lowerLevelSelector] || convert(ultimateBackgroundColor).rgb
const lowerLevelStackedBackground =
stacked[lowerLevelSelector] || convert(ultimateBackgroundColor).rgb
if (computedDirectives.background) {
let inheritRule = null
@ -405,27 +496,37 @@ export const init = ({
findRules({
component: combination.component,
variant: combination.variant,
parent: combination.parent
})
parent: combination.parent,
}),
)
const lastVariantRule = variantRules[variantRules.length - 1]
if (lastVariantRule) {
inheritRule = lastVariantRule
} else {
const normalRules = ruleset.filter(findRules({
component: combination.component,
parent: combination.parent
}))
const normalRules = ruleset.filter(
findRules({
component: combination.component,
parent: combination.parent,
}),
)
const lastNormalRule = normalRules[normalRules.length - 1]
inheritRule = lastNormalRule
}
const inheritSelector = ruleToSelector({ ...inheritRule, parent: combination.parent }, true)
const inheritSelector = ruleToSelector(
{ ...inheritRule, parent: combination.parent },
true,
)
const inheritedBackground = computed[inheritSelector].background
dynamicVars.inheritedBackground = inheritedBackground
const rgb = convert(findColor(computedDirectives.background, { dynamicVars, staticVars })).rgb
const rgb = convert(
findColor(computedDirectives.background, {
dynamicVars,
staticVars,
}),
).rgb
if (!stacked[selector]) {
let blend
@ -435,31 +536,48 @@ export const init = ({
} else if (alpha <= 0) {
blend = lowerLevelStackedBackground
} else {
blend = alphaBlend(rgb, computedDirectives.opacity, lowerLevelStackedBackground)
blend = alphaBlend(
rgb,
computedDirectives.opacity,
lowerLevelStackedBackground,
)
}
stacked[selector] = blend
computed[selector].background = { ...rgb, a: computedDirectives.opacity ?? 1 }
computed[selector].background = {
...rgb,
a: computedDirectives.opacity ?? 1,
}
}
}
if (computedDirectives.shadow) {
dynamicVars.shadow = flattenDeep(findShadow(flattenDeep(computedDirectives.shadow), { dynamicVars, staticVars }))
dynamicVars.shadow = flattenDeep(
findShadow(flattenDeep(computedDirectives.shadow), {
dynamicVars,
staticVars,
}),
)
}
if (!stacked[selector]) {
computedDirectives.background = 'transparent'
computedDirectives.opacity = 0
stacked[selector] = lowerLevelStackedBackground
computed[selector].background = { ...lowerLevelStackedBackground, a: 0 }
computed[selector].background = {
...lowerLevelStackedBackground,
a: 0,
}
}
dynamicVars.stacked = stacked[selector]
dynamicVars.background = computed[selector].background
const dynamicSlots = Object.entries(computedDirectives).filter(([k]) => k.startsWith('--'))
const dynamicSlots = Object.entries(computedDirectives).filter(([k]) =>
k.startsWith('--'),
)
dynamicSlots.forEach(([k, v]) => {
const [type, value] = v.split('|').map(x => x.trim()) // woah, Extreme!
const [type, value] = v.split('|').map((x) => x.trim()) // woah, Extreme!
switch (type) {
case 'color': {
const color = findColor(value, { dynamicVars, staticVars })
@ -470,7 +588,10 @@ export const init = ({
break
}
case 'shadow': {
const shadow = value.split(/,/g).map(s => s.trim()).filter(x => x)
const shadow = value
.split(/,/g)
.map((s) => s.trim())
.filter((x) => x)
dynamicVars[k] = shadow
if (combination.component === rootComponentName) {
staticVars[k.substring(2)] = shadow
@ -491,81 +612,97 @@ export const init = ({
dynamicVars,
selector: cssSelector,
...combination,
directives: computedDirectives
directives: computedDirectives,
}
return rule
}
} catch (e) {
const { component, variant, state } = combination
throw new Error(`Error processing combination ${component}.${variant}:${state.join(':')}: ${e}`)
throw new Error(
`Error processing combination ${component}.${variant}:${state.join(':')}: ${e}`,
)
}
}
const processInnerComponent = (component, parent) => {
const combinations = []
const {
states: originalStates = {},
variants: originalVariants = {}
} = component
const { states: originalStates = {}, variants: originalVariants = {} } =
component
let validInnerComponents
if (editMode) {
const temp = (component.validInnerComponentsLite || component.validInnerComponents || [])
validInnerComponents = temp
.filter(c => virtualComponents.has(c) && !nonEditableComponents.has(c))
const temp =
component.validInnerComponentsLite ||
component.validInnerComponents ||
[]
validInnerComponents = temp.filter(
(c) => virtualComponents.has(c) && !nonEditableComponents.has(c),
)
} else if (liteMode) {
validInnerComponents = (component.validInnerComponentsLite || component.validInnerComponents || [])
} else if (component.name === 'Root' || component.states != null || component.background?.includes('--parent')) {
validInnerComponents =
component.validInnerComponentsLite ||
component.validInnerComponents ||
[]
} else if (
component.name === 'Root' ||
component.states != null ||
component.background?.includes('--parent')
) {
validInnerComponents = component.validInnerComponents || []
} else {
validInnerComponents = component
.validInnerComponents
?.filter(
c => virtualComponents.has(c)
|| transparentComponents.has(c)
|| extraCompileComponents.has(c)
)
|| []
validInnerComponents =
component.validInnerComponents?.filter(
(c) =>
virtualComponents.has(c) ||
transparentComponents.has(c) ||
extraCompileComponents.has(c),
) || []
}
// Normalizing states and variants to always include "normal"
const states = { normal: '', ...originalStates }
const variants = { normal: '', ...originalVariants }
const innerComponents = (validInnerComponents).map(name => {
const innerComponents = validInnerComponents.map((name) => {
const result = components[name]
if (result === undefined) console.error(`Component ${component.name} references a component ${name} which does not exist!`)
if (result === undefined)
console.error(
`Component ${component.name} references a component ${name} which does not exist!`,
)
return result
})
// Optimization: we only really need combinations without "normal" because all states implicitly have it
const permutationStateKeys = Object.keys(states).filter(s => s !== 'normal')
const stateCombinations = (onlyNormalState && !virtualComponents.has(component.name))
? [
['normal']
]
: [
['normal'],
...getAllPossibleCombinations(permutationStateKeys)
.map(combination => ['normal', ...combination])
.filter(combo => {
// Optimization: filter out some hard-coded combinations that don't make sense
if (combo.indexOf('disabled') >= 0) {
return !(
combo.indexOf('hover') >= 0 ||
const permutationStateKeys = Object.keys(states).filter(
(s) => s !== 'normal',
)
const stateCombinations =
onlyNormalState && !virtualComponents.has(component.name)
? [['normal']]
: [
['normal'],
...getAllPossibleCombinations(permutationStateKeys)
.map((combination) => ['normal', ...combination])
.filter((combo) => {
// Optimization: filter out some hard-coded combinations that don't make sense
if (combo.indexOf('disabled') >= 0) {
return !(
combo.indexOf('hover') >= 0 ||
combo.indexOf('focused') >= 0 ||
combo.indexOf('pressed') >= 0
)
}
return true
})
]
)
}
return true
}),
]
const stateVariantCombination = Object.keys(variants).map(variant => {
return stateCombinations.map(state => ({ variant, state }))
}).reduce((acc, x) => [...acc, ...x], [])
const stateVariantCombination = Object.keys(variants)
.map((variant) => {
return stateCombinations.map((state) => ({ variant, state }))
})
.reduce((acc, x) => [...acc, ...x], [])
stateVariantCombination.forEach(combination => {
stateVariantCombination.forEach((combination) => {
combination.component = component.name
combination.lazy = component.lazy || parent?.lazy
combination.parent = parent
@ -576,16 +713,16 @@ export const init = ({
if (
!liteMode &&
parent?.component !== 'Root' &&
!virtualComponents.has(component.name) &&
!transparentComponents.has(component.name) &&
extraCompileComponents.has(component.name)
!virtualComponents.has(component.name) &&
!transparentComponents.has(component.name) &&
extraCompileComponents.has(component.name)
) {
combination.lazy = true
}
combinations.push(combination)
innerComponents.forEach(innerComponent => {
innerComponents.forEach((innerComponent) => {
combinations.push(...processInnerComponent(innerComponent, combination))
})
})
@ -594,19 +731,23 @@ export const init = ({
}
const t0 = performance.now()
const combinations = processInnerComponent(components[rootComponentName] ?? components.Root)
const combinations = processInnerComponent(
components[rootComponentName] ?? components.Root,
)
const t1 = performance.now()
if (debug) {
console.debug('Tree traveral took ' + (t1 - t0) + ' ms')
}
const result = combinations.map((combination) => {
if (combination.lazy) {
return async () => processCombination(combination)
} else {
return processCombination(combination)
}
}).filter(x => x)
const result = combinations
.map((combination) => {
if (combination.lazy) {
return async () => processCombination(combination)
} else {
return processCombination(combination)
}
})
.filter((x) => x)
const t2 = performance.now()
if (debug) {
console.debug('Eager processing took ' + (t2 - t1) + ' ms')
@ -616,7 +757,7 @@ export const init = ({
const eager = []
const lazy = []
result.forEach(x => {
result.forEach((x) => {
if (typeof x === 'function') {
lazy.push(x)
} else {
@ -629,6 +770,6 @@ export const init = ({
eager,
staticVars,
engineChecksum,
themeChecksum: sum([lazy, eager])
themeChecksum: sum([lazy, eager]),
}
}

View file

@ -4,7 +4,15 @@ import apiService from '../api/api.service.js'
import { promiseInterval } from '../promise_interval/promise_interval.js'
import { useInterfaceStore } from 'src/stores/interface.js'
const update = ({ store, statuses, timeline, showImmediately, userId, listId, pagination }) => {
const update = ({
store,
statuses,
timeline,
showImmediately,
userId,
listId,
pagination,
}) => {
const ccTimeline = camelCase(timeline)
store.dispatch('addNewStatuses', {
@ -13,7 +21,7 @@ const update = ({ store, statuses, timeline, showImmediately, userId, listId, pa
listId,
statuses,
showImmediately,
pagination
pagination,
})
}
@ -29,7 +37,7 @@ const fetchAndUpdate = ({
bookmarkFolderId = false,
tag = false,
until,
since
since,
}) => {
const args = { timeline, credentials }
const rootState = store.rootState || store.state
@ -54,14 +62,18 @@ const fetchAndUpdate = ({
args.bookmarkFolderId = bookmarkFolderId
args.tag = tag
args.withMuted = !hideMutedPosts
if (loggedIn && ['friends', 'public', 'publicAndExternal', 'bubble'].includes(timeline)) {
if (
loggedIn &&
['friends', 'public', 'publicAndExternal', 'bubble'].includes(timeline)
) {
args.replyVisibility = replyVisibility
}
const numStatusesBeforeFetch = timelineData.statuses.length
return apiService.fetchTimeline(args)
.then(response => {
return apiService
.fetchTimeline(args)
.then((response) => {
if (response.errors) {
if (timeline === 'favorites') {
rootState.instance.pleromaPublicFavouritesAvailable = false
@ -71,10 +83,23 @@ const fetchAndUpdate = ({
}
const { data: statuses, pagination } = response
if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) {
if (
!older &&
statuses.length >= 20 &&
!timelineData.loading &&
numStatusesBeforeFetch > 0
) {
store.dispatch('queueFlush', { timeline, id: timelineData.maxId })
}
update({ store, statuses, timeline, showImmediately, userId, listId, pagination })
update({
store,
statuses,
timeline,
showImmediately,
userId,
listId,
pagination,
})
return { statuses, pagination }
})
.catch((error) => {
@ -82,26 +107,54 @@ const fetchAndUpdate = ({
level: 'error',
messageKey: 'timeline.error',
messageArgs: [error.message],
timeout: 5000
timeout: 5000,
})
})
}
const startFetching = ({ timeline = 'friends', credentials, store, userId = false, listId = false, statusId = false, bookmarkFolderId = false, tag = false }) => {
const startFetching = ({
timeline = 'friends',
credentials,
store,
userId = false,
listId = false,
statusId = false,
bookmarkFolderId = false,
tag = false,
}) => {
const rootState = store.rootState || store.state
const timelineData = rootState.statuses.timelines[camelCase(timeline)]
const showImmediately = timelineData.visibleStatuses.length === 0
timelineData.userId = userId
timelineData.listId = listId
timelineData.bookmarkFolderId = bookmarkFolderId
fetchAndUpdate({ timeline, credentials, store, showImmediately, userId, listId, statusId, bookmarkFolderId, tag })
fetchAndUpdate({
timeline,
credentials,
store,
showImmediately,
userId,
listId,
statusId,
bookmarkFolderId,
tag,
})
const boundFetchAndUpdate = () =>
fetchAndUpdate({ timeline, credentials, store, userId, listId, statusId, bookmarkFolderId, tag })
fetchAndUpdate({
timeline,
credentials,
store,
userId,
listId,
statusId,
bookmarkFolderId,
tag,
})
return promiseInterval(boundFetchAndUpdate, 10000)
}
const timelineFetcher = {
fetchAndUpdate,
startFetching
startFetching,
}
export default timelineFetcher

View file

@ -11,7 +11,7 @@ const highlightStyle = (prefs) => {
const customProps = {
'--____highlight-solidColor': solidColor,
'--____highlight-tintColor': tintColor,
'--____highlight-tintColor2': tintColor2
'--____highlight-tintColor2': tintColor2,
}
if (type === 'striped') {
return {
@ -20,15 +20,15 @@ const highlightStyle = (prefs) => {
`${tintColor} ,`,
`${tintColor} 20px,`,
`${tintColor2} 20px,`,
`${tintColor2} 40px`
`${tintColor2} 40px`,
].join(' '),
backgroundPosition: '0 0',
...customProps
...customProps,
}
} else if (type === 'solid') {
return {
backgroundColor: tintColor2,
...customProps
...customProps,
}
} else if (type === 'side') {
return {
@ -36,21 +36,18 @@ const highlightStyle = (prefs) => {
'linear-gradient(to right,',
`${solidColor} ,`,
`${solidColor} 2px,`,
'transparent 6px'
'transparent 6px',
].join(' '),
backgroundPosition: '0 0',
...customProps
...customProps,
}
}
}
const highlightClass = (user) => {
return 'USER____' + user.screen_name
?.replace(/\./g, '_')
.replace(/@/g, '_AT_')
return (
'USER____' + user.screen_name?.replace(/\./g, '_').replace(/@/g, '_AT_')
)
}
export {
highlightClass,
highlightStyle
}
export { highlightClass, highlightStyle }

View file

@ -1,13 +1,16 @@
import { includes } from 'lodash'
const generateProfileLink = (id, screenName, restrictedNicknames) => {
const complicated = !screenName || (isExternal(screenName) || includes(restrictedNicknames, screenName))
const complicated =
!screenName ||
isExternal(screenName) ||
includes(restrictedNicknames, screenName)
return {
name: (complicated ? 'external-user-profile' : 'user-profile'),
params: (complicated ? { id } : { name: screenName })
name: complicated ? 'external-user-profile' : 'user-profile',
params: complicated ? { id } : { name: screenName },
}
}
const isExternal = screenName => screenName && screenName.includes('@')
const isExternal = (screenName) => screenName && screenName.includes('@')
export default generateProfileLink