import { get, set } from 'lodash' import { defineStore } from 'pinia' import { useInterfaceStore } from 'src/stores/interface.js' import { ensureFinalFallback } from '../i18n/languages.js' import { instanceDefaultProperties } from '../modules/config.js' import { instanceDefaultConfig, staticOrApiConfigDefault, } 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 = { // Stuff from apiConfig name: 'Pleroma FE', registrationOpen: true, server: 'http://localhost:4040/', textlimit: 5000, bannerlimit: null, avatarlimit: null, backgroundlimit: null, uploadlimit: null, fieldsLimits: null, private: false, federating: true, federationPolicy: null, themesIndex: null, stylesIndex: null, palettesIndex: null, themeData: null, // used for theme editor v2 vapidPublicKey: null, // Stuff from static/config.json loginMethod: 'password', disableUpdateNotification: false, // Instance-wide configurations that should not be changed by individual users instanceIdentity: { ...staticOrApiConfigDefault, }, // Instance admins can override default settings for the whole instance prefsStorage: { ...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: [], // Moderation stuff staffAccounts: [], accountActivationRequired: null, accountApprovalRequired: null, birthdayRequired: false, birthdayMinAge: 0, restrictedNicknames: [], // Feature-set, apparently, not everything here is reported... featureSet: { postFormats: [], mailerEnabled: false, safeDM: true, shoutAvailable: false, pleromaExtensionsAvailable: true, pleromaChatMessagesAvailable: false, pleromaCustomEmojiReactionsAvailable: false, pleromaBookmarkFoldersAvailable: false, pleromaPublicFavouritesAvailable: true, statusNotificationTypeAvailable: true, gopherAvailable: false, editingAvailable: false, mediaProxyAvailable: false, suggestionsEnabled: false, suggestionsWeb: '', quotingAvailable: false, groupActorAvailable: false, blockExpiration: false, tagPolicyAvailable: false, pollsAvailable: false, localBubbleInstances: [], // Akkoma }, // Html stuff instanceSpecificPanelContent: '', tos: '', // Version Information backendVersion: '', backendRepository: '', frontendVersion: '', pollsAvailable: false, pollLimits: { max_options: 4, max_option_chars: 255, min_expiration: 60, max_expiration: 60 * 60 * 24, }, } 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: { instanceDefaultConfig(state) { return instanceDefaultProperties .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 }, remoteInteractionLink(state) { const server = this.server.endsWith('/') ? this.server.slice(0, -1) : this.server const link = server + REMOTE_INTERACTION_URL return ({ statusId, nickname }) => { if (statusId) { return `${link}?status_id=${statusId}` } else { return `${link}?nickname=${nickname}` } } }, }, actions: { set({ path, value }) { if (get(defaultState, path) === undefined) console.error(`Unknown instance option ${path}, value: ${value}`) set(this, path, value) switch (name) { case 'name': useInterfaceStore().setPageTitle() break case 'shoutAvailable': if (value) { window.vuex.dispatch('initializeSocket') } 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({ credentials: window.vuex.state.users.currentUser.credentials, }) } catch (e) { console.warn("Can't load known domains\n", e) } }, }, })