Compare commits

...

33 commits

Author SHA1 Message Date
Henry Jameson
f94dacfcf9 Merge remote-tracking branch 'origin/develop' into shigusegubu-themes3 2026-08-05 19:45:17 +03:00
Henry Jameson
4d17aaaad6 Merge remote-tracking branch 'origin/develop' into shigusegubu-themes3 2026-08-05 19:45:09 +03:00
HJ
8d3d7ab7ae Merge pull request 'fix-sonarqube-sw' (#3537) from fix-sonarqube-sw into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma-fe/pulls/3537
2026-08-05 16:44:54 +00:00
Henry Jameson
2d52ce8017 lint 2026-08-05 19:40:46 +03:00
HJ
51c00f6807 Merge pull request 'fix-third-column' (#3536) from fix-third-column into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma-fe/pulls/3536
2026-08-05 16:36:34 +00:00
Henry Jameson
621966d812 another security issue found by sonarqube (might need to extend later) 2026-08-05 19:35:58 +03:00
Henry Jameson
2f207a274a fix security issue sonarqube detected 2026-08-05 19:27:50 +03:00
Henry Jameson
a3bc98589e lint 2026-08-05 19:18:33 +03:00
Henry Jameson
92b8d1607f lint 2026-08-05 19:15:37 +03:00
Henry Jameson
8e19fbf23e changelog 2026-08-05 19:13:58 +03:00
Henry Jameson
d202b94104 fix third column setting change not affecting UI immideately 2026-08-05 19:13:07 +03:00
HJ
1b7e35fcb3 Merge pull request 'fix #3525' (#3535) from fix-avatar-upload into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma-fe/pulls/3535
2026-08-05 15:44:46 +00:00
Henry Jameson
c20d47ee31 changelog 2026-08-05 18:40:53 +03:00
Henry Jameson
a4cd10aa2e fix #3525 2026-08-05 18:39:49 +03:00
Henry Jameson
d23ec577a0 partial: false 2026-08-05 18:13:48 +03:00
HJ
76c53ddc7a Merge pull request 'fix switching main PSF scope to direct locking scope' (#3534) from fix-dm-post-status-form into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma-fe/pulls/3534
2026-08-05 15:11:07 +00:00
Henry Jameson
22a27741e8 fix switching main PSF scope to direct locking scope 2026-08-05 18:08:29 +03:00
HJ
e9fb826820 Merge pull request 'CI improvements' (#3533) from ci-improvements into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma-fe/pulls/3533
2026-08-05 15:04:04 +00:00
Henry Jameson
d915bb2f9f space 2026-08-05 18:02:45 +03:00
Henry Jameson
7dce6130f8 more badges 2026-08-05 18:01:47 +03:00
Henry Jameson
9112155f81 small update for a better way of setting up proxy 2026-08-05 17:52:38 +03:00
Henry Jameson
9f5bb82174 status badges for readme.md 2026-08-05 17:49:33 +03:00
Henry Jameson
23934a3003 cleanup 2026-08-05 17:45:28 +03:00
Henry Jameson
4999ee10e5 lost file 2026-08-05 17:43:20 +03:00
Henry Jameson
55816a0da4 clone fix 2026-08-05 17:39:57 +03:00
HJ
de2b6fba8a Merge pull request 'quote-small-fix' (#3532) from quote-small-fix into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma-fe/pulls/3532
2026-08-05 14:34:55 +00:00
Henry Jameson
9c51ff467e cleanup 2026-08-05 17:34:35 +03:00
Henry Jameson
24b715d676 reorganize our CI stuff 2026-08-05 17:31:06 +03:00
Henry Jameson
4c295601c2 lint 2026-08-05 17:02:04 +03:00
Henry Jameson
60d75c1ec7 cleanup 2026-08-05 16:56:27 +03:00
Henry Jameson
fcf53d640f poll form refactor + improvements 2026-08-05 16:56:15 +03:00
Henry Jameson
ec8995ac92 Merge remote-tracking branch 'origin/develop' into quote-small-fix 2026-08-05 16:42:41 +03:00
Henry Jameson
60c3dddca3 small cleanup in quote code 2026-08-05 16:31:50 +03:00
23 changed files with 180 additions and 80 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
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
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
memory: 'high'
depends_on:
- build
- test
variables:
artifacts_uploader_settings: &artifacts_uploader_settings
user:

View file

@ -10,6 +10,9 @@ labels:
platform: linux/amd64
memory: 'high'
depends_on:
- build
variables:
artifacts_uploader_settings: &artifacts_uploader_settings
user:
@ -45,20 +48,6 @@ steps:
exit 1
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:
image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0
when:

View file

@ -1,12 +1,14 @@
# Pleroma-FE
# Pleroma-FE
> Highly-customizable frontend designed for Pleroma.
![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
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.
@ -32,9 +34,14 @@ yarn unit
# 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:
* `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.
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 {
try {
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()
.trim()
} catch (e) {

View file

@ -0,0 +1 @@
fix image cropper error preventing avatar upload

View file

@ -0,0 +1 @@
Fixed layout selector (third column/reverse) not working immideately upon change

View file

@ -8,7 +8,8 @@
"dev": "node build/update-emoji.js && vite dev",
"build": "node build/update-emoji.js && vite build",
"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",
"e2e:pw": "playwright test --config test/e2e-playwright/playwright.config.mjs",
"e2e": "sh ./tools/e2e/run.sh",

View file

@ -74,6 +74,8 @@ export default {
},
data: () => ({
mobileActivePanel: 'timeline',
updateMobileState: null,
updateScrollState: null,
}),
provide() {
return {
@ -211,31 +213,32 @@ export default {
hideShoutbox() {
return this.isChats || useMergedConfigStore().mergedConfig.hideShoutbox
},
thirdColumnMode() {
return this.mergedConfig.thirdColumnMode
},
reverseSetting() {
return this.mergedConfig.sidebarRight
},
reverseLayout() {
const { thirdColumnMode, sidebarRight: reverseSetting } =
useMergedConfigStore().mergedConfig
if (this.layoutType !== 'wide') {
return reverseSetting
return this.reverseSetting
} else {
return thirdColumnMode === 'notifications'
? reverseSetting
: !reverseSetting
return this.thirdColumnMode === 'notifications'
? this.reverseSetting
: !this.reverseSetting
}
},
noSticky() {
return useMergedConfigStore().mergedConfig.disableStickyHeaders
return this.mergedConfig.disableStickyHeaders
},
showScrollbars() {
return useMergedConfigStore().mergedConfig.showScrollbars
return this.mergedConfig.showScrollbars
},
scrollParent() {
return window /* this.$refs.appContentRef */
},
showInstanceSpecificPanel() {
return (
this.instanceSpecificPanelPresent &&
!useMergedConfigStore().mergedConfig.hideISP
)
return this.instanceSpecificPanelPresent && !this.mergedConfig.hideISP
},
...mapState(useMergedConfigStore, ['mergedConfig']),
...mapState(useInterfaceStore, [

View file

@ -70,6 +70,9 @@ const ImageCropper = {
)
},
onCropperSelectionChange(event) {
if (!this.$refs.cropperCanvas) {
return // Cropper sends even before component is fully initialized
}
const cropperCanvas = this.$refs.cropperCanvas
const cropperCanvasRect = cropperCanvas.getBoundingClientRect()
const selection = event.detail

View file

@ -134,12 +134,11 @@ const Notifications = {
return this.minimalMode || layoutType === 'mobile'
},
teleportTarget() {
const { layoutType } = useInterfaceStore()
const map = {
wide: '#notifs-column',
mobile: '#mobile-notifications',
}
return map[layoutType] || '#notifs-sidebar'
return map[this.layoutType] || '#notifs-sidebar'
},
popoversZLayer() {
const { layoutType } = useInterfaceStore()
@ -162,6 +161,7 @@ const Notifications = {
},
...mapState(useAnnouncementsStore, ['unreadAnnouncementCount']),
...mapState(useChatsStore, ['unreadChatsCount']),
...mapState(useInterfaceStore, ['layoutType']),
},
mounted() {
this.scrollerRef = this.$refs.root.closest('.column.-scrollable')

View file

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

View file

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

View file

@ -378,8 +378,8 @@ const PostStatusForm = {
set(value) {
if (value) {
this.newStatus.quote = {}
this.newStatus.quote.thread = value
this.newStatus.quote.id = value ? this.repliedStatus.id : ''
this.newStatus.quote.thread = true
this.newStatus.quote.id = this.repliedStatus.id
} else {
this.newStatus.quote = null
}
@ -509,9 +509,7 @@ const PostStatusForm = {
// Error handling
pollContentError() {
return (
this.pollFormVisible && this.newStatus.poll && this.newStatus.poll.error
)
return this.pollFormVisible && this.newStatus.poll?.error
},
// Featureset detection
@ -959,6 +957,7 @@ const PostStatusForm = {
// Quote
toggleQuoteForm() {
// This is for the "attach quote" button
if (this.newStatus.quote?.thread) return
if (!this.hasQuote) {
this.newStatus.quote = {}
this.newStatus.quote.thread = false

View file

@ -234,7 +234,7 @@
ref="scopeSelector"
:show-all="showAllScopes"
:user-default="userDefaultScope"
:original-scope="newStatus.visibility"
:original-scope="repliedStatus?.visibility"
:initial-scope="newStatus.visibility"
@change="changeVis"
/>

View file

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

View file

@ -113,6 +113,7 @@ export default {
localDraft: null,
}
},
emits: ['update:modelValue'],
created() {
if (
this.realDraftMode &&
@ -244,7 +245,9 @@ export default {
},
configSink() {
if (this.path == null) {
return (k, v) => this.$emit('update:modelValue', v)
return () => {
/* no-op */
}
}
switch (this.realSource) {
@ -385,11 +388,13 @@ export default {
if (this.realDraftMode) {
this.draft = this.getValue(e)
} else {
this.$emit('update:modelValue', this.getValue(e))
this.configSink(this.path, this.getValue(e))
}
},
commitDraft() {
if (this.realDraftMode) {
this.$emit('update:modelValue', this.draft)
this.configSink(this.path, this.draft)
}
},

View file

@ -6,6 +6,7 @@ import SharedComputedObject from '../helpers/shared_computed_object.js'
import UnitSetting from '../helpers/unit_setting.vue'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
const GeneralTab = {
@ -46,6 +47,11 @@ const GeneralTab = {
},
...SharedComputedObject(),
},
methods: {
updateLayout() {
useInterfaceStore().setLayoutWidth()
},
},
}
export default GeneralTab

View file

@ -105,6 +105,7 @@
id="thirdColumnMode"
path="thirdColumnMode"
:options="thirdColumnModeOptions"
@change="updateLayout"
>
{{ $t('settings.third_column_mode') }}
</ChoiceSetting>

View file

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

View file

@ -198,6 +198,10 @@ self.addEventListener('push', async (event) => {
self.addEventListener('message', async (event) => {
await setSettings()
const { type, content } = event.data
if (self.location.origin !== event.origin) {
console.error('SW Message with strange origin received', event)
return
}
if (type === 'desktopNotification') {
const { title, ...rest } = content