Merge commit 'a44a3f6d4047568921469ff9fbd212f553b1e7f4' into glitch-soc/merge-upstream

This commit is contained in:
Claire committed 2025-09-27 21:46:57 +02:00
commit 106b0a9d93
14 files changed
+583 -216

No files matched your search

@@ -1,134 +1,23 @@
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import {
followAccount,
unblockAccount,
unmuteAccount,
} from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
import { Avatar } from 'mastodon/components/avatar';
import { Button } from 'mastodon/components/button';
import { DisplayName } from 'mastodon/components/display_name';
import { FollowButton } from 'mastodon/components/follow_button';
import { ShortNumber } from 'mastodon/components/short_number';
import { autoPlayGif, me } from 'mastodon/initial_state';
import { autoPlayGif } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';
import { makeGetAccount } from 'mastodon/selectors';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
cancel_follow_request: {
id: 'account.cancel_follow_request',
defaultMessage: 'Withdraw follow request',
},
requested: {
id: 'account.requested',
defaultMessage: 'Awaiting approval. Click to cancel follow request',
},
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
});
import { useAppSelector } from 'mastodon/store';
const getAccount = makeGetAccount();
export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => {
const intl = useIntl();
const account = useAppSelector((s) => getAccount(s, accountId));
const dispatch = useAppDispatch();
const handleFollow = useCallback(() => {
if (!account) return;
if (
account.getIn(['relationship', 'following']) ||
account.getIn(['relationship', 'requested'])
) {
dispatch(
openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }),
);
} else {
dispatch(followAccount(account.get('id')));
}
}, [account, dispatch]);
const handleBlock = useCallback(() => {
if (account?.relationship?.blocking) {
dispatch(unblockAccount(account.get('id')));
}
}, [account, dispatch]);
const handleMute = useCallback(() => {
if (account?.relationship?.muting) {
dispatch(unmuteAccount(account.get('id')));
}
}, [account, dispatch]);
const handleEditProfile = useCallback(() => {
window.open('/settings/profile', '_blank');
}, []);
if (!account) return null;
let actionBtn;
if (me !== account.get('id')) {
if (!account.get('relationship')) {
// Wait until the relationship is loaded
actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) {
actionBtn = (
<Button
text={intl.formatMessage(messages.cancel_follow_request)}
title={intl.formatMessage(messages.requested)}
onClick={handleFollow}
/>
);
} else if (account.getIn(['relationship', 'muting'])) {
actionBtn = (
<Button
text={intl.formatMessage(messages.unmute)}
onClick={handleMute}
/>
);
} else if (!account.getIn(['relationship', 'blocking'])) {
actionBtn = (
<Button
disabled={account.relationship?.blocked_by}
className={classNames({
'button--destructive': account.getIn(['relationship', 'following']),
})}
text={intl.formatMessage(
account.getIn(['relationship', 'following'])
? messages.unfollow
: messages.follow,
)}
onClick={handleFollow}
/>
);
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = (
<Button
text={intl.formatMessage(messages.unblock)}
onClick={handleBlock}
/>
);
}
} else {
actionBtn = (
<Button
text={intl.formatMessage(messages.edit_profile)}
onClick={handleEditProfile}
/>
);
}
return (
<div className='account-card'>
<Link to={`/@${account.get('acct')}`} className='account-card__permalink'>
@@ -186,7 +75,9 @@ export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => {
</div>
</div>
<div className='account-card__actions__button'>{actionBtn}</div>
<div className='account-card__actions__button'>
<FollowButton accountId={account.get('id')} />
</div>
</div>
</div>
);
@@ -1,19 +1,13 @@
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { isList } from 'immutable';
import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji';
import { useAppSelector } from '@/mastodon/store';
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
import { toSupportedLocale } from './locale';
import { determineEmojiMode } from './mode';
import { cleanExtraEmojis } from './normalize';
import { emojifyElement, emojifyText } from './render';
import type {
CustomEmojiMapArg,
EmojiAppState,
ExtraCustomEmojiMap,
} from './types';
import type { CustomEmojiMapArg, EmojiAppState } from './types';
import { stringHasAnyEmoji } from './utils';
interface UseEmojifyOptions {
@@ -30,20 +24,7 @@ export function useEmojify({
const [emojifiedText, setEmojifiedText] = useState<string | null>(null);
const appState = useEmojiAppState();
const extra: ExtraCustomEmojiMap = useMemo(() => {
if (!extraEmojis) {
return {};
}
if (isList(extraEmojis)) {
return (
extraEmojis.toJS() as ApiCustomEmojiJSON[]
).reduce<ExtraCustomEmojiMap>(
(acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }),
{},
);
}
return extraEmojis;
}, [extraEmojis]);
const extra = useMemo(() => cleanExtraEmojis(extraEmojis), [extraEmojis]);
const emojify = useCallback(
async (input: string) => {
@@ -51,11 +32,11 @@ export function useEmojify({
if (deep) {
const wrapper = document.createElement('div');
wrapper.innerHTML = input;
if (await emojifyElement(wrapper, appState, extra)) {
if (await emojifyElement(wrapper, appState, extra ?? {})) {
result = wrapper.innerHTML;
}
} else {
result = await emojifyText(text, appState, extra);
result = await emojifyText(text, appState, extra ?? {});
}
if (result) {
setEmojifiedText(result);
@@ -1,3 +1,5 @@
import { isList } from 'immutable';
import {
VARIATION_SELECTOR_CODE,
KEYCAP_CODE,
@@ -7,7 +9,11 @@ import {
EMOJIS_WITH_DARK_BORDER,
EMOJIS_WITH_LIGHT_BORDER,
} from './constants';
import type { TwemojiBorderInfo } from './types';
import type {
CustomEmojiMapArg,
ExtraCustomEmojiMap,
TwemojiBorderInfo,
} from './types';
// Misc codes that have special handling
const SKIER_CODE = 0x26f7;
@@ -150,6 +156,21 @@ export function twemojiToUnicodeInfo(
return hexNumbersToString(mappedCodes);
}
export function cleanExtraEmojis(extraEmojis?: CustomEmojiMapArg) {
if (!extraEmojis) {
return null;
}
if (!isList(extraEmojis)) {
return extraEmojis;
}
return extraEmojis
.toJSON()
.reduce<ExtraCustomEmojiMap>(
(acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }),
{},
);
}
function hexStringToNumbers(hexString: string): number[] {
return hexString
.split('-')
@@ -25,8 +25,6 @@ import { domain } from 'mastodon/initial_state';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' },
dismiss: {
@@ -1,11 +1,12 @@
import { useState, useEffect } from 'react';
const breakpoints = {
narrow: 479, // Device width under which horizontal space is constrained
openable: 759, // Device width at which the sidebar becomes an openable hamburger menu
full: 1174, // Device width at which all 3 columns can be displayed
};
type Breakpoint = 'openable' | 'full';
type Breakpoint = keyof typeof breakpoints;
export const useBreakpoint = (breakpoint: Breakpoint) => {
const [isMatching, setIsMatching] = useState(false);