Merge remote-tracking branch 'soapbox/next' into next_

This commit is contained in:
marcin mikołajczak 2022-04-11 20:11:47 +02:00
commit 267ab4b153
28 changed files with 265 additions and 59 deletions

Binary file not shown.

View file

@ -0,0 +1,66 @@
{
"uri": "pixelfed.social",
"title": "pixelfed",
"short_description": "Pixelfed is an image sharing platform, an ethical alternative to centralized platforms",
"description": "Pixelfed is an image sharing platform, an ethical alternative to centralized platforms",
"email": "hello@pixelfed.org",
"version": "2.7.2 (compatible; Pixelfed 0.11.2)",
"urls": {
"streaming_api": "wss://pixelfed.social"
},
"stats": {
"user_count": 45061,
"status_count": 301357,
"domain_count": 5028
},
"thumbnail": "https://pixelfed.social/img/pixelfed-icon-color.png",
"languages": [
"en"
],
"registrations": true,
"approval_required": false,
"contact_account": {
"id": "1",
"username": "admin",
"acct": "admin",
"display_name": "Admin",
"discoverable": true,
"locked": false,
"followers_count": 419,
"following_count": 2,
"statuses_count": 6,
"note": "pixelfed.social Admin. Managed by @dansup",
"url": "https://pixelfed.social/admin",
"avatar": "https://pixelfed.social/storage/avatars/000/000/000/001/LSHNCgwbby7wu3iCYV6H_avatar.png?v=4",
"created_at": "2018-06-01T03:54:08.000000Z",
"avatar_static": "https://pixelfed.social/storage/avatars/000/000/000/001/LSHNCgwbby7wu3iCYV6H_avatar.png?v=4",
"bot": false,
"emojis": [],
"fields": [],
"header": "https://pixelfed.social/storage/headers/missing.png",
"header_static": "https://pixelfed.social/storage/headers/missing.png",
"last_status_at": null
},
"rules": [
{
"id": "1",
"text": "Sexually explicit or violent media must be marked as sensitive when posting"
},
{
"id": "2",
"text": "No racism, sexism, homophobia, transphobia, xenophobia, or casteism"
},
{
"id": "3",
"text": "No incitement of violence or promotion of violent ideologies"
},
{
"id": "4",
"text": "No harassment, dogpiling or doxxing of other users"
},
{
"id": "5",
"text": "No content illegal in United States"
}
]
}

View file

@ -0,0 +1,19 @@
import { rootState } from '../../jest/test-helpers';
import { getSoapboxConfig } from '../soapbox';
const ASCII_HEART = '❤'; // '\u2764\uFE0F'
const RED_HEART_RGI = '❤️'; // '\u2764'
describe('getSoapboxConfig()', () => {
it('returns RGI heart on Pleroma > 2.3', () => {
const state = rootState.setIn(['instance', 'version'], '2.7.2 (compatible; Pleroma 2.3.0)');
expect(getSoapboxConfig(state).allowedEmoji.includes(RED_HEART_RGI)).toBe(true);
expect(getSoapboxConfig(state).allowedEmoji.includes(ASCII_HEART)).toBe(false);
});
it('returns an ASCII heart on Pleroma < 2.3', () => {
const state = rootState.setIn(['instance', 'version'], '2.7.2 (compatible; Pleroma 2.0.0)');
expect(getSoapboxConfig(state).allowedEmoji.includes(ASCII_HEART)).toBe(true);
expect(getSoapboxConfig(state).allowedEmoji.includes(RED_HEART_RGI)).toBe(false);
});
});

Binary file not shown.

View file

@ -0,0 +1,15 @@
import * as React from 'react';
interface IInlineSVG {
loader?: JSX.Element,
}
const InlineSVG: React.FC<IInlineSVG> = ({ loader }): JSX.Element => {
if (loader) {
return loader;
} else {
throw 'You used react-inlinesvg without a loader! This will cause jumpy loading during render.';
}
};
export default InlineSVG;

View file

@ -37,16 +37,14 @@ const SidebarNavigationLink = ({ icon, text, to, count }: ISidebarNavigationLink
</span>
) : null}
<div className='h-5 w-5'>
<Icon
src={icon}
className={classNames({
'h-full w-full': true,
'text-primary-700 dark:text-white': !isActive,
'text-white': isActive,
})}
/>
</div>
<Icon
src={icon}
className={classNames({
'h-5 w-5': true,
'text-primary-700 dark:text-white': !isActive,
'text-white': isActive,
})}
/>
</span>
<Text weight='semibold' theme='inherit'>{text}</Text>

View file

