387 lines
10 KiB
JavaScript
387 lines
10 KiB
JavaScript
|
|
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)
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
})
|