Merge branch 'develop' into stable

This commit is contained in:
Floatingghost 2025-03-01 12:20:57 +00:00
commit 578ef52df6
21 changed files with 730 additions and 415 deletions

View file

@ -6,7 +6,6 @@
<title>Akkoma</title>
<link rel="stylesheet" href="/static/font/tiresias.css">
<link rel="stylesheet" href="/static/font/css/lato.css">
<link rel="stylesheet" href="/static/mfm.css">
<link rel="stylesheet" href="/static/custom.css">
<link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder">
<!--server-generated-meta-->

View file

@ -59,7 +59,8 @@ export default {
{
'-reverse': this.reverseLayout,
'-no-sticky-headers': this.noSticky,
'-has-new-post-button': this.newPostButtonShown
'-has-new-post-button': this.newPostButtonShown,
'-wide-timeline': this.widenTimeline
},
'-' + this.layoutType
]
@ -93,6 +94,9 @@ export default {
newPostButtonShown () {
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
},
widenTimeline () {
return this.$store.getters.mergedConfig.widenTimeline
},
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
editingAvailable () { return this.$store.state.instance.editingAvailable },
layoutType () { return this.$store.state.interface.layoutType },

View file

@ -172,6 +172,10 @@ nav {
background-color: rgba(0, 0, 0, 0.15);
background-color: var(--underlay, rgba(0, 0, 0, 0.15));
z-index: -1000;
.-wide-timeline & {
margin:0 calc(var(--columnGap) / -2);
}
}
.app-layout {
@ -187,12 +191,17 @@ nav {
grid-template-rows: 1fr;
box-sizing: border-box;
margin: 0 auto;
padding: 0 calc(var(--columnGap) / 2);
align-content: flex-start;
flex-wrap: wrap;
justify-content: center;
min-height: 100vh;
overflow-x: clip;
&.-wide-timeline {
--maxiColumn: minmax(var(--miniColumn), 1fr);
}
.column {
--___columnMargin: var(--columnGap);

View file

@ -1,4 +1,4 @@
const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow', 'rotate', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
const MFM_TAGS = ['bg', 'blur', 'bounce', 'center', 'fg', 'flip', 'font', 'jelly', 'jump', 'position', 'rainbow', 'rotate', 'scale', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
.map(tag => ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true }))
/**

View file

@ -88,10 +88,8 @@ const Gallery = {
set(this.sizes, id, { width, height })
},
rowStyle (row) {
if (row.audio) {
return { 'padding-bottom': '25%' } // fixed reduced height for audio
} else if (!row.minimal && !row.grid) {
return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` }
if (!row.audio && !row.minimal && !row.grid) {
return { 'aspect-ratio': `1/${(1 / (row.items.length + 0.6))}` }
}
},
itemStyle (id, row) {

View file

@ -96,9 +96,15 @@
.gallery-row {
position: relative;
height: 0;
width: 100%;
flex-grow: 1;
.Status & {
max-height: 10em;
}
&.-audio {
aspect-ratio: 4/1; // this is terrible, but it's how it was before so I'm not changing it >:(
}
&:not(:first-child) {
margin-top: 0.5em;

View file

@ -121,6 +121,19 @@ export default {
}
}
const mfmStyleFromDataAttributes = (attributes) => {
// CSS selectors can check if a data-* attribute is true, but can't use other values, so we want to add them to the style attribute
// Here we turn e.g. `{'data-mfm-some': '1deg', 'data-mfm-thing': '5s'}` to "--mfm-some: 1deg;--mfm-thing: 5s;"
// Note that we only add the value to `style` when they contain only letters, numbers, dot, or minus signs
// At the moment of writing, this should be enough for legitimate purposes and reduces the chance of injection by using special characters
// There is a special case for the `color` value, who is provided without `#`, but requires this in the `style` attribute
return Object.keys(attributes).filter(
(key) => key.startsWith('data-mfm-') && attributes[key] !== true && /^[a-zA-Z0-9.\-]*$/.test(attributes[key])
).map(
(key) => '--mfm-' + key.substr(9) + (key === 'data-mfm-color' ? ': #' : ': ') + attributes[key] + ';'
).reduce((a,v) => a+v, '')
}
// Processor to use with html_tree_converter
const processItem = (item, index, array, what) => {
// Handle text nodes - just add emoji
@ -191,6 +204,15 @@ export default {
if (this.handleLinks && attrs?.['class']?.includes?.('h-card')) {
return ['', children.map(processItem), '']
}
let mfm_style = mfmStyleFromDataAttributes(attrs)
if (mfm_style !== '') {
return [
opener.slice(0,-1) + ' style="' + mfm_style + '">',
children.map(processItem),
closer
]
}
}
if (children !== undefined) {

View file

@ -159,6 +159,16 @@
{{ $t('settings.show_page_backgrounds') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="centerAlignBio">
{{ $t('settings.center_align_bio') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="compactUserInfo">
{{ $t('settings.compact_user_info') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="stopGifs">
{{ $t('settings.stop_gifs') }}
@ -269,6 +279,11 @@
{{ $t('settings.right_sidebar') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="widenTimeline">
{{ $t('settings.widen_timeline') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
v-if="user"

View file

@ -3,6 +3,7 @@
.StatusBody {
display: flex;
flex-direction: column;
overflow: hidden;
.translation {
border: 1px solid var(--accent, $fallback--link);
@ -23,24 +24,6 @@
transition: 0.05s;
}
._mfm_x2_ {
.emoji {
height: 100px;
}
}
._mfm_x3_ {
.emoji {
height: 150px;
}
}
._mfm_x4_ {
.emoji {
height: 200px;
}
}
.attachments {
margin-top: 0.5em;
}

View file

@ -1,7 +1,7 @@
<template>
<div
class="StatusBody"
:class="{ '-compact': compact, 'mfm-disabled': !renderMisskeyMarkdown }"
:class="{ '-compact': compact }"
>
<div class="body">
<div

View file

@ -0,0 +1,423 @@
/**
* "FEP-c16b: Formatting MFM functions" attributes that Akkoma supports
*/
.StatusContent:not(.mfm-disabled) {
/* The following are the non-animated MFM */
.mfm-center {
display: block;
text-align: center;
}
.mfm-flip {
display: inline-block;
transform: scaleX(-1);
}
.mfm-flip[data-mfm-v] {
transform: scaleY(-1);
}
.mfm-flip[data-mfm-v][data-mfm-h] {
transform: scale(-1, -1);
}
.mfm-font[data-mfm-serif] {
font-family: serif;
}
.mfm-font[data-mfm-monospace] {
font-family: monospace;
}
.mfm-font[data-mfm-cursive] {
font-family: cursive;
}
.mfm-font[data-mfm-fantasy] {
font-family: fantasy;
}
.mfm-font[data-mfm-emoji] {
font-family: emoji;
}
.mfm-font[data-mfm-math] {
font-family: math;
}
.mfm-blur {
filter: blur(6px);
transition: filter 0.3s;
&:hover {
filter: blur(0);
}
}
.mfm-rotate {
display: inline-block;
transform: rotate(calc(var(--mfm-deg, 90) * 1deg));
transform-origin: center center;
}
.mfm-x2 {
--mfm-zoom-size: 200%;
}
.mfm-x3 {
--mfm-zoom-size: 400%;
}
.mfm-x4 {
--mfm-zoom-size: 600%;
}
.mfm-x2,
.mfm-x3,
.mfm-x4,
.mfm-tada {
.emoji {
--emoji-size: 2em;
}
font-size: var(--mfm-zoom-size);
.mfm-x2,
.mfm-x3,
.mfm-x4,
.mfm-tada {
/* only half effective */
font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
.mfm-x2,
.mfm-x3,
.mfm-x4,
.mfm-tada {
/* disabled */
font-size: 100%;
}
}
}
.mfm-position {
display: inline-block;
transform: translate(calc(var(--mfm-x, 0) * 1em), calc(var(--mfm-y, 0) * 1em));
}
.mfm-scale {
display: inline-block;
transform: scale(var(--mfm-x, 1), var(--mfm-y, 1));
}
.mfm-fg {
color: var(--mfm-color, #f00);
}
.mfm-bg {
background-color: var(--mfm-color, #0f0);
}
/* The following are the animated MFM */
/* .mfm-hover means that we should only play animation when hovering over the StatusContent
* So either StatusContent does not have this class,
* or it has the class and we are hovering over StatusContent
*/
&:not(.mfm-hover:not(:hover)) {
.mfm-jelly {
display: inline-block;
animation: mfm-rubberBand var(--mfm-speed, 1s) linear infinite both;
}
.mfm-twitch {
display: inline-block;
animation: mfm-twitch var(--mfm-speed, 0.5s) ease infinite;
}
.mfm-shake {
display: inline-block;
animation: mfm-shake var(--mfm-speed, 0.5s) ease infinite;
}
.mfm-spin {
display: inline-block;
animation: mfm-spin var(--mfm-speed, 1.5s) linear infinite;
}
.mfm-spin[data-mfm-y] {
animation-name: mfm-spinY;
}
.mfm-spin[data-mfm-x] {
animation-name: mfm-spinX;
}
.mfm-spin[data-mfm-alternate] {
animation-direction: alternate;
}
.mfm-spin[data-mfm-left] {
animation-direction: reverse;
}
.mfm-jump {
display: inline-block;
animation: mfm-jump var(--mfm-speed, 0.75s) linear infinite;
}
.mfm-bounce {
display: inline-block;
animation: mfm-bounce var(--mfm-speed, 0.75s) linear infinite;
transform-origin: center bottom;
}
.mfm-rainbow {
animation: mfm-rainbow var(--mfm-speed, 1s) linear infinite;
}
.mfm-tada {
display: inline-block;
animation: mfm-tada var(--mfm-speed, 1s) linear infinite both;
--mfm-zoom-size: 150%;
}
}
/* animation keyframes */
@keyframes mfm-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes mfm-spinX {
0% { transform: perspective(128px) rotateX(0deg); }
100% { transform: perspective(128px) rotateX(360deg); }
}
@keyframes mfm-spinY {
0% { transform: perspective(128px) rotateY(0deg); }
100% { transform: perspective(128px) rotateY(360deg); }
}
@keyframes mfm-jump {
0% { transform: translateY(0); }
25% { transform: translateY(-16px); }
50% { transform: translateY(0); }
75% { transform: translateY(-8px); }
100% { transform: translateY(0); }
}
@keyframes mfm-bounce {
0% { transform: translateY(0) scale(1, 1); }
25% { transform: translateY(-16px) scale(1, 1); }
50% { transform: translateY(0) scale(1, 1); }
75% { transform: translateY(0) scale(1.5, 0.75); }
100% { transform: translateY(0) scale(1, 1); }
}
@keyframes mfm-twitch {
0% { transform: translate(7px, -2px); }
5% { transform: translate(-3px, 1px); }
10% { transform: translate(-7px, -1px); }
15% { transform: translate(0, -1px); }
20% { transform: translate(-8px, 6px); }
25% { transform: translate(-4px, -3px); }
30% { transform: translate(-4px, -6px); }
35% { transform: translate(-8px, -8px); }
40% { transform: translate(4px, 6px); }
45% { transform: translate(-3px, 1px); }
50% { transform: translate(2px, -10px); }
55% { transform: translate(-7px, 0); }
60% { transform: translate(-2px, 4px); }
65% { transform: translate(3px, -8px); }
70% { transform: translate(6px, 7px); }
75% { transform: translate(-7px, -2px); }
80% { transform: translate(-7px, -8px); }
85% { transform: translate(9px, 3px); }
90% { transform: translate(-3px, -2px); }
95% { transform: translate(-10px, 2px); }
100% { transform: translate(-2px, -6px); }
}
@keyframes mfm-shake {
0% { transform: translate(-3px, -1px) rotate(-8deg); }
5% { transform: translate(0, -1px) rotate(-10deg); }
10% { transform: translate(1px, -3px) rotate(0deg); }
15% { transform: translate(1px, 1px) rotate(11deg); }
20% { transform: translate(-2px, 1px) rotate(1deg); }
25% { transform: translate(-1px, -2px) rotate(-2deg); }
30% { transform: translate(-1px, 2px) rotate(-3deg); }
35% { transform: translate(2px, 1px) rotate(6deg); }
40% { transform: translate(-2px, -3px) rotate(-9deg); }
45% { transform: translate(0, -1px) rotate(-12deg); }
50% { transform: translate(1px, 2px) rotate(10deg); }
55% { transform: translate(0, -3px) rotate(8deg); }
60% { transform: translate(1px, -1px) rotate(8deg); }
65% { transform: translate(0, -1px) rotate(-7deg); }
70% { transform: translate(-1px, -3px) rotate(6deg); }
75% { transform: translate(0, -2px) rotate(4deg); }
80% { transform: translate(-2px, -1px) rotate(3deg); }
85% { transform: translate(1px, -3px) rotate(-10deg); }
90% { transform: translate(1px, 0) rotate(3deg); }
95% { transform: translate(-2px, 0) rotate(-3deg); }
100% { transform: translate(2px, 1px) rotate(2deg); }
}
@keyframes mfm-rubberBand {
0% { transform: scale3d(1, 1, 1); }
30% { transform: scale3d(1.25, 0.75, 1); }
40% { transform: scale3d(0.75, 1.25, 1); }
50% { transform: scale3d(1.15, 0.85, 1); }
65% { transform: scale3d(0.95, 1.05, 1); }
75% { transform: scale3d(1.05, 0.95, 1); }
100% { transform: scale3d(1, 1, 1); }
}
@keyframes mfm-rainbow {
0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
}
@keyframes mfm-tada {
0%,
100% { transform: scale3d(1, 1, 1); }
10%,
20% { transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); }
30%,
50%,
70%,
90% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); }
40%,
60%,
80% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); }
}
/**
* Legacy MFM
* This is for backwards compatibility with posts formatted on Akkoma before support for FEP-c16b
* Note that it uses the keyframes as defined above for the FEP-c16b compatible MFM representation
*/
.mfm {
display: inline-block;
}
/* The following are the legacy non-animated MFM */
._mfm_flip_[data-h][data-v] {
transform: scale(-1, -1);
}
._mfm_flip_[data-v] {
transform: scaleY(-1);
}
._mfm_flip_:not([data-v]) {
transform: scaleX(-1);
}
._mfm_x2_ {
font-size: 200%;
}
._mfm_x3_ {
font-size: 400%;
}
._mfm_x4_ {
font-size: 600%;
}
._mfm_x2_ {
.emoji {
height: 100px;
}
}
._mfm_x3_ {
.emoji {
height: 150px;
}
}
._mfm_x4_ {
.emoji {
height: 200px;
}
}
._mfm_blur_ {
filter: blur(6px);
transition: filter 0.3s;
}
._mfm_blur_:hover {
filter: blur(0);
}
._mfm_rotate_ {
transform: rotate(90deg);
transform-origin: center center;
}
/* The following are the legacy animated MFM */
/* .mfm-hover means that we should only play animation when hovering over the StatusContent
* So either StatusContent does not have this class,
* or it has the class and we are hovering over StatusContent
*/
&:not(.mfm-hover:not(:hover)) {
._mfm_tada_ {
font-size: 150%;
animation: mfm-tada 1s linear infinite both;
}
._mfm_jelly_ {
animation: mfm-rubberBand 1s linear infinite both;
}
._mfm_twitch_ {
animation: mfm-twitch 0.5s ease infinite;
}
._mfm_shake_ {
animation: mfm-shake 0.5s ease infinite;
}
._mfm_spin_ {
animation: mfm-spin 0.5s linear infinite;
}
._mfm_spin_[data-x] {
animation-name: mfm-spinX;
}
._mfm_spin_[data-y] {
animation-name: mfm-spinY;
}
._mfm_spin_[left] {
animation-direction: reverse;
}
._mfm_spin_[alternate] {
animation-direction: alternate;
}
._mfm_jump_ {
animation: mfm-jump 0.75s linear infinite;
}
._mfm_bounce_ {
animation: mfm-bounce 0.75s linear infinite;
transform-origin: center bottom;
}
._mfm_rainbow_ {
animation: mfm-rainbow 1s linear infinite;
}
}
}

