Merge commit '4896d2c4c6d3bd6b878c5a075b6611c65d4203b2' into glitch-soc/merge-upstream

Conflicts:
- `app/views/settings/preferences/appearance/show.html.haml`:
  Upstream changed stuff too close to glitch-soc's theming system changes.
  Applied upstream's changes.
- `streaming/index.js`:
  Upstream refactored a bunch of stuff where our code was different due to
  local-only posts.
  Applied upstream's changes while taking care of local-only posts.
This commit is contained in:
Claire
2025-10-28 22:10:12 +01:00
136 changed files with 1041 additions and 1409 deletions

View File

@@ -70,7 +70,7 @@ function loaded() {
};
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

View File

@@ -624,6 +624,7 @@ export function fetchComposeSuggestions(token) {
fetchComposeSuggestionsEmojis(dispatch, getState, token);
break;
case '#':
case '':
fetchComposeSuggestionsTags(dispatch, getState, token);
break;
default:
@@ -665,11 +666,11 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
dispatch(useEmoji(suggestion));
} else if (suggestion.type === 'hashtag') {
completion = `#${suggestion.name}`;
startPosition = position - 1;
completion = suggestion.name.slice(token.length - 1);
startPosition = position + token.length;
} else if (suggestion.type === 'account') {
completion = getState().getIn(['accounts', suggestion.id, 'acct']);
startPosition = position;
completion = `@${getState().getIn(['accounts', suggestion.id, 'acct'])}`;
startPosition = position - 1;
}
// We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that
@@ -729,7 +730,7 @@ function insertIntoTagHistory(recognizedTags, text) {
// complicated because of new normalization rules, it's no longer just
// a case sensitivity issue
const names = recognizedTags.map(tag => {
const matches = text.match(new RegExp(`#${tag.name}`, 'i'));
const matches = text.match(new RegExp(`[#]${tag.name}`, 'i'));
if (matches && matches.length > 0) {
return matches[0].slice(1);

View File

@@ -1,8 +1,5 @@
import escapeTextContentForBrowser from 'escape-html';
import { makeEmojiMap } from 'mastodon/models/custom_emoji';
import emojify from '../../features/emoji/emoji';
import { expandSpoilers } from '../../initial_state';
const domParser = new DOMParser();
@@ -88,11 +85,10 @@ export function normalizeStatus(status, normalOldStatus) {
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 emojiMap = makeEmojiMap(normalStatus.emojis);
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.contentHtml = normalStatus.content;
normalStatus.spoilerHtml = escapeTextContentForBrowser(spoilerText);
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
// 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) {
const emojiMap = makeEmojiMap(status.get('emojis').toJS());
const normalTranslation = {
detected_source_language: translation.detected_source_language,
language: translation.language,
provider: translation.provider,
contentHtml: emojify(translation.content, emojiMap),
spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap),
contentHtml: translation.content,
spoilerHtml: escapeTextContentForBrowser(translation.spoiler_text),
spoiler_text: translation.spoiler_text,
};
@@ -149,9 +143,8 @@ export function normalizeStatusTranslation(translation, status) {
export function normalizeAnnouncement(announcement) {
const normalAnnouncement = { ...announcement };
const emojiMap = makeEmojiMap(normalAnnouncement.emojis);
normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
normalAnnouncement.contentHtml = normalAnnouncement.content;
return normalAnnouncement;
}

View File

@@ -32,13 +32,20 @@ import {
const randomUpTo = max =>
Math.floor(Math.random() * Math.floor(max));
/**
* @typedef {import('mastodon/store').AppDispatch} Dispatch
* @typedef {import('mastodon/store').GetState} GetState
* @typedef {import('redux').UnknownAction} UnknownAction
* @typedef {function(Dispatch, GetState): Promise<void>} FallbackFunction
*/
/**
* @param {string} timelineId
* @param {string} channelName
* @param {Object.<string, string>} params
* @param {Object} options
* @param {function(Function, Function): Promise<void>} [options.fallback]
* @param {function(): void} [options.fillGaps]
* @param {FallbackFunction} [options.fallback]
* @param {function(): UnknownAction} [options.fillGaps]
* @param {function(object): boolean} [options.accept]
* @returns {function(): void}
*/
@@ -46,13 +53,14 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
const { messages } = getLocale();
return connectStream(channelName, params, (dispatch, getState) => {
// @ts-ignore
const locale = getState().getIn(['meta', 'locale']);
// @ts-expect-error
let pollingId;
/**
* @param {function(Function, Function): Promise<void>} fallback
* @param {FallbackFunction} fallback
*/
const useFallback = async fallback => {
@@ -132,7 +140,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
};
/**
* @param {Function} dispatch
* @param {Dispatch} dispatch
*/
async function refreshHomeTimelineAndNotification(dispatch) {
await dispatch(expandHomeTimeline({ maxId: undefined }));
@@ -151,7 +159,11 @@ async function refreshHomeTimelineAndNotification(dispatch) {
* @returns {function(): void}
*/
export const connectUserStream = () =>
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
connectTimelineStream('home', 'user', {}, {
fallback: refreshHomeTimelineAndNotification,
// @ts-expect-error
fillGaps: fillHomeTimelineGaps
});
/**
* @param {Object} options
@@ -159,7 +171,10 @@ export const connectUserStream = () =>
* @returns {function(): void}
*/
export const connectCommunityStream = ({ onlyMedia } = {}) =>
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) });
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, {
// @ts-expect-error
fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia }))
});
/**
* @param {Object} options
@@ -168,7 +183,10 @@ export const connectCommunityStream = ({ onlyMedia } = {}) =>
* @returns {function(): void}
*/
export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) =>
connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote }) });
connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, {}, {
// @ts-expect-error
fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote })
});
/**
* @param {string} columnId
@@ -191,4 +209,7 @@ export const connectDirectStream = () =>
* @returns {function(): void}
*/
export const connectListStream = listId =>
connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) });
connectTimelineStream(`list:${listId}`, 'list', { list: listId }, {
// @ts-expect-error
fillGaps: () => fillListTimelineGaps(listId)
});

View File

@@ -1,11 +1,6 @@
import { useCallback } from 'react';
import classNames from 'classnames';
import { useLinks } from 'mastodon/hooks/useLinks';
import { useAppSelector } from '../store';
import { isModernEmojiEnabled } from '../utils/environment';
import { EmojiHTML } from './emoji/html';
import { useElementHandledLink } from './status/handled_link';
@@ -21,22 +16,6 @@ export const AccountBio: React.FC<AccountBioProps> = ({
accountId,
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({
hashtagAccountId: showDropdown ? accountId : undefined,
});
@@ -62,30 +41,7 @@ export const AccountBio: React.FC<AccountBioProps> = ({
htmlString={note}
extraEmojis={extraEmojis}
className={classNames(className, 'translate')}
onClickCapture={handleClick}
ref={handleNodeChange}
{...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);
}
}
}

View File

@@ -61,7 +61,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
static defaultProps = {
autoFocus: true,
searchTokens: ['@', ':', '#'],
searchTokens: ['@', '', ':', '#', ''],
};
state = {

View File

@@ -25,7 +25,7 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
word = str.slice(left, right + caretPosition);
}
if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) {
if (!word || word.trim().length < 3 || ['@', '', ':', '#', ''].indexOf(word[0]) === -1) {
return [null, null];
}

View File

@@ -74,6 +74,6 @@ export const Linked: Story = {
acct: username,
})
: undefined;
return <LinkedDisplayName {...args} displayProps={{ account }} />;
return <LinkedDisplayName displayProps={{ account, ...args }} />;
},
};

View File

