From ec2937ec1f3b0ae153f79604eb35b57ffe0f9af2 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 13 Nov 2023 17:29:25 +0200 Subject: add options for marking single notification as read --- src/components/notification/notification.vue | 1 + 1 file changed, 1 insertion(+) (limited to 'src/components/notification/notification.vue') diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 6b3315f9..01ad395f 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -6,6 +6,7 @@ class="Notification" :compact="true" :statusoid="notification.status" + @interacted="interacted" />
-- cgit v1.2.3-70-g09d2 From aad3225d25460170a8dd48f8ffcbc63f99a28b7f Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 16 Nov 2023 19:26:18 +0200 Subject: refactored notifications into their own module separate from statuses (WIP) --- src/components/notification/notification.vue | 2 +- src/components/notifications/notifications.js | 16 +-- src/main.js | 2 + src/modules/notifications.js | 158 +++++++++++++++++++++ src/modules/statuses.js | 141 +----------------- src/modules/users.js | 2 +- .../desktop_notification_utils.js | 2 +- .../entity_normalizer/entity_normalizer.service.js | 1 - .../notification_utils/notification_utils.js | 2 +- .../notifications_fetcher.service.js | 2 +- 10 files changed, 170 insertions(+), 158 deletions(-) create mode 100644 src/modules/notifications.js (limited to 'src/components/notification/notification.vue') diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 01ad395f..a8eceab0 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -249,7 +249,7 @@ diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index 4cbe8093..a1088dff 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -65,7 +65,7 @@ const Notifications = { return notificationsFromStore(this.$store) }, error () { - return this.$store.state.statuses.notifications.error + return this.$store.state.notifications.error }, unseenNotifications () { return unseenNotificationsFromStore(this.$store) @@ -86,7 +86,7 @@ const Notifications = { return this.unseenNotifications.length + (this.unreadChatCount) + this.unreadAnnouncementCount }, loading () { - return this.$store.state.statuses.notifications.loading + return this.$store.state.notifications.loading }, noHeading () { const { layoutType } = this.$store.state.interface @@ -160,17 +160,7 @@ const Notifications = { this.showScrollTop = this.$refs.root.offsetTop < this.scrollerRef.scrollTop }, notificationClicked (notification) { - const { type, id, seen } = notification - if (!seen) { - switch (type) { - case 'mention': - case 'pleroma:report': - case 'follow_request': - break - default: - this.markOneAsSeen(id) - } - } + // const { type, id, seen } = notification }, notificationInteracted (notification) { const { id, seen } = notification diff --git a/src/main.js b/src/main.js index 0b7c7674..85eb1f4c 100644 --- a/src/main.js +++ b/src/main.js @@ -6,6 +6,7 @@ import './lib/event_target_polyfill.js' import interfaceModule from './modules/interface.js' import instanceModule from './modules/instance.js' import statusesModule from './modules/statuses.js' +import notificationsModule from './modules/notifications.js' import listsModule from './modules/lists.js' import usersModule from './modules/users.js' import apiModule from './modules/api.js' @@ -78,6 +79,7 @@ const persistedStateOptions = { // TODO refactor users/statuses modules, they depend on each other users: usersModule, statuses: statusesModule, + notifications: notificationsModule, lists: listsModule, api: apiModule, config: configModule, diff --git a/src/modules/notifications.js b/src/modules/notifications.js new file mode 100644 index 00000000..03f220c7 --- /dev/null +++ b/src/modules/notifications.js @@ -0,0 +1,158 @@ +import apiService from '../services/api/api.service.js' + +import { + isStatusNotification, + isValidNotification, + maybeShowNotification +} from '../services/notification_utils/notification_utils.js' + +import { + closeDesktopNotification, + closeAllDesktopNotifications +} from '../services/desktop_notification_utils/desktop_notification_utils.js' + +const emptyNotifications = () => ({ + desktopNotificationSilence: true, + maxId: 0, + minId: Number.POSITIVE_INFINITY, + data: [], + idStore: {}, + loading: false +}) + +export const defaultState = () => ({ + ...emptyNotifications() +}) + +export const notifications = { + state: defaultState(), + mutations: { + addNewNotifications (state, { notifications }) { + notifications.forEach(notification => { + state.data.push(notification) + state.idStore[notification.id] = notification + }) + }, + clearNotifications (state) { + state = emptyNotifications() + }, + updateNotificationsMinMaxId (state, id) { + state.maxId = id > state.maxId ? id : state.maxId + state.minId = id < state.minId ? id : state.minId + }, + setNotificationsLoading (state, { value }) { + state.loading = value + }, + setNotificationsSilence (state, { value }) { + state.desktopNotificationSilence = value + }, + markNotificationsAsSeen (state) { + state.data.forEach((notification) => { + notification.seen = true + }) + }, + markSingleNotificationAsSeen (state, { id }) { + const notification = find(state.data, n => n.id === id) + if (notification) notification.seen = true + }, + dismissNotification (state, { id }) { + state.data = state.data.filter(n => n.id !== id) + }, + dismissNotifications (state, { finder }) { + state.data = state.data.filter(n => finder) + }, + updateNotification (state, { id, updater }) { + const notification = find(state.data, n => n.id === id) + notification && updater(notification) + } + }, + actions: { + addNewNotifications (store, { notifications, older }) { + const { commit, dispatch, state, rootState } = store + const validNotifications = notifications.filter((notification) => { + // If invalid notification, update ids but don't add it to store + if (!isValidNotification(notification)) { + console.error('Invalid notification:', notification) + commit('updateNotificationsMinMaxId', notification.id) + return false + } + return true + }) + + const statusNotifications = validNotifications.filter(notification => isStatusNotification(notification.type) && notification.status) + + // Synchronous commit to add all the statuses + commit('addNewStatuses', { statuses: statusNotifications.map(notification => notification.status) }) + + // Update references to statuses in notifications to ones in the store + statusNotifications.forEach(notification => { + const id = notification.status.id + const referenceStatus = rootState.statuses.allStatusesObject[id] + console.log() + + if (referenceStatus) { + notification.status = referenceStatus + } + }) + + validNotifications.forEach(notification => { + if (notification.type === 'pleroma:report') { + dispatch('addReport', notification.report) + } + + if (notification.type === 'pleroma:emoji_reaction') { + dispatch('fetchEmojiReactionsBy', notification.status.id) + } + + // Only add a new notification if we don't have one for the same action + // eslint-disable-next-line no-prototype-builtins + if (!state.idStore.hasOwnProperty(notification.id)) { + commit('updateNotificationsMinMaxId', notification.id) + + maybeShowNotification(store, notification) + } else if (notification.seen) { + state.idStore[notification.id].seen = true + } + }) + + commit('addNewNotifications', { notifications }) + }, + setNotificationsLoading ({ rootState, commit }, { value }) { + commit('setNotificationsLoading', { value }) + }, + setNotificationsSilence ({ rootState, commit }, { value }) { + commit('setNotificationsSilence', { value }) + }, + markNotificationsAsSeen ({ rootState, state, commit }) { + commit('markNotificationsAsSeen') + apiService.markNotificationsAsSeen({ + id: state.maxId, + credentials: rootState.users.currentUser.credentials + }).then(() => { + closeAllDesktopNotifications(rootState) + }) + }, + markSingleNotificationAsSeen ({ rootState, commit }, { id }) { + commit('markSingleNotificationAsSeen', { id }) + apiService.markNotificationsAsSeen({ + single: true, + id, + credentials: rootState.users.currentUser.credentials + }).then(() => { + closeDesktopNotification(rootState, id) + }) + }, + dismissNotificationLocal ({ rootState, commit }, { id }) { + commit('dismissNotification', { id }) + }, + dismissNotification ({ rootState, commit }, { id }) { + commit('dismissNotification', { id }) + rootState.api.backendInteractor.dismissNotification({ id }) + }, + updateNotification ({ rootState, commit }, { id, updater }) { + commit('updateNotification', { id, updater }) + } + } +} + +export default notifications diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 6e331d16..d6f19589 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -12,15 +12,6 @@ import { isArray, omitBy } from 'lodash' -import { - isStatusNotification, - isValidNotification, - maybeShowNotification -} from '../services/notification_utils/notification_utils.js' -import { - closeDesktopNotification, - closeAllDesktopNotifications -} from '../services/desktop_notification_utils/desktop_notification_utils.js' import apiService from '../services/api/api.service.js' const emptyTl = (userId = 0) => ({ @@ -40,22 +31,12 @@ const emptyTl = (userId = 0) => ({ flushMarker: 0 }) -const emptyNotifications = () => ({ - desktopNotificationSilence: true, - maxId: 0, - minId: Number.POSITIVE_INFINITY, - data: [], - idStore: {}, - loading: false -}) - export const defaultState = () => ({ allStatuses: [], scrobblesNextFetch: {}, allStatusesObject: {}, conversationsObject: {}, maxId: 0, - notifications: emptyNotifications(), favorites: new Set(), timelines: { mentions: emptyTl(), @@ -164,9 +145,6 @@ const removeStatusFromGlobalStorage = (state, status) => { // TODO: Need to remove from allStatusesObject? - // Remove possible notification - remove(state.notifications.data, ({ action: { id } }) => id === status.id) - // Remove from conversation const conversationId = status.statusnet_conversation_id if (state.conversationsObject[conversationId]) { @@ -342,52 +320,6 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us } } -const updateNotificationsMinMaxId = (state, notification) => { - state.notifications.maxId = notification.id > state.notifications.maxId - ? notification.id - : state.notifications.maxId - state.notifications.minId = notification.id < state.notifications.minId - ? notification.id - : state.notifications.minId -} - -const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters, newNotificationSideEffects }) => { - each(notifications, (notification) => { - // If invalid notification, update ids but don't add it to store - if (!isValidNotification(notification)) { - console.error('Invalid notification:', notification) - updateNotificationsMinMaxId(state, notification) - return - } - - if (isStatusNotification(notification.type)) { - notification.action = addStatusToGlobalStorage(state, notification.action).item - notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item - } - - if (notification.type === 'pleroma:report') { - dispatch('addReport', notification.report) - } - - if (notification.type === 'pleroma:emoji_reaction') { - dispatch('fetchEmojiReactionsBy', notification.status.id) - } - - // Only add a new notification if we don't have one for the same action - // eslint-disable-next-line no-prototype-builtins - if (!state.notifications.idStore.hasOwnProperty(notification.id)) { - updateNotificationsMinMaxId(state, notification) - - state.notifications.data.push(notification) - state.notifications.idStore[notification.id] = notification - - newNotificationSideEffects(notification) - } else if (notification.seen) { - state.notifications.idStore[notification.id].seen = true - } - }) -} - const removeStatus = (state, { timeline, userId }) => { const timelineObject = state.timelines[timeline] if (userId) { @@ -400,7 +332,6 @@ const removeStatus = (state, { timeline, userId }) => { export const mutations = { addNewStatuses, - addNewNotifications, removeStatus, showNewStatuses (state, { timeline }) { const oldTimeline = (state.timelines[timeline]) @@ -422,9 +353,6 @@ export const mutations = { const userId = excludeUserId ? state.timelines[timeline].userId : undefined state.timelines[timeline] = emptyTl(userId) }, - clearNotifications (state) { - state.notifications = emptyNotifications() - }, setFavorited (state, { status, value }) { const newStatus = state.allStatusesObject[status.id] @@ -507,31 +435,6 @@ export const mutations = { const newStatus = state.allStatusesObject[id] newStatus.nsfw = nsfw }, - setNotificationsLoading (state, { value }) { - state.notifications.loading = value - }, - setNotificationsSilence (state, { value }) { - state.notifications.desktopNotificationSilence = value - }, - markNotificationsAsSeen (state) { - each(state.notifications.data, (notification) => { - notification.seen = true - }) - }, - markSingleNotificationAsSeen (state, { id }) { - const notification = find(state.notifications.data, n => n.id === id) - if (notification) notification.seen = true - }, - dismissNotification (state, { id }) { - state.notifications.data = state.notifications.data.filter(n => n.id !== id) - }, - dismissNotifications (state, { finder }) { - state.notifications.data = state.notifications.data.filter(n => finder) - }, - updateNotification (state, { id, updater }) { - const notification = find(state.notifications.data, n => n.id === id) - notification && updater(notification) - }, queueFlush (state, { timeline, id }) { state.timelines[timeline].flushMarker = id }, @@ -615,20 +518,9 @@ const statuses = { actions: { addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) { commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination }) - }, - addNewNotifications (store, { notifications, older }) { - const { commit, dispatch, rootGetters } = store - const newNotificationSideEffects = (notification) => { - maybeShowNotification(store, notification) - } - commit('addNewNotifications', { dispatch, notifications, older, rootGetters, newNotificationSideEffects }) - }, - setNotificationsLoading ({ rootState, commit }, { value }) { - commit('setNotificationsLoading', { value }) - }, - setNotificationsSilence ({ rootState, commit }, { value }) { - commit('setNotificationsSilence', { value }) + const deletions = statuses.filter(status => status.type === 'deletion') + console.log(deletions) }, fetchStatus ({ rootState, dispatch }, id) { return rootState.api.backendInteractor.fetchStatus({ id }) @@ -725,35 +617,6 @@ const statuses = { queueFlushAll ({ rootState, commit }) { commit('queueFlushAll') }, - markNotificationsAsSeen ({ rootState, commit }) { - commit('markNotificationsAsSeen') - apiService.markNotificationsAsSeen({ - id: rootState.statuses.notifications.maxId, - credentials: rootState.users.currentUser.credentials - }).then(() => { - closeAllDesktopNotifications(rootState) - }) - }, - markSingleNotificationAsSeen ({ rootState, commit }, { id }) { - commit('markSingleNotificationAsSeen', { id }) - apiService.markNotificationsAsSeen({ - single: true, - id, - credentials: rootState.users.currentUser.credentials - }).then(() => { - closeDesktopNotification(rootState, id) - }) - }, - dismissNotificationLocal ({ rootState, commit }, { id }) { - commit('dismissNotification', { id }) - }, - dismissNotification ({ rootState, commit }, { id }) { - commit('dismissNotification', { id }) - rootState.api.backendInteractor.dismissNotification({ id }) - }, - updateNotification ({ rootState, commit }, { id, updater }) { - commit('updateNotification', { id, updater }) - }, fetchFavsAndRepeats ({ rootState, commit }, id) { Promise.all([ rootState.api.backendInteractor.fetchFavoritedByUsers({ id }), diff --git a/src/modules/users.js b/src/modules/users.js index 79268bc3..6669d623 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -498,7 +498,7 @@ const users = { store.commit('addNewUsers', users) store.commit('addNewUsers', targetUsers) - const notificationsObject = store.rootState.statuses.notifications.idStore + const notificationsObject = store.rootState.notifications.idStore const relevantNotifications = Object.entries(notificationsObject) .filter(([k, val]) => notificationIds.includes(k)) .map(([k, val]) => val) diff --git a/src/services/desktop_notification_utils/desktop_notification_utils.js b/src/services/desktop_notification_utils/desktop_notification_utils.js index dbca4173..bde9f18c 100644 --- a/src/services/desktop_notification_utils/desktop_notification_utils.js +++ b/src/services/desktop_notification_utils/desktop_notification_utils.js @@ -7,7 +7,7 @@ const state = { failCreateNotif: false } export const showDesktopNotification = (rootState, desktopNotificationOpts) => { if (!('Notification' in window && window.Notification.permission === 'granted')) return - if (rootState.statuses.notifications.desktopNotificationSilence) { return } + if (rootState.notifications.desktopNotificationSilence) { return } if (isSWSupported()) { swDesktopNotification(desktopNotificationOpts) diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 610ba1ab..85da5223 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -439,7 +439,6 @@ export const parseNotification = (data) => { output.type = mastoDict[data.type] || data.type output.seen = data.pleroma.is_seen output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null - output.action = output.status // TODO: Refactor, this is unneeded output.target = output.type !== 'move' ? null : parseUser(data.target) diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index 01cbd5f1..2c25000e 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -6,7 +6,7 @@ import FaviconService from 'src/services/favicon_service/favicon_service.js' let cachedBadgeUrl = null -export const notificationsFromStore = store => store.state.statuses.notifications.data +export const notificationsFromStore = store => store.state.notifications.data export const visibleTypes = store => { const rootState = store.rootState || store.state diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index 6c247210..3c280b74 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -21,7 +21,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => { const args = { credentials } const { getters } = store const rootState = store.rootState || store.state - const timelineData = rootState.statuses.notifications + const timelineData = rootState.notifications const hideMutedPosts = getters.mergedConfig.hideMutedPosts args.includeTypes = mastoApiNotificationTypes -- cgit v1.2.3-70-g09d2 From 53a4b1f9a6a9aa6bc044609c3accb074d924daf9 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 31 Jan 2024 17:39:51 +0200 Subject: better virtual components and stuff --- src/App.scss | 28 +- src/components/icon.style.js | 13 +- src/components/link.style.js | 25 ++ src/components/notification/notification.vue | 2 +- src/components/notifications/notifications.scss | 16 +- src/components/panel.style.js | 1 + src/components/panel_header.style.js | 1 + src/components/status/status.vue | 2 +- src/components/status_body/status_body.scss | 5 +- src/components/text.style.js | 38 ++- src/components/underlay.style.js | 3 +- src/panel.scss | 11 - src/services/color_convert/color_convert.js | 3 +- src/services/style_setter/style_setter.js | 19 +- src/services/theme_data/pleromafe.t3.js | 16 +- src/services/theme_data/theme_data_3.service.js | 304 ++++++++++++++++----- .../specs/services/theme_data/theme_data3.spec.js | 2 +- 17 files changed, 353 insertions(+), 136 deletions(-) create mode 100644 src/components/link.style.js (limited to 'src/components/notification/notification.vue') diff --git a/src/App.scss b/src/App.scss index ef68ac50..8e9f3171 100644 --- a/src/App.scss +++ b/src/App.scss @@ -24,8 +24,7 @@ body { font-family: sans-serif; font-family: var(--interfaceFont, sans-serif); margin: 0; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; overscroll-behavior-y: none; @@ -111,8 +110,7 @@ body { a { text-decoration: none; - color: $fallback--link; - color: var(--link, $fallback--link); + color: var(--link); } h4 { @@ -128,8 +126,7 @@ h4 { i[class*="icon-"], .svg-inline--fa, .iconLetter { - color: $fallback--icon; - color: var(--icon, $fallback--icon); + color: var(--icon); } .button-unstyled:hover, @@ -763,17 +760,11 @@ option { } .faint { - color: $fallback--faint; - color: var(--faint, $fallback--faint); -} - -.faint-link { - color: $fallback--faint; - color: var(--faint, $fallback--faint); + --text: var(--textFaint); + --textGreentext: var(--textGreentextFaint); + --link: var(--linkFaint); - &:hover { - text-decoration: underline; - } + color: var(--text); } .visibility-notice { @@ -816,6 +807,11 @@ option { opacity: 0.25; } +.timeago { + --link: var(--text); + --linkFaint: var(--textFaint); +} + .login-hint { text-align: center; diff --git a/src/components/icon.style.js b/src/components/icon.style.js index 732cf16f..adc72fc5 100644 --- a/src/components/icon.style.js +++ b/src/components/icon.style.js @@ -1,4 +1,15 @@ export default { name: 'Icon', - selector: '.icon' + virtual: true, + selector: '.svg-inline--fa', + defaultRules: [ + { + component: 'Icon', + directives: { + textColor: '--text', + textOpacity: 0.5, + textOpacityMode: 'mixrgb' + } + } + ] } diff --git a/src/components/link.style.js b/src/components/link.style.js new file mode 100644 index 00000000..0128fd91 --- /dev/null +++ b/src/components/link.style.js @@ -0,0 +1,25 @@ +export default { + name: 'Link', + selector: 'a', + virtual: true, + states: { + faint: '.faint' + }, + defaultRules: [ + { + component: 'Link', + directives: { + textColor: '--link' + } + }, + { + component: 'Link', + state: ['faint'], + directives: { + textColor: '--link', + textOpacity: 0.5, + textOpacityMode: 'fake' + } + } + ] +} diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index a8eceab0..5c425200 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -155,7 +155,7 @@ .button-default { flex-shrink: 0; diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js index 23e4ca61..d92bbbe0 100644 --- a/src/services/color_convert/color_convert.js +++ b/src/services/color_convert/color_convert.js @@ -173,7 +173,7 @@ export const mixrgb = (a, b) => { * @returns {String} CSS rgba() color */ export const rgba2css = function (rgba) { - return `rgba(${Math.floor(rgba.r)}, ${Math.floor(rgba.g)}, ${Math.floor(rgba.b)}, ${rgba.a})` + return `rgba(${Math.floor(rgba.r)}, ${Math.floor(rgba.g)}, ${Math.floor(rgba.b)}, ${rgba.a ?? 1})` } /** @@ -188,7 +188,6 @@ export const rgba2css = function (rgba) { */ export const getTextColor = function (bg, text, preserve) { const contrast = getContrastRatio(bg, text) - console.log(contrast) if (contrast < 4.5) { const base = typeof text.a !== 'undefined' ? { a: text.a } : {} diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 43fe3c73..ba98c4d9 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -1,10 +1,15 @@ import { convert } from 'chromatism' import { rgb2hex, hex2rgb, rgba2css, getCssColor, relativeLuminance } from '../color_convert/color_convert.js' import { getColors, computeDynamicColor, getOpacitySlot } from '../theme_data/theme_data.service.js' +import { init } from '../theme_data/theme_data_3.service.js' +import { + sampleRules +} from 'src/services/theme_data/pleromafe.t3.js' import { defaultState } from '../../modules/config.js' export const applyTheme = (input) => { - const { rules } = generatePreset(input) + const { rules, t3b } = generatePreset(input) + const themes3 = init(sampleRules, t3b) const head = document.head const body = document.body body.classList.add('hidden') @@ -18,6 +23,10 @@ export const applyTheme = (input) => { styleSheet.insertRule(`:root { ${rules.colors} }`, 'index-max') styleSheet.insertRule(`:root { ${rules.shadows} }`, 'index-max') styleSheet.insertRule(`:root { ${rules.fonts} }`, 'index-max') + themes3.css.forEach(rule => { + console.log(rule) + styleSheet.insertRule(rule, 'index-max') + }) body.classList.remove('hidden') } @@ -326,7 +335,7 @@ export const generateShadows = (input, colors) => { } } -export const composePreset = (colors, radii, shadows, fonts) => { +export const composePreset = (colors, radii, shadows, fonts, t3b) => { return { rules: { ...shadows.rules, @@ -339,7 +348,8 @@ export const composePreset = (colors, radii, shadows, fonts) => { ...colors.theme, ...radii.theme, ...fonts.theme - } + }, + t3b } } @@ -349,7 +359,8 @@ export const generatePreset = (input) => { colors, generateRadii(input), generateShadows(input, colors.theme.colors, colors.mod), - generateFonts(input) + generateFonts(input), + colors.theme.colors ) } diff --git a/src/services/theme_data/pleromafe.t3.js b/src/services/theme_data/pleromafe.t3.js index 8d8e19cd..9fadf0ee 100644 --- a/src/services/theme_data/pleromafe.t3.js +++ b/src/services/theme_data/pleromafe.t3.js @@ -11,30 +11,30 @@ export const sampleRules = [ { component: 'Panel', directives: { - background: '#FFFFFF', - opacity: 0.9 + background: '--fg' + // opacity: 0.9 } }, { component: 'PanelHeader', directives: { - background: '#000000', - opacity: 0.9 + background: '--fg' + // opacity: 0.9 } }, { component: 'Button', directives: { - background: '#000000', - opacity: 0.8 + background: '--fg' + // opacity: 0.8 } }, { component: 'Button', state: ['hover'], directives: { - background: '#FF00FF', - opacity: 0.9 + background: '#FFFFFF' + // opacity: 0.9 } } ] diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index 2ef5e17f..c9468d07 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -1,11 +1,12 @@ import { convert } from 'chromatism' -import { alphaBlend, getTextColor, rgba2css } from '../color_convert/color_convert.js' +import { alphaBlend, getTextColor, rgba2css, mixrgb } from '../color_convert/color_convert.js' import Underlay from 'src/components/underlay.style.js' import Panel from 'src/components/panel.style.js' import PanelHeader from 'src/components/panel_header.style.js' import Button from 'src/components/button.style.js' import Text from 'src/components/text.style.js' +import Link from 'src/components/link.style.js' import Icon from 'src/components/icon.style.js' const root = Underlay @@ -15,6 +16,7 @@ const components = { PanelHeader, Button, Text, + Link, Icon } @@ -35,9 +37,9 @@ export const getAllPossibleCombinations = (array) => { return combos.reduce((acc, x) => [...acc, ...x], []) } -export const ruleToSelector = (rule) => { +export const ruleToSelector = (rule, isParent) => { const component = components[rule.component] - const { states, variants, selector } = component + const { states, variants, selector, outOfTreeSelector } = component const applicableStates = ((rule.state || []).filter(x => x !== 'normal')).map(state => states[state]) @@ -47,56 +49,104 @@ export const ruleToSelector = (rule) => { applicableVariant = variants[applicableVariantName] } - const selectors = [selector, applicableVariant, ...applicableStates] + let realSelector + if (isParent) { + realSelector = selector + } else { + if (outOfTreeSelector) realSelector = outOfTreeSelector + else realSelector = selector + } + + const selectors = [realSelector, applicableVariant, ...applicableStates] .toSorted((a, b) => { if (a.startsWith(':')) return 1 - else return -1 + if (!a.startsWith('.')) return -1 + else return 0 }) .join('') if (rule.parent) { - return ruleToSelector(rule.parent) + ' ' + selectors + return ruleToSelector(rule.parent, true) + ' ' + selectors } return selectors } -export const init = (ruleset) => { +export const init = (extraRuleset, palette) => { const rootName = root.name const rules = [] const rulesByComponent = {} + const ruleset = [ + ...Object.values(components).map(c => c.defaultRules || []).reduce((acc, arr) => [...acc, ...arr], []), + ...extraRuleset + ] + const addRule = (rule) => { rules.push(rule) rulesByComponent[rule.component] = rulesByComponent[rule.component] || [] rulesByComponent[rule.component].push(rule) } - const findRules = (combination) => rule => { - if (combination.component !== rule.component) return false - if (Object.prototype.hasOwnProperty.call(rule, 'variant')) { - if (combination.variant !== rule.variant) return false - } else { - if (combination.variant !== 'normal') return false + const findRules = (combination, parent) => rule => { + // inexact search + const doesCombinationMatch = () => { + if (combination.component !== rule.component) return false + if (Object.prototype.hasOwnProperty.call(rule, 'variant')) { + if (combination.variant !== rule.variant) return false + } else { + if (combination.variant !== 'normal') return false + } + + if (Object.prototype.hasOwnProperty.call(rule, 'state')) { + const ruleStatesSet = new Set(['normal', ...(rule.state || [])]) + const combinationSet = new Set(['normal', ...combination.state]) + const setsAreEqual = combination.state.every(state => ruleStatesSet.has(state)) && + [...ruleStatesSet].every(state => combinationSet.has(state)) + return setsAreEqual + } else { + if (combination.state.length !== 1 || combination.state[0] !== 'normal') return false + return true + } } + const combinationMatches = doesCombinationMatch() + if (!parent || !combinationMatches) return combinationMatches - if (Object.prototype.hasOwnProperty.call(rule, 'state')) { - const ruleStatesSet = new Set(['normal', ...(rule.state || [])]) - const combinationSet = new Set(['normal', ...combination.state]) - const setsAreEqual = combination.state.every(state => ruleStatesSet.has(state)) && - [...ruleStatesSet].every(state => combinationSet.has(state)) - return setsAreEqual - } else { - if (combination.state.length !== 1 || combination.state[0] !== 'normal') return false - return true + // exact search + + // unroll parents into array + const unroll = (item) => { + const out = [] + let currentParent = item.parent + while (currentParent) { + const { parent: newParent, ...rest } = currentParent + out.push(rest) + currentParent = newParent + } + return out } + const { parent: _, ...rest } = parent + const pathSearch = [rest, ...unroll(parent)] + const pathRule = unroll(rule) + if (pathSearch.length !== pathRule.length) return false + const pathsMatch = pathSearch.every((searchRule, i) => { + const existingRule = pathRule[i] + if (existingRule.component !== searchRule.component) return false + if (existingRule.variant !== searchRule.variant) return false + const existingRuleStatesSet = new Set(['normal', ...(existingRule.state || [])]) + const searchStatesSet = new Set(['normal', ...(searchRule.state || [])]) + const setsAreEqual = existingRule.state.every(state => searchStatesSet.has(state)) && + [...searchStatesSet].every(state => existingRuleStatesSet.has(state)) + return setsAreEqual + }) + return pathsMatch } const findLowerLevelRule = (parent, filter = () => true) => { let lowerLevelComponent = null let currentParent = parent while (currentParent) { - const rulesParent = ruleset.filter(findRules(currentParent, true)) - rulesParent > 1 && console.log('OOPS') + const rulesParent = ruleset.filter(findRules(currentParent)) + rulesParent > 1 && console.warn('OOPS') lowerLevelComponent = rulesParent[rulesParent.length - 1] currentParent = currentParent.parent if (lowerLevelComponent && filter(lowerLevelComponent)) currentParent = null @@ -104,6 +154,35 @@ export const init = (ruleset) => { return filter(lowerLevelComponent) ? lowerLevelComponent : null } + const findColor = (color) => { + if (typeof color === 'string' && color.startsWith('--')) { + const name = color.substring(2) + return palette[name] + } + return color + } + + const getTextColorAlpha = (rule, lowerRule, value) => { + const opacity = rule.directives.textOpacity + const textColor = convert(findColor(value)).rgb + if (opacity === null || opacity === undefined || opacity >= 1) { + return convert(textColor).hex + } + const backgroundColor = convert(lowerRule.cache.background).rgb + if (opacity === 0) { + return convert(backgroundColor).hex + } + const opacityMode = rule.directives.textOpacityMode + switch (opacityMode) { + case 'fake': + return convert(alphaBlend(textColor, opacity, backgroundColor)).hex + case 'mixrgb': + return convert(mixrgb(backgroundColor, textColor)).hex + default: + return rgba2css({ a: opacity, ...textColor }) + } + } + const processInnerComponent = (component, parent) => { const { validInnerComponents = [], @@ -124,79 +203,156 @@ export const init = (ruleset) => { const VIRTUAL_COMPONENTS = new Set(['Text', 'Link', 'Icon']) stateVariantCombination.forEach(combination => { - const existingRules = ruleset.filter(findRules({ component: component.name, ...combination })) - const lastRule = existingRules[existingRules.length - 1] + let needRuleAdd = false - if (existingRules.length !== 0) { - const { directives } = lastRule - const rgb = convert(directives.background).rgb + if (VIRTUAL_COMPONENTS.has(component.name)) { + const selector = component.name + ruleToSelector({ component: component.name, ...combination }) + const virtualName = [ + '--', + component.name.toLowerCase(), + combination.variant === 'normal' + ? '' + : combination.variant[0].toUpperCase() + combination.variant.slice(1).toLowerCase(), + ...combination.state.filter(x => x !== 'normal').toSorted().map(state => state[0].toUpperCase() + state.slice(1).toLowerCase()) + ].join('') - // TODO: DEFAULT TEXT COLOR - const bg = findLowerLevelRule(parent)?.cache.background || convert('#FFFFFF').rgb + const lowerLevel = findLowerLevelRule(parent, (r) => { + if (components[r.component].validInnerComponents.indexOf(component.name) < 0) return false + if (r.cache.background === undefined) return false + if (r.cache.textDefined) { + return !r.cache.textDefined[selector] + } + return true + }) - if (!lastRule.cache?.background) { - const blend = directives.opacity < 1 ? alphaBlend(rgb, directives.opacity, bg) : rgb - lastRule.cache = lastRule.cache || {} - lastRule.cache.background = blend + if (!lowerLevel) return + + let inheritedTextColorRule + const inheritedTextColorRules = findLowerLevelRule(parent, (r) => { + return r.cache?.textDefined?.[selector] + }) + + if (!inheritedTextColorRule) { + const generalTextColorRules = ruleset.filter(findRules({ component: component.name, ...combination }, null, true)) + inheritedTextColorRule = generalTextColorRules[generalTextColorRules.length - 1] + } else { + inheritedTextColorRule = inheritedTextColorRules[inheritedTextColorRules.length - 1] + } - addRule(lastRule) + let inheritedTextColor + let inheritedTextOpacity = {} + if (inheritedTextColorRule) { + inheritedTextColor = findColor(inheritedTextColorRule.directives.textColor) + // also inherit opacity settings + const { textOpacity, textOpacityMode } = inheritedTextColorRule.directives + inheritedTextOpacity = { textOpacity, textOpacityMode } + } else { + // Emergency fallback + inheritedTextColor = '#000000' } + + const textColor = getTextColor( + convert(lowerLevel.cache.background).rgb, + convert(inheritedTextColor).rgb, + component.name === 'Link' // make it configurable? + ) + + lowerLevel.cache.textDefined = lowerLevel.cache.textDefined || {} + lowerLevel.cache.textDefined[selector] = textColor + lowerLevel.virtualDirectives = lowerLevel.virtualDirectives || {} + lowerLevel.virtualDirectives[virtualName] = getTextColorAlpha(inheritedTextColorRule, lowerLevel, textColor) + + const directives = { + textColor, + ...inheritedTextOpacity + } + + // Debug: lets you see what it think background color should be + directives.background = convert(lowerLevel.cache.background).hex + + addRule({ + parent, + virtual: true, + component: component.name, + ...combination, + cache: { background: lowerLevel.cache.background }, + directives + }) } else { - if (VIRTUAL_COMPONENTS.has(component.name)) { - const selector = component.name + ruleToSelector({ component: component.name, ...combination }) - - const lowerLevel = findLowerLevelRule(parent, (r) => { - if (components[r.component].validInnerComponents.indexOf(component.name) < 0) return false - if (r.cache?.background === undefined) return false - if (r.cache.textDefined) { - return !r.cache.textDefined[selector] - } - return true - }) - if (!lowerLevel) return - lowerLevel.cache.textDefined = lowerLevel.cache.textDefined || {} - lowerLevel.cache.textDefined[selector] = true - addRule({ - parent, - component: component.name, - ...combination, - directives: { - // TODO: DEFAULT TEXT COLOR - textColor: getTextColor(convert(lowerLevel.cache.background).rgb, convert('#FFFFFF').rgb, component.name === 'Link'), - // Debug: lets you see what it think background color should be - background: convert(lowerLevel.cache.background).hex + const existingGlobalRules = ruleset.filter(findRules({ component: component.name, ...combination }, null)) + const existingRules = ruleset.filter(findRules({ component: component.name, ...combination }, parent)) + + // Global (general) rules + if (existingGlobalRules.length !== 0) { + const lastRule = existingGlobalRules[existingGlobalRules.length - 1] + const { directives } = lastRule + lastRule.cache = lastRule.cache || {} + + if (directives.background) { + const rgb = convert(findColor(directives.background)).rgb + + // TODO: DEFAULT TEXT COLOR + const bg = findLowerLevelRule(parent)?.cache.background || convert('#FFFFFF').rgb + + if (!lastRule.cache.background) { + const blend = directives.opacity < 1 ? alphaBlend(rgb, directives.opacity, bg) : rgb + lastRule.cache.background = blend + + needRuleAdd = true } - }) + } + + if (needRuleAdd) { + addRule(lastRule) + } } - } + if (existingRules.length !== 0) { + console.warn('MORE EXISTING RULES', existingRules) + } + } innerComponents.forEach(innerComponent => processInnerComponent(innerComponent, { parent, component: name, ...combination })) }) } processInnerComponent(components[rootName]) - // console.info(rules.map(x => [ - // (parent?.component || 'root') + ' -> ' + x.component, - // // 'Cached background:' + convert(bg).hex, - // // 'Color: ' + convert(x.directives.background).hex + ' A:' + x.directives.opacity, - // JSON.stringify(x.directives) - // // '=> Blend: ' + convert(x.cache.background).hex - // ].join(' '))) - return { raw: rules, css: rules.map(rule => { - const header = ruleToSelector(rule) + ' {' + if (rule.virtual) return '' + + let selector = ruleToSelector(rule).replace(/\/\*.*\*\//g, '') + if (!selector) { + selector = 'body' + } + 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 'background': return 'background-color: ' + rgba2css({ ...convert(v).rgb, a: rule.directives.opacity ?? 1 }) - case 'textColor': return 'color: ' + rgba2css({ ...convert(v).rgb, a: rule.directives.opacity ?? 1 }) + case 'background': { + return 'background-color: ' + rgba2css({ ...convert(findColor(v)).rgb, a: rule.directives.opacity ?? 1 }) + } + case 'textColor': { + return 'color: ' + v + } default: return '' } }).filter(x => x).map(x => ' ' + x).join(';\n') - return [header, directives, footer].join('\n') - }) + + return [ + header, + directives + ';', + ' color: var(--text);', + '', + virtualDirectives, + footer + ].join('\n') + }).filter(x => x) } } diff --git a/test/unit/specs/services/theme_data/theme_data3.spec.js b/test/unit/specs/services/theme_data/theme_data3.spec.js index 915bb5ce..0d5870cc 100644 --- a/test/unit/specs/services/theme_data/theme_data3.spec.js +++ b/test/unit/specs/services/theme_data/theme_data3.spec.js @@ -17,7 +17,7 @@ describe.only('Theme Data 3', () => { describe('init', () => { it('test simple case', () => { - const out = init(sampleRules) + const out = init(sampleRules, palette) // console.log(JSON.stringify(out, null, 2)) console.log('\n' + out.css.join('\n') + '\n') }) -- cgit v1.2.3-70-g09d2 From d8827932bc8f329c66bdf23fa0eb7af4ed41179a Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 27 Feb 2024 01:08:04 +0200 Subject: fix collapsed notifications incorrect styles --- src/components/notification/notification.vue | 1 - src/components/notifications/notifications.scss | 14 -------------- src/components/rich_content/rich_content.jsx | 8 +++++++- src/components/rich_content/rich_content.scss | 9 +++++++++ src/components/status_body/status_body.vue | 2 ++ src/components/status_content/status_content.vue | 10 ---------- 6 files changed, 18 insertions(+), 26 deletions(-) (limited to 'src/components/notification/notification.vue') diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 5c425200..632ae6e9 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -247,7 +247,6 @@ />