Merge remote-tracking branch 'origin/develop' into from/develop/tusooa/sw-cache-assets
This commit is contained in:
commit
a1f43234cd
235 changed files with 6354 additions and 4065 deletions
|
|
@ -56,7 +56,7 @@
|
|||
.post-textarea {
|
||||
resize: vertical;
|
||||
height: 10em;
|
||||
overflow: none;
|
||||
overflow: visible;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,7 +177,8 @@
|
|||
.text {
|
||||
flex: 2;
|
||||
margin: 8px;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
|
||||
h1 {
|
||||
font-size: 1rem;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ export default {
|
|||
},
|
||||
{
|
||||
component: 'Button',
|
||||
parent: { component: 'Attachment' },
|
||||
parent: {
|
||||
component: 'Attachment'
|
||||
},
|
||||
directives: {
|
||||
background: '#FFFFFF',
|
||||
opacity: 0.5
|
||||
|
|
|
|||
18
src/components/bubble_timeline/bubble_timeline.js
Normal file
18
src/components/bubble_timeline/bubble_timeline.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
const BubbleTimeline = {
|
||||
components: {
|
||||
Timeline
|
||||
},
|
||||
computed: {
|
||||
timeline () { return this.$store.state.statuses.timelines.bubble }
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
|
||||
},
|
||||
unmounted () {
|
||||
this.$store.dispatch('stopFetchingTimeline', 'bubble')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BubbleTimeline
|
||||
9
src/components/bubble_timeline/bubble_timeline.vue
Normal file
9
src/components/bubble_timeline/bubble_timeline.vue
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<Timeline
|
||||
:title="$t('nav.bubble')"
|
||||
:timeline="timeline"
|
||||
:timeline-name="'bubble'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script src="./bubble_timeline.js"></script>
|
||||
|
|
@ -9,9 +9,9 @@ export default {
|
|||
// However, cascading still works, so resulting state will be result of merging of all relevant states/variants
|
||||
// normal: '' // normal state is implicitly added, it is always included
|
||||
toggled: '.toggled',
|
||||
focused: ':focus-visible',
|
||||
focused: ':focus-within',
|
||||
pressed: ':focus:active',
|
||||
hover: ':hover:not(:disabled)',
|
||||
hover: ':is(:hover, :focus-visible):not(:disabled)',
|
||||
disabled: ':disabled'
|
||||
},
|
||||
// Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it.
|
||||
|
|
@ -89,6 +89,13 @@ export default {
|
|||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'focused'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'disabled'],
|
||||
directives: {
|
||||
|
|
@ -99,7 +106,7 @@ export default {
|
|||
{
|
||||
state: ['disabled'],
|
||||
directives: {
|
||||
background: '$blend(--accent 0.25 --parent)',
|
||||
background: '$blend(--inheritedBackground 0.25 --parent)',
|
||||
shadow: ['--buttonDefaultBevel']
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ export default {
|
|||
states: {
|
||||
toggled: '.toggled',
|
||||
disabled: ':disabled',
|
||||
hover: ':hover:not(:disabled)',
|
||||
focused: ':focus-within'
|
||||
hover: ':is(:hover, :focus-visible):not(:disabled)',
|
||||
focused: ':focus-within:not(:is(:focus-visible))'
|
||||
},
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.heading {
|
||||
|
|
|
|||
|
|
@ -107,8 +107,7 @@
|
|||
.outgoing {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-content: end;
|
||||
justify-content: flex-end;
|
||||
place-content: end flex-end;
|
||||
|
||||
.chat-message-inner {
|
||||
align-items: flex-end;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@
|
|||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: inline;
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -190,21 +190,16 @@ export default {
|
|||
|
||||
.header {
|
||||
grid-area: header;
|
||||
justify-self: center;
|
||||
align-self: baseline;
|
||||
place-self: baseline center;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.invalid-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
background-color: rgba(100 0 0 / 50%);
|
||||
place-items: center center;
|
||||
background-color: rgb(100 0 0 / 50%);
|
||||
|
||||
.alert {
|
||||
padding: 0.5em 1em;
|
||||
|
|
@ -214,7 +209,7 @@ export default {
|
|||
.assists {
|
||||
grid-area: assists;
|
||||
display: grid;
|
||||
grid-auto-flow: rows;
|
||||
grid-auto-flow: row;
|
||||
grid-auto-rows: 2em;
|
||||
grid-gap: 0.5em;
|
||||
}
|
||||
|
|
@ -266,14 +261,14 @@ export default {
|
|||
.preview-window {
|
||||
--__grid-color1: rgb(102 102 102);
|
||||
--__grid-color2: rgb(153 153 153);
|
||||
--__grid-color1-disabled: rgba(102 102 102 / 20%);
|
||||
--__grid-color2-disabled: rgba(153 153 153 / 20%);
|
||||
--__grid-color1-disabled: rgb(102 102 102 / 20%);
|
||||
--__grid-color2-disabled: rgb(153 153 153 / 20%);
|
||||
|
||||
&.-light-grid {
|
||||
--__grid-color1: rgb(205 205 205);
|
||||
--__grid-color2: rgb(255 255 255);
|
||||
--__grid-color1-disabled: rgba(205 205 205 / 20%);
|
||||
--__grid-color2-disabled: rgba(255 255 255 / 20%);
|
||||
--__grid-color1-disabled: rgb(205 205 205 / 20%);
|
||||
--__grid-color2-disabled: rgb(255 255 255 / 20%);
|
||||
}
|
||||
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -339,11 +339,6 @@ const conversation = {
|
|||
canDive () {
|
||||
return this.isTreeView && this.isExpanded
|
||||
},
|
||||
focused () {
|
||||
return (id) => {
|
||||
return (this.isExpanded) && id === this.highlight
|
||||
}
|
||||
},
|
||||
maybeHighlight () {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
},
|
||||
|
|
@ -406,6 +401,9 @@ const conversation = {
|
|||
})
|
||||
}
|
||||
},
|
||||
isFocused (id) {
|
||||
return (this.isExpanded) && id === this.highlight
|
||||
},
|
||||
getReplies (id) {
|
||||
return this.replies[id] || []
|
||||
},
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@
|
|||
:statusoid="status"
|
||||
:expandable="!isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:focused="focused(status.id)"
|
||||
:focused="isFocused(status.id)"
|
||||
:in-conversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
:replies="getReplies(status.id)"
|
||||
|
|
@ -168,7 +168,7 @@
|
|||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||
:profile-user-id="profileUserId"
|
||||
|
||||
:focused="focused"
|
||||
:is-focused-function="isFocused"
|
||||
:get-replies="getReplies"
|
||||
:highlight="maybeHighlight"
|
||||
:set-highlight="setHighlight"
|
||||
|
|
@ -199,7 +199,7 @@
|
|||
:statusoid="status"
|
||||
:expandable="!isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:focused="focused(status.id)"
|
||||
:focused="isFocused(status.id)"
|
||||
:in-conversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
:replies="getReplies(status.id)"
|
||||
|
|
@ -322,10 +322,7 @@
|
|||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: calc(var(--___margin) * -1);
|
||||
bottom: calc(var(--___margin) * -1);
|
||||
left: calc(var(--___margin) * -1);
|
||||
right: calc(var(--___margin) * -1);
|
||||
inset: calc(var(--___margin) * -1);
|
||||
background: var(--background);
|
||||
backdrop-filter: var(--__panel-backdrop-filter);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@
|
|||
transition-timing-function: ease-out;
|
||||
transition-duration: 100ms;
|
||||
|
||||
@media all and (min-width: 800px) {
|
||||
@media all and (width >= 800px) {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
|
@ -70,10 +70,7 @@
|
|||
mask-size: contain;
|
||||
background-color: var(--text);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
|
|
|
|||
|
|
@ -29,14 +29,11 @@
|
|||
// TODO: unify with other modals.
|
||||
.dark-overlay {
|
||||
&::before {
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
content: " ";
|
||||
display: block;
|
||||
cursor: default;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background: rgb(27 31 35 / 50%);
|
||||
z-index: 2000;
|
||||
}
|
||||
|
|
@ -45,13 +42,9 @@
|
|||
.dialog-container {
|
||||
display: grid;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
inset: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
place-items: center center;
|
||||
}
|
||||
|
||||
.dialog-modal.panel {
|
||||
|
|
@ -98,8 +91,7 @@
|
|||
#modal.-mobile {
|
||||
.dialog-container {
|
||||
justify-content: stretch;
|
||||
align-items: end;
|
||||
justify-items: stretch;
|
||||
place-items: end stretch;
|
||||
|
||||
&.-center-mobile {
|
||||
align-items: center;
|
||||
|
|
@ -114,7 +106,6 @@
|
|||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
grid-template-columns: 1fr;
|
||||
grid-auto-columns: none;
|
||||
grid-auto-rows: auto;
|
||||
grid-auto-flow: row dense;
|
||||
|
||||
|
|
|
|||
|
|
@ -127,7 +127,6 @@
|
|||
max-width: 100%;
|
||||
|
||||
p {
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
|
@ -135,8 +134,7 @@
|
|||
.poll-indicator-container {
|
||||
border-radius: var(--roundness);
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
place-items: center center;
|
||||
align-self: start;
|
||||
height: 0;
|
||||
padding-bottom: 62.5%;
|
||||
|
|
@ -147,13 +145,9 @@
|
|||
box-sizing: border-box;
|
||||
border: 1px solid var(--border);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
place-items: center center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
ref="suggestorPopover"
|
||||
class="autocomplete-panel"
|
||||
placement="bottom"
|
||||
:trigger-attrs="{ 'aria-hidden': true }"
|
||||
:hide-trigger="true"
|
||||
>
|
||||
<template #content>
|
||||
<div
|
||||
|
|
@ -159,10 +159,7 @@
|
|||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
|
||||
/* DEBUG STUFF */
|
||||
|
|
|
|||
|
|
@ -64,8 +64,7 @@
|
|||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
overflow: auto hidden;
|
||||
}
|
||||
|
||||
.additional-tabs {
|
||||
|
|
@ -153,7 +152,13 @@
|
|||
transition: mask-size 150ms;
|
||||
mask-size: 100% 20px, 100% 20px, auto;
|
||||
// Autoprefixed seem to ignore this one, and also syntax is different
|
||||
/* stylelint-disable mask-composite */
|
||||
/* stylelint-disable declaration-property-value-no-unknown */
|
||||
|
||||
/* TODO check if this is still needed */
|
||||
mask-composite: xor;
|
||||
/* stylelint-enable declaration-property-value-no-unknown */
|
||||
/* stylelint-enable mask-composite */
|
||||
mask-composite: exclude;
|
||||
|
||||
&.scrolled {
|
||||
|
|
@ -197,8 +202,7 @@
|
|||
&-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--__amount), 1fr);
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
place-items: center center;
|
||||
justify-content: center;
|
||||
grid-template-rows: repeat(1, auto);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
ref="popover"
|
||||
trigger="click"
|
||||
popover-class="emoji-picker popover-default"
|
||||
:trigger-attrs="{ 'aria-hidden': true, tabindex: -1 }"
|
||||
:hide-trigger="true"
|
||||
@show="onPopoverShown"
|
||||
@close="onPopoverClosed"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -84,8 +84,6 @@ const EmojiReactions = {
|
|||
counterTriggerAttrs (reaction) {
|
||||
return {
|
||||
class: [
|
||||
'btn',
|
||||
'button-default',
|
||||
'emoji-reaction-count-button',
|
||||
{
|
||||
'-picked-reaction': this.reactedWith(reaction.name),
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@
|
|||
<UserListPopover
|
||||
:users="accountsForEmoji[reaction.name]"
|
||||
class="emoji-reaction-popover"
|
||||
:normal-button="true"
|
||||
:trigger-attrs="counterTriggerAttrs(reaction)"
|
||||
@show="fetchEmojiReactionsByIfMissing()"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -72,12 +72,11 @@
|
|||
flex: 1 1 0;
|
||||
line-height: 1.2;
|
||||
white-space: normal;
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
visibility: "hidden";
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -101,10 +101,7 @@
|
|||
|
||||
.gallery-row-inner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-content: stretch;
|
||||
|
|
@ -160,7 +157,13 @@
|
|||
linear-gradient(to top, white, white);
|
||||
|
||||
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
||||
/* stylelint-disable mask-composite */
|
||||
/* stylelint-disable declaration-property-value-no-unknown */
|
||||
|
||||
/* TODO check if this is still needed */
|
||||
mask-composite: xor;
|
||||
/* stylelint-enable declaration-property-value-no-unknown */
|
||||
/* stylelint-enable mask-composite */
|
||||
mask-composite: exclude;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import Cropper from 'cropperjs'
|
||||
import 'cropperjs/dist/cropper.css'
|
||||
import 'cropperjs' // This adds all of the cropperjs's components into DOM
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faCircleNotch
|
||||
|
|
@ -19,19 +18,6 @@ const ImageCropper = {
|
|||
type: Function,
|
||||
required: true
|
||||
},
|
||||
cropperOptions: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {
|
||||
aspectRatio: 1,
|
||||
autoCropArea: 1,
|
||||
viewMode: 1,
|
||||
movable: false,
|
||||
zoomable: false,
|
||||
guides: false
|
||||
}
|
||||
}
|
||||
},
|
||||
mimes: {
|
||||
type: String,
|
||||
default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon'
|
||||
|
|
@ -48,7 +34,6 @@ const ImageCropper = {
|
|||
},
|
||||
data () {
|
||||
return {
|
||||
cropper: undefined,
|
||||
dataUrl: undefined,
|
||||
filename: undefined,
|
||||
submitting: false
|
||||
|
|
@ -67,27 +52,30 @@ const ImageCropper = {
|
|||
},
|
||||
methods: {
|
||||
destroy () {
|
||||
if (this.cropper) {
|
||||
this.cropper.destroy()
|
||||
}
|
||||
this.$refs.input.value = ''
|
||||
this.dataUrl = undefined
|
||||
this.$emit('close')
|
||||
},
|
||||
submit (cropping = true) {
|
||||
this.submitting = true
|
||||
this.submitHandler(cropping && this.cropper, this.file)
|
||||
.then(() => this.destroy())
|
||||
.finally(() => {
|
||||
this.submitting = false
|
||||
})
|
||||
|
||||
let cropperPromise
|
||||
if (cropping) {
|
||||
cropperPromise = this.$refs.cropperSelection.$toCanvas()
|
||||
} else {
|
||||
cropperPromise = Promise.resolve()
|
||||
}
|
||||
cropperPromise.then(canvas => {
|
||||
this.submitHandler(canvas, this.file)
|
||||
.then(() => this.destroy())
|
||||
.finally(() => {
|
||||
this.submitting = false
|
||||
})
|
||||
})
|
||||
},
|
||||
pickImage () {
|
||||
this.$refs.input.click()
|
||||
},
|
||||
createCropper () {
|
||||
this.cropper = new Cropper(this.$refs.img, this.cropperOptions)
|
||||
},
|
||||
getTriggerDOM () {
|
||||
return typeof this.trigger === 'object' ? this.trigger : document.querySelector(this.trigger)
|
||||
},
|
||||
|
|
@ -103,6 +91,29 @@ const ImageCropper = {
|
|||
reader.readAsDataURL(this.file)
|
||||
this.$emit('changed', this.file, reader)
|
||||
}
|
||||
},
|
||||
inSelection(selection, maxSelection) {
|
||||
return (
|
||||
selection.x >= maxSelection.x
|
||||
&& selection.y >= maxSelection.y
|
||||
&& (selection.x + selection.width) <= (maxSelection.x + maxSelection.width)
|
||||
&& (selection.y + selection.height) <= (maxSelection.y + maxSelection.height)
|
||||
)
|
||||
},
|
||||
onCropperSelectionChange(event) {
|
||||
const cropperCanvas = this.$refs.cropperCanvas
|
||||
const cropperCanvasRect = cropperCanvas.getBoundingClientRect()
|
||||
const selection = event.detail
|
||||
const maxSelection = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: cropperCanvasRect.width,
|
||||
height: cropperCanvasRect.height,
|
||||
}
|
||||
|
||||
if (!this.inSelection(selection, maxSelection)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,52 @@
|
|||
<template>
|
||||
<div class="image-cropper">
|
||||
<div v-if="dataUrl">
|
||||
<div class="image-cropper-image-container">
|
||||
<img
|
||||
ref="img"
|
||||
<cropper-canvas
|
||||
ref="cropperCanvas"
|
||||
background
|
||||
class="image-cropper-canvas"
|
||||
height="25em"
|
||||
>
|
||||
<cropper-image
|
||||
ref="cropperImage"
|
||||
:src="dataUrl"
|
||||
alt=""
|
||||
@load.stop="createCropper"
|
||||
alt="Picture"
|
||||
class="image-cropper-image"
|
||||
translatable
|
||||
scalable
|
||||
/>
|
||||
<cropper-shade hidden />
|
||||
<cropper-handle
|
||||
action="select"
|
||||
plain
|
||||
/>
|
||||
<cropper-selection
|
||||
ref="cropperSelection"
|
||||
initial-coverage="1"
|
||||
aspect-ratio="1"
|
||||
movable
|
||||
resizable
|
||||
@change="onCropperSelectionChange"
|
||||
>
|
||||
</div>
|
||||
<cropper-grid
|
||||
role="grid"
|
||||
covered
|
||||
/>
|
||||
<cropper-crosshair centered />
|
||||
<cropper-handle
|
||||
action="move"
|
||||
theme-color="rgba(255, 255, 255, 0.35)"
|
||||
/>
|
||||
<cropper-handle action="n-resize" />
|
||||
<cropper-handle action="e-resize" />
|
||||
<cropper-handle action="s-resize" />
|
||||
<cropper-handle action="w-resize" />
|
||||
<cropper-handle action="ne-resize" />
|
||||
<cropper-handle action="nw-resize" />
|
||||
<cropper-handle action="se-resize" />
|
||||
<cropper-handle action="sw-resize" />
|
||||
</cropper-selection>
|
||||
</cropper-canvas>
|
||||
<div class="image-cropper-buttons-wrapper">
|
||||
<button
|
||||
class="button-default btn"
|
||||
|
|
@ -55,20 +93,18 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
&-image-container {
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
&-canvas {
|
||||
height: 25em;
|
||||
width: 25em;
|
||||
}
|
||||
|
||||
&-buttons-wrapper {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
grid-gap: 0.5em;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
|
||||
button {
|
||||
margin-top: 5px;
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ export default {
|
|||
name: 'Input',
|
||||
selector: '.input',
|
||||
states: {
|
||||
hover: ':hover:not(.disabled)',
|
||||
hover: ':is(:hover, :focus-visible):not(.disabled)',
|
||||
focused: ':focus-within',
|
||||
disabled: '.disabled'
|
||||
},
|
||||
|
|
|
|||
|
|
@ -68,10 +68,13 @@
|
|||
margin: 0.5em 0 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
line-height: 1.2em;
|
||||
// cap description at 3 lines, the 1px is to clean up some stray pixels
|
||||
// TODO: fancier fade-out at the bottom to show off that it's too long?
|
||||
|
||||
/* cap description at 3 lines, the 1px is to clean up some stray pixels
|
||||
TODO: fancier fade-out at the bottom to show off that it's too long?
|
||||
*/
|
||||
max-height: calc(1.2em * 3 - 1px);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ export default {
|
|||
selector: '.list-item',
|
||||
states: {
|
||||
active: '.-active',
|
||||
hover: ':hover:not(.-non-interactive)'
|
||||
hover: ':is(:hover, :focus-visible, :has(:focus-visible)):not(.-non-interactive)'
|
||||
},
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
import { mapStores } from 'pinia'
|
||||
import oauthApi from '../../services/new_api/oauth.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes
|
||||
|
|
@ -17,11 +19,11 @@ const LoginForm = {
|
|||
computed: {
|
||||
isPasswordAuth () { return this.requiredPassword },
|
||||
isTokenAuth () { return this.requiredToken },
|
||||
...mapStores(useOAuthStore),
|
||||
...mapState({
|
||||
registrationOpen: state => state.instance.registrationOpen,
|
||||
instance: state => state.instance,
|
||||
loggingIn: state => state.users.loggingIn,
|
||||
oauth: state => state.oauth
|
||||
}),
|
||||
...mapGetters(
|
||||
'authFlow', ['requiredPassword', 'requiredToken', 'requiredMFA']
|
||||
|
|
@ -34,32 +36,37 @@ const LoginForm = {
|
|||
this.isTokenAuth ? this.submitToken() : this.submitPassword()
|
||||
},
|
||||
submitToken () {
|
||||
const { clientId, clientSecret } = this.oauth
|
||||
const data = {
|
||||
clientId,
|
||||
clientSecret,
|
||||
instance: this.instance.server,
|
||||
commit: this.$store.commit
|
||||
}
|
||||
|
||||
oauthApi.getOrCreateApp(data)
|
||||
.then((app) => { oauthApi.login({ ...app, ...data }) })
|
||||
// NOTE: we do not really need the app token, but obtaining a token and
|
||||
// calling verify_credentials is the only way to ensure the app still works.
|
||||
this.oauthStore.ensureAppToken()
|
||||
.then(() => {
|
||||
const app = {
|
||||
clientId: this.oauthStore.clientId,
|
||||
clientSecret: this.oauthStore.clientSecret,
|
||||
}
|
||||
oauthApi.login({ ...app, ...data })
|
||||
})
|
||||
},
|
||||
submitPassword () {
|
||||
const { clientId } = this.oauth
|
||||
const data = {
|
||||
clientId,
|
||||
oauth: this.oauth,
|
||||
instance: this.instance.server,
|
||||
commit: this.$store.commit
|
||||
}
|
||||
this.error = false
|
||||
|
||||
oauthApi.getOrCreateApp(data).then((app) => {
|
||||
// NOTE: we do not really need the app token, but obtaining a token and
|
||||
// calling verify_credentials is the only way to ensure the app still works.
|
||||
this.oauthStore.ensureAppToken().then(() => {
|
||||
const app = {
|
||||
clientId: this.oauthStore.clientId,
|
||||
clientSecret: this.oauthStore.clientSecret,
|
||||
}
|
||||
|
||||
oauthApi.getTokenWithCredentials(
|
||||
{
|
||||
...app,
|
||||
instance: data.instance,
|
||||
instance: this.instance.server,
|
||||
username: this.user.username,
|
||||
password: this.user.password
|
||||
}
|
||||
|
|
|
|||
44
src/components/login_form/login_form.scss
Normal file
44
src/components/login_form/login_form.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -170,7 +170,8 @@ $modal-view-button-icon-margin: 0.5em;
|
|||
min-height: 1em;
|
||||
max-width: 500px;
|
||||
max-height: 9.5em;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@
|
|||
top: 100%;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
transition: opacity 0.2s ease;
|
||||
z-index: 1;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
.MentionsLine {
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
|
||||
.mention-link:not(:first-child)::before {
|
||||
content: " ";
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default {
|
|||
'Avatar'
|
||||
],
|
||||
states: {
|
||||
hover: ':hover:not(.disabled)',
|
||||
hover: ':is(:hover, :focus-visible, :has(:focus-visible)):not(.disabled)',
|
||||
active: '.-active',
|
||||
disabled: '.disabled'
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import mfaApi from '../../services/new_api/mfa.js'
|
||||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
import { mapStores } from 'pinia'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes
|
||||
|
|
@ -18,17 +20,24 @@ export default {
|
|||
...mapGetters({
|
||||
authSettings: 'authFlow/settings'
|
||||
}),
|
||||
...mapStores(useOAuthStore),
|
||||
...mapState({
|
||||
instance: 'instance',
|
||||
oauth: 'oauth'
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('authFlow', ['requireTOTP', 'abortMFA']),
|
||||
...mapActions({ login: 'authFlow/login' }),
|
||||
clearError () { this.error = false },
|
||||
|
||||
focusOnCodeInput () {
|
||||
const codeInput = this.$refs.codeInput
|
||||
codeInput.focus()
|
||||
codeInput.setSelectionRange(0, codeInput.value.length)
|
||||
},
|
||||
|
||||
submit () {
|
||||
const { clientId, clientSecret } = this.oauth
|
||||
const { clientId, clientSecret } = this.oauthStore
|
||||
|
||||
const data = {
|
||||
clientId,
|
||||
|
|
@ -42,6 +51,7 @@ export default {
|
|||
if (result.error) {
|
||||
this.error = result.error
|
||||
this.code = null
|
||||
this.focusOnCodeInput()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
<label for="code">{{ $t('login.recovery_code') }}</label>
|
||||
<input
|
||||
id="code"
|
||||
ref="codeInput"
|
||||
v-model="code"
|
||||
class="input form-control"
|
||||
>
|
||||
|
|
@ -71,4 +72,5 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./recovery_form.js"></script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import mfaApi from '../../services/new_api/mfa.js'
|
||||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
import { mapStores } from 'pinia'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes
|
||||
|
|
@ -18,17 +20,24 @@ export default {
|
|||
...mapGetters({
|
||||
authSettings: 'authFlow/settings'
|
||||
}),
|
||||
...mapStores(useOAuthStore),
|
||||
...mapState({
|
||||
instance: 'instance',
|
||||
oauth: 'oauth'
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('authFlow', ['requireRecovery', 'abortMFA']),
|
||||
...mapActions({ login: 'authFlow/login' }),
|
||||
clearError () { this.error = false },
|
||||
|
||||
focusOnCodeInput () {
|
||||
const codeInput = this.$refs.codeInput
|
||||
codeInput.focus()
|
||||
codeInput.setSelectionRange(0, codeInput.value.length)
|
||||
},
|
||||
|
||||
submit () {
|
||||
const { clientId, clientSecret } = this.oauth
|
||||
const { clientId, clientSecret } = this.oauthStore
|
||||
|
||||
const data = {
|
||||
clientId,
|
||||
|
|
@ -42,6 +51,7 @@ export default {
|
|||
if (result.error) {
|
||||
this.error = result.error
|
||||
this.code = null
|
||||
this.focusOnCodeInput()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
</label>
|
||||
<input
|
||||
id="code"
|
||||
ref="codeInput"
|
||||
v-model="code"
|
||||
class="input form-control"
|
||||
>
|
||||
|
|
@ -74,4 +75,5 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./totp_form.js"></script>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
import SideDrawer from '../side_drawer/side_drawer.vue'
|
||||
import Notifications from '../notifications/notifications.vue'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||
|
||||
import {
|
||||
unseenNotificationsFromStore,
|
||||
countExtraNotifications
|
||||
} from '../../services/notification_utils/notification_utils'
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||
|
||||
import { mapGetters } from 'vuex'
|
||||
import { mapState } from 'pinia'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes,
|
||||
|
|
@ -18,7 +23,6 @@ import {
|
|||
faMinus,
|
||||
faCheckDouble
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
|
||||
library.add(
|
||||
faTimes,
|
||||
|
|
@ -71,10 +75,9 @@ const MobileNav = {
|
|||
return this.$route.name === 'chat'
|
||||
},
|
||||
...mapState(useAnnouncementsStore, ['unreadAnnouncementCount']),
|
||||
...mapGetters(['unreadChatCount']),
|
||||
chatsPinned () {
|
||||
return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats')
|
||||
},
|
||||
...mapState(useServerSideStorageStore, {
|
||||
pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems).has('chats')
|
||||
}),
|
||||
shouldConfirmLogout () {
|
||||
return this.$store.getters.mergedConfig.modalOnLogout
|
||||
},
|
||||
|
|
|
|||
|
|
@ -220,8 +220,7 @@
|
|||
margin-top: 3.5em;
|
||||
width: 100vw;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
overflow: hidden scroll;
|
||||
|
||||
.notifications {
|
||||
padding: 0;
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 801px) {
|
||||
@media all and (width >= 801px) {
|
||||
.new-status-button:not(.always-show) {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,10 +41,7 @@ export default {
|
|||
.modal-view {
|
||||
z-index: var(--ZI_modals);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ import { filterNavigation } from 'src/components/navigation/filter.js'
|
|||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faCity,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faChevronDown,
|
||||
|
|
@ -29,6 +32,7 @@ import {
|
|||
library.add(
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faCity,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faChevronDown,
|
||||
|
|
@ -76,19 +80,19 @@ const NavPanel = {
|
|||
this.editMode = !this.editMode
|
||||
},
|
||||
toggleCollapse () {
|
||||
this.$store.commit('setPreference', { path: 'simple.collapseNav', value: !this.collapsed })
|
||||
this.$store.dispatch('pushServerSideStorage')
|
||||
useServerSideStorageStore().setPreference({ path: 'simple.collapseNav', value: !this.collapsed })
|
||||
useServerSideStorageStore().pushServerSideStorage()
|
||||
},
|
||||
isPinned (item) {
|
||||
return this.pinnedItems.has(item)
|
||||
},
|
||||
togglePin (item) {
|
||||
if (this.isPinned(item)) {
|
||||
this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value: item })
|
||||
useServerSideStorageStore().removeCollectionPreference({ path: 'collections.pinnedNavItems', value: item })
|
||||
} else {
|
||||
this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value: item })
|
||||
useServerSideStorageStore().addCollectionPreference({ path: 'collections.pinnedNavItems', value: item })
|
||||
}
|
||||
this.$store.dispatch('pushServerSideStorage')
|
||||
useServerSideStorageStore().pushServerSideStorage()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -96,20 +100,25 @@ const NavPanel = {
|
|||
unreadAnnouncementCount: 'unreadAnnouncementCount',
|
||||
supportsAnnouncements: store => store.supportsAnnouncements
|
||||
}),
|
||||
...mapPiniaState(useServerSideStorageStore, {
|
||||
collapsed: store => store.prefsStorage.simple.collapseNav,
|
||||
pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems)
|
||||
}),
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
followRequestCount: state => state.api.followRequests.length,
|
||||
privateMode: state => state.instance.private,
|
||||
federating: state => state.instance.federating,
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
||||
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems),
|
||||
collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav,
|
||||
bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable
|
||||
bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable,
|
||||
bubbleTimeline: state => state.instance.localBubbleInstances.length > 0
|
||||
}),
|
||||
timelinesItems () {
|
||||
return filterNavigation(
|
||||
Object
|
||||
.entries({ ...TIMELINES })
|
||||
// do not show in timeliens list since it's in a better place now
|
||||
.filter(([key]) => key !== 'bookmarks')
|
||||
.map(([k, v]) => ({ ...v, name: k })),
|
||||
{
|
||||
hasChats: this.pleromaChatMessagesAvailable,
|
||||
|
|
@ -117,6 +126,7 @@ const NavPanel = {
|
|||
isFederating: this.federating,
|
||||
isPrivate: this.privateMode,
|
||||
currentUser: this.currentUser,
|
||||
supportsBubbleTimeline: this.bubbleTimeline,
|
||||
supportsBookmarkFolders: this.bookmarkFolders
|
||||
}
|
||||
)
|
||||
|
|
@ -132,6 +142,7 @@ const NavPanel = {
|
|||
isFederating: this.federating,
|
||||
isPrivate: this.privateMode,
|
||||
currentUser: this.currentUser,
|
||||
supportsBubbleTimeline: this.bubbleTimeline,
|
||||
supportsBookmarkFolders: this.bookmarkFolders
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFederating, isPrivate, currentUser, supportsBookmarkFolders }) => {
|
||||
export const filterNavigation = (list = [], {
|
||||
hasChats,
|
||||
hasAnnouncements,
|
||||
isFederating,
|
||||
isPrivate,
|
||||
currentUser,
|
||||
supportsBookmarkFolders,
|
||||
supportsBubbleTimeline
|
||||
}) => {
|
||||
return list.filter(({ criteria, anon, anonRoute }) => {
|
||||
const set = new Set(criteria || [])
|
||||
if (!isFederating && set.has('federating')) return false
|
||||
|
|
@ -7,6 +15,8 @@ export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFede
|
|||
if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) return false
|
||||
if (!hasChats && set.has('chats')) return false
|
||||
if (!hasAnnouncements && set.has('announcements')) return false
|
||||
if (!supportsBubbleTimeline && set.has('supportsBubbleTimeline')) return false
|
||||
if (!supportsBookmarkFolders && set.has('supportsBookmarkFolders')) return false
|
||||
if (supportsBookmarkFolders && set.has('!supportsBookmarkFolders')) return false
|
||||
return true
|
||||
})
|
||||
|
|
@ -19,11 +29,11 @@ export const getListEntries = store => store.allLists.map(list => ({
|
|||
iconLetter: list.title[0]
|
||||
}))
|
||||
|
||||
export const getBookmarkFolderEntries = store => store.allFolders.map(folder => ({
|
||||
export const getBookmarkFolderEntries = store => store.allFolders ? store.allFolders.map(folder => ({
|
||||
name: 'bookmark-folder-' + folder.id,
|
||||
routeObject: { name: 'bookmark-folder', params: { id: folder.id } },
|
||||
labelRaw: folder.name,
|
||||
iconEmoji: folder.emoji,
|
||||
iconEmojiUrl: folder.emoji_url,
|
||||
iconLetter: folder.name[0]
|
||||
}))
|
||||
})) : []
|
||||
|
|
|
|||
|
|
@ -27,6 +27,13 @@ export const TIMELINES = {
|
|||
label: 'nav.public_tl',
|
||||
criteria: ['!private']
|
||||
},
|
||||
bubble: {
|
||||
route: 'bubble',
|
||||
anon: true,
|
||||
icon: 'city',
|
||||
label: 'nav.bubble',
|
||||
criteria: ['!private', 'federating', 'supportsBubbleTimeline']
|
||||
},
|
||||
twkn: {
|
||||
route: 'public-external-timeline',
|
||||
anon: true,
|
||||
|
|
@ -34,11 +41,11 @@ export const TIMELINES = {
|
|||
label: 'nav.twkn',
|
||||
criteria: ['!private', 'federating']
|
||||
},
|
||||
// bookmarks are still technically a timeline so we should show it in the dropdown
|
||||
bookmarks: {
|
||||
route: 'bookmarks',
|
||||
icon: 'bookmark',
|
||||
label: 'nav.bookmarks',
|
||||
criteria: ['!supportsBookmarkFolders']
|
||||
},
|
||||
favorites: {
|
||||
routeObject: { name: 'user-profile', query: { tab: 'favorites' } },
|
||||
|
|
@ -53,6 +60,15 @@ export const TIMELINES = {
|
|||
}
|
||||
|
||||
export const ROOT_ITEMS = {
|
||||
bookmarks: {
|
||||
route: 'bookmarks',
|
||||
icon: 'bookmark',
|
||||
label: 'nav.bookmarks',
|
||||
// shows bookmarks entry in a better suited location
|
||||
// hides it when bookmark folders are supported since
|
||||
// we show custom component instead of it
|
||||
criteria: ['!supportsBookmarkFolders']
|
||||
},
|
||||
interactions: {
|
||||
route: 'interactions',
|
||||
icon: 'bell',
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import { routeTo } from 'src/components/navigation/navigation.js'
|
|||
import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faThumbtack } from '@fortawesome/free-solid-svg-icons'
|
||||
import { mapStores } from 'pinia'
|
||||
import { mapStores, mapState as mapPiniaState } from 'pinia'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
|
||||
library.add(faThumbtack)
|
||||
|
||||
|
|
@ -19,11 +21,11 @@ const NavigationEntry = {
|
|||
},
|
||||
togglePin (value) {
|
||||
if (this.isPinned(value)) {
|
||||
this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value })
|
||||
useServerSideStorageStore().removeCollectionPreference({ path: 'collections.pinnedNavItems', value })
|
||||
} else {
|
||||
this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value })
|
||||
useServerSideStorageStore().addCollectionPreference({ path: 'collections.pinnedNavItems', value })
|
||||
}
|
||||
this.$store.dispatch('pushServerSideStorage')
|
||||
useServerSideStorageStore().pushServerSideStorage()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -35,9 +37,11 @@ const NavigationEntry = {
|
|||
},
|
||||
...mapStores(useAnnouncementsStore),
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems)
|
||||
})
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
...mapPiniaState(useServerSideStorageStore, {
|
||||
pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'
|
|||
import {
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faCity,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faComments,
|
||||
|
|
@ -20,10 +21,12 @@ import {
|
|||
import { useListsStore } from 'src/stores/lists'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
|
||||
library.add(
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faCity,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faComments,
|
||||
|
|
@ -54,7 +57,10 @@ const NavPanel = {
|
|||
supportsAnnouncements: store => store.supportsAnnouncements
|
||||
}),
|
||||
...mapPiniaState(useBookmarkFoldersStore, {
|
||||
bookmarks: getBookmarkFolderEntries
|
||||
bookmarks: getBookmarkFolderEntries,
|
||||
}),
|
||||
...mapPiniaState(useServerSideStorageStore, {
|
||||
pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems)
|
||||
}),
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
|
|
@ -62,7 +68,7 @@ const NavPanel = {
|
|||
privateMode: state => state.instance.private,
|
||||
federating: state => state.instance.federating,
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
||||
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems)
|
||||
bubbleTimeline: state => state.instance.localBubbleInstances.length > 0
|
||||
}),
|
||||
pinnedList () {
|
||||
if (!this.currentUser) {
|
||||
|
|
@ -76,7 +82,9 @@ const NavPanel = {
|
|||
hasAnnouncements: this.supportsAnnouncements,
|
||||
isFederating: this.federating,
|
||||
isPrivate: this.privateMode,
|
||||
currentUser: this.currentUser
|
||||
currentUser: this.currentUser,
|
||||
supportsBubbleTimeline: this.bubbleTimeline,
|
||||
supportsBookmarkFolders: this.bookmarks
|
||||
})
|
||||
}
|
||||
return filterNavigation(
|
||||
|
|
@ -95,6 +103,8 @@ const NavPanel = {
|
|||
{
|
||||
hasChats: this.pleromaChatMessagesAvailable,
|
||||
hasAnnouncements: this.supportsAnnouncements,
|
||||
supportsBubbleTimeline: this.bubbleTimeline,
|
||||
supportsBookmarkFolders: this.bookmarks,
|
||||
isFederating: this.federating,
|
||||
isPrivate: this.privateMode,
|
||||
currentUser: this.currentUser
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
.Notification {
|
||||
border-bottom: 1px solid;
|
||||
border-color: var(--border);
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
|
||||
&.Status {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
|
|
@ -31,8 +31,6 @@
|
|||
& .status-username,
|
||||
& .mute-thread,
|
||||
& .mute-words {
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,10 +13,7 @@
|
|||
|
||||
.notification-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +128,6 @@
|
|||
|
||||
.notification-details {
|
||||
min-width: 0;
|
||||
word-wrap: break-word;
|
||||
line-height: var(--post-line-height);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
|
@ -164,7 +160,8 @@
|
|||
}
|
||||
|
||||
h1 {
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
margin: 0 0 0.3em;
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import oauth from '../../services/new_api/oauth.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
|
||||
const oac = {
|
||||
props: ['code'],
|
||||
mounted () {
|
||||
if (this.code) {
|
||||
const { clientId, clientSecret } = this.$store.state.oauth
|
||||
const oauthStore = useOAuthStore()
|
||||
const { clientId, clientSecret } = oauthStore
|
||||
|
||||
oauth.getToken({
|
||||
clientId,
|
||||
|
|
@ -12,7 +14,7 @@ const oac = {
|
|||
instance: this.$store.state.instance.server,
|
||||
code: this.code
|
||||
}).then((result) => {
|
||||
this.$store.commit('setToken', result.access_token)
|
||||
oauthStore.setToken(result.access_token)
|
||||
this.$store.dispatch('loginUser', result.access_token)
|
||||
this.$router.push({ name: 'friends' })
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
|
|
|
|||
63
src/components/poll/poll.scss
Normal file
63
src/components/poll/poll.scss
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
.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;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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') }}
|
||||
·
|
||||
</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">
|
||||
·
|
||||
</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') }}
|
||||
·
|
||||
</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">
|
||||
·
|
||||
</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" />
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* global process */
|
||||
const Popover = {
|
||||
name: 'Popover',
|
||||
props: {
|
||||
|
|
@ -48,6 +49,12 @@ const Popover = {
|
|||
// Use styled button (to avoid nested buttons)
|
||||
normalButton: Boolean,
|
||||
|
||||
// Whether to hide the trigger totally
|
||||
hideTrigger: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
triggerAttrs: {
|
||||
type: Object,
|
||||
default: {}
|
||||
|
|
@ -79,6 +86,26 @@ const Popover = {
|
|||
childrenShown: new Set()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
allTriggerAttrs () {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if ('aria-hidden' in this.triggerAttrs) {
|
||||
throw new Error('Do not use aria-hidden in triggerAttrs. Instead set hideTrigger to true')
|
||||
}
|
||||
}
|
||||
|
||||
const attrs = {
|
||||
...this.triggerAttrs,
|
||||
}
|
||||
|
||||
if (this.hideTrigger) {
|
||||
attrs['aria-hidden'] = true
|
||||
attrs.tabindex = 1
|
||||
}
|
||||
|
||||
return attrs
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setAnchorEl (el) {
|
||||
this.anchorEl = el
|
||||
|
|
|
|||
|
|
@ -15,11 +15,7 @@
|
|||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
bottom: -1px;
|
||||
left: -1px;
|
||||
right: -1px;
|
||||
z-index: -1px;
|
||||
inset: -1px;
|
||||
box-shadow: var(--_shadow);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
class="popover-trigger-button"
|
||||
:class="normalButton ? 'button-default btn' : 'button-unstyled'"
|
||||
type="button"
|
||||
v-bind="triggerAttrs"
|
||||
v-bind="allTriggerAttrs"
|
||||
@click="onClick"
|
||||
>
|
||||
<slot name="trigger" />
|
||||
|
|
|
|||
|
|
@ -363,12 +363,6 @@ const PostStatusForm = {
|
|||
}
|
||||
},
|
||||
safeToSaveDraft () {
|
||||
console.log('safe', (
|
||||
this.newStatus.status ||
|
||||
this.newStatus.spoilerText ||
|
||||
this.newStatus.files?.length ||
|
||||
this.newStatus.hasPoll
|
||||
) && this.saveable)
|
||||
return (
|
||||
this.newStatus.status ||
|
||||
this.newStatus.spoilerText ||
|
||||
|
|
|
|||
|
|
@ -205,6 +205,7 @@
|
|||
min-height: calc(var(--post-line-height) * 1em);
|
||||
resize: none;
|
||||
background: transparent;
|
||||
text-wrap: stable;
|
||||
|
||||
&.scrollable-form {
|
||||
overflow-y: auto;
|
||||
|
|
|
|||
|
|
@ -418,7 +418,7 @@
|
|||
margin: 0.6em;
|
||||
}
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
@media all and (width <= 800px) {
|
||||
.registration-form .container {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
|
|
|
|||
18
src/components/scroll_top_button/scroll_top_button.js
Normal file
18
src/components/scroll_top_button/scroll_top_button.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
const ScrollTopButton = {
|
||||
props: {
|
||||
fast: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scrollToTop() {
|
||||
const speed = this.fast ? 'instant' : 'smooth';
|
||||
|
||||
window.scrollTo({ top: 0, behavior: speed })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ScrollTopButton
|
||||
29
src/components/scroll_top_button/scroll_top_button.vue
Normal file
29
src/components/scroll_top_button/scroll_top_button.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="rightside-button scroll-to-top">
|
||||
<button
|
||||
class="button-unstyled scroll-to-top-button"
|
||||
type="button"
|
||||
:title="$t('general.scroll_to_top')"
|
||||
@click="scrollToTop"
|
||||
>
|
||||
<FALayers class="fa-scale-110 fa-old-padding-layer">
|
||||
<FAIcon icon="arrow-up" />
|
||||
<FAIcon
|
||||
icon="minus"
|
||||
transform="up-7"
|
||||
/>
|
||||
</FALayers>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./scroll_top_button.js"></script>
|
||||
<style lang="scss">
|
||||
.scroll-to-top {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.-scrolled .scroll-to-top {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -34,7 +34,7 @@ export default {
|
|||
notEditable: true, // for now
|
||||
states: {
|
||||
pressed: ':active',
|
||||
hover: ':hover:not(:disabled)',
|
||||
hover: ':is(:hover, :focus-visible, :has(:focus-visible)):not(:disabled)',
|
||||
disabled: ':disabled'
|
||||
},
|
||||
validInnerComponents: [
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -154,7 +157,7 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
@media all and (width <= 800px) {
|
||||
.search-nav-heading {
|
||||
.tab-switcher .tabs .tab-wrapper {
|
||||
display: block;
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import Popover from 'components/popover/popover.vue'
|
|||
import ConfirmModal from 'components/confirm_modal/confirm_modal.vue'
|
||||
import ModifiedIndicator from '../helpers/modified_indicator.vue'
|
||||
import EmojiEditingPopover from '../helpers/emoji_editing_popover.vue'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
const EmojiTab = {
|
||||
components: {
|
||||
|
|
@ -232,7 +233,7 @@ const EmojiTab = {
|
|||
})
|
||||
},
|
||||
displayError (msg) {
|
||||
this.$store.useInterfaceStore().pushGlobalNotice({
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'admin_dash.emoji.error',
|
||||
messageArgs: [msg],
|
||||
level: 'error'
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
.emoji-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1em 1em;
|
||||
gap: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import StringSetting from '../helpers/string_setting.vue'
|
|||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
|
|
@ -80,7 +81,7 @@ const FrontendsTab = {
|
|||
this.$store.dispatch('loadFrontendsStuff')
|
||||
if (response.error) {
|
||||
const reason = await response.error.json()
|
||||
this.$store.useInterfaceStore().pushGlobalNotice({
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
level: 'error',
|
||||
messageKey: 'admin_dash.frontend.failure_installing_frontend',
|
||||
messageArgs: {
|
||||
|
|
@ -90,7 +91,7 @@ const FrontendsTab = {
|
|||
timeout: 5000
|
||||
})
|
||||
} else {
|
||||
this.$store.useInterfaceStore().pushGlobalNotice({
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
level: 'success',
|
||||
messageKey: 'admin_dash.frontend.success_installing_frontend',
|
||||
messageArgs: {
|
||||
|
|
|
|||
|
|
@ -13,15 +13,11 @@
|
|||
// fix buttons showing through
|
||||
z-index: 2;
|
||||
opacity: 0.9;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
dd {
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
max-width: 10em;
|
||||
|
|
|
|||
46
src/components/settings_modal/helpers/help_indicator.vue
Normal file
46
src/components/settings_modal/helpers/help_indicator.vue
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<span class="HelpIndicator">
|
||||
<Popover
|
||||
trigger="click"
|
||||
:trigger-attrs="{ 'aria-label': $t('settings.setting_changed') }"
|
||||
>
|
||||
<template #trigger>
|
||||
|
||||
<FAIcon icon="circle-question" />
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="help-tooltip">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faCircleQuestion
|
||||
)
|
||||
|
||||
export default {
|
||||
components: { Popover }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.HelpIndicator {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.help-tooltip {
|
||||
margin: 0.5em 1em;
|
||||
min-width: 10em;
|
||||
max-width: 30vw;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -78,7 +78,6 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
draft: {
|
||||
// TODO allow passing shared draft object?
|
||||
get () {
|
||||
if (this.realSource === 'admin' || this.path == null) {
|
||||
return get(this.$store.state.adminSettings.draft, this.canonPath)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
max-width: 90vw;
|
||||
height: 90vh;
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
@media all and (width <= 800px) {
|
||||
max-width: 100vw;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
@ -84,7 +84,7 @@
|
|||
> li {
|
||||
margin: 1em 0;
|
||||
line-height: 1.5em;
|
||||
vertical-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&.two-column {
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
*/
|
||||
transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
@media all and (width <= 800px) {
|
||||
/* For mobile, the modal takes 100% of the available screen.
|
||||
This ensures the minimized modal is always 50px above the browser bottom
|
||||
bar regardless of whether or not it is visible.
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
<AppearanceTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="expertLevel > 0 && !isMobileLayout"
|
||||
v-if="expertLevel > 0"
|
||||
:label="$t('settings.style.themes3.editor.title')"
|
||||
icon="palette"
|
||||
data-tab-name="style"
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
<StyleTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="expertLevel > 0 && !isMobileLayout"
|
||||
v-if="expertLevel > 0"
|
||||
:label="$t('settings.theme_old')"
|
||||
icon="paint-brush"
|
||||
data-tab-name="theme"
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ const AppearanceTab = {
|
|||
},
|
||||
onImportFailure (result) {
|
||||
console.error('Failure importing theme:', result)
|
||||
this.$store.useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_theme_imported', level: 'error' })
|
||||
useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_theme_imported', level: 'error' })
|
||||
},
|
||||
importValidator (parsed, filename) {
|
||||
if (filename.endsWith('.json')) {
|
||||
|
|
|
|||
|
|
@ -26,8 +26,7 @@
|
|||
|
||||
.palettes-container {
|
||||
height: 15em;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden auto;
|
||||
scrollbar-gutter: stable;
|
||||
border-radius: var(--roundness);
|
||||
border: 1px solid var(--border);
|
||||
|
|
@ -112,8 +111,7 @@
|
|||
flex-wrap: wrap;
|
||||
margin: -0.5em 0;
|
||||
height: 25em;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overflow: hidden auto;
|
||||
scrollbar-gutter: stable;
|
||||
border-radius: var(--roundness);
|
||||
border: 1px solid var(--border);
|
||||
|
|
|
|||
|
|
@ -336,6 +336,15 @@
|
|||
{{ $t('settings.show_scrollbars') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<UnitSetting
|
||||
path="themeEditorMinWidth"
|
||||
:units="['px', 'rem']"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.theme_editor_min_width') }}
|
||||
</UnitSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import Importer from 'src/components/importer/importer.vue'
|
|||
import Exporter from 'src/components/exporter/exporter.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import { mapState } from 'vuex'
|
||||
import { useOAuthTokensStore } from 'src/stores/oauth_tokens'
|
||||
|
||||
const DataImportExportTab = {
|
||||
data () {
|
||||
|
|
@ -15,7 +16,7 @@ const DataImportExportTab = {
|
|||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchTokens')
|
||||
useOAuthTokensStore().fetchTokens()
|
||||
this.fetchBackups()
|
||||
},
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -1,48 +1,209 @@
|
|||
import { filter, trim, debounce } from 'lodash'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { mapState, mapActions } from 'pinia'
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
import {
|
||||
newImporter,
|
||||
newExporter
|
||||
} from 'src/services/export_import/export_import.js'
|
||||
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import UnitSetting from '../helpers/unit_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import HelpIndicator from '../helpers/help_indicator.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
|
||||
const SUPPORTED_TYPES = new Set(['word', 'regexp', 'user', 'user_regexp'])
|
||||
|
||||
const FilteringTab = {
|
||||
data () {
|
||||
return {
|
||||
muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n'),
|
||||
replyVisibilityOptions: ['all', 'following', 'self'].map(mode => ({
|
||||
key: mode,
|
||||
value: mode,
|
||||
label: this.$t(`settings.reply_visibility_${mode}`)
|
||||
}))
|
||||
})),
|
||||
muteFiltersDraftObject: cloneDeep(useServerSideStorageStore().prefsStorage.simple.muteFilters),
|
||||
muteFiltersDraftDirty: Object.fromEntries(
|
||||
Object.entries(
|
||||
useServerSideStorageStore().prefsStorage.simple.muteFilters
|
||||
).map(([k]) => [k, false])
|
||||
),
|
||||
exportedFilter: null,
|
||||
filterImporter: newImporter({
|
||||
validator (parsed) {
|
||||
if (Array.isArray(parsed)) return false
|
||||
if (!SUPPORTED_TYPES.has(parsed.type)) return false
|
||||
return true
|
||||
},
|
||||
onImport: (data) => {
|
||||
const {
|
||||
enabled = true,
|
||||
expires = null,
|
||||
hide = false,
|
||||
name = '',
|
||||
value = ''
|
||||
} = data
|
||||
|
||||
this.createFilter({
|
||||
enabled,
|
||||
expires,
|
||||
hide,
|
||||
name,
|
||||
value
|
||||
})
|
||||
},
|
||||
onImportFailure (result) {
|
||||
console.error('Failure importing filter:', result)
|
||||
useInterfaceStore()
|
||||
.pushGlobalNotice({
|
||||
messageKey: 'settings.filter.import_failure',
|
||||
level: 'error'
|
||||
})
|
||||
}
|
||||
}),
|
||||
filterExporter: newExporter({
|
||||
filename: 'pleromafe_mute-filter',
|
||||
getExportedObject: () => this.exportedFilter
|
||||
})
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
UnitSetting,
|
||||
IntegerSetting
|
||||
IntegerSetting,
|
||||
Checkbox,
|
||||
Select,
|
||||
HelpIndicator
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject(),
|
||||
muteWordsString: {
|
||||
get () {
|
||||
return this.muteWordsStringLocal
|
||||
},
|
||||
set (value) {
|
||||
this.muteWordsStringLocal = value
|
||||
this.debouncedSetMuteWords(value)
|
||||
...mapState(
|
||||
useServerSideStorageStore,
|
||||
{
|
||||
muteFilters: store => Object.entries(store.prefsStorage.simple.muteFilters),
|
||||
muteFiltersObject: store => store.prefsStorage.simple.muteFilters
|
||||
}
|
||||
),
|
||||
muteFiltersDraft () {
|
||||
return Object.entries(this.muteFiltersDraftObject)
|
||||
},
|
||||
debouncedSetMuteWords () {
|
||||
return debounce((value) => {
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'muteWords',
|
||||
value: filter(value.split('\n'), (word) => trim(word).length > 0)
|
||||
})
|
||||
}, 1000)
|
||||
muteFiltersExpired () {
|
||||
const now = Date.now()
|
||||
return Object
|
||||
.entries(this.muteFiltersDraftObject)
|
||||
.filter(([, { expires }]) => expires != null && expires <= now)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useServerSideStorageStore, ['setPreference', 'unsetPreference', 'pushServerSideStorage']),
|
||||
getDatetimeLocal (timestamp) {
|
||||
const date = new Date(timestamp)
|
||||
let 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.setPreference({ path: 'simple.muteFilters.' + newId , value: filter })
|
||||
this.pushServerSideStorage()
|
||||
},
|
||||
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.setPreference({ path: 'simple.muteFilters.' + newId , value: filter })
|
||||
this.pushServerSideStorage()
|
||||
},
|
||||
deleteFilter (id) {
|
||||
delete this.muteFiltersDraftObject[id]
|
||||
this.unsetPreference({ path: 'simple.muteFilters.' + id , value: null })
|
||||
this.pushServerSideStorage()
|
||||
},
|
||||
purgeExpiredFilters () {
|
||||
this.muteFiltersExpired.forEach(([id]) => {
|
||||
console.log(id)
|
||||
delete this.muteFiltersDraftObject[id]
|
||||
this.unsetPreference({ path: 'simple.muteFilters.' + id , value: null })
|
||||
})
|
||||
this.pushServerSideStorage()
|
||||
},
|
||||
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.setPreference({ path: 'simple.muteFilters.' + id , value: this.muteFiltersDraftObject[id] })
|
||||
this.pushServerSideStorage()
|
||||
this.muteFiltersDraftDirty[id] = false
|
||||
},
|
||||
},
|
||||
// Updating nested properties
|
||||
watch: {
|
||||
replyVisibility () {
|
||||
|
|
|
|||
89
src/components/settings_modal/tabs/filtering_tab.scss
Normal file
89
src/components/settings_modal/tabs/filtering_tab.scss
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
.filtering-tab {
|
||||
.muteFilterContainer {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--roundness);
|
||||
height: 33vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mute-filter {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--roundness);
|
||||
margin: 0.5em;
|
||||
padding: 0.5em;
|
||||
display: grid;
|
||||
align-items: baseline;
|
||||
grid-gap: 0.5em;
|
||||
}
|
||||
|
||||
.never {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.filter-name {
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.alert,
|
||||
.button-default {
|
||||
display: inline-block;
|
||||
line-height: 2;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
.filter-enabled {
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
text-align: right;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.filter-field {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
grid-template-rows: subgrid;
|
||||
grid-column: 1 / span 3;
|
||||
align-items: baseline;
|
||||
|
||||
label {
|
||||
grid-column: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.filter-field-value {
|
||||
grid-column: 2 / span 2;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-buttons {
|
||||
grid-column: 1 / span 3;
|
||||
justify-self: end;
|
||||
display: inline-grid;
|
||||
grid-gap: 0.5em;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
max-width: 100%;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.muteFiltersActions,
|
||||
.muteFiltersActionsBottom {
|
||||
display: grid;
|
||||
align-items: baseline;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: 0.5em;
|
||||
margin: 0.5em 0;
|
||||
|
||||
.total {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.settings-modal.-mobile .filtering-tab {
|
||||
.filter-buttons {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,88 @@
|
|||
<template>
|
||||
<div :label="$t('settings.filtering')">
|
||||
<div
|
||||
:label="$t('settings.filtering')"
|
||||
class="filtering-tab"
|
||||
>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.posts') }}</h2>
|
||||
<h2>{{ $t('settings.filter.clutter') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
v-if="user"
|
||||
id="replyVisibility"
|
||||
path="replyVisibility"
|
||||
:options="replyVisibilityOptions"
|
||||
>
|
||||
{{ $t('settings.replies_in_timeline') }}
|
||||
</ChoiceSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
expert="1"
|
||||
path="hidePostStats"
|
||||
>
|
||||
{{ $t('settings.hide_post_stats') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
expert="1"
|
||||
path="hideUserStats"
|
||||
>
|
||||
{{ $t('settings.hide_user_stats') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideBotIndication">
|
||||
{{ $t('settings.hide_actor_type_indication') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideScrobbles">
|
||||
{{ $t('settings.hide_scrobbles') }}
|
||||
</BooleanSetting>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<UnitSetting
|
||||
key="hideScrobblesAfter"
|
||||
path="hideScrobblesAfter"
|
||||
:units="['m', 'h', 'd']"
|
||||
unit-set="time"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.hide_scrobbles_after') }}
|
||||
</UnitSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<h3>{{ $t('settings.attachments') }}</h3>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
path="maxThumbnails"
|
||||
expert="1"
|
||||
:min="0"
|
||||
>
|
||||
{{ $t('settings.max_thumbnails') }}
|
||||
</IntegerSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideAttachments">
|
||||
{{ $t('settings.hide_attachments_in_tl') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideAttachmentsInConv">
|
||||
{{ $t('settings.hide_attachments_in_convo') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.filter.mute_filter') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path="hideFilteredStatuses">
|
||||
{{ $t('settings.hide_filtered_statuses') }}
|
||||
{{ $t('settings.hide_muted_statuses') }}
|
||||
</BooleanSetting>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
|
|
@ -13,8 +90,9 @@
|
|||
parent-path="hideFilteredStatuses"
|
||||
:parent-invert="true"
|
||||
path="hideWordFilteredPosts"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.hide_wordfiltered_statuses') }}
|
||||
{{ $t('settings.hide_filtered_statuses') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
|
|
@ -50,83 +128,218 @@
|
|||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hidePostStats">
|
||||
{{ $t('settings.hide_post_stats') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideBotIndication">
|
||||
{{ $t('settings.hide_actor_type_indication') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<ChoiceSetting
|
||||
v-if="user"
|
||||
id="replyVisibility"
|
||||
path="replyVisibility"
|
||||
:options="replyVisibilityOptions"
|
||||
>
|
||||
{{ $t('settings.replies_in_timeline') }}
|
||||
</ChoiceSetting>
|
||||
<li>
|
||||
<h3>{{ $t('settings.wordfilter') }}</h3>
|
||||
<textarea
|
||||
id="muteWords"
|
||||
v-model="muteWordsString"
|
||||
class="input resize-height"
|
||||
/>
|
||||
<div>{{ $t('settings.filtering_explanation') }}</div>
|
||||
</li>
|
||||
<h3>{{ $t('settings.attachments') }}</h3>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
path="maxThumbnails"
|
||||
expert="1"
|
||||
:min="0"
|
||||
>
|
||||
{{ $t('settings.max_thumbnails') }}
|
||||
</IntegerSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideAttachments">
|
||||
{{ $t('settings.hide_attachments_in_tl') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideAttachmentsInConv">
|
||||
{{ $t('settings.hide_attachments_in_convo') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideScrobbles">
|
||||
{{ $t('settings.hide_scrobbles') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<UnitSetting
|
||||
key="hideScrobblesAfter"
|
||||
path="hideScrobblesAfter"
|
||||
:units="['m', 'h', 'd']"
|
||||
unit-set="time"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.hide_scrobbles_after') }}
|
||||
</UnitSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
v-if="expertLevel > 0"
|
||||
class="setting-item"
|
||||
>
|
||||
<h2>{{ $t('settings.user_profiles') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path="hideUserStats">
|
||||
{{ $t('settings.hide_user_stats') }}
|
||||
</BooleanSetting>
|
||||
<h3>{{ $t('settings.filter.custom_filters') }}</h3>
|
||||
<p class="muteFiltersActions">
|
||||
<span class="total">
|
||||
{{ $t('settings.filter.total_count', { count: muteFiltersDraft.length }) }}
|
||||
</span>
|
||||
<button
|
||||
class="add-button button-default"
|
||||
type="button"
|
||||
@click="createFilter()"
|
||||
>
|
||||
{{ $t('settings.filter.new') }}
|
||||
</button>
|
||||
<button
|
||||
class="add-button button-default"
|
||||
type="button"
|
||||
@click="importFilter()"
|
||||
>
|
||||
{{ $t('settings.filter.import') }}
|
||||
</button>
|
||||
</p>
|
||||
<div class="muteFilterContainer">
|
||||
<div
|
||||
v-for="filter in muteFiltersDraft"
|
||||
:key="filter[0]"
|
||||
class="mute-filter"
|
||||
:style="{ order: filter[1].order }"
|
||||
>
|
||||
<div class="filter-name">
|
||||
<label
|
||||
:for="'filterName' + filter[0]"
|
||||
>
|
||||
{{ $t('settings.filter.name') }}
|
||||
</label>
|
||||
{{ ' ' }}
|
||||
<input
|
||||
:id="'filterName' + filter[0]"
|
||||
class="input"
|
||||
:value="filter[1].name"
|
||||
@input="updateFilter(filter[0], 'name', $event.target.value)"
|
||||
>
|
||||
<span
|
||||
v-if="filter[1].expires !== null && Date.now() > filter[1].expires"
|
||||
class="alert neutral"
|
||||
>
|
||||
{{ $t('settings.filter.expired') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="filter-enabled">
|
||||
<Checkbox
|
||||
:id="'filterHide' + filter[0]"
|
||||
:model-value="filter[1].hide"
|
||||
:name="'filterHide' + filter[0]"
|
||||
@update:model-value="updateFilter(filter[0], 'hide', $event)"
|
||||
>
|
||||
{{ $t('settings.filter.hide') }}
|
||||
</Checkbox>
|
||||
{{ ' ' }}
|
||||
<Checkbox
|
||||
:id="'filterEnabled' + filter[0]"
|
||||
:model-value="filter[1].enabled"
|
||||
:name="'filterEnabled' + filter[0]"
|
||||
@update:model-value="updateFilter(filter[0], 'enabled', $event)"
|
||||
>
|
||||
{{ $t('settings.enabled') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div class="filter-type filter-field">
|
||||
<label :for="'filterType' + filter[0]">
|
||||
<HelpIndicator>
|
||||
<p>
|
||||
{{ $t('settings.filter.help.word') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('settings.filter.help.user') }}
|
||||
</p>
|
||||
<i18n-t
|
||||
keypath="settings.filter.help.regexp"
|
||||
tag="p"
|
||||
>
|
||||
<template #link>
|
||||
<a
|
||||
:href="$t('settings.filter.help.regexp_url')"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t('settings.filter.help.regexp_link') }}
|
||||
</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</HelpIndicator>
|
||||
{{ $t('settings.filter.type') }}
|
||||
</label>
|
||||
<Select
|
||||
:id="'filterType' + filter[0]"
|
||||
class="filter-field-value"
|
||||
:model-value="filter[1].type"
|
||||
@update:model-value="updateFilter(filter[0], 'type', $event)"
|
||||
>
|
||||
<option value="word">
|
||||
{{ $t('settings.filter.plain') }}
|
||||
</option>
|
||||
<option value="regexp">
|
||||
{{ $t('settings.filter.regexp') }}
|
||||
</option>
|
||||
<option value="user">
|
||||
{{ $t('settings.filter.user') }}
|
||||
</option>
|
||||
<option value="user_regexp">
|
||||
{{ $t('settings.filter.user_regexp') }}
|
||||
</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="filter-value filter-field">
|
||||
<label
|
||||
:for="'filterValue' + filter[0]"
|
||||
>
|
||||
{{ $t('settings.filter.value') }}
|
||||
</label>
|
||||
{{ ' ' }}
|
||||
<input
|
||||
:id="'filterValue' + filter[0]"
|
||||
class="input filter-field-value"
|
||||
:value="filter[1].value"
|
||||
@input="updateFilter(filter[0], 'value', $event.target.value)"
|
||||
>
|
||||
</div>
|
||||
<div class="filter-expires filter-field">
|
||||
<label
|
||||
:for="'filterExpires' + filter[0]"
|
||||
>
|
||||
{{ $t('settings.filter.expires') }}
|
||||
</label>
|
||||
{{ ' ' }}
|
||||
<div class="filter-field-value">
|
||||
<input
|
||||
:id="'filterExpires' + filter[0]"
|
||||
class="input"
|
||||
:class="{ disabled: filter[1].expires === null }"
|
||||
type="datetime-local"
|
||||
:disabled="filter[1].expires === null"
|
||||
:value="filter[1].expires ? getDatetimeLocal(filter[1].expires) : null"
|
||||
@input="updateFilter(filter[0], 'expires', $event.target.value)"
|
||||
>
|
||||
{{ ' ' }}
|
||||
<Checkbox
|
||||
:id="'filterExpiresNever' + filter[0]"
|
||||
:model-value="filter[1].expires === null"
|
||||
:name="'filterExpiresNever' + filter[0]"
|
||||
class="input-inset input-boolean never"
|
||||
@update:model-value="updateFilter(filter[0], 'expires-never', $event)"
|
||||
>
|
||||
{{ $t('settings.filter.never_expires') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!checkRegexValid(filter[0])"
|
||||
class="alert error"
|
||||
>
|
||||
{{ $t('settings.filter.regexp_error') }}
|
||||
</div>
|
||||
<div class="filter-buttons">
|
||||
<button
|
||||
class="delete-button button-default -danger"
|
||||
type="button"
|
||||
@click="deleteFilter(filter[0])"
|
||||
>
|
||||
{{ $t('settings.filter.delete') }}
|
||||
</button>
|
||||
<button
|
||||
class="export-button button-default"
|
||||
type="button"
|
||||
@click="exportFilter(filter[0])"
|
||||
>
|
||||
{{ $t('settings.filter.export') }}
|
||||
</button>
|
||||
<button
|
||||
class="copy-button button-default"
|
||||
type="button"
|
||||
@click="copyFilter(filter[0])"
|
||||
>
|
||||
{{ $t('settings.filter.copy') }}
|
||||
</button>
|
||||
<button
|
||||
class="save-button button-default"
|
||||
:class="{ disabled: !muteFiltersDraftDirty[filter[0]] }"
|
||||
:disabled="!muteFiltersDraftDirty[filter[0]]"
|
||||
type="button"
|
||||
@click="saveFilter(filter[0])"
|
||||
>
|
||||
{{ $t('settings.filter.save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="muteFiltersActionsBottom">
|
||||
<span class="total">
|
||||
{{ $t('settings.filter.expired_count', { count: muteFiltersExpired.length }) }}
|
||||
</span>
|
||||
<button
|
||||
class="add-button button-default"
|
||||
type="button"
|
||||
:class="{ disabled: muteFiltersExpired.length === 0 }"
|
||||
:disabled="muteFiltersExpired.length === 0"
|
||||
@click="purgeExpiredFilters()"
|
||||
>
|
||||
{{ $t('settings.filter.purge_expired') }}
|
||||
</button>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./filtering_tab.js"></script>
|
||||
<style src="./filtering_tab.scss"></style>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
|||
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
|
||||
import withLoadMore from 'src/components/../hocs/with_load_more/with_load_more'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import { useOAuthTokensStore } from 'src/stores/oauth_tokens'
|
||||
|
||||
const BlockList = withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
||||
|
|
@ -39,7 +40,7 @@ const MutesAndBlocks = {
|
|||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchTokens')
|
||||
useOAuthTokensStore().fetchTokens()
|
||||
this.$store.dispatch('getKnownDomains')
|
||||
},
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ const ProfileTab = {
|
|||
this.submitBackground('')
|
||||
}
|
||||
},
|
||||
submitAvatar (cropper, file) {
|
||||
submitAvatar (canvas, file) {
|
||||
const that = this
|
||||
return new Promise((resolve, reject) => {
|
||||
function updateAvatar (avatar, avatarName) {
|
||||
|
|
@ -232,8 +232,8 @@ const ProfileTab = {
|
|||
})
|
||||
}
|
||||
|
||||
if (cropper) {
|
||||
cropper.getCroppedCanvas().toBlob((data) => updateAvatar(data, file.name), file.type)
|
||||
if (canvas) {
|
||||
canvas.toBlob((data) => updateAvatar(data, file.name), file.type)
|
||||
} else {
|
||||
updateAvatar(file, file.name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Mfa from './mfa.vue'
|
||||
import localeService from 'src/services/locale/locale.service.js'
|
||||
import { useOAuthTokensStore } from 'src/stores/oauth_tokens'
|
||||
|
||||
const SecurityTab = {
|
||||
data () {
|
||||
|
|
@ -28,7 +29,7 @@ const SecurityTab = {
|
|||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchTokens')
|
||||
useOAuthTokensStore().fetchTokens()
|
||||
this.fetchAliases()
|
||||
},
|
||||
components: {
|
||||
|
|
@ -40,11 +41,11 @@ const SecurityTab = {
|
|||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
pleromaBackend () {
|
||||
return this.$store.state.instance.pleromaBackend
|
||||
pleromaExtensionsAvailable () {
|
||||
return this.$store.state.instance.pleromaExtensionsAvailable
|
||||
},
|
||||
oauthTokens () {
|
||||
return this.$store.state.oauthTokens.tokens.map(oauthToken => {
|
||||
return useOAuthTokensStore().tokens.map(oauthToken => {
|
||||
return {
|
||||
id: oauthToken.id,
|
||||
appName: oauthToken.app_name,
|
||||
|
|
@ -151,7 +152,7 @@ const SecurityTab = {
|
|||
},
|
||||
revokeToken (id) {
|
||||
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
|
||||
this.$store.dispatch('revokeToken', id)
|
||||
useOAuthTokensStore().revokeToken(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -643,7 +643,7 @@ export default {
|
|||
parser (string) { return deserialize(string) },
|
||||
onImportFailure (result) {
|
||||
console.error('Failure importing style:', result)
|
||||
this.$store.useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_theme_imported', level: 'error' })
|
||||
useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_theme_imported', level: 'error' })
|
||||
},
|
||||
onImport
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
.StyleTab {
|
||||
min-width: var(--themeEditorMinWidth, fit-content);
|
||||
|
||||
.style-control {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
@ -185,7 +187,7 @@
|
|||
.state-selector,
|
||||
.variant-selector {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr minmax(1fr, 10em);
|
||||
grid-template-columns: 1fr minmax(10em, 1fr);
|
||||
grid-template-rows: auto;
|
||||
grid-auto-flow: column;
|
||||
grid-gap: 0.5em;
|
||||
|
|
|
|||
|
|
@ -241,10 +241,7 @@ export default {
|
|||
|
||||
.underlay-preview {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
inset: 0 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
.theme-tab {
|
||||
min-width: var(--themeEditorMinWidth, fit-content);
|
||||
|
||||
.deprecation-warning {
|
||||
padding: 0.5em;
|
||||
margin: 2em;
|
||||
|
|
|
|||
|
|
@ -112,8 +112,7 @@
|
|||
grid-area: preview;
|
||||
min-width: 25em;
|
||||
margin-left: 0.125em;
|
||||
align-self: start;
|
||||
justify-self: center;
|
||||
place-self: start center;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,8 +107,7 @@
|
|||
}
|
||||
|
||||
.shout-window {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden auto;
|
||||
max-height: 20em;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@ import MentionLink from 'src/components/mention_link/mention_link.vue'
|
|||
import StatusActionButtons from 'src/components/status_action_buttons/status_action_buttons.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import { muteWordHits } from '../../services/status_parser/status_parser.js'
|
||||
import { muteFilterHits } from '../../services/status_parser/status_parser.js'
|
||||
import { unescape, uniqBy } from 'lodash'
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
|
@ -23,6 +24,7 @@ import {
|
|||
faLock,
|
||||
faLockOpen,
|
||||
faGlobe,
|
||||
faIgloo,
|
||||
faTimes,
|
||||
faRetweet,
|
||||
faReply,
|
||||
|
|
@ -42,6 +44,7 @@ import {
|
|||
library.add(
|
||||
faEnvelope,
|
||||
faGlobe,
|
||||
faIgloo,
|
||||
faLock,
|
||||
faLockOpen,
|
||||
faTimes,
|
||||
|
|
@ -161,9 +164,6 @@ const Status = {
|
|||
},
|
||||
computed: {
|
||||
...controlledOrUncontrolledGetters(['replying', 'mediaPlaying']),
|
||||
muteWords () {
|
||||
return this.mergedConfig.muteWords
|
||||
},
|
||||
showReasonMutedThread () {
|
||||
return (
|
||||
this.status.thread_muted ||
|
||||
|
|
@ -221,8 +221,11 @@ const Status = {
|
|||
loggedIn () {
|
||||
return !!this.currentUser
|
||||
},
|
||||
muteWordHits () {
|
||||
return muteWordHits(this.status, this.muteWords)
|
||||
muteFilterHits () {
|
||||
return muteFilterHits(
|
||||
Object.values(useServerSideStorageStore().prefsStorage.simple.muteFilters),
|
||||
this.status
|
||||
)
|
||||
},
|
||||
botStatus () {
|
||||
return this.status.user.actor_type === 'Service'
|
||||
|
|
@ -256,7 +259,7 @@ const Status = {
|
|||
return [
|
||||
this.userIsMuted ? 'user' : null,
|
||||
this.status.thread_muted ? 'thread' : null,
|
||||
(this.muteWordHits.length > 0) ? 'wordfilter' : null,
|
||||
(this.muteFilterHits.length > 0) ? 'filtered' : null,
|
||||
(this.muteBotStatuses && this.botStatus) ? 'bot' : null,
|
||||
(this.muteSensitiveStatuses && this.sensitiveStatus) ? 'nsfw' : null
|
||||
].filter(_ => _)
|
||||
|
|
@ -267,14 +270,14 @@ const Status = {
|
|||
switch (this.muteReasons[0]) {
|
||||
case 'user': return this.$t('status.muted_user')
|
||||
case 'thread': return this.$t('status.thread_muted')
|
||||
case 'wordfilter':
|
||||
case 'filtered':
|
||||
return this.$t(
|
||||
'status.muted_words',
|
||||
'status.muted_filters',
|
||||
{
|
||||
word: this.muteWordHits[0],
|
||||
numWordsMore: this.muteWordHits.length - 1
|
||||
name: this.muteFilterHits[0].name,
|
||||
filterMore: this.muteFilterHits.length - 1
|
||||
},
|
||||
this.muteWordHits.length
|
||||
this.muteFilterHits.length
|
||||
)
|
||||
case 'bot': return this.$t('status.bot_muted')
|
||||
case 'nsfw': return this.$t('status.sensitive_muted')
|
||||
|
|
@ -312,6 +315,7 @@ const Status = {
|
|||
(relationshipReblog && relationshipReblog.muting)
|
||||
},
|
||||
shouldNotMute () {
|
||||
if (this.isFocused) return true
|
||||
const { status } = this
|
||||
const { reblog } = status
|
||||
return (
|
||||
|
|
@ -326,7 +330,7 @@ const Status = {
|
|||
// Don't mute statuses in muted conversation when said conversation is opened
|
||||
(this.inConversation && status.thread_muted)
|
||||
// No excuses if post has muted words
|
||||
) && !this.muteWordHits.length > 0
|
||||
) && !this.muteFilterHits.length > 0
|
||||
},
|
||||
hideMutedUsers () {
|
||||
return this.mergedConfig.hideMutedPosts
|
||||
|
|
@ -345,7 +349,8 @@ const Status = {
|
|||
(this.muted && this.hideFilteredStatuses) ||
|
||||
(this.userIsMuted && this.hideMutedUsers) ||
|
||||
(this.status.thread_muted && this.hideMutedThreads) ||
|
||||
(this.muteWordHits.length > 0 && this.hideWordFilteredPosts)
|
||||
(this.muteFilterHits.length > 0 && this.hideWordFilteredPosts) ||
|
||||
(this.muteFilterHits.some(x => x.hide))
|
||||
)
|
||||
},
|
||||
isFocused () {
|
||||
|
|
@ -481,6 +486,8 @@ const Status = {
|
|||
return 'lock-open'
|
||||
case 'direct':
|
||||
return 'envelope'
|
||||
case 'local':
|
||||
return 'igloo'
|
||||
default:
|
||||
return 'globe'
|
||||
}
|
||||
|
|
@ -533,6 +540,7 @@ const Status = {
|
|||
this.controlledToggleThreadDisplay()
|
||||
},
|
||||
scrollIfHighlighted (highlightId) {
|
||||
if (this.$el.getBoundingClientRect == null) return
|
||||
const id = highlightId
|
||||
if (this.status.id === id) {
|
||||
const rect = this.$el.getBoundingClientRect()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
.Status {
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
|
||||
&:hover {
|
||||
--_still-image-img-visibility: visible;
|
||||
|
|
@ -92,7 +92,10 @@
|
|||
|
||||
a {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -283,8 +286,6 @@
|
|||
|
||||
& .status-username,
|
||||
& .mute-reason {
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
|
@ -364,7 +365,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
@media all and (width <= 800px) {
|
||||
.repeater-avatar {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
|
@ -374,7 +375,6 @@
|
|||
height: 40px;
|
||||
|
||||
// TODO define those other way somehow?
|
||||
// stylelint-disable rscss/class-format
|
||||
&.-compact {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
|
|
|||
|
|
@ -540,6 +540,7 @@
|
|||
:status="status"
|
||||
:replying="replying"
|
||||
@toggle-replying="toggleReplying"
|
||||
@interacted="e => $emit('interacted')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -67,6 +67,9 @@ export default {
|
|||
'doAction',
|
||||
'outerClose'
|
||||
],
|
||||
emits: [
|
||||
'interacted'
|
||||
],
|
||||
components: {
|
||||
StatusBookmarkFolderMenu,
|
||||
EmojiPicker,
|
||||
|
|
@ -120,7 +123,9 @@ export default {
|
|||
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
||||
}
|
||||
},
|
||||
doActionWrap (button, close) {
|
||||
doActionWrap (button, close = () => {}) {
|
||||
if (this.button.interactive ? !this.button.interactive(this.funcArg) : false) return
|
||||
this.$emit('interacted')
|
||||
if (button.name === 'emoji') {
|
||||
this.$refs.picker.showPicker()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
:title="$t(button.label(funcArg))"
|
||||
target="_blank"
|
||||
:tabindex="0"
|
||||
:disabled="buttonClass.disabled"
|
||||
:disabled="button.interactive ? !button.interactive(funcArg) : false"
|
||||
:href="getComponent(button) == 'a' ? button.link?.(funcArg) || remoteInteractionLink : undefined"
|
||||
@click="doActionWrap(button, outerClose)"
|
||||
>
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
:style="{ '--fa-animation-duration': '750ms' }"
|
||||
fixed-width
|
||||
/>
|
||||
<template v-if="!buttonClass.disabled && button.toggleable?.(funcArg) && button.active">
|
||||
<template v-if="!buttonClass.disabled && (!button.interactive || button?.interactive(funcArg)) && button.toggleable?.(funcArg) && button.active">
|
||||
<FAIcon
|
||||
v-if="button.active(funcArg)"
|
||||
class="active-marker"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export default {
|
|||
MuteConfirm
|
||||
},
|
||||
props: ['button', 'status'],
|
||||
emits: ['interacted'],
|
||||
mounted () {
|
||||
if (this.button.name === 'mute') {
|
||||
this.$store.dispatch('fetchDomainMutes')
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@
|
|||
:button="button"
|
||||
:status="status"
|
||||
v-bind="$attrs"
|
||||
@interacted="e => $emit('interacted')"
|
||||
/>
|
||||
<teleport to="#modal">
|
||||
<MuteConfirm
|
||||
|
|
|
|||
|
|
@ -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 = [{
|
||||
|
|
@ -27,8 +30,8 @@ export const BUTTONS = [{
|
|||
label: ({ status }) => status.repeated
|
||||
? 'tool_tip.unrepeat'
|
||||
: 'tool_tip.repeat',
|
||||
icon ({ status }) {
|
||||
if (PRIVATE_SCOPES.has(status.visibility)) {
|
||||
icon ({ status, currentUser }) {
|
||||
if (currentUser.id !== status.user.id && PRIVATE_SCOPES.has(status.visibility)) {
|
||||
return 'lock'
|
||||
}
|
||||
return 'retweet'
|
||||
|
|
@ -37,7 +40,7 @@ export const BUTTONS = [{
|
|||
active: ({ status }) => status.repeated,
|
||||
counter: ({ status }) => status.repeat_num,
|
||||
anonLink: true,
|
||||
interactive: ({ status, loggedIn }) => loggedIn && !PRIVATE_SCOPES.has(status.visibility),
|
||||
interactive: ({ status, currentUser }) => !!currentUser && (currentUser.id === status.user.id || !PRIVATE_SCOPES.has(status.visibility)),
|
||||
toggleable: true,
|
||||
confirm: ({ status, getters }) => !status.repeated && getters.mergedConfig.modalOnRepeat,
|
||||
confirmStrings: {
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { mapState } from 'vuex'
|
||||
import { mapState } from 'pinia'
|
||||
|
||||
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
|
||||
import ActionButtonContainer from './action_button_container.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import genRandomSeed from 'src/services/random_seed/random_seed.service.js'
|
||||
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
|
||||
import { BUTTONS } from './buttons_definitions.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
|
|
@ -18,7 +20,7 @@ library.add(
|
|||
|
||||
const StatusActionButtons = {
|
||||
props: ['status', 'replying'],
|
||||
emits: ['toggleReplying'],
|
||||
emits: ['toggleReplying', 'interacted'],
|
||||
data () {
|
||||
return {
|
||||
showPin: false,
|
||||
|
|
@ -36,8 +38,8 @@ const StatusActionButtons = {
|
|||
ActionButtonContainer
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedStatusActions)
|
||||
...mapState(useServerSideStorageStore, {
|
||||
pinnedItems: store => new Set(store.prefsStorage.collections.pinnedStatusActions)
|
||||
}),
|
||||
buttons () {
|
||||
return BUTTONS.filter(x => x.if ? x.if(this.funcArg) : true)
|
||||
|
|
@ -101,12 +103,12 @@ const StatusActionButtons = {
|
|||
return this.pinnedItems.has(button.name)
|
||||
},
|
||||
unpin (button) {
|
||||
this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedStatusActions', value: button.name })
|
||||
this.$store.dispatch('pushServerSideStorage')
|
||||
useServerSideStorageStore().removeCollectionPreference({ path: 'collections.pinnedStatusActions', value: button.name })
|
||||
useServerSideStorageStore().pushServerSideStorage()
|
||||
},
|
||||
pin (button) {
|
||||
this.$store.commit('addCollectionPreference', { path: 'collections.pinnedStatusActions', value: button.name })
|
||||
this.$store.dispatch('pushServerSideStorage')
|
||||
useServerSideStorageStore().addCollectionPreference({ path: 'collections.pinnedStatusActions', value: button.name })
|
||||
useServerSideStorageStore().pushServerSideStorage()
|
||||
},
|
||||
getComponent (button) {
|
||||
if (!this.$store.state.users.currentUser && button.anonLink) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue