Merge branch 'mfm' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2026-07-07 11:42:10 +03:00
commit 0b3702824c
21 changed files with 351 additions and 191 deletions

View file

@ -36,6 +36,12 @@ const BasicUserCard = {
allowNonSquareEmoji() { allowNonSquareEmoji() {
return useMergedConfigStore().mergedConfig.nonSquareEmoji return useMergedConfigStore().mergedConfig.nonSquareEmoji
}, },
pauseMfm() {
return useMergedConfigStore().mergedConfig.pauseMfm
},
scaleMfm() {
return useMergedConfigStore().mergedConfig.scaleMfm
},
}, },
} }

View file

@ -32,6 +32,8 @@
:html="user.name" :html="user.name"
:emoji="user.emoji" :emoji="user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji" :allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
/> />
</div> </div>
<div> <div>

View file

@ -21,5 +21,11 @@ export default {
allowNonSquareEmoji() { allowNonSquareEmoji() {
return useMergedConfigStore().mergedConfig.nonSquareEmoji return useMergedConfigStore().mergedConfig.nonSquareEmoji
}, },
pauseMfm() {
return useMergedConfigStore().mergedConfig.pauseMfm
},
scaleMfm() {
return useMergedConfigStore().mergedConfig.scaleMfm
},
}, },
} }

View file

@ -20,6 +20,8 @@
:html="htmlTitle" :html="htmlTitle"
:emoji="user.emoji || []" :emoji="user.emoji || []"
:allow-non-square-emoji="allowNonSquareEmoji" :allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
:is-local="user.is_local" :is-local="user.is_local"
/> />
</div> </div>

View file

@ -217,6 +217,12 @@ const Notification = {
allowNonSquareEmoji() { allowNonSquareEmoji() {
return this.mergedConfig.nonSquareEmoji return this.mergedConfig.nonSquareEmoji
}, },
pauseMfm() {
return this.mergedConfig.pauseMfm
},
scaleMfm() {
return this.mergedConfig.scaleMfm
},
shouldConfirmApprove() { shouldConfirmApprove() {
return this.mergedConfig.modalOnApproveFollow return this.mergedConfig.modalOnApproveFollow
}, },

View file

@ -72,6 +72,8 @@
:html="notification.from_profile.name_html" :html="notification.from_profile.name_html"
:emoji="notification.from_profile.emoji" :emoji="notification.from_profile.emoji"
:allow-non-square-emoji="allowNonSquareEmoji" :allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
:is-local="notification.from_profile.is_local" :is-local="notification.from_profile.is_local"
/> />
</bdi> </bdi>

View file

@ -57,6 +57,12 @@ export default {
allowNonSquareEmoji() { allowNonSquareEmoji() {
return useMergedConfigStore().mergedConfig.nonSquareEmoji return useMergedConfigStore().mergedConfig.nonSquareEmoji
}, },
pauseMfm() {
return useMergedConfigStore().mergedConfig.pauseMfm
},
scaleMfm() {
return useMergedConfigStore().mergedConfig.scaleMfm
},
loggedIn() { loggedIn() {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
}, },

View file

@ -25,6 +25,8 @@
:handle-links="false" :handle-links="false"
:emoji="emoji" :emoji="emoji"
:allow-non-square-emoji="allowNonSquareEmoji" :allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
/> />
</div> </div>
<div <div

View file

@ -118,6 +118,16 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
pauseMfm: {
required: false,
type: Boolean,
default: false,
},
scaleMfm: {
required: false,
type: Boolean,
default: false,
},
}, },
// NEVER EVER TOUCH DATA INSIDE RENDER // NEVER EVER TOUCH DATA INSIDE RENDER
render() { render() {
@ -320,12 +330,16 @@ export default {
const isMFM = classname?.startsWith('mfm-') const isMFM = classname?.startsWith('mfm-')
if (isMFM) { if (isMFM) {
const mfmOperator = /^mfm-(\w+)$/.exec(classname)?.[1] const mfmOperator = /^mfm-(\w+)$/.exec(classname)?.[1]
newAttrs['class'] = 'mfm' newAttrs['class'] = [
'mfm',
this.pauseMfm ? '-pause' : '',
this.scaleMfm ? '-scale' : '',
].filter(x => x).join(' ')
newAttrs['data-mfm-operator'] = mfmOperator newAttrs['data-mfm-operator'] = mfmOperator
switch(mfmOperator) { switch(mfmOperator) {
case 'position': { case 'position': {
const x = fullAttrs['data-mfm-x'].replace(/\.+$/,'') || 0 const x = Number.parseFloat(fullAttrs['data-mfm-x']) || 0
const y = fullAttrs['data-mfm-y'].replace(/\.+$/,'') || 0 const y = Number.parseFloat(fullAttrs['data-mfm-y']) || 0
newAttrs.style = [ newAttrs.style = [
'transform:', 'transform:',
`translate(calc(${x} * (var(--emoji-size) / 2)), `, `translate(calc(${x} * (var(--emoji-size) / 2)), `,
@ -334,8 +348,8 @@ export default {
break break
} }
case 'scale': { case 'scale': {
const x = fullAttrs['data-mfm-x'].replace(/\.+$/,'') || 1 const x = Number.parseFloat(fullAttrs['data-mfm-x']) || 1
const y = fullAttrs['data-mfm-y'].replace(/\.+$/,'') || 1 const y = Number.parseFloat(fullAttrs['data-mfm-y']) || 1
newAttrs.style = [ newAttrs.style = [
'transform:', 'transform:',
`scale(${x}, ${y})`, `scale(${x}, ${y})`,
@ -343,7 +357,7 @@ export default {
break break
} }
case 'rotate': { case 'rotate': {
const deg = fullAttrs['data-mfm-deg'] || 0 const deg = Number.parseFloat(fullAttrs['data-mfm-deg']) || 0
newAttrs.style = [ newAttrs.style = [
`transform: rotate(${deg}deg)`, `transform: rotate(${deg}deg)`,
'transform-origin: center', 'transform-origin: center',
@ -418,7 +432,8 @@ export default {
case 'twitch': case 'twitch':
case 'shake': case 'shake':
case 'jump': case 'jump':
case 'bounce': { case 'bounce':
case 'rainbow': {
const speed = fullAttrs['data-mfm-speed'] || '1s' const speed = fullAttrs['data-mfm-speed'] || '1s'
const delay = fullAttrs['data-mfm-delay'] || 0 const delay = fullAttrs['data-mfm-delay'] || 0

View file

@ -3,7 +3,44 @@
.mfm { .mfm {
display: inline-block; display: inline-block;
font-size: calc(var(--emoji-size) / 2);
&.-scale {
font-size: calc(var(--emoji-size) / 2);
}
&:not(.-scale) {
--emoji-size: 2em;
}
&.-pause {
animation-play-state: paused;
}
&[data-mfm-operator="sparkle"] {
position: relative;
&::before,
&::after {
content: '✨';
position: absolute;
opacity: 0.4;
z-index: -1;
animation-name: cheap-sparkle;
animation-duration: 1s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}
&::before {
left: 0;
animation-direction: alternate;
}
&::after {
right: 0;
animation-direction: alternate-reverse;
}
}
.emoji { .emoji {
/* Misskey's emoji width knows no bounds */ /* Misskey's emoji width knows no bounds */
@ -12,6 +49,10 @@
} }
} }
&:hover .mfm {
animation-play-state: running;
}
&.-faint { &.-faint {
color: var(--text); color: var(--text);
/* stylelint-disable declaration-no-important */ /* stylelint-disable declaration-no-important */
@ -149,257 +190,269 @@ a .RichContent {
} }
@keyframes mfm-jump { @keyframes mfm-jump {
0% { 0% {
transform: translateY(0) transform: translateY(0);
} }
25% { 25% {
transform: translateY(-1em) transform: translateY(-1em);
} }
50% { 50% {
transform: translateY(0) transform: translateY(0);
} }
75% { 75% {
transform: translateY(-0.5em) transform: translateY(-0.5em);
} }
100% { 100% {
transform: translateY(0) transform: translateY(0);
} }
} }
@keyframes mfm-bounce { @keyframes mfm-bounce {
0% { 0% {
transform: translateY(0) scale(1) transform: translateY(0) scale(1);
} }
25% { 25% {
transform: translateY(-16px) scale(1) transform: translateY(-16px) scale(1);
} }
50% { 50% {
transform: translateY(0) scale(1) transform: translateY(0) scale(1);
} }
75% { 75% {
transform: translateY(0) scale(1.5,.75) transform: translateY(0) scale(1.5,.75);
} }
100% { 100% {
transform: translateY(0) scale(1) transform: translateY(0) scale(1);
} }
} }
@keyframes mfm-twitch { @keyframes mfm-twitch {
0% { 0% {
transform: translate(7px,-2px) transform: translate(7px, -2px);
} }
5% { 5% {
transform: translate(-3px,1px) transform: translate(-3px, 1px);
} }
10% { 10% {
transform: translate(-7px,-1px) transform: translate(-7px, -1px);
} }
15% { 15% {
transform: translateY(-1px) transform: translateY(-1px);
} }
20% { 20% {
transform: translate(-8px,6px) transform: translate(-8px, 6px);
} }
25% { 25% {
transform: translate(-4px,-3px) transform: translate(-4px, -3px);
} }
30% { 30% {
transform: translate(-4px,-6px) transform: translate(-4px, -6px);
} }
35% { 35% {
transform: translate(-8px,-8px) transform: translate(-8px, -8px);
} }
40% { 40% {
transform: translate(4px,6px) transform: translate(4px, 6px);
} }
45% { 45% {
transform: translate(-3px,1px) transform: translate(-3px, 1px);
} }
50% { 50% {
transform: translate(2px,-10px) transform: translate(2px, -10px);
} }
55% { 55% {
transform: translate(-7px) transform: translate(-7px);
} }
60% { 60% {
transform: translate(-2px,4px) transform: translate(-2px, 4px);
} }
65% { 65% {
transform: translate(3px,-8px) transform: translate(3px, -8px);
} }
70% { 70% {
transform: translate(6px,7px) transform: translate(6px, 7px);
} }
75% { 75% {
transform: translate(-7px,-2px) transform: translate(-7px, -2px);
} }
80% { 80% {
transform: translate(-7px,-8px) transform: translate(-7px, -8px);
} }
85% { 85% {
transform: translate(9px,3px) transform: translate(9px, 3px);
} }
90% { 90% {
transform: translate(-3px,-2px) transform: translate(-3px, -2px);
} }
95% { 95% {
transform: translate(-10px,2px) transform: translate(-10px, 2px);
} }
100% { 100% {
transform: translate(-2px,-6px) transform: translate(-2px, -6px);
} }
} }
@keyframes mfm-shake { @keyframes mfm-shake {
0% { 0% {
transform: translate(-3px,-1px) rotate(-8deg) transform: translate(-3px, -1px) rotate(-8deg);
} }
5% { 5% {
transform: translateY(-1px) rotate(-10deg) transform: translateY(-1px) rotate(-10deg);
} }
10% { 10% {
transform: translate(1px,-3px) rotate(0) transform: translate(1px, -3px) rotate(0);
} }
15% { 15% {
transform: translate(1px,1px) rotate(11deg) transform: translate(1px, 1px) rotate(11deg);
} }
20% { 20% {
transform: translate(-2px,1px) rotate(1deg) transform: translate(-2px, 1px) rotate(1deg);
} }
25% { 25% {
transform: translate(-1px,-2px) rotate(-2deg) transform: translate(-1px, -2px) rotate(-2deg);
} }
30% { 30% {
transform: translate(-1px,2px) rotate(-3deg) transform: translate(-1px, 2px) rotate(-3deg);
} }
35% { 35% {
transform: translate(2px,1px)rotate(6deg) transform: translate(2px, 1px) rotate(6deg);
} }
40% { 40% {
transform: translate(-2px,-3px)rotate(-9deg) transform: translate(-2px, -3px) rotate(-9deg);
} }
45% { 45% {
transform: translateY(-1px)rotate(-12deg) transform: translateY(-1px) rotate(-12deg);
} }
50% { 50% {
transform: translate(1px,2px)rotate(10deg) transform: translate(1px, 2px) rotate(10deg);
} }
55% { 55% {
transform: translateY(-3px)rotate(8deg) transform: translateY(-3px) rotate(8deg);
} }
60% { 60% {
transform: translate(1px,-1px)rotate(8deg) transform: translate(1px, -1px) rotate(8deg);
} }
65% { 65% {
transform: translateY(-1px)rotate(-7deg) transform: translateY(-1px) rotate(-7deg);
} }
70% { 70% {
transform: translate(-1px,-3px)rotate(6deg) transform: translate(-1px, -3px) rotate(6deg);
} }
75% { 75% {
transform: translateY(-2px)rotate(4deg) transform: translateY(-2px) rotate(4deg);
} }
80% { 80% {
transform: translate(-2px,-1px)rotate(3deg) transform: translate(-2px, -1px) rotate(3deg);
} }
85% { 85% {
transform: translate(1px,-3px)rotate(-10deg) transform: translate(1px, -3px) rotate(-10deg);
} }
90% { 90% {
transform: translate(1px)rotate(3deg) transform: translate(1px) rotate(3deg);
} }
95% { 95% {
transform: translate(-2px)rotate(-3deg) transform: translate(-2px) rotate(-3deg);
} }
100% { 100% {
transform: translate(2px,1px)rotate(2deg) transform: translate(2px, 1px) rotate(2deg);
} }
} }
@keyframes mfm-rubberBand { @keyframes mfm-rubberBand {
0% { 0% {
transform: scale(1) transform: scale(1);
} }
30% { 30% {
transform: scale(1.25,.75) transform: scale(1.25, .75);
} }
40% { 40% {
transform: scale(.75,1.25) transform: scale(.75, 1.25);
} }
50% { 50% {
transform: scale(1.15,.85) transform: scale(1.15, .85);
} }
65% { 65% {
transform: scale(.95,1.05) transform: scale(.95, 1.05);
} }
75% { 75% {
transform: scale(1.05,.95) transform: scale(1.05, .95);
} }
100% { 100% {
transform: scale(1) transform: scale(1);
} }
} }
@keyframes mfm-rainbow { @keyframes mfm-rainbow {
0% { 0% {
filter: hue-rotate()contrast(150%)saturate(150%) filter: hue-rotate() contrast(150%) saturate(150%);
} }
100% { 100% {
filter: hue-rotate(360deg)contrast(150%)saturate(150%) filter: hue-rotate(360deg) contrast(150%) saturate(150%);
} }
}
@keyframes cheap-sparkle {
0% {
filter: hue-rotate() contrast(150%) saturate(150%);
transform: translateY(-0.5em);
}
100% {
filter: hue-rotate(360deg) contrast(150%) saturate(150%);
transform: translateY(0.5em);
}
} }

View file

@ -174,6 +174,16 @@
{{ $t('settings.non_square_emoji') }} {{ $t('settings.non_square_emoji') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<BooleanSetting path="scaleMfm">
{{ $t('settings.scale_mfm') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="pauseMfm">
{{ $t('settings.pause_mfm') }}
</BooleanSetting>
</li>
<li> <li>
<BooleanSetting <BooleanSetting
:local="true" :local="true"

View file

@ -136,6 +136,12 @@ const Status = {
allowNonSquareEmoji() { allowNonSquareEmoji() {
return this.mergedConfig.nonSquareEmoji return this.mergedConfig.nonSquareEmoji
}, },
pauseMfm() {
return this.mergedConfig.pauseMfm
},
scaleMfm() {
return this.mergedConfig.scaleMfm
},
repeaterClass() { repeaterClass() {
const user = this.statusoid.user const user = this.statusoid.user
return highlightClass(user) return highlightClass(user)

View file

@ -71,6 +71,8 @@
:html="retweeterHtml" :html="retweeterHtml"
:emoji="retweeterUser.emoji" :emoji="retweeterUser.emoji"
:allow-non-square-emoji="allowNonSquareEmoji" :allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
:is-local="retweeterUser.is_local" :is-local="retweeterUser.is_local"
/> />
</router-link> </router-link>

View file

@ -59,6 +59,12 @@ const StatusBody = {
allowNonSquareEmoji() { allowNonSquareEmoji() {
return this.mergedConfig.nonSquareEmoji return this.mergedConfig.nonSquareEmoji
}, },
pauseMfm() {
return this.mergedConfig.pauseMfm
},
scaleMfm() {
return this.mergedConfig.scaleMfm
},
// This is a bit hacky, but we want to approximate post height before rendering // This is a bit hacky, but we want to approximate post height before rendering
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them) // so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
// as well as approximate line count by counting characters and approximating ~80 // as well as approximate line count by counting characters and approximating ~80

View file

@ -16,6 +16,8 @@
:emoji="status.emojis" :emoji="status.emojis"
:is-local="status.isLocal" :is-local="status.isLocal"
:allow-non-square-emoji="allowNonSquareEmoji" :allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
/> />
<button <button
v-show="hasLongSubject && showingLongSubject" v-show="hasLongSubject && showingLongSubject"
@ -49,6 +51,8 @@
:attentions="status.attentions" :attentions="status.attentions"
:is-local="status.is_local" :is-local="status.is_local"
:allow-non-square-emoji="allowNonSquareEmoji" :allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
@parse-ready="onParseReady" @parse-ready="onParseReady"
/> />
<div <div

View file

@ -433,6 +433,12 @@ export default {
allowNonSquareEmoji() { allowNonSquareEmoji() {
return this.mergedConfig.nonSquareEmoji return this.mergedConfig.nonSquareEmoji
}, },
pauseMfm() {
return this.mergedConfig.pauseMfm
},
scaleMfm() {
return this.mergedConfig.scaleMfm
},
hideUserStats() { hideUserStats() {
return this.mergedConfig.hideUserStats return this.mergedConfig.hideUserStats
}, },

View file

@ -141,6 +141,8 @@
:html="editable ? newName : user.name_unescaped" :html="editable ? newName : user.name_unescaped"
:emoji="editable ? emoji : user.emoji" :emoji="editable ? emoji : user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji" :allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
/> />
</router-link> </router-link>
<EmojiInput <EmojiInput
@ -487,6 +489,8 @@
:html="editable ? escapedNewBio : user.description_html" :html="editable ? escapedNewBio : user.description_html"
:emoji="editable ? emoji : user.emoji" :emoji="editable ? emoji : user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji" :allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
:handle-links="true" :handle-links="true"
/> />
</template> </template>
@ -542,6 +546,8 @@
:html="field.name" :html="field.name"
:emoji="editable ? emoji : user.emoji" :emoji="editable ? emoji : user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji" :allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
/> />
</dt> </dt>
<dd <dd
@ -552,6 +558,8 @@
:html="field.value" :html="field.value"
:emoji="editable ? emoji : user.emoji" :emoji="editable ? emoji : user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji" :allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
/> />
</dd> </dd>
</dl> </dl>

View file

@ -27,6 +27,12 @@ const UserListPopover = {
allowNonSquareEmoji() { allowNonSquareEmoji() {
return useMergedConfigStore().mergedConfig.nonSquareEmoji return useMergedConfigStore().mergedConfig.nonSquareEmoji
}, },
pauseMfm() {
return useMergedConfigStore().mergedConfig.pauseMfm
},
scaleMfm() {
return useMergedConfigStore().mergedConfig.scaleMfm
},
}, },
methods: { methods: {
generateProfileLink(user) { generateProfileLink(user) {

View file

@ -29,6 +29,8 @@
:html="user.name_html" :html="user.name_html"
:emoji="user.emoji" :emoji="user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji" :allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
/> />
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
<span class="user-list-screen-name">{{ user.screen_name_ui }}</span><UnicodeDomainIndicator :user="user" /> <span class="user-list-screen-name">{{ user.screen_name_ui }}</span><UnicodeDomainIndicator :user="user" />

View file

@ -807,6 +807,8 @@
"sensitive_by_default": "Mark posts as sensitive by default", "sensitive_by_default": "Mark posts as sensitive by default",
"stop_gifs": "Pause animated images until you hover on them", "stop_gifs": "Pause animated images until you hover on them",
"non_square_emoji": "Allow non-square emoji", "non_square_emoji": "Allow non-square emoji",
"pause_mfm": "Pause MFM animations until you hover on them",
"scale_mfm": "Scale MFM animations with emoji size",
"streaming": "Automatically show new posts when scrolled to the top", "streaming": "Automatically show new posts when scrolled to the top",
"auto_update": "Show new posts automatically", "auto_update": "Show new posts automatically",
"user_mutes": "Users", "user_mutes": "Users",

View file

@ -237,6 +237,14 @@ export const INSTANCE_DEFAULT_CONFIG_DEFINITIONS = {
description: 'Allow emoji to be non-square (max 3:1 aspect)', description: 'Allow emoji to be non-square (max 3:1 aspect)',
default: true, default: true,
}, },
pauseMfm: {
description: 'Pause MFM animations',
default: true,
},
scaleMfm: {
description: 'Scale MFM animation with emoji size',
default: false,
},
replyVisibility: { replyVisibility: {
description: 'Type of replies to show', description: 'Type of replies to show',
default: 'all', default: 'all',