@@ -9,9 +9,8 @@ import { Skeleton } from '../skeleton';
import type { DisplayNameProps } from './index';
export const DisplayNameWithoutDomain: FC<
Omit<DisplayNameProps, 'variant' | 'localDomain'> &
ComponentPropsWithoutRef<'span'>
> = ({ account, className, children, ...props }) => {
Omit<DisplayNameProps, 'variant'> & ComponentPropsWithoutRef<'span'>
> = ({ account, className, children, localDomain: _, ...props }) => {
return (
<AnimateEmojiProvider
{...props}

View File

@@ -5,9 +5,8 @@ import { EmojiHTML } from '../emoji/html';
import type { DisplayNameProps } from './index';
export const DisplayNameSimple: FC<
Omit<DisplayNameProps, 'variant' | 'localDomain'> &
ComponentPropsWithoutRef<'span'>
> = ({ account, ...props }) => {
Omit<DisplayNameProps, 'variant'> & ComponentPropsWithoutRef<'span'>
> = ({ account, localDomain: _, ...props }) => {
if (!account) {
return null;
}

View File

@@ -7,8 +7,6 @@ import {
useState,
} from 'react';
import classNames from 'classnames';
import { cleanExtraEmojis } from '@/mastodon/features/emoji/normalize';
import { autoPlayGif } from '@/mastodon/initial_state';
import { polymorphicForwardRef } from '@/types/polymorphic';
@@ -65,11 +63,7 @@ export const AnimateEmojiProvider = polymorphicForwardRef<
const parentContext = useContext(AnimateEmojiContext);
if (parentContext !== null) {
return (
<Wrapper
{...props}
className={classNames(className, 'animate-parent')}
ref={ref}
>
<Wrapper {...props} className={className} ref={ref}>
{children}
</Wrapper>
);
@@ -78,7 +72,7 @@ export const AnimateEmojiProvider = polymorphicForwardRef<
return (
<Wrapper
{...props}
className={classNames(className, 'animate-parent')}
className={className}
onMouseEnter={handleEnter}
onMouseLeave={handleLeave}
ref={ref}

View File

@@ -1,9 +1,6 @@
import { useMemo } from 'react';
import classNames from 'classnames';
import type { CustomEmojiMapArg } from '@/mastodon/features/emoji/types';
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
import type {
OnAttributeHandler,
OnElementHandler,
@@ -22,7 +19,7 @@ export interface EmojiHTMLProps {
onAttribute?: OnAttributeHandler;
}
export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
export const EmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
(
{
extraEmojis,
@@ -59,32 +56,4 @@ export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
);
},
);
ModernEmojiHTML.displayName = 'ModernEmojiHTML';
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;
EmojiHTML.displayName = 'EmojiHTML';

View File

@@ -23,8 +23,6 @@ import { domain } from 'mastodon/initial_state';
import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { useLinks } from '../hooks/useLinks';
export const HoverCardAccount = forwardRef<
HTMLDivElement,
{ accountId?: string }
@@ -66,8 +64,6 @@ export const HoverCardAccount = forwardRef<
!isMutual &&
!isFollower;
const handleClick = useLinks();
return (
<div
ref={ref}
@@ -110,7 +106,7 @@ export const HoverCardAccount = forwardRef<
className='hover-card__bio'
/>
<div className='account-fields' onClickCapture={handleClick}>
<div className='account-fields'>
<AccountFields
fields={account.fields.take(2)}
emojis={account.emojis}

View File

@@ -4,7 +4,7 @@ import type { OnElementHandler } from '@/mastodon/utils/html';
import { polymorphicForwardRef } from '@/types/polymorphic';
import type { EmojiHTMLProps } from '../emoji/html';
import { ModernEmojiHTML } from '../emoji/html';
import { EmojiHTML } from '../emoji/html';
import { useElementHandledLink } from '../status/handled_link';
export const HTMLBlock = polymorphicForwardRef<
@@ -25,6 +25,6 @@ export const HTMLBlock = polymorphicForwardRef<
(...args) => onParentElement?.(...args) ?? onLinkElement(...args),
[onLinkElement, onParentElement],
);
return <ModernEmojiHTML {...props} onElement={onElement} />;
return <EmojiHTML {...props} onElement={onElement} />;
},
);

View File

@@ -13,9 +13,7 @@ import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import { openModal } from 'mastodon/actions/modal';
import { fetchPoll, vote } from 'mastodon/actions/polls';
import { Icon } from 'mastodon/components/icon';
import emojify from 'mastodon/features/emoji/emoji';
import { useIdentity } from 'mastodon/identity_context';
import { makeEmojiMap } from 'mastodon/models/custom_emoji';
import type * as Model from 'mastodon/models/poll';
import type { Status } from 'mastodon/models/status';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
@@ -235,12 +233,11 @@ const PollOption: React.FC<PollOptionProps> = (props) => {
let titleHtml = option.translation?.titleHtml ?? option.titleHtml;
if (!titleHtml) {
const emojiMap = makeEmojiMap(poll.emojis);
titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
titleHtml = escapeTextContentForBrowser(title);
}
return titleHtml;
}, [option, poll, title]);
}, [option, title]);
// Handlers
const handleOptionChange = useCallback(() => {

View File

@@ -26,7 +26,12 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
...props
}) => {
// Handle hashtags
if (text.startsWith('#') || prevText?.endsWith('#')) {
if (
text.startsWith('#') ||
prevText?.endsWith('#') ||
text.startsWith('') ||
prevText?.endsWith('')
) {
const hashtag = text.slice(1).trim();
return (
<Link

View File

@@ -15,8 +15,6 @@ import { Poll } from 'mastodon/components/poll';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { languages as preloadedLanguages } from 'mastodon/initial_state';
import { isModernEmojiEnabled } from '../utils/environment';
import { EmojiHTML } from './emoji/html';
import { HandledLink } from './status/handled_link';
@@ -72,6 +70,17 @@ const mapStateToProps = state => ({
languages: state.getIn(['server', 'translationLanguages', 'items']),
});
const compareUrls = (href1, href2) => {
try {
const url1 = new URL(href1);
const url2 = new URL(href2);
return url1.origin === url2.origin && url1.pathname === url2.pathname && url1.search === url2.search;
} catch {
return false;
}
};
class StatusContent extends PureComponent {
static propTypes = {
identity: identityContextPropShape,
@@ -108,41 +117,6 @@ class StatusContent extends PureComponent {
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 => link.href === 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 () {
@@ -153,22 +127,6 @@ class StatusContent extends PureComponent {
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) => {
this.startXY = [e.clientX, e.clientY];
};
@@ -206,7 +164,7 @@ class StatusContent extends PureComponent {
handleElement = (element, { key, ...props }, children) => {
if (element instanceof HTMLAnchorElement) {
const mention = this.props.status.get('mentions').find(item => element.href === item.get('url'));
const mention = this.props.status.get('mentions').find(item => compareUrls(element.href, item.get('url')));
return (
<HandledLink
{...props}

View File

@@ -1,30 +1,10 @@
import { EmojiHTML } from '@/mastodon/components/emoji/html';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import { isModernEmojiEnabled } from '../utils/environment';
import type { OnAttributeHandler } from '../utils/html';
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) => {
if (name === 'rel' && tagName === 'a') {
if (value === 'me') {
@@ -47,10 +27,6 @@ interface Props {
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
<span className='verified-badge'>
<Icon id='check' icon={CheckIcon} className='verified-badge__mark' />
<EmojiHTML
as='span'
htmlString={stripRelMe(link)}
onAttribute={onAttribute}
/>
<EmojiHTML as='span' htmlString={link} onAttribute={onAttribute} />
</span>
);

View File

@@ -49,7 +49,6 @@ import { ShortNumber } from 'mastodon/components/short_number';
import { AccountNote } from 'mastodon/features/account/components/account_note';
import { DomainPill } from 'mastodon/features/account/components/domain_pill';
import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
import { useLinks } from 'mastodon/hooks/useLinks';
import { useIdentity } from 'mastodon/identity_context';
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';
@@ -198,7 +197,6 @@ export const AccountHeader: React.FC<{
state.relationships.get(accountId),
);
const hidden = useAppSelector((state) => getAccountHidden(state, accountId));
const handleLinkClick = useLinks();
const handleBlock = useCallback(() => {
if (!account) {
@@ -852,10 +850,7 @@ export const AccountHeader: React.FC<{
{!(suspended || hidden) && (
<div className='account__header__extra'>
<div
className='account__header__bio'
onClickCapture={handleLinkClick}
>
<div className='account__header__bio'>
{account.id !== me && signedIn && (
<AccountNote accountId={accountId} />
)}

View File

@@ -1,6 +1,5 @@
import Trie from 'substring-trie';
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
import { assetHost } from 'mastodon/utils/config';
import { autoPlayGif } from '../../initial_state';
@@ -153,13 +152,9 @@ const emojifyNode = (node, customEmojis) => {
* Legacy emoji processing function.
* @param {string} str
* @param {object} customEmojis
* @param {boolean} force If true, always emojify even if modern emoji is enabled
* @returns {string}
*/
const emojify = (str, customEmojis = {}, force = false) => {
if (isModernEmojiEnabled() && !force) {
return str;
}
const emojify = (str, customEmojis = {}) => {
const wrapper = document.createElement('div');
wrapper.innerHTML = str;

View File

@@ -14,8 +14,7 @@ import { uncompress as emojiMartUncompress } from 'emoji-mart/dist/utils/data';
import data from './emoji_data.json';
import emojiMap from './emoji_map.json';
import { unicodeToFilename } from './unicode_to_filename';
import { unicodeToUnifiedName } from './unicode_to_unified_name';
import { unicodeToFilename, unicodeToUnifiedName } from './unicode_utils';
emojiMartUncompress(data);

View File

@@ -9,7 +9,7 @@ import type {
ShortCodesToEmojiData,
} from 'virtual:mastodon-emoji-compressed';
import { unicodeToUnifiedName } from './unicode_to_unified_name';
import { unicodeToUnifiedName } from './unicode_utils';
type Emojis = Record<
NonNullable<keyof ShortCodesToEmojiData>,
@@ -23,7 +23,7 @@ type Emojis = Record<
const [
shortCodesToEmojiData,
skins,
_skins,
categories,
short_names,
_emojisWithoutShortCodes,
@@ -47,4 +47,4 @@ Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
};
});
export { emojis, skins, categories, short_names };
export { emojis, categories, short_names };

View File

@@ -1,7 +1,7 @@
// This code is largely borrowed from:
// 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';
let originalPool = {};
@@ -10,8 +10,8 @@ let emojisList = {};
let emoticonsList = {};
let customEmojisList = [];
for (let emoji in data.emojis) {
let emojiData = data.emojis[emoji];
for (let emoji in emojis) {
let emojiData = emojis[emoji];
let { short_names, emoticons } = emojiData;
let id = short_names[0];
@@ -84,14 +84,14 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo
if (include.length || exclude.length) {
pool = {};
data.categories.forEach(category => {
categories.forEach(category => {
let isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true;
let isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false;
if (!isIncluded || isExcluded) {
return;
}
category.emojis.forEach(emojiId => pool[emojiId] = data.emojis[emojiId]);
category.emojis.forEach(emojiId => pool[emojiId] = emojis[emojiId]);
});
if (custom.length) {
@@ -171,7 +171,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo
if (results) {
if (emojisToShowFilter) {
results = results.filter((result) => emojisToShowFilter(data.emojis[result.id]));
results = results.filter((result) => emojisToShowFilter(emojis[result.id]));
}
if (results && results.length > maxResults) {

View File

@@ -2,7 +2,6 @@ import type { EmojiProps, PickerProps } from 'emoji-mart';
import EmojiRaw from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
import PickerRaw from 'emoji-mart/dist-es/components/picker/nimble-picker';
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
import { assetHost } from 'mastodon/utils/config';
import { EMOJI_MODE_NATIVE } from './constants';
@@ -27,7 +26,7 @@ const Emoji = ({
sheetSize={sheetSize}
sheetColumns={sheetColumns}
sheetRows={sheetRows}
native={mode === EMOJI_MODE_NATIVE && isModernEmojiEnabled()}
native={mode === EMOJI_MODE_NATIVE}
backgroundImageFn={backgroundImageFn}
{...props}
/>
@@ -51,7 +50,7 @@ const Picker = ({
sheetColumns={sheetColumns}
sheetRows={sheetRows}
backgroundImageFn={backgroundImageFn}
native={mode === EMOJI_MODE_NATIVE && isModernEmojiEnabled()}
native={mode === EMOJI_MODE_NATIVE}
{...props}
/>
);

View File

@@ -8,7 +8,7 @@ import type {
ShortCodesToEmojiDataKey,
} from 'virtual:mastodon-emoji-compressed';
import { unicodeToFilename } from './unicode_to_filename';
import { unicodeToFilename } from './unicode_utils';
type UnicodeMapping = Record<
FilenameData[number][0],

View File

@@ -209,50 +209,9 @@ function intersect(a, b) {
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 {
getData,
getSanitizedData,
uniq,
intersect,
deepMerge,
unifiedToNative,
measureScrollbar,
};

View File

@@ -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;
}

View File

@@ -1,8 +1,9 @@
import { initialState } from '@/mastodon/initial_state';
import { loadWorker } from '@/mastodon/utils/workers';
import { toSupportedLocale } from './locale';
import { emojiLogger } from './utils';
// eslint-disable-next-line import/default -- Importing via worker loader.
import EmojiWorker from './worker?worker&inline';
const userLocale = toSupportedLocale(initialState?.meta.locale ?? 'en');
@@ -16,9 +17,7 @@ export function initializeEmoji() {
log('initializing emojis');
if (!worker && 'Worker' in window) {
try {
worker = loadWorker(new URL('./worker', import.meta.url), {
type: 'module',
});
worker = new EmojiWorker();
} catch (err) {
console.warn('Error creating web worker:', err);
}

View File

@@ -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;
};

View File

@@ -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;
};

View 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;
}

View File

@@ -24,6 +24,14 @@ import StatusListContainer from '../ui/containers/status_list_container';
const messages = defineMessages({
title: { id: 'column.firehose', defaultMessage: 'Live feeds' },
title_local: {
id: 'column.firehose_local',
defaultMessage: 'Live feed for this server',
},
title_singular: {
id: 'column.firehose_singular',
defaultMessage: 'Live feed',
},
});
const ColumnSettings = () => {
@@ -161,13 +169,23 @@ const Firehose = ({ feedType, multiColumn }) => {
/>
);
let title;
if (canViewFeed(signedIn, permissions, localLiveFeedAccess) && canViewFeed(signedIn, permissions, remoteLiveFeedAccess)) {
title = messages.title;
} else if (canViewFeed(signedIn, permissions, localLiveFeedAccess)) {
title = messages.title_local;
} else {
title = messages.title_singular;
}
return (
<Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='globe'
iconComponent={PublicIcon}
active={hasUnread}
title={intl.formatMessage(messages.title)}
title={intl.formatMessage(title)}
onPin={handlePin}
onClick={handleHeaderClick}
multiColumn={multiColumn}

View File

@@ -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);

View File

@@ -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);

View File

@@ -10,10 +10,8 @@ import ReactSwipeableViews from 'react-swipeable-views';
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
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 { createAppSelector, useAppSelector } from '@/mastodon/store';
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.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) ?? [],
);
export const ModernAnnouncements: FC = () => {
export const Announcements: FC = () => {
const intl = useIntl();
const announcements = useAppSelector(announcementSelector);
@@ -112,7 +110,3 @@ export const ModernAnnouncements: FC = () => {
</div>
);
};
export const Announcements = isModernEmojiEnabled()
? ModernAnnouncements
: LegacyAnnouncements;

View File

@@ -61,6 +61,10 @@ const messages = defineMessages({
},
explore: { id: 'explore.title', defaultMessage: 'Trending' },
firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' },
firehose_singular: {
id: 'column.firehose_singular',
defaultMessage: 'Live feed',
},
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
@@ -275,7 +279,12 @@ export const NavigationPanel: React.FC<{ multiColumn?: boolean }> = ({
icon='globe'
iconComponent={PublicIcon}
isActive={isFirehoseActive}
text={intl.formatMessage(messages.firehose)}
text={intl.formatMessage(
canViewFeed(signedIn, permissions, localLiveFeedAccess) &&
canViewFeed(signedIn, permissions, remoteLiveFeedAccess)
? messages.firehose
: messages.firehose_singular,
)}
/>
)}

View File

@@ -1,47 +1,17 @@
import { useCallback, useMemo } from 'react';
import { useHistory } from 'react-router-dom';
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 { useElementHandledLink } from '@/mastodon/components/status/handled_link';
import type { Status } from '@/mastodon/models/status';
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
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<{
status: Status;
className?: string;
}> = ({ status, className }) => {
const history = useHistory();
const mentions = useMemo(
() => (status.get('mentions') as List<Mention>).toJS(),
[status],
@@ -57,55 +27,10 @@ export const EmbeddedStatusContent: React.FC<{
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 (
<EmojiHTML
{...htmlHandlers}
className={className}
ref={handleContentRef}
lang={status.get('language') as string}
htmlString={status.get('contentHtml') as string}
/>

View File

@@ -14,7 +14,6 @@ import { IconButton } from 'mastodon/components/icon_button';
import InlineAccount from 'mastodon/components/inline_account';
import MediaAttachments from 'mastodon/components/media_attachments';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import emojify from 'mastodon/features/emoji/emoji';
import { EmojiHTML } from '@/mastodon/components/emoji/html';
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
@@ -48,13 +47,8 @@ class CompareHistoryModal extends PureComponent {
const { index, versions, language, onClose } = this.props;
const currentVersion = versions.get(index);
const emojiMap = currentVersion.get('emojis').reduce((obj, emoji) => {
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
return obj;
}, {});
const content = emojify(currentVersion.get('content'), emojiMap);
const spoilerContent = emojify(escapeTextContentForBrowser(currentVersion.get('spoiler_text')), emojiMap);
const content = currentVersion.get('content');
const spoilerContent = escapeTextContentForBrowser(currentVersion.get('spoiler_text'));
const formattedDate = <RelativeTimestamp timestamp={currentVersion.get('created_at')} short={false} />;
const formattedName = <InlineAccount accountId={currentVersion.get('account')} />;
@@ -99,7 +93,7 @@ class CompareHistoryModal extends PureComponent {
<EmojiHTML
as="span"
className='poll__option__text translate'
htmlString={emojify(escapeTextContentForBrowser(option.get('title')), emojiMap)}
htmlString={escapeTextContentForBrowser(option.get('title'))}
lang={language}
/>
</label>

View File

@@ -22,12 +22,11 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex
import { layoutFromWindow } from 'mastodon/is_mobile';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { handleAnimateGif } from '../emoji/handlers';
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
import { clearHeight } from '../../actions/height_cache';
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
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 { NavigationBar } from './components/navigation_bar';
@@ -382,11 +381,6 @@ class UI extends PureComponent {
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
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('dragover', this.handleDragOver, false);
document.addEventListener('drop', this.handleDrop, false);
@@ -412,8 +406,6 @@ class UI extends PureComponent {
window.removeEventListener('blur', this.handleWindowBlur);
window.removeEventListener('beforeunload', this.handleBeforeUnload);
window.removeEventListener('resize', this.handleResize);
window.removeEventListener('mouseover', handleAnimateGif);
window.removeEventListener('mouseout', handleAnimateGif);
document.removeEventListener('dragenter', this.handleDragEnter);
document.removeEventListener('dragover', this.handleDragOver);

View File

@@ -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;
};

View File

@@ -28,6 +28,7 @@
"account.disable_notifications": "Спиране на известяване при публикуване от @{name}",
"account.domain_blocking": "Блокиране на домейн",
"account.edit_profile": "Редактиране на профила",
"account.edit_profile_short": "Редактиране",
"account.enable_notifications": "Известяване при публикуване от @{name}",
"account.endorse": "Представи в профила",
"account.familiar_followers_many": "Последвано от {name1}, {name2}, и {othersCount, plural, one {един друг, когото познавате} other {# други, които познавате}}",
@@ -40,6 +41,9 @@
"account.featured_tags.last_status_never": "Няма публикации",
"account.follow": "Последване",
"account.follow_back": "Последване взаимно",
"account.follow_request_cancel": "Отказване на заявката",
"account.follow_request_cancel_short": "Отказ",
"account.follow_request_short": "Заявка",
"account.followers": "Последователи",
"account.followers.empty": "Още никой не следва потребителя.",
"account.followers_counter": "{count, plural, one {{counter} последовател} other {{counter} последователи}}",
@@ -238,6 +242,9 @@
"confirmations.missing_alt_text.secondary": "Все пак да се публикува",
"confirmations.missing_alt_text.title": "Добавяте ли алтернативен текст?",
"confirmations.mute.confirm": "Заглушаване",
"confirmations.quiet_post_quote_info.dismiss": "Без друго напомняне",
"confirmations.quiet_post_quote_info.got_it": "Схванах",
"confirmations.quiet_post_quote_info.title": "Цитиране на публикации за тиха публика",
"confirmations.redraft.confirm": "Изтриване и преработване",
"confirmations.redraft.message": "Наистина ли искате да изтриете тази публикация и да я направите чернова? Означаванията като любими и подсилванията ще се изгубят, а и отговорите към първоначалната публикация ще осиротеят.",
"confirmations.redraft.title": "Изтривате и преработвате ли публикацията?",
@@ -247,7 +254,11 @@
"confirmations.revoke_quote.confirm": "Премахване на публикация",
"confirmations.revoke_quote.message": "Действието е неотменимо.",
"confirmations.revoke_quote.title": "Премахвате ли публикацията?",
"confirmations.unblock.confirm": "Отблокиране",
"confirmations.unblock.title": "Отблокирате ли @{name}?",
"confirmations.unfollow.confirm": "Без следване",
"confirmations.unfollow.title": "Спирате ли следване на {name}?",
"confirmations.withdraw_request.confirm": "Оттегляне на заявката",
"content_warning.hide": "Скриване на публ.",
"content_warning.show": "Нека се покаже",
"content_warning.show_more": "Показване на още",
@@ -442,10 +453,12 @@
"ignore_notifications_modal.private_mentions_title": "Пренебрегвате ли известия от непоискани лични споменавания?",
"info_button.label": "Помощ",
"info_button.what_is_alt_text": "<h1>Какво е алтернативен текст?</h1> <p>Алтернативният текст осигурява описания на изображение за хора със зрителни увреждания, връзки с ниска честотна лента или търсещите допълнителен контекст.</p> <p>Може да подобрите достъпността и разбираемостта за всеки, пишейки ясен, кратък и обективен алтернативен текст.</p> <ul> <li>Уловете важните елементи</li> <li>Обобщете текста в образите</li> <li>Употребявайте правилна структура на изречението</li> <li>Избягвайте излишна информация</li> <li>Съсредоточете се върху тенденциите и ключови констатации в сложни онагледявания (като диаграми и карти)</li> </ul>",
"interaction_modal.action": "Трябва да влезете с акаунта си, в който и да е сървър на Mastodon, когото използвате, за да взаимодействате с публикация на {name}.",
"interaction_modal.go": "Напред",
"interaction_modal.no_account_yet": "Още ли нямате акаунт?",
"interaction_modal.on_another_server": "На различен сървър",
"interaction_modal.on_this_server": "На този сървър",
"interaction_modal.title": "Влезте, за да продължите",
"interaction_modal.username_prompt": "Напр. {example}",
"intervals.full.days": "{number, plural, one {# ден} other {# дни}}",
"intervals.full.hours": "{number, plural, one {# час} other {# часа}}",
@@ -596,6 +609,7 @@
"notification.moderation_warning.action_suspend": "Вашият акаунт е спрян.",
"notification.own_poll": "Анкетата ви приключи",
"notification.poll": "Анкета, в която гласувахте, приключи",
"notification.quoted_update": "{name} редактира публикация, която цитирахте",
"notification.reblog": "{name} подсили ваша публикация",
"notification.reblog.name_and_others_with_link": "{name} и <a>{count, plural, one {# друг} other {# други}}</a> подсилиха ваша публикация",
"notification.relationships_severance_event": "Изгуби се връзката с {name}",
@@ -715,10 +729,17 @@
"privacy.private.short": "Последователи",
"privacy.public.long": "Всеки във и извън Mastodon",
"privacy.public.short": "Публично",
"privacy.quote.anyone": "{visibility}, всеки може да цитира",
"privacy.quote.disabled": "{visibility}, цитатите са изключени",
"privacy.quote.limited": "{visibility}, цитатите са ограничени",
"privacy.unlisted.additional": "Това действие е точно като публичното, с изключение на това, че публикацията няма да се появява в каналите на живо, хаштаговете, разглеждането или търсенето в Mastodon, дори ако сте избрали да се публично видими на ниво акаунт.",
"privacy.unlisted.short": "Тиха публика",
"privacy_policy.last_updated": "Последно осъвременяване на {date}",
"privacy_policy.title": "Политика за поверителност",
"quote_error.edit": "Не може да се добавят цитати, редайтирайки публикация.",
"quote_error.poll": "Не може да се цитира при анкетиране.",
"quote_error.unauthorized": "Нямате право да цитирате тази публикация.",
"quote_error.upload": "Цитирането не е позволено с мултимедийни прикачвания.",
"recommended": "Препоръчано",
"refresh": "Опресняване",
"regeneration_indicator.please_stand_by": "Изчакайте.",
@@ -734,6 +755,8 @@
"relative_time.minutes": "{number}м.",
"relative_time.seconds": "{number}с.",
"relative_time.today": "днес",
"remove_quote_hint.button_label": "Схванах",
"remove_quote_hint.message": "Може да го направите от менюто възможности {icon}.",
"reply_indicator.attachments": "{count, plural, one {# прикаване} other {# прикачвания}}",
"reply_indicator.cancel": "Отказ",
"reply_indicator.poll": "Анкета",
@@ -825,13 +848,22 @@
"status.admin_account": "Отваряне на интерфейс за модериране за @{name}",
"status.admin_domain": "Отваряне на модериращия интерфейс за {domain}",
"status.admin_status": "Отваряне на публикацията в модериращия интерфейс",
"status.all_disabled": "Подсилването и цитатите са изключени",
"status.block": "Блокиране на @{name}",
"status.bookmark": "Отмятане",
"status.cancel_reblog_private": "Край на подсилването",
"status.cannot_quote": "Не е позволено да цитирате тази публикация",
"status.cannot_reblog": "Публикацията не може да се подсилва",
"status.context.loading": "Зареждане на още отговори",
"status.context.loading_error": "Не можаха да се заредят нови отговори",
"status.context.loading_success": "Новите отговори заредени",
"status.context.more_replies_found": "Още намерени отговори",
"status.context.retry": "Друг опит",
"status.context.show": "Показване",
"status.continued_thread": "Продължена нишка",
"status.copy": "Копиране на връзката към публикация",
"status.delete": "Изтриване",
"status.delete.success": "Публикацията е изтрита",
"status.detailed_status": "Подробен изглед на разговора",
"status.direct": "Частно споменаване на @{name}",
"status.direct_indicator": "Частно споменаване",
@@ -855,23 +887,32 @@
"status.open": "Разширяване на публикацията",
"status.pin": "Закачане в профила",
"status.quote_error.filtered": "Скрито поради един от филтрите ви",
"status.quote_error.limited_account_hint.title": "Този акаунт е бил скрит от модераторите на {domain}.",
"status.quote_error.not_available": "Неналична публикация",
"status.quote_error.pending_approval": "Публикацията чака одобрение",
"status.quote_error.revoked": "Премахната публикация от автора",
"status.quote_followers_only": "Само последователи могат да цитират тази публикация",
"status.quote_manual_review": "Авторът ще преглежда ръчно",
"status.quote_policy_change": "Промяна кой може да цитира",
"status.quote_post_author": "Цитирах публикация от @{name}",
"status.quote_private": "Частните публикации не може да се цитират",
"status.read_more": "Още за четене",
"status.reblog": "Подсилване",
"status.reblog_or_quote": "Подсилване или цитиране",
"status.reblog_private": "Споделете пак с последователите си",
"status.reblogged_by": "{name} подсили",
"status.reblogs": "{count, plural, one {подсилване} other {подсилвания}}",
"status.reblogs.empty": "Още никого не е подсилвал публикацията. Подсилващият ще се покаже тук.",
"status.redraft": "Изтриване и преработване",
"status.remove_bookmark": "Премахване на отметката",
"status.remove_favourite": "Премахване от любими",
"status.remove_quote": "Премахване",
"status.replied_in_thread": "Отговорено в нишката",
"status.replied_to": "В отговор до {name}",
"status.reply": "Отговор",
"status.replyAll": "Отговор на нишка",
"status.report": "Докладване на @{name}",
"status.request_quote": "Заявка за цитиране",
"status.revoke_quote": "Премахване на моя публикация от публикацията на @{name}",
"status.sensitive_warning": "Деликатно съдържание",
"status.share": "Споделяне",
@@ -910,6 +951,7 @@
"upload_button.label": "Добавете файл с образ, видео или звук",
"upload_error.limit": "Превишено ограничението за качване на файлове.",
"upload_error.poll": "Качването на файлове не е позволено с анкети.",
"upload_error.quote": "Цитирайки, не може да качвате файл.",
"upload_form.drag_and_drop.instructions": "Натиснете интервал или enter, за да подберете мултимедийно прикачване. Провлачвайки, ползвайте клавишите със стрелки, за да премествате мултимедията във всяка дадена посока. Натиснете пак интервал или enter, за да се стовари мултимедийното прикачване в новото си положение или натиснете Esc за отмяна.",
"upload_form.drag_and_drop.on_drag_cancel": "Провлачването е отменено. Мултимедийното прикачване {item} е спуснато.",
"upload_form.drag_and_drop.on_drag_end": "Мултимедийното прикачване {item} е спуснато.",
@@ -935,6 +977,11 @@
"video.volume_up": "Увеличаване на звука",
"visibility_modal.button_title": "Задаване на видимост",
"visibility_modal.header": "Видимост и взаимодействие",
"visibility_modal.helper.privacy_editing": "Видимостта не може да се променя след публикуване на публикацията.",
"visibility_modal.privacy_label": "Видимост",
"visibility_modal.quote_followers": "Само последователи",
"visibility_modal.quote_public": "Някой"
"visibility_modal.quote_label": "Кой може да цитира",
"visibility_modal.quote_nobody": "Само аз",
"visibility_modal.quote_public": "Някой",
"visibility_modal.save": "Запазване"
}

View File

@@ -173,6 +173,8 @@
"column.edit_list": "Edit list",
"column.favourites": "Favorites",
"column.firehose": "Live feeds",
"column.firehose_local": "Live feed for this server",
"column.firehose_singular": "Live feed",
"column.follow_requests": "Follow requests",
"column.home": "Home",
"column.list_members": "Manage list members",

View File

@@ -333,6 +333,7 @@
"empty_column.bookmarked_statuses": "Järjehoidjatesse pole veel lisatud postitusi. Kui lisad mõne, näed neid siin.",
"empty_column.community": "Kohalik ajajoon on tühi. Kirjuta midagi avalikult, et pall veerema ajada!",
"empty_column.direct": "Sul pole veel ühtegi privaatset mainimist. Kui saadad või saad mõne, ilmuvad need siin.",
"empty_column.disabled_feed": "See infovoog on serveri peakasutajate poolt välja lülitatud.",
"empty_column.domain_blocks": "Siin ei ole veel peidetud domeene.",
"empty_column.explore_statuses": "Praegu pole ühtegi trendi. Tule hiljem tagasi!",
"empty_column.favourited_statuses": "Pole veel lemmikpostitusi. Kui märgid mõne, näed neid siin.",

View File

@@ -333,6 +333,7 @@
"empty_column.bookmarked_statuses": "Tú hevur enn einki goymt uppslag. Tú tú goymir eitt uppslag, kemur tað her.",
"empty_column.community": "Lokala tíðarlinjan er tóm. Skriva okkurt alment fyri at fáa boltin á rull!",
"empty_column.direct": "Tú hevur ongar privatar umrøður enn. Tá tú sendir ella móttekur eina privata umrøðu, so verður hon sjónlig her.",
"empty_column.disabled_feed": "Hendan rásin er gjørd óvirkin av ambætaraumsitarunum hjá tær.",
"empty_column.domain_blocks": "Enn eru eingi blokeraði domenir.",
"empty_column.explore_statuses": "Einki rák er beint nú. Royn aftur seinni!",
"empty_column.favourited_statuses": "Tú hevur ongar yndispostar enn. Tá tú gevur einum posti yndismerki, so sært tú hann her.",

View File

@@ -333,6 +333,7 @@
"empty_column.bookmarked_statuses": "Níl aon phostáil leabharmharcaithe agat fós. Nuair a dhéanann tú leabharmharc, beidh sé le feiceáil anseo.",
"empty_column.community": "Tá an amlíne áitiúil folamh. Foilsigh rud éigin go poiblí le tús a chur le cúrsaí!",
"empty_column.direct": "Níl aon tagairtí príobháideacha agat fós. Nuair a sheolann tú nó a gheobhaidh tú ceann, beidh sé le feiceáil anseo.",
"empty_column.disabled_feed": "Tá an fotha seo díchumasaithe ag riarthóirí do fhreastalaí.",
"empty_column.domain_blocks": "Níl aon fearainn bhactha ann go fóill.",
"empty_column.explore_statuses": "Níl rud ar bith ag treochtáil faoi láthair. Tar ar ais ar ball!",
"empty_column.favourited_statuses": "Níl aon postálacha is fearr leat fós. Nuair is fearr leat ceann, beidh sé le feiceáil anseo.",

View File

@@ -172,7 +172,7 @@
"column.domain_blocks": "Dominios blocate",
"column.edit_list": "Modificar lista",
"column.favourites": "Favorites",
"column.firehose": "Fluxos in directo",
"column.firehose": "Fluxos in vivo",
"column.follow_requests": "Requestas de sequimento",
"column.home": "Initio",
"column.list_members": "Gerer le membros del lista",
@@ -333,6 +333,7 @@
"empty_column.bookmarked_statuses": "Tu non ha ancora messages in marcapaginas. Quando tu adde un message al marcapaginas, illo apparera hic.",
"empty_column.community": "Le chronologia local es vacue. Scribe qualcosa public pro poner le cosas in marcha!",
"empty_column.direct": "Tu non ha ancora mentiones private. Quando tu invia o recipe un mention, illo apparera hic.",
"empty_column.disabled_feed": "Iste canal ha essite disactivate per le adminsistratores de tu servitor.",
"empty_column.domain_blocks": "Il non ha dominios blocate ancora.",
"empty_column.explore_statuses": "Il non ha tendentias in iste momento. Reveni plus tarde!",
"empty_column.favourited_statuses": "Tu non ha alcun message favorite ancora. Quando tu marca un message como favorite, illo apparera hic.",
@@ -460,7 +461,7 @@
"ignore_notifications_modal.not_following_title": "Ignorar notificationes de personas que tu non seque?",
"ignore_notifications_modal.private_mentions_title": "Ignorar notificationes de mentiones private non requestate?",
"info_button.label": "Adjuta",
"info_button.what_is_alt_text": "<h1>Que es texto alternative?</h1><p>Le texto alternative forni descriptiones de imagines a personas con impedimentos visual, con connexiones lente, o qui cerca contexto additional.</p><p>Tu pote meliorar le accessibilitate e le comprension pro totes scribente un texto alternative clar, concise e objective.</p><ul><li>Captura le elementos importante</li><li>Summarisa texto in imagines</li><li>Usa le structura de phrase normal</li><li>Evita information redundante</li><li>In figuras complexe (como diagrammas o mappas), concentra te sur le tendentias e punctos clave</li></ul>",
"info_button.what_is_alt_text": "<h1>Que es texto alternative?</h1><p>Le texto alternative forni descriptiones de imagines a personas con impedimentos visual, con connexiones lente a internet, o qui cerca contexto supplementari.</p><p>Tu pote meliorar le accessibilitate e le comprension pro totes si tu scribe un texto alternative clar, concise e objective.</p><ul><li>Captura le elementos importante</li><li>Summarisa texto in imagines</li><li>Usa un structura conventional de phrases</li><li>Evita information redundante</li><li>In figuras complexe (como diagrammas o mappas), concentra te sur le tendentias e punctos clave</li></ul>",
"interaction_modal.action": "Pro interager con le message de {name}, tu debe acceder a tu conto sur le servitor Mastodon que tu usa.",
"interaction_modal.go": "Revenir",
"interaction_modal.no_account_yet": "Tu non ha ancora un conto?",
@@ -574,8 +575,8 @@
"navigation_bar.follows_and_followers": "Sequites e sequitores",
"navigation_bar.import_export": "Importar e exportar",
"navigation_bar.lists": "Listas",
"navigation_bar.live_feed_local": "Canal in directo (local)",
"navigation_bar.live_feed_public": "Canal in directo (public)",
"navigation_bar.live_feed_local": "Canal in vivo (local)",
"navigation_bar.live_feed_public": "Canal in vivo (public)",
"navigation_bar.logout": "Clauder session",
"navigation_bar.moderation": "Moderation",
"navigation_bar.more": "Plus",
@@ -748,7 +749,7 @@
"privacy.quote.anyone": "{visibility}, omnes pote citar",
"privacy.quote.disabled": "{visibility}, citation disactivate",
"privacy.quote.limited": "{visibility}, citation limitate",
"privacy.unlisted.additional": "Isto es exactemente como public, excepte que le message non apparera in fluxos in directo, in hashtags, in Explorar, o in le recerca de Mastodon, mesmo si tu ha optate pro render tote le conto discoperibile.",
"privacy.unlisted.additional": "Isto es exactemente como public, excepte que le message non apparera in fluxos in vivo, in hashtags, in Explorar, o in le recerca de Mastodon, mesmo si tu ha optate pro render tote le conto discoperibile.",
"privacy.unlisted.long": "Non apparera in le resultatos de recerca, tendentias e chronologias public de Mastodon",
"privacy.unlisted.short": "Public, non listate",
"privacy_policy.last_updated": "Ultime actualisation {date}",

View File

@@ -38,6 +38,8 @@
"account.follow": "Sige",
"account.follow_back": "Sige tamyen",
"account.follow_back_short": "Sige tambyen",
"account.follow_request": "Solisita segirle",
"account.follow_request_cancel": "Anula solisitud",
"account.follow_request_cancel_short": "Anula",
"account.follow_request_short": "Solisitud",
"account.followers": "Suivantes",
@@ -62,6 +64,7 @@
"account.mute_short": "Silensia",
"account.muted": "Silensiado",
"account.muting": "Silensyando",
"account.mutual": "Vos sigesh mutualmente",
"account.no_bio": "No ay deskripsion.",
"account.open_original_page": "Avre pajina orijnala",
"account.posts": "Publikasyones",
@@ -97,6 +100,7 @@
"alert.unexpected.title": "Atyo!",
"alt_text_badge.title": "Teksto alternativo",
"alt_text_modal.add_alt_text": "Adjusta teksto alternativo",
"alt_text_modal.add_text_from_image": "Adjusta teksto de imaje",
"alt_text_modal.cancel": "Anula",
"alt_text_modal.change_thumbnail": "Troka minyatura",
"alt_text_modal.done": "Fecho",
@@ -210,6 +214,7 @@
"confirmations.logout.message": "Estas siguro ke keres salir de tu kuento?",
"confirmations.logout.title": "Salir?",
"confirmations.missing_alt_text.confirm": "Adjusta teksto alternativo",
"confirmations.missing_alt_text.secondary": "Puvlika de todos modos",
"confirmations.missing_alt_text.title": "Adjustar teksto alternativo?",
"confirmations.mute.confirm": "Silensia",
"confirmations.quiet_post_quote_info.got_it": "Entyendo",
@@ -382,6 +387,7 @@
"hints.profiles.see_more_followers": "Ve mas suivantes en {domain}",
"hints.profiles.see_more_follows": "Ve mas segidos en {domain}",
"hints.profiles.see_more_posts": "Ve mas puvlikasyones en {domain}",
"home.column_settings.show_quotes": "Muestra sitas",
"home.column_settings.show_reblogs": "Amostra repartajasyones",
"home.column_settings.show_replies": "Amostra repuestas",
"home.hide_announcements": "Eskonde pregones",
@@ -631,6 +637,7 @@
"privacy_policy.title": "Politika de privasita",
"recommended": "Rekomendado",
"refresh": "Arefreska",
"regeneration_indicator.please_stand_by": "Aspera por favor.",
"relative_time.days": "{number} d",
"relative_time.full.days": "antes {number, plural, one {# diya} other {# diyas}}",
"relative_time.full.hours": "antes {number, plural, one {# ora} other {# oras}}",
@@ -733,8 +740,12 @@
"status.bookmark": "Marka",
"status.cancel_reblog_private": "No repartaja",
"status.cannot_reblog": "Esta publikasyon no se puede repartajar",
"status.contains_quote": "Kontriene sita",
"status.context.loading_success": "Muevas repuestas kargadas",
"status.context.more_replies_found": "Se toparon mas repuestas",
"status.context.retry": "Reprova",
"status.context.show": "Amostra",
"status.continued_thread": "Kontinuasion del filo",
"status.copy": "Kopia atadijo de publikasyon",
"status.delete": "Efasa",
"status.delete.success": "Puvlikasyon kitada",
@@ -760,9 +771,18 @@
"status.pin": "Fiksa en profil",
"status.quote": "Sita",
"status.quote.cancel": "Anula la sita",
"status.quote_error.limited_account_hint.action": "Amostra entanto",
"status.quote_error.limited_account_hint.title": "Este kuento fue eskondido por los moderadores de {domain}.",
"status.quote_error.not_available": "Puvlikasyon no desponivle",
"status.quote_error.pending_approval": "Puvlikasyon esta asperando",
"status.quote_noun": "Sita",
"status.quote_policy_change": "Troka ken puede sitar",
"status.quote_post_author": "Sito una puvlikasyon de @{name}",
"status.quote_private": "No se puede sitar puvlikasyones privadas",
"status.quotes": "{count, plural, one {sita} other {sitas}}",
"status.read_more": "Melda mas",
"status.reblog": "Repartaja",
"status.reblog_or_quote": "Repartaja o partaja",
"status.reblogged_by": "{name} repartajo",
"status.reblogs.empty": "Ainda nadie tiene repartajado esta publikasyon. Kuando algien lo aga, se amostrara aki.",
"status.redraft": "Efasa i eskrive de muevo",
@@ -823,7 +843,12 @@
"video.pause": "Pauza",
"video.play": "Reproduze",
"video.unmute": "Desilensia",
"visibility_modal.button_title": "Konfigura la vizibilita",
"visibility_modal.header": "Vizibilita i enteraksyon",
"visibility_modal.privacy_label": "Vizivilita",
"visibility_modal.quote_followers": "Solo suivantes",
"visibility_modal.quote_label": "Ken puede sitar",
"visibility_modal.quote_nobody": "Solo yo",
"visibility_modal.quote_public": "Todos",
"visibility_modal.save": "Guadra"
}

View File

@@ -28,6 +28,7 @@
"account.disable_notifications": "Cancelar notificações de @{name}",
"account.domain_blocking": "Bloqueando domínio",
"account.edit_profile": "Editar perfil",
"account.edit_profile_short": "Editar",
"account.enable_notifications": "Notificar novos toots de @{name}",
"account.endorse": "Recomendar",
"account.familiar_followers_many": "Seguido por {name1}, {name2}, e {othersCount, plural, one {um outro que você conhece} other {# outros que você conhece}}",
@@ -40,6 +41,11 @@
"account.featured_tags.last_status_never": "Sem publicações",
"account.follow": "Seguir",
"account.follow_back": "Seguir de volta",
"account.follow_back_short": "Seguir de volta",
"account.follow_request": "Pedir para seguir",
"account.follow_request_cancel": "Cancelar solicitação",
"account.follow_request_cancel_short": "Cancelar",
"account.follow_request_short": "Solicitação",
"account.followers": "Seguidores",
"account.followers.empty": "Nada aqui.",
"account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}",
@@ -240,6 +246,8 @@
"confirmations.mute.confirm": "Silenciar",
"confirmations.quiet_post_quote_info.dismiss": "Não me lembrar novamente",
"confirmations.quiet_post_quote_info.got_it": "Entendi",
"confirmations.quiet_post_quote_info.message": "Ao citar uma publicação pública silenciosa, sua postagem será oculta das linhas de tempo em tendência.",
"confirmations.quiet_post_quote_info.title": "Citando publicações públicas silenciadas",
"confirmations.redraft.confirm": "Excluir e rascunhar",
"confirmations.redraft.message": "Você tem certeza de que quer apagar essa postagem e rascunhá-la? Favoritos e impulsos serão perdidos, e respostas à postagem original ficarão órfãs.",
"confirmations.redraft.title": "Excluir e rascunhar publicação?",
@@ -249,7 +257,12 @@
"confirmations.revoke_quote.confirm": "Remover publicação",
"confirmations.revoke_quote.message": "Essa ação não pode ser desfeita.",
"confirmations.revoke_quote.title": "Remover publicação?",
"confirmations.unblock.confirm": "Desbloquear",
"confirmations.unblock.title": "Desbloquear {name}?",
"confirmations.unfollow.confirm": "Deixar de seguir",
"confirmations.unfollow.title": "Deixar de seguir {name}?",
"confirmations.withdraw_request.confirm": "Retirar solicitação",
"confirmations.withdraw_request.title": "Cancelar solicitação para seguir {name}?",
"content_warning.hide": "Ocultar post",
"content_warning.show": "Mostrar mesmo assim",
"content_warning.show_more": "Mostrar mais",
@@ -320,6 +333,7 @@
"empty_column.bookmarked_statuses": "Nada aqui. Quando você salvar um toot, ele aparecerá aqui.",
"empty_column.community": "A linha local está vazia. Publique algo para começar!",
"empty_column.direct": "Você ainda não tem mensagens privadas. Quando você enviar ou receber uma, será exibida aqui.",
"empty_column.disabled_feed": "Este feed foi desativado pelos administradores do servidor.",
"empty_column.domain_blocks": "Nada aqui.",
"empty_column.explore_statuses": "Nada está em alta no momento. Volte mais tarde!",
"empty_column.favourited_statuses": "Você ainda não tem publicações favoritas. Quanto você marcar uma como favorita, ela aparecerá aqui.",
@@ -448,10 +462,12 @@
"ignore_notifications_modal.private_mentions_title": "Ignorar notificações de menções privadas não solicitadas?",
"info_button.label": "Ajuda",
"info_button.what_is_alt_text": "<h1>O que é texto alternativo?</h1><p>O texto alternativo fornece descrições de imagens para pessoas com deficiências visuais, conexões de internet de baixa largura de banda ou aquelas que buscam mais contexto.</p><p>Você pode melhorar a acessibilidade e a compreensão para todos escrevendo texto alternativo claro, conciso e objetivo.</p> <ul> <li>Capture elementos importantes</li> <li>Resuma textos em imagens</li> <li>Use estrutura de frases regular</li> <li>Evite informações redundantes</li> <li>Foque em tendências e descobertas principais em visuais complexos (como diagramas ou mapas)</li> </ul>",
"interaction_modal.action": "Para interagir com o post de {name}, você precisa entrar em sua conta em qualquer servidor Mastodon que você use.",
"interaction_modal.go": "Ir",
"interaction_modal.no_account_yet": "Não possui uma conta ainda?",
"interaction_modal.on_another_server": "Em um servidor diferente",
"interaction_modal.on_this_server": "Neste servidor",
"interaction_modal.title": "Faça login para continuar",
"interaction_modal.username_prompt": "p. e.x.: {example}",
"intervals.full.days": "{number, plural, one {# dia} other {# dias}}",
"intervals.full.hours": "{number, plural, one {# hora} other {# horas}}",
@@ -734,9 +750,11 @@
"privacy.quote.disabled": "{visibility} Citações desabilitadas",
"privacy.quote.limited": "{visibility} Citações limitadas",
"privacy.unlisted.additional": "Isso se comporta exatamente como público, exceto que a publicação não aparecerá nos _feeds ao vivo_ ou nas _hashtags_, explorar, ou barra de busca, mesmo que você seja escolhido em toda a conta.",
"privacy.unlisted.short": "Público (silencioso)",
"privacy.unlisted.long": "Oculto para os resultados de pesquisa do Mastodon, tendências e linhas do tempo públicas",
"privacy.unlisted.short": "Público silenciado",
"privacy_policy.last_updated": "Atualizado {date}",
"privacy_policy.title": "Política de privacidade",
"quote_error.edit": "Citações não podem ser adicionadas durante a edição de uma publicação.",
"quote_error.poll": "Citações não permitidas com enquetes.",
"quote_error.quote": "Apenas uma citação por vez é permitido.",
"quote_error.unauthorized": "Você não é autorizado a citar essa publicação.",
@@ -756,6 +774,9 @@
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.today": "hoje",
"remove_quote_hint.button_label": "Entendi",
"remove_quote_hint.message": "Você pode fazê-lo no menu de opções {icon}.",
"remove_quote_hint.title": "Deseja remover sua citação publicada?",
"reply_indicator.attachments": "{count, plural, one {# attachment} other {# attachments}}",
"reply_indicator.cancel": "Cancelar",
"reply_indicator.poll": "Enquete",
@@ -851,7 +872,15 @@
"status.block": "Bloquear @{name}",
"status.bookmark": "Salvar",
"status.cancel_reblog_private": "Desfazer boost",
"status.cannot_quote": "Você não tem permissão para citar esta publicação",
"status.cannot_reblog": "Este toot não pode receber boost",
"status.contains_quote": "Contém citação",
"status.context.loading": "Carregando mais respostas",
"status.context.loading_error": "Não foi possível carregar novas respostas",
"status.context.loading_success": "Novas respostas carregadas",
"status.context.more_replies_found": "Mais respostas encontradas",
"status.context.retry": "Tentar novamente",
"status.context.show": "Mostrar",
"status.continued_thread": "Continuação da conversa",
"status.copy": "Copiar link",
"status.delete": "Excluir",
@@ -881,24 +910,33 @@
"status.quote": "Citar",
"status.quote.cancel": "Cancelar citação",
"status.quote_error.filtered": "Oculto devido a um dos seus filtros",
"status.quote_error.limited_account_hint.action": "Mostrar mesmo assim",
"status.quote_error.limited_account_hint.title": "Esta conta foi oculta pelos moderadores do {domain}.",
"status.quote_error.not_available": "Publicação indisponível",
"status.quote_error.pending_approval": "Publicação pendente",
"status.quote_error.pending_approval_popout.body": "No Mastodon, você pode controlar se alguém pode citar você. Esta publicação está pendente enquanto estamos recebendo a aprovação do autor original.",
"status.quote_error.revoked": "Publicação removida pelo autor",
"status.quote_followers_only": "Apenas seguidores podem citar sua publicação",
"status.quote_manual_review": "Autor irá revisar manualmente",
"status.quote_noun": "Citar",
"status.quote_policy_change": "Mude quem pode citar",
"status.quote_post_author": "Publicação citada por @{name}",
"status.quote_private": "Publicações privadas não podem ser citadas",
"status.quotes": "{count, plural, one {# voto} other {# votos}}",
"status.quotes.empty": "Ninguém citou essa publicação até agora. Quando alguém citar aparecerá aqui.",
"status.quotes.local_other_disclaimer": "Citações rejeitadas pelo autor não serão exibidas.",
"status.quotes.remote_other_disclaimer": "Apenas citações do {domain} têm a garantia de serem exibidas aqui. Citações rejeitadas pelo autor não serão exibidas.",
"status.read_more": "Ler mais",
"status.reblog": "Dar boost",
"status.reblog_or_quote": "Acelerar ou citar",
"status.reblog_private": "Compartilhar novamente com seus seguidores",
"status.reblogged_by": "{name} deu boost",
"status.reblogs": "{count, plural, one {boost} other {boosts}}",
"status.reblogs.empty": "Nada aqui. Quando alguém der boost, o usuário aparecerá aqui.",
"status.redraft": "Excluir e rascunhar",
"status.remove_bookmark": "Remover do Salvos",
"status.remove_favourite": "Remover dos favoritos",
"status.remove_quote": "Remover",
"status.replied_in_thread": "Respondido na conversa",
"status.replied_to": "Em resposta a {name}",
"status.reply": "Responder",
@@ -970,6 +1008,8 @@
"visibility_modal.button_title": "Selecionar Visibilidade",
"visibility_modal.header": "Visibilidade e interação",
"visibility_modal.helper.direct_quoting": "Menções privadas escritas no Mastodon.",
"visibility_modal.helper.privacy_editing": "A visibilidade não pode ser alterada após uma publicação ser publicada.",
"visibility_modal.helper.privacy_private_self_quote": "As auto-citações de publicações privadas não podem ser públicas.",
"visibility_modal.helper.private_quoting": "Posts somente para seguidores feitos no Mastodon não podem ser citados por outros.",
"visibility_modal.helper.unlisted_quoting": "Quando as pessoas citam você, sua publicação também será ocultada das linhas de tempo de tendência.",
"visibility_modal.instructions": "Controle quem pode interagir com este post. Você também pode aplicar as configurações para todos os posts futuros navegando para <link>Preferências > Postagem padrão</link>.",

View File

@@ -333,6 +333,7 @@
"empty_column.bookmarked_statuses": "Ainda não tem nenhuma publicação salva. Quando salvar uma, ela aparecerá aqui.",
"empty_column.community": "A cronologia local está vazia. Escreve algo publicamente para começar!",
"empty_column.direct": "Ainda não tens qualquer menção privada. Quando enviares ou receberes uma, ela irá aparecer aqui.",
"empty_column.disabled_feed": "Esta cronologia foi desativada pelos administradores do seu servidor.",
"empty_column.domain_blocks": "Ainda não há qualquer domínio bloqueado.",
"empty_column.explore_statuses": "Não há nada em destaque neste momento. Volte mais tarde!",
"empty_column.favourited_statuses": "Ainda não assinalaste qualquer publicação como favorita. Quando o fizeres, ela aparecerá aqui.",

View File

@@ -9,11 +9,8 @@ import { me, reduceMotion } from 'mastodon/initial_state';
import ready from 'mastodon/ready';
import { store } from 'mastodon/store';
import {
isProduction,
isDevelopment,
isModernEmojiEnabled,
} from './utils/environment';
import { initializeEmoji } from './features/emoji';
import { isProduction, isDevelopment } from './utils/environment';
function main() {
perf.start('main()');
@@ -33,10 +30,7 @@ function main() {
});
}
if (isModernEmojiEnabled()) {
const { initializeEmoji } = await import('@/mastodon/features/emoji');
initializeEmoji();
}
initializeEmoji();
const root = createRoot(mountNode);
root.render(<Mastodon {...props} />);

View File

@@ -8,11 +8,10 @@ import type {
ApiAccountRoleJSON,
ApiAccountJSON,
} from 'mastodon/api_types/accounts';
import emojify from 'mastodon/features/emoji/emoji';
import { unescapeHTML } from 'mastodon/utils/html';
import { CustomEmojiFactory, makeEmojiMap } from './custom_emoji';
import type { CustomEmoji, EmojiMap } from './custom_emoji';
import { CustomEmojiFactory } from './custom_emoji';
import type { CustomEmoji } from './custom_emoji';
// AccountField
interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
@@ -102,17 +101,11 @@ export const accountDefaultValues: AccountShape = {
const AccountFactory = ImmutableRecord<AccountShape>(accountDefaultValues);
function createAccountField(
jsonField: ApiAccountFieldJSON,
emojiMap: EmojiMap,
) {
function createAccountField(jsonField: ApiAccountFieldJSON) {
return AccountFieldFactory({
...jsonField,
name_emojified: emojify(
escapeTextContentForBrowser(jsonField.name),
emojiMap,
),
value_emojified: emojify(jsonField.value, emojiMap),
name_emojified: escapeTextContentForBrowser(jsonField.name),
value_emojified: jsonField.value,
value_plain: unescapeHTML(jsonField.value),
});
}
@@ -120,8 +113,6 @@ function createAccountField(
export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
const { moved, ...accountJSON } = serverJSON;
const emojiMap = makeEmojiMap(accountJSON.emojis);
const displayName =
accountJSON.display_name.trim().length === 0
? accountJSON.username
@@ -134,7 +125,7 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
...accountJSON,
moved: moved?.id,
fields: ImmutableList(
serverJSON.fields.map((field) => createAccountField(field, emojiMap)),
serverJSON.fields.map((field) => createAccountField(field)),
),
emojis: ImmutableList(
serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji)),
@@ -142,11 +133,8 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
roles: ImmutableList(
serverJSON.roles?.map((role) => AccountRoleFactory(role)),
),
display_name_html: emojify(
escapeTextContentForBrowser(displayName),
emojiMap,
),
note_emojified: emojify(accountNote, emojiMap),
display_name_html: escapeTextContentForBrowser(displayName),
note_emojified: accountNote,
note_plain: unescapeHTML(accountNote),
url:
accountJSON.url?.startsWith('http://') ||

View File

@@ -1,10 +1,9 @@
import escapeTextContentForBrowser from 'escape-html';
import type { ApiPollJSON, ApiPollOptionJSON } from 'mastodon/api_types/polls';
import emojify from 'mastodon/features/emoji/emoji';
import { CustomEmojiFactory, makeEmojiMap } from './custom_emoji';
import type { CustomEmoji, EmojiMap } from './custom_emoji';
import { CustomEmojiFactory } from './custom_emoji';
import type { CustomEmoji } from './custom_emoji';
interface PollOptionTranslation {
title: string;
@@ -17,16 +16,12 @@ export interface PollOption extends ApiPollOptionJSON {
translation: PollOptionTranslation | null;
}
export function createPollOptionTranslationFromServerJSON(
translation: { title: string },
emojiMap: EmojiMap,
) {
export function createPollOptionTranslationFromServerJSON(translation: {
title: string;
}) {
return {
...translation,
titleHtml: emojify(
escapeTextContentForBrowser(translation.title),
emojiMap,
),
titleHtml: escapeTextContentForBrowser(translation.title),
} as PollOptionTranslation;
}
@@ -50,8 +45,6 @@ export function createPollFromServerJSON(
serverJSON: ApiPollJSON,
previousPoll?: Poll,
) {
const emojiMap = makeEmojiMap(serverJSON.emojis);
return {
...pollDefaultValues,
...serverJSON,
@@ -60,20 +53,15 @@ export function createPollFromServerJSON(
const option = {
...optionJSON,
voted: serverJSON.own_votes?.includes(index) || false,
titleHtml: emojify(
escapeTextContentForBrowser(optionJSON.title),
emojiMap,
),
titleHtml: escapeTextContentForBrowser(optionJSON.title),
} as PollOption;
const prevOption = previousPoll?.options[index];
if (prevOption?.translation && prevOption.title === option.title) {
const { translation } = prevOption;
option.translation = createPollOptionTranslationFromServerJSON(
translation,
emojiMap,
);
option.translation =
createPollOptionTranslationFromServerJSON(translation);
}
return option;

View File

@@ -2,9 +2,6 @@
// If there are no polyfills, then this is just Promise.resolve() which means
// it will execute in the same tick of the event loop (i.e. near-instant).
// eslint-disable-next-line import/extensions -- This file is virtual so it thinks it has an extension
import 'vite/modulepreload-polyfill';
import { loadIntlPolyfills } from './intl';
function importExtraPolyfills() {
@@ -17,6 +14,7 @@ export function loadPolyfills() {
const needsExtraPolyfills = !window.requestIdleCallback;
return Promise.all([
loadVitePreloadPolyfill(),
loadIntlPolyfills(),
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- those properties might not exist in old browsers, even if they are always here in types
needsExtraPolyfills ? importExtraPolyfills() : Promise.resolve(),
@@ -31,5 +29,13 @@ async function loadEmojiPolyfills() {
}
}
// Loads Vite's module preload polyfill for older browsers, but not in a Worker context.
function loadVitePreloadPolyfill() {
if (typeof document === 'undefined') return;
// @ts-expect-error -- This is a virtual module provided by Vite.
// eslint-disable-next-line import/extensions
return import('vite/modulepreload-polyfill');
}
// Null unless polyfill is needed.
export let emojiRegexPolyfill: RegExp | null = null;

View File

@@ -1,7 +1,6 @@
import type { Reducer } from '@reduxjs/toolkit';
import { importPolls } from 'mastodon/actions/importer/polls';
import { makeEmojiMap } from 'mastodon/models/custom_emoji';
import { createPollOptionTranslationFromServerJSON } from 'mastodon/models/poll';
import type { Poll } from 'mastodon/models/poll';
@@ -20,16 +19,11 @@ const statusTranslateSuccess = (state: PollsState, pollTranslation?: Poll) => {
if (!poll) return;
const emojiMap = makeEmojiMap(poll.emojis);
pollTranslation.options.forEach((item, index) => {
const option = poll.options[index];
if (!option) return;
option.translation = createPollOptionTranslationFromServerJSON(
item,
emojiMap,
);
option.translation = createPollOptionTranslationFromServerJSON(item);
});
};

View File

@@ -138,10 +138,15 @@ const channelNameWithInlineParams = (channelName, params) => {
return `${channelName}&${Object.keys(params).map(key => `${key}=${params[key]}`).join('&')}`;
};
/**
* @typedef {import('mastodon/store').AppDispatch} Dispatch
* @typedef {import('mastodon/store').GetState} GetState
*/
/**
* @param {string} channelName
* @param {Object.<string, string>} params
* @param {function(Function, Function): { onConnect: (function(): void), onReceive: (function(StreamEvent): void), onDisconnect: (function(): void) }} callbacks
* @param {function(Dispatch, GetState): { onConnect: (function(): void), onReceive: (function(StreamEvent): void), onDisconnect: (function(): void) }} callbacks
* @returns {function(): void}
*/
// @ts-expect-error
@@ -229,7 +234,7 @@ const handleEventSourceMessage = (e, received) => {
* @param {string} streamingAPIBaseURL
* @param {string} accessToken
* @param {string} channelName
* @param {{ connected: Function, received: function(StreamEvent): void, disconnected: Function, reconnected: Function }} callbacks
* @param {{ connected: function(): void, received: function(StreamEvent): void, disconnected: function(): void, reconnected: function(): void }} callbacks
* @returns {WebSocketClient | EventSource}
*/
const createConnection = (streamingAPIBaseURL, accessToken, channelName, { connected, received, disconnected, reconnected }) => {
@@ -242,12 +247,9 @@ const createConnection = (streamingAPIBaseURL, accessToken, channelName, { conne
// @ts-expect-error
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken);
// @ts-expect-error
ws.onopen = connected;
ws.onmessage = e => received(JSON.parse(e.data));
// @ts-expect-error
ws.onclose = disconnected;
// @ts-expect-error
ws.onreconnect = reconnected;
return ws;

View File

@@ -12,16 +12,8 @@ export function isProduction() {
else return import.meta.env.PROD;
}
export type Features = 'modern_emojis' | 'fasp' | 'http_message_signatures';
export type Features = 'fasp' | 'http_message_signatures';
export function isFeatureEnabled(feature: Features) {
return initialState?.features.includes(feature) ?? false;
}
export function isModernEmojiEnabled() {
try {
return isFeatureEnabled('modern_emojis');
} catch {
return false;
}
}

View File

@@ -4031,6 +4031,7 @@ a.account__display-name {
background: lighten($ui-highlight-color, 5%);
}
.follow_requests-unlocked_explanation,
.switch-to-advanced {
color: $light-text-color;
background-color: $ui-base-color;
@@ -4041,7 +4042,7 @@ a.account__display-name {
font-size: 13px;
line-height: 18px;
.switch-to-advanced__toggle {
a {
color: $ui-button-tertiary-color;
font-weight: bold;
}
@@ -5223,8 +5224,7 @@ a.status-card {
}
}
.empty-column-indicator,
.follow_requests-unlocked_explanation {
.empty-column-indicator {
color: $dark-text-color;
text-align: center;
padding: 20px;
@@ -5263,10 +5263,8 @@ a.status-card {
}
.follow_requests-unlocked_explanation {
background: var(--surface-background-color);
border-bottom: 1px solid var(--background-border-color);
contain: initial;
flex-grow: 0;
margin: 16px;
margin-bottom: 0;
}
.error-column {