Merge branch 'fixes-roundup5' into 'develop'

Fixes roundup5

See merge request pleroma/pleroma-fe!2081
This commit is contained in:
HJ 2025-03-13 17:33:47 +00:00
commit 598c569c93
25 changed files with 280 additions and 225 deletions

View file

View file

@ -129,6 +129,21 @@ PanelHeader {
background: --fg
}
PanelHeader ButtonUnstyled Icon {
textColor: --text;
textAuto: 'no-preserve'
}
PanelHeader Button Icon {
textColor: --text;
textAuto: 'no-preserve'
}
PanelHeader Button Text {
textColor: --text;
textAuto: 'no-preserve'
}
Tab:hover {
background: --bg;
shadow: --buttonDefaultBevel
@ -172,6 +187,14 @@ MenuItem:hover {
background: --fg
}
MenuItem:active {
background: --fg
}
MenuItem:active:hover {
background: --fg
}
Popover {
shadow: --buttonDefaultBevel, 5 5 0 0 #000000 / 0.2;
roundness: 0

View file

@ -15,7 +15,9 @@ export default {
},
{
component: 'Button',
parent: { component: 'Attachment' },
parent: {
component: 'Attachment'
},
directives: {
background: '#FFFFFF',
opacity: 0.5

View file

@ -99,7 +99,7 @@ export default {
{
state: ['disabled'],
directives: {
background: '$blend(--accent 0.25 --parent)',
background: '$blend(--inheritedBackground 0.25 --parent)',
shadow: ['--buttonDefaultBevel']
}
},

View file

@ -1,7 +1,7 @@
<template>
<label
class="checkbox"
:class="{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }"
:class="[{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }, radio ? '-radio' : '-checkbox']"
>
<span
v-if="!!$slots.before"
@ -19,9 +19,9 @@
@change="$emit('update:modelValue', $event.target.checked)"
>
<i
class="input -checkbox checkbox-indicator"
class="input checkbox-indicator"
:aria-hidden="true"
:class="{ disabled }"
:class="[{ disabled }, radio ? '-radio' : '-checkbox']"
@transitionend.capture="onTransitionEnd"
/>
<span
@ -37,6 +37,7 @@
<script>
export default {
props: [
'radio',
'modelValue',
'indeterminate',
'disabled'
@ -107,6 +108,19 @@ export default {
box-sizing: border-box;
}
&.-radio {
.checkbox-indicator {
&,
&::before {
border-radius: 9999px;
}
&::before {
content: "•";
}
}
}
.disabled {
.checkbox-indicator::before {
background-color: var(--background);

View file

@ -0,0 +1,44 @@
.login-panel {
.login-form {
display: flex;
flex-direction: column;
padding: 0.6em;
}
.btn {
min-height: 2em;
width: 10em;
}
.register {
flex: 1 1;
}
.login-bottom {
margin-top: 1em;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.form-group {
display: flex;
flex-direction: column;
padding: 0.3em 0.5em 0.6em;
line-height: 24px;
}
.login-error {
display: flex;
line-height: 2;
margin: 0.5em;
animation-name: shakeError;
animation-duration: 0.4s;
animation-timing-function: ease-in-out;
}
.error-message {
flex: 1;
}
}

View file

@ -1,5 +1,5 @@
<template>
<div class="login panel panel-default">
<div class="login-panel panel panel-default">
<!-- Default panel contents -->
<div class="panel-heading">
@ -70,14 +70,13 @@
</div>
</div>
</form>
</div>
<div
v-if="error"
class="form-group"
>
<div class="alert error">
{{ error }}
<div
v-if="error"
class="login-error alert error"
>
<span class="error-message">
{{ error }}
</span>
<button
class="button-unstyled"
@click="clearError"
@ -94,57 +93,4 @@
<script src="./login_form.js"></script>
<style lang="scss">
.login-form {
display: flex;
flex-direction: column;
padding: 0.6em;
.btn {
min-height: 2em;
width: 10em;
}
.register {
flex: 1 1;
}
.login-bottom {
margin-top: 1em;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.form-group {
display: flex;
flex-direction: column;
padding: 0.3em 0.5em 0.6em;
line-height: 24px;
}
.form-bottom {
display: flex;
padding: 0.5em;
height: 32px;
button {
width: 10em;
}
p {
margin: 0.35em;
padding: 0.35em;
display: flex;
}
}
.error {
text-align: center;
animation-name: shakeError;
animation-duration: 0.4s;
animation-timing-function: ease-in-out;
}
}
</style>
<style src="./login_form.scss"/>

View file

@ -238,4 +238,4 @@
<script src="./mrf_transparency_panel.js"></script>
<style src="./mrf_transparency_panel.scss" lang="scss"/>
<style src="./mrf_transparency_panel.scss" lang="scss" />

View file

@ -1,7 +1,7 @@
import Timeago from 'components/timeago/timeago.vue'
import genRandomSeed from '../../services/random_seed/random_seed.service.js'
import RichContent from 'components/rich_content/rich_content.jsx'
import { forEach, map } from 'lodash'
import Checkbox from 'components/checkbox/checkbox.vue'
import { usePollsStore } from 'src/stores/polls'
export default {
@ -9,7 +9,8 @@ export default {
props: ['basePoll', 'emoji'],
components: {
Timeago,
RichContent
RichContent,
Checkbox
},
data () {
return {
@ -44,6 +45,13 @@ export default {
expired () {
return (this.poll && this.poll.expired) || false
},
expirationLabel () {
if (this.$store.getters.mergedConfig.useAbsoluteTimeFormat) {
return this.expired ? 'polls.expired_at' : 'polls.expires_at'
} else {
return this.expired ? 'polls.expired' : 'polls.expires_in'
}
},
loggedIn () {
return this.$store.state.users.currentUser
},
@ -78,26 +86,15 @@ export default {
resultTitle (option) {
return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}`
},
activateOption (index) {
// forgive me father: doing checking the radio/checkboxes
// in code because of customized input elements need either
// a) an extra element for the actual graphic, or b) use a
// pseudo element for the label. We use b) which mandates
// using "for" and "id" matching which isn't nice when the
// same poll appears multiple times on the site (notifs and
// timeline for example). With code we can make sure it just
// works without altering the pseudo element implementation.
const allElements = this.$el.querySelectorAll('input')
const clickedElement = this.$el.querySelector(`input[value="${index}"]`)
activateOption (index, value) {
let result
if (this.poll.multiple) {
// Checkboxes, toggle only the clicked one
clickedElement.checked = !clickedElement.checked
result = this.choices || this.options.map(() => false)
} else {
// Radio button, uncheck everything and check the clicked one
forEach(allElements, element => { element.checked = false })
clickedElement.checked = true
result = this.options.map(() => false)
}
this.choices = map(allElements, e => e.checked)
result[index] = value
this.choices = result
},
optionId (index) {
return `poll${this.poll.id}-${index}`

View file

@ -0,0 +1,62 @@
.poll {
.votes {
display: flex;
flex-direction: column;
margin: 0 0 0.5em;
}
.poll-option {
margin: 0.75em 0.5em;
.input {
line-height: inherit;
}
}
.option-result {
height: 100%;
display: flex;
flex-direction: row;
position: relative;
color: var(--textLight);
}
.option-result-label {
display: flex;
align-items: center;
padding: 0.1em 0.25em;
z-index: 1;
word-break: break-word;
}
.result-percentage {
width: 3.5em;
flex-shrink: 0;
}
.result-fill {
height: 100%;
position: absolute;
border-radius: var(--roundness);
top: 0;
left: 0;
transition: width 0.5s;
}
input {
width: 3.5em;
}
&.loading * {
cursor: progress;
}
.poll-vote-button {
padding: 0 1em;
margin-right: 0.5em;
}
.poll-checkbox {
display: none;
}
}

View file

@ -37,36 +37,56 @@
:role="poll.multiple ? 'checkbox' : 'radio'"
:aria-labelledby="`option-vote-${randomSeed}-${index}`"
:aria-checked="choices[index]"
class="input unstyled"
@click="activateOption(index)"
>
<!-- TODO: USE CHECKBOX -->
<input
v-if="poll.multiple"
type="checkbox"
class="input -checkbox poll-checkbox"
<Checkbox
:radio="!poll.multiple"
:disabled="loading"
:value="index"
:model-value="choices[index]"
@update:model-value="value => activateOption(index, value)"
>
<input
v-else
type="radio"
:disabled="loading"
:value="index"
class="input -radio"
>
<label class="option-vote">
<RichContent
:id="`option-vote-${randomSeed}-${index}`"
:html="option.title_html"
:handle-links="false"
:emoji="emoji"
/>
</label>
</Checkbox>
</div>
</div>
</div>
<div class="footer faint">
<p>
<span
v-if="poll.pleroma?.non_anonymous"
:title="$t('polls.non_anonymous_title')"
>
{{ $t('polls.non_anonymous') }}
&nbsp;·&nbsp;
</span>
<span class="total">
<template v-if="typeof poll.voters_count === 'number'">
{{ $t("polls.people_voted_count", { count: poll.voters_count }, poll.voters_count) }}
</template>
<template v-else>
{{ $t("polls.votes_count", { count: poll.votes_count }, poll.votes_count) }}
</template>
<span v-if="expiresAt !== null">
&nbsp;·&nbsp;
</span>
</span>
<span v-if="expiresAt !== null">
<i18n-t
scope="global"
:keypath="expirationLabel"
>
<Timeago
:time="expiresAt"
:auto-update="60"
:now-threshold="0"
/>
</i18n-t>
</span>
</p>
<button
v-if="!showResults"
class="btn button-default poll-vote-button"
@ -76,113 +96,10 @@
>
{{ $t('polls.vote') }}
</button>
<span
v-if="poll.pleroma?.non_anonymous"
:title="$t('polls.non_anonymous_title')"
>
{{ $t('polls.non_anonymous') }}
&nbsp;·&nbsp;
</span>
<div class="total">
<template v-if="typeof poll.voters_count === 'number'">
{{ $t("polls.people_voted_count", { count: poll.voters_count }, poll.voters_count) }}
</template>
<template v-else>
{{ $t("polls.votes_count", { count: poll.votes_count }, poll.votes_count) }}
</template>
<span v-if="expiresAt !== null">
&nbsp;·&nbsp;
</span>
</div>
<span v-if="expiresAt !== null">
<i18n-t
scope="global"
:keypath="expired ? 'polls.expired' : 'polls.expires_in'"
>
<Timeago
:time="expiresAt"
:auto-update="60"
:now-threshold="0"
/>
</i18n-t>
</span>
</div>
</div>
</template>
<script src="./poll.js"></script>
<style lang="scss">
.poll {
.votes {
display: flex;
flex-direction: column;
margin: 0 0 0.5em;
}
.poll-option {
margin: 0.75em 0.5em;
.input {
line-height: inherit;
}
}
.option-result {
height: 100%;
display: flex;
flex-direction: row;
position: relative;
color: var(--textLight);
}
.option-result-label {
display: flex;
align-items: center;
padding: 0.1em 0.25em;
z-index: 1;
word-break: break-word;
}
.result-percentage {
width: 3.5em;
flex-shrink: 0;
}
.result-fill {
height: 100%;
position: absolute;
border-radius: var(--roundness);
top: 0;
left: 0;
transition: width 0.5s;
}
.option-vote {
display: flex;
align-items: center;
}
input {
width: 3.5em;
}
.footer {
display: flex;
align-items: center;
}
&.loading * {
cursor: progress;
}
.poll-vote-button {
padding: 0 0.5em;
margin-right: 0.5em;
}
.poll-checkbox {
display: none;
}
}
</style>
<style src="./poll.scss" lang="scss" />

View file

@ -119,10 +119,13 @@
:key="hashtag.url"
class="status trend search-result"
>
<div class="hashtag">
<router-link :to="{ name: 'tag-timeline', params: { tag: hashtag.name } }">
<router-link
class="list-item hashtag"
:to="{ name: 'tag-timeline', params: { tag: hashtag.name } }"
>
<span class="name">
#{{ hashtag.name }}
</router-link>
</span>
<div v-if="lastHistoryRecord(hashtag)">
<span v-if="lastHistoryRecord(hashtag).accounts == 1">
{{ $t('search.person_talking', { count: lastHistoryRecord(hashtag).accounts }) }}
@ -131,7 +134,7 @@
{{ $t('search.people_talking', { count: lastHistoryRecord(hashtag).accounts }) }}
</span>
</div>
</div>
</router-link>
<div
v-if="lastHistoryRecord(hashtag)"
class="count"
@ -199,10 +202,13 @@
.hashtag {
flex: 1 1 auto;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.name {
color: var(--link);
}
}
.count {

View file

@ -540,6 +540,7 @@
:status="status"
:replying="replying"
@toggle-replying="toggleReplying"
@interacted="e => $emit('interacted')"
/>
</div>
</div>

View file

@ -67,6 +67,9 @@ export default {
'doAction',
'outerClose'
],
emits: [
'interacted'
],
components: {
StatusBookmarkFolderMenu,
EmojiPicker,
@ -120,7 +123,8 @@ export default {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
}
},
doActionWrap (button, close) {
doActionWrap (button, close = () => {}) {
this.$emit('interacted')
if (button.name === 'emoji') {
this.$refs.picker.showPicker()
} else {

View file

@ -22,6 +22,7 @@ export default {
MuteConfirm
},
props: ['button', 'status'],
emits: ['interacted'],
mounted () {
if (this.button.name === 'mute') {
this.$store.dispatch('fetchDomainMutes')

View file

@ -79,6 +79,7 @@
:button="button"
:status="status"
v-bind="$attrs"
@interacted="e => $emit('interacted')"
/>
<teleport to="#modal">
<MuteConfirm

View file

@ -1,4 +1,7 @@
import { useEditStatusStore } from 'src/stores/editStatus.js'
import { useReportsStore } from 'src/stores/reports.js'
import { useStatusHistoryStore } from 'src/stores/statusHistory.js'
const PRIVATE_SCOPES = new Set(['private', 'direct'])
const PUBLIC_SCOPES = new Set(['public', 'unlisted'])
export const BUTTONS = [{
@ -138,6 +141,34 @@ export const BUTTONS = [{
return dispatch('bookmark', { id: status.id })
}
}
}, {
// =========
// EDIT HISTORY
// =========
name: 'editHistory',
icon: 'history',
label: 'status.status_history',
if ({ status, state }) {
return state.instance.editingAvailable &&
status.edited_at !== null
},
action ({ status }) {
const originalStatus = { ...status }
const stripFieldsList = [
'attachments',
'created_at',
'emojis',
'text',
'raw_html',
'nsfw',
'poll',
'summary',
'summary_raw_html'
]
stripFieldsList.forEach(p => delete originalStatus[p])
useStatusHistoryStore().openStatusHistoryModal(originalStatus)
return Promise.resolve()
}
}, {
// =========
// EDIT
@ -216,8 +247,8 @@ export const BUTTONS = [{
icon: 'flag',
label: 'user_card.report',
if: ({ loggedIn }) => loggedIn,
action ({ dispatch, status }) {
dispatch('openUserReportingModal', { userId: status.user.id, statusIds: [status.id] })
action ({ status }) {
return useReportsStore().openUserReportingModal({ userId: status.user.id, statusIds: [status.id] })
}
}].map(button => {
return Object.fromEntries(

View file

@ -18,7 +18,7 @@ library.add(
const StatusActionButtons = {
props: ['status', 'replying'],
emits: ['toggleReplying'],
emits: ['toggleReplying', 'interacted'],
data () {
return {
showPin: false,

View file

@ -3,7 +3,7 @@
.StatusActionButtons {
.quick-action-buttons {
display: grid;
grid-template-columns: repeat(auto-fill, 4em);
grid-template-columns: repeat(auto-fill, minmax(10%, 3em));
grid-auto-flow: row dense;
grid-auto-rows: 1fr;
grid-gap: 1.25em 1em;

View file

@ -17,6 +17,7 @@
:get-component="getComponent"
:close="() => {}"
:do-action="doAction"
@interacted="e => $emit('interacted')"
/>
<button
v-if="showPin && currentUser"
@ -86,8 +87,9 @@
:func-arg="funcArg"
:get-class="getClass"
:get-component="getComponent"
:outerClose="close"
:outer-close="close"
:do-action="doAction"
@interacted="e => $emit('interacted')"
/>
<button
v-if="showPin && currentUser"

View file

@ -14,7 +14,10 @@
class="input menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': status.bookmark_folder_id == folder.id }"
/>
<StillImage :src="folder.emoji_url" class="emoji" />
<StillImage
:src="folder.emoji_url"
class="emoji"
/>
{{ ' ' + folder.name }}
</button>
</div>

View file

@ -231,8 +231,9 @@
"single_choice": "Single choice",
"multiple_choices": "Multiple choices",
"expiry": "Poll age",
"expires_in": "Poll ends in {0}",
"expires_at": "Poll ends {0}",
"expired": "Poll ended {0} ago",
"expired_at": "Poll ended {0}",
"not_enough_options": "Too few unique options in poll",
"non_anonymous": "Public poll",
"non_anonymous_title": "Other instances may display the options you voted for"

View file

@ -516,7 +516,7 @@ export const init = ({
.filter(c => virtualComponents.has(c) && !nonEditableComponents.has(c))
} else if (liteMode) {
validInnerComponents = (component.validInnerComponentsLite || component.validInnerComponents || [])
} else if (component.name === 'Root') {
} else if (component.name === 'Root' || component.states != null) {
validInnerComponents = component.validInnerComponents || []
} else {
validInnerComponents = component

View file

@ -32,7 +32,7 @@ export const useReportsStore = defineStore('reports', {
this.reportModal.activated = false
},
setReportState ({ id, state }) {
const oldState = window.vuex.state.reports.reports[id].state
const oldState = this.reports[id].state
this.reports[id].state = state
window.vuex.state.api.backendInteractor.setReportState({ id, state }).catch(e => {
console.error('Failed to set report state', e)

View file

@ -9,7 +9,7 @@
*/
// Existing emojis we have
const oldEmojiFilename = '../static/emoji.json'
const oldEmojiFilename = '../src/assets/emoji.json'
// The file downloaded from https://gist.github.com/oliveratgithub/0bf11a9aff0d6da7b46f1490f86a71eb
const newEmojiFilename = 'emojis.json'