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 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 { Tab:hover {
background: --bg; background: --bg;
shadow: --buttonDefaultBevel shadow: --buttonDefaultBevel
@ -172,6 +187,14 @@ MenuItem:hover {
background: --fg background: --fg
} }
MenuItem:active {
background: --fg
}
MenuItem:active:hover {
background: --fg
}
Popover { Popover {
shadow: --buttonDefaultBevel, 5 5 0 0 #000000 / 0.2; shadow: --buttonDefaultBevel, 5 5 0 0 #000000 / 0.2;
roundness: 0 roundness: 0

View file

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

View file

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

View file

@ -1,7 +1,7 @@
<template> <template>
<label <label
class="checkbox" class="checkbox"
:class="{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }" :class="[{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }, radio ? '-radio' : '-checkbox']"
> >
<span <span
v-if="!!$slots.before" v-if="!!$slots.before"
@ -19,9 +19,9 @@
@change="$emit('update:modelValue', $event.target.checked)" @change="$emit('update:modelValue', $event.target.checked)"
> >
<i <i
class="input -checkbox checkbox-indicator" class="input checkbox-indicator"
:aria-hidden="true" :aria-hidden="true"
:class="{ disabled }" :class="[{ disabled }, radio ? '-radio' : '-checkbox']"
@transitionend.capture="onTransitionEnd" @transitionend.capture="onTransitionEnd"
/> />
<span <span
@ -37,6 +37,7 @@
<script> <script>
export default { export default {
props: [ props: [
'radio',
'modelValue', 'modelValue',
'indeterminate', 'indeterminate',
'disabled' 'disabled'
@ -107,6 +108,19 @@ export default {
box-sizing: border-box; box-sizing: border-box;
} }
&.-radio {
.checkbox-indicator {
&,
&::before {
border-radius: 9999px;
}
&::before {
content: "•";
}
}
}
.disabled { .disabled {
.checkbox-indicator::before { .checkbox-indicator::before {
background-color: var(--background); 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> <template>
<div class="login panel panel-default"> <div class="login-panel panel panel-default">
<!-- Default panel contents --> <!-- Default panel contents -->
<div class="panel-heading"> <div class="panel-heading">
@ -70,14 +70,13 @@
</div> </div>
</div> </div>
</form> </form>
</div>
<div <div
v-if="error" v-if="error"
class="form-group" class="login-error alert error"
> >
<div class="alert error"> <span class="error-message">
{{ error }} {{ error }}
</span>
<button <button
class="button-unstyled" class="button-unstyled"
@click="clearError" @click="clearError"
@ -94,57 +93,4 @@
<script src="./login_form.js"></script> <script src="./login_form.js"></script>
<style lang="scss"> <style src="./login_form.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>

View file

@ -238,4 +238,4 @@
<script src="./mrf_transparency_panel.js"></script> <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 Timeago from 'components/timeago/timeago.vue'
import genRandomSeed from '../../services/random_seed/random_seed.service.js' import genRandomSeed from '../../services/random_seed/random_seed.service.js'
import RichContent from 'components/rich_content/rich_content.jsx' 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' import { usePollsStore } from 'src/stores/polls'
export default { export default {
@ -9,7 +9,8 @@ export default {
props: ['basePoll', 'emoji'], props: ['basePoll', 'emoji'],
components: { components: {
Timeago, Timeago,
RichContent RichContent,
Checkbox
}, },
data () { data () {
return { return {
@ -44,6 +45,13 @@ export default {
expired () { expired () {
return (this.poll && this.poll.expired) || false 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 () { loggedIn () {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
}, },
@ -78,26 +86,15 @@ export default {
resultTitle (option) { resultTitle (option) {
return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}` return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}`
}, },
activateOption (index) { activateOption (index, value) {
// forgive me father: doing checking the radio/checkboxes let result
// 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}"]`)
if (this.poll.multiple) { if (this.poll.multiple) {
// Checkboxes, toggle only the clicked one result = this.choices || this.options.map(() => false)
clickedElement.checked = !clickedElement.checked
} else { } else {
// Radio button, uncheck everything and check the clicked one result = this.options.map(() => false)
forEach(allElements, element => { element.checked = false })
clickedElement.checked = true
} }
this.choices = map(allElements, e => e.checked) result[index] = value
this.choices = result
}, },
optionId (index) { optionId (index) {
return `poll${this.poll.id}-${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,45 +37,25 @@
:role="poll.multiple ? 'checkbox' : 'radio'" :role="poll.multiple ? 'checkbox' : 'radio'"
:aria-labelledby="`option-vote-${randomSeed}-${index}`" :aria-labelledby="`option-vote-${randomSeed}-${index}`"
:aria-checked="choices[index]" :aria-checked="choices[index]"
class="input unstyled"
@click="activateOption(index)"
> >
<!-- TODO: USE CHECKBOX --> <Checkbox
<input :radio="!poll.multiple"
v-if="poll.multiple"
type="checkbox"
class="input -checkbox poll-checkbox"
:disabled="loading" :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 <RichContent
:id="`option-vote-${randomSeed}-${index}`" :id="`option-vote-${randomSeed}-${index}`"
:html="option.title_html" :html="option.title_html"
:handle-links="false" :handle-links="false"
:emoji="emoji" :emoji="emoji"
/> />
</label> </Checkbox>
</div> </div>
</div> </div>
</div> </div>
<div class="footer faint"> <div class="footer faint">
<button <p>
v-if="!showResults"
class="btn button-default poll-vote-button"
type="button"
:disabled="isDisabled"
@click="vote"
>
{{ $t('polls.vote') }}
</button>
<span <span
v-if="poll.pleroma?.non_anonymous" v-if="poll.pleroma?.non_anonymous"
:title="$t('polls.non_anonymous_title')" :title="$t('polls.non_anonymous_title')"
@ -83,7 +63,7 @@
{{ $t('polls.non_anonymous') }} {{ $t('polls.non_anonymous') }}
&nbsp;·&nbsp; &nbsp;·&nbsp;
</span> </span>
<div class="total"> <span class="total">
<template v-if="typeof poll.voters_count === 'number'"> <template v-if="typeof poll.voters_count === 'number'">
{{ $t("polls.people_voted_count", { count: poll.voters_count }, poll.voters_count) }} {{ $t("polls.people_voted_count", { count: poll.voters_count }, poll.voters_count) }}
</template> </template>
@ -93,11 +73,11 @@
<span v-if="expiresAt !== null"> <span v-if="expiresAt !== null">
&nbsp;·&nbsp; &nbsp;·&nbsp;
</span> </span>
</div> </span>
<span v-if="expiresAt !== null"> <span v-if="expiresAt !== null">
<i18n-t <i18n-t
scope="global" scope="global"
:keypath="expired ? 'polls.expired' : 'polls.expires_in'" :keypath="expirationLabel"
> >
<Timeago <Timeago
:time="expiresAt" :time="expiresAt"
@ -106,83 +86,20 @@
/> />
</i18n-t> </i18n-t>
</span> </span>
</p>
<button
v-if="!showResults"
class="btn button-default poll-vote-button"
type="button"
:disabled="isDisabled"
@click="vote"
>
{{ $t('polls.vote') }}
</button>
</div> </div>
</div> </div>
</template> </template>
<script src="./poll.js"></script> <script src="./poll.js"></script>
<style lang="scss"> <style src="./poll.scss" 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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,7 @@
import { useEditStatusStore } from 'src/stores/editStatus.js' 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 PRIVATE_SCOPES = new Set(['private', 'direct'])
const PUBLIC_SCOPES = new Set(['public', 'unlisted']) const PUBLIC_SCOPES = new Set(['public', 'unlisted'])
export const BUTTONS = [{ export const BUTTONS = [{
@ -138,6 +141,34 @@ export const BUTTONS = [{
return dispatch('bookmark', { id: status.id }) 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 // EDIT
@ -216,8 +247,8 @@ export const BUTTONS = [{
icon: 'flag', icon: 'flag',
label: 'user_card.report', label: 'user_card.report',
if: ({ loggedIn }) => loggedIn, if: ({ loggedIn }) => loggedIn,
action ({ dispatch, status }) { action ({ status }) {
dispatch('openUserReportingModal', { userId: status.user.id, statusIds: [status.id] }) return useReportsStore().openUserReportingModal({ userId: status.user.id, statusIds: [status.id] })
} }
}].map(button => { }].map(button => {
return Object.fromEntries( return Object.fromEntries(

View file

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

View file

@ -3,7 +3,7 @@
.StatusActionButtons { .StatusActionButtons {
.quick-action-buttons { .quick-action-buttons {
display: grid; 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-flow: row dense;
grid-auto-rows: 1fr; grid-auto-rows: 1fr;
grid-gap: 1.25em 1em; grid-gap: 1.25em 1em;

View file

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

View file

@ -14,7 +14,10 @@
class="input menu-checkbox -radio" class="input menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': status.bookmark_folder_id == folder.id }" :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 }} {{ ' ' + folder.name }}
</button> </button>
</div> </div>

View file

@ -231,8 +231,9 @@
"single_choice": "Single choice", "single_choice": "Single choice",
"multiple_choices": "Multiple choices", "multiple_choices": "Multiple choices",
"expiry": "Poll age", "expiry": "Poll age",
"expires_in": "Poll ends in {0}", "expires_at": "Poll ends {0}",
"expired": "Poll ended {0} ago", "expired": "Poll ended {0} ago",
"expired_at": "Poll ended {0}",
"not_enough_options": "Too few unique options in poll", "not_enough_options": "Too few unique options in poll",
"non_anonymous": "Public poll", "non_anonymous": "Public poll",
"non_anonymous_title": "Other instances may display the options you voted for" "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)) .filter(c => virtualComponents.has(c) && !nonEditableComponents.has(c))
} else if (liteMode) { } else if (liteMode) {
validInnerComponents = (component.validInnerComponentsLite || component.validInnerComponents || []) validInnerComponents = (component.validInnerComponentsLite || component.validInnerComponents || [])
} else if (component.name === 'Root') { } else if (component.name === 'Root' || component.states != null) {
validInnerComponents = component.validInnerComponents || [] validInnerComponents = component.validInnerComponents || []
} else { } else {
validInnerComponents = component validInnerComponents = component

View file

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

View file

@ -9,7 +9,7 @@
*/ */
// Existing emojis we have // 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 // The file downloaded from https://gist.github.com/oliveratgithub/0bf11a9aff0d6da7b46f1490f86a71eb
const newEmojiFilename = 'emojis.json' const newEmojiFilename = 'emojis.json'