Merge remote-tracking branch 'upstream/develop' into neckbeard

This commit is contained in:
Your New SJW Waifu 2024-05-22 07:54:20 -05:00
commit 215ea3e790
32 changed files with 223 additions and 51 deletions

View File

@ -0,0 +1 @@
Display quotes count on posts and add quotes list page

View File

@ -0,0 +1 @@
Option to only show scrobbles that are recent enough

View File

@ -0,0 +1 @@
Add caching system for themes3

View File

@ -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",

View File

@ -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)

View File

@ -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([^/@]+)',

View File

@ -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

View File

@ -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>

View File

@ -17,6 +17,10 @@ export default {
units: {
type: Array,
default: () => allCssUnits
},
unitSet: {
type: String,
default: 'none'
}
},
computed: {
@ -30,6 +34,10 @@ export default {
},
methods: {
...Setting.methods,
getUnitString (value) {
if (this.unitSet === 'none') return value
return this.$t(['settings', 'units', this.unitSet, value].join('.'))
},
updateValue (e) {
this.configSink(this.path, parseInt(e.target.value) + this.stateUnit)
},

View File

@ -1,7 +1,7 @@
<template>
<span
v-if="matchesExpertLevel"
class="SizeSetting"
class="UnitSetting"
>
<label
:for="path"
@ -23,7 +23,7 @@
:id="path"
:model-value="stateUnit"
:disabled="disabled"
class="css-unit-input"
class="unit-input unstyled"
@change="updateUnit"
>
<option
@ -31,7 +31,7 @@
:key="option"
:value="option"
>
{{ option }}
{{ getUnitString(option) }}
</option>
</Select>
{{ ' ' }}
@ -42,20 +42,19 @@
</span>
</template>
<script src="./size_setting.js"></script>
<script src="./unit_setting.js"></script>
<style lang="scss">
.SizeSetting {
.UnitSetting {
.number-input {
max-width: 6.5em;
text-align: right;
}
.css-unit-input,
.css-unit-input select {
margin-left: 0.5em;
width: 4em;
max-width: 4em;
.unit-input,
.unit-input select {
min-width: 4em;
width: auto;
}
}

View File

@ -31,10 +31,6 @@
margin-bottom: 1em;
}
select {
min-width: 10em;
}
textarea {
width: 100%;
max-width: 100%;

View File

@ -31,10 +31,6 @@
margin-bottom: 1em;
}
select {
min-width: 10em;
}
textarea {
width: 100%;
max-width: 100%;

View File

@ -1,6 +1,7 @@
import { filter, trim, debounce } from 'lodash'
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import UnitSetting from '../helpers/unit_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
@ -19,6 +20,7 @@ const FilteringTab = {
components: {
BooleanSetting,
ChoiceSetting,
UnitSetting,
IntegerSetting
},
computed: {

View File

@ -119,6 +119,17 @@
{{ $t('settings.hide_scrobbles') }}
</BooleanSetting>
</li>
<li>
<UnitSetting
key="hideScrobblesAfter"
path="hideScrobblesAfter"
:units="['m', 'h', 'd']"
unitSet="time"
expert="1"
>
{{ $t('settings.hide_scrobbles_after') }}
</UnitSetting>
</li>
</ul>
</div>
<div

View File

@ -3,7 +3,7 @@ import ChoiceSetting from '../helpers/choice_setting.vue'
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import FloatSetting from '../helpers/float_setting.vue'
import SizeSetting, { defaultHorizontalUnits } from '../helpers/size_setting.vue'
import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
@ -64,7 +64,7 @@ const GeneralTab = {
ChoiceSetting,
IntegerSetting,
FloatSetting,
SizeSetting,
UnitSetting,
InterfaceLanguageSwitcher,
ScopeSelector,
ProfileSettingIndicator

View File

@ -134,7 +134,7 @@
<li v-if="expertLevel > 0">
{{ $t('settings.column_sizes') }}
<div class="column-settings">
<SizeSetting
<UnitSetting
v-for="column in columns"
:key="column"
:path="column + 'ColumnWidth'"
@ -142,7 +142,7 @@
expert="1"
>
{{ $t('settings.column_sizes_' + column) }}
</SizeSetting>
</UnitSetting>
</div>
</li>
<li class="select-multiple">
@ -200,6 +200,14 @@
<div class="setting-item">
<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"

View File

@ -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
},
@ -422,7 +425,27 @@ const Status = {
return this.quotedStatus && this.displayQuote
},
scrobblePresent () {
return !this.mergedConfig.hideScrobbles && this.status.user.latestScrobble && this.status.user.latestScrobble.artist
if (this.mergedConfig.hideScrobbles) return false
if (!this.status.user.latestScrobble) return false
const value = this.mergedConfig.hideScrobblesAfter.match(/\d+/gs)[0]
const unit = this.mergedConfig.hideScrobblesAfter.match(/\D+/gs)[0]
let multiplier = 60 * 1000 // minutes is smallest unit
switch (unit) {
case 'm':
break
case 'h':
multiplier *= 60 // hour
break
case 'd':
multiplier *= 60 // hour
multiplier *= 24 // day
break
}
const maxAge = Number(value) * multiplier
const createdAt = Date.parse(this.status.user.latestScrobble.created_at)
const age = Date.now() - createdAt
if (age > maxAge) return false
return this.status.user.latestScrobble.artist
},
scrobble () {
return this.status.user.latestScrobble

View File

@ -374,6 +374,7 @@
font-weight: bolder;
font-size: 1.1em;
line-height: 1em;
color: var(--text);
}
&:hover .stat-title {

View File

@ -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>

View File

@ -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) {

View File

@ -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'
}
}

View File

@ -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í",

View File

@ -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…",
@ -394,6 +395,14 @@
"desc": "To enable two-factor authentication, enter the code from your two-factor app:"
}
},
"units": {
"time": {
"m": "minutes",
"s": "seconds",
"h": "hours",
"d": "days"
}
},
"lists_navigation": "Show lists in navigation",
"allow_following_move": "Allow auto-follow when following account moves",
"attachmentRadius": "Attachments",
@ -501,6 +510,7 @@
"mute_bot_posts": "Mute bot posts",
"hide_actor_type_indication": "Hide actor type (bots, groups, etc.) indication in posts",
"hide_scrobbles": "Hide scrobbles",
"hide_scrobbles_after": "Hide scrobbles older than",
"mute_sensitive_posts": "Mute sensitive posts",
"hide_all_muted_posts": "Hide muted posts",
"max_thumbnails": "Maximum amount of thumbnails per post (empty = no limit)",
@ -635,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",
@ -1047,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",

View File

@ -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 })
},

View File

@ -28,6 +28,7 @@ export const defaultState = {
theme: undefined,
customTheme: undefined,
customThemeSource: undefined,
forceThemeRecompilation: false,
hideISP: false,
hideInstanceWallpaper: false,
hideShoutbox: false,
@ -42,6 +43,7 @@ export const defaultState = {
hideAttachments: false,
hideAttachmentsInConv: false,
hideScrobbles: false,
hideScrobblesAfter: '2d',
maxThumbnails: 512,
hideNsfw: true,
preloadImage: true,

View File

@ -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

View File

@ -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])
}

View File

@ -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) {

View File

@ -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

View File

@ -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))
}
}
)

View File

@ -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
}
}

View File

@ -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 = {