MASSIVE refactor, replacing instance module with store, separating emoji stuff into its own store, making sure everything refers to new stores (WIP)

This commit is contained in:
Henry Jameson 2026-01-22 17:16:51 +02:00
commit 5bdf341560
95 changed files with 801 additions and 833 deletions

243
src/stores/emoji.js Normal file
View file

@ -0,0 +1,243 @@
import { defineStore } from 'pinia'
import { ensureFinalFallback } from 'src/i18n/languages.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { annotationsLoader } from 'virtual:pleroma-fe/emoji-annotations'
const defaultState = {
// Custom emoji from server
customEmoji: [],
customEmojiFetched: false,
// Unicode emoji from bundle
emoji: {},
emojiFetched: false,
unicodeEmojiAnnotations: {},
}
const SORTED_EMOJI_GROUP_IDS = [
'smileys-and-emotion',
'people-and-body',
'animals-and-nature',
'food-and-drink',
'travel-and-places',
'activities',
'objects',
'symbols',
'flags',
]
const REGIONAL_INDICATORS = (() => {
const start = 0x1f1e6
const end = 0x1f1ff
const A = 'A'.codePointAt(0)
const res = new Array(end - start + 1)
for (let i = start; i <= end; ++i) {
const letter = String.fromCodePoint(A + i - start)
res[i - start] = {
replacement: String.fromCodePoint(i),
imageUrl: false,
displayText: 'regional_indicator_' + letter,
displayTextI18n: {
key: 'emoji.regional_indicator',
args: { letter },
},
}
}
return res
})()
const loadAnnotations = (lang) => {
return annotationsLoader[lang]().then((k) => k.default)
}
const injectAnnotations = (emoji, annotations) => {
const availableLangs = Object.keys(annotations)
return {
...emoji,
annotations: availableLangs.reduce((acc, cur) => {
acc[cur] = annotations[cur][emoji.replacement]
return acc
}, {}),
}
}
const injectRegionalIndicators = (groups) => {
groups.symbols.push(...REGIONAL_INDICATORS)
return groups
}
export const useEmojiStore = defineStore('emoji', {
state: () => ({ ...defaultState }),
getters: {
groupedCustomEmojis(state) {
const packsOf = (emoji) => {
const packs = emoji.tags
.filter((k) => k.startsWith('pack:'))
.map((k) => {
const packName = k.slice(5) // remove 'pack:' prefix
return {
id: `custom-${packName}`,
text: packName,
}
})
if (!packs.length) {
return [
{
id: 'unpacked',
},
]
} else {
return packs
}
}
return this.customEmoji.reduce((res, emoji) => {
packsOf(emoji).forEach(({ id: packId, text: packName }) => {
if (!res[packId]) {
res[packId] = {
id: packId,
text: packName,
image: emoji.imageUrl,
emojis: [],
}
}
res[packId].emojis.push(emoji)
})
return res
}, {})
},
standardEmojiList(state) {
return SORTED_EMOJI_GROUP_IDS.map((groupId) =>
(this.emoji[groupId] || []).map((k) =>
injectAnnotations(k, this.unicodeEmojiAnnotations),
),
).reduce((a, b) => a.concat(b), [])
},
standardEmojiGroupList(state) {
return SORTED_EMOJI_GROUP_IDS.map((groupId) => ({
id: groupId,
emojis: (this.emoji[groupId] || []).map((k) =>
injectAnnotations(k, this.unicodeEmojiAnnotations),
),
}))
},
},
actions: {
async getStaticEmoji() {
try {
// See build/emojis_plugin for more details
const values = (await import('/src/assets/emoji.json')).default
const emoji = Object.keys(values).reduce((res, groupId) => {
res[groupId] = values[groupId].map((e) => ({
displayText: e.slug,
imageUrl: false,
replacement: e.emoji,
}))
return res
}, {})
this.emoji = injectRegionalIndicators(emoji)
} catch (e) {
console.warn("Can't load static emoji\n", e)
}
},
loadUnicodeEmojiData(language) {
const langList = ensureFinalFallback(language)
return Promise.all(
langList.map(async (lang) => {
if (!this.unicodeEmojiAnnotations[lang]) {
try {
const annotations = await loadAnnotations(lang)
this.unicodeEmojiAnnotations[lang] = annotations
} catch (e) {
console.warn(
`Error loading unicode emoji annotations for ${lang}: `,
e,
)
// ignore
}
}
}),
)
},
async getCustomEmoji() {
try {
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
const caseInsensitiveStrCmp = (a, b) => {
const la = a.toLowerCase()
const lb = b.toLowerCase()
return la > lb ? 1 : la < lb ? -1 : 0
}
const noPackLast = (a, b) => {
const aNull = a === ''
const bNull = b === ''
if (aNull === bNull) {
return 0
} else if (aNull && !bNull) {
return 1
} else {
return -1
}
}
const byPackThenByName = (a, b) => {
const packOf = (emoji) =>
(emoji.tags.filter((k) => k.startsWith('pack:'))[0] || '').slice(
5,
)
const packOfA = packOf(a)
const packOfB = packOf(b)
return (
noPackLast(packOfA, packOfB) ||
caseInsensitiveStrCmp(packOfA, packOfB) ||
caseInsensitiveStrCmp(a.displayText, b.displayText)
)
}
const emoji = Object.entries(values)
.map(([key, value]) => {
const imageUrl = value.image_url
return {
displayText: key,
imageUrl: imageUrl ? useInstanceStore().server + imageUrl : value,
tags: imageUrl
? value.tags.sort((a, b) => (a > b ? 1 : 0))
: ['utf'],
replacement: `:${key}: `,
}
// Technically could use tags but those are kinda useless right now,
// should have been "pack" field, that would be more useful
})
.sort(byPackThenByName)
this.customEmoji = emoji
} else {
throw res
}
} catch (e) {
console.warn("Can't load custom emojis\n", e)
}
},
fetchEmoji() {
if (!this.customEmojiFetched) {
this.getCustomEmoji().then(() => (this.customEmojiFetched = true))
}
if (!this.emojiFetched) {
this.getStaticEmoji().then(() => (this.emojiFetched = true))
}
},
},
})

