@@ -134,11 +137,15 @@ const mobile = computed(() => {
})
const contrast = computed(() => {
+ if (props.modelValue == null) return null
const bg = hex2rgb(props.modelValue.bg)
+ const text = hex2rgb(props.modelValue.text)
+ const link = hex2rgb(props.modelValue.link)
+ if (text == null || link == null) return null
return {
- text: hints(getContrastRatio(bg, hex2rgb(props.modelValue.text))),
- link: hints(getContrastRatio(bg, hex2rgb(props.modelValue.link))),
+ text: hints(getContrastRatio(bg, text)),
+ link: hints(getContrastRatio(bg, link)),
}
})
@@ -232,15 +239,6 @@ const updatePalette = (paletteKey, value) => {
grid-column: 1 / span 2;
}
}
-
- .color-input {
- display: grid;
- gap: 0.5em;
-
- label {
- flex: 1;
- }
- }
}
}
diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx
index fc17a529f..cdaebe2ff 100644
--- a/src/components/rich_content/rich_content.jsx
+++ b/src/components/rich_content/rich_content.jsx
@@ -313,7 +313,134 @@ export default {
const newChildren = Array.isArray(children)
? [...children].reverse().map(processItemReverse).reverse()
: children
- return
{newChildren}
+ const attrs = getAttrs(opener)
+ const newAttrs = { ...attrs }
+ const fullAttrs = getAttrs(opener, () => true)
+ const classname = fullAttrs['class']
+ const isMFM = classname?.startsWith('mfm-')
+ if (isMFM) {
+ const mfmOperator = /^mfm-(\w+)$/.exec(classname)?.[1]
+ newAttrs['class'] = 'mfm'
+ 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
+ newAttrs.style = [
+ 'transform:',
+ `translate(calc(${x} * (var(--emoji-size) / 2)), `,
+ `calc(${y} * (var(--emoji-size) / 2)))`,
+ ].join(' ')
+ break
+ }
+ case 'scale': {
+ const x = fullAttrs['data-mfm-x'].replace(/\.+$/,'') || 1
+ const y = fullAttrs['data-mfm-y'].replace(/\.+$/,'') || 1
+ newAttrs.style = [
+ 'transform:',
+ `scale(${x}, ${y})`,
+ ].join(' ')
+ break
+ }
+ case 'rotate': {
+ const deg = fullAttrs['data-mfm-deg'] || 0
+ newAttrs.style = [
+ `transform: rotate(${deg}deg)`,
+ 'transform-origin: center',
+ ].join(';')
+ break
+ }
+ case 'bg': {
+ const color = fullAttrs['data-mfm-color'] || 0
+ newAttrs.style = [
+ `background-color: #${color}`,
+ ].join(' ')
+ break
+ }
+ case 'fg': {
+ const color = fullAttrs['data-mfm-color'] || 0
+ newAttrs.style = [
+ `color: #${color}`,
+ ].join(';')
+ break
+ }
+ case 'spin': {
+ const speed = fullAttrs['data-mfm-speed'] || '1s'
+ const delay = fullAttrs['data-mfm-delay'] || 0
+ const left = fullAttrs['data-mfm-left'] != null
+ const alternate = fullAttrs['data-mfm-alternate'] != null
+ const y = fullAttrs['data-mfm-y'] != null
+ const x = fullAttrs['data-mfm-x'] != null
+
+ const anim = [
+ x ? 'mfm-spinX' : null,
+ y ? 'mfm-spinY' : null,
+ 'mfm-spin'
+ ].filter(a => a)[0]
+
+ const direction = [
+ alternate ? 'alternate' : null,
+ left ? 'reverse' : null,
+ 'normal',
+ ].filter(a => a)[0]
+
+ newAttrs.style = [
+ `animation-name: ${anim}`,
+ `animation-duration: ${speed}`,
+ 'animation-iteration-count: infinite',
+ `animation-delay: ${delay}`,
+ `animation-direction: ${direction}`,
+ 'animation-fill-mode: none',
+ 'animation-timing-function: linear',
+ ].join(';')
+ break
+ }
+ case 'flip': {
+ newAttrs.style = 'transform: scaleX(-1)'
+ break
+ }
+ case 'border': {
+ const width = fullAttrs['data-mfm-width'] || '0'
+ const style = fullAttrs['data-mfm-style'] || 'solid'
+ const color = fullAttrs['data-mfm-color'] || 'transparent'
+ const radius = fullAttrs['data-mfm-radius'] || '0'
+ const noclip = fullAttrs['data-mfm-noclip'] || false
+
+ newAttrs.style = [
+ `border: ${width} ${style} ${color}`,
+ `border-radius: ${radius}`,
+ `overflow: ${noclip ? 'visible' : 'clip'}`
+ ].join(';')
+ break
+ }
+ case 'tada':
+ case 'jelly':
+ case 'twitch':
+ case 'shake':
+ case 'jump':
+ case 'bounce': {
+ const speed = fullAttrs['data-mfm-speed'] || '1s'
+ const delay = fullAttrs['data-mfm-delay'] || 0
+
+ const rules = [
+ `animation-name: mfm-${mfmOperator}`,
+ `animation-duration: ${speed}`,
+ 'animation-iteration-count: infinite',
+ `animation-delay: ${delay}`,
+ 'animation-direction: normal',
+ 'animation-fill-mode: none',
+ 'animation-timing-function: linear',
+ ].join(';')
+ newAttrs.style = rules
+ break
+ }
+ default:
+ console.log(mfmOperator, opener)
+ console.log(mfmOperator)
+ break
+ }
+ }
+ return
{newChildren}
} else {
return
}
diff --git a/src/components/rich_content/rich_content.scss b/src/components/rich_content/rich_content.scss
index 33effc623..f048e529e 100644
--- a/src/components/rich_content/rich_content.scss
+++ b/src/components/rich_content/rich_content.scss
@@ -1,6 +1,17 @@
.RichContent {
font-family: var(--font);
+ .mfm {
+ display: inline-block;
+ font-size: calc(var(--emoji-size) / 2);
+
+ .emoji {
+ /* Misskey's emoji width knows no bounds */
+ /* stylelint-disable-next-line declaration-no-important */
+ max-width: unset !important;
+ }
+ }
+
&.-faint {
color: var(--text);
/* stylelint-disable declaration-no-important */
@@ -106,3 +117,289 @@ a .RichContent {
/* stylelint-disable-next-line declaration-no-important */
color: var(--link) !important;
}
+
+@keyframes mfm-spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes mfm-spinX {
+ 0% {
+ transform: perspective(8em) rotateX(0);
+ }
+
+ 100% {
+ transform: perspective(8em) rotateX(360deg);
+ }
+}
+
+@keyframes mfm-spinY {
+ 0% {
+ transform: perspective(8em) rotateY(0);
+ }
+
+ 100% {
+ transform: perspective(8em) rotateY(360deg);
+ }
+}
+
+@keyframes mfm-jump {
+ 0% {
+ transform: translateY(0)
+ }
+
+ 25% {
+ transform: translateY(-1em)
+ }
+
+ 50% {
+ transform: translateY(0)
+ }
+
+ 75% {
+ transform: translateY(-0.5em)
+ }
+
+ 100% {
+ transform: translateY(0)
+ }
+}
+
+@keyframes mfm-bounce {
+ 0% {
+ transform: translateY(0) scale(1)
+ }
+
+ 25% {
+ transform: translateY(-16px) scale(1)
+ }
+
+ 50% {
+ transform: translateY(0) scale(1)
+ }
+
+ 75% {
+ transform: translateY(0) scale(1.5,.75)
+ }
+
+ 100% {
+ transform: translateY(0) scale(1)
+ }
+}
+
+@keyframes mfm-twitch {
+ 0% {
+ transform: translate(7px,-2px)
+ }
+
+ 5% {
+ transform: translate(-3px,1px)
+ }
+
+ 10% {
+ transform: translate(-7px,-1px)
+ }
+
+ 15% {
+ transform: translateY(-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)
+ }
+
+ 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: translateY(-1px) rotate(-10deg)
+ }
+
+ 10% {
+ transform: translate(1px,-3px) rotate(0)
+ }
+
+ 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: translateY(-1px)rotate(-12deg)
+ }
+
+ 50% {
+ transform: translate(1px,2px)rotate(10deg)
+ }
+
+ 55% {
+ transform: translateY(-3px)rotate(8deg)
+ }
+
+ 60% {
+ transform: translate(1px,-1px)rotate(8deg)
+ }
+
+ 65% {
+ transform: translateY(-1px)rotate(-7deg)
+ }
+
+ 70% {
+ transform: translate(-1px,-3px)rotate(6deg)
+ }
+
+ 75% {
+ transform: translateY(-2px)rotate(4deg)
+ }
+
+ 80% {
+ transform: translate(-2px,-1px)rotate(3deg)
+ }
+
+ 85% {
+ transform: translate(1px,-3px)rotate(-10deg)
+ }
+
+ 90% {
+ transform: translate(1px)rotate(3deg)
+ }
+
+ 95% {
+ transform: translate(-2px)rotate(-3deg)
+ }
+
+ 100% {
+ transform: translate(2px,1px)rotate(2deg)
+ }
+}
+
+@keyframes mfm-rubberBand {
+ 0% {
+ transform: scale(1)
+ }
+
+ 30% {
+ transform: scale(1.25,.75)
+ }
+
+ 40% {
+ transform: scale(.75,1.25)
+ }
+
+ 50% {
+ transform: scale(1.15,.85)
+ }
+
+ 65% {
+ transform: scale(.95,1.05)
+ }
+
+ 75% {
+ transform: scale(1.05,.95)
+ }
+
+ 100% {
+ transform: scale(1)
+ }
+}
+
+@keyframes mfm-rainbow {
+ 0% {
+ filter: hue-rotate()contrast(150%)saturate(150%)
+ }
+
+ 100% {
+ filter: hue-rotate(360deg)contrast(150%)saturate(150%)
+ }
+}
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 0cfc7cdba..996dc6437 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -97,7 +97,6 @@ const Status = {
expandable: Boolean,
focused: Boolean,
- highlight: Boolean,
compact: Boolean,
isPreview: Boolean,
noHeading: Boolean,
@@ -320,7 +319,7 @@ const Status = {
},
shouldNotMute() {
if (this.ignoreMute) return true
- if (this.isFocused) return true
+ if (this.focused) return true
const { status } = this
const { reblog } = status
return (
@@ -357,16 +356,6 @@ const Status = {
this.muteFilterHits.some((x) => x.hide))
)
},
- isFocused() {
- // retweet or root of an expanded conversation
- if (this.focused) {
- return true
- } else if (!this.inConversation) {
- return false
- }
- // use conversation highlight only when in conversation
- return this.status.id === this.highlight
- },
isReply() {
return !!(
this.status.in_reply_to_status_id && this.status.in_reply_to_user_id
@@ -415,7 +404,7 @@ const Status = {
shouldDisplayFavsAndRepeats() {
return (
!this.hidePostStats &&
- this.isFocused &&
+ this.focused &&
(this.combinedFavsAndRepeatsUsers.length > 0 ||
this.statusFromGlobalRepository.quotes_count)
)
@@ -558,9 +547,9 @@ const Status = {
toggleThreadDisplay() {
this.controlledToggleThreadDisplay()
},
- scrollIfHighlighted(highlightId) {
+ scrollIfFocused(focusedId) {
if (this.$el.getBoundingClientRect == null) return
- const id = highlightId
+ const id = focusedId
if (this.status.id === id) {
const rect = this.$el.getBoundingClientRect()
if (rect.top < 100) {
@@ -577,13 +566,13 @@ const Status = {
},
},
watch: {
- highlight: function (id) {
- this.scrollIfHighlighted(id)
+ focused: function (id) {
+ this.scrollIfFocused(id)
},
'status.repeat_num': function (num) {
// refetch repeats when repeat_num is changed in any way
if (
- this.isFocused &&
+ this.focused &&
this.statusFromGlobalRepository.rebloggedBy &&
this.statusFromGlobalRepository.rebloggedBy.length !== num
) {
@@ -593,7 +582,7 @@ const Status = {
'status.fave_num': function (num) {
// refetch favs when fave_num is changed in any way
if (
- this.isFocused &&
+ this.focused &&
this.statusFromGlobalRepository.favoritedBy &&
this.statusFromGlobalRepository.favoritedBy.length !== num
) {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index f301ad890..4dcc1e07d 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -3,7 +3,7 @@
v-if="!hideStatus"
ref="root"
class="Status"
- :class="[{ '-focused': isFocused }, { '-conversation': inlineExpanded }]"
+ :class="[{ '-focused': focused }, { '-conversation': inlineExpanded }]"
>
diff --git a/src/components/thread_tree/thread_tree.js b/src/components/thread_tree/thread_tree.js
index 7631d127f..75232a259 100644
--- a/src/components/thread_tree/thread_tree.js
+++ b/src/components/thread_tree/thread_tree.js
@@ -19,10 +19,8 @@ const ThreadTree = {
pinnedStatusIdsObject: Object,
profileUserId: String,
- isFocusedFunction: Function,
- highlight: String,
+ focused: String,
getReplies: Function,
- setHighlight: Function,
toggleExpanded: Function,
simple: Boolean,
diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue
index 72f8512e5..556858ad7 100644
--- a/src/components/thread_tree/thread_tree.vue
+++ b/src/components/thread_tree/thread_tree.vue
@@ -8,9 +8,8 @@
:inline-expanded="collapsable && isExpanded"
:expandable="!isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
- :focused="isFocusedFunction(status.id)"
:in-conversation="isExpanded"
- :highlight="highlight === status.id"
+ :focused="focused === status.id || focused === status.retweeted_status?.id"
:in-profile="inProfile"
:profile-user-id="profileUserId"
class="conversation-status conversation-status-treeview status-fadein panel-body"
@@ -42,10 +41,8 @@
:pinned-status-ids-object="pinnedStatusIdsObject"
:profile-user-id="profileUserId"
- :is-focused-function="isFocusedFunction"
:get-replies="getReplies"
- :highlight="highlight"
- :set-highlight="setHighlight"
+ :focused="focused"
:toggle-expanded="toggleExpanded"
:simple="simple"