diff options
24 files changed, 157 insertions, 28 deletions
diff --git a/changelog.d/quotes-count.add b/changelog.d/quotes-count.add new file mode 100644 index 00000000..86779b96 --- /dev/null +++ b/changelog.d/quotes-count.add @@ -0,0 +1 @@ +Display quotes count on posts and add quotes list page
\ No newline at end of file diff --git a/changelog.d/themes3-cache.add b/changelog.d/themes3-cache.add new file mode 100644 index 00000000..a275e44c --- /dev/null +++ b/changelog.d/themes3-cache.add @@ -0,0 +1 @@ +Add caching system for themes3 diff --git a/package.json b/package.json index cbbcb170..fb157fae 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "click-outside-vue3": "4.0.1", "cropperjs": "1.5.13", "escape-html": "1.0.3", + "hash-sum": "^2.0.0", "js-cookie": "3.0.5", "localforage": "1.10.0", "parse-link-header": "2.0.0", diff --git a/src/boot/after_store.js b/src/boot/after_store.js index bfb671ed..bcab7a66 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -14,7 +14,7 @@ import { windowWidth, windowHeight } from '../services/window_utils/window_utils import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' -import { applyTheme, applyConfig } from '../services/style_setter/style_setter.js' +import { applyTheme, applyConfig, tryLoadCache } from '../services/style_setter/style_setter.js' import FaviconService from '../services/favicon_service/favicon_service.js' import { initServiceWorker, updateFocus } from '../services/sw/sw.js' @@ -353,21 +353,25 @@ const afterStoreSetup = async ({ store, i18n }) => { await setConfig({ store }) - const { customTheme, customThemeSource } = store.state.config + const { customTheme, customThemeSource, forceThemeRecompilation } = store.state.config const { theme } = store.state.instance const customThemePresent = customThemeSource || customTheme - if (customThemePresent) { - if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) { - applyTheme(customThemeSource) - } else { - applyTheme(customTheme) - } + if (!forceThemeRecompilation && tryLoadCache()) { store.commit('setThemeApplied') - } else if (theme) { - // do nothing, it will load asynchronously } else { - console.error('Failed to load any theme!') + if (customThemePresent) { + if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) { + applyTheme(customThemeSource) + } else { + applyTheme(customTheme) + } + store.commit('setThemeApplied') + } else if (theme) { + // do nothing, it will load asynchronously + } else { + console.error('Failed to load any theme!') + } } applyConfig(store.state.config) diff --git a/src/boot/routes.js b/src/boot/routes.js index 2dc900e7..31e3dbb0 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -25,6 +25,7 @@ import ListsTimeline from 'components/lists_timeline/lists_timeline.vue' import ListsEdit from 'components/lists_edit/lists_edit.vue' import NavPanel from 'src/components/nav_panel/nav_panel.vue' import AnnouncementsPage from 'components/announcements_page/announcements_page.vue' +import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue' export default (store) => { const validateAuthenticatedRoute = (to, from, next) => { @@ -51,6 +52,7 @@ export default (store) => { { name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline }, { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline }, { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, + { name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline }, { name: 'remote-user-profile-acct', path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)', diff --git a/src/components/quotes_timeline/quotes_timeline.js b/src/components/quotes_timeline/quotes_timeline.js new file mode 100644 index 00000000..a5f42da5 --- /dev/null +++ b/src/components/quotes_timeline/quotes_timeline.js @@ -0,0 +1,26 @@ +import Timeline from '../timeline/timeline.vue' + +const QuotesTimeline = { + created () { + this.$store.commit('clearTimeline', { timeline: 'quotes' }) + this.$store.dispatch('startFetchingTimeline', { timeline: 'quotes', statusId: this.statusId }) + }, + components: { + Timeline + }, + computed: { + statusId () { return this.$route.params.id }, + timeline () { return this.$store.state.statuses.timelines.quotes } + }, + watch: { + statusId () { + this.$store.commit('clearTimeline', { timeline: 'quotes' }) + this.$store.dispatch('startFetchingTimeline', { timeline: 'quotes', statusId: this.statusId }) + } + }, + unmounted () { + this.$store.dispatch('stopFetchingTimeline', 'quotes') + } +} + +export default QuotesTimeline diff --git a/src/components/quotes_timeline/quotes_timeline.vue b/src/components/quotes_timeline/quotes_timeline.vue new file mode 100644 index 00000000..835abd12 --- /dev/null +++ b/src/components/quotes_timeline/quotes_timeline.vue @@ -0,0 +1,10 @@ +<template> + <Timeline + :title="$t('nav.quotes')" + :timeline="timeline" + :timeline-name="'quotes'" + :status-id="statusId" + /> +</template> + +<script src='./quotes_timeline.js'></script> diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 05930e00..208c49ee 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -201,6 +201,14 @@ <h2>{{ $t('settings.post_look_feel') }}</h2> <ul class="setting-list"> <li> + <BooleanSetting + path="forceThemeRecompilation" + :expert="1" + > + {{ $t('settings.force_theme_recompilation_debug') }} + </BooleanSetting> + </li> + <li> <ChoiceSetting id="conversationDisplay" path="conversationDisplay" diff --git a/src/components/status/status.js b/src/components/status/status.js index e4fb1f51..bf4e4275 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -373,6 +373,9 @@ const Status = { hidePostStats () { return this.mergedConfig.hidePostStats }, + shouldDisplayFavsAndRepeats () { + return !this.hidePostStats && this.isFocused && (this.combinedFavsAndRepeatsUsers.length > 0 || this.statusFromGlobalRepository.quotes_count) + }, muteBotStatuses () { return this.mergedConfig.muteBotStatuses }, diff --git a/src/components/status/status.scss b/src/components/status/status.scss index dfdc1189..63809ff2 100644 --- a/src/components/status/status.scss +++ b/src/components/status/status.scss @@ -374,6 +374,7 @@ font-weight: bolder; font-size: 1.1em; line-height: 1em; + color: var(--text); } &:hover .stat-title { diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 2a17bb13..61a58cda 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -484,7 +484,7 @@ <transition name="fade"> <div - v-if="!hidePostStats && isFocused && combinedFavsAndRepeatsUsers.length > 0" + v-if="shouldDisplayFavsAndRepeats" class="favs-repeated-users" > <div class="stats"> @@ -512,6 +512,19 @@ </div> </div> </UserListPopover> + <router-link + v-if="statusFromGlobalRepository.quotes_count > 0" + :to="{ name: 'quotes', params: { id: status.id } }" + > + <div + class="stat-count" + > + <a class="stat-title">{{ $t('status.quotes') }}</a> + <div class="stat-number"> + {{ statusFromGlobalRepository.quotes_count }} + </div> + </div> + </router-link> <div class="avatar-row"> <AvatarList :users="combinedFavsAndRepeatsUsers" /> </div> diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 47e4a45e..59170f49 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -25,6 +25,7 @@ const Timeline = { 'title', 'userId', 'listId', + 'statusId', 'tag', 'embedded', 'count', @@ -121,6 +122,7 @@ const Timeline = { showImmediately, userId: this.userId, listId: this.listId, + statusId: this.statusId, tag: this.tag }) }, @@ -183,6 +185,7 @@ const Timeline = { showImmediately: true, userId: this.userId, listId: this.listId, + statusId: this.statusId, tag: this.tag }).then(({ statuses }) => { if (statuses && statuses.length === 0) { diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js index 5a2a86c2..c4586b32 100644 --- a/src/components/timeline_menu/timeline_menu.js +++ b/src/components/timeline_menu/timeline_menu.js @@ -19,7 +19,8 @@ export const timelineNames = () => { bookmarks: 'nav.bookmarks', dms: 'nav.dms', 'public-timeline': 'nav.public_tl', - 'public-external-timeline': 'nav.twkn' + 'public-external-timeline': 'nav.twkn', + quotes: 'nav.quotes' } } diff --git a/src/i18n/cs.json b/src/i18n/cs.json index 9c04ede7..c3ba7ca4 100644 --- a/src/i18n/cs.json +++ b/src/i18n/cs.json @@ -678,7 +678,7 @@ "autohide_floating_post_button": "Automaticky skrýt tlačítko nového příspěvku (mobilní zařízení)", "minimal_scopes_mode": "Minimalizovat možnosti rozsahu příspěvků", "conversation_display": "Styl zobrazení konverzací", - "conversation_display_tree": "Stromový styl", + "conversation_display_tree": "Stromové zobrazení", "conversation_display_tree_quick": "Stromový styl", "show_scrollbars": "Zobrazit posuvníky bočních sloupců", "third_column_mode": "Pokud je volné místo, zobrazit třetí sloupec obsahující", @@ -863,7 +863,7 @@ "favorites": "Oblíbené", "follow": "Sledovat", "follow_sent": "Požadavek odeslán!", - "follow_progress": "Odeslílám požadavek…", + "follow_progress": "Odesílám požadavek…", "follow_unfollow": "Přestat sledovat", "followees": "Sledovaní", "followers": "Sledující", diff --git a/src/i18n/en.json b/src/i18n/en.json index 7a03b49d..f626e933 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -189,7 +189,8 @@ "mobile_notifications": "Open notifications (there are unread ones)", "mobile_notifications_close": "Close notifications", "mobile_notifications_mark_as_seen": "Mark all as seen", - "announcements": "Announcements" + "announcements": "Announcements", + "quotes": "Quotes" }, "notifications": { "broken_favorite": "Unknown status, searching for it…", @@ -644,6 +645,7 @@ "subject_line_email": "Like email: \"re: subject\"", "subject_line_mastodon": "Like mastodon: copy as is", "subject_line_noop": "Do not copy", + "force_theme_recompilation_debug": "Disable theme cahe, force recompile on each boot (DEBUG)", "conversation_display": "Conversation display style", "conversation_display_tree": "Tree-style", "conversation_display_tree_quick": "Tree view", @@ -1056,6 +1058,7 @@ "status": { "favorites": "Favorites", "repeats": "Repeats", + "quotes": "Quotes", "repeat_confirm": "Do you really want to repeat this status?", "repeat_confirm_title": "Repeat confirmation", "repeat_confirm_accept_button": "Repeat", diff --git a/src/modules/api.js b/src/modules/api.js index fee584e8..3dbead5b 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -202,12 +202,13 @@ const api = { timeline = 'friends', tag = false, userId = false, - listId = false + listId = false, + statusId = false }) { if (store.state.fetchers[timeline]) return const fetcher = store.state.backendInteractor.startFetchingTimeline({ - timeline, store, userId, listId, tag + timeline, store, userId, listId, statusId, tag }) store.commit('addFetcher', { fetcherName: timeline, fetcher }) }, diff --git a/src/modules/config.js b/src/modules/config.js index a59a80d8..8001b854 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -28,6 +28,7 @@ export const defaultState = { theme: undefined, customTheme: undefined, customThemeSource: undefined, + forceThemeRecompilation: false, hideISP: false, hideInstanceWallpaper: false, hideShoutbox: false, diff --git a/src/modules/instance.js b/src/modules/instance.js index f12cc735..0a5c1ae7 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -378,7 +378,8 @@ const instance = { commit('setInstanceOption', { name: 'themeData', value: themeData }) // No need to apply theme if there's user theme already const { customTheme } = rootState.config - if (customTheme) return + const { themeApplied } = rootState.interface + if (customTheme || themeApplied) return // New theme presets don't have 'theme' property, they use 'source' const themeSource = themeData.source diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index a47c638c..fa417193 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -108,6 +108,7 @@ const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements' const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}` const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}` const PLEROMA_SCROBBLES_URL = id => `/api/v1/pleroma/accounts/${id}/scrobbles` +const PLEROMA_STATUS_QUOTES_URL = id => `/api/v1/pleroma/statuses/${id}/quotes` const PLEROMA_USER_FAVORITES_TIMELINE_URL = id => `/api/v1/pleroma/accounts/${id}/favourites` const PLEROMA_ADMIN_CONFIG_URL = '/api/pleroma/admin/config' @@ -685,6 +686,7 @@ const fetchTimeline = ({ until = false, userId = false, listId = false, + statusId = false, tag = false, withMuted = false, replyVisibility = 'all', @@ -702,7 +704,8 @@ const fetchTimeline = ({ favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, publicFavorites: PLEROMA_USER_FAVORITES_TIMELINE_URL, tag: MASTODON_TAG_TIMELINE_URL, - bookmarks: MASTODON_BOOKMARK_TIMELINE_URL + bookmarks: MASTODON_BOOKMARK_TIMELINE_URL, + quotes: PLEROMA_STATUS_QUOTES_URL } const isNotifications = timeline === 'notifications' const params = [] @@ -721,6 +724,10 @@ const fetchTimeline = ({ url = url(listId) } + if (timeline === 'quotes') { + url = url(statusId) + } + if (minId) { params.push(['min_id', minId]) } diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 62ee8549..8ceb897d 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -5,8 +5,8 @@ import followRequestFetcher from '../../services/follow_request_fetcher/follow_r import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js' const backendInteractorService = credentials => ({ - startFetchingTimeline ({ timeline, store, userId = false, listId = false, tag }) { - return timelineFetcher.startFetching({ timeline, store, credentials, userId, listId, tag }) + startFetchingTimeline ({ timeline, store, userId = false, listId = false, statusId = false, tag }) { + return timelineFetcher.startFetching({ timeline, store, credentials, userId, listId, statusId, tag }) }, fetchTimeline (args) { diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 21e67b67..e41e7125 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -331,6 +331,7 @@ export const parseStatus = (data) => { output.quote_id = pleroma.quote_id ? pleroma.quote_id : (output.quote ? output.quote.id : undefined) output.quote_url = pleroma.quote_url output.quote_visible = pleroma.quote_visible + output.quotes_count = pleroma.quotes_count } else { output.text = data.content output.summary = data.spoiler_text diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 95198ec7..369d2c9f 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -1,6 +1,6 @@ import { hex2rgb } from '../color_convert/color_convert.js' import { generatePreset } from '../theme_data/theme_data.service.js' -import { init } from '../theme_data/theme_data_3.service.js' +import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js' import { convertTheme2To3 } from '../theme_data/theme2_to_theme3.js' import { getCssRules } from '../theme_data/css_utils.js' import { defaultState } from '../../modules/config.js' @@ -87,9 +87,37 @@ export const generateTheme = async (input, callbacks) => { return { lazyProcessFunc: processChunk } } -export const applyTheme = async (input) => { +export const tryLoadCache = () => { + const json = localStorage.getItem('pleroma-fe-theme-cache') + if (!json) return null + let cache + try { + cache = JSON.parse(json) + } catch (e) { + console.error('Failed to decode theme cache:', e) + return false + } + if (cache.engineChecksum === getEngineChecksum()) { + const styleSheet = new CSSStyleSheet() + const lazyStyleSheet = new CSSStyleSheet() + + cache.data[0].forEach(rule => styleSheet.insertRule(rule, 'index-max')) + cache.data[1].forEach(rule => lazyStyleSheet.insertRule(rule, 'index-max')) + + document.adoptedStyleSheets = [styleSheet, lazyStyleSheet] + + return true + } else { + console.warn('Engine checksum doesn\'t match, cache not usable, clearing') + localStorage.removeItem('pleroma-fe-theme-cache') + } +} + +export const applyTheme = async (input, onFinish = (data) => {}) => { const styleSheet = new CSSStyleSheet() + const styleArray = [] const lazyStyleSheet = new CSSStyleSheet() + const lazyStyleArray = [] const { lazyProcessFunc } = await generateTheme( input, @@ -97,8 +125,10 @@ export const applyTheme = async (input) => { onNewRule (rule, isLazy) { if (isLazy) { lazyStyleSheet.insertRule(rule, 'index-max') + lazyStyleArray.push(rule) } else { styleSheet.insertRule(rule, 'index-max') + styleArray.push(rule) } }, onEagerFinished () { @@ -106,6 +136,9 @@ export const applyTheme = async (input) => { }, onLazyFinished () { document.adoptedStyleSheets = [styleSheet, lazyStyleSheet] + const cache = { engineChecksum: getEngineChecksum(), data: [styleArray, lazyStyleArray] } + onFinish(cache) + localStorage.setItem('pleroma-fe-theme-cache', JSON.stringify(cache)) } } ) diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index 7457ab99..15b4493e 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -1,4 +1,5 @@ import { convert, brightness } from 'chromatism' +import sum from 'hash-sum' import { flattenDeep } from 'lodash' import { alphaBlend, @@ -142,8 +143,12 @@ componentsContext.keys().forEach(key => { components[component.name] = component }) +const engineChecksum = sum(components) + const ruleToSelector = genericRuleToSelector(components) +export const getEngineChecksum = () => engineChecksum + export const init = (extraRuleset, ultimateBackgroundColor) => { const staticVars = {} const stacked = {} @@ -463,6 +468,7 @@ export const init = (extraRuleset, ultimateBackgroundColor) => { return { lazy: result.filter(x => typeof x === 'function'), eager: result.filter(x => typeof x !== 'function'), - staticVars + staticVars, + engineChecksum } } diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 8501907e..2fed14bc 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -24,6 +24,7 @@ const fetchAndUpdate = ({ showImmediately = false, userId = false, listId = false, + statusId = false, tag = false, until, since @@ -47,6 +48,7 @@ const fetchAndUpdate = ({ args.userId = userId args.listId = listId + args.statusId = statusId args.tag = tag args.withMuted = !hideMutedPosts if (loggedIn && ['friends', 'public', 'publicAndExternal'].includes(timeline)) { @@ -78,15 +80,15 @@ const fetchAndUpdate = ({ }) } -const startFetching = ({ timeline = 'friends', credentials, store, userId = false, listId = false, tag = false }) => { +const startFetching = ({ timeline = 'friends', credentials, store, userId = false, listId = false, statusId = 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 - fetchAndUpdate({ timeline, credentials, store, showImmediately, userId, listId, tag }) + fetchAndUpdate({ timeline, credentials, store, showImmediately, userId, listId, statusId, tag }) const boundFetchAndUpdate = () => - fetchAndUpdate({ timeline, credentials, store, userId, listId, tag }) + fetchAndUpdate({ timeline, credentials, store, userId, listId, statusId, tag }) return promiseInterval(boundFetchAndUpdate, 10000) } const timelineFetcher = { |
