Emoji: Remove final flag (#36409)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,6 +23,7 @@
|
|||||||
/public/packs
|
/public/packs
|
||||||
/public/packs-dev
|
/public/packs-dev
|
||||||
/public/packs-test
|
/public/packs-test
|
||||||
|
stats.html
|
||||||
.env
|
.env
|
||||||
.env.production
|
.env.production
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function loaded() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
document.querySelectorAll('.emojify').forEach((content) => {
|
document.querySelectorAll('.emojify').forEach((content) => {
|
||||||
content.innerHTML = emojify(content.innerHTML, {}, true); // Force emojify as public doesn't load the new emoji system.
|
content.innerHTML = emojify(content.innerHTML);
|
||||||
});
|
});
|
||||||
|
|
||||||
document
|
document
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import escapeTextContentForBrowser from 'escape-html';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
|
||||||
import { makeEmojiMap } from 'mastodon/models/custom_emoji';
|
|
||||||
|
|
||||||
import emojify from '../../features/emoji/emoji';
|
|
||||||
import { expandSpoilers } from '../../initial_state';
|
import { expandSpoilers } from '../../initial_state';
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
@@ -88,11 +85,10 @@ export function normalizeStatus(status, normalOldStatus) {
|
|||||||
|
|
||||||
const spoilerText = normalStatus.spoiler_text || '';
|
const spoilerText = normalStatus.spoiler_text || '';
|
||||||
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||||
const emojiMap = makeEmojiMap(normalStatus.emojis);
|
|
||||||
|
|
||||||
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
||||||
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
|
normalStatus.contentHtml = normalStatus.content;
|
||||||
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
normalStatus.spoilerHtml = escapeTextContentForBrowser(spoilerText);
|
||||||
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
|
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
|
||||||
|
|
||||||
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
|
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
|
||||||
@@ -128,14 +124,12 @@ export function normalizeStatus(status, normalOldStatus) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeStatusTranslation(translation, status) {
|
export function normalizeStatusTranslation(translation, status) {
|
||||||
const emojiMap = makeEmojiMap(status.get('emojis').toJS());
|
|
||||||
|
|
||||||
const normalTranslation = {
|
const normalTranslation = {
|
||||||
detected_source_language: translation.detected_source_language,
|
detected_source_language: translation.detected_source_language,
|
||||||
language: translation.language,
|
language: translation.language,
|
||||||
provider: translation.provider,
|
provider: translation.provider,
|
||||||
contentHtml: emojify(translation.content, emojiMap),
|
contentHtml: translation.content,
|
||||||
spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap),
|
spoilerHtml: escapeTextContentForBrowser(translation.spoiler_text),
|
||||||
spoiler_text: translation.spoiler_text,
|
spoiler_text: translation.spoiler_text,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,9 +143,8 @@ export function normalizeStatusTranslation(translation, status) {
|
|||||||
|
|
||||||
export function normalizeAnnouncement(announcement) {
|
export function normalizeAnnouncement(announcement) {
|
||||||
const normalAnnouncement = { ...announcement };
|
const normalAnnouncement = { ...announcement };
|
||||||
const emojiMap = makeEmojiMap(normalAnnouncement.emojis);
|
|
||||||
|
|
||||||
normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
|
normalAnnouncement.contentHtml = normalAnnouncement.content;
|
||||||
|
|
||||||
return normalAnnouncement;
|
return normalAnnouncement;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { useLinks } from 'mastodon/hooks/useLinks';
|
|
||||||
|
|
||||||
import { useAppSelector } from '../store';
|
import { useAppSelector } from '../store';
|
||||||
import { isModernEmojiEnabled } from '../utils/environment';
|
|
||||||
|
|
||||||
import { EmojiHTML } from './emoji/html';
|
import { EmojiHTML } from './emoji/html';
|
||||||
import { useElementHandledLink } from './status/handled_link';
|
import { useElementHandledLink } from './status/handled_link';
|
||||||
@@ -21,22 +16,6 @@ export const AccountBio: React.FC<AccountBioProps> = ({
|
|||||||
accountId,
|
accountId,
|
||||||
showDropdown = false,
|
showDropdown = false,
|
||||||
}) => {
|
}) => {
|
||||||
const handleClick = useLinks(showDropdown);
|
|
||||||
const handleNodeChange = useCallback(
|
|
||||||
(node: HTMLDivElement | null) => {
|
|
||||||
if (
|
|
||||||
!showDropdown ||
|
|
||||||
!node ||
|
|
||||||
node.childNodes.length === 0 ||
|
|
||||||
isModernEmojiEnabled()
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
addDropdownToHashtags(node, accountId);
|
|
||||||
},
|
|
||||||
[showDropdown, accountId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const htmlHandlers = useElementHandledLink({
|
const htmlHandlers = useElementHandledLink({
|
||||||
hashtagAccountId: showDropdown ? accountId : undefined,
|
hashtagAccountId: showDropdown ? accountId : undefined,
|
||||||
});
|
});
|
||||||
@@ -62,30 +41,7 @@ export const AccountBio: React.FC<AccountBioProps> = ({
|
|||||||
htmlString={note}
|
htmlString={note}
|
||||||
extraEmojis={extraEmojis}
|
extraEmojis={extraEmojis}
|
||||||
className={classNames(className, 'translate')}
|
className={classNames(className, 'translate')}
|
||||||
onClickCapture={handleClick}
|
|
||||||
ref={handleNodeChange}
|
|
||||||
{...htmlHandlers}
|
{...htmlHandlers}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function addDropdownToHashtags(node: HTMLElement | null, accountId: string) {
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const childNode of node.childNodes) {
|
|
||||||
if (!(childNode instanceof HTMLElement)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
childNode instanceof HTMLAnchorElement &&
|
|
||||||
(childNode.classList.contains('hashtag') ||
|
|
||||||
childNode.innerText.startsWith('#')) &&
|
|
||||||
!childNode.dataset.menuHashtag
|
|
||||||
) {
|
|
||||||
childNode.dataset.menuHashtag = accountId;
|
|
||||||
} else if (childNode.childNodes.length > 0) {
|
|
||||||
addDropdownToHashtags(childNode, accountId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import { cleanExtraEmojis } from '@/mastodon/features/emoji/normalize';
|
import { cleanExtraEmojis } from '@/mastodon/features/emoji/normalize';
|
||||||
import { autoPlayGif } from '@/mastodon/initial_state';
|
import { autoPlayGif } from '@/mastodon/initial_state';
|
||||||
import { polymorphicForwardRef } from '@/types/polymorphic';
|
import { polymorphicForwardRef } from '@/types/polymorphic';
|
||||||
@@ -65,11 +63,7 @@ export const AnimateEmojiProvider = polymorphicForwardRef<
|
|||||||
const parentContext = useContext(AnimateEmojiContext);
|
const parentContext = useContext(AnimateEmojiContext);
|
||||||
if (parentContext !== null) {
|
if (parentContext !== null) {
|
||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper {...props} className={className} ref={ref}>
|
||||||
{...props}
|
|
||||||
className={classNames(className, 'animate-parent')}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
@@ -78,7 +72,7 @@ export const AnimateEmojiProvider = polymorphicForwardRef<
|
|||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper
|
||||||
{...props}
|
{...props}
|
||||||
className={classNames(className, 'animate-parent')}
|
className={className}
|
||||||
onMouseEnter={handleEnter}
|
onMouseEnter={handleEnter}
|
||||||
onMouseLeave={handleLeave}
|
onMouseLeave={handleLeave}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import type { CustomEmojiMapArg } from '@/mastodon/features/emoji/types';
|
import type { CustomEmojiMapArg } from '@/mastodon/features/emoji/types';
|
||||||
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
|
||||||
import type {
|
import type {
|
||||||
OnAttributeHandler,
|
OnAttributeHandler,
|
||||||
OnElementHandler,
|
OnElementHandler,
|
||||||
@@ -22,7 +19,7 @@ export interface EmojiHTMLProps {
|
|||||||
onAttribute?: OnAttributeHandler;
|
onAttribute?: OnAttributeHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
export const EmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
extraEmojis,
|
extraEmojis,
|
||||||
@@ -59,32 +56,4 @@ export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
ModernEmojiHTML.displayName = 'ModernEmojiHTML';
|
EmojiHTML.displayName = 'EmojiHTML';
|
||||||
|
|
||||||
export const LegacyEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
|
||||||
(props, ref) => {
|
|
||||||
const {
|
|
||||||
as: asElement,
|
|
||||||
htmlString,
|
|
||||||
extraEmojis,
|
|
||||||
className,
|
|
||||||
onElement,
|
|
||||||
onAttribute,
|
|
||||||
...rest
|
|
||||||
} = props;
|
|
||||||
const Wrapper = asElement ?? 'div';
|
|
||||||
return (
|
|
||||||
<Wrapper
|
|
||||||
{...rest}
|
|
||||||
ref={ref}
|
|
||||||
dangerouslySetInnerHTML={{ __html: htmlString }}
|
|
||||||
className={classNames(className, 'animate-parent')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
LegacyEmojiHTML.displayName = 'LegacyEmojiHTML';
|
|
||||||
|
|
||||||
export const EmojiHTML = isModernEmojiEnabled()
|
|
||||||
? ModernEmojiHTML
|
|
||||||
: LegacyEmojiHTML;
|
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ import { domain } from 'mastodon/initial_state';
|
|||||||
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
import { useLinks } from '../hooks/useLinks';
|
|
||||||
|
|
||||||
export const HoverCardAccount = forwardRef<
|
export const HoverCardAccount = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
{ accountId?: string }
|
{ accountId?: string }
|
||||||
@@ -66,8 +64,6 @@ export const HoverCardAccount = forwardRef<
|
|||||||
!isMutual &&
|
!isMutual &&
|
||||||
!isFollower;
|
!isFollower;
|
||||||
|
|
||||||
const handleClick = useLinks();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -110,7 +106,7 @@ export const HoverCardAccount = forwardRef<
|
|||||||
className='hover-card__bio'
|
className='hover-card__bio'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='account-fields' onClickCapture={handleClick}>
|
<div className='account-fields'>
|
||||||
<AccountFields
|
<AccountFields
|
||||||
fields={account.fields.take(2)}
|
fields={account.fields.take(2)}
|
||||||
emojis={account.emojis}
|
emojis={account.emojis}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { OnElementHandler } from '@/mastodon/utils/html';
|
|||||||
import { polymorphicForwardRef } from '@/types/polymorphic';
|
import { polymorphicForwardRef } from '@/types/polymorphic';
|
||||||
|
|
||||||
import type { EmojiHTMLProps } from '../emoji/html';
|
import type { EmojiHTMLProps } from '../emoji/html';
|
||||||
import { ModernEmojiHTML } from '../emoji/html';
|
import { EmojiHTML } from '../emoji/html';
|
||||||
import { useElementHandledLink } from '../status/handled_link';
|
import { useElementHandledLink } from '../status/handled_link';
|
||||||
|
|
||||||
export const HTMLBlock = polymorphicForwardRef<
|
export const HTMLBlock = polymorphicForwardRef<
|
||||||
@@ -25,6 +25,6 @@ export const HTMLBlock = polymorphicForwardRef<
|
|||||||
(...args) => onParentElement?.(...args) ?? onLinkElement(...args),
|
(...args) => onParentElement?.(...args) ?? onLinkElement(...args),
|
||||||
[onLinkElement, onParentElement],
|
[onLinkElement, onParentElement],
|
||||||
);
|
);
|
||||||
return <ModernEmojiHTML {...props} onElement={onElement} />;
|
return <EmojiHTML {...props} onElement={onElement} />;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
|||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
import { fetchPoll, vote } from 'mastodon/actions/polls';
|
import { fetchPoll, vote } from 'mastodon/actions/polls';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import emojify from 'mastodon/features/emoji/emoji';
|
|
||||||
import { useIdentity } from 'mastodon/identity_context';
|
import { useIdentity } from 'mastodon/identity_context';
|
||||||
import { makeEmojiMap } from 'mastodon/models/custom_emoji';
|
|
||||||
import type * as Model from 'mastodon/models/poll';
|
import type * as Model from 'mastodon/models/poll';
|
||||||
import type { Status } from 'mastodon/models/status';
|
import type { Status } from 'mastodon/models/status';
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
@@ -235,12 +233,11 @@ const PollOption: React.FC<PollOptionProps> = (props) => {
|
|||||||
let titleHtml = option.translation?.titleHtml ?? option.titleHtml;
|
let titleHtml = option.translation?.titleHtml ?? option.titleHtml;
|
||||||
|
|
||||||
if (!titleHtml) {
|
if (!titleHtml) {
|
||||||
const emojiMap = makeEmojiMap(poll.emojis);
|
titleHtml = escapeTextContentForBrowser(title);
|
||||||
titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return titleHtml;
|
return titleHtml;
|
||||||
}, [option, poll, title]);
|
}, [option, title]);
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
const handleOptionChange = useCallback(() => {
|
const handleOptionChange = useCallback(() => {
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import { Poll } from 'mastodon/components/poll';
|
|||||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
||||||
import { languages as preloadedLanguages } from 'mastodon/initial_state';
|
import { languages as preloadedLanguages } from 'mastodon/initial_state';
|
||||||
|
|
||||||
import { isModernEmojiEnabled } from '../utils/environment';
|
|
||||||
|
|
||||||
import { EmojiHTML } from './emoji/html';
|
import { EmojiHTML } from './emoji/html';
|
||||||
import { HandledLink } from './status/handled_link';
|
import { HandledLink } from './status/handled_link';
|
||||||
|
|
||||||
@@ -119,41 +117,6 @@ class StatusContent extends PureComponent {
|
|||||||
|
|
||||||
onCollapsedToggle(collapsed);
|
onCollapsedToggle(collapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit if modern emoji is enabled, as it handles links using the HandledLink component.
|
|
||||||
if (isModernEmojiEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const links = node.querySelectorAll('a');
|
|
||||||
|
|
||||||
let link, mention;
|
|
||||||
|
|
||||||
for (var i = 0; i < links.length; ++i) {
|
|
||||||
link = links[i];
|
|
||||||
|
|
||||||
if (link.classList.contains('status-link')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
link.classList.add('status-link');
|
|
||||||
|
|
||||||
mention = this.props.status.get('mentions').find(item => compareUrls(link, item.get('url')));
|
|
||||||
|
|
||||||
if (mention) {
|
|
||||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
|
||||||
link.setAttribute('title', `@${mention.get('acct')}`);
|
|
||||||
link.setAttribute('href', `/@${mention.get('acct')}`);
|
|
||||||
link.setAttribute('data-hover-card-account', mention.get('id'));
|
|
||||||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
|
||||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
|
||||||
link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`);
|
|
||||||
link.setAttribute('data-menu-hashtag', this.props.status.getIn(['account', 'id']));
|
|
||||||
} else {
|
|
||||||
link.setAttribute('title', link.href);
|
|
||||||
link.classList.add('unhandled-link');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
@@ -164,22 +127,6 @@ class StatusContent extends PureComponent {
|
|||||||
this._updateStatusLinks();
|
this._updateStatusLinks();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMentionClick = (mention, e) => {
|
|
||||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/@${mention.get('acct')}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onHashtagClick = (hashtag, e) => {
|
|
||||||
hashtag = hashtag.replace(/^#/, '');
|
|
||||||
|
|
||||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/tags/${hashtag}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseDown = (e) => {
|
handleMouseDown = (e) => {
|
||||||
this.startXY = [e.clientX, e.clientY];
|
this.startXY = [e.clientX, e.clientY];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,30 +1,10 @@
|
|||||||
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
||||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||||
|
|
||||||
import { isModernEmojiEnabled } from '../utils/environment';
|
|
||||||
import type { OnAttributeHandler } from '../utils/html';
|
import type { OnAttributeHandler } from '../utils/html';
|
||||||
|
|
||||||
import { Icon } from './icon';
|
import { Icon } from './icon';
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
|
||||||
|
|
||||||
const stripRelMe = (html: string) => {
|
|
||||||
if (isModernEmojiEnabled()) {
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
const document = domParser.parseFromString(html, 'text/html').documentElement;
|
|
||||||
|
|
||||||
document.querySelectorAll<HTMLAnchorElement>('a[rel]').forEach((link) => {
|
|
||||||
link.rel = link.rel
|
|
||||||
.split(' ')
|
|
||||||
.filter((x: string) => x !== 'me')
|
|
||||||
.join(' ');
|
|
||||||
});
|
|
||||||
|
|
||||||
const body = document.querySelector('body');
|
|
||||||
return body?.innerHTML ?? '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAttribute: OnAttributeHandler = (name, value, tagName) => {
|
const onAttribute: OnAttributeHandler = (name, value, tagName) => {
|
||||||
if (name === 'rel' && tagName === 'a') {
|
if (name === 'rel' && tagName === 'a') {
|
||||||
if (value === 'me') {
|
if (value === 'me') {
|
||||||
@@ -47,10 +27,6 @@ interface Props {
|
|||||||
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
|
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
|
||||||
<span className='verified-badge'>
|
<span className='verified-badge'>
|
||||||
<Icon id='check' icon={CheckIcon} className='verified-badge__mark' />
|
<Icon id='check' icon={CheckIcon} className='verified-badge__mark' />
|
||||||
<EmojiHTML
|
<EmojiHTML as='span' htmlString={link} onAttribute={onAttribute} />
|
||||||
as='span'
|
|
||||||
htmlString={stripRelMe(link)}
|
|
||||||
onAttribute={onAttribute}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ import { ShortNumber } from 'mastodon/components/short_number';
|
|||||||
import { AccountNote } from 'mastodon/features/account/components/account_note';
|
import { AccountNote } from 'mastodon/features/account/components/account_note';
|
||||||
import { DomainPill } from 'mastodon/features/account/components/domain_pill';
|
import { DomainPill } from 'mastodon/features/account/components/domain_pill';
|
||||||
import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
|
import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
|
||||||
import { useLinks } from 'mastodon/hooks/useLinks';
|
|
||||||
import { useIdentity } from 'mastodon/identity_context';
|
import { useIdentity } from 'mastodon/identity_context';
|
||||||
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
|
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
|
||||||
import type { Account } from 'mastodon/models/account';
|
import type { Account } from 'mastodon/models/account';
|
||||||
@@ -198,7 +197,6 @@ export const AccountHeader: React.FC<{
|
|||||||
state.relationships.get(accountId),
|
state.relationships.get(accountId),
|
||||||
);
|
);
|
||||||
const hidden = useAppSelector((state) => getAccountHidden(state, accountId));
|
const hidden = useAppSelector((state) => getAccountHidden(state, accountId));
|
||||||
const handleLinkClick = useLinks();
|
|
||||||
|
|
||||||
const handleBlock = useCallback(() => {
|
const handleBlock = useCallback(() => {
|
||||||
if (!account) {
|
if (!account) {
|
||||||
@@ -852,10 +850,7 @@ export const AccountHeader: React.FC<{
|
|||||||
|
|
||||||
{!(suspended || hidden) && (
|
{!(suspended || hidden) && (
|
||||||
<div className='account__header__extra'>
|
<div className='account__header__extra'>
|
||||||
<div
|
<div className='account__header__bio'>
|
||||||
className='account__header__bio'
|
|
||||||
onClickCapture={handleLinkClick}
|
|
||||||
>
|
|
||||||
{account.id !== me && signedIn && (
|
{account.id !== me && signedIn && (
|
||||||
<AccountNote accountId={accountId} />
|
<AccountNote accountId={accountId} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import Trie from 'substring-trie';
|
import Trie from 'substring-trie';
|
||||||
|
|
||||||
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
|
||||||
import { assetHost } from 'mastodon/utils/config';
|
import { assetHost } from 'mastodon/utils/config';
|
||||||
|
|
||||||
import { autoPlayGif } from '../../initial_state';
|
import { autoPlayGif } from '../../initial_state';
|
||||||
@@ -153,13 +152,9 @@ const emojifyNode = (node, customEmojis) => {
|
|||||||
* Legacy emoji processing function.
|
* Legacy emoji processing function.
|
||||||
* @param {string} str
|
* @param {string} str
|
||||||
* @param {object} customEmojis
|
* @param {object} customEmojis
|
||||||
* @param {boolean} force If true, always emojify even if modern emoji is enabled
|
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
const emojify = (str, customEmojis = {}, force = false) => {
|
const emojify = (str, customEmojis = {}) => {
|
||||||
if (isModernEmojiEnabled() && !force) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.innerHTML = str;
|
wrapper.innerHTML = str;
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ import { uncompress as emojiMartUncompress } from 'emoji-mart/dist/utils/data';
|
|||||||
|
|
||||||
import data from './emoji_data.json';
|
import data from './emoji_data.json';
|
||||||
import emojiMap from './emoji_map.json';
|
import emojiMap from './emoji_map.json';
|
||||||
import { unicodeToFilename } from './unicode_to_filename';
|
import { unicodeToFilename, unicodeToUnifiedName } from './unicode_utils';
|
||||||
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
|
||||||
|
|
||||||
emojiMartUncompress(data);
|
emojiMartUncompress(data);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type {
|
|||||||
ShortCodesToEmojiData,
|
ShortCodesToEmojiData,
|
||||||
} from 'virtual:mastodon-emoji-compressed';
|
} from 'virtual:mastodon-emoji-compressed';
|
||||||
|
|
||||||
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
import { unicodeToUnifiedName } from './unicode_utils';
|
||||||
|
|
||||||
type Emojis = Record<
|
type Emojis = Record<
|
||||||
NonNullable<keyof ShortCodesToEmojiData>,
|
NonNullable<keyof ShortCodesToEmojiData>,
|
||||||
@@ -23,7 +23,7 @@ type Emojis = Record<
|
|||||||
|
|
||||||
const [
|
const [
|
||||||
shortCodesToEmojiData,
|
shortCodesToEmojiData,
|
||||||
skins,
|
_skins,
|
||||||
categories,
|
categories,
|
||||||
short_names,
|
short_names,
|
||||||
_emojisWithoutShortCodes,
|
_emojisWithoutShortCodes,
|
||||||
@@ -47,4 +47,4 @@ Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export { emojis, skins, categories, short_names };
|
export { emojis, categories, short_names };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// This code is largely borrowed from:
|
// This code is largely borrowed from:
|
||||||
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js
|
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js
|
||||||
|
|
||||||
import * as data from './emoji_mart_data_light';
|
import { emojis, categories } from './emoji_mart_data_light';
|
||||||
import { getData, getSanitizedData, uniq, intersect } from './emoji_utils';
|
import { getData, getSanitizedData, uniq, intersect } from './emoji_utils';
|
||||||
|
|
||||||
let originalPool = {};
|
let originalPool = {};
|
||||||
@@ -10,8 +10,8 @@ let emojisList = {};
|
|||||||
let emoticonsList = {};
|
let emoticonsList = {};
|
||||||
let customEmojisList = [];
|
let customEmojisList = [];
|
||||||
|
|
||||||
for (let emoji in data.emojis) {
|
for (let emoji in emojis) {
|
||||||
let emojiData = data.emojis[emoji];
|
let emojiData = emojis[emoji];
|
||||||
let { short_names, emoticons } = emojiData;
|
let { short_names, emoticons } = emojiData;
|
||||||
let id = short_names[0];
|
let id = short_names[0];
|
||||||
|
|
||||||
@@ -84,14 +84,14 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo
|
|||||||
if (include.length || exclude.length) {
|
if (include.length || exclude.length) {
|
||||||
pool = {};
|
pool = {};
|
||||||
|
|
||||||
data.categories.forEach(category => {
|
categories.forEach(category => {
|
||||||
let isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true;
|
let isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true;
|
||||||
let isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false;
|
let isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false;
|
||||||
if (!isIncluded || isExcluded) {
|
if (!isIncluded || isExcluded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
category.emojis.forEach(emojiId => pool[emojiId] = data.emojis[emojiId]);
|
category.emojis.forEach(emojiId => pool[emojiId] = emojis[emojiId]);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (custom.length) {
|
if (custom.length) {
|
||||||
@@ -171,7 +171,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo
|
|||||||
|
|
||||||
if (results) {
|
if (results) {
|
||||||
if (emojisToShowFilter) {
|
if (emojisToShowFilter) {
|
||||||
results = results.filter((result) => emojisToShowFilter(data.emojis[result.id]));
|
results = results.filter((result) => emojisToShowFilter(emojis[result.id]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (results && results.length > maxResults) {
|
if (results && results.length > maxResults) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { EmojiProps, PickerProps } from 'emoji-mart';
|
|||||||
import EmojiRaw from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
|
import EmojiRaw from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
|
||||||
import PickerRaw from 'emoji-mart/dist-es/components/picker/nimble-picker';
|
import PickerRaw from 'emoji-mart/dist-es/components/picker/nimble-picker';
|
||||||
|
|
||||||
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
|
||||||
import { assetHost } from 'mastodon/utils/config';
|
import { assetHost } from 'mastodon/utils/config';
|
||||||
|
|
||||||
import { EMOJI_MODE_NATIVE } from './constants';
|
import { EMOJI_MODE_NATIVE } from './constants';
|
||||||
@@ -27,7 +26,7 @@ const Emoji = ({
|
|||||||
sheetSize={sheetSize}
|
sheetSize={sheetSize}
|
||||||
sheetColumns={sheetColumns}
|
sheetColumns={sheetColumns}
|
||||||
sheetRows={sheetRows}
|
sheetRows={sheetRows}
|
||||||
native={mode === EMOJI_MODE_NATIVE && isModernEmojiEnabled()}
|
native={mode === EMOJI_MODE_NATIVE}
|
||||||
backgroundImageFn={backgroundImageFn}
|
backgroundImageFn={backgroundImageFn}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -51,7 +50,7 @@ const Picker = ({
|
|||||||
sheetColumns={sheetColumns}
|
sheetColumns={sheetColumns}
|
||||||
sheetRows={sheetRows}
|
sheetRows={sheetRows}
|
||||||
backgroundImageFn={backgroundImageFn}
|
backgroundImageFn={backgroundImageFn}
|
||||||
native={mode === EMOJI_MODE_NATIVE && isModernEmojiEnabled()}
|
native={mode === EMOJI_MODE_NATIVE}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
ShortCodesToEmojiDataKey,
|
ShortCodesToEmojiDataKey,
|
||||||
} from 'virtual:mastodon-emoji-compressed';
|
} from 'virtual:mastodon-emoji-compressed';
|
||||||
|
|
||||||
import { unicodeToFilename } from './unicode_to_filename';
|
import { unicodeToFilename } from './unicode_utils';
|
||||||
|
|
||||||
type UnicodeMapping = Record<
|
type UnicodeMapping = Record<
|
||||||
FilenameData[number][0],
|
FilenameData[number][0],
|
||||||
|
|||||||
@@ -209,50 +209,9 @@ function intersect(a, b) {
|
|||||||
return uniqA.filter(item => uniqB.indexOf(item) >= 0);
|
return uniqA.filter(item => uniqB.indexOf(item) >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deepMerge(a, b) {
|
|
||||||
let o = {};
|
|
||||||
|
|
||||||
for (let key in a) {
|
|
||||||
let originalValue = a[key],
|
|
||||||
value = originalValue;
|
|
||||||
|
|
||||||
if (Object.hasOwn(b, key)) {
|
|
||||||
value = b[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
value = deepMerge(originalValue, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
o[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return o;
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/sonicdoe/measure-scrollbar
|
|
||||||
function measureScrollbar() {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
|
|
||||||
div.style.width = '100px';
|
|
||||||
div.style.height = '100px';
|
|
||||||
div.style.overflow = 'scroll';
|
|
||||||
div.style.position = 'absolute';
|
|
||||||
div.style.top = '-9999px';
|
|
||||||
|
|
||||||
document.body.appendChild(div);
|
|
||||||
const scrollbarWidth = div.offsetWidth - div.clientWidth;
|
|
||||||
document.body.removeChild(div);
|
|
||||||
|
|
||||||
return scrollbarWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getData,
|
getData,
|
||||||
getSanitizedData,
|
getSanitizedData,
|
||||||
uniq,
|
uniq,
|
||||||
intersect,
|
intersect,
|
||||||
deepMerge,
|
|
||||||
unifiedToNative,
|
|
||||||
measureScrollbar,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import { autoPlayGif } from '@/mastodon/initial_state';
|
|
||||||
|
|
||||||
const PARENT_MAX_DEPTH = 10;
|
|
||||||
|
|
||||||
export function handleAnimateGif(event: MouseEvent) {
|
|
||||||
// We already check this in ui/index.jsx, but just to be sure.
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { target, type } = event;
|
|
||||||
const animate = type === 'mouseover'; // Mouse over = animate, mouse out = don't animate.
|
|
||||||
|
|
||||||
if (target instanceof HTMLImageElement) {
|
|
||||||
setAnimateGif(target, animate);
|
|
||||||
} else if (!(target instanceof HTMLElement) || target === document.body) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parent: HTMLElement | null = null;
|
|
||||||
let iter = 0;
|
|
||||||
|
|
||||||
if (target.classList.contains('animate-parent')) {
|
|
||||||
parent = target;
|
|
||||||
} else {
|
|
||||||
// Iterate up to PARENT_MAX_DEPTH levels up the DOM tree to find a parent with the class 'animate-parent'.
|
|
||||||
let current: HTMLElement | null = target;
|
|
||||||
while (current) {
|
|
||||||
if (iter >= PARENT_MAX_DEPTH) {
|
|
||||||
return; // We can just exit right now.
|
|
||||||
}
|
|
||||||
current = current.parentElement;
|
|
||||||
if (current?.classList.contains('animate-parent')) {
|
|
||||||
parent = current;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
iter++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Affect all animated children within the parent.
|
|
||||||
if (parent) {
|
|
||||||
const animatedChildren =
|
|
||||||
parent.querySelectorAll<HTMLImageElement>('img.custom-emoji');
|
|
||||||
for (const child of animatedChildren) {
|
|
||||||
setAnimateGif(child, animate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAnimateGif(image: HTMLImageElement, animate: boolean) {
|
|
||||||
const { classList, dataset } = image;
|
|
||||||
if (
|
|
||||||
!classList.contains('custom-emoji') ||
|
|
||||||
!dataset.static ||
|
|
||||||
!dataset.original
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
image.src = animate ? dataset.original : dataset.static;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// taken from:
|
|
||||||
// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
|
|
||||||
export const unicodeToFilename = (str) => {
|
|
||||||
let result = '';
|
|
||||||
let charCode = 0;
|
|
||||||
let p = 0;
|
|
||||||
let i = 0;
|
|
||||||
while (i < str.length) {
|
|
||||||
charCode = str.charCodeAt(i++);
|
|
||||||
if (p) {
|
|
||||||
if (result.length > 0) {
|
|
||||||
result += '-';
|
|
||||||
}
|
|
||||||
result += (0x10000 + ((p - 0xD800) << 10) + (charCode - 0xDC00)).toString(16);
|
|
||||||
p = 0;
|
|
||||||
} else if (0xD800 <= charCode && charCode <= 0xDBFF) {
|
|
||||||
p = charCode;
|
|
||||||
} else {
|
|
||||||
if (result.length > 0) {
|
|
||||||
result += '-';
|
|
||||||
}
|
|
||||||
result += charCode.toString(16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
function padLeft(str, num) {
|
|
||||||
while (str.length < num) {
|
|
||||||
str = '0' + str;
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const unicodeToUnifiedName = (str) => {
|
|
||||||
let output = '';
|
|
||||||
|
|
||||||
for (let i = 0; i < str.length; i += 2) {
|
|
||||||
if (i > 0) {
|
|
||||||
output += '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
output += padLeft(str.codePointAt(i).toString(16).toUpperCase(), 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
};
|
|
||||||
43
app/javascript/mastodon/features/emoji/unicode_utils.ts
Normal file
43
app/javascript/mastodon/features/emoji/unicode_utils.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// taken from:
|
||||||
|
// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
|
||||||
|
export function unicodeToFilename(str: string) {
|
||||||
|
let result = '';
|
||||||
|
let charCode = 0;
|
||||||
|
let p = 0;
|
||||||
|
let i = 0;
|
||||||
|
while (i < str.length) {
|
||||||
|
charCode = str.charCodeAt(i++);
|
||||||
|
if (p) {
|
||||||
|
if (result.length > 0) {
|
||||||
|
result += '-';
|
||||||
|
}
|
||||||
|
result += (0x10000 + ((p - 0xd800) << 10) + (charCode - 0xdc00)).toString(
|
||||||
|
16,
|
||||||
|
);
|
||||||
|
p = 0;
|
||||||
|
} else if (0xd800 <= charCode && charCode <= 0xdbff) {
|
||||||
|
p = charCode;
|
||||||
|
} else {
|
||||||
|
if (result.length > 0) {
|
||||||
|
result += '-';
|
||||||
|
}
|
||||||
|
result += charCode.toString(16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unicodeToUnifiedName(str: string) {
|
||||||
|
let output = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < str.length; i += 2) {
|
||||||
|
if (i > 0) {
|
||||||
|
output += '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
output +=
|
||||||
|
str.codePointAt(i)?.toString(16).toUpperCase().padStart(4, '0') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
@@ -1,458 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent, useCallback, useMemo } from 'react';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import { animated, useTransition } from '@react-spring/web';
|
|
||||||
import ReactSwipeableViews from 'react-swipeable-views';
|
|
||||||
|
|
||||||
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
|
|
||||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
|
||||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
|
||||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
|
||||||
import { AnimatedNumber } from 'mastodon/components/animated_number';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
|
||||||
import EmojiPickerDropdown from 'mastodon/features/compose/containers/emoji_picker_dropdown_container';
|
|
||||||
import { unicodeMapping } from 'mastodon/features/emoji/emoji_unicode_mapping_light';
|
|
||||||
import { autoPlayGif, reduceMotion, disableSwiping, mascot } from 'mastodon/initial_state';
|
|
||||||
import { assetHost } from 'mastodon/utils/config';
|
|
||||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
|
||||||
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
|
|
||||||
next: { id: 'lightbox.next', defaultMessage: 'Next' },
|
|
||||||
});
|
|
||||||
|
|
||||||
class ContentWithRouter extends ImmutablePureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
announcement: ImmutablePropTypes.map.isRequired,
|
|
||||||
...WithRouterPropTypes,
|
|
||||||
};
|
|
||||||
|
|
||||||
setRef = c => {
|
|
||||||
this.node = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this._updateLinks();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
this._updateLinks();
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateLinks () {
|
|
||||||
const node = this.node;
|
|
||||||
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const links = node.querySelectorAll('a');
|
|
||||||
|
|
||||||
for (var i = 0; i < links.length; ++i) {
|
|
||||||
let link = links[i];
|
|
||||||
|
|
||||||
if (link.classList.contains('status-link')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
link.classList.add('status-link');
|
|
||||||
|
|
||||||
let mention = this.props.announcement.get('mentions').find(item => link.href === item.get('url'));
|
|
||||||
|
|
||||||
if (mention) {
|
|
||||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
|
||||||
link.setAttribute('title', mention.get('acct'));
|
|
||||||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
|
||||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
|
||||||
} else {
|
|
||||||
let status = this.props.announcement.get('statuses').find(item => link.href === item.get('url'));
|
|
||||||
if (status) {
|
|
||||||
link.addEventListener('click', this.onStatusClick.bind(this, status), false);
|
|
||||||
}
|
|
||||||
link.setAttribute('title', link.href);
|
|
||||||
link.classList.add('unhandled-link');
|
|
||||||
}
|
|
||||||
|
|
||||||
link.setAttribute('target', '_blank');
|
|
||||||
link.setAttribute('rel', 'noopener');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMentionClick = (mention, e) => {
|
|
||||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/@${mention.get('acct')}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onHashtagClick = (hashtag, e) => {
|
|
||||||
hashtag = hashtag.replace(/^#/, '');
|
|
||||||
|
|
||||||
if (this.props.history&& e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/tags/${hashtag}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onStatusClick = (status, e) => {
|
|
||||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { announcement } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className='announcements__item__content translate animate-parent'
|
|
||||||
ref={this.setRef}
|
|
||||||
dangerouslySetInnerHTML={{ __html: announcement.get('contentHtml') }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const Content = withRouter(ContentWithRouter);
|
|
||||||
|
|
||||||
class Emoji extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
emoji: PropTypes.string.isRequired,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
hovered: PropTypes.bool.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { emoji, emojiMap, hovered } = this.props;
|
|
||||||
|
|
||||||
if (unicodeMapping[emoji]) {
|
|
||||||
const { filename, shortCode } = unicodeMapping[this.props.emoji];
|
|
||||||
const title = shortCode ? `:${shortCode}:` : '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
draggable='false'
|
|
||||||
className='emojione'
|
|
||||||
alt={emoji}
|
|
||||||
title={title}
|
|
||||||
src={`${assetHost}/emoji/${filename}.svg`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (emojiMap.get(emoji)) {
|
|
||||||
const filename = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']);
|
|
||||||
const shortCode = `:${emoji}:`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
draggable='false'
|
|
||||||
className='emojione custom-emoji'
|
|
||||||
alt={shortCode}
|
|
||||||
title={shortCode}
|
|
||||||
src={filename}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Reaction extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
announcementId: PropTypes.string.isRequired,
|
|
||||||
reaction: ImmutablePropTypes.map.isRequired,
|
|
||||||
addReaction: PropTypes.func.isRequired,
|
|
||||||
removeReaction: PropTypes.func.isRequired,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
style: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
hovered: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClick = () => {
|
|
||||||
const { reaction, announcementId, addReaction, removeReaction } = this.props;
|
|
||||||
|
|
||||||
if (reaction.get('me')) {
|
|
||||||
removeReaction(announcementId, reaction.get('name'));
|
|
||||||
} else {
|
|
||||||
addReaction(announcementId, reaction.get('name'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseEnter = () => this.setState({ hovered: true });
|
|
||||||
|
|
||||||
handleMouseLeave = () => this.setState({ hovered: false });
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { reaction } = this.props;
|
|
||||||
|
|
||||||
let shortCode = reaction.get('name');
|
|
||||||
|
|
||||||
if (unicodeMapping[shortCode]) {
|
|
||||||
shortCode = unicodeMapping[shortCode].shortCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<animated.button
|
|
||||||
className={classNames('reactions-bar__item', { active: reaction.get('me') })}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
title={`:${shortCode}:`}
|
|
||||||
style={this.props.style}
|
|
||||||
// This does not use animate-parent as this component is directly rendered by React.
|
|
||||||
onMouseEnter={this.handleMouseEnter}
|
|
||||||
onMouseLeave={this.handleMouseLeave}
|
|
||||||
>
|
|
||||||
<span className='reactions-bar__item__emoji'>
|
|
||||||
<Emoji hovered={this.state.hovered} emoji={reaction.get('name')} emojiMap={this.props.emojiMap} />
|
|
||||||
</span>
|
|
||||||
<span className='reactions-bar__item__count'>
|
|
||||||
<AnimatedNumber value={reaction.get('count')} />
|
|
||||||
</span>
|
|
||||||
</animated.button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReactionsBar = ({
|
|
||||||
announcementId,
|
|
||||||
reactions,
|
|
||||||
emojiMap,
|
|
||||||
addReaction,
|
|
||||||
removeReaction,
|
|
||||||
}) => {
|
|
||||||
const visibleReactions = useMemo(() => reactions.filter(x => x.get('count') > 0).toArray(), [reactions]);
|
|
||||||
|
|
||||||
const handleEmojiPick = useCallback((emoji) => {
|
|
||||||
addReaction(announcementId, emoji.native.replaceAll(/:/g, ''));
|
|
||||||
}, [addReaction, announcementId]);
|
|
||||||
|
|
||||||
const transitions = useTransition(visibleReactions, {
|
|
||||||
from: {
|
|
||||||
scale: 0,
|
|
||||||
},
|
|
||||||
enter: {
|
|
||||||
scale: 1,
|
|
||||||
},
|
|
||||||
leave: {
|
|
||||||
scale: 0,
|
|
||||||
},
|
|
||||||
keys: visibleReactions.map(x => x.get('name')),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames('reactions-bar', {
|
|
||||||
'reactions-bar--empty': visibleReactions.length === 0
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{transitions(({ scale }, reaction) => (
|
|
||||||
<Reaction
|
|
||||||
key={reaction.get('name')}
|
|
||||||
reaction={reaction}
|
|
||||||
style={{ transform: scale.to((s) => `scale(${s})`) }}
|
|
||||||
addReaction={addReaction}
|
|
||||||
removeReaction={removeReaction}
|
|
||||||
announcementId={announcementId}
|
|
||||||
emojiMap={emojiMap}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{visibleReactions.length < 8 && (
|
|
||||||
<EmojiPickerDropdown
|
|
||||||
onPickEmoji={handleEmojiPick}
|
|
||||||
button={<Icon id='plus' icon={AddIcon} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
ReactionsBar.propTypes = {
|
|
||||||
announcementId: PropTypes.string.isRequired,
|
|
||||||
reactions: ImmutablePropTypes.list.isRequired,
|
|
||||||
addReaction: PropTypes.func.isRequired,
|
|
||||||
removeReaction: PropTypes.func.isRequired,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
class Announcement extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
announcement: ImmutablePropTypes.map.isRequired,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
addReaction: PropTypes.func.isRequired,
|
|
||||||
removeReaction: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
selected: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
unread: !this.props.announcement.get('read'),
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
const { selected, announcement } = this.props;
|
|
||||||
if (!selected && this.state.unread !== !announcement.get('read')) {
|
|
||||||
this.setState({ unread: !announcement.get('read') });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { announcement } = this.props;
|
|
||||||
const { unread } = this.state;
|
|
||||||
const startsAt = announcement.get('starts_at') && new Date(announcement.get('starts_at'));
|
|
||||||
const endsAt = announcement.get('ends_at') && new Date(announcement.get('ends_at'));
|
|
||||||
const now = new Date();
|
|
||||||
const hasTimeRange = startsAt && endsAt;
|
|
||||||
const skipTime = announcement.get('all_day');
|
|
||||||
|
|
||||||
let timestamp = null;
|
|
||||||
if (hasTimeRange) {
|
|
||||||
const skipYear = startsAt.getFullYear() === endsAt.getFullYear() && endsAt.getFullYear() === now.getFullYear();
|
|
||||||
const skipEndDate = startsAt.getDate() === endsAt.getDate() && startsAt.getMonth() === endsAt.getMonth() && startsAt.getFullYear() === endsAt.getFullYear();
|
|
||||||
timestamp = (
|
|
||||||
<>
|
|
||||||
<FormattedDate value={startsAt} year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /> - <FormattedDate value={endsAt} year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month={skipEndDate ? undefined : 'short'} day={skipEndDate ? undefined : '2-digit'} hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const publishedAt = new Date(announcement.get('published_at'));
|
|
||||||
timestamp = (
|
|
||||||
<FormattedDate value={publishedAt} year={publishedAt.getFullYear() === now.getFullYear() ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='announcements__item'>
|
|
||||||
<strong className='announcements__item__range'>
|
|
||||||
<FormattedMessage id='announcement.announcement' defaultMessage='Announcement' />
|
|
||||||
<span> · {timestamp}</span>
|
|
||||||
</strong>
|
|
||||||
|
|
||||||
<Content announcement={announcement} />
|
|
||||||
|
|
||||||
<ReactionsBar
|
|
||||||
reactions={announcement.get('reactions')}
|
|
||||||
announcementId={announcement.get('id')}
|
|
||||||
addReaction={this.props.addReaction}
|
|
||||||
removeReaction={this.props.removeReaction}
|
|
||||||
emojiMap={this.props.emojiMap}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{unread && <span className='announcements__item__unread' />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Announcements extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
announcements: ImmutablePropTypes.list,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
dismissAnnouncement: PropTypes.func.isRequired,
|
|
||||||
addReaction: PropTypes.func.isRequired,
|
|
||||||
removeReaction: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
index: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
|
||||||
if (props.announcements.size > 0 && state.index >= props.announcements.size) {
|
|
||||||
return { index: props.announcements.size - 1 };
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this._markAnnouncementAsRead();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
this._markAnnouncementAsRead();
|
|
||||||
}
|
|
||||||
|
|
||||||
_markAnnouncementAsRead () {
|
|
||||||
const { dismissAnnouncement, announcements } = this.props;
|
|
||||||
const { index } = this.state;
|
|
||||||
const announcement = announcements.get(announcements.size - 1 - index);
|
|
||||||
if (!announcement.get('read')) dismissAnnouncement(announcement.get('id'));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChangeIndex = index => {
|
|
||||||
this.setState({ index: index % this.props.announcements.size });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleNextClick = () => {
|
|
||||||
this.setState({ index: (this.state.index + 1) % this.props.announcements.size });
|
|
||||||
};
|
|
||||||
|
|
||||||
handlePrevClick = () => {
|
|
||||||
this.setState({ index: (this.props.announcements.size + this.state.index - 1) % this.props.announcements.size });
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { announcements, intl } = this.props;
|
|
||||||
const { index } = this.state;
|
|
||||||
|
|
||||||
if (announcements.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='announcements'>
|
|
||||||
<img className='announcements__mastodon' alt='' draggable='false' src={mascot || elephantUIPlane} />
|
|
||||||
|
|
||||||
<div className='announcements__container'>
|
|
||||||
<ReactSwipeableViews animateHeight animateTransitions={!reduceMotion} index={index} onChangeIndex={this.handleChangeIndex}>
|
|
||||||
{announcements.map((announcement, idx) => (
|
|
||||||
<Announcement
|
|
||||||
key={announcement.get('id')}
|
|
||||||
announcement={announcement}
|
|
||||||
emojiMap={this.props.emojiMap}
|
|
||||||
addReaction={this.props.addReaction}
|
|
||||||
removeReaction={this.props.removeReaction}
|
|
||||||
intl={intl}
|
|
||||||
selected={index === idx}
|
|
||||||
disabled={disableSwiping}
|
|
||||||
/>
|
|
||||||
)).reverse()}
|
|
||||||
</ReactSwipeableViews>
|
|
||||||
|
|
||||||
{announcements.size > 1 && (
|
|
||||||
<div className='announcements__pagination'>
|
|
||||||
<IconButton disabled={announcements.size === 1} title={intl.formatMessage(messages.previous)} icon='chevron-left' iconComponent={ChevronLeftIcon} onClick={this.handlePrevClick} size={13} />
|
|
||||||
<span>{index + 1} / {announcements.size}</span>
|
|
||||||
<IconButton disabled={announcements.size === 1} title={intl.formatMessage(messages.next)} icon='chevron-right' iconComponent={ChevronRightIcon} onClick={this.handleNextClick} size={13} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(Announcements);
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
|
|
||||||
import { addReaction, removeReaction, dismissAnnouncement } from 'mastodon/actions/announcements';
|
|
||||||
|
|
||||||
import Announcements from '../components/announcements';
|
|
||||||
|
|
||||||
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
announcements: state.getIn(['announcements', 'items']),
|
|
||||||
emojiMap: customEmojiMap(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
dismissAnnouncement: id => dispatch(dismissAnnouncement(id)),
|
|
||||||
addReaction: (id, name) => dispatch(addReaction(id, name)),
|
|
||||||
removeReaction: (id, name) => dispatch(removeReaction(id, name)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Announcements);
|
|
||||||
@@ -10,10 +10,8 @@ import ReactSwipeableViews from 'react-swipeable-views';
|
|||||||
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
|
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
|
||||||
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
|
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||||
import { IconButton } from '@/mastodon/components/icon_button';
|
import { IconButton } from '@/mastodon/components/icon_button';
|
||||||
import LegacyAnnouncements from '@/mastodon/features/getting_started/containers/announcements_container';
|
|
||||||
import { mascot, reduceMotion } from '@/mastodon/initial_state';
|
import { mascot, reduceMotion } from '@/mastodon/initial_state';
|
||||||
import { createAppSelector, useAppSelector } from '@/mastodon/store';
|
import { createAppSelector, useAppSelector } from '@/mastodon/store';
|
||||||
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
|
||||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||||
|
|
||||||
@@ -32,7 +30,7 @@ const announcementSelector = createAppSelector(
|
|||||||
(announcements.get('items')?.toJS() as IAnnouncement[] | undefined) ?? [],
|
(announcements.get('items')?.toJS() as IAnnouncement[] | undefined) ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ModernAnnouncements: FC = () => {
|
export const Announcements: FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const announcements = useAppSelector(announcementSelector);
|
const announcements = useAppSelector(announcementSelector);
|
||||||
@@ -112,7 +110,3 @@ export const ModernAnnouncements: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Announcements = isModernEmojiEnabled()
|
|
||||||
? ModernAnnouncements
|
|
||||||
: LegacyAnnouncements;
|
|
||||||
|
|||||||
@@ -1,47 +1,17 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
|
|
||||||
import type { List } from 'immutable';
|
import type { List } from 'immutable';
|
||||||
|
|
||||||
import type { History } from 'history';
|
|
||||||
|
|
||||||
import type { ApiMentionJSON } from '@/mastodon/api_types/statuses';
|
|
||||||
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
||||||
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
|
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
|
||||||
import type { Status } from '@/mastodon/models/status';
|
import type { Status } from '@/mastodon/models/status';
|
||||||
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
|
||||||
|
|
||||||
import type { Mention } from './embedded_status';
|
import type { Mention } from './embedded_status';
|
||||||
|
|
||||||
const handleMentionClick = (
|
|
||||||
history: History,
|
|
||||||
mention: ApiMentionJSON,
|
|
||||||
e: MouseEvent,
|
|
||||||
) => {
|
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
history.push(`/@${mention.acct}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHashtagClick = (
|
|
||||||
history: History,
|
|
||||||
hashtag: string,
|
|
||||||
e: MouseEvent,
|
|
||||||
) => {
|
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
history.push(`/tags/${hashtag.replace(/^#/, '')}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EmbeddedStatusContent: React.FC<{
|
export const EmbeddedStatusContent: React.FC<{
|
||||||
status: Status;
|
status: Status;
|
||||||
className?: string;
|
className?: string;
|
||||||
}> = ({ status, className }) => {
|
}> = ({ status, className }) => {
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const mentions = useMemo(
|
const mentions = useMemo(
|
||||||
() => (status.get('mentions') as List<Mention>).toJS(),
|
() => (status.get('mentions') as List<Mention>).toJS(),
|
||||||
[status],
|
[status],
|
||||||
@@ -57,55 +27,10 @@ export const EmbeddedStatusContent: React.FC<{
|
|||||||
hrefToMention,
|
hrefToMention,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleContentRef = useCallback(
|
|
||||||
(node: HTMLDivElement | null) => {
|
|
||||||
if (!node || isModernEmojiEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const links = node.querySelectorAll<HTMLAnchorElement>('a');
|
|
||||||
|
|
||||||
for (const link of links) {
|
|
||||||
if (link.classList.contains('status-link')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
link.classList.add('status-link');
|
|
||||||
|
|
||||||
const mention = mentions.find((item) => link.href === item.url);
|
|
||||||
|
|
||||||
if (mention) {
|
|
||||||
link.addEventListener(
|
|
||||||
'click',
|
|
||||||
handleMentionClick.bind(null, history, mention),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
link.setAttribute('title', `@${mention.acct}`);
|
|
||||||
link.setAttribute('href', `/@${mention.acct}`);
|
|
||||||
} else if (
|
|
||||||
link.textContent.startsWith('#') ||
|
|
||||||
link.previousSibling?.textContent?.endsWith('#')
|
|
||||||
) {
|
|
||||||
link.addEventListener(
|
|
||||||
'click',
|
|
||||||
handleHashtagClick.bind(null, history, link.text),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`);
|
|
||||||
} else {
|
|
||||||
link.setAttribute('title', link.href);
|
|
||||||
link.classList.add('unhandled-link');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[mentions, history],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmojiHTML
|
<EmojiHTML
|
||||||
{...htmlHandlers}
|
{...htmlHandlers}
|
||||||
className={className}
|
className={className}
|
||||||
ref={handleContentRef}
|
|
||||||
lang={status.get('language') as string}
|
lang={status.get('language') as string}
|
||||||
htmlString={status.get('contentHtml') as string}
|
htmlString={status.get('contentHtml') as string}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { IconButton } from 'mastodon/components/icon_button';
|
|||||||
import InlineAccount from 'mastodon/components/inline_account';
|
import InlineAccount from 'mastodon/components/inline_account';
|
||||||
import MediaAttachments from 'mastodon/components/media_attachments';
|
import MediaAttachments from 'mastodon/components/media_attachments';
|
||||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||||
import emojify from 'mastodon/features/emoji/emoji';
|
|
||||||
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
||||||
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
|
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||||
|
|
||||||
@@ -48,13 +47,8 @@ class CompareHistoryModal extends PureComponent {
|
|||||||
const { index, versions, language, onClose } = this.props;
|
const { index, versions, language, onClose } = this.props;
|
||||||
const currentVersion = versions.get(index);
|
const currentVersion = versions.get(index);
|
||||||
|
|
||||||
const emojiMap = currentVersion.get('emojis').reduce((obj, emoji) => {
|
const content = currentVersion.get('content');
|
||||||
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
|
const spoilerContent = escapeTextContentForBrowser(currentVersion.get('spoiler_text'));
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const content = emojify(currentVersion.get('content'), emojiMap);
|
|
||||||
const spoilerContent = emojify(escapeTextContentForBrowser(currentVersion.get('spoiler_text')), emojiMap);
|
|
||||||
|
|
||||||
const formattedDate = <RelativeTimestamp timestamp={currentVersion.get('created_at')} short={false} />;
|
const formattedDate = <RelativeTimestamp timestamp={currentVersion.get('created_at')} short={false} />;
|
||||||
const formattedName = <InlineAccount accountId={currentVersion.get('account')} />;
|
const formattedName = <InlineAccount accountId={currentVersion.get('account')} />;
|
||||||
@@ -99,7 +93,7 @@ class CompareHistoryModal extends PureComponent {
|
|||||||
<EmojiHTML
|
<EmojiHTML
|
||||||
as="span"
|
as="span"
|
||||||
className='poll__option__text translate'
|
className='poll__option__text translate'
|
||||||
htmlString={emojify(escapeTextContentForBrowser(option.get('title')), emojiMap)}
|
htmlString={escapeTextContentForBrowser(option.get('title'))}
|
||||||
lang={language}
|
lang={language}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -22,12 +22,11 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex
|
|||||||
import { layoutFromWindow } from 'mastodon/is_mobile';
|
import { layoutFromWindow } from 'mastodon/is_mobile';
|
||||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||||
|
|
||||||
import { handleAnimateGif } from '../emoji/handlers';
|
|
||||||
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
||||||
import { clearHeight } from '../../actions/height_cache';
|
import { clearHeight } from '../../actions/height_cache';
|
||||||
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
import { initialState, me, owner, singleUserMode, trendsEnabled, landingPage, localLiveFeedAccess, disableHoverCards, autoPlayGif } from '../../initial_state';
|
import { initialState, me, owner, singleUserMode, trendsEnabled, landingPage, localLiveFeedAccess, disableHoverCards } from '../../initial_state';
|
||||||
|
|
||||||
import BundleColumnError from './components/bundle_column_error';
|
import BundleColumnError from './components/bundle_column_error';
|
||||||
import { NavigationBar } from './components/navigation_bar';
|
import { NavigationBar } from './components/navigation_bar';
|
||||||
@@ -382,11 +381,6 @@ class UI extends PureComponent {
|
|||||||
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
|
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
|
||||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||||
|
|
||||||
if (!autoPlayGif) {
|
|
||||||
window.addEventListener('mouseover', handleAnimateGif, { passive: true });
|
|
||||||
window.addEventListener('mouseout', handleAnimateGif, { passive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('dragenter', this.handleDragEnter, false);
|
document.addEventListener('dragenter', this.handleDragEnter, false);
|
||||||
document.addEventListener('dragover', this.handleDragOver, false);
|
document.addEventListener('dragover', this.handleDragOver, false);
|
||||||
document.addEventListener('drop', this.handleDrop, false);
|
document.addEventListener('drop', this.handleDrop, false);
|
||||||
@@ -412,8 +406,6 @@ class UI extends PureComponent {
|
|||||||
window.removeEventListener('blur', this.handleWindowBlur);
|
window.removeEventListener('blur', this.handleWindowBlur);
|
||||||
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
window.removeEventListener('mouseover', handleAnimateGif);
|
|
||||||
window.removeEventListener('mouseout', handleAnimateGif);
|
|
||||||
|
|
||||||
document.removeEventListener('dragenter', this.handleDragEnter);
|
document.removeEventListener('dragenter', this.handleDragEnter);
|
||||||
document.removeEventListener('dragover', this.handleDragOver);
|
document.removeEventListener('dragover', this.handleDragOver);
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { isFulfilled, isRejected } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
import { openURL } from 'mastodon/actions/search';
|
|
||||||
import { useAppDispatch } from 'mastodon/store';
|
|
||||||
|
|
||||||
import { isModernEmojiEnabled } from '../utils/environment';
|
|
||||||
|
|
||||||
const isMentionClick = (element: HTMLAnchorElement) =>
|
|
||||||
element.classList.contains('mention') &&
|
|
||||||
!element.classList.contains('hashtag');
|
|
||||||
|
|
||||||
const isHashtagClick = (element: HTMLAnchorElement) =>
|
|
||||||
element.textContent.startsWith('#') ||
|
|
||||||
element.previousSibling?.textContent?.endsWith('#');
|
|
||||||
|
|
||||||
export const useLinks = (skipHashtags?: boolean) => {
|
|
||||||
const history = useHistory();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const handleHashtagClick = useCallback(
|
|
||||||
(element: HTMLAnchorElement) => {
|
|
||||||
const { textContent } = element;
|
|
||||||
|
|
||||||
if (!textContent) return;
|
|
||||||
|
|
||||||
history.push(`/tags/${textContent.replace(/^#/, '')}`);
|
|
||||||
},
|
|
||||||
[history],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleMentionClick = useCallback(
|
|
||||||
async (element: HTMLAnchorElement) => {
|
|
||||||
const result = await dispatch(openURL({ url: element.href }));
|
|
||||||
|
|
||||||
if (isFulfilled(result)) {
|
|
||||||
if (result.payload.accounts[0]) {
|
|
||||||
history.push(`/@${result.payload.accounts[0].acct}`);
|
|
||||||
} else if (result.payload.statuses[0]) {
|
|
||||||
history.push(
|
|
||||||
`/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
window.location.href = element.href;
|
|
||||||
}
|
|
||||||
} else if (isRejected(result)) {
|
|
||||||
window.location.href = element.href;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch, history],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClick = useCallback(
|
|
||||||
(e: React.MouseEvent) => {
|
|
||||||
// Exit early if modern emoji is enabled, as this is handled by HandledLink.
|
|
||||||
if (isModernEmojiEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = (e.target as HTMLElement).closest('a');
|
|
||||||
|
|
||||||
if (!target || e.button !== 0 || e.ctrlKey || e.metaKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMentionClick(target)) {
|
|
||||||
e.preventDefault();
|
|
||||||
void handleMentionClick(target);
|
|
||||||
} else if (isHashtagClick(target) && !skipHashtags) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleHashtagClick(target);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[skipHashtags, handleMentionClick, handleHashtagClick],
|
|
||||||
);
|
|
||||||
|
|
||||||
return handleClick;
|
|
||||||
};
|
|
||||||
@@ -9,11 +9,8 @@ import { me, reduceMotion } from 'mastodon/initial_state';
|
|||||||
import ready from 'mastodon/ready';
|
import ready from 'mastodon/ready';
|
||||||
import { store } from 'mastodon/store';
|
import { store } from 'mastodon/store';
|
||||||
|
|
||||||
import {
|
import { initializeEmoji } from './features/emoji';
|
||||||
isProduction,
|
import { isProduction, isDevelopment } from './utils/environment';
|
||||||
isDevelopment,
|
|
||||||
isModernEmojiEnabled,
|
|
||||||
} from './utils/environment';
|
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
perf.start('main()');
|
perf.start('main()');
|
||||||
@@ -33,10 +30,7 @@ function main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isModernEmojiEnabled()) {
|
initializeEmoji();
|
||||||
const { initializeEmoji } = await import('@/mastodon/features/emoji');
|
|
||||||
initializeEmoji();
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = createRoot(mountNode);
|
const root = createRoot(mountNode);
|
||||||
root.render(<Mastodon {...props} />);
|
root.render(<Mastodon {...props} />);
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ import type {
|
|||||||
ApiAccountRoleJSON,
|
ApiAccountRoleJSON,
|
||||||
ApiAccountJSON,
|
ApiAccountJSON,
|
||||||
} from 'mastodon/api_types/accounts';
|
} from 'mastodon/api_types/accounts';
|
||||||
import emojify from 'mastodon/features/emoji/emoji';
|
|
||||||
import { unescapeHTML } from 'mastodon/utils/html';
|
import { unescapeHTML } from 'mastodon/utils/html';
|
||||||
|
|
||||||
import { CustomEmojiFactory, makeEmojiMap } from './custom_emoji';
|
import { CustomEmojiFactory } from './custom_emoji';
|
||||||
import type { CustomEmoji, EmojiMap } from './custom_emoji';
|
import type { CustomEmoji } from './custom_emoji';
|
||||||
|
|
||||||
// AccountField
|
// AccountField
|
||||||
interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
|
interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
|
||||||
@@ -102,17 +101,11 @@ export const accountDefaultValues: AccountShape = {
|
|||||||
|
|
||||||
const AccountFactory = ImmutableRecord<AccountShape>(accountDefaultValues);
|
const AccountFactory = ImmutableRecord<AccountShape>(accountDefaultValues);
|
||||||
|
|
||||||
function createAccountField(
|
function createAccountField(jsonField: ApiAccountFieldJSON) {
|
||||||
jsonField: ApiAccountFieldJSON,
|
|
||||||
emojiMap: EmojiMap,
|
|
||||||
) {
|
|
||||||
return AccountFieldFactory({
|
return AccountFieldFactory({
|
||||||
...jsonField,
|
...jsonField,
|
||||||
name_emojified: emojify(
|
name_emojified: escapeTextContentForBrowser(jsonField.name),
|
||||||
escapeTextContentForBrowser(jsonField.name),
|
value_emojified: jsonField.value,
|
||||||
emojiMap,
|
|
||||||
),
|
|
||||||
value_emojified: emojify(jsonField.value, emojiMap),
|
|
||||||
value_plain: unescapeHTML(jsonField.value),
|
value_plain: unescapeHTML(jsonField.value),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -120,8 +113,6 @@ function createAccountField(
|
|||||||
export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
|
export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
|
||||||
const { moved, ...accountJSON } = serverJSON;
|
const { moved, ...accountJSON } = serverJSON;
|
||||||
|
|
||||||
const emojiMap = makeEmojiMap(accountJSON.emojis);
|
|
||||||
|
|
||||||
const displayName =
|
const displayName =
|
||||||
accountJSON.display_name.trim().length === 0
|
accountJSON.display_name.trim().length === 0
|
||||||
? accountJSON.username
|
? accountJSON.username
|
||||||
@@ -134,7 +125,7 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
|
|||||||
...accountJSON,
|
...accountJSON,
|
||||||
moved: moved?.id,
|
moved: moved?.id,
|
||||||
fields: ImmutableList(
|
fields: ImmutableList(
|
||||||
serverJSON.fields.map((field) => createAccountField(field, emojiMap)),
|
serverJSON.fields.map((field) => createAccountField(field)),
|
||||||
),
|
),
|
||||||
emojis: ImmutableList(
|
emojis: ImmutableList(
|
||||||
serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji)),
|
serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji)),
|
||||||
@@ -142,11 +133,8 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
|
|||||||
roles: ImmutableList(
|
roles: ImmutableList(
|
||||||
serverJSON.roles?.map((role) => AccountRoleFactory(role)),
|
serverJSON.roles?.map((role) => AccountRoleFactory(role)),
|
||||||
),
|
),
|
||||||
display_name_html: emojify(
|
display_name_html: escapeTextContentForBrowser(displayName),
|
||||||
escapeTextContentForBrowser(displayName),
|
note_emojified: accountNote,
|
||||||
emojiMap,
|
|
||||||
),
|
|
||||||
note_emojified: emojify(accountNote, emojiMap),
|
|
||||||
note_plain: unescapeHTML(accountNote),
|
note_plain: unescapeHTML(accountNote),
|
||||||
url:
|
url:
|
||||||
accountJSON.url?.startsWith('http://') ||
|
accountJSON.url?.startsWith('http://') ||
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import escapeTextContentForBrowser from 'escape-html';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
|
||||||
import type { ApiPollJSON, ApiPollOptionJSON } from 'mastodon/api_types/polls';
|
import type { ApiPollJSON, ApiPollOptionJSON } from 'mastodon/api_types/polls';
|
||||||
import emojify from 'mastodon/features/emoji/emoji';
|
|
||||||
|
|
||||||
import { CustomEmojiFactory, makeEmojiMap } from './custom_emoji';
|
import { CustomEmojiFactory } from './custom_emoji';
|
||||||
import type { CustomEmoji, EmojiMap } from './custom_emoji';
|
import type { CustomEmoji } from './custom_emoji';
|
||||||
|
|
||||||
interface PollOptionTranslation {
|
interface PollOptionTranslation {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -17,16 +16,12 @@ export interface PollOption extends ApiPollOptionJSON {
|
|||||||
translation: PollOptionTranslation | null;
|
translation: PollOptionTranslation | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPollOptionTranslationFromServerJSON(
|
export function createPollOptionTranslationFromServerJSON(translation: {
|
||||||
translation: { title: string },
|
title: string;
|
||||||
emojiMap: EmojiMap,
|
}) {
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
...translation,
|
...translation,
|
||||||
titleHtml: emojify(
|
titleHtml: escapeTextContentForBrowser(translation.title),
|
||||||
escapeTextContentForBrowser(translation.title),
|
|
||||||
emojiMap,
|
|
||||||
),
|
|
||||||
} as PollOptionTranslation;
|
} as PollOptionTranslation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +45,6 @@ export function createPollFromServerJSON(
|
|||||||
serverJSON: ApiPollJSON,
|
serverJSON: ApiPollJSON,
|
||||||
previousPoll?: Poll,
|
previousPoll?: Poll,
|
||||||
) {
|
) {
|
||||||
const emojiMap = makeEmojiMap(serverJSON.emojis);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...pollDefaultValues,
|
...pollDefaultValues,
|
||||||
...serverJSON,
|
...serverJSON,
|
||||||
@@ -60,20 +53,15 @@ export function createPollFromServerJSON(
|
|||||||
const option = {
|
const option = {
|
||||||
...optionJSON,
|
...optionJSON,
|
||||||
voted: serverJSON.own_votes?.includes(index) || false,
|
voted: serverJSON.own_votes?.includes(index) || false,
|
||||||
titleHtml: emojify(
|
titleHtml: escapeTextContentForBrowser(optionJSON.title),
|
||||||
escapeTextContentForBrowser(optionJSON.title),
|
|
||||||
emojiMap,
|
|
||||||
),
|
|
||||||
} as PollOption;
|
} as PollOption;
|
||||||
|
|
||||||
const prevOption = previousPoll?.options[index];
|
const prevOption = previousPoll?.options[index];
|
||||||
if (prevOption?.translation && prevOption.title === option.title) {
|
if (prevOption?.translation && prevOption.title === option.title) {
|
||||||
const { translation } = prevOption;
|
const { translation } = prevOption;
|
||||||
|
|
||||||
option.translation = createPollOptionTranslationFromServerJSON(
|
option.translation =
|
||||||
translation,
|
createPollOptionTranslationFromServerJSON(translation);
|
||||||
emojiMap,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return option;
|
return option;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { Reducer } from '@reduxjs/toolkit';
|
import type { Reducer } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { importPolls } from 'mastodon/actions/importer/polls';
|
import { importPolls } from 'mastodon/actions/importer/polls';
|
||||||
import { makeEmojiMap } from 'mastodon/models/custom_emoji';
|
|
||||||
import { createPollOptionTranslationFromServerJSON } from 'mastodon/models/poll';
|
import { createPollOptionTranslationFromServerJSON } from 'mastodon/models/poll';
|
||||||
import type { Poll } from 'mastodon/models/poll';
|
import type { Poll } from 'mastodon/models/poll';
|
||||||
|
|
||||||
@@ -20,16 +19,11 @@ const statusTranslateSuccess = (state: PollsState, pollTranslation?: Poll) => {
|
|||||||
|
|
||||||
if (!poll) return;
|
if (!poll) return;
|
||||||
|
|
||||||
const emojiMap = makeEmojiMap(poll.emojis);
|
|
||||||
|
|
||||||
pollTranslation.options.forEach((item, index) => {
|
pollTranslation.options.forEach((item, index) => {
|
||||||
const option = poll.options[index];
|
const option = poll.options[index];
|
||||||
if (!option) return;
|
if (!option) return;
|
||||||
|
|
||||||
option.translation = createPollOptionTranslationFromServerJSON(
|
option.translation = createPollOptionTranslationFromServerJSON(item);
|
||||||
item,
|
|
||||||
emojiMap,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,16 +12,8 @@ export function isProduction() {
|
|||||||
else return import.meta.env.PROD;
|
else return import.meta.env.PROD;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Features = 'modern_emojis' | 'fasp';
|
export type Features = 'fasp' | 'http_message_signatures';
|
||||||
|
|
||||||
export function isFeatureEnabled(feature: Features) {
|
export function isFeatureEnabled(feature: Features) {
|
||||||
return initialState?.features.includes(feature) ?? false;
|
return initialState?.features.includes(feature) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isModernEmojiEnabled() {
|
|
||||||
try {
|
|
||||||
return isFeatureEnabled('modern_emojis');
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||||||
store[:use_blurhash] = object_account_user.setting_use_blurhash
|
store[:use_blurhash] = object_account_user.setting_use_blurhash
|
||||||
store[:use_pending_items] = object_account_user.setting_use_pending_items
|
store[:use_pending_items] = object_account_user.setting_use_pending_items
|
||||||
store[:show_trends] = Setting.trends && object_account_user.setting_trends
|
store[:show_trends] = Setting.trends && object_account_user.setting_trends
|
||||||
store[:emoji_style] = object_account_user.settings['web.emoji_style'] if Mastodon::Feature.modern_emojis_enabled?
|
store[:emoji_style] = object_account_user.settings['web.emoji_style']
|
||||||
else
|
else
|
||||||
store[:auto_play_gif] = Setting.auto_play_gif
|
store[:auto_play_gif] = Setting.auto_play_gif
|
||||||
store[:display_media] = Setting.display_media
|
store[:display_media] = Setting.display_media
|
||||||
|
|||||||
@@ -31,16 +31,15 @@
|
|||||||
label: I18n.t('simple_form.labels.defaults.setting_theme'),
|
label: I18n.t('simple_form.labels.defaults.setting_theme'),
|
||||||
wrapper: :with_label
|
wrapper: :with_label
|
||||||
|
|
||||||
- if Mastodon::Feature.modern_emojis_enabled?
|
.fields-group
|
||||||
.fields-group
|
= f.simple_fields_for :settings, current_user.settings do |ff|
|
||||||
= f.simple_fields_for :settings, current_user.settings do |ff|
|
= ff.input :'web.emoji_style',
|
||||||
= ff.input :'web.emoji_style',
|
collection: %w(auto twemoji native),
|
||||||
collection: %w(auto twemoji native),
|
include_blank: false,
|
||||||
include_blank: false,
|
hint: I18n.t('simple_form.hints.defaults.setting_emoji_style'),
|
||||||
hint: I18n.t('simple_form.hints.defaults.setting_emoji_style'),
|
label: I18n.t('simple_form.labels.defaults.setting_emoji_style'),
|
||||||
label: I18n.t('simple_form.labels.defaults.setting_emoji_style'),
|
label_method: ->(emoji_style) { I18n.t("emoji_styles.#{emoji_style}", default: emoji_style) },
|
||||||
label_method: ->(emoji_style) { I18n.t("emoji_styles.#{emoji_style}", default: emoji_style) },
|
wrapper: :with_label
|
||||||
wrapper: :with_label
|
|
||||||
|
|
||||||
- unless I18n.locale == :en
|
- unless I18n.locale == :en
|
||||||
.flash-message.translation-prompt
|
.flash-message.translation-prompt
|
||||||
|
|||||||
Reference in New Issue
Block a user