View file

@ -1,8 +1,8 @@
import { get, set } from 'lodash'
import { defineStore } from 'pinia'
import { ensureFinalFallback } from 'src/i18n/languages.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { ensureFinalFallback } from '../i18n/languages.js'
import { instanceDefaultProperties } from '../modules/config.js'
import {
instanceDefaultConfig,
@ -10,40 +10,6 @@ import {
} from '../modules/default_config_state.js'
import apiService from '../services/api/api.service.js'
import { annotationsLoader } from 'virtual:pleroma-fe/emoji-annotations'
const SORTED_EMOJI_GROUP_IDS = [
'smileys-and-emotion',
'people-and-body',
'animals-and-nature',
'food-and-drink',
'travel-and-places',
'activities',
'objects',
'symbols',
'flags',
]
const REGIONAL_INDICATORS = (() => {
const start = 0x1f1e6
const end = 0x1f1ff
const A = 'A'.codePointAt(0)
const res = new Array(end - start + 1)
for (let i = start; i <= end; ++i) {
const letter = String.fromCodePoint(A + i - start)
res[i - start] = {
replacement: String.fromCodePoint(i),
imageUrl: false,
displayText: 'regional_indicator_' + letter,
displayTextI18n: {
key: 'emoji.regional_indicator',
args: { letter },
},
}
}
return res
})()
const REMOTE_INTERACTION_URL = '/main/ostatus'
const defaultState = {
@ -80,15 +46,6 @@ const defaultState = {
...instanceDefaultConfig,
},
// Custom emoji from server
customEmoji: [],
customEmojiFetched: false,
// Unicode emoji from bundle
emoji: {},
emojiFetched: false,
unicodeEmojiAnnotations: {},
// Known domains list for user's domain-muting
knownDomains: [],
@ -143,27 +100,6 @@ const defaultState = {
},
}
const loadAnnotations = (lang) => {
return annotationsLoader[lang]().then((k) => k.default)
}
const injectAnnotations = (emoji, annotations) => {
const availableLangs = Object.keys(annotations)
return {
...emoji,
annotations: availableLangs.reduce((acc, cur) => {
acc[cur] = annotations[cur][emoji.replacement]
return acc
}, {}),
}
}
const injectRegionalIndicators = (groups) => {
groups.symbols.push(...REGIONAL_INDICATORS)
return groups
}
export const useInstanceStore = defineStore('instance', {
state: () => ({ ...defaultState }),
getters: {
@ -172,59 +108,6 @@ export const useInstanceStore = defineStore('instance', {
.map((key) => [key, state[key]])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
},
groupedCustomEmojis(state) {
const packsOf = (emoji) => {
const packs = emoji.tags
.filter((k) => k.startsWith('pack:'))
.map((k) => {
const packName = k.slice(5) // remove 'pack:' prefix
return {
id: `custom-${packName}`,
text: packName,
}
})
if (!packs.length) {
return [
{
id: 'unpacked',
},
]
} else {
return packs
}
}
return this.customEmoji.reduce((res, emoji) => {
packsOf(emoji).forEach(({ id: packId, text: packName }) => {
if (!res[packId]) {
res[packId] = {
id: packId,
text: packName,
image: emoji.imageUrl,
emojis: [],
}
}
res[packId].emojis.push(emoji)
})
return res
}, {})
},
standardEmojiList(state) {
return SORTED_EMOJI_GROUP_IDS.map((groupId) =>
(this.emoji[groupId] || []).map((k) =>
injectAnnotations(k, this.unicodeEmojiAnnotations),
),
).reduce((a, b) => a.concat(b), [])
},
standardEmojiGroupList(state) {
return SORTED_EMOJI_GROUP_IDS.map((groupId) => ({
id: groupId,
emojis: (this.emoji[groupId] || []).map((k) =>
injectAnnotations(k, this.unicodeEmojiAnnotations),
),
}))
},
instanceDomain(state) {
return new URL(this.server).hostname
},
@ -259,121 +142,6 @@ export const useInstanceStore = defineStore('instance', {
break
}
},
async getStaticEmoji() {
try {
// See build/emojis_plugin for more details
const values = (await import('/src/assets/emoji.json')).default
const emoji = Object.keys(values).reduce((res, groupId) => {
res[groupId] = values[groupId].map((e) => ({
displayText: e.slug,
imageUrl: false,
replacement: e.emoji,
}))
return res
}, {})
this.emoji = injectRegionalIndicators(emoji)
} catch (e) {
console.warn("Can't load static emoji\n", e)
}
},
loadUnicodeEmojiData(language) {
const langList = ensureFinalFallback(language)
return Promise.all(
langList.map(async (lang) => {
if (!this.unicodeEmojiAnnotations[lang]) {
try {
const annotations = await loadAnnotations(lang)
this.unicodeEmojiAnnotations[lang] = annotations
} catch (e) {
console.warn(
`Error loading unicode emoji annotations for ${lang}: `,
e,
)
// ignore
}
}
}),
)
},
async getCustomEmoji() {
try {
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
const caseInsensitiveStrCmp = (a, b) => {
const la = a.toLowerCase()
const lb = b.toLowerCase()
return la > lb ? 1 : la < lb ? -1 : 0
}
const noPackLast = (a, b) => {
const aNull = a === ''
const bNull = b === ''
if (aNull === bNull) {
return 0
} else if (aNull && !bNull) {
return 1
} else {
return -1
}
}
const byPackThenByName = (a, b) => {
const packOf = (emoji) =>
(emoji.tags.filter((k) => k.startsWith('pack:'))[0] || '').slice(
5,
)
const packOfA = packOf(a)
const packOfB = packOf(b)
return (
noPackLast(packOfA, packOfB) ||
caseInsensitiveStrCmp(packOfA, packOfB) ||
caseInsensitiveStrCmp(a.displayText, b.displayText)
)
}
const emoji = Object.entries(values)
.map(([key, value]) => {
const imageUrl = value.image_url
return {
displayText: key,
imageUrl: imageUrl ? this.server + imageUrl : value,
tags: imageUrl
? value.tags.sort((a, b) => (a > b ? 1 : 0))
: ['utf'],
replacement: `:${key}: `,
}
// Technically could use tags but those are kinda useless right now,
// should have been "pack" field, that would be more useful
})
.sort(byPackThenByName)
this.customEmoji = emoji
} else {
throw res
}
} catch (e) {
console.warn("Can't load custom emojis\n", e)
}
},
fetchEmoji() {
if (!this.customEmojiFetched) {
this.customEmojiFetched = true
window.vuex.dispatch('getCustomEmoji')
}
if (!this.emojiFetched) {
this.emojiFetched = true
window.vuex.dispatch('getStaticEmoji')
}
},
async getKnownDomains() {
try {
this.knownDomains = await apiService.fetchKnownDomains({

View file

@ -5,6 +5,8 @@ import {
generatePreset,
} from 'src/services/theme_data/theme_data.service.js'
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
import {
applyTheme,
getResourcesIndex,
@ -86,7 +88,7 @@ export const useInterfaceStore = defineStore('interface', {
},
setPageTitle(option = '') {
try {
document.title = `${option} ${window.vuex.state.instance.name}`
document.title = `${option} ${useInstanceStore().name}`
} catch (error) {
console.error(`${error}`)
}
@ -182,7 +184,7 @@ export const useInterfaceStore = defineStore('interface', {
const mobileLayout = width <= 800
const normalOrMobile = mobileLayout ? 'mobile' : 'normal'
const { thirdColumnMode } = window.vuex.getters.mergedConfig
const { thirdColumnMode } = useSyncConfigStore().mergedConfig
if (thirdColumnMode === 'none' || !window.vuex.state.users.currentUser) {
this.layoutType = normalOrMobile
} else {
@ -221,15 +223,15 @@ export const useInterfaceStore = defineStore('interface', {
async fetchPalettesIndex() {
try {
const value = await getResourcesIndex('/static/palettes/index.json')
window.vuex.commit('setInstanceOption', {
name: 'palettesIndex',
useInstanceStore().set({
path: 'palettesIndex',
value,
})
return value
} catch (e) {
console.error('Could not fetch palettes index', e)
window.vuex.commit('setInstanceOption', {
name: 'palettesIndex',
useInstanceStore().set({
path: 'palettesIndex',
value: { _error: e },
})
return Promise.resolve({})
@ -239,7 +241,7 @@ export const useInterfaceStore = defineStore('interface', {
this.resetThemeV3Palette()
this.resetThemeV2()
window.vuex.commit('setOption', { name: 'palette', value })
useSyncConfigStore().setPreference({ path: 'palette', value })
this.applyTheme({ recompile: true })
},
@ -247,7 +249,7 @@ export const useInterfaceStore = defineStore('interface', {
this.resetThemeV3Palette()
this.resetThemeV2()
window.vuex.commit('setOption', { name: 'paletteCustomData', value })
useSyncConfigStore().setPreference({ path: 'paletteCustomData', value })
this.applyTheme({ recompile: true })
},
@ -257,12 +259,12 @@ export const useInterfaceStore = defineStore('interface', {
'/static/styles/index.json',
deserialize,
)
window.vuex.commit('setInstanceOption', { name: 'stylesIndex', value })
useInstanceStore().set({ path: 'stylesIndex', value })
return value
} catch (e) {
console.error('Could not fetch styles index', e)
window.vuex.commit('setInstanceOption', {
name: 'stylesIndex',
useInstanceStore().set({
path: 'stylesIndex',
value: { _error: e },
})
return Promise.resolve({})
@ -273,7 +275,7 @@ export const useInterfaceStore = defineStore('interface', {
this.resetThemeV2()
this.resetThemeV3Palette()
window.vuex.commit('setOption', { name: 'style', value })
useSyncConfigStore().setPreference({ path: 'style', value })
this.useStylePalette = true
this.applyTheme({ recompile: true }).then(() => {
@ -285,7 +287,7 @@ export const useInterfaceStore = defineStore('interface', {
this.resetThemeV2()
this.resetThemeV3Palette()
window.vuex.commit('setOption', { name: 'styleCustomData', value })
useSyncConfigStore().setPreference({ path: 'styleCustomData', value })
this.useStylePalette = true
this.applyTheme({ recompile: true }).then(() => {
@ -295,12 +297,12 @@ export const useInterfaceStore = defineStore('interface', {
async fetchThemesIndex() {
try {
const value = await getResourcesIndex('/static/styles.json')
window.vuex.commit('setInstanceOption', { name: 'themesIndex', value })
useInstanceStore().set({ path: 'themesIndex', value })
return value
} catch (e) {
console.error('Could not fetch themes index', e)
window.vuex.commit('setInstanceOption', {
name: 'themesIndex',
useInstanceStore().set({
path: 'themesIndex',
value: { _error: e },
})
return Promise.resolve({})
@ -311,7 +313,7 @@ export const useInterfaceStore = defineStore('interface', {
this.resetThemeV3Palette()
this.resetThemeV2()
window.vuex.commit('setOption', { name: 'theme', value })
useSyncConfigStore().setPreference({ name: 'theme', value })
this.applyTheme({ recompile: true })
},
@ -320,27 +322,30 @@ export const useInterfaceStore = defineStore('interface', {
this.resetThemeV3Palette()
this.resetThemeV2()
window.vuex.commit('setOption', { name: 'customTheme', value })
window.vuex.commit('setOption', { name: 'customThemeSource', value })
useSyncConfigStore().setPreference({ path: 'customTheme', value })
useSyncConfigStore().setPreference({ path: 'customThemeSource', value })
this.applyTheme({ recompile: true })
},
resetThemeV3() {
window.vuex.commit('setOption', { name: 'style', value: null })
window.vuex.commit('setOption', { name: 'styleCustomData', value: null })
useSyncConfigStore().setPreference({ path: 'style', value: null })
useSyncConfigStore().setPreference({
path: 'styleCustomData',
value: null,
})
},
resetThemeV3Palette() {
window.vuex.commit('setOption', { name: 'palette', value: null })
window.vuex.commit('setOption', {
useSyncConfigStore().setPreference({ path: 'palette', value: null })
useSyncConfigStore().setPreference({
name: 'paletteCustomData',
value: null,
})
},
resetThemeV2() {
window.vuex.commit('setOption', { name: 'theme', value: null })
window.vuex.commit('setOption', { name: 'customTheme', value: null })
window.vuex.commit('setOption', {
name: 'customThemeSource',
useSyncConfigStore().setPreference({ path: 'theme', value: null })
useSyncConfigStore().setPreference({ path: 'customTheme', value: null })
useSyncConfigStore().setPreference({
path: 'customThemeSource',
value: null,
})
},
@ -385,14 +390,14 @@ export const useInterfaceStore = defineStore('interface', {
}
const { style: instanceStyleName, palette: instancePaletteName } =
window.vuex.state.instance
useInstanceStore()
let {
theme: instanceThemeV2Name,
themesIndex,
stylesIndex,
palettesIndex,
} = window.vuex.state.instance
} = useInstanceStore()
const {
style: userStyleName,
@ -508,8 +513,8 @@ export const useInterfaceStore = defineStore('interface', {
)
if (this.useStylePalette) {
window.vuex.commit('setOption', {
name: 'palette',
useSyncConfigStore().setPreference({
path: 'palette',
value: firstStylePaletteName,
})
}

View file

@ -397,7 +397,7 @@ export const _doMigrations = (cache, live) => {
console.debug('Found hotpatch migration, applying')
return window._PLEROMA_HOTPATCH.reverseMigrations.call(
{},
'serverSideStorage',
'syncConfigStore',
{ from: data._version, to: VERSION },
data,
)
@ -408,7 +408,7 @@ export const _doMigrations = (cache, live) => {
return cache
}
export const useServerSideStorageStore = defineStore('serverSideStorage', {
export const useSyncConfigStore = defineStore('syncConfigStore', {
state() {
return cloneDeep(defaultState)
},
@ -560,13 +560,13 @@ export const useServerSideStorageStore = defineStore('serverSideStorage', {
username,
)
},
clearServerSideStorage() {
clearSyncConfig() {
const blankState = { ...cloneDeep(defaultState) }
Object.keys(this).forEach((k) => {
this[k] = blankState[k]
})
},
setServerSideStorage(userData) {
setSyncConfig(userData) {
const live = userData.storage
this.raw = live
let cache = this.cache
@ -633,7 +633,7 @@ export const useServerSideStorageStore = defineStore('serverSideStorage', {
this.flagStorage = this.cache.flagStorage
this.prefsStorage = this.cache.prefsStorage
},
pushServerSideStorage({ force = false } = {}) {
pushSyncConfig({ force = false } = {}) {
const needPush = this.dirty || force
if (!needPush) return
this.updateCache({ username: window.vuex.state.users.currentUser.fqn })
@ -641,7 +641,7 @@ export const useServerSideStorageStore = defineStore('serverSideStorage', {
window.vuex.state.api.backendInteractor
.updateProfileJSON({ params })
.then((user) => {
this.setServerSideStorage(user)
this.setSyncConfig(user)
this.dirty = false
})
},