Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85629031
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
65 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index 7e0cf859c8..bb8de17b95 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -1,162 +1,162 @@
import useVuelidate from '@vuelidate/core'
import { required, requiredIf, sameAs } from '@vuelidate/validators'
import { mapState as mapPiniaState } from 'pinia'
import { mapActions, mapState } from 'vuex'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import TermsOfServicePanel from 'src/components/terms_of_service_panel/terms_of_service_panel.vue'
import localeService from '../../services/locale/locale.service.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { DAY } from 'src/services/date_utils/date_utils.js'
const registration = {
setup() {
return { v$: useVuelidate() }
},
data: () => ({
user: {
email: '',
fullname: '',
username: '',
password: '',
confirm: '',
birthday: '',
reason: '',
language: [''],
},
captcha: {},
}),
components: {
InterfaceLanguageSwitcher,
TermsOfServicePanel,
},
validations() {
return {
user: {
email: { required: requiredIf(() => this.accountActivationRequired) },
username: { required },
fullname: { required },
password: { required },
confirm: {
required,
sameAs: sameAs(this.user.password),
},
birthday: {
required: requiredIf(() => this.birthdayRequired),
maxValue: (value) => {
return (
!this.birthdayRequired ||
new Date(value).getTime() <= this.birthdayMin.getTime()
)
},
},
reason: { required: requiredIf(() => this.accountApprovalRequired) },
language: {},
},
}
},
created() {
if ((!this.registrationOpen && !this.token) || this.signedIn) {
this.$router.push({ name: 'root' })
}
this.setCaptcha()
},
computed: {
token() {
return this.$route.params.token
},
bioPlaceholder() {
return this.replaceNewlines(this.$t('registration.bio_placeholder'))
},
reasonPlaceholder() {
return this.replaceNewlines(this.$t('registration.reason_placeholder'))
},
birthdayMin() {
const minAge = this.birthdayMinAge
const today = new Date()
today.setUTCMilliseconds(0)
today.setUTCSeconds(0)
today.setUTCMinutes(0)
today.setUTCHours(0)
const minDate = new Date()
minDate.setTime(today.getTime() - minAge * DAY)
return minDate
},
birthdayMinAttr() {
return this.birthdayMin.toJSON().replace(/T.+$/, '')
},
birthdayMinFormatted() {
const browserLocale = localeService.internalToBrowserLocale(
this.$i18n.locale,
)
return (
this.user.birthday &&
new Date(Date.parse(this.birthdayMin)).toLocaleDateString(
browserLocale,
{ timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' },
)
)
},
...mapPiniaState(useInstanceStore, {
registrationOpen: (store) => store.registrationOpen,
embeddedToS: (store) => store.embeddedToS,
termsOfService: (store) => store.tos,
accountActivationRequired: (store) => store.accountActivationRequired,
accountApprovalRequired: (store) => store.accountApprovalRequired,
birthdayRequired: (store) => store.birthdayRequired,
birthdayMinAge: (store) => store.birthdayMinAge,
}),
...mapState({
signedIn: (state) => !!state.users.currentUser,
isPending: (state) => state.users.signUpPending,
serverValidationErrors: (state) => state.users.signUpErrors,
signUpNotice: (state) => state.users.signUpNotice,
hasSignUpNotice: (state) => !!state.users.signUpNotice.message,
}),
},
methods: {
...mapActions(['signUp', 'getCaptcha']),
async submit() {
this.user.nickname = this.user.username
this.user.token = this.token
this.user.captcha_solution = this.captcha.solution
this.user.captcha_token = this.captcha.token
this.user.captcha_answer_data = this.captcha.answer_data
if (this.user.language) {
this.user.language = localeService.internalToBackendLocaleMulti(
this.user.language.filter((k) => k),
)
}
this.v$.$touch()
if (!this.v$.$invalid) {
try {
const status = await this.signUp(this.user)
if (status === 'ok') {
this.$router.push({ name: 'friends' })
}
// If status is not 'ok' (i.e. it needs further actions to be done
// before you can login), display sign up notice, do not switch anywhere
} catch (error) {
console.warn('Registration failed: ', error)
this.setCaptcha()
}
}
},
setCaptcha() {
this.getCaptcha().then((cpt) => {
this.captcha = cpt
})
},
replaceNewlines(str) {
- return str.replaceAll('\s*\n\s*', ' \n')
+ return str.replaceAll(/\s*\n\s*/g, ' \n')
},
},
}
export default registration
diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js
index a2d33deffc..09d3ecc2d3 100644
--- a/src/components/settings_modal/helpers/setting.js
+++ b/src/components/settings_modal/helpers/setting.js
@@ -1,420 +1,420 @@
import { cloneDeep, get, isEqual, set } from 'lodash'
import DraftButtons from './draft_buttons.vue'
import LocalSettingIndicator from './local_setting_indicator.vue'
import ModifiedIndicator from './modified_indicator.vue'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useLocalConfigStore } from 'src/stores/local_config.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
export default {
components: {
ModifiedIndicator,
DraftButtons,
LocalSettingIndicator,
},
props: {
modelValue: {
type: String,
default: null,
},
description: {
type: String,
default: null,
},
path: {
type: [String, Array],
required: false,
},
showDescription: {
type: Boolean,
required: false,
},
descriptionPathOverride: {
type: [String, Array],
required: false,
},
suggestions: {
type: [String, Array],
required: false,
},
subgroup: {
type: String,
required: false,
},
disabled: {
type: Boolean,
default: false,
},
local: {
type: Boolean,
default: false,
},
parentPath: {
type: [String, Array],
},
parentInvert: {
type: Boolean,
default: false,
},
expert: {
type: [Number, String],
default: 0,
},
source: {
type: String,
default: undefined,
},
hideDraftButtons: {
// this is for the weird backend hybrid (Boolean|String or Boolean|Number) settings
required: false,
type: Boolean,
},
hideLabel: {
type: Boolean,
},
hideDescription: {
type: Boolean,
},
swapDescriptionAndLabel: {
type: Boolean,
},
backendDescriptionPath: {
type: [String, Array],
},
overrideBackendDescription: {
type: Boolean,
},
overrideBackendDescriptionLabel: {
type: [Boolean, String],
},
draftMode: {
type: Boolean,
default: undefined,
},
timedApplyMode: {
type: Boolean,
default: false,
},
},
inject: {
defaultSource: {
default: 'default',
},
defaultDraftMode: {
default: false,
},
},
data() {
return {
localDraft: null,
}
},
created() {
if (
this.realDraftMode &&
(this.realSource !== 'admin' || this.path == null)
) {
this.draft = cloneDeep(this.state)
}
},
computed: {
draft: {
get() {
if (this.realSource === 'admin' || this.path == null) {
return get(useAdminSettingsStore().draft, this.canonPath)
} else {
return this.localDraft
}
},
set(value) {
if (this.realSource === 'admin' || this.path == null) {
useAdminSettingsStore().updateAdminDraft({
path: this.canonPath,
value,
})
} else {
this.localDraft = value
}
},
},
state() {
if (this.path == null) {
return this.modelValue
}
const value = get(this.configSource, this.canonPath)
if (value === undefined) {
return this.defaultState
} else {
return value
}
},
visibleState() {
return this.realDraftMode ? this.draft : this.state
},
realSource() {
return this.source || this.defaultSource
},
realDraftMode() {
return this.draftMode === undefined
? this.defaultDraftMode
: this.draftMode
},
backendDescription() {
return get(useAdminSettingsStore().descriptions, this.descriptionPath)
},
backendDescriptionLabel() {
if (this.realSource !== 'admin') return ''
if (
this.overrideBackendDescriptionLabel !== '' &&
typeof this.overrideBackendDescriptionLabel === 'string'
) {
return this.overrideBackendDescriptionLabel
}
if (!this.backendDescription || this.overrideBackendDescriptionLabel) {
return this.$t(
[
'admin_dash',
'temp_overrides',
- ...this.canonPath.map((p) => p.replaceAll('\.', '_DOT_')),
+ ...this.canonPath.map((p) => p.replaceAll('.', '_DOT_')),
'label',
].join('.'),
)
} else {
return this.swapDescriptionAndLabel
? this.backendDescription?.description
: this.backendDescription?.label
}
},
backendDescriptionDescription() {
if (this.description) return this.description
if (this.realSource !== 'admin') return ''
if (this.hideDescription) return null
if (!this.backendDescription || this.overrideBackendDescription) {
return this.$t(
[
'admin_dash',
'temp_overrides',
- ...this.canonPath.map((p) => p.replaceAll('\.', '_DOT_')),
+ ...this.canonPath.map((p) => p.replaceAll('.', '_DOT_')),
'description',
].join('.'),
)
} else {
return this.swapDescriptionAndLabel
? this.backendDescription?.label
: this.backendDescription?.description
}
},
backendDescriptionSuggestions() {
return this.backendDescription?.suggestions || this.suggestions
},
shouldBeDisabled() {
if (this.path == null) {
return this.disabled
}
let parentValue = null
if (this.parentPath !== undefined && this.realSource === 'admin') {
if (this.realDraftMode) {
parentValue = get(useAdminSettingsStore().draft, this.parentPath)
} else {
parentValue = get(this.configSource, this.parentPath)
}
}
return (
this.disabled ||
(parentValue !== null
? this.parentInvert
? parentValue
: !parentValue
: false)
)
},
configSource() {
switch (this.realSource) {
case 'profile':
return this.$store.state.profileConfig
case 'admin':
return useAdminSettingsStore().config
default:
return useMergedConfigStore().mergedConfig
}
},
configSink() {
if (this.path == null) {
return (k, v) => this.$emit('update:modelValue', v)
}
switch (this.realSource) {
case 'profile':
return (k, v) =>
this.$store.dispatch('setProfileOption', { name: k, value: v })
case 'admin':
return (k, v) =>
useAdminSettingsStore().pushAdminSetting({ path: k, value: v })
default:
return (readPath, value) => {
const writePath = `${readPath}`
if (!this.timedApplyMode) {
if (this.local) {
useLocalConfigStore().set({
path: writePath,
value,
})
} else {
useSyncConfigStore().setSimplePrefAndSave({
path: writePath,
value,
})
}
} else {
if (useInterfaceStore().temporaryChangesTimeoutId !== null) {
console.error("Can't track more than one temporary change")
return
}
const oldValue = get(this.configSource, readPath)
if (this.local) {
useLocalConfigStore().setTemporarily({ path: writePath, value })
} else {
useSyncConfigStore().setPreference({ path: writePath, value })
}
const confirm = () => {
if (this.local) {
useLocalConfigStore().set({ path: writePath, value })
} else {
useSyncConfigStore().pushSyncConfig()
}
useInterfaceStore().clearTemporaryChanges()
}
const revert = () => {
if (this.local) {
useLocalConfigStore().unsetTemporarily({
path: writePath,
value,
})
} else {
useSyncConfigStore().setPreference({
path: writePath,
value: oldValue,
})
}
useInterfaceStore().clearTemporaryChanges()
}
useInterfaceStore().setTemporaryChanges({ confirm, revert })
}
}
}
},
defaultState() {
switch (this.realSource) {
case 'profile':
return {}
default: {
return get(useMergedConfigStore().mergedConfigDefault, this.path)
}
}
},
isProfileSetting() {
return this.realSource === 'profile'
},
isLocalSetting() {
return this.local
},
isChanged() {
if (this.path == null) return false
switch (this.realSource) {
case 'profile':
case 'admin':
return false
default:
return this.state !== this.defaultState
}
},
canonPath() {
if (this.path == null) return null
return Array.isArray(this.path) ? this.path : this.path.split('.')
},
descriptionPath() {
if (this.path == null) return null
if (this.descriptionPathOverride) return this.descriptionPathOverride
const path = Array.isArray(this.path) ? this.path : this.path.split('.')
if (this.subgroup) {
return [
...path.slice(0, path.length - 1),
':subgroup,' + this.subgroup,
...path.slice(path.length - 1),
]
}
return path
},
isDirty() {
if (this.path == null) return false
if (this.realSource === 'admin' && this.canonPath.length > 3) {
return false // should not show draft buttons for "grouped" values
} else {
return this.realDraftMode && !isEqual(this.draft, this.state)
}
},
canHardReset() {
return (
this.realSource === 'admin' &&
useAdminSettingsStore().modifiedPaths?.has(this.canonPath.join(' -> '))
)
},
matchesExpertLevel() {
const settingExpertLevel = this.expert || 0
const userToggleExpert =
useMergedConfigStore().mergedConfig.expertLevel || 0
return settingExpertLevel <= userToggleExpert
},
},
methods: {
getValue(e) {
return e.target.value
},
update(e) {
if (this.realDraftMode) {
this.draft = this.getValue(e)
} else {
this.configSink(this.path, this.getValue(e))
}
},
commitDraft() {
if (this.realDraftMode) {
this.configSink(this.path, this.draft)
}
},
reset() {
if (this.realDraftMode) {
this.draft = cloneDeep(this.state)
} else {
set(
useMergedConfigStore().mergedConfig,
this.path,
cloneDeep(this.defaultState),
)
}
},
hardReset() {
switch (this.realSource) {
case 'admin':
return this.$store
.dispatch('resetAdminSetting', { path: this.path })
.then(() => {
this.draft = this.state
})
default:
console.warn('Hard reset not implemented yet!')
}
},
},
}
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 9eea7e4596..5b8fa5b57a 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -1,426 +1,426 @@
import { parseLinkHeader } from '@web3-storage/parse-link-header'
import escapeHtml from 'escape-html'
import { unescape as lodashUnescape } from 'lodash'
import punycode from 'punycode.js'
import { fileType } from '../file_type/file_type.service.js'
import { isStatusNotification } from '../notification_utils/notification_utils.js'
/** NOTICE! **
* Do not initialize UI-generated data here.
* It will override existing data.
*
* i.e. user.pinnedStatusIds was set to [] here
* UI code would update it with data but upon next user fetch
* it would be reverted back to []
*/
export const parseUser = (data) => {
const output = {}
output._original = data // used for server-side settings
// case for users in "mentions" property for statuses in MastoAPI
const mastoShort = !Object.hasOwn(data, 'avatar')
output.inLists = null
output.id = String(data.id)
output.screen_name = data.acct
output.fqn = data.fqn
output.statusnet_profile_url = data.url
if (Object.hasOwn(data, 'mute_expires_at')) {
output.mute_expires_at =
data.mute_expires_at == null ? false : data.mute_expires_at
}
if (Object.hasOwn(data, 'block_expires_at')) {
output.block_expires_at =
data.block_expires_at == null ? false : data.block_expires_at
}
// There's nothing else to get
if (mastoShort) {
return output
}
output.emoji = data.emojis
output.name = escapeHtml(data.display_name)
output.name_html = output.name
output.name_unescaped = data.display_name
output.description = data.note
// TODO cleanup this shit, output.description is overriden with source data
output.description_html = data.note
output.fields = data.fields
output.fields_html = data.fields.map((field) => {
return {
name: escapeHtml(field.name),
value: field.value,
}
})
output.fields_text = data.fields.map((field) => {
return {
- name: unescape(field.name.replaceAll('<[^>]*>', '')),
- value: unescape(field.value.replaceAll('<[^>]*>', '')),
+ name: unescape(field.name.replaceAll(/<[^>]*>/g, '')),
+ value: unescape(field.value.replaceAll(/<[^>]*>/g, '')),
}
})
// Utilize avatar_static for gif avatars?
output.profile_image_url = data.avatar
output.profile_image_url_original = data.avatar
// Same, utilize header_static?
output.cover_photo = data.header
output.friends_count = data.following_count
output.bot = data.bot
output.privileges = []
if (data.pleroma) {
if (data.pleroma.settings_store) {
output.storage = data.pleroma.settings_store['pleroma-fe']
output.user_highlight = data.pleroma.settings_store['user_highlight']
}
const relationship = data.pleroma.relationship
output.background_image = data.pleroma.background_image
output.favicon = data.pleroma.favicon
output.token = data.pleroma.chat_token
if (relationship) {
output.relationship = relationship
}
output.allow_following_move = data.pleroma.allow_following_move
output.hide_favorites = data.pleroma.hide_favorites
output.hide_follows = data.pleroma.hide_follows
output.hide_followers = data.pleroma.hide_followers
output.hide_follows_count = data.pleroma.hide_follows_count
output.hide_followers_count = data.pleroma.hide_followers_count
output.rights = {
moderator: data.pleroma.is_moderator,
admin: data.pleroma.is_admin,
}
// TODO: Clean up in UI? This is duplication from what BE does for qvitterapi
if (output.rights.admin) {
output.role = 'admin'
} else if (output.rights.moderator) {
output.role = 'moderator'
} else {
output.role = 'member'
}
output.birthday = data.pleroma.birthday
if (data.pleroma.privileges) {
output.privileges = new Set(data.pleroma.privileges)
} else if (data.pleroma.is_admin) {
output.privileges = new Set([
'users_read',
'users_manage_invites',
'users_manage_activation_state',
'users_manage_tags',
'users_manage_credentials',
'users_delete',
'messages_read',
'messages_delete',
'instances_delete',
'reports_manage_reports',
'moderation_log_read',
'announcements_manage_announcements',
'emoji_manage_emoji',
'statistics_read',
])
} else if (data.pleroma.is_moderator) {
output.privileges = new Set(['messages_delete', 'reports_manage_reports'])
} else {
output.privileges = new Set()
}
}
if (data.source) {
output.description = data.source.note
output.default_scope = data.source.privacy
output.fields = data.source.fields
if (data.source.pleroma) {
output.no_rich_text = data.source.pleroma.no_rich_text
output.show_role =
typeof data.source.pleroma.show_role === 'boolean'
? data.source.pleroma.show_role
: true
output.discoverable = data.source.pleroma.discoverable
output.show_birthday = data.pleroma.show_birthday
output.actor_type = data.source.pleroma.actor_type
}
}
// TODO: handle is_local
output.is_local = !output.screen_name.includes('@')
output.created_at = new Date(data.created_at)
output.locked = data.locked
output.last_status_at = new Date(data.last_status_at)
output.followers_count = data.followers_count
output.statuses_count = data.statuses_count
if (data.pleroma) {
output.follow_request_count = data.pleroma.follow_request_count
output.tags = new Set(data.pleroma.tags)
// deactivated was changed to is_active in Pleroma 2.3.0
// so check if is_active is present
output.deactivated =
data.pleroma.is_active !== undefined
? !data.pleroma.is_active // new backend
: data.pleroma.deactivated // old backend
output.notification_settings = data.pleroma.notification_settings
output.unread_chat_count = data.pleroma.unread_chat_count
}
output.tags = output.tags || new Set()
output.rights = output.rights || {}
output.notification_settings = output.notification_settings || {}
// Convert punycode to unicode for UI
output.screen_name_ui = output.screen_name
if (output.screen_name?.includes('@')) {
const parts = output.screen_name.split('@')
const unicodeDomain = punycode.toUnicode(parts[1])
if (unicodeDomain !== parts[1]) {
// Add some identifier so users can potentially spot spoofing attempts:
// lain.com and xn--lin-6cd.com would appear identical otherwise.
output.screen_name_ui_contains_non_ascii = true
output.screen_name_ui = [parts[0], unicodeDomain].join('@')
} else {
output.screen_name_ui_contains_non_ascii = false
}
}
return output
}
export const parseAttachment = (data) => {
const output = {}
// Not exactly same...
output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type
output.meta = data.meta // not present in BE yet
output.id = data.id
if (data.type !== 'unknown') {
// treat gifv like it is "video"
output.type = data.type === 'gifv' ? 'video' : data.type
} else {
output.type = fileType(output.mimetype)
}
output.url = data.url
output.large_thumb_url = data.preview_url
output.description = lodashUnescape(data.description)
return output
}
export const parseSource = (data) => {
const output = {}
output.text = data.text
output.spoiler_text = data.spoiler_text
output.content_type = data.content_type
return output
}
export const parseStatus = (data) => {
const output = {}
output.favorited = data.favourited
output.fave_num = data.favourites_count
output.repeated = data.reblogged
output.repeat_num = data.reblogs_count
output.bookmarked = data.bookmarked
output.type = data.reblog ? 'retweet' : 'status'
output.nsfw = data.sensitive
output.raw_html = data.content
output.emojis = data.emojis
output.tags = data.tags
output.edited_at = data.edited_at
const { pleroma } = data
if (data.pleroma) {
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 = 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_visible = pleroma.quote_visible || true
output.quotes_count = pleroma.quotes_count
output.bookmark_folder_id = pleroma.bookmark_folder
} else {
output.text = data.content
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
if (output.type === 'retweet') {
output.retweeted_status = parseStatus(data.reblog)
}
output.summary_raw_html = escapeHtml(data.spoiler_text)
output.external_url = data.uri || data.url
output.poll = data.poll
if (output.poll) {
output.poll.options = (output.poll.options || []).map((field) => ({
...field,
title_html: escapeHtml(field.title),
}))
}
output.pinned = data.pinned
output.muted = data.muted
output.id = String(data.id)
output.visibility = data.visibility
output.card = data.card
output.created_at = new Date(data.created_at)
// Converting to string, the right way.
output.in_reply_to_status_id = output.in_reply_to_status_id
? String(output.in_reply_to_status_id)
: null
output.in_reply_to_user_id = output.in_reply_to_user_id
? String(output.in_reply_to_user_id)
: null
output.user = parseUser(data.account)
output.attentions = (data.mentions || []).map(parseUser)
output.attachments = (data.media_attachments || []).map(parseAttachment)
const retweetedStatus = data.reblog
if (retweetedStatus) {
output.retweeted_status = parseStatus(retweetedStatus)
}
output.favoritedBy = []
output.rebloggedBy = []
if (Object.hasOwn(data, 'originalStatus')) {
Object.assign(output, data.originalStatus)
}
return output
}
export const parseNotification = (data) => {
const mastoDict = {
favourite: 'like',
reblog: 'repeat',
}
const output = {}
output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen
// TODO: null check should be a temporary fix, I guess.
// Investigate why backend does this.
output.status =
isStatusNotification(output.type) && data.status !== null
? parseStatus(data.status)
: null
output.target = output.type !== 'move' ? null : parseUser(data.target)
output.from_profile = parseUser(data.account)
output.emoji = data.emoji
output.emoji_url = data.emoji_url
if (data.report) {
output.report = data.report
output.report.content = data.report.content
output.report.acct = parseUser(data.report.account)
output.report.actor = parseUser(data.report.actor)
output.report.statuses = data.report.statuses.map(parseStatus)
}
output.created_at = new Date(data.created_at)
output.id = Number.parseInt(data.id)
return output
}
export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
const flakeId = opts.flakeId
const parsedLinkHeader = parseLinkHeader(linkHeader)
if (!parsedLinkHeader) return
const maxId = parsedLinkHeader.next?.max_id
const minId = parsedLinkHeader.prev?.min_id
return {
maxId: flakeId ? maxId : Number.parseInt(maxId, 10),
minId: flakeId ? minId : Number.parseInt(minId, 10),
}
}
export const parseChat = (chat) => {
const output = {}
output.id = chat.id
output.account = parseUser(chat.account)
output.unread = chat.unread
output.lastMessage = parseChatMessage(chat.last_message)
output.updated_at = new Date(chat.updated_at)
return output
}
export const parseChatMessage = (message) => {
if (!message) {
return
}
if (message.isNormalized) {
return message
}
const output = message
output.id = message.id
output.created_at = new Date(message.created_at)
output.chat_id = message.chat_id
output.emojis = message.emojis
output.content = message.content
if (message.attachment) {
output.attachments = [parseAttachment(message.attachment)]
} else {
output.attachments = []
}
output.pending = !!message.pending
output.error = false
output.idempotency_key = message.idempotency_key
output.isNormalized = true
return output
}
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 0cab385a34..4eed080c1f 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -1,369 +1,369 @@
import sum from 'hash-sum'
import localforage from 'localforage'
import { chunk, throttle } from 'lodash'
import { getCssRules } from '../theme_data/css_utils.js'
import { getEngineChecksum, init } from '../theme_data/theme_data_3.service.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { promisedRequest } from 'src/api/helpers.js'
import { ROOT_CONFIG } from 'src/modules/default_config_state.js'
// On platforms where this is not supported, it will return undefined
// Otherwise it will return an array
const supportsAdoptedStyleSheets = !!document.adoptedStyleSheets
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.replaceAll('backdrop-filter:[^;]+;', '') // Remove backdrop-filter
+ newRule = newRule.replaceAll(/backdrop-filter:[^;]+;/g, '') // Remove backdrop-filter
}
if (newRule.startsWith('::-webkit')) {
if (typeof CSS.supports !== 'function') return
// firefox doesn't like invalid selectors
const fullWebkitScrollbarSupport =
CSS.supports('selector(::-webkit-scrollbar)') &&
CSS.supports('selector(::-webkit-scrollbar-button)') &&
CSS.supports('selector(::-webkit-resizer)') &&
CSS.supports('selector(::-webkit-scrollbar-thumb)')
if (!fullWebkitScrollbarSupport) return
}
this.rules.push(
- newRule.replaceAll('var\(--shadowFilter\)[^;]*;', ''), // Remove shadowFilter references
+ newRule.replaceAll(/var\(--shadowFilter\)[^;]*;/g, ''), // Remove shadowFilter references
)
},
}
stylesheets[id] = newStyleSheet
return newStyleSheet
}
export const adoptStyleSheets = throttle(() => {
if (supportsAdoptedStyleSheets) {
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) => {
try {
css.insertRule(r)
} catch (e) {
console.warn('Error inserting rule:', e, 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'
const generateTheme = (inputRuleset, callbacks, debug) => {
const {
onNewRule = () => {
/* no-op */
},
onLazyFinished = () => {
/* no-op */
},
onEagerFinished = () => {
/* no-op */
},
} = callbacks
const themes3 = init({
inputRuleset,
debug,
})
getCssRules(themes3.eager, debug).forEach((rule) => {
// Hacks to support multiple selectors on same component
onNewRule(rule, false)
})
onEagerFinished()
// Optimization - instead of processing all lazy rules in one go, process them in small chunks
// so that UI can do other things and be somewhat responsive while less important rules are being
// processed
let counter = 0
const chunks = chunk(themes3.lazy, 200)
// let t0 = performance.now()
const processChunk = () => {
const chunk = chunks[counter]
Promise.all(chunk.map((x) => x())).then((result) => {
getCssRules(result.filter(Boolean), debug).forEach((rule) => {
onNewRule(rule, true)
})
// const t1 = performance.now()
// console.debug('Chunk ' + counter + ' took ' + (t1 - t0) + 'ms')
// t0 = t1
counter += 1
if (counter < chunks.length) {
setTimeout(processChunk, 0)
} else {
onLazyFinished()
}
})
}
return { lazyProcessFunc: processChunk }
}
export const tryLoadCache = async () => {
console.info('Trying to load compiled theme data from cache')
const cache = await localforage.getItem('pleromafe-theme-cache')
if (!cache) return null
try {
if (
cache.engineChecksum === getEngineChecksum() &&
cache.checksum !== undefined &&
cache.checksum === useMergedConfigStore().mergedConfig.themeChecksum
) {
const eagerStyles = createStyleSheet(EAGER_STYLE_ID, 10)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID, 20)
cache.data[0].forEach((rule) => eagerStyles.addRule(rule))
cache.data[1].forEach((rule) => lazyStyles.addRule(rule))
eagerStyles.ready = true
lazyStyles.ready = true
console.info(`Loaded theme from cache`)
return true
} else {
console.warn("Checksums don't match, cache not usable, clearing")
localStorage.removeItem('pleroma-fe-theme-cache')
}
} catch (e) {
console.error('Failed to load theme cache:', e)
return false
}
}
export const applyTheme = (
input,
onEagerFinish = () => {
/* no-op */
},
onFinish = () => {
/* no-op */
},
debug,
) => {
const eagerStyles = createStyleSheet(EAGER_STYLE_ID, 10)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID, 20)
eagerStyles.clear()
lazyStyles.clear()
const { lazyProcessFunc } = generateTheme(
input,
{
onNewRule(rule, isLazy) {
if (isLazy) {
lazyStyles.addRule(rule)
} else {
eagerStyles.addRule(rule)
}
},
onEagerFinished() {
eagerStyles.ready = true
adoptStyleSheets()
onEagerFinish()
console.info(
'Eager part of theme finished, waiting for lazy part to finish to store cache',
)
},
onLazyFinished() {
lazyStyles.ready = true
adoptStyleSheets()
const data = [eagerStyles.rules, lazyStyles.rules]
const checksum = sum(data)
const cache = {
checksum,
engineChecksum: getEngineChecksum(),
data,
}
useSyncConfigStore().setSimplePrefAndSave({
path: 'themeChecksum',
value: checksum,
})
onFinish(cache)
localforage.setItem('pleromafe-theme-cache', cache)
console.info('Theme cache stored')
},
},
debug,
)
setTimeout(lazyProcessFunc, 0)
}
const extractStyleConfig = ({
sidebarColumnWidth,
contentColumnWidth,
notifsColumnWidth,
themeEditorMinWidth,
emojiReactionsScale,
emojiSize,
navbarSize,
panelHeaderSize,
textSize,
forcedRoundness,
}) => {
const result = {
sidebarColumnWidth,
contentColumnWidth,
notifsColumnWidth,
themeEditorMinWidth:
Number.parseInt(themeEditorMinWidth) === 0
? 'fit-content'
: themeEditorMinWidth,
emojiReactionsScale,
emojiSize,
navbarSize,
panelHeaderSize,
textSize,
}
switch (forcedRoundness) {
case 'disable':
break
case '0':
result.forcedRoundness = '0'
break
case '1':
result.forcedRoundness = '1px'
break
case '2':
result.forcedRoundness = '0.4rem'
break
default:
}
return result
}
const defaultStyleConfig = extractStyleConfig(ROOT_CONFIG)
export const applyStyleConfig = (input) => {
const config = extractStyleConfig(input)
if (config === defaultStyleConfig) {
return
}
const rules = Object.entries(config)
.filter(([, v]) => v)
.map(([k, v]) => `--${k}: ${v}`)
.join(';')
const styleSheet = createStyleSheet('theme-holder', 30)
styleSheet.clear()
styleSheet.addRule(`:root { ${rules} }`)
// TODO find a way to make this not apply to theme previews
if (Object.hasOwn(config, 'forcedRoundness')) {
styleSheet.addRule(` *:not(.preview-block) {
--roundness: var(--forcedRoundness) !important;
}`)
}
styleSheet.ready = true
adoptStyleSheets()
}
export const getResourcesIndex = async (url, parser = (x) => x) => {
const cache = 'no-store'
const customUrl = url.replace(/\.(\w+)$/, '.custom.$1')
let builtin
let custom
const resourceTransform = (resources) => {
return Object.entries(resources).map(([k, v]) => {
if (typeof v === 'object') {
return [k, () => Promise.resolve(v)]
} else if (typeof v === 'string') {
return [
k,
() =>
promisedRequest({
url: v,
cache,
})
.then(({ data: text }) => parser(text))
.catch((e) => {
console.error(e)
return null
}),
]
} else {
console.error(`Unknown resource format - ${k} is a ${typeof v}`)
return [k, null]
}
})
}
try {
const { data: builtinData } = await promisedRequest({ url, cache })
builtin = resourceTransform(builtinData)
} catch {
builtin = []
console.warn(`Builtin resources at ${url} unavailable`)
}
try {
const { data: customData } = await promisedRequest({
url: customUrl,
cache,
})
custom = resourceTransform(customData)
} catch {
custom = []
console.warn(`Custom resources at ${customUrl} unavailable`)
}
const total = [...custom, ...builtin]
if (total.length === 0) {
return Promise.reject(
new Error(
`Resource at ${url} and ${customUrl} completely unavailable. Panicking`,
),
)
}
return Promise.resolve(Object.fromEntries(total))
}
diff --git a/src/services/theme_data/iss_utils.js b/src/services/theme_data/iss_utils.js
index 3dc3aa65dd..67579a1187 100644
--- a/src/services/theme_data/iss_utils.js
+++ b/src/services/theme_data/iss_utils.js
@@ -1,222 +1,216 @@
import { sortBy } from 'lodash'
// "Unrolls" a tree structure of item: { parent: { ...item2, parent: { ...item3, parent: {...} } }}
// into an array [item2, item3] for iterating
export const unroll = (item) => {
const out = []
let currentParent = item
while (currentParent) {
out.push(currentParent)
currentParent = currentParent.parent
}
return out
}
// This gives you an array of arrays of all possible unique (i.e. order-insensitive) combinations
// Can only accept primitives. Duplicates are not supported and can cause unexpected behavior
export const getAllPossibleCombinations = (array) => {
const combos = [array.map((x) => [x])]
for (let comboSize = 2; comboSize <= array.length; comboSize++) {
const previous = combos[combos.length - 1]
const newCombos = previous.map((self) => {
const selfSet = new Set()
self.forEach((x) => selfSet.add(x))
const nonSelf = array.filter((x) => !selfSet.has(x))
return nonSelf.map((x) => [...self, x])
})
const flatCombos = newCombos.reduce((acc, x) => [...acc, ...x], [])
const uniqueComboStrings = new Set()
const uniqueCombos = flatCombos.map(sortBy).filter((x) => {
if (uniqueComboStrings.has(x.join())) {
return false
} else {
uniqueComboStrings.add(x.join())
return true
}
})
combos.push(uniqueCombos)
}
return combos.reduce((acc, x) => [...acc, ...x], [])
}
/**
* Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true)
* selector.
*
* "path" here refers to "fake" selector that cannot be actually used in UI but is used for internal
* purposes
*
* @param {Object} components - object containing all components definitions
*
* @returns {Function}
* @param {Object} rule - rule in question to convert to CSS selector
* @param {boolean} ignoreOutOfTreeSelector - wthether to ignore aformentioned field in
* component definition and use selector
* @param {boolean} isParent - (mostly) internal argument used when recursing
*
* @returns {String} CSS selector (or path)
*/
export const genericRuleToSelector =
(components) => (rule, ignoreOutOfTreeSelector, liteMode, children) => {
const isParent = !!children
if (!rule && !isParent) return null
const component = components[rule.component]
const { states = {}, variants = {}, outOfTreeSelector } = component
const expand = (array = [], subArray = []) => {
if (array.length === 0) return subArray.map((x) => [x])
if (subArray.length === 0) return array.map((x) => [x])
return array
.map((a) => {
return subArray.map((b) => [a, b])
})
.flat()
}
let componentSelectors = Array.isArray(component.selector)
? component.selector
: [component.selector]
if (ignoreOutOfTreeSelector || liteMode)
componentSelectors = [componentSelectors[0]]
componentSelectors = componentSelectors.map((selector) => {
if (selector === ':root') {
return ''
} else if (isParent) {
return selector
} else {
if (outOfTreeSelector && !ignoreOutOfTreeSelector)
return outOfTreeSelector
return selector
}
})
const applicableVariantName = rule.variant || 'normal'
let variantSelectors = null
if (applicableVariantName !== 'normal') {
variantSelectors = variants[applicableVariantName]
} else {
variantSelectors = variants?.normal ?? ''
}
variantSelectors = Array.isArray(variantSelectors)
? variantSelectors
: [variantSelectors]
if (ignoreOutOfTreeSelector || liteMode)
variantSelectors = [variantSelectors[0]]
const applicableStates = (rule.state || []).filter((x) => x !== 'normal')
// const applicableStates = (rule.state || [])
const statesSelectors = applicableStates.map((state) => {
const selector = states[state] || ''
let arraySelector = Array.isArray(selector) ? selector : [selector]
if (ignoreOutOfTreeSelector || liteMode)
arraySelector = [arraySelector[0]]
return arraySelector
- .sort((a) => {
- if (a.startsWith(':')) return 1
- if (/^[a-z]/.exec(a)) return -1
- else return 0
- })
- .join('')
})
const statesSelectorsFlat = statesSelectors.reduce((acc, s) => {
return expand(acc, s).map((st) => st.join(''))
}, [])
const componentVariant = expand(componentSelectors, variantSelectors).map(
(cv) => cv.join(''),
)
const componentVariantStates = expand(
componentVariant,
statesSelectorsFlat,
).map((cvs) => cvs.join(''))
const selectors = expand(componentVariantStates, children).map((cvsc) =>
cvsc.join(' '),
)
/*
*/
if (rule.parent) {
return genericRuleToSelector(components)(
rule.parent,
ignoreOutOfTreeSelector,
liteMode,
selectors,
)
}
return selectors.join(', ').trim()
}
/**
* Check if combination matches
*
* @param {Object} criteria - criteria to match against
* @param {Object} subject - rule/combination to check match
* @param {boolean} strict - strict checking:
* By default every variant and state inherits from "normal" state/variant
* so when checking if combination matches, it WILL match against "normal"
* state/variant. In strict mode inheritance is ignored an "normal" does
* not match
*/
export const combinationsMatch = (criteria, subject, strict) => {
if (criteria.component !== subject.component) return false
// All variants inherit from normal
if (subject.variant !== 'normal' || strict) {
if (criteria.variant !== subject.variant) return false
}
// Subject states > 1 essentially means state is "normal" and therefore matches
if (subject.state.length > 1 || strict) {
const subjectStatesSet = new Set(subject.state)
const criteriaStatesSet = new Set(criteria.state)
const setsAreEqual =
[...criteriaStatesSet].every((state) => subjectStatesSet.has(state)) &&
[...subjectStatesSet].every((state) => criteriaStatesSet.has(state))
if (!setsAreEqual) return false
}
return true
}
/**
* Search for rule that matches `criteria` in set of rules
* meant to be used in a ruleset.filter() function
*
* @param {Object} criteria - criteria to search for
* @param {boolean} strict - whether search strictly or not (see combinationsMatch)
*
* @return function that returns true/false if subject matches
*/
export const findRules = (criteria, strict) => (subject) => {
// If we searching for "general" rules - ignore "specific" ones
if (criteria.parent === null && !!subject.parent) return false
if (!combinationsMatch(criteria, subject, strict)) return false
if (criteria.parent !== undefined && criteria.parent !== null) {
if (!subject.parent && !strict) return true
const pathCriteria = unroll(criteria)
const pathSubject = unroll(subject)
if (pathCriteria.length < pathSubject.length) return false
// Search: .a .b .c
// Matches: .a .b .c; .b .c; .c; .z .a .b .c
// Does not match .a .b .c .d, .a .b .e
for (let i = 0; i < pathCriteria.length; i++) {
const criteriaParent = pathCriteria[i]
const subjectParent = pathSubject[i]
if (!subjectParent) return true
if (!combinationsMatch(criteriaParent, subjectParent, strict))
return false
}
}
return true
}
// Pre-fills 'normal' state/variant if missing
export const normalizeCombination = (rule) => {
rule.variant = rule.variant ?? 'normal'
rule.state = [...new Set(['normal', ...(rule.state || [])])]
}
diff --git a/src/services/user_highlighter/user_highlighter.js b/src/services/user_highlighter/user_highlighter.js
index 9724595c04..e3f94ea1aa 100644
--- a/src/services/user_highlighter/user_highlighter.js
+++ b/src/services/user_highlighter/user_highlighter.js
@@ -1,54 +1,54 @@
import { hex2rgb } from '../color_convert/color_convert.js'
const highlightStyle = (prefs) => {
if (prefs === undefined) return
const { color = '#FFFFFF', type } = prefs
if (typeof color !== 'string') return
const rgb = hex2rgb(color)
if (rgb == null) return
const solidColor = `rgb(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)})`
const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .1)`
const tintColor2 = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .2)`
const customProps = {
'--____highlight-solidColor': solidColor,
'--____highlight-tintColor': tintColor,
'--____highlight-tintColor2': tintColor2,
}
if (type === 'striped') {
return {
backgroundImage: [
'repeating-linear-gradient(135deg,',
`${tintColor} ,`,
`${tintColor} 20px,`,
`${tintColor2} 20px,`,
`${tintColor2} 40px`,
].join(' '),
backgroundPosition: '0 0',
...customProps,
}
} else if (type === 'solid') {
return {
backgroundColor: tintColor2,
...customProps,
}
} else if (type === 'side') {
return {
backgroundImage: [
'linear-gradient(to right,',
`${solidColor} ,`,
`${solidColor} 2px,`,
'transparent 6px',
].join(' '),
backgroundPosition: '0 0',
...customProps,
}
}
}
const highlightClass = (user) => {
return (
- 'USER____' + user.screen_name?.replaceAll('\.', '_').replace(/@/g, '_AT_')
+ 'USER____' + user.screen_name?.replaceAll('.', '_').replace(/@/g, '_AT_')
)
}
export { highlightClass, highlightStyle }
diff --git a/test/unit/specs/components/rich_content.spec.js b/test/unit/specs/components/rich_content.spec.js
index 6ea574304e..ebdf4ef453 100644
--- a/test/unit/specs/components/rich_content.spec.js
+++ b/test/unit/specs/components/rich_content.spec.js
@@ -1,541 +1,541 @@
import { mount, shallowMount } from '@vue/test-utils'
import RichContent from 'src/components/rich_content/rich_content.jsx'
const attentions = []
const global = {
mocks: {
$store: {
state: {},
getters: {
mergedConfig: () => ({
mentionLinkShowTooltip: true,
}),
findUserByUrl: () => null,
},
},
},
stubs: {
FAIcon: true,
},
}
const makeMention = (who, noClass) => {
attentions.push({ statusnet_profile_url: `https://fake.tld/@${who}` })
return noClass
? `<span><a href="https://fake.tld/@${who}">@<span>${who}</span></a></span>`
: `<span class="h-card"><a class="u-url mention" href="https://fake.tld/@${who}">@<span>${who}</span></a></span>`
}
const p = (...data) => `<p>${data.join('')}</p>`
const compwrap = (...data) =>
`<span class="RichContent">${data.join('')}</span>`
const mentionsLine = (times) =>
[
'<mentions-line-stub mentions="',
new Array(times).fill('[object Object]').join(','),
'"></mentions-line-stub>',
].join('')
describe('RichContent', () => {
it('renders simple post without exploding', () => {
const html = p('Hello world!')
const wrapper = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
})
expect(wrapper.html().replaceAll('\n', '')).to.eql(compwrap(html))
})
it('unescapes everything as needed', () => {
const html = [p('Testing 'em all'), 'Testing 'em all'].join('')
const expected = [p("Testing 'em all"), "Testing 'em all"].join('')
const wrapper = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
})
expect(wrapper.html().replaceAll('\n', '')).to.eql(compwrap(expected))
})
it('replaces mention with mentionsline', () => {
const html = p(makeMention('John'), ' how are you doing today?')
const wrapper = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
})
expect(wrapper.html().replaceAll('\n', '')).to.eql(
compwrap(p(mentionsLine(1), ' how are you doing today?')),
)
})
it('replaces mentions at the end of the hellpost', () => {
const html = [
p('How are you doing today, fine gentlemen?'),
p(makeMention('John'), makeMention('Josh'), makeMention('Jeremy')),
].join('')
const expected = [
p('How are you doing today, fine gentlemen?'),
// TODO fix this extra line somehow?
p(
'<mentions-line-stub mentions="',
'[object Object],',
'[object Object],',
'[object Object]',
'"></mentions-line-stub>',
),
].join('')
const wrapper = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
})
expect(wrapper.html().replaceAll('\n', '')).to.eql(compwrap(expected))
})
it('Does not touch links if link handling is disabled', () => {
const html = [
[makeMention('Jack'), "let's meet up with ", makeMention('Janet')].join(
'',
),
[makeMention('John'), makeMention('Josh'), makeMention('Jeremy')].join(
'',
),
].join('\n')
const strippedHtml = [
[
makeMention('Jack', true),
"let's meet up with ",
makeMention('Janet', true),
].join(''),
[
makeMention('John', true),
makeMention('Josh', true),
makeMention('Jeremy', true),
].join(''),
].join('\n')
const wrapper = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: false,
greentext: true,
emoji: [],
html,
},
})
expect(wrapper.html()).to.eql(compwrap(strippedHtml))
})
it('Adds greentext and cyantext to the post', () => {
const html = ['>preordering videogames', '>any year'].join('\n')
const expected = [
'<span class="greentext">>preordering videogames</span>',
'<span class="greentext">>any year</span>',
].join('\n')
const wrapper = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: false,
greentext: true,
emoji: [],
html,
},
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('Does not add greentext and cyantext if setting is set to false', () => {
const html = ['>preordering videogames', '>any year'].join('\n')
const wrapper = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: false,
greentext: false,
emoji: [],
html,
},
})
expect(wrapper.html()).to.eql(compwrap(html))
})
it('Adds emoji to post', () => {
const html = p('Ebin :DDDD :spurdo:')
const expected = p(
'Ebin :DDDD ',
'<anonymous-stub shortcode="spurdo" islocal="true" class="emoji img" src="about:blank" title=":spurdo:" alt=":spurdo:"></anonymous-stub>',
)
const wrapper = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: false,
greentext: false,
emoji: [{ url: 'about:blank', shortcode: 'spurdo' }],
html,
},
})
expect(wrapper.html().replaceAll('\n', '')).to.eql(compwrap(expected))
})
it("Doesn't add nonexistent emoji to post", () => {
const html = p('Lol :lol:')
const wrapper = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: false,
greentext: false,
emoji: [],
html,
},
})
expect(wrapper.html().replaceAll('\n', '')).to.eql(compwrap(html))
})
it('Greentext + last mentions', () => {
const html = [
'>quote',
makeMention('lol'),
'>quote',
'>quote',
].join('\n')
const expected = [
'<span class="greentext">>quote</span>',
mentionsLine(1),
'<span class="greentext">>quote</span>',
'<span class="greentext">>quote</span>',
].join('\n')
const wrapper = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('One buggy example', () => {
const html = [
'Bruh',
'Bruh',
[makeMention('foo'), makeMention('bar'), makeMention('baz')].join(''),
'Bruh',
].join('<br>')
const expected = ['Bruh', 'Bruh', mentionsLine(3), 'Bruh'].join('<br>')
const wrapper = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
})
expect(wrapper.html().replaceAll('\n', '')).to.eql(compwrap(expected))
})
it('buggy example/hashtags', () => {
const html = [
'<p>',
'<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg">',
'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>',
' <a class="hashtag" data-tag="nou" href="https://shitposter.club/tag/nou">',
'#nou</a>',
' <a class="hashtag" data-tag="screencap" href="https://shitposter.club/tag/screencap">',
'#screencap</a>',
' </p>',
].join('')
const expected = [
'<p>',
'<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg" target="_blank">',
'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>',
' <hashtag-link-stub url="https://shitposter.club/tag/nou" content="#nou" tag="nou">',
'</hashtag-link-stub>',
' <hashtag-link-stub url="https://shitposter.club/tag/screencap" content="#screencap" tag="screencap">',
'</hashtag-link-stub>',
' </p>',
].join('')
const wrapper = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
})
expect(wrapper.html().replaceAll('\n', '')).to.eql(compwrap(expected))
})
it('rich contents of a mention are handled properly', () => {
attentions.push({ statusnet_profile_url: 'lol' })
const html = [
p(
'<a href="lol" class="mention">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
),
p('Testing'),
].join('')
const expected = [
p(
'<span class="MentionsLine">',
'<span class="MentionLink mention-link">',
'<a href="lol" class="original" target="_blank">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
'</span>',
'</span>',
),
p('Testing'),
].join('')
const wrapper = mount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
})
expect(
- wrapper.html().replaceAll('\n', '').replaceAll('<!--.*?-->', ''),
+ wrapper.html().replaceAll('\n', '').replaceAll(/<!--.*?-->/g, ''),
).to.eql(compwrap(expected))
})
it('rich contents of nested mentions are handled properly', () => {
attentions.push({ statusnet_profile_url: 'lol' })
const html = [
'<span class="poast-style">',
'<a href="lol" class="mention">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
' ',
'<a href="lol" class="mention">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
' ',
'</span>',
'Testing',
].join('')
const expected = [
'<span>',
'<span class="MentionsLine">',
'<span class="MentionLink mention-link">',
'<a href="lol" class="original" target="_blank">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
'</span>',
'<span class="MentionLink mention-link">',
'<a href="lol" class="original" target="_blank">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
'</span>',
'</span>',
' ',
'</span>',
'Testing',
].join('')
const wrapper = mount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
})
expect(
- wrapper.html().replaceAll('\n', '').replaceAll('<!--.*?-->', ''),
+ wrapper.html().replaceAll('\n', '').replaceAll(/<!--.*?-->/g, ''),
).to.eql(compwrap(expected))
})
it('rich contents of a link are handled properly', () => {
const html = [
'<p>',
'Freenode is dead.</p>',
'<p>',
'<a href="https://isfreenodedeadyet.com/">',
'<span>',
'https://</span>',
'<span>',
'isfreenodedeadyet.com/</span>',
'<span>',
'</span>',
'</a>',
'</p>',
].join('')
const expected = [
'<p>',
'Freenode is dead.</p>',
'<p>',
'<a href="https://isfreenodedeadyet.com/" target="_blank">',
'<span>',
'https://</span>',
'<span>',
'isfreenodedeadyet.com/</span>',
'<span>',
'</span>',
'</a>',
'</p>',
].join('')
const wrapper = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
})
expect(wrapper.html().replaceAll('\n', '')).to.eql(compwrap(expected))
})
it.skip('[INFORMATIVE] Performance testing, 10 000 simple posts', () => {
const amount = 20
const onePost = p(
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
' i just landed in l a where are you',
)
const TestComponent = {
template: `
<div v-if="!vhtml">
${new Array(amount).fill(`<RichContent html="${onePost}" :greentext="true" :handleLinks="handeLinks" :emoji="[]" :attentions="attentions"/>`)}
</div>
<div v-else="vhtml">
${new Array(amount).fill(`<div v-html="${onePost}"/>`)}
</div>
`,
props: ['handleLinks', 'attentions', 'vhtml'],
}
const ptest = (handleLinks, vhtml) => {
const t0 = performance.now()
const wrapper = mount(TestComponent, {
global,
props: {
attentions,
handleLinks,
vhtml,
},
})
const t1 = performance.now()
wrapper.destroy()
const t2 = performance.now()
return `Mount: ${t1 - t0}ms, destroy: ${t2 - t1}ms, avg ${(t1 - t0) / amount}ms - ${(t2 - t1) / amount}ms per item`
}
console.debug(`${amount} items with links handling:`)
console.debug(ptest(true))
console.debug(`${amount} items without links handling:`)
console.debug(ptest(false))
console.debug(`${amount} items plain v-html:`)
console.debug(ptest(false, true))
})
})
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Aug 8, 4:59 PM (1 d, 21 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1723730
Default Alt Text
(65 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline
Log In to Comment