View file

@ -64,6 +64,7 @@
</template>
<script src="./status_content.js"></script>
<style lang="scss" src="./mfm.scss" />
<style lang="scss">
.StatusContent {
flex: 1;
@ -75,23 +76,6 @@
height: 50px;
}
}
&.mfm-hover:not(:hover) {
.mfm {
animation: none !important;
}
}
&.mfm-disabled {
span {
font-size: 100% !important;
}
.mfm {
animation: none !important;
}
.emoji {
height: 32px !important;
}
}
}
.quote-inline,

View file

@ -117,6 +117,11 @@ export default {
shouldConfirmMute () {
return this.mergedConfig.modalOnMute
},
compactUserInfo () {
return this.$store.getters.mergedConfig.compactUserInfo
&& (this.$store.state.interface.layoutType !== 'mobile')
&& this.switcher
},
...mapGetters(['mergedConfig'])
},
components: {

View file

@ -21,6 +21,13 @@
position: relative;
}
.user-buttons {
grid-area: edit;
display: flex;
padding: .5em 0 .5em 0;
justify-self: end;
}
.panel-body {
word-wrap: break-word;
border-bottom-right-radius: inherit;
@ -53,7 +60,6 @@
}
&-bio {
text-align: center;
display: block;
line-height: 1.3;
padding: 1em;
@ -100,15 +106,14 @@
padding: 0 26px;
.container {
min-width: 0;
padding: 16px 0 6px;
display: flex;
align-items: flex-start;
max-height: 56px;
> * {
min-width: 0;
}
display: grid;
grid-template-areas:
"pfp name edit"
"pfp summary summary"
"stats stats stats";
grid-template-columns: auto 1fr auto;
align-items: start;
.Avatar {
--_avatarShadowBox: var(--avatarShadow);
@ -123,6 +128,7 @@
}
&-avatar-link {
grid-area: pfp;
position: relative;
cursor: pointer;
@ -153,8 +159,8 @@
.external-link-button, .edit-profile-button {
cursor: pointer;
width: 2.5em;
text-align: center;
width: 2.3em;
text-align: right;
margin: -0.5em 0;
padding: 0.5em 0;
@ -165,12 +171,16 @@
}
.user-summary {
display: block;
grid-area: summary;
display: grid;
grid-template-areas:
"name name name name name"
"hand role lock avg _";
grid-template-columns:
auto auto auto auto 1fr;
justify-items: start;
margin-left: 0.6em;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 0;
// This is so that text doesn't get overlapped by avatar's shadow if it has
// big one
z-index: 1;
@ -178,56 +188,82 @@
--emoji-size: 1.7em;
.top-line,
.bottom-line {
display: flex;
}
}
.user-name {
text-overflow: ellipsis;
overflow: hidden;
flex: 1 1 auto;
margin-right: 1em;
font-size: 1.1em;
}
.bottom-line {
font-weight: light;
font-size: 1.1em;
align-items: baseline;
.lock-icon {
.user-locked {
margin-left: 0.5em;
grid-area: lock;
}
.user-screen-name {
min-width: 1px;
flex: 0 1 auto;
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
grid-area: hand;
}
.dailyAvg {
min-width: 1px;
flex: 0 0 auto;
margin-left: 1em;
font-size: 0.7em;
color: $fallback--text;
color: var(--text, $fallback--text);
grid-area: avg;
}
.user-role {
flex: none;
color: $fallback--text;
color: var(--alertNeutralText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--alertNeutral, $fallback--fg);
.user-roles {
display: flex;
grid-area: role;
.user-role {
color: $fallback--text;
color: var(--alertNeutralText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--alertNeutral, $fallback--fg);
}
}
}
.user-counts {
grid-area: stats;
display: flex;
line-height:16px;
padding-top: 0.5em;
text-align: center;
justify-content: space-around;
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
align-self: center;
.user-count {
padding: .5em 0 .5em 0;
margin: 0 .5em;
h5 {
font-size:1em;
font-weight: bolder;
margin: 0 0 0.25em;
}
a {
text-decoration: none;
}
}
}
.user-name {
text-align: start;
text-overflow: ellipsis;
overflow: hidden;
margin-left: 0.6em;
font-size: 1.1em;
grid-area: name;
align-self: center;
white-space: nowrap;
max-width: 100%;
z-index: 1; // so shadow from user avatar doesn't overlap it
}
.user-meta {
margin-bottom: .15em;
display: flex;
@ -290,34 +326,21 @@
margin: 0;
}
}
&.-compact {
.container {
grid-template-areas:
"pfp name stats edit"
"pfp summary stats edit";
grid-template-columns: auto auto 1fr auto;
}
.user-counts {
padding-top: 0;
justify-content: space-evenly;
}
}
}
.sidebar .edit-profile-button {
display: none;
}
.user-counts {
display: flex;
line-height:16px;
padding: .5em 1.5em 0em 1.5em;
text-align: center;
justify-content: space-between;
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
flex-wrap: wrap;
}
.user-count {
flex: 1 0 auto;
padding: .5em 0 .5em 0;
margin: 0 .5em;
h5 {
font-size:1em;
font-weight: bolder;
margin: 0 0 0.25em;
}
a {
text-decoration: none;
}
}

View file

@ -9,7 +9,10 @@
class="background-image"
/>
<div class="panel-heading -flexible-height">
<div class="user-info">
<div
class="user-info"
:class="{ '-compact': this.compactUserInfo }"
>
<div class="container">
<a
v-if="allowZoomingAvatar"
@ -29,6 +32,7 @@
</a>
<router-link
v-else
class="user-info-avatar-link"
:to="userProfileLink(user)"
>
<UserAvatar
@ -36,15 +40,80 @@
:user="user"
/>
</router-link>
<RichContent
:title="user.name"
class="user-name"
:html="user.name"
:emoji="user.emoji"
/>
<div class="user-summary">
<div class="top-line">
<RichContent
:title="user.name"
class="user-name"
:html="user.name"
:emoji="user.emoji"
<router-link
class="user-screen-name"
:title="user.screen_name_ui"
:to="userProfileLink(user)"
>
@{{ user.screen_name_ui }}
</router-link>
<span class="user-roles" v-if="!hideBio && (user.deactivated || !!visibleRole || user.bot)">
<span
v-if="user.deactivated"
class="alert user-role"
>
{{ $t('user_card.deactivated') }}
</span>
<span
v-if="!!visibleRole"
class="alert user-role"
>
{{ $t(`general.role.${visibleRole}`) }}
</span>
<span
v-if="user.bot"
class="alert user-role"
>
{{ $t('user_card.bot') }}
</span>
</span>
<span class="user-locked" v-if="user.locked">
<FAIcon
class="lock-icon"
icon="lock"
size="sm"
/>
<button
</span>
<span
v-if="!mergedConfig.hideUserStats && !hideBio"
class="dailyAvg"
>{{ dailyAvg }} {{ $t('user_card.per_day') }}</span>
</div>
<div
v-if="!mergedConfig.hideUserStats && switcher"
class="user-counts"
>
<div
class="user-count"
@click.prevent="setProfileView('statuses')"
>
<h5>{{ $t('user_card.statuses') }}</h5>
<span>{{ user.statuses_count }} <br></span>
</div>
<div
class="user-count"
@click.prevent="setProfileView('friends')"
>
<h5>{{ $t('user_card.followees') }}</h5>
<span>{{ hideFollowsCount ? $t('user_card.hidden') : user.friends_count }}</span>
</div>
<div
class="user-count"
@click.prevent="setProfileView('followers')"
>
<h5>{{ $t('user_card.followers') }}</h5>
<span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span>
</div>
</div>
<div class="user-buttons">
<button
v-if="!isOtherUser && user.is_local"
class="button-unstyled edit-profile-button"
@click.stop="openProfileTab"
@ -83,47 +152,6 @@
:user="user"
:relationship="relationship"
/>
</div>
<div class="bottom-line">
<router-link
class="user-screen-name"
:title="user.screen_name_ui"
:to="userProfileLink(user)"
>
@{{ user.screen_name_ui }}
</router-link>
<template v-if="!hideBio">
<span
v-if="user.deactivated"
class="alert user-role"
>
{{ $t('user_card.deactivated') }}
</span>
<span
v-if="!!visibleRole"
class="alert user-role"
>
{{ $t(`general.role.${visibleRole}`) }}
</span>
<span
v-if="user.bot"
class="alert user-role"
>
{{ $t('user_card.bot') }}
</span>
</template>
<span v-if="user.locked">
<FAIcon
class="lock-icon"
icon="lock"
size="sm"
/>
</span>
<span
v-if="!mergedConfig.hideUserStats && !hideBio"
class="dailyAvg"
>{{ dailyAvg }} {{ $t('user_card.per_day') }}</span>
</div>
</div>
</div>
<div class="user-meta">
@ -269,38 +297,13 @@
v-if="!hideBio"
class="panel-body"
>
<div
v-if="!mergedConfig.hideUserStats && switcher"
class="user-counts"
>
<div
class="user-count"
@click.prevent="setProfileView('statuses')"
>
<h5>{{ $t('user_card.statuses') }}</h5>
<span>{{ user.statuses_count }} <br></span>
</div>
<div
class="user-count"
@click.prevent="setProfileView('friends')"
>
<h5>{{ $t('user_card.followees') }}</h5>
<span>{{ hideFollowsCount ? $t('user_card.hidden') : user.friends_count }}</span>
</div>
<div
class="user-count"
@click.prevent="setProfileView('followers')"
>
<h5>{{ $t('user_card.followers') }}</h5>
<span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span>
</div>
</div>
<RichContent
v-if="!hideBio"
class="user-card-bio"
:html="user.description_html"
:emoji="user.emoji"
:handle-links="true"
:style='{"text-align": this.$store.getters.mergedConfig.centerAlignBio ? "center" : "start"}'
/>
</div>
<teleport to="#modal">

View file

@ -482,6 +482,7 @@
"blocks_tab": "Blocks",
"bot": "Dies ist ein Bot Account",
"btnRadius": "Knöpfe",
"center_align_bio": "Zentrale Textausrichtung in der Bio",
"cBlue": "Blau (Antworten, folgt dir)",
"cGreen": "Grün (Retweet)",
"cOrange": "Orange (Favorisieren)",
@ -496,6 +497,7 @@
"checkboxRadius": "Auswahlfelder",
"collapse_subject": "Beiträge mit Inhaltswarnungen einklappen",
"columns": "Spalten",
"compact_user_info": "Kompakte Benutzerinfos wenn genug Platz",
"composing": "Verfassen",
"confirm_dialogs": "Bestätigung erforderlich für:",
"confirm_dialogs_approve_follow": "Annehmen einer Followanfrage",
@ -934,6 +936,7 @@
"title": "Version"
},
"virtual_scrolling": "Anzeige der Zeitleiste optimieren",
"widen_timeline": "Zeitleiste verbreitern, um horizontalen Platz zu füllen",
"word_filter": "Wortfilter",
"wordfilter": "Wortfilter"
},

View file

@ -488,6 +488,7 @@
"blocks_tab": "Blocks",
"bot": "This is a bot account",
"btnRadius": "Buttons",
"center_align_bio": "Center text in user bio",
"cBlue": "Blue (Reply, follow)",
"cGreen": "Green (Retweet)",
"cOrange": "Orange (Favorite)",
@ -502,6 +503,7 @@
"checkboxRadius": "Checkboxes",
"collapse_subject": "Collapse posts with content warnings",
"columns": "Columns",
"compact_user_info": "Compact user info when enough space",
"composing": "Composing",
"confirm_dialogs": "Require confirmation for:",
"confirm_dialogs_approve_follow": "Accepting a follow request",
@ -948,6 +950,7 @@
},
"virtual_scrolling": "Optimize timeline rendering",
"use_blurhash": "Use blurhashes for NSFW thumbnails",
"widen_timeline": "Widen the Timeline to fill horizontal space",
"word_filter": "Word filter",
"wordfilter": "Wordfilter"
},

