Skip to content

Commit 9080074

Browse files
ClearlyClairehiyuki2578
authored andcommitted
Play animated custom emoji on hover (mastodon#11348)
* Play animated custom emoji on hover in status * Play animated custom emoji on hover in display names * Play animated custom emoji on hover in bios/bio fields * Add support for animation on hover on public pages emojis too * Fix tests * Code style cleanup
1 parent 832eb30 commit 9080074

7 files changed

Lines changed: 149 additions & 22 deletions

File tree

app/javascript/mastodon/components/display_name.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import ImmutablePropTypes from 'react-immutable-proptypes';
33
import PropTypes from 'prop-types';
4+
import { autoPlayGif } from 'mastodon/initial_state';
45

56
export default class DisplayName extends React.PureComponent {
67

@@ -10,6 +11,47 @@ export default class DisplayName extends React.PureComponent {
1011
localDomain: PropTypes.string,
1112
};
1213

14+
_updateEmojis () {
15+
const node = this.node;
16+
17+
if (!node || autoPlayGif) {
18+
return;
19+
}
20+
21+
const emojis = node.querySelectorAll('.custom-emoji');
22+
23+
for (var i = 0; i < emojis.length; i++) {
24+
let emoji = emojis[i];
25+
if (emoji.classList.contains('status-emoji')) {
26+
continue;
27+
}
28+
emoji.classList.add('status-emoji');
29+
30+
emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
31+
emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
32+
}
33+
}
34+
35+
componentDidMount () {
36+
this._updateEmojis();
37+
}
38+
39+
componentDidUpdate () {
40+
this._updateEmojis();
41+
}
42+
43+
handleEmojiMouseEnter = ({ target }) => {
44+
target.src = target.getAttribute('data-original');
45+
}
46+
47+
handleEmojiMouseLeave = ({ target }) => {
48+
target.src = target.getAttribute('data-static');
49+
}
50+
51+
setRef = (c) => {
52+
this.node = c;
53+
}
54+
1355
render () {
1456
const { others, localDomain } = this.props;
1557

@@ -39,7 +81,7 @@ export default class DisplayName extends React.PureComponent {
3981
}
4082

4183
return (
42-
<span className='display-name'>
84+
<span className='display-name' ref={this.setRef}>
4385
{displayName} {suffix}
4486
</span>
4587
);

app/javascript/mastodon/components/status_content.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Permalink from './permalink';
77
import classnames from 'classnames';
88
import PollContainer from 'mastodon/containers/poll_container';
99
import Icon from 'mastodon/components/icon';
10+
import { autoPlayGif } from 'mastodon/initial_state';
1011

1112
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
1213

@@ -71,12 +72,35 @@ export default class StatusContent extends React.PureComponent {
7172
}
7273
}
7374

75+
_updateStatusEmojis () {
76+
const node = this.node;
77+
78+
if (!node || autoPlayGif) {
79+
return;
80+
}
81+
82+
const emojis = node.querySelectorAll('.custom-emoji');
83+
84+
for (var i = 0; i < emojis.length; i++) {
85+
let emoji = emojis[i];
86+
if (emoji.classList.contains('status-emoji')) {
87+
continue;
88+
}
89+
emoji.classList.add('status-emoji');
90+
91+
emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
92+
emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
93+
}
94+
}
95+
7496
componentDidMount () {
7597
this._updateStatusLinks();
98+
this._updateStatusEmojis();
7699
}
77100

78101
componentDidUpdate () {
79102
this._updateStatusLinks();
103+
this._updateStatusEmojis();
80104
}
81105

82106
onMentionClick = (mention, e) => {
@@ -95,6 +119,14 @@ export default class StatusContent extends React.PureComponent {
95119
}
96120
}
97121

122+
handleEmojiMouseEnter = ({ target }) => {
123+
target.src = target.getAttribute('data-original');
124+
}
125+
126+
handleEmojiMouseLeave = ({ target }) => {
127+
target.src = target.getAttribute('data-static');
128+
}
129+
98130
handleMouseDown = (e) => {
99131
this.startXY = [e.clientX, e.clientY];
100132
}

app/javascript/mastodon/features/account/components/header.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,47 @@ class Header extends ImmutablePureComponent {
7979
return !location.pathname.match(/\/(followers|following)\/?$/);
8080
}
8181

82+
_updateEmojis () {
83+
const node = this.node;
84+
85+
if (!node || autoPlayGif) {
86+
return;
87+
}
88+
89+
const emojis = node.querySelectorAll('.custom-emoji');
90+
91+
for (var i = 0; i < emojis.length; i++) {
92+
let emoji = emojis[i];
93+
if (emoji.classList.contains('status-emoji')) {
94+
continue;
95+
}
96+
emoji.classList.add('status-emoji');
97+
98+
emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
99+
emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
100+
}
101+
}
102+
103+
componentDidMount () {
104+
this._updateEmojis();
105+
}
106+
107+
componentDidUpdate () {
108+
this._updateEmojis();
109+
}
110+
111+
handleEmojiMouseEnter = ({ target }) => {
112+
target.src = target.getAttribute('data-original');
113+
}
114+
115+
handleEmojiMouseLeave = ({ target }) => {
116+
target.src = target.getAttribute('data-static');
117+
}
118+
119+
setRef = (c) => {
120+
this.node = c;
121+
}
122+
82123
render () {
83124
const { account, intl, domain, identity_proofs } = this.props;
84125

@@ -201,7 +242,7 @@ class Header extends ImmutablePureComponent {
201242
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
202243

203244
return (
204-
<div className={classNames('account__header', { inactive: !!account.get('moved') })}>
245+
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
205246
<div className='account__header__image'>
206247
<div className='account__header__info'>
207248
{info}

app/javascript/mastodon/features/emoji/emoji.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const emojify = (str, customEmojis = {}) => {
2929
// if you want additional emoji handler, add statements below which set replacement and return true.
3030
if (shortname in customEmojis) {
3131
const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url;
32-
replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`;
32+
replacement = `<img draggable="false" class="emojione custom-emoji" alt="${shortname}" title="${shortname}" src="${filename}" data-original="${customEmojis[shortname].url}" data-static="${customEmojis[shortname].static_url}" />`;
3333
return true;
3434
}
3535
return false;

app/javascript/packs/public.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ function main() {
4444
}
4545
};
4646

47+
const getEmojiAnimationHandler = (swapTo) => {
48+
return ({ target }) => {
49+
target.src = target.getAttribute(swapTo);
50+
};
51+
};
52+
4753
ready(() => {
4854
const locale = document.documentElement.lang;
4955

@@ -108,6 +114,9 @@ function main() {
108114
if (parallaxComponents.length > 0 ) {
109115
new Rellax('.parallax', { speed: -1 });
110116
}
117+
118+
delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
119+
delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
111120
});
112121

113122
delegate(document, '.webapp-btn', 'click', ({ target, button }) => {

app/lib/formatter.rb

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,7 @@ def count_tag_nesting(tag)
139139
def encode_custom_emojis(html, emojis, animate = false)
140140
return html if emojis.empty?
141141

142-
emoji_map = if animate
143-
emojis.each_with_object({}) { |e, h| h[e.shortcode] = full_asset_url(e.image.url) }
144-
else
145-
emojis.each_with_object({}) { |e, h| h[e.shortcode] = full_asset_url(e.image.url(:static)) }
146-
end
142+
emoji_map = emojis.each_with_object({}) { |e, h| h[e.shortcode] = [full_asset_url(e.image.url), full_asset_url(e.image.url(:static))] }
147143

148144
i = -1
149145
tag_open_index = nil
@@ -159,7 +155,14 @@ def encode_custom_emojis(html, emojis, animate = false)
159155
emoji = emoji_map[shortcode]
160156

161157
if emoji
162-
replacement = "<img draggable=\"false\" class=\"emojione\" alt=\":#{encode(shortcode)}:\" title=\":#{encode(shortcode)}:\" src=\"#{encode(emoji)}\" />"
158+
original_url, static_url = emoji
159+
replacement = begin
160+
if animate
161+
"<img draggable=\"false\" class=\"emojione\" alt=\":#{encode(shortcode)}:\" title=\":#{encode(shortcode)}:\" src=\"#{encode(original_url)}\" />"
162+
else
163+
"<img draggable=\"false\" class=\"emojione custom-emoji\" alt=\":#{encode(shortcode)}:\" title=\":#{encode(shortcode)}:\" src=\"#{encode(static_url)}\" data-original=\"#{original_url}\" data-static=\"#{static_url}\" />"
164+
end
165+
end
163166
before_html = shortname_start_index.positive? ? html[0..shortname_start_index - 1] : ''
164167
html = before_html + replacement + html[i + 1..-1]
165168
i += replacement.size - (shortcode.size + 2) - 1

spec/lib/formatter_spec.rb

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@
261261
let(:text) { ':coolcat: Beep boop' }
262262

263263
it 'converts the shortcode to an image tag' do
264-
is_expected.to match(/<img draggable="false" class="emojione" alt=":coolcat:"/)
264+
is_expected.to match(/<img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
265265
end
266266
end
267267
end
@@ -330,15 +330,15 @@
330330
let(:text) { ':coolcat: Beep boop' }
331331

332332
it 'converts the shortcode to an image tag' do
333-
is_expected.to match(/<p><img draggable="false" class="emojione" alt=":coolcat:"/)
333+
is_expected.to match(/<p><img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
334334
end
335335
end
336336

337337
context 'given a post with an emoji shortcode in the middle' do
338338
let(:text) { 'Beep :coolcat: boop' }
339339

340340
it 'converts the shortcode to an image tag' do
341-
is_expected.to match(/Beep <img draggable="false" class="emojione" alt=":coolcat:"/)
341+
is_expected.to match(/Beep <img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
342342
end
343343
end
344344

@@ -354,7 +354,7 @@
354354
let(:text) { 'Beep boop :coolcat:' }
355355

356356
it 'converts the shortcode to an image tag' do
357-
is_expected.to match(/boop <img draggable="false" class="emojione" alt=":coolcat:"/)
357+
is_expected.to match(/boop <img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
358358
end
359359
end
360360
end
@@ -377,15 +377,15 @@
377377
let(:text) { '<p>:coolcat: Beep boop<br />' }
378378

379379
it 'converts the shortcode to an image tag' do
380-
is_expected.to match(/<p><img draggable="false" class="emojione" alt=":coolcat:"/)
380+
is_expected.to match(/<p><img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
381381
end
382382
end
383383

384384
context 'given a post with an emoji shortcode in the middle' do
385385
let(:text) { '<p>Beep :coolcat: boop</p>' }
386386

387387
it 'converts the shortcode to an image tag' do
388-
is_expected.to match(/Beep <img draggable="false" class="emojione" alt=":coolcat:"/)
388+
is_expected.to match(/Beep <img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
389389
end
390390
end
391391

@@ -401,7 +401,7 @@
401401
let(:text) { '<p>Beep boop<br />:coolcat:</p>' }
402402

403403
it 'converts the shortcode to an image tag' do
404-
is_expected.to match(/<br><img draggable="false" class="emojione" alt=":coolcat:"/)
404+
is_expected.to match(/<br><img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
405405
end
406406
end
407407
end
@@ -500,15 +500,15 @@
500500
let(:text) { ':coolcat: Beep boop' }
501501

502502
it 'converts the shortcode to an image tag' do
503-
is_expected.to match(/<p><img draggable="false" class="emojione" alt=":coolcat:"/)
503+
is_expected.to match(/<p><img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
504504
end
505505
end
506506

507507
context 'given a post with an emoji shortcode in the middle' do
508508
let(:text) { 'Beep :coolcat: boop' }
509509

510510
it 'converts the shortcode to an image tag' do
511-
is_expected.to match(/Beep <img draggable="false" class="emojione" alt=":coolcat:"/)
511+
is_expected.to match(/Beep <img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
512512
end
513513
end
514514

@@ -524,7 +524,7 @@
524524
let(:text) { 'Beep boop :coolcat:' }
525525

526526
it 'converts the shortcode to an image tag' do
527-
is_expected.to match(/boop <img draggable="false" class="emojione" alt=":coolcat:"/)
527+
is_expected.to match(/boop <img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
528528
end
529529
end
530530
end
@@ -551,15 +551,15 @@
551551
let(:text) { '<p>:coolcat: Beep boop<br />' }
552552

553553
it 'converts shortcode to image tag' do
554-
is_expected.to match(/<p><img draggable="false" class="emojione" alt=":coolcat:"/)
554+
is_expected.to match(/<p><img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
555555
end
556556
end
557557

558558
context 'given a post with an emoji shortcode in the middle' do
559559
let(:text) { '<p>Beep :coolcat: boop</p>' }
560560

561561
it 'converts shortcode to image tag' do
562-
is_expected.to match(/Beep <img draggable="false" class="emojione" alt=":coolcat:"/)
562+
is_expected.to match(/Beep <img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
563563
end
564564
end
565565

@@ -575,7 +575,7 @@
575575
let(:text) { '<p>Beep boop<br />:coolcat:</p>' }
576576

577577
it 'converts shortcode to image tag' do
578-
is_expected.to match(/<br><img draggable="false" class="emojione" alt=":coolcat:"/)
578+
is_expected.to match(/<br><img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
579579
end
580580
end
581581
end

0 commit comments

Comments
 (0)