@ -33,7 +33,7 @@ const SidebarNavigation = () => {
{account && (
<>
<SidebarNavigationLink
to={`/@${account.get('acct')}`}
to={`/@${account.acct}`}
icon={require('icons/user.svg')}
text={<FormattedMessage id='tabs_bar.profile' defaultMessage='Profile' />}
/>
@ -79,7 +79,7 @@ const SidebarNavigation = () => {
/>
)} */}
{(account && instance.get('invites_enabled')) && (
{(account && instance.invites_enabled) && (
<SidebarNavigationLink
to={`${baseURL}/invites`}
icon={require('@tabler/icons/icons/mailbox.svg')}
@ -101,7 +101,7 @@ const SidebarNavigation = () => {
src={require('@tabler/icons/icons/users.svg')}
className={classNames('primary-navigation__icon', { 'svg-icon--active': location.pathname === '/timeline/local' })}
/>
{instance.get('title')}
{instance.title}
</NavLink>
) : (
<NavLink to='/timeline/local' className='btn grouped'>

View file

@ -1,8 +1,7 @@
import classNames from 'classnames';
import React from 'react';
import InlineSVG from 'react-inlinesvg';
import { Text } from 'soapbox/components/ui';
import { Text, Icon } from 'soapbox/components/ui';
import { shortNumberFormat } from 'soapbox/utils/numbers';
const COLORS = {
@ -53,10 +52,10 @@ const StatusActionButton = React.forwardRef((props: IStatusActionButton, ref: Re
)}
{...filteredProps}
>
<InlineSVG
<Icon
src={icon}
className={classNames(
'p-1 rounded-full box-content',
'rounded-full',
'group-focus:outline-none group-focus:ring-2 group-focus:ring-offset-2 dark:ring-offset-0 group-focus:ring-primary-500',
{
'fill-accent-300 hover:fill-accent-300': active && filled && color === COLORS.accent,

View file

@ -578,9 +578,10 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
'😮': messages.reactionOpenMouth,
'😢': messages.reactionCry,
'😩': messages.reactionWeary,
'': messages.favourite,
};
const meEmojiTitle = intl.formatMessage(meEmojiReact ? reactMessages[meEmojiReact] : messages.favourite);
const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiReact || ''] || messages.favourite);
const menu = this._makeMenu(publicStatus);
let reblogIcon = require('@tabler/icons/icons/repeat.svg');

Binary file not shown.

View file

@ -1,10 +1,10 @@
import classNames from 'classnames';
import React from 'react';
import InlineSVG from 'react-inlinesvg';
import { defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { Text } from 'soapbox/components/ui';
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
const sizes = {
md: 'p-4 sm:rounded-xl',
@ -54,7 +54,7 @@ const CardHeader: React.FC<ICardHeader> = ({ children, backHref, onBackClick }):
return (
<Comp {...backAttributes} className='mr-2 text-gray-900 dark:text-gray-100' aria-label={intl.formatMessage(messages.back)}>
<InlineSVG src={require('@tabler/icons/icons/arrow-left.svg')} className='h-6 w-6' />
<SvgIcon src={require('@tabler/icons/icons/arrow-left.svg')} className='h-6 w-6' />
<span className='sr-only' data-testid='back-button'>Back</span>
</Comp>
);

View file

@ -1,34 +1,8 @@
import React from 'react';
import { removeVS16s, toCodePoints } from 'soapbox/utils/emoji';
import { joinPublicPath } from 'soapbox/utils/static';
// Taken from twemoji-parser
// https://github.com/twitter/twemoji-parser/blob/a97ef3994e4b88316812926844d51c296e889f76/src/index.js
const removeVS16s = (rawEmoji: string): string => {
const vs16RegExp = /\uFE0F/g;
const zeroWidthJoiner = String.fromCharCode(0x200d);
return rawEmoji.indexOf(zeroWidthJoiner) < 0 ? rawEmoji.replace(vs16RegExp, '') : rawEmoji;
};
const toCodePoints = (unicodeSurrogates: string): string[] => {
const points = [];
let char = 0;
let previous = 0;
let i = 0;
while (i < unicodeSurrogates.length) {
char = unicodeSurrogates.charCodeAt(i++);
if (previous) {
points.push((0x10000 + ((previous - 0xd800) << 10) + (char - 0xdc00)).toString(16));
previous = 0;
} else if (char > 0xd800 && char <= 0xdbff) {
previous = char;
} else {
points.push(char.toString(16));
}
}
return points;
};
interface IEmoji extends React.ImgHTMLAttributes<HTMLImageElement> {
emoji: string,
}

View file

@ -1,7 +1,7 @@
import classNames from 'classnames';
import React from 'react';
import InlineSVG from 'react-inlinesvg';
import SvgIcon from '../icon/svg-icon';
import Text from '../text/text';
interface IIconButton extends React.ButtonHTMLAttributes<HTMLButtonElement> {
@ -24,7 +24,7 @@ const IconButton = React.forwardRef((props: IIconButton, ref: React.ForwardedRef
{...filteredProps}
data-testid='icon-button'
>
<InlineSVG src={src} className={iconClassName} />
<SvgIcon src={src} className={iconClassName} />
{text ? (
<Text tag='span' theme='muted' size='sm'>

View file

@ -0,0 +1,15 @@
import * as React from 'react';
import { render, screen } from '../../../../jest/test-helpers';
import SvgIcon from '../svg-icon';
describe('<SvgIcon />', () => {
it('renders loading element with default size', () => {
render(<SvgIcon className='text-primary-500' src={require('@tabler/icons/icons/code.svg')} />);
const svg = screen.getByTestId('svg-icon-loader');
expect(svg.getAttribute('width')).toBe('24');
expect(svg.getAttribute('height')).toBe('24');
expect(svg.getAttribute('class')).toBe('text-primary-500');
});
});

View file

@ -1,14 +1,16 @@
import React from 'react';
import InlineSVG from 'react-inlinesvg';
import SvgIcon from './svg-icon';
interface IIcon {
className?: string,
count?: number,
alt?: string,
src: string,
size?: number,
}
const Icon = ({ src, alt, count, ...filteredProps }: IIcon): JSX.Element => (
const Icon = ({ src, alt, count, size, ...filteredProps }: IIcon): JSX.Element => (
<div className='relative' data-testid='icon'>
{count ? (
<span className='absolute -top-2 -right-3 block px-1.5 py-0.5 bg-accent-500 text-xs text-white rounded-full ring-2 ring-white'>
@ -16,7 +18,7 @@ const Icon = ({ src, alt, count, ...filteredProps }: IIcon): JSX.Element => (
</span>
) : null}
<InlineSVG src={src} title={alt} {...filteredProps} />
<SvgIcon src={src} size={size} alt={alt} {...filteredProps} />
</div>
);

View file

@ -0,0 +1,39 @@
import React from 'react';
import InlineSVG from 'react-inlinesvg'; // eslint-disable-line no-restricted-imports
interface ISvgIcon {
className?: string,
alt?: string,
src: string,
size?: number,
}
/** Renders an inline SVG with an empty frame loading state */
const SvgIcon: React.FC<ISvgIcon> = ({ src, alt, size = 24, className }): JSX.Element => {
const loader = (
<svg
className={className}
width={size}
height={size}
data-src={src}
data-testid='svg-icon-loader'
/>
);
return (
<InlineSVG
className={className}
src={src}
title={alt}
width={size}
height={size}
loader={loader}
data-testid='svg-icon'
>
/* If the fetch fails, fall back to displaying the loader */
{loader}
</InlineSVG>
);
};
export default SvgIcon;

View file

@ -1,9 +1,9 @@
import classNames from 'classnames';
import React from 'react';
import InlineSVG from 'react-inlinesvg';
import { defineMessages, useIntl } from 'react-intl';
import Icon from '../icon/icon';
import SvgIcon from '../icon/svg-icon';
import Tooltip from '../tooltip/tooltip';
const messages = defineMessages({
@ -72,7 +72,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
tabIndex={-1}
className='text-gray-400 hover:text-gray-500 h-full px-2 focus:ring-primary-500 focus:ring-2'
>
<InlineSVG
<SvgIcon
src={revealed ? require('@tabler/icons/icons/eye-off.svg') : require('@tabler/icons/icons/eye.svg')}
className='h-4 w-4'
/>

View file

@ -2,12 +2,12 @@ import classNames from 'classnames';
import { Map as ImmutableMap } from 'immutable';
import debounce from 'lodash/debounce';
import * as React from 'react';
import InlineSVG from 'react-inlinesvg';
import { defineMessages, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import AutosuggestAccountInput from 'soapbox/components/autosuggest_account_input';
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
import { useAppSelector } from 'soapbox/hooks';
import {
@ -140,12 +140,12 @@ const Search = (props: ISearch) => {
className='absolute inset-y-0 right-0 px-3 flex items-center cursor-pointer'
onClick={handleClear}
>
<InlineSVG
<SvgIcon
src={require('@tabler/icons/icons/search.svg')}
className={classNames('h-4 w-4 text-gray-400', { hidden: hasValue })}
/>
<InlineSVG
<SvgIcon
src={require('@tabler/icons/icons/x.svg')}
className={classNames('h-4 w-4 text-gray-400', { hidden: !hasValue })}
aria-label={intl.formatMessage(messages.placeholder)}

View file

@ -355,9 +355,10 @@ class ActionBar extends React.PureComponent<IActionBar, IActionBarState> {
'😮': messages.reactionOpenMouth,
'😢': messages.reactionCry,
'😩': messages.reactionWeary,
'': messages.favourite,
};
const meEmojiTitle = intl.formatMessage(meEmojiReact ? reactMessages[meEmojiReact] : messages.favourite);
const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiReact || ''] || messages.favourite);
const menu: Menu = [];

View file

@ -0,0 +1,39 @@
import {
removeVS16s,
toCodePoints,
} from '../emoji';
const ASCII_HEART = '❤'; // '\u2764\uFE0F'
const RED_HEART_RGI = '❤️'; // '\u2764'
const JOY = '😂';
describe('removeVS16s()', () => {
it('removes Variation Selector-16 characters from emoji', () => {
// Sanity check
expect(ASCII_HEART).not.toBe(RED_HEART_RGI);
// It normalizes an emoji with VS16s
expect(removeVS16s(RED_HEART_RGI)).toBe(ASCII_HEART);
// Leaves a regular emoji alone
expect(removeVS16s(JOY)).toBe(JOY);
});
});
describe('toCodePoints()', () => {
it('converts a plain emoji', () => {
expect(toCodePoints('😂')).toEqual(['1f602']);
});
it('converts a VS16 emoji', () => {
expect(toCodePoints(RED_HEART_RGI)).toEqual(['2764', 'fe0f']);
});
it('converts an ASCII character', () => {
expect(toCodePoints(ASCII_HEART)).toEqual(['2764']);
});
it('converts a sequence emoji', () => {
expect(toCodePoints('🇺🇸')).toEqual(['1f1fa', '1f1f8']);
});
});

View file

@ -0,0 +1,35 @@
// Taken from twemoji-parser
// https://github.com/twitter/twemoji-parser/blob/a97ef3994e4b88316812926844d51c296e889f76/src/index.js
/** Remove Variation Selector-16 characters from emoji */
// https://emojipedia.org/variation-selector-16/
const removeVS16s = (rawEmoji: string): string => {
const vs16RegExp = /\uFE0F/g;
const zeroWidthJoiner = String.fromCharCode(0x200d);
return rawEmoji.indexOf(zeroWidthJoiner) < 0 ? rawEmoji.replace(vs16RegExp, '') : rawEmoji;
};
/** Convert emoji into an array of Unicode codepoints */
const toCodePoints = (unicodeSurrogates: string): string[] => {
const points = [];
let char = 0;
let previous = 0;
let i = 0;
while (i < unicodeSurrogates.length) {
char = unicodeSurrogates.charCodeAt(i++);
if (previous) {
points.push((0x10000 + ((previous - 0xd800) << 10) + (char - 0xdc00)).toString(16));
previous = 0;
} else if (char > 0xd800 && char <= 0xdbff) {
previous = char;
} else {
points.push(char.toString(16));
}
}
return points;
};
export {
removeVS16s,
toCodePoints,
};

View file

@ -19,6 +19,7 @@ export const MASTODON = 'Mastodon';
export const PLEROMA = 'Pleroma';
export const MITRA = 'Mitra';
export const TRUTHSOCIAL = 'TruthSocial';
export const PIXELFED = 'Pixelfed';
const getInstanceFeatures = (instance: Instance) => {
const v = parseVersion(instance.version);
@ -41,6 +42,7 @@ const getInstanceFeatures = (instance: Instance) => {
bookmarks: any([
v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
v.software === PLEROMA && gte(v.version, '0.9.9'),
v.software === PIXELFED,
]),
lists: any([
v.software === MASTODON && gte(v.compatVersion, '2.1.0'),
@ -73,6 +75,7 @@ const getInstanceFeatures = (instance: Instance) => {
conversations: any([
v.software === MASTODON && gte(v.compatVersion, '2.6.0'),
v.software === PLEROMA && gte(v.version, '0.9.9'),
v.software === PIXELFED,
]),
emojiReacts: v.software === PLEROMA && gte(v.version, '2.0.0'),
emojiReactsRGI: v.software === PLEROMA && gte(v.version, '2.2.49'),
@ -83,7 +86,7 @@ const getInstanceFeatures = (instance: Instance) => {
chats: v.software === PLEROMA && gte(v.version, '2.1.0'),
chatsV2: v.software === PLEROMA && gte(v.version, '2.3.0'),
scopes: v.software === PLEROMA ? 'read write follow push admin' : 'read write follow push',
federating: federation.get('enabled', true), // Assume true unless explicitly false
federating: federation.get('enabled', true) === true, // Assume true unless explicitly false
richText: v.software === PLEROMA,
securityAPI: any([
v.software === PLEROMA,