View file

@ -56,6 +56,8 @@ export const defaultState = {
autohideFloatingPostButton: false,
pauseOnUnfocused: true,
displayPageBackgrounds: true,
centerAlignBio: false,
compactUserInfo: true,
stopGifs: undefined,
replyVisibility: 'all',
thirdColumnMode: 'notifications',
@ -77,6 +79,7 @@ export const defaultState = {
hideScopeNotice: false,
useStreamingApi: false,
sidebarRight: undefined, // instance default
widenTimeline: undefined, // instance default
subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined, // instance default
postContentType: undefined, // instance default

View file

@ -61,6 +61,7 @@ const defaultState = {
showNavShortcuts: true,
showWiderShortcuts: true,
sidebarRight: false,
widenTimeline: false,
subjectLineBehavior: 'email',
theme: 'pleroma-dark',
virtualScrolling: true,

View file

@ -1,227 +0,0 @@
.mfm {
display: inline-block;
}
._mfm_tada_ {
font-size: 150%;
animation: mfm-tada 1s linear infinite both;
}
._mfm_jelly_ {
animation: mfm-jelly 1s linear infinite both;
}
._mfm_twitch_ {
animation: mfm-twitch 0.5s ease infinite;
}
._mfm_shake_ {
animation: mfm-shake 0.5s ease infinite;
}
._mfm_spin_ {
animation: mfm-spin 0.5s linear infinite;
}
._mfm_spin_[data-x] {
animation-name: mfm-spinX;
}
._mfm_spin_[data-y] {
animation-name: mfm-spinY;
}
._mfm_spin_[left] {
animation-direction: reverse;
}
._mfm_spin_[alternate] {
animation-direction: alternate;
}
._mfm_jump_ {
animation: mfm-jump 0.75s linear infinite;
}
._mfm_bounce_ {
animation: mfm-bounce 0.75s linear infinite;
transform-origin: center bottom;
}
._mfm_flip_[data-h][data-v] {
transform: scale(-1, -1);
}
._mfm_flip_[data-v] {
transform: scaleY(-1);
}
._mfm_flip_:not([data-v]) {
transform: scaleX(-1);
}
._mfm_x2_ {
font-size: 200%;
}
._mfm_x3_ {
font-size: 400%;
}
._mfm_x4_ {
font-size: 600%;
}
._mfm_blur_ {
filter: blur(6px);
transition: filter 0.3s
}
._mfm_blur_:hover {
filter: blur(0px);
}
._mfm_rainbow_ {
animation: mfm-rainbow 1s linear infinite;
}
._mfm_rotate_ {
transform: rotate(90deg);
transform-origin: center center;
}
/* sparkle */
@keyframes mfm-tada {
from {
transform: scale3d(1, 1, 1);
}
10%,
20% {
transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
}
30%,
50%,
70%,
90% {
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
}
40%,
60%,
80% {
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
}
to {
transform: scale3d(1, 1, 1);
}
}
@keyframes bounce {
0% {
transform: scaleX(0.9) scaleY(0.9);
}
19% {
transform: scaleX(1.1) scaleY(1.1);
}
48% {
transform: scaleX(0.95) scaleY(0.95);
}
100% {
transform: scaleX(1) scaleY(1);
}
}
@keyframes mfm-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes mfm-spinX {
0% { transform: perspective(128px) rotateX(0deg); }
100% { transform: perspective(128px) rotateX(360deg); }
}
@keyframes mfm-spinY {
0% { transform: perspective(128px) rotateY(0deg); }
100% { transform: perspective(128px) rotateY(360deg); }
}
@keyframes mfm-jump {
0% { transform: translateY(0); }
25% { transform: translateY(-16px); }
50% { transform: translateY(0); }
75% { transform: translateY(-8px); }
100% { transform: translateY(0); }
}
@keyframes mfm-bounce {
0% { transform: translateY(0) scale(1, 1); }
25% { transform: translateY(-16px) scale(1, 1); }
50% { transform: translateY(0) scale(1, 1); }
75% { transform: translateY(0) scale(1.5, 0.75); }
100% { transform: translateY(0) scale(1, 1); }
}
@keyframes mfm-twitch {
0% { transform: translate(7px, -2px); }
5% { transform: translate(-3px, 1px); }
10% { transform: translate(-7px, -1px); }
15% { transform: translate(0, -1px); }
20% { transform: translate(-8px, 6px); }
25% { transform: translate(-4px, -3px); }
30% { transform: translate(-4px, -6px); }
35% { transform: translate(-8px, -8px); }
40% { transform: translate(4px, 6px); }
45% { transform: translate(-3px, 1px); }
50% { transform: translate(2px, -10px); }
55% { transform: translate(-7px, 0); }
60% { transform: translate(-2px, 4px); }
65% { transform: translate(3px, -8px); }
70% { transform: translate(6px, 7px); }
75% { transform: translate(-7px, -2px); }
80% { transform: translate(-7px, -8px); }
85% { transform: translate(9px, 3px); }
90% { transform: translate(-3px, -2px); }
95% { transform: translate(-10px, 2px); }
100% { transform: translate(-2px, -6px); }
}
@keyframes mfm-shake {
0% { transform: translate(-3px, -1px) rotate(-8deg); }
5% { transform: translate(0, -1px) rotate(-10deg); }
10% { transform: translate(1px, -3px) rotate(0deg); }
15% { transform: translate(1px, 1px) rotate(11deg); }
20% { transform: translate(-2px, 1px) rotate(1deg); }
25% { transform: translate(-1px, -2px) rotate(-2deg); }
30% { transform: translate(-1px, 2px) rotate(-3deg); }
35% { transform: translate(2px, 1px) rotate(6deg); }
40% { transform: translate(-2px, -3px) rotate(-9deg); }
45% { transform: translate(0, -1px) rotate(-12deg); }
50% { transform: translate(1px, 2px) rotate(10deg); }
55% { transform: translate(0, -3px) rotate(8deg); }
60% { transform: translate(1px, -1px) rotate(8deg); }
65% { transform: translate(0, -1px) rotate(-7deg); }
70% { transform: translate(-1px, -3px) rotate(6deg); }
75% { transform: translate(0, -2px) rotate(4deg); }
80% { transform: translate(-2px, -1px) rotate(3deg); }
85% { transform: translate(1px, -3px) rotate(-10deg); }
90% { transform: translate(1px, 0) rotate(3deg); }
95% { transform: translate(-2px, 0) rotate(-3deg); }
100% { transform: translate(2px, 1px) rotate(2deg); }
}
@keyframes mfm-jelly {
from { transform: scale3d(1, 1, 1); }
30% { transform: scale3d(1.25, 0.75, 1); }
40% { transform: scale3d(0.75, 1.25, 1); }
50% { transform: scale3d(1.15, 0.85, 1); }
65% { transform: scale3d(0.95, 1.05, 1); }
75% { transform: scale3d(1.05, 0.95, 1); }
to { transform: scale3d(1, 1, 1); }
}
@keyframes mfm-rainbow {
0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
}

View file

@ -40,6 +40,64 @@ describe('RichContent', () => {
expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(html))
})
it('it adds a # to the MFM color style value', () => {
const html_ok = '<span class="mfm-fg" data-mfm-color="fff">this text is not white</span>'
const expected_ok = '<span class="mfm-fg" data-mfm-color="fff" style="--mfm-color: #fff;">this text is not white</span>'
const wrapper_ok = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html: html_ok
}
})
expect(wrapper_ok.html()).to.eql(compwrap(expected_ok))
})
it('does not allow injection through MFM data- attributes', () => {
const html_ok = '<span class="mfm-spin" data-mfm-speed="-0.2s">brrr</span>'
const expected_ok = '<span class="mfm-spin" data-mfm-speed="-0.2s" style="--mfm-speed: -0.2s;">brrr</span>'
const wrapper_ok = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html: html_ok
}
})
const html_nok1 = '<span class="mfm-spin" data-mfm-speed="<">brrr</span>'
const wrapper_nok1 = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html: html_nok1
}
})
const html_nok2 = '<span class="mfm-spin" data-mfm-speed="\\">brrr</span>'
const wrapper_nok2 = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html: html_nok2
}
})
expect(wrapper_ok.html()).to.eql(compwrap(expected_ok))
expect(wrapper_nok1.html()).to.eql(compwrap(html_nok1))
expect(wrapper_nok2.html()).to.eql(compwrap(html_nok2))
})
it('unescapes everything as needed', () => {
const html = [
p('Testing &#39;em all'),