Merge remote-tracking branch 'origin/develop' into 2-11-3-fixes

This commit is contained in:
Henry Jameson 2026-08-12 00:28:32 +03:00
commit a125a9af6c
117 changed files with 591 additions and 557 deletions

View file

@ -0,0 +1,36 @@
when:
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
- event: manual
branch: ${CI_REPO_DEFAULT_BRANCH}
depends_on:
- test
- test-e2e
labels:
platform: linux/amd64
memory: 'high'
steps:
build:
image: docker.io/node:20-alpine
commands:
- apk add --no-cache zip git
- yarn --frozen-lockfile
- yarn build
- if [ "${CI_PIPELINE_EVENT}" = "push" ] || [ "${CI_PIPELINE_EVENT}" = "manual" ]; then zip -9qr ${CI_REPO_DEFAULT_BRANCH}.zip dist/; fi
upload-artifacts:
image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0
settings:
user:
from_secret: pleroma-ci-user
password:
from_secret: pleroma-ci-password
update: true
owner: 'pleroma'
package_name: 'pleroma-fe-builds'
package_version: ${CI_REPO_DEFAULT_BRANCH}
file_source: ./${CI_REPO_DEFAULT_BRANCH}.zip
file_name: latest.zip

View file

@ -6,38 +6,12 @@ when:
- event: manual - event: manual
branch: ${CI_REPO_DEFAULT_BRANCH} branch: ${CI_REPO_DEFAULT_BRANCH}
depends_on:
- test
- test-e2e
labels: labels:
platform: linux/amd64 platform: linux/amd64
memory: 'high'
steps: steps:
build: build:
image: docker.io/node:20-alpine image: docker.io/node:20-alpine
commands: commands:
- apk add --no-cache zip git
- yarn --frozen-lockfile - yarn --frozen-lockfile
- yarn build - yarn build
- if [ "${CI_PIPELINE_EVENT}" = "push" ] || [ "${CI_PIPELINE_EVENT}" = "manual" ]; then zip -9qr ${CI_REPO_DEFAULT_BRANCH}.zip dist/; fi
upload-artifacts:
image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0
when:
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
- event: manual
branch: ${CI_REPO_DEFAULT_BRANCH}
settings:
user:
from_secret: pleroma-ci-user
password:
from_secret: pleroma-ci-password
update: true
owner: 'pleroma'
package_name: 'pleroma-fe-builds'
package_version: ${CI_REPO_DEFAULT_BRANCH}
file_source: ./${CI_REPO_DEFAULT_BRANCH}.zip
file_name: latest.zip

View file

@ -0,0 +1,45 @@
when:
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
- event: manual
branch: ${CI_REPO_DEFAULT_BRANCH}
labels:
platform: linux/amd64
memory: 'high'
depends_on:
- build
- test
- test-e2e
clone:
- name: git
image: woodpeckerci/plugin-git
settings:
depth: 0
partial: false
variables:
script_file_entrypoint: &script_file_entrypoint
- /bin/sh
- -c
- 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh'
steps:
generate-coverage:
image: mcr.microsoft.com/playwright:v1.61.0-jammy
environment:
FF_NETWORK_PER_BUILD: "true"
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
entrypoint: *script_file_entrypoint
commands:
- yarn --frozen-lockfile
- yarn unit-ci-coverage
code-analysis:
image: sonarsource/sonar-scanner-cli:11
environment:
SONAR_TOKEN:
from_secret: sonarqube-token
entrypoint: sonar-scanner

View file

@ -10,6 +10,10 @@ labels:
platform: linux/amd64 platform: linux/amd64
memory: 'high' memory: 'high'
depends_on:
- build
- test
variables: variables:
artifacts_uploader_settings: &artifacts_uploader_settings artifacts_uploader_settings: &artifacts_uploader_settings
user: user:

View file

@ -10,6 +10,9 @@ labels:
platform: linux/amd64 platform: linux/amd64
memory: 'high' memory: 'high'
depends_on:
- build
variables: variables:
artifacts_uploader_settings: &artifacts_uploader_settings artifacts_uploader_settings: &artifacts_uploader_settings
user: user:
@ -45,20 +48,6 @@ steps:
exit 1 exit 1
fi fi
code-analysis:
image: sonarsource/sonar-scanner-cli:11
when:
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
- event: manual
branch: ${CI_REPO_DEFAULT_BRANCH}
environment:
SONAR_TOKEN:
from_secret: sonarqube-token
entrypoint: *script_file_entrypoint
commands:
- sonar-scanner
upload-artifacts: upload-artifacts:
image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0
when: when:

View file

@ -1,12 +1,14 @@
# Pleroma-FE # Pleroma-FE
> Highly-customizable frontend designed for Pleroma. > Highly-customizable frontend designed for Pleroma.
![screenshot](./image-1.png) ![screenshot](./image-1.png)
[![Pipeline Status](https://ci.pleroma.com/api/badges/2/status.svg)](https://ci.pleroma.com/repos/2) [![Coverage](https://sonarqube.pleroma.dev/api/project_badges/measure?project=Pleroma-FE&metric=coverage&token=sqb_f8a5ec836caede0e2c6c62a9bfe66e62a89e2b01)](https://sonarqube.pleroma.dev/dashboard?id=Pleroma-FE) [![Maintainability Rating](https://sonarqube.pleroma.dev/api/project_badges/measure?project=Pleroma-FE&metric=software_quality_maintainability_rating&token=sqb_f8a5ec836caede0e2c6c62a9bfe66e62a89e2b01)](https://sonarqube.pleroma.dev/dashboard?id=Pleroma-FE) [![Reliability Rating](https://sonarqube.pleroma.dev/api/project_badges/measure?project=Pleroma-FE&metric=software_quality_reliability_rating&token=sqb_f8a5ec836caede0e2c6c62a9bfe66e62a89e2b01)](https://sonarqube.pleroma.dev/dashboard?id=Pleroma-FE) [![Security Rating](https://sonarqube.pleroma.dev/api/project_badges/measure?project=Pleroma-FE&metric=software_quality_security_rating&token=sqb_f8a5ec836caede0e2c6c62a9bfe66e62a89e2b01)](https://sonarqube.pleroma.dev/dashboard?id=Pleroma-FE) <a href="https://translate.pleroma.social/engage/pleroma/"><img src="https://translate.pleroma.social/widget/pleroma/pleroma-fe/svg-badge.svg" alt="Translation status"></a>
# For Translators # For Translators
To translate Pleroma-FE, use our weblate server: https://translate.pleroma.social/. If you need to add your language it should be added as a json file in [src/i18n/](https://git.pleroma.social/pleroma/pleroma-fe/src/src/i18n/) folder and added in a list within [src/i18n/languages.js](https://git.pleroma.social/pleroma/pleroma-fe/src/src/i18n/languages.js). To translate Pleroma-FE, use our weblate server: https://translate.pleroma.social/. If you need to add your language it should be added as a json file in [src/i18n/](https://git.pleroma.social/pleroma/pleroma-fe/src/src/i18n/) folder and added in a list within [src/i18n/languages.js](https://git.pleroma.social/pleroma/pleroma-fe/src/src/i18n/languages.js).
Pleroma-FE will set your language by your browser locale, but you can change language in settings. Pleroma-FE will set your language by your browser locale, but you can change language in settings.
@ -32,9 +34,14 @@ yarn unit
# For Contributors: # For Contributors:
You can make local build proxy requests to specific instance with an environment variable `VITE_PROXY_TARGET`, generally it is a good idea to also set port for that specific proxy, if you intend on testing multiple instances, i.e.:
```bash
yarn && PORT=8080 VITE_PROXY_TARGET=https://coolinstance.tld yarn dev
```
You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/src/config/local.example.json)) to enable some convenience dev options: You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/src/config/local.example.json)) to enable some convenience dev options:
* `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases.
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/pleroma/frontend_configurations`. Only works in dev mode. * `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/pleroma/frontend_configurations`. Only works in dev mode.
FE Build process also leaves current commit hash in global variable `___pleromafe_commit_hash` so that you can easily see which pleroma-fe commit instance is running, also helps pinpointing which commit was used when FE was bundled into BE. FE Build process also leaves current commit hash in global variable `___pleromafe_commit_hash` so that you can easily see which pleroma-fe commit instance is running, also helps pinpointing which commit was used when FE was bundled into BE.

View file

@ -7,7 +7,9 @@ export const getCommitHash = () => {
} else { } else {
try { try {
return childProcess return childProcess
.execSync('git rev-parse --short HEAD') .execSync(
'PATH=/usr/bin:/bin:/usr/local/bin:/sbin:/usr/sbin git rev-parse --short HEAD',
)
.toString() .toString()
.trim() .trim()
} catch (e) { } catch (e) {

View file

@ -35,7 +35,7 @@ const getAllAccessibleAnnotations = async (projectRoot) => {
}), }),
) )
) )
.filter((k) => k) .filter(Boolean)
.join(',\n') .join(',\n')
return ` return `

View file

@ -3,7 +3,7 @@ import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with {
type: 'json', type: 'json',
} }
Object.keys(emojis).map((k) => { Object.keys(emojis).forEach((k) => {
emojis[k].forEach((e) => { emojis[k].forEach((e) => {
delete e.unicode_version delete e.unicode_version
delete e.emoji_version delete e.emoji_version

View file

@ -0,0 +1 @@
Fixed server-side domain mutes rendering/api calls

View file

@ -0,0 +1 @@
Fix Theme 2 fonts falling back to serif after upgrade

View file

@ -0,0 +1 @@
Fixed status index approxmiation in timeline rendering for dynamically changing viewport geometries

View file

@ -8,7 +8,8 @@
"dev": "node build/update-emoji.js && vite dev", "dev": "node build/update-emoji.js && vite dev",
"build": "node build/update-emoji.js && vite build", "build": "node build/update-emoji.js && vite build",
"unit": "node build/update-emoji.js && vitest --run --coverage", "unit": "node build/update-emoji.js && vitest --run --coverage",
"unit-ci": "node build/update-emoji.js && vitest --run --coverage --browser.headless", "unit-ci": "node build/update-emoji.js && vitest --run --browser.headless",
"unit-ci-coverage": "node build/update-emoji.js && vitest --run --coverage --browser.headless",
"unit:watch": "node build/update-emoji.js && vitest --coverage", "unit:watch": "node build/update-emoji.js && vitest --coverage",
"e2e:pw": "playwright test --config test/e2e-playwright/playwright.config.mjs", "e2e:pw": "playwright test --config test/e2e-playwright/playwright.config.mjs",
"e2e": "sh ./tools/e2e/run.sh", "e2e": "sh ./tools/e2e/run.sh",

View file

@ -1,3 +1,5 @@
sonar.projectKey=Pleroma-FE sonar.projectKey=Pleroma-FE
sonar.qualitygatesonar.host.url.wait=true sonar.qualitygatesonar.host.url.wait=true
sonar.host.url=https://sonarqube.pleroma.dev sonar.host.url=https://sonarqube.pleroma.dev
sonar.sources=src,build,tools
sonar.tests=test

View file

@ -237,10 +237,10 @@ export const changeStatusScope = ({
credentials, credentials,
}) => { }) => {
const payload = {} const payload = {}
if (typeof sensitive !== 'undefined') { if (sensitive !== undefined) {
payload['sensitive'] = sensitive payload['sensitive'] = sensitive
} }
if (typeof visibility !== 'undefined') { if (visibility !== undefined) {
payload['visibility'] = visibility payload['visibility'] = visibility
} }
@ -260,15 +260,15 @@ export const announcementToPayload = ({
}) => { }) => {
const payload = { content } const payload = { content }
if (typeof startsAt !== 'undefined') { if (startsAt !== undefined) {
payload.starts_at = startsAt ? new Date(startsAt).toISOString() : null payload.starts_at = startsAt ? new Date(startsAt).toISOString() : null
} }
if (typeof endsAt !== 'undefined') { if (endsAt !== undefined) {
payload.ends_at = endsAt ? new Date(endsAt).toISOString() : null payload.ends_at = endsAt ? new Date(endsAt).toISOString() : null
} }
if (typeof allDay !== 'undefined') { if (allDay !== undefined) {
payload.all_day = allDay payload.all_day = allDay
} }

View file

@ -18,7 +18,7 @@ export const chats = ({ credentials }) =>
url: PLEROMA_CHATS_URL, url: PLEROMA_CHATS_URL,
credentials, credentials,
}).then(({ data }) => ({ }).then(({ data }) => ({
data: data.map(parseChat).filter((c) => c), data: data.map(parseChat).filter(Boolean),
})) }))
export const getOrCreateChat = ({ accountId, credentials }) => export const getOrCreateChat = ({ accountId, credentials }) =>
@ -40,7 +40,7 @@ export const chatMessages = ({
method: 'GET', method: 'GET',
credentials, credentials,
}).then(({ data }) => ({ }).then(({ data }) => ({
data: data.map(parseChatMessage).filter((c) => c), data: data.map(parseChatMessage).filter(Boolean),
})) }))
} }

View file

@ -37,11 +37,7 @@ export const paramsString = (params = {}) => {
arrays.forEach(([k, array]) => { arrays.forEach(([k, array]) => {
array.forEach((v) => { array.forEach((v) => {
if ( if (typeof v === 'object' || typeof v === 'function' || v === undefined)
typeof v === 'object' ||
typeof v === 'function' ||
typeof v === 'undefined'
)
throw new TypeError('Array param cannot contain non-primitives!') throw new TypeError('Array param cannot contain non-primitives!')
}) })
}) })

View file

@ -29,9 +29,8 @@ const Announcement = {
currentUser: (state) => state.users.currentUser, currentUser: (state) => state.users.currentUser,
}), }),
canEditAnnouncement() { canEditAnnouncement() {
return ( return this.currentUser?.privileges.has(
this.currentUser && 'announcements_manage_announcements',
this.currentUser.privileges.has('announcements_manage_announcements')
) )
}, },
content() { content() {

View file

@ -33,9 +33,8 @@ const AnnouncementsPage = {
return useAnnouncementsStore().announcements return useAnnouncementsStore().announcements
}, },
canPostAnnouncement() { canPostAnnouncement() {
return ( return this.currentUser?.privileges.has(
this.currentUser && 'announcements_manage_announcements',
this.currentUser.privileges.has('announcements_manage_announcements')
) )
}, },
}, },

View file

@ -165,16 +165,16 @@ const Attachment = {
useMediaViewerStore().setCurrentMedia(this.attachment) useMediaViewerStore().setCurrentMedia(this.attachment)
}, },
onEdit(event) { onEdit(event) {
this.edit && this.edit(this.attachment, event) this.edit?.(this.attachment, event)
}, },
onRemove() { onRemove() {
this.remove && this.remove(this.attachment) this.remove?.(this.attachment)
}, },
onShiftUp() { onShiftUp() {
this.shiftUp && this.shiftUp(this.attachment) this.shiftUp?.(this.attachment)
}, },
onShiftDn() { onShiftDn() {
this.shiftDn && this.shiftDn(this.attachment) this.shiftDn?.(this.attachment)
}, },
stopFlash() { stopFlash() {
this.$refs.flash.closePlayer() this.$refs.flash.closePlayer()

View file

@ -1,9 +1,9 @@
<template> <template>
<div <div
v-if="isMessage" v-if="isMessage"
:id="`chatmessage-${message.id}`"
class="chat-message-wrapper" class="chat-message-wrapper"
:class="[classnames, { 'hovered-message-chain': hoveredMessageChain }]" :class="[classnames, { 'hovered-message-chain': hoveredMessageChain }]"
:id="`chatmessage-${message.id}`"
@mouseover="onHover(true)" @mouseover="onHover(true)"
@mouseleave="onHover(false)" @mouseleave="onHover(false)"
> >
@ -40,7 +40,10 @@
</template> </template>
</i18n-t> </i18n-t>
</StatusPopover> </StatusPopover>
<span v-else class="reply-label"> <span
v-else
class="reply-label"
>
{{ $t('status.broken_reply') }} {{ $t('status.broken_reply') }}
</span> </span>
</template> </template>
@ -83,7 +86,10 @@
:user="author" :user="author"
/> />
</UserPopover> </UserPopover>
<div v-else class="avatar-spacer" /> <div
v-else
class="avatar-spacer"
/>
</div> </div>
<div class="chat-message-inner"> <div class="chat-message-inner">
<div class="message-bubble-wrapper"> <div class="message-bubble-wrapper">
@ -98,7 +104,6 @@
@mouseenter="hovered = true" @mouseenter="hovered = true"
@mouseleave="hovered = false" @mouseleave="hovered = false"
> >
<StatusActionButtons <StatusActionButtons
v-if="isStatus" v-if="isStatus"
class="chat-message-toolbar" class="chat-message-toolbar"
@ -112,9 +117,9 @@
@toggle-replying="$emit('replyRequested', message)" @toggle-replying="$emit('replyRequested', message)"
/> />
<div <div
v-else
class="chat-message-toolbar" class="chat-message-toolbar"
:class="{ '-visible': hovered || menuOpened }" :class="{ '-visible': hovered || menuOpened }"
v-else
> >
<Popover <Popover
trigger="click" trigger="click"
@ -221,7 +226,10 @@
v-if="isStatus && repliedTo" v-if="isStatus && repliedTo"
class="reply-indicator" class="reply-indicator"
> >
<FAIcon class="icon" icon="reply" /> <FAIcon
class="icon"
icon="reply"
/>
</div> </div>
<div class="end-spacer" /> <div class="end-spacer" />
</div> </div>
@ -232,7 +240,10 @@
v-else v-else
class="chat-message-date-separator" class="chat-message-date-separator"
> >
<ChatMessageDate :date="chatItem.date" :show-time="chatItem.isTime" /> <ChatMessageDate
:date="chatItem.date"
:show-time="chatItem.isTime"
/>
</div> </div>
</template> </template>

View file

@ -7,7 +7,7 @@
:previous-item="getPreviousItem(index)" :previous-item="getPreviousItem(index)"
:hovered-message-chain="chatItem.messageChainId === hoveredMessageChainId" :hovered-message-chain="chatItem.messageChainId === hoveredMessageChainId"
:focused="chatItem.id === focusedId" :focused="chatItem.id === focusedId"
:repliedTo="chatItem.id === repliedId" :replied-to="chatItem.id === repliedId"
@hover="onMessageHover" @hover="onMessageHover"
@delete="onMessageDelete" @delete="onMessageDelete"
@reply-requested="onReplyRequested" @reply-requested="onReplyRequested"

View file

@ -97,7 +97,7 @@ const Chat = {
mounted() { mounted() {
window.addEventListener('resize', this.handleResize) window.addEventListener('resize', this.handleResize)
window.addEventListener('scroll', this.handleScroll) window.addEventListener('scroll', this.handleScroll)
if (typeof document.hidden !== 'undefined') { if (document.hidden !== undefined) {
document.addEventListener( document.addEventListener(
'visibilitychange', 'visibilitychange',
this.handleVisibilityChange, this.handleVisibilityChange,
@ -112,7 +112,7 @@ const Chat = {
unmounted() { unmounted() {
window.removeEventListener('scroll', this.handleScroll) window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('resize', this.handleResize) window.removeEventListener('resize', this.handleResize)
if (typeof document.hidden !== 'undefined') if (document.hidden !== undefined)
document.removeEventListener( document.removeEventListener(
'visibilitychange', 'visibilitychange',
this.handleVisibilityChange, this.handleVisibilityChange,

View file

@ -24,7 +24,7 @@
v-if="messages[0]?.summary_raw_html" v-if="messages[0]?.summary_raw_html"
:html="messages[0].summary_raw_html" :html="messages[0].summary_raw_html"
:emoji="messages[0].emojis" :emoji="messages[0].emojis"
/> />
<template v-else> <template v-else>
{{ $t('timeline.conversation') }} {{ $t('timeline.conversation') }}
</template> </template>

View file

@ -11,11 +11,11 @@
{{ label }} {{ label }}
</label> </label>
<Checkbox <Checkbox
v-if="typeof fallback !== 'undefined' && showOptionalCheckbox && !hideOptionalCheckbox" v-if="fallback !== undefined && showOptionalCheckbox && !hideOptionalCheckbox"
:model-value="present" :model-value="present"
:disabled="disabled" :disabled="disabled"
class="opt" class="opt"
@update:model-value="updateValue(typeof modelValue === 'undefined' ? fallback : undefined)" @update:model-value="updateValue(modelValue === undefined ? fallback : undefined)"
/> />
<div <div
class="input color-input-field" class="input color-input-field"

View file

@ -62,7 +62,7 @@ const sortAndFilterConversation = (conversation, statusoid) => {
} else { } else {
conversation = filter(conversation, (status) => status.type !== 'retweet') conversation = filter(conversation, (status) => status.type !== 'retweet')
} }
return conversation.filter((_) => _).sort(sortById) return conversation.filter(Boolean).sort(sortById)
} }
const conversation = { const conversation = {
@ -239,9 +239,9 @@ const conversation = {
depth, depth,
}, },
walk(forest, forest[id], depth + 1, processed), walk(forest, forest[id], depth + 1, processed),
].reduce((a, b) => a.concat(b), []) ].flat()
}) })
.reduce((a, b) => a.concat(b), []) .flat()
const linearized = walk( const linearized = walk(
threads.forest, threads.forest,
@ -307,9 +307,9 @@ const conversation = {
(tl, cur) => (tl, cur) =>
tl.filter( tl.filter(
(k) => (k) =>
this.getReplies(cur.id) !this.getReplies(cur.id)
.map((v) => v.id) .map((v) => v.id)
.indexOf(k.id) === -1, .includes(k.id),
), ),
this.conversation, this.conversation,
) )
@ -375,7 +375,7 @@ const conversation = {
return !!(this.expanded || this.isPage) return !!(this.expanded || this.isPage)
}, },
hiddenStyle() { hiddenStyle() {
const height = (this.status && this.status.virtualHeight) || '120px' const height = this.status?.virtualHeight || '120px'
return this.virtualHidden ? { height } : {} return this.virtualHidden ? { height } : {}
}, },
threadDisplayStatus() { threadDisplayStatus() {

View file

@ -14,9 +14,9 @@
v-if="conversation[0]?.summary_raw_html" v-if="conversation[0]?.summary_raw_html"
:html="conversation[0].summary_raw_html" :html="conversation[0].summary_raw_html"
:emoji="conversation[0].emojis" :emoji="conversation[0].emojis"
/> />
<template v-else> <template v-else>
{{ $t('timeline.conversation') }} {{ $t('timeline.conversation') }}
</template> </template>
</h1> </h1>
<button <button

View file

@ -45,8 +45,7 @@ export default {
data: () => ({ data: () => ({
searchBarHidden: true, searchBarHidden: true,
supportsMask: supportsMask:
window.CSS && window.CSS?.supports &&
window.CSS.supports &&
(window.CSS.supports('mask-size', 'contain') || (window.CSS.supports('mask-size', 'contain') ||
window.CSS.supports('-webkit-mask-size', 'contain') || window.CSS.supports('-webkit-mask-size', 'contain') ||
window.CSS.supports('-moz-mask-size', 'contain') || window.CSS.supports('-moz-mask-size', 'contain') ||

View file

@ -42,9 +42,7 @@ const EditStatusModal = {
}, },
isFormVisible(val) { isFormVisible(val) {
if (val) { if (val) {
this.$nextTick( this.$nextTick(() => this.$el?.querySelector('textarea').focus())
() => this.$el && this.$el.querySelector('textarea').focus(),
)
} }
}, },
}, },

View file

@ -188,8 +188,8 @@ const EmojiInput = {
} }
return { return {
names: names.filter((k) => k), names: names.filter(Boolean),
keywords: keywords.filter((k) => k), keywords: keywords.filter(Boolean),
} }
} }
}, },

View file

@ -79,7 +79,7 @@ export const suggestUsers = ({ dispatch, state }) => {
const userSearch = (query) => dispatch('searchUsers', { query }) const userSearch = (query) => dispatch('searchUsers', { query })
const debounceUserSearch = (query) => { const debounceUserSearch = (query) => {
cancelUserSearch && cancelUserSearch() cancelUserSearch?.()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
timeout = setTimeout(() => { timeout = setTimeout(() => {
userSearch(query).then(resolve).catch(reject) userSearch(query).then(resolve).catch(reject)

View file

@ -57,7 +57,7 @@ const maybeLocalizedKeywords = (emoji, languages, nameLocalizer) => {
languages.forEach((lang) => { languages.forEach((lang) => {
const keywords = emoji.annotations[lang]?.keywords || [] const keywords = emoji.annotations[lang]?.keywords || []
const name = emoji.annotations[lang]?.name const name = emoji.annotations[lang]?.name
res.push(...keywords.concat([name]).filter((k) => k)) res.push(...keywords.concat([name]).filter(Boolean))
}) })
} }
return res return res
@ -408,7 +408,7 @@ const EmojiPicker = {
isFirstRow: index === 0, isFirstRow: index === 0,
})), })),
) )
.reduce((a, c) => a.concat(c), []) .flat()
}, },
languages() { languages() {
return ensureFinalFallback( return ensureFinalFallback(

View file

@ -44,7 +44,7 @@ const Flash = {
}) })
}, },
closePlayer() { closePlayer() {
this.ruffleInstance && this.ruffleInstance.remove() this.ruffleInstance?.remove()
this.player = false this.player = false
this.$emit('playerClosed') this.$emit('playerClosed')
}, },

View file

@ -35,7 +35,7 @@ export default {
'sans-serif', 'sans-serif',
'monospace', 'monospace',
...(this.options || []), ...(this.options || []),
].filter((_) => _), ].filter(Boolean),
} }
}, },
methods: { methods: {

View file

@ -2,7 +2,7 @@
<div class="font-control"> <div class="font-control">
<div class="setting-item"> <div class="setting-item">
<Checkbox <Checkbox
v-if="typeof fallback !== 'undefined'" v-if="fallback !== undefined"
:id="name + '-o'" :id="name + '-o'"
class="font-checkbox setting-control setting-label" class="font-checkbox setting-control setting-label"
:model-value="present" :model-value="present"

View file

@ -50,7 +50,7 @@ const ImageCropper = {
}, },
readFile() { readFile() {
const fileInput = this.$refs.input const fileInput = this.$refs.input
if (fileInput.files != null && fileInput.files[0] != null) { if (fileInput?.files?.[0]) {
this.file = fileInput.files[0] this.file = fileInput.files[0]
const reader = new window.FileReader() const reader = new window.FileReader()
reader.onload = (e) => { reader.onload = (e) => {

View file

@ -59,12 +59,12 @@ const ListsNew = {
membersUsers() { membersUsers() {
return [...this.membersUserIds, ...this.addedUserIds] return [...this.membersUserIds, ...this.addedUserIds]
.map((userId) => this.findUser(userId)) .map((userId) => this.findUser(userId))
.filter((user) => user) .filter(Boolean)
}, },
searchUsers() { searchUsers() {
return this.searchUserIds return this.searchUserIds
.map((userId) => this.findUser(userId)) .map((userId) => this.findUser(userId))
.filter((user) => user) .filter(Boolean)
}, },
...mapState({ ...mapState({
currentUser: (state) => state.users.currentUser, currentUser: (state) => state.users.currentUser,

View file

@ -75,13 +75,11 @@ const MentionLink = {
}, },
computed: { computed: {
user() { user() {
return ( return this.url && this.$store?.getters.findUserByUrl(this.url)
this.url && this.$store && this.$store.getters.findUserByUrl(this.url)
)
}, },
isYou() { isYou() {
// FIXME why user !== currentUser??? // FIXME why user !== currentUser???
return this.user && this.user.id === this.currentUser.id return this.user?.id === this.currentUser.id
}, },
userName() { userName() {
return this.user && this.userNameFullUi.split('@')[0] return this.user && this.userNameFullUi.split('@')[0]
@ -94,10 +92,10 @@ const MentionLink = {
) )
}, },
userNameFull() { userNameFull() {
return this.user && this.user.screen_name return this.user?.screen_name
}, },
userNameFullUi() { userNameFullUi() {
return this.user && this.user.screen_name_ui return this.user?.screen_name_ui
}, },
highlightData() { highlightData() {
return this.highlight[this.user?.screen_name] return this.highlight[this.user?.screen_name]

View file

@ -11,7 +11,7 @@ import { useInstanceStore } from 'src/stores/instance.js'
*/ */
const toInstanceReasonObject = (instances, info, key) => { const toInstanceReasonObject = (instances, info, key) => {
return instances.map((instance) => { return instances.map((instance) => {
if (info[key] && info[key][instance] && info[key][instance].reason) { if (info[key]?.[instance]?.reason) {
return { instance, reason: info[key][instance].reason } return { instance, reason: info[key][instance].reason }
} }
return { instance, reason: '' } return { instance, reason: '' }

View file

@ -15,8 +15,7 @@ export const filterNavigation = (
if (!isFederating && set.has('federating')) return false if (!isFederating && set.has('federating')) return false
if (!currentUser && isPrivate && set.has('!private')) return false if (!currentUser && isPrivate && set.has('!private')) return false
if (!currentUser && !(anon || anonRoute)) return false if (!currentUser && !(anon || anonRoute)) return false
if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) if (!currentUser?.locked && set.has('lockedUser')) return false
return false
if (!hasChats && set.has('chats')) return false if (!hasChats && set.has('chats')) return false
if (!hasAnnouncements && set.has('announcements')) return false if (!hasAnnouncements && set.has('announcements')) return false
if (!supportsBubbleTimeline && set.has('supportsBubbleTimeline')) if (!supportsBubbleTimeline && set.has('supportsBubbleTimeline'))

View file

@ -11,7 +11,7 @@
{{ label || $t('settings.style.themes3.editor.opacity') }} {{ label || $t('settings.style.themes3.editor.opacity') }}
</label> </label>
<Checkbox <Checkbox
v-if="typeof fallback !== 'undefined'" v-if="fallback !== undefined"
:model-value="present" :model-value="present"
:disabled="disabled" :disabled="disabled"
class="opt" class="opt"

View file

@ -39,13 +39,13 @@ export default {
return storePoll || {} return storePoll || {}
}, },
options() { options() {
return (this.poll && this.poll.options) || [] return this.poll?.options || []
}, },
expiresAt() { expiresAt() {
return (this.poll && this.poll.expires_at) || null return this.poll?.expires_at || null
}, },
expired() { expired() {
return (this.poll && this.poll.expired) || false return this.poll?.expired || false
}, },
expirationLabel() { expirationLabel() {
if (useMergedConfigStore().mergedConfig.useAbsoluteTimeFormat) { if (useMergedConfigStore().mergedConfig.useAbsoluteTimeFormat) {

View file

@ -25,6 +25,10 @@ export default {
}, },
emits: ['update:modelValue'], emits: ['update:modelValue'],
computed: { computed: {
// Model value
options() {
return pollFallback(this.modelValue, 'options')
},
pollType: { pollType: {
get() { get() {
return pollFallback(this.modelValue, 'pollType') return pollFallback(this.modelValue, 'pollType')
@ -36,14 +40,6 @@ export default {
}) })
}, },
}, },
options: {
get() {
return pollFallback(this.modelValue, 'options')
},
set(newVal) {
this.$emit('update:modelValue', { ...this.modelValue, options: newVal })
},
},
expiryAmount: { expiryAmount: {
get() { get() {
return pollFallback(this.modelValue, 'expiryAmount') return pollFallback(this.modelValue, 'expiryAmount')
@ -66,6 +62,8 @@ export default {
}) })
}, },
}, },
// Configuration
pollLimits() { pollLimits() {
return useInstanceStore().limits.pollLimits return useInstanceStore().limits.pollLimits
}, },
@ -116,19 +114,34 @@ export default {
}, },
addOption() { addOption() {
if (this.options.length < this.maxOptions) { if (this.options.length < this.maxOptions) {
this.options = [...this.options, ''] this.$emit('update:modelValue', {
...this.modelValue,
options: [...this.options, ''],
})
return true return true
} }
return false return false
}, },
deleteOption(index) { deleteOption(index) {
if (this.options.length > 2) { if (this.options.length > 2) {
this.options.splice(index, 1) const options = [...this.options]
this.options = this.options options.splice(index, 1)
this.$emit('update:modelValue', {
...this.modelValue,
options,
})
} }
}, },
updateOption(index, value) { updateOption(index, value) {
this.options = this.options const options = [...this.options]
options[index] = value
this.$emit('update:modelValue', {
...this.modelValue,
options,
})
}, },
convertExpiryToUnit(unit, amount) { convertExpiryToUnit(unit, amount) {
// Note: we want seconds and not milliseconds // Note: we want seconds and not milliseconds

View file

@ -11,14 +11,15 @@
<div class="input-container"> <div class="input-container">
<input <input
:id="`poll-${index}`" :id="`poll-${index}`"
v-model="options[index]" :model-value="options[index]"
size="1" size="1"
class="input poll-option-input" class="input poll-option-input"
type="text" type="text"
:placeholder="$t('polls.option')" :placeholder="$t('polls.option')"
:maxlength="maxLength" :maxlength="maxLength"
:aria-label="$t('polls.option')"
@keydown.enter.stop.prevent="nextOption(index)" @keydown.enter.stop.prevent="nextOption(index)"
@change="updateOption" @change="(e) => updateOption(index, e.target.value)"
> >
</div> </div>
<button <button
@ -69,6 +70,7 @@
class="input expiry-amount hide-number-spinner" class="input expiry-amount hide-number-spinner"
:min="minExpirationInCurrentUnit" :min="minExpirationInCurrentUnit"
:max="maxExpirationInCurrentUnit" :max="maxExpirationInCurrentUnit"
:aria-label="$t('polls.expiry_amount')"
@change="expiryAmountChange" @change="expiryAmountChange"
> >
{{ ' ' }} {{ ' ' }}
@ -77,6 +79,7 @@
unstyled="true" unstyled="true"
class="expiry-unit" class="expiry-unit"
@change="expiryAmountChange" @change="expiryAmountChange"
:aria-label="$t('polls.expiry_unit')"
> >
<option <option
v-for="unit in expiryUnits" v-for="unit in expiryUnits"

View file

@ -129,9 +129,7 @@ const Popover = {
// Popover will be anchored around this element, trigger ref is the container, so // Popover will be anchored around this element, trigger ref is the container, so
// its children are what are inside the slot. Expect only one v-slot:trigger. // its children are what are inside the slot. Expect only one v-slot:trigger.
const anchorEl = const anchorEl =
this.anchorEl || this.anchorEl || this.$refs.trigger?.children[0] || this.$el
(this.$refs.trigger && this.$refs.trigger.children[0]) ||
this.$el
// SVGs don't have offsetWidth/Height, use fallback // SVGs don't have offsetWidth/Height, use fallback
const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
@ -155,8 +153,7 @@ const Popover = {
// Minor optimization, don't call a slow reflow call if we don't have to // Minor optimization, don't call a slow reflow call if we don't have to
const parentScreenBox = const parentScreenBox =
this.boundTo && (this.boundTo?.x === 'container' || this.boundTo?.y === 'container') &&
(this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
this.containerBoundingClientRect() this.containerBoundingClientRect()
const margin = this.margin || {} const margin = this.margin || {}
@ -164,7 +161,7 @@ const Popover = {
// What are the screen bounds for the popover? Viewport vs container // What are the screen bounds for the popover? Viewport vs container
// when using viewport, using default margin values to dodge the navbar // when using viewport, using default margin values to dodge the navbar
const xBounds = const xBounds =
this.boundTo && this.boundTo.x === 'container' this.boundTo?.x === 'container'
? { ? {
min: parentScreenBox.left + (margin.left || 0), min: parentScreenBox.left + (margin.left || 0),
max: parentScreenBox.right - (margin.right || 0), max: parentScreenBox.right - (margin.right || 0),
@ -175,7 +172,7 @@ const Popover = {
} }
const yBounds = const yBounds =
this.boundTo && this.boundTo.y === 'container' this.boundTo?.y === 'container'
? { ? {
min: parentScreenBox.top + (margin.top || 0), min: parentScreenBox.top + (margin.top || 0),
max: parentScreenBox.bottom - (margin.bottom || 0), max: parentScreenBox.bottom - (margin.bottom || 0),
@ -247,12 +244,12 @@ const Popover = {
if (bottomBoundary + content.offsetHeight > yBounds.max) usingTop = true if (bottomBoundary + content.offsetHeight > yBounds.max) usingTop = true
if (topBoundary - content.offsetHeight < yBounds.min) usingTop = false if (topBoundary - content.offsetHeight < yBounds.min) usingTop = false
const yOffset = (this.offset && this.offset.y) || 0 const yOffset = this.offset?.y || 0
translateY = usingTop translateY = usingTop
? topBoundary - yOffset - content.offsetHeight ? topBoundary - yOffset - content.offsetHeight
: bottomBoundary + yOffset : bottomBoundary + yOffset
const xOffset = (this.offset && this.offset.x) || 0 const xOffset = this.offset?.x || 0
translateX = origin.x + horizOffset + xOffset translateX = origin.x + horizOffset + xOffset
} else { } else {
// Default to whatever user wished with placement prop // Default to whatever user wished with placement prop
@ -268,12 +265,12 @@ const Popover = {
if (rightBoundary + content.offsetWidth > xBounds.max) usingLeft = true if (rightBoundary + content.offsetWidth > xBounds.max) usingLeft = true
if (leftBoundary - content.offsetWidth < xBounds.min) usingLeft = false if (leftBoundary - content.offsetWidth < xBounds.min) usingLeft = false
const xOffset = (this.offset && this.offset.x) || 0 const xOffset = this.offset?.x || 0
translateX = usingLeft translateX = usingLeft
? leftBoundary - xOffset - content.offsetWidth ? leftBoundary - xOffset - content.offsetWidth
: rightBoundary + xOffset : rightBoundary + xOffset
const yOffset = (this.offset && this.offset.y) || 0 const yOffset = this.offset?.y || 0
translateY = origin.y + vertOffset + yOffset translateY = origin.y + vertOffset + yOffset
} }
@ -298,7 +295,7 @@ const Popover = {
}, 0) }, 0)
const wasHidden = this.hidden const wasHidden = this.hidden
this.hidden = false this.hidden = false
this.parentPopover && this.parentPopover.onChildPopoverState(this, true) this.parentPopover?.onChildPopoverState(this, true)
if (this.trigger === 'click' || this.stayOnClick) { if (this.trigger === 'click' || this.stayOnClick) {
document.addEventListener('click', this.onClickOutside) document.addEventListener('click', this.onClickOutside)
} }
@ -316,7 +313,7 @@ const Popover = {
if (this.disabled) return if (this.disabled) return
if (!this.hidden) this.$emit('close') if (!this.hidden) this.$emit('close')
this.hidden = true this.hidden = true
this.parentPopover && this.parentPopover.onChildPopoverState(this, false) this.parentPopover?.onChildPopoverState(this, false)
if (this.trigger === 'click') { if (this.trigger === 'click') {
document.removeEventListener('click', this.onClickOutside) document.removeEventListener('click', this.onClickOutside)
} }
@ -366,7 +363,7 @@ const Popover = {
onClickOutside(e) { onClickOutside(e) {
if (this.disableClickOutside) return if (this.disableClickOutside) return
if (this.hidden) return if (this.hidden) return
if (this.$refs.content && this.$refs.content.contains(e.target)) return if (this.$refs.content?.contains(e.target)) return
if (this.$el.contains(e.target)) return if (this.$el.contains(e.target)) return
if (this.childrenShown.size > 0) return if (this.childrenShown.size > 0) return
this.hidePopover() this.hidePopover()

View file

@ -316,7 +316,7 @@ const PostStatusForm = {
}, },
// -Edit // -Edit
isEdit() { isEdit() {
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== '' return this.statusId !== undefined && this.statusId.trim() !== ''
}, },
// -Reply // -Reply
isReply() { isReply() {
@ -378,8 +378,8 @@ const PostStatusForm = {
set(value) { set(value) {
if (value) { if (value) {
this.newStatus.quote = {} this.newStatus.quote = {}
this.newStatus.quote.thread = value this.newStatus.quote.thread = true
this.newStatus.quote.id = value ? this.repliedStatus.id : '' this.newStatus.quote.id = this.repliedStatus.id
} else { } else {
this.newStatus.quote = null this.newStatus.quote = null
} }
@ -509,9 +509,7 @@ const PostStatusForm = {
// Error handling // Error handling
pollContentError() { pollContentError() {
return ( return this.pollFormVisible && this.newStatus.poll?.error
this.pollFormVisible && this.newStatus.poll && this.newStatus.poll.error
)
}, },
// Featureset detection // Featureset detection
@ -622,7 +620,7 @@ const PostStatusForm = {
this.newStatus.quote = null this.newStatus.quote = null
this.newStatus.nsfw = this.defaultNewStatus.nsfw this.newStatus.nsfw = this.defaultNewStatus.nsfw
this.newStatus.mediaDescriptions = {} this.newStatus.mediaDescriptions = {}
this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile() this.$refs.mediaUpload?.clearFile()
if (this.preserveFocus) { if (this.preserveFocus) {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.textarea.focus() this.$refs.textarea.focus()
@ -812,7 +810,7 @@ const PostStatusForm = {
} }
}, },
fileDrop(e) { fileDrop(e) {
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) { if (e.dataTransfer?.types.includes('Files')) {
e.preventDefault() // allow dropping text like before e.preventDefault() // allow dropping text like before
this.dropFiles = e.dataTransfer.files this.dropFiles = e.dataTransfer.files
clearTimeout(this.dropStopTimeout) clearTimeout(this.dropStopTimeout)
@ -829,7 +827,7 @@ const PostStatusForm = {
}, },
fileDrag(e) { fileDrag(e) {
e.dataTransfer.dropEffect = this.uploadFileLimitReached ? 'none' : 'copy' e.dataTransfer.dropEffect = this.uploadFileLimitReached ? 'none' : 'copy'
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) { if (e.dataTransfer?.types.includes('Files')) {
clearTimeout(this.dropStopTimeout) clearTimeout(this.dropStopTimeout)
this.showDropIcon = 'show' this.showDropIcon = 'show'
} }
@ -962,6 +960,7 @@ const PostStatusForm = {
// Quote // Quote
toggleQuoteForm() { toggleQuoteForm() {
// This is for the "attach quote" button // This is for the "attach quote" button
if (this.newStatus.quote?.thread) return
if (!this.hasQuote) { if (!this.hasQuote) {
this.newStatus.quote = {} this.newStatus.quote = {}
this.newStatus.quote.thread = false this.newStatus.quote.thread = false

View file

@ -1,8 +1,8 @@
<template> <template>
<div <div
v-if="initialized"
ref="form" ref="form"
class="post-status-form" class="post-status-form"
v-if="initialized"
> >
<form <form
autocomplete="off" autocomplete="off"
@ -176,12 +176,12 @@
<input <input
v-if="mentionsLine" v-if="mentionsLine"
:value="mentionsLineReadOnly ? mentionsString : newStatus.mentionsLine" :value="mentionsLineReadOnly ? mentionsString : newStatus.mentionsLine"
@change="onMentionsLineUpdate"
type="text" type="text"
:placeholder="$t('post_status.mentions_line')" :placeholder="$t('post_status.mentions_line')"
:disabled="mentionsLineReadOnly || (posting && !optimisticPosting)" :disabled="mentionsLineReadOnly || (posting && !optimisticPosting)"
size="1" size="1"
class="input mentions-input form-post-mentions unstyled" class="input mentions-input form-post-mentions unstyled"
@change="onMentionsLineUpdate"
> >
<EmojiInput <EmojiInput
ref="emoji-input" ref="emoji-input"
@ -234,7 +234,7 @@
ref="scopeSelector" ref="scopeSelector"
:show-all="showAllScopes" :show-all="showAllScopes"
:user-default="userDefaultScope" :user-default="userDefaultScope"
:original-scope="newStatus.visibility" :original-scope="repliedStatus?.visibility"
:initial-scope="newStatus.visibility" :initial-scope="newStatus.visibility"
@change="changeVis" @change="changeVis"
/> />
@ -271,14 +271,14 @@
<PollForm <PollForm
v-if="pollsAvailable" v-if="pollsAvailable"
ref="pollForm" ref="pollForm"
:visible="pollFormVisible"
v-model="newStatus.poll" v-model="newStatus.poll"
:visible="pollFormVisible"
/> />
<QuoteForm <QuoteForm
v-if="quotingAvailable" v-if="quotingAvailable"
:id="newStatus.quote?.id"
ref="quoteForm" ref="quoteForm"
:visible="quoteFormVisible" :visible="quoteFormVisible"
:id="newStatus.quote?.id"
:url="newStatus.quote?.url" :url="newStatus.quote?.url"
@update:url="url => newStatus.quote.url = url" @update:url="url => newStatus.quote.url = url"
@update:id="id => newStatus.quote.id = id" @update:id="id => newStatus.quote.id = id"

View file

@ -40,9 +40,7 @@ const PostStatusModal = {
}, },
isFormVisible(val) { isFormVisible(val) {
if (val) { if (val) {
this.$nextTick( this.$nextTick(() => this.$el?.querySelector('textarea').focus())
() => this.$el && this.$el.querySelector('textarea').focus(),
)
} }
}, },
}, },

View file

@ -102,7 +102,7 @@ export default {
type: 'statuses', type: 'statuses',
}) })
.then((data) => { .then((data) => {
if (data?.statuses && data.statuses.length === 1) { if (data?.statuses?.length === 1) {
this.$emit('update:id', data.statuses[0].id) this.$emit('update:id', data.statuses[0].id)
} else { } else {
this.handleError(true) this.handleError(true)

View file

@ -10,6 +10,7 @@
size="1" size="1"
class="input" class="input"
:placeholder="$t('post_status.quote_url')" :placeholder="$t('post_status.quote_url')"
:aria-label="$t('post_status.quote_url')"
> >
</div> </div>
<Quote <Quote

View file

@ -11,7 +11,7 @@
{{ label }} {{ label }}
</label> </label>
<input <input
v-if="typeof fallback !== 'undefined'" v-if="fallback !== undefined"
:id="name + '-o'" :id="name + '-o'"
:aria-labelledby="name + '-label'" :aria-labelledby="name + '-label'"
class="input -checkbox opt visible-for-screenreader-only" class="input -checkbox opt visible-for-screenreader-only"
@ -20,7 +20,7 @@
@change="$emit('update:modelValue', !present ? fallback : undefined)" @change="$emit('update:modelValue', !present ? fallback : undefined)"
> >
<label <label
v-if="typeof fallback !== 'undefined'" v-if="fallback !== undefined"
class="opt-l" class="opt-l"
:for="name + '-o'" :for="name + '-o'"
:aria-hidden="true" :aria-hidden="true"

View file

@ -128,7 +128,7 @@ const registration = {
this.user.captcha_answer_data = this.captcha.answer_data this.user.captcha_answer_data = this.captcha.answer_data
if (this.user.language) { if (this.user.language) {
this.user.language = localeService.internalToBackendLocaleMulti( this.user.language = localeService.internalToBackendLocaleMulti(
this.user.language.filter((k) => k), this.user.language.filter(Boolean),
) )
} }
@ -154,7 +154,7 @@ const registration = {
}) })
}, },
replaceNewlines(str) { replaceNewlines(str) {
return str.replace(/\s*\n\s*/g, ' \n') return str.replaceAll(/\s*\n\s*/g, ' \n')
}, },
}, },
} }

View file

@ -181,7 +181,7 @@ export default {
} }
// Processor to use with html_tree_converter // Processor to use with html_tree_converter
const processItem = (item, index, array, what) => { const processItem = (item, index, array) => {
// Handle text nodes - just add emoji // Handle text nodes - just add emoji
if (typeof item === 'string') { if (typeof item === 'string') {
const emptyText = item.trim() === '' const emptyText = item.trim() === ''
@ -251,18 +251,14 @@ export default {
return ['', [mentionsLinePadding, renderImage(opener)], ''] return ['', [mentionsLinePadding, renderImage(opener)], '']
} else if (Tag === 'a' && this.handleLinks) { } else if (Tag === 'a' && this.handleLinks) {
// replace mentions with MentionLink // replace mentions with MentionLink
if (fullAttrs.class && fullAttrs.class.includes('mention')) { if (fullAttrs.class?.includes('mention')) {
// Handling mentions here // Handling mentions here
return renderMention(attrs, children) return renderMention(attrs, children)
} else { } else {
currentMentions = null currentMentions = null
} }
} else if (Tag === 'span') { } else if (Tag === 'span') {
if ( if (this.handleLinks && fullAttrs.class?.includes('h-card')) {
this.handleLinks &&
fullAttrs.class &&
fullAttrs.class.includes('h-card')
) {
return ['', children.map(processItem), ''] return ['', children.map(processItem), '']
} }
} }
@ -281,7 +277,7 @@ export default {
// Processor for back direction (for finding "last" stuff, just easier this way) // Processor for back direction (for finding "last" stuff, just easier this way)
let encounteredTextReverse = false let encounteredTextReverse = false
const processItemReverse = (item, index, array, what) => { const processItemReverse = (item, index, array) => {
// Handle text nodes - just add emoji // Handle text nodes - just add emoji
if (typeof item === 'string') { if (typeof item === 'string') {
const emptyText = item.trim() === '' const emptyText = item.trim() === ''
@ -300,7 +296,7 @@ export default {
const attrs = getAttrs(opener, () => true) const attrs = getAttrs(opener, () => true)
// should only be this // should only be this
if ( if (
(fullAttrs.class && fullAttrs.class.includes('hashtag')) || // Pleroma style fullAttrs.class?.includes('hashtag') || // Pleroma style
fullAttrs.rel === 'tag' // Mastodon style fullAttrs.rel === 'tag' // Mastodon style
) { ) {
return renderHashtag(attrs, children, encounteredTextReverse) return renderHashtag(attrs, children, encounteredTextReverse)
@ -385,13 +381,13 @@ export default {
x ? 'mfm-spinX' : null, x ? 'mfm-spinX' : null,
y ? 'mfm-spinY' : null, y ? 'mfm-spinY' : null,
'mfm-spin', 'mfm-spin',
].filter((a) => a)[0] ].filter(Boolean)[0]
const direction = [ const direction = [
alternate ? 'alternate' : null, alternate ? 'alternate' : null,
left ? 'reverse' : null, left ? 'reverse' : null,
'normal', 'normal',
].filter((a) => a)[0] ].filter(Boolean)[0]
newAttrs.style = [ newAttrs.style = [
`animation-name: ${anim}`, `animation-name: ${anim}`,
@ -479,7 +475,7 @@ export default {
> >
{this.collapse {this.collapse
? pass2.map((x) => { ? pass2.map((x) => {
if (typeof x === 'string') return x.replace(/\n/g, ' ') if (typeof x === 'string') return x.replaceAll('\n', ' ')
if (!Array.isArray(x)) return x if (!Array.isArray(x)) return x
return x.map((y) => (y.type === 'br' ? ' ' : y)) return x.map((y) => (y.type === 'br' ? ' ' : y))
}) })
@ -547,8 +543,8 @@ export const preProcessPerLine = (html, greentext) => {
(string.includes('&gt;') || string.includes('&lt;')) (string.includes('&gt;') || string.includes('&lt;'))
) { ) {
const cleanedString = string const cleanedString = string
.replace(/<[^>]+?>/gi, '') // remove all tags .replaceAll(/<[^>]+?>/gi, '') // remove all tags
.replace(/@\w+/gi, '') // remove mentions (even failed ones) .replaceAll(/@\w+/gi, '') // remove mentions (even failed ones)
.trim() .trim()
if (cleanedString.startsWith('&gt;')) { if (cleanedString.startsWith('&gt;')) {
return `<span class='greentext'>${string}</span>` return `<span class='greentext'>${string}</span>`

View file

@ -11,7 +11,7 @@
{{ label }} {{ label }}
</label> </label>
<Checkbox <Checkbox
v-if="typeof fallback !== 'undefined'" v-if="fallback !== undefined"
:model-value="present" :model-value="present"
:disabled="disabled" :disabled="disabled"
class="opt" class="opt"

View file

@ -122,7 +122,7 @@ const Search = {
return 'statuses' return 'statuses'
}, },
lastHistoryRecord(hashtag) { lastHistoryRecord(hashtag) {
return hashtag.history && hashtag.history[0] return hashtag.history?.[0]
}, },
}, },
} }

View file

@ -122,7 +122,7 @@ const EmojiTab = {
return this.refreshPackList() return this.refreshPackList()
} else { } else {
this.displayError(resp.error) this.displayError(resp.error)
return Promise.reject(resp) throw new Error(resp)
} }
}) })
.then(() => { .then(() => {
@ -139,7 +139,7 @@ const EmojiTab = {
return this.refreshPackList() return this.refreshPackList()
} else { } else {
this.displayError(resp.error) this.displayError(resp.error)
return Promise.reject(resp) throw new Error(resp)
} }
}) })
.then(() => { .then(() => {
@ -239,7 +239,7 @@ const EmojiTab = {
return this.refreshPackList() return this.refreshPackList()
} else { } else {
this.displayError(resp.error) this.displayError(resp.error)
return Promise.reject(resp) throw new Error(resp)
} }
}) })
.then(() => { .then(() => {
@ -259,7 +259,7 @@ const EmojiTab = {
return this.refreshPackList() return this.refreshPackList()
} else { } else {
this.displayError(resp.error) this.displayError(resp.error)
return Promise.reject(resp) throw new Error(resp)
} }
}) })
.then(() => { .then(() => {
@ -280,7 +280,7 @@ const EmojiTab = {
return this.refreshPackList() return this.refreshPackList()
} else { } else {
this.displayError(resp.error) this.displayError(resp.error)
return Promise.reject(resp) throw new Error(resp)
} }
}) })
.then(() => { .then(() => {
@ -300,7 +300,7 @@ const EmojiTab = {
sortPackFiles(nameOfPack) { sortPackFiles(nameOfPack) {
// Sort by key // Sort by key
const sorted = Object.keys(this.knownPacks[nameOfPack].files) const sorted = Object.keys(this.knownPacks[nameOfPack].files)
.sort() .sort((a, b) => a.localeCompare(b))
.reduce((acc, key) => { .reduce((acc, key) => {
if (key.length === 0) return acc if (key.length === 0) return acc
acc[key] = this.knownPacks[nameOfPack].files[key] acc[key] = this.knownPacks[nameOfPack].files[key]

View file

@ -262,7 +262,7 @@ export default {
.then((resp) => { .then((resp) => {
if (resp.error !== undefined) { if (resp.error !== undefined) {
this.$emit('displayError', resp.error) this.$emit('displayError', resp.error)
return Promise.reject(resp.error) throw new Error(resp.error)
} }
return resp.json() return resp.json()

View file

@ -28,12 +28,12 @@ export default {
methods: { methods: {
...Setting.methods, ...Setting.methods,
getValue(e) { getValue(e) {
if (!this.truncate === 1) { if (this.truncate === 1) {
return Number.parseInt(e.target.value) return Number.parseInt(e.target.value)
} else if (this.truncate > 1) { } else if (this.truncate > 1) {
return Math.trunc(e.target.value / this.truncate) * this.truncate return Math.trunc(e.target.value / this.truncate) * this.truncate
} }
return parseFloat(e.target.value) return Number.parseFloat(e.target.value)
}, },
}, },
} }

View file

@ -180,7 +180,7 @@ export default {
[ [
'admin_dash', 'admin_dash',
'temp_overrides', 'temp_overrides',
...this.canonPath.map((p) => p.replace(/\./g, '_DOT_')), ...this.canonPath.map((p) => p.replaceAll('.', '_DOT_')),
'label', 'label',
].join('.'), ].join('.'),
) )
@ -199,7 +199,7 @@ export default {
[ [
'admin_dash', 'admin_dash',
'temp_overrides', 'temp_overrides',
...this.canonPath.map((p) => p.replace(/\./g, '_DOT_')), ...this.canonPath.map((p) => p.replaceAll('.', '_DOT_')),
'description', 'description',
].join('.'), ].join('.'),
) )

View file

@ -67,7 +67,10 @@ export default {
return this.$t(['settings', 'units', this.unitSet, value].join('.')) return this.$t(['settings', 'units', this.unitSet, value].join('.'))
}, },
updateValue(e) { updateValue(e) {
this.configSink(this.path, parseFloat(e.target.value) + this.stateUnit) this.configSink(
this.path,
Number.parseFloat(e.target.value) + this.stateUnit,
)
}, },
updateUnit(e) { updateUnit(e) {
let value = this.stateValue let value = this.stateValue

View file

@ -236,7 +236,7 @@ const AppearanceTab = {
}, },
stylePalettes() { stylePalettes() {
const ruleset = useInterfaceStore().styleDataUsed || [] const ruleset = useInterfaceStore().styleDataUsed || []
if (!ruleset?.length === 0) return if (ruleset.length === 0) return
const meta = ruleset.find((x) => x.component === '@meta') const meta = ruleset.find((x) => x.component === '@meta')
const result = ruleset const result = ruleset
.filter((x) => x.component.startsWith('@palette')) .filter((x) => x.component.startsWith('@palette'))
@ -257,7 +257,7 @@ const AppearanceTab = {
const result = { const result = {
name: `${meta.directives.name || this.$t('settings.style.themes3.palette.imported')}: ${variant}`, name: `${meta.directives.name || this.$t('settings.style.themes3.palette.imported')}: ${variant}`,
key: `style.${variant.toLowerCase().replace(/ /g, '_')}`, key: `style.${variant.toLowerCase().replaceAll(' ', '_')}`,
bg, bg,
fg, fg,
text, text,
@ -277,7 +277,7 @@ const AppearanceTab = {
return !window.IntersectionObserver return !window.IntersectionObserver
}, },
instanceWallpaper() { instanceWallpaper() {
useInstanceStore().instanceIdentity.background return useInstanceStore().instanceIdentity.background
}, },
instanceWallpaperUsed() { instanceWallpaperUsed() {
return ( return (

View file

@ -1,5 +1,4 @@
import { mapActions, mapState } from 'pinia' import { mapState } from 'pinia'
import { v4 as uuidv4 } from 'uuid'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
import Select from 'src/components/select/select.vue' import Select from 'src/components/select/select.vue'
@ -12,7 +11,6 @@ import UnitSetting from '../helpers/unit_setting.vue'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
const ClutterTab = { const ClutterTab = {
components: { components: {
@ -33,120 +31,6 @@ const ClutterTab = {
store.instanceIdentity.showInstanceSpecificPanel && store.instanceIdentity.showInstanceSpecificPanel &&
store.instanceIdentity.instanceSpecificPanelContent, store.instanceIdentity.instanceSpecificPanelContent,
}), }),
...mapState(useSyncConfigStore, {
muteFilters: (store) =>
Object.entries(store.prefsStorage.simple.muteFilters),
muteFiltersObject: (store) => store.prefsStorage.simple.muteFilters,
}),
},
methods: {
...mapActions(useSyncConfigStore, [
'setSimplePrefAndSave',
'unsetSimplePrefAndSave',
'pushSyncConfig',
]),
getDatetimeLocal(timestamp) {
const date = new Date(timestamp)
const fmt = new Intl.NumberFormat('en-US', { minimumIntegerDigits: 2 })
const datetime = [
date.getFullYear(),
'-',
fmt.format(date.getMonth() + 1),
'-',
fmt.format(date.getDate()),
'T',
fmt.format(date.getHours()),
':',
fmt.format(date.getMinutes()),
].join('')
return datetime
},
checkRegexValid(id) {
const filter = this.muteFiltersObject[id]
if (filter.type !== 'regexp') return true
if (filter.type !== 'user_regexp') return true
const { value } = filter
let valid = true
try {
new RegExp(value)
} catch {
valid = false
console.error('Invalid RegExp: ' + value)
}
return valid
},
createFilter(
filter = {
type: 'word',
value: '',
name: 'New Filter',
enabled: true,
expires: null,
hide: false,
},
) {
const newId = uuidv4()
filter.order = this.muteFilters.length + 2
this.muteFiltersDraftObject[newId] = filter
this.setSimplePrefAndSave({ path: 'muteFilters.' + newId, value: filter })
this.pushSyncConfig()
},
exportFilter(id) {
this.exportedFilter = { ...this.muteFiltersDraftObject[id] }
delete this.exportedFilter.order
this.filterExporter.exportData()
},
importFilter() {
this.filterImporter.importData()
},
copyFilter(id) {
const filter = { ...this.muteFiltersDraftObject[id] }
const newId = uuidv4()
this.muteFiltersDraftObject[newId] = filter
this.setSimplePrefAndSave({ path: 'muteFilters.' + newId, value: filter })
this.pushSyncConfig()
},
deleteFilter(id) {
delete this.muteFiltersDraftObject[id]
this.unsetSimplePrefAndSave({ path: 'muteFilters.' + id, value: null })
this.pushSyncConfig()
},
purgeExpiredFilters() {
this.muteFiltersExpired.forEach(([id]) => {
delete this.muteFiltersDraftObject[id]
this.unsetSimplePrefAndSave({ path: 'muteFilters.' + id, value: null })
})
this.pushSyncConfig()
},
updateFilter(id, field, value) {
const filter = { ...this.muteFiltersDraftObject[id] }
if (field === 'expires-never') {
if (!value) {
const offset = 1000 * 60 * 60 * 24 * 14 // 2 weeks
const date = Date.now() + offset
filter.expires = date
} else {
filter.expires = null
}
} else if (field === 'expires') {
const parsed = Date.parse(value)
filter.expires = parsed.valueOf()
} else {
filter[field] = value
}
this.muteFiltersDraftObject[id] = filter
this.muteFiltersDraftDirty[id] = true
},
saveFilter(id) {
this.setSimplePrefAndSave({
path: 'muteFilters.' + id,
value: this.muteFiltersDraftObject[id],
})
this.pushSyncConfig()
this.muteFiltersDraftDirty[id] = false
},
}, },
// Updating nested properties // Updating nested properties
watch: { watch: {

View file

@ -190,21 +190,24 @@ const FilteringTab = {
} }
return valid return valid
}, },
createFilter( createFilter(filter) {
filter = { const newId = uuidv4()
const newFilter = {
type: 'word', type: 'word',
value: '', value: '',
name: 'New Filter', name: 'New Filter',
enabled: true, enabled: true,
expires: null, expires: null,
hide: false, hide: false,
}, ...filter,
) { }
const newId = uuidv4()
filter.order = this.muteFilters.length + 2 newFilter.order = this.muteFilters.length + 2
this.muteFiltersDraftObject[newId] = filter this.muteFiltersDraftObject[newId] = newFilter
this.setSimplePrefAndSave({ path: 'muteFilters.' + newId, value: filter }) this.setSimplePrefAndSave({
path: 'muteFilters.' + newId,
value: newFilter,
})
}, },
exportFilter(id) { exportFilter(id) {
this.exportedFilter = { ...this.muteFiltersDraftObject[id] } this.exportedFilter = { ...this.muteFiltersDraftObject[id] }

View file

@ -153,7 +153,6 @@
</div> </div>
</template> </template>
<template #item="{item}"> <template #item="{item}">
{{ item }}
<DomainMuteCard :domain="item" /> <DomainMuteCard :domain="item" />
</template> </template>
<template #empty> <template #empty>

View file

@ -319,7 +319,7 @@ export default {
return useInterfaceStore().themeDataUsed return useInterfaceStore().themeDataUsed
}, },
shadowsAvailable() { shadowsAvailable() {
return Object.keys(DEFAULT_SHADOWS).sort() return Object.keys(DEFAULT_SHADOWS).sort((a, b) => a.localeCompare(b))
}, },
currentShadowOverriden: { currentShadowOverriden: {
get() { get() {
@ -346,7 +346,7 @@ export default {
}, },
}, },
currentShadowFallback() { currentShadowFallback() {
return (this.previewTheme.shadows || {})[this.shadowSelected] return this.previewTheme.shadows?.[this.shadowSelected]
}, },
currentShadow: { currentShadow: {
get() { get() {
@ -425,8 +425,8 @@ export default {
this.dismissWarning() this.dismissWarning()
const version = const version =
origin === 'localStorage' && !theme.colors ? 'l1' : fileVersion origin === 'localStorage' && !theme.colors ? 'l1' : fileVersion
const snapshotEngineVersion = (theme || {}).themeEngineVersion const snapshotEngineVersion = theme?.themeEngineVersion
const themeEngineVersion = (source || {}).themeEngineVersion || 2 const themeEngineVersion = source?.themeEngineVersion || 2
const versionsMatch = themeEngineVersion === CURRENT_VERSION const versionsMatch = themeEngineVersion === CURRENT_VERSION
const sourceSnapshotMismatch = const sourceSnapshotMismatch =
theme !== undefined && theme !== undefined &&
@ -611,7 +611,7 @@ export default {
*/ */
normalizeLocalState(theme, version = 0, source, forceSource = false) { normalizeLocalState(theme, version = 0, source, forceSource = false) {
let input let input
if (typeof source !== 'undefined') { if (source !== undefined) {
if (forceSource || source?.themeEngineVersion === CURRENT_VERSION) { if (forceSource || source?.themeEngineVersion === CURRENT_VERSION) {
input = source input = source
version = source.themeEngineVersion version = source.themeEngineVersion

View file

@ -181,14 +181,14 @@
name="accentColor" name="accentColor"
:fallback="previewTheme.colors?.link" :fallback="previewTheme.colors?.link"
:label="$t('settings.accent')" :label="$t('settings.accent')"
:show-optional-checkbox="typeof linkColorLocal !== 'undefined'" :show-optional-checkbox="linkColorLocal !== undefined"
/> />
<ColorInput <ColorInput
v-model="linkColorLocal" v-model="linkColorLocal"
name="linkColor" name="linkColor"
:fallback="previewTheme.colors?.accent" :fallback="previewTheme.colors?.accent"
:label="$t('settings.links')" :label="$t('settings.links')"
:show-optional-checkbox="typeof accentColorLocal !== 'undefined'" :show-optional-checkbox="accentColorLocal !== undefined"
/> />
<ContrastRatio :contrast="previewContrast.bgLink" /> <ContrastRatio :contrast="previewContrast.bgLink" />
</div> </div>

View file

@ -61,7 +61,7 @@ const SideDrawer = {
this.toggleDrawer, this.toggleDrawer,
) )
if (this.currentUser && this.currentUser.locked) { if (this.currentUser?.locked) {
this.$store.dispatch('startFetchingFollowRequests') this.$store.dispatch('startFetchingFollowRequests')
} }
}, },

View file

@ -128,8 +128,7 @@ const Status = {
computed: { computed: {
showReasonMutedThread() { showReasonMutedThread() {
return ( return (
(this.status.thread_muted || (this.status.thread_muted || this.status.reblog?.thread_muted) &&
(this.status.reblog && this.status.reblog.thread_muted)) &&
!this.inConversation !this.inConversation
) )
}, },
@ -263,7 +262,7 @@ const Status = {
this.muteFilterHits.length > 0 ? 'filtered' : null, this.muteFilterHits.length > 0 ? 'filtered' : null,
this.muteBotStatuses && this.botStatus ? 'bot' : null, this.muteBotStatuses && this.botStatus ? 'bot' : null,
this.muteSensitiveStatuses && this.sensitiveStatus ? 'nsfw' : null, this.muteSensitiveStatuses && this.sensitiveStatus ? 'nsfw' : null,
].filter((_) => _) ].filter(Boolean)
}, },
muteLocalized() { muteLocalized() {
if (this.muteReasons.length === 0) return null if (this.muteReasons.length === 0) return null

View file

@ -20,9 +20,9 @@
:get-component="getComponent" :get-component="getComponent"
:close="() => { /* no-op */ }" :close="() => { /* no-op */ }"
:do-action="doAction" :do-action="doAction"
@emoji-picker-shown="onEmojiPickerShown"
:default-button-style="useDefaultButtons" :default-button-style="useDefaultButtons"
:hide-label="hideLabels" :hide-label="hideLabels"
@emoji-picker-shown="onEmojiPickerShown"
/> />
<button <button
v-if="showPin && currentUser" v-if="showPin && currentUser"

View file

@ -147,16 +147,15 @@ const StatusBody = {
return this.status.attachments.map((file) => file.type) return this.status.attachments.map((file) => file.type)
}, },
collapsedStatus() { collapsedStatus() {
return this.status.raw_html.replace(/(\n|<br\s?\/?>)/g, ' ') return this.status.raw_html.replaceAll('(\n|<br\s?\/?>)', ' ')
}, },
...mapState(useMergedConfigStore, ['mergedConfig']), ...mapState(useMergedConfigStore, ['mergedConfig']),
}, },
mounted() { mounted() {
this.status.attentions && this.status.attentions?.forEach((attn) => {
this.status.attentions.forEach((attn) => { const { id } = attn
const { id } = attn this.$store.dispatch('fetchUserIfMissing', id)
this.$store.dispatch('fetchUserIfMissing', id) })
})
}, },
methods: { methods: {
onParseReady(event) { onParseReady(event) {
@ -168,7 +167,7 @@ const StatusBody = {
.filter((mention) => !mention.notifying) .filter((mention) => !mention.notifying)
.forEach((mention) => { .forEach((mention) => {
const { content, url } = mention const { content, url } = mention
const cleanedString = content.replace(/<[^>]+?>/gi, '') // remove all tags const cleanedString = content.replaceAll(/<[^>]+?>/gi, '') // remove all tags
if (!cleanedString.startsWith('@')) return if (!cleanedString.startsWith('@')) return
const handle = cleanedString.slice(1) const handle = cleanedString.slice(1)
const host = url.replace(/^https?:\/\//, '').replace(/\/.+?$/, '') const host = url.replace(/^https?:\/\//, '').replace(/\/.+?$/, '')

View file

@ -51,7 +51,7 @@ const StillImage = {
} }
const image = this.$refs.src const image = this.$refs.src
if (!image) return if (!image) return
this.imageLoadHandler && this.imageLoadHandler(image) this.imageLoadHandler?.(image)
const canvas = this.$refs.canvas const canvas = this.$refs.canvas
if (!canvas) return if (!canvas) return
const width = image.naturalWidth const width = image.naturalWidth
@ -61,7 +61,7 @@ const StillImage = {
canvas.getContext('2d').drawImage(image, 0, 0, width, height) canvas.getContext('2d').drawImage(image, 0, 0, width, height)
}, },
onError() { onError() {
this.imageLoadError && this.imageLoadError() this.imageLoadError?.()
}, },
}, },
watch: { watch: {

View file

@ -111,7 +111,11 @@ export default {
type="button" type="button"
role="tab" role="tab"
> >
<img src={props.image} title={props['image-tooltip']} /> <img
src={props.image}
alt={props['image-tooltip']}
title={props['image-tooltip']}
/>
{props.label ? '' : props.label} {props.label ? '' : props.label}
</button> </button>
</div> </div>

View file

@ -263,7 +263,10 @@ const Timeline = {
// Start from approximating the index of some visible status by using the // Start from approximating the index of some visible status by using the
// the center of the screen on the timeline. // the center of the screen on the timeline.
let approxIndex = Math.floor(statuses.length * (centerOfScreen / height)) let approxIndex = Math.min(
Math.floor(statuses.length * (centerOfScreen / height)),
statuses.length - 1,
)
let err = statuses[approxIndex].getBoundingClientRect().y let err = statuses[approxIndex].getBoundingClientRect().y
// if we have a previous scroll index that can be used, test if it's // if we have a previous scroll index that can be used, test if it's

View file

@ -196,7 +196,7 @@ export default {
}, },
computed: { computed: {
escapedNewBio() { escapedNewBio() {
return ldEscape(this.newBio).replace(/\n/g, '<br>') return ldEscape(this.newBio).replaceAll('\n', '<br>')
}, },
somethingToSave() { somethingToSave() {
if (this.newName !== this.user.name_unescaped) return true if (this.newName !== this.user.name_unescaped) return true

View file

@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
const arg = process.argv[2] const arg = process.argv[2]
if (typeof arg === 'undefined') { if (arg === undefined) {
console.info('This is a very simple and tiny tool that checks en.json with any other language and') console.info('This is a very simple and tiny tool that checks en.json with any other language and')
console.info('outputs all the things present in english but missing in foreign language.') console.info('outputs all the things present in english but missing in foreign language.')
console.info('') console.info('')

View file

@ -242,6 +242,8 @@
"single_choice": "Single choice", "single_choice": "Single choice",
"multiple_choices": "Multiple choices", "multiple_choices": "Multiple choices",
"expiry": "Poll age", "expiry": "Poll age",
"expiry_unit": "Expiry unit",
"expiry_amount": "Expiry amount",
"expires_at": "Poll ends {0}", "expires_at": "Poll ends {0}",
"expires_in": "Poll ends in {0}", "expires_in": "Poll ends in {0}",
"expired": "Poll ended {0} ago", "expired": "Poll ended {0} ago",

View file

@ -18,9 +18,9 @@ const hasLanguageFile = (code) => languages.includes(code)
const languageFileMap = import.meta.glob(['./*.json', '!./en.json']) const languageFileMap = import.meta.glob(['./*.json', '!./en.json'])
const loadLanguageFile = (code) => { const loadLanguageFile = async (code) => {
const jsonName = langCodeToJsonName(code) const jsonName = langCodeToJsonName(code)
if (jsonName === 'en') return Promise.resolve({ default: enMessages }) if (jsonName === 'en') return { default: enMessages }
return languageFileMap[`./${jsonName}.json`]() return languageFileMap[`./${jsonName}.json`]()
} }

View file

@ -74,7 +74,7 @@ export default function createPersistedState({
if (saveImmedeatelyActions.includes(mutation.type)) { if (saveImmedeatelyActions.includes(mutation.type)) {
setState(key, reducer(cloneDeep(state), paths), storage).then( setState(key, reducer(cloneDeep(state), paths), storage).then(
(success) => { (success) => {
if (typeof success !== 'undefined') { if (success !== undefined) {
if ( if (
mutation.type === 'setOption' || mutation.type === 'setOption' ||
mutation.type === 'setCurrentUser' mutation.type === 'setCurrentUser'
@ -185,7 +185,7 @@ export const piniaPersistPlugin =
} }
const fallbackValue = await storage.getItem(vuexKey) const fallbackValue = await storage.getItem(vuexKey)
if (fallbackValue && fallbackValue[id]) { if (fallbackValue?.[id]) {
console.info(`Migrating ${id} store data from vuex to pinia`) console.info(`Migrating ${id} store data from vuex to pinia`)
const res = fallbackValue[id] const res = fallbackValue[id]
await storage.setItem(key, res) await storage.setItem(key, res)
@ -198,7 +198,7 @@ export const piniaPersistPlugin =
const setState = (state) => { const setState = (state) => {
if (!loadedGuard.loaded) { if (!loadedGuard.loaded) {
console.info('waiting for old state to be loaded...') console.info('waiting for old state to be loaded...')
return Promise.reject() throw new Error('Waiting')
} else { } else {
return storage.setItem(key, state) return storage.setItem(key, state)
} }

View file

@ -325,7 +325,7 @@ const api = {
const token = state.wsToken const token = state.wsToken
if ( if (
useInstanceCapabilitiesStore().shoutAvailable && useInstanceCapabilitiesStore().shoutAvailable &&
typeof token !== 'undefined' && token !== undefined &&
state.socket === null state.socket === null
) { ) {
const socket = new Socket('/socket', { params: { token } }) const socket = new Socket('/socket', { params: { token } })
@ -336,7 +336,7 @@ const api = {
} }
}, },
disconnectFromSocket({ commit, state }) { disconnectFromSocket({ commit, state }) {
state.socket && state.socket.disconnect() state.socket?.disconnect()
commit('setSocket', null) commit('setSocket', null)
}, },
}, },

View file

@ -49,8 +49,8 @@ const emptyTl = (userId = 0) => ({
visibleStatuses: [], visibleStatuses: [],
visibleStatusesObject: {}, visibleStatusesObject: {},
newStatusCount: 0, newStatusCount: 0,
maxId: 0, maxId: '',
minId: 0, minId: '',
minVisibleId: 0, minVisibleId: 0,
loading: false, loading: false,
followers: [], followers: [],
@ -64,7 +64,7 @@ export const defaultState = () => ({
scrobblesNextFetch: {}, scrobblesNextFetch: {},
allStatusesObject: {}, allStatusesObject: {},
conversationsObject: {}, conversationsObject: {},
maxId: 0, maxId: '',
favorites: new Set(), favorites: new Set(),
timelines: { timelines: {
mentions: emptyTl(), mentions: emptyTl(),
@ -134,7 +134,7 @@ const sortById = (a, b) => {
const sortTimeline = (timeline) => { const sortTimeline = (timeline) => {
timeline.visibleStatuses = timeline.visibleStatuses.sort(sortById) timeline.visibleStatuses = timeline.visibleStatuses.sort(sortById)
timeline.statuses = timeline.statuses.sort(sortById) timeline.statuses = timeline.statuses.sort(sortById)
timeline.minVisibleId = (last(timeline.visibleStatuses) || {}).id timeline.minVisibleId = last(timeline.visibleStatuses)?.id
return timeline return timeline
} }
@ -222,11 +222,11 @@ const addNewStatuses = (
const newer = const newer =
timeline && timeline &&
(maxNew > timelineObject.maxId || timelineObject.maxId === 0) && (maxNew > timelineObject.maxId || timelineObject.maxId === '') &&
statuses.length > 0 statuses.length > 0
const older = const older =
timeline && timeline &&
(minNew < timelineObject.minId || timelineObject.minId === 0) && (minNew < timelineObject.minId || timelineObject.minId === '') &&
statuses.length > 0 statuses.length > 0
if (!noIdUpdate && newer) { if (!noIdUpdate && newer) {
@ -525,7 +525,7 @@ export const mutations = {
}, },
addRepeats(state, { id, rebloggedByUsers, currentUser }) { addRepeats(state, { id, rebloggedByUsers, currentUser }) {
const newStatus = state.allStatusesObject[id] const newStatus = state.allStatusesObject[id]
newStatus.rebloggedBy = rebloggedByUsers.filter((_) => _) newStatus.rebloggedBy = rebloggedByUsers.filter(Boolean)
// repeats stats can be incorrect based on polling condition, let's update them using the most recent data // repeats stats can be incorrect based on polling condition, let's update them using the most recent data
newStatus.repeat_num = newStatus.rebloggedBy.length newStatus.repeat_num = newStatus.rebloggedBy.length
newStatus.repeated = !!newStatus.rebloggedBy.find( newStatus.repeated = !!newStatus.rebloggedBy.find(
@ -534,7 +534,7 @@ export const mutations = {
}, },
addFavs(state, { id, favoritedByUsers, currentUser }) { addFavs(state, { id, favoritedByUsers, currentUser }) {
const newStatus = state.allStatusesObject[id] const newStatus = state.allStatusesObject[id]
newStatus.favoritedBy = favoritedByUsers.filter((_) => _) newStatus.favoritedBy = favoritedByUsers.filter(Boolean)
// favorites stats can be incorrect based on polling condition, let's update them using the most recent data // favorites stats can be incorrect based on polling condition, let's update them using the most recent data
newStatus.fave_num = newStatus.favoritedBy.length newStatus.fave_num = newStatus.favoritedBy.length
newStatus.favorited = !!newStatus.favoritedBy.find( newStatus.favorited = !!newStatus.favoritedBy.find(
@ -879,7 +879,7 @@ const statuses = {
store.commit('addNewUsers', data.accounts) store.commit('addNewUsers', data.accounts)
store.commit( store.commit(
'addNewUsers', 'addNewUsers',
data.statuses.map((s) => s.user).filter((u) => u), data.statuses.map((s) => s.user).filter(Boolean),
) )
store.dispatch('addNewStatuses', { store.dispatch('addNewStatuses', {
statuses: data.statuses, statuses: data.statuses,

View file

@ -75,13 +75,13 @@ const mergeArrayLength = (oldValue, newValue) => {
} }
} }
const getNotificationPermission = () => { const getNotificationPermission = async () => {
const Notification = window.Notification const Notification = window.Notification
if (!Notification) return Promise.resolve(null) if (!Notification) return null
if (Notification.permission === 'default') if (Notification.permission === 'default')
return Notification.requestPermission() return Notification.requestPermission()
return Promise.resolve(Notification.permission) return Notification.permission
} }
const blockUser = (store, args) => { const blockUser = (store, args) => {
@ -271,7 +271,7 @@ export const mutations = {
state.currentUser.blockIds = blockIds state.currentUser.blockIds = blockIds
}, },
addBlockId(state, blockId) { addBlockId(state, blockId) {
if (state.currentUser.blockIds.indexOf(blockId) === -1) { if (state.currentUser.blockIds.includes(blockId)) {
state.currentUser.blockIds.push(blockId) state.currentUser.blockIds.push(blockId)
} }
}, },
@ -285,7 +285,7 @@ export const mutations = {
state.currentUser.muteIdsMaxId = muteIdsMaxId state.currentUser.muteIdsMaxId = muteIdsMaxId
}, },
addMuteId(state, muteId) { addMuteId(state, muteId) {
if (state.currentUser.muteIds.indexOf(muteId) === -1) { if (state.currentUser.muteIds.includes(muteId)) {
state.currentUser.muteIds.push(muteId) state.currentUser.muteIds.push(muteId)
} }
}, },
@ -293,7 +293,7 @@ export const mutations = {
state.currentUser.domainMutes = domainMutes state.currentUser.domainMutes = domainMutes
}, },
addDomainMute(state, domain) { addDomainMute(state, domain) {
if (state.currentUser.domainMutes.indexOf(domain) === -1) { if (state.currentUser.domainMutes.includes(domain)) {
state.currentUser.domainMutes.push(domain) state.currentUser.domainMutes.push(domain)
} }
}, },
@ -385,12 +385,12 @@ const users = {
mutations, mutations,
getters, getters,
actions: { actions: {
fetchUserIfMissing(store, id) { async fetchUserIfMissing(store, id) {
const user = store.getters.findUser(id) const user = store.getters.findUser(id)
if (!user) { if (!user) {
return store.dispatch('fetchUser', id) return store.dispatch('fetchUser', id)
} else { } else {
return Promise.resolve(user) return user
} }
}, },
updateUserAdminData(store, { userAdminData }) { updateUserAdminData(store, { userAdminData }) {
@ -640,7 +640,7 @@ const users = {
}, },
addNewNotifications(store, { notifications }) { addNewNotifications(store, { notifications }) {
const users = map(notifications, 'from_profile') const users = map(notifications, 'from_profile')
const targetUsers = map(notifications, 'target').filter((_) => _) const targetUsers = map(notifications, 'target').filter(Boolean)
const notificationIds = notifications.map((_) => _.id) const notificationIds = notifications.map((_) => _.id)
store.commit('addNewUsers', users) store.commit('addNewUsers', users)
store.commit('addNewUsers', targetUsers) store.commit('addNewUsers', targetUsers)

View file

@ -148,6 +148,14 @@
background-image: background-image:
linear-gradient(to bottom, var(--background), var(--background)), linear-gradient(to bottom, var(--background), var(--background)),
linear-gradient(to bottom, var(--__panel-background), var(--__panel-background)); linear-gradient(to bottom, var(--__panel-background), var(--__panel-background));
overflow: hidden;
text-overflow: ellipsis;
.title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
--_shadow: var(--shadow); --_shadow: var(--shadow);

View file

@ -12,10 +12,7 @@ export const maybeShowChatNotification = (chat) => {
body: chat.lastMessage.content, body: chat.lastMessage.content,
} }
if ( if (chat.lastMessage.attachment?.type === 'image') {
chat.lastMessage.attachment &&
chat.lastMessage.attachment.type === 'image'
) {
opts.image = chat.lastMessage.attachment.preview_url opts.image = chat.lastMessage.attachment.preview_url
} }

View file

@ -11,7 +11,7 @@ import { contrastRatio, convert, invertLightness } from 'chromatism'
* @param {Number} [b] - Blue component * @param {Number} [b] - Blue component
*/ */
export const rgb2hex = (r, g, b) => { export const rgb2hex = (r, g, b) => {
if (r === null || typeof r === 'undefined') { if (r === null || r === undefined) {
return undefined return undefined
} }
// TODO: clean up this mess // TODO: clean up this mess
@ -130,7 +130,7 @@ export const arithmeticBlend = (origin, value, operator) => {
* @returns {Object} sRGB of resulting color * @returns {Object} sRGB of resulting color
*/ */
export const alphaBlend = (fg, fga, bg) => { export const alphaBlend = (fg, fga, bg) => {
if (fga === 1 || typeof fga === 'undefined') { if (fga === 1 || fga === undefined) {
return fg return fg
} }
@ -210,16 +210,16 @@ export const rgba2css = function (rgba) {
} }
if (rgba !== null) { if (rgba !== null) {
if (rgba.r !== undefined && !isNaN(rgba.r)) { if (rgba.r !== undefined && !Number.isNaN(rgba.r)) {
base.r = rgba.r base.r = rgba.r
} }
if (rgba.g !== undefined && !isNaN(rgba.g)) { if (rgba.g !== undefined && !Number.isNaN(rgba.g)) {
base.g = rgba.g base.g = rgba.g
} }
if (rgba.b !== undefined && !isNaN(rgba.b)) { if (rgba.b !== undefined && !Number.isNaN(rgba.b)) {
base.b = rgba.b base.b = rgba.b
} }
if (rgba.a !== undefined && !isNaN(rgba.a)) { if (rgba.a !== undefined && !Number.isNaN(rgba.a)) {
base.a = rgba.a base.a = rgba.a
} }
} else { } else {

View file

@ -62,8 +62,8 @@ export const parseUser = (data) => {
}) })
output.fields_text = data.fields.map((field) => { output.fields_text = data.fields.map((field) => {
return { return {
name: unescape(field.name.replace(/<[^>]*>/g, '')), name: unescape(field.name.replaceAll(/<[^>]*>/g, '')),
value: unescape(field.value.replace(/<[^>]*>/g, '')), value: unescape(field.value.replaceAll(/<[^>]*>/g, '')),
} }
}) })
@ -191,7 +191,7 @@ export const parseUser = (data) => {
// Convert punycode to unicode for UI // Convert punycode to unicode for UI
output.screen_name_ui = output.screen_name output.screen_name_ui = output.screen_name
if (output.screen_name && output.screen_name.includes('@')) { if (output.screen_name?.includes('@')) {
const parts = output.screen_name.split('@') const parts = output.screen_name.split('@')
const unicodeDomain = punycode.toUnicode(parts[1]) const unicodeDomain = punycode.toUnicode(parts[1])
if (unicodeDomain !== parts[1]) { if (unicodeDomain !== parts[1]) {

View file

@ -3,7 +3,7 @@ import { capitalize } from 'lodash'
function humanizeErrors(errors) { function humanizeErrors(errors) {
return Object.entries(errors).reduce((errs, [k, val]) => { return Object.entries(errors).reduce((errs, [k, val]) => {
const message = val.reduce((acc, message) => { const message = val.reduce((acc, message) => {
const key = capitalize(k.replace(/_/g, ' ')) const key = capitalize(k.replaceAll('_', ' '))
return acc + [key, message].join(' ') + '. ' return acc + [key, message].join(' ') + '. '
}, '') }, '')
return [...errs, message] return [...errs, message]

View file

@ -43,7 +43,7 @@ export const fileTypeExt = (url) => {
} }
export const fileMatchesSomeType = (types, file) => export const fileMatchesSomeType = (types, file) =>
types.some((type) => fileType(file.mimetype) === type) types.includes(fileType(file.mimetype))
const fileTypeService = { const fileTypeService = {
fileType, fileType,

View file

@ -25,7 +25,7 @@ const visibleTypes = (notificationVisibility) => {
notificationVisibility.emojiReactions && 'pleroma:emoji_reaction', notificationVisibility.emojiReactions && 'pleroma:emoji_reaction',
notificationVisibility.reports && 'pleroma:report', notificationVisibility.reports && 'pleroma:report',
notificationVisibility.polls && 'poll', notificationVisibility.polls && 'poll',
].filter((_) => _) ].filter(Boolean)
} }
const statusNotifications = new Set([ const statusNotifications = new Set([
@ -95,9 +95,7 @@ export const filteredNotificationsFromStore = (
types, types,
) => { ) => {
// map is just to clone the array since sort mutates it and it causes some issues // map is just to clone the array since sort mutates it and it causes some issues
const sortedNotifications = notificationsFromStore(store) const sortedNotifications = notificationsFromStore(store).sort(sortById)
.map((_) => _)
.sort(sortById)
// TODO implement sorting elsewhere and make it optional // TODO implement sorting elsewhere and make it optional
return sortedNotifications.filter((notification) => return sortedNotifications.filter((notification) =>
(types || visibleTypes(notificationVisibility)).includes(notification.type), (types || visibleTypes(notificationVisibility)).includes(notification.type),
@ -175,13 +173,7 @@ export const prepareNotificationObject = (notification, i18n) => {
} }
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow... // Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
if ( if (!status.nsfw && status?.attachments?.[0]?.mimetype.startsWith('image/')) {
status &&
status.attachments &&
status.attachments.length > 0 &&
!status.nsfw &&
status.attachments[0].mimetype.startsWith('image/')
) {
notifObj.image = status.attachments[0].url notifObj.image = status.attachments[0].url
} }

View file

@ -25,13 +25,13 @@ const createRuffleService = () => {
script.src = '/static/ruffle/ruffle.js' script.src = '/static/ruffle/ruffle.js'
script.type = 'text/javascript' script.type = 'text/javascript'
script.onerror = (e) => { script.onerror = (e) => {
reject(e) reject(new Error('Ruffle script errorred', e))
} }
script.onabort = (e) => { script.onabort = (e) => {
reject(e) reject(new Error('Ruffle script aborted', e))
} }
script.oncancel = (e) => { script.oncancel = (e) => {
reject(e) reject(new Error('Ruffle script cancelled', e))
} }
script.onload = () => { script.onload = () => {
ruffleInstance = window.RufflePlayer ruffleInstance = window.RufflePlayer

View file

@ -88,5 +88,5 @@ export const muteFilterHits = (muteFilters, status) => {
} }
} }
}) })
.filter((_) => _) .filter(Boolean)
} }

View file

@ -29,7 +29,7 @@ export const createStyleSheet = (id, priority = 1000) => {
addRule(rule) { addRule(rule) {
let newRule = rule let newRule = rule
if (!CSS.supports?.('backdrop-filter', 'blur()')) { if (!CSS.supports?.('backdrop-filter', 'blur()')) {
newRule = newRule.replace(/backdrop-filter:[^;]+;/g, '') // Remove backdrop-filter newRule = newRule.replaceAll(/backdrop-filter:[^;]+;/g, '') // Remove backdrop-filter
} }
if (newRule.startsWith('::-webkit')) { if (newRule.startsWith('::-webkit')) {
@ -44,7 +44,7 @@ export const createStyleSheet = (id, priority = 1000) => {
} }
this.rules.push( this.rules.push(
newRule.replace(/var\(--shadowFilter\)[^;]*;/g, ''), // Remove shadowFilter references newRule.replaceAll(/var\(--shadowFilter\)[^;]*;/g, ''), // Remove shadowFilter references
) )
}, },
} }
@ -92,6 +92,11 @@ export const adoptStyleSheets = throttle(() => {
const EAGER_STYLE_ID = 'pleroma-eager-styles' const EAGER_STYLE_ID = 'pleroma-eager-styles'
const LAZY_STYLE_ID = 'pleroma-lazy-styles' const LAZY_STYLE_ID = 'pleroma-lazy-styles'
export const hasInvalidCachedThemeRules = (data) =>
data
.flat()
.some((rule) => /--(?:mono)?font:\s*\[object Object\](?:;|$)/i.test(rule))
const generateTheme = (inputRuleset, callbacks, debug) => { const generateTheme = (inputRuleset, callbacks, debug) => {
const { const {
onNewRule = () => { onNewRule = () => {
@ -151,7 +156,8 @@ export const tryLoadCache = async () => {
if ( if (
cache.engineChecksum === getEngineChecksum() && cache.engineChecksum === getEngineChecksum() &&
cache.checksum !== undefined && cache.checksum !== undefined &&
cache.checksum === useMergedConfigStore().mergedConfig.themeChecksum cache.checksum === useMergedConfigStore().mergedConfig.themeChecksum &&
!hasInvalidCachedThemeRules(cache.data)
) { ) {
const eagerStyles = createStyleSheet(EAGER_STYLE_ID, 10) const eagerStyles = createStyleSheet(EAGER_STYLE_ID, 10)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID, 20) const lazyStyles = createStyleSheet(LAZY_STYLE_ID, 20)
@ -318,7 +324,7 @@ export const getResourcesIndex = async (url, parser = noop) => {
const resourceTransform = (resources) => { const resourceTransform = (resources) => {
return Object.entries(resources).map(([k, v]) => { return Object.entries(resources).map(([k, v]) => {
if (typeof v === 'object') { if (typeof v === 'object') {
return [k, () => Promise.resolve(v)] return [k, () => v]
} else if (typeof v === 'string') { } else if (typeof v === 'string') {
return [ return [
k, k,
@ -362,11 +368,9 @@ export const getResourcesIndex = async (url, parser = noop) => {
const total = [...custom, ...builtin] const total = [...custom, ...builtin]
if (total.length === 0) { if (total.length === 0) {
return Promise.reject( throw new Error(
new Error( `Resource at ${url} and ${customUrl} completely unavailable. Panicking`,
`Resource at ${url} and ${customUrl} completely unavailable. Panicking`,
),
) )
} }
return Promise.resolve(Object.fromEntries(total)) return Object.fromEntries(total)
} }

View file

@ -1,7 +1,9 @@
/* global process */ /* global process */
function urlBase64ToUint8Array(base64String) { function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4) const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/') const base64 = (base64String + padding)
.replaceAll('-', '+')
.replaceAll('_', '/')
const rawData = window.atob(base64) const rawData = window.atob(base64)
return Uint8Array.from([...rawData].map((char) => char.codePointAt(0))) return Uint8Array.from([...rawData].map((char) => char.codePointAt(0)))
@ -26,10 +28,8 @@ function getOrCreateServiceWorker() {
} }
function subscribePush(registration, isEnabled, vapidPublicKey) { function subscribePush(registration, isEnabled, vapidPublicKey) {
if (!isEnabled) if (!isEnabled) throw new Error('Web Push is disabled in config')
return Promise.reject(new Error('Web Push is disabled in config')) if (!vapidPublicKey) throw new Error('VAPID public key is not found')
if (!vapidPublicKey)
return Promise.reject(new Error('VAPID public key is not found'))
const subscribeOptions = { const subscribeOptions = {
userVisibleOnly: false, userVisibleOnly: false,
@ -38,10 +38,10 @@ function subscribePush(registration, isEnabled, vapidPublicKey) {
return registration.pushManager.subscribe(subscribeOptions) return registration.pushManager.subscribe(subscribeOptions)
} }
function unsubscribePush(registration) { async function unsubscribePush(registration) {
return registration.pushManager.getSubscription().then((subscription) => { return registration.pushManager.getSubscription().then((subscription) => {
if (subscription === null) { if (subscription === null) {
return Promise.resolve('No subscription') return 'No subscription'
} }
return subscription.unsubscribe() return subscription.unsubscribe()
}) })

View file

@ -165,7 +165,7 @@ export const getCssRules = (rules, debug) =>
header, header,
directives, directives,
rule.component === 'Text' && rule.component === 'Text' &&
rule.state.indexOf('faint') < 0 && !rule.state.includes('faint') &&
rule.directives.textNoCssColor !== 'yes' rule.directives.textNoCssColor !== 'yes'
? ' color: var(--text);' ? ' color: var(--text);'
: '', : '',

View file

@ -24,7 +24,7 @@ export const getAllPossibleCombinations = (array) => {
const nonSelf = array.filter((x) => !selfSet.has(x)) const nonSelf = array.filter((x) => !selfSet.has(x))
return nonSelf.map((x) => [...self, x]) return nonSelf.map((x) => [...self, x])
}) })
const flatCombos = newCombos.reduce((acc, x) => [...acc, ...x], []) const flatCombos = newCombos.flat()
const uniqueComboStrings = new Set() const uniqueComboStrings = new Set()
const uniqueCombos = flatCombos.map(sortBy).filter((x) => { const uniqueCombos = flatCombos.map(sortBy).filter((x) => {
if (uniqueComboStrings.has(x.join())) { if (uniqueComboStrings.has(x.join())) {
@ -36,7 +36,7 @@ export const getAllPossibleCombinations = (array) => {
}) })
combos.push(uniqueCombos) combos.push(uniqueCombos)
} }
return combos.reduce((acc, x) => [...acc, ...x], []) return combos.flat()
} }
/** /**
@ -110,13 +110,6 @@ export const genericRuleToSelector =
let arraySelector = Array.isArray(selector) ? selector : [selector] let arraySelector = Array.isArray(selector) ? selector : [selector]
if (ignoreOutOfTreeSelector || liteMode) if (ignoreOutOfTreeSelector || liteMode)
arraySelector = [arraySelector[0]] arraySelector = [arraySelector[0]]
arraySelector
.sort((a) => {
if (a.startsWith(':')) return 1
if (/^[a-z]/.exec(a)) return -1
else return 0
})
.join('')
return arraySelector return arraySelector
}) })

View file

@ -191,14 +191,16 @@ export const convertTheme2To3 = (data) => {
newRules.push(rule) newRules.push(rule)
if (rule.component === 'Button') { if (rule.component === 'Button') {
newRules.push({ ...rule, component: 'ScrollbarElement' }) newRules.push(
newRules.push({ ...rule, component: 'Tab' }) { ...rule, component: 'ScrollbarElement' },
newRules.push({ { ...rule, component: 'Tab' },
...rule, {
component: 'Tab', ...rule,
state: ['active'], component: 'Tab',
directives: { opacity: 0 }, state: ['active'],
}) directives: { opacity: 0 },
},
)
} }
if (rule.component === 'Panel') { if (rule.component === 'Panel') {
newRules.push({ ...rule, component: 'Post' }) newRules.push({ ...rule, component: 'Post' })
@ -250,8 +252,10 @@ export const convertTheme2To3 = (data) => {
} }
newRules.push(rule) newRules.push(rule)
if (rule.component === 'Button') { if (rule.component === 'Button') {
newRules.push({ ...rule, component: 'ScrollbarElement' }) newRules.push(
newRules.push({ ...rule, component: 'Tab' }) { ...rule, component: 'ScrollbarElement' },
{ ...rule, component: 'Tab' },
)
} }
}) })
return newRules return newRules
@ -349,16 +353,20 @@ export const convertTheme2To3 = (data) => {
newRules.push({ ...rule, parent: { component: 'Notification' } }) newRules.push({ ...rule, parent: { component: 'Notification' } })
} }
if (key === 'buttonPressed') { if (key === 'buttonPressed') {
newRules.push({ ...rule, state: ['toggled'] }) newRules.push(
newRules.push({ ...rule, state: ['toggled', 'focus'] }) { ...rule, state: ['toggled'] },
newRules.push({ ...rule, state: ['pressed', 'focus'] }) { ...rule, state: ['toggled', 'focus'] },
newRules.push({ ...rule, state: ['toggled', 'focus', 'hover'] }) { ...rule, state: ['pressed', 'focus'] },
newRules.push({ ...rule, state: ['pressed', 'focus', 'hover'] }) { ...rule, state: ['toggled', 'focus', 'hover'] },
{ ...rule, state: ['pressed', 'focus', 'hover'] },
)
} }
if (rule.component === 'Button') { if (rule.component === 'Button') {
newRules.push({ ...rule, component: 'ScrollbarElement' }) newRules.push(
newRules.push({ ...rule, component: 'Tab' }) { ...rule, component: 'ScrollbarElement' },
{ ...rule, component: 'Tab' },
)
} }
}) })
return newRules return newRules
@ -512,15 +520,17 @@ export const convertTheme2To3 = (data) => {
{ ...newRule, component: 'Tab' }, { ...newRule, component: 'Tab' },
{ ...newRule, component: 'ScrollbarElement' }, { ...newRule, component: 'ScrollbarElement' },
] ]
if (newRule.state?.indexOf('toggled') >= 0) { if (newRule.state?.includes('toggled')) {
rules.push({ ...newRule, state: [...newRule.state, 'focused'] }) rules.push(
rules.push({ ...newRule, state: [...newRule.state, 'hover'] }) { ...newRule, state: [...newRule.state, 'focused'] },
rules.push({ { ...newRule, state: [...newRule.state, 'hover'] },
...newRule, {
state: [...newRule.state, 'hover', 'focused'], ...newRule,
}) state: [...newRule.state, 'hover', 'focused'],
},
)
} }
if (newRule.state?.indexOf('hover') >= 0) { if (newRule.state?.includes('hover')) {
rules.push({ ...newRule, state: [...newRule.state, 'focused'] }) rules.push({ ...newRule, state: [...newRule.state, 'focused'] })
} }
return rules return rules
@ -559,9 +569,9 @@ export const convertTheme2To3 = (data) => {
const flatExtRules = extendedRules const flatExtRules = extendedRules
.filter(Boolean) .filter(Boolean)
.reduce((acc, x) => [...acc, ...x], []) .flat()
.filter(Boolean) .filter(Boolean)
.reduce((acc, x) => [...acc, ...x], []) .flat()
return [ return [
generateRoot(), generateRoot(),

View file

@ -244,10 +244,7 @@ export const OPACITIES = Object.entries(SLOT_INHERITANCE).reduce((acc, [k]) => {
...acc, ...acc,
[opacity]: { [opacity]: {
defaultValue: DEFAULT_OPACITY[opacity] || 1, defaultValue: DEFAULT_OPACITY[opacity] || 1,
affectedSlots: [ affectedSlots: [...(acc[opacity]?.affectedSlots || []), k],
...((acc[opacity] && acc[opacity].affectedSlots) || []),
k,
],
}, },
} }
} else { } else {
@ -413,7 +410,7 @@ export const getColors = (sourceColors, sourceOpacity) =>
outputColor.a = Number( outputColor.a = Number(
opacityOverriden opacityOverriden
? sourceOpacity[opacitySlot] ? sourceOpacity[opacitySlot]
: (OPACITIES[opacitySlot] || {}).defaultValue, : OPACITIES[opacitySlot]?.defaultValue,
) )
} }
} }
@ -460,7 +457,7 @@ export const generatePreset = (input) => {
return composePreset( return composePreset(
colors, colors,
generateRadii(input), generateRadii(input),
generateShadows(input, colors.theme.colors, colors.mod), generateShadows(input, colors.theme.colors),
generateFonts(input), generateFonts(input),
) )
} }

View file

@ -262,7 +262,7 @@ export const init = ({
...r, ...r,
})), })),
) )
.reduce((acc, arr) => [...acc, ...arr], []), .flat(),
...inputRuleset, ...inputRuleset,
].map((rule) => { ].map((rule) => {
normalizeCombination(rule) normalizeCombination(rule)
@ -690,11 +690,11 @@ export const init = ({
.map((combination) => ['normal', ...combination]) .map((combination) => ['normal', ...combination])
.filter((combo) => { .filter((combo) => {
// Optimization: filter out some hard-coded combinations that don't make sense // Optimization: filter out some hard-coded combinations that don't make sense
if (combo.indexOf('disabled') >= 0) { if (combo.includes('disabled')) {
return !( return !(
combo.indexOf('hover') >= 0 || combo.includes('hover') ||
combo.indexOf('focused') >= 0 || combo.includes('focused') ||
combo.indexOf('pressed') >= 0 combo.includes('pressed')
) )
} }
return true return true
@ -705,13 +705,13 @@ export const init = ({
.map((variant) => { .map((variant) => {
return stateCombinations.map((state) => ({ variant, state })) return stateCombinations.map((state) => ({ variant, state }))
}) })
.reduce((acc, x) => [...acc, ...x], []) .flat()
stateVariantCombination.forEach((combination) => { stateVariantCombination.forEach((combination) => {
combination.component = component.name combination.component = component.name
combination.lazy = component.lazy || parent?.lazy combination.lazy = component.lazy || parent?.lazy
combination.parent = parent combination.parent = parent
if (!liteMode && combination.state.indexOf('hover') >= 0) { if (!liteMode && combination.state.includes('hover')) {
combination.lazy = true combination.lazy = true
} }

View file

@ -47,7 +47,7 @@ const highlightStyle = (prefs) => {
const highlightClass = (user) => { const highlightClass = (user) => {
return ( return (
'USER____' + user.screen_name?.replace(/\./g, '_').replace(/@/g, '_AT_') 'USER____' + user.screen_name?.replaceAll('.', '_').replaceAll('@', '_AT_')
) )
} }

View file

@ -61,7 +61,7 @@ export const useChatsStore = defineStore('chats', {
addNewChats(chats) { addNewChats(chats) {
window.vuex.commit( window.vuex.commit(
'addNewUsers', 'addNewUsers',
chats.map((k) => k.account).filter((k) => k), chats.map((k) => k.account).filter(Boolean),
) )
chats.forEach((updatedChat) => { chats.forEach((updatedChat) => {
@ -69,8 +69,7 @@ export const useChatsStore = defineStore('chats', {
if (chat) { if (chat) {
const isNewMessage = const isNewMessage =
(chat.lastMessage && chat.lastMessage.id) !== chat.lastMessage?.id !== updatedChat.lastMessage?.id
(updatedChat.lastMessage && updatedChat.lastMessage.id)
chat.lastMessage = updatedChat.lastMessage chat.lastMessage = updatedChat.lastMessage
chat.unread = updatedChat.unread chat.unread = updatedChat.unread
chat.updated_at = updatedChat.updated_at chat.updated_at = updatedChat.updated_at

View file

@ -120,23 +120,19 @@ export const useEmojiStore = defineStore('emoji', {
}, {}) }, {})
}, },
standardEmojiList(state) { standardEmojiList(state) {
return ( return SORTED_EMOJI_GROUP_IDS.map((groupId) =>
SORTED_EMOJI_GROUP_IDS.map((groupId) => (this.emoji[groupId] || []).map((k) =>
(this.emoji[groupId] || []).map((k) => injectAnnotations(k, this.unicodeEmojiAnnotations),
injectAnnotations(k, this.unicodeEmojiAnnotations), ),
), ).flat()
).reduce((a, b) => a.concat(b), []) ?? []
)
}, },
standardEmojiGroupList(state) { standardEmojiGroupList(state) {
return ( return SORTED_EMOJI_GROUP_IDS.map((groupId) => ({
SORTED_EMOJI_GROUP_IDS.map((groupId) => ({ id: groupId,
id: groupId, emojis: (this.emoji[groupId] || []).map((k) =>
emojis: (this.emoji[groupId] || []).map((k) => injectAnnotations(k, this.unicodeEmojiAnnotations),
injectAnnotations(k, this.unicodeEmojiAnnotations), ),
), }))
})) ?? []
)
}, },
}, },
actions: { actions: {
@ -146,7 +142,7 @@ export const useEmojiStore = defineStore('emoji', {
async getStaticEmoji() { async getStaticEmoji() {
try { try {
// See build/emojis_plugin for more details // See build/emojis_plugin for more details
const values = (await import('/src/assets/emoji.json')).default const values = (await import('src/assets/emoji.json')).default
const emoji = Object.keys(values).reduce((res, groupId) => { const emoji = Object.keys(values).reduce((res, groupId) => {
res[groupId] = values[groupId].map((e) => ({ res[groupId] = values[groupId].map((e) => ({
@ -231,7 +227,7 @@ export const useEmojiStore = defineStore('emoji', {
.then((allPacks) => { .then((allPacks) => {
// Sort by key // Sort by key
return Object.keys(allPacks) return Object.keys(allPacks)
.sort() .sort((a, b) => a.localeCompare(b))
.reduce((acc, key) => { .reduce((acc, key) => {
if (key.length === 0) return acc if (key.length === 0) return acc
acc[key] = allPacks[key] acc[key] = allPacks[key]

Some files were not shown because too many files have changed in this diff Show more