Merge remote-tracking branch 'origin/develop' into timed-user-mutes
This commit is contained in:
commit
385f921c41
72 changed files with 1336 additions and 851 deletions
|
|
@ -14,6 +14,7 @@ import EditStatusModal from './components/edit_status_modal/edit_status_modal.vu
|
|||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||
import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
|
||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||
import { getOrCreateServiceWorker } from './services/sw/sw'
|
||||
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
|
@ -77,6 +78,7 @@ export default {
|
|||
this.setThemeBodyClass()
|
||||
this.removeSplash()
|
||||
}
|
||||
getOrCreateServiceWorker()
|
||||
},
|
||||
unmounted () {
|
||||
window.removeEventListener('resize', this.updateMobileState)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
/* stylelint-disable no-descending-specificity */
|
||||
@use "panel";
|
||||
|
||||
@import '@fortawesome/fontawesome-svg-core/styles.css';
|
||||
@import '@kazvmoe-infra/pinch-zoom-element/dist/pinch-zoom.css';
|
||||
|
||||
:root {
|
||||
--status-margin: 0.75em;
|
||||
--post-line-height: 1.4;
|
||||
|
|
@ -30,6 +33,7 @@ body {
|
|||
font-family: sans-serif;
|
||||
font-family: var(--font);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import VueVirtualScroller from 'vue-virtual-scroller'
|
|||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
import { config } from '@fortawesome/fontawesome-svg-core';
|
||||
config.autoAddCss = false
|
||||
|
||||
import App from '../App.vue'
|
||||
import routes from './routes'
|
||||
|
|
@ -21,6 +23,7 @@ import { useOAuthStore } from 'src/stores/oauth'
|
|||
import { useI18nStore } from 'src/stores/i18n'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow'
|
||||
|
||||
let staticInitialResults = null
|
||||
|
||||
|
|
@ -63,10 +66,11 @@ const getInstanceConfig = async ({ store }) => {
|
|||
const textlimit = data.max_toot_chars
|
||||
const vapidPublicKey = data.pleroma.vapid_public_key
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaExtensionsAvailable', value: data.pleroma })
|
||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
|
||||
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
|
||||
store.dispatch('setInstanceOption', { name: 'birthdayRequired', value: !!data.pleroma.metadata.birthday_required })
|
||||
store.dispatch('setInstanceOption', { name: 'birthdayMinAge', value: data.pleroma.metadata.birthday_min_age || 0 })
|
||||
store.dispatch('setInstanceOption', { name: 'birthdayRequired', value: !!data.pleroma?.metadata.birthday_required })
|
||||
store.dispatch('setInstanceOption', { name: 'birthdayMinAge', value: data.pleroma?.metadata.birthday_min_age || 0 })
|
||||
|
||||
if (vapidPublicKey) {
|
||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||
|
|
@ -78,6 +82,8 @@ const getInstanceConfig = async ({ store }) => {
|
|||
console.error('Could not load instance config, potentially fatal')
|
||||
console.error(error)
|
||||
}
|
||||
// We should check for scrobbles support here but it requires userId
|
||||
// so instead we check for it where it's fetched (statuses.js)
|
||||
}
|
||||
|
||||
const getBackendProvidedConfig = async () => {
|
||||
|
|
@ -153,7 +159,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
: config.logoMargin
|
||||
})
|
||||
copyInstanceOption('logoLeft')
|
||||
store.commit('authFlow/setInitialStrategy', config.loginMethod)
|
||||
useAuthFlowStore().setInitialStrategy(config.loginMethod)
|
||||
|
||||
copyInstanceOption('redirectRootNoLogin')
|
||||
copyInstanceOption('redirectRootLogin')
|
||||
|
|
@ -242,7 +248,8 @@ const resolveStaffAccounts = ({ store, accounts }) => {
|
|||
|
||||
const getNodeInfo = async ({ store }) => {
|
||||
try {
|
||||
const res = await preloadFetch('/nodeinfo/2.1.json')
|
||||
let res = await preloadFetch('/nodeinfo/2.1.json')
|
||||
if (!res.ok) res = await preloadFetch('/nodeinfo/2.0.json')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
const metadata = data.metadata
|
||||
|
|
@ -254,7 +261,12 @@ const getNodeInfo = async ({ store }) => {
|
|||
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
||||
store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaCustomEmojiReactionsAvailable', value: features.includes('pleroma_custom_emoji_reactions') })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'pleromaCustomEmojiReactionsAvailable',
|
||||
value:
|
||||
features.includes('pleroma_custom_emoji_reactions') ||
|
||||
features.includes('custom_emoji_reactions')
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaBookmarkFoldersAvailable', value: features.includes('pleroma:bookmark_folders') })
|
||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||
|
|
@ -264,6 +276,7 @@ const getNodeInfo = async ({ store }) => {
|
|||
store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') })
|
||||
store.dispatch('setInstanceOption', { name: 'groupActorAvailable', value: features.includes('pleroma:group_actors') })
|
||||
store.dispatch('setInstanceOption', { name: 'blockExpiration', value: features.includes('pleroma:block_expiration') })
|
||||
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances ?? [] })
|
||||
|
||||
const uploadLimits = metadata.uploadLimits
|
||||
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
|
||||
|
|
@ -282,7 +295,6 @@ const getNodeInfo = async ({ store }) => {
|
|||
const software = data.software
|
||||
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
|
||||
store.dispatch('setInstanceOption', { name: 'backendRepository', value: software.repository })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' })
|
||||
|
||||
const priv = metadata.private
|
||||
store.dispatch('setInstanceOption', { name: 'private', value: priv })
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import PublicTimeline from 'components/public_timeline/public_timeline.vue'
|
||||
import BubbleTimeline from 'components/bubble_timeline/bubble_timeline.vue'
|
||||
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
|
||||
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
|
||||
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
|
||||
|
|
@ -54,6 +55,7 @@ export default (store) => {
|
|||
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
||||
{ name: 'bubble', path: '/bubble', component: BubbleTimeline },
|
||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||
{ name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import { h, resolveComponent } from 'vue'
|
|||
import LoginForm from '../login_form/login_form.vue'
|
||||
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
|
||||
import MFATOTPForm from '../mfa_form/totp_form.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { mapState } from 'pinia'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow'
|
||||
|
||||
const AuthForm = {
|
||||
name: 'AuthForm',
|
||||
|
|
@ -15,7 +16,7 @@ const AuthForm = {
|
|||
if (this.requiredRecovery) { return 'MFARecoveryForm' }
|
||||
return 'LoginForm'
|
||||
},
|
||||
...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])
|
||||
...mapState(useAuthFlowStore, ['requiredTOTP', 'requiredRecovery'])
|
||||
},
|
||||
components: {
|
||||
MFARecoveryForm,
|
||||
|
|
|
|||
18
src/components/bubble_timeline/bubble_timeline.js
Normal file
18
src/components/bubble_timeline/bubble_timeline.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
const BubbleTimeline = {
|
||||
components: {
|
||||
Timeline
|
||||
},
|
||||
computed: {
|
||||
timeline () { return this.$store.state.statuses.timelines.bubble }
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
|
||||
},
|
||||
unmounted () {
|
||||
this.$store.dispatch('stopFetchingTimeline', 'bubble')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BubbleTimeline
|
||||
9
src/components/bubble_timeline/bubble_timeline.vue
Normal file
9
src/components/bubble_timeline/bubble_timeline.vue
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<Timeline
|
||||
:title="$t('nav.bubble')"
|
||||
:timeline="timeline"
|
||||
:timeline-name="'bubble'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script src="./bubble_timeline.js"></script>
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
class="textColor unstyled"
|
||||
:class="{ disabled: !present || disabled }"
|
||||
type="text"
|
||||
:value="modelValue || fallback"
|
||||
:value="modelValue ?? fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="updateValue($event.target.value)"
|
||||
>
|
||||
|
|
|
|||
82
src/components/component_preview/component_preview.js
Normal file
82
src/components/component_preview/component_preview.js
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||
|
||||
import genRandomSeed from 'src/services/random_seed/random_seed.service.js'
|
||||
import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Checkbox,
|
||||
ColorInput
|
||||
},
|
||||
props: [
|
||||
'shadow',
|
||||
'shadowControl',
|
||||
'previewClass',
|
||||
'previewStyle',
|
||||
'previewCss',
|
||||
'disabled',
|
||||
'invalid',
|
||||
'noColorControl'
|
||||
],
|
||||
emits: ['update:shadow'],
|
||||
data () {
|
||||
return {
|
||||
colorOverride: undefined,
|
||||
lightGrid: false,
|
||||
zoom: 100,
|
||||
randomSeed: genRandomSeed()
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.update()
|
||||
},
|
||||
computed: {
|
||||
hideControls () {
|
||||
return typeof this.shadow === 'string'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
previewCss () {
|
||||
this.update()
|
||||
},
|
||||
previewStyle () {
|
||||
this.update()
|
||||
},
|
||||
zoom () {
|
||||
this.update()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateProperty (axis, value) {
|
||||
this.$emit('update:shadow', { axis, value: Number(value) })
|
||||
},
|
||||
update () {
|
||||
const sheet = createStyleSheet('style-component-preview', 90)
|
||||
|
||||
sheet.clear()
|
||||
|
||||
const result = [this.previewCss]
|
||||
if (this.colorOverride) result.push(`--background: ${this.colorOverride}`)
|
||||
|
||||
const styleRule = [
|
||||
'#component-preview-', this.randomSeed, ' {\n',
|
||||
'.preview-block {\n',
|
||||
`zoom: ${this.zoom / 100};`,
|
||||
this.previewStyle,
|
||||
'\n}',
|
||||
'\n}'
|
||||
].join('')
|
||||
|
||||
sheet.addRule(styleRule)
|
||||
sheet.addRule([
|
||||
'#component-preview-', this.randomSeed, ' {\n',
|
||||
...result,
|
||||
'\n}'
|
||||
].join(''))
|
||||
|
||||
sheet.ready = true
|
||||
adoptStyleSheets()
|
||||
}
|
||||
}
|
||||
}
|
||||
151
src/components/component_preview/component_preview.scss
Normal file
151
src/components/component_preview/component_preview.scss
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
.ComponentPreview {
|
||||
display: grid;
|
||||
grid-template-columns: 1em 1fr 1fr 1em;
|
||||
grid-template-rows: 2em 1fr 1fr 1fr 1em 2em max-content;
|
||||
grid-template-areas:
|
||||
"header header header header "
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"x-slide x-slide x-slide . "
|
||||
"x-num x-num y-num y-num "
|
||||
"assists assists assists assists";
|
||||
grid-gap: 0.5em;
|
||||
|
||||
&:not(.-shadow-controls) {
|
||||
grid-template-areas:
|
||||
"header header header header "
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"assists assists assists assists";
|
||||
grid-template-rows: 2em 1fr 1fr 1fr max-content;
|
||||
}
|
||||
|
||||
.header {
|
||||
grid-area: header;
|
||||
place-self: baseline center;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.invalid-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center center;
|
||||
background-color: rgb(100 0 0 / 50%);
|
||||
|
||||
.alert {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.assists {
|
||||
grid-area: assists;
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
grid-auto-rows: 2em;
|
||||
grid-gap: 0.5em;
|
||||
}
|
||||
|
||||
.input-light-grid {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.input-number {
|
||||
min-width: 2em;
|
||||
}
|
||||
|
||||
.x-shift-number {
|
||||
grid-area: x-num;
|
||||
justify-self: right;
|
||||
}
|
||||
|
||||
.y-shift-number {
|
||||
grid-area: y-num;
|
||||
justify-self: left;
|
||||
}
|
||||
|
||||
.x-shift-number,
|
||||
.y-shift-number {
|
||||
input {
|
||||
max-width: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
.x-shift-slider {
|
||||
grid-area: x-slide;
|
||||
height: auto;
|
||||
align-self: start;
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
.y-shift-slider {
|
||||
grid-area: y-slide;
|
||||
writing-mode: vertical-lr;
|
||||
justify-self: left;
|
||||
min-height: 10em;
|
||||
}
|
||||
|
||||
.x-shift-slider,
|
||||
.y-shift-slider {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.preview-window {
|
||||
--__grid-color1: rgb(102 102 102);
|
||||
--__grid-color2: rgb(153 153 153);
|
||||
--__grid-color1-disabled: rgb(102 102 102 / 20%);
|
||||
--__grid-color2-disabled: rgb(153 153 153 / 20%);
|
||||
|
||||
&.-light-grid {
|
||||
--__grid-color1: rgb(205 205 205);
|
||||
--__grid-color2: rgb(255 255 255);
|
||||
--__grid-color1-disabled: rgb(205 205 205 / 20%);
|
||||
--__grid-color2-disabled: rgb(255 255 255 / 20%);
|
||||
}
|
||||
|
||||
position: relative;
|
||||
grid-area: preview;
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 10em;
|
||||
min-height: 10em;
|
||||
background-color: var(--__grid-color2);
|
||||
background-image:
|
||||
linear-gradient(45deg, var(--__grid-color1) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, var(--__grid-color1) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, var(--__grid-color1) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, var(--__grid-color1) 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0;
|
||||
border-radius: var(--roundness);
|
||||
|
||||
&.disabled {
|
||||
background-color: var(--__grid-color2-disabled);
|
||||
background-image:
|
||||
linear-gradient(45deg, var(--__grid-color1-disabled) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, var(--__grid-color1-disabled) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, var(--__grid-color1-disabled) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, var(--__grid-color1-disabled) 75%);
|
||||
}
|
||||
|
||||
.preview-block {
|
||||
background: var(--background, var(--bg));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 33%;
|
||||
min-height: 33%;
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: var(--border);
|
||||
border-radius: var(--roundness);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
<template>
|
||||
<div
|
||||
:id="'component-preview-' + randomSeed"
|
||||
class="ComponentPreview"
|
||||
:class="{ '-shadow-controls': shadowControl }"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||
<component
|
||||
:is="'style'"
|
||||
v-html="previewCss"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||
<label
|
||||
v-show="shadowControl"
|
||||
role="heading"
|
||||
|
|
@ -74,7 +69,6 @@
|
|||
<div
|
||||
class="preview-block"
|
||||
:class="previewClass"
|
||||
:style="style"
|
||||
>
|
||||
{{ $t('settings.style.themes3.editor.test_string') }}
|
||||
</div>
|
||||
|
|
@ -116,203 +110,5 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Checkbox,
|
||||
ColorInput
|
||||
},
|
||||
props: [
|
||||
'shadow',
|
||||
'shadowControl',
|
||||
'previewClass',
|
||||
'previewStyle',
|
||||
'previewCss',
|
||||
'disabled',
|
||||
'invalid',
|
||||
'noColorControl'
|
||||
],
|
||||
emits: ['update:shadow'],
|
||||
data () {
|
||||
return {
|
||||
colorOverride: undefined,
|
||||
lightGrid: false,
|
||||
zoom: 100
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
style () {
|
||||
const result = [
|
||||
this.previewStyle,
|
||||
`zoom: ${this.zoom / 100}`
|
||||
]
|
||||
if (this.colorOverride) result.push(`--background: ${this.colorOverride}`)
|
||||
return result
|
||||
},
|
||||
hideControls () {
|
||||
return typeof this.shadow === 'string'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateProperty (axis, value) {
|
||||
this.$emit('update:shadow', { axis, value: Number(value) })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.ComponentPreview {
|
||||
display: grid;
|
||||
grid-template-columns: 1em 1fr 1fr 1em;
|
||||
grid-template-rows: 2em 1fr 1fr 1fr 1em 2em max-content;
|
||||
grid-template-areas:
|
||||
"header header header header "
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"x-slide x-slide x-slide . "
|
||||
"x-num x-num y-num y-num "
|
||||
"assists assists assists assists";
|
||||
grid-gap: 0.5em;
|
||||
|
||||
&:not(.-shadow-controls) {
|
||||
grid-template-areas:
|
||||
"header header header header "
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"assists assists assists assists";
|
||||
grid-template-rows: 2em 1fr 1fr 1fr max-content;
|
||||
}
|
||||
|
||||
.header {
|
||||
grid-area: header;
|
||||
place-self: baseline center;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.invalid-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center center;
|
||||
background-color: rgb(100 0 0 / 50%);
|
||||
|
||||
.alert {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.assists {
|
||||
grid-area: assists;
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
grid-auto-rows: 2em;
|
||||
grid-gap: 0.5em;
|
||||
}
|
||||
|
||||
.input-light-grid {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.input-number {
|
||||
min-width: 2em;
|
||||
}
|
||||
|
||||
.x-shift-number {
|
||||
grid-area: x-num;
|
||||
justify-self: right;
|
||||
}
|
||||
|
||||
.y-shift-number {
|
||||
grid-area: y-num;
|
||||
justify-self: left;
|
||||
}
|
||||
|
||||
.x-shift-number,
|
||||
.y-shift-number {
|
||||
input {
|
||||
max-width: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
.x-shift-slider {
|
||||
grid-area: x-slide;
|
||||
height: auto;
|
||||
align-self: start;
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
.y-shift-slider {
|
||||
grid-area: y-slide;
|
||||
writing-mode: vertical-lr;
|
||||
justify-self: left;
|
||||
min-height: 10em;
|
||||
}
|
||||
|
||||
.x-shift-slider,
|
||||
.y-shift-slider {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.preview-window {
|
||||
--__grid-color1: rgb(102 102 102);
|
||||
--__grid-color2: rgb(153 153 153);
|
||||
--__grid-color1-disabled: rgb(102 102 102 / 20%);
|
||||
--__grid-color2-disabled: rgb(153 153 153 / 20%);
|
||||
|
||||
&.-light-grid {
|
||||
--__grid-color1: rgb(205 205 205);
|
||||
--__grid-color2: rgb(255 255 255);
|
||||
--__grid-color1-disabled: rgb(205 205 205 / 20%);
|
||||
--__grid-color2-disabled: rgb(255 255 255 / 20%);
|
||||
}
|
||||
|
||||
position: relative;
|
||||
grid-area: preview;
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 10em;
|
||||
min-height: 10em;
|
||||
background-color: var(--__grid-color2);
|
||||
background-image:
|
||||
linear-gradient(45deg, var(--__grid-color1) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, var(--__grid-color1) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, var(--__grid-color1) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, var(--__grid-color1) 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0;
|
||||
border-radius: var(--roundness);
|
||||
|
||||
&.disabled {
|
||||
background-color: var(--__grid-color2-disabled);
|
||||
background-image:
|
||||
linear-gradient(45deg, var(--__grid-color1-disabled) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, var(--__grid-color1-disabled) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, var(--__grid-color1-disabled) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, var(--__grid-color1-disabled) 75%);
|
||||
}
|
||||
|
||||
.preview-block {
|
||||
background: var(--background, var(--bg));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 33%;
|
||||
min-height: 33%;
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: var(--border);
|
||||
border-radius: var(--roundness);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script src="./component_preview.js" />
|
||||
<style src="./component_preview.scss" lang="scss" />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
import { mapStores } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
import { mapStores, mapActions, mapState as mapPiniaState } from 'pinia'
|
||||
import oauthApi from '../../services/new_api/oauth.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes
|
||||
|
|
@ -25,13 +26,10 @@ const LoginForm = {
|
|||
instance: state => state.instance,
|
||||
loggingIn: state => state.users.loggingIn,
|
||||
}),
|
||||
...mapGetters(
|
||||
'authFlow', ['requiredPassword', 'requiredToken', 'requiredMFA']
|
||||
)
|
||||
...mapPiniaState(useAuthFlowStore, ['requiredPassword', 'requiredToken', 'requiredMFA'])
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('authFlow', ['requireMFA']),
|
||||
...mapActions({ login: 'authFlow/login' }),
|
||||
...mapActions(useAuthFlowStore, ['requireMFA', 'login']),
|
||||
submit () {
|
||||
this.isTokenAuth ? this.submitToken() : this.submitPassword()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import mfaApi from '../../services/new_api/mfa.js'
|
||||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
import { mapStores } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
import { mapStores, mapActions, mapState as mapPiniaState } from 'pinia'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes
|
||||
|
|
@ -17,8 +18,8 @@ export default {
|
|||
error: false
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters({
|
||||
authSettings: 'authFlow/settings'
|
||||
...mapPiniaState(useAuthFlowStore, {
|
||||
authSettings: store => store.settings
|
||||
}),
|
||||
...mapStores(useOAuthStore),
|
||||
...mapState({
|
||||
|
|
@ -26,8 +27,7 @@ export default {
|
|||
})
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('authFlow', ['requireTOTP', 'abortMFA']),
|
||||
...mapActions({ login: 'authFlow/login' }),
|
||||
...mapActions(useAuthFlowStore, ['requireTOTP', 'abortMFA', 'login']),
|
||||
clearError () { this.error = false },
|
||||
|
||||
focusOnCodeInput () {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import mfaApi from '../../services/new_api/mfa.js'
|
||||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
import { mapStores } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
import { mapStores, mapActions, mapState as mapPiniaState } from 'pinia'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes
|
||||
|
|
@ -17,8 +18,8 @@ export default {
|
|||
error: false
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters({
|
||||
authSettings: 'authFlow/settings'
|
||||
...mapPiniaState(useAuthFlowStore, {
|
||||
authSettings: store => store.settings
|
||||
}),
|
||||
...mapStores(useOAuthStore),
|
||||
...mapState({
|
||||
|
|
@ -26,8 +27,7 @@ export default {
|
|||
})
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('authFlow', ['requireRecovery', 'abortMFA']),
|
||||
...mapActions({ login: 'authFlow/login' }),
|
||||
...mapActions(useAuthFlowStore, ['requireRecovery', 'abortMFA', 'login']),
|
||||
clearError () { this.error = false },
|
||||
|
||||
focusOnCodeInput () {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'
|
|||
import {
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faCity,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faChevronDown,
|
||||
|
|
@ -31,6 +32,7 @@ import {
|
|||
library.add(
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faCity,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faChevronDown,
|
||||
|
|
@ -108,12 +110,15 @@ const NavPanel = {
|
|||
privateMode: state => state.instance.private,
|
||||
federating: state => state.instance.federating,
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
||||
bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable
|
||||
bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable,
|
||||
bubbleTimeline: state => state.instance.localBubbleInstances.length > 0
|
||||
}),
|
||||
timelinesItems () {
|
||||
return filterNavigation(
|
||||
Object
|
||||
.entries({ ...TIMELINES })
|
||||
// do not show in timeliens list since it's in a better place now
|
||||
.filter(([key]) => key !== 'bookmarks')
|
||||
.map(([k, v]) => ({ ...v, name: k })),
|
||||
{
|
||||
hasChats: this.pleromaChatMessagesAvailable,
|
||||
|
|
@ -121,6 +126,7 @@ const NavPanel = {
|
|||
isFederating: this.federating,
|
||||
isPrivate: this.privateMode,
|
||||
currentUser: this.currentUser,
|
||||
supportsBubbleTimeline: this.bubbleTimeline,
|
||||
supportsBookmarkFolders: this.bookmarkFolders
|
||||
}
|
||||
)
|
||||
|
|
@ -136,6 +142,7 @@ const NavPanel = {
|
|||
isFederating: this.federating,
|
||||
isPrivate: this.privateMode,
|
||||
currentUser: this.currentUser,
|
||||
supportsBubbleTimeline: this.bubbleTimeline,
|
||||
supportsBookmarkFolders: this.bookmarkFolders
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFederating, isPrivate, currentUser, supportsBookmarkFolders }) => {
|
||||
export const filterNavigation = (list = [], {
|
||||
hasChats,
|
||||
hasAnnouncements,
|
||||
isFederating,
|
||||
isPrivate,
|
||||
currentUser,
|
||||
supportsBookmarkFolders,
|
||||
supportsBubbleTimeline
|
||||
}) => {
|
||||
return list.filter(({ criteria, anon, anonRoute }) => {
|
||||
const set = new Set(criteria || [])
|
||||
if (!isFederating && set.has('federating')) return false
|
||||
|
|
@ -7,6 +15,8 @@ export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFede
|
|||
if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) return false
|
||||
if (!hasChats && set.has('chats')) return false
|
||||
if (!hasAnnouncements && set.has('announcements')) return false
|
||||
if (!supportsBubbleTimeline && set.has('supportsBubbleTimeline')) return false
|
||||
if (!supportsBookmarkFolders && set.has('supportsBookmarkFolders')) return false
|
||||
if (supportsBookmarkFolders && set.has('!supportsBookmarkFolders')) return false
|
||||
return true
|
||||
})
|
||||
|
|
@ -19,11 +29,11 @@ export const getListEntries = store => store.allLists.map(list => ({
|
|||
iconLetter: list.title[0]
|
||||
}))
|
||||
|
||||
export const getBookmarkFolderEntries = store => store.allFolders.map(folder => ({
|
||||
export const getBookmarkFolderEntries = store => store.allFolders ? store.allFolders.map(folder => ({
|
||||
name: 'bookmark-folder-' + folder.id,
|
||||
routeObject: { name: 'bookmark-folder', params: { id: folder.id } },
|
||||
labelRaw: folder.name,
|
||||
iconEmoji: folder.emoji,
|
||||
iconEmojiUrl: folder.emoji_url,
|
||||
iconLetter: folder.name[0]
|
||||
}))
|
||||
})) : []
|
||||
|
|
|
|||
|
|
@ -27,6 +27,13 @@ export const TIMELINES = {
|
|||
label: 'nav.public_tl',
|
||||
criteria: ['!private']
|
||||
},
|
||||
bubble: {
|
||||
route: 'bubble',
|
||||
anon: true,
|
||||
icon: 'city',
|
||||
label: 'nav.bubble',
|
||||
criteria: ['!private', 'federating', 'supportsBubbleTimeline']
|
||||
},
|
||||
twkn: {
|
||||
route: 'public-external-timeline',
|
||||
anon: true,
|
||||
|
|
@ -34,11 +41,11 @@ export const TIMELINES = {
|
|||
label: 'nav.twkn',
|
||||
criteria: ['!private', 'federating']
|
||||
},
|
||||
// bookmarks are still technically a timeline so we should show it in the dropdown
|
||||
bookmarks: {
|
||||
route: 'bookmarks',
|
||||
icon: 'bookmark',
|
||||
label: 'nav.bookmarks',
|
||||
criteria: ['!supportsBookmarkFolders']
|
||||
},
|
||||
favorites: {
|
||||
routeObject: { name: 'user-profile', query: { tab: 'favorites' } },
|
||||
|
|
@ -53,6 +60,15 @@ export const TIMELINES = {
|
|||
}
|
||||
|
||||
export const ROOT_ITEMS = {
|
||||
bookmarks: {
|
||||
route: 'bookmarks',
|
||||
icon: 'bookmark',
|
||||
label: 'nav.bookmarks',
|
||||
// shows bookmarks entry in a better suited location
|
||||
// hides it when bookmark folders are supported since
|
||||
// we show custom component instead of it
|
||||
criteria: ['!supportsBookmarkFolders']
|
||||
},
|
||||
interactions: {
|
||||
route: 'interactions',
|
||||
icon: 'bell',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'
|
|||
import {
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faCity,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faComments,
|
||||
|
|
@ -25,6 +26,7 @@ import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
|||
library.add(
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faCity,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faComments,
|
||||
|
|
@ -65,7 +67,8 @@ const NavPanel = {
|
|||
followRequestCount: state => state.api.followRequests.length,
|
||||
privateMode: state => state.instance.private,
|
||||
federating: state => state.instance.federating,
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
||||
bubbleTimeline: state => state.instance.localBubbleInstances.length > 0
|
||||
}),
|
||||
pinnedList () {
|
||||
if (!this.currentUser) {
|
||||
|
|
@ -79,7 +82,9 @@ const NavPanel = {
|
|||
hasAnnouncements: this.supportsAnnouncements,
|
||||
isFederating: this.federating,
|
||||
isPrivate: this.privateMode,
|
||||
currentUser: this.currentUser
|
||||
currentUser: this.currentUser,
|
||||
supportsBubbleTimeline: this.bubbleTimeline,
|
||||
supportsBookmarkFolders: this.bookmarks
|
||||
})
|
||||
}
|
||||
return filterNavigation(
|
||||
|
|
@ -98,6 +103,8 @@ const NavPanel = {
|
|||
{
|
||||
hasChats: this.pleromaChatMessagesAvailable,
|
||||
hasAnnouncements: this.supportsAnnouncements,
|
||||
supportsBubbleTimeline: this.bubbleTimeline,
|
||||
supportsBookmarkFolders: this.bookmarks,
|
||||
isFederating: this.federating,
|
||||
isPrivate: this.privateMode,
|
||||
currentUser: this.currentUser
|
||||
|
|
|
|||
|
|
@ -60,11 +60,14 @@
|
|||
}
|
||||
|
||||
.extra-button {
|
||||
border-left: 1px solid var(--icon);
|
||||
border-left: 1px solid;
|
||||
border-image-source: linear-gradient(to bottom, transparent 0%, var(--icon) var(--__horizontal-gap) calc(100% - var(--__horizontal-gap)), transparent 100%);
|
||||
border-image-slice: 1;
|
||||
padding-left: calc(var(--__horizontal-gap) - 1px);
|
||||
border-right: var(--__horizontal-gap) solid transparent;
|
||||
border-top: var(--__horizontal-gap) solid transparent;
|
||||
border-bottom: var(--__horizontal-gap) solid transparent;
|
||||
padding-right: var(--__horizontal-gap);
|
||||
padding-top: var(--__horizontal-gap);
|
||||
padding-bottom: var(--__horizontal-gap);
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
.main-button {
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import { newImporter } from 'src/services/export_import/export_import.js'
|
|||
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
|
||||
import { init } from 'src/services/theme_data/theme_data_3.service.js'
|
||||
import {
|
||||
getCssRules,
|
||||
getScopedVersion
|
||||
getCssRules
|
||||
} from 'src/services/theme_data/css_utils.js'
|
||||
import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
|
||||
import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
|
||||
|
|
@ -155,19 +155,23 @@ const AppearanceTab = {
|
|||
}))
|
||||
})
|
||||
|
||||
this.previewTheme('stock', 'v3')
|
||||
|
||||
if (window.IntersectionObserver) {
|
||||
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(({ target, isIntersecting }) => {
|
||||
if (!isIntersecting) return
|
||||
const theme = this.availableStyles.find(x => x.key === target.dataset.themeKey)
|
||||
this.$nextTick(() => {
|
||||
if (theme) theme.ready = true
|
||||
if (theme) this.previewTheme(theme.key, theme.version, theme.data)
|
||||
})
|
||||
observer.unobserve(target)
|
||||
})
|
||||
}, {
|
||||
root: this.$refs.themeList
|
||||
})
|
||||
} else {
|
||||
this.availableStyles.forEach(theme => this.previewTheme(theme.key, theme.version, theme.data))
|
||||
}
|
||||
},
|
||||
updated () {
|
||||
|
|
@ -391,7 +395,6 @@ const AppearanceTab = {
|
|||
inputRuleset: [...input, paletteRule].filter(x => x),
|
||||
ultimateBackgroundColor: '#000000',
|
||||
liteMode: true,
|
||||
debug: true,
|
||||
onlyNormalState: true
|
||||
})
|
||||
}
|
||||
|
|
@ -400,7 +403,6 @@ const AppearanceTab = {
|
|||
inputRuleset: [],
|
||||
ultimateBackgroundColor: '#000000',
|
||||
liteMode: true,
|
||||
debug: true,
|
||||
onlyNormalState: true
|
||||
})
|
||||
}
|
||||
|
|
@ -409,10 +411,15 @@ const AppearanceTab = {
|
|||
this.compilationCache[key] = theme3
|
||||
}
|
||||
|
||||
return getScopedVersion(
|
||||
getCssRules(theme3.eager),
|
||||
'#theme-preview-' + key
|
||||
).join('\n')
|
||||
|
||||
const sheet = createStyleSheet('appearance-tab-previews', 90)
|
||||
sheet.addRule([
|
||||
'#theme-preview-', key, ' {\n',
|
||||
getCssRules(theme3.eager).join('\n'),
|
||||
'\n}'
|
||||
].join(''))
|
||||
sheet.ready = true
|
||||
adoptStyleSheets()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,14 +16,6 @@
|
|||
:disabled="switchInProgress"
|
||||
@click="resetTheming"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<component
|
||||
:is="'style'"
|
||||
v-html="previewTheme('stock', 'v3')"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
|
||||
<preview id="theme-preview-stock" />
|
||||
<h4 class="theme-name">
|
||||
{{ $t('settings.style.stock_theme_used') }}
|
||||
|
|
@ -61,16 +53,6 @@
|
|||
:disabled="switchInProgress"
|
||||
@click="style.version === 'v2' ? setTheme(style.key) : setStyle(style.key)"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div v-if="style.ready || noIntersectionObserver">
|
||||
<component
|
||||
:is="'style'"
|
||||
v-html="previewTheme(style.key, style.version, style.data)"
|
||||
/>
|
||||
</div>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
|
||||
<preview :id="'theme-preview-' + style.key" />
|
||||
<h4 class="theme-name">
|
||||
{{ style.name }}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import InterfaceLanguageSwitcher from 'src/components/interface_language_switche
|
|||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
|
||||
import { clearCache, cacheKey, emojiCacheKey } from 'src/services/sw/sw.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faGlobe
|
||||
|
|
@ -98,6 +99,21 @@ const GeneralTab = {
|
|||
methods: {
|
||||
changeDefaultScope (value) {
|
||||
this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
|
||||
},
|
||||
clearCache (key) {
|
||||
clearCache(key)
|
||||
.then(() => {
|
||||
this.$store.dispatch('settingsSaved', { success: true })
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.dispatch('settingsSaved', { error })
|
||||
})
|
||||
},
|
||||
clearAssetCache () {
|
||||
this.clearCache(cacheKey)
|
||||
},
|
||||
clearEmojiCache () {
|
||||
this.clearCache(emojiCacheKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -509,6 +509,29 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
class="setting-item"
|
||||
>
|
||||
<h2>{{ $t('settings.cache') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="clearAssetCache"
|
||||
>
|
||||
{{ $t('settings.clear_asset_cache') }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="clearEmojiCache"
|
||||
>
|
||||
{{ $t('settings.clear_emoji_cache') }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ const SecurityTab = {
|
|||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
pleromaBackend () {
|
||||
return this.$store.state.instance.pleromaBackend
|
||||
pleromaExtensionsAvailable () {
|
||||
return this.$store.state.instance.pleromaExtensionsAvailable
|
||||
},
|
||||
oauthTokens () {
|
||||
return useOAuthTokensStore().tokens.map(oauthToken => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ref, reactive, computed, watch, watchEffect, provide, getCurrentInstance } from 'vue'
|
||||
import { ref, reactive, computed, watch, provide, getCurrentInstance } from 'vue'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import { get, set, unset, throttle } from 'lodash'
|
||||
|
||||
|
|
@ -19,11 +19,9 @@ import Preview from '../theme_tab/theme_preview.vue'
|
|||
|
||||
import VirtualDirectivesTab from './virtual_directives_tab.vue'
|
||||
|
||||
import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js'
|
||||
import { init, findColor } from 'src/services/theme_data/theme_data_3.service.js'
|
||||
import {
|
||||
getCssRules,
|
||||
getScopedVersion
|
||||
} from 'src/services/theme_data/css_utils.js'
|
||||
import { getCssRules } from 'src/services/theme_data/css_utils.js'
|
||||
import { serialize } from 'src/services/theme_data/iss_serializer.js'
|
||||
import { deserializeShadow, deserialize } from 'src/services/theme_data/iss_deserializer.js'
|
||||
import {
|
||||
|
|
@ -372,6 +370,9 @@ export default {
|
|||
const path = getPath(component, directive)
|
||||
|
||||
usedRule = get(real, path) // get real
|
||||
if (usedRule === '') {
|
||||
return usedRule
|
||||
}
|
||||
if (!usedRule) {
|
||||
usedRule = get(fallback, path)
|
||||
}
|
||||
|
|
@ -379,7 +380,7 @@ export default {
|
|||
return postProcess(usedRule)
|
||||
},
|
||||
set (value) {
|
||||
if (value) {
|
||||
if (value != null) {
|
||||
set(allEditedRules.value, getPath(component, directive), value)
|
||||
} else {
|
||||
unset(allEditedRules.value, getPath(component, directive))
|
||||
|
|
@ -667,7 +668,7 @@ export default {
|
|||
})
|
||||
|
||||
exports.clearStyle = () => {
|
||||
onImport(interfaceStore().styleDataUsed)
|
||||
onImport(interfaceStore.styleDataUsed)
|
||||
}
|
||||
|
||||
exports.exportStyle = () => {
|
||||
|
|
@ -685,19 +686,26 @@ export default {
|
|||
const overallPreviewRules = ref([])
|
||||
exports.overallPreviewRules = overallPreviewRules
|
||||
|
||||
const overallPreviewCssRules = ref([])
|
||||
watchEffect(throttle(() => {
|
||||
watch([overallPreviewRules], () => {
|
||||
let css = null
|
||||
try {
|
||||
overallPreviewCssRules.value = getScopedVersion(
|
||||
getCssRules(overallPreviewRules.value),
|
||||
'#edited-style-preview'
|
||||
).join('\n')
|
||||
css = getCssRules(overallPreviewRules.value).map(r => r.replace('html', '&'))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return
|
||||
}
|
||||
}, 500))
|
||||
|
||||
exports.overallPreviewCssRules = overallPreviewCssRules
|
||||
const sheet = createStyleSheet('style-tab-overall-preview', 90)
|
||||
|
||||
sheet.clear()
|
||||
sheet.addRule([
|
||||
'#edited-style-preview {\n',
|
||||
css.join('\n'),
|
||||
'\n}'
|
||||
].join(''))
|
||||
sheet.ready = true
|
||||
adoptStyleSheets()
|
||||
})
|
||||
|
||||
const updateOverallPreview = throttle(() => {
|
||||
try {
|
||||
|
|
@ -721,12 +729,12 @@ export default {
|
|||
console.error('Could not compile preview theme', e)
|
||||
return null
|
||||
}
|
||||
}, 5000)
|
||||
}, 1000)
|
||||
//
|
||||
// Apart from "hover" we can't really show how component looks like in
|
||||
// certain states, so we have to fake them.
|
||||
const simulatePseudoSelectors = (css, prefix) => css
|
||||
.replace(prefix, '.component-preview .preview-block')
|
||||
.replace(prefix, '.preview-block')
|
||||
.replace(':active', '.preview-active')
|
||||
.replace(':hover', '.preview-hover')
|
||||
.replace(':active', '.preview-active')
|
||||
|
|
|
|||
|
|
@ -6,14 +6,6 @@
|
|||
<div class="setting-item heading">
|
||||
<h2> {{ $t('settings.style.themes3.editor.title') }} </h2>
|
||||
<div class="meta-preview">
|
||||
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<component
|
||||
:is="'style'"
|
||||
v-html="overallPreviewCssRules"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
|
||||
<Preview id="edited-style-preview" />
|
||||
<teleport
|
||||
v-if="isActive"
|
||||
|
|
@ -155,12 +147,6 @@
|
|||
</ul>
|
||||
</div>
|
||||
<div class="preview-container">
|
||||
<!-- eslint-disable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||
<component
|
||||
:is="'style'"
|
||||
v-html="previewCss"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||
<ComponentPreview
|
||||
class="component-preview"
|
||||
:show-text="componentHas('Text')"
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
getCssRules,
|
||||
getScopedVersion
|
||||
} from 'src/services/theme_data/css_utils.js'
|
||||
import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js'
|
||||
|
||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||
import RangeInput from 'src/components/range_input/range_input.vue'
|
||||
|
|
@ -68,7 +69,6 @@ const colorConvert = (color) => {
|
|||
export default {
|
||||
data () {
|
||||
return {
|
||||
themeV3Preview: [],
|
||||
themeImporter: newImporter({
|
||||
validator: this.importValidator,
|
||||
onImport: this.onImport,
|
||||
|
|
@ -697,10 +697,16 @@ export default {
|
|||
liteMode: true
|
||||
})
|
||||
|
||||
this.themeV3Preview = getScopedVersion(
|
||||
const sheet = createStyleSheet('theme-tab-overall-preview', 90)
|
||||
const rule = getScopedVersion(
|
||||
getCssRules(theme3.eager),
|
||||
'#theme-preview'
|
||||
'&'
|
||||
).join('\n')
|
||||
|
||||
sheet.clear()
|
||||
sheet.addRule('#theme-preview {\n' + rule + '\n}')
|
||||
sheet.ready = true
|
||||
adoptStyleSheets()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
|
|||
|
|
@ -123,12 +123,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- eslint-disable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||
<component
|
||||
:is="'style'"
|
||||
v-html="themeV3Preview"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||
<preview id="theme-preview" />
|
||||
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
faLock,
|
||||
faLockOpen,
|
||||
faGlobe,
|
||||
faIgloo,
|
||||
faTimes,
|
||||
faRetweet,
|
||||
faReply,
|
||||
|
|
@ -43,6 +44,7 @@ import {
|
|||
library.add(
|
||||
faEnvelope,
|
||||
faGlobe,
|
||||
faIgloo,
|
||||
faLock,
|
||||
faLockOpen,
|
||||
faTimes,
|
||||
|
|
@ -484,6 +486,8 @@ const Status = {
|
|||
return 'lock-open'
|
||||
case 'direct':
|
||||
return 'envelope'
|
||||
case 'local':
|
||||
return 'igloo'
|
||||
default:
|
||||
return 'globe'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,4 +102,20 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.-extra {
|
||||
.action-counter {
|
||||
justify-self: end;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.extra-button {
|
||||
justify-self: end;
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@
|
|||
/>
|
||||
</component>
|
||||
<span
|
||||
v-if="!extra && button.counter?.(funcArg) > 0"
|
||||
v-if="button.counter?.(funcArg) > 0"
|
||||
class="action-counter"
|
||||
>
|
||||
{{ button.counter?.(funcArg) }}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,23 @@ const StatusContent = {
|
|||
hideTallStatus () {
|
||||
return this.mightHideBecauseTall && !this.showingTall
|
||||
},
|
||||
shouldShowToggle () {
|
||||
return this.mightHideBecauseSubject || this.mightHideBecauseTall
|
||||
},
|
||||
toggleButtonClasses () {
|
||||
return {
|
||||
'cw-status-hider': !this.showingMore && this.mightHideBecauseSubject,
|
||||
'tall-status-hider': !this.showingMore && this.mightHideBecauseTall,
|
||||
'status-unhider': this.showingMore,
|
||||
}
|
||||
},
|
||||
toggleText () {
|
||||
if (this.showingMore) {
|
||||
return this.mightHideBecauseSubject ? this.$t('status.hide_content') : this.$t('general.show_less')
|
||||
} else {
|
||||
return this.mightHideBecauseSubject ? this.$t('status.show_content') : this.$t('general.show_more')
|
||||
}
|
||||
},
|
||||
showingMore () {
|
||||
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@
|
|||
&.-tall-status {
|
||||
position: relative;
|
||||
height: 16em;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
|
||||
.media-body {
|
||||
|
|
@ -82,6 +81,10 @@
|
|||
mask-composite: exclude;
|
||||
}
|
||||
}
|
||||
|
||||
&.-expanded {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
& .tall-status-hider,
|
||||
|
|
@ -95,6 +98,13 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.status-unhider {
|
||||
margin-top: auto;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.tall-status-hider {
|
||||
position: absolute;
|
||||
height: 5em;
|
||||
|
|
@ -118,6 +128,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
&.-compact {
|
||||
align-items: start;
|
||||
flex-direction: row;
|
||||
|
|
@ -166,11 +180,11 @@
|
|||
line-height: inherit;
|
||||
margin: 0;
|
||||
border: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.text-wrapper {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,17 +31,9 @@
|
|||
</button>
|
||||
</div>
|
||||
<div
|
||||
:class="{'-tall-status': hideTallStatus}"
|
||||
class="text-wrapper"
|
||||
:class="{'-tall-status': hideTallStatus, '-expanded': showingMore}"
|
||||
>
|
||||
<button
|
||||
v-show="hideTallStatus"
|
||||
class="button-unstyled -link tall-status-hider"
|
||||
:class="{ '-focused': focused }"
|
||||
@click.prevent="toggleShowMore"
|
||||
>
|
||||
{{ $t("general.show_more") }}
|
||||
</button>
|
||||
<RichContent
|
||||
v-if="!hideSubjectStatus && !(singleLine && status.summary_raw_html)"
|
||||
:class="{ '-single-line': singleLine }"
|
||||
|
|
@ -54,45 +46,45 @@
|
|||
:attentions="status.attentions"
|
||||
@parse-ready="onParseReady"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-show="hideSubjectStatus"
|
||||
class="button-unstyled -link cw-status-hider"
|
||||
@click.prevent="toggleShowMore"
|
||||
<div
|
||||
v-show="shouldShowToggle"
|
||||
:class="toggleButtonClasses"
|
||||
>
|
||||
{{ $t("status.show_content") }}
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('image')"
|
||||
icon="image"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('video')"
|
||||
icon="video"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('audio')"
|
||||
icon="music"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('unknown')"
|
||||
icon="file"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="status.poll && status.poll.options"
|
||||
icon="poll-h"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="status.card"
|
||||
icon="link"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-show="showingMore && !fullContent"
|
||||
class="button-unstyled -link status-unhider"
|
||||
@click.prevent="toggleShowMore"
|
||||
>
|
||||
{{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default toggle-button"
|
||||
:class="{ '-focused': focused }"
|
||||
:aria-expanded="showingMore"
|
||||
@click.prevent="toggleShowMore"
|
||||
>
|
||||
{{ toggleText }}
|
||||
<template v-if="!showingMore">
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('image')"
|
||||
icon="image"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('video')"
|
||||
icon="video"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('audio')"
|
||||
icon="music"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('unknown')"
|
||||
icon="file"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="status.poll && status.poll.options"
|
||||
icon="poll-h"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="status.card"
|
||||
icon="link"
|
||||
/>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot v-if="!hideSubjectStatus" />
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ export const timelineNames = (supportsBookmarkFolders) => {
|
|||
dms: 'nav.dms',
|
||||
'public-timeline': 'nav.public_tl',
|
||||
'public-external-timeline': 'nav.twkn',
|
||||
quotes: 'nav.quotes'
|
||||
quotes: 'nav.quotes',
|
||||
bubble: 'nav.bubble'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +59,8 @@ const TimelineMenu = {
|
|||
currentUser: state => state.users.currentUser,
|
||||
privateMode: state => state.instance.private,
|
||||
federating: state => state.instance.federating,
|
||||
bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable
|
||||
bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable,
|
||||
bubbleTimeline: state => state.instance.localBubbleInstances.length > 0
|
||||
}),
|
||||
timelinesList () {
|
||||
return filterNavigation(
|
||||
|
|
@ -68,7 +70,8 @@ const TimelineMenu = {
|
|||
isFederating: this.federating,
|
||||
isPrivate: this.privateMode,
|
||||
currentUser: this.currentUser,
|
||||
supportsBookmarkFolders: this.bookmarkFolders
|
||||
supportsBookmarkFolders: this.bookmarkFolders,
|
||||
supportsBubbleTimeline: this.bubbleTimeline
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,7 +149,10 @@ export default {
|
|||
},
|
||||
showModerationMenu () {
|
||||
const privileges = this.loggedIn.privileges
|
||||
return this.loggedIn.role === 'admin' || privileges.includes('users_manage_activation_state') || privileges.includes('users_delete') || privileges.includes('users_manage_tags')
|
||||
return this.loggedIn.role === 'admin' ||
|
||||
privileges.includes('users_manage_activation_state') ||
|
||||
privileges.includes('users_delete') ||
|
||||
privileges.includes('users_manage_tags')
|
||||
},
|
||||
hasNote () {
|
||||
return this.relationship.note
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ const UserProfile = {
|
|||
return this.isUs || !this.user.hide_followers
|
||||
},
|
||||
favoritesTabVisible () {
|
||||
return this.isUs || !this.user.hide_favorites
|
||||
return this.isUs || (this.$store.state.instance.pleromaPublicFavouritesAvailable && !this.user.hide_favorites)
|
||||
},
|
||||
formattedBirthday () {
|
||||
const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@
|
|||
"flash_security": "Note that this can be potentially dangerous since Flash content is still arbitrary code.",
|
||||
"flash_fail": "Failed to load flash content, see console for details.",
|
||||
"scope_in_timeline": {
|
||||
"local": "Non-federated",
|
||||
"direct": "Direct",
|
||||
"private": "Followers-only",
|
||||
"public": "Public",
|
||||
|
|
@ -171,6 +172,7 @@
|
|||
"interactions": "Interactions",
|
||||
"dms": "Direct messages",
|
||||
"public_tl": "Public timeline",
|
||||
"bubble": "Bubble timeline",
|
||||
"timeline": "Timeline",
|
||||
"home_timeline": "Home timeline",
|
||||
"twkn": "Known Network",
|
||||
|
|
@ -289,7 +291,8 @@
|
|||
"text/plain": "Plain text",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown",
|
||||
"text/bbcode": "BBCode"
|
||||
"text/bbcode": "BBCode",
|
||||
"text/x.misskeymarkdown": "MFM"
|
||||
},
|
||||
"content_type_selection": "Post format",
|
||||
"content_warning": "Subject (optional)",
|
||||
|
|
@ -1088,7 +1091,10 @@
|
|||
"reset_value": "Reset",
|
||||
"reset_value_tooltip": "Reset draft",
|
||||
"hard_reset_value": "Hard reset",
|
||||
"hard_reset_value_tooltip": "Remove setting from storage, forcing use of default value"
|
||||
"hard_reset_value_tooltip": "Remove setting from storage, forcing use of default value",
|
||||
"cache": "Cache",
|
||||
"clear_asset_cache": "Clear asset cache",
|
||||
"clear_emoji_cache": "Clear emoji cache"
|
||||
},
|
||||
"admin_dash": {
|
||||
"window_title": "Administration",
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ const api = {
|
|||
statusId = false,
|
||||
bookmarkFolderId = false
|
||||
}) {
|
||||
if (timeline === 'favourites' && !store.rootState.instance.pleromaPublicFavouritesAvailable) return
|
||||
if (store.state.fetchers[timeline]) return
|
||||
|
||||
const fetcher = store.state.backendInteractor.startFetchingTimeline({
|
||||
|
|
@ -281,6 +282,7 @@ const api = {
|
|||
// Bookmark folders
|
||||
startFetchingBookmarkFolders (store) {
|
||||
if (store.state.fetchers.bookmarkFolders) return
|
||||
if (!store.rootState.instance.pleromaBookmarkFoldersAvailable) return
|
||||
const fetcher = store.state.backendInteractor.startFetchingBookmarkFolders({ store })
|
||||
store.commit('addFetcher', { fetcherName: 'bookmarkFolders', fetcher })
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
|
||||
const PASSWORD_STRATEGY = 'password'
|
||||
const TOKEN_STRATEGY = 'token'
|
||||
|
||||
// MFA strategies
|
||||
const TOTP_STRATEGY = 'totp'
|
||||
const RECOVERY_STRATEGY = 'recovery'
|
||||
|
||||
// initial state
|
||||
const state = {
|
||||
settings: {},
|
||||
strategy: PASSWORD_STRATEGY,
|
||||
initStrategy: PASSWORD_STRATEGY // default strategy from config
|
||||
}
|
||||
|
||||
const resetState = (state) => {
|
||||
state.strategy = state.initStrategy
|
||||
state.settings = {}
|
||||
}
|
||||
|
||||
// getters
|
||||
const getters = {
|
||||
settings: (state) => {
|
||||
return state.settings
|
||||
},
|
||||
requiredPassword: (state) => {
|
||||
return state.strategy === PASSWORD_STRATEGY
|
||||
},
|
||||
requiredToken: (state) => {
|
||||
return state.strategy === TOKEN_STRATEGY
|
||||
},
|
||||
requiredTOTP: (state) => {
|
||||
return state.strategy === TOTP_STRATEGY
|
||||
},
|
||||
requiredRecovery: (state) => {
|
||||
return state.strategy === RECOVERY_STRATEGY
|
||||
}
|
||||
}
|
||||
|
||||
// mutations
|
||||
const mutations = {
|
||||
setInitialStrategy (state, strategy) {
|
||||
if (strategy) {
|
||||
state.initStrategy = strategy
|
||||
state.strategy = strategy
|
||||
}
|
||||
},
|
||||
requirePassword (state) {
|
||||
state.strategy = PASSWORD_STRATEGY
|
||||
},
|
||||
requireToken (state) {
|
||||
state.strategy = TOKEN_STRATEGY
|
||||
},
|
||||
requireMFA (state, { settings }) {
|
||||
state.settings = settings
|
||||
state.strategy = TOTP_STRATEGY // default strategy of MFA
|
||||
},
|
||||
requireRecovery (state) {
|
||||
state.strategy = RECOVERY_STRATEGY
|
||||
},
|
||||
requireTOTP (state) {
|
||||
state.strategy = TOTP_STRATEGY
|
||||
},
|
||||
abortMFA (state) {
|
||||
resetState(state)
|
||||
}
|
||||
}
|
||||
|
||||
// actions
|
||||
const actions = {
|
||||
|
||||
async login ({ state, dispatch }, { access_token: accessToken }) {
|
||||
useOAuthStore().setToken(accessToken)
|
||||
await dispatch('loginUser', accessToken, { root: true })
|
||||
resetState(state)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ import api from './api.js'
|
|||
import config from './config.js'
|
||||
import profileConfig from './profileConfig.js'
|
||||
import adminSettings from './adminSettings.js'
|
||||
import authFlow from './auth_flow.js'
|
||||
import drafts from './drafts.js'
|
||||
import chats from './chats.js'
|
||||
|
||||
|
|
@ -19,7 +18,6 @@ export default {
|
|||
config,
|
||||
profileConfig,
|
||||
adminSettings,
|
||||
authFlow,
|
||||
drafts,
|
||||
chats
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ const defaultState = {
|
|||
emoji: {},
|
||||
emojiFetched: false,
|
||||
unicodeEmojiAnnotations: {},
|
||||
pleromaBackend: true,
|
||||
pleromaExtensionsAvailable: true,
|
||||
postFormats: [],
|
||||
restrictedNicknames: [],
|
||||
safeDM: true,
|
||||
|
|
@ -156,6 +156,8 @@ const defaultState = {
|
|||
pleromaChatMessagesAvailable: false,
|
||||
pleromaCustomEmojiReactionsAvailable: false,
|
||||
pleromaBookmarkFoldersAvailable: false,
|
||||
pleromaPublicFavouritesAvailable: true,
|
||||
statusNotificationTypeAvailable: true,
|
||||
gopherAvailable: false,
|
||||
mediaProxyAvailable: false,
|
||||
suggestionsEnabled: false,
|
||||
|
|
@ -163,6 +165,7 @@ const defaultState = {
|
|||
quotingAvailable: false,
|
||||
groupActorAvailable: false,
|
||||
blockExpiration: false,
|
||||
localBubbleInstances: [], // Akkoma
|
||||
|
||||
// Html stuff
|
||||
instanceSpecificPanelContent: '',
|
||||
|
|
@ -341,7 +344,10 @@ const instance = {
|
|||
|
||||
async getCustomEmoji ({ commit, state }) {
|
||||
try {
|
||||
const res = await window.fetch('/api/pleroma/emoji.json')
|
||||
let res = await window.fetch('/api/v1/pleroma/emoji')
|
||||
if (!res.ok) {
|
||||
res = await window.fetch('/api/pleroma/emoji.json')
|
||||
}
|
||||
if (res.ok) {
|
||||
const result = await res.json()
|
||||
const values = Array.isArray(result) ? Object.assign({}, ...result) : result
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export const defaultState = () => ({
|
|||
conversationsObject: {},
|
||||
maxId: 0,
|
||||
favorites: new Set(),
|
||||
pleromaScrobblesAvailable: true, // not reported in nodeinfo
|
||||
timelines: {
|
||||
mentions: emptyTl(),
|
||||
public: emptyTl(),
|
||||
|
|
@ -50,7 +51,8 @@ export const defaultState = () => ({
|
|||
tag: emptyTl(),
|
||||
dms: emptyTl(),
|
||||
bookmarks: emptyTl(),
|
||||
list: emptyTl()
|
||||
list: emptyTl(),
|
||||
bubble: emptyTl()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -108,12 +110,21 @@ const sortTimeline = (timeline) => {
|
|||
}
|
||||
|
||||
const getLatestScrobble = (state, user) => {
|
||||
const scrobblesSupport = state.pleromaScrobblesAvailable
|
||||
if (!scrobblesSupport) return
|
||||
|
||||
if (state.scrobblesNextFetch[user.id] && state.scrobblesNextFetch[user.id] > Date.now()) {
|
||||
return
|
||||
}
|
||||
|
||||
state.scrobblesNextFetch[user.id] = Date.now() + 24 * 60 * 60 * 1000
|
||||
if (!scrobblesSupport) return
|
||||
apiService.fetchScrobbles({ accountId: user.id }).then((scrobbles) => {
|
||||
if (scrobbles?.error) {
|
||||
state.pleromaScrobblesAvailable = false
|
||||
return
|
||||
}
|
||||
|
||||
if (scrobbles.length > 0) {
|
||||
user.latestScrobble = scrobbles[0]
|
||||
|
||||
|
|
|
|||
|
|
@ -607,6 +607,7 @@ const users = {
|
|||
return new Promise((resolve, reject) => {
|
||||
const commit = store.commit
|
||||
const dispatch = store.dispatch
|
||||
const rootState = store.rootState
|
||||
commit('beginLogin')
|
||||
store.rootState.api.backendInteractor.verifyCredentials(accessToken)
|
||||
.then((data) => {
|
||||
|
|
@ -673,8 +674,10 @@ const users = {
|
|||
// Start fetching notifications
|
||||
dispatch('startFetchingNotifications')
|
||||
|
||||
// Start fetching chats
|
||||
dispatch('startFetchingChats')
|
||||
if (rootState.instance.pleromaChatMessagesAvailable) {
|
||||
// Start fetching chats
|
||||
dispatch('startFetchingChats')
|
||||
}
|
||||
}
|
||||
|
||||
dispatch('startFetchingLists')
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const TAG_USER_URL = '/api/pleroma/admin/users/tag'
|
|||
const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
|
||||
const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate'
|
||||
const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
|
||||
const ADMIN_USERS_URL = '/api/pleroma/admin/users'
|
||||
const ADMIN_USERS_URL = '/api/v1/pleroma/admin/users'
|
||||
const SUGGESTIONS_URL = '/api/v1/suggestions'
|
||||
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
|
||||
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
|
||||
|
|
@ -61,6 +61,7 @@ const MASTODON_LIST_TIMELINE_URL = id => `/api/v1/timelines/list/${id}`
|
|||
const MASTODON_LIST_ACCOUNTS_URL = id => `/api/v1/lists/${id}/accounts`
|
||||
const MASTODON_TAG_TIMELINE_URL = tag => `/api/v1/timelines/tag/${tag}`
|
||||
const MASTODON_BOOKMARK_TIMELINE_URL = '/api/v1/bookmarks'
|
||||
const AKKOMA_BUBBLE_TIMELINE_URL = '/api/v1/timelines/bubble'
|
||||
const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/'
|
||||
const MASTODON_USER_MUTES_URL = '/api/v1/mutes/'
|
||||
const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
|
||||
|
|
@ -99,7 +100,7 @@ const PLEROMA_CHAT_URL = id => `/api/v1/pleroma/chats/by-account-id/${id}`
|
|||
const PLEROMA_CHAT_MESSAGES_URL = id => `/api/v1/pleroma/chats/${id}/messages`
|
||||
const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read`
|
||||
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
|
||||
const PLEROMA_ADMIN_REPORTS = '/api/pleroma/admin/reports'
|
||||
const PLEROMA_ADMIN_REPORTS = '/api/v1/pleroma/admin/reports'
|
||||
const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
|
||||
const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements'
|
||||
const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
|
||||
|
|
@ -111,10 +112,10 @@ const PLEROMA_USER_FAVORITES_TIMELINE_URL = id => `/api/v1/pleroma/accounts/${id
|
|||
const PLEROMA_BOOKMARK_FOLDERS_URL = '/api/v1/pleroma/bookmark_folders'
|
||||
const PLEROMA_BOOKMARK_FOLDER_URL = id => `/api/v1/pleroma/bookmark_folders/${id}`
|
||||
|
||||
const PLEROMA_ADMIN_CONFIG_URL = '/api/pleroma/admin/config'
|
||||
const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/pleroma/admin/config/descriptions'
|
||||
const PLEROMA_ADMIN_FRONTENDS_URL = '/api/pleroma/admin/frontends'
|
||||
const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = '/api/pleroma/admin/frontends/install'
|
||||
const PLEROMA_ADMIN_CONFIG_URL = '/api/v1/pleroma/admin/config'
|
||||
const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/v1/pleroma/admin/config/descriptions'
|
||||
const PLEROMA_ADMIN_FRONTENDS_URL = '/api/v1/pleroma/admin/frontends'
|
||||
const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = '/api/v1/pleroma/admin/frontends/install'
|
||||
|
||||
const PLEROMA_EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji'
|
||||
const PLEROMA_EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import'
|
||||
|
|
@ -714,7 +715,8 @@ const fetchTimeline = ({
|
|||
publicFavorites: PLEROMA_USER_FAVORITES_TIMELINE_URL,
|
||||
tag: MASTODON_TAG_TIMELINE_URL,
|
||||
bookmarks: MASTODON_BOOKMARK_TIMELINE_URL,
|
||||
quotes: PLEROMA_STATUS_QUOTES_URL
|
||||
quotes: PLEROMA_STATUS_QUOTES_URL,
|
||||
bubble: AKKOMA_BUBBLE_TIMELINE_URL
|
||||
}
|
||||
const isNotifications = timeline === 'notifications'
|
||||
const params = []
|
||||
|
|
@ -764,7 +766,7 @@ const fetchTimeline = ({
|
|||
if (replyVisibility !== 'all') {
|
||||
params.push(['reply_visibility', replyVisibility])
|
||||
}
|
||||
if (includeTypes.length > 0) {
|
||||
if (includeTypes.size > 0) {
|
||||
includeTypes.forEach(type => {
|
||||
params.push(['include_types[]', type])
|
||||
})
|
||||
|
|
|
|||
|
|
@ -95,6 +95,32 @@ export const getContrastRatioLayers = (text, layers, bedrock) => {
|
|||
return getContrastRatio(alphaBlendLayers(bedrock, layers), text)
|
||||
}
|
||||
|
||||
/**
|
||||
* Blending of two solid colors with a user-defined operator: origin +- value
|
||||
*
|
||||
* @param {Object} origin - base color
|
||||
* @param {Object} value - modification argument
|
||||
* @param {string} operator - math operator to use
|
||||
*/
|
||||
export const arithmeticBlend = (origin, value, operator) => {
|
||||
const func = (a, b) => {
|
||||
switch (operator) {
|
||||
case '+':
|
||||
return Math.min(a + b, 255)
|
||||
case '-':
|
||||
return Math.max(a - b, 0)
|
||||
default:
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
r: func(origin.r, value.r),
|
||||
g: func(origin.g, value.g),
|
||||
b: func(origin.b, value.b),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This performs alpha blending between solid background and semi-transparent foreground
|
||||
*
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ export const parseUser = (data) => {
|
|||
|
||||
output.bot = data.bot
|
||||
|
||||
output.privileges = []
|
||||
|
||||
if (data.pleroma) {
|
||||
if (data.pleroma.settings_store) {
|
||||
output.storage = data.pleroma.settings_store['pleroma-fe']
|
||||
|
|
@ -317,20 +319,18 @@ export const parseStatus = (data) => {
|
|||
|
||||
output.edited_at = data.edited_at
|
||||
|
||||
const { pleroma } = data
|
||||
|
||||
if (data.pleroma) {
|
||||
const { pleroma } = data
|
||||
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 = data.pleroma.in_reply_to_account_acct
|
||||
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.quote = pleroma.quote ? parseStatus(pleroma.quote) : undefined
|
||||
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.quote_visible = pleroma.quote_visible || true
|
||||
output.quotes_count = pleroma.quotes_count
|
||||
output.bookmark_folder_id = pleroma.bookmark_folder
|
||||
} else {
|
||||
|
|
@ -338,6 +338,12 @@ export const parseStatus = (data) => {
|
|||
output.summary = data.spoiler_text
|
||||
}
|
||||
|
||||
const quoteRaw = pleroma?.quote || data.quote
|
||||
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_url = data.quote?.url ?? quoteData?.url ?? pleroma.quote_url
|
||||
|
||||
output.in_reply_to_status_id = data.in_reply_to_id
|
||||
output.in_reply_to_user_id = data.in_reply_to_account_id
|
||||
output.replies_count = data.replies_count
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@ import { useInterfaceStore } from 'src/stores/interface.js'
|
|||
import apiService from '../api/api.service.js'
|
||||
import { promiseInterval } from '../promise_interval/promise_interval.js'
|
||||
|
||||
const update = ({ store, notifications, older }) => {
|
||||
store.dispatch('addNewNotifications', { notifications, older })
|
||||
}
|
||||
//
|
||||
// For using include_types when fetching notifications.
|
||||
// Note: chat_mention excluded as pleroma-fe polls them separately
|
||||
const mastoApiNotificationTypes = [
|
||||
const mastoApiNotificationTypes = new Set([
|
||||
'mention',
|
||||
'status',
|
||||
'favourite',
|
||||
|
|
@ -14,21 +18,22 @@ const mastoApiNotificationTypes = [
|
|||
'move',
|
||||
'poll',
|
||||
'pleroma:emoji_reaction',
|
||||
'pleroma:chat_mention',
|
||||
'pleroma:report'
|
||||
]
|
||||
|
||||
const update = ({ store, notifications, older }) => {
|
||||
store.dispatch('addNewNotifications', { notifications, older })
|
||||
}
|
||||
'pleroma:report',
|
||||
'test'
|
||||
])
|
||||
|
||||
const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
|
||||
|
||||
const args = { credentials }
|
||||
const { getters } = store
|
||||
const rootState = store.rootState || store.state
|
||||
const timelineData = rootState.notifications
|
||||
const hideMutedPosts = getters.mergedConfig.hideMutedPosts
|
||||
|
||||
if (rootState.instance.pleromaChatMessagesAvailable) {
|
||||
mastoApiNotificationTypes.add('pleroma:chat_mention')
|
||||
}
|
||||
|
||||
args.includeTypes = mastoApiNotificationTypes
|
||||
args.withMuted = !hideMutedPosts
|
||||
|
||||
|
|
@ -72,7 +77,17 @@ const fetchNotifications = ({ store, args, older }) => {
|
|||
return apiService.fetchTimeline(args)
|
||||
.then((response) => {
|
||||
if (response.errors) {
|
||||
throw new Error(`${response.status} ${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))
|
||||
return fetchNotifications({ store, args, older })
|
||||
} else {
|
||||
throw new Error(`${response.status} ${response.statusText}`)
|
||||
}
|
||||
}
|
||||
const notifications = response.data
|
||||
update({ store, notifications, older })
|
||||
|
|
|
|||
|
|
@ -1,47 +1,76 @@
|
|||
import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js'
|
||||
import { getCssRules } from '../theme_data/css_utils.js'
|
||||
import { defaultState } from 'src/modules/default_config_state.js'
|
||||
import { chunk } from 'lodash'
|
||||
import { chunk, throttle } from 'lodash'
|
||||
import localforage from 'localforage'
|
||||
|
||||
// On platforms where this is not supported, it will return undefined
|
||||
// Otherwise it will return an array
|
||||
const supportsAdoptedStyleSheets = !!document.adoptedStyleSheets
|
||||
|
||||
const createStyleSheet = (id) => {
|
||||
if (supportsAdoptedStyleSheets) {
|
||||
return {
|
||||
el: null,
|
||||
sheet: new CSSStyleSheet(),
|
||||
rules: []
|
||||
const stylesheets = {}
|
||||
|
||||
export const createStyleSheet = (id, priority = 1000) => {
|
||||
if (stylesheets[id]) return stylesheets[id]
|
||||
const newStyleSheet = {
|
||||
rules: [],
|
||||
ready: false,
|
||||
priority,
|
||||
clear () {
|
||||
this.rules = []
|
||||
},
|
||||
addRule (rule) {
|
||||
let newRule = rule
|
||||
if (!CSS.supports?.('backdrop-filter', 'blur()')) {
|
||||
newRule = newRule.replace(/backdrop-filter:[^;]+;/g, '') // Remove backdrop-filter
|
||||
}
|
||||
this.rules.push(
|
||||
newRule
|
||||
.replace(/var\(--shadowFilter\)[^;]*;/g, '') // Remove shadowFilter references
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const el = document.getElementById(id)
|
||||
// Clear all rules in it
|
||||
for (let i = el.sheet.cssRules.length - 1; i >= 0; --i) {
|
||||
el.sheet.deleteRule(i)
|
||||
}
|
||||
|
||||
return {
|
||||
el,
|
||||
sheet: el.sheet,
|
||||
rules: []
|
||||
}
|
||||
stylesheets[id] = newStyleSheet
|
||||
return newStyleSheet
|
||||
}
|
||||
|
||||
const EAGER_STYLE_ID = 'pleroma-eager-styles'
|
||||
const LAZY_STYLE_ID = 'pleroma-lazy-styles'
|
||||
|
||||
const adoptStyleSheets = (styles) => {
|
||||
export const adoptStyleSheets = throttle(() => {
|
||||
if (supportsAdoptedStyleSheets) {
|
||||
document.adoptedStyleSheets = styles.map(s => s.sheet)
|
||||
document.adoptedStyleSheets = Object
|
||||
.values(stylesheets)
|
||||
.filter(x => x.ready)
|
||||
.sort((a, b) => a.priority - b.priority)
|
||||
.map(sheet => {
|
||||
const css = new CSSStyleSheet()
|
||||
sheet.rules.forEach(r => css.insertRule(r))
|
||||
return css
|
||||
})
|
||||
} else {
|
||||
const holder = document.getElementById('custom-styles-holder')
|
||||
|
||||
for (let i = holder.sheet.cssRules.length - 1; i >= 0; --i) {
|
||||
holder.sheet.deleteRule(i)
|
||||
}
|
||||
|
||||
Object
|
||||
.values(stylesheets)
|
||||
.filter(x => x.ready)
|
||||
.sort((a, b) => a.priority - b.priority)
|
||||
.forEach(sheet => {
|
||||
sheet.rules.forEach(r => holder.sheet.insertRule(r))
|
||||
})
|
||||
}
|
||||
// Some older browsers do not support document.adoptedStyleSheets.
|
||||
// In this case, we use the <style> elements.
|
||||
// Since the <style> elements we need are already in the DOM, there
|
||||
// is nothing to do here.
|
||||
}
|
||||
}, 500)
|
||||
|
||||
|
||||
const EAGER_STYLE_ID = 'pleroma-eager-styles'
|
||||
const LAZY_STYLE_ID = 'pleroma-lazy-styles'
|
||||
|
||||
export const generateTheme = (inputRuleset, callbacks, debug) => {
|
||||
const {
|
||||
|
|
@ -94,13 +123,17 @@ export const tryLoadCache = async () => {
|
|||
if (!cache) return null
|
||||
try {
|
||||
if (cache.engineChecksum === getEngineChecksum()) {
|
||||
const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
|
||||
const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
|
||||
const eagerStyles = createStyleSheet(EAGER_STYLE_ID, 10)
|
||||
const lazyStyles = createStyleSheet(LAZY_STYLE_ID, 20)
|
||||
|
||||
cache.data[0].forEach(rule => eagerStyles.sheet.insertRule(rule, 'index-max'))
|
||||
cache.data[1].forEach(rule => lazyStyles.sheet.insertRule(rule, 'index-max'))
|
||||
cache.data[0].forEach(rule => eagerStyles.addRule(rule))
|
||||
cache.data[1].forEach(rule => lazyStyles.addRule(rule))
|
||||
|
||||
adoptStyleSheets([eagerStyles, lazyStyles])
|
||||
eagerStyles.ready = true
|
||||
lazyStyles.ready = true
|
||||
|
||||
// Don't do this, we need to wait until config adopts its styles first
|
||||
//adoptStyleSheets()
|
||||
|
||||
console.info(`Loaded theme from cache`)
|
||||
return true
|
||||
|
|
@ -120,57 +153,29 @@ export const applyTheme = (
|
|||
onFinish = () => {},
|
||||
debug
|
||||
) => {
|
||||
const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
|
||||
const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
|
||||
const eagerStyles = createStyleSheet(EAGER_STYLE_ID, 10)
|
||||
const lazyStyles = createStyleSheet(LAZY_STYLE_ID, 20)
|
||||
|
||||
const insertRule = (styles, rule) => {
|
||||
try {
|
||||
// Try to use modern syntax first
|
||||
try {
|
||||
styles.sheet.insertRule(rule, 'index-max')
|
||||
styles.rules.push(rule)
|
||||
} catch {
|
||||
// Fallback for older browsers that don't support 'index-max'
|
||||
styles.sheet.insertRule(rule, styles.sheet.cssRules.length)
|
||||
styles.rules.push(rule)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Can\'t insert rule due to lack of support', e, rule)
|
||||
|
||||
// Try to sanitize the rule for better compatibility
|
||||
try {
|
||||
// Remove any potentially problematic CSS features
|
||||
let sanitizedRule = rule
|
||||
.replace(/backdrop-filter:[^;]+;/g, '') // Remove backdrop-filter
|
||||
.replace(/var\(--shadowFilter\)[^;]*;/g, '') // Remove shadowFilter references
|
||||
|
||||
if (sanitizedRule !== rule) {
|
||||
styles.sheet.insertRule(sanitizedRule, styles.sheet.cssRules.length)
|
||||
styles.rules.push(sanitizedRule)
|
||||
}
|
||||
} catch (e2) {
|
||||
console.error('Failed to insert even sanitized rule', e2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { lazyProcessFunc } = generateTheme(
|
||||
input,
|
||||
{
|
||||
onNewRule (rule, isLazy) {
|
||||
if (isLazy) {
|
||||
insertRule(lazyStyles, rule)
|
||||
lazyStyles.addRule(rule)
|
||||
} else {
|
||||
insertRule(eagerStyles, rule)
|
||||
eagerStyles.addRule(rule)
|
||||
}
|
||||
},
|
||||
onEagerFinished () {
|
||||
adoptStyleSheets([eagerStyles])
|
||||
eagerStyles.ready = true
|
||||
adoptStyleSheets()
|
||||
onEagerFinish()
|
||||
console.info('Eager part of theme finished, waiting for lazy part to finish to store cache')
|
||||
},
|
||||
onLazyFinished () {
|
||||
adoptStyleSheets([eagerStyles, lazyStyles])
|
||||
lazyStyles.ready = true
|
||||
adoptStyleSheets()
|
||||
const cache = { engineChecksum: getEngineChecksum(), data: [eagerStyles.rules, lazyStyles.rules] }
|
||||
onFinish(cache)
|
||||
localforage.setItem('pleromafe-theme-cache', cache)
|
||||
|
|
@ -234,28 +239,23 @@ export const applyConfig = (input) => {
|
|||
return
|
||||
}
|
||||
|
||||
const head = document.head
|
||||
|
||||
const rules = Object
|
||||
.entries(config)
|
||||
.filter(([, v]) => v)
|
||||
.map(([k, v]) => `--${k}: ${v}`).join(';')
|
||||
|
||||
document.getElementById('style-config')?.remove()
|
||||
const styleEl = document.createElement('style')
|
||||
styleEl.id = 'style-config'
|
||||
head.appendChild(styleEl)
|
||||
const styleSheet = styleEl.sheet
|
||||
const styleSheet = createStyleSheet('theme-holder', 30)
|
||||
|
||||
styleSheet.toString()
|
||||
styleSheet.insertRule(`:root { ${rules} }`, 'index-max')
|
||||
styleSheet.addRule(`:root { ${rules} }`)
|
||||
|
||||
// TODO find a way to make this not apply to theme previews
|
||||
if (Object.prototype.hasOwnProperty.call(config, 'forcedRoundness')) {
|
||||
styleSheet.insertRule(` *:not(.preview-block) {
|
||||
styleSheet.addRule(` *:not(.preview-block) {
|
||||
--roundness: var(--forcedRoundness) !important;
|
||||
}`, 'index-max')
|
||||
}`)
|
||||
}
|
||||
styleSheet.ready = true
|
||||
adoptStyleSheets()
|
||||
}
|
||||
|
||||
export const getResourcesIndex = async (url, parser = JSON.parse) => {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ function isPushSupported () {
|
|||
}
|
||||
|
||||
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))
|
||||
|
|
@ -146,3 +147,11 @@ export function unregisterPushNotifications (token) {
|
|||
]).catch((e) => console.warn(`Failed to disable Web Push Notifications: ${e.message}`))
|
||||
}
|
||||
}
|
||||
|
||||
export const shouldCache = process.env.NODE_ENV === 'production'
|
||||
export const cacheKey = 'pleroma-fe'
|
||||
export const emojiCacheKey = 'pleroma-fe-emoji'
|
||||
|
||||
export const clearCache = (key) => caches.delete(key)
|
||||
|
||||
export { getOrCreateServiceWorker }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { convert, brightness } from 'chromatism'
|
||||
import { alphaBlend, 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 { 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]
|
||||
|
|
@ -81,6 +81,23 @@ export const colorFunctions = {
|
|||
return alphaBlend(background, amount, foreground)
|
||||
}
|
||||
},
|
||||
shift: {
|
||||
argsNeeded: 2,
|
||||
documentation: 'Arithmetic blend between two colors',
|
||||
args: [
|
||||
'origin: base color',
|
||||
'value: shift value',
|
||||
'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
|
||||
|
||||
return arithmeticBlend(origin, value, operatorArg)
|
||||
}
|
||||
},
|
||||
boost: {
|
||||
argsNeeded: 2,
|
||||
documentation: 'If given color is dark makes it darker, if color is light - makes it lighter',
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const fetchAndUpdate = ({
|
|||
args.bookmarkFolderId = bookmarkFolderId
|
||||
args.tag = tag
|
||||
args.withMuted = !hideMutedPosts
|
||||
if (loggedIn && ['friends', 'public', 'publicAndExternal'].includes(timeline)) {
|
||||
if (loggedIn && ['friends', 'public', 'publicAndExternal', 'bubble'].includes(timeline)) {
|
||||
args.replyVisibility = replyVisibility
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +63,10 @@ const fetchAndUpdate = ({
|
|||
return apiService.fetchTimeline(args)
|
||||
.then(response => {
|
||||
if (response.errors) {
|
||||
if (timeline === 'favorites') {
|
||||
rootState.instance.pleromaPublicFavouritesAvailable = false
|
||||
return
|
||||
}
|
||||
throw new Error(`${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
|
|
|
|||
69
src/stores/auth_flow.js
Normal file
69
src/stores/auth_flow.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
const PASSWORD_STRATEGY = 'password'
|
||||
const TOKEN_STRATEGY = 'token'
|
||||
|
||||
// MFA strategies
|
||||
const TOTP_STRATEGY = 'totp'
|
||||
const RECOVERY_STRATEGY = 'recovery'
|
||||
|
||||
export const useAuthFlowStore = defineStore('authFlow', {
|
||||
// initial state
|
||||
state: () => ({
|
||||
settings: {},
|
||||
strategy: PASSWORD_STRATEGY,
|
||||
initStrategy: PASSWORD_STRATEGY // default strategy from config
|
||||
}),
|
||||
// getters
|
||||
getters: {
|
||||
requiredPassword: (state) => {
|
||||
return state.strategy === PASSWORD_STRATEGY
|
||||
},
|
||||
requiredToken: (state) => {
|
||||
return state.strategy === TOKEN_STRATEGY
|
||||
},
|
||||
requiredTOTP: (state) => {
|
||||
return state.strategy === TOTP_STRATEGY
|
||||
},
|
||||
requiredRecovery: (state) => {
|
||||
return state.strategy === RECOVERY_STRATEGY
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setInitialStrategy (strategy) {
|
||||
if (strategy) {
|
||||
this.initStrategy = strategy
|
||||
this.strategy = strategy
|
||||
}
|
||||
},
|
||||
requirePassword () {
|
||||
this.strategy = PASSWORD_STRATEGY
|
||||
},
|
||||
requireToken () {
|
||||
this.strategy = TOKEN_STRATEGY
|
||||
},
|
||||
requireMFA ({ settings }) {
|
||||
this.settings = settings
|
||||
this.strategy = TOTP_STRATEGY // default strategy of MFA
|
||||
},
|
||||
requireRecovery () {
|
||||
this.strategy = RECOVERY_STRATEGY
|
||||
},
|
||||
requireTOTP () {
|
||||
this.strategy = TOTP_STRATEGY
|
||||
},
|
||||
abortMFA () {
|
||||
this.resetState()
|
||||
},
|
||||
resetState () {
|
||||
this.strategy = this.initStrategy
|
||||
this.settings = {}
|
||||
},
|
||||
async login ({ access_token: accessToken }) {
|
||||
useOAuthStore().setToken(accessToken)
|
||||
await window.vuex.dispatch('loginUser', accessToken, { root: true })
|
||||
this.resetState()
|
||||
}
|
||||
}
|
||||
})
|
||||
109
src/sw.js
109
src/sw.js
|
|
@ -1,8 +1,10 @@
|
|||
/* eslint-env serviceworker */
|
||||
|
||||
import 'virtual:pleroma-fe/service_worker_env'
|
||||
import { storage } from 'src/lib/storage.js'
|
||||
import { parseNotification } from './services/entity_normalizer/entity_normalizer.service.js'
|
||||
import { prepareNotificationObject } from './services/notification_utils/notification_utils.js'
|
||||
import { shouldCache, cacheKey, emojiCacheKey } from './services/sw/sw.js'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
// Collects all messages for service workers
|
||||
// Needed because service workers cannot use dynamic imports
|
||||
|
|
@ -85,6 +87,80 @@ const showPushNotification = async (event) => {
|
|||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const cacheFiles = self.serviceWorkerOption.assets
|
||||
const isEmoji = req => {
|
||||
if (req.method !== 'GET') {
|
||||
return false
|
||||
}
|
||||
const url = new URL(req.url)
|
||||
|
||||
return url.pathname.startsWith('/emoji/')
|
||||
}
|
||||
const isNotMedia = req => {
|
||||
if (req.method !== 'GET') {
|
||||
return false
|
||||
}
|
||||
const url = new URL(req.url)
|
||||
return !url.pathname.startsWith('/media/')
|
||||
}
|
||||
const isAsset = req => {
|
||||
const url = new URL(req.url)
|
||||
return cacheFiles.includes(url.pathname)
|
||||
}
|
||||
|
||||
const isSuccessful = (resp) => {
|
||||
if (!resp.ok) {
|
||||
return false
|
||||
}
|
||||
if ((new URL(resp.url)).pathname === '/index.html') {
|
||||
// For index.html itself, there is no fallback possible.
|
||||
return true
|
||||
}
|
||||
const type = resp.headers.get('Content-Type')
|
||||
// Backend will revert to index.html if the file does not exist, so text/html for emojis and assets is a failure
|
||||
return type && !type.includes('text/html')
|
||||
}
|
||||
|
||||
self.addEventListener('install', async (event) => {
|
||||
if (shouldCache) {
|
||||
event.waitUntil((async () => {
|
||||
// Do not preload i18n and emoji annotations to speed up loading
|
||||
const shouldPreload = (route) => {
|
||||
return !route.startsWith('/static/js/i18n/') && !route.startsWith('/static/js/emoji-annotations/')
|
||||
}
|
||||
const cache = await caches.open(cacheKey)
|
||||
await Promise.allSettled(cacheFiles.filter(shouldPreload).map(async (route) => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Cache/add
|
||||
// originally we used addAll() but it will raise a problem in one edge case:
|
||||
// when the file for the route is not found, backend will return index.html with code 200
|
||||
// but it's wrong, and it's cached, so we end up with a bad cache.
|
||||
// this can happen when you refresh when you are in the process of upgrading
|
||||
// the frontend.
|
||||
const resp = await fetch(route)
|
||||
if (isSuccessful(resp)) {
|
||||
await cache.put(route, resp)
|
||||
}
|
||||
}))
|
||||
})())
|
||||
}
|
||||
})
|
||||
|
||||
self.addEventListener('activate', async (event) => {
|
||||
if (shouldCache) {
|
||||
event.waitUntil((async () => {
|
||||
const cache = await caches.open(cacheKey)
|
||||
const keys = await cache.keys()
|
||||
await Promise.all(
|
||||
keys.filter(request => {
|
||||
const url = new URL(request.url)
|
||||
const shouldKeep = cacheFiles.includes(url.pathname)
|
||||
return !shouldKeep
|
||||
}).map(k => cache.delete(k))
|
||||
)
|
||||
})())
|
||||
}
|
||||
})
|
||||
|
||||
self.addEventListener('push', async (event) => {
|
||||
if (event.data) {
|
||||
// Supposedly, we HAVE to return a promise inside waitUntil otherwise it will
|
||||
|
|
@ -143,4 +219,35 @@ self.addEventListener('notificationclick', (event) => {
|
|||
}))
|
||||
})
|
||||
|
||||
console.log('sw here')
|
||||
self.addEventListener('fetch', (event) => {
|
||||
// Do not mess up with remote things
|
||||
const isSameOrigin = (new URL(event.request.url)).origin === self.location.origin
|
||||
if (shouldCache && event.request.method === 'GET' && isSameOrigin && isNotMedia(event.request)) {
|
||||
// this is a bit spammy
|
||||
// console.debug('[Service worker] fetch:', event.request.url)
|
||||
event.respondWith((async () => {
|
||||
const r = await caches.match(event.request)
|
||||
const isEmojiReq = isEmoji(event.request)
|
||||
|
||||
if (r && isSuccessful(r)) {
|
||||
console.debug('[Service worker] already cached:', event.request.url)
|
||||
return r
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(event.request)
|
||||
if (response.ok &&
|
||||
isSuccessful(response) &&
|
||||
(isEmojiReq || isAsset(event.request))) {
|
||||
console.debug(`[Service worker] caching ${isEmojiReq ? 'emoji' : 'asset'}:`, event.request.url)
|
||||
const cache = await caches.open(isEmojiReq ? emojiCacheKey : cacheKey)
|
||||
await cache.put(event.request.clone(), response.clone())
|
||||
}
|
||||
return response
|
||||
} catch (e) {
|
||||
console.error('[Service worker] error when caching emoji:', e)
|
||||
throw e
|
||||
}
|
||||
})())
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue