diff --git a/build/sw_plugin.js b/build/sw_plugin.js
index 90ab856ad..a2c792b7d 100644
--- a/build/sw_plugin.js
+++ b/build/sw_plugin.js
@@ -11,6 +11,11 @@ const getSWMessagesAsText = async () => {
}
const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)))
+const swEnvName = 'virtual:pleroma-fe/service_worker_env'
+const swEnvNameResolved = '\0' + swEnvName
+const getDevSwEnv = () => `self.serviceWorkerOption = { assets: [] };`
+const getProdSwEnv = ({ assets }) => `self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };`
+
export const devSwPlugin = ({
swSrc,
swDest,
@@ -32,12 +37,16 @@ export const devSwPlugin = ({
const name = id.startsWith('/') ? id.slice(1) : id
if (name === swDest) {
return swFullSrc
+ } else if (name === swEnvName) {
+ return swEnvNameResolved
}
return null
},
async load (id) {
if (id === swFullSrc) {
return readFile(swFullSrc, 'utf-8')
+ } else if (id === swEnvNameResolved) {
+ return getDevSwEnv()
}
return null
},
@@ -79,6 +88,21 @@ export const devSwPlugin = ({
contents: await getSWMessagesAsText()
}))
}
+ }, {
+ name: 'sw-env',
+ setup (b) {
+ b.onResolve(
+ { filter: new RegExp('^' + swEnvName + '$') },
+ args => ({
+ path: args.path,
+ namespace: 'sw-env'
+ }))
+ b.onLoad(
+ { filter: /.*/, namespace: 'sw-env' },
+ () => ({
+ contents: getDevSwEnv()
+ }))
+ }
}]
})
const text = res.outputFiles[0].text
@@ -126,6 +150,30 @@ export const buildSwPlugin = ({
configFile: false
}
},
+ generateBundle: {
+ order: 'post',
+ sequential: true,
+ async handler (_, bundle) {
+ const assets = Object.keys(bundle)
+ .filter(name => !/\.map$/.test(name))
+ .map(name => '/' + name)
+ config.plugins.push({
+ name: 'build-sw-env-plugin',
+ resolveId (id) {
+ if (id === swEnvName) {
+ return swEnvNameResolved
+ }
+ return null
+ },
+ load (id) {
+ if (id === swEnvNameResolved) {
+ return getProdSwEnv({ assets })
+ }
+ return null
+ }
+ })
+ }
+ },
closeBundle: {
order: 'post',
sequential: true,
diff --git a/changelog.d/action-button-extra-counter.add b/changelog.d/action-button-extra-counter.add
new file mode 100644
index 000000000..7d5c77447
--- /dev/null
+++ b/changelog.d/action-button-extra-counter.add
@@ -0,0 +1 @@
+Display counter for status action buttons when they are on the menu
diff --git a/changelog.d/akkoftermapth.skip b/changelog.d/akkoftermapth.skip
new file mode 100644
index 000000000..e69de29bb
diff --git a/changelog.d/akkoma-sharkey-net-support.add b/changelog.d/akkoma-sharkey-net-support.add
new file mode 100644
index 000000000..4b4bff7fe
--- /dev/null
+++ b/changelog.d/akkoma-sharkey-net-support.add
@@ -0,0 +1 @@
+Added support for Akkoma and IceShrimp.NET backend
diff --git a/changelog.d/akkoma.skip b/changelog.d/akkoma.skip
new file mode 100644
index 000000000..e69de29bb
diff --git a/changelog.d/arithmetic-blend.add b/changelog.d/arithmetic-blend.add
new file mode 100644
index 000000000..c579dca28
--- /dev/null
+++ b/changelog.d/arithmetic-blend.add
@@ -0,0 +1,2 @@
+Add arithmetic blend ISS function
+
diff --git a/changelog.d/bookmark-button-align.fix b/changelog.d/bookmark-button-align.fix
new file mode 100644
index 000000000..64bc2c807
--- /dev/null
+++ b/changelog.d/bookmark-button-align.fix
@@ -0,0 +1 @@
+Fix bookmark button alignment in the extra actions menu
diff --git a/changelog.d/csp.add b/changelog.d/csp.add
new file mode 100644
index 000000000..260337b97
--- /dev/null
+++ b/changelog.d/csp.add
@@ -0,0 +1 @@
+Compatibility with stricter CSP (Akkoma backend)
diff --git a/changelog.d/migrate-auth-flow-pinia.skip b/changelog.d/migrate-auth-flow-pinia.skip
new file mode 100644
index 000000000..e69de29bb
diff --git a/changelog.d/small-fixes.skip b/changelog.d/small-fixes.skip
new file mode 100644
index 000000000..e69de29bb
diff --git a/changelog.d/sw-cache-assets.add b/changelog.d/sw-cache-assets.add
new file mode 100644
index 000000000..5f7414eee
--- /dev/null
+++ b/changelog.d/sw-cache-assets.add
@@ -0,0 +1 @@
+Cache assets and emojis with service worker
diff --git a/changelog.d/unify-show-hide-buttons.add b/changelog.d/unify-show-hide-buttons.add
new file mode 100644
index 000000000..663bc38a5
--- /dev/null
+++ b/changelog.d/unify-show-hide-buttons.add
@@ -0,0 +1 @@
+Unify show/hide content buttons
diff --git a/changelog.d/zoomlag.skip b/changelog.d/zoomlag.skip
new file mode 100644
index 000000000..e69de29bb
diff --git a/index.html b/index.html
index a2f928361..fb92252c5 100644
--- a/index.html
+++ b/index.html
@@ -5,138 +5,16 @@
+
-
-
-
+
+
-
+
diff --git a/package.json b/package.json
index 2f9896d18..eea94a10e 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,7 @@
"@fortawesome/free-regular-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/vue-fontawesome": "3.0.8",
- "@kazvmoe-infra/pinch-zoom-element": "1.2.0",
+ "@kazvmoe-infra/pinch-zoom-element": "1.3.0",
"@kazvmoe-infra/unicode-emoji-json": "0.4.0",
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.1.13",
"@vuelidate/core": "2.0.3",
@@ -47,7 +47,7 @@
"url": "0.11.4",
"utf8": "3.0.0",
"uuid": "11.1.0",
- "vue": "3.5.13",
+ "vue": "3.5.17",
"vue-i18n": "11",
"vue-router": "4.5.1",
"vue-virtual-scroller": "^2.0.0-beta.7",
@@ -66,7 +66,7 @@
"@vitest/ui": "^3.0.7",
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
"@vue/babel-plugin-jsx": "1.4.0",
- "@vue/compiler-sfc": "3.5.13",
+ "@vue/compiler-sfc": "3.5.17",
"@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.21",
"babel-plugin-lodash": "3.3.4",
@@ -90,17 +90,17 @@
"http-proxy-middleware": "3.0.5",
"iso-639-1": "3.1.5",
"lodash": "4.17.21",
- "msw": "2.7.6",
+ "msw": "2.10.2",
"nightwatch": "3.12.1",
"playwright": "1.52.0",
"postcss": "8.5.3",
"postcss-html": "^1.5.0",
"postcss-scss": "^4.0.6",
- "sass": "1.87.0",
+ "sass": "1.89.2",
"selenium-server": "3.141.59",
- "semver": "7.7.1",
+ "semver": "7.7.2",
"serve-static": "2.2.0",
- "shelljs": "0.9.2",
+ "shelljs": "0.10.0",
"sinon": "20.0.0",
"sinon-chai": "4.0.0",
"stylelint": "16.19.1",
diff --git a/public/static/splash.css b/public/static/splash.css
new file mode 100644
index 000000000..abdc19fc2
--- /dev/null
+++ b/public/static/splash.css
@@ -0,0 +1,126 @@
+body {
+ margin: 0;
+ padding: 0;
+}
+
+#splash {
+ --scale: 1;
+ width: 100vw;
+ height: 100vh;
+ display: grid;
+ grid-template-rows: auto;
+ grid-template-columns: auto;
+ align-content: center;
+ align-items: center;
+ justify-content: center;
+ justify-items: center;
+ flex-direction: column;
+ background: #0f161e;
+ font-family: sans-serif;
+ color: #b9b9ba;
+ position: absolute;
+ z-index: 9999;
+ font-size: calc(1vw + 1vh + 1vmin);
+}
+
+#splash-credit {
+ position: absolute;
+ font-size: 14px;
+ bottom: 16px;
+ right: 16px;
+}
+
+#splash-container {
+ align-items: center;
+}
+
+#mascot-container {
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ perspective: 60em;
+ perspective-origin: 0 -15em;
+ transform-style: preserve-3d;
+}
+
+#mascot {
+ width: calc(10em * var(--scale));
+ height: calc(10em * var(--scale));
+ object-fit: contain;
+ object-position: bottom;
+ transform: translateZ(-2em);
+}
+
+#throbber {
+ display: grid;
+ width: calc(5em * 0.5 * var(--scale));
+ height: calc(8em * 0.5 * var(--scale));
+ margin-left: 4.1em;
+ z-index: 2;
+ grid-template-rows: repeat(8, 1fr);
+ grid-template-columns: repeat(5, 1fr);
+ grid-template-areas: "P P . L L"
+ "P P . L L"
+ "P P . L L"
+ "P P . L L"
+ "P P . . ."
+ "P P . . ."
+ "P P . E E"
+ "P P . E E";
+
+ --logoChunkSize: calc(2em * 0.5 * var(--scale))
+}
+
+.chunk {
+ background-color: #e2b188;
+ box-shadow: 0.01em 0.01em 0.1em 0 #e2b188;
+}
+
+#chunk-P {
+ grid-area: P;
+ border-top-left-radius: calc(var(--logoChunkSize) / 2);
+}
+
+#chunk-L {
+ grid-area: L;
+ border-bottom-right-radius: calc(var(--logoChunkSize) / 2);
+}
+
+#chunk-E {
+ grid-area: E;
+ border-bottom-right-radius: calc(var(--logoChunkSize) / 2);
+}
+
+#status {
+ margin-top: 1em;
+ line-height: 2;
+ width: 100%;
+ text-align: center;
+}
+
+#statusError {
+ display: none;
+ margin-top: 1em;
+ font-size: calc(1vw + 1vh + 1vmin);
+ line-height: 2;
+ width: 100%;
+ text-align: center;
+}
+
+#statusStack {
+ display: none;
+ margin-top: 1em;
+ font-size: calc((1vw + 1vh + 1vmin) / 2.5);
+ width: calc(100vw - 5em);
+ padding: 1em;
+ text-overflow: ellipsis;
+ overflow-x: hidden;
+ text-align: left;
+ line-height: 2;
+}
+
+@media (prefers-reduced-motion) {
+ #throbber {
+ animation: none !important;
+ }
+}
diff --git a/src/App.js b/src/App.js
index 013ea323c..a251682dc 100644
--- a/src/App.js
+++ b/src/App.js
@@ -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)
diff --git a/src/App.scss b/src/App.scss
index 24afac8ab..d56306c9e 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -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;
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 28ff1e530..3ef92fd11 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -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 })
diff --git a/src/boot/routes.js b/src/boot/routes.js
index da87c6c61..02abf8ce6 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -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 },
{
diff --git a/src/components/auth_form/auth_form.js b/src/components/auth_form/auth_form.js
index a86a3dca2..243cbf574 100644
--- a/src/components/auth_form/auth_form.js
+++ b/src/components/auth_form/auth_form.js
@@ -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,
diff --git a/src/components/bubble_timeline/bubble_timeline.js b/src/components/bubble_timeline/bubble_timeline.js
new file mode 100644
index 000000000..6f73dd2b8
--- /dev/null
+++ b/src/components/bubble_timeline/bubble_timeline.js
@@ -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
diff --git a/src/components/bubble_timeline/bubble_timeline.vue b/src/components/bubble_timeline/bubble_timeline.vue
new file mode 100644
index 000000000..4aefa2729
--- /dev/null
+++ b/src/components/bubble_timeline/bubble_timeline.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue
index 26b67cfe8..bcdf435fc 100644
--- a/src/components/color_input/color_input.vue
+++ b/src/components/color_input/color_input.vue
@@ -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)"
>
diff --git a/src/components/component_preview/component_preview.js b/src/components/component_preview/component_preview.js
new file mode 100644
index 000000000..9f830cd72
--- /dev/null
+++ b/src/components/component_preview/component_preview.js
@@ -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()
+ }
+ }
+}
diff --git a/src/components/component_preview/component_preview.scss b/src/components/component_preview/component_preview.scss
new file mode 100644
index 000000000..bb83b7908
--- /dev/null
+++ b/src/components/component_preview/component_preview.scss
@@ -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);
+ }
+ }
+}
diff --git a/src/components/component_preview/component_preview.vue b/src/components/component_preview/component_preview.vue
index be9d25ac5..0d81128c7 100644
--- a/src/components/component_preview/component_preview.vue
+++ b/src/components/component_preview/component_preview.vue
@@ -1,14 +1,9 @@
-
-
-
@@ -116,203 +110,5 @@
-
-
+
+
diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index ed3f5dfc6..9566aa903 100644
--- a/src/components/login_form/login_form.js
+++ b/src/components/login_form/login_form.js
@@ -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()
},
diff --git a/src/components/mfa_form/recovery_form.js b/src/components/mfa_form/recovery_form.js
index 2d0f4fdff..84479b1ec 100644
--- a/src/components/mfa_form/recovery_form.js
+++ b/src/components/mfa_form/recovery_form.js
@@ -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 () {
diff --git a/src/components/mfa_form/totp_form.js b/src/components/mfa_form/totp_form.js
index 857d055ff..e369d8a5d 100644
--- a/src/components/mfa_form/totp_form.js
+++ b/src/components/mfa_form/totp_form.js
@@ -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 () {
diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js
index 681aaf05b..a155abe0c 100644
--- a/src/components/nav_panel/nav_panel.js
+++ b/src/components/nav_panel/nav_panel.js
@@ -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
}
)
diff --git a/src/components/navigation/filter.js b/src/components/navigation/filter.js
index 12ab9266e..54abb67b4 100644
--- a/src/components/navigation/filter.js
+++ b/src/components/navigation/filter.js
@@ -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]
-}))
+})) : []
diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js
index a8e1fc966..d1c2b6763 100644
--- a/src/components/navigation/navigation.js
+++ b/src/components/navigation/navigation.js
@@ -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',
diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js
index 50acbbaf1..f9a900fc6 100644
--- a/src/components/navigation/navigation_pins.js
+++ b/src/components/navigation/navigation_pins.js
@@ -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
diff --git a/src/components/popover/popover.scss b/src/components/popover/popover.scss
index 828b81cd1..a166e2196 100644
--- a/src/components/popover/popover.scss
+++ b/src/components/popover/popover.scss
@@ -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 {
diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js
index fd18b91e5..838ac6d6c 100644
--- a/src/components/settings_modal/tabs/appearance_tab.js
+++ b/src/components/settings_modal/tabs/appearance_tab.js
@@ -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()
}
}
}
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index d49a28651..cbbb8ff9c 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -16,14 +16,6 @@
:disabled="switchInProgress"
@click="resetTheming"
>
-
-
-