diff --git a/src/components/basic_user_card/basic_user_card.js b/src/components/basic_user_card/basic_user_card.js
index ae1f1c9c6..27e275bfb 100644
--- a/src/components/basic_user_card/basic_user_card.js
+++ b/src/components/basic_user_card/basic_user_card.js
@@ -36,6 +36,12 @@ const BasicUserCard = {
allowNonSquareEmoji() {
return useMergedConfigStore().mergedConfig.nonSquareEmoji
},
+ pauseMfm() {
+ return useMergedConfigStore().mergedConfig.pauseMfm
+ },
+ scaleMfm() {
+ return useMergedConfigStore().mergedConfig.scaleMfm
+ },
},
}
diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index 49786bf1b..39b269ea8 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -32,6 +32,8 @@
:html="user.name"
:emoji="user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
+ :pause-mfm="pauseMfm"
+ :scale-mfm="scaleMfm"
/>
diff --git a/src/components/chat_title/chat_title.js b/src/components/chat_title/chat_title.js
index 1d12384cc..e552ce9fd 100644
--- a/src/components/chat_title/chat_title.js
+++ b/src/components/chat_title/chat_title.js
@@ -21,5 +21,11 @@ export default {
allowNonSquareEmoji() {
return useMergedConfigStore().mergedConfig.nonSquareEmoji
},
+ pauseMfm() {
+ return useMergedConfigStore().mergedConfig.pauseMfm
+ },
+ scaleMfm() {
+ return useMergedConfigStore().mergedConfig.scaleMfm
+ },
},
}
diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue
index 313e66ce3..764e0de61 100644
--- a/src/components/chat_title/chat_title.vue
+++ b/src/components/chat_title/chat_title.vue
@@ -20,6 +20,8 @@
:html="htmlTitle"
:emoji="user.emoji || []"
:allow-non-square-emoji="allowNonSquareEmoji"
+ :pause-mfm="pauseMfm"
+ :scale-mfm="scaleMfm"
:is-local="user.is_local"
/>
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index b6ee27f9c..1bfe540a0 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -217,6 +217,12 @@ const Notification = {
allowNonSquareEmoji() {
return this.mergedConfig.nonSquareEmoji
},
+ pauseMfm() {
+ return this.mergedConfig.pauseMfm
+ },
+ scaleMfm() {
+ return this.mergedConfig.scaleMfm
+ },
shouldConfirmApprove() {
return this.mergedConfig.modalOnApproveFollow
},
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 39bd15426..daa208f09 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -72,6 +72,8 @@
:html="notification.from_profile.name_html"
:emoji="notification.from_profile.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
+ :pause-mfm="pauseMfm"
+ :scale-mfm="scaleMfm"
:is-local="notification.from_profile.is_local"
/>
diff --git a/src/components/poll/poll.js b/src/components/poll/poll.js
index 3ea9a1890..b93a6699d 100644
--- a/src/components/poll/poll.js
+++ b/src/components/poll/poll.js
@@ -57,6 +57,12 @@ export default {
allowNonSquareEmoji() {
return useMergedConfigStore().mergedConfig.nonSquareEmoji
},
+ pauseMfm() {
+ return useMergedConfigStore().mergedConfig.pauseMfm
+ },
+ scaleMfm() {
+ return useMergedConfigStore().mergedConfig.scaleMfm
+ },
loggedIn() {
return this.$store.state.users.currentUser
},
diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue
index 9ac859824..89bb1324e 100644
--- a/src/components/poll/poll.vue
+++ b/src/components/poll/poll.vue
@@ -25,6 +25,8 @@
:handle-links="false"
:emoji="emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
+ :pause-mfm="pauseMfm"
+ :scale-mfm="scaleMfm"
/>
x).join(' ')
newAttrs['data-mfm-operator'] = mfmOperator
switch(mfmOperator) {
case 'position': {
- const x = fullAttrs['data-mfm-x'].replace(/\.+$/,'') || 0
- const y = fullAttrs['data-mfm-y'].replace(/\.+$/,'') || 0
+ const x = Number.parseFloat(fullAttrs['data-mfm-x']) || 0
+ const y = Number.parseFloat(fullAttrs['data-mfm-y']) || 0
newAttrs.style = [
'transform:',
`translate(calc(${x} * (var(--emoji-size) / 2)), `,
@@ -334,8 +348,8 @@ export default {
break
}
case 'scale': {
- const x = fullAttrs['data-mfm-x'].replace(/\.+$/,'') || 1
- const y = fullAttrs['data-mfm-y'].replace(/\.+$/,'') || 1
+ const x = Number.parseFloat(fullAttrs['data-mfm-x']) || 1
+ const y = Number.parseFloat(fullAttrs['data-mfm-y']) || 1
newAttrs.style = [
'transform:',
`scale(${x}, ${y})`,
@@ -343,7 +357,7 @@ export default {
break
}
case 'rotate': {
- const deg = fullAttrs['data-mfm-deg'] || 0
+ const deg = Number.parseFloat(fullAttrs['data-mfm-deg']) || 0
newAttrs.style = [
`transform: rotate(${deg}deg)`,
'transform-origin: center',
@@ -418,7 +432,8 @@ export default {
case 'twitch':
case 'shake':
case 'jump':
- case 'bounce': {
+ case 'bounce':
+ case 'rainbow': {
const speed = fullAttrs['data-mfm-speed'] || '1s'
const delay = fullAttrs['data-mfm-delay'] || 0
diff --git a/src/components/rich_content/rich_content.scss b/src/components/rich_content/rich_content.scss
index f048e529e..b2d756da2 100644
--- a/src/components/rich_content/rich_content.scss
+++ b/src/components/rich_content/rich_content.scss
@@ -3,7 +3,44 @@
.mfm {
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 {
/* Misskey's emoji width knows no bounds */
@@ -12,6 +49,10 @@
}
}
+ &:hover .mfm {
+ animation-play-state: running;
+ }
+
&.-faint {
color: var(--text);
/* stylelint-disable declaration-no-important */
@@ -149,257 +190,269 @@ a .RichContent {
}
@keyframes mfm-jump {
- 0% {
- transform: translateY(0)
- }
+ 0% {
+ transform: translateY(0);
+ }
- 25% {
- transform: translateY(-1em)
- }
+ 25% {
+ transform: translateY(-1em);
+ }
- 50% {
- transform: translateY(0)
- }
+ 50% {
+ transform: translateY(0);
+ }
- 75% {
- transform: translateY(-0.5em)
- }
+ 75% {
+ transform: translateY(-0.5em);
+ }
- 100% {
- transform: translateY(0)
- }
+ 100% {
+ transform: translateY(0);
+ }
}
@keyframes mfm-bounce {
- 0% {
- transform: translateY(0) scale(1)
- }
+ 0% {
+ transform: translateY(0) scale(1);
+ }
- 25% {
- transform: translateY(-16px) scale(1)
- }
+ 25% {
+ transform: translateY(-16px) scale(1);
+ }
- 50% {
- transform: translateY(0) scale(1)
- }
+ 50% {
+ transform: translateY(0) scale(1);
+ }
- 75% {
- transform: translateY(0) scale(1.5,.75)
- }
+ 75% {
+ transform: translateY(0) scale(1.5,.75);
+ }
- 100% {
- transform: translateY(0) scale(1)
- }
+ 100% {
+ transform: translateY(0) scale(1);
+ }
}
@keyframes mfm-twitch {
- 0% {
- transform: translate(7px,-2px)
- }
+ 0% {
+ transform: translate(7px, -2px);
+ }
- 5% {
- transform: translate(-3px,1px)
- }
+ 5% {
+ transform: translate(-3px, 1px);
+ }
- 10% {
- transform: translate(-7px,-1px)
- }
+ 10% {
+ transform: translate(-7px, -1px);
+ }
- 15% {
- transform: translateY(-1px)
- }
+ 15% {
+ transform: translateY(-1px);
+ }
- 20% {
- transform: translate(-8px,6px)
- }
+ 20% {
+ transform: translate(-8px, 6px);
+ }
- 25% {
- transform: translate(-4px,-3px)
- }
+ 25% {
+ transform: translate(-4px, -3px);
+ }
- 30% {
- transform: translate(-4px,-6px)
- }
+ 30% {
+ transform: translate(-4px, -6px);
+ }
- 35% {
- transform: translate(-8px,-8px)
- }
+ 35% {
+ transform: translate(-8px, -8px);
+ }
- 40% {
- transform: translate(4px,6px)
- }
+ 40% {
+ transform: translate(4px, 6px);
+ }
- 45% {
- transform: translate(-3px,1px)
- }
+ 45% {
+ transform: translate(-3px, 1px);
+ }
- 50% {
- transform: translate(2px,-10px)
- }
+ 50% {
+ transform: translate(2px, -10px);
+ }
- 55% {
- transform: translate(-7px)
- }
+ 55% {
+ transform: translate(-7px);
+ }
- 60% {
- transform: translate(-2px,4px)
- }
+ 60% {
+ transform: translate(-2px, 4px);
+ }
- 65% {
- transform: translate(3px,-8px)
- }
+ 65% {
+ transform: translate(3px, -8px);
+ }
- 70% {
- transform: translate(6px,7px)
- }
+ 70% {
+ transform: translate(6px, 7px);
+ }
- 75% {
- transform: translate(-7px,-2px)
- }
+ 75% {
+ transform: translate(-7px, -2px);
+ }
- 80% {
- transform: translate(-7px,-8px)
- }
+ 80% {
+ transform: translate(-7px, -8px);
+ }
- 85% {
- transform: translate(9px,3px)
- }
+ 85% {
+ transform: translate(9px, 3px);
+ }
- 90% {
- transform: translate(-3px,-2px)
- }
+ 90% {
+ transform: translate(-3px, -2px);
+ }
- 95% {
- transform: translate(-10px,2px)
- }
+ 95% {
+ transform: translate(-10px, 2px);
+ }
- 100% {
- transform: translate(-2px,-6px)
- }
+ 100% {
+ transform: translate(-2px, -6px);
+ }
}
@keyframes mfm-shake {
- 0% {
- transform: translate(-3px,-1px) rotate(-8deg)
- }
+ 0% {
+ transform: translate(-3px, -1px) rotate(-8deg);
+ }
- 5% {
- transform: translateY(-1px) rotate(-10deg)
- }
+ 5% {
+ transform: translateY(-1px) rotate(-10deg);
+ }
- 10% {
- transform: translate(1px,-3px) rotate(0)
- }
+ 10% {
+ transform: translate(1px, -3px) rotate(0);
+ }
- 15% {
- transform: translate(1px,1px) rotate(11deg)
- }
+ 15% {
+ transform: translate(1px, 1px) rotate(11deg);
+ }
- 20% {
- transform: translate(-2px,1px) rotate(1deg)
- }
+ 20% {
+ transform: translate(-2px, 1px) rotate(1deg);
+ }
- 25% {
- transform: translate(-1px,-2px) rotate(-2deg)
- }
+ 25% {
+ transform: translate(-1px, -2px) rotate(-2deg);
+ }
- 30% {
- transform: translate(-1px,2px) rotate(-3deg)
- }
+ 30% {
+ transform: translate(-1px, 2px) rotate(-3deg);
+ }
- 35% {
- transform: translate(2px,1px)rotate(6deg)
- }
+ 35% {
+ transform: translate(2px, 1px) rotate(6deg);
+ }
- 40% {
- transform: translate(-2px,-3px)rotate(-9deg)
- }
+ 40% {
+ transform: translate(-2px, -3px) rotate(-9deg);
+ }
- 45% {
- transform: translateY(-1px)rotate(-12deg)
- }
+ 45% {
+ transform: translateY(-1px) rotate(-12deg);
+ }
- 50% {
- transform: translate(1px,2px)rotate(10deg)
- }
+ 50% {
+ transform: translate(1px, 2px) rotate(10deg);
+ }
- 55% {
- transform: translateY(-3px)rotate(8deg)
- }
+ 55% {
+ transform: translateY(-3px) rotate(8deg);
+ }
- 60% {
- transform: translate(1px,-1px)rotate(8deg)
- }
+ 60% {
+ transform: translate(1px, -1px) rotate(8deg);
+ }
- 65% {
- transform: translateY(-1px)rotate(-7deg)
- }
+ 65% {
+ transform: translateY(-1px) rotate(-7deg);
+ }
- 70% {
- transform: translate(-1px,-3px)rotate(6deg)
- }
+ 70% {
+ transform: translate(-1px, -3px) rotate(6deg);
+ }
- 75% {
- transform: translateY(-2px)rotate(4deg)
- }
+ 75% {
+ transform: translateY(-2px) rotate(4deg);
+ }
- 80% {
- transform: translate(-2px,-1px)rotate(3deg)
- }
+ 80% {
+ transform: translate(-2px, -1px) rotate(3deg);
+ }
- 85% {
- transform: translate(1px,-3px)rotate(-10deg)
- }
+ 85% {
+ transform: translate(1px, -3px) rotate(-10deg);
+ }
- 90% {
- transform: translate(1px)rotate(3deg)
- }
+ 90% {
+ transform: translate(1px) rotate(3deg);
+ }
- 95% {
- transform: translate(-2px)rotate(-3deg)
- }
+ 95% {
+ transform: translate(-2px) rotate(-3deg);
+ }
- 100% {
- transform: translate(2px,1px)rotate(2deg)
- }
+ 100% {
+ transform: translate(2px, 1px) rotate(2deg);
+ }
}
@keyframes mfm-rubberBand {
- 0% {
- transform: scale(1)
- }
+ 0% {
+ transform: scale(1);
+ }
- 30% {
- transform: scale(1.25,.75)
- }
+ 30% {
+ transform: scale(1.25, .75);
+ }
- 40% {
- transform: scale(.75,1.25)
- }
+ 40% {
+ transform: scale(.75, 1.25);
+ }
- 50% {
- transform: scale(1.15,.85)
- }
+ 50% {
+ transform: scale(1.15, .85);
+ }
- 65% {
- transform: scale(.95,1.05)
- }
+ 65% {
+ transform: scale(.95, 1.05);
+ }
- 75% {
- transform: scale(1.05,.95)
- }
+ 75% {
+ transform: scale(1.05, .95);
+ }
- 100% {
- transform: scale(1)
- }
+ 100% {
+ transform: scale(1);
+ }
}
@keyframes mfm-rainbow {
- 0% {
- filter: hue-rotate()contrast(150%)saturate(150%)
- }
+ 0% {
+ filter: hue-rotate() contrast(150%) saturate(150%);
+ }
- 100% {
- filter: hue-rotate(360deg)contrast(150%)saturate(150%)
- }
+ 100% {
+ 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);
+ }
}
diff --git a/src/components/settings_modal/tabs/posts_tab.vue b/src/components/settings_modal/tabs/posts_tab.vue
index fc33d5476..cbba67f12 100644
--- a/src/components/settings_modal/tabs/posts_tab.vue
+++ b/src/components/settings_modal/tabs/posts_tab.vue
@@ -174,6 +174,16 @@
{{ $t('settings.non_square_emoji') }}
+
+
+ {{ $t('settings.scale_mfm') }}
+
+
+
+
+ {{ $t('settings.pause_mfm') }}
+
+
diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js
index 7a46a1a3b..e6752cb1a 100644
--- a/src/components/status_body/status_body.js
+++ b/src/components/status_body/status_body.js
@@ -59,6 +59,12 @@ const StatusBody = {
allowNonSquareEmoji() {
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
// so we count newlines (masto uses for paragraphs, GS uses between them)
// as well as approximate line count by counting characters and approximating ~80
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index b611d69b8..e8994180a 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -16,6 +16,8 @@
:emoji="status.emojis"
:is-local="status.isLocal"
:allow-non-square-emoji="allowNonSquareEmoji"
+ :pause-mfm="pauseMfm"
+ :scale-mfm="scaleMfm"
/>
@@ -542,6 +546,8 @@
:html="field.name"
:emoji="editable ? emoji : user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
+ :pause-mfm="pauseMfm"
+ :scale-mfm="scaleMfm"
/>
diff --git a/src/components/user_list_popover/user_list_popover.js b/src/components/user_list_popover/user_list_popover.js
index 7f9ba4ffb..84107d6b1 100644
--- a/src/components/user_list_popover/user_list_popover.js
+++ b/src/components/user_list_popover/user_list_popover.js
@@ -27,6 +27,12 @@ const UserListPopover = {
allowNonSquareEmoji() {
return useMergedConfigStore().mergedConfig.nonSquareEmoji
},
+ pauseMfm() {
+ return useMergedConfigStore().mergedConfig.pauseMfm
+ },
+ scaleMfm() {
+ return useMergedConfigStore().mergedConfig.scaleMfm
+ },
},
methods: {
generateProfileLink(user) {
diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue
index 02fa5a2d2..ddeb6f6f6 100644
--- a/src/components/user_list_popover/user_list_popover.vue
+++ b/src/components/user_list_popover/user_list_popover.vue
@@ -29,6 +29,8 @@
:html="user.name_html"
:emoji="user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
+ :pause-mfm="pauseMfm"
+ :scale-mfm="scaleMfm"
/>
{{ user.screen_name_ui }}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 4a35b48f5..025f7ad8b 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -807,6 +807,8 @@
"sensitive_by_default": "Mark posts as sensitive by default",
"stop_gifs": "Pause animated images until you hover on them",
"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",
"auto_update": "Show new posts automatically",
"user_mutes": "Users",
diff --git a/src/modules/default_config_state.js b/src/modules/default_config_state.js
index ec190af28..d9a2ba247 100644
--- a/src/modules/default_config_state.js
+++ b/src/modules/default_config_state.js
@@ -237,6 +237,14 @@ export const INSTANCE_DEFAULT_CONFIG_DEFINITIONS = {
description: 'Allow emoji to be non-square (max 3:1 aspect)',
default: true,
},
+ pauseMfm: {
+ description: 'Pause MFM animations',
+ default: true,
+ },
+ scaleMfm: {
+ description: 'Scale MFM animation with emoji size',
+ default: false,
+ },
replyVisibility: {
description: 'Type of replies to show',
default